Description
What is this
This is a design document for const generics. Any discussions about its content should be on zulip. The conclusions of these discussions should then be edited back into this issue. Please do not post any comments directly in this issue.
Content
adapted from a summary by @PoignardAzur.
Quite a lot of functions might not want their exact output to be part of their stability guarantees. A good example for this is mem::size_of::<T>()
.
With const generics, changing the return value of such functions can however end up being a breaking change.
// The fields of `MyIndex` are private, so it should not be a breaking
// change to change it to a `u64`.
struct MyIndex(u32);
// This is currently allowed but would break if the size of `MyIndex` changes.
//
// If we keep constants which use `size_of` opaque, this would not compile even
// though the sizes match up, removing a stability hazard.
let _: [u8; 4] = [0; size_of::<MyIndex>()];
While this is already an issue today, it may get even worse with the addition of complex generic constants, e.g:
trait Trait<const N: usize> {
type Assoc;
fn get_zero() -> Self::Assoc;
}
impl<const N: usize> Trait<N> for [u8; N] where N <= 4 {
type Assoc = u32;
fn get_zero() -> u32 { 0 }
}
impl<const N: usize> Trait<N> for [u8; N] where N > 4 {
type Assoc = u64;
fn get_zero() -> u64 { 0 }
}
Current stability guidelines
Looking at the SemVer Compatibility doc this is somewhat under-documented.
- Changing public fields is explicitly described as a major change. Changing private fields (in a struct which already has private fields) isn't mentionned, and is therefore implied not to be a breaking change that requires a major or minor semver bump.
- Changing the value of constants or the code of constant functions isn't mentioned at all.
Should we worry about this?
Currently any breakage tends to be fairly explicit, as it mostly happens by unifying an array, where the length is the result of a function call, with some array of a concrete length. This means that places where breakage may occur tend to be fairly obvious.
This may change in the future however. By using a bound like N > 4
for trait implementations it will be less clear whether one currently depends on the concrete return value of such a function. It will also make it less obvious what actually went wrong, making the resulting breakage more difficult to figure out.
And in any case, the current situation isn't ideal. Stability guarantees should be better documented, and enforced by machine rules instead of doc comments.
How could we remedy this
The language could have an annotation to mark that a constants value isn't part of the stability guarantee, and therefore shouldn't be relied on.
This annotation would have no effect within a crate, but would prevent downstream crates from unifying the constant with its value, among other things.
For instance:
// crate_a
#[opaque]
const A: i32 = 42;
let array: [i32; 42] = [0; A]; // no problem
// crate_b
let array: [i32; 42] = [0; A]; // error!
Opaqueness would be infectious:
#[opaque]
const X: i32 = 0;
#[opaque]
const Y: i32 = X; // OK
const Z: i32 = X; // ERROR!
Const functions could be declared as opaque as well, so that their output isn't a stability guarantee:
#[opaque]
const fn count_unicode_grapheme_clusters(text: &str) -> usize {
// ...
}
Arguably, some developers may even want opaqueness to be the default for functions. It's unclear how common opaque const functions would be. On one hand, library authors don't want to bump semver whenever they change internal code.
On the other hand, there are functions where it's absolutely clear the output is never expected to change, like Vec::size
or iter::find
, even if their internal algorithm changes.
The most notable function is mem::size_of
. Its output is explicitly not part of Rusts stability guarantees but people still want to rely on the exact size of their structs. So while we won't be able to completely forbid that, we should consider to lint in these cases instead. @PoignardAzur discusses this in more detail.
Next steps
While this is strongly related to const generics its actual design and implementation are mostly independent of it. I would expect us to take the following steps for this:
- open a lang team MCP to add the notion of opaque constants and to clarify the stability guarantees of constants and functions.
- implement opaque constants in the compiler
- add them to relevant methods and constants in the standard library, doing crater runs and probably linting in case one relies on the value of an opaque constant.