Skip to main content

llvm_transforms/
mem2reg.rs

1//! mem2reg: promotes scalar alloca/load/store triples to SSA values.
2//!
3//! Implements the Cytron et al. (1991) algorithm:
4//!
5//! 1. **Identify promotable allocas** — scalar allocas in the entry block
6//!    whose address is only ever used as the operand of non-volatile
7//!    `load`/`store` instructions.
8//! 2. **Insert φ-nodes** at the iterated dominance frontier (IDF) of every
9//!    block that stores to the alloca.
10//! 3. **Rename** — walk the dominator tree keeping a "current definition"
11//!    stack per alloca.  Each `store` pushes a new definition; each `load`
12//!    is replaced by the top of the stack; φ-nodes are filled in with the
13//!    reaching definition from each predecessor.
14//!
15//! After the pass, all promoted allocas, their loads, and their stores are
16//! removed.  Non-promotable memory operations are left unchanged.
17
18use crate::const_prop::subst_kind;
19use crate::pass::FunctionPass;
20use llvm_analysis::{Cfg, DomTree};
21use llvm_ir::{BlockId, Context, Function, InstrId, InstrKind, Instruction, TypeId, ValueRef};
22use std::collections::{HashMap, HashSet, VecDeque};
23
24/// mem2reg pass.
25///
26/// # Correctness
27///
28/// Preconditions for a promoted slot:
29/// - The alloca is entry-block local and scalar (`alloca T` with no element count).
30/// - Its address is only used by non-volatile `load`/`store`.
31/// - The pointer value does not escape (no call args, no stores of the pointer,
32///   no GEP/cast/phi/select uses of the pointer itself).
33///
34/// Postconditions for each promoted slot:
35/// - Every `load` is replaced by the reaching SSA value from the dominator walk.
36/// - Phi nodes are inserted only at iterated dominance-frontier blocks.
37/// - Every inserted phi has one incoming value per CFG predecessor.
38/// - No promoted `alloca`/`load`/`store` remains in the block bodies.
39pub struct Mem2Reg;
40
41impl FunctionPass for Mem2Reg {
42    fn name(&self) -> &'static str {
43        "mem2reg"
44    }
45
46    fn run_on_function(&mut self, ctx: &mut Context, func: &mut Function) -> bool {
47        if func.blocks.is_empty() {
48            return false;
49        }
50
51        let promotable = find_promotable_allocas(func);
52        if promotable.is_empty() {
53            return false;
54        }
55
56        let cfg = Cfg::compute(func);
57        let dom = DomTree::compute(func, &cfg);
58        let df = dom.dominance_frontier(&cfg);
59
60        // Step 2 — insert phi nodes at the IDF of each alloca's def sites.
61        let phi_map = insert_phis(ctx, func, &promotable, &df, &cfg);
62
63        // Step 3 — rename: walk the dominator tree collecting substitutions.
64        let mut subst: HashMap<InstrId, ValueRef> = HashMap::new();
65        let mut instrs_to_remove: HashSet<InstrId> = HashSet::new();
66        let mut phi_updates: Vec<(InstrId, BlockId, ValueRef)> = Vec::new();
67
68        // Initialise stacks: each alloca starts with an `undef`.
69        let mut stacks: HashMap<InstrId, Vec<ValueRef>> = promotable
70            .iter()
71            .map(|(&iid, &ty)| (iid, vec![ValueRef::Constant(ctx.const_undef(ty))]))
72            .collect();
73
74        // Build dominator-children list.
75        let n = func.num_blocks();
76        let mut dom_children: Vec<Vec<BlockId>> = vec![Vec::new(); n];
77        for bi in 0..n {
78            let bid = BlockId(bi as u32);
79            if let Some(idom) = dom.idom(bid) {
80                dom_children[idom.0 as usize].push(bid);
81            }
82        }
83
84        rename_dfs(
85            BlockId(0),
86            func,
87            &promotable,
88            &phi_map,
89            &cfg,
90            &dom_children,
91            &mut stacks,
92            &mut subst,
93            &mut instrs_to_remove,
94            &mut phi_updates,
95        );
96
97        // Apply phi incoming-value updates collected during the rename.
98        for (phi_iid, pred, new_val) in phi_updates {
99            if let InstrKind::Phi {
100                ref mut incoming, ..
101            } = func.instr_mut(phi_iid).kind
102            {
103                for (val, blk) in incoming.iter_mut() {
104                    if *blk == pred {
105                        *val = new_val;
106                        break;
107                    }
108                }
109            }
110        }
111
112        // Apply load-substitution to all remaining instructions.
113        if !subst.is_empty() {
114            for instr in func.instructions.iter_mut() {
115                let new_kind = subst_kind(instr.kind.clone(), &subst);
116                instr.kind = new_kind;
117            }
118        }
119
120        // Remove promoted allocas, loads, and stores from block bodies.
121        instrs_to_remove.extend(promotable.keys().copied());
122        for bb in &mut func.blocks {
123            bb.body.retain(|id| !instrs_to_remove.contains(id));
124        }
125
126        true
127    }
128}
129
130// ---------------------------------------------------------------------------
131// Step 1 — find promotable allocas
132// ---------------------------------------------------------------------------
133
134/// An alloca is **promotable** iff:
135/// - It lives in the entry block (BlockId(0)).
136/// - It has no `num_elements` (scalar alloca, not `alloca T, N`).
137/// - Every use of its address is a non-volatile `load ptr` or
138///   `store val, ptr` (address never captured or passed elsewhere).
139///
140/// # Correctness
141///
142/// This predicate is the safety gate for mem2reg: if a pointer can escape or
143/// alias unknown memory effects, replacing memory traffic with SSA values is
144/// unsound. The checks below conservatively reject such cases.
145fn find_promotable_allocas(func: &Function) -> HashMap<InstrId, TypeId> {
146    let mut result = HashMap::new();
147    let entry = match func.blocks.first() {
148        Some(b) => b,
149        None => return result,
150    };
151
152    let alloca_iids: Vec<InstrId> = entry
153        .body
154        .iter()
155        .copied()
156        .filter(|&iid| {
157            matches!(
158                func.instr(iid).kind,
159                InstrKind::Alloca {
160                    num_elements: None,
161                    ..
162                }
163            )
164        })
165        .collect();
166
167    'outer: for &alloca_iid in &alloca_iids {
168        let alloc_ty = match func.instr(alloca_iid).kind {
169            InstrKind::Alloca { alloc_ty, .. } => alloc_ty,
170            _ => unreachable!(),
171        };
172        let ptr = ValueRef::Instruction(alloca_iid);
173
174        for instr in &func.instructions {
175            match &instr.kind {
176                // Non-volatile load from the alloca address — OK.
177                InstrKind::Load {
178                    ptr: p,
179                    volatile: false,
180                    ..
181                } if *p == ptr => {}
182                // Non-volatile store of a non-self value to the alloca address — OK.
183                InstrKind::Store {
184                    ptr: p,
185                    val,
186                    volatile: false,
187                    ..
188                } if *p == ptr => {
189                    // Don't promote if the alloca's address is stored through itself.
190                    if *val == ptr {
191                        continue 'outer;
192                    }
193                }
194                // Any other use of the alloca's address → not promotable.
195                kind if kind.operands().contains(&ptr) => continue 'outer,
196                _ => {}
197            }
198        }
199
200        result.insert(alloca_iid, alloc_ty);
201    }
202
203    result
204}
205
206// ---------------------------------------------------------------------------
207// Step 2 — phi insertion
208// ---------------------------------------------------------------------------
209
210/// For each promotable alloca, compute its IDF and insert a phi in each
211/// block of the IDF.  Returns a map `BlockId → [(alloca_iid, phi_iid)]`.
212///
213/// # Correctness
214///
215/// IDF placement is sufficient and minimal for SSA construction (Cytron et al):
216/// phis are only inserted where distinct reaching definitions can meet.
217fn insert_phis(
218    ctx: &mut Context,
219    func: &mut Function,
220    promotable: &HashMap<InstrId, TypeId>,
221    df: &HashMap<BlockId, Vec<BlockId>>,
222    cfg: &Cfg,
223) -> HashMap<BlockId, Vec<(InstrId, InstrId)>> {
224    let mut phi_map: HashMap<BlockId, Vec<(InstrId, InstrId)>> = HashMap::new();
225
226    for (&alloca_iid, &alloc_ty) in promotable {
227        let def_blocks = find_def_blocks(func, alloca_iid);
228        let idf = iterated_df(&def_blocks, df);
229
230        for block_id in idf {
231            let preds = cfg.predecessors(block_id);
232            let undef = ctx.const_undef(alloc_ty);
233            let incoming: Vec<(ValueRef, BlockId)> = preds
234                .iter()
235                .map(|&p| (ValueRef::Constant(undef), p))
236                .collect();
237
238            let phi_name = func.fresh_name();
239            let phi_iid = func.alloc_instr(Instruction {
240                name: Some(phi_name),
241                ty: alloc_ty,
242                kind: InstrKind::Phi {
243                    ty: alloc_ty,
244                    incoming,
245                },
246            });
247            // Phi nodes go at the front of the block body.
248            func.blocks[block_id.0 as usize].body.insert(0, phi_iid);
249            phi_map
250                .entry(block_id)
251                .or_default()
252                .push((alloca_iid, phi_iid));
253        }
254    }
255
256    phi_map
257}
258
259fn find_def_blocks(func: &Function, alloca_iid: InstrId) -> Vec<BlockId> {
260    let ptr = ValueRef::Instruction(alloca_iid);
261    let mut result = Vec::new();
262    for (bi, bb) in func.blocks.iter().enumerate() {
263        for iid in bb.instrs() {
264            if let InstrKind::Store { ptr: p, .. } = &func.instr(iid).kind {
265                if *p == ptr {
266                    result.push(BlockId(bi as u32));
267                    break;
268                }
269            }
270        }
271    }
272    result
273}
274
275fn iterated_df(def_blocks: &[BlockId], df: &HashMap<BlockId, Vec<BlockId>>) -> Vec<BlockId> {
276    let mut in_idf: HashSet<BlockId> = HashSet::new();
277    let mut worklist: VecDeque<BlockId> = def_blocks.iter().copied().collect();
278    while let Some(b) = worklist.pop_front() {
279        for &y in df.get(&b).map(|v| v.as_slice()).unwrap_or(&[]) {
280            if in_idf.insert(y) {
281                worklist.push_back(y);
282            }
283        }
284    }
285    in_idf.into_iter().collect()
286}
287
288// ---------------------------------------------------------------------------
289// Step 3 — rename (iterative DFS over dominator tree)
290// ---------------------------------------------------------------------------
291
292/// Rename all loads and stores for the promotable allocas in the subtree
293/// rooted at `block` in the dominator tree.
294///
295/// # Correctness
296///
297/// The per-alloca stack models the current reaching definition along the DFS
298/// path in the dominator tree:
299/// - visiting a store/phi pushes a new definition
300/// - visiting a load reads the current top definition
301/// - leaving a block restores previous stack heights
302///
303/// This ensures every replaced load observes the same definition as the
304/// original memory semantics for promotable slots.
305#[allow(clippy::too_many_arguments)]
306fn rename_dfs(
307    block: BlockId,
308    func: &mut Function,
309    promotable: &HashMap<InstrId, TypeId>,
310    phi_map: &HashMap<BlockId, Vec<(InstrId, InstrId)>>,
311    cfg: &Cfg,
312    dom_children: &[Vec<BlockId>],
313    stacks: &mut HashMap<InstrId, Vec<ValueRef>>,
314    subst: &mut HashMap<InstrId, ValueRef>,
315    instrs_to_remove: &mut HashSet<InstrId>,
316    phi_updates: &mut Vec<(InstrId, BlockId, ValueRef)>,
317) {
318    // Track stack heights so we can undo pushes when we leave this block.
319    let mut saved: Vec<(InstrId, usize)> = Vec::new();
320
321    // Push the phi defined in this block as the new reaching def.
322    if let Some(phis) = phi_map.get(&block) {
323        for &(alloca_iid, phi_iid) in phis {
324            let stack = stacks.get_mut(&alloca_iid).unwrap();
325            saved.push((alloca_iid, stack.len()));
326            stack.push(ValueRef::Instruction(phi_iid));
327        }
328    }
329
330    // Process non-terminator instructions.
331    let body: Vec<InstrId> = func.blocks[block.0 as usize].body.clone();
332    for iid in body {
333        match func.instr(iid).kind.clone() {
334            InstrKind::Alloca { .. } if promotable.contains_key(&iid) => {
335                // The alloca itself — nothing to push; removal handled later.
336            }
337            InstrKind::Load {
338                ptr: ValueRef::Instruction(alloca_iid),
339                volatile: false,
340                ..
341            } => {
342                if let Some(stack) = stacks.get(&alloca_iid) {
343                    let def = *stack.last().unwrap();
344                    subst.insert(iid, def);
345                    instrs_to_remove.insert(iid);
346                }
347            }
348            InstrKind::Store {
349                val,
350                ptr: ValueRef::Instruction(alloca_iid),
351                volatile: false,
352                ..
353            } => {
354                if stacks.contains_key(&alloca_iid) {
355                    // If the stored value is itself a replaced load, resolve it.
356                    let resolved = if let ValueRef::Instruction(vid) = val {
357                        subst.get(&vid).copied().unwrap_or(val)
358                    } else {
359                        val
360                    };
361                    let stack = stacks.get_mut(&alloca_iid).unwrap();
362                    saved.push((alloca_iid, stack.len()));
363                    stack.push(resolved);
364                    instrs_to_remove.insert(iid);
365                }
366            }
367            _ => {}
368        }
369    }
370
371    // Record updates for phi incoming values in successor blocks.
372    for &succ in cfg.successors(block) {
373        if let Some(phis) = phi_map.get(&succ) {
374            for &(alloca_iid, phi_iid) in phis {
375                let def = *stacks[&alloca_iid].last().unwrap();
376                phi_updates.push((phi_iid, block, def));
377            }
378        }
379    }
380
381    // Recurse into dominator-tree children.
382    let children = dom_children[block.0 as usize].clone();
383    for child in children {
384        rename_dfs(
385            child,
386            func,
387            promotable,
388            phi_map,
389            cfg,
390            dom_children,
391            stacks,
392            subst,
393            instrs_to_remove,
394            phi_updates,
395        );
396    }
397
398    // Undo stack pushes made in this block (restore caller's view).
399    for (alloca_iid, saved_len) in saved.into_iter().rev() {
400        stacks.get_mut(&alloca_iid).unwrap().truncate(saved_len);
401    }
402}
403
404// ---------------------------------------------------------------------------
405// Tests
406// ---------------------------------------------------------------------------
407
408#[cfg(test)]
409mod tests {
410    use super::*;
411    use crate::pass::FunctionPass;
412    use llvm_ir::{Builder, Context, Linkage, Module, ValueRef};
413
414    // Build:  f(i32 %x) -> i32 {
415    //   entry: %p = alloca i32
416    //          store %x, %p
417    //          %v = load i32, %p
418    //          ret %v
419    // }
420    // After mem2reg: alloca/store/load removed, ret directly uses %x.
421    fn make_simple_fn() -> (Context, Module) {
422        let mut ctx = Context::new();
423        let mut module = Module::new("test");
424        let mut b = Builder::new(&mut ctx, &mut module);
425        b.add_function(
426            "f",
427            b.ctx.i32_ty,
428            vec![b.ctx.i32_ty],
429            vec!["x".into()],
430            false,
431            Linkage::External,
432        );
433        let entry = b.add_block("entry");
434        b.position_at_end(entry);
435        let x = b.get_arg(0);
436        let p = b.build_alloca("p", b.ctx.i32_ty);
437        b.build_store(x, p);
438        let v = b.build_load("v", b.ctx.i32_ty, p);
439        b.build_ret(v);
440        (ctx, module)
441    }
442
443    #[test]
444    fn mem2reg_simple_store_load() {
445        let (mut ctx, mut module) = make_simple_fn();
446
447        // Before: entry body = [alloca, store, load]
448        assert_eq!(module.functions[0].blocks[0].body.len(), 3);
449
450        let mut pass = Mem2Reg;
451        let changed = pass.run_on_function(&mut ctx, &mut module.functions[0]);
452        assert!(changed);
453
454        // After: entry body is empty (all three promoted away).
455        let func = &module.functions[0];
456        assert_eq!(
457            func.blocks[0].body.len(),
458            0,
459            "alloca, store, and load should all be removed"
460        );
461
462        // ret should use %x (ArgId 0) directly.
463        let tid = func.blocks[0].terminator.unwrap();
464        if let InstrKind::Ret { val: Some(v) } = &func.instr(tid).kind {
465            assert_eq!(
466                *v,
467                ValueRef::Argument(llvm_ir::ArgId(0)),
468                "ret should use arg %x directly after mem2reg"
469            );
470        } else {
471            panic!("terminator should be ret with a value");
472        }
473    }
474
475    // Build a function with an if-else that stores different values to an alloca,
476    // then loads after the merge.  mem2reg should insert a phi at the merge block.
477    //
478    //   entry:  %p = alloca i32
479    //           br i1 %cond, %then, %else
480    //   then:   store i32 1, %p; br %merge
481    //   else:   store i32 2, %p; br %merge
482    //   merge:  %v = load i32, %p; ret %v
483    fn make_phi_fn() -> (Context, Module) {
484        let mut ctx = Context::new();
485        let mut module = Module::new("test");
486        let mut b = Builder::new(&mut ctx, &mut module);
487        b.add_function(
488            "f",
489            b.ctx.i32_ty,
490            vec![b.ctx.i1_ty],
491            vec!["cond".into()],
492            false,
493            Linkage::External,
494        );
495        let entry = b.add_block("entry");
496        let then_b = b.add_block("then");
497        let else_b = b.add_block("else");
498        let merge = b.add_block("merge");
499
500        b.position_at_end(entry);
501        let cond = b.get_arg(0);
502        let p = b.build_alloca("p", b.ctx.i32_ty);
503        b.build_cond_br(cond, then_b, else_b);
504
505        b.position_at_end(then_b);
506        let c1 = b.const_int(b.ctx.i32_ty, 1);
507        b.build_store(c1, p);
508        b.build_br(merge);
509
510        b.position_at_end(else_b);
511        let c2 = b.const_int(b.ctx.i32_ty, 2);
512        b.build_store(c2, p);
513        b.build_br(merge);
514
515        b.position_at_end(merge);
516        let v = b.build_load("v", b.ctx.i32_ty, p);
517        b.build_ret(v);
518
519        (ctx, module)
520    }
521
522    #[test]
523    fn mem2reg_inserts_phi() {
524        let (mut ctx, mut module) = make_phi_fn();
525        let mut pass = Mem2Reg;
526        let changed = pass.run_on_function(&mut ctx, &mut module.functions[0]);
527        assert!(changed);
528
529        let func = &module.functions[0];
530        // merge block (BlockId 3) should have a phi as its first (and only body) instruction.
531        let merge_body = &func.blocks[3].body;
532        assert!(!merge_body.is_empty(), "merge block should have a phi");
533        assert!(
534            matches!(func.instr(merge_body[0]).kind, InstrKind::Phi { .. }),
535            "first instruction in merge should be a phi"
536        );
537        // ret should use the phi result.
538        let tid = func.blocks[3].terminator.unwrap();
539        if let InstrKind::Ret {
540            val: Some(ValueRef::Instruction(phi_iid)),
541        } = &func.instr(tid).kind
542        {
543            assert!(
544                matches!(func.instr(*phi_iid).kind, InstrKind::Phi { .. }),
545                "ret should reference the inserted phi"
546            );
547        } else {
548            panic!("ret should use the phi result");
549        }
550    }
551
552    #[test]
553    fn non_promotable_alloca_unchanged() {
554        // Alloca whose address is passed to a call — not promotable.
555        let mut ctx = Context::new();
556        let mut module = Module::new("test");
557        let mut b = Builder::new(&mut ctx, &mut module);
558        // Declare an external function taking a pointer.
559        let ptr_ty = b.ctx.ptr_ty;
560        let void_ty = b.ctx.void_ty;
561        let callee_ty = b.ctx.mk_fn_type(void_ty, vec![ptr_ty], false);
562        b.add_function("f", b.ctx.i32_ty, vec![], vec![], false, Linkage::External);
563        let entry = b.add_block("entry");
564        b.position_at_end(entry);
565        let p = b.build_alloca("p", b.ctx.i32_ty);
566        // Call with p as argument — captures address.
567        b.build_call(
568            "",
569            b.ctx.void_ty,
570            callee_ty,
571            ValueRef::Global(llvm_ir::GlobalId(0)),
572            vec![p],
573        );
574        let c0 = b.const_int(b.ctx.i32_ty, 0);
575        b.build_ret(c0);
576
577        let before = module.functions[0].blocks[0].body.len();
578        let mut pass = Mem2Reg;
579        let changed = pass.run_on_function(&mut ctx, &mut module.functions[0]);
580        assert!(!changed, "non-promotable alloca must not be removed");
581        assert_eq!(module.functions[0].blocks[0].body.len(), before);
582    }
583}