Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
27 commits
Select commit Hold shift + click to select a range
1ddeed6
Initialize ZST results in MIR construction and lowering
Amanieu Sep 25, 2026
53efecc
SimplifyMatch: evaluate all constants instead of just scalars
Amanieu Sep 25, 2026
7c30889
Preserve direct ZST writes in RemoveZsts
Amanieu Sep 25, 2026
0d66911
Don't hardcode basic block names in hint codegen tests
Amanieu Sep 26, 2026
e53461b
Bless incremental tests
Amanieu Sep 26, 2026
c1901f7
[DO NOT MERGE] Work around test failure by adjusting inliner threshold
Amanieu Sep 26, 2026
34f8c05
Re-introduce ResultsVisitor::visit_block_exit
Amanieu Sep 23, 2026
1c34f63
Add precise MIR liveness analysis
Amanieu Sep 23, 2026
3e531ec
Remove incorrect comments about Rvalue::Aggregate not being allowed
Amanieu Sep 23, 2026
e61dd0d
Add MIR move elimination pass
Amanieu Sep 23, 2026
9157f5e
Add supporting APIs for MIR storage reconstruction
Amanieu Sep 23, 2026
eab2d36
Reconstruct storage lifetimes after move elimination
Amanieu Sep 23, 2026
7a51296
Document move-elimination semantics
Amanieu Sep 23, 2026
349a03a
Add MIR StorageAlloc statement
Amanieu Sep 23, 2026
1decd8f
Use StorageAlloc before borrowing uninitialized locals
Amanieu Sep 23, 2026
373e47e
Use Copy for resume_place in async drop elaboration
Amanieu Sep 23, 2026
b120d20
Emit StorageAlloc for sibling fields in SROA
Amanieu Sep 27, 2026
f6837a1
Emit StorageAlloc for direct call destinations when inlining
Amanieu Sep 27, 2026
4d63be2
Allow dead store elimination to remove StorageAlloc
Amanieu Sep 27, 2026
2b9027a
Add more test coverage for StorageAlloc
Amanieu Sep 28, 2026
b8b68b1
Re-bless tests
Amanieu Sep 28, 2026
dc6558c
Reject StorageAlloc in MaybeRequiresStorage
Amanieu Sep 28, 2026
2d1d941
Merge branch 'move-elimination/zst-initialization' into move-eliminat…
Amanieu Sep 30, 2026
2683066
Protect in-place variadic arguments in Miri
Amanieu Sep 23, 2026
2193267
Separate evaluation of rvalues from assignment evaluation
Amanieu Sep 23, 2026
a3f2031
Ensure all call destinations are uninitialized on unwind
Amanieu Sep 23, 2026
f686996
Add Miri support for move elimination semantics
Amanieu Sep 23, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
The table of contents is too big for display.
Diff view
Diff view
  •  
  •  
  •  
1 change: 1 addition & 0 deletions compiler/rustc_borrowck/src/dataflow.rs
Original file line number Diff line number Diff line change
Expand Up @@ -530,6 +530,7 @@ impl<'tcx> rustc_mir_dataflow::Analysis<'tcx> for Borrows<'_, 'tcx> {

mir::StatementKind::FakeRead(..)
| mir::StatementKind::SetDiscriminant { .. }
| mir::StatementKind::StorageAlloc(_)
| mir::StatementKind::StorageLive(..)
| mir::StatementKind::PlaceMention(..)
| mir::StatementKind::AscribeUserType(..)
Expand Down
3 changes: 2 additions & 1 deletion compiler/rustc_borrowck/src/def_use.rs
Original file line number Diff line number Diff line change
Expand Up @@ -76,7 +76,8 @@ pub(crate) fn categorize(context: PlaceContext) -> Option<DefUse> {
// Backwards incompatible drop hint is not a use, just a marker for linting.
PlaceContext::NonUse(NonUseContext::BackwardIncompatibleDropHint) => None,

PlaceContext::MutatingUse(MutatingUseContext::SetDiscriminant) => {
PlaceContext::NonUse(NonUseContext::StorageAlloc)
| PlaceContext::MutatingUse(MutatingUseContext::SetDiscriminant) => {
bug!("These statements are not allowed in this MIR phase")
}
}
Expand Down
4 changes: 3 additions & 1 deletion compiler/rustc_borrowck/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -874,7 +874,9 @@ impl<'a, 'tcx> ResultsVisitor<'tcx, Borrowck<'a, 'tcx>> for MirBorrowckCtxt<'a,
state,
);
}
StatementKind::Nop | StatementKind::SetDiscriminant { .. } => {
StatementKind::Nop
| StatementKind::StorageAlloc(..)
| StatementKind::SetDiscriminant { .. } => {
bug!("Statement not allowed in this MIR phase")
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,7 @@ impl<'a, 'tcx> Visitor<'tcx> for LoanInvalidationsGenerator<'a, 'tcx> {
}
StatementKind::ConstEvalCounter
| StatementKind::Nop
| StatementKind::StorageAlloc(..)
| StatementKind::SetDiscriminant { .. } => {
bug!("Statement not allowed in this MIR phase")
}
Expand Down
1 change: 1 addition & 0 deletions compiler/rustc_borrowck/src/type_check/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -730,6 +730,7 @@ impl<'a, 'tcx> Visitor<'tcx> for TypeChecker<'a, 'tcx> {
| StatementKind::BackwardIncompatibleDropHint { .. }
| StatementKind::Nop => {}
StatementKind::Intrinsic(NonDivergingIntrinsic::CopyNonOverlapping(..))
| StatementKind::StorageAlloc(..)
| StatementKind::SetDiscriminant { .. } => {
bug!("Statement not allowed in this MIR phase")
}
Expand Down
7 changes: 5 additions & 2 deletions compiler/rustc_codegen_cranelift/src/base.rs
Original file line number Diff line number Diff line change
Expand Up @@ -614,7 +614,9 @@ fn codegen_stmt<'tcx>(fx: &mut FunctionCx<'_, '_, 'tcx>, cur_block: Block, stmt:
fx.set_debug_loc(stmt.source_info);

match &stmt.kind {
StatementKind::StorageLive(..) | StatementKind::StorageDead(..) => {} // Those are not very useful
StatementKind::StorageAlloc(..)
| StatementKind::StorageLive(..)
| StatementKind::StorageDead(..) => {} // Those are not very useful
_ => {
if fx.clif_comments.enabled() {
let inst = fx.bcx.func.layout.last_inst(cur_block).unwrap();
Expand Down Expand Up @@ -928,7 +930,8 @@ fn codegen_stmt<'tcx>(fx: &mut FunctionCx<'_, '_, 'tcx>, cur_block: Block, stmt:
Rvalue::CopyForDeref(_) => bug!("`CopyForDeref` in codegen"),
}
}
StatementKind::StorageLive(_)
StatementKind::StorageAlloc(_)
| StatementKind::StorageLive(_)
| StatementKind::StorageDead(_)
| StatementKind::ConstEvalCounter
| StatementKind::Nop
Expand Down
1 change: 1 addition & 0 deletions compiler/rustc_codegen_cranelift/src/constant.rs
Original file line number Diff line number Diff line change
Expand Up @@ -571,6 +571,7 @@ pub(crate) fn mir_operand_get_const_val<'tcx>(
StatementKind::Assign(_)
| StatementKind::FakeRead(_)
| StatementKind::SetDiscriminant { .. }
| StatementKind::StorageAlloc(_)
| StatementKind::StorageLive(_)
| StatementKind::StorageDead(_)
| StatementKind::AscribeUserType(_, _)
Expand Down
3 changes: 3 additions & 0 deletions compiler/rustc_codegen_ssa/src/mir/statement.rs
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,9 @@ impl<'a, 'tcx, Bx: BuilderMethods<'a, 'tcx>> FunctionCx<'a, 'tcx, Bx> {
mir::StatementKind::SetDiscriminant { ref place, variant_index } => {
self.codegen_place(bx, (**place).as_ref()).codegen_set_discr(bx, variant_index);
}
mir::StatementKind::StorageAlloc(_) => {
// This is a no-op in codegen.
}
mir::StatementKind::StorageLive(local) => {
if let LocalRef::Place(cg_place) = self.locals[local] {
cg_place.storage_live(bx);
Expand Down
1 change: 1 addition & 0 deletions compiler/rustc_const_eval/src/check_consts/check.rs
Original file line number Diff line number Diff line change
Expand Up @@ -707,6 +707,7 @@ impl<'tcx> Visitor<'tcx> for Checker<'_, 'tcx> {
StatementKind::Assign(..)
| StatementKind::SetDiscriminant { .. }
| StatementKind::FakeRead(..)
| StatementKind::StorageAlloc(_)
| StatementKind::StorageLive(_)
| StatementKind::StorageDead(_)
| StatementKind::PlaceMention(..)
Expand Down
6 changes: 4 additions & 2 deletions compiler/rustc_const_eval/src/const_eval/machine.rs
Original file line number Diff line number Diff line change
Expand Up @@ -941,8 +941,10 @@ impl<'tcx> interpret::Machine<'tcx> for CompileTimeMachine<'tcx> {
) -> InterpResult<'tcx> {
use rustc_middle::mir::AssertKind::*;
// Convert `AssertKind<Operand>` to `AssertKind<Scalar>`.
let eval_to_int =
|op| ecx.read_immediate(&ecx.eval_operand(op, None)?).map(|x| x.to_const_int());
let mut eval_to_int = |op| {
let op = ecx.eval_operand(op, None)?;
ecx.read_immediate(&op).map(|x| x.to_const_int())
};
let err = match msg {
BoundsCheck { len, index } => {
let len = eval_to_int(len)?;
Expand Down
36 changes: 24 additions & 12 deletions compiler/rustc_const_eval/src/interpret/call.rs
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
//! Manages calling a concrete function (with known MIR body) with argument passing,
//! and returning the return value to the caller.

use std::assert_matches;
use std::borrow::Cow;
use std::{assert_matches, debug_assert_matches};

use either::{Left, Right};
use rustc_abi::{self as abi, ExternAbi, FieldIdx, Integer, VariantIdx};
Expand Down Expand Up @@ -432,8 +432,8 @@ impl<'tcx, M: Machine<'tcx>> InterpCx<'tcx, M> {
self.storage_live_dyn(local, meta)?;
}
// Now we can finally actually evaluate the callee place.
let callee_arg =
self.eval_place(*callee_arg, /* skip_validity_for_simple_deref */ false)?;
let callee_arg = self
.eval_place_for_write(*callee_arg, /* skip_validity_for_simple_deref */ false)?;
// We allow some transmutes here.
// FIXME: Depending on the PassMode, this should reset some padding to uninitialized. (This
// is true for all `copy_op`, but there are a lot of special cases for argument passing
Expand Down Expand Up @@ -516,6 +516,12 @@ impl<'tcx, M: Machine<'tcx>> InterpCx<'tcx, M> {
// *Before* pushing the new frame, determine whether the return destination is in memory.
// Need to use `place_to_op` to be *sure* we get the mplace if there is one.
let destination_mplace = self.place_to_op(destination)?.as_mplace_or_imm().left();
if destination_mplace.is_none() {
// Protection below clears in-memory destinations. Clear immediate destinations too,
// while we can still access the caller's locals. Evaluated arguments already hold
// copies of any immediate values, so this cannot affect argument passing.
self.write_uninit(destination)?;
}

// Push the "raw" frame -- this leaves locals uninitialized.
self.push_stack_frame_raw(instance, body, destination, cont)?;
Expand Down Expand Up @@ -599,6 +605,7 @@ impl<'tcx, M: Machine<'tcx>> InterpCx<'tcx, M> {
let (callee_arg_idx, callee_abi) = callee_args_abis.next().unwrap();
assert!(callee_abi.layout.is_1zst() && callee_abi.is_ignore());
ecx.storage_live(local)?;
ecx.allocate_local_for_write(local)?;
// And skip it in the caller, if present. We can tell whether it is present by
// comparing the number of arguments on the caller and callee side.
if caller_fn_abi.args.len() == callee_fn_abi.args.len() {
Expand All @@ -618,8 +625,9 @@ impl<'tcx, M: Machine<'tcx>> InterpCx<'tcx, M> {
// This argument is a VaList holding the remaining caller-side arguments.
ecx.storage_live(local)?;

let place =
ecx.eval_place(dest, /* skip_validity_for_simple_deref */ false)?;
let place = ecx.eval_place_for_write(
dest, /* skip_validity_for_simple_deref */ false,
)?;
let mplace = ecx.force_allocation(&place)?;

// Consume the remaining arguments by putting them into the variable argument
Expand All @@ -645,6 +653,9 @@ impl<'tcx, M: Machine<'tcx>> InterpCx<'tcx, M> {
} else if Some(local) == body.spread_arg {
// Make the local live once, then fill in the value field by field.
ecx.storage_live(local)?;
// Function arguments start allocated, including an empty spread tuple for
// which the loop below has no fields to initialize.
ecx.allocate_local_for_write(local)?;
// Must be a tuple
let ty::Tuple(fields) = ty.kind() else {
span_bug!(ecx.cur_span(), "non-tuple type for `spread_arg`: {ty}")
Expand Down Expand Up @@ -997,10 +1008,11 @@ impl<'tcx, M: Machine<'tcx>> InterpCx<'tcx, M> {
// as that "executes" the goto to the return block, but we don't want to,
// only the tail called function should return to the current return block.

// The arguments need to all be copied since the current stack frame will be removed
// before the callee even starts executing.
// FIXME(explicit_tail_calls,#144855): does this match what codegen does?
let args = args.iter().map(|fn_arg| FnArg::Copy(fn_arg.copy_fn_arg())).collect::<Vec<_>>();
// Tail-call arguments are evaluated as ordinary operands, so none of them may donate a
// place in the frame that is about to be destroyed.
for arg in args {
debug_assert_matches!(arg, FnArg::Copy(_));
}
// Remove the frame from the stack.
let frame = self.pop_stack_frame_raw()?;
// Remember where this frame would have returned to.
Expand All @@ -1017,7 +1029,7 @@ impl<'tcx, M: Machine<'tcx>> InterpCx<'tcx, M> {
self.init_fn_call(
fn_val,
(caller_abi, caller_fn_abi),
&*args,
args,
with_caller_location,
frame.return_place(),
ret,
Expand Down Expand Up @@ -1127,11 +1139,11 @@ impl<'tcx, M: Machine<'tcx>> InterpCx<'tcx, M> {
// Get out the return value. Must happen *before* the frame is popped as we have to get the
// local's value out.
let return_op =
self.local_to_op(mir::RETURN_PLACE, None).expect("return place should always be live");
if unwinding { None } else { Some(self.local_to_op(mir::RETURN_PLACE, None)?) };
// Remove the frame from the stack.
let frame = self.pop_stack_frame_raw()?;
// Copy the return value and remember the return continuation.
if !unwinding {
if let Some(return_op) = return_op {
self.copy_op_allow_transmute(&return_op, frame.return_place())?;
trace!("return value: {:?}", self.dump_place(frame.return_place()));
}
Expand Down
7 changes: 7 additions & 0 deletions compiler/rustc_const_eval/src/interpret/eval_context.rs
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,12 @@ pub struct InterpCx<'tcx, M: Machine<'tcx>> {
/// The virtual memory system.
pub memory: Memory<'tcx, M>,

/// Temporary operand snapshots that survive deallocation of their source
/// local.
///
/// These are freed after each MIR statement or terminator.
pub(super) operand_temps: Vec<MPlaceTy<'tcx, M::Provenance>>,

/// The recursion limit (cached from `tcx.recursion_limit(())`)
pub recursion_limit: Limit,
}
Expand Down Expand Up @@ -254,6 +260,7 @@ impl<'tcx, M: Machine<'tcx>> InterpCx<'tcx, M> {
typing_env,
layout_cache: RefCell::new(FxHashMap::default()),
memory: Memory::new(),
operand_temps: Vec::new(),
recursion_limit: tcx.recursion_limit(),
}
}
Expand Down
6 changes: 6 additions & 0 deletions compiler/rustc_const_eval/src/interpret/machine.rs
Original file line number Diff line number Diff line change
Expand Up @@ -165,6 +165,12 @@ pub trait Machine<'tcx>: Sized {
/// Whether memory accesses should be alignment-checked.
fn enforce_alignment(ecx: &InterpCx<'tcx, Self>) -> bool;

/// Whether to enforce the local allocation semantics required by MIR move elimination.
#[inline(always)]
fn move_elimination_semantics(ecx: &InterpCx<'tcx, Self>) -> bool {
ecx.tcx.sess.opts.unstable_opts.mir_move_elimination
}

/// Gives the machine a chance to detect more misalignment than the built-in checks would catch.
#[inline(always)]
fn alignment_check(
Expand Down
82 changes: 73 additions & 9 deletions compiler/rustc_const_eval/src/interpret/operand.rs
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
//! Functions concerning immediate values and operands, and reading from operands.
//! All high-level functions to read from memory work on operands as sources.

use std::assert_matches;
use std::{assert_matches, mem};

use either::{Either, Left, Right};
use rustc_abi as abi;
Expand All @@ -17,7 +17,7 @@ use tracing::trace;

use super::{
CtfeProvenance, Frame, InterpCx, InterpResult, MPlaceTy, Machine, MemPlace, MemPlaceMeta,
OffsetMode, PlaceTy, Pointer, Projectable, Provenance, Scalar, alloc_range, err_ub,
MemoryKind, OffsetMode, PlaceTy, Pointer, Projectable, Provenance, Scalar, alloc_range, err_ub,
from_known_layout, interp_ok, mir_assign_valid_types, throw_ub,
};
use crate::enter_trace_span;
Expand Down Expand Up @@ -830,21 +830,85 @@ impl<'tcx, M: Machine<'tcx>> InterpCx<'tcx, M> {
interp_ok(op)
}

/// Evaluate the operand, returning a place where you can then find the data.
/// If you already know the layout, you can save two table lookups
/// by passing it in here.
/// Capture a fixed copy of an operand that is independent of any further
/// changes to its backing allocation.
pub(super) fn snapshot_operand(
&mut self,
op: OpTy<'tcx, M::Provenance>,
) -> InterpResult<'tcx, OpTy<'tcx, M::Provenance>> {
// Load into an immediate if possible.
if let Right(imm) = self.read_immediate_raw(&op)? {
return interp_ok(imm.into());
}

// Otherwise use a temporary allocation that is freed at the end of the
// machine step.
let temp = self.allocate(op.layout, MemoryKind::Stack)?;
self.copy_op_no_validate(&op, &temp, /*allow_transmute*/ false)?;
self.operand_temps.push(temp.clone());
interp_ok(temp.into())
}

/// Deallocate operand snapshots created in the current MIR step.
pub(super) fn clear_operand_temps(&mut self) -> InterpResult<'tcx> {
for temp in mem::take(&mut self.operand_temps) {
self.deallocate_ptr(temp.ptr(), None, MemoryKind::Stack)?;
}
interp_ok(())
}

/// Evaluate the operand, returning a place where you can then find the
/// data.
///
/// If you already know the layout, you can save two table lookups by
/// passing it in here.
///
/// Under move-elimination semantics, the result is a snapshot that remains
/// valid if later operand evaluation frees its source storage.
#[inline]
pub fn eval_operand(
&self,
&mut self,
mir_op: &mir::Operand<'tcx>,
layout: Option<TyAndLayout<'tcx>>,
) -> InterpResult<'tcx, OpTy<'tcx, M::Provenance>> {
let _trace =
enter_trace_span!(M, step::eval_operand, ?mir_op, tracing_separate_thread = Empty);
let op = self.eval_operand_no_snapshot(mir_op, layout)?;
// Whole-local moves already detached their values; constants cannot change.
let needs_snapshot = match mir_op {
mir::Operand::Copy(_) => true,
mir::Operand::Move(place) => !place.projection.is_empty(),
mir::Operand::Constant(_) | mir::Operand::RuntimeChecks(_) => false,
};
if M::move_elimination_semantics(self) && needs_snapshot {
self.snapshot_operand(op)
} else {
interp_ok(op)
}
}

/// Evaluate an operand without snapshotting.
///
/// Whole-local moves still copy out the value and deallocate the local.
///
/// The caller must not evaluate any other operand before consuming the
/// result, since those may cause this operand's backing local to be freed.
#[inline]
pub fn eval_operand_no_snapshot(
&mut self,
mir_op: &mir::Operand<'tcx>,
layout: Option<TyAndLayout<'tcx>>,
) -> InterpResult<'tcx, OpTy<'tcx, M::Provenance>> {
let _trace = enter_trace_span!(
M,
step::eval_operand_no_snapshot,
?mir_op,
tracing_separate_thread = Empty
);

use rustc_middle::mir::Operand::*;
let op = match mir_op {
// FIXME: do some more logic on `move` to invalidate the old location
&Move(place) if M::move_elimination_semantics(self) && place.projection.is_empty() => {
self.move_out_local(place.local, layout)?
}
&Copy(place) | &Move(place) => self.eval_place_to_op(place, layout)?,

&RuntimeChecks(checks) => {
Expand Down
13 changes: 13 additions & 0 deletions compiler/rustc_const_eval/src/interpret/place.rs
Original file line number Diff line number Diff line change
Expand Up @@ -644,6 +644,19 @@ where
interp_ok(place)
}

/// Computes a destination place, allocating its base local if it is currently live but without
/// an allocation.
pub fn eval_place_for_write(
&mut self,
mir_place: mir::Place<'tcx>,
skip_validity_for_simple_deref: bool,
) -> InterpResult<'tcx, PlaceTy<'tcx, M::Provenance>> {
if M::move_elimination_semantics(self) && !mir_place.is_indirect_first_projection() {
self.allocate_local_for_write(mir_place.local)?;
}
self.eval_place(mir_place, skip_validity_for_simple_deref)
}

/// Given a place, returns either the underlying mplace or a reference to where the value of
/// this place is stored.
#[inline(always)]
Expand Down
Loading
Loading