Rust · 50293 bytes Raw Blame History
1 //! Global Value Numbering (GVN).
2 //!
3 //! Extends local CSE to cross-block redundancy elimination. Processes
4 //! blocks in dominator-tree preorder, maintaining a scoped hash table
5 //! of value numbers. When a block computes an expression already
6 //! available from a dominating block, the redundant instruction is
7 //! replaced with the dominating definition.
8 //!
9 //! Uses the same canonical Key structure as local CSE (cse.rs).
10
11 use std::collections::HashMap;
12 use crate::ir::inst::*;
13 use crate::ir::types::IrType;
14 use crate::ir::walk::{compute_immediate_dominators, dominator_tree_children};
15 use super::pass::Pass;
16
17 pub struct Gvn;
18
19 impl Pass for Gvn {
20 fn name(&self) -> &'static str { "gvn" }
21
22 fn run(&self, module: &mut Module) -> bool {
23 let pure_calls: Vec<PureCallPolicy> = module
24 .functions
25 .iter()
26 .map(PureCallPolicy::for_function)
27 .collect();
28 let mut changed = false;
29 for func in &mut module.functions {
30 if gvn_function(func, &pure_calls) { changed = true; }
31 }
32 changed
33 }
34 }
35
36 /// Canonical key for value numbering (same as CSE).
37 #[derive(Debug, Clone, PartialEq, Eq, Hash)]
38 struct Key {
39 tag: u32,
40 operands: Vec<ValueId>,
41 aux: i128,
42 name: Option<String>,
43 ty: IrType,
44 }
45
46 #[derive(Debug, Clone, Copy, PartialEq, Eq)]
47 enum PureArgPolicy {
48 ByValue,
49 ReadOnlyWrapperPtr,
50 Unsupported,
51 }
52
53 #[derive(Debug, Clone)]
54 struct PureCallPolicy {
55 reusable: bool,
56 arg_policies: Vec<PureArgPolicy>,
57 }
58
59 impl PureCallPolicy {
60 fn for_function(func: &Function) -> Self {
61 let arg_policies = func.params.iter().map(|param| classify_pure_arg(func, param)).collect::<Vec<_>>();
62 let reusable = func.is_pure
63 && !matches!(func.return_type, IrType::Void)
64 && !func.return_type.is_ptr()
65 && arg_policies.iter().all(|policy| *policy != PureArgPolicy::Unsupported);
66 Self {
67 reusable,
68 arg_policies,
69 }
70 }
71 }
72
73 fn is_scalar_type(ty: &IrType) -> bool {
74 matches!(ty, IrType::Bool | IrType::Int(_) | IrType::Float(_))
75 }
76
77 fn inst_map(func: &Function) -> HashMap<ValueId, &Inst> {
78 func.blocks
79 .iter()
80 .flat_map(|block| block.insts.iter())
81 .map(|inst| (inst.id, inst))
82 .collect()
83 }
84
85 fn classify_pure_arg(func: &Function, param: &Param) -> PureArgPolicy {
86 if !param.ty.is_ptr() {
87 return PureArgPolicy::ByValue;
88 }
89 let IrType::Ptr(inner) = &param.ty else {
90 return PureArgPolicy::Unsupported;
91 };
92 if !is_scalar_type(inner.as_ref()) {
93 return PureArgPolicy::Unsupported;
94 }
95 if pointer_param_is_read_only_scalar(func, param.id) {
96 PureArgPolicy::ReadOnlyWrapperPtr
97 } else {
98 PureArgPolicy::Unsupported
99 }
100 }
101
102 fn pointer_param_is_read_only_scalar(func: &Function, param_id: ValueId) -> bool {
103 let defs = inst_map(func);
104 let mut shadow_slots = Vec::new();
105
106 for block in &func.blocks {
107 for inst in &block.insts {
108 let uses = super::util::inst_uses(&inst.kind);
109 if !uses.contains(&param_id) {
110 continue;
111 }
112 match &inst.kind {
113 InstKind::Store(value, addr) if *value == param_id => {
114 let Some(def) = defs.get(addr) else {
115 return false;
116 };
117 match &def.kind {
118 InstKind::Alloca(slot_ty) => {
119 let IrType::Ptr(inner) = slot_ty else {
120 return false;
121 };
122 if !is_scalar_type(inner.as_ref()) {
123 return false;
124 }
125 shadow_slots.push(*addr);
126 }
127 _ => return false,
128 }
129 }
130 // After mem2reg, the entry shadow slot often folds away and
131 // the PURE callee just reads the incoming by-ref scalar
132 // directly.
133 InstKind::Load(addr) if *addr == param_id => {}
134 _ => return false,
135 }
136 }
137 if let Some(term) = &block.terminator {
138 if super::util::terminator_uses(term).contains(&param_id) {
139 return false;
140 }
141 }
142 }
143
144 for slot in shadow_slots {
145 let mut alias_ptrs = Vec::new();
146 for block in &func.blocks {
147 for inst in &block.insts {
148 let uses = super::util::inst_uses(&inst.kind);
149 if !uses.contains(&slot) {
150 continue;
151 }
152 match &inst.kind {
153 InstKind::Store(value, addr) if *value == param_id && *addr == slot => {}
154 InstKind::Load(addr) if *addr == slot => alias_ptrs.push(inst.id),
155 _ => return false,
156 }
157 }
158 if let Some(term) = &block.terminator {
159 if super::util::terminator_uses(term).contains(&slot) {
160 return false;
161 }
162 }
163 }
164
165 for alias in alias_ptrs {
166 for block in &func.blocks {
167 for inst in &block.insts {
168 let uses = super::util::inst_uses(&inst.kind);
169 if !uses.contains(&alias) {
170 continue;
171 }
172 match &inst.kind {
173 InstKind::Load(addr) if *addr == alias => {}
174 _ => return false,
175 }
176 }
177 if let Some(term) = &block.terminator {
178 if super::util::terminator_uses(term).contains(&alias) {
179 return false;
180 }
181 }
182 }
183 }
184 }
185
186 true
187 }
188
189 fn wrapper_alloca_values(
190 func: &Function,
191 pure_calls: &[PureCallPolicy],
192 ) -> HashMap<ValueId, ValueId> {
193 let mut wrappers = HashMap::new();
194 let candidates: Vec<ValueId> = func
195 .blocks
196 .iter()
197 .flat_map(|block| block.insts.iter())
198 .filter_map(|inst| match &inst.kind {
199 InstKind::Alloca(inner_ty) if is_scalar_type(inner_ty) => Some(inst.id),
200 _ => None,
201 })
202 .collect();
203
204 'candidate: for alloca_id in candidates {
205 let mut stored_value = None;
206
207 for scan_block in &func.blocks {
208 for scan_inst in &scan_block.insts {
209 let uses = super::util::inst_uses(&scan_inst.kind);
210 if !uses.contains(&alloca_id) {
211 continue;
212 }
213 match &scan_inst.kind {
214 InstKind::Store(value, addr) if *addr == alloca_id => {
215 if stored_value.replace(*value).is_some() {
216 continue 'candidate;
217 }
218 }
219 InstKind::Call(FuncRef::Internal(idx), args) => {
220 let Some(policy) = pure_calls.get(*idx as usize) else {
221 continue 'candidate;
222 };
223 if !policy.reusable {
224 continue 'candidate;
225 }
226 let valid_arg = args.iter().enumerate().any(|(arg_idx, arg)| {
227 *arg == alloca_id
228 && policy
229 .arg_policies
230 .get(arg_idx)
231 .copied()
232 == Some(PureArgPolicy::ReadOnlyWrapperPtr)
233 });
234 if !valid_arg {
235 continue 'candidate;
236 }
237 }
238 _ => continue 'candidate,
239 }
240 }
241 if let Some(term) = &scan_block.terminator {
242 if super::util::terminator_uses(term).contains(&alloca_id) {
243 continue 'candidate;
244 }
245 }
246 }
247
248 if let Some(value) = stored_value {
249 wrappers.insert(alloca_id, value);
250 }
251 }
252
253 wrappers
254 }
255
256 fn resolve_value(map: &HashMap<ValueId, ValueId>, mut value: ValueId) -> ValueId {
257 while let Some(&next) = map.get(&value) {
258 if next == value {
259 break;
260 }
261 value = next;
262 }
263 value
264 }
265
266 fn key_of(
267 inst: &Inst,
268 replacements: &HashMap<ValueId, ValueId>,
269 pure_calls: &[PureCallPolicy],
270 wrapper_values: &HashMap<ValueId, ValueId>,
271 ) -> Option<Key> {
272 let mk = |tag: u32, ops: Vec<ValueId>, aux: i128| -> Option<Key> {
273 Some(Key { tag, operands: ops, aux, name: None, ty: inst.ty.clone() })
274 };
275 let mk_named = |tag: u32, name: String| -> Option<Key> {
276 Some(Key { tag, operands: vec![], aux: 0, name: Some(name), ty: inst.ty.clone() })
277 };
278 let remap = |v: ValueId| resolve_value(replacements, v);
279 fn canon(a: ValueId, b: ValueId) -> Vec<ValueId> {
280 if a.0 <= b.0 { vec![a, b] } else { vec![b, a] }
281 }
282
283 match &inst.kind {
284 // Pure arithmetic — commutative ops get canonicalized operand order.
285 InstKind::IAdd(a, b) => mk(1, canon(remap(*a), remap(*b)), 0),
286 InstKind::ISub(a, b) => mk(2, vec![remap(*a), remap(*b)], 0),
287 InstKind::IMul(a, b) => mk(3, canon(remap(*a), remap(*b)), 0),
288 InstKind::IDiv(a, b) => mk(4, vec![remap(*a), remap(*b)], 0),
289 InstKind::IMod(a, b) => mk(5, vec![remap(*a), remap(*b)], 0),
290 InstKind::INeg(a) => mk(6, vec![remap(*a)], 0),
291 InstKind::FAdd(a, b) => mk(10, canon(remap(*a), remap(*b)), 0),
292 InstKind::FSub(a, b) => mk(11, vec![remap(*a), remap(*b)], 0),
293 InstKind::FMul(a, b) => mk(12, canon(remap(*a), remap(*b)), 0),
294 InstKind::FDiv(a, b) => mk(13, vec![remap(*a), remap(*b)], 0),
295 InstKind::FNeg(a) => mk(14, vec![remap(*a)], 0),
296 InstKind::FAbs(a) => mk(15, vec![remap(*a)], 0),
297 InstKind::FSqrt(a) => mk(16, vec![remap(*a)], 0),
298 InstKind::FPow(a, b) => mk(17, vec![remap(*a), remap(*b)], 0),
299 InstKind::ICmp(op, a, b) => {
300 let op_val = *op as i128;
301 match op {
302 CmpOp::Eq | CmpOp::Ne => mk(20, canon(remap(*a), remap(*b)), op_val),
303 _ => mk(20, vec![remap(*a), remap(*b)], op_val),
304 }
305 }
306 InstKind::FCmp(op, a, b) => mk(21, vec![remap(*a), remap(*b)], *op as i128),
307 InstKind::And(a, b) => mk(30, canon(remap(*a), remap(*b)), 0),
308 InstKind::Or(a, b) => mk(31, canon(remap(*a), remap(*b)), 0),
309 InstKind::Not(a) => mk(32, vec![remap(*a)], 0),
310 InstKind::Select(c, t, f) => mk(33, vec![remap(*c), remap(*t), remap(*f)], 0),
311 InstKind::BitAnd(a, b) => mk(40, canon(remap(*a), remap(*b)), 0),
312 InstKind::BitOr(a, b) => mk(41, canon(remap(*a), remap(*b)), 0),
313 InstKind::BitXor(a, b) => mk(42, canon(remap(*a), remap(*b)), 0),
314 InstKind::BitNot(a) => mk(43, vec![remap(*a)], 0),
315 InstKind::Shl(a, b) => mk(44, vec![remap(*a), remap(*b)], 0),
316 InstKind::LShr(a, b) => mk(45, vec![remap(*a), remap(*b)], 0),
317 InstKind::AShr(a, b) => mk(46, vec![remap(*a), remap(*b)], 0),
318 InstKind::CountLeadingZeros(a) => mk(47, vec![remap(*a)], 0),
319 InstKind::CountTrailingZeros(a) => mk(48, vec![remap(*a)], 0),
320 InstKind::PopCount(a) => mk(49, vec![remap(*a)], 0),
321 // Conversions.
322 InstKind::IntToFloat(a, w) => mk(50, vec![remap(*a)], w.bits() as i128),
323 InstKind::FloatToInt(a, w) => mk(51, vec![remap(*a)], w.bits() as i128),
324 InstKind::FloatExtend(a, w) => mk(52, vec![remap(*a)], w.bits() as i128),
325 InstKind::FloatTrunc(a, w) => mk(53, vec![remap(*a)], w.bits() as i128),
326 InstKind::IntExtend(a, w, s) => mk(54, vec![remap(*a)], (w.bits() as i128) * if *s { 1 } else { -1 }),
327 InstKind::IntTrunc(a, w) => mk(55, vec![remap(*a)], w.bits() as i128),
328 // Constants.
329 InstKind::ConstInt(v, w) => mk(60, vec![], *v * 100 + w.bits() as i128),
330 InstKind::ConstFloat(v, w) => mk(61, vec![], ((*v).to_bits() as i128) ^ (w.bits() as i128)),
331 InstKind::ConstBool(v) => mk(62, vec![], *v as i128),
332 // GlobalAddr.
333 InstKind::GlobalAddr(name) => mk_named(70, name.clone()),
334 // GEP.
335 InstKind::GetElementPtr(base, idxs) => {
336 let mut ops = vec![remap(*base)];
337 ops.extend(idxs.iter().copied().map(remap));
338 mk(80, ops, 0)
339 }
340 // Reusable PURE calls: by-value arguments only, real value return,
341 // and no pointer result identity to preserve. For lowered
342 // Fortran scalar-by-reference args, look through the wrapper
343 // alloca to the stored SSA value when the callee only reads the
344 // pointee as a scalar input.
345 InstKind::Call(FuncRef::Internal(idx), args) => {
346 let policy = pure_calls.get(*idx as usize)?;
347 if !policy.reusable || policy.arg_policies.len() != args.len() {
348 return None;
349 }
350 let mut ops = Vec::with_capacity(args.len());
351 for (arg, arg_policy) in args.iter().zip(policy.arg_policies.iter()) {
352 match arg_policy {
353 PureArgPolicy::ByValue => ops.push(remap(*arg)),
354 PureArgPolicy::ReadOnlyWrapperPtr => {
355 let wrapper = remap(*arg);
356 let stored = wrapper_values.get(&wrapper)?;
357 ops.push(remap(*stored));
358 }
359 PureArgPolicy::Unsupported => return None,
360 }
361 }
362 mk(90, ops, *idx as i128)
363 }
364 // Impure: loads, stores, runtime calls, external calls, alloca — not GVN candidates.
365 InstKind::Load(..) | InstKind::Store(..) | InstKind::Alloca(..)
366 | InstKind::Call(..) | InstKind::RuntimeCall(..)
367 | InstKind::ConstString(..) | InstKind::Undef(..)
368 | InstKind::ExtractField(..) | InstKind::InsertField(..) => None,
369 }
370 }
371
372 fn gvn_function(func: &mut Function, pure_calls: &[PureCallPolicy]) -> bool {
373 let idoms = compute_immediate_dominators(func);
374 let children = dominator_tree_children(&idoms);
375 let wrapper_values = wrapper_alloca_values(func, pure_calls);
376
377 // Scoped value number table: Key → dominating ValueId.
378 let mut vn_table: HashMap<Key, ValueId> = HashMap::new();
379 // Replacement map: redundant ValueId → dominating ValueId.
380 let mut replacements: HashMap<ValueId, ValueId> = HashMap::new();
381
382 // Process blocks in dominator-tree preorder (DFS from entry).
383 let mut stack = vec![(func.entry, 0usize)]; // (block, depth for scope management)
384 let mut scope_stack: Vec<Vec<Key>> = Vec::new(); // keys to remove when leaving scope
385
386 while let Some((block_id, depth)) = stack.pop() {
387 // Pop scope entries for blocks we've left.
388 while scope_stack.len() > depth {
389 if let Some(keys) = scope_stack.pop() {
390 for key in keys {
391 vn_table.remove(&key);
392 }
393 }
394 }
395
396 // Process this block's instructions.
397 let mut new_keys = Vec::new();
398 let block = func.block(block_id);
399 for inst in &block.insts {
400 if let Some(key) = key_of(inst, &replacements, pure_calls, &wrapper_values) {
401 if let Some(&existing) = vn_table.get(&key) {
402 // This expression is already available from a dominating block.
403 replacements.insert(inst.id, existing);
404 } else {
405 // First occurrence — register in the table.
406 vn_table.insert(key.clone(), inst.id);
407 new_keys.push(key);
408 }
409 }
410 }
411
412 scope_stack.push(new_keys);
413
414 // Push children (reverse order so leftmost child is processed first).
415 if let Some(kids) = children.get(&block_id) {
416 for &child in kids.iter().rev() {
417 stack.push((child, depth + 1));
418 }
419 }
420 }
421
422 if replacements.is_empty() { return false; }
423
424 // Apply replacements: substitute all uses of redundant values, then
425 // drop the now-dead duplicate instructions directly. This matters
426 // for PURE calls because DCE conservatively treats generic calls as
427 // side-effecting.
428 for block in &mut func.blocks {
429 for inst in &mut block.insts {
430 inst.kind = remap_operands(&inst.kind, &replacements);
431 }
432 block.insts.retain(|inst| !replacements.contains_key(&inst.id));
433 if let Some(ref mut term) = block.terminator {
434 remap_terminator_operands(term, &replacements);
435 }
436 }
437
438 true
439 }
440
441 /// Remap operands in an instruction using the replacement map.
442 fn remap_operands(kind: &InstKind, map: &HashMap<ValueId, ValueId>) -> InstKind {
443 super::loop_utils::remap_inst_kind(kind, map)
444 }
445
446 /// Remap operands in a terminator.
447 fn remap_terminator_operands(term: &mut Terminator, map: &HashMap<ValueId, ValueId>) {
448 let r = |v: &ValueId| *map.get(v).unwrap_or(v);
449 match term {
450 Terminator::Return(Some(v)) => *v = r(v),
451 Terminator::Branch(_, args) => {
452 for a in args.iter_mut() { *a = r(a); }
453 }
454 Terminator::CondBranch { cond, true_args, false_args, .. } => {
455 *cond = r(cond);
456 for a in true_args.iter_mut() { *a = r(a); }
457 for a in false_args.iter_mut() { *a = r(a); }
458 }
459 Terminator::Switch { selector, .. } => {
460 *selector = r(selector);
461 }
462 _ => {}
463 }
464 }
465
466 #[cfg(test)]
467 mod tests {
468 use super::*;
469 use std::fs;
470 use std::path::PathBuf;
471 use crate::ir::types::{IrType, IntWidth};
472 use crate::opt::pass::Pass;
473 use crate::opt::pass::PassManager;
474 use crate::opt::{
475 bce::Bce, call_resolve::CallResolve, const_fold::ConstFold, const_prop::ConstProp,
476 dead_func::DeadFuncElim, dse::Dse, fusion::LoopFusion, global_lsf::GlobalLsf,
477 interchange::LoopInterchange, inline::Inline, licm::Licm, lsf::LocalLsf,
478 mem2reg::Mem2Reg, peel::LoopPeel, preheader::PreheaderInsert, simplify_cfg::SimplifyCfg,
479 sroa::Sroa, strength_reduce::StrengthReduce, unroll::LoopUnroll, unswitch::LoopUnswitch,
480 fission::LoopFission,
481 };
482 use crate::opt::pipeline::OptLevel;
483 use crate::lexer::{Position, Span};
484 use crate::lexer::{SourceForm, tokenize};
485 use crate::parser::Parser;
486 use crate::preprocess::PreprocConfig;
487 use crate::sema::{resolve, validate};
488 use crate::ir::lower;
489
490 fn span() -> Span {
491 let pos = Position { line: 0, col: 0 };
492 Span { file_id: 0, start: pos, end: pos }
493 }
494
495 fn push_inst(func: &mut Function, block: BlockId, kind: InstKind, ty: IrType) -> ValueId {
496 let id = func.next_value_id();
497 func.register_type(id, ty.clone());
498 func.block_mut(block).insts.push(Inst { id, kind, ty, span: span() });
499 id
500 }
501
502 fn lower_fixture(name: &str) -> Module {
503 let path = PathBuf::from("test_programs").join(name);
504 let source = fs::read_to_string(&path).expect("fixture source");
505 let pp = crate::preprocess::preprocess(
506 &source,
507 &PreprocConfig {
508 filename: path.to_string_lossy().into_owned(),
509 fixed_form: false,
510 ..PreprocConfig::default()
511 },
512 )
513 .expect("preprocess fixture");
514 let tokens = tokenize(&pp.text, 0, SourceForm::FreeForm).expect("tokenize fixture");
515 let mut parser = Parser::new(&tokens);
516 let units = parser.parse_file().expect("parse fixture");
517 let (st, type_layouts) = resolve::resolve_file(&units).expect("resolve fixture");
518 let diags = validate::validate_file(&units, &st);
519 assert!(
520 !diags.iter().any(|diag| diag.kind == validate::DiagKind::Error),
521 "fixture should lower cleanly: {:?}",
522 diags
523 );
524 lower::lower_file(&units, &st, &type_layouts)
525 }
526
527 fn build_pre_gvn_o2_pipeline() -> PassManager {
528 let mut pm = PassManager::new();
529 pm.add(Box::new(CallResolve));
530 pm.add(Box::new(Mem2Reg));
531 pm.add(Box::new(ConstFold));
532 pm.add(Box::new(Sroa));
533 pm.add(Box::new(Mem2Reg));
534 pm.add(Box::new(Inline::for_level(OptLevel::O2)));
535 pm.add(Box::new(SimplifyCfg));
536 pm.add(Box::new(DeadFuncElim));
537 pm.add(Box::new(Bce));
538 pm.add(Box::new(StrengthReduce));
539 pm.add(Box::new(LocalLsf));
540 pm.add(Box::new(GlobalLsf));
541 pm.add(Box::new(crate::opt::cse::LocalCse));
542 pm.add(Box::new(PreheaderInsert));
543 pm.add(Box::new(LoopPeel));
544 pm.add(Box::new(LoopUnswitch));
545 pm.add(Box::new(Licm));
546 pm.add(Box::new(ConstProp));
547 pm.add(Box::new(Dse));
548 pm.add(Box::new(LoopInterchange));
549 pm.add(Box::new(LoopFission));
550 pm.add(Box::new(LoopFusion));
551 pm.add(Box::new(LoopUnroll));
552 pm
553 }
554
555 #[test]
556 fn gvn_no_op_on_empty() {
557 let mut m = Module::new("test".into());
558 let mut f = Function::new("test".into(), vec![], IrType::Void);
559 f.block_mut(f.entry).terminator = Some(Terminator::Return(None));
560 m.add_function(f);
561 let pass = Gvn;
562 assert!(!pass.run(&mut m));
563 }
564
565 #[test]
566 fn gvn_reuses_pure_by_value_calls() {
567 let mut m = Module::new("test".into());
568
569 let param = Param {
570 name: "x".into(),
571 ty: IrType::Int(IntWidth::I32),
572 id: ValueId(0),
573 fortran_noalias: false,
574 };
575 let mut callee = Function::new("square".into(), vec![param], IrType::Int(IntWidth::I32));
576 callee.is_pure = true;
577 callee.block_mut(callee.entry).terminator = Some(Terminator::Return(Some(ValueId(0))));
578 m.add_function(callee);
579
580 let mut caller = Function::new("main".into(), vec![], IrType::Int(IntWidth::I32));
581 let entry = caller.entry;
582 let c7 = push_inst(
583 &mut caller,
584 entry,
585 InstKind::ConstInt(7, IntWidth::I32),
586 IrType::Int(IntWidth::I32),
587 );
588 let call1 = push_inst(
589 &mut caller,
590 entry,
591 InstKind::Call(FuncRef::Internal(0), vec![c7]),
592 IrType::Int(IntWidth::I32),
593 );
594 let call2 = push_inst(
595 &mut caller,
596 entry,
597 InstKind::Call(FuncRef::Internal(0), vec![c7]),
598 IrType::Int(IntWidth::I32),
599 );
600 let sum = push_inst(
601 &mut caller,
602 entry,
603 InstKind::IAdd(call1, call2),
604 IrType::Int(IntWidth::I32),
605 );
606 caller.block_mut(entry).terminator = Some(Terminator::Return(Some(sum)));
607 m.add_function(caller);
608
609 let pass = Gvn;
610 assert!(pass.run(&mut m));
611
612 let caller = &m.functions[1];
613 let calls: Vec<&Inst> = caller.blocks[0]
614 .insts
615 .iter()
616 .filter(|inst| matches!(inst.kind, InstKind::Call(..)))
617 .collect();
618 assert_eq!(calls.len(), 1, "redundant PURE call should be removed");
619 let kept_call = calls[0].id;
620
621 let add = caller.blocks[0]
622 .insts
623 .iter()
624 .find(|inst| matches!(inst.kind, InstKind::IAdd(..)))
625 .expect("caller should still contain the sum");
626 match add.kind {
627 InstKind::IAdd(lhs, rhs) => {
628 assert_eq!(lhs, kept_call);
629 assert_eq!(rhs, kept_call);
630 }
631 _ => unreachable!(),
632 }
633 }
634
635 #[test]
636 fn gvn_reuses_pure_calls_after_operand_canonicalization() {
637 let mut m = Module::new("test".into());
638
639 let param = Param {
640 name: "x".into(),
641 ty: IrType::Int(IntWidth::I32),
642 id: ValueId(0),
643 fortran_noalias: false,
644 };
645 let mut callee = Function::new("square".into(), vec![param], IrType::Int(IntWidth::I32));
646 callee.is_pure = true;
647 callee.block_mut(callee.entry).terminator = Some(Terminator::Return(Some(ValueId(0))));
648 m.add_function(callee);
649
650 let mut caller = Function::new("main".into(), vec![], IrType::Int(IntWidth::I32));
651 let entry = caller.entry;
652 let a = push_inst(
653 &mut caller,
654 entry,
655 InstKind::ConstInt(3, IntWidth::I32),
656 IrType::Int(IntWidth::I32),
657 );
658 let b = push_inst(
659 &mut caller,
660 entry,
661 InstKind::ConstInt(4, IntWidth::I32),
662 IrType::Int(IntWidth::I32),
663 );
664 let sum1 = push_inst(
665 &mut caller,
666 entry,
667 InstKind::IAdd(a, b),
668 IrType::Int(IntWidth::I32),
669 );
670 let sum2 = push_inst(
671 &mut caller,
672 entry,
673 InstKind::IAdd(a, b),
674 IrType::Int(IntWidth::I32),
675 );
676 let call1 = push_inst(
677 &mut caller,
678 entry,
679 InstKind::Call(FuncRef::Internal(0), vec![sum1]),
680 IrType::Int(IntWidth::I32),
681 );
682 let call2 = push_inst(
683 &mut caller,
684 entry,
685 InstKind::Call(FuncRef::Internal(0), vec![sum2]),
686 IrType::Int(IntWidth::I32),
687 );
688 caller.block_mut(entry).terminator = Some(Terminator::Return(Some(call2)));
689 m.add_function(caller);
690
691 let pass = Gvn;
692 assert!(pass.run(&mut m));
693
694 let caller = &m.functions[1];
695 let call_count = caller.blocks[0]
696 .insts
697 .iter()
698 .filter(|inst| matches!(inst.kind, InstKind::Call(..)))
699 .count();
700 assert_eq!(call_count, 1, "PURE call should reuse equivalent dominating args");
701
702 match caller.blocks[0].terminator.as_ref().expect("return terminator") {
703 Terminator::Return(Some(v)) => assert_eq!(*v, call1),
704 other => panic!("unexpected terminator: {:?}", other),
705 }
706 }
707
708 #[test]
709 fn pure_pointer_param_policy_accepts_read_only_scalar_pattern() {
710 let param = Param {
711 name: "n".into(),
712 ty: IrType::Ptr(Box::new(IrType::Int(IntWidth::I32))),
713 id: ValueId(0),
714 fortran_noalias: false,
715 };
716 let mut callee = Function::new("heavy_fact".into(), vec![param], IrType::Int(IntWidth::I32));
717 callee.is_pure = true;
718 let entry = callee.entry;
719
720 let slot = push_inst(
721 &mut callee,
722 entry,
723 InstKind::Alloca(IrType::Ptr(Box::new(IrType::Int(IntWidth::I32)))),
724 IrType::Ptr(Box::new(IrType::Ptr(Box::new(IrType::Int(IntWidth::I32))))),
725 );
726 push_inst(&mut callee, entry, InstKind::Store(ValueId(0), slot), IrType::Void);
727 let arg_ptr = push_inst(
728 &mut callee,
729 entry,
730 InstKind::Load(slot),
731 IrType::Ptr(Box::new(IrType::Int(IntWidth::I32))),
732 );
733 let arg_val = push_inst(
734 &mut callee,
735 entry,
736 InstKind::Load(arg_ptr),
737 IrType::Int(IntWidth::I32),
738 );
739 callee.block_mut(entry).terminator = Some(Terminator::Return(Some(arg_val)));
740
741 let policy = PureCallPolicy::for_function(&callee);
742 assert!(policy.reusable);
743 assert_eq!(policy.arg_policies, vec![PureArgPolicy::ReadOnlyWrapperPtr]);
744 }
745
746 #[test]
747 fn wrapper_alloca_values_accept_scalar_by_ref_call_pattern() {
748 let mut m = Module::new("test".into());
749
750 let param = Param {
751 name: "n".into(),
752 ty: IrType::Ptr(Box::new(IrType::Int(IntWidth::I32))),
753 id: ValueId(0),
754 fortran_noalias: false,
755 };
756 let mut callee = Function::new("heavy_fact".into(), vec![param], IrType::Int(IntWidth::I32));
757 callee.is_pure = true;
758 let callee_entry = callee.entry;
759 let slot = push_inst(
760 &mut callee,
761 callee_entry,
762 InstKind::Alloca(IrType::Ptr(Box::new(IrType::Int(IntWidth::I32)))),
763 IrType::Ptr(Box::new(IrType::Ptr(Box::new(IrType::Int(IntWidth::I32))))),
764 );
765 push_inst(&mut callee, callee_entry, InstKind::Store(ValueId(0), slot), IrType::Void);
766 let arg_ptr = push_inst(
767 &mut callee,
768 callee_entry,
769 InstKind::Load(slot),
770 IrType::Ptr(Box::new(IrType::Int(IntWidth::I32))),
771 );
772 let arg_val = push_inst(
773 &mut callee,
774 callee_entry,
775 InstKind::Load(arg_ptr),
776 IrType::Int(IntWidth::I32),
777 );
778 callee.block_mut(callee_entry).terminator = Some(Terminator::Return(Some(arg_val)));
779 m.add_function(callee);
780
781 let pure_calls = vec![PureCallPolicy::for_function(&m.functions[0])];
782
783 let mut caller = Function::new("main".into(), vec![], IrType::Int(IntWidth::I32));
784 let entry = caller.entry;
785 let c = push_inst(
786 &mut caller,
787 entry,
788 InstKind::ConstInt(6, IntWidth::I32),
789 IrType::Int(IntWidth::I32),
790 );
791 let wrapper = push_inst(
792 &mut caller,
793 entry,
794 InstKind::Alloca(IrType::Int(IntWidth::I32)),
795 IrType::Ptr(Box::new(IrType::Int(IntWidth::I32))),
796 );
797 push_inst(&mut caller, entry, InstKind::Store(c, wrapper), IrType::Void);
798 let call = push_inst(
799 &mut caller,
800 entry,
801 InstKind::Call(FuncRef::Internal(0), vec![wrapper]),
802 IrType::Int(IntWidth::I32),
803 );
804 caller.block_mut(entry).terminator = Some(Terminator::Return(Some(call)));
805
806 let wrappers = wrapper_alloca_values(&caller, &pure_calls);
807 assert_eq!(wrappers.get(&wrapper), Some(&c));
808 }
809
810 #[test]
811 fn gvn_reuses_pure_calls_through_scalar_wrapper_allocas() {
812 let mut m = Module::new("test".into());
813
814 let param = Param {
815 name: "n".into(),
816 ty: IrType::Ptr(Box::new(IrType::Int(IntWidth::I32))),
817 id: ValueId(0),
818 fortran_noalias: false,
819 };
820 let mut callee = Function::new("heavy_fact".into(), vec![param], IrType::Int(IntWidth::I32));
821 callee.is_pure = true;
822 let callee_entry = callee.entry;
823 let shadow = push_inst(
824 &mut callee,
825 callee_entry,
826 InstKind::Alloca(IrType::Ptr(Box::new(IrType::Int(IntWidth::I32)))),
827 IrType::Ptr(Box::new(IrType::Ptr(Box::new(IrType::Int(IntWidth::I32))))),
828 );
829 push_inst(&mut callee, callee_entry, InstKind::Store(ValueId(0), shadow), IrType::Void);
830 let arg_ptr = push_inst(
831 &mut callee,
832 callee_entry,
833 InstKind::Load(shadow),
834 IrType::Ptr(Box::new(IrType::Int(IntWidth::I32))),
835 );
836 let arg_val = push_inst(
837 &mut callee,
838 callee_entry,
839 InstKind::Load(arg_ptr),
840 IrType::Int(IntWidth::I32),
841 );
842 callee.block_mut(callee_entry).terminator = Some(Terminator::Return(Some(arg_val)));
843 m.add_function(callee);
844
845 let mut caller = Function::new("main".into(), vec![], IrType::Int(IntWidth::I32));
846 let entry = caller.entry;
847 let first = push_inst(
848 &mut caller,
849 entry,
850 InstKind::Alloca(IrType::Int(IntWidth::I32)),
851 IrType::Ptr(Box::new(IrType::Int(IntWidth::I32))),
852 );
853 let second = push_inst(
854 &mut caller,
855 entry,
856 InstKind::Alloca(IrType::Int(IntWidth::I32)),
857 IrType::Ptr(Box::new(IrType::Int(IntWidth::I32))),
858 );
859 let c1 = push_inst(
860 &mut caller,
861 entry,
862 InstKind::ConstInt(6, IntWidth::I32),
863 IrType::Int(IntWidth::I32),
864 );
865 let c2 = push_inst(
866 &mut caller,
867 entry,
868 InstKind::ConstInt(6, IntWidth::I32),
869 IrType::Int(IntWidth::I32),
870 );
871 let wrap1 = push_inst(
872 &mut caller,
873 entry,
874 InstKind::Alloca(IrType::Int(IntWidth::I32)),
875 IrType::Ptr(Box::new(IrType::Int(IntWidth::I32))),
876 );
877 push_inst(&mut caller, entry, InstKind::Store(c2, wrap1), IrType::Void);
878 let call1 = push_inst(
879 &mut caller,
880 entry,
881 InstKind::Call(FuncRef::Internal(0), vec![wrap1]),
882 IrType::Int(IntWidth::I32),
883 );
884 push_inst(&mut caller, entry, InstKind::Store(call1, first), IrType::Void);
885 let c3 = push_inst(
886 &mut caller,
887 entry,
888 InstKind::ConstInt(6, IntWidth::I32),
889 IrType::Int(IntWidth::I32),
890 );
891 let c4 = push_inst(
892 &mut caller,
893 entry,
894 InstKind::ConstInt(6, IntWidth::I32),
895 IrType::Int(IntWidth::I32),
896 );
897 let wrap2 = push_inst(
898 &mut caller,
899 entry,
900 InstKind::Alloca(IrType::Int(IntWidth::I32)),
901 IrType::Ptr(Box::new(IrType::Int(IntWidth::I32))),
902 );
903 push_inst(&mut caller, entry, InstKind::Store(c4, wrap2), IrType::Void);
904 let call2 = push_inst(
905 &mut caller,
906 entry,
907 InstKind::Call(FuncRef::Internal(0), vec![wrap2]),
908 IrType::Int(IntWidth::I32),
909 );
910 push_inst(&mut caller, entry, InstKind::Store(call2, second), IrType::Void);
911 let left = push_inst(&mut caller, entry, InstKind::Load(first), IrType::Int(IntWidth::I32));
912 let right = push_inst(&mut caller, entry, InstKind::Load(second), IrType::Int(IntWidth::I32));
913 let sum = push_inst(
914 &mut caller,
915 entry,
916 InstKind::IAdd(left, right),
917 IrType::Int(IntWidth::I32),
918 );
919 caller.block_mut(entry).terminator = Some(Terminator::Return(Some(sum)));
920 m.add_function(caller);
921
922 let pure_calls: Vec<PureCallPolicy> = m.functions.iter().map(PureCallPolicy::for_function).collect();
923 assert!(pure_calls[0].reusable);
924 assert_eq!(pure_calls[0].arg_policies, vec![PureArgPolicy::ReadOnlyWrapperPtr]);
925 let wrappers = wrapper_alloca_values(&m.functions[1], &pure_calls);
926 assert_eq!(wrappers.get(&wrap1), Some(&c2));
927 assert_eq!(wrappers.get(&wrap2), Some(&c4));
928
929 let mut replacements = HashMap::new();
930 replacements.insert(c2, c1);
931 replacements.insert(c3, c1);
932 replacements.insert(c4, c1);
933 let caller_before = &m.functions[1];
934 let call1_inst = caller_before.blocks[0]
935 .insts
936 .iter()
937 .find(|inst| inst.id == call1)
938 .expect("call1 inst");
939 let call2_inst = caller_before.blocks[0]
940 .insts
941 .iter()
942 .find(|inst| inst.id == call2)
943 .expect("call2 inst");
944 let key1 = key_of(call1_inst, &replacements, &pure_calls, &wrappers).expect("call1 should key");
945 let key2 = key_of(call2_inst, &replacements, &pure_calls, &wrappers).expect("call2 should key");
946 assert_eq!(key1, key2, "wrapper-based call keys should line up before the pass runs");
947
948 let pass = Gvn;
949 assert!(pass.run(&mut m));
950
951 let caller = &m.functions[1];
952 let call_count = caller.blocks[0]
953 .insts
954 .iter()
955 .filter(|inst| matches!(inst.kind, InstKind::Call(..)))
956 .count();
957 assert_eq!(call_count, 1, "wrapper-alloca PURE calls should dedupe");
958 }
959
960 #[test]
961 fn gvn_reuses_pure_calls_for_recursive_factorial_shape() {
962 let mut m = Module::new("test".into());
963
964 let param = Param {
965 name: "n".into(),
966 ty: IrType::Ptr(Box::new(IrType::Int(IntWidth::I32))),
967 id: ValueId(0),
968 fortran_noalias: false,
969 };
970 let mut callee = Function::new("heavy_fact".into(), vec![param], IrType::Int(IntWidth::I32));
971 callee.is_pure = true;
972 let entry = callee.entry;
973 let if_end = callee.create_block("if_end");
974 let if_then = callee.create_block("if_then");
975 let if_else = callee.create_block("if_else");
976
977 let shadow = push_inst(
978 &mut callee,
979 entry,
980 InstKind::Alloca(IrType::Ptr(Box::new(IrType::Int(IntWidth::I32)))),
981 IrType::Ptr(Box::new(IrType::Ptr(Box::new(IrType::Int(IntWidth::I32))))),
982 );
983 push_inst(&mut callee, entry, InstKind::Store(ValueId(0), shadow), IrType::Void);
984 let result = push_inst(
985 &mut callee,
986 entry,
987 InstKind::Alloca(IrType::Int(IntWidth::I32)),
988 IrType::Ptr(Box::new(IrType::Int(IntWidth::I32))),
989 );
990 let p1 = push_inst(
991 &mut callee,
992 entry,
993 InstKind::Load(shadow),
994 IrType::Ptr(Box::new(IrType::Int(IntWidth::I32))),
995 );
996 let n1 = push_inst(&mut callee, entry, InstKind::Load(p1), IrType::Int(IntWidth::I32));
997 let one = push_inst(
998 &mut callee,
999 entry,
1000 InstKind::ConstInt(1, IntWidth::I32),
1001 IrType::Int(IntWidth::I32),
1002 );
1003 let cond = push_inst(
1004 &mut callee,
1005 entry,
1006 InstKind::ICmp(CmpOp::Le, n1, one),
1007 IrType::Bool,
1008 );
1009 callee.block_mut(entry).terminator = Some(Terminator::CondBranch {
1010 cond,
1011 true_dest: if_then,
1012 true_args: vec![],
1013 false_dest: if_else,
1014 false_args: vec![],
1015 });
1016
1017 let then_one = push_inst(
1018 &mut callee,
1019 if_then,
1020 InstKind::ConstInt(1, IntWidth::I32),
1021 IrType::Int(IntWidth::I32),
1022 );
1023 push_inst(&mut callee, if_then, InstKind::Store(then_one, result), IrType::Void);
1024 callee.block_mut(if_then).terminator = Some(Terminator::Branch(if_end, vec![]));
1025
1026 let p2 = push_inst(
1027 &mut callee,
1028 if_else,
1029 InstKind::Load(shadow),
1030 IrType::Ptr(Box::new(IrType::Int(IntWidth::I32))),
1031 );
1032 let n2 = push_inst(&mut callee, if_else, InstKind::Load(p2), IrType::Int(IntWidth::I32));
1033 let p3 = push_inst(
1034 &mut callee,
1035 if_else,
1036 InstKind::Load(shadow),
1037 IrType::Ptr(Box::new(IrType::Int(IntWidth::I32))),
1038 );
1039 let n3 = push_inst(&mut callee, if_else, InstKind::Load(p3), IrType::Int(IntWidth::I32));
1040 let one2 = push_inst(
1041 &mut callee,
1042 if_else,
1043 InstKind::ConstInt(1, IntWidth::I32),
1044 IrType::Int(IntWidth::I32),
1045 );
1046 let dec1 = push_inst(
1047 &mut callee,
1048 if_else,
1049 InstKind::ISub(n3, one2),
1050 IrType::Int(IntWidth::I32),
1051 );
1052 let p4 = push_inst(
1053 &mut callee,
1054 if_else,
1055 InstKind::Load(shadow),
1056 IrType::Ptr(Box::new(IrType::Int(IntWidth::I32))),
1057 );
1058 let n4 = push_inst(&mut callee, if_else, InstKind::Load(p4), IrType::Int(IntWidth::I32));
1059 let one3 = push_inst(
1060 &mut callee,
1061 if_else,
1062 InstKind::ConstInt(1, IntWidth::I32),
1063 IrType::Int(IntWidth::I32),
1064 );
1065 let dec2 = push_inst(
1066 &mut callee,
1067 if_else,
1068 InstKind::ISub(n4, one3),
1069 IrType::Int(IntWidth::I32),
1070 );
1071 let wrap = push_inst(
1072 &mut callee,
1073 if_else,
1074 InstKind::Alloca(IrType::Int(IntWidth::I32)),
1075 IrType::Ptr(Box::new(IrType::Int(IntWidth::I32))),
1076 );
1077 push_inst(&mut callee, if_else, InstKind::Store(dec2, wrap), IrType::Void);
1078 let rec = push_inst(
1079 &mut callee,
1080 if_else,
1081 InstKind::Call(FuncRef::Internal(0), vec![wrap]),
1082 IrType::Int(IntWidth::I32),
1083 );
1084 let prod = push_inst(
1085 &mut callee,
1086 if_else,
1087 InstKind::IMul(n2, rec),
1088 IrType::Int(IntWidth::I32),
1089 );
1090 push_inst(&mut callee, if_else, InstKind::Store(prod, result), IrType::Void);
1091 callee.block_mut(if_else).terminator = Some(Terminator::Branch(if_end, vec![]));
1092
1093 let final_val = push_inst(&mut callee, if_end, InstKind::Load(result), IrType::Int(IntWidth::I32));
1094 callee.block_mut(if_end).terminator = Some(Terminator::Return(Some(final_val)));
1095 m.add_function(callee);
1096
1097 let mut caller = Function::new("main".into(), vec![], IrType::Void);
1098 let caller_entry = caller.entry;
1099 let unit = push_inst(
1100 &mut caller,
1101 caller_entry,
1102 InstKind::ConstInt(6, IntWidth::I32),
1103 IrType::Int(IntWidth::I32),
1104 );
1105 let unit2 = push_inst(
1106 &mut caller,
1107 caller_entry,
1108 InstKind::ConstInt(6, IntWidth::I32),
1109 IrType::Int(IntWidth::I32),
1110 );
1111 let wrap1 = push_inst(
1112 &mut caller,
1113 caller_entry,
1114 InstKind::Alloca(IrType::Int(IntWidth::I32)),
1115 IrType::Ptr(Box::new(IrType::Int(IntWidth::I32))),
1116 );
1117 push_inst(&mut caller, caller_entry, InstKind::Store(unit2, wrap1), IrType::Void);
1118 let call1 = push_inst(
1119 &mut caller,
1120 caller_entry,
1121 InstKind::Call(FuncRef::Internal(0), vec![wrap1]),
1122 IrType::Int(IntWidth::I32),
1123 );
1124 let wrap2 = push_inst(
1125 &mut caller,
1126 caller_entry,
1127 InstKind::Alloca(IrType::Int(IntWidth::I32)),
1128 IrType::Ptr(Box::new(IrType::Int(IntWidth::I32))),
1129 );
1130 push_inst(&mut caller, caller_entry, InstKind::Store(unit, wrap2), IrType::Void);
1131 let call2 = push_inst(
1132 &mut caller,
1133 caller_entry,
1134 InstKind::Call(FuncRef::Internal(0), vec![wrap2]),
1135 IrType::Int(IntWidth::I32),
1136 );
1137 let sum = push_inst(
1138 &mut caller,
1139 caller_entry,
1140 InstKind::IAdd(call1, call2),
1141 IrType::Int(IntWidth::I32),
1142 );
1143 caller.block_mut(caller_entry).terminator = Some(Terminator::Return(Some(sum)));
1144 m.add_function(caller);
1145
1146 let pass = Gvn;
1147 assert!(pass.run(&mut m));
1148 let caller = &m.functions[1];
1149 let call_count = caller.blocks[0]
1150 .insts
1151 .iter()
1152 .filter(|inst| matches!(inst.kind, InstKind::Call(..)))
1153 .count();
1154 assert_eq!(call_count, 1, "recursive PURE factorial calls should dedupe");
1155 }
1156
1157 #[test]
1158 fn gvn_reuses_pure_calls_when_callee_follows_caller() {
1159 let mut m = Module::new("test".into());
1160
1161 let mut caller = Function::new("main".into(), vec![], IrType::Int(IntWidth::I32));
1162 let entry = caller.entry;
1163 let c1 = push_inst(
1164 &mut caller,
1165 entry,
1166 InstKind::ConstInt(6, IntWidth::I32),
1167 IrType::Int(IntWidth::I32),
1168 );
1169 let wrap1 = push_inst(
1170 &mut caller,
1171 entry,
1172 InstKind::Alloca(IrType::Int(IntWidth::I32)),
1173 IrType::Ptr(Box::new(IrType::Int(IntWidth::I32))),
1174 );
1175 push_inst(&mut caller, entry, InstKind::Store(c1, wrap1), IrType::Void);
1176 let call1 = push_inst(
1177 &mut caller,
1178 entry,
1179 InstKind::Call(FuncRef::Internal(1), vec![wrap1]),
1180 IrType::Int(IntWidth::I32),
1181 );
1182 let c2 = push_inst(
1183 &mut caller,
1184 entry,
1185 InstKind::ConstInt(6, IntWidth::I32),
1186 IrType::Int(IntWidth::I32),
1187 );
1188 let wrap2 = push_inst(
1189 &mut caller,
1190 entry,
1191 InstKind::Alloca(IrType::Int(IntWidth::I32)),
1192 IrType::Ptr(Box::new(IrType::Int(IntWidth::I32))),
1193 );
1194 push_inst(&mut caller, entry, InstKind::Store(c2, wrap2), IrType::Void);
1195 let call2 = push_inst(
1196 &mut caller,
1197 entry,
1198 InstKind::Call(FuncRef::Internal(1), vec![wrap2]),
1199 IrType::Int(IntWidth::I32),
1200 );
1201 let sum = push_inst(
1202 &mut caller,
1203 entry,
1204 InstKind::IAdd(call1, call2),
1205 IrType::Int(IntWidth::I32),
1206 );
1207 caller.block_mut(entry).terminator = Some(Terminator::Return(Some(sum)));
1208 m.add_function(caller);
1209
1210 let param = Param {
1211 name: "n".into(),
1212 ty: IrType::Ptr(Box::new(IrType::Int(IntWidth::I32))),
1213 id: ValueId(0),
1214 fortran_noalias: false,
1215 };
1216 let mut callee = Function::new("heavy_fact".into(), vec![param], IrType::Int(IntWidth::I32));
1217 callee.is_pure = true;
1218 let callee_entry = callee.entry;
1219 let shadow = push_inst(
1220 &mut callee,
1221 callee_entry,
1222 InstKind::Alloca(IrType::Ptr(Box::new(IrType::Int(IntWidth::I32)))),
1223 IrType::Ptr(Box::new(IrType::Ptr(Box::new(IrType::Int(IntWidth::I32))))),
1224 );
1225 push_inst(&mut callee, callee_entry, InstKind::Store(ValueId(0), shadow), IrType::Void);
1226 let arg_ptr = push_inst(
1227 &mut callee,
1228 callee_entry,
1229 InstKind::Load(shadow),
1230 IrType::Ptr(Box::new(IrType::Int(IntWidth::I32))),
1231 );
1232 let arg_val = push_inst(
1233 &mut callee,
1234 callee_entry,
1235 InstKind::Load(arg_ptr),
1236 IrType::Int(IntWidth::I32),
1237 );
1238 callee.block_mut(callee_entry).terminator = Some(Terminator::Return(Some(arg_val)));
1239 m.add_function(callee);
1240
1241 let pass = Gvn;
1242 assert!(pass.run(&mut m));
1243 let caller = &m.functions[0];
1244 let call_count = caller.blocks[0]
1245 .insts
1246 .iter()
1247 .filter(|inst| matches!(inst.kind, InstKind::Call(..)))
1248 .count();
1249 assert_eq!(call_count, 1, "caller-before-callee ordering should still dedupe");
1250 }
1251
1252 #[test]
1253 fn gvn_matches_real_pure_recursive_fixture_after_pre_o2_passes() {
1254 let mut module = lower_fixture("pure_recursive_reuse.f90");
1255 let pm = build_pre_gvn_o2_pipeline();
1256 pm.run(&mut module);
1257
1258 let pure_calls: Vec<PureCallPolicy> = module.functions.iter().map(PureCallPolicy::for_function).collect();
1259 assert!(
1260 pure_calls.iter().any(|policy| policy.reusable),
1261 "at least one function in the fixture should remain a reusable PURE callee:\n{}",
1262 crate::ir::printer::print_module(&module)
1263 );
1264
1265 let caller_idx = module
1266 .functions
1267 .iter()
1268 .position(|func| func.name == "__prog_pure_recursive_reuse")
1269 .expect("program entry in fixture");
1270 let wrappers = wrapper_alloca_values(&module.functions[caller_idx], &pure_calls);
1271 assert!(
1272 !wrappers.is_empty(),
1273 "caller should still expose scalar by-ref wrapper allocas before GVN"
1274 );
1275
1276 let pass = Gvn;
1277 assert!(pass.run(&mut module), "GVN should make progress on the real fixture");
1278
1279 let caller = &module.functions[caller_idx];
1280 let call_count = caller.blocks[0]
1281 .insts
1282 .iter()
1283 .filter(|inst| matches!(inst.kind, InstKind::Call(FuncRef::Internal(_), _)))
1284 .count();
1285 assert_eq!(call_count, 1, "real fixture caller should end with one PURE recursive call");
1286 }
1287
1288 #[test]
1289 fn gvn_does_not_reuse_pure_calls_with_pointer_args() {
1290 let mut m = Module::new("test".into());
1291
1292 let param = Param {
1293 name: "p".into(),
1294 ty: IrType::Ptr(Box::new(IrType::Int(IntWidth::I32))),
1295 id: ValueId(0),
1296 fortran_noalias: false,
1297 };
1298 let mut callee = Function::new("peek".into(), vec![param], IrType::Int(IntWidth::I32));
1299 callee.is_pure = true;
1300 let callee_entry = callee.entry;
1301 let zero = push_inst(
1302 &mut callee,
1303 callee_entry,
1304 InstKind::ConstInt(0, IntWidth::I32),
1305 IrType::Int(IntWidth::I32),
1306 );
1307 callee.block_mut(callee_entry).terminator = Some(Terminator::Return(Some(zero)));
1308 m.add_function(callee);
1309
1310 let mut caller = Function::new("main".into(), vec![], IrType::Int(IntWidth::I32));
1311 let entry = caller.entry;
1312 let slot = push_inst(
1313 &mut caller,
1314 entry,
1315 InstKind::Alloca(IrType::Int(IntWidth::I32)),
1316 IrType::Ptr(Box::new(IrType::Int(IntWidth::I32))),
1317 );
1318 let call1 = push_inst(
1319 &mut caller,
1320 entry,
1321 InstKind::Call(FuncRef::Internal(0), vec![slot]),
1322 IrType::Int(IntWidth::I32),
1323 );
1324 let call2 = push_inst(
1325 &mut caller,
1326 entry,
1327 InstKind::Call(FuncRef::Internal(0), vec![slot]),
1328 IrType::Int(IntWidth::I32),
1329 );
1330 let sum = push_inst(
1331 &mut caller,
1332 entry,
1333 InstKind::IAdd(call1, call2),
1334 IrType::Int(IntWidth::I32),
1335 );
1336 caller.block_mut(entry).terminator = Some(Terminator::Return(Some(sum)));
1337 m.add_function(caller);
1338
1339 let pass = Gvn;
1340 assert!(!pass.run(&mut m), "pointer-arg PURE calls must stay distinct");
1341
1342 let caller = &m.functions[1];
1343 let call_count = caller.blocks[0]
1344 .insts
1345 .iter()
1346 .filter(|inst| matches!(inst.kind, InstKind::Call(..)))
1347 .count();
1348 assert_eq!(call_count, 2);
1349 }
1350 }
1351