Pre-RFC: `?` operator in constants

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, or
  • try {} block.

This RFC adds another case to this list, the nearest enclosing

  • const item or const {} 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 the expr?, and especially not the whole constant expression), and
  • if the value matches Result::Err(e), then attempt to evaluate format!("{e}") (or even an entire Error::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 to const { foo() }?; i.e. evaluate to a constant Result, 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 a const 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 which const {} is no longer merely a restriction, but changes the meaning of some possible contents compared to {}. (Adding const 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 on Options, is that unwrap() produces a panic message identifying the particular call site, whereas ? propagates the None 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 was expr!.

    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 of const 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 proposed const { 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, like fallible_const! { NonZeroU8::new() } to suggest the analogy with const {}, 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 fns as a concise alternative to unwrap() 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 returning Result or Option.

    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 how todo!(), unreachable!() and unimplemented!() are more specific than panic!(). 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 implementing Try (e.g. Option and Result only), since there is no declared or inferrable return type from the block, and so FromResidual 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 only Option and Result, 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()? }>().

18 Likes

I wonder if expanding the let-else feature to const definitions would be a better alternative. You can already use that today, at some verbosity cost:

const NEW_STYLE: NonZeroU8 = {
    let Some(x) = NonZeroU8::new(10 - 10) else {
        panic!("`NEW_STYLE` was `0`")
    };
    x
};
2 Likes

I have mixed feelings about this. It still requires to use const {} blocks in expression context, so this only reduces boilerplate from .unwrap() to ?, which isn't a big reduction.

It's less clear about the intent. In non-messy code, .unwrap() signals that I know/trust something won't fail, but ? is associated with possibility of runtime failures. Const-unwrapping would be a better served by something like Swift's ! counterpart to ?.

const { x? }/const { x }? won't have the same ok-wrapping symmetry as try { x? }/try { x }?.

This problem could be tackled better form completely different direction.

  • Perhaps there should be NonZeroU8::const_new(2) with the function declared in a way that makes it const-only (e.g. pub const fn const_new(v: const u8)), so that it can panic! and be certain it will be a compile-time panic.

  • Rust needs support for more literals (ideally user-defined). In this particular example it could be something like 2_nzu8.

9 Likes

I assume you mean something like:

const Some(NEW_STYLE): NonZeroU8 = NonZeroU8::new(10 - 10) else {...};

The problem with that is that doesn’t nest at all: it can't express

const FOO: Foo = foo(...)?.with_bar(...)?;

unless you also add a try block, and it doesn't fit at all in inline consts which have no pattern side. It’s much more important to help with inline consts (which have a small syntactic cost already) than with named consts.

Many readers are not so confident as this that an unwrap() does mean that. (And in general, people have wildly different ideas about how and when unwrap() should be used.) The purpose of this proposal is to make something which is purely syntactically obviously not a run-time failure.

I did mention that in the alternatives section. As I see it, this has the problem that it expects library authors to think harder about serving const use cases, and to clutter their API with such functions, rather than allowing the use of existing fallible constructors. (To be fair, if a library author doesn’t think about that, it’s likely the type won’t end up practically const constructible at all.)

That does not help with data types which are not numbers. (It is true that I haven’t presented any concrete examples of such.)

TBH, what I really want is for Rust to have a magic compile-time-only IntegerLiteral "type".

So you can just write foo_that_takes_nonzero(3) and it works (via some impl const FromIntegerLiteral for NonZeroU8 or whatever), the same way that foo_that_takes_u8(3) and foo_that_takes_i32(3) work without a suffix.

(Suffixes don't namespace well and are annoying to use anyway, so let's use the type system if we can.)

11 Likes

Most of the pre-RFC doesn't make it clear that Try is still involved; it sounds completely divorced from the Try trait until you mention FromResidual. I'd expect it to work with "any" Try implementor, and to call Try::branch, etc. In contrast with making Result and Option magical for this use case.[1]

Of course calling Try::branch can't work with "any" implementor; using Try would require ~const traits or whatever we're calling it now. But I think what you proposed could still be phrased in terms of traits.

// core-provided unwrap-in-const, effectively
pub trait ConstTry: const Try<Residual: Debug> {}
impl<T: const Try<Residual: Debug>> ConstTry for T {}

// Or opt-in
pub trait ConstTry: const Try {
    // and perhaps an `expect` analog, or `const_unwrap`, etc etc
    const fn compile_fail(Self::Residual) -> !;
}

  1. Incidentally I don't see why the MVP can't work with all of the stableTry implementors. ↩︎

1 Like

I’ll keep in mind to clarify the relationship if this becomes an actual RFC. However, I didn’t focus on Try because my intent is that this feature could be implemented and stabilized before Try and before const traits. The surface behavior is just “you can, for at least Option and Result, use ? in const eval” without specifying how that happens, just like today you cannot write programs on stable which depend on Try existing, but can still use ?.

8 Likes

user-defined number literals would be easy to add by simply desugaring 12345suffix to suffix!(12345). For example, 8470984567059454067496_bigint would desugar to bigint!(8470984567059454067496_), and 2nz would desugar to nz!(2). This could be defined as

macro_rules! nz {
    ($lit:literal) => {
        const { ::core::num::NonZero::new($lit).unwrap() }
    };
}

But suffixes starting with b, o, x, or e, only followed by (hex)digits, wouldn't be supported, since 0b11 is a 3 in binary, and 2e5 is 2 * 105.

The Generic Integers V2 RFC mentions something similar in the future possibilities: Bound-based generalisation would allow you to replace NonZeroU8 with int<1..=255> – and every integer literal in that range could be inferred as this type.

I share the concerns already mentioned: When I see ? anywhere, I really don't expect it to panic in case of an error. Using unwrap in const contexts is fine. About the motivation:

This also applies to

  • Array indexing (out of bounds errors)
  • String slicing (non UTF8 boundaries, out of bounds errors)
  • recursion (stack overflow)
  • float division (division by zero)
  • integer arithmetic (by default, integer overflow in debug mode panics)
  • lots of library functions, e.g. AtomicI64::load, or <[T]>::windows

Integer casts with as do not panic, but are just as hazardous (they may truncate or wrap around).

In my experience, it is only possible to write panic-free code with thorough testing and fuzzing. You simply cannot stop using all the language and library features that may panic, if your application is moderately complex. I actually consider .unwrap() less hazardous than some other language features, because it is explicit and requires me to consider the error case when writing the code. When fuzzing my Rust code, most of the panics I encountered were stack overflows, integer overflows, and out-of-bounds errors. I do not agree with the sentiment that .unwrap() should be avoided at all cost. Sometimes it is the most elegant solution for a problem. Most of the time, it's easy to show that the panic is unreachable. And if it's in a const context, it's obvious that the panic can't possibly be an issue.

3 Likes

I don’t think exiting const eval via ? would then be considered panicking, but rather that the compiler just notionally inserts a try block around the const block and reports any early return as a compiler error (which would be distinct from the error that panicking in const currently causes – more in line with what you get from a Result-returning main).

The biggest problem with the proposal AFAICS is the fact that you’d be using the "prepared for error" syntax to handle "error should be impossible" cases.

1 Like

In term of prior act C++ added lots of way to manipulate exceptions in constexpr context to report errors. Le latest in date is this (I’m not sure it has been merge yet, but there is a high chance it will eventually be):

https://isocpp.org/files/papers/P3068R6.html

Since adding the constexpr keyword in C++11, WG21 has gradually expanded the scope of language features for use in constant-evaluated code. At first, users couldn't even use if, else, or loops. C++14 added support for them. C++17 added support for constexpr lambdas. C++20 finally added the ability to use allocation, std::vector and std::string. These improvements have been widely appreciated, and lead to simpler code that doesn't need to work around differences between normal and constexpr C++.

The last major language feature from C++ still not present in constexpr code is the ability to throw an exception. This absence forces library authors to use more intrusive error reporting mechanisms. One example would be the use of std::expected and std::optional. Another one is the complete omission of error handling. This leaves users with long and confusing errors generated by the compiler.

1 Like

Yes, but, either a constant expression panics when you build the program, or it never does.[1] Thus, the build is itself nearly all of the testing you need. Therefore, the potential for a constant expression to fail is much less of a problem than the potential for a run-time expression to fail. IMO, it makes sense to treat those failures as much less worthy of being explicit about them — they are not fundamentally different from any other sort of compilation failure, and a program can fail to compile due to any part of it.

Indeed, I listed this as the first drawback. I feel it is acceptable because it can be understood as: “I have done the amount of preparation appropriate for the particular situation (which is zero).”


  1. Except when constants depend on generics inside a library. ↩︎

5 Likes

The error message makes no sense. I can declare const FOO: Option<NonZero<u32>>, and returning None in its initializer is entirely valid. Why should it be treated as an error on an ad-hoc basis?

Also, the error arguably becomes worse. unwrap(), like any panic, produces effectively a backtrace, pointing to the specific panic in code. The proposal instead lumps all errors in a single error type (None in case of Option), making it impossible to distinguish different places and causes of errors. Since traits are const unstable, we can't even use Display & friends to make nice-looking error types, like anyhow does.

Again, expr or expr?, the error span is entirely wrong. We need to know the specific place where evaluation failed, and this proposal doesn't even in principle include a mechanism to define it.

Also, it seems your proposal is entirely equivalent to simple syntax sugar for const { try { expr }.expect("error msg") }, so why bother?

That's a major downside. You are proposing to split "const blocks" into "explicit" and "implicit" ones, and the users should keep track. Not impossible, but it certainly increases the inconsistency of the language and adds edge cases.


The proposal also doesn't compose with const fn. A const fn can return Option<T>/Result<T>, and thus the bahaviour of ? inside it should be kept consistent with the usual semantics. We also can't add ad-hoc semantics to ? inside const fn, because it may be evaluated at runtime, where the semantics of ? are already defined. This means that the RFC can only apply to top-level constant expressions, which severely restricts its usefulness and makes the inconsistency even worse (extracting a subexpression of const expression into a function significantly changes semantics). Nor would the RFC solve in any way the problem of panics in constants: const functions would still need to utilize panics as the only proper way to return a properly scoped error.

It is not ad-hoc. It is treated as an error because the ? operator was used. If ? is not used, nothing special happens — the semantics are exactly as today. You can always declare a constant Option<NonZero<u32>> if you want; the example code is doing something other than that.

I define exactly how an error should be reported; it is exactly as much information as an .unwrap() in the same syntactic position (without RUST_BACKTRACE set) produces. If desired, a backtrace could be obtained from the const evaluator too.

I understand that ?, when used in a function as is possible today, can hide the origin of errors. I agree this is bad, and that is why I specified behavior which does not do that; it does not propagate errors to a single site and then report them, but fails immediately with span information.

Again, expr or expr? , the error span is entirely wrong. We need to know the specific place where evaluation failed, and this proposal doesn't even in principle include a mechanism to define it.

I'm not sure what you mean by “the specific place where evaluation failed”, if not the span and possible backtrace. What information does unwrap() obtain that this does not? As far as I know, all the information an unwrap gives is:

  • the error value, if the type is Result and not Option
  • the span of the unwrap() call site (modulo #[track_caller])
  • a backtrace if requested

We can do exactly the same thing with this ?.

Are you perhaps comparing to calling a panicking function instead of a None-returning one? In that case, I agree that a panicking function can sometimes provide a more informative backtrace. But, as a matter of API design, I don't think we should prefer that all const-constructible types have panicking constructors. If we did that anyway (perhaps along with const-only functions), then I would agree that we don't have a use for this const-?. The purpose of const-? isn't to replace having functions panic; it is to concisely dispose of errors returned from error-returning-not-panicking functions.

The point of this proposal is to have a clean and concise syntax for fallible literals. If you don't mind writing that doubly-nested structure, then there is no value for you in this proposal — except for more precise error reporting. Using a try {}.unwrap() conflates all branching sites into one, so you do not know which one failed; this proposal does not do that.

The proposal also doesn't compose with const fn . A const fn can return Option<T>/Result<T> , and thus the bahaviour of ? inside it should be kept consistent with the usual semantics

I am not proposing to change the behavior of ? inside const fn (or rather, the behavior it will have when const traits are available). const fn is a kind of fn and gets the ? behavior that all fns have. Consistency there cannot reasonably be broken.

The reason that this proposal only applies to “top-level constant expressions” is precisely so that we're not interfering with the established meaning of ? in functions. It only adds a use for ? that is not in functions, and therefore free to be given a related but unequal meaning. Certainly that it isn't exactly the same meaning is a drawback, which I have already listed in the drawbacks section.

6 Likes

I would teach this as the same thing as what happens when you ? in main -- it's not that you think of it as "panicking", but as automatic top-level error handling that displays said error to the user. That user is just the person running the compiler, in this case.

6 Likes

Yes, there are rules. But as an end-user, this looks entirely ad-hoc. Rust doesn't have any precedent for operators behaving entirely differently based on context. I also don't think it would be desirable, given that it negatively affects the locality of reasoning. Yes, const initializers are usually small, but it still doesn't look like a solid design.

So how does const { try { bar ? } } behave? Do you propose to ban try blocks inside const blocks and constant items (but not const funcions)? Do you propose to have two entirely different behaviours of the same operator in the same syntactic area?

What about const { try { bat ? } ? }? Where is the error reported?

I don't think that's relevant in this discussion. The types have whatever constructors they do, with whatever panics they need to enforce their invariants. The design of types and functions is entirely unrelated to the question of ergonomic constants. So the existing system is here to stay in the vast majority of applications. How much of an ergonomic benefit are we talking exactly, in practice? I have a strong feeling that it's "near zero".

I'm also not going to change calls to assert!(..) in my constants to something returning a result. For once, there is simply nothing for me to substitute in its place, other than an explicit if-let, which isn't more ergonomic in any way. And even if I tried to do that, what would be the type of errors that I'm supposed to return?

I don't think that adding confusion about the semantics of basic operators and error handling in Rust is justified by a "clean and concise" syntax for panics. In fact, isn't this just a roundabout proposal to add a special operator for easy unwraps? Those proposals were already made multiple times and rejected. Why should const literals warrant a different treatment?

But you are proposing to break consistency between const fn and const { }! That's also a very significant change! Thus far Rust didn't have this kind of inconsistency: async fn is the same as a funciton returning async { }, try fn and gen fn are expected to correspond to try { } and gen { } blocks. Why should const fn and const { } be different?

It also seems to undermine work to enable effect-polymorphic code. I don't know whether that work will end up in something shippable, but you'd need a stronger justification than "it's clean and concise" to add extra blockers to it.


By the way, are you also proposing that return None in a const { } should result in a logged compiler error? And if not, you're adding yet another inconsistency between the normal behavioiur of ? and error returns.

I specified that already:

So, in const { try { bar ? } }, the ? exits the try block — just like in || { try { bar? } } or async { try { bar? } } or any other combination of two scopes that ? works in.

I get the impression you think I am advocating for writing more Result/Option-returning constructors or using more Results in constants instead of panicking; I do not intend to. The sole purpose of this syntax is to make it more convenient to use constructors which return an error instead of panicking — not to recommend writing such constructors. (Though, of course, given const-? existing, library authors may choose to write error-returning constructors more often than otherwise. But I hope they will choose in a way that, for the particular way that function is expected to be used, gives good error messages and promotes correctness, not just use ? because it is “the new way” or “better”.)

Because constant expressions are in a significantly different position: it is not hazardous for a syntax to quietly introduce a possible compilation error in the same way that it is hazardous to quietly introduce a possible run-time panic.

They are already different; the equivalence you propose with async and gen does not hold. A const block inside a fn is a constant evaluated before the function is compiled, regardless of whether that function is then later used during compilation; the const block cannot use variables from the function, unlike async and gen. In order to have an opportunity to have such an inconsistency, we would need to have const-only functions, and say that const {} gains extra capabilities inside a const-only function it doesn’t have inside any other kind of expression context. (Which, yes, could be reasonable; for example, it might be nice if we could write const-only fns that allocate memory by declaring constants and getting new &'static references from them.)

One could argue that const fn should have had a distinct keyword/adjective, because it's really more like const_eval_compatible fn than it is like any of the other stable uses of const.

I haven't followed the proposals for general effect polymorphism in detail (I am somewhat skeptical there will ever be a coherent, usable design for polymorphism over async) but as far as I am aware, none of them will influence const items or const blocks. The const effect will mean "can or can't be used at const-eval time” (for fns and traits), not “is or isn't a constant”. My proposal for ?-in-constants never changes what ? should mean in a const fn, and so if someday we have ~const fns too, then ? in them will mean the same thing it means in fns today and const fns when ?-in-const-fn stabilizes (which could in principle happen before ~const).

"Can be evaluated in const evaluation" is different in many ways from “is a constant”. My proposal is only about the latter, not the former, and is careful not to affect the former.

try is already inconsistent in this sense:

#![feature(try_blocks)]
fn foo() -> Option<i32> {
    let v: Option<i32> = try {
        return Some(1);
        2
    };
    v.map(|x| x * 100)
}
fn main() {
    println!("{:?}", foo());
}

This program prints 1, not 100, because the return is exiting the function, not the try block. return does not interact with the same set of nearest-enclosing constructs that ? does, but a smaller set.

5 Likes

Addressing just this point, and not the rest (which honestly tonally seems a little aggressive >_>)

*/*, -/-, </<T>, the bubble operator itself (?Sized). Not that these ambiguities are bad, necessarily, for the most part they make sense in context.

Wrt the RFC, :+1: no major notes.

6 Likes

These are different operators that just happen to use the same sigils. * for Mul is parsed differently than dereferencing *, since one is a binary operator and the other is unary. Likewise, < and ? are parsed differently in a type context than in an expression context, but that's not what this is about – they are syntactically distinct.

A better example is that expr? behaves differently in try blocks – especially if the homogeneous try blocks RFC is accepted. But even this feels less surprising than ? being the same as .unwrap() in a const context.

Another thing to consider: People will always have to understand that panicking in a const context causes a compilation error. This proposal adds another way to cause a compilation error, so it increases the language surface for very little gain.

2 Likes

This should just be a trait. The {integer} thing in the compiler already works similar to a trait, but only for built-in types, and with a default, and probably with slightly different rules than real traits.

1 Like