about summary refs log tree commit diff
path: root/compiler/rustc_const_eval
diff options
context:
space:
mode:
authorRalf Jung <post@ralfj.de>2023-07-23 21:35:54 +0200
committerRalf Jung <post@ralfj.de>2023-07-24 15:35:47 +0200
commita593de4fab309968d305f9c6812c2203d4431464 (patch)
tree6a960abae1d1e4acef22c1ca10c30b98bdcff0d1 /compiler/rustc_const_eval
parent42f5419dd29046612138413cbde4567def218341 (diff)
downloadrust-a593de4fab309968d305f9c6812c2203d4431464.tar.gz
rust-a593de4fab309968d305f9c6812c2203d4431464.zip
interpret: support projecting into Place::Local without force_allocation
Diffstat (limited to 'compiler/rustc_const_eval')
-rw-r--r--compiler/rustc_const_eval/messages.ftl2
-rw-r--r--compiler/rustc_const_eval/src/errors.rs4
-rw-r--r--compiler/rustc_const_eval/src/interpret/eval_context.rs5
-rw-r--r--compiler/rustc_const_eval/src/interpret/operand.rs109
-rw-r--r--compiler/rustc_const_eval/src/interpret/place.rs225
-rw-r--r--compiler/rustc_const_eval/src/interpret/projection.rs284
-rw-r--r--compiler/rustc_const_eval/src/interpret/terminator.rs8
-rw-r--r--compiler/rustc_const_eval/src/interpret/visitor.rs18
8 files changed, 404 insertions, 251 deletions
diff --git a/compiler/rustc_const_eval/messages.ftl b/compiler/rustc_const_eval/messages.ftl
index d8eade5bd2a..86e47b2dbce 100644
--- a/compiler/rustc_const_eval/messages.ftl
+++ b/compiler/rustc_const_eval/messages.ftl
@@ -423,8 +423,6 @@ const_eval_uninit_int = {$front_matter}: encountered uninitialized memory, but e
 const_eval_uninit_raw_ptr = {$front_matter}: encountered uninitialized memory, but expected a raw pointer
 const_eval_uninit_ref = {$front_matter}: encountered uninitialized memory, but expected a reference
 const_eval_uninit_str = {$front_matter}: encountered uninitialized data in `str`
-const_eval_uninit_unsized_local =
-    unsized local is used while uninitialized
 const_eval_unreachable = entering unreachable code
 const_eval_unreachable_unwind =
     unwinding past a stack frame that does not allow unwinding
diff --git a/compiler/rustc_const_eval/src/errors.rs b/compiler/rustc_const_eval/src/errors.rs
index ca38cce710e..201b9425aff 100644
--- a/compiler/rustc_const_eval/src/errors.rs
+++ b/compiler/rustc_const_eval/src/errors.rs
@@ -835,7 +835,7 @@ impl<'tcx> ReportErrorExt for InvalidProgramInfo<'tcx> {
                 rustc_middle::error::middle_adjust_for_foreign_abi_error
             }
             InvalidProgramInfo::SizeOfUnsizedType(_) => const_eval_size_of_unsized,
-            InvalidProgramInfo::UninitUnsizedLocal => const_eval_uninit_unsized_local,
+            InvalidProgramInfo::ConstPropNonsense => panic!("We had const-prop nonsense, this should never be printed"),
         }
     }
     fn add_args<G: EmissionGuarantee>(
@@ -846,7 +846,7 @@ impl<'tcx> ReportErrorExt for InvalidProgramInfo<'tcx> {
         match self {
             InvalidProgramInfo::TooGeneric
             | InvalidProgramInfo::AlreadyReported(_)
-            | InvalidProgramInfo::UninitUnsizedLocal => {}
+            | InvalidProgramInfo::ConstPropNonsense => {}
             InvalidProgramInfo::Layout(e) => {
                 let diag: DiagnosticBuilder<'_, ()> = e.into_diagnostic().into_diagnostic(handler);
                 for (name, val) in diag.args() {
diff --git a/compiler/rustc_const_eval/src/interpret/eval_context.rs b/compiler/rustc_const_eval/src/interpret/eval_context.rs
index 04e046fbda3..0e6125388a6 100644
--- a/compiler/rustc_const_eval/src/interpret/eval_context.rs
+++ b/compiler/rustc_const_eval/src/interpret/eval_context.rs
@@ -1014,9 +1014,12 @@ impl<'a, 'mir, 'tcx: 'mir, M: Machine<'mir, 'tcx>> std::fmt::Debug
 {
     fn fmt(&self, fmt: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
         match self.place {
-            Place::Local { frame, local } => {
+            Place::Local { frame, local, offset } => {
                 let mut allocs = Vec::new();
                 write!(fmt, "{:?}", local)?;
+                if let Some(offset) = offset {
+                    write!(fmt, "+{:#x}", offset.bytes())?;
+                }
                 if frame != self.ecx.frame_idx() {
                     write!(fmt, " ({} frames up)", self.ecx.frame_idx() - frame)?;
                 }
diff --git a/compiler/rustc_const_eval/src/interpret/operand.rs b/compiler/rustc_const_eval/src/interpret/operand.rs
index 47383c0eabc..e6d0a73c521 100644
--- a/compiler/rustc_const_eval/src/interpret/operand.rs
+++ b/compiler/rustc_const_eval/src/interpret/operand.rs
@@ -13,7 +13,7 @@ use rustc_target::abi::{self, Abi, Align, HasDataLayout, Size};
 
 use super::{
     alloc_range, from_known_layout, mir_assign_valid_types, AllocId, ConstValue, Frame, GlobalId,
-    InterpCx, InterpResult, MPlaceTy, Machine, MemPlace, MemPlaceMeta, Place, PlaceTy, Pointer,
+    InterpCx, InterpResult, MPlaceTy, Machine, MemPlace, MemPlaceMeta, PlaceTy, Pointer,
     Provenance, Scalar,
 };
 
@@ -240,37 +240,69 @@ impl<'tcx, Prov: Provenance> ImmTy<'tcx, Prov> {
         let int = self.to_scalar().assert_int();
         ConstInt::new(int, self.layout.ty.is_signed(), self.layout.ty.is_ptr_sized_integral())
     }
+
+    /// Compute the "sub-immediate" that is located within the `base` at the given offset with the
+    /// given layout.
+    pub(super) fn offset(
+        &self,
+        offset: Size,
+        layout: TyAndLayout<'tcx>,
+        cx: &impl HasDataLayout,
+    ) -> Self {
+        // This makes several assumptions about what layouts we will encounter; we match what
+        // codegen does as good as we can (see `extract_field` in `rustc_codegen_ssa/src/mir/operand.rs`).
+        let inner_val: Immediate<_> = match (**self, self.layout.abi) {
+            // if the entire value is uninit, then so is the field (can happen in ConstProp)
+            (Immediate::Uninit, _) => Immediate::Uninit,
+            // the field contains no information, can be left uninit
+            _ if layout.is_zst() => Immediate::Uninit,
+            // the field covers the entire type
+            _ if layout.size == self.layout.size => {
+                assert!(match (self.layout.abi, layout.abi) {
+                    (Abi::Scalar(..), Abi::Scalar(..)) => true,
+                    (Abi::ScalarPair(..), Abi::ScalarPair(..)) => true,
+                    _ => false,
+                });
+                assert!(offset.bytes() == 0);
+                **self
+            }
+            // extract fields from types with `ScalarPair` ABI
+            (Immediate::ScalarPair(a_val, b_val), Abi::ScalarPair(a, b)) => {
+                assert!(matches!(layout.abi, Abi::Scalar(..)));
+                Immediate::from(if offset.bytes() == 0 {
+                    debug_assert_eq!(layout.size, a.size(cx));
+                    a_val
+                } else {
+                    debug_assert_eq!(offset, a.size(cx).align_to(b.align(cx).abi));
+                    debug_assert_eq!(layout.size, b.size(cx));
+                    b_val
+                })
+            }
+            // everything else is a bug
+            _ => bug!("invalid field access on immediate {}, layout {:#?}", self, self.layout),
+        };
+
+        ImmTy::from_immediate(inner_val, layout)
+    }
 }
 
 impl<'tcx, Prov: Provenance> OpTy<'tcx, Prov> {
-    pub fn len(&self, cx: &impl HasDataLayout) -> InterpResult<'tcx, u64> {
-        if self.layout.is_unsized() {
-            if matches!(self.op, Operand::Immediate(Immediate::Uninit)) {
-                // Uninit unsized places shouldn't occur. In the interpreter we have them
-                // temporarily for unsized arguments before their value is put in; in ConstProp they
-                // remain uninit and this code can actually be reached.
-                throw_inval!(UninitUnsizedLocal);
+    pub(super) fn meta(&self) -> InterpResult<'tcx, MemPlaceMeta<Prov>> {
+        Ok(if self.layout.is_unsized() {
+            if matches!(self.op, Operand::Immediate(_)) {
+                // Unsized immediate OpTy cannot occur. We create a MemPlace for all unsized locals during argument passing.
+                // However, ConstProp doesn't do that, so we can run into this nonsense situation.
+                throw_inval!(ConstPropNonsense);
             }
             // There are no unsized immediates.
-            self.assert_mem_place().len(cx)
+            self.assert_mem_place().meta
         } else {
-            match self.layout.fields {
-                abi::FieldsShape::Array { count, .. } => Ok(count),
-                _ => bug!("len not supported on sized type {:?}", self.layout.ty),
-            }
-        }
+            MemPlaceMeta::None
+        })
     }
 
-    /// Replace the layout of this operand. There's basically no sanity check that this makes sense,
-    /// you better know what you are doing! If this is an immediate, applying the wrong layout can
-    /// not just lead to invalid data, it can actually *shift the data around* since the offsets of
-    /// a ScalarPair are entirely determined by the layout, not the data.
-    pub fn transmute(&self, layout: TyAndLayout<'tcx>) -> Self {
-        assert_eq!(
-            self.layout.size, layout.size,
-            "transmuting with a size change, that doesn't seem right"
-        );
-        OpTy { layout, ..*self }
+    pub fn len(&self, cx: &impl HasDataLayout) -> InterpResult<'tcx, u64> {
+        self.meta()?.len(self.layout, cx)
     }
 
     /// Offset the operand in memory (if possible) and change its metadata.
@@ -286,13 +318,9 @@ impl<'tcx, Prov: Provenance> OpTy<'tcx, Prov> {
         match self.as_mplace_or_imm() {
             Left(mplace) => Ok(mplace.offset_with_meta(offset, meta, layout, cx)?.into()),
             Right(imm) => {
-                assert!(
-                    matches!(*imm, Immediate::Uninit),
-                    "Scalar/ScalarPair cannot be offset into"
-                );
                 assert!(!meta.has_meta()); // no place to store metadata here
                 // Every part of an uninit is uninit.
-                Ok(ImmTy::uninit(layout).into())
+                Ok(imm.offset(offset, layout, cx).into())
             }
         }
     }
@@ -502,13 +530,24 @@ impl<'mir, 'tcx: 'mir, M: Machine<'mir, 'tcx>> InterpCx<'mir, 'tcx, M> {
         &self,
         place: &PlaceTy<'tcx, M::Provenance>,
     ) -> InterpResult<'tcx, OpTy<'tcx, M::Provenance>> {
-        let op = match **place {
-            Place::Ptr(mplace) => Operand::Indirect(mplace),
-            Place::Local { frame, local } => {
-                *self.local_to_op(&self.stack()[frame], local, None)?
+        match place.as_mplace_or_local() {
+            Left(mplace) => Ok(mplace.into()),
+            Right((frame, local, offset)) => {
+                let base = self.local_to_op(&self.stack()[frame], local, None)?;
+                let mut field = if let Some(offset) = offset {
+                    // This got offset. We can be sure that the field is sized.
+                    base.offset(offset, place.layout, self)?
+                } else {
+                    assert_eq!(place.layout, base.layout);
+                    // Unsized cases are possible here since an unsized local will be a
+                    // `Place::Local` until the first projection calls `place_to_op` to extract the
+                    // underlying mplace.
+                    base
+                };
+                field.align = Some(place.align);
+                Ok(field)
             }
-        };
-        Ok(OpTy { op, layout: place.layout, align: Some(place.align) })
+        }
     }
 
     /// Evaluate a place with the goal of reading from it. This lets us sometimes
diff --git a/compiler/rustc_const_eval/src/interpret/place.rs b/compiler/rustc_const_eval/src/interpret/place.rs
index a9b2b43f1e6..dd07c5fa877 100644
--- a/compiler/rustc_const_eval/src/interpret/place.rs
+++ b/compiler/rustc_const_eval/src/interpret/place.rs
@@ -2,11 +2,14 @@
 //! into a place.
 //! All high-level functions to write to memory work on places as destinations.
 
+use std::assert_matches::assert_matches;
+
 use either::{Either, Left, Right};
 
 use rustc_ast::Mutability;
 use rustc_index::IndexSlice;
 use rustc_middle::mir;
+use rustc_middle::mir::interpret::PointerArithmetic;
 use rustc_middle::ty;
 use rustc_middle::ty::layout::{LayoutOf, TyAndLayout};
 use rustc_middle::ty::Ty;
@@ -44,6 +47,27 @@ impl<Prov: Provenance> MemPlaceMeta<Prov> {
             Self::None => false,
         }
     }
+
+    pub(crate) fn len<'tcx>(
+        &self,
+        layout: TyAndLayout<'tcx>,
+        cx: &impl HasDataLayout,
+    ) -> InterpResult<'tcx, u64> {
+        if layout.is_unsized() {
+            // We need to consult `meta` metadata
+            match layout.ty.kind() {
+                ty::Slice(..) | ty::Str => self.unwrap_meta().to_target_usize(cx),
+                _ => bug!("len not supported on unsized type {:?}", layout.ty),
+            }
+        } else {
+            // Go through the layout. There are lots of types that support a length,
+            // e.g., SIMD types. (But not all repr(simd) types even have FieldsShape::Array!)
+            match layout.fields {
+                abi::FieldsShape::Array { count, .. } => Ok(count),
+                _ => bug!("len not supported on sized type {:?}", layout.ty),
+            }
+        }
+    }
 }
 
 #[derive(Copy, Clone, Hash, PartialEq, Eq, Debug)]
@@ -73,9 +97,13 @@ pub enum Place<Prov: Provenance = AllocId> {
     /// A place referring to a value allocated in the `Memory` system.
     Ptr(MemPlace<Prov>),
 
-    /// To support alloc-free locals, we are able to write directly to a local.
+    /// To support alloc-free locals, we are able to write directly to a local. The offset indicates
+    /// where in the local this place is located; if it is `None`, no projection has been applied.
+    /// Such projections are meaningful even if the offset is 0, since they can change layouts.
     /// (Without that optimization, we'd just always be a `MemPlace`.)
-    Local { frame: usize, local: mir::Local },
+    /// Note that this only stores the frame index, not the thread this frame belongs to -- that is
+    /// implicit. This means a `Place` must never be moved across interpreter thread boundaries!
+    Local { frame: usize, local: mir::Local, offset: Option<Size> },
 }
 
 #[derive(Clone, Debug)]
@@ -132,6 +160,11 @@ impl<Prov: Provenance> MemPlace<Prov> {
         MemPlace { ptr, meta: MemPlaceMeta::None }
     }
 
+    #[inline(always)]
+    pub fn from_ptr_with_meta(ptr: Pointer<Option<Prov>>, meta: MemPlaceMeta<Prov>) -> Self {
+        MemPlace { ptr, meta }
+    }
+
     /// Adjust the provenance of the main pointer (metadata is unaffected).
     pub fn map_provenance(self, f: impl FnOnce(Option<Prov>) -> Option<Prov>) -> Self {
         MemPlace { ptr: self.ptr.map_provenance(f), ..self }
@@ -150,7 +183,7 @@ impl<Prov: Provenance> MemPlace<Prov> {
     }
 
     #[inline]
-    pub(super) fn offset_with_meta<'tcx>(
+    fn offset_with_meta<'tcx>(
         self,
         offset: Size,
         meta: MemPlaceMeta<Prov>,
@@ -162,18 +195,10 @@ impl<Prov: Provenance> MemPlace<Prov> {
         );
         Ok(MemPlace { ptr: self.ptr.offset(offset, cx)?, meta })
     }
-}
 
-impl<Prov: Provenance> Place<Prov> {
-    /// Asserts that this points to some local variable.
-    /// Returns the frame idx and the variable idx.
     #[inline]
-    #[cfg_attr(debug_assertions, track_caller)] // only in debug builds due to perf (see #98980)
-    pub fn assert_local(&self) -> (usize, mir::Local) {
-        match self {
-            Place::Local { frame, local } => (*frame, *local),
-            _ => bug!("assert_local: expected Place::Local, got {:?}", self),
-        }
+    fn offset<'tcx>(&self, offset: Size, cx: &impl HasDataLayout) -> InterpResult<'tcx, Self> {
+        self.offset_with_meta(offset, MemPlaceMeta::None, cx)
     }
 }
 
@@ -231,28 +256,16 @@ impl<'tcx, Prov: Provenance> MPlaceTy<'tcx, Prov> {
         layout: TyAndLayout<'tcx>,
         meta: MemPlaceMeta<Prov>,
     ) -> Self {
-        let mut mplace = MemPlace::from_ptr(ptr);
-        mplace.meta = meta;
-
-        MPlaceTy { mplace, layout, align: layout.align.abi }
+        MPlaceTy {
+            mplace: MemPlace::from_ptr_with_meta(ptr, meta),
+            layout,
+            align: layout.align.abi,
+        }
     }
 
     #[inline]
     pub(crate) fn len(&self, cx: &impl HasDataLayout) -> InterpResult<'tcx, u64> {
-        if self.layout.is_unsized() {
-            // We need to consult `meta` metadata
-            match self.layout.ty.kind() {
-                ty::Slice(..) | ty::Str => self.mplace.meta.unwrap_meta().to_target_usize(cx),
-                _ => bug!("len not supported on unsized type {:?}", self.layout.ty),
-            }
-        } else {
-            // Go through the layout. There are lots of types that support a length,
-            // e.g., SIMD types. (But not all repr(simd) types even have FieldsShape::Array!)
-            match self.layout.fields {
-                abi::FieldsShape::Array { count, .. } => Ok(count),
-                _ => bug!("len not supported on sized type {:?}", self.layout.ty),
-            }
-        }
+        self.mplace.meta.len(self.layout, cx)
     }
 }
 
@@ -283,10 +296,12 @@ impl<'tcx, Prov: Provenance> OpTy<'tcx, Prov> {
 impl<'tcx, Prov: Provenance> PlaceTy<'tcx, Prov> {
     /// A place is either an mplace or some local.
     #[inline]
-    pub fn as_mplace_or_local(&self) -> Either<MPlaceTy<'tcx, Prov>, (usize, mir::Local)> {
+    pub fn as_mplace_or_local(
+        &self,
+    ) -> Either<MPlaceTy<'tcx, Prov>, (usize, mir::Local, Option<Size>)> {
         match **self {
             Place::Ptr(mplace) => Left(MPlaceTy { mplace, layout: self.layout, align: self.align }),
-            Place::Local { frame, local } => Right((frame, local)),
+            Place::Local { frame, local, offset } => Right((frame, local, offset)),
         }
     }
 
@@ -300,6 +315,49 @@ impl<'tcx, Prov: Provenance> PlaceTy<'tcx, Prov> {
             )
         })
     }
+
+    /// Offset the place in memory and change its metadata.
+    ///
+    /// This can go wrong very easily if you give the wrong layout for the new place!
+    pub(crate) fn offset_with_meta(
+        &self,
+        offset: Size,
+        meta: MemPlaceMeta<Prov>,
+        layout: TyAndLayout<'tcx>,
+        cx: &impl HasDataLayout,
+    ) -> InterpResult<'tcx, Self> {
+        Ok(match self.as_mplace_or_local() {
+            Left(mplace) => mplace.offset_with_meta(offset, meta, layout, cx)?.into(),
+            Right((frame, local, old_offset)) => {
+                assert_matches!(meta, MemPlaceMeta::None); // we couldn't store it anyway...
+                let new_offset = cx
+                    .data_layout()
+                    .offset(old_offset.unwrap_or(Size::ZERO).bytes(), offset.bytes())?;
+                PlaceTy {
+                    place: Place::Local {
+                        frame,
+                        local,
+                        offset: Some(Size::from_bytes(new_offset)),
+                    },
+                    align: self.align.restrict_for_offset(offset),
+                    layout,
+                }
+            }
+        })
+    }
+
+    /// Offset the place in memory.
+    ///
+    /// This can go wrong very easily if you give the wrong layout for the new place!
+    pub fn offset(
+        &self,
+        offset: Size,
+        layout: TyAndLayout<'tcx>,
+        cx: &impl HasDataLayout,
+    ) -> InterpResult<'tcx, Self> {
+        assert!(layout.is_sized());
+        self.offset_with_meta(offset, MemPlaceMeta::None, layout, cx)
+    }
 }
 
 // FIXME: Working around https://github.com/rust-lang/rust/issues/54385
@@ -308,6 +366,20 @@ where
     Prov: Provenance + 'static,
     M: Machine<'mir, 'tcx, Provenance = Prov>,
 {
+    /// Get the metadata of the given place.
+    pub(super) fn place_meta(
+        &self,
+        place: &PlaceTy<'tcx, M::Provenance>,
+    ) -> InterpResult<'tcx, MemPlaceMeta<M::Provenance>> {
+        if place.layout.is_unsized() {
+            // For `Place::Local`, the metadata is stored with the local, not the place. So we have
+            // to look that up first.
+            self.place_to_op(place)?.meta()
+        } else {
+            Ok(MemPlaceMeta::None)
+        }
+    }
+
     /// Take a value, which represents a (thin or wide) reference, and make it a place.
     /// Alignment is just based on the type. This is the inverse of `MemPlace::to_ref()`.
     ///
@@ -327,11 +399,9 @@ where
             Immediate::Uninit => throw_ub!(InvalidUninitBytes(None)),
         };
 
-        let mplace = MemPlace { ptr: ptr.to_pointer(self)?, meta };
         // `ref_to_mplace` is called on raw pointers even if they don't actually get dereferenced;
         // we hence can't call `size_and_align_of` since that asserts more validity than we want.
-        let align = layout.align.abi;
-        Ok(MPlaceTy { mplace, layout, align })
+        Ok(MPlaceTy::from_aligned_ptr_with_meta(ptr.to_pointer(self)?, layout, meta))
     }
 
     /// Take an operand, representing a pointer, and dereference it to a place.
@@ -422,7 +492,7 @@ where
         local: mir::Local,
     ) -> InterpResult<'tcx, PlaceTy<'tcx, M::Provenance>> {
         let layout = self.layout_of_local(&self.stack()[frame], local, None)?;
-        let place = Place::Local { frame, local };
+        let place = Place::Local { frame, local, offset: None };
         Ok(PlaceTy { place, layout, align: layout.align.abi })
     }
 
@@ -430,7 +500,7 @@ where
     /// place; for reading, a more efficient alternative is `eval_place_to_op`.
     #[instrument(skip(self), level = "debug")]
     pub fn eval_place(
-        &mut self,
+        &self,
         mir_place: mir::Place<'tcx>,
     ) -> InterpResult<'tcx, PlaceTy<'tcx, M::Provenance>> {
         let mut place = self.local_to_place(self.frame_idx(), mir_place.local)?;
@@ -509,16 +579,23 @@ where
         // See if we can avoid an allocation. This is the counterpart to `read_immediate_raw`,
         // but not factored as a separate function.
         let mplace = match dest.place {
-            Place::Local { frame, local } => {
-                match M::access_local_mut(self, frame, local)? {
-                    Operand::Immediate(local) => {
-                        // Local can be updated in-place.
-                        *local = src;
-                        return Ok(());
-                    }
-                    Operand::Indirect(mplace) => {
-                        // The local is in memory, go on below.
-                        *mplace
+            Place::Local { frame, local, offset } => {
+                if offset.is_some() {
+                    // This has been projected to a part of this local. We could have complicated
+                    // logic to still keep this local as an `Operand`... but it's much easier to
+                    // just fall back to the indirect path.
+                    *self.force_allocation(dest)?
+                } else {
+                    match M::access_local_mut(self, frame, local)? {
+                        Operand::Immediate(local) => {
+                            // Local can be updated in-place.
+                            *local = src;
+                            return Ok(());
+                        }
+                        Operand::Indirect(mplace) => {
+                            // The local is in memory, go on below.
+                            *mplace
+                        }
                     }
                 }
             }
@@ -593,15 +670,23 @@ where
     pub fn write_uninit(&mut self, dest: &PlaceTy<'tcx, M::Provenance>) -> InterpResult<'tcx> {
         let mplace = match dest.as_mplace_or_local() {
             Left(mplace) => mplace,
-            Right((frame, local)) => {
-                match M::access_local_mut(self, frame, local)? {
-                    Operand::Immediate(local) => {
-                        *local = Immediate::Uninit;
-                        return Ok(());
-                    }
-                    Operand::Indirect(mplace) => {
-                        // The local is in memory, go on below.
-                        MPlaceTy { mplace: *mplace, layout: dest.layout, align: dest.align }
+            Right((frame, local, offset)) => {
+                if offset.is_some() {
+                    // This has been projected to a part of this local. We could have complicated
+                    // logic to still keep this local as an `Operand`... but it's much easier to
+                    // just fall back to the indirect path.
+                    // FIXME: share the logic with `write_immediate_no_validate`.
+                    self.force_allocation(dest)?
+                } else {
+                    match M::access_local_mut(self, frame, local)? {
+                        Operand::Immediate(local) => {
+                            *local = Immediate::Uninit;
+                            return Ok(());
+                        }
+                        Operand::Indirect(mplace) => {
+                            // The local is in memory, go on below.
+                            MPlaceTy { mplace: *mplace, layout: dest.layout, align: dest.align }
+                        }
                     }
                 }
             }
@@ -728,8 +813,8 @@ where
         place: &PlaceTy<'tcx, M::Provenance>,
     ) -> InterpResult<'tcx, MPlaceTy<'tcx, M::Provenance>> {
         let mplace = match place.place {
-            Place::Local { frame, local } => {
-                match M::access_local_mut(self, frame, local)? {
+            Place::Local { frame, local, offset } => {
+                let whole_local = match M::access_local_mut(self, frame, local)? {
                     &mut Operand::Immediate(local_val) => {
                         // We need to make an allocation.
 
@@ -742,10 +827,11 @@ where
                             throw_unsup_format!("unsized locals are not supported");
                         }
                         let mplace = *self.allocate(local_layout, MemoryKind::Stack)?;
+                        // Preserve old value. (As an optimization, we can skip this if it was uninit.)
                         if !matches!(local_val, Immediate::Uninit) {
-                            // Preserve old value. (As an optimization, we can skip this if it was uninit.)
-                            // We don't have to validate as we can assume the local
-                            // was already valid for its type.
+                            // We don't have to validate as we can assume the local was already
+                            // valid for its type. We must not use any part of `place` here, that
+                            // could be a projection to a part of the local!
                             self.write_immediate_to_mplace_no_validate(
                                 local_val,
                                 local_layout,
@@ -753,18 +839,25 @@ where
                                 mplace,
                             )?;
                         }
-                        // Now we can call `access_mut` again, asserting it goes well,
-                        // and actually overwrite things.
+                        // Now we can call `access_mut` again, asserting it goes well, and actually
+                        // overwrite things. This points to the entire allocation, not just the part
+                        // the place refers to, i.e. we do this before we apply `offset`.
                         *M::access_local_mut(self, frame, local).unwrap() =
                             Operand::Indirect(mplace);
                         mplace
                     }
                     &mut Operand::Indirect(mplace) => mplace, // this already was an indirect local
+                };
+                if let Some(offset) = offset {
+                    whole_local.offset(offset, self)?
+                } else {
+                    // Preserve wide place metadata, do not call `offset`.
+                    whole_local
                 }
             }
             Place::Ptr(mplace) => mplace,
         };
-        // Return with the original layout, so that the caller can go on
+        // Return with the original layout and align, so that the caller can go on
         Ok(MPlaceTy { mplace, layout: place.layout, align: place.align })
     }
 
@@ -874,7 +967,7 @@ where
         let vtable = self.read_pointer(&vtable)?;
         let (ty, _) = self.get_ptr_vtable(vtable)?;
         let layout = self.layout_of(ty)?;
-        let data = data.transmute(layout);
+        let data = data.offset(Size::ZERO, layout, self)?;
         Ok((data, vtable))
     }
 }
diff --git a/compiler/rustc_const_eval/src/interpret/projection.rs b/compiler/rustc_const_eval/src/interpret/projection.rs
index d7d31fe1887..5a3f19e5dc9 100644
--- a/compiler/rustc_const_eval/src/interpret/projection.rs
+++ b/compiler/rustc_const_eval/src/interpret/projection.rs
@@ -7,17 +7,15 @@
 //! but we still need to do bounds checking and adjust the layout. To not duplicate that with MPlaceTy, we actually
 //! implement the logic on OpTy, and MPlaceTy calls that.
 
-use either::{Left, Right};
-
 use rustc_middle::mir;
 use rustc_middle::ty;
-use rustc_middle::ty::layout::LayoutOf;
+use rustc_middle::ty::layout::{LayoutOf, TyAndLayout};
 use rustc_middle::ty::Ty;
-use rustc_target::abi::{self, Abi, VariantIdx};
+use rustc_target::abi::Size;
+use rustc_target::abi::{self, VariantIdx};
 
 use super::{
-    ImmTy, Immediate, InterpCx, InterpResult, MPlaceTy, Machine, MemPlaceMeta, OpTy, PlaceTy,
-    Provenance, Scalar,
+    InterpCx, InterpResult, MPlaceTy, Machine, MemPlaceMeta, OpTy, PlaceTy, Provenance, Scalar,
 };
 
 // FIXME: Working around https://github.com/rust-lang/rust/issues/54385
@@ -28,58 +26,70 @@ where
 {
     //# Field access
 
-    /// Offset a pointer to project to a field of a struct/union. Unlike `place_field`, this is
-    /// always possible without allocating, so it can take `&self`. Also return the field's layout.
-    /// This supports both struct and array fields.
-    ///
-    /// This also works for arrays, but then the `usize` index type is restricting.
-    /// For indexing into arrays, use `mplace_index`.
-    pub fn mplace_field(
+    fn project_field(
         &self,
-        base: &MPlaceTy<'tcx, M::Provenance>,
+        base_layout: TyAndLayout<'tcx>,
+        base_meta: MemPlaceMeta<M::Provenance>,
         field: usize,
-    ) -> InterpResult<'tcx, MPlaceTy<'tcx, M::Provenance>> {
-        let offset = base.layout.fields.offset(field);
-        let field_layout = base.layout.field(self, field);
+    ) -> InterpResult<'tcx, (Size, MemPlaceMeta<M::Provenance>, TyAndLayout<'tcx>)> {
+        let offset = base_layout.fields.offset(field);
+        let field_layout = base_layout.field(self, field);
 
         // Offset may need adjustment for unsized fields.
         let (meta, offset) = if field_layout.is_unsized() {
+            if base_layout.is_sized() {
+                // An unsized field of a sized type? Sure...
+                // But const-prop actually feeds us such nonsense MIR!
+                throw_inval!(ConstPropNonsense);
+            }
             // Re-use parent metadata to determine dynamic field layout.
             // With custom DSTS, this *will* execute user-defined code, but the same
             // happens at run-time so that's okay.
-            match self.size_and_align_of(&base.meta, &field_layout)? {
-                Some((_, align)) => (base.meta, offset.align_to(align)),
+            match self.size_and_align_of(&base_meta, &field_layout)? {
+                Some((_, align)) => (base_meta, offset.align_to(align)),
                 None => {
                     // For unsized types with an extern type tail we perform no adjustments.
                     // NOTE: keep this in sync with `PlaceRef::project_field` in the codegen backend.
-                    assert!(matches!(base.meta, MemPlaceMeta::None));
-                    (base.meta, offset)
+                    assert!(matches!(base_meta, MemPlaceMeta::None));
+                    (base_meta, offset)
                 }
             }
         } else {
-            // base.meta could be present; we might be accessing a sized field of an unsized
+            // base_meta could be present; we might be accessing a sized field of an unsized
             // struct.
             (MemPlaceMeta::None, offset)
         };
 
+        Ok((offset, meta, field_layout))
+    }
+
+    /// Offset a pointer to project to a field of a struct/union. Unlike `place_field`, this is
+    /// always possible without allocating, so it can take `&self`. Also return the field's layout.
+    /// This supports both struct and array fields.
+    ///
+    /// This also works for arrays, but then the `usize` index type is restricting.
+    /// For indexing into arrays, use `mplace_index`.
+    pub fn mplace_field(
+        &self,
+        base: &MPlaceTy<'tcx, M::Provenance>,
+        field: usize,
+    ) -> InterpResult<'tcx, MPlaceTy<'tcx, M::Provenance>> {
+        let (offset, meta, field_layout) = self.project_field(base.layout, base.meta, field)?;
+
         // We do not look at `base.layout.align` nor `field_layout.align`, unlike
         // codegen -- mostly to see if we can get away with that
         base.offset_with_meta(offset, meta, field_layout, self)
     }
 
     /// Gets the place of a field inside the place, and also the field's type.
-    /// Just a convenience function, but used quite a bit.
-    /// This is the only projection that might have a side-effect: We cannot project
-    /// into the field of a local `ScalarPair`, we have to first allocate it.
     pub fn place_field(
-        &mut self,
+        &self,
         base: &PlaceTy<'tcx, M::Provenance>,
         field: usize,
     ) -> InterpResult<'tcx, PlaceTy<'tcx, M::Provenance>> {
-        // FIXME: We could try to be smarter and avoid allocation for fields that span the
-        // entire place.
-        let base = self.force_allocation(base)?;
-        Ok(self.mplace_field(&base, field)?.into())
+        let (offset, meta, field_layout) =
+            self.project_field(base.layout, self.place_meta(base)?, field)?;
+        base.offset_with_meta(offset, meta, field_layout, self)
     }
 
     pub fn operand_field(
@@ -87,56 +97,8 @@ where
         base: &OpTy<'tcx, M::Provenance>,
         field: usize,
     ) -> InterpResult<'tcx, OpTy<'tcx, M::Provenance>> {
-        let base = match base.as_mplace_or_imm() {
-            Left(ref mplace) => {
-                // We can reuse the mplace field computation logic for indirect operands.
-                let field = self.mplace_field(mplace, field)?;
-                return Ok(field.into());
-            }
-            Right(value) => value,
-        };
-
-        let field_layout = base.layout.field(self, field);
-        let offset = base.layout.fields.offset(field);
-        // This makes several assumptions about what layouts we will encounter; we match what
-        // codegen does as good as we can (see `extract_field` in `rustc_codegen_ssa/src/mir/operand.rs`).
-        let field_val: Immediate<_> = match (*base, base.layout.abi) {
-            // if the entire value is uninit, then so is the field (can happen in ConstProp)
-            (Immediate::Uninit, _) => Immediate::Uninit,
-            // the field contains no information, can be left uninit
-            _ if field_layout.is_zst() => Immediate::Uninit,
-            // the field covers the entire type
-            _ if field_layout.size == base.layout.size => {
-                assert!(match (base.layout.abi, field_layout.abi) {
-                    (Abi::Scalar(..), Abi::Scalar(..)) => true,
-                    (Abi::ScalarPair(..), Abi::ScalarPair(..)) => true,
-                    _ => false,
-                });
-                assert!(offset.bytes() == 0);
-                *base
-            }
-            // extract fields from types with `ScalarPair` ABI
-            (Immediate::ScalarPair(a_val, b_val), Abi::ScalarPair(a, b)) => {
-                assert!(matches!(field_layout.abi, Abi::Scalar(..)));
-                Immediate::from(if offset.bytes() == 0 {
-                    debug_assert_eq!(field_layout.size, a.size(self));
-                    a_val
-                } else {
-                    debug_assert_eq!(offset, a.size(self).align_to(b.align(self).abi));
-                    debug_assert_eq!(field_layout.size, b.size(self));
-                    b_val
-                })
-            }
-            // everything else is a bug
-            _ => span_bug!(
-                self.cur_span(),
-                "invalid field access on immediate {}, layout {:#?}",
-                base,
-                base.layout
-            ),
-        };
-
-        Ok(ImmTy::from_immediate(field_val, field_layout).into())
+        let (offset, meta, field_layout) = self.project_field(base.layout, base.meta()?, field)?;
+        base.offset_with_meta(offset, meta, field_layout, self)
     }
 
     //# Downcasting
@@ -177,83 +139,110 @@ where
         Ok(base)
     }
 
-    //# Slice indexing
+    //# Slice and array indexing
 
-    #[inline(always)]
-    pub fn operand_index(
+    /// Compute the offset and field layout for accessing the given index.
+    fn project_index(
         &self,
-        base: &OpTy<'tcx, M::Provenance>,
+        base_layout: TyAndLayout<'tcx>,
+        base_meta: MemPlaceMeta<M::Provenance>,
         index: u64,
-    ) -> InterpResult<'tcx, OpTy<'tcx, M::Provenance>> {
+    ) -> InterpResult<'tcx, (Size, TyAndLayout<'tcx>)> {
         // Not using the layout method because we want to compute on u64
-        match base.layout.fields {
+        match base_layout.fields {
             abi::FieldsShape::Array { stride, count: _ } => {
                 // `count` is nonsense for slices, use the dynamic length instead.
-                let len = base.len(self)?;
+                let len = base_meta.len(base_layout, self)?;
                 if index >= len {
                     // This can only be reached in ConstProp and non-rustc-MIR.
                     throw_ub!(BoundsCheckFailed { len, index });
                 }
                 let offset = stride * index; // `Size` multiplication
                 // All fields have the same layout.
-                let field_layout = base.layout.field(self, 0);
-                base.offset(offset, field_layout, self)
+                let field_layout = base_layout.field(self, 0);
+                Ok((offset, field_layout))
             }
             _ => span_bug!(
                 self.cur_span(),
                 "`mplace_index` called on non-array type {:?}",
-                base.layout.ty
+                base_layout.ty
             ),
         }
     }
 
+    #[inline(always)]
+    pub fn operand_index(
+        &self,
+        base: &OpTy<'tcx, M::Provenance>,
+        index: u64,
+    ) -> InterpResult<'tcx, OpTy<'tcx, M::Provenance>> {
+        let (offset, field_layout) = self.project_index(base.layout, base.meta()?, index)?;
+        base.offset(offset, field_layout, self)
+    }
+
+    /// Index into an array.
+    pub fn mplace_index(
+        &self,
+        base: &MPlaceTy<'tcx, M::Provenance>,
+        index: u64,
+    ) -> InterpResult<'tcx, MPlaceTy<'tcx, M::Provenance>> {
+        let (offset, field_layout) = self.project_index(base.layout, base.meta, index)?;
+        base.offset(offset, field_layout, self)
+    }
+
+    pub fn place_index(
+        &self,
+        base: &PlaceTy<'tcx, M::Provenance>,
+        index: u64,
+    ) -> InterpResult<'tcx, PlaceTy<'tcx, M::Provenance>> {
+        let (offset, field_layout) =
+            self.project_index(base.layout, self.place_meta(base)?, index)?;
+        base.offset(offset, field_layout, self)
+    }
+
     /// Iterates over all fields of an array. Much more efficient than doing the
     /// same by repeatedly calling `operand_index`.
     pub fn operand_array_fields<'a>(
         &self,
         base: &'a OpTy<'tcx, Prov>,
     ) -> InterpResult<'tcx, impl Iterator<Item = InterpResult<'tcx, OpTy<'tcx, Prov>>> + 'a> {
-        let len = base.len(self)?; // also asserts that we have a type where this makes sense
         let abi::FieldsShape::Array { stride, .. } = base.layout.fields else {
             span_bug!(self.cur_span(), "operand_array_fields: expected an array layout");
         };
+        let len = base.len(self)?;
         let field_layout = base.layout.field(self, 0);
         let dl = &self.tcx.data_layout;
         // `Size` multiplication
         Ok((0..len).map(move |i| base.offset(stride * i, field_layout, dl)))
     }
 
-    /// Index into an array.
-    pub fn mplace_index(
+    /// Iterates over all fields of an array. Much more efficient than doing the
+    /// same by repeatedly calling `place_index`.
+    pub fn place_array_fields<'a>(
         &self,
-        base: &MPlaceTy<'tcx, M::Provenance>,
-        index: u64,
-    ) -> InterpResult<'tcx, MPlaceTy<'tcx, M::Provenance>> {
-        Ok(self.operand_index(&base.into(), index)?.assert_mem_place())
-    }
-
-    pub fn place_index(
-        &mut self,
-        base: &PlaceTy<'tcx, M::Provenance>,
-        index: u64,
-    ) -> InterpResult<'tcx, PlaceTy<'tcx, M::Provenance>> {
-        // There's not a lot we can do here, since we cannot have a place to a part of a local. If
-        // we are accessing the only element of a 1-element array, it's still the entire local...
-        // that doesn't seem worth it.
-        let base = self.force_allocation(base)?;
-        Ok(self.mplace_index(&base, index)?.into())
+        base: &'a PlaceTy<'tcx, Prov>,
+    ) -> InterpResult<'tcx, impl Iterator<Item = InterpResult<'tcx, PlaceTy<'tcx, Prov>>> + 'a> {
+        let abi::FieldsShape::Array { stride, .. } = base.layout.fields else {
+            span_bug!(self.cur_span(), "place_array_fields: expected an array layout");
+        };
+        let len = self.place_meta(base)?.len(base.layout, self)?;
+        let field_layout = base.layout.field(self, 0);
+        let dl = &self.tcx.data_layout;
+        // `Size` multiplication
+        Ok((0..len).map(move |i| base.offset(stride * i, field_layout, dl)))
     }
 
     //# ConstantIndex support
 
-    fn operand_constant_index(
+    fn project_constant_index(
         &self,
-        base: &OpTy<'tcx, M::Provenance>,
+        base_layout: TyAndLayout<'tcx>,
+        base_meta: MemPlaceMeta<M::Provenance>,
         offset: u64,
         min_length: u64,
         from_end: bool,
-    ) -> InterpResult<'tcx, OpTy<'tcx, M::Provenance>> {
-        let n = base.len(self)?;
+    ) -> InterpResult<'tcx, (Size, TyAndLayout<'tcx>)> {
+        let n = base_meta.len(base_layout, self)?;
         if n < min_length {
             // This can only be reached in ConstProp and non-rustc-MIR.
             throw_ub!(BoundsCheckFailed { len: min_length, index: n });
@@ -267,33 +256,49 @@ where
             offset
         };
 
-        self.operand_index(base, index)
+        self.project_index(base_layout, base_meta, index)
+    }
+
+    fn operand_constant_index(
+        &self,
+        base: &OpTy<'tcx, M::Provenance>,
+        offset: u64,
+        min_length: u64,
+        from_end: bool,
+    ) -> InterpResult<'tcx, OpTy<'tcx, M::Provenance>> {
+        let (offset, layout) =
+            self.project_constant_index(base.layout, base.meta()?, offset, min_length, from_end)?;
+        base.offset(offset, layout, self)
     }
 
     fn place_constant_index(
-        &mut self,
+        &self,
         base: &PlaceTy<'tcx, M::Provenance>,
         offset: u64,
         min_length: u64,
         from_end: bool,
     ) -> InterpResult<'tcx, PlaceTy<'tcx, M::Provenance>> {
-        let base = self.force_allocation(base)?;
-        Ok(self
-            .operand_constant_index(&base.into(), offset, min_length, from_end)?
-            .assert_mem_place()
-            .into())
+        let (offset, layout) = self.project_constant_index(
+            base.layout,
+            self.place_meta(base)?,
+            offset,
+            min_length,
+            from_end,
+        )?;
+        base.offset(offset, layout, self)
     }
 
     //# Subslicing
 
-    fn operand_subslice(
+    fn project_subslice(
         &self,
-        base: &OpTy<'tcx, M::Provenance>,
+        base_layout: TyAndLayout<'tcx>,
+        base_meta: MemPlaceMeta<M::Provenance>,
         from: u64,
         to: u64,
         from_end: bool,
-    ) -> InterpResult<'tcx, OpTy<'tcx, M::Provenance>> {
-        let len = base.len(self)?; // also asserts that we have a type where this makes sense
+    ) -> InterpResult<'tcx, (Size, MemPlaceMeta<M::Provenance>, TyAndLayout<'tcx>)> {
+        let len = base_meta.len(base_layout, self)?; // also asserts that we have a type where this makes sense
         let actual_to = if from_end {
             if from.checked_add(to).map_or(true, |to| to > len) {
                 // This can only be reached in ConstProp and non-rustc-MIR.
@@ -306,16 +311,16 @@ where
 
         // Not using layout method because that works with usize, and does not work with slices
         // (that have count 0 in their layout).
-        let from_offset = match base.layout.fields {
+        let from_offset = match base_layout.fields {
             abi::FieldsShape::Array { stride, .. } => stride * from, // `Size` multiplication is checked
             _ => {
-                span_bug!(self.cur_span(), "unexpected layout of index access: {:#?}", base.layout)
+                span_bug!(self.cur_span(), "unexpected layout of index access: {:#?}", base_layout)
             }
         };
 
         // Compute meta and new layout
         let inner_len = actual_to.checked_sub(from).unwrap();
-        let (meta, ty) = match base.layout.ty.kind() {
+        let (meta, ty) = match base_layout.ty.kind() {
             // It is not nice to match on the type, but that seems to be the only way to
             // implement this.
             ty::Array(inner, _) => {
@@ -323,25 +328,38 @@ where
             }
             ty::Slice(..) => {
                 let len = Scalar::from_target_usize(inner_len, self);
-                (MemPlaceMeta::Meta(len), base.layout.ty)
+                (MemPlaceMeta::Meta(len), base_layout.ty)
             }
             _ => {
-                span_bug!(self.cur_span(), "cannot subslice non-array type: `{:?}`", base.layout.ty)
+                span_bug!(self.cur_span(), "cannot subslice non-array type: `{:?}`", base_layout.ty)
             }
         };
         let layout = self.layout_of(ty)?;
+        Ok((from_offset, meta, layout))
+    }
+
+    fn operand_subslice(
+        &self,
+        base: &OpTy<'tcx, M::Provenance>,
+        from: u64,
+        to: u64,
+        from_end: bool,
+    ) -> InterpResult<'tcx, OpTy<'tcx, M::Provenance>> {
+        let (from_offset, meta, layout) =
+            self.project_subslice(base.layout, base.meta()?, from, to, from_end)?;
         base.offset_with_meta(from_offset, meta, layout, self)
     }
 
     pub fn place_subslice(
-        &mut self,
+        &self,
         base: &PlaceTy<'tcx, M::Provenance>,
         from: u64,
         to: u64,
         from_end: bool,
     ) -> InterpResult<'tcx, PlaceTy<'tcx, M::Provenance>> {
-        let base = self.force_allocation(base)?;
-        Ok(self.operand_subslice(&base.into(), from, to, from_end)?.assert_mem_place().into())
+        let (from_offset, meta, layout) =
+            self.project_subslice(base.layout, self.place_meta(base)?, from, to, from_end)?;
+        base.offset_with_meta(from_offset, meta, layout, self)
     }
 
     //# Applying a general projection
@@ -349,7 +367,7 @@ where
     /// Projects into a place.
     #[instrument(skip(self), level = "trace")]
     pub fn place_projection(
-        &mut self,
+        &self,
         base: &PlaceTy<'tcx, M::Provenance>,
         proj_elem: mir::PlaceElem<'tcx>,
     ) -> InterpResult<'tcx, PlaceTy<'tcx, M::Provenance>> {
diff --git a/compiler/rustc_const_eval/src/interpret/terminator.rs b/compiler/rustc_const_eval/src/interpret/terminator.rs
index 7964c6be008..f5e38570330 100644
--- a/compiler/rustc_const_eval/src/interpret/terminator.rs
+++ b/compiler/rustc_const_eval/src/interpret/terminator.rs
@@ -60,7 +60,7 @@ impl<'mir, 'tcx: 'mir, M: Machine<'mir, 'tcx>> InterpCx<'mir, 'tcx, M> {
     }
 
     pub fn fn_arg_field(
-        &mut self,
+        &self,
         arg: &FnArg<'tcx, M::Provenance>,
         field: usize,
     ) -> InterpResult<'tcx, FnArg<'tcx, M::Provenance>> {
@@ -239,7 +239,7 @@ impl<'mir, 'tcx: 'mir, M: Machine<'mir, 'tcx>> InterpCx<'mir, 'tcx, M> {
 
     /// Evaluate the arguments of a function call
     pub(super) fn eval_fn_call_arguments(
-        &mut self,
+        &self,
         ops: &[mir::Operand<'tcx>],
     ) -> InterpResult<'tcx, Vec<FnArg<'tcx, M::Provenance>>> {
         ops.iter()
@@ -382,12 +382,14 @@ impl<'mir, 'tcx: 'mir, M: Machine<'mir, 'tcx>> InterpCx<'mir, 'tcx, M> {
             // This all has to be in memory, there are no immediate unsized values.
             let src = caller_arg_copy.assert_mem_place();
             // The destination cannot be one of these "spread args".
-            let (dest_frame, dest_local) = callee_arg.assert_local();
+            let (dest_frame, dest_local, dest_offset) =
+                callee_arg.as_mplace_or_local().right().expect("calee fn arguments must be locals");
             // We are just initializing things, so there can't be anything here yet.
             assert!(matches!(
                 *self.local_to_op(&self.stack()[dest_frame], dest_local, None)?,
                 Operand::Immediate(Immediate::Uninit)
             ));
+            assert_eq!(dest_offset, None);
             // Allocate enough memory to hold `src`.
             let Some((size, align)) = self.size_and_align_of_mplace(&src)? else {
                 span_bug!(
diff --git a/compiler/rustc_const_eval/src/interpret/visitor.rs b/compiler/rustc_const_eval/src/interpret/visitor.rs
index 879ae198f7e..8d7c1953abe 100644
--- a/compiler/rustc_const_eval/src/interpret/visitor.rs
+++ b/compiler/rustc_const_eval/src/interpret/visitor.rs
@@ -78,14 +78,14 @@ pub trait ValueMut<'mir, 'tcx: 'mir, M: Machine<'mir, 'tcx>>: Sized {
     /// Projects to the given enum variant.
     fn project_downcast(
         &self,
-        ecx: &mut InterpCx<'mir, 'tcx, M>,
+        ecx: &InterpCx<'mir, 'tcx, M>,
         variant: VariantIdx,
     ) -> InterpResult<'tcx, Self>;
 
     /// Projects to the n-th field.
     fn project_field(
         &self,
-        ecx: &mut InterpCx<'mir, 'tcx, M>,
+        ecx: &InterpCx<'mir, 'tcx, M>,
         field: usize,
     ) -> InterpResult<'tcx, Self>;
 }
@@ -165,7 +165,7 @@ impl<'mir, 'tcx: 'mir, M: Machine<'mir, 'tcx>> ValueMut<'mir, 'tcx, M>
     #[inline(always)]
     fn project_downcast(
         &self,
-        ecx: &mut InterpCx<'mir, 'tcx, M>,
+        ecx: &InterpCx<'mir, 'tcx, M>,
         variant: VariantIdx,
     ) -> InterpResult<'tcx, Self> {
         ecx.operand_downcast(self, variant)
@@ -174,7 +174,7 @@ impl<'mir, 'tcx: 'mir, M: Machine<'mir, 'tcx>> ValueMut<'mir, 'tcx, M>
     #[inline(always)]
     fn project_field(
         &self,
-        ecx: &mut InterpCx<'mir, 'tcx, M>,
+        ecx: &InterpCx<'mir, 'tcx, M>,
         field: usize,
     ) -> InterpResult<'tcx, Self> {
         ecx.operand_field(self, field)
@@ -255,7 +255,7 @@ impl<'mir, 'tcx: 'mir, M: Machine<'mir, 'tcx>> ValueMut<'mir, 'tcx, M>
     #[inline(always)]
     fn project_downcast(
         &self,
-        ecx: &mut InterpCx<'mir, 'tcx, M>,
+        ecx: &InterpCx<'mir, 'tcx, M>,
         variant: VariantIdx,
     ) -> InterpResult<'tcx, Self> {
         ecx.mplace_downcast(self, variant)
@@ -264,7 +264,7 @@ impl<'mir, 'tcx: 'mir, M: Machine<'mir, 'tcx>> ValueMut<'mir, 'tcx, M>
     #[inline(always)]
     fn project_field(
         &self,
-        ecx: &mut InterpCx<'mir, 'tcx, M>,
+        ecx: &InterpCx<'mir, 'tcx, M>,
         field: usize,
     ) -> InterpResult<'tcx, Self> {
         ecx.mplace_field(self, field)
@@ -306,7 +306,7 @@ impl<'mir, 'tcx: 'mir, M: Machine<'mir, 'tcx>> ValueMut<'mir, 'tcx, M>
     #[inline(always)]
     fn project_downcast(
         &self,
-        ecx: &mut InterpCx<'mir, 'tcx, M>,
+        ecx: &InterpCx<'mir, 'tcx, M>,
         variant: VariantIdx,
     ) -> InterpResult<'tcx, Self> {
         ecx.place_downcast(self, variant)
@@ -315,7 +315,7 @@ impl<'mir, 'tcx: 'mir, M: Machine<'mir, 'tcx>> ValueMut<'mir, 'tcx, M>
     #[inline(always)]
     fn project_field(
         &self,
-        ecx: &mut InterpCx<'mir, 'tcx, M>,
+        ecx: &InterpCx<'mir, 'tcx, M>,
         field: usize,
     ) -> InterpResult<'tcx, Self> {
         ecx.place_field(self, field)
@@ -508,7 +508,7 @@ macro_rules! make_value_visitor {
                     }
                     FieldsShape::Array { .. } => {
                         // Let's get an mplace (or immediate) first.
-                        // This might `force_allocate` if `v` is a `PlaceTy`, but `place_index` does that anyway.
+                        // FIXME: This might `force_allocate` if `v` is a `PlaceTy`!
                         let op = v.to_op_for_proj(self.ecx())?;
                         // Now we can go over all the fields.
                         // This uses the *run-time length*, i.e., if we are a slice,