Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

RFC: #[cfg(accessible(..) / version(..))] #2523

Merged
merged 50 commits into from
Sep 26, 2019

Conversation

Centril
Copy link
Contributor

@Centril Centril commented Aug 12, 2018

🖼️ Rendered

⏭ Tracking issue (#[cfg(version(..))]

⏭ Tracking issue (#[cfg(accessible(..))]

📝 Summary

Permit users to #[cfg(..)] on whether:

  • they have a certain minimum Rust version (#[cfg(version(1.27))]).
  • a certain external path is accessible (#[cfg(accessible(::std::mem::ManuallyDrop))]).

💖 Thanks

To @eddyb, @rpjohnst, @kennytm, @aturon, and @rkruppe for discussing the feature with me.
To @Mark-Simulacrum, @MajorBreakfast, @alexreg, @alercah, and @joshtriplett for reviewing the draft version of this RFC.

@Centril Centril added the T-lang Relevant to the language team, which will review and decide on the RFC. label Aug 12, 2018
@Centril Centril self-assigned this Aug 12, 2018
@Centril Centril changed the title RFC: #[cfg(accessible(..) / version(..) / nightly)] RFC: #[cfg(accessible(..) / version = ".." / nightly)] Aug 12, 2018
@Centril
Copy link
Contributor Author

Centril commented Aug 12, 2018

cc @MajorBreakfast re. use case for futures. :)

@Centril
Copy link
Contributor Author

Centril commented Sep 13, 2019

I owe some folks an answer and now I have the time, nay, the obligation, to give some. :)

@XAMPPRocky

Rereading this RFC, I have two related concerns about the version attribute. Firstly I don't think the current syntax of cfg(version(1.38)) clearly conveys the semantics described in the RFC, I don't it's obvious that it is version(>=1.38) and not version(=1.38). Related is that I think #[cfg(version(1.38))] could imply that you can rely on having access to the 1.38 version of the Rust toolchain, which would not always be the case.

I think that's fair -- version(1.38) clearly does leave out some information as compared to version(>= 1.38). On the other hand, it is slightly longer and the context of the-specific-version + what it is gating might be sufficient to clarify the semantics. We haven't spent too much effort on optimizing the syntax of the attribute since those are mostly details which can be fleshed out more on the tracking issue to be. I'd be open to a change if there's a strong push for it.

To me version doesn't convey enough information about why someone needs that compiler version. The reasoning behind why a specific version isn't apparent without additional documentation.

Such documentation can be provided locally by the person who does the gating in the specific item that is cfged.

Sometimes it may be more obvious such as 1.39 for async await, however if the reason I need 1.39 is for a non obvious language feature, such as NLL, or improved pattern match syntax, I won't know why unless I change the code to a lower version and test it.

I find that oftentimes, folks do not do such gating for mostly convenience features like e.g. exclusive range syntax or NLL. On the off-chance that this is done, it is rare enough that a comment is sufficient.

I agree with @nrc and others that feature detection for these language features is more intuitive, and I think if we wanted to provide this, as @nrc brought up we could use the existing feature gate names. For me #[cfg(accessible(<path> | <feature_gate>)] would be ideal, as it lets authors use accessible like feature in nightly.

I believe you've availed yourself of the rationale in the RFC and I think we just have a difference of opinion (unlikely to be resolved though a lot of more discussion) but I'll refer to it and similar reasonings in case others have not:

@moxian

Make it a hard error to use #[accessible(..)] referencing a path that is currently unstable. This would catch future-proofing attempts right at the moment they happen, and "discourage" them more thoroughly.

I believe this is @scottmcm's original suggestion; I'd personally be open to changing to this (but others on the team may not be) but I think it's a small enough matter to reconsider in the tracking issue.

If we don't provide rust_feature(..) but only version(..) the alternative compilers would be forced to lie about the version of the lang they support, and would declare themselves to be compliant with the latest feature they support. I.e. if they support int_to_from_bytes, but not repr128, they would just declare themselves be compatible with "1.32.0" and not "1.26.0".
To come from another angle: I don't think it's reasonable to demand alternative compilers to develop features in the same order they appeared in rustc.

This is feasible as long as an alternate compiler is just used for bootstrapping is substantially incomplete to be used for anything else. Otherwise, this flexibility is at the expense of users who now must consider whether some feature provided in a certain version has been removed from the alternative compiler. (I think it also makes for a specification nightmare if you have to consider what exactly that feature meant in the spec.) I think alternative compilers should not lie in this manner and claim to be of the first version for which the compiler implements all the features.

allow the code author to more clearly declare intent (it's much easier to understand cfg(accessible( ::core::features::repr128 )) than it is cfg(version(1.26)), because who the hell knows what happened in 1.26?)

Check https://blog.rust-lang.org/ for the blog post entry. It's easy and quick to check this and anything of importance will be noted there if it concerns language features. (I know this because I write those blog posts.)

@dekellum

Some practical problems and implementation challenges were identified with cfg(accessible(…)). If answering the unresolved question (1) "Is it technically feasible" is going to take more than say, an additional week of time, then please move this to §Possible future work as well. We need cfg(version(…)) badly, and it we needed it last year.

I will split the tracking issues and feature gates so as to make the implementations, discussions, and tests more focused. This should assuage your concern in practical terms. I personally don't think implementation or stabilization of one need to block the other.

Consider adding cfg(deprecated(…)) as possible future work

This would use the same path syntax as cfg(accessible(…)) but evaluate true at compile time if the path is marked deprecated.

For a real world use case, see discussion and the sad resolution of rust-lang-nursery/log#320.

Open to it if more cases can be shown but I currently feel as @sfackler in rust-lang/log#320 (comment).

Please stop all further deprecation in libstd until at least cfg(version(…)) is available

You'll need to make that case to the libs team as that's not for the language team to decide. However, it's orthogonal future work so I think it's best to consider it in depth elsewhere.

@briansmith

It would be useful for the RFC to mention how one would implement a "I only support the latest stable version of Rust" policy. Each time a new version of rustc is released, I read the release notes, go through each item and improve the code according to the new features. Along the way, I check that the latest stable version of the toolchain is able to build by code and pass the tests. This often means my crate will only build with the latest version of Rust. But, even if it were to accidentally compile with an older version, I don't "support' older versions in any sense and I don't want people filing bugs that my crate doesn't build or otherwise work correctly on older toolchains. Instead, I want to clearly communicate "Make sure you're using the latest version of the toolchain."

Suppose 1.37 is the latest stable. Then, if you truly want to do this then you can use:

#[cfg(any(not(version(1.37)), version(1.38))]
compile_error!("ring only support latest stable 1.37.0");

However, I don't think I should ink that into the RFC text as it might suggest encouragement (for which there is no consensus).

@dekellum
Copy link

I find the recent new life here, and incremental approach you are suggesting, @Centril, to be encouraging. A couple of minor points that may have gelled somewhere in the interim months:

(a) Might changing the name from version, to something like min_rustc_version(1.38) clarify that its both a >= bounds and only relevant to rustc versions (not any other compiler)?

(b) Feature names could be made as specific as necessary to account for changes during the stabilization process. So for example maybe a feature we effectively have now on latest nightly is named async_await_2. Prior versions (_1 or _0) might have been for the await!() macro or even for more minor but still breaking differences, like before rust-lang/rust#64292 was merged. If the feature changes in any other breaking ways before stabilization, then the stable version could be named async_await_3, otherwise `async_await_2 could remain valid on stable releases with the benefit that code started on nightly could potentially just work on some subsequent stable.

I agree that these are implementation details that don't need to be resolved here.

@joshtriplett
Copy link
Member

joshtriplett commented Sep 13, 2019 via email

@Centril
Copy link
Contributor Author

Centril commented Sep 13, 2019

(a) Might changing the name from version, to something like min_rustc_version(1.38) clarify that its both a >= bounds and only relevant to rustc versions (not any other compiler)?

Two points here:

  • It is relevant to all Rust compilers, not just rustc. ;)
  • min_version might be a reasonable attribute name if we collectively think version is too ambiguous. version(>= 1.37) could also work. Tho I feel @joshtriplett provides a good rationale for why version(1.37) makes sense (in fact, Cargo semver = X.Y.Z is the inspiration for the concrete syntax). If people can cope with Cargo.toml's way of encoding bounds then they should be able to cope with version(...).

(b) Feature names could be made as specific as necessary to account for changes during the stabilization process. So for example maybe a feature we effectively have now on latest nightly is named async_await_2. Prior versions (_1 or _0) might have been for the await!() macro or even for more minor but still breaking differences, like before rust-lang/rust#64292 was merged. If the feature changes in any other breaking ways before stabilization, then the stable version could be named async_await_3, otherwise `async_await_2 could remain valid on stable releases with the benefit that code started on nightly could potentially just work on some subsequent stable.

The breaking ways may not affect everyone using a specific feature. As such, it may create needless churn on nightly and additionally discourage folks from using nightly. Speaking as a rustc developer myself, keeping track of this also doesn't sound like much fun to be honest.

@dekellum
Copy link

On keeping the version name:

like a Cargo semver bound,

version(>= 1.37) could also work

I'd personally favor using an explicit ^1.37 or >= 1.37 then, for clarity, just like I tend to do in Cargo.toml. That works for me. Note the former implies "<2", a hypothetical concern.

On my suggestion of versioning or sub-feature name suffixes:

keeping track of this also doesn't sound like much fun to be honest.

I'm just suggesting that the current RFCs feature design, would allow such a scheme as a tool to manage the breakage and selectively allow future-proofing. If you don't want to use that tool, that is your prerogative. :)

This flag requires that a `path` fragment be specified in it inside parenthesis
but not inside a string literal. The `$path` must start with leading `::`
and may not refer to any parts of the own crate (e.g. with `::crate::foo`,
`::self::foo`, or `::super::foo` if such paths are legal).
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Does this imply that #[cfg(accessible(path)] can be used with dependencies other than core or std? For example, can I write #[cfg(accessible(::lazy_static::lazy_static)]?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, you can do that.

@Havvy
Copy link
Contributor

Havvy commented Sep 19, 2019

We say that a release announcement of a new version of Rust includes a new version of the language version. Before this RFC, this is purely a rhetorical/theoretical/social device. Nothing can see the language version. It doesn't exist mechanically. This RFC changes that to something that is mechanical, and that should, IMO, be mentioned as a drawback, if only for academic purposes. Any industrial second implementation (Note: I don't expect there to ever be one) would have to understand the feature set of each six week release to properly set their version.

It also semi-locks us into this release versioning scheme. I say semi-locks because we could at any time freeze the cfg(version(...)) version if we decide it's bad or retroactively tie it to rustc's version instead.

`rustc` in the numbering and what features it provides. This is probably not
too unreasonable as we can expect `rustc` to be the reference implementation
and that other ones will probably lag behind. Indeed, this is the experience
with `GHC` and alternative Haskell compilers.
Copy link
Contributor

@gnzlbg gnzlbg Sep 19, 2019

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A drawback of this approach is that using #[cfg(version(1.28))] would make libsyntax fail to parse this syntax on all pre-existing toolchains.

That is, if you have to support toolchains older than whatever toolchain version this attribute is this shipped in, you need to put the #[cfg(version(...))] behind a feature gated module like this to avoid older toolchains from trying to parse it:

// In the module where you actually want to use `#[cfg(version)]`
cfg_if! {
    if #[cfg(toolchain_supports_cfg_version)] {
        mod internal;
        use internal::*;
    }
}

// and then in a separate internal.rs file
#[cfg(version(1.42))]
struct Foo;

and then have a build.rs that detects the rust toolchain version and sets --cfg toolchain_supports_cfg_version.

Copy link
Contributor

@gnzlbg gnzlbg Sep 19, 2019

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

and then have a build.rs that detects the rust toolchain version and sets --cfg toolchain_supports_cfg_version.

I mean, at that point, we can just implement this as a library that can be easily used from build.rs to define such config macros, which would allow the code above to just be written as:

// In the module where you actually want to use `#[cfg(version)]`
#[cfg(rust_ge_1_42_0)]
struct Foo;

Copy link
Contributor

@gnzlbg gnzlbg Sep 19, 2019

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Note that the following syntax does not have this problem and is IIRC backward compatible down to Rust 1.0:

#[cfg(rust_version = "1.42.0")] // in the cargo sense
struct Foo;

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@gnzlbg I think everyone considering using this feature understands that it will only work going forward, in versions that support version and accessible. That's still quite useful.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@joshtriplett when it comes to older versions, there's a difference between "doesn't work" and "can't be parsed". I'd like to avoid the second situation if possible.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This does bring though the idea that whichever release version becomes stable will probably become the new minimum stable rust version that a lot of libraries will bump to. It might be good to pair it's release with a major feature(s) so that the most amount of people can be benefit from them.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's probably not that difficult to support two versions; it's mostly just a bit of parsing inside or outside the string and they'll be uniform otherwise.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@XAMPPRocky that's pretty clever =P will just have to find some major feature to couple with heh.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This does bring though the idea that whichever release version becomes stable will probably become the new minimum stable rust version that a lot of libraries will bump to.

At best, depending on how cfg(version) is shipped, projects might be able to just use it for detecting newer features, but it will live alongside the feature-detection code that these projects are already using. The "old" feature detection code is simple and low maintenance, so bumping the MSRV to be able to exclusively use cfg(version) is something that doesn't add much value, while compromising something that's important for these project: the only reason a project would do feature detection is to be able to support older toolchains, so the project must see value in that.

I think that maybe eventually all projects might be able to just use cfg(version), but that will be a consequence of them bumping the MSRV due to other issues, and not because there is a lot of value in bumping the MSRV to a toolchain that supports cfg(version).

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

At best, depending on how cfg(version) is shipped, projects might be able to just use it for detecting newer features

I don't think cfg(version) has to be immediately valuable. Eventually more features will be covered under cfg(version) than those features that aren't, and then it will be more compelling for more libraries.

so bumping the MSRV to be able to exclusively use cfg(version) is something that doesn't add much value

Unless your MSRV is one version behind cfg(version) your value won't be exclusively cfg(version). If your MSRV is 1.15 for example and cfg(version) was released in 1.40.0 you'd be getting everything from 1.16.0–1.40.0, and you'd be able to use cfg(version) to ease future feature development.

@ogoffart
Copy link

Another point is that things that are gated sill need to use valid rust grammar.

Let's assume that the version 1.42 adds support for throwing function, and using .if:

#[cfg(version(1.42))]
fn foo(x : bool) -> u32 throw(SomeError) {
    x.if { something() } else { 0 }
}

This won't compile in current rust, even after this rfc is implemented, because this is not valid grammar, and rustc still check the grammar of items even if they are going to be removed by attribute macro or cfg attribute.

I suppose it would not make sense to relax the parsing rules of items that are disabled by #[cfg]

So one must wrap the item inside a macro such as cfg-if!{...} but which would not expand the items for the disabled configuration. Something like this should work: #[cfg($cfg)] identity!{ $($body)* }.

Btw, I've made a crate with a macro if_rust_version with macro_rules that allows conditional like that depending on the rust version: https://github.com/ogoffart/if_rust_version

@rfcbot rfcbot added the finished-final-comment-period The final comment period is finished for this RFC. label Sep 22, 2019
@rfcbot
Copy link
Collaborator

rfcbot commented Sep 22, 2019

The final comment period, with a disposition to merge, as per the review above, is now complete.

As the automated representative of the governance process, I would like to thank the author for their work and everyone else who contributed.

The RFC will be merged soon.

@rfcbot rfcbot removed the final-comment-period Will be merged/postponed/closed in ~10 calendar days unless new substational objections are raised. label Sep 22, 2019
@Centril Centril merged commit 7afbf18 into rust-lang:master Sep 26, 2019
@Centril
Copy link
Contributor Author

Centril commented Sep 26, 2019

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
A-cfg Conditional compilation related proposals & ideas A-paths Path related proposals & ideas A-versioning Versioning related proposals & ideas disposition-merge This RFC is in PFCP or FCP with a disposition to merge it. finished-final-comment-period The final comment period is finished for this RFC. T-lang Relevant to the language team, which will review and decide on the RFC.
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet