Open chirsz-ever opened 6 months ago
I'll just note that we could add macros to the prelude on an edition boundary.
Also, are there any places where macro invocations are not currently allowed, but const _: () = assert!(...);
is?
Finally, is static_assert
the best name? Would const_assert
be better?
Assuming inline_const
gets stabilized, this could also be spelled:
const { assert!(0 == 1) }
This has the advantage that you don't need a new macro. Though the advantage of the macro is that it'll work as a top level item.
@ChrisDenton We could, theoretically, make const { assert!(0 == 1); }
work at top level. That'd require a lang proposal, but it seems doable if someone defines the semantics.
I'd be happy to review such a proposal if someone wanted to pitch it. It'd likely be a fairly simple lang RFC. "Allow const { ... }
blocks at top level, with the semantic of evaluating everything in the block."
Wouldn't inline-const have the same diagnostic issue as OP's const _: () = assert!(condition);
?
@cuviper I think we could easily make const blocks have better error messages than const _
. We could have it just say assertion failed
.
One of the issues with the const _: () = assert!(...)
pattern is just that it isn't immediately obvious that all const evaluations get called all the time, and maybe we don't wan't that to be the case forever (maybe unused evaluations could be validated but pruned if unused). Having a dedicated macro or non-assignment block makes the behavior more intuitive.
@joshtriplett Top-level const
block, just like comptime
block in Zig and static
block in Nim? This deserves a new RFC.
I think the semantics could be easily defined:
const {
do_sth();
}
is identical to
const _: () = {
do_sth();
};
@chirsz-ever In a context that has const generics (e.g. a function with const generic arguments), it'd also be possible for const { ... }
to look at const generics, which const _: () = { ... };
can't.
maybe we don't wan't that to be the case forever (maybe unused evaluations could be validated but pruned if unused).
I doubt that's possible. Plenty of crates already rely on the current behaviour.
As one of the alternatives to this proposal, one should consider the static-assertions crate. It also provides many examples of more complex assertions (type equality/inequality, presence or absence of trait implementations, object safety).
Personally I feel that simple equality & predicate assertions aren't worth adding a separate feature. They may have an issue with discoverability, but a separate static assertion feature would also require reading a specific manual, and we could just add a "static assertions" chapter to the reference/whatever other manual, with the const item trick documented. More complex assertions are a more compelling case for a separate feature. They require more complex tricks than a simple anonymous const item, and they are hard to provide good error messages for without special compiler support.
This could "just" be a change in constant evaluation (miri) to produce a nicer error message, e.g.
error[E0080]: constant assertion failed
--> src/main.rs:1:15
|
1 | const _: () = assert!(1 == 2, "panic message");
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ the evaluated program panicked
= note: panic message
= note: this error originates in the macro `$crate::panic::panic_2021` which comes from the expansion of the macro `assert` (in Nightly builds, run with -Z macro-backtrace for more info)
instead of the current
error[E0080]: evaluation of constant value failed
--> src/main.rs:1:15
|
1 | const _: () = assert!(1 == 2, "panic message");
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ the evaluated program panicked at 'panic message', src/main.rs:1:15
|
= note: this error originates in the macro `$crate::panic::panic_2021` which comes from the expansion of the macro `assert` (in Nightly builds, run with -Z macro-backtrace for more info)
especially since assert!
is already a compiler built-in.
The macro should be able to be used in generic context to assert about generic parameters
To note, this defers the assertion from pre-monomorphization to post-monomorphization. I personally think it's important that the difference is made clear as we make post-monomorphization const
panics more accessible. IMO const_assert!(...)
is somewhat ambiguous between being const _: () = assert!(...);
(pre-monomorphization, cannot capture generics) or const { assert!(...) };
(post-monomorphization, can capture generics), as both are still constant evaluated asserts, but static_assert!
should unambiguously mean a pre-monomorphization assert.
The difference between const _: () = assert!(...);
and const { assert!(...) };
isn't obvious if you don't already know that it exists, but at least the different syntactic forms provide some amount of affordance to the different semantics, as does the fact that only the latter form can use captured generics. For that reason I think it's important that if std provides a post-mono-capable const_assert!
that a) std also provide a pre-mono static_assert!
, and b) const_assert!
should always be post-mono (never eagerly evaluate the assertion pre-mono).
The fact that const { assert!(...) };
is easier to "reinvent" than const _: () = assert!(...);
does lead me to thinking std should provide a pre-mono static_assert!
, if only to have a nice visible location to call out that static_assert!
is not const { assert!(...) };
. The fact that post-mono errors should be avoided does suggest std shouldn't provide a post-mono const_assert!
.
I will point out that static_assert
in C++ is post-monomorphization, so I think it's a bad idea to use the same name in Rust for a pre-monomorphization version because people won't expect it to behave differently.
This just makes me think new macros would be more confusing then helpful. Most people will end up using const { assert!(...) }
and that's fine.
If people do need the pre-monomorphization version and don't know about it then is that something that diagnostics could call attention to?
I will point out that
static_assert
in C++ is post-monomorphization
Small clarification: while C++ (dependent[^1]) static_assert
is done at template instantiation time, and this is more eager than post-monomorphization constant evaluation in Rust. Instantiated but unused static_assert
will fail C++ compilation, but rustc is (at least currently; see bottom of post) surprisingly eager to skip evaluation of constants which don't directly impact the compilation result[^3], even if said evaluation could panic, causing compilation to fail as a side effect.
[^1]: Prior to P2593/CWG2518 (C++11 defect report)[^2], a static_assert
in a template definition which cannot be instantiated to pass made the program ill-formed, no diagnostic required. MSVC currently still issues a pre-instantiation error for non-dependent failing static_assert
.
[^2]: That C++ removed pre-instantiation static_assert
failures is a decent indicator that we should probably avoid having const
blocks conditionally pre- or post-mono based on if they capture generics. I wouldn't want us to end up needing dependent_false
style tricks like MSVC C++ needs.
The most equivalent behavior for Rust to C++ `static_assert` (post CWG2518, ignoring the differences between instantiation-time and monomorphization-time failure) would be to permit `const` blocks at item and `impl` scope and for `const` blocks to always capture (be dependent on) all generics in scope.
[^3]: In this way, while we do have stronger promises around const
evaluation (and panics) happening at compile-time nowadays, its roots as "just an optimization" over copy/paste doing the evaluation at each runtime usage show through here. You shouldn't be trusting constant assertions without somehow "observing" "evaluation" of the assertion because of this.
C++ compilers don't really have an equivalent to cargo check
, but the fact check doesn't diagnose post-mono errors (only cargo build
does any monomorphization work) certainly exacerbates the downsides of post-mono assertions.
I would personally love for constant evaluation (and thus constant evaluation errors) to happen at instantiation time instead of monomorphization time. But last I heard, the experiment with tracking required constant instantiations the way type instantiations are[^4] led to large (double-digit percentage, IIRC) regressions in check times[^5], so is unlikey to happen for check. That optimizations (dead code elimination) can impact the set of constants evaluated by build
is considered a bug, at least; see the various issues reachable from the inline-const tracking issue.
[^4]: That type instantiations are always checked while const
instantiations aren't is one reason to still use the array type equality version of const_assert!
even though the const-panic version has significantly better diagnostics.
[^5]: Doing more work takes time, who would've thought. And most significant compilation time improvements have been around culling processing of unneeded monomorphization, so "fixing" this functionally reverts an unfortunate quantity of compiler perf work.
In short: it's complicated. Adding a macro is a value add if (and likely only if) it can help (document and) tame the complexity somewhat.
Having const asserts not happen on dead call trees (where the instantiation isn't "real" in the sense that it could be known at compile time that it'll never be called) can be desirable if the call tree itself has been eliminated by a if const {}
.
Making const eval happen more eagerly makes asserts run for code that's intended never to be called and the resulting errors may logically be false positives.
So I think pushes for more eager const eval are going to hurt some uses if no alternative to opt out is provided.
The solution for that is to actually have "const if
". Semantics should never rely on optimization. (User linker shenanigans notwistanding.)
const
block is stabilized in 1.79. Now we have complete mechanisms for triggering compile-time errors.
// the most case
const { assert!(condition) };
// out of functions
const _:() = assert!(condition);
Do you know how to define a static_assert
macro expanded to the second form when placed out of functions?
There is no functionality available to macros to expand to different tokens based on whether it is in item position (outside function) or expression/statement position (inside function). IIRC there was some tentative support for allowing top-level const
blocks, which would allow using just const { assert!( … ) }
in both positions.
Personally, I still think there is some benefit in distinguishing between the pre-mono[^1] form (const _: () = { … }
) and the post-mono[^2] form (const { … }
). But I also agree that the simple form being available uniformly is valuable.
[^1]: The guarantee that if a constant value is "evaluated" at runtime then it was evaluated at compile time is FCP accepted, as is a similar guarantee for top-level const
items. While extending that same guarantee to items in function scope makes some sense, not doing so also makes some sense
[^2]: The conditionality of exactly when post-mono checks are asserted are why I don't particularly like them.
Proposal
Problem statement
People often want to check some conditions at compile-time, but currently Rust does not have an intuitionistic way to do it.
Motivating examples or use cases
Here are use cases in rust-lang/rust and Rust-for-Linux:
https://github.com/rust-lang/rust/blob/432fffa8afb8fcfe658e6548e5e8f10ad2001329/library/std/src/io/error/repr_bitpacked.rs#L352
https://github.com/rust-lang/rust/blob/432fffa8afb8fcfe658e6548e5e8f10ad2001329/compiler/rustc_index/src/lib.rs#L43
https://github.com/rust-lang/rust/blob/432fffa8afb8fcfe658e6548e5e8f10ad2001329/compiler/rustc_middle/src/ty/consts/kind.rs#L75
https://github.com/Rust-for-Linux/linux/blob/cae4454fe293141be428436e5278261494cef02a/rust/kernel/static_assert.rs
People define their own versions of
static_assert
macros to meet the requirements.Solution sketch
Add a new macro
static_assert
which check it's argument in compile-time.static_assert
is allowed to used at module's top-level, not likeassert
which can only used in a function or initialization context:Like
assert
anddebug_assert
,static_assert_eq
andstatic_assert_ne
could also be added to the standard library.The error message for failed assertion would be like:
The macro can take a custom error message like
assert
, but currently we cannot call non-const formatting macro in constants. My opinion is just keeping the behavior same asassert
, in the future when formating is allowd in const context, we would automaticly get the feature.The macro should be able to be used in generic context to assert about generic parameters:
Maybe some drawbacks:
todo
,dbg
andmatches
were added to prelude, maybe this is not a big problem?const_assert
. I thinkstatic_assert
is better, becaus C, C++ and rustc developers just used it.Alternatives
There are several alternatives, the precondition is that
const_panic
is stablized since Rust 1.57, so we could perform assertion at const context, which would cause a compile-time error.We can write
const _: () = assert!(condition)
. This is stable, and works for most cases. The drawback is that it is not intuitionistic with grammar noise. People need to lean them from somewhere, such as TRPL or Rust Cookbook.Another drawback of
const _: () = assert!(condition)
is that, due to E0401, you cannot assert about generic parameters:playground
This code cannot be compiled currently, so Rust-For-Liunx has to use a
build_assert
macro.With
const
block stable since 1.79, we could writeconst { assert!(condition) }
to perform static assertion. It has no problem with generic parameters:playground
But the
const
block has another issue: it is an expression so it is not able to be written at top level. I think it is easy to define the top-levelconst
block:is identical to
Links and related work
static_assertions
crateWhat happens now?
This issue contains an API change proposal (or ACP) and is part of the libs-api team feature lifecycle. Once this issue is filed, the libs-api team will review open proposals as capability becomes available. Current response times do not have a clear estimate, but may be up to several months.
Possible responses
The libs team may respond in various different ways. First, the team will consider the problem (this doesn't require any concrete solution or alternatives to have been proposed):
Second, if there's a concrete solution: