Allow external crate to use unnecessary #[feature] on stable

Every so often, I run into a crate (from crates.io) that uses #[feature]. It forces me to switch to nightly even when the feature already available on stable. It is inconvenient.

Why not, instead of forbidding all #[feature], only block unimplemented feature?

1 Like

Sometimes features change in nightly before stabilization; was the crate using the most up-to-date version of the feature, or an old version? Just accepting #![feature] for stabilized features does not cater for that.

That’s just one reason why I don’t like this.

The issue is solely with the crate that still has the now unneeded #![feature] and should be fixed there.

5 Likes

I'm not sure what you mean by that, but if you can (on nightly) use a #![feature], then it most certainly is implemented.

Anyway, to the point: the very purpose of feature flags is that you can't use them on stable, so that code that runs on stable doesn't rely on unstable features. If you absolutely can't live without one of these, then switch to nightly. Breaking the stability guarantees of stable Rust by effectively making it unstable would be unimaginably bad.

It’s not about me wanting to use nightly feature. It’s about me wanting to use a crate that uses nightly feature.

There’s no substantial difference: the end result is that you (perhaps transitively) depend on a nightly feature. It also doesn’t violate stabilty guarantees any less.

If you want to depend on a crate that uses #![feature] without using a nightly compiler, then your only choice is to lobby the maintainer of that crate to provide a version that works on a stable compiler (if that’s even possible).

This is by design.

5 Likes

@H2CO3, the issue is about using a stable feature in the situation when the dependency crate didn’t remove the no longer necessary feature attribute yet.

Like this:

// warning: the feature `tool_attributes` has been stable since 1.30.0 and no longer requires
#![feature(tool_attributes)]

pub fn dependency_crate_do_work() {}
8 Likes

We’ve discussed before making feature accept all stable features on stable, so that crates using those features become autostable when the features do (unless they no longer compiled on the latest nightly). I think this is a good idea; I haven’t run into this issue yet, but it seems absurd and frustrating if the only reason you have to fork a dependency is because they have a now unnecessary feature pragma at the top of a now otherwise-stable crate.

11 Likes

It seems like these two points should be strongly correlated, if they are keeping on top of updates to the feature so that they compile on the latest variant of the feature, then they are likely to release a new version when the feature stabilizes.

There are quite a few features that have no changes for which this could be a good idea, but there are enough large features (e.g. maybe_uninit or pin) that have major breaking changes while they're in development; so IMO the downsides of having crates that fail because of a feature change (which could be a really subtle and hard to diagnose failure for some changes) outweighs the benefit of not having to delete a single line and push a new release.

3 Likes

Oh, sorry. In this case I totally misunderstood what’s being asked / suggested. My bad.

This could have been avoided if feature has semantic versioning. Like so:

#![feature(foo@2.0.0)]

This has been suggested before.

The biggest argument against is that this would stabilize the feature names as part of the language when they’re meant to not have any meaning and were picked arbitrarily.

If a crate is maintained enough to keep up with a nightly feature, it’s maintained enough to publish a version without the feature opt in attribute.

12 Likes

This is the fairly important point of fact: how often do we make changes to nightly features which could result in "subtle and hard to diagnose failure" as opposed to compiler errors? It seems very uncommon to me, but I could be wrong. It also seems like we should try to avoid ever doing that, because that will result in "subtle and hard to diagnose failure" for people on nightly as well, even if we don't allow them on stable.

4 Likes

To clarify, I believe the downsides of having even trivial compiler errors that tell you exactly how to fix the code outweigh the benefits. (And then as @CAD97 just brought up there is the additional issue that this will make feature names part of the Rust language that all compilers and tooling must understand).

The likely places to have subtle failures is for language features like NLL/specialization/const generics, unfortunately I don’t have much experience with using them (before stabilization in the case of NLL). It seems that avoiding changes with subtle implications while fixing implementation issues in feature such as these would increase the workload for the developers to think through all possible implications of their changes (e.g. imagine specialization allowed you to write a function such that it appeared it would be specialized, but actually just called the default in some specific case; fixing that bug would change the runtime behaviour without any compiler indication).

1 Like

I basically agree with this argument, and think the costs of this change would outweigh the potential benefits. But there’s one angle I just thought of that seems worth asking about:

What if a library wants to support an older-than-latest stable version, and thus would intentionally not remove its nightly feature flags on feature stabilisation, despite being actively maintained?

My guess is that in practice a library that wants to support older stable versions will be very unlikely to bother with nightly churn in the first place. And if they do want to go to that much trouble, they presumably already have the nightly feature usage under a cargo feature, and there’s no reason not to update that cargo feature-specific code to use the latest stable right away. Does that sound right?

This doesn't make sense to me. That's exactly the experience users have today, but in strictly fewer cases than today: an upstream crate does not compile because it is not compatible with stable Rust.

(The "adding it to the language" thing is a complete distraction in my opinion: its a list of strings, who cares?)

1 Like

You would only want to keep the feature flags if you wanted to support an older-than-latest nightly version. (And I'd suggest not doing that in the context of releasing a general library.) Otherwise, if you need to support an older-than-latest stable version, then you use build.rs to sniff the rust version and enable a crate-private cfg knob to turn on things that require newer Rust versions. This is what regex and serde do, for example.

Correctly implemented, these should not have "subtle changes in behavior" because they shouldn't impact runtime behavior. Take NLL as a good example: the implementation of NLL might change subtly to accept or not accept different code samples, but that's always a compile time change in whether code passes compilation.

The actual place I see subtle failures as possible is if we change the invariants of unsafe library APIs without changing the API in any way. That seems like a stretch to me - it would suggest a late breaking realization about an unsafe API's invariants that didn't also impact the way the API was best expressed. The invariants around pin changed a lot for example, but during the early period when the API was also churning a lot.

I do think you get at the actual concern, but I'm not convinced its a problem. I would say that today the situation on nightly is that its possible for the language or std to change in a way that breaks your program without breaking your build. This would make that hypothetically possible on stable (though the author of the crate at least gets a warning about the unnecessary feature flag). But I doubt we've ever made a change like that, and it seems easy to avoid.

1 Like

The problem as I see it is around features that are both

  1. stable enough that it was used and stabilized in the same form,
  2. useful enough that they were used rather than a stable-compatible shim, and
  3. old enough that libraries that used them in development are no longer in active development (otherwise they’d just remove the flag)

so this leads me to believe that some sort of “semi-stable” feature flag would be the proper response to this, for features that are likely to fall into the above case.

Note that, importantly, #![feature] is not a part of the Rust language. It’s a research feature implemented by rustc nightly to opt-in to unstable features not yet added to stable. rustc beta and rustc stable recognize this attribute and give a special error for it, as they know what it likely is (as rustc stable is just rustc nightly with a different config), but this doesn’t make the attribute part of the language.

I honestly don’t know where I stand on this. But whatever choice is made, it should make sense from the perspective of just stable Rust.


There’s also been some mention of removing old feature names from the compiler as a form of cleaning. This would be impossible if stabilized feature names are whitelisted on stable. cc @Centril, haven’t you argued this position before?

1 Like

I suspect the rules will need to be slightly changed if the feature attribute itself becomes stable, like this post proposes.

E.g. #![feature(ident)] is a feature error if ident belongs to the list of unstable features, and a lint from the unused group otherwise (probably deny-by-default).

This way we can garbage collect stable features as we want, it will only affect diagnostics wording.
There is a minor compatibility issue though - introduction of a new feature foo is technically a breaking change since it moves #![feature(foo)] from a lint to a hard error, but that's probably not an issue in practice.

It's not entirely clear how the "unstable" list is collected though.
For language features it's hard-coded, but for library features it needs to be actively collected, which may be unfriendly to lazy compilation (i.e. want to check feature(foo) -> look through the whole standard library? other dependencies?).

1 Like