RFC 0911: const-fn

lang (typesystem | machine | const-eval)

Summary

Allow marking free functions and inherent methods as const, enabling them to be called in constants contexts, with constant arguments.

Motivation

As it is right now, UnsafeCell is a stabilization and safety hazard: the field it is supposed to be wrapping is public. This is only done out of the necessity to initialize static items containing atomics, mutexes, etc. - for example:

#[lang="unsafe_cell"]
struct UnsafeCell<T> { pub value: T }
struct AtomicUsize { v: UnsafeCell<usize> }
const ATOMIC_USIZE_INIT: AtomicUsize = AtomicUsize {
    v: UnsafeCell { value: 0 }
};

This approach is fragile and doesn't compose well - consider having to initialize an AtomicUsize static with usize::MAX - you would need a const for each possible value.

Also, types like AtomicPtr<T> or Cell<T> have no way at all to initialize them in constant contexts, leading to overuse of UnsafeCell or static mut, disregarding type safety and proper abstractions.

During implementation, the worst offender I've found was std::thread_local: all the fields of std::thread_local::imp::Key are public, so they can be filled in by a macro - and they're also marked "stable" (due to the lack of stability hygiene in macros).

A pre-RFC for the removal of the dangerous (and often misused) static mut received positive feedback, but only under the condition that abstractions could be created and used in const and static items.

Another concern is the ability to use certain intrinsics, like size_of, inside constant expressions, including fixed-length array types. Unlike keyword-based alternatives, const fn provides an extensible and composable building block for such features.

The design should be as simple as it can be, while keeping enough functionality to solve the issues mentioned above.

The intention of this RFC is to introduce a minimal change that enables safe abstraction resembling the kind of code that one writes outside of a constant. Compile-time pure constants (the existing const items) with added parametrization over types and values (arguments) should suffice.

This RFC explicitly does not introduce a general CTFE mechanism. In particular, conditional branching and virtual dispatch are still not supported in constant expressions, which imposes a severe limitation on what one can express.

Detailed design

Functions and inherent methods can be marked as const:

const fn foo(x: T, y: U) -> Foo {
    stmts;
    expr
}
impl Foo {
    const fn new(x: T) -> Foo {
        stmts;
        expr
    }

    const fn transform(self, y: U) -> Foo {
        stmts;
        expr
    }
}

Traits, trait implementations and their methods cannot be const - this allows us to properly design a constness/CTFE system that interacts well with traits - for more details, see Alternatives.

Only simple by-value bindings are allowed in arguments, e.g. x: T. While by-ref bindings and destructuring can be supported, they're not necessary and they would only complicate the implementation.

The body of the function is checked as if it were a block inside a const:

const FOO: Foo = {
    // Currently, only item "statements" are allowed here.
    stmts;
    // The function's arguments and constant expressions can be freely combined.
    expr
}

As the current const items are not formally specified (yet), there is a need to expand on the rules for const values (pure compile-time constants), instead of leaving them implicit:

For the purpose of rvalue promotion (to static memory), arguments are considered potentially varying, because the function can still be called with non-constant values at runtime.

const functions and methods can be called from any constant expression:

// Standalone example.
struct Point { x: i32, y: i32 }

impl Point {
    const fn new(x: i32, y: i32) -> Point {
        Point { x: x, y: y }
    }

    const fn add(self, other: Point) -> Point {
        Point::new(self.x + other.x, self.y + other.y)
    }
}

const ORIGIN: Point = Point::new(0, 0);

const fn sum_test(xs: [Point; 3]) -> Point {
    xs[0].add(xs[1]).add(xs[2])
}

const A: Point = Point::new(1, 0);
const B: Point = Point::new(0, 1);
const C: Point = A.add(B);
const D: Point = sum_test([A, B, C]);

// Assuming the Foo::new methods used here are const.
static FLAG: AtomicBool = AtomicBool::new(true);
static COUNTDOWN: AtomicUsize = AtomicUsize::new(10);
#[thread_local]
static TLS_COUNTER: Cell<u32> = Cell::new(1);

Type parameters and their bounds are not restricted, though trait methods cannot be called, as they are never const in this design. Accessing trait methods can still be useful - for example, they can be turned into function pointers:

const fn arithmetic_ops<T: Int>() -> [fn(T, T) -> T; 4] {
    [Add::add, Sub::sub, Mul::mul, Div::div]
}

const functions can also be unsafe, allowing construction of types that require invariants to be maintained (e.g. std::ptr::Unique requires a non-null pointer)

struct OptionalInt(u32);
impl OptionalInt {
    /// Value must be non-zero
    const unsafe fn new(val: u32) -> OptionalInt {
        OptionalInt(val)
    }
}

Drawbacks

Alternatives

const fn map_vec3<T: Copy, F: const Fn(T) -> T>(xs: [T; 3], f: F) -> [T; 3] {
    [f([xs[0]), f([xs[1]), f([xs[2])]
}

const fn neg_vec3<T: Copy + const Neg>(xs: [T; 3]) -> [T; 3] {
    map_vec3(xs, |x| -x)
}

const impl Add for Point {
    fn add(self, other: Point) -> Point {
        Point {
            x: self.x + other.x,
            y: self.y + other.y
        }
    }
}

Having const trait methods (where all implementations are const) seems useful, but it would not allow the usecase above on its own. Trait implementations with const methods (instead of the entire impl being const) would allow direct calls, but it's not obvious how one could write a function generic over a type which implements a trait and requiring that a certain method of that trait is implemented as const.

Unresolved questions

History

Updates since being accepted

Since it was accepted, the RFC has been updated as follows:

  1. Allowed const unsafe fn