-
Notifications
You must be signed in to change notification settings - Fork 12.7k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
There's currently no way to specify bounds requiring constants in types to be well-formed #68436
Comments
Note that the syntax However, I'm worried that while this may be a pragmatic way to write "this must const-eval successfully" bounds, it would go against WF principles to use it as such. cc @rust-lang/wg-traits |
Suggesting new syntax, how about just adding this to the language? where (bool_expr)
// E.g.:
struct Arr<T, const N: usize>
where T: Sized,
(N <= (usize::max_value() / size_of::<T>())),
{
data: [T; N],
} Being mandatorily wrapped in brackets should be good enough to not need any special restrictions around the syntax for expressions evaluating to
As long as ill-formed types error out, that's pretty much Good Enough™. Sure, proper bounds to error out early are much nicer, though. Note that this does require the compiler to check whether or not the bounds make sense at all. I.e. this should not compile, even if you don't use the type: struct Arr<T, const N: usize>
where T: Sized,
(N == 0),
{
data: [T; {32 / N}], // D:
} Edit: To clarify, in case someone fears |
…rent, r=nikomatsakis typeck: always expose repeat count `AnonConst`s' parent in `generics_of`. This should reduce some of the confusion around rust-lang#43408, although, if you look at the changed test outputs (for the last commit), they all hit rust-lang#68436, so nothing new will start compiling. We can let counts of "repeat expressions" (`N` in `[x; N]`) always have the correct generics parenting, because they're always in a body, so nothing in the `where` clauses or `impl` trait/type of the parent can use it, and therefore no query cycles can occur. <hr/> Other potential candidates we might want to apply the same approach to, are: * ~~(easy) `enum` discriminants (see also rust-lang#70453)~~ opened rust-lang#70825 * (trickier) array *type* (not *expression*) lengths nested in: * bodies * types of (associated or not) `const`/`static` * RHS of `type` aliases and associated `type`s * `fn` signatures We should've done so from the start, the only reason we haven't is because I was squeamish about blacklisting some of the cases, but if we whitelist instead we should be fine. Also, lazy normalization is taking forever 😞. <hr/> There's also 5 other commits here: * "typeck: track any errors injected during writeback and taint tables appropriately." - fixes rust-lang#66706, as the next commit would otherwise trigger an ICE again * "typeck: workaround WF hole in `to_const`." - its purpose is to emulate most of rust-lang#70107's direct effect, at least in the case of repeat expressions, where the count always goes through `to_const` * this is the reason no new code can really compile, as the WF checks require rust-lang#68436 to bypass * however, this has more test changes than I hoped, so it should be reviewed separately, and maybe even landed separately (as rust-lang#70107 might take a while, as it's blocked on a few of my PRs) * "ty: erase lifetimes early in `ty::Const::eval`." - first attempt at fixing rust-lang#70773 * still useful, I believe the new approach is less likely to cause issues long-term * I could take this out or move it into another PR if desired or someone else could take over (cc @Skinny121) * "traits/query/normalize: add some `debug!` logging for the result." - debugging aid for rust-lang#70773 * "borrow_check/type_check: normalize `Aggregate` and `Call` operands." - actually fixes rust-lang#70773 r? @nikomatsakis cc @pnkfelix @varkor @yodaldevoid @oli-obk @estebank
…ent, r=nikomatsakis typeck: always expose explicit enum discriminant `AnonConst`s' parent in `generics_of`. This is similar to rust-lang#70452 but for explicit `enum` discriminant constant expressions. However, unlike rust-lang#70452, this PR should have no effect on stable code, as while it alleviates rust-lang#43408 errors, there is no way to actually compile an `enum` with generic parameters *and* explicit discriminants, without `#![feature(arbitrary_enum_discriminant)]`, as explicit discriminant expression don't count as uses of parameters (if they did, they would count as invariant uses). <hr/> There's also 2 other commits here, both related to rust-lang#70453: * "ty: use `delay_span_bug` in `ty::AdtDef::eval_explicit_discr`." - hides the ICEs demonstrated on rust-lang#70453, when there are other errors (which the next commit adds) * "typeck/wfcheck: require that explicit enum discriminants const-evaluate succesfully." - closes rust-lang#70453 by picking alternative "2", i.e. erroring when a discriminant doesn't fully const-evaluate from the perspective of the `enum` definition In the future, it might be possible to allow `enum` discriminants to actually depend on parameters, but that will likely require rust-lang#68436 + some way to restrict the values so no two variants can end up with overlapping discriminants. As this PR would close rust-lang#70453, it shouldn't be merged until a decision is reached there. r? @nikomatsakis
@Evrey what you're asking for is already possible: struct Arr<T, const N: usize>
where T: Sized,
Assert::<{N <= (usize::max_value() / std::mem::size_of::<T>())}>: IsTrue,
{
data: [T; N],
}
enum Assert<const COND: bool> {}
trait IsTrue {}
impl IsTrue for Assert<true> {} The problem lies elsewhere. It's that failure to evaluate the constant expression won't be linked to the fact that the user messed up the generic parameters: #![feature(const_generics)]
#![allow(incomplete_features)]
struct Arr<const N: usize>
where Assert::<{N + N}>: IsValid,
{
}
enum Assert<const CHECK: usize> {}
trait IsValid {}
impl<const CHECK: usize> IsValid for Assert<CHECK> {}
fn main() {
let x: Arr<{usize::max_value()}> = Arr {};
} What we want to be able to avoid is having to work around this by writing #![feature(const_generics)]
#![allow(incomplete_features)]
struct Arr<const N: usize>
where Assert::<{N < usize::max_value() / 2}>: IsTrue,
{
}
enum Assert<const CHECK: bool> {}
trait IsTrue {}
impl IsTrue for Assert<true> {}
fn main() {
let x: Arr<{usize::max_value()}> = Arr {};
} (which ICEs the compiler right now, I'm opening an issue for that) |
OFFSET as a const parameter is blocked on rust-lang/rust#68436
I wonder how: fn foo<const N: usize>() {
let _: [u8; { 2*N }] = ...;
} is fundamentally different from: const fn double(n: usize) -> usize { 2*n }
fn foo<const N: usize>() {
let _: [u8; { double(N) }] = ...;
} Moreover, today we already can write So arguably for consistency sake we should use the same behavior for inline expressions as well, maybe with some additional warnings asking to add bounds for trivial cases. In general case compiler would have to prove (or disprove) that provided bounds are sufficient to guarantee lack of overflow/underflow errors, which is far from trivial. |
@newpavlov they are not different (at least for const wf), we probably just missed one of these cases before #70107. const fn double(v: usize) -> usize {
v * 2
}
pub const FOO: usize = double(usize::MAX); Is not a monomorphization error, at least not in the sense which is relevant here. They may not get detected when running They also require global reasoning, which is kind of bad when writing complex software :) fn a<const N: usize>() {
let _: [u8; N + 10];
}
fn foo<const N: usize>() {
a::<{N * 2>()
}
fn main() {
foo::<{usize::MAX / 2}>()
} This causes a monomorphization error in |
@lcnr And let's not forget that underflow/overflow errors is just one class of errors, which could happen at "const runtime". What are fundamental reasons for special-casing them? |
Well, other people working on this might give you a more elaborate (correct) answer here, but we have to require this trait A {
fn foo();
}
impl<const N: usize> A for Const<N> {
fn foo() {
let _: [u8; N + 1];
}
}
fn test<const N: usize>() where Const<N>: A {
// ...
} I can think of some ways to keep this sound, but we have to prove that the const evaluation is successful |
Okay so probably this is overkill BUT. I had this idea the other day that maybe one could put trait-like restrictions/predicates on const type parameters, e.g: struct ModPNumber<p: const usize> where p: IsPrime {
...
} The predicates for this case would be of type If such a feature was to be implemented, the same predicate checker could be used at a lower level to check for well-formedness of the You couldn't write that restriction of well-formedness in the code being compiled, but I'm guessing that the same engine could be used internally to check for that property. Maybe I'm missing an obvious edge case, though. Take this with a grain of salt. :3 |
While |
I don't understand. Why would it be hard? Maybe we're talking about different things. I'm imagining this:
On the other hand, I just realized something. You cannot compute something before checking if it is well-formed. Right? If so, I think my approach would not work... unless you had other machinery that checked well-formedness before each operation, or something like that. And that would work but would also be very costly on degenerate cases (eg computing 1000 by doing 1 + 1 + 1 + 1 + 1 .... + 1). Hmm. |
#![feature(const_generics, const_evaluatable_checked)]
struct ArrayWrapper<T, const N: usize>([T; N]);
impl<T: Default, const N: usize> Default for ArrayWrapper<T, N> where [T; {N-1}]: {
fn default() -> Self {
Self([Default::default(); N])
}
}
impl<T> Default for ArrayWrapper<T, 0> {
fn default() -> Self {
Self([])
}
} Currently, this code fails to compile because |
Actually, it seems that this is the behavior when the default impl for |
@drmeepster this is a bug, and one of the most difficult ones we will have to face for this feature. fixing that one will be a quite invasive change so we probably don't even want to fix it until we fixed most of the remaining issues here. |
despite this, this feature is still advanced enough to reimplement transmute in pure rust: #![feature(const_panic, const_generics, const_evaluatable_checked, untagged_unions)]
use std::mem::{ManuallyDrop, self};
struct ConstAssert<const Assert: ()>;
union TransmuteImpl<T, U>{
t: ManuallyDrop<T>,
u: ManuallyDrop<U>
}
const fn const_assert<T: Sized, U: Sized>(){
assert!(mem::size_of::<T>() == mem::size_of::<U>(), "T and U must have same size");
assert!(mem::align_of::<T>() == mem::align_of::<U>(), "T and U must have same alignment");
}
pub unsafe fn transmute<T: Sized, U: Sized>(t: T) -> U
where ConstAssert<{const_assert::<T, U>()}>: {
ManuallyDrop::into_inner(TransmuteImpl{t: ManuallyDrop::new(t)}.u)
}
fn main(){
unsafe{
println!("{:?}", transmute::<u8, i8>(255))
}
} |
Nice! Although the const eval diagnostic is quite uninformative. Maybe we should report generic parameters in const eval errors, then at least we'd get enough details to track down potential call sites. |
I would like to bring attention to the following application case. I am working on generic implementation of Weierstrass curve arithmetic and my code is littered with bounds like this
These bounds straightforwardly translate to the "explicit dumb bounds" (proposal 2). Using type aliases I made it a bit nicer, but overall I have to explicitly specify that for a type containing constant BTW I think that the linked code is a good case for measuring advancement of const generics and const eval. Most constants in the |
For an example where a reimplemented transmute helped, @Nemo157 and I used the feature for a proof of concept to restrict trait implementers to a word size in #74304 (comment) ( playground ). Since |
It would definitely help to have a better idea of where the failed In the meantime, the following method yields more informative messages #![feature(const_generics, const_evaluatable_checked)]
const fn eq_size<T, U>() -> bool {
use core::mem::size_of;
size_of::<T>() == size_of::<U>()
}
struct Assert<const EXPR: bool>;
trait True {}
impl True for Assert<true> {}
trait WordSized: Sized
where
Assert<{ eq_size::<Self, *const ()>() }>: True, {}
impl WordSized for u32 {}
impl WordSized for u64 {}
Trying something similar on @drmeepster's example -- while not a great message at least it shows the call site. unsafe fn transmute<T: Sized, U: Sized>(t: T) -> U
where
Assert<{ eq_size::<T, U>() }>: True,
|
Combing my method and @memoryruins's method leads to a more informative error, showing the call site but also showing an informative error message. #![feature(const_panic, const_generics, const_evaluatable_checked, untagged_unions)]
use std::mem::{ManuallyDrop, self};
pub struct ConstAssert<const ASSERT: ()>;
union TransmuteImpl<T, U>{
t: ManuallyDrop<T>,
u: ManuallyDrop<U>
}
pub struct ConstCheck<const CHECK: bool>;
pub trait True{}
impl True for ConstCheck<true>{}
pub const fn transmute_check<T: Sized, U: Sized>() -> bool {
mem::size_of::<T>() == mem::size_of::<U>() && mem::align_of::<T>() == mem::align_of::<U>()
}
pub const fn transmute_assert<T: Sized, U: Sized>(){
assert!(mem::size_of::<T>() == mem::size_of::<U>(), "T and U must have same size");
assert!(mem::align_of::<T>() == mem::align_of::<U>(), "T and U must have same alignment");
}
pub unsafe fn transmute<T: Sized, U: Sized>(t: T) -> U
where ConstCheck<{transmute_check::<T,U>()}>: True, ConstAssert<{transmute_assert::<T, U>()}>: {
ManuallyDrop::into_inner(TransmuteImpl{t: ManuallyDrop::new(t)}.u)
}
fn main(){
let i: u32 = unsafe{transmute([0u8; 4])};
} errors with:
While still being quite ugly, these errors tell both the call site and the panic message. |
I do consider
The last three share
A further reasonable extension could be to ensure that we don't need to specify bounds that cannot possibly fail. So
are fine for any integer, as division by a positive nonzero integer can't ever be problematic Another reasonable (albeit more complex) extension is that we could make
But any such comfort improvements should imo be a separate feature gate that will get stabilized after plain wf bounds, as the ability to do something at all is more important than the ability to do something conveniently |
@oli-obk |
We can always be more permissive in the future, but I think we should treat const generics like type generics, so i do expect that you will have to write almost all the same bounds as those that you write with the I do think that for the initial implementation we should focus on shipping a feature in a way that we already know works in practice instead of going for the full shebang in one go. Like with all complex features, if we go with the full thing, we'll just end up splitting it into a |
@oli-obk wrote:
Can you explain why it works? You said nothing have to be proved but what is the case for overloading and specialization? The other way around, can we always assume that the relation |
We compare the MIR (or to be precise, a datastructure that is created by processing MIR) of the array length and check if an equal constant exists in the where bounds.
well, equality has to be proven, but that's similar to how you have to prove two variables have the same type in order to be able to assign one variable to the other. I'm not sure what you mean with overloading and specialization. Rust doesn't actually have overloading. Specialization of functions with such bounds I have not thought about yet, but we can also punt on that question until it becomes a problem (and start out by requiring all bounds on all specializations to be equal and then think about relaxing that).
I'm afraid I have no idea what you are talking about. Can you break it down into examples? I also don't know what
Is this a question? I don't know how D does it or how trait dispatch relates to this issue
Sure, such bounds are extensions we can think about in the future, but I still believe starting out with the minimal feature (that mirrors what typenum does) is the best way forward. We don't have to worry about any higher effects on the type system, since for everything we're implementing there already exists a type level equivalent.
We cannot assume anything about the cc @lcnr I think we should create a tracking issue that just tracks |
|
yea I meant that. Thanks. I didn't realize I was entering For extensions beyond exact matching constants (proposal 2), please open issues on the RFC repository or even better, discussions in the internals forums. |
@oli-obk wrote:
Okay, I may formulated my question a bit too imprecise. Why you can leave out all commented constraints in proposal 2 over proposal 1 or 3, I'm a bit out of look here. For those who are interested, I've formulated my concerns above in Future Direction: Const Generics and Dispatching |
You can leave out all the bounds, because you are enforcing that the constant mentioned must compile successfully, and the caller of that function must prove that either by bubbling the constraint to its callers or by inserting enough concrete values and types to make the expression evaluable. |
But that's not the answer to my question because how does the caller know which constraint have to be bubbled up (uppropagated?) when you left out some necessary constraints in your example. |
from The other bounds are
The constraint is that |
@oli-obk, thanks for clarification. |
I am still not able to use associated consts in, for example, array sizes. Are there any plans to implement this? (I have enabled |
When we have a const expression with a generic parameter in it, we cannot be sure that the const expression is valid for all the values that the generic parameter might take.
If we do not force the caller of the function to specify that the expression must be well-formed, we may get post-monomorphisation errors, as we can only determine that the expression is well-formed after substituting the concrete values for the generic parameters. We therefore need some way to specify such a bound. This is currently not possible.
This was discussed in #66962 (comment), #66962 (comment), #68388 (comment).
@eddyb suggested the syntax
where [u8; { /* some expression involving N */ }]:
, though it is not clear whether this would be appropriate. This needs some discussion and careful consideration of the WF implications.The text was updated successfully, but these errors were encountered: