min_generic_const_exprs

written anonymously by two random people you will never guess who

What do we want to avoid

unused substs

see https://github.com/rust-lang/project-const-generics/blob/master/design-docs/anon-const-substs.md#unused-substs

Closures with anon consts in args cause "closure/generation that references itself" errors https://github.com/rust-lang/rust/issues/85665

#![feature(generic_const_exprs)]
struct Foo<F: Fn([(); N + 1]), const N: usize>(F);

fn bar() {
    Foo::<_, 2>(|_|{});
}

AnonConsts mark all params as invariant

#![feature(generic_const_exprs)]
struct Foo<T, U, const N: usize>([(); N + 1])
where
    [(); N + 1]:;

cycle errors from ConstEquate calling typeck

anon consts can refer to themselves via their own where-bounds see https://github.com/rust-lang/project-const-generics/blob/master/design-docs/anon-const-substs.md#consts-in-where-bounds-can-reference-themselves

additionally by requiring each const to be typeck'd to build thir in order to unify consts we can trivially cause cycles if typechecking a const requires solving a ConstEquate obligation

cycle error from using Self in trait where clause

the following currently cycles and should be handled somehow:

trait Trait where evaluatable { <Self as Trait>::CONST } {
    const CONST: usize;
}

unification of anon consts is fragile af

e.g. https://github.com/rust-lang/rust/pull/90529

by using thir to unify consts we run the risk of accidentally exposing impl details of how we lower language things to thir which is concerning

Potential Solution

Introduce a feature(min_generic_const_exprs) that only allows assoc and free constants as anon consts:

// requires generic free constants and generic associated constants
//
//     GFCs and GACs
//
// I do want these anyways, so :shrug:
const ADD_ONE<const N: usize> = N + 1;

fn push<T, const N: usize>(a: [T; N], v: T) -> [T; ADD_ONE::<N>] {
    // ...
}

or using an associated constant

trait AddOne<const N: usize> {
    const ADDED: usize;
}
impl<const N: usize> AddOne<N> for () {
    const ADDED: usize = N + 1;
}

fn push<T, const N: usize>(a: [T; N], v: T) -> [T; <() as AddOne<N>>::ADDED] {
    // ...    
}
fn push<T, const N: usize>(a: [T; N], v: T) -> [T; N + 1] {
    //~^ error: `N + 1` is not a free const or an assoc const
}

The following also compiles:

const ADD<const LHS: usize, const RHS: usize> = LHS + RHS;
fn foo<const N: usize>() -> [(); ADD<N, 3>] {
    [(); ADD<N, { 1 + 2 }>]
} 

Advantages

  • no unused substs, all are explicit
  • solves almost all where-bound cycles as we do not require typeck to unify consts
  • unification is vastly simpler and has no forwards compatibility issues nor does it rely on typeck
    • for assoc consts we just check that the trait refs are the same
    • for free consts we just check that the items are the same and substs unify
  • izi to implement :3

Disadvantages

  • trait Trait where evaluatable { <Self as Trait>::CONST } still cycles
  • fairly restrictive (i.e. N + 1 is not allowed) but this will be solved by feature(generic_const_exprs)

Ways to "safely" extend this (not too relevant)

fn push<T, const N: usize>(a: [T; N], v: T) -> [T; const<N> { N + 1 }] {
    // ...
}

const<N> where x { expr } has completely separate generics from its parent.

We can still keep feature(generic_const_exprs) to allow anon const expressions which inherit their parents generics like N + 1.

Advantages

  • Less annoying to use than ADD_ONE::<N>
  • Allows crates to be more composable as the ecosystem does not need one crate providing an ADD_ONE const
  • By requiring generic consts to be marked with const<GENERICS> WHERE_CLAUSES we can avoid any unused_substs issues

Disadvantages

  • syntax bikeshed
    Image Not Showing Possible Reasons
    • The image file may be corrupted
    • The server hosting the image is unavailable
    • The image path is incorrect
    • The image format is not supported
    Learn More →
  • these will be substantially different from ordinary inline consts as they require explicit mention of the used params and where bounds
  • HOW TO UNIFY?!?!

feature(min_generic_const_exprs) language future compat

by keeping the special case for anon consts which only consist of named constants, or by correctly dealing with both unused substs and allowing the evaluation of subtrees of AbstractConsts, feature(generic_const_exprs) should not introduce any breaking changes after feature(min_generic_const_exprs) has been stabilized.

feature(min_generic_const_exprs) library concerns

Issue: By requiring the use of named constants which only unify with themselves, constants for the same expr, e.g. N + 1, which are defined in separate libraries, do not unify.

Solution: Add a module to the std called std::constants and add constants for pretty much all common ops, e.g. std::constants::ADD. Add lints when redefining these constants.

Issue: If we later stabilize feature(generic_const_exprs) we need ADD<N, 1> to unify with N + 1. Otherwise we wouldn't be able to update the std api.

Solution: Add an attribute, e.g. #[transparent_const], which, when applied to named constants, causes rustc to "unwrap" that constant when used in types. With this #[transparent_const] const ADD<const N: usize, const M: usize>: usize = N + M unifies with addition.

Issue: Unifying associated constants with other constants, consider the following example

trait ReturnsArray {
    const RETURN_LENGTH<const N: usize>;
    fn foo<const N: usize>(arr: [u8; N]) -> [u8; RETURN_LENGTH::<N>];
}

impl ReturnsArray for () {
    const RETURN_LENGTH<const N: usize> = ADD<N, 1>;
    fn foo<const N: usize>(arr: [u8; N]) -> [u8; RETURN_LENGTH::<N>] {
        // `ADD<N, 1>` and `RETURN_LENGTH::<N>` don't trivially unify.
        // so this would error.
        arr.push(0); 
    }
}

Solution we probably again need something like#[transparent_const] or even do this implicitly if the definition of an associated constant is another associated consts or sth.