Skip to content

Commit d4a3d01

Browse files
committed
feat: extend copy propagation to locals
1 parent f3c0958 commit d4a3d01

File tree

16 files changed

+414
-70
lines changed

16 files changed

+414
-70
lines changed

libs/@local/hashql/mir/src/body/operand.rs

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
//! Operands represent the inputs to MIR operations. They can either reference
44
//! a storage location (place) or contain an immediate constant value.
55
6-
use super::{constant::Constant, place::Place};
6+
use super::{constant::Constant, local::Local, place::Place};
77

88
/// An operand in a HashQL MIR operation.
99
///
@@ -59,6 +59,12 @@ impl<'heap> Operand<'heap> {
5959
}
6060
}
6161

62+
impl From<Local> for Operand<'_> {
63+
fn from(local: Local) -> Self {
64+
Operand::Place(Place::local(local))
65+
}
66+
}
67+
6268
impl<'heap> From<Place<'heap>> for Operand<'heap> {
6369
fn from(place: Place<'heap>) -> Self {
6470
Operand::Place(place)

libs/@local/hashql/mir/src/body/place.rs

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -315,10 +315,11 @@ impl<'heap> Place<'heap> {
315315
/// This is the simplest form of a place, representing direct access to a local variable
316316
/// without navigating through any structured data. The resulting place has an empty
317317
/// projection sequence.
318-
pub fn local(local: Local, interner: &Interner<'heap>) -> Self {
318+
#[must_use]
319+
pub const fn local(local: Local) -> Self {
319320
Self {
320321
local,
321-
projections: interner.projections.intern_slice(&[]),
322+
projections: Interned::empty(),
322323
}
323324
}
324325

libs/@local/hashql/mir/src/builder/body.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -70,7 +70,7 @@ impl<'env, 'heap> BodyBuilder<'env, 'heap> {
7070
};
7171
let local = self.local_decls.push(decl);
7272

73-
Place::local(local, self.interner)
73+
Place::local(local)
7474
}
7575

7676
/// Reserves a new basic block and returns its ID.

libs/@local/hashql/mir/src/pass/analysis/data_dependency/resolve.rs

Lines changed: 6 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -299,7 +299,7 @@ fn resolve_params_const<'heap, A: Allocator + Clone>(
299299
} else {
300300
// We have finished (we have terminated on a param, which is divergent, therefore the place
301301
// is still valid, just doesn't have a constant value)
302-
ResolutionResult::Resolved(Operand::Place(Place::local(place.local, state.interner)))
302+
ResolutionResult::Resolved(Operand::Place(Place::local(place.local)))
303303
}
304304
}
305305

@@ -382,15 +382,14 @@ pub(crate) fn resolve<'heap, A: Allocator + Clone>(
382382
[] => {
383383
// Base case: no more projections to resolve.
384384
// Check for constant propagation through Load.
385-
let operand = if let Some(constant) = state
385+
let operand = state
386386
.graph
387387
.constant_bindings
388388
.find_by_kind(place.local, EdgeKind::Load)
389-
{
390-
Operand::Constant(constant)
391-
} else {
392-
Operand::Place(Place::local(place.local, state.interner))
393-
};
389+
.map_or_else(
390+
|| Operand::Place(Place::local(place.local)),
391+
Operand::Constant,
392+
);
394393

395394
return ResolutionResult::Resolved(operand);
396395
}

libs/@local/hashql/mir/src/pass/transform/administrative_reduction/visitor.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -221,7 +221,7 @@ impl<'heap, A: Allocator> AdministrativeReductionVisitor<'_, '_, 'heap, A> {
221221
.enumerate()
222222
.map(|(param, argument)| Statement {
223223
kind: StatementKind::Assign(Assign {
224-
lhs: Place::local(Local::new(local_offset + param), self.interner),
224+
lhs: Place::local(Local::new(local_offset + param)),
225225
rhs: RValue::Load(argument),
226226
}),
227227
span,

libs/@local/hashql/mir/src/pass/transform/cfg_simplify/mod.rs

Lines changed: 3 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -122,12 +122,7 @@ impl<A: BumpAllocator> CfgSimplify<A> {
122122
/// 3. Replace `A`'s terminator with `B`'s terminator
123123
///
124124
/// SSA invariants may be temporarily broken; the [`SsaRepair`] runs afterward to fix them.
125-
fn simplify_goto<'heap>(
126-
context: &MirContext<'_, 'heap>,
127-
body: &mut Body<'heap>,
128-
id: BasicBlockId,
129-
goto: Goto<'heap>,
130-
) -> bool {
125+
fn simplify_goto<'heap>(body: &mut Body<'heap>, id: BasicBlockId, goto: Goto<'heap>) -> bool {
131126
// Self-loops cannot be optimized as there's no simplification possible.
132127
if goto.target.block == id {
133128
return false;
@@ -170,7 +165,7 @@ impl<A: BumpAllocator> CfgSimplify<A> {
170165
block.statements.push(Statement {
171166
span: block.terminator.span,
172167
kind: StatementKind::Assign(Assign {
173-
lhs: Place::local(param, context.interner),
168+
lhs: Place::local(param),
174169
rhs: RValue::Load(arg),
175170
}),
176171
});
@@ -424,7 +419,7 @@ impl<A: BumpAllocator> CfgSimplify<A> {
424419
.transfer_into(&self.alloc);
425420

426421
let changed = match &body.basic_blocks[id].terminator.kind {
427-
&TerminatorKind::Goto(goto) => Self::simplify_goto(context, body, id, goto),
422+
&TerminatorKind::Goto(goto) => Self::simplify_goto(body, id, goto),
428423
TerminatorKind::SwitchInt(_) => Self::simplify_switch_int(context, body, id),
429424
TerminatorKind::Return(_)
430425
| TerminatorKind::GraphRead(_)

libs/@local/hashql/mir/src/pass/transform/copy_propagation/mod.rs

Lines changed: 64 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -1,27 +1,45 @@
11
//! Copy and constant propagation transformation pass.
22
//!
3-
//! This pass propagates constant values through the MIR by tracking which locals hold known
4-
//! constants and substituting uses of those locals with the constant values directly.
3+
//! This pass propagates both constants and copies through the MIR by tracking which locals hold
4+
//! known values (either constants or references to other locals) and substituting uses accordingly.
55
//!
66
//! Unlike [`ForwardSubstitution`], this pass does not perform full data dependency analysis and
77
//! cannot resolve values through projections or chained access paths. It is faster but less
8-
//! comprehensive, making it suitable for quick constant folding in simpler cases.
8+
//! comprehensive, making it suitable for quick propagation in simpler cases.
99
//!
1010
//! # Algorithm
1111
//!
1212
//! The pass operates in a single forward traversal (reverse postorder):
1313
//!
14-
//! 1. For each block, propagates constants through block parameters when all predecessors pass the
15-
//! same constant value
16-
//! 2. For each assignment `_x = <operand>`, if the operand is a constant or a local known to hold a
17-
//! constant, records that `_x` holds that constant
18-
//! 3. For each use of a local known to hold a constant, substitutes the use with the constant
14+
//! 1. For each block, propagates values through block parameters when all predecessors pass the
15+
//! same value (constant or local)
16+
//! 2. For each assignment `_x = <operand>`, records what `_x` holds:
17+
//! - If the operand is a constant, records `_x → constant`
18+
//! - If the operand is a local (possibly with a known value), records `_x → known value`
19+
//! 3. For each use of a local with a known value, substitutes the use with that value
20+
//!
21+
//! # Examples
22+
//!
23+
//! Constant propagation:
24+
//! ```text
25+
//! _1 = const 42; use(_1) → _1 = const 42; use(const 42)
26+
//! ```
27+
//!
28+
//! Copy propagation:
29+
//! ```text
30+
//! _2 = _1; use(_2) → _2 = _1; use(_1)
31+
//! ```
32+
//!
33+
//! Chained propagation:
34+
//! ```text
35+
//! _2 = _1; _3 = _2; use(_3) → _2 = _1; _3 = _1; use(_1)
36+
//! ```
1937
//!
2038
//! # Limitations
2139
//!
2240
//! - Does not handle projections: `_2 = (_1,); use(_2.0)` is not simplified
2341
//! - Does not perform fix-point iteration for loops
24-
//! - Only tracks constants, not arbitrary value equivalences
42+
//! - Assumes SSA-like semantics (locals are assigned at most once)
2543
//!
2644
//! For more comprehensive value propagation including projections, see [`ForwardSubstitution`].
2745
//!
@@ -46,6 +64,7 @@ use crate::{
4664
local::{Local, LocalVec},
4765
location::Location,
4866
operand::Operand,
67+
place::Place,
4968
rvalue::RValue,
5069
statement::Assign,
5170
},
@@ -165,6 +184,21 @@ where
165184
.flatten()
166185
}
167186

187+
#[derive(Debug, Copy, Clone, PartialEq, Eq, Hash)]
188+
enum KnownValue<'heap> {
189+
Constant(Constant<'heap>),
190+
Local(Local),
191+
}
192+
193+
impl<'heap> From<KnownValue<'heap>> for Operand<'heap> {
194+
fn from(value: KnownValue<'heap>) -> Self {
195+
match value {
196+
KnownValue::Constant(constant) => Operand::Constant(constant),
197+
KnownValue::Local(local) => Operand::Place(Place::local(local)),
198+
}
199+
}
200+
}
201+
168202
pub struct CopyPropagation<A: BumpAllocator = Scratch> {
169203
alloc: A,
170204
}
@@ -196,7 +230,7 @@ impl<'env, 'heap, A: ResetAllocator> TransformPass<'env, 'heap> for CopyPropagat
196230

197231
let mut visitor = CopyPropagationVisitor {
198232
interner: context.interner,
199-
constants: IdVec::with_capacity_in(body.local_decls.len(), &self.alloc),
233+
values: IdVec::with_capacity_in(body.local_decls.len(), &self.alloc),
200234
changed: false,
201235
};
202236

@@ -208,10 +242,10 @@ impl<'env, 'heap, A: ResetAllocator> TransformPass<'env, 'heap> for CopyPropagat
208242
let mut args = Vec::new_in(&self.alloc);
209243

210244
for &mut id in reverse_postorder {
211-
for (local, constant) in
245+
for (local, value) in
212246
propagate_block_params(&mut args, body, id, |operand| visitor.try_eval(operand))
213247
{
214-
visitor.constants.insert(local, constant);
248+
visitor.values.insert(local, value);
215249
}
216250

217251
Ok(()) =
@@ -224,7 +258,7 @@ impl<'env, 'heap, A: ResetAllocator> TransformPass<'env, 'heap> for CopyPropagat
224258

225259
struct CopyPropagationVisitor<'env, 'heap, A: Allocator> {
226260
interner: &'env Interner<'heap>,
227-
constants: LocalVec<Option<Constant<'heap>>, A>,
261+
values: LocalVec<Option<KnownValue<'heap>>, A>,
228262
changed: bool,
229263
}
230264

@@ -233,19 +267,21 @@ impl<'heap, A: Allocator> CopyPropagationVisitor<'_, 'heap, A> {
233267
///
234268
/// Returns `Int` if the operand is a constant integer or a local known to hold one,
235269
/// `Place` if it's a non-constant place, or `Other` for operands that can't be simplified.
236-
fn try_eval(&self, operand: Operand<'heap>) -> Option<Constant<'heap>> {
237-
if let Operand::Constant(constant) = operand {
238-
return Some(constant);
270+
fn try_eval(&self, operand: Operand<'heap>) -> Option<KnownValue<'heap>> {
271+
let place = match operand {
272+
Operand::Place(place) => place,
273+
Operand::Constant(constant) => return Some(KnownValue::Constant(constant)),
274+
};
275+
276+
if !place.projections.is_empty() {
277+
return None;
239278
}
240279

241-
if let Operand::Place(place) = operand
242-
&& place.projections.is_empty()
243-
&& let Some(&constant) = self.constants.lookup(place.local)
244-
{
245-
return Some(constant);
280+
if let Some(&known) = self.values.lookup(place.local) {
281+
return Some(known);
246282
}
247283

248-
None
284+
Some(KnownValue::Local(place.local))
249285
}
250286
}
251287

@@ -262,12 +298,10 @@ impl<'heap, A: Allocator> VisitorMut<'heap> for CopyPropagationVisitor<'_, 'heap
262298
}
263299

264300
fn visit_operand(&mut self, _: Location, operand: &mut Operand<'heap>) -> Self::Result<()> {
265-
if let Operand::Place(place) = operand
266-
&& place.projections.is_empty()
267-
&& let Some(&constant) = self.constants.lookup(place.local)
268-
{
269-
*operand = Operand::Constant(constant);
270-
self.changed = true;
301+
if let Some(known) = self.try_eval(*operand) {
302+
let known: Operand<'heap> = known.into();
303+
self.changed |= known != *operand;
304+
*operand = known;
271305
}
272306

273307
Ok(())
@@ -292,16 +326,8 @@ impl<'heap, A: Allocator> VisitorMut<'heap> for CopyPropagationVisitor<'_, 'heap
292326
return Ok(());
293327
};
294328

295-
match load {
296-
Operand::Place(place) if place.projections.is_empty() => {
297-
if let Some(&constant) = self.constants.lookup(place.local) {
298-
self.constants.insert(lhs.local, constant);
299-
}
300-
}
301-
Operand::Place(_) => {}
302-
&mut Operand::Constant(constant) => {
303-
self.constants.insert(lhs.local, constant);
304-
}
329+
if let Some(known) = self.try_eval(*load) {
330+
self.values.insert(lhs.local, known);
305331
}
306332

307333
Ok(())

0 commit comments

Comments
 (0)