Skip to content

Commit

Permalink
Auto merge of rust-lang#131349 - RalfJung:const-stability-checks, r=<…
Browse files Browse the repository at this point in the history
…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
  • Loading branch information
bors committed Oct 7, 2024
2 parents 7caad69 + eb8d6c1 commit eddc24c
Show file tree
Hide file tree
Showing 83 changed files with 1,509 additions and 521 deletions.
3 changes: 3 additions & 0 deletions compiler/rustc_attr/messages.ftl
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,9 @@ attr_non_ident_feature =
attr_rustc_allowed_unstable_pairing =
`rustc_allowed_through_unstable_modules` attribute must be paired with a `stable` attribute
attr_rustc_const_stable_indirect_pairing =
`const_stable_indirect` attribute does not make sense on `rustc_const_stable` function, its behavior is already implied
attr_rustc_promotable_pairing =
`rustc_promotable` attribute must be paired with either a `rustc_const_unstable` or a `rustc_const_stable` attribute
Expand Down
106 changes: 98 additions & 8 deletions compiler/rustc_attr/src/builtin.rs
Original file line number Diff line number Diff line change
Expand Up @@ -92,7 +92,15 @@ impl Stability {
#[derive(HashStable_Generic)]
pub struct ConstStability {
pub level: StabilityLevel,
pub feature: Symbol,
/// Says whether this function has an explicit `rustc_const_(un)stable` attribute.
/// If `false`, the const stability information was inferred from the regular
/// stability information.
pub has_const_stable_attr: bool,
/// This can be `None` for functions that are not const-callable from outside code under any
/// feature gate, but are tracked here for the purpose of `safe_to_expose_on_stable`.
pub feature: Option<Symbol>,
/// This is true iff the `const_stable_indirect` attribute is present.
pub const_stable_indirect: bool,
/// whether the function has a `#[rustc_promotable]` attribute
pub promotable: bool,
}
Expand Down Expand Up @@ -268,17 +276,22 @@ pub fn find_stability(

/// Collects stability info from `rustc_const_stable`/`rustc_const_unstable`/`rustc_promotable`
/// attributes in `attrs`. Returns `None` if no stability attributes are found.
///
/// The second component is the `const_stable_indirect` attribute if present, which can be meaningful
/// even if there is no `rustc_const_stable`/`rustc_const_unstable`.
pub fn find_const_stability(
sess: &Session,
attrs: &[Attribute],
item_sp: Span,
) -> Option<(ConstStability, Span)> {
) -> (Option<(ConstStability, Span)>, Option<Span>) {
let mut const_stab: Option<(ConstStability, Span)> = None;
let mut promotable = false;
let mut const_stable_indirect = None;

for attr in attrs {
match attr.name_or_empty() {
sym::rustc_promotable => promotable = true,
sym::rustc_const_stable_indirect => const_stable_indirect = Some(attr.span),
sym::rustc_const_unstable => {
if const_stab.is_some() {
sess.dcx()
Expand All @@ -287,8 +300,16 @@ pub fn find_const_stability(
}

if let Some((feature, level)) = parse_unstability(sess, attr) {
const_stab =
Some((ConstStability { level, feature, promotable: false }, attr.span));
const_stab = Some((
ConstStability {
level,
has_const_stable_attr: true,
feature: Some(feature),
const_stable_indirect: false,
promotable: false,
},
attr.span,
));
}
}
sym::rustc_const_stable => {
Expand All @@ -298,15 +319,23 @@ pub fn find_const_stability(
break;
}
if let Some((feature, level)) = parse_stability(sess, attr) {
const_stab =
Some((ConstStability { level, feature, promotable: false }, attr.span));
const_stab = Some((
ConstStability {
level,
has_const_stable_attr: true,
feature: Some(feature),
const_stable_indirect: false,
promotable: false,
},
attr.span,
));
}
}
_ => {}
}
}

// Merge the const-unstable info into the stability info
// Merge promotable and not_exposed_on_stable into stability info
if promotable {
match &mut const_stab {
Some((stab, _)) => stab.promotable = promotable,
Expand All @@ -317,8 +346,69 @@ pub fn find_const_stability(
}
}
}
if const_stable_indirect.is_some() {
match &mut const_stab {
Some((stab, _)) => {
if stab.is_const_unstable() {
stab.const_stable_indirect = true;
} else {
_ = sess.dcx().emit_err(session_diagnostics::RustcConstStableIndirectPairing {
span: item_sp,
})
}
}
_ => {
// We ignore the `#[rustc_const_stable_indirect]` here, it should be picked up by
// the `default_const_unstable` logic.
}
}
}

const_stab
(const_stab, const_stable_indirect)
}

/// Called for `fn` that don't have a const stability.
///
/// `effective_reg_stability` must be the effecive regular stability, i.e. after applying all the
/// rules about "inherited" stability.
pub fn default_const_stability(
_sess: &Session,
is_const_fn: bool,
const_stable_indirect: bool,
effective_reg_stability: Option<&Stability>,
) -> Option<ConstStability> {
// Intrinsics are *not* `const fn` here, and yet we want to add a default const stability
// for them if they are marked `const_stable_indirect`.
if (is_const_fn || const_stable_indirect)
&& let Some(reg_stability) = effective_reg_stability
&& reg_stability.level.is_unstable()
{
// This has a feature gate, reuse that for const stability.
// We only want to do this if it is an unstable feature gate.
Some(ConstStability {
feature: Some(reg_stability.feature),
has_const_stable_attr: false,
const_stable_indirect,
promotable: false,
level: reg_stability.level,
})
} else if const_stable_indirect {
// Make it const-unstable without a feature gate, to record the `const_stable_indirect`.
Some(ConstStability {
feature: None,
has_const_stable_attr: false,
const_stable_indirect,
promotable: false,
level: StabilityLevel::Unstable {
reason: UnstableReason::Default,
issue: None,
is_soft: false,
implied_by: None,
},
})
} else {
None
}
}

/// Collects stability info from `rustc_default_body_unstable` attributes in `attrs`.
Expand Down
7 changes: 7 additions & 0 deletions compiler/rustc_attr/src/session_diagnostics.rs
Original file line number Diff line number Diff line change
Expand Up @@ -318,6 +318,13 @@ pub(crate) struct RustcPromotablePairing {
pub span: Span,
}

#[derive(Diagnostic)]
#[diag(attr_rustc_const_stable_indirect_pairing)]
pub(crate) struct RustcConstStableIndirectPairing {
#[primary_span]
pub span: Span,
}

#[derive(Diagnostic)]
#[diag(attr_rustc_allowed_unstable_pairing, code = E0789)]
pub(crate) struct RustcAllowedUnstablePairing {
Expand Down
29 changes: 20 additions & 9 deletions compiler/rustc_builtin_macros/src/proc_macro_harness.rs
Original file line number Diff line number Diff line change
Expand Up @@ -313,14 +313,23 @@ fn mk_decls(cx: &mut ExtCtxt<'_>, macros: &[ProcMacro]) -> P<ast::Item> {
match m {
ProcMacro::Derive(cd) => {
cx.resolver.declare_proc_macro(cd.id);
cx.expr_call(span, proc_macro_ty_method_path(cx, custom_derive), thin_vec![
cx.expr_str(span, cd.trait_name),
cx.expr_array_ref(
span,
cd.attrs.iter().map(|&s| cx.expr_str(span, s)).collect::<ThinVec<_>>(),
),
local_path(cx, cd.function_name),
])
// The call needs to use `harness_span` so that the const stability checker
// accepts it.
cx.expr_call(
harness_span,
proc_macro_ty_method_path(cx, custom_derive),
thin_vec![
cx.expr_str(span, cd.trait_name),
cx.expr_array_ref(
span,
cd.attrs
.iter()
.map(|&s| cx.expr_str(span, s))
.collect::<ThinVec<_>>(),
),
local_path(cx, cd.function_name),
],
)
}
ProcMacro::Attr(ca) | ProcMacro::Bang(ca) => {
cx.resolver.declare_proc_macro(ca.id);
Expand All @@ -330,7 +339,9 @@ fn mk_decls(cx: &mut ExtCtxt<'_>, macros: &[ProcMacro]) -> P<ast::Item> {
ProcMacro::Derive(_) => unreachable!(),
};

cx.expr_call(span, proc_macro_ty_method_path(cx, ident), thin_vec![
// The call needs to use `harness_span` so that the const stability checker
// accepts it.
cx.expr_call(harness_span, proc_macro_ty_method_path(cx, ident), thin_vec![
cx.expr_str(span, ca.function_name.name),
local_path(cx, ca.function_name),
])
Expand Down
14 changes: 8 additions & 6 deletions compiler/rustc_const_eval/messages.ftl
Original file line number Diff line number Diff line change
Expand Up @@ -41,8 +41,6 @@ const_eval_const_context = {$kind ->
*[other] {""}
}
const_eval_const_stable = const-stable functions can only call other const-stable functions
const_eval_copy_nonoverlapping_overlapping =
`copy_nonoverlapping` called on overlapping ranges
Expand Down Expand Up @@ -396,17 +394,21 @@ const_eval_uninhabited_enum_variant_read =
read discriminant of an uninhabited enum variant
const_eval_uninhabited_enum_variant_written =
writing discriminant of an uninhabited enum variant
const_eval_unmarked_const_fn_exposed = `{$def_path}` cannot be (indirectly) exposed to stable
.help = either mark the callee as `#[rustc_const_stable_indirect]`, or the caller as `#[rustc_const_unstable]`
const_eval_unreachable = entering unreachable code
const_eval_unreachable_unwind =
unwinding past a stack frame that does not allow unwinding
const_eval_unsized_local = unsized locals are not supported
const_eval_unstable_const_fn = `{$def_path}` is not yet stable as a const fn
const_eval_unstable_in_stable =
const-stable function cannot use `#[feature({$gate})]`
.unstable_sugg = if the function is not (yet) meant to be stable, make this function unstably const
.bypass_sugg = otherwise, as a last resort `#[rustc_allow_const_fn_unstable]` can be used to bypass stability checks (but requires team approval)
const_eval_unstable_in_stable_exposed =
const function that might be (indirectly) exposed to stable cannot use `#[feature({$gate})]`
.unstable_sugg = if the function is not (yet) meant to be exposed to stable, add `#[rustc_const_unstable]` (this what you probably want to do)
.bypass_sugg = otherwise, as a last resort `#[rustc_allow_const_fn_unstable]` can be used to bypass stability checks (this requires team approval)
const_eval_unterminated_c_string =
reading a null-terminated string starting at {$pointer} with no null found before end of allocation
Expand Down
Loading

0 comments on commit eddc24c

Please sign in to comment.