This is a concrete proposal to address the problem described in Compile-time faillable expression.
Note: I don't think this feature is a high priority for Rust, and I am writing this in the spirit of “here is how we could solve this problem” more than “please add this as soon as possible”. I do think that it should be quite simple to implement, and provides benefit proportionate to that.
Summary
Allow the ?
operator to be used in const
items and blocks, to propagate the error “to the developer”.
Motivation
Sometimes when constructing a constant value, steps in the construction are fallible;
a constructor might have preconditions whose failure is returned by Result::Err
or Option::None
rather than by panicking
(for example, std::num::NonZero::new()
).
In normal execution at run time, this is useful type safety ensuring the error is handled appropriately.
However, at compile time, it is usually the case that the best possible handling is simply to stop compilation and report details of the failure. The straightforward solution today is to use the methods .unwrap()
or .expect("msg")
. (In the rest of this text, I will use .unwrap()
to refer to both.)
However, in Rust code outside of constants, .unwrap()
is a sign of a hazard: it means “this code may panic under these conditions”, and a code reviewer or other reader may need to take a moment to consider whether this error case and panic branch is:
- impossible,
- unrecoverable,
- not worth handling better, or
- a sign of code that needs to be improved.
.expect()
is slightly more helpful because it has a message, but is much less often used (and the message might still not be clear about whether this is an impossible case). Therefore, it is useful to enable programs to be written without occurrences of .unwrap()
, to reduce the number of occasions for these considerations.
Furthermore, there are case where effectively-constant values are most conveniently written inline and not as constants, meaning there is no guarantee at all that the code will not panic provided it compiled:
some_function_that_wants_nonzero(NonZeroU32::new(2).unwrap());
This code could be made more explicit about its lack of a run-time panic by adding a const
block:
some_function_that_wants_nonzero(const { NonZeroU32::new(2).unwrap() });
However, this is now quite verbose; we are spending 37 characters and 13 tokens to express the constant “2”.
I believe Rust can and should offer something better here, and I believe the ?
operator is a well-precedented, if not totally ideal, way to do so.
Besides making use of existing types clearer, this will also make it more pleasant to decide that a type should have error-returning rather than panicking constructors, or to decide to use a constrained type that must have a fallible constructor instead of one which has no constraints.
Guide-level explanation
In addition to being usable in functions to propagate errors to their callers, the ?
operator can be used in const
items and in const {...}
blocks to propagate errors “to the author of the code” — that is, cause a compilation error. This behaves very similarly to using .unwrap()
in a constant, except that the error message is more straightforward because it is not a panic message:
use std::num::NonZeroU8;
const OLD_STYLE: NonZeroU8 = NonZeroU8::new(10 - 10).unwrap();
const NEW_STYLE: NonZeroU8 = NonZeroU8::new(10 - 10)?;
error[E0080]: evaluation of constant value failed
--> src/lib.rs:2:30
|
2 | const OLD_STYLE: NonZeroU8 = NonZeroU8::new(10 - 10).unwrap();
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ the evaluated program panicked at 'called `Option::unwrap()` on a `None` value', src/lib.rs:2:54
error[E0080]: evaluation of constant value failed
--> src/lib.rs:3:30
|
3 | const NEW_STYLE: NonZeroU8 = NonZeroU8::new(10 - 10)?;
| ^^^^^^^^^^^^^^^^^^^^^^^ this expression returned `None`
However, unlike .unwrap()
, the ?
makes it immediately visible that a run-time panic is not a possibility here — the error is being propagated to the party who can handle it appropriately (which happens to be the developer rather than other code).
?
can also be used in inline const
blocks:
some_function_that_wants_nonzero(const { NonZeroU32::new(2)? });
A single const expression can of course contain multiple uses of ?
:
// A program might choose to define such an alias-function for concision.
// This could also unwrap() internally, but ? means it doesn’t need to.
fn nz(x: usize) -> Option<NonZero<usize>> {
NonZero::<usize>::new(x)
}
const ARRAY_SIZE: Vector3<NonZero<usize>> = vec3(nz(12)?, nz(64)?, nz(2)?);
Reference-level explanation
Scoping
The ?
operator currently has the effect of a possible early exit from the nearest enclosing
- function or closure body,
async {}
block, ortry {}
block.
This RFC adds another case to this list, the nearest enclosing
const
item orconst {}
block.
?
in this position is currently an error, so adding this case is fully backwards-compatible (but that error is, if I understand correctly, more due to lack of const trait function calls in general than a specific exclusion of this case, so that might not be true indefinitely).
Execution
When const evaluation of expr?
results in an Err
(or other “residual” case) passed from expr
to ?
, the compiler reports an error. This error should:
- have the span of the
expr
(not theexpr?
, and especially not the whole constant expression), and - if the value matches
Result::Err(e)
, then attempt to evaluateformat!("{e}")
(or even an entireError::source()
chain) and display that if it succeeds.
These together should mean that that developers of code containing ?
will get the best error messages that the developers of the fallible functions they call can provide, in a way which unwrap()
does not. (Though it is possible that unwrap()
could be given some const-eval-only magic to talk to the compiler error system and improve that too.)
Unlike run-time ?
, there is no call to FromResidual
(the mechanism which in the case of Result
becomes a call to From::from()
on the error type), because there is no expected return type to convert to.
Other positions of ?
This RFC does not extend the ?
operator to constant expression positions which are not marked with the const
keyword. For example,
foo::<{ bar()? }>()
should remain an error. This is to avoid misleading readers who may not be familiar with exactly which things in Rust are merely required to be constants (as opposed to being purposefully declared as constants) into thinking that this is a run-time error propagation.
Drawbacks
-
The
?
operator usually means “don’t fail yet; let someone else handle this error”. We would be adding a case where the?
operator always causes failure (of compilation) on error, within the scope of only a single expression, rather than involving at least the caller of the current function and giving some code a chance to recover from the error. This could be seen as diluting its meaning. -
A vaguely plausible alternative meaning of
const { foo()? }
would be that it is identical toconst { foo() }?
; i.e. evaluate to a constantResult
, then perform run-time control flow based on its value. Not doing that could be surprising. -
If the reader of code inside a large
const {}
block does not notice that it is aconst
block (because they are looking at the code near the end of the block, not the beginning), they would expect the?
inside to perform run-time control flow. Arguably such large blocks would be unclear code in themselves, though. -
Macros which put caller-supplied code inside
const
blocks will inherit this meaning of?
, which might be undesirable; it is a new way in whichconst {}
is no longer merely a restriction, but changes the meaning of some possible contents compared to{}
. (Addingconst
could already change floating-point evaluation behavior, and possibly other evaluation results in the future, but “slightly different values” is a different sort of quirk than “control flow with a different destination”.) -
This RFC proposes a piece of syntax that, when evaluated, produces a specific compilation error, rather than an ordinary panic that happens to have gone through the constant evaluator. This might be difficult to implement; I am not familiar with the implementation of const evaluation of panicking/unwinding.
-
Outside of constants, a difference between naive use of
unwrap()
and naive use of?
, particularly onOption
s, is thatunwrap()
produces a panic message identifying the particular call site, whereas?
propagates theNone
with no information about which?
usage produced it, which can make errors less debuggable. The proposed behavior of const?
does not have this problem, but we might end up encouraging more uncautious use of?
in non-const contexts.
Rationale and alternatives
-
If we suppose that the motivation for this RFC is a problem worth solving, then the chief alternative to
const { expr? }
would be to provide a dedicated syntax for a “fallible constant”. For example, in the discussion which spawned this RFC, the original proposal wasexpr!
.The rationale for using the
?
operator instead is that it allows us to introduce no new syntax and almost no new semantics; we are taking the combination ofconst
and?
which was previously an error, and giving it a meaning which is almost entirely a logical extension of how?
behaves in other contexts. However, it is not ideal from the perspective of fallible numeric literal construction, because it requires adding 2 pieces of syntax around the already verbose constructor call. -
A macro could solve 80% of the problem; for example, if we define
macro_rules! k { ($e:expr) => { const { match Try::branch($e) { // needs const traits :( ControlFlow::Continue(x) => x, ControlFlow::Break(e) => panic!("{e:?}"), // needs const fmt :( } } } } // or for compatibility with current stable, but duck-typed: macro_rules! k { ($e:expr) => { const { $e.unwrap() } } }
then
k!(NonZeroU8::new(2))
is equivalent in control flow to our proposedconst { NonZeroU8::new(2)? }
, and a few characters shorter. However, it cannot produce nearly as nice an error message, and it is less self-explanatory. It could be given a more explanatory name, likefallible_const! { NonZeroU8::new() }
to suggest the analogy withconst {}
, but the longer the name, the less useful it is for introducing readable inline constants.It is also plausible that some fallible constants will contain many failure points, which would require many uses of wrapping
k!()
rather than tidy postfix?
. -
We could add nothing to the language or standard library, and endorse continuing to use ordinary
.unwrap()
in constants. -
We could expect library authors to provide panicking
const fn
s as a concise alternative tounwrap()
with error-returning ones. This would be bad because- libraries would add more largely-redundant API surface,
- which could then be used outside
const
context, resulting in more unmarked panic sites, whereas.unwrap()
is a consistent panic site marker for all calls to functions returningResult
orOption
.
If Rust had a kind of function which could only be called within const evaluation, then that would be a useful alternative for this situation, but that would be a whole other proposal with its own syntax and drawbacks.
-
We could add more specific methods to the
unwrap()
&expect()
family, along the lines of howtodo!()
,unreachable!()
andunimplemented!()
are more specific thanpanic!()
. These could make it clear when an unwrap is “impossible to fail”. However, this would not give any additional guarantee that there actually won't be a run-time panic; it would merely make the intent of the author clearer.
Prior art
I am not aware of any similar features in other languages or past proposals.
Unresolved questions
-
(Presuming that RFC 3058
try_trait_v2
is not superseded:) It may be necessary for const?
to allow only some subset of the types implementingTry
(e.g.Option
andResult
only), since there is no declared or inferrable return type from the block, and soFromResidual
will not be usable in the exact same way. I have not considered carefully whether this is true or not.I believe it would be an acceptable outcome for const
?
to be stabilized while accepting onlyOption
andResult
, then expanded later or never. -
The exact quality of error message will depend on what is feasible to implement in the compiler, which might be significantly worse than what is proposed in this RFC.
Future possibilities
-
If fallible constants are a very frequent pattern (or very frequent in some problem domain), we might choose to add a dedicated syntax or macro for them in addition to supporting the composition of
const
and?
. -
We might decide that it's OK to allow unmarked usage like
foo::<{ bar()? }>()
.