Rust: Tracking issue for `&mut T` in const fn

Created on 5 Jan 2019  路  20Comments  路  Source: rust-lang/rust

Basically, because &mut references are unique in rust, mutations (except through interior mutability) are not actually observable. Therefore, const fn can support mutation. i.e.:

  • &mut arguments could be accepted
  • locals could be mutably borrowed (&mut expressions)
  • non-initialization assignments could be made (x = y; or *x = y;).

The const_mut_refs feature gate allows all of the above in const fn, but not in other const items. Allowing mutable references outside of const fn is tracked in https://github.com/rust-lang/rust/issues/71212

A-const-eval A-const-fn B-unstable C-tracking-issue F-const_mut_refs T-lang

Most helpful comment

I'm nominating this for @rust-lang/lang consideration for stabilization. This feature gate allows &mut references in arguments, return values and local variables in const fn. They are not permitted in any other const context, so you can't use mutable references in const items, static items, array length initializers, const generics or enum discriminant initializers. The reason is that we aren't sure how to enforce that no mutable references escape to the final value of the constant. With const fn there's no danger of this, because the borrow checker forbids returning mutable references to local variables. Any mutable references in the arguments that could escape to the return type must by construction be created within another const fn, as they can't even be created in any other context.

Examples:

const fn foo<'a>(x: &'a mut i32) -> &'a mut i32 {
     let mut y = 42;
     let z = &mut y;
     *z = 99;
     x
}

is legal, but

const FOO: () = {
     let mut y = 42;
     let z = &mut y;
     *z = 99;
};

is not, because we want to prevent

const FOO: &mut i32 = &mut 42;

and the easiest way to do this is to forbid mutable references. #71212 is tracking adding mutable references to const contexts other than const fn again, but that's actually not terribly important compared with all the use cases for mutable references in const fn.

Said uses cases being several of the issues that link to this issue (scroll up) and more concretely:

  • MaybeUninit::zeroed (https://github.com/rust-lang/rust/issues/62061)
  • std::mem::replace
  • User functions which currently have to pass an argument and returning it again in order to mutate a value via a function call

There are no open questions known to me (beyond the use of &mut outside of const fn) and the implementation in the miri engine is heavily tested in the miri tool since the early days of miri.

cc @rust-lang/wg-const-eval

All 20 comments

Thanks for filing this issue. :)

  • non-initialization assignments could be made (x = y; or *x = y;).

The first half of this, i.e. x = y, is covered by https://github.com/rust-lang/rust/issues/48821 which is being stabilized in https://github.com/rust-lang/rust/pull/57175.

I could be wrong, but based on previous conversation with @eddyb, this is very much in the plans. They originally wanted to even allow CTFE to mutate statics during the scope of their execution, until it was discussed out that this is undesirable because it would allow a single function call to behave differently depending on whether or not CTFE was used to precompute the function.

Please cc @oli-obk and/or @RalfJung on all CTFE-related things.

In terms of the implementation, we plan to allow all pointers where the memory they point to is on the CTFE "stack" (i.e. not a static) and maybe "heap" in the future, with maybe some limitations around const fn itself.

But miri can already interpret more than everything you'd reasonably want to run in CTFE.

I think this is really useful.

i.e. pre-seeding a hash function with a context specific data:

pub const fn context_hash(context: &'static [u8]) -> Sha256 {
    let mut h = Sha256::new();
    h.input(context);
    let res = h.result_reset();
    h.input(&res);
    h.input(&res);
    h
}

This can give a good performance boost in some applications(don't need to do the context hashing every time you hash something)

Generally, const function must not have side effects, function to stay pure(what equals) must:

  1. Do not change anything, references from outside world point to.
  2. Do not use static values as they can be changed.
  3. Do not change anything inside types providing interior mutability.

&mut to something can be used as usize for arithmetic, so in this case it will not cause side effect.

In practice, we can allow some cases, where we can mutate some constant at time of evaluating another, the problem is order of evaluating constants as well as syntax like const mut(To mark mutable things inside const context) and co (we won't violate mutability rules, right?).
Const function with side effect will be much more difficult to implement, and, moreover, syntax and ordering is not clear.

Should this issue be blocking const EMPTY_MAX: &'static mut [()] = &mut [(); ::std::usize::MAX]? Currently it does, but I think we should be able to allow this (as well as = &mut []; for a slice of any type).

@cramertj That (&'static mut [T; 0]) is a silly artificial limitation AFAIK, with a FIXME in the code, we just never fixed it (it's limited to runtime fns, IIRC) - feel free to submit a PR and maybe we can FCP that or w/e. This issue is about non-'static &mut in CTFE.

&'static mut T where T is a ZST is an orthogonal extension of that, which we even used to have to some extent, but we got rid of it when we RFC'd/stabilized rvalue-to-'static promotion.

I'm not sure we have any issue for it, maybe you can create one? It would also help to provide realistic examples for ZST expressions you want to get a &'static mut out of (e.g. [(); n] vs [Struct {...}; n] vs [Foo::new(); n] - the last one might need to use actual layout_of which may be more controversial).

[Foo::new(); n]

No please no function calls in implicit promotion.

Ah fair, I was thinking more from the fact that the value doesn't really matter if it's a ZST, but if there are preconditions (e.g. assertions) they would need to run first anyway.

I'm nominating this for @rust-lang/lang consideration for stabilization. This feature gate allows &mut references in arguments, return values and local variables in const fn. They are not permitted in any other const context, so you can't use mutable references in const items, static items, array length initializers, const generics or enum discriminant initializers. The reason is that we aren't sure how to enforce that no mutable references escape to the final value of the constant. With const fn there's no danger of this, because the borrow checker forbids returning mutable references to local variables. Any mutable references in the arguments that could escape to the return type must by construction be created within another const fn, as they can't even be created in any other context.

Examples:

const fn foo<'a>(x: &'a mut i32) -> &'a mut i32 {
     let mut y = 42;
     let z = &mut y;
     *z = 99;
     x
}

is legal, but

const FOO: () = {
     let mut y = 42;
     let z = &mut y;
     *z = 99;
};

is not, because we want to prevent

const FOO: &mut i32 = &mut 42;

and the easiest way to do this is to forbid mutable references. #71212 is tracking adding mutable references to const contexts other than const fn again, but that's actually not terribly important compared with all the use cases for mutable references in const fn.

Said uses cases being several of the issues that link to this issue (scroll up) and more concretely:

  • MaybeUninit::zeroed (https://github.com/rust-lang/rust/issues/62061)
  • std::mem::replace
  • User functions which currently have to pass an argument and returning it again in order to mutate a value via a function call

There are no open questions known to me (beyond the use of &mut outside of const fn) and the implementation in the miri engine is heavily tested in the miri tool since the early days of miri.

cc @rust-lang/wg-const-eval

You can use mutable references in const items with just this by putting your expression into a const function and then calling the function. I even made a macro_rules macro that can do it for you (playground)

macro_rules! mut_in_const_item {
    (const $id:ident: $ty:ty = $e:expr) => {
        const $id: $ty = {
            const fn __mut_in_const_item() -> $ty {
                $e
            }

            __mut_in_const_item()
        };
    }
}

Is there a reason the compiler can't do this for other const contexts? Should this question be asked in a different place?

Should this question be asked in a different place?

yes :laughing: in https://github.com/rust-lang/rust/issues/71212

We can do an analysis that figures out that using &mut is fine, we just need to implement it, #71212 tracks that.

I have two questions:

  1. Can you estimate the work required for #71212?
  2. Are there other cases where things are allowed in const fn but not other consts right now?

Are there other cases where things are allowed in const fn but not other consts right now?

No, there is no precedent for this. We have other differences between different const contexts though (e.g. constants cannot even mention statics, but statics can mention other statics).

  1. Can you estimate the work required for #71212?

@ecstatic-morse is the expert on dataflow, maybe they know what needs to be done to exclude any mutable references in the final expression of a constant. We're already forbidding

const FOO: &i32 = {
    let x = 42;
    &x
};

so all we need to figure out is how to forbid

const FOO: &i32 = {
    let x = 2;
    &mut (1 + x)
};

because

const FOO: &i32 = {
    let x = 2;
    &(1 + x)
};

is ok.

Why is it a problem if the type of the constant contains no mutable references (as in the example you wish to forbid)?

The mutability of the thing could be hidden. Take for example a user-written wrapper that emulates mutable references:

pub struct MutRef<T>(*mut T);

impl<T> MutRef<T> {
    pub const fn new(x: &'static mut T) -> Self {
        Self(x)
    }
    pub fn into_mut_ref(self) -> &'static mut T {
        unsafe {
            &mut *self.0
        }
    }
}

This would allow you to create a

const FOO: MutRef<i32> = MutRef::new(&mut 42);

and then call FOO.into_mut_ref(), because constants aren't moved out of, they are implicitly copied even if they are !Copy.

Unnominating, let's give mutable references outside const fn another try. I'll post an idea in #71212

Sometime between nightly-2020-06-03 and nightly-2020-10-25 the compiler changed such that code that previously worked with only a const_fn feature now also requires const_mut_refs (see https://github.com/tock/tock/pull/2175). A minimal example is here:

pub struct Foo {
    bar: Cell<Option<&'static mut u32>>,
}

impl Foo {
    pub const fn new() -> Self {
        Self {
            bar: Cell::new(None)
        }
    }
}

This limitation feels really artificial, because there is not actually a mutable reference being used in const context. I assume it is just difficult to implement the const-mutable-reference-checking in such a way that this is allowable?

Sometime between nightly-2020-06-03 and nightly-2020-10-25 the compiler changed such that code that previously worked with only a const_fn feature now also requires const_mut_refs (see tock/tock#2175)

Yes, there is work going on towards removing the general const_fn feature and having more specific sub-features instead. In fact, just the const_mut_refs feature should be enough.

So, there is not really a new restriction; this is just the old restriction under a more specific name.

Adding to that, the limitation is indeed artificial, because we rather implemented a conservative check than accidentally allow something unsound. We have just recently made some changes that should allow us to try to tackle mutable references in constant items again.

Was this page helpful?
0 / 5 - 0 ratings