Skip to content

Latest commit

 

History

History
435 lines (371 loc) · 18.3 KB

File metadata and controls

435 lines (371 loc) · 18.3 KB

r[const-eval]

Constant evaluation

r[const-eval.intro] Constant evaluation is the process of computing the result of expressions during compilation. Only a subset of all expressions can be evaluated at compile-time.

r[const-eval.const-expr]

Constant expressions

r[const-eval.const-expr.intro] Certain forms of expressions, called constant expressions, can be evaluated at compile time.

r[const-eval.const-expr.const-context] Expressions in a const context must be constant expressions.

r[const-eval.const-expr.evaluation] Expressions in const contexts are always evaluated at compile time.

r[const-eval.const-expr.runtime-context] Outside of const contexts, constant expressions may be, but are not guaranteed to be, evaluated at compile time.

r[const-eval.const-expr.error] Behaviors such as out of bounds array indexing or overflow are compiler errors if the value must be evaluated at compile time (i.e. in const contexts). Otherwise, these behaviors are warnings, but will likely panic at run-time.

r[const-eval.const-expr.list] The following expressions are constant expressions, so long as any operands are also constant expressions and do not cause any Drop::drop calls to be run.

r[const-eval.const-expr.literal]

r[const-eval.const-expr.parameter]

r[const-eval.const-expr.path-item]

r[const-eval.const-expr.path-static]

  • Paths to statics with these restrictions:

    • Writes to static items are not allowed in any constant evaluation context.
    • Reads from extern statics are not allowed in any constant evaluation context.
    • If the evaluation is not carried out in an initializer of a static item, then reads from any mutable static are not allowed. A mutable static is a static mut item, or a static item with an interior-mutable type.

    These requirements are checked only when the constant is evaluated. In other words, having such accesses syntactically occur in const contexts is allowed as long as they never get executed.

r[const-eval.const-expr.tuple]

r[const-eval.const-expr.array]

r[const-eval.const-expr.constructor]

r[const-eval.const-expr.block]

r[const-eval.const-expr.field]

r[const-eval.const-expr.index]

r[const-eval.const-expr.range]

r[const-eval.const-expr.closure]

r[const-eval.const-expr.builtin-arith-logic]

r[const-eval.const-expr.borrows]

  • All forms of borrows, including raw borrows, except borrows of expressions whose temporary scopes would be extended (see temporary lifetime extension) to the end of the program and which are either:

    • Mutable borrows.
    • Shared borrows of expressions that result in values with interior mutability.
    // Due to being in tail position, this borrow extends the scope of the
    // temporary to the end of the program. Since the borrow is mutable,
    // this is not allowed in a const expression.
    const C: &u8 = &mut 0; // ERROR not allowed
    // Const blocks are similar to initializers of `const` items.
    let _: &u8 = const { &mut 0 }; // ERROR not allowed
    # use core::sync::atomic::AtomicU8;
    // This is not allowed as 1) the temporary scope is extended to the
    // end of the program and 2) the temporary has interior mutability.
    const C: &AtomicU8 = &AtomicU8::new(0); // ERROR not allowed
    # use core::sync::atomic::AtomicU8;
    // As above.
    let _: &_ = const { &AtomicU8::new(0) }; // ERROR not allowed
    # #![allow(static_mut_refs)]
    // Even though this borrow is mutable, it's not of a temporary, so
    // this is allowed.
    const C: &u8 = unsafe { static mut S: u8 = 0; &mut S }; // OK
    # use core::sync::atomic::AtomicU8;
    // Even though this borrow is of a value with interior mutability,
    // it's not of a temporary, so this is allowed.
    const C: &AtomicU8 = {
        static S: AtomicU8 = AtomicU8::new(0); &S // OK
    };
    # use core::sync::atomic::AtomicU8;
    // This shared borrow of an interior mutable temporary is allowed
    // because its scope is not extended.
    const C: () = { _ = &AtomicU8::new(0); }; // OK
    // Even though the borrow is mutable and the temporary lives to the
    // end of the program due to promotion, this is allowed because the
    // borrow is not in tail position and so the scope of the temporary
    // is not extended via temporary lifetime extension.
    const C: () = { let _: &'static mut [u8] = &mut []; }; // OK
    //                                              ~~
    //                                     Promoted temporary.

    [!NOTE] In other words --- to focus on what's allowed rather than what's not allowed --- shared borrows of interior mutable data and mutable borrows are only allowed in a const context when the borrowed place expression is transient, indirect, or static.

    A place expression is transient if it is a variable local to the current const context or an expression whose temporary scope is contained inside the current const context.

    // The borrow is of a variable local to the initializer, therefore
    // this place expression is transient.
    const C: () = { let mut x = 0; _ = &mut x; };
    // The borrow is of a temporary whose scope has not been extended,
    // therefore this place expression is transient.
    const C: () = { _ = &mut 0u8; };
    // When a temporary is promoted but not lifetime extended, its
    // place expression is still treated as transient.
    const C: () = { let _: &'static mut [u8] = &mut []; };

    A place expression is indirect if it is a dereference expression.

    const C: () = { _ = &mut *(&mut 0); };

    A place expression is static if it is a static item.

    # #![allow(static_mut_refs)]
    const C: &u8 = unsafe { static mut S: u8 = 0; &mut S };

    [!NOTE] One surprising consequence of these rules is that we allow this,

    const C: &[u8] = { let x: &mut [u8] = &mut []; x }; // OK
    //                                    ~~~~~~~
    // Empty arrays are promoted even behind mutable borrows.

    but we disallow this similar code:

    const C: &[u8] = &mut []; // ERROR
    //               ~~~~~~~
    //           Tail expression.

    The difference between these is that, in the first, the empty array is promoted but its scope does not undergo temporary lifetime extension, so we consider the place expression to be transient (even though after promotion the place indeed lives to the end of the program). In the second, the scope of the empty array temporary does undergo lifetime extension, and so it is rejected due to being a mutable borrow of a lifetime-extended temporary (and therefore borrowing a non-transient place expression).

    The effect is surprising because temporary lifetime extension, in this case, causes less code to compile than would without it.

    See issue #143129 for more details.

r[const-eval.const-expr.deref]

  • Dereference expressions.

    # use core::cell::UnsafeCell;
    const _: u8 = unsafe {
        let x: *mut u8 = &raw mut *&mut 0;
        //                        ^^^^^^^
        //             Dereference of mutable reference.
        *x = 1; // Dereference of mutable pointer.
        *(x as *const u8) // Dereference of constant pointer.
    };
    const _: u8 = unsafe {
        let x = &UnsafeCell::new(0);
        *x.get() = 1; // Mutation of interior mutable value.
        *x.get()
    };

r[const-eval.const-expr.group]

r[const-eval.const-expr.cast]

  • Cast expressions, except
    • pointer to address casts and
    • function pointer to address casts.

r[const-eval.const-expr.const-fn]

r[const-eval.const-expr.loop]

r[const-eval.const-expr.if-match]

Const initializers

r[const-eval.const-expr.final-value-provenance] The representation of the final value of a constant or static initializer must only contain bytes with provenance in whole-pointer groups. If a byte has provenance but is not part of an adjacent group of correctly-ordered bytes that form an entire pointer, compilation will fail.

# use core::mem::MaybeUninit;
#
#[repr(C, align(32))]
struct Pair {
    x: u128,             // Offset  0, 16 bytes.
    y: MaybeUninit<u64>, // Offset 16,  8 bytes.
                         // Offset 24,  8 bytes of padding.
}

const _: Pair = unsafe {
//    ^^^^^^^ ERROR: Partial pointer in final value of constant.
    let mut m = MaybeUninit::<Pair>::uninit();
    // Store pointer that extends halfway into trailing padding.
    m.as_mut_ptr().byte_add(20).cast::<&u8>().write_unaligned(&0);
    // Initialize fields.
    (*m.as_mut_ptr()).x = 0;
    (*m.as_mut_ptr()).y = MaybeUninit::new(0);
    // Now `m` contains a pointer fragment in the padding.
    m.assume_init()
};

Note

The bytes with provenance must form a complete pointer in the correct order. In the example above, the pointer is written at offset 20, but a pointer requires 8 bytes (assuming an 8-byte pointer). Four of those bytes fit in the y field; the rest extend into the padding at offset 24. When the fields are initialized, the y bytes get overwritten, leaving only a partial pointer (4 bytes) in the padding. These 4 bytes have provenance but don't form a complete pointer, causing compilation to fail.

This restriction ensures that any bytes with provenance in the final value represent complete pointers. Binary formats such as ELF cannot represent pointer fragments, so the compiler cannot emit them in the final binary.

Reversing the order of the pointer bytes also causes compilation to fail, even though all bytes are present:

# use core::mem::{self, MaybeUninit};
const PTR_WIDTH: usize = mem::size_of::<*const u8>();
const _: MaybeUninit<u64> = unsafe {
//       ^^^^^^^^^^^^^^^^
//       ERROR: Partial pointer in final value of constant.
    let mut m = MaybeUninit::<u64>::uninit();
    let ptr: *const u8 = &0;
    let ptr_bytes = &raw const ptr as *const MaybeUninit<u8>;
    // Write pointer bytes in reverse order.
    let dst: *mut MaybeUninit<u8> = m.as_mut_ptr().cast();
    let mut i = 0;
    while i < PTR_WIDTH {
        dst.add(i).write(ptr_bytes.add(PTR_WIDTH - 1 - i).read());
        i += 1;
    }
    m
};

Note

If a byte in the representation of the final value is uninitialized, then it may end up having provenance, which can cause compilation to fail. rustc tries (but does not guarantee) to only actually fail if the initializer copies or overwrites parts of a pointer and those bytes appear in the final value.

E.g., rustc currently accepts this, even though the padding bytes are uninitialized:

#[repr(C, align(32))]
struct Pair { x: u128, y: u64 }
// The padding bytes are uninitialized.
const _: Pair = Pair { x: 0, y: 0 }; // OK.

Constant evaluation makes the details of typed copies observable: depending on whether a copy is performed field-by-field or as a memory-block copy, provenance in padding bytes might be discarded or preserved (both in the source and in the destination). The language allows the compiler to reject any final initializer value with an uninitialized padding byte to preserve implementation flexibility (e.g., the compiler may in the future always set padding bytes to uninitialized).

Manually initializing (e.g., zeroing) the padding bytes ensures the final value is accepted:

# use std::mem::MaybeUninit;
# #[repr(C, align(32))]
# struct Pair { x: u128, y: u64 }
const _: Pair = unsafe { // OK.
    let mut m = Pair { x: 0, y: 0 };
    # // Trigger failure if padding were not zeroed.
    # let ptr: *const u8 = &0;
    # let ptr_bytes = &raw const ptr as *const MaybeUninit<u8>;
    # let dst: *mut MaybeUninit<u8> = (&raw mut m).byte_add(31).cast();
    # dst.write(ptr_bytes.read());
    // Explicitly zero the padding.
    (&raw mut m).byte_add(24).cast::<u64>().write_unaligned(0);
    m
};

r[const-eval.const-context]

Const context

r[const-eval.const-context.def] A const context is one of the following:

r[const-eval.const-context.array-length]

r[const-eval.const-context.repeat-length]

r[const-eval.const-context.init]

r[const-eval.const-context.generic]

r[const-eval.const-context.block]

r[const-eval.const-context.outer-generics] Const contexts that are used as parts of types (array type and repeat length expressions as well as const generic arguments) can only make restricted use of surrounding generic parameters: such an expression must either be a single bare const generic parameter, or an arbitrary expression not making use of any generics.

r[const-eval.const-fn]

Const functions

r[const-eval.const-fn.intro] A const function is a function that can be called from a const context. It is defined with the const qualifier, and also includes tuple struct and tuple enum variant constructors.

[!EXAMPLE]

const fn square(x: i32) -> i32 { x * x }

const VALUE: i32 = square(12);

r[const-eval.const-fn.const-context] When called from a const context, a const function is interpreted by the compiler at compile time. The interpretation happens in the environment of the compilation target and not the host. So usize is 32 bits if you are compiling against a 32 bit system, irrelevant of whether you are building on a 64 bit or a 32 bit system.

r[const-eval.const-fn.outside-context] When a const function is called from outside a const context, it behaves the same as if it did not have the const qualifier.

r[const-eval.const-fn.body-restriction] The body of a const function may only use constant expressions.

r[const-eval.const-fn.async] Const functions are not allowed to be async.

r[const-eval.const-fn.type-restrictions] The types of a const function's parameters and return type are restricted to those that are compatible with a const context.