or
or
By clicking below, you agree to our terms of service.
New to HackMD? Sign up
Syntax | Example | Reference | |
---|---|---|---|
# Header | Header | 基本排版 | |
- Unordered List |
|
||
1. Ordered List |
|
||
- [ ] Todo List |
|
||
> Blockquote | Blockquote |
||
**Bold font** | Bold font | ||
*Italics font* | Italics font | ||
~~Strikethrough~~ | |||
19^th^ | 19th | ||
H~2~O | H2O | ||
++Inserted text++ | Inserted text | ||
==Marked text== | Marked text | ||
[link text](https:// "title") | Link | ||
 | Image | ||
`Code` | Code |
在筆記中貼入程式碼 | |
```javascript var i = 0; ``` |
|
||
:smile: | ![]() |
Emoji list | |
{%youtube youtube_id %} | Externals | ||
$L^aT_eX$ | LaTeX | ||
:::info This is a alert area. ::: |
This is a alert area. |
On a scale of 0-10, how likely is it that you would recommend HackMD to your friends, family or business associates?
Please give us some advice and help us improve HackMD.
Syncing
xxxxxxxxxx
The plan for
min_generic_const_args
Where does
min_const_generics
fall shortThe
min_const_generics
feature was stabilized at the end of 2020 with a few significant limitations. These limitations are explained nicely in WithoutBoats' blog post titled Shipping Const Generics in 2020 and is reccomended reading before continuing.The focus of this document is on allowing more complex expressions involving generic parameters to be used as const arguments, "No complex expressions based on generic types or consts" as titled in boats' blog post.
This document intends to explain:
Why does
generic_const_exprs
not workAllowing complex expressions involving generic parameters has been experimented with under the
generic_const_exprs
feature gate for the past few years. In short it allows writing code such as[u8; <T as Trait>::ASSOC]
orwhere Foo<{ N + 1 }>: Other<M>
; arbitrary expressions are supported as const arguments regardless of whether they involve generic parameters or not.Background implementation details
Currently const arguments (even under
min_const_generics
) are implemented by desugaring the const argument to a const item (called anonymous constants) and then referring to it.For example under
min_const_generics
:The implementation strategy for
generic_const_exprs
is that the implicitly createdANON
has the same generics and where clauses defined on it asfoo
. Additionally we also implicitly createterminates { ... }
where clauses for any const arguments used in the signature (more on this later).The previous example with the
generic_const_exprs
feature gate enabled would be desugared like so:With all that covered we can begin to talk about the issues with the current design of
generic_const_exprs
.Const arguments that fail to evaluate
In Rust not all expressions are guaranteed to terminate and return a value, they can loop forever, panic, encounter UB, etc. This introduces some complexity for const generics as we must decide on how to handle a const argument such as
Foo<{ usize::MAX + 1 }>
orFoo<{ loop {} }>
.While the previous examples are relatively easy to determine that they will panic and loop forever respectively, it is not so easy in the general case. This is quite literally the halting problem.
A conservative analysis is, in general, possible in order to guarantee termination of functions (a lot of functional programming languages implement this). Implementing this would require another function modifier, e.g.
const terminates fn foo() -> usize;
and generally complicate the language significantly.The (stabilized)
min_const_generics
feature handles this by requiring any complex expressions to not use generic parameters, this means it is always possible to try to evaluate the expression. If it fails to evaluate then we error, otherwise we have a value.As
generic_const_exprs
supports arbitrary expressions that do use generic parameters, such asFoo<{ N + 1 }>
, it is no longer always possible to evaluate a const argument as it may involve generic parameters that are only known after monomorphization.Given the example of
Foo<{ N + 1 }>
it would panic whenusize::MAX
is provided as the value ofN
. Some ways this could potentially be handled, one of which was already mentioned, would be:terminates
function modifier and have an addition function that is guaranteed to terminate if the type system can proveN < usize::MAX
N
that would happen to cause it to fail to evaluate{ N + 1 }
will terminateThe last option is what
generic_const_exprs
implements, to give a concrete example:Sidenote: In the actual implementation these bounds are not written as they are in the example. I opted to use
terminates { ... }
syntax as it is significantly more readable than what is actually implemented.These
terminates
bounds are a huge design issue with the current feature as they require putting large amounts of implementation details of the function in its public API. I would not expect us to want to stabilize anything with this solution.Unrecoverable CTFE errors
In general the trait system should be able to fail in arbitrary ways without causing compilation to fail. This requirement comes from a few places:
In these cases we can wind up evaluating generic constants with generic arguments that would cause evaluation failures. For example the following example:
In this example coherence attempts to check whether the two impls are overlapping or not. In order to do this we wind up inferring
N=0
and then trying to prove all of the where clauses on the generic impl.The generic impl has only one where clause:
terminates { N - 1 }
, with generic arguments applied this is actuallyterminates { 0 - 1 }
. Evaluating0 - 1
panics which emits a hard error by the const eval machinery.As hard errors are unrecoverable we wind up erroring on this code with the message that evaluating
0 - 1
failed even though in theory this ought to succeed compilation.While
terminates
bounds were used in this example as it is a very simple demonstration of the issue, they are not strictly required to demonstrate this and so removingterminates
bounds would not solve this.Changing const eval to not emit a hard error when failing to evaluate constants is undesirable as the current setup prevents a lot of potential issues that could arise from forgetting to emit an error in other cases.
Any design for generic const arguments needs to be able to avoid evaluating constants that might not succeed or else there will be weird behaviour and error messages in coherence and trait solving.
Checking equality of generic consts causes query cycles
The type system representation of constants must be able to represent the actual expressions written not just a usage of an anon const. This is so that two separate
{ N + 1 }
expressions are considered to be equal.For example the following code should compile even though
N + 1
is written in two different places and would result in two diferent anon consts:The way that
generic_const_exprs
handles this is by looking at the THIR (post-typeck hir) of anon consts and then lowering it to a new AST specifically for type level expressions.The requirement that we first type check the anon const before being able to represent its normalized form in the type system causes a lot of query cycle isues when anon consts are used in where clauses.
These cycles tend to look something like the following:
A concrete example of this kind of thing would be the following code:
In this example we might encounter a cycle like so:
{ <T as Trait<{ N + 1 }>>::ASSOC }
T: Trait<{ N + 1 }>
T: Trait<{ N + 1 }>
T: Trait<{ <T as Trait<{ N + 1 }>>::ASSOC }>
{ <T as Trait<{ N + 1 }>>::ASSOC }
is potentially normalizeable to{ N + 1 }
in which case this would be a valid way of provingT: Trait<{ N + 1 }>
{ N + 1 }
is equal to{ <T as Trait<{ N + 1 }>>::ASSOC }
requires type checking both anon consts. As we are already type checking{ N + 1 }
we have reached a cycle.Attempting to represent type system constants through a lowering step after type checking therefore seems like a dead end to me as this is code that we should support (equivalent code written with associated types compiles fine).
Unused generic parameters
The current desugaring can easily result in an anon const having more generics defined on it than are actually used by the body. Take the following example:
The type
[u64; ANON::<T, U, N>]
"uses" all of the generic parameters in scope which causes a number of issues:Self
typeEvaluating constants with where clauses that do not hold
Const eval should generally not be run on bodies that do not type check as doing so can easily cause ICEs. Attempting to change const eval to support running on bodies that do not type check would introduce a large amount of complexity to const eval machinery and is undesirable.
Unfortunately the current implementation of
generic_const_exprs
does not check that where clauses on anon consts hold before evaluating them. This results in const eval ICEing sometimes when using complex expressions with generic parameters in where clauses.In theory it would be simple to ensure that the where clauses hold before evaluating a constant. In practice this would drastically worsen the previous issue about query cycles to the point where evaluating any anon const in a where clause would cause a query cycle.
This is caused by the fact that anon consts are desugared to having all of the where clauses in scope (similar to how they have more generics than is strictly necessary). This means that an anon const inside of a where clause, would be desugared to having that same where clause. Example:
Attempting to evaluate
ANON::<concrete, concrete>
would first require provingterminates { ANON::<concrete, concrete> }
, which is proven by evaluatingANON::<concrete, concrete>
.Any design for supporting complex expressions involving generic parameters needs to be able to only evaluate constants when they are wellformed.
Can we fix
generic_const_exprs
?Having highlighted the core issues with the current implementation of the feature it's worth trying to figure out if we can just solve these and then stabilize it. To summarize the requirements above we would wind up with:
N + 1
whereN=usize::MAX
" and "how to avoid hitting unrecoverable CTFE errors" as these wind up being incredibly similar problemsTo start with we would have to stop using the current desugaring with creating const items- it's fundamentally incompatible with wanting to represent type system constants without requiring type checking a body first.
Instead we need to have a new kind of AST for representing expressions in the type system. There are a number of concerns here:
a.foo(N)
would add a significant amount of complexity to the type system which is undesirable, so we cant just say "all of them".Regardless, this would solve issues 1, 2, and 3. The new desugaring would result in only syntactically used generic parameters being present in the AST. Additionally the minimal required where clauses to hold for the const argument to be wellformed can be derived from the AST.
For the next issue (4) I can see a couple possible solutions:
generic_const_exprs
and only permit calls to functions that are statically known to terminateSimply accepting a buggy type system implementation seems out of the question to me. It is not a good look for the language to decide to stabilize something with bugs without any plan for how they could ever be resolved.
As an additional point, if we accepted the buggy behaviour we would still need to decide how to handle
N + 1
as an argument when a concrete value ofusize::MAX
was provided forN
. Post monomorphization errors are an "obvious" solution to this that would work from a technical standpoint.Introducing a termination checker is a significant undertaking that would take a long time to fully work out the design of- let alone have it in a stabilizable state. I imagine this would take many years and also significantly increase the complexity of the language.
In conclusion while there is potentially a path here to having everything work, there is a large number of open questions and issues with those potential solutions. Regardless of what is decided it would potentially take years to get something into a stabilizable state, and that assumes that this will actually work out.
While
generic_const_exprs
itself might be a rather long way off, it should be possible to carve out a subset of the feature that is possible to get working nicely in a much shorter timeframe, e.g. amin_generic_const_args
feature.What is
min_generic_const_args
On stable there are limitations to what a const generic parameter can have as its argument, concretely this is:
{ N }
{ foo() + 10 / 2 }
The
min_generic_const_args
will extend this to also support:<T as Trait<N>>::ASSOC
so long as the definition is marked#[type_system_constant]
.Similar to how uses of const parameters does not require explicit braces, the same is true of uses of associated constants.
Foo<<T as Trait>::ASSOC>
is legal without requiring it to be written asFOO<{ <T as Trait>::ASSOC }>
. (maybe, might get cut, but it seems nice)In order for an associated constant to be usable with generic parameters as an argument to a const generic parameter it must be marked with
#[type_system_constant]
on the trait definition. This attribute will require all trait implementations to specify a valid const generic argument as the value of the associated constant instead of an arbitrary unrestricted expression.Example:
If trait definition was rewritten to to use the
#[type_system_constant]
attribute then this would compile:This restriction is required to ensure that implementors of
Trait
are not able to write arbitrary expressions as the value of the associated constant, instead a valid const generic argument must be written. For example:Does this actually fix the original problems?
Reusing the list of issues from earlier:
The first problem is easy to solve with associated constants as they have an obvious representation, a
DefId
+ list of generic args (the same as we do for associated types). This also does not introduce any of the complexities with trying to represent arbitrary expressions:It is also trivial to tell what generic parameters a const argument uses as they are explicitly defined on the trait, the same as how associated types are handled in the compiler which has been shown to work adequately.
The requirement that all associated constants in the type system are defined as being equal to a valid const argument means that ensuring all constants in the type system terminate is as easy as it is under
min_const_generics
. The value for an associated constant can either be assumed to terminate or simply evaluated and then error if they do not.This also means that it is trivial to avoid evaluating constants that might not terminate as every const generic argument will have already been checked somewhere that it can be evaluated without issues, e.g. in the caller or in an associated const's definition.
Determining that const arguments are well formed before evaluating them is also trivial as we do not wind up with such recursive where clauses as all where clauses are explicitly written by the user on the
trait
or the associated const itself.Future extensions to
min_generic_const_args
size_of
/align_of
to be permitted too?where N is 1..
?