RFC: Associated const underscore#3527
Conversation
73c9d1f to
9db99f3
Compare
|
When does associated const code get run? at type definition? when substituting concrete types into generic arguments? never? pub struct S<T>(T);
impl<T> S<T> {
const _: () = assert!(core::mem::size_of::<T>() % 2 == 0);
}when does the |
|
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. |
9db99f3 to
5055393
Compare
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 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"
}; |
|
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: () = ...;
} |
|
This would be so helpful! Thanks for writing this, David! In favor of these constants being evaluated: rust-lang/rust#112090 |
|
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. |
52c6f4b to
2495bd6
Compare
|
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 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 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 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.) |
|
One further thought: the whole |
|
@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 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. |
| # Reference-level explanation | ||
| [reference-level-explanation]: #reference-level-explanation |
There was a problem hiding this comment.
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
};
}|
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. |
|
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 |
|
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 @rfcbot fcp merge lang |
|
@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. |
|
For the change to the text required... @rustbot author |
|
I suggested having the @rfcbot reviewed |
| 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>; | ||
| }; | ||
| } | ||
| ``` |
There was a problem hiding this comment.
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>;
}There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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>: {}There was a problem hiding this comment.
From what I can tell by some testing yes, this would work totally fine!
There was a problem hiding this comment.
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)}>;
}| impl Private { | ||
| pub const _: Private = Private; // no warning | ||
| } |
There was a problem hiding this comment.
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.
|
🔔 This is now entering its final comment period, as per the review above. 🔔 |
|
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 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? |
It’s also impossible to evaluate if the pub struct Unit;
impl Unit {
const K: () = assert!(false); // no error
}compiles successfully, and only errors once you make use of |
| 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 — only typechecked. |
There was a problem hiding this comment.
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".
|
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).
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!), |
|
@rfcbot concern ralfj-constants-exist-to-be-used |
|
@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?
} |
|
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. |
| 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 | ||
| ``` |
There was a problem hiding this comment.
This particular error message is no longer the case nowadays. (I would ask someone to please register a concern about this.)
There was a problem hiding this comment.
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
whereclause already though? Just makederive(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!
There was a problem hiding this comment.
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).
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
@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
-
correct me if I’m wrong about this ↩
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
|
@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 For my part, given that this doesn't introduce something new and that I would expect |
| 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 — only typechecked. |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
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. |
View all comments
Allow
_for the name of associated constants. This RFC builds on RFC 2526 which added support for freeconstitems with the name_, but not associated consts.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 ofSelf.Rendered