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

Open
wants to merge 49 commits into
base: master
from

Conversation

Projects
None yet
@Centril
Copy link
Contributor

Centril commented Aug 12, 2018

🖼️ Rendered

📝 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 added some commits Aug 10, 2018

@Centril Centril added the T-lang 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

This comment has been minimized.

Copy link
Contributor Author

Centril commented Aug 12, 2018

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

@Centril

This comment has been minimized.

Copy link
Contributor Author

Centril commented Jan 11, 2019

Updated to mention the target_has_atomic case per #2523 (comment).

fn do_stuff() {
...
}
```

This comment has been minimized.

@Nemo157

Nemo157 Jan 11, 2019

Contributor

What happens if there are overlapping fields and inherent items in a type? e.g. if the above Person struct was extended in the future with an inherent method pub fn ssn(&self) -> Result<ValidatedSSN, ValidationError> would there be any way to distinguish between this method and the ssn field?

This comment has been minimized.

@Centril

Centril Jan 11, 2019

Author Contributor

I don't think you could distinguish. Similarly, if you had a macro named foo and a function named foo you couldn't distinguish between those either since accessible considers all namespaces. If you wanted this level of accuracy then, as potential future work, accessible(field, ::foobar::Person::ssn) and accessible(fn, ::foobar::Person::ssn) could be possible.

@scottmcm

This comment has been minimized.

Copy link
Member

scottmcm commented Jan 29, 2019

Can we say that accessible only ever finds things that are #[stable]? I feel like it if can find unstable things, stabilizing things will regularly break people who accessible'd an old version...

@joshtriplett

This comment has been minimized.

Copy link
Member

joshtriplett commented Jan 29, 2019

@scottmcm One of the standard reasons to do this will be to add integration with unstable APIs and continue to support them once stable. (That said, accessible should only find unstable things on a compiler that supports using unstable things.)

@Centril

This comment has been minimized.

Copy link
Contributor Author

Centril commented Jan 29, 2019

So I think that there is some risk with nightly and accessible(..) and @scottmcm's suggestion reduces some of that risk (doesn't and cannot completely remove it...). However, it also removes some of the benefits and primarily makes accessible(..) something that you use to benefit from new stable or beta versions of rustc. Even with that restriction, accessible(..) is still quite useful.

I am personally happy to accept either approach. It sounds like @joshtriplett prefers accessible(..) not to take #[stable(..)] into account. Therefore, in an effort to make progress, I propose that we leave this question of #[stable(..)] unresolved for now and record that in the text. How does that sound?


Also, I think we'd want to check the absence of #[unstable(..)] instead of #[stable(..)] since you might want to do accessible(::some_random_crate::foo) and some_random_crate isn't a staged_api so it cannot have #[stable(..)]. I think this follows the spirit of @scottmcm's suggestion if not the letter.

(That said, accessible should only find unstable things on a compiler that supports using unstable things.)

That's what the reference says. :)

@scottmcm

This comment has been minimized.

Copy link
Member

scottmcm commented Jan 29, 2019

After a Discord discussion, I think my request is the following:

  • That accepting this RFC officially updates the stability policy to explicitly call out that breakage caused by misuse of accessible is allowed breakage, and that the rust team will not delay releases or unstabilize features because they broke a crate accessible-ing on them.
    • I don't think this is controversial; it's true already when people do workaround versions of this, falling under the "look, we need to be able to stabilize features" principle.
  • That the documentation discourage using this to "preemptively" add stable support for a feature before it's released as stable. AKA this is for supporting past versions of rust, not future ones.
@joshtriplett

This comment has been minimized.

Copy link
Member

joshtriplett commented Jan 29, 2019

@scottmcm I agree; if you're using accessible to add conditional support for an unstable feature, you're still using an unstable feature and you're subject to breakage if that feature changes.

I would phrase the second point differently, though. Using accessible isn't "preemptively adding stable support", it's adding optional support for an unstable feature. If you're using that you and the feature changes out from under you then you get to keep all the pieces. I would just suggest documenting that using accessible to access an unstable feature means you're subject to breakage if that unstable feature changes.

@Nemo157

This comment has been minimized.

Copy link
Contributor

Nemo157 commented Jan 29, 2019

If you make a stable release using cfg(accessible) pointing at an unstable path, your crate will compile on the current stable compiler, but may break in a future stable compiler. Under that interpretation you are adding stable support in your crate of an unstable feature of std (which is basically lying to your users and should be strongly discouraged).

@Centril

This comment has been minimized.

Copy link
Contributor Author

Centril commented Feb 17, 2019

@scottmcm, @joshtriplett: I've tried to outline the changes you requested in the RFC and I've also updated relevant RFCs (#1105 and #1122) with links back to this one. Hopefully the added text should cover the spirit of what you said. Ideally, we can polish the finer details in the actual documentation to-be-written if we should stabilize. Let me know what you think... :)

@scottmcm

This comment has been minimized.

Copy link
Member

scottmcm commented Feb 18, 2019

The updates cover what I wanted. Agreed that specific details of the wording can be worked out later.

@rfcbot reviewed

@moxian

This comment has been minimized.

Copy link

moxian commented Mar 16, 2019

There are two totally distinct use cases for #[accessible(..)]:

  • Making current stable code work with old stable compilers (with reduced functionality). "past-proofing" the code
  • Making current unstable code work with future stable compilers. "future-proofing" the code.

I believe nobody in this thread has the problem with the former case.
The latter one is very controversial, and I personally strongly oppose it.

If you #[accessible(..)] on an a path that is currently unstable, the only benefit you (as a library author) get is not needing to remove a #[cfg(feature = "unstable")] once the path gets stabilized. I'd argue that this benefit is negligible compared the potential fallout this might cause if the stabilized version differs slightly from the nightly counterpart.

Consider a simple, but useful crate like boolinator. Imagine the author adds a #[accessible(..)] anticipating improvments from a currently-unstable function, and then goes AWOL. The feature gets stabilized 2 years later with a slightly different signature, and now everyone who (transitively) depends on the crate suddenly cannot update their rust compiler! It'd be extremely hard for rust team to say "well, the breakage is due to misuse of #[accessible(..)], so we don't care", because nobody apart from boolinator author even knew the accessible is there in the first place!
People can try and send PRs to fix the crate, but if author is AWOL or now-disinterested in rust, those won't land, and the ecosystem will be in an unfortunate state for potentially a prolonged while.

Allowing such usage is nothing short of a time bomb.

Compare this situation with the crate introducing unstable feature, hiding the new functionality behind #[cfg(feature = unstable)] (as they have to today), and then author going AWOL in a similar fashion.
This way nothing breaks, and people are still free to send PRs or whatever to remove the feature gate or transform it into now-harmless #[accessible(..)] if the feature is deemed valuable. No damage to ecosystem whatsoever.

  • That the documentation discourage using this to "preemptively" add stable support for a feature before it's released as stable. AKA this is for supporting past versions of rust, not future ones.

From personal experience: if the #[accessible(..)] does something like what I want (and I want to "future-proof" my crate), and my code compiles with it (and even passes tests!) - I won't bother reading the docs (well, I might skim them), since I feel I know the stuff already. I'm fairly confident I'm not alone in this.

No matter how much documentation you put around it, people will misuse it. And it's not them, but other people who will pay the price.

I don't like being unable to update rust version due to third-order dependency accidentally misusing a feature I don't care about.

My proposal:
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.
That is

  • you are free to use #[accessible(..)] with a path that doesn't exist
  • you are free to use #[accessible(..)] with a path that exists and is stable
  • you cannot use #[accessible(..)] with a path that does exist but is not stable
@Ixrec

This comment has been minimized.

Copy link
Contributor

Ixrec commented Mar 16, 2019

@moxian For completeness, I suspect compatibility with past unstable compilers is also a use case someone might have, and I think we can't forbid unstable future-proofing without also forbidding unstable past-proofing, so presumably your suggestion is to forbid both (which seems entirely reasonable to me; I just think it's an important detail to keep in mind).

@moxian

This comment has been minimized.

Copy link

moxian commented Mar 17, 2019

@Ixrec thanks for bringing this up!

(This is probably not what you're referring to, but adding for completeness) My proposal indeed prevents currently-stable code from being compiled with old nightly compiler. This is not a goal, but a side-effect.
However:

  • the nightly compiler in question must be at an older version than current stable, which with 6-week release cadence means 2+ months old and
  • it is already the case that currently stable code can't be compiled with old nightly compilers without modifications (you'd need to at least add #![feature(..)])

It is also true that this does not allow currently nightly-only code to past-proof itself against past nightly compilers (and this is something nightly-only crate authors might want).
I would be totally fine with introducing a separate, perma-unstable nightly-only #[accessible_unstable(..)], which works exactly like #[accessible(..)], but without the hard erroring on referencing unstable paths part. (The only reason I didn't bring this up in my comment above, is that I failed to recognize nightly-only past-proofing as a use case).

P.s.: And, yes, we can't allow past-proofing (current stable) -> (old nightly) without also allowing future proofing (current nightly) -> (future stable), and the latter one inevitably leads to problems.
Past-proofing is fine to do as long as it stays within the same channel, and its flip-side of future-proofing is kinda already impossible to do without crossing channel boundary (so it also is "fine" to do as long as it stays within the same channel).

@moxian

This comment has been minimized.

Copy link

moxian commented Mar 17, 2019

Since I'm already here, I'll comment on the cfg(version(..)) as well.

I think "feature detection" achieves everything that "version detection" does but in a more portable way in regards to alternative compilers, and at no cost* to rustc. I would prefer there to not be a cfg(version(..)) in the language.

* Needing to stabilize feature gate names is a cost, but one does not need to stabilize feature gate names in order to implement achieve feature detection. Read below.

The only way in which "feature detection" is inferior is working around bugs in old rustc releases (as described in #2523 (comment)). But cfg(version(..)) as worded in this RFC matches against minimal rustc version, and is thus not best suited to solve this particular problem.

My reading of #2523 (comment) and #2523 (comment) is that matching feature gate names is a poor idea, but having a plain list of implemented features under ::core::features, and matching on those via cfg(accessible(..)) is acceptable solution.

Now let me summarize the unaddressed arguments raised specifically against this approach, and give my answers:

.0 (paraphrasing #2523 (comment) by @rpjohnst ): We don't want to stabilize feature gate names, but we are open to adding items under ::core::features for features that don't add paths.
And I'm fine with that.

.1a #2523 (comment) by @Ixrec

  • would cfg(feature) completely ignore nightly features, or would we commit to changing the feature's name every time its behavior changes significantly or gets split up into smaller feature flags or whatever?

Since, as noted by @scottmcm above (#2523 (comment)), we should only "past-proof" things, this is a non-question - you cannot cfg on an unstable feature. I'm fine with allowing nightly only code to cfg(accessible_unstable(..)) on an unstable feature, but then you're on your own.

.1b (same comment)

  • would we be open to retroactively adding feature names for things added/fixed in a specific Rust version that people want to detect with cfg(feature), but don't have an associated feature flag today? (maybe one of std's soundness fixes?)

I don't know

.2 #2523 (comment) by @Centril

On the subject of stability and monotonical increase of language and library features, I think that rust_feature(...) could hurt that by allowing other compilers to selectively pick what features they like and don't like or merely selectively pick which features they will implement first. There will be social pressures to implement the important bits of the language in all implementations, but for smaller features this could happen.

On the contrary, I think this is a good thing.
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.

.3a #2523 (comment) by @Centril

Someone suggested upthread that cfg(accessible(...)) plus dummy items such as std::core::features::Specialization equals feature detection. Instead of doing this in the standard library, you could have a library with a build.rs to conditionally expose those items. This would remove the need to write your own build script and, modulo duplication in the dependency graph, (most of) the compile-time costs.
Now, I haven't thought much about it but: couldn't a crate achieve a similar effect today (without cfg(accessible(...))) by exposing a conditionally generated proc macro?

Why? To what end? These complications doesn't seem to offer any advantages; having the language introspect about its own version is a simple and effective solution without any contortions.

These complications

  • allow alternative compilers to be honest about the feature set they implement
  • 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?)
  • are not complicated at all. They do not force rust compiler to have technical debt of feature gates everywhere in the code - just a small list of [path-less] features currently stabilized.

.3b (same comment)

The Rust language is versioned. When we release new versions of rustc the language team is introducing new things into the "language spec" which did not exist in prior versions; the library team does the same thing with the standard library. All conformant Rust compilers which adhere to a certain Rust version must include what the version of rustc included at that version.
[and hence depending on the specific min version is just as good as feature detection]

It would be nice to not needlessly hinder development of "not-fully-conformant" Rust compilers. In fact, AFAIK there's only one conformant Rust compiler - rustc. mrustc only compiles "assumed-valid rust" so by definition does not support all of rustc functionality

TL;DR

The biggest reason to add cfg(version(..)) and not a flavour of cfg(has_rust_feature(..)) is deliberately prohibiting alternative compilers from only implementing part of the Rust spec, and I don't agree it's a goal worth having

@dekellum

This comment has been minimized.

Copy link

dekellum commented Mar 22, 2019

Regarding the pushback against adding cfg(version(…)) as proposed:

What appears to be categorically lacking in this pushback is any real world examples of the feature being abused, and found detrimental, as has been possible for quite some time with build.rs, crates like version_check, and frequently and prominently used in the wild:

https://crates.io/crates/version_check/reverse_dependencies
https://crates.io/crates/rustc_version/reverse_dependencies

Without such concrete evidence of real-world abuse or detriment, it would seem the pushback claims are at best, and assuming good intention, spurious.

On adding cfg(has_rust_feature(…))

I like that extension, but it doesn't replace every desirable and sorely needed use case of cfg(version(…)), see above. I'd like to see it added in the future, as proposed.

Consider defering cfg(accessible(…)) as future work as well

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.

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.

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

Why waist user's time with deprecation of items you (apparently) won't later remove, and at the same time offer users no convenient means of dealing with the deprecation? That is super poor form.

@jethrogb

This comment has been minimized.

Copy link
Contributor

jethrogb commented Mar 22, 2019

What appears to be categorically lacking in this pushback is any real world examples of the feature being abused, and found detrimental, as has been possible for quite some time with build.rs, crates like version_check, and frequently and prominently used in the wild:

I think this is actually a perfect example. All those build scripts will stop working with an alternative implementation of the Rust compiler unless the alternative exactly mirrors the main Rust compiler versioning.

@dekellum

This comment has been minimized.

Copy link

dekellum commented Mar 22, 2019

@jethrogb I don't think that is true. Feel free to elaborate though? Take a look at how this currently working, for example: https://github.com/Geal/nom/blob/master/build.rs

@jethrogb

This comment has been minimized.

Copy link
Contributor

jethrogb commented Mar 22, 2019

Right, that checks whether the Rust compiler is a particular version and then enables a feature that happens to have been introduced in that version, as opposed to checking whether the compiler that's used supports a feature.

@dekellum

This comment has been minimized.

Copy link

dekellum commented Mar 22, 2019

Well OK, sure, if we can't get anywhere with this feature here...

...then build scripts like nom's (linked above) will get extended to handle the versions of your theoretical alternative rust compilers, like it does now for rustc --version. Tell me: are such alternative compilers very close to being released? I hadn't read about these.

Otherwise, no, I don't think this is a good example of the abuse that has been implied, or just FUD, in some of the pushback written above.

dekellum added a commit to dekellum/olio that referenced this pull request Mar 26, 2019

dekellum added a commit to dekellum/body-image that referenced this pull request Mar 26, 2019

dekellum added a commit to dekellum/hyperx that referenced this pull request Mar 26, 2019

dekellum added a commit to dekellum/tao-log that referenced this pull request Mar 26, 2019

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
You can’t perform that action at this time.