Change:
On the last Nightly:
Reduced to:
Mir of with : Mir of with : and got removed while is still assigned and used. Also the fields of now get and assigned.
The are a couple of things going on here: The immediate cause is the fact that we . This means that never gets evaluated, leaving uninitialized. Normally, this would not be a problem - const-prop explicitly when trying to access an uninitialized local, so failing to evaluate should just mean that we don't const-propagate anything that relies on . Unfortunately, it's possible for an uninitialized local to be incorrectly marked as initialized. Inside , we , even if it was previously uninitialied. When processing an , , which will incorrectly mark an uninitialized source as initialized. While implementing const-propagation for would fix this specific issue, the issue means that we risk introducing undefined behavior into valid programs whenever const-propagation doesn't evaluate something that would initialize a local. I see a few ways different of solving this: Change to . When is called on an uninitialized local, we keep the local as a , but would store in it. This would ensure that has no user-visible effect - uninitialized locals will remain uninitialized until actually written to. Make aware of whether not or not it is being used for 'reading' or 'writing'. When 'reading', we call before overwriting a local with , allowing the machine to assert that it is valid. When 'writing', we skip calling , allowing to be used when initializing a previously uninitialized local (which does not require that the target local be initialized). cc
Without having digested all the details yet, my first thought is: this is caused by const-prop trying to shoehorn the Miri engine (built for evaluation of whole programs) into an engine for partial evaluation. The was never meant for tracking things like "unkown value", it is a hack introduced for handling unsized locals as I didn't find a better way to implement them. It's meaning is "a local whose size we don't know yet". can determine the size, and thus complete allocation of the local. In terms of the Rust Abstract Machine (which the Miri engine models), I don't know what an "uninitialized local" is -- but a local that's marked but whose memory is entirely uninitialized should certainly qualify. Thus I would say that correctly preserves the uninitialized nature of the local. The issue is that const-prop is taking the difference between and , which is an implementation detail of the engine (but doesn't correspond to anything real, it's basically just an optimization), and tries to assign meaning to it. No wonder this went wrong. What exactly is it that const-prop needs?
Cc
Zero cost uninitialized locals is the base feature we need. Essentially if we decide we don't need a local, reading its memory should yield an uninit value and its memory should not require any heap allocations. I get the shoehorning, we so are doing this a lot in const prop, mainly to keep the engine modifications minimal. I guess we could add some more machine hooks to get rid of hacks.
Even with , reading it will yield an uninit value. So not requiring heap allocations is a correctness condition? That's what analysis says, basically.
Well... it shouldn't be. The check is actually just a check ensuring we don't ICE for unsized values. This is not worded correctly. The uninitialized local is marked as live its memory is still uninitialized. But that doesn't change the fact that the analysis is spot on. We take references to locals that may never get a value propagated into. The early bail out in is just so we don't hit the panic in it's also not for correctness. Yea, so basically whenever we don't fully propagate into a local, but later take a reference to that local, we end up with a const pointer to an uninitialized const allocation. Basically unless the local's memory is valid (as per the validation rules for statics), we should not permit taking references to it.
Doesn't this also affect normal uses of an un-propagated local? We'll end up copying data from an uninitialized allocation, which will result in the same issue.
Yes, any local that we don't fully propagate into but that is referred to by a local that we marked as "could be propagated".
I'd say it's less about validity and more about whether we actually did all the computation for this local. That's not the same thing. Basically, const-prop needs a state representing "unknown". This is distinct from "(known to be) uninitialized". And that's not surprising at all, static analyses and optimizations almost always have such a state. This is a first step towards supporting abstract interpretation in Miri.
We inverted this by treating any initialized values as "!unknown". The issue here is that taking a reference to a value works even if the value is not fully initialized. We have no concept of fully initialized, and in case the local is of union type, we can't rely on the uninitialized state to signal "maybe unknown". So yea, the validity check won't work here either. and I talked about using dataflow for const prop, so we'd mark locals only as known when they have been successfully propagated into instead of relying on the initialization state. Until we do this we should probably just turn off propagating reference taking.
I agree disabling ref propagation makes sense. We have quite a few bugs that I believe would be resolved by that. In addition, I believe there's only 1 or 2 MIR tests that would be affected. Going forward, perhaps we should try to find some way to get more coverage of mir-opt=2?
The other problem is (as you hinted at) that things, at least in theory, might be "partially known", such as in where is, say, a pair: now the first field is known but the second is not.
A general worry about this is how our const-prop looks not at all like the usual "compiler construction literature const-prop": the usual thing is to do a fixed-point (static) analysis on a domain including all possible values as well as "unknown". That could also handle loops, which I suppose you currently have to carefully avoid. It is certainly possible to implement constant propagation differently, but that means none of the standard intuition about it applies.

Discussion:
self.check_binary_op(*op, left, right, source_info, place_layout, overflow_check)?; } <del> // Work around: avoid ICE in miri. FIXME(wesleywiser) // The Miri engine ICEs when taking a reference to an uninitialized unsized // local. There's nothing it can do here: taking a reference needs an allocation // which needs to know the size. Normally that's okay as during execution // (e.g. for CTFE) it can never happen. But here in const_prop // unknown data is uninitialized, so if e.g. a function argument is unsized // and has a reference taken, we get an ICE. </del> <ins> // Do not try creating references (#67862) </ins> Rvalue::Ref(_, _, place_ref) => { <del> trace!("checking Ref({:?})", place_ref); </del> <ins> trace!("skipping Ref({:?})", place_ref); </ins> <del> if let Some(local) = place_ref.as_local() { let alive = if let LocalValue::Live(_) = self.ecx.frame().locals[local].value { true } else { false }; if !alive { trace!("skipping Ref({:?}) to uninitialized local", place); return None; } } </del> <ins> return None; </ins> } _ => {}

fn main() { <del> *(&(4, 5).1); </del> <ins> *(&(4, 5).1); // This does not currently propagate (#67862) </ins> } // END RUST SOURCE

//     ... //     _4 = const main::promoted[0]; //     _2 = &((*_4).1: i32); <del> //     _1 = const 5i32; </del> <ins> //     _1 = (*_2); </ins> //     ... // } // END rustc.main.ConstProp.after.mir

<ins> // compile-flags: -Z mir-opt-level=2 // run-pass struct Baz<T: ?Sized> { a: T } fn main() { let d : Baz<[i32; 4]> = Baz { a: [1,2,3,4] }; assert_eq!([1, 2, 3, 4], d.a); } </ins>

<ins> // compile-flags: -Z mir-opt-level=3 // run-pass struct X { x: isize } fn f1(a: &mut X, b: &mut isize, c: isize) -> isize { let r = a.x + *b + c; a.x = 0; *b = 10; return r; } fn f2<F>(a: isize, f: F) -> isize where F: FnOnce(isize) { f(1); return a; } pub fn main() { let mut a = X {x: 1}; let mut b = 2; let c = 3; assert_eq!(f1(&mut a, &mut b, c), 6); assert_eq!(a.x, 0); assert_eq!(f2(a.x, |_| a.x = 50), 0); } </ins>

<ins> // compile-flags: -Z mir-opt-level=2 // run-pass use std::cell::Cell; #[derive(Debug)] struct B<'a> { a: [Cell<Option<&'a B<'a>>>; 2] } impl<'a> B<'a> { fn new() -> B<'a> { B { a: [Cell::new(None), Cell::new(None)] } } } fn f() { let b2 = B::new(); b2.a[0].set(Some(&b2)); } fn main() { f(); } </ins>

<ins> // compile-flags: -Z mir-opt-level=2 // run-pass fn e220() -> (i64, i64) { #[inline(never)] fn get_displacement() -> [i64; 2] { [139776, 963904] } let res = get_displacement(); match (&res[0], &res[1]) { (arg0, arg1) => (*arg0, *arg1), } } fn main() { assert_eq!(e220(), (139776, 963904)); } </ins>