Auto merge of #144347 - scottmcm:ssa-enums-v0, r=WaffleLapkin

No longer need `alloca`s for consuming `Result<!, i32>` and similar

In optimized builds GVN gets rid of these already, but in `opt-level=0` we actually make `alloca`s for this, which particularly impacts `?`-style things that use actually-only-one-variant types like this.

While doing so, rewrite `LocalAnalyzer::process_place` to be non-recursive, solving a 6+ year old FIXME.

r? codegen
This commit is contained in:
bors
2025-07-27 06:37:55 +00:00
3 changed files with 111 additions and 65 deletions
+64 -51
View File
@@ -1,12 +1,13 @@
//! An analysis to determine which locals require allocas and
//! which do not.
use rustc_abi as abi;
use rustc_data_structures::graph::dominators::Dominators;
use rustc_index::bit_set::DenseBitSet;
use rustc_index::{IndexSlice, IndexVec};
use rustc_middle::mir::visit::{MutatingUseContext, NonMutatingUseContext, PlaceContext, Visitor};
use rustc_middle::mir::{self, DefLocation, Location, TerminatorKind, traversal};
use rustc_middle::ty::layout::{HasTyCtxt, LayoutOf};
use rustc_middle::ty::layout::LayoutOf;
use rustc_middle::{bug, span_bug};
use tracing::debug;
@@ -99,63 +100,75 @@ fn process_place(
context: PlaceContext,
location: Location,
) {
let cx = self.fx.cx;
if !place_ref.projection.is_empty() {
const COPY_CONTEXT: PlaceContext =
PlaceContext::NonMutatingUse(NonMutatingUseContext::Copy);
if let Some((place_base, elem)) = place_ref.last_projection() {
let mut base_context = if context.is_mutating_use() {
PlaceContext::MutatingUse(MutatingUseContext::Projection)
} else {
PlaceContext::NonMutatingUse(NonMutatingUseContext::Projection)
};
// Allow uses of projections that are ZSTs or from scalar fields.
let is_consume = matches!(
context,
PlaceContext::NonMutatingUse(
NonMutatingUseContext::Copy | NonMutatingUseContext::Move,
)
);
if is_consume {
let base_ty = place_base.ty(self.fx.mir, cx.tcx());
let base_ty = self.fx.monomorphize(base_ty);
// ZSTs don't require any actual memory access.
let elem_ty = base_ty.projection_ty(cx.tcx(), self.fx.monomorphize(elem)).ty;
let span = self.fx.mir.local_decls[place_ref.local].source_info.span;
if cx.spanned_layout_of(elem_ty, span).is_zst() {
return;
// `PlaceElem::Index` is the only variant that can mention other `Local`s,
// so check for those up-front before any potential short-circuits.
for elem in place_ref.projection {
if let mir::PlaceElem::Index(index_local) = *elem {
self.visit_local(index_local, COPY_CONTEXT, location);
}
}
if let mir::ProjectionElem::Field(..) = elem {
let layout = cx.spanned_layout_of(base_ty.ty, span);
if cx.is_backend_immediate(layout) || cx.is_backend_scalar_pair(layout) {
// Recurse with the same context, instead of `Projection`,
// potentially stopping at non-operand projections,
// which would trigger `not_ssa` on locals.
base_context = context;
// If our local is already memory, nothing can make it *more* memory
// so we don't need to bother checking the projections further.
if self.locals[place_ref.local] == LocalKind::Memory {
return;
}
if place_ref.is_indirect_first_projection() {
// If this starts with a `Deref`, we only need to record a read of the
// pointer being dereferenced, as all the subsequent projections are
// working on a place which is always supported. (And because we're
// looking at codegen MIR, it can only happen as the first projection.)
self.visit_local(place_ref.local, COPY_CONTEXT, location);
return;
}
if context.is_mutating_use() {
// If it's a mutating use it doesn't matter what the projections are,
// if there are *any* then we need a place to write. (For example,
// `_1 = Foo()` works in SSA but `_2.0 = Foo()` does not.)
let mut_projection = PlaceContext::MutatingUse(MutatingUseContext::Projection);
self.visit_local(place_ref.local, mut_projection, location);
return;
}
// Scan through to ensure the only projections are those which
// `FunctionCx::maybe_codegen_consume_direct` can handle.
let base_ty = self.fx.monomorphized_place_ty(mir::PlaceRef::from(place_ref.local));
let mut layout = self.fx.cx.layout_of(base_ty);
for elem in place_ref.projection {
layout = match *elem {
mir::PlaceElem::Field(fidx, ..) => layout.field(self.fx.cx, fidx.as_usize()),
mir::PlaceElem::Downcast(_, vidx)
if let abi::Variants::Single { index: single_variant } =
layout.variants
&& vidx == single_variant =>
{
layout.for_variant(self.fx.cx, vidx)
}
mir::PlaceElem::Subtype(subtype_ty) => {
let subtype_ty = self.fx.monomorphize(subtype_ty);
self.fx.cx.layout_of(subtype_ty)
}
_ => {
self.locals[place_ref.local] = LocalKind::Memory;
return;
}
}
}
if let mir::ProjectionElem::Deref = elem {
// Deref projections typically only read the pointer.
base_context = PlaceContext::NonMutatingUse(NonMutatingUseContext::Copy);
}
self.process_place(&place_base, base_context, location);
// HACK(eddyb) this emulates the old `visit_projection_elem`, this
// entire `visit_place`-like `process_place` method should be rewritten,
// now that we have moved to the "slice of projections" representation.
if let mir::ProjectionElem::Index(local) = elem {
self.visit_local(
local,
PlaceContext::NonMutatingUse(NonMutatingUseContext::Copy),
location,
);
}
} else {
self.visit_local(place_ref.local, context, location);
debug_assert!(
!self.fx.cx.is_backend_ref(layout),
"Post-projection {place_ref:?} layout should be non-Ref, but it's {layout:?}",
);
}
// Even with supported projections, we still need to have `visit_local`
// check for things that can't be done in SSA (like `SharedBorrow`).
self.visit_local(place_ref.local, context, location);
}
}
+16 -14
View File
@@ -921,9 +921,10 @@ fn maybe_codegen_consume_direct(
match self.locals[place_ref.local] {
LocalRef::Operand(mut o) => {
// Moves out of scalar and scalar pair fields are trivial.
for elem in place_ref.projection.iter() {
match elem {
// We only need to handle the projections that
// `LocalAnalyzer::process_place` let make it here.
for elem in place_ref.projection {
match *elem {
mir::ProjectionElem::Field(f, _) => {
assert!(
!o.layout.ty.is_any_ptr(),
@@ -932,17 +933,18 @@ fn maybe_codegen_consume_direct(
);
o = o.extract_field(self, bx, f.index());
}
mir::ProjectionElem::Index(_)
| mir::ProjectionElem::ConstantIndex { .. } => {
// ZSTs don't require any actual memory access.
// FIXME(eddyb) deduplicate this with the identical
// checks in `codegen_consume` and `extract_field`.
let elem = o.layout.field(bx.cx(), 0);
if elem.is_zst() {
o = OperandRef::zero_sized(elem);
} else {
return None;
}
mir::PlaceElem::Downcast(_, vidx) => {
debug_assert_eq!(
o.layout.variants,
abi::Variants::Single { index: vidx },
);
let layout = o.layout.for_variant(bx.cx(), vidx);
o = OperandRef { val: o.val, layout }
}
mir::PlaceElem::Subtype(subtype_ty) => {
let subtype_ty = self.monomorphize(subtype_ty);
let layout = self.cx.layout_of(subtype_ty);
o = OperandRef { val: o.val, layout }
}
_ => return None,
}
@@ -0,0 +1,31 @@
//@ compile-flags: -Copt-level=0
//@ only-64bit
#![crate_type = "lib"]
use std::ops::ControlFlow;
pub enum Never {}
#[no_mangle]
pub fn make_unmake_result_never(x: i32) -> i32 {
// CHECK-LABEL: define i32 @make_unmake_result_never(i32 %x)
// CHECK: start:
// CHECK-NEXT: ret i32 %x
let y: Result<i32, Never> = Ok(x);
let Ok(z) = y;
z
}
#[no_mangle]
pub fn extract_control_flow_never(x: ControlFlow<&str, Never>) -> &str {
// CHECK-LABEL: define { ptr, i64 } @extract_control_flow_never(ptr align 1 %x.0, i64 %x.1)
// CHECK: start:
// CHECK-NEXT: %[[P0:.+]] = insertvalue { ptr, i64 } poison, ptr %x.0, 0
// CHECK-NEXT: %[[P1:.+]] = insertvalue { ptr, i64 } %[[P0]], i64 %x.1, 1
// CHECK-NEXT: ret { ptr, i64 } %[[P1]]
let ControlFlow::Break(s) = x;
s
}