Skip to content

RFC: Associated const underscore#3527

Open
dtolnay wants to merge 7 commits into
rust-lang:masterfrom
dtolnay:assocunderscore
Open

RFC: Associated const underscore#3527
dtolnay wants to merge 7 commits into
rust-lang:masterfrom
dtolnay:assocunderscore

Conversation

@dtolnay

@dtolnay dtolnay commented Nov 12, 2023

Copy link
Copy Markdown
Member

View all comments

Allow _ for the name of associated constants. This RFC builds on RFC 2526 which added support for free const items with the name _, but not associated consts.

// RFC 2526 (stable in Rust 1.37)
const _: () = { /* ... */ };

impl Thing {
    // this RFC
    const _: () = { /* ... */ };
}

Constants named _ are not nameable by other code and do not appear in documentation, but are useful when macro-generated code must typecheck some expression in the context of a specific choice of Self.

Rendered

@dtolnay dtolnay added T-lang Relevant to the language team, which will review and decide on the RFC. A-syntax Syntax related proposals & ideas A-const Proposals relating to const items labels Nov 12, 2023
@dtolnay dtolnay force-pushed the assocunderscore branch 2 times, most recently from 73c9d1f to 9db99f3 Compare November 12, 2023 21:22
Comment thread text/0000-associated-const-underscore.md Outdated
@programmerjake

Copy link
Copy Markdown
Member

When does associated const code get run? at type definition? when substituting concrete types into generic arguments? never?
so, if I write:

pub struct S<T>(T);

impl<T> S<T> {
    const _: () = assert!(core::mem::size_of::<T>() % 2 == 0);
}

when does the assert get run?

@dtolnay

dtolnay commented Nov 13, 2023

Copy link
Copy Markdown
Member Author

Great question! I had not thought about that. I have added a paragraph proposing that they are evaluated never, aligning with the behavior of a named associated constant that is accessed never, but I'd welcome considering other options.

@programmerjake

programmerjake commented Nov 13, 2023

Copy link
Copy Markdown
Member

I have added a paragraph proposing that they are evaluated never, aligning with the behavior of a named associated constant that is accessed never, but I'd welcome considering other options.

I think that never being evaluated will be highly unintuitive, we should instead require them to be evaluated (though memoization is allowed) when generating any code that creates a type Self where enough generics have concrete types/values such that all generics used in the const are concrete types/values.

so:

pub struct S<T, U, const V: usize>(T, U);

impl<T, U, const V: usize> S<T, U, V> {
    const _: () = {
        let v: Option<T> = None; // use `T`
        panic!("1: T");
    };
    const _: () = {
        let v: Option<(T, U)> = None; // use `T` and `U`
        panic!("2: T and U");
    };
    const _: () = {
        let v: Option<(T, U)> = None; // use `T` and `U`
        let v = [(); V]; // use `V`
        panic!("3: T, U, and V");
    };
}

pub fn f<T, U>(v: S<T, U, 8>>) {} // doesn't error, since only V has a concrete value
pub fn g<U, const V: usize>(v: S<i8, U, V>>) {} // errors with message "1: T"
pub fn h<const V: usize>() {
    let f = f::<u8, i8, V>; // errors with messages "1: T" and "2: T and U"
}
const _: () = {
    let v: S<u8, i8, 5>; // errors with messages "1: T" and "2: T and U" and "3: T, U, and V"
};

@programmerjake

programmerjake commented Nov 13, 2023

Copy link
Copy Markdown
Member

so, basically as if:

pub struct S<T, U, const V: usize>(T, U);

impl<T, U, const V: usize> S<T, U, V> {
    const _: () = ...;
}

was instead written:

pub struct S<T, U, const V: usize>(T, U, PhantomData<[(); {Self::_CONST; 0}]>);

impl<T, U, const V: usize> S<T, U, V> {
    const _CONST: () = ...;
}

@joshlf

joshlf commented Nov 16, 2023

Copy link
Copy Markdown
Contributor

This would be so helpful! Thanks for writing this, David!

In favor of these constants being evaluated: rust-lang/rust#112090

@trueb2

trueb2 commented Nov 16, 2023

Copy link
Copy Markdown

It seems that the intent of the change is to allow undocumented static assertions within traits, where this is easier to implement. There are probably corner cases that I cannot think of immediately. I found the use of anonymous const blocks non-intuitive on first encounter but clearly appreciate their use now.

I would expect code in const blocks to only affect compilation; therefore, they are fundamentally different than creating symmetry to let bindings.

I also anticipate a natural progression that if all of the const blocks symmetry is added, they will be sugared out by keyword or macro. Like C++ static_assert or Zig comptime where intent is clear/concise.

@scottmcm

scottmcm commented Nov 18, 2023

Copy link
Copy Markdown
Member

I was all set to propose merge as "of course" until I saw the note about evaluation :/

I do agree that not evaluating it is consistent with named associated constants.

Unfortunately, I feel like it would be particularly likely for someone to move a const _: () = assert!(...); into an associated constant and not notice that it's just never running. At least with a named one you get an error if it's never used, and adding a use somewhere gives an opportunity to evaluate it when instantiated.

Brainstorming: How much would checks here be needed to put into inherent impls vs wanting them only in trait impl blocks? For example, I'm pondering an alternative where you'd expand derive(Eq) to

impl<T> ::core::cmp::Eq for Thing<'a, T> {
    #[coverage(off)]
    priv fn _assert_fields_are_total_eq<'a, T: ::core::cmp::Eq>() {
        let _: ::core::cmp::AssertParamIsEq<Field<'a, T>>;
    }
}

even though there's no _assert_fields_are_total_eq on Eq, assuming a lang change where you're allowed to put extra items in impl blocks so long as they're priv (placeholder, but it's already a keyword, so that's convenient), which makes them visible inside that impl block only. (No need for doc(hidden) on it because it'd be treated the same as other non-pub things and only included in docs with --document-private-items.)

Being a function means the fact that it's not ever evaluated seem more normal, and helper functions in impls that don't have to be put in separate inherent impl blocks seems like an independently-useful feature too. (But also a bigger one, because anything that deals in visibility is complicated.)

@nikomatsakis

Copy link
Copy Markdown
Contributor

One further thought: the whole const _: () = { .. } thing was a total hack -- albeit, a useful one. I do wonder if it's time to add something more first-class, e.g. "anonymous modules", something like that.

@scottmcm scottmcm added the I-lang-nominated Indicates that an issue has been nominated for prioritizing at the next lang team meeting. label Nov 22, 2023
@traviscross

Copy link
Copy Markdown
Contributor

@rustbot labels -I-lang-nominated

We discussed this in the T-lang meeting last week.

There was consensus that the problem this is trying to solve is a real problem.

However, people were concerned about whether it might be too surprising that these were not evaluated. It wasn't that people wanted them evaluated exactly; the concern was simply the potential for surprise.

Aside from the practical motivation (which may be better served by some more specialized feature), the main reason to allow this (with the proposed behavior) would seem to be consistency on two fronts. We allow const _ elsewhere, and we don't evaluate unused associated constants.

The consensus was that we wanted more discussion in the RFC itself about this behavior, e.g. acknowledgment that this might be surprising, discussion of things we could do to mitigate the surprise, discussion of why maybe this would be less surprising than it seems or why the consequences of that might not be severe, an argument for why we should do it anyway, etc.

Please renominate for T-lang after addressing these in the RFC so the team can review again.

@rustbot rustbot removed the I-lang-nominated Indicates that an issue has been nominated for prioritizing at the next lang team meeting. label Dec 13, 2023
Comment on lines +546 to +547
# Reference-level explanation
[reference-level-explanation]: #reference-level-explanation

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Consider discussing the relationship with #3373 — probably just to confirm that associated const underscore is not exempt from that lint.

pub struct Struct;

const _: () = {
    impl Trait1 for Struct {}  // no warning
};

impl Struct {
    const _: () = {
        impl Trait2 for Struct {}  // WARNING
    };
}

@PoignardAzur

PoignardAzur commented May 3, 2024

Copy link
Copy Markdown

If the concern is about people being surprised by code not being evaluated, how about introducing wildcard associated functions?

Eg:

impl Thing {
    fn _() {
        /* ... */
    };
}

This would probably be more intuitive to most people. If someone reads this code:

impl Thing {
    fn _() {
        assert!(...);
    };
}

It's immediately obvious that something is wrong here because that function is obviously never going to be run.

@clarfonthey

Copy link
Copy Markdown
Contributor

Finding myself wanting this specifically for assertions: is there a particular reason why we couldn't change the associated type behaviour at an edition boundary, and then warn for associated-const-underscore usages on previous editions? Or is this not specific to associated constants and actually specific for all named constants, and it's just that we're wanting this in particular for the associated constants?

Right now I'm using them as a way of confirming validity of generic consts, since even simple things like casting a u8 to a usize so it can be used as an array length is not allowed. I have to accept a usize as my generic const, then panic if its length is more than 255, which I would prefer to guarantee statically.

@traviscross

Copy link
Copy Markdown
Contributor

Reading back through, this makes sense to me. There are later things that we could do, many of which are mentioned in this RFC as alternatives. On basic consistency grounds, though, it seems worth allowing this regardless, and that it should work in the same way as named associated constants (e.g., with respect to evaluation). Doing this simplifies the language definition.

Included in this proposal is striking the text from this RFC that suggests that we will suppress the dead_code lint in these cases. We do want to lint on these cases as the body is not evaluated. It should work in exactly the same way as unused underscore-prefixed associated constants (e.g., const _X) at the time of stabilization, which will mean that we need to change the behavior of those.

@rfcbot fcp merge lang

@rust-rfcbot

rust-rfcbot commented Jun 17, 2026

Copy link
Copy Markdown
Collaborator

@traviscross has proposed to merge this. The next step is review by the rest of the tagged team members:

Concerns:

Once a majority of reviewers approve (and at most 2 approvals are outstanding), this will enter its final comment period. If you spot a major issue that hasn't been raised at any point in this process, please speak up!

cc @rust-lang/lang-advisors: FCP proposed for lang, please feel free to register concerns.
See this document for info about what commands tagged team members can give me.

@rust-rfcbot rust-rfcbot added proposed-final-comment-period Currently awaiting signoff of all team members in order to enter the final comment period. disposition-merge This RFC is in PFCP or FCP with a disposition to merge it. labels Jun 17, 2026
@traviscross

Copy link
Copy Markdown
Contributor

For the change to the text required...

@rustbot author

@rustbot rustbot added the S-waiting-on-author Status: This is awaiting some action from the author. label Jun 17, 2026
@tmandry

tmandry commented Jun 17, 2026

Copy link
Copy Markdown
Member

I suggested having the dead_code lint fire, and the team agreed. To me it's important to implement this ahead of stabilization so that we don't expose the very surprising behavior of assertions that never fire (even though that is what we have today with unnamed associated consts whose names start with underscore - agreed that we should make those consistent).

@rfcbot reviewed

Comment on lines +72 to +84
This RFC proposes that `derive(Eq)` should generate its output as follows
instead, and the nonpublic `assert_receiver_is_total_eq` can be removed from the
trait.

```rust
impl ::core::cmp::Eq for Thing {}

impl Thing {
const _: () = {
let _: ::core::cmp::AssertParamIsEq<Field>;
};
}
```

@steffahn steffahn Jun 17, 2026

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

As an alternative: What if we allow you to have no body in the never-evaluated case?

I.e. the logic being “if it can never be evaluated, you don't need to provide a value anyway”

Then the use-case for derive(Eq) would not look like

impl ::core::cmp::Eq for Thing {}

impl Thing {
    const _: () = {
        let _: ::core::cmp::AssertParamIsEq<Field>;
    };
}

but like

impl ::core::cmp::Eq for Thing {}

impl Thing {
    const _: ::core::cmp::AssertParamIsEq<Field>;
}

View changes since the review

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

I like this, and it composes nicely with the existing proposal: We could do either, or both. If we do allow it, we should do so universally, not just for associated consts.

@steffahn steffahn Jun 17, 2026

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Wait… these are as powerful as just listing Type:, in where clauses, aren’t they…

But… couldn’t the compiler just use a where clause already though? Just make derive(Eq) create something like this:

impl ::core::cmp::Eq for Thing {}

impl Thing where ::core::cmp::AssertParamIsEq<Field>: {}

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

From what I can tell by some testing yes, this would work totally fine!

@steffahn steffahn Jun 17, 2026

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

@tmandry

If we do allow it, we should do so universally, not just for associated consts.

Yes, I agree. It would be the item-level equivalent of let _: Type;.
The question of evaluation conditions is also harmless for these, since there’s nothing to evaluate.

Interestingly, with the way that generic const parameters are implemented, you could then use something like

const _: [(); {{/* your expression here */}; 0}];

for achieving the explicit effect of always evaluating a certain unnamed const expression. In this approach, /* your expression here */ will be evaluated on top-level, or inside of impl blocks, even generic ones (though depending on the generic will lead to compilation errors, at least with the current way these are handled by the type system).

struct Foo;

struct Bar<T>(T);

impl Foo {
    const _: [(); {{assert!(std::mem::size_of::<Self>() == 0)}; 0}]; // this would work (even when mentioning `Self`)
}

impl<T> Bar<T> {
    const _: [(); {{assert!(std::mem::size_of::<Self>() == 0)}; 0}]; // this would still compiation-error
}

(playground with stable equivalents in named constants)

error: generic `Self` types are currently not permitted in anonymous constants
  --> src/lib.rs:10:49
   |
10 |     const _: [(); {{assert!(std::mem::size_of::<Self>() == 0)}; 0}]; // this would still compiation-error
   |                                                 ^^^^
   |
note: not a concrete type
  --> src/lib.rs:9:9
   |
 9 | impl<T> Bar<T> {
   |         ^^^^^^

whereas something like this would just evaluate:

impl<T> Bar<T> {
    const _: [(); {{assert!(true)}; 0}]; // works
    const _: [(); {{assert!(false)}; 0}]; // compilation error due to panic
}

(playground with stable equivalents in named constants)


Using something like enum Check<const C: ()> {}, this can looks a bit less ugly, e.g.:

const _: Check<{/* your expression here */}>;

struct Foo;
impl Foo {
    const _: Check<{assert!(std::mem::size_of::<Self>() == 0)}>;
}

struct Bar<T>(T);
impl<T> Bar<T> {
    const _: Check<{assert!(true)}>;
}

(-> moral equivalent on nightly with min_adt_const_params)

Comment on lines +590 to +592
impl Private {
pub const _: Private = Private; // no warning
}

@traviscross traviscross Jun 17, 2026

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

We warn today on pub const _: () = ();. Probably I'd expect us to warn here too ("visibility qualifiers have no effect") for that reason separately from public-in-private.

View changes since the review

@traviscross traviscross removed I-lang-nominated Indicates that an issue has been nominated for prioritizing at the next lang team meeting. P-lang-drag-1 Lang team prioritization drag level 1. labels Jun 17, 2026
@rust-rfcbot rust-rfcbot added final-comment-period Will be merged/postponed/closed in ~10 calendar days unless new substational objections are raised. and removed proposed-final-comment-period Currently awaiting signoff of all team members in order to enter the final comment period. labels Jun 17, 2026
@rust-rfcbot

Copy link
Copy Markdown
Collaborator

🔔 This is now entering its final comment period, as per the review above. 🔔

@RalfJung

RalfJung commented Jun 17, 2026

Copy link
Copy Markdown
Member

EDIT: Oh I missed that this is an RFC, not a rustc PR. 🤦 sorry for that.

The PR description is more than two years old and rather brief. Is there an up-to-date summary of this feature somewhere?

What is the intended behavior if the impl block is generic? In that case the constant is impossible to ever evaluate, so that should definitely be a hard error.

Even for non-generic impl blocks -- do we even guarantee to evaluate these constants? AFAIK we only guarantee evaluation of top-level (non-associated) consts. It seems strange to make a distinction between generic and non-generic impl blocks, so I don't think we should add a guarantee for non-generic impl blocks.

So IOW I don't think that this feature can actually work in general, and the special cases it can work in make it sufficiently odd that we're better off without it.

Could a t-lang member please raise these as formal concerns?

@steffahn

steffahn commented Jun 17, 2026

Copy link
Copy Markdown
Member

What is the intended behavior if the impl block is generic? In that case the constant is impossible to ever evaluate, so that should definitely be a hard error.

It’s also impossible to evaluate if the impl block isn’t generic. As the RFC points out, currently already

pub struct Unit;

impl Unit {
    const K: () = assert!(false);  // no error
}

(playground)

compiles successfully, and only errors once you make use of Unit::K somewhere.

7. The underscore const's value is evaluated in exactly the situations that an
ordinary named associated constant would be evaluated. Named associated
constants are evaluated when accessed. Underscore associated constants
cannot be accessed, so are never evaluated &mdash; only typechecked.

@RalfJung RalfJung Jun 17, 2026

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

I don't think we should have constants that exist just for type-checking and are never evaluated. That seems extremely confusing. The point of constants generally is to compute a value to be used later, or in the case of anonymous constants, to ensure that an expression can be successfully evaluated.

This RFC is repurposing/misusing an existing language feature to do something that it wasn't meant for. It's the language feature equivalent of "let's use this existing keyword for this new feature -- it makes no sense but adding a new keyword is too much of a hassle".

@RalfJung

RalfJung commented Jun 17, 2026

Copy link
Copy Markdown
Member

Yeah, after realizing that this is an RFC I have now read the relevant parts of it. :) Sorry for the confusion.

I don't think this is a good idea, we shouldn't have constants that are impossible to ever evaluate. See my in-line comment above for details (I have edited it a few times).
I am raising this as a formal concern as a t-lang advisor; could a t-lang member please tell the bot?

As the RFC points out, currently already
compiles successfully, and only errors once you make use of Unit::K somewhere.

Indeed. Constants exist to be used, that's when they get evaluated. Some constants are "always used" and hence implicitly evaluated; for those (and only for those!), _ makes sense as a name.

@traviscross

Copy link
Copy Markdown
Contributor

@rfcbot concern ralfj-constants-exist-to-be-used

#3527 (comment)

@rust-rfcbot rust-rfcbot added proposed-final-comment-period Currently awaiting signoff of all team members in order to enter the final comment period. and removed final-comment-period Will be merged/postponed/closed in ~10 calendar days unless new substational objections are raised. labels Jun 17, 2026
@traviscross

traviscross commented Jun 17, 2026

Copy link
Copy Markdown
Contributor

@RalfJung: Based on the model you're suggesting, would you also suggest we should be linting strongly, perhaps even at deny-by-default, against this?

struct S;
impl S {
    const _UNIQUE_NAME: u8 = 1 + 1; // How strongly should we lint?
}

@traviscross traviscross added I-lang-nominated Indicates that an issue has been nominated for prioritizing at the next lang team meeting. P-lang-drag-2 Lang team prioritization drag level 2. labels Jun 17, 2026
@RalfJung

Copy link
Copy Markdown
Member

That just looks like dead code to me. I don't mind us having dead code. But it seems quite strange to have code that's always dead.

Comment on lines +219 to +250
There's this less successful alternative using a where-clause with 0 trait
bounds on a wacky array type:

```rust
impl ::core::cmp::Eq for Thing
where
[(); {
let _: ::core::cmp::AssertParamIsEq<Field>;
0
}]:
{}
```

This does not work when the type has generic parameters, even with
`feature(generic_const_exprs)` enabled. The diagnostic pushes us toward
using a const function. For this use case, if a const function were
sufficient, there wouldn't be any use for a where-clause.

```console
error: overly complex generic constant
--> src/lib.rs:13:10
|
13 | [(); {
| __________^
14 | | let _: ::core::cmp::AssertParamIsEq<Field<'a, T>>;
15 | | 0
16 | | }]:
| |_____^ blocks are not supported in generic constants
|
= help: consider moving this anonymous constant into a `const` function
= note: this operation may be supported in the future
```

@steffahn steffahn Jun 17, 2026

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

This particular error message is no longer the case nowadays. (I would ask someone to please register a concern about this.)

View changes since the review

@steffahn steffahn Jun 17, 2026

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Moreover, by placing the AssertParamIsEq directly into the where bounds, instead of inside of a let _: …; inside of an array type, (and the whole thing on a separate impl block; to avoid overflow when using Self), the limitation that “This does not work when the type has generic parameters” is also easy to overcome.

For more details on that, let me quote from this other thread:

As an alternative: What if we allow you to have no body in the never-evaluated case?
[…]

Wait… these are as powerful as just listing Type:, in where clauses, aren’t they…

couldn’t the compiler just use a where clause already though? Just make derive(Eq) create something like this:

impl ::core::cmp::Eq for Thing {}

impl Thing where ::core::cmp::AssertParamIsEq<Field>: {}

From what I can tell by some testing yes, this would work totally fine!

@steffahn steffahn Jun 17, 2026

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

For more details on that, let me quote from this other thread

I would suggest for an additional concern to be raised on that point as well (or maybe combine the two), as I seem to have falsified the (implied) claim from the motivation

A number of alternative expansions come to mind using only existing syntax, none of which are adequate to this use case.

(Where “this use case” refers to finding a workable expansion for derive(Eq) that doesn’t need an additional, hidden method of Eq in order to function.)

The above implies that no alternative expansions using existing syntax would be possible, since it does demonstrate a solution that works, for all that I can tell (specifically one fitting the “Just do everything through where-clauses” subheading).

@traviscross traviscross Jun 17, 2026

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

This is in the motivation section, and we don't consider that section to be normative. So I don't see a need to raise a concern. Of course, as an editorial matter, we should fix errors in any section before merging.

@steffahn steffahn Jun 17, 2026

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

@traviscross (Maybe I should have separated these into separate comment threads as well.) I can understand your comment about the first concern. That specific code snippet not erroring anymore doesn't change


The second concern I was suggesting to raise though, I feel is different. My concern is - essentially - that in light of this alternative expansions that has been overlooked, I believe that the entire motivation section ends up being “wrong” (i.e. failing to actually provide a conclusive motivating argument). To qualify this statement slightly: I could of course be missed overlooking something myself; there could exist some reason for why this alternative expansion isn’t actually “adequate”.

Still, to me this seems much more significant in nature than just “editorial”!


I might of course have myself missed a reason

I currently don’t think I’m wrong on this though. If I am right, and if the feature would still seems desirable to have at that point, then that would require some significant additional motivation to be documented. If there were to be a fundamental changes to the motivation section, then I don’t believe the RFC should not be in an already-accepted state at that point; instead further (at least final) commentary should still be allowed.

Assuming that once completed, an FCP isn’t usually undone1, I thus believe this this should itself be a blocking concern.

Footnotes

  1. correct me if I’m wrong about this

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

As mentioned in #3527 (comment), it's my sense that in the meeting we mostly set aside the motivation given in the RFC. For my part, I certainly did. So undermining the motivation section doesn't move me. We sometimes accept the normative parts of an RFC under a different theory than the author held.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Nice find, @steffahn.

I wouldn't have picked that discussion if I knew the motivation given in the RFC was unneeded. My understanding at the time was that we could come up with better ways in the future to express this, not that they already existed.

Now that we've had the discussion, and we've come up with a way to make everything fit in a way that simplifies the reference according to @traviscross, that's fine. In doing so, we also reduce the number of edge cases that macros have to handle specially. I'll leave my box checked.

@traviscross

traviscross commented Jun 17, 2026

Copy link
Copy Markdown
Contributor

@RalfJung: Perhaps, reasonably, you're focusing on the motivations given in the RFC? For context, in the lang meeting today, where we read and discussed this, we mostly set aside those motivations. All present agreed there are probably better ways to solve the use cases described here. Discussion instead turned on the point that, for whatever its flaws, we already accept this behavior today, including having associated constants whose initializers are never evaluated. E.g.:

mod _0 {
    pub struct S<T>(pub T);
    impl<T> S<T> {
        const _1: u8 = panic!(); // Never evaluated; always dead.
    }
}

The behavior of the proposed associated const _ exactly matches the behavior of an unused associated const _UNIQUE_NAME. It just avoids having to pick a unique name.

For my part, given that this doesn't introduce something new and that I would expect const _ to work in the same way as const _UNUSED_UNIQUE_NAME everywhere, I'm sufficiently motivated on the grounds of simplifying our language definition to allow const _ consistently.

Comment on lines +644 to +647
7. The underscore const's value is evaluated in exactly the situations that an
ordinary named associated constant would be evaluated. Named associated
constants are evaluated when accessed. Underscore associated constants
cannot be accessed, so are never evaluated &mdash; only typechecked.

@Darksonn Darksonn Jun 17, 2026

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

That seems surprising. It's relatively common to define this:

macro_rules! static_assert {
    ($condition:expr $(,$arg:literal)?) => {
        const _: () = ::core::assert!($condition $(,$arg)?);
    };
}

and I might expect

impl<T> MyStruct<T> {
    static_assert!(size_of::<Self>() <= 24);
}

to work as a post-mono error.

View changes since the review

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Agreed it's surprising (which is why we decided in the proposed FCP to lint it). It's probably a good reason to come up with a better way than anonymous constants for expressing these static checks. Yet this too compiles without error:

macro_rules! static_assert {
    ($condition:expr $(,$arg:literal)?) => {
        const _X: () = ::core::assert!($condition $(,$arg)?);
    };
}

struct S<T>(T);
impl<T> S<T> {
    static_assert!(size_of::<Self>() <= 24);
}

fn main() {
    let _x = S([0u8; 32]);
}

Under the proposed FCP, we would lint both the above and the case you gave as dead_code (or similar), which would flag to the user that the check is inoperative.

@RalfJung

Copy link
Copy Markdown
Member

Included in this proposal is striking the text from this RFC that suggests that we will suppress the dead_code lint in these cases. We do want to lint on these cases as the body is not evaluated. It should work in exactly the same way as unused underscore-prefixed associated constants (e.g., const _X) at the time of stabilization, which will mean that we need to change the behavior of those.

So to be clear, this (as a lib crate) should emit two warnings, one for each of these consts?

pub struct S;

impl S {
    const _X: i32 = 1;
    const _: i32 = 1;
}

@traviscross

Copy link
Copy Markdown
Contributor

Included in this proposal is striking the text from this RFC that suggests that we will suppress the dead_code lint in these cases. We do want to lint on these cases as the body is not evaluated. It should work in exactly the same way as unused underscore-prefixed associated constants (e.g., const _X) at the time of stabilization, which will mean that we need to change the behavior of those.

So to be clear, this (as a lib crate) should emit two warnings, one for each of these consts?

pub struct S;

impl S {
    const _X: i32 = 1;
    const _: i32 = 1;
}

Correct.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

A-const Proposals relating to const items A-syntax Syntax related proposals & ideas disposition-merge This RFC is in PFCP or FCP with a disposition to merge it. I-lang-nominated Indicates that an issue has been nominated for prioritizing at the next lang team meeting. I-lang-radar Items that are on lang's radar and will need eventual work or consideration. P-lang-drag-2 Lang team prioritization drag level 2. proposed-final-comment-period Currently awaiting signoff of all team members in order to enter the final comment period. S-waiting-on-author Status: This is awaiting some action from the author. 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.