---
title: "Design meeting 2025-08-20: Equality of type system values"
tags: ["T-lang", "design-meeting", "minutes"]
date: 2025-08-20
discussion: https://rust-lang.zulipchat.com/#narrow/channel/410673-t-lang.2Fmeetings/topic/Design.20meeting.202025-08-20.3A.20Extending.20const.20generics.20part.201/
url: https://hackmd.io/NogdwBOJT0GoWUjkaGmzFA
---
# Complexities of Type System Values
> **Reading guide**: Reading top to bottom should be a good order, if you run out of time the final section with the summaries and tables isn't strictly necessary reading. Other than that there's no real decisions to be made this meeting just trying to get everyone on the same page about the information in this doc as it will be useful for future meetings/decision making.
## 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 conversions 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 compatibility 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][ty_byte]. 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:
| 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: |
But if we want to support references then we must handle the two undesirable kinds of RVs some other way:
Handling Provenance:
| Solution | [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]
Allowing floats in const generics requires handling signed zeros 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:
| Ideal x 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: |
| [Lossy Conversion][sol_lossy] | :white_check_mark: | :white_check_mark: | :x: | :white_check_mark: | :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: |
| Accepted with bitwise equality | :x: | :white_check_mark: |:white_check_mark: |:white_check_mark: | :woman-shrugging: | :white_check_mark: |
"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.
It's not clear to me how the Lossy Conversion solution would be implemented. We could convert `-0.0` to `+0.0` but there's no obvious choice for NaNs.
One potential option here would be to "mix and match" solutions. We could per-value reject NaNs while lossily converting negative zeros to positive zeros:
| Solution | [One Equality][ideal_equality] | [Semver][ideal_semver] | [Roundtripping][ideal_roundtrip] | [No PMEs][ideal_pmes] | [Const Pats][ideal_const_pat] | [Pattern Types][ideal_pat] |
| -------- | -------- | -------- | ------ | - | - | - |
| Mix-and-Match | :white_check_mark: | :woman-shrugging: | :x: | :woman-shrugging: | :white_check_mark: | :white_check_mark: |
Semver is a :woman-shrugging: because we already have semver issues from const patterns not supporting NaN values. This solution wouldn't make the existing semver problems worse, but if const patterns did not break this ideal then const generics would be the only reason `NaN`s have semver issues.
### [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
---
# Discussion
## Attendance
- People: TC, Josh, Tomas, Zachary Sample, Boxy, Amanieu, Rob, Eric Holk, Tyler, Mark (simulacrum), Niko
## Meeting roles
- Driver: TC
- Minutes: Tomas
## General
TC: Thank you Boxy for this extensive document. Please give us some background and what you're expecting.
Boxy: Theres' not really any decision to make on this meeting. I just want to make sure that everyone knows about the complexity of this feature. I just want to get to a point where everyone understands these complexities. And in the future we'd be able to come to a conclusion of what ideals we care about and set out what trade-offs.
TC: Maybe give us some of the long history of this. What's created the opportunity now for pushing forward on this.
Boxy: The const generics feature got stabilised in 2021. There were still problems with it and other features to work on e.g. supporting generic parametrs inside of const generics. We didn't want to try and commit to a design for this feature until we had a better idea of what the other features would be about. Implementation wise this was close to being done and just needed design work. About a year ago we got a much better grasp of how supporting constant type parameters in const generic parameters would work about. The surrounding ecosystem of const generic works has become more viable.
TC: I propose we skip vibe checks -- our shared vibe is likely, "yes, great, let's make this better" -- and so the most important thing is to build understanding and get questions answered. Boxy, any questions below that stood out to you?
Boxy: All of them seem important.
TC: Let's take them then.
## Trait bounds to require a type to be usable in const patterns?
Josh:
> This necessitates either post mono errors or a trait bound to require a type be "useable in const patterns".
This seems like a promising path to go down, avoiding post-mono errors by requiring declaration of how we want to use a value. Also seems like it would simplify semver handling. Is this worth exploring further? Are there major issues with pursuing that?
Boxy: It's closer to an idle musing. It doesnt' come up much in the rest of the doc because it's not relevant other to this specific case. Trait bound wouldn't help other in the case const generics don't support more values than ?? do. The unstable StructuralPartialEq trait wouldn't support the const trait. It doesn't constantly require that fields themselves are StructuralPartialEq to.
Josh: What your saying makes sense -- it's more relevant to the case where you're more concerned about types rather than values. I found motivated by the fascinating semver hazards you've highlighted. That seems like the kind of thing to give people working on semver tooling cold sweats. Given that, I'm wondering if there's a way to say "this constant function is only capable of returning things that are f32 not NaN".
Boxy: I could potentiallly see solutions like that. A trait that only supports the values of const patterns.
Josh: Yeah, I'd love to be able to declare that this is usable in a pattern and it always will be. If there's a design that would make that possible, I'd love to see that explored to make semver more stable and less hazard.
Tyler: I'd also love to see that.
TC: To some degree, we're likely to need to accept some of these SemVer hazards. We're talking about compile-time evaluation, so it's macro-like in this way. We have a number of avenues for mitigating this, such as better documentation. Many of the great examples in this document made me think about what we could do better in the Reference, e.g. by adding admonitions.
Niko: What Josh said makes sense. But when it comes to Semver one thing I've noted is: for a lot of applications, some of the semver hazards where const generics can be quite important and they want the compilation to break in those cases.. an example I've commonly seen in C++ is e.g. asserting that the size of a struct is exactly 32 bytes because you have some wicked specialized code that assumes that, or that two structs have the same size, etc. Some of the examples struck me as similar. One person's semver hazard can be another person's useful assertion.
TC: +1.
## "In any way"
> Observability: If two TSVs can be observed to be different in any way then the TSVs must be unequal
scottmcm: we have loads of types that are Eq but which have observable differences -- slices and vecs jumping out most. Are those fundamentally just not going to happen in TSVs?
Boxy: Vecs are interesting because the only way you'd get them in const generics is if you get const heap allocation. If we did have that, we'd probably have to handle provenance in const genericts. I don't know what the problem with slices would be. They seem to have normal standard qualities of equality.
Boxy: The hard part is supporting references.
scottmcm: so can we (look below) use slices instead of Vecs?
## Unsized types
scottmcm: Can we support things like `[i32]` as a TSV? Feels like we're, in practice, allocating in the compiler to hold them anyway, so the restrictions that make `!Sized` hard at runtime might just not apply here?
Boxy: More context on the history fo this feature. References are hard. The unstable implementation splits references behind a separate feature flag. I think there's no problem just having an unsized type for TSVs without a reference. We don't have a notion of compile-type only functions. You couldn't run a const fn that returns a slice that you wouldn't be able to also write in runtime.
scottmcm: We would have to restrict this to literals.
Josh: There are things with strings that we can do with constant that you can't do at runtime without memory allocation. If I have two string constants I need to allocate memory. At compile time I can just concatenate them and create a new string constant. (Which may or may not take up additional space in the binary, depending on whether the compiler/linker can overlap them.)
TC: This reminds me of a pattern I've used before: you have something that's going to be dynamically sized. You have to run it twice: once to produce its length and then second to actually produce the right const generic. Is there anything we can do about improving that pattern?
E.g., you have a string you want to put into a constant or static. You want to generate an array of that many bytes. You can't write a const function that returns that array without knowing its length. So you have something that computes it to return the length, so you can set that, then something that returns the value of that length.
Boxy: We've stabilised a function that lets you ??. It seems related. If we had the notion of return position in impl traits with const values maybe we could define the type as being some array of length that's not specified and the compiler would figure it out. It's very speculative.
scottmcm: There's a bunch of interesting things if we have a path to get to a "const-only fn".
TC: This reminds me of our discussion about inferring the length of statics and consts. What you had said about that is that opaque types are the right answer, and I think that's right. It's interesting that opaque types may similarly help here.
zach: Related to the talk about calculating length ahead of time, one semi-concrete example is concatenating a const `&[&u8]]` into a const `&[u8]`: Because the input is a `const`, you can evaluate the length you need "one const-level up" and then do the *actual* concatenation one level lower where the length is constant
e.g.:
```rust
const SLICES: &[&[u8]] = &[b"hello", b", ", b"world"];
const LENGTH: usize = sum of lengths of SLICES;
const CONCATED: [u8; LENGTH] = do the concatenation of SLICES;
```
## Strings
Josh: What about string constants, in const generics? How do they measure up to these requirements?
I don't know if this is a tangent, or whether it's a simple or complex answer; we don't need to spend a lot of time on it. Primarily trying to find out if it's on the radar and whether it has excessive complexity. The case I'm thinking of is something provided literally like `MyType<SomeParam, "string constant">`.
Josh: I think we partially covered that above. It seems the usecase of having a literal constant would just work.
Boxy: Strings are more interesting about unsized types. For all other outsized types you can write out the underlying type. If you have a const generic slice you need to write out the length. Strings are different becaues there's no sized version of a string. They feel like the only unsized type that we have to support.
scottmcm: Makes me wonder how would people feel about if we just said "we could add that type". Whether we can backwards-compatiby have strings return it. Or deref to an unized type. In the same sort of way of having str be a library type that wraps an array of u8. Have one of these types that's a fixed-length UTF-8 buffer.
Tyler: What problem are we trying to solve? If we have a sized string literal type. It works around a lot of problems with references. That stems from the requirement that any two TSV are observably different. What does that mean exactly? Does observably different mean they that after they're converted ot RV that they're different?
Boxy: The need for them to be not observably different is -- we need it to be the case that if two TSV are equal you can't do anything in the type system that give you different results. You take a TSV, convert it to RV and const fns can't observe anything differently based on these RVs. Addresses are weird because they aren't observable at compile time but they are observable at runtime. We tend to consider that references to values of constansn are abritrary at runtiem.
Niko: You said they have to behave the same at compilation time. Spell out what goes wrong if I have two TSVs that are considered equal and behave differently. What would happen? Say -0 and +0.
Boxy: You could have some associated type resolve to a completely different type. You could conceiveably write code that normalizes to different tyeps based on whether they're -0 or +0.
Niko: Another thing: I have a function paraterized by a constant that's a floating point. At runtime, that function returns a sign of that floating point. And if those values are -0 or +0, they get monomorphized into the same thing and at runtime it could return an unpredictable value.
Tyler: We could take the value of const fn and turn it into a value.
Niko: It's non-deterministic.
Tyler: You definitely don't want to infect the type system. Having two inputs that are cossidered equal, run them through a const fn and observe a different.
## Importance of ideals
nikomatsakis: The ideals all seem good but also of varying importance. Are some of them more important than others, in your mind? As one example, I think that "one equality" is probably less important than some of the others. Might that be a useful area of feedback from the lang team? (which things we should preserve and when/why)
Boxy: I don't know about "more important" and "less important". Some of them are probably easier to say we care about or not. The language has already taken active steps to make the equality one hold. E..g in pattern matching. So not upholding that for type system values would field weird. The intutive semver one kind of already doesn't hold on stable. So maybe you could make a point that we care less about. Lossless roundtripping one depends. Some of in here are about forwards compatibility concerns and I hold those very highly. I would hold the const patterns ideal very highly.
tmandry: +1 for pattern types being good
Niko: That's a useful perspective. What are we already locked into. Which ones impact future design.
## Post-mono errors
TC: The document talks about some things not possible without post-mono errors, e.g.:
> 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.
TC: We've generally, on the lang side, been OK with post-mono errors when necessary. My sense is that we're not allergic to these. We prefer to give an error as early as possible. If the soonest that can be done is post-mono, that can be OK. See, e.g., our statement on this regarding `array_chunks`:
https://github.com/rust-lang/rust/issues/74985#issuecomment-2686269351
Boxy: That's good to know. The document talks a lot about post-mono errors. But I thinks very possible to design this in a way that we won't wind up needeing to do.
## Padding as abstract bytes
scottmcm: under references I see
> References to ADTs may contain padding bytes which are treated as Abstract Bytes
But then under structs it talks about
> As padding is erased on moves we do not need to care about padding of ADTs that are not behind References.
Can you elaborate on why padding is only a problem behind references? Why can't it be treated as erased behind references too? Is it just that the reference might be turned into a pointer and that pointer used to read something else? If so, is that not a pointer problem more than a reference problem? When is `CastKind::Transmute` banned for making TSVs?
Boxy: Generally optimise when a type or padding is allowed to be lost on a move. All arguments for const generic parameters could be considered a move. If we didn't want padding to be lost ??
## Structural properties
> 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.
TC: It's interesting that this pattern of needing to know that a property is structural is appearing in many places. E.g., we rely on this with `Freeze`. As we work on pin projections, we're confronted by the fact that `Unpin` doesn't currently work this way, and yet it'd be better if it did. This is just musing on what we might draw from this, e.g. whether there are any broader features, patterns, or lessons that might derive from this.
tmandry: Related to that, let's talk about the next one.
## semver hazard for structs
> #### 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].
tmandry: The straightforward (to me) way to solve this is to stabilize `StructuralPartialEq`, make types opt-in, and require every field type implements it to use it.
In fact, if we are going to pick one thing to do next, I would propose doing this. Reasons I think that:
* ADTs in const generics *seem* to be in high demand
* There's a lead time where this needs to propagate in the ecosystem
* There don't seem to be that many thorny design/implementation questions
Tyler: If we're picking one thing to stabilize maybe we want to do ADTs in const generics. I'd like to have a vibe check on that.
Niko: What would we be stabilizing and what would happen?
Tyler: If you want your type to be usabel as const genereic, youd' derive StrcuturalPartialEq. You're creating a new semver hazard (you're not going to create now fields that aren't StructuralPartialEq).
Niko: Would we be in any way enforcing the runtime equality and sstructuraly partial eq?
Tyler: My assumption is we'd forsee to use
Boxy: StructuralPartialEq is automatically derived on stabel if you derive PartialEq. The unstable imlementation currently does const-time type derive. It enforces the equality is the same. We could potentially add checks that no private fields or unsafe fields are allowed. But I don't think StructuralPartialEq holds for this. You'd need a different trait.
Tyler: Is the trait stable?
Boxy: The trait is unstable but it's implemented on Stable. You can observe whether the trait is implemented.
Tyle: There should be some derive that requires you derive PartialEq and all your traits implement the derived trait.
## function pointers
nikomatsakis: The doc says:
> 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.
I don't totally follow this. It seems to me that it would be *possible* to imagine "abstract values" -- i.e., a raw pointer can be an address or it can be a kind of abstract value. We already have the notion of this for handling references to statics, right? I am paging this back in now, I remember us talking at the time about the possibility of admitting those statics in TSVs using some special kind of provenance or other handling -- are we saying we don't want to handle that at this time but we could consider it later or that it is fundamentally impossible?
EDIT: reading more I think I am seeing the point.
boxy: its possible to support, we could have fn items instead of fn ptrs
## purpose of the "undesirable regular values" of section
nikomatsakis: I don't quite understand the purpose of the Undesirable Regular Values section. Is it saying: these are subcategories of things that are undesirable?
EDIT: reading more I think I am seeing the point.
## "we must handle raw pointers with provenance", backwards compat
nikomatsakis: In ["the table"](https://hackmd.io/NogdwBOJT0GoWUjkaGmzFA?view#Raw-Pointers88) it says:
> We must handle Raw Pointers with Provenance as we cannot support Provenance in TSVs
This sentence does not parse for me. I think perhaps it means "we are only looking at raw pointers *without* provenance"? In that case, I feel a bit confused -- do we not support raw pointers in patterns today? If we do, that implies that "rejecting per type" can't satisfy the Const Patterns Parity ideal, right?
## forwards compatibility question
nikomatsakis: Is it correct that if we Reject Per-Type we can later move to Reject Per-Value if we decide to do so? (e.g., for references)
## lossy conversation
nikomatsakis: It seems like we'd really like to avoid lossy conversion. Are there places that you think it would be a *good* or perhaps *necessary* choice?
Boxy: References are something that we can very much support with lossy conversion. We could also special case references to statics in TSVs but that breaks the one-equality ideal.
nm/boxy: "its a weird option"
## `dyn Trait` in consts
tmandry: Shouldn't we consider two `Type as Trait` vtables the same, regardless of their function pointers? At least assuming we don't allow incoherent impls. If we do, we could mark those somehow as part of the vtable at const eval time and stop you from using an incoherent impl `dyn Trait` as a const generic.
Side note: We should stick associated consts in vtables so you can access them from a `&dyn Trait`, both at runtime and const eval time.