Pre-RFC: Revamped const_trait_impl aka RFC 2632

@U_007D

It turns out that it was reverted due to breakage:

:+1:

For the time being, the blanket impl won't propagate the constness of the From impl, so I'll just do an impl const Into for now. Thanks.

Syntax Change: We use ~const instead of ?const to signify the "const-if-const"ness. Please comment if you can come up with something better than ~const , this is definitely up for bikeshed.

Is ~const intended as placeholder syntax, to revert to either just const (which already serves to define both the const and non-const impls today) or ?const (as "might be const" to be more explicit about the const-if-const aspect) before feature stabilization? (Unless I missed it, the Pre-RFC text doesn't appear to speak to this.)

Either might be reasonable interpretations of "const-if-const" (at least to my user's eye view) without adding a new sigil to the language, and I was wondering about the Pre-RFC authors' thoughts on this.

1 Like

I have wondered the same thing. I'm not thrilled by the idea of introducing yet another sigil that may confuse people new to Rust and that is hard to search for. ?const (which looks similar to ?Sized) would be more intuitive I believe.

2 Likes

Perhaps adding ~const isn't that big of a problem.

To me ?Sized keeps being confusing; possibly because it's negative, taking away an assumption.

But ~const is positive, adding (conditionally) an assumption. It may end up being easy to grasp.

But isn't its meaning different than ?Sized? Looking at this without prior knowledge of the problem I would expect const to be the default just like Sized is the default and ?const being the opt-out, which is definitely not the case.

1 Like

I read ?Sized as "maybe sized". Likewise, ?const means "maybe const".

I understand that const being opt-in while Sized is (usually) opt-out is an inconsistency. But I don't see it as a problem for understanding what ?const means, because the inconsistency only relates to the default behavior when the trait bound/modifier is absent.

6 Likes

Another difference between ?Sized and ~const is that ?Sized means you explicitly allow DST to be used, so it is more like "I don't care about the sized-ness of this type".

..while ~const is not "I don't care about the constness of the trait implementation" but rather "constness depends on whether you want to use this in const contexts".

When you add a prefix to something, I'd expect it to do the same thing to the thing that it applies to, regardless of what the thing actually is.

Adding ?Sized relaxes a bound, whereas ~const makes a bound stricter. Adding the former is not a breaking change, but adding the latter is.

If so, why not use const? Without any sigils.

  • const would mean the generic parameter always has to be const
  • ~const is suggested to mean a const bound that gets "activated"
    only when the fn is used in a const context

What is your view? Is this easy to learn? Difficult? Easier that ?Sized? Intuitive?

~const means that generic type maybe const, maybe not? If yes - ?const looks more intuitive for me. Reads like maybe const.

The hope behind suggesting ~ was to emphasize the fundamental difference

  • ?Sized - "do not dare to assume the type is Sized, shish!"
  • ~const - "do assume it's const if you're called in a const context"

How would you assess intuitiveness and learnability here?..

1 Like

There are plenty of ways of reading ?Sized in ways that sync with the meaning of ?const. I read it as "possibly Unsized" which is even less sensical. Viewing prefix ? as an operator currently its meaning is clear:

All type parameters have an implicit bound of Sized . The special syntax ?Sized can be used to remove this bound if it’s not appropriate. [T + ?Sized actually means T + Sized + ?Sized or just plain T]

So to use ? here it should be the case that all type parameters are implicitly T + const. Thus T + ?const actually means T + const + ?const i.e. T. But that's not true or intended to be true.

I don't know if this definition as an operator is useful going forward, it's currently unique syntax. Hopefully Rust doesn't accrue too many new implicit default bounds.

I can certainly imagine changing this syntax to just mean "maybe whatever", in which case ?Sized and ?const and ?async all make sense, with unique meanings for what new constraints those imply.

1 Like

~ and ? here definitely has subtle difference. ?Sized means maybe Sized / Option<Sized>, but ~const means conditional const -- "const if in const context".

1 Like

From personal experience, it took me quite a while to understand the reasoning behind ~const, and even now I'm not 100% sure I fully do. From a newcomer's perspective, I think the first questions would be "what exactly is a 'const context'?", and "if something can be const, why not make it const always?"

But aside from this, my main issue with ~const is that I see it ending up everywhere: if you can mark something as ~const, why wouldn't you? Together with these observations:

... this could end up in a blog post "Rust: ~const ALL the things!" :wink:

5 Likes

Well, that's only true if the function / method in question was already a const fn. Adding ~const in the process of turning something into a const fn can't cause breakage, because the only possible existing calls are guaranteed to be runtime ones on which the ~const bounds won't have any impact.

The accumulation of all these subtle distinctions adds a lot of redundant complexity to Rust and points to a core design problem with the current model imo.

Const shouldn't affect the semantics of a function foo at all. Its behaviour ought to be the same. By using const the programmer actually wanted to change when it was executed or i.o.w how it was called. Hence, a much simpler and more orthogonal design ought to aim to mark call sites, not function definitions.

There are a few challenges for such an approach for the compiler writers, but non that can't be overcome imo.

  • The restrictions on what can be done at compile time should be removed. It's just regular rust code simply executed at an earlier stage. Doing I/O and calling external creates are really a question of what is the API exposed by the interpreter, not a fundamental semantic difference.
  • Security (and build reproducibility) is similarly a question of isolating the execution of the interpreter and not a fundamental semantic difference either.

Warm + wasi are an awesome pre-existing set of technologies with native rust implementations that can be used to address the second point in particular.

1 Like

Sorry for being slow.. aren't the call sites already marked?
It's pretty clear when an fn is called in a const context?..

That's right. Const call sites are already marked.

I was making the complementary point that marking function definitions as const should be removed.

The current model tries to paper over the issues of cross compilation but by doing so it essentially forces conditional compilation style issues on everyone. The most fundamental tool of abstraction in Rust is the trait system. By restricting functions and traits/impls to be some checked subset of Rust everything beyond a simple function needs to reason about this subset.

This is a clear inversion of the Pareto principle (80/20 rule). I rather if I need to be extra careful with my reasoning in the few instances when I use platform dependent code in a cross compilation scenario where executing say size_of<T> can change the result, than to need to reason about ~const bounds everytime I need a generic function to call a trait method.

In other words, this is another instance of an XY problem.

We need determinism for a sound type system. Otherwise [u8; foo()] be assignable [1, 2, 3] according to the typechecker as foo() evaluates to 3, but then during codegen it may evaluate to say 100, thus exposing uninitialized memory.

Another thing is that we need to know what memory we need to include into the executable and where there are relocations. This is impossible to get unless the code is instrumented to give this information. This instrumentation is pretty much exactly as restrictive as the const eval engine as OS libraries can't be instrumented and this instrumentation can't support pointer to integer casts.

Build reproducibility wouldn't be possible when allowing calls into external libraries without disabling ASLR, clearing the AT_RANDOM auxiliary value (which is random) and a whole lot of other things. Not all platforms support disabling ASLR in the first place for security reasons.

2 Likes