Skip to content

Commit abd5ba1

Browse files
authored
BE-258: HashQL: Add Changed enum to track MIR transformation pass modifications (#8210)
1 parent 86f263f commit abd5ba1

File tree

91 files changed

+321
-172
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

91 files changed

+321
-172
lines changed

.claude/skills/testing-hashql/SKILL.md

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -180,6 +180,12 @@ For testing MIR transformation and analysis passes directly with programmaticall
180180
- Edge cases requiring specific MIR structures hard to produce from source
181181
- Benchmarking pass performance
182182

183+
**Key features:**
184+
185+
- Transform passes return `Changed` enum (`Yes`, `No`, `Unknown`) to indicate modifications
186+
- Test harness captures and includes `Changed` value in snapshots for verification
187+
- Snapshot format: before MIR → `Changed: Yes/No/Unknown` separator → after MIR
188+
183189
**Quick Example:**
184190

185191
```rust
@@ -201,10 +207,10 @@ builder
201207
let body = builder.finish(0, TypeBuilder::synthetic(&env).integer());
202208
```
203209

204-
📖 **Full Guide:** [resources/mir-builder-guide.md](resources/mir-builder-guide.md)
210+
📖 **Full Guide:** [references/mir-builder-guide.md](references/mir-builder-guide.md)
205211

206212
## References
207213

208-
- [compiletest Guide](resources/compiletest-guide.md) - Detailed UI test documentation
209-
- [Testing Strategies](resources/testing-strategies.md) - Choosing the right approach
210-
- [MIR Builder Guide](resources/mir-builder-guide.md) - Programmatic MIR construction for tests
214+
- [compiletest Guide](references/compiletest-guide.md) - Detailed UI test documentation
215+
- [Testing Strategies](references/testing-strategies.md) - Choosing the right approach
216+
- [MIR Builder Guide](references/mir-builder-guide.md) - Programmatic MIR construction for tests

.claude/skills/testing-hashql/references/mir-builder-guide.md

Lines changed: 13 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -228,10 +228,11 @@ builder.build_block(bb_merge).ret(x); // x receives value from param
228228

229229
## Test Harness Pattern
230230

231-
Standard pattern used across transform pass tests:
231+
Standard pattern used across transform pass tests. The harness captures and displays
232+
the `Changed` return value to verify pass behavior:
232233

233234
```rust
234-
use std::path::PathBuf;
235+
use std::{io::Write as _, path::PathBuf};
235236
use bstr::ByteVec as _;
236237
use hashql_core::{
237238
pretty::Formatter,
@@ -274,12 +275,16 @@ fn assert_pass<'heap>(
274275
.format(DefIdSlice::from_raw(&bodies), &[])
275276
.expect("should be able to write bodies");
276277

277-
text_format
278-
.writer
279-
.extend(b"\n\n------------------------------------\n\n");
280-
281-
// Run the pass
282-
YourPass::new().run(context, &mut bodies[0]);
278+
// Run the pass and capture change status
279+
let changed = YourPass::new().run(context, &mut bodies[0]);
280+
281+
// Include Changed value in snapshot for verification
282+
write!(
283+
text_format.writer,
284+
"\n\n{:=^50}\n\n",
285+
format!(" Changed: {changed:?} ")
286+
)
287+
.expect("infallible");
283288

284289
// Format after
285290
text_format

libs/@local/hashql/compiletest/src/suite/mir_pass_transform_cfg_simplify.rs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ use hashql_mir::{
1414
context::MirContext,
1515
def::{DefId, DefIdSlice, DefIdVec},
1616
intern::Interner,
17-
pass::{TransformPass as _, transform::CfgSimplify},
17+
pass::{Changed, TransformPass as _, transform::CfgSimplify},
1818
};
1919

2020
use super::{RunContext, Suite, SuiteDiagnostic, common::process_issues, mir_reify::mir_reify};
@@ -63,7 +63,7 @@ pub(crate) fn mir_pass_transform_cfg_simplify<'heap>(
6363

6464
let mut pass = CfgSimplify::new_in(&mut scratch);
6565
for body in bodies.as_mut_slice() {
66-
pass.run(&mut context, body);
66+
let _: Changed = pass.run(&mut context, body);
6767
}
6868

6969
process_issues(diagnostics, context.diagnostics)?;

libs/@local/hashql/compiletest/src/suite/mir_pass_transform_dse.rs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ use hashql_mir::{
1111
context::MirContext,
1212
def::{DefId, DefIdSlice, DefIdVec},
1313
intern::Interner,
14-
pass::{TransformPass as _, transform::DeadStoreElimination},
14+
pass::{Changed, TransformPass as _, transform::DeadStoreElimination},
1515
};
1616

1717
use super::{
@@ -45,7 +45,7 @@ pub(crate) fn mir_pass_transform_dse<'heap>(
4545
// CFG -> SROA -> Inst -> DSE
4646
let mut pass = DeadStoreElimination::new_in(&mut scratch);
4747
for body in bodies.as_mut_slice() {
48-
pass.run(&mut context, body);
48+
let _: Changed = pass.run(&mut context, body);
4949
}
5050

5151
process_issues(diagnostics, context.diagnostics)?;

libs/@local/hashql/compiletest/src/suite/mir_pass_transform_inst_simplify.rs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ use hashql_mir::{
1111
context::MirContext,
1212
def::{DefId, DefIdSlice, DefIdVec},
1313
intern::Interner,
14-
pass::{TransformPass as _, transform::InstSimplify},
14+
pass::{Changed, TransformPass as _, transform::InstSimplify},
1515
};
1616

1717
use super::{
@@ -44,7 +44,7 @@ pub(crate) fn mir_pass_transform_inst_simplify<'heap>(
4444

4545
let mut pass = InstSimplify::new_in(&mut scratch);
4646
for body in bodies.as_mut_slice() {
47-
pass.run(&mut context, body);
47+
let _: Changed = pass.run(&mut context, body);
4848
}
4949

5050
process_issues(diagnostics, context.diagnostics)?;

libs/@local/hashql/compiletest/src/suite/mir_pass_transform_sroa.rs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ use hashql_mir::{
1111
context::MirContext,
1212
def::{DefId, DefIdSlice, DefIdVec},
1313
intern::Interner,
14-
pass::{TransformPass as _, transform::Sroa},
14+
pass::{Changed, TransformPass as _, transform::Sroa},
1515
};
1616

1717
use super::{
@@ -44,7 +44,7 @@ pub(crate) fn mir_pass_transform_sroa<'heap>(
4444

4545
let mut pass = Sroa::new_in(&mut scratch);
4646
for body in bodies.as_mut_slice() {
47-
pass.run(&mut context, body);
47+
let _: Changed = pass.run(&mut context, body);
4848
}
4949

5050
process_issues(diagnostics, context.diagnostics)?;

libs/@local/hashql/mir/benches/transform.rs

Lines changed: 32 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
clippy::similar_names
66
)]
77

8-
use core::hint::black_box;
8+
use core::{cmp, hint::black_box};
99

1010
use codspeed_criterion_compat::{BatchSize, Bencher, Criterion, criterion_group, criterion_main};
1111
use hashql_core::{
@@ -301,10 +301,10 @@ fn create_complex_cfg<'heap>(env: &Environment<'heap>, interner: &Interner<'heap
301301

302302
#[expect(unsafe_code)]
303303
#[inline]
304-
fn run_bencher(
304+
fn run_bencher<T>(
305305
bencher: &mut Bencher,
306306
body: for<'heap> fn(&Environment<'heap>, &Interner<'heap>) -> Body<'heap>,
307-
mut func: impl for<'env, 'heap> FnMut(&mut MirContext<'env, 'heap>, &mut Body<'heap>),
307+
mut func: impl for<'env, 'heap> FnMut(&mut MirContext<'env, 'heap>, &mut Body<'heap>) -> T,
308308
) {
309309
// NOTE: `heap` must not be moved or reassigned; `heap_ptr` assumes its address is stable
310310
// for the entire duration of this function.
@@ -348,8 +348,8 @@ fn run_bencher(
348348
diagnostics: DiagnosticIssues::new(),
349349
};
350350

351-
func(black_box(&mut context), black_box(body));
352-
context.diagnostics
351+
let value = func(black_box(&mut context), black_box(body));
352+
(context.diagnostics, value)
353353
},
354354
BatchSize::PerIteration,
355355
);
@@ -440,30 +440,45 @@ fn pipeline(criterion: &mut Criterion) {
440440
let mut scratch = Scratch::new();
441441

442442
run_bencher(bencher, create_linear_cfg, |context, body| {
443-
CfgSimplify::new_in(&mut scratch).run(context, body);
444-
Sroa::new_in(&mut scratch).run(context, body);
445-
InstSimplify::new().run(context, body);
446-
DeadStoreElimination::new_in(&mut scratch).run(context, body);
443+
let mut changed = CfgSimplify::new_in(&mut scratch).run(context, body);
444+
changed = cmp::max(changed, Sroa::new_in(&mut scratch).run(context, body));
445+
changed = cmp::max(changed, InstSimplify::new().run(context, body));
446+
changed = cmp::max(
447+
changed,
448+
DeadStoreElimination::new_in(&mut scratch).run(context, body),
449+
);
450+
451+
changed
447452
});
448453
});
449454
group.bench_function("diamond", |bencher| {
450455
let mut scratch = Scratch::new();
451456

452457
run_bencher(bencher, create_diamond_cfg, |context, body| {
453-
CfgSimplify::new_in(&mut scratch).run(context, body);
454-
Sroa::new_in(&mut scratch).run(context, body);
455-
InstSimplify::new().run(context, body);
456-
DeadStoreElimination::new_in(&mut scratch).run(context, body);
458+
let mut changed = CfgSimplify::new_in(&mut scratch).run(context, body);
459+
changed = cmp::max(changed, Sroa::new_in(&mut scratch).run(context, body));
460+
changed = cmp::max(changed, InstSimplify::new().run(context, body));
461+
changed = cmp::max(
462+
changed,
463+
DeadStoreElimination::new_in(&mut scratch).run(context, body),
464+
);
465+
466+
changed
457467
});
458468
});
459469
group.bench_function("complex", |bencher| {
460470
let mut scratch = Scratch::new();
461471

462472
run_bencher(bencher, create_complex_cfg, |context, body| {
463-
CfgSimplify::new_in(&mut scratch).run(context, body);
464-
Sroa::new_in(&mut scratch).run(context, body);
465-
InstSimplify::new().run(context, body);
466-
DeadStoreElimination::new_in(&mut scratch).run(context, body);
473+
let mut changed = CfgSimplify::new_in(&mut scratch).run(context, body);
474+
changed = cmp::max(changed, Sroa::new_in(&mut scratch).run(context, body));
475+
changed = cmp::max(changed, InstSimplify::new().run(context, body));
476+
changed = cmp::max(
477+
changed,
478+
DeadStoreElimination::new_in(&mut scratch).run(context, body),
479+
);
480+
481+
changed
467482
});
468483
});
469484
}

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -119,7 +119,7 @@ impl<'env, 'heap, A: Allocator + Clone> AnalysisPass<'env, 'heap>
119119

120120
graph.derive(&body.local_decls, |id, _| id);
121121

122-
let Ok(()) = DataDependencyAnalysisVisitor {
122+
Ok(()) = DataDependencyAnalysisVisitor {
123123
graph: &mut graph,
124124
constant_bindings: &mut constant_bindings,
125125
context,

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

Lines changed: 50 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,55 @@ const fn simplify_type_name(name: &'static str) -> &'static str {
5858
}
5959
}
6060

61+
/// Indicates whether a pass modified the MIR.
62+
///
63+
/// Passes return this to signal whether they made changes, enabling the pass manager to skip
64+
/// dependent re-analyses when nothing changed.
65+
#[must_use]
66+
#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
67+
pub enum Changed {
68+
/// The pass definitely made modifications.
69+
Yes = 2,
70+
/// The pass may have made modifications, but precise tracking was not possible.
71+
Unknown = 1,
72+
/// The pass made no modifications.
73+
No = 0,
74+
}
75+
76+
impl Changed {
77+
/// Returns `true` if the MIR may have changed.
78+
///
79+
/// This is the conservative choice: returns `true` for both [`Changed::Yes`] and
80+
/// [`Changed::Unknown`], ensuring dependent analyses are invalidated when in doubt, but may
81+
/// result in unnecessary passes to be run.
82+
#[must_use]
83+
pub const fn conservative(self) -> bool {
84+
match self {
85+
Self::Yes | Self::Unknown => true,
86+
Self::No => false,
87+
}
88+
}
89+
90+
/// Returns `true` only if the MIR definitely changed.
91+
///
92+
/// This is the optimistic choice: returns `true` only for [`Changed::Yes`], assuming
93+
/// [`Changed::Unknown`] did not actually modify anything. Use with caution, as this may
94+
/// skip necessary re-runs of passes.
95+
#[must_use]
96+
pub const fn optimistic(self) -> bool {
97+
match self {
98+
Self::Yes => true,
99+
Self::Unknown | Self::No => false,
100+
}
101+
}
102+
}
103+
104+
impl From<bool> for Changed {
105+
fn from(value: bool) -> Self {
106+
if value { Self::Yes } else { Self::No }
107+
}
108+
}
109+
61110
/// A transformation or analysis pass over MIR.
62111
///
63112
/// Passes operate on a [`Body`] within a [`MirContext`], allowing them to read and modify the
@@ -86,7 +135,7 @@ pub trait TransformPass<'env, 'heap> {
86135
///
87136
/// The `context` provides access to the heap allocator, type environment, interner, and
88137
/// diagnostic collection. The `body` can be read and modified in place.
89-
fn run(&mut self, context: &mut MirContext<'env, 'heap>, body: &mut Body<'heap>);
138+
fn run(&mut self, context: &mut MirContext<'env, 'heap>, body: &mut Body<'heap>) -> Changed;
90139

91140
/// Returns a human-readable name for this pass.
92141
///

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

Lines changed: 17 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -64,7 +64,7 @@ use crate::{
6464
terminator::{Goto, Terminator, TerminatorKind},
6565
},
6666
context::MirContext,
67-
pass::TransformPass,
67+
pass::{Changed, TransformPass},
6868
};
6969

7070
/// Control-flow graph simplification pass.
@@ -479,8 +479,9 @@ impl<'env, 'heap, A: BumpAllocator> TransformPass<'env, 'heap> for CfgSimplify<A
479479
/// Uses a worklist algorithm that processes blocks in reverse postorder and re-enqueues
480480
/// predecessors when changes occur. This ensures optimization opportunities propagate
481481
/// backward through the CFG until a fixed point is reached.
482-
fn run(&mut self, context: &mut MirContext<'env, 'heap>, body: &mut Body<'heap>) {
482+
fn run(&mut self, context: &mut MirContext<'env, 'heap>, body: &mut Body<'heap>) -> Changed {
483483
let mut queue = WorkQueue::new(body.basic_blocks.len());
484+
let mut changed = false;
484485

485486
// Process in reverse of reverse-postorder (i.e., roughly postorder) to handle
486487
// successors before predecessors, maximizing optimization opportunities.
@@ -500,6 +501,7 @@ impl<'env, 'heap, A: BumpAllocator> TransformPass<'env, 'heap> for CfgSimplify<A
500501
simplified = true;
501502
}
502503

504+
changed |= simplified;
503505
if !simplified {
504506
continue;
505507
}
@@ -510,11 +512,22 @@ impl<'env, 'heap, A: BumpAllocator> TransformPass<'env, 'heap> for CfgSimplify<A
510512
}
511513
}
512514

515+
if !changed {
516+
// We haven't made any changes, meaning that we can skip further analysis, as no blocks
517+
// will be made unreachable, and SSA won't be violated.
518+
return Changed::No;
519+
}
520+
513521
// Unreachable blocks will be dead, therefore must be removed
514522
let mut dbe = DeadBlockElimination::new_in(&mut self.alloc);
515-
dbe.run(context, body);
523+
let _: Changed = dbe.run(context, body);
516524

517525
// Simplifications may break SSA (e.g., merged blocks with conflicting definitions).
518-
SsaRepair::new_in(&mut self.alloc).run(context, body);
526+
let _: Changed = SsaRepair::new_in(&mut self.alloc).run(context, body);
527+
528+
// We ignore the changed of the sub-passes above, because we **know** that we already
529+
// modified, if they don't doesn't matter.
530+
531+
changed.into()
519532
}
520533
}

0 commit comments

Comments
 (0)