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

Update const-stability rules because we're not in min-const-fn times any more #129815

Open
RalfJung opened this issue Aug 31, 2024 · 9 comments
Open
Labels
C-discussion Category: Discussion or questions that doesn't represent real issues. T-compiler Relevant to the compiler team, which will review and decide on the PR/issue. WG-const-eval Working group: Const evaluation

Comments

@RalfJung
Copy link
Member

The rustc_const_unstable/rustc_const_stable system was designed in a time when most const fn in the standard library were not meant to be const-stable, no matter whether they are #[stable] or #[unstable]. However, as time passes, it becomes more and more common that functions are const fn from the start, and become const-stable the same time they become stable. So maybe we should reconsider some aspects of that system.

In particular, I think it would make sense to treat a function like this

#[unstable(...)]
const fn foo() {}

as-if it also carried a rustc_const_unstable attribute (with the same feature gate and issue as the unstable attribute). That would avoid confusion such as what we saw here.

The more interesting question is what to do with

#[stable(...)]
const fn foo() {}

If the body of the function doesn't do anything const-unstable, it seems fine to just treat this as const-stable, maybe? Or do we want to still force people to add an explicit rustc_const_stable? I am not sure I see the value in that -- the dangerous case is when that function calls some const-unstable things, such as const-unstable intrinsics. That will still require rustc_allow_const_fn_unstable, and that is the point where we have to ensure we get involved.

To avoid any kind of accidental changes, we probably want to start by making it an error for a const fn to carry stable or unstable without also carrying rustc_const_stable or rustc_const_unstable. I had a brief look at the stability system but couldn't find an obvious place to add such a lint; so if someone has an idea please let me know. :)

And speaking of rustc_allow_const_fn_unstable -- I feel like the way we use that also may need to change. A lot of the current uses of that attribute are a case of "some public stable function is implemented using still-unstable functions" -- but the still-unstable functions are not actually doing anything funky const-wise. They could be marked rustc_const_stable, meaning "wg-const-eval approved their use on stable", even while t-libs-api still debates whether the API itself (independent of its const usage) should exist on stable.

Cc @rust-lang/wg-const-eval

@rustbot rustbot added the needs-triage This issue may need triage. Remove it if it has been sufficiently triaged. label Aug 31, 2024
@RalfJung
Copy link
Member Author

RalfJung commented Aug 31, 2024

They could be marked rustc_const_stable, meaning "wg-const-eval approved their use on stable", even while t-libs-api still debates whether the API itself (independent of its const usage) should exist on stable.

The thing that makes this currently not work well is that we can't just do

    #[unstable(feature = "ptr_alignment_type", issue = "102070")]
    #[rustc_const_stable]
    pub const fn as_usize(self) -> usize {

We need to say under which feature gate and since when the function is stable, which makes no sense for a function that is still unstable. Really here we want to use rustc_const_stable to just mark "the function may be called from const-stable functions, but it can't be called directly by users" -- this shouldn't show up in the docs or so, it is a purely internal decision.

We have at least one feature gates exist just due to this: raw_vec_internals_const.

Maybe we need a new attribute like #[rustc_const_expose_on_stable] for that? This could only be used on #[unstable] functions, and wouldn't have a feature gate or issue number. If that function ever becomes #[stable], it can then immediately also become const-stable. #[rustc_const_stable] would then only be used for functions that became const-stable at a different time / under a different feature gate than when they became #[stable]. (People are anyway confused that you can mark an unstable item #[rustc_const_stable] so this would also help with that.)

@jieyouxu jieyouxu added T-compiler Relevant to the compiler team, which will review and decide on the PR/issue. WG-const-eval Working group: Const evaluation labels Aug 31, 2024
@RalfJung
Copy link
Member Author

RalfJung commented Aug 31, 2024

Another issue with the current system around rustc_allow_const_fn_unstable is that using this with a library feature is kind of leaky: if the functions in that library feature change to use more unstable things, suddenly those things are also exposed on stable, possibly without the person doing that change even realizing.

We should define what exactly the goal of this "transitive stability check" for const fn is, and then ensure the implementation can achieve that goal. Right now, I think it fails to achieve the desired goal -- it is too easy to accidentally bypass, or to forget to Cc us when an intrinsic gets exposed on stable the first time (e.g. the ptr_metadata and aggregate_raw_ptr intrinsics got exposed and we didn't even realize). Granted, that was caused by rustc_allow_const_fn_unstable being added without consulting us, which we can't do much about -- but we could re-design the system so that rustc_allow_const_fn_unstable is hardly ever required, which makes it a more significant signal and hopefully makes it less likely that the attribute is overlooked.

I think the goal should be to protect language features and intrinsics from being const-exposed on stable, even transitively. We don't really care about unstable library features being const-exposed on stable.

So we need to partition the const fn in the standard library in two sets:

  1. Those that don't use any unstable language feature or intrinsics, and thus could be exposed on stable without further involvement from wg-const-eval. These functions may or may not be directly const-callable on stable; that question is orthogonal!
  2. Those that do use unstable language feature or intrinsics.

rustc_allow_const_fn_unstable can be used to put a function in the first set even though it uses an unstable language feature or intrinsic. But this should not let the function call functions from the second set! The first set may only ever call functions from the first set, no exceptions.

Furthermore, every function that is directly const-callable on stable needs to be in the first set.

Proposed design

Fundamentally the main thing I think I want to change is to disambiguate whether a rustc_const_unstable function is "unstable because t-libs-api is not sure yet whether we want this as a public API" or "unstable because it does things that shouldn't be const-exposed on stable". Currently we have no way of distinguishing these cases, and that's not great.

So overall I would propose the following:

  • We only use rustc_const_stable and rustc_const_unstable on publicly reachable functions, and they control whether a function can be directly const-called. This matches the way the stable and unstable attributes work. If a const fn has a #[stable] or #[unstable] attribute and no const stability attribute, its const-stability matches its regular stability.
  • We have a new attribute rustc_const_not_exposed_on_stable that indicates that a function may never, not even indirectly, be const-called on stable. This attribute is incompatible with rustc_const_stable (and in particular, a #[stable] function that is rustc_const_not_exposed_on_stable must also be #[rustc_const_unstable]).
  • In functions not marked rustc_const_not_exposed_on_stable, you can only
    • use language features and intrinsics that are either const-unstable, or allowed via rustc_allow_const_fn_unstable
    • call const fn that are not marked rustc_const_not_exposed_on_stable

rustc_const_not_exposed_on_stable can be added by t-libs without our involvement, but every use of rustc_allow_const_fn_unstable has to be approved by t-lang and involve wg-const-eval. (The names of the attributes could probably be improved to make it more clear that rustc_allow_const_fn_unstable is scary, but rustc_const_not_exposed_on_stable is harmless as we are enforcing that it is indeed not exposed on stable.)

This variant puts a const fn in set 1 by default unless indicated otherwise. That's because I think most functions fall in that set. The main alternative we could consider is to put a const fn in set 2 by default unless indicated otherwise, and require a rustc_const_expose_on_stable attribute to move it to set 1.

The status quo is that we interpret rustc_const_unstable to also mean rustc_const_not_exposed_on_stable, and hence allow all unstable things to be used in these functions -- but that is incompatible with the common pattern of using unstable APIs internally to implement stable APIs, which leads to more use of rustc_allow_const_fn_unstable and thus makes the entire system less effective at ensuring that we don't accidentally const-expose unstable const features.

@RalfJung
Copy link
Member Author

RalfJung commented Aug 31, 2024

To avoid any kind of accidental changes, we probably want to start by making it an error for a const fn to carry stable or unstable without also carrying rustc_const_stable or rustc_const_unstable.

FWIW that doesn't work, stdarch has a ton of unstable const fn without an attribute. Even std has some such cases.

But we can at least ensure that stable const fn have an attribute.
EDIT: Ah, we already do that. :)

@compiler-errors compiler-errors added the C-discussion Category: Discussion or questions that doesn't represent real issues. label Aug 31, 2024
@fmease fmease changed the title Update const-stability rules becuase we're not in min-const-fn times any more Update const-stability rules because we're not in min-const-fn times any more Sep 2, 2024
@RalfJung
Copy link
Member Author

RalfJung commented Oct 5, 2024

We should also use this opportunity to resolve the issues around hashbrown, whose const fn currently cannot be exposed from std without risking that hashbrown bypasses this entire system.

We'll need some way for hashbrown to opt-in to "make sure my const fn are fully stable unless marked with rustc_const_not_exposed_on_stable". Then std can rely on all other const fn being safe-to-expose on stable.

@RalfJung
Copy link
Member Author

RalfJung commented Oct 6, 2024

There is also currently a gaping hole in our recursive const stability checks: if we have a const fn that is entirely unmarked (no #[unstable] nor #[rustc_const_unstable]), then it can freely call arbitrary const things (including unstable!) and it can freely be called by stable const things.

@RalfJung
Copy link
Member Author

RalfJung commented Oct 6, 2024 via email

bors added a commit to rust-lang-ci/rust that referenced this issue Oct 7, 2024
…try>

Const stability checks v2

The const stability system has served us well ever since `const fn` were first stabilized. It's main feature is that it enforces *recursive* validity -- a stable const fn cannot internally make use of unstable const features without an explicit marker in the form of `#[rustc_allow_const_fn_unstable]`. This is done to make sure that we don't accidentally expose unstable const features on stable in a way that would be hard to take back. As part of this, it is enforced that a `#[rustc_const_stable]` can only call `#[rustc_const_stable]` functions. However, some problems have been coming up with increased usage:
- It is baffling that we have to mark private or even unstable functions as `#[rustc_const_stable]` when they are used as helpers in regular stable `const fn`, and often people will rather add `#[rustc_allow_const_fn_unstable]` instead which was not our intention.
- The system has several gaping holes: a private `const fn` without stability attributes whose inherited stability (walking up parent modules) is `#[stable]` is allowed to call *arbitrary* unstable const operations, but can itself be called from stable `const fn`. Similarly, `#[allow_internal_unstable]` on a macro completely bypasses the recursive nature of the check.

Fundamentally, the problem is that we have *three* disjoint categories of functions, and not enough attributes to distinguish them:
1. const-stable functions
2. private/unstable functions that are meant to be callable from const-stable functions
3. functions that can make use of unstable const features

Functions in the first two categories cannot use unstable const features and they can only call functions from the first two categories.

This PR implements the following system:
- `#[rustc_const_stable]` puts functions in the first category. It may only be applied to `#[stable]` functions.
- `#[rustc_const_unstable]` by default puts functions in the third category. The new attribute `#[rustc_const_stable_indirect]` can be added to such a function to move it into the second category.
- `const fn` without a const stability marker are in the second category if they are still unstable. They automatically inherit the feature gate for regular calls, it can now also be used for const-calls.

Also, all the holes mentioned above have been closed. There's still one potential hole that is hard to avoid, which is when MIR building automatically inserts calls to a particular function in stable functions -- which happens in the panic machinery. Those need to be manually marked `#[rustc_const_stable_indirect]` to be sure they follow recursive const stability. But that's a fairly rare and special case so IMO it's fine.

The net effect of this is that a `#[unstable]` or unmarked function can be constified simply by marking it as `const fn`, and it will then be const-callable from stable `const fn` and subject to recursive const stability requirements. If it is publicly reachable (which implies it cannot be unmarked), it will be const-unstable under the same feature gate. Only if the function ever becomes `#[stable]` does it need a `#[rustc_const_unstable]` or `#[rustc_const_stable]` marker to decide if this should also imply const-stability.

Adding `#[rustc_const_unstable]` is only needed for (a) functions that need to use unstable const lang features (including intrinsics), or (b) `#[stable]` functions that are not yet intended to be const-stable. Adding `#[rustc_const_stable]` is only needed for functions that are actually meant to be directly callable from stable const code. `#[rustc_const_stable_indirect]` is used to mark intrinsics as const-callable and for `#[rustc_const_unstable]` functions that are actually called from other, exposed-on-stable `const fn`. No other attributes are required.

I think in the future we may want to tweak this further, so that in the hopefully common case where a public function's const-stability just exactly mirrors its regular stability, we never have to add any attribute. But right now, once the function is stable this requires `#[rustc_const_stable]`.

Note that the handling of inherited stability is an utter spaghetti mess with few comments and [odd behavior](https://rust-lang.zulipchat.com/#narrow/stream/131828-t-compiler/topic/Trying.20to.20understand.20stability.2Ers), so I hope I didn't screw up this part...

### Open question

There is one point I could see we might want to do differently, and that is putting `#[rustc_const_unstable]`  functions (but not intrinsics) in category 2 by default, and requiring an extra attribute for `#[rustc_const_not_exposed_on_stable]` or so. This would require a bunch of extra annotations, but would have the advantage that turning a `#[rustc_const_unstable]` into `#[rustc_const_stable]`  will never change the way the function is const-checked. Currently, we often discover in the const stabilization PR that a function needs some other unstable const things, and then we rush to quickly deal with that. In this alternative universe, we'd work towards getting rid of the `rustc_const_not_exposed_on_stable` before stabilization, and once that is done stabilization becomes a trivial matter. `#[rustc_const_stable_indirect]` would then only be used for intrinsics.

I think I like this idea, but might want to do it in a follow-up PR, as it will need a whole bunch of annotations in the standard library. Also, we probably want to convert all const intrinsics to the "new" form (`#[rustc_intrinsic]` instead of an `extern` block) before doing this to avoid having to deal with two different ways of declaring intrinsics.

Cc `@rust-lang/wg-const-eval` `@rust-lang/libs-api`
Part of rust-lang#129815 (but not finished since this is not yet sufficient to safely let us expose `const fn` from hashbrown)
Fixes rust-lang#131073 by making it so that const-stable functions are always stable
@jieyouxu jieyouxu removed the needs-triage This issue may need triage. Remove it if it has been sufficiently triaged. label Oct 10, 2024
@RalfJung
Copy link
Member Author

#131349 implements a variant of the proposal above, see there for the details. Compared to above, I decided that #[rustc_const_unstable] should default to not having recursive const stability, but everything else defaults to having recursive const stability. That seems to be the option that requires the smallest number of attributes. rustc_const_unstable is generally used for functions that are stable but not const-stable, and presumably the reason for that is that they can't be const-stable because they are using some unstable const feature.

What that does not yet do is handle the cross-crate case. Here my proposal is as follows: in a -Zforce-unstable-if-unmarked crate, we recognize the #[rustc_const_stable_indirect] attribute and enforce recursive const stability on those functions. All other const fn may use arbitrary unstable features (that are enabled in this crate).

So e.g. for hashbrown this would mean adding that attribute to HashMap::with_hasher and RawTable::new. @Amanieu does that sound acceptable?

The alternative would be to make all const fn in a -Zforce-unstable-if-unmarked crate subject to recursive const stability rules, but that's not great -- we generally want crates that build without -Zforce-unstable-if-unmarked to also build with -Zforce-unstable-if-unmarked, but this alternative would break that property.

@Amanieu
Copy link
Member

Amanieu commented Oct 24, 2024

Sure, it's fine to add this attribute to hashbrown under the rustc-dep-of-std Cargo feature. We don't test rustc-dep-of-std under hashbrown CI so this doesn't even need a intermediate rustc nightly with the attribute available.

bors added a commit to rust-lang-ci/rust that referenced this issue Oct 25, 2024
…try>

Const stability checks v2

The const stability system has served us well ever since `const fn` were first stabilized. It's main feature is that it enforces *recursive* validity -- a stable const fn cannot internally make use of unstable const features without an explicit marker in the form of `#[rustc_allow_const_fn_unstable]`. This is done to make sure that we don't accidentally expose unstable const features on stable in a way that would be hard to take back. As part of this, it is enforced that a `#[rustc_const_stable]` can only call `#[rustc_const_stable]` functions. However, some problems have been coming up with increased usage:
- It is baffling that we have to mark private or even unstable functions as `#[rustc_const_stable]` when they are used as helpers in regular stable `const fn`, and often people will rather add `#[rustc_allow_const_fn_unstable]` instead which was not our intention.
- The system has several gaping holes: a private `const fn` without stability attributes whose inherited stability (walking up parent modules) is `#[stable]` is allowed to call *arbitrary* unstable const operations, but can itself be called from stable `const fn`. Similarly, `#[allow_internal_unstable]` on a macro completely bypasses the recursive nature of the check.

Fundamentally, the problem is that we have *three* disjoint categories of functions, and not enough attributes to distinguish them:
1. const-stable functions
2. private/unstable functions that are meant to be callable from const-stable functions
3. functions that can make use of unstable const features

Functions in the first two categories cannot use unstable const features and they can only call functions from the first two categories.

This PR implements the following system:
- `#[rustc_const_stable]` puts functions in the first category. It may only be applied to `#[stable]` functions.
- `#[rustc_const_unstable]` by default puts functions in the third category. The new attribute `#[rustc_const_stable_indirect]` can be added to such a function to move it into the second category.
- `const fn` without a const stability marker are in the second category if they are still unstable. They automatically inherit the feature gate for regular calls, it can now also be used for const-calls.

Also, all the holes mentioned above have been closed. There's still one potential hole that is hard to avoid, which is when MIR building automatically inserts calls to a particular function in stable functions -- which happens in the panic machinery. Those need to be manually marked `#[rustc_const_stable_indirect]` to be sure they follow recursive const stability. But that's a fairly rare and special case so IMO it's fine.

The net effect of this is that a `#[unstable]` or unmarked function can be constified simply by marking it as `const fn`, and it will then be const-callable from stable `const fn` and subject to recursive const stability requirements. If it is publicly reachable (which implies it cannot be unmarked), it will be const-unstable under the same feature gate. Only if the function ever becomes `#[stable]` does it need a `#[rustc_const_unstable]` or `#[rustc_const_stable]` marker to decide if this should also imply const-stability.

Adding `#[rustc_const_unstable]` is only needed for (a) functions that need to use unstable const lang features (including intrinsics), or (b) `#[stable]` functions that are not yet intended to be const-stable. Adding `#[rustc_const_stable]` is only needed for functions that are actually meant to be directly callable from stable const code. `#[rustc_const_stable_indirect]` is used to mark intrinsics as const-callable and for `#[rustc_const_unstable]` functions that are actually called from other, exposed-on-stable `const fn`. No other attributes are required.

Also see the updated dev-guide at rust-lang/rustc-dev-guide#2098.

I think in the future we may want to tweak this further, so that in the hopefully common case where a public function's const-stability just exactly mirrors its regular stability, we never have to add any attribute. But right now, once the function is stable this requires `#[rustc_const_stable]`.

### Open question

There is one point I could see we might want to do differently, and that is putting `#[rustc_const_unstable]`  functions (but not intrinsics) in category 2 by default, and requiring an extra attribute for `#[rustc_const_not_exposed_on_stable]` or so. This would require a bunch of extra annotations, but would have the advantage that turning a `#[rustc_const_unstable]` into `#[rustc_const_stable]`  will never change the way the function is const-checked. Currently, we often discover in the const stabilization PR that a function needs some other unstable const things, and then we rush to quickly deal with that. In this alternative universe, we'd work towards getting rid of the `rustc_const_not_exposed_on_stable` before stabilization, and once that is done stabilization becomes a trivial matter. `#[rustc_const_stable_indirect]` would then only be used for intrinsics.

I think I like this idea, but might want to do it in a follow-up PR, as it will need a whole bunch of annotations in the standard library. Also, we probably want to convert all const intrinsics to the "new" form (`#[rustc_intrinsic]` instead of an `extern` block) before doing this to avoid having to deal with two different ways of declaring intrinsics.

Cc `@rust-lang/wg-const-eval` `@rust-lang/libs-api`
Part of rust-lang#129815 (but not finished since this is not yet sufficient to safely let us expose `const fn` from hashbrown)
Fixes rust-lang#131073 by making it so that const-stable functions are always stable

try-job: test-various
bors added a commit to rust-lang-ci/rust that referenced this issue Oct 25, 2024
…ompiler-errors

Const stability checks v2

The const stability system has served us well ever since `const fn` were first stabilized. It's main feature is that it enforces *recursive* validity -- a stable const fn cannot internally make use of unstable const features without an explicit marker in the form of `#[rustc_allow_const_fn_unstable]`. This is done to make sure that we don't accidentally expose unstable const features on stable in a way that would be hard to take back. As part of this, it is enforced that a `#[rustc_const_stable]` can only call `#[rustc_const_stable]` functions. However, some problems have been coming up with increased usage:
- It is baffling that we have to mark private or even unstable functions as `#[rustc_const_stable]` when they are used as helpers in regular stable `const fn`, and often people will rather add `#[rustc_allow_const_fn_unstable]` instead which was not our intention.
- The system has several gaping holes: a private `const fn` without stability attributes whose inherited stability (walking up parent modules) is `#[stable]` is allowed to call *arbitrary* unstable const operations, but can itself be called from stable `const fn`. Similarly, `#[allow_internal_unstable]` on a macro completely bypasses the recursive nature of the check.

Fundamentally, the problem is that we have *three* disjoint categories of functions, and not enough attributes to distinguish them:
1. const-stable functions
2. private/unstable functions that are meant to be callable from const-stable functions
3. functions that can make use of unstable const features

Functions in the first two categories cannot use unstable const features and they can only call functions from the first two categories.

This PR implements the following system:
- `#[rustc_const_stable]` puts functions in the first category. It may only be applied to `#[stable]` functions.
- `#[rustc_const_unstable]` by default puts functions in the third category. The new attribute `#[rustc_const_stable_indirect]` can be added to such a function to move it into the second category.
- `const fn` without a const stability marker are in the second category if they are still unstable. They automatically inherit the feature gate for regular calls, it can now also be used for const-calls.

Also, all the holes mentioned above have been closed. There's still one potential hole that is hard to avoid, which is when MIR building automatically inserts calls to a particular function in stable functions -- which happens in the panic machinery. Those need to be manually marked `#[rustc_const_stable_indirect]` to be sure they follow recursive const stability. But that's a fairly rare and special case so IMO it's fine.

The net effect of this is that a `#[unstable]` or unmarked function can be constified simply by marking it as `const fn`, and it will then be const-callable from stable `const fn` and subject to recursive const stability requirements. If it is publicly reachable (which implies it cannot be unmarked), it will be const-unstable under the same feature gate. Only if the function ever becomes `#[stable]` does it need a `#[rustc_const_unstable]` or `#[rustc_const_stable]` marker to decide if this should also imply const-stability.

Adding `#[rustc_const_unstable]` is only needed for (a) functions that need to use unstable const lang features (including intrinsics), or (b) `#[stable]` functions that are not yet intended to be const-stable. Adding `#[rustc_const_stable]` is only needed for functions that are actually meant to be directly callable from stable const code. `#[rustc_const_stable_indirect]` is used to mark intrinsics as const-callable and for `#[rustc_const_unstable]` functions that are actually called from other, exposed-on-stable `const fn`. No other attributes are required.

Also see the updated dev-guide at rust-lang/rustc-dev-guide#2098.

I think in the future we may want to tweak this further, so that in the hopefully common case where a public function's const-stability just exactly mirrors its regular stability, we never have to add any attribute. But right now, once the function is stable this requires `#[rustc_const_stable]`.

### Open question

There is one point I could see we might want to do differently, and that is putting `#[rustc_const_unstable]`  functions (but not intrinsics) in category 2 by default, and requiring an extra attribute for `#[rustc_const_not_exposed_on_stable]` or so. This would require a bunch of extra annotations, but would have the advantage that turning a `#[rustc_const_unstable]` into `#[rustc_const_stable]`  will never change the way the function is const-checked. Currently, we often discover in the const stabilization PR that a function needs some other unstable const things, and then we rush to quickly deal with that. In this alternative universe, we'd work towards getting rid of the `rustc_const_not_exposed_on_stable` before stabilization, and once that is done stabilization becomes a trivial matter. `#[rustc_const_stable_indirect]` would then only be used for intrinsics.

I think I like this idea, but might want to do it in a follow-up PR, as it will need a whole bunch of annotations in the standard library. Also, we probably want to convert all const intrinsics to the "new" form (`#[rustc_intrinsic]` instead of an `extern` block) before doing this to avoid having to deal with two different ways of declaring intrinsics.

Cc `@rust-lang/wg-const-eval` `@rust-lang/libs-api`
Part of rust-lang#129815 (but not finished since this is not yet sufficient to safely let us expose `const fn` from hashbrown)
Fixes rust-lang#131073 by making it so that const-stable functions are always stable

try-job: test-various
flip1995 pushed a commit to flip1995/rust-clippy that referenced this issue Oct 31, 2024
…rrors

Const stability checks v2

The const stability system has served us well ever since `const fn` were first stabilized. It's main feature is that it enforces *recursive* validity -- a stable const fn cannot internally make use of unstable const features without an explicit marker in the form of `#[rustc_allow_const_fn_unstable]`. This is done to make sure that we don't accidentally expose unstable const features on stable in a way that would be hard to take back. As part of this, it is enforced that a `#[rustc_const_stable]` can only call `#[rustc_const_stable]` functions. However, some problems have been coming up with increased usage:
- It is baffling that we have to mark private or even unstable functions as `#[rustc_const_stable]` when they are used as helpers in regular stable `const fn`, and often people will rather add `#[rustc_allow_const_fn_unstable]` instead which was not our intention.
- The system has several gaping holes: a private `const fn` without stability attributes whose inherited stability (walking up parent modules) is `#[stable]` is allowed to call *arbitrary* unstable const operations, but can itself be called from stable `const fn`. Similarly, `#[allow_internal_unstable]` on a macro completely bypasses the recursive nature of the check.

Fundamentally, the problem is that we have *three* disjoint categories of functions, and not enough attributes to distinguish them:
1. const-stable functions
2. private/unstable functions that are meant to be callable from const-stable functions
3. functions that can make use of unstable const features

Functions in the first two categories cannot use unstable const features and they can only call functions from the first two categories.

This PR implements the following system:
- `#[rustc_const_stable]` puts functions in the first category. It may only be applied to `#[stable]` functions.
- `#[rustc_const_unstable]` by default puts functions in the third category. The new attribute `#[rustc_const_stable_indirect]` can be added to such a function to move it into the second category.
- `const fn` without a const stability marker are in the second category if they are still unstable. They automatically inherit the feature gate for regular calls, it can now also be used for const-calls.

Also, all the holes mentioned above have been closed. There's still one potential hole that is hard to avoid, which is when MIR building automatically inserts calls to a particular function in stable functions -- which happens in the panic machinery. Those need to be manually marked `#[rustc_const_stable_indirect]` to be sure they follow recursive const stability. But that's a fairly rare and special case so IMO it's fine.

The net effect of this is that a `#[unstable]` or unmarked function can be constified simply by marking it as `const fn`, and it will then be const-callable from stable `const fn` and subject to recursive const stability requirements. If it is publicly reachable (which implies it cannot be unmarked), it will be const-unstable under the same feature gate. Only if the function ever becomes `#[stable]` does it need a `#[rustc_const_unstable]` or `#[rustc_const_stable]` marker to decide if this should also imply const-stability.

Adding `#[rustc_const_unstable]` is only needed for (a) functions that need to use unstable const lang features (including intrinsics), or (b) `#[stable]` functions that are not yet intended to be const-stable. Adding `#[rustc_const_stable]` is only needed for functions that are actually meant to be directly callable from stable const code. `#[rustc_const_stable_indirect]` is used to mark intrinsics as const-callable and for `#[rustc_const_unstable]` functions that are actually called from other, exposed-on-stable `const fn`. No other attributes are required.

Also see the updated dev-guide at rust-lang/rustc-dev-guide#2098.

I think in the future we may want to tweak this further, so that in the hopefully common case where a public function's const-stability just exactly mirrors its regular stability, we never have to add any attribute. But right now, once the function is stable this requires `#[rustc_const_stable]`.

### Open question

There is one point I could see we might want to do differently, and that is putting `#[rustc_const_unstable]`  functions (but not intrinsics) in category 2 by default, and requiring an extra attribute for `#[rustc_const_not_exposed_on_stable]` or so. This would require a bunch of extra annotations, but would have the advantage that turning a `#[rustc_const_unstable]` into `#[rustc_const_stable]`  will never change the way the function is const-checked. Currently, we often discover in the const stabilization PR that a function needs some other unstable const things, and then we rush to quickly deal with that. In this alternative universe, we'd work towards getting rid of the `rustc_const_not_exposed_on_stable` before stabilization, and once that is done stabilization becomes a trivial matter. `#[rustc_const_stable_indirect]` would then only be used for intrinsics.

I think I like this idea, but might want to do it in a follow-up PR, as it will need a whole bunch of annotations in the standard library. Also, we probably want to convert all const intrinsics to the "new" form (`#[rustc_intrinsic]` instead of an `extern` block) before doing this to avoid having to deal with two different ways of declaring intrinsics.

Cc `@rust-lang/wg-const-eval` `@rust-lang/libs-api`
Part of rust-lang/rust#129815 (but not finished since this is not yet sufficient to safely let us expose `const fn` from hashbrown)
Fixes rust-lang/rust#131073 by making it so that const-stable functions are always stable

try-job: test-various
@RalfJung
Copy link
Member Author

RalfJung commented Nov 1, 2024

We don't test rustc-dep-of-std under hashbrown CI so this doesn't even need a intermediate rustc nightly with the attribute available.

Hm, but that seems risky if there is an unusual condition about const fn being marked in a certain way. It seems likely we'll get sudden compile failures when the hashbrown dependency of std is bumped.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
C-discussion Category: Discussion or questions that doesn't represent real issues. T-compiler Relevant to the compiler team, which will review and decide on the PR/issue. WG-const-eval Working group: Const evaluation
Projects
None yet
Development

No branches or pull requests

5 participants