Skip to content

Never allow const fn calls in promoteds #19

Closed
@oli-obk

Description

@oli-obk

I want to reiterate on the discussion about const fn calls to promoteds. During the stabilization of min_const_fn, we recently punted on the topic by not promoting any const fn calls, except a specific list of "known to be safe" functions like std::mem::size_of.

I want to argue for forbidding the promotion of const fn calls forever (leaving in the existing escape hatch for the libstd is not very expensive, and we may just want to start linting about it for a few years and remove it at some point).

I show how three future extensions to the const evaluator are either incompatible with promoting const fn calls or increase the compiler complexity, suprises for users and general lanugage complexity.

panicky code in const fn

The function foo, defined as

const fn foo() { [()][42] }

will panic when executed. If used in a promoted, e.g.

let x: &'static () = &foo();

this would create a compile-time error about a panic reached during const eval. Unfortunately that means that

let x = &foo();

would also cause this compile-time error, even though the user has no reason to suspect this to not compile.

While we can demote such compile-time errors to panics by compiling them to a call to the panic machinery with the original message, this would break the backtrace and would generally require some rather complex code to support this pretty useless case.

heap allocations in const fn

The function foo, defined as

const fn foo() -> *mut u32 { Box::leak(Box::new(42)) }

will create a heap allocation if executed. When evaluated during const eval, it will not create a heap allocation, but sort of create a new unnamed static item for every call.

This means that

const FOO: *mut u32 = foo();

would be perfectly legal code. When used in a promoted,

fn bar() {
    let x: &'static *mut u32 = &foo()
}

we'd also not create a heap allocation, but rather another unnamed static.

This also means that

fn bar() {
    let x: *mut u32 = *&foo();
}

would do that, even though the user expects that to produce a new heap allocation, place it in a temporary and return a reference to it. In reality there would be only one single allocation. The user might even be tempted to mutate memory via that *mut u32, which would definitely be UB. While I don't expect the above code to be written, I imagine that there are many ways for accidentally sneaking a & operator into some expression that will end up causing a promotion to happen.

Defined runtime code can be promoted and suddenly stop compiling

const fn eq(x: &[u32], y: &[u32]) -> bool {
    if x.len() != y.len() {
        return false;
    }
    if x.as_ptr() == y.as_ptr() {
        return true;
    }
    x.iter().eq(y.iter())
}

Will work just fine at compile-time, but at runtime, comparing pointers to different allocations can be problematic. While this case is deterministic between compile-time and runtime due to the "check every element" fallback,

const fn foo(x: &u32, y: &u32) -> bool {
    x.as_ptr() == y.as_ptr()
}

depends on its arguments. E.g. foo(&42, &42) may return false at compile-time and true at runtime due to deduplication in llvm.

If we extend this to promotion,

let x = &foo(&42, &42);

will get evaluated at compile-time, but have a result different from runtime, even though the user did not request that.

Conclusion

Since I presume that we'll want all of the above const eval features (we even support the first one on stable rustc) at some point or another, I suggest that we never allow promoting const fn calls without an explicit request for constness, e.g. by inserting a constant and promoting a use of that constant, or by a future extension to the language for unnamed constant expressions.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions