# Complexities of Type System Values
## Intro
Const generics currently only supports integers, bool, and char, as types of [Const Generic Parameters][term_cgp]. These types have incredibly simple representations and no difficult questions about how equality of their values should work in the type system. However, to support other more complex types, these questions do show up.
This document intends to outline most of the design constraints and complexities of supporting more types as the type of Const Generic Parameters.
There are two kinds of values, [Regular Values][term_rv_full] ([RV][term_rv]s) and [Type-System Values][term_tsv_full] ([TSV][term_tsv]s).
- Regular Value: The kind of value that you would encounter at runtime (or during const eval) that expressions evaluate to.
- Type-System Value: The way that values in the type-system are represented, for example the final value of a [Const Generic Argument][term_cga].
In a function call, we can see both both of these values appear. We see RVs for normal arguments to the function, while we see TSVs for Const Generic Arguments:
```rust
fn foo<const N: usize>(_a: usize) -> usize {
N
}
foo::<10>(20);
```
Here the `10` is a TSV, whereas the `20` is a RV.
When an expression is used as a Const Generic Argument, the expression is evaluated to a RV then implicitly converted to a TSVs. The opposite is true when a Const Generic Parameter is used as an expression, the TSV is converted to a RV:
```rust
fn foo<const N: usize>(_a: usize) -> usize {
// This `N` expressions converts a TSV of a `usize` to a RV of a usize
N
}
// - `10` is evaluted to a RV
// - The RV of `10` undergoes a RV->TSV conversion
// - Inside of `foo` the TSV of `10` undergoes a TSV->RV conversion
foo::<10>(20);
```
The complexities of supporting more types in const generics specifically stem from the following questions:
1. What are the semantics of the conversion operations?
2. What is the behaviour of equality of of TSVs?
3. How to handle undesirable (for const generics) RVs?
In this document we'll look at ways to answer these questions by covering:
- core soundness requirements of TSVs;
- secondary desirable properties (ideals);
- which RVs would be undesirable as TSVs;
- strategies for handling undesirable RVs;
- how each strategy for handling undesirable RVs interacts with each undesirable RV and our ideals.
## Core Type System Requirements
The design for equality of TSVs is quite constrained. A number of properties simply have to be maintained for the type system to work. This section goes over these requirements.
### Observable Equality
There are a few properties that must hold for equality of TSVs in order for the type system to be coherent and sound:
- Reflexive: All TSVs must be equal to themselves
- Transitive: If some TSVs `TSV-1` is equal to `TSV-2` which is equal to `TSV-3` then `TSV-1` must be equal to `TSV-3`
- Symmetric: If some TSVs `TSV-1` is equal to `TSV-2` then `TSV-2` must be equal to `TSV-1`
- Observability: If two TSVs can be observed to be different in any way then the TSVs must be unequal
## Ideals to Strive for
There are a number of properties that it may be nice for Rust to have that are difficult to uphold when supporting more types in const generics. It is not a soundness requirement that these properties hold, only nice if they do. This section goes over these "Ideals".
### One Equality
It would be nice if equality of TSVs was the same as equality of RVs.
Specifically, whether or not two TSVs are equal should be the same as whether they are equal after undergoing a TSV->RV conversion. Similarly, whether or not two RVs are equal should be the same as whether they are equal after undergoing a RV->TSV conversion.
This would mean that there aren't "two" meanings of equality: TSV equality and RV equality.
### Lossless Roundtripping
Conversion from a RV to a TSV (and vice versa) would ideally not be lossy. If roundtripping a value through a const generic changed the value we could easily break user expectations. In the worst case people may write buggy (unsound) unsafe code that incorrectly relies on a Const Generic Parameter having a specific value.
### Intuitive Semver
It should be possible to automatically check any Semver violations and it should also be easy to understand when Semver has been broken.
#### Errors on Specific Const Values
Some parts of the language restrict the set of allowed values of a type beyond what the type itself requires:
- `const` items restrict the set of valid values allowed as the final value of the constant
- e.g. pointers to free'd allocations are forbidden
- const patterns restrict the set of valid values allowed as a pattern
- e.g. values containing a `NaN` are forbidden
This means that every `const fn` has an implicit API contract as to whether the returned value is useable in const items or const patterns.
Changing a `const fn` to return a value that can no longer be used in a const item or a const pattern could break downstream crates ([playground](https://play.rust-lang.org/?version=stable&mode=debug&edition=2024&gist=76765a62e9e5ac4ebcbe8aa6033bf2d7)):
```rust
// crate a
pub const fn const_item_breakage() -> *const i32 {
&1 as *const i32
// Change to this to break downstream
// let x = 1;
// &raw const x
}
pub const fn pattern_breakage() -> f32 {
// Change this to `f32::NAN` to break downstream
0.0
}
// crate b
const CONST_ITEM: *const i32 = a::foo();
const FOR_PATTERN: f32 = a::foo();
fn bar(arg: f32) {
match arg {
FOR_PATTERN => (),
_ => panic!("I don't like your float"),
};
}
```
In the general case this is uncheckable by a tool such as `cargo-semver-checks` as it requires reasoning about all possible returned values from all `const fn`s. This means that this ideal *already* does not hold on stable.
#### Compile Time Assertions
Results of const eval can be depended on in a number of ways that result in compile time breakage when upstream crates change the return values of `const fn`s.
Changing the return value of a `const fn` can completely change what type is written in a downstream crate. For example `[u8; upstream::foo()]` could be `[u8; 2]` in one version and `[u8; 3]` in the next ([playground](https://play.rust-lang.org/?version=stable&mode=debug&edition=2024&gist=c858f1b731162d7a7f9f79aca5889e9f)):
```rust
// crate a
pub const fn foo() -> usize {
// change this to `2` to break downstream
1
}
// crate b
fn bar(arg: [u8; a::foo()]) -> [u8; 1] {
arg
}
```
Similarly, `const fn` from upstream crates can be used to compute the value of a const item used as a const pattern. If the upstream crate then changed what value was returned, then the match may not longer be exhausted resulting an error being emitted ([playground](https://play.rust-lang.org/?version=stable&mode=debug&edition=2024&gist=3d861e59aa1062599521ea9a732289c6)):
```rust
// crate a
pub const fn foo() -> bool {
// Change this to `false` to break downsteam
true
}
// crate b
const TRUE: bool = a::foo();
pub fn flip_bool(arg: bool) -> bool {
match arg {
TRUE => false,
false => true,
}
}
```
`const fn`s can be used to compute enum variant discriminants which is itself another source of breakage from changing the return value of a `const fn`. `upstream::foo()` may have previously been a valid enum discriminant but now may overlap with another variant's discriminant ([playground](https://play.rust-lang.org/?version=stable&mode=debug&edition=2024&gist=17822ee12ebdf8552204b5ff4efd21e3)):
```rust
// crate a
pub const fn foo() -> u8 {
// Change this to `0` to break downstream
1
}
// crate b
#[repr(u8)]
enum Foo {
A = 0,
B = a::foo(),
}
```
In the general case this is uncheckable by a tool such as `cargo-semver-checks` as it requires reasoning about all possible returned values from all `const fn`s. This means that this ideal *already* does not hold on stable.
### Good Pattern Types
These two points holding have a few nice implications for pattern types.
#### We shouldn't support multiple TSVs that would lower to the same Pattern
If we supported this then we would not be able to have lossless TSV<->Pattern conversions. Having lossless TSV<->Pattern covnersions is a necessity for supporting Const Generic Parameters in pattern types:
```rust
impl<const N: u32> Trait for u32 is N {
fn get_n() -> u32 { N }
}
fn main() {
<(u32 is 2) as Trait>::get_n();
}
```
Supporting this code requires that after the `2` in `u32 is 2` undergoes a TSV->Pat conversion, we can undergo a Pat->TSV conversion to get `2` back out of the `u32 is 2` type's pattern to use as an argument to the `N` generic parameter.
#### We should be forwards compatible with const generics supporting all values useable in const patterns
There are a few types for which pattern matching is only useful when combined with const patterns:
- Integers
- `bool`
- `char`
- Floats only support const patterns of non-NaNs
- `str`
- Raw Pointers only support const patterns of Raw Pointers without provenance
If we do not support these in const generics then these types would be unuseable in pattern types. The only types here not already supported by const generics are Floats, `str` and Raw Pointers.
For other types it would simply be a useability annoyance for const generics to not support all of values. Without const generics support for all values supported by const patterns, it would not be possible to write a `MyType is N` pattern type corresponding to all patterns writeable by the user.
### Const Patterns Parity
We currently do not support Const Generic Parameters as const patterns:
```rust
fn foo<const N: usize>(n: usize) {
match n {
N => (),
//~^ ERROR: constant parameters cannot be referenced in patterns
_ => (),
}
}
```
If we were to start supporting this one day it could be problematic if const generics supported more values than const patterns do.
For example, `NaN` values are forbidden in pattern matching but if we were to support them in const generics this could result in post mono errors:
```rust
fn foo<const N: f32>(f: f32) {
match f {
// This would be a post-mono error if `N` was `f32::NAN`
N => (),
_ => (),
}
}
```
Supporting more values in const generics than const patterns would mean that Const Generic Parameters cannot be used as patterns without post mono errors.
If const generics were to support more types than const patterns then there would be similar forwards compatibility concerns:
```rust
fn foo<T, const N: T>(v: T) {
match t {
N => (),
_ => (),
}
}
```
Const patterns do not support function pointers, if const generics were to support function pointers then it would be possible to call `foo` with `fn()` as an argument to the type parameter `T`, resulting in a const pattern of a function pointer which is not supported.
This necessitates either post mono errors or a trait bound to require a type be "useable in const patterns". This constrains the design space of a feature allowing uses of Const Generic Parameters in patterns, which is a forwards compatibiltiy hazard.
### No Post-mono Errors
Supporting more kinds of types in const generics would ideally not force us to have more post mono errors in the language than otherwise necessary.
## Undesirable Regular Values
There are a number of RVs where either supporting them as TSVs is difficult, or supporting them as TSVs would compromise on our ideals. This section intends to outline all RVs that are potentially undesirable and why.
### Provenance
Rust currently does not have a specified aliasing model. This makes reasoning about equality of Provenances impossible as we do not know what a Provenance is. Attempting to do so anyway would be a forwards compatibility hazard as it could restrict how the aliasing model could work.
For this reason we will consider it to be impossible to support Provenance in TSVs.
### Abstract Bytes
Abstract Bytes are effectively arbitrary data with no type. For const generics' purposes we can only really encounter untyped bytes from Unions and pointees of Raw Pointers/References.
Rust currently does not have a specified memory model. This makes reasoning about Abstract Bytes difficult/impossible to do. Attempting to do so anyway would be a forwards compatibility hazard as it could restrict how the memory model could work.
For this reason we will consider it to be impossible to support Abstract Bytes in TSVs.
### Raw Pointers
#### Requirement: [Oberservable Equality][req_equality]
Observably different things about RVs of raw pointers:
- Raw Pointers without Provenance:
- The address being pointed to
- Raw Pointers with Provenance:
- The Provenance
- this is handled in the [Provenance][ty_prov] heading
- The data this pointer has the Provenance to read
- This is a list of [Abstract Bytes][ty_byte] and is handled in that heading
- The offset from the pointed-to allocation
- Transmuted `fn` pointer:
- This is equivalent to simply using `fn` in const generics so will be handled in the [FnPtr][ty_fnptr] heading
- Wide pointer metadata
- Slice metadata is trivial. `usize` is already supported
- `str` metadata is trivial. `usize` is already supported
- `dyn` type metadata is effectively a list of `fn` pointers so will be handled in the [FnPtr][ty_fnptr] heading
Neither Abstract Bytes or Provenance can be supported in TSVs. This means that Raw Pointers with Provenance must be handled somehow.
This just leaves (optionally wide) Raw Pointers without Provenance which we can trivially support as it is effectively equivalent to a `usize`.
#### Ideal: [One Equality][ideal_equality]
Runtime equality of raw pointers is based off of addresses. In order to uphold this ideal we would need to support Raw Pointers without Provenance in TSVs and no more. This means not supporting `fn` Raw Pointers.
#### Ideal: [Lossless Roundtripping][ideal_roundtrip]
N/A
#### Ideal: [Intuitive Semver][ideal_semver]
N/A
#### Ideal: [Good Pattern Types][ideal_pat]
Currently only Raw Pointers without Provenance are supported in const patterns. In order to uphold this ideal we would need to support Raw Pointers without Provenance in TSVs.
If const patterns were to support Raw Pointers with Provenance we would be unable to uphold this ideal. We currently cannot support Raw Pointers with Provenance as TSVs. Additionally, even if we could we'd have to map TSVs of raw pointers of the same address with different provenance to the same pattern.
#### Ideal: [Const Patterns Parity][ideal_const_pat]
Values of raw pointers used as a const pattern must be:
- A Raw Pointer without Provenance
Behaviour of the equality is address based.
In order to uphold this ideal we would need to only support Raw Pointers without Provenance in TSVs.
#### Ideal: [No Post-mono Errors][ideal_pmes]
N/A
### References
#### Requirement: [Oberservable Equality][req_equality]
Largely the same as Raw Pointers with Provenance from the [Raw Pointers Observable Equality][raw_ptr_req_equality] heading.
It may seem that unlike Raw Pointers, References shouldn't contain [Provenance][ty_prov]. Unfortunately this is not the case.
As Rust does not have a specified aliasing model it is not clear what Provenance a Reference is allowed to have. For this reason we must assume it could be basically anything and so still have to handle Provenance somehow.
As a concrete example, it's possible that References could be allowed to have "wider" provenance than just the value of the pointee type. E.g. an `&u8` that can be used for reads of `[u8; 2]` because it's a reference to the first element of an array of length >=2.
It also may seem that unlike Raw Pointers, References shouldn't contain [Abstract Bytes]. Unfortunately this also is not the case for two reasons:
- References to ADTs may contain padding bytes which are treated as Abstract Bytes
- References with "wider" provenance allow reading from bytes with no type associated with them
TSVs of References must handle Provenance and Abstract Bytes somehow.
[raw_ptr_req_equality]: #Requirement-Oberservable-Equality
#### Ideal: [One Equality][ideal_equality]
Equality of TSVs of references must take into account *all* accessible bytes. This is different from RVs where we only care about the pointee type and its notion of equality.
#### Ideal: [Lossless Roundtripping][ideal_roundtrip]
N/A
#### Ideal: [Intuitive Semver][ideal_semver]
N/A
#### Ideal: [Good Pattern Types][ideal_pat]
Const patterns support References with initialised padding, references to sub-parts of allocations, and references to statics. To uphold this ideal we would need to be forwards compatible with RV-TSV conversions supporting these kinds of values of References. We would also need to do so without padding, the rest of the allocation, or static identity, mattering for TSV equality as otherwise we would have to map multiple TSVs to the same Pattern.
#### Ideal: [Const Patterns Parity][ideal_const_pat]
Values of references used as a const pattern must be:
- A readonly reference to a (recursively) valid value of the pointee type
Behaviour of the equality is equivalent to `PartialEq`:
- Padding is ignored
- Not address sensitive
- Data valid to read that isn't part of the pointee type is ignored
As Const Patterns support all References it would be impossible to not uphold this ideal.
#### Ideal: [No Post-mono Errors][ideal_pmes]
N/A
### Floats
#### Requirement: [Oberservable Equality][req_equality]
All bits of floats are observable during const eval. Type system equality there *must* be equivalent to bitwise equality.
#### Ideal: [One Equality][ideal_equality]
Runtime equality of floats differs from the behaviour of type-system-equality in two main ways:
1. NaN values are never considered equal to themselves or other NaNs
2. Positive and Negative Zero are considered equal to eachother
#### Ideal: [Lossless Roundtripping][ideal_roundtrip]
N/A
#### Ideal: [Intuitive Semver][ideal_semver]
NA
#### Ideal: [Good Pattern Types][ideal_pat]
In order to uphold this ideal RV->TSV conversions would need to support all float values other than NaNs as they are all able to be used in patterns.
#### Ideal: [Const Patterns Parity][ideal_const_pat]
Values of floats used as a const pattern must be:
- Not a NaN value
Behaviour of equality:
- Positive 0 and Negative 0 are considered equal: [playground](https://play.rust-lang.org/?version=stable&mode=debug&edition=2024&gist=abb18be75d5876d8c5c67588f6cb43a3)
In order to uphold this ideal either TSVs would need to not support `NaN` values, or const patterns would need to be extended to support `NaN` patterns.
#### Ideal: [No Post-mono Errors][ideal_pmes]
N/A
### Function Pointers
#### Requirement: [Oberservable Equality][req_equality]
Nothing about function pointers is currently observable at compile time. `==` on function pointers results in a hard error at compile time (see: [Tracking issue for comparing raw pointers in constants](https://github.com/rust-lang/rust/issues/53020)).
However we must assume that at runtime different functions or same functions with different (up to lifetimes) generic arguments, are observably different to call. Equality of function pointers in the type system would have to be effectively equivalent to lifetime-agnostic type equality of the function item types of the pointed-to functions.
#### Ideal: [One Equality][ideal_equality]
Codegen can arbitrarily result in different function pointers for the same underlying function. It can also result in function pointers for different functions being equal due to functions being merged as an optimization.
Type system equality cannot emulate this behaviour at all, forcing us to accept runtime equality of function points arbitrarily differing from type system equality of function pointers.
#### Ideal: [Lossless Roundtripping][ideal_roundtrip]
N/A
#### Ideal: [Intuitive Semver][ideal_semver]
N/A
#### Ideal: [Good Pattern Types][ideal_pat]
N/A
#### Ideal: [Const Patterns Parity][ideal_const_pat]
Function pointers are not supported in const patterns.
In order to uphold this ideal TSVs must not support function pointers, or we would have to support function pointers in const patterns (though doing so would compromise the One Equality ideal).
#### Ideal: [No Post-mono Errors][ideal_pmes]
N/A
### Structs/Enums
#### Requirement: [Oberservable Equality][req_equality]
As padding is erased on moves we do not need to care about padding of ADTs that are not behind References. Padding in the pointee of References is referenced in the [References heading][ty_ref].
For Enums we must take into account which variant is active.
Requirements for equality of Structs and Enum Variants is the sum of the requirements of all the field types. Notably, this includes even *private* fields.
#### Ideal: [One Equality][ideal_equality]
Structs/Enums can have arbitrary behaviour for `PartialEq` impls. To uphold this ideal we would need to bound the behaviour of `PartialEq` of types used in TSVs. Luckily const patterns already deals with this problem by introducing the `StructuralPartialEq` trait. To uphold this ideal we would require that all Structs/Enums used in TSVs implement the `StructuralPartialEq` trait.
#### Ideal: [Lossless Roundtripping][ideal_roundtrip]
N/A
#### Ideal: [Intuitive Semver][ideal_semver]
Values for private fields being part of the public API of a type is likely a semver footgun. While it is technically possible to encounter breakage on stable due to private fields changing value, const generics would allow encountering this without users having to go out of their way to read private fields of upstream types. I don't think this is intuitive so it may make sense to forbid Structs/Enums in TSVs that are not ["fully public"][term_fp].
#### Ideal: [Good Pattern Types][ideal_pat]
In order to uphold this ideal we must not require more than `StructuralPartialEq` to be implemented, atleast not in a way that is backwards incompatible to stop requiring.
#### Ideal: [Const Patterns Parity][ideal_const_pat]
Structs/Enums are allowed in const patterns if they implement `StructuralPartialEq` and if all values used for fields meet the requirements of being a Const Pattern.
#### Ideal: [No Post-mono Errors][ideal_pmes]
N/A
### TypeId
`TypeId` is pretty much the same as the Structs section.
If it doesn't wind up otherwise forbidden in TSVs it may make sense to do so as we have taken special care to ensure that `TypeId`'s bits are entirely opaque during const eval. Allowing `TypeId` in TSVs before we implement `const PartialEq` would allow bypassing this restriction to some extent.
### Unions
Unions consist of a list of [Abstract Bytes][ty_byte]. As Abstract Bytes are not able to be supported in TSVs we consider Unions to not be able to be supported in TSVs.
## Strategies for Handling Undesirable Regular Values
To satisfy as many of our ideals and to uphold our requirements, we have to come up with strategies to handle undesirable RVs.
We discuss three strategies here:
- [Per-Value Rejection][sol_value]
- [Per-Type Rejection][sol_type]
- [Lossy Conversion][sol_lossy]
It's worth noting that these options don't have to be "all or nothing". We could pick options only for some types and pick others for different types. We could also pick an option for only some of the undesirable RVs of a type but pick a different option for different undesirable RVs of that same type.
### Per-Value Rejection
Similar to how Const Patterns work by rejecting specific values from being used, we could forbid only some values of a type from being used as a TSV. Effectively this would mean that conversions from a RV to a TSV is fallible.
#### Requirement: [Oberservable Equality][req_equality]
N/A
#### Ideal: [One Equality][ideal_equality]
N/A
#### Ideal: [Lossless Roundtripping][ideal_roundtrip]
N/A
#### Ideal: [Intuitive Semver][ideal_semver]
Unless we were to reject the exact same set of values as either const items or const patterns, we would be introducing another kind of breakage users need to be aware of.
For example if const generics were to not support values of negative zero (-0.0) even though they are supported by const patterns and const items:
```rust
// upstream crate
pub struct Foo(f32);
pub const fn mk_foo() -> Foo {
// version a
Foo(0.0)
// version b
// Foo(-0.0)
}
// downstream crate
fn my_const_generics<const N: Foo>() {}
fn breakage() {
my_const_generics::<{ upstream::mk_foo() }>();
}
```
The downstream crate would compile with version `a` of the upstream crate but not version `b`. Note that `mk_foo()` would be valid as a const pattern in both versions of the upstream crate.
#### Ideal: [Good Pattern Types][ideal_pat]
As only some values of floats and raw pointers are supported in patterns, Per-Value Rejection is a necessity for upholding this ideal (unless we change pattern matching).
#### Ideal [Const Patterns Parity][ideal_const_pat]
N/A
#### Ideal: [No Post-mono Errors][ideal_pmes]
On stable rust we will never have a RV->TSV conversion occur in codegen as it can always occur during type checking. This means that *on stable* Per-Value Rejection will not introduce any post mono errors.
Future Const Generics features will allow for generic parameters to be used in const generic arguments. Some designs for this extension may result in RV->TSV conversions occuring in codegen whereas others may not.
If we cannot guarantee the RV->TSV conversion always succeeds post-mono then we will have post mono errors from this conversion being fallible. The TL;DR here is that it is extremely unlikely that fallible RV->TSV will cause post mono errors but there *is* a remote possibility. See [this aside][aside_pmes] for detailed information.
### Per-Type Rejection
If any of the values in a type would be undesirable as a const generic argument we could altogether forbid the type from being used as the type of a Const Generic Parameter.
#### Requirement: [Oberservable Equality][req_equality]
N/A
#### Ideal: [One Equality][ideal_equality]
N/A
#### Ideal: [Lossless Roundtripping][ideal_roundtrip]
N/A
#### Ideal: [Intuitive Semver][ideal_semver]
Forbidding entire types from use in const generics gives us the ability to have an explicit *opt-in* to a type being a valid type of a Const Generic Parameter. Whether this is a trait or some other kind of marker is not too important.
With an explicit opt-in to a type being usable in const generics, library authors would get compilation errors when adding a private field with a type unusable in const generics to an accepted type. This would make it simple to machine-check semver violations (the guarantee would be explicitly removed), and also intuitive for library authors as they have explicitly stopped guaranteeing something they previously were.
#### Ideal: [Good Pattern Types][ideal_pat]
Rejecting types from const generics that are supported in const patterns would violate this ideal. As there is overlap between what values const generics cant support and what const patterns cant support, we can't uphold this ideal while also Per-Type Rejecting all types with values not valid for use in const generics.
#### Ideal: [Const Patterns Parity][ideal_const_pat]
N/A
#### Ideal: [No Post-mono Errors][ideal_pmes]
With per-type rejection we would be able to avoid post monomorphization errors from undesirable values as const generic arguments.
While this does not impact whether evaluation of constants may result in post-mono errors. It does mean that constants which do evaluate successfully cannot not result in any additional post-mono errors.
I do not expect a choice between per-value or per-type rejection of undesirable values to have any impact on how well we could support `<T, const N: T>`. Though it is worth noting that having an explicit opt-in would give the possibility of explicitly bounding the type parameter `T` by a requirement that all its values be valid in const generics (e.g. `<T: ConstParamTy, const N: T>`).
### Lossy Conversion
When converting a RV to a TSV, if the RV is unusable in the type system in some way, we could implicitly convert it to a different value that we *can* handle in the type system.
For example we may wish to lossily convert RVs of `-0.0` to a TSV of `0.0`, ensuring that runtime eq of a floating type const generic is equivalent to type system eq of the const generic.
If roundtripping a value through a const generic is lossy then care must be taken in the design of const generics to not allow lossy roundtripping of *private* data as there may be library invariants that could be broken.
To use the previous example of converting negative zeros to positive zeros:
```rust
// crate a
pub struct NonPosF32 {
/// SAFETY: must be a negative float
data: f32,
}
// crate b
fn foo<const F: a::NonPosF32>() {}
fn main() {
foo::<{ a::NonPosF32::new(-0.0) }>();
}
```
It would be unsound if the const generic argument `{ NonPosF32::new(-0.0) }` implicitly turned `data: -0.0` into `data: +0.0` breaking the library invariant of `NonPosF32` that its field is never positive.
In order to soundly support lossy RV->TSV conversions all ADTs used in const generics would need to be known to not have safety invariants for values of fields that could be Lossily Roundtripped.
#### Requirement: [Observable Equality][req_equality]
N/A
#### Ideal: [One Equality][ideal_equality]
N/A
#### Ideal: [Lossless Roundtripping][ideal_roundtrip]
This method of handling undesirable values does not uphold this ideal.
#### Ideal: [Intuitive Semver][ideal_semver]
Lossy Conversion requires some special handling of ADTs as previously mentioned. I believe that an explicit opt-in to being useable in const generics would be "intuitive" semver wise as the only semver breakage possible would be to remove the opt-in. I believe that explicitly removing a guarantee should be checkable by tools like cargo-semver-checks and should also be intuitive to users that it is a breaking change.
#### Ideal: [Pattern Type Forwards Compat][ideal_pat]
Lossy Conversion can be a useful tool to allow TSV equality to be equivalent to const pattern equality. In cases where TSV equality must take into account information that pattern matching doesnt, we can simply "remove" that information from the TSV.
For example const patterns of references do not take padding into account but TSVs must. Lossy Conversion can be used to erase padding, allowing TSV equality and runtime equality to be equivalent.
#### Ideal: [Const Patterns Parity][ideal_const_pat]
N/A
#### Ideal: [No Post-mono Errors][ideal_pmes]
N/A
## Summary: Undesirable Regular Values x Handling Strategy x Ideals
- :woman-shrugging: It's a bit nuanced
- :white_check_mark: This ideal would hold
- Note that for the [Good Pattern Types Ideal][ideal_pat] a :white_check_mark: would mean we would only be *forwards compatible* with good support for pattern types. *Not* that this option would already have good support for pattern types.
- :x: This ideal would not hold
Also note that the ideals and solutions in the tables are *clickable* links to previous parts of this document and so can be used for quick refreshers.
### [Provenance][ty_prov]
We must handle Provenance somehow. Instead of elaborating options in this heading we instead defer to the References, Raw Pointers, and Abstract Bytes headings. From an implementation perspective it is not *required* that handling provenance of References, Raw Pointers, and Abstract Bytes all be the same.
### [Abstract Bytes][ty_byte]
We must handle Abstract Bytes somehow. Instead of elaborating options in this heading we instead defer to the References, Raw Pointers and Unions headings. From an implementation perspective it is not *required* that handling Abstract Bytes of References, Raw Pointers, and Unions all be the same.
### [Raw Pointers][ty_ptr]
We must handle Raw Pointers with Provenance as we cannot support Provenance in TSVs:
| Solution | [One Equality][ideal_equality] | [Semver][ideal_semver] | [Roundtripping][ideal_roundtrip] | [No PMEs][ideal_pmes] | [Const Pats][ideal_const_pat] | [Pattern Types][ideal_pat] |
| -------- | -------- | -------- | ------ | - | - | - |
| [Reject Per-Value][sol_value] | :white_check_mark: | :woman-shrugging: | :white_check_mark: | :woman-shrugging: | :white_check_mark: | :white_check_mark: |
| [Reject Per-Type][sol_type] | :white_check_mark: | :white_check_mark: | :white_check_mark: | :white_check_mark: | :white_check_mark: | :white_check_mark: |
While we could theoretically use "Lossy Conversion" for Raw Pointers with Provenance, any such approach seems undesirable and very unintuitive for users.
Semver of "Reject Per-Value" is a :woman-shrugging: because we already have semver issues from const patterns only supporting integer raw pointers.
Rejecting Raw Pointers with Provenance also indirectly handles Abstract Bytes arising from Raw Pointers which we also needed to handle.
We also must choose whether we want to "handle" `fn` pointer Raw Pointers:
| Solution | [One Equality][ideal_equality] | [Semver][ideal_semver] | [Roundtripping][ideal_roundtrip] | [No PMEs][ideal_pmes] | [Const Pats][ideal_const_pat] | [Pattern Types][ideal_pat] |
| -------- | -------- | -------- | ------ | - | - | - |
| Same as Prev Table | .. | .. | .. | .. | .. | .. |
| Accepted | :x: | :white_check_mark: | :white_check_mark: | :white_check_mark: | :x: | :white_check_mark: |
### [References][ty_ref]
We must handle Abstract Bytes in the pointee of a reference and we must also handle Provenance of the Reference.
The easiest solution would be to reject references in const generics:
| References | [One Equality][ideal_equality] | [Semver][ideal_semver] | [Roundtripping][ideal_roundtrip] | [No PMEs][ideal_pmes] | [Const Pats][ideal_const_pat] | [Pattern Types][ideal_pat] |
| -------- | -------- | -------- | ------ | - | - | - |
| [Reject Per-Type][sol_type] | :white_check_mark: | :white_check_mark: | :white_check_mark: | :white_check_mark: | :white_check_mark: | :white_check_mark: |
But if we want to support references then we must handle the two undesirable kinds of RVs some other way:
Handling Provenance:
| Provenance | [One Equality][ideal_equality] | [Semver][ideal_semver] | [Roundtripping][ideal_roundtrip] | [No PMEs][ideal_pmes] | [Const Pats][ideal_const_pat] | [Pattern Types][ideal_pat] |
| -------- | -------- | -------- | ------ | - | - | - |
| [Lossy Conversion][sol_lossy] | :white_check_mark: | :white_check_mark:| :x: | :white_check_mark: | :white_check_mark: | :white_check_mark: |
"Lossy Conversion" would mean entirely disregarding the provenance and treating references in TSVs the same a TSV of the pointee. Conceptually this would be as if we "shrank" the provenance of the reference to the minimum needed for a reference to be valid.
"Reject Per-Value" is absent from this table as all references have provenance. This would not be meaningfully different from "Reject Per-Type".
Handling Abstract Bytes (e.g. padding):
| Solution | [One Equality][ideal_equality] | [Semver][ideal_semver] | [Roundtripping][ideal_roundtrip] | [No PMEs][ideal_pmes] | [Const Pats][ideal_const_pat] | [Pattern Types][ideal_pat] |
| -------- | -------- | -------- | ------ | - | - | - |
| [Reject Per-Value][sol_value] | :white_check_mark: | :x: | :white_check_mark: | :woman-shrugging: | :white_check_mark: | :white_check_mark: |
| [Reject Per-Type][sol_type] | :white_check_mark: | :white_check_mark: | :white_check_mark: | :white_check_mark: | :white_check_mark: | :white_check_mark: |
| [Lossy Conversion][sol_lossy] | :white_check_mark: | :white_check_mark: | :x: | :white_check_mark: | :white_check_mark: | :white_check_mark: |
"Reject Per-Value" would mean erroring on initialised Abstract Bytes bytes.
"Reject Per-Type" would mean rejecting ADTs with padding from being used in TSVs.
"Lossy Conversion" would mean converting all Abstract Bytes to Uninit.
### [Floats][ty_floats]
The easiest solution would be to reject floats in Const Generics:
| Floats | [One Equality][ideal_equality] | [Semver][ideal_semver] | [Roundtripping][ideal_roundtrip] | [No PMEs][ideal_pmes] | [Const Pats][ideal_const_pat] | [Pattern Types][ideal_pat] |
| -------- | -------- | -------- | ------ | - | - | - |
| [Reject Per-Type][sol_type] | :white_check_mark: | :white_check_mark: | :white_check_mark: | :white_check_mark: | :white_check_mark: | :white_check_mark: |
Allowing floats in const generics requires handling +0.0/-0.0 and NaNs in some way if we wish to uphold all of our ideals. Unfortunately all solutions here wind up requiring compromising on atleast one of our ideals.
| NaNs | [One Equality][ideal_equality] | [Semver][ideal_semver] | [Roundtripping][ideal_roundtrip] | [No PMEs][ideal_pmes] | [Const Pats][ideal_const_pat] | [Pattern Types][ideal_pat] |
| -------- | -------- | -------- | ------ | - | - | - |
| [Reject Per-Value][sol_value] | :white_check_mark: | :woman-shrugging: | :white_check_mark: | :woman-shrugging: | :white_check_mark: | :white_check_mark: |
| [Lossy Conversion][sol_lossy] with bitwise equaltiy | :x: | :white_check_mark: | :x: | :white_check_mark: | :white_check_mark: | :white_check_mark: |
| Accepted with bitwise equality | :x: | :white_check_mark: |:white_check_mark: |:white_check_mark: | :woman-shrugging: | :white_check_mark: |
"Lossy Conversion" would mean picking one NaN value to use as the "actual" representation of NaNs then considering it equal to itself and no other floats.
"Const Pats" for Accepted is :woman-shrugging: because even though technically we would support TSVs with no valid pattern, the restriction that NaNs are not allowed in patterns is not a hard limitation and is more of a lint. I would not expect this to actually cause any problems.
| +0.0 and -0.0 | [One Equality][ideal_equality] | [Semver][ideal_semver] | [Roundtripping][ideal_roundtrip] | [No PMEs][ideal_pmes] | [Const Pats][ideal_const_pat] | [Pattern Types][ideal_pat] |
| -------- | -------- | -------- | ------ | - | - | - |
| [Reject Per-Value][sol_value] | :white_check_mark: | :x: | :white_check_mark: | :woman-shrugging: | :white_check_mark: | :x: |
| [Lossy Conversion][sol_lossy] with bitwise equaltiy | :x: | :white_check_mark: | :x: | :white_check_mark: | :white_check_mark: | :white_check_mark: |
| Accepted with bitwise equality | :x: | :white_check_mark: |:white_check_mark: |:white_check_mark: | :woman-shrugging: | :white_check_mark: |
"Lossy Conversion" would mean converting `-0.0` to `+0.0` (or vice versa).
### [Function Pointers][ty_fnptr]
Allowing function pointers, or function pointers transmuted to raw pointers, in const generics requires us to give up on a few ideals:
| Solution | [One Equality][ideal_equality] | [Semver][ideal_semver] | [Roundtripping][ideal_roundtrip] | [No PMEs][ideal_pmes] | [Const Pats][ideal_const_pat] | [Pattern Types][ideal_pat] |
| -------- | -------- | -------- | ------ | - | - | - |
| Accepted | :x: | :white_check_mark: | :white_check_mark: | :white_check_mark: | :woman-shrugging: | :white_check_mark: |
| [Reject Per-Type][sol_type] | :white_check_mark: | :white_check_mark: | :white_check_mark: | :white_check_mark: | :white_check_mark: | :white_check_mark: |
"Const Pats" is :woman-shrugging: because while we do support more values than const patterns do, it is for a type that is not supported in const patterns. This has slightly different (and less scary) implications for this ideal.
"One Equality" no longer holds because runtime equality of function pointers is pure rng.
### [Structs/Enums][ty_adts]
N/A
No values are undesirable
### [Unions][ty_union]
We must handle unions as they consist of Abstract Bytes which we cannot support:
| Solution | [One Equality][ideal_equality] | [Semver][ideal_semver] | [Roundtripping][ideal_roundtrip] | [No PMEs][ideal_pmes] | [Const Pats][ideal_const_pat] | [Pattern Types][ideal_pat] |
| -------- | -------- | -------- | ------ | - | - | - |
| [Reject Per-Type][sol_type] | :white_check_mark: | :white_check_mark: | :white_check_mark: | :white_check_mark: | :white_check_mark: | :white_check_mark: |
## Asides
The footnotes. If you have read from the top of the document down to here then you are done :-)
### Per-Value Reject Post Mono Errors
Designs for supporting uses of generic parameters in Const Generic Arguments have to handle panics in const evaluation during codegen somehow. Some of the options for avoiding post mono errors from fallible const evaluation will also avoid post mono errors from fallible RV->TSV conversions.
This is a rough outline of the possible designs for this future extension to the language and how they would handle avoiding post mono errors, and how that interactions with a fallible RV->TSV conversion:
- Require explicit `where evaluatable { /* expr */ }` bounds requiring the caller to attempt checking that `/* expr */` can be evaluated successfully
- This was the ([unworkable](https://hackmd.io/@BoxyUwU/BJ6_bfmD0)) design for `generic_const_exprs`.
- This design would not result in post mono errors if we only support some subset of values being valid
- Only support expressions which are not too generic to be evaluated pre-monomorphization (or are bare uses of Const Generic Parameters that we know the caller can evaluate)
- This is the current stable design of const generics.
- The `min_generic_const_args` prototype for supporting associated constants in the type system uses a slight derivative of this design
- This design will not result in post mono errors if we only support some subset of values being valid
- Introduce a termination checker and panic effect to be able to write functions that are guaranteed to return *some* value eventually :>
- This has not been experimented with whatsoever and seems highly unlikely to happen any time soon
- While this would prevent post-mono errors from const eval errors, it would not necessarily prevent post-mono errors from fallible RV->TSV conversions.
- Accept arbitrary expressions that may fail post-monomorphization
- This design will naturally result in post mono errors when only supporting some subset of values, but given we would already have post mono errors from const eval errors during monomorphization that doesn't seem that big of a deal
Fallible RV->TSV conversion would only introduce post-mono errors in a future where we have a termination checker to prevent post-mono errors from const eval errors/non-termination in const generic arguments.
[go back to Per-Value Reject][pvj_pmes]
[pvj_pmes]: #Ideal-No-Post-mono-Errors68
## Terminology
#### Const Generic Parameter
The definition site of const generics, i.e. `const N: Type` in generics. The struct `Foo` in `struct Foo<const N: usize>` has one Const Generic Parameter with the name `N` and has type `usize`.
#### Const Generic Argument
The "callsite" of const generics, i.e. an argument to a Const Generic Parameter. In `Foo<{ (u32::MAX + 1) as usize }>` the generic argument `{ (u32::MAX + 1 ) as usize }` is a Const Generic Argument.
#### Regular Value
The kind of value that you would encounter at runtime (or during const eval) that expressions evaluate to.
### RV
Shorthand for Regular Value
#### Type-System Value
Also known as a ValTree. The way that values in the type-system are represented, for example the final value of a Const Generic Argument.
### TSV
Shorthand for Type-System Value
### Fully Public
I would consider an ADT to be "fully public" if all places that can name the ADT can name all fields in all variants. Additionally, all types of fields must be "fully public" too.
[term_cgp]: #Const-Generic-Parameter
[term_cga]: #Const-Generic-Argument
[term_rv_full]: #Regular-Value
[term_rv]: #RV
[term_tsv_full]: #Type-System-Value
[term_tsv]: #TSV
[term_fp]: #Fully-Public
[aside_pmes]: #Per-Value-Reject-Post-Mono-Errors
[aside_prov]: #Undesirable-Values-Provenance
[aside_byte]: #Undesirable-Values-Abstract-Bytes
[req_equality]: #Observable-Equality
[ideal_roundtrip]: #Lossless-Roundtripping
[ideal_pmes]: #No-Post-mono-Errors
[ideal_equality]: #One-Equality
[ideal_semver]: #Intuitive-Semver
[ideal_const_pat]: #Const-Patterns-Parity
[ideal_pat]: #Good-Pattern-Types
[sol_value]: #Per-Value-Rejection
[sol_type]: #Per-Type-Rejection
[sol_lossy]: #Lossy-Conversion
[ty_prov]: #Provenance
[ty_fnptr]: #Function-Pointers
[ty_ref]: #References
[ty_ptr]: #Raw-Pointers
[ty_union]: #Unions
[ty_floats]: #Floats
[ty_byte]: #Abstract-Bytes
[ty_adts]: #Structs/Enums