diff --git a/crates/herkos/src/codegen/instruction.rs b/crates/herkos/src/codegen/instruction.rs index 5ae955f..bbc7398 100644 --- a/crates/herkos/src/codegen/instruction.rs +++ b/crates/herkos/src/codegen/instruction.rs @@ -103,6 +103,14 @@ pub fn generate_instruction_with_info( val2, condition, } => backend.emit_select(*dest, *val1, *val2, *condition), + + // Phi nodes must be lowered to Assign instructions by the lower_phis pass + // before codegen runs. Reaching this arm is a compiler bug. + IrInstr::Phi { .. } => { + unreachable!( + "IrInstr::Phi must be lowered before codegen (lower_phis pass missed this block)" + ) + } }; Ok(code) } diff --git a/crates/herkos/src/codegen/mod.rs b/crates/herkos/src/codegen/mod.rs index c618d96..21feebd 100644 --- a/crates/herkos/src/codegen/mod.rs +++ b/crates/herkos/src/codegen/mod.rs @@ -165,7 +165,7 @@ impl<'a, B: Backend> CodeGenerator<'a, B> { /// Generate a complete Rust module from IR with full module info. /// /// This is the main entry point. It generates a module wrapper structure. - pub fn generate_module_with_info(&self, info: &ModuleInfo) -> Result { + pub fn generate_module_with_info(&self, info: &LoweredModuleInfo) -> Result { module::generate_module_with_info(self.backend, info) } } @@ -500,7 +500,8 @@ mod tests { let backend = SafeBackend::new(); let codegen = CodeGenerator::new(&backend); - let code = codegen.generate_module_with_info(&info).unwrap(); + let lowered = crate::ir::lower_phis::lower(info); + let code = codegen.generate_module_with_info(&lowered).unwrap(); println!("Generated wrapper code:\n{}", code); @@ -566,7 +567,8 @@ mod tests { let backend = SafeBackend::new(); let codegen = CodeGenerator::new(&backend); - let code = codegen.generate_module_with_info(&info).unwrap(); + let lowered = crate::ir::lower_phis::lower(info); + let code = codegen.generate_module_with_info(&lowered).unwrap(); println!("Generated wrapper code:\n{}", code); @@ -633,7 +635,8 @@ mod tests { let backend = SafeBackend::new(); let codegen = CodeGenerator::new(&backend); - let code = codegen.generate_module_with_info(&info).unwrap(); + let lowered = crate::ir::lower_phis::lower(info); + let code = codegen.generate_module_with_info(&lowered).unwrap(); println!("Generated code with immutable global:\n{}", code); diff --git a/crates/herkos/src/codegen/module.rs b/crates/herkos/src/codegen/module.rs index c6d763c..6e1b25f 100644 --- a/crates/herkos/src/codegen/module.rs +++ b/crates/herkos/src/codegen/module.rs @@ -15,7 +15,10 @@ use anyhow::{Context, Result}; /// Generate a complete Rust module from IR functions with full module info. /// /// This is the main entry point. It generates a module wrapper structure. -pub fn generate_module_with_info(backend: &B, info: &ModuleInfo) -> Result { +pub fn generate_module_with_info( + backend: &B, + info: &LoweredModuleInfo, +) -> Result { generate_wrapper_module(backend, info) } diff --git a/crates/herkos/src/ir/builder/core.rs b/crates/herkos/src/ir/builder/core.rs index a74f51f..490bf2b 100644 --- a/crates/herkos/src/ir/builder/core.rs +++ b/crates/herkos/src/ir/builder/core.rs @@ -8,12 +8,12 @@ //! //! ## Architecture //! -//! The builder maintains **two stacks** that together implement Wasm semantics: +//! The builder maintains **three** key data structures that together implement Wasm semantics: //! -//! ### Value Stack (`value_stack: Vec`) +//! ### Value Stack (`value_stack: Vec`) //! //! Replaces Wasm's implicit evaluation stack. Instead of pushing raw values, we push -//! variable IDs. Example: +//! [`UseVar`] tokens for already-defined SSA variables. Example: //! //! ```text //! i32.const 5 → emit: v0 = const 5; push(v0) @@ -23,12 +23,34 @@ //! //! ### Control Stack (`control_stack: Vec`) //! -//! Tracks nested blocks/loops/if structures. Each frame records: -//! - `kind`: Block | Loop | If | Else -//! - `start_block`: jump target for loops (backward) -//! - `end_block`: jump target for block exit (forward/join) -//! - `else_block`: false branch for If -//! - `result_var`: PHI node if block has a result type +//! Tracks nested blocks/loops/if structures. `ControlFrame` is an enum with one +//! variant per construct (`Block`, `Loop`, `If`, `Else`). Each variant holds only +//! the fields relevant to it, making illegal states unrepresentable. Key fields: +//! - `end_block`: join point for forward branches / block exit +//! - `Loop::start_block`: backward-branch target (re-enter the loop) +//! - `result_var`: phi convergence slot if the block has a result type +//! - `locals_at_entry`: snapshot of `local_vars` at frame push +//! - `branch_incoming`: predecessor snapshots from forward `br`/`br_if`/`br_table` +//! - `Loop::loop_phi_vars` / `phi_patches`: phi source collection for loop back-edges +//! +//! ### Local Variable Map (`local_vars: Vec`) +//! +//! Maps each Wasm local index to the **current** SSA variable holding its value. +//! This map is the key to SSA construction for mutable locals: +//! +//! - At function entry, each local (param or declared) gets a fresh variable via +//! `new_pre_alloc_var()`. The map starts as `[v0, v1, ..., vN]`. +//! - `local.get i` → emit `vX = Assign(local_vars[i])` and push vX. A fresh copy +//! is emitted (rather than re-using `local_vars[i]`) so that a later `local.set` +//! cannot retroactively change what was already pushed onto the value stack. +//! - `local.set i` → allocate `vY`, emit `vY = Assign(popped_value)`, set +//! `local_vars[i] = vY`. Subsequent `local.get i` will see vY. +//! - `local.tee i` → same as `local.set`, but the value is also left on the stack. +//! +//! When control flow merges (at `End` of block/loop/if), different predecessors may +//! hold different variables for the same local. The builder compares predecessor +//! snapshots and inserts `IrInstr::Phi` nodes for diverging locals, then updates +//! `local_vars` to the phi dest variables. See `translate.rs` for the full protocol. //! //! ## Flow //! @@ -39,66 +61,166 @@ //! `value_stack`, allocates new variables, emits IR instructions, pushes results. //! //! 3. Control flow instructions (`block`, `loop`, `if`, `br`, etc.) manipulate the control stack -//! and create new basic blocks as needed. +//! and create new basic blocks as needed. Branch instructions (`br`, `br_if`, `br_table`) +//! additionally snapshot `local_vars` and record it as a phi predecessor via +//! `record_forward_branch()` or `record_loop_back_branch()`. +//! +//! 4. At each `End`: predecessor snapshots are collected, `IrInstr::Phi` nodes are inserted for +//! locals with differing values, and `local_vars` is updated to the phi dest variables. //! -//! 4. Return: `IrFunction` with all blocks, variables typed, and control flow explicit. +//! 5. Return: `IrFunction` with all blocks, variables typed, control flow explicit, and phi +//! nodes at every join point. The `lower_phis` pass then destructs these phis into +//! predecessor-block assignments before codegen. //! //! ## Invariants //! //! - **Entry block**: First `new_block()` call returns `BlockId(0)` (function entry). //! - **Value stack**: Operations pop N arguments, push ≤1 result. //! - **Control stack**: Push/pop balanced; each frame has a unique start/end pair. -//! - **Local variables**: `local_vars[i]` = VarId for Wasm local index i (set once at start). +//! - **Local variables**: `local_vars[i]` always holds the *current* SSA variable for Wasm +//! local `i`. It is updated on every `local.set`/`local.tee` and at every join point +//! (where it may be replaced by a phi dest variable). //! -//! ## Example 1 +//! ## Example 1 — simple arithmetic with a local //! -//! Wasm: `(func (param $a i32) → i32 (local.get $a) (i32.const 1) (i32.add))` +//! ```wasm +//! (func (param $a i32) (result i32) +//! local.get $a +//! i32.const 1 +//! i32.add) +//! ``` //! //! Generated IR: //! ```text //! block_0: -//! v2 = i32_add(v0, v1) -//! return v2 +//! v2 = Assign(v0) ;; local.get $a → fresh SSA copy of param +//! v3 = Const(1) ;; i32.const 1 +//! v4 = I32Add(v2, v3) ;; i32.add +//! return v4 +//! +//! ;; v0 = param $a (implicitly defined at function entry) +//! ;; v1 = result convergence slot allocated by push_control (internal) //! ``` -//! where v0 = param $a, v1 = const 1 (allocated during init/translation). //! -//! ## Example 2 (if-else) +//! Note: `local.get` emits a fresh `Assign` copy (v2) rather than pushing v0 directly. +//! This ensures a later `local.set $a` cannot retroactively change values already +//! pushed on the value stack. +//! +//! ## Example 2 — local.set creates fresh SSA variables //! +//! ```wasm +//! (func (param $n i32) (result i32) +//! local.get $n ;; read $n +//! i32.const 1 +//! i32.add +//! local.set $n ;; overwrite $n with $n+1 +//! local.get $n) ;; read updated $n +//! ``` +//! +//! Generated IR (local_vars evolution shown inline): //! ```text -//! Wasm: -//! if i32 -//! (const 1) -//! else -//! (const 2) -//! end +//! block_0: +//! ;; local_vars = [v0] (v0 = param $n) +//! v2 = Assign(v0) ;; local.get $n → copy; local_vars = [v0] (unchanged) +//! v3 = Const(1) +//! v4 = I32Add(v2, v3) +//! v5 = Assign(v4) ;; local.set $n → fresh var; local_vars = [v5] +//! v6 = Assign(v5) ;; local.get $n → copy of v5; local_vars = [v5] (unchanged) +//! return v6 //! ``` //! -//! Generated blocks: +//! ## Example 3 — if/else with phi at join +//! +//! ```wasm +//! ;; (func (param $cond i32) (param $n i32) (result i32) +//! ;; local.get $cond +//! ;; if +//! ;; i32.const 10 +//! ;; local.set $n ;; then-branch: $n ← 10 +//! ;; else +//! ;; i32.const 20 +//! ;; local.set $n ;; else-branch: $n ← 20 +//! ;; end +//! ;; local.get $n) ;; which value? +//! ``` +//! +//! SSA IR after translation (simplified var names): +//! ```text +//! block_0 (entry): +//! ;; local_vars = [v_cond, v_n] +//! v_cv = Assign(v_cond) ;; local.get $cond +//! BranchIf(v_cv, then=block_1, else=block_2) +//! +//! block_1 (then): +//! ;; At If push: locals_at_entry = [v_cond, v_n] +//! ;; local_vars = [v_cond, v_n] ← snapshot preserved +//! v_ten = Const(10) +//! v_nt = Assign(v_ten) ;; local.set $n → local_vars = [v_cond, v_nt] +//! jump block_3 +//! ;; At Else: then_pred_info = (block_1, [v_cond, v_nt]) +//! +//! block_2 (else): +//! ;; local_vars restored to [v_cond, v_n] ← locals_at_entry +//! v_twenty = Const(20) +//! v_ne = Assign(v_twenty) ;; local.set $n → local_vars = [v_cond, v_ne] +//! jump block_3 +//! +//! block_3 (join): +//! ;; Predecessors: (block_1, [v_cond, v_nt]) and (block_2, [v_cond, v_ne]) +//! ;; $n differs → insert phi; $cond same → no phi +//! v_phi_n = Phi [(block_1, v_nt), (block_2, v_ne)] +//! ;; local_vars = [v_cond, v_phi_n] +//! v_r = Assign(v_phi_n) ;; local.get $n +//! return v_r +//! ``` +//! +//! ## Example 4 — loop with phi vars for the back-edge +//! +//! ```wasm +//! ;; (func (param $n i32) (result i32) +//! ;; (local $i i32) ;; zero-initialized +//! ;; loop $L +//! ;; local.get $i +//! ;; i32.const 1 +//! ;; i32.add +//! ;; local.set $i ;; $i++ +//! ;; local.get $i +//! ;; local.get $n +//! ;; i32.lt_s +//! ;; br_if $L) ;; keep looping while $i < $n +//! ;; local.get $i) ;; return final $i +//! ``` //! +//! SSA IR after translation (simplified): //! ```text -//! ┌──────────────────┐ -//! │ block_0 │ Entry -//! │ (condition) │ -//! │ br_if block_2 │ -//! │ br block_3 │ -//! └────────┬─────────┘ -//! │ -//! ┌────┴────┐ -//! ▼ ▼ -//! ┌─────────┐ ┌─────────┐ -//! │ block_1 │ │ block_2 │ True and False branches -//! │ v1=1 │ │ v2=2 │ -//! │ br block│ │ br block│ -//! │ 3 │ │ 3 │ -//! └────┬────┘ └────┬────┘ -//! │ │ -//! └────┬──────┘ -//! ▼ -//! ┌──────────────┐ -//! │ block_3 │ Join point -//! │ v0=phi(v1,v2)│ -//! └──────────────┘ +//! block_0 (pre-loop): +//! ;; local_vars = [v_n, v_i0] (v_n=param, v_i0=0-init local) +//! ;; push_control(Loop) snapshots locals_at_entry=[v_n, v_i0] +//! ;; and substitutes local_vars = [p_n, p_i] ← phi vars +//! jump block_1 +//! +//! block_1 (loop header): ← br_if backward target +//! ;; Phi instructions inserted here by emit_loop_phis at End: +//! p_n = Phi [(block_0, v_n), (block_2, v_n2)] ;; $n unchanged → trivial +//! p_i = Phi [(block_0, v_i0), (block_2, v_inew)] ;; $i modified → real phi +//! v_ic = Assign(p_i) ;; local.get $i +//! v_ip = I32Add(v_ic, Const(1)) +//! v_inew = Assign(v_ip) ;; local.set $i → local_vars = [p_n, v_inew] +//! v_ic2 = Assign(v_inew) ;; local.get $i +//! v_nc = Assign(p_n) ;; local.get $n +//! v_cmp = I32LtS(v_ic2, v_nc) +//! ;; br_if 0: record_loop_back_branch → phi_patches += (p_i, block_1, v_inew) +//! BranchIf(v_cmp, if_true=block_1, if_false=block_2) +//! +//! block_2 (loop exit): +//! ;; local_vars = [p_n, v_inew] (fall-through from br_if false path) +//! v_ret = Assign(v_inew) ;; local.get $i +//! return v_ret //! ``` +//! +//! After `lower_phis`, the trivial `p_n = Phi[..., v_n]` becomes `p_n = Assign(v_n)` +//! and the real `p_i` phi is rewritten as predecessor-block assignments: +//! `block_0` gets `p_i = v_i0`, `block_1` gets `p_i = v_inew` (before its terminator). use super::super::types::*; use anyhow::{Context, Result}; @@ -106,130 +228,121 @@ use wasmparser::ValType; /// Control flow frame for tracking nested blocks/loops/if. /// -/// Each control structure (block, loop, if, else) pushes a frame onto the control stack. -/// When translating branch instructions (br, br_if, br_table), we look up the frame -/// at the specified depth to determine the branch target. When an End operator is hit, -/// we pop the frame and finalize the structure. +/// Each variant holds only the fields relevant to that control construct, +/// making illegal states unrepresentable. /// /// # Frame Lifecycle /// /// 1. **Push**: When Block, Loop, If, or Else operator is encountered /// 2. **Use**: When br/br_if/br_table references it by depth /// 3. **Pop**: When End operator is encountered for that structure -/// -/// # Invariants -/// -/// - Every frame has start_block ≠ end_block (except in edge cases) -/// - If result_var is Some, both branches must produce that variable's type -/// - else_block is only Some for If frames (deferred activation) -/// - All BlockIds in a frame must be distinct #[derive(Debug, Clone)] -pub(super) struct ControlFrame { - /// Kind of control structure (Block, Loop, If, Else) - /// - /// Determines how `br` instructions behave: - /// - **Block**: `br` jumps forward to end_block (exit the block) - /// - **Loop**: `br` jumps backward to start_block (re-enter the loop) - /// - **If**: `br` jumps forward to end_block (exit the if/else) - /// - **Else**: `br` jumps forward to end_block (exit the else branch) - pub(super) kind: ControlKind, - - /// Start block (where to jump when looping, or where we are for blocks) - /// - /// **Meaning depends on kind**: - /// - **Block**: The current block we're building in (no new block created) - /// - **Loop**: The loop header block (where backward `br` jumps to re-enter) - /// - **If**: The then-block (where true branch starts) - /// - **Else**: The else-block where the else branch is executing. This is the - /// *same BlockId* as the parent If frame's `else_block` (retrieved when - /// `Operator::Else` was processed and activated). Unlike Loop's `start_block`, - /// it is NOT used by branch resolution — `br` inside an else body targets - /// `end_block`, not `start_block`. Stored here for documentation/context only. - /// - /// For backward branches (br in a loop), this is the jump target. - pub(super) start_block: BlockId, - - /// End block (join point where all paths converge) - /// - /// **Meaning is consistent across all kinds**: - /// - **Block**: Where `br` jumps to (exit the block) - /// - **Loop**: Where `br` with depth > 0 jumps to (exit the loop) - /// - **If**: Where both then and else branches merge - /// - **Else**: Where the else branch merges back to the if's end - /// - /// This block is activated with `start_block(end_block)` when the structure's - /// End operator is encountered. It's the join point where control flow resumes - /// after the entire control structure (block/loop/if/else) completes. - pub(super) end_block: BlockId, +pub(super) enum ControlFrame { + /// A `block ... end` construct. + /// `br N` targeting this frame jumps forward to `end_block`. + Block { + /// Join point where all paths (fall-through + forward branches) converge. + end_block: BlockId, + /// Phi convergence slot for the block's result value; `None` if no result. + result_var: Option, + /// Forward branches (`br`/`br_if`/`br_table`) that target this frame's `end_block`. + branch_incoming: Vec<(BlockId, Vec)>, + }, + + /// A `loop ... end` construct. + /// `br N` targeting this frame jumps *backward* to `start_block` (re-enters the loop). + Loop { + /// Loop header block — target of backward branches. + start_block: BlockId, + /// Block immediately before the loop header; entry predecessor for loop phis. + pre_loop_block: BlockId, + /// Exit join point (target of forward `br` that exits the loop). + end_block: BlockId, + /// Phi convergence slot for the loop's result value; `None` if no result. + result_var: Option, + /// Pre-loop snapshot of `local_vars`; used as the entry predecessor for loop phis. + locals_at_entry: Vec, + /// Forward branches that exit the loop (depth > 0 past the loop frame). + branch_incoming: Vec<(BlockId, Vec)>, + /// Pre-allocated phi vars (one per Wasm local); substituted into `local_vars` at push. + loop_phi_vars: Vec, + }, + + /// The then-branch of an `if ... end` or `if ... else ... end` construct. + /// `br N` targeting this frame jumps forward to `end_block`. + If { + /// Pre-allocated else block (activated at `Operator::Else` or as empty-else at `End`). + else_block: BlockId, + /// Join point where then and else branches converge. + end_block: BlockId, + /// Phi convergence slot for the if's result value; `None` if no result. + result_var: Option, + /// Pre-if snapshot of `local_vars`; restored at `Operator::Else`. + locals_at_entry: Vec, + /// Forward branches from the then-body targeting `end_block`. + branch_incoming: Vec<(BlockId, Vec)>, + }, + + /// The else-branch of an `if ... else ... end` construct. + /// `br N` targeting this frame jumps forward to `end_block`. + Else { + /// Join point inherited from the If frame. + end_block: BlockId, + /// Phi convergence slot inherited from the If frame. + result_var: Option, + /// Forward branches from *both* then-body and else-body targeting `end_block`. + branch_incoming: Vec<(BlockId, Vec)>, + /// Then-branch fall-through info; `None` if the then-branch ended in dead code. + then_pred_info: Option<(BlockId, Vec)>, + }, +} - /// Else block (only for If constructs; None for Block/Loop/Else) - /// - /// For If frames: - /// - Allocated upfront when If operator is processed - /// - Activated (with `start_block()`) when Else operator is encountered - /// - If no Else operator appears before End, remains empty (only contains jump to end_block) - /// - /// This field enables deferred activation: we know where the else branch will - /// execute before we actually start generating its code. Both then and else - /// branches must eventually jump to end_block. - /// - /// **Lifecycle**: When `Operator::Else` is processed, this BlockId is retrieved, - /// activated, and becomes the `start_block` of the newly pushed Else frame. - /// In other words: `If frame's else_block` == `Else frame's start_block` - /// (same BlockId, different lifecycle phase: deferred vs. active). - /// - /// For Block/Loop/Else frames: always None. - pub(super) else_block: Option, +impl ControlFrame { + /// End block (join point where all paths converge). + pub(super) fn end_block(&self) -> BlockId { + match self { + ControlFrame::Block { end_block, .. } + | ControlFrame::Loop { end_block, .. } + | ControlFrame::If { end_block, .. } + | ControlFrame::Else { end_block, .. } => *end_block, + } + } - /// Result type of the control structure (None if no result) - /// - /// If the block/loop/if has a result type (e.g., "block i32 ... end"), - /// both branches must produce a value of this type at their exit. - /// - /// Example: - /// ```wasm - /// block i32 ◄─── result_type = Some(I32) - /// i32.const 5 - /// end ◄─── Must have an i32 value on stack - /// ``` - /// - /// Used to allocate result_var if needed. - pub(super) result_type: Option, + /// Mutable reference to the forward-branch predecessor list. + pub(super) fn branch_incoming_mut(&mut self) -> &mut Vec<(BlockId, Vec)> { + match self { + ControlFrame::Block { + branch_incoming, .. + } + | ControlFrame::Loop { + branch_incoming, .. + } + | ControlFrame::If { + branch_incoming, .. + } + | ControlFrame::Else { + branch_incoming, .. + } => branch_incoming, + } + } - /// Result variable (PHI node placeholder for the control structure's result) - /// - /// If result_type is Some, result_var holds the VarId that will contain - /// the final result value at the join point (end_block). - /// - /// **Flow**: - /// 1. When the control structure is pushed, result_var is allocated (if result_type is Some) - /// 2. As each branch executes, it computes a value (v0, v1, etc.) - /// 3. Before jumping to end_block, each branch assigns: result_var = branch_value - /// 4. At end_block, result_var contains the unified result - /// - /// **Example**: - /// ```text - /// block i32 - /// result_var = v5 - /// - /// v5 = v1 - /// br end_block - /// end - /// - /// (at end_block) - /// v5 contains the result, pushed onto value_stack - /// ``` - /// - /// If result_var is None, the structure produces no value. - pub(super) result_var: Option, -} + /// Loop phi vars (one per Wasm local); empty slice for non-Loop frames. + pub(super) fn loop_phi_vars(&self) -> &[UseVar] { + match self { + ControlFrame::Loop { loop_phi_vars, .. } => loop_phi_vars, + _ => &[], + } + } -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -pub(super) enum ControlKind { - Block, // Forward branches only - Loop, // Backward branch to start - If, // Conditional with possible else - Else, // Else branch of if + /// Result var (the phi convergence slot), if any. + pub(super) fn result_var(&self) -> Option { + match self { + ControlFrame::Block { result_var, .. } + | ControlFrame::Loop { result_var, .. } + | ControlFrame::If { result_var, .. } + | ControlFrame::Else { result_var, .. } => *result_var, + } + } } /// Module-level context for function translation. @@ -269,15 +382,15 @@ pub struct IrBuilder { pub(super) next_block_id: u32, /// Wasm value stack (now SSA variables instead of actual values) - pub(super) value_stack: Vec, + pub(super) value_stack: Vec, /// Control flow stack for nested blocks/loops/if pub(super) control_stack: Vec, - /// Mapping from Wasm local index → VarId. + /// Mapping from Wasm local index → UseVar. /// Populated at the start of each `translate_function` call. /// Indices 0..param_count-1 are parameters; param_count.. are declared locals. - pub(super) local_vars: Vec, + pub(super) local_vars: Vec, /// Callee function signatures: (param_count, return_type) per function index. /// Set at the start of each `translate_function` call. @@ -294,6 +407,23 @@ pub struct IrBuilder { /// Function import details: (module_name, func_name) for each imported function. /// Indexed by import_idx (0..num_imported_functions-1). pub(super) func_imports: Vec<(String, String)>, + + /// True when the current insertion point is unreachable code. + /// + /// Set to `true` by `Br`, `BrTable`, `Return`, and `Unreachable` instructions. + /// Cleared by `start_real_block()`. + /// + /// When `dead_code` is true, emitted instructions are discarded and branches + /// are NOT recorded as phi predecessors. + pub(super) dead_code: bool, + + /// Deferred loop phi sources from backward branches. + /// + /// Each entry is `(phi_dest, pred_block, src_var)`. When a `Br` targets a loop + /// header (backward edge), we record the current values of all loop phi vars here + /// instead of directly mutating the phi instructions. Consumed at `End` of each + /// Loop frame by `emit_loop_phis()`. + pub(super) phi_patches: Vec<(UseVar, BlockId, UseVar)>, } impl IrBuilder { @@ -315,14 +445,47 @@ impl IrBuilder { type_signatures: Vec::new(), num_imported_functions: 0, func_imports: Vec::new(), + dead_code: false, + phi_patches: Vec::new(), } } - /// Allocate a new SSA variable. - pub(super) fn new_var(&mut self) -> VarId { + /// Allocate a new SSA variable definition token. + /// + /// Returns a [`DefVar`] that must be consumed by exactly one call to + /// [`emit_def`] or [`emit_phi_def`]. The compiler will reject any attempt + /// to emit the same variable twice. + pub(super) fn new_var(&mut self) -> DefVar { let id = VarId(self.next_var_id); self.next_var_id += 1; - id + DefVar(id) + } + + /// Emit an instruction that produces a value. + /// + /// Consumes `dest` (enforcing single-definition) and returns a [`UseVar`] + /// that can be read any number of times. The closure receives the raw + /// [`VarId`] to embed in the [`IrInstr`]. + pub(super) fn emit_def(&mut self, dest: DefVar, f: impl FnOnce(VarId) -> IrInstr) -> UseVar { + let id = dest.into_var_id(); + self.emit_void(f(id)); + UseVar(id) + } + + /// Allocate a variable for phi pre-allocation or function parameters. + /// + /// Returns both the raw [`VarId`] (for use in [`IrInstr`] dest fields that are + /// assembled and prepended to non-current blocks) and a [`UseVar`] for later + /// reading. Unlike [`new_var`]+[`emit_def`], this does **not** enforce + /// single-definition at compile time — use it only for: + /// - Function entry parameters/locals (implicitly defined by the call) + /// - `push_control` result_var (phi convergence slot assigned by each branch) + /// - Loop phi pre-allocation (emitted later by `emit_loop_phis`) + /// - `insert_phis_at_join` (phi dests inserted into non-current blocks) + pub(super) fn new_pre_alloc_var(&mut self) -> (VarId, UseVar) { + let id = VarId(self.next_var_id); + self.next_var_id += 1; + (id, UseVar(id)) } /// Allocate a new basic block. @@ -332,12 +495,14 @@ impl IrBuilder { id } - /// Emit an instruction to the current block. - pub(super) fn emit(&mut self, instr: IrInstr) { + /// Emit an instruction (with no result, or whose result is already embedded) to the current block. + pub(super) fn emit_void(&mut self, instr: IrInstr) { if let Some(block) = self.blocks.iter_mut().find(|b| b.id == self.current_block) { block.instructions.push(instr); } else { - // Current block doesn't exist yet, create it + // Current block doesn't exist yet, create it as a fallback. + // This handles cases where instructions are emitted before explicit block creation, + // which is valid in the IR builder's lazy block creation model. self.blocks.push(IrBlock { id: self.current_block, instructions: vec![instr], @@ -353,6 +518,27 @@ impl IrBuilder { } } + /// Set a terminator only if code is reachable (not dead). + /// + /// This is the safe way to terminate blocks in branches that may be unreachable + /// (e.g., the fall-through of an if-then without else). + pub(super) fn terminate_if_live(&mut self, term: IrTerminator) { + if !self.dead_code { + self.terminate(term); + } + } + + /// Record the current block and local state as a phi predecessor, only if code is reachable. + /// + /// This is used at join points (End of Block/Loop/If) to collect predecessor information + /// needed to build phi nodes. The predecessor is silently dropped if the current block + /// is unreachable (dead code after an unconditional branch or return). + pub(super) fn push_predecessor_if_live(&self, preds: &mut Vec<(BlockId, Vec)>) { + if !self.dead_code { + preds.push((self.current_block, self.local_vars.clone())); + } + } + /// Translate a function from Wasm bytecode to IR. pub fn translate_function( &mut self, @@ -370,22 +556,26 @@ impl IrBuilder { self.next_block_id = 0; self.current_block = BlockId(0); self.local_vars.clear(); + self.dead_code = false; + self.phi_patches.clear(); self.func_signatures = module_ctx.func_signatures.clone(); self.type_signatures = module_ctx.type_signatures.clone(); self.num_imported_functions = module_ctx.num_imported_functions; self.func_imports = module_ctx.func_imports.clone(); // Allocate VarIds for all locals (params first, then declared locals). - // This ensures local_index maps directly to the correct VarId. - let mut local_index_to_var: Vec = Vec::new(); + // This ensures local_index maps directly to the correct UseVar. + // Parameters and zero-initialized locals are implicitly defined at function entry + // (not via emit_def), so we use new_pre_alloc_var to get both VarId and UseVar. + let mut local_index_to_var: Vec = Vec::new(); // Allocate variables for parameters let param_vars: Vec<(VarId, WasmType)> = params .iter() .map(|(_, ty)| { - let var = self.new_var(); - local_index_to_var.push(var); - (var, *ty) + let (var_id, use_var) = self.new_pre_alloc_var(); + local_index_to_var.push(use_var); + (var_id, *ty) }) .collect(); @@ -393,9 +583,9 @@ impl IrBuilder { let mut func_locals: Vec<(VarId, WasmType)> = Vec::new(); for vt in locals { let ty = WasmType::from_wasmparser(*vt); - let var = self.new_var(); - local_index_to_var.push(var); - func_locals.push((var, ty)); + let (var_id, use_var) = self.new_pre_alloc_var(); + local_index_to_var.push(use_var); + func_locals.push((var_id, ty)); } self.local_vars = local_index_to_var; @@ -413,7 +603,7 @@ impl IrBuilder { }); // Push function-level control frame - self.push_control(ControlKind::Block, entry, entry, None, return_type); + self.push_block(entry, return_type); // Translate each Wasm operator to IR for op in operators { @@ -433,29 +623,73 @@ impl IrBuilder { }) } - /// Push a control frame onto the control stack. - pub(super) fn push_control( + /// Allocate a result variable if the block has a result type. + fn alloc_result_var(&mut self, result_type: Option) -> Option { + if result_type.is_some() { + let (_, use_var) = self.new_pre_alloc_var(); + Some(use_var) + } else { + None + } + } + + /// Push a Block control frame onto the control stack. + pub(super) fn push_block(&mut self, end_block: BlockId, result_type: Option) { + let result_var = self.alloc_result_var(result_type); + self.control_stack.push(ControlFrame::Block { + end_block, + result_var, + branch_incoming: Vec::new(), + }); + } + + /// Push a Loop control frame onto the control stack. + /// + /// Pre-allocates phi VarIds for all locals and immediately updates `self.local_vars` + /// to point to them. This ensures all code inside the loop body reads/writes through + /// the phi vars, making backward-branch phi sources correct. + /// + /// Must be called while `self.current_block` still points to the pre-loop block + /// (before switching to the loop header). + pub(super) fn push_loop( &mut self, - kind: ControlKind, start_block: BlockId, end_block: BlockId, - else_block: Option, result_type: Option, ) { - // Allocate a result variable if block has result type - let result_var = if result_type.is_some() { - Some(self.new_var()) - } else { - None - }; - - self.control_stack.push(ControlFrame { - kind, + let result_var = self.alloc_result_var(result_type); + let locals_at_entry = self.local_vars.clone(); + let pre_loop_block = self.current_block; + let loop_phi_vars: Vec = (0..self.local_vars.len()) + .map(|_| self.new_pre_alloc_var().1) + .collect(); + self.local_vars.clone_from(&loop_phi_vars); + self.control_stack.push(ControlFrame::Loop { start_block, + pre_loop_block, end_block, + result_var, + locals_at_entry, + branch_incoming: Vec::new(), + loop_phi_vars, + }); + } + + /// Push an If control frame onto the control stack. + pub(super) fn push_if( + &mut self, + else_block: BlockId, + end_block: BlockId, + result_type: Option, + ) { + let result_var = self.alloc_result_var(result_type); + let locals_at_entry = self.local_vars.clone(); + self.control_stack.push(ControlFrame::If { else_block, - result_type, + end_block, result_var, + locals_at_entry, + branch_incoming: Vec::new(), }); } @@ -487,14 +721,40 @@ impl IrBuilder { let frame = &self.control_stack[frame_idx]; // Loops branch back to start, others branch forward to end - let target = match frame.kind { - ControlKind::Loop => frame.start_block, - _ => frame.end_block, + let target = match frame { + ControlFrame::Loop { start_block, .. } => *start_block, + _ => frame.end_block(), }; Ok(target) } + /// Resolve branch target and metadata for relative depth N. + /// + /// Returns `(target_block, is_loop, frame_idx)` needed for recording phi predecessors + /// and determining terminator type. + pub(super) fn resolve_branch_info(&self, depth: u32) -> Result<(BlockId, bool, usize)> { + let frame_idx = self + .control_stack + .len() + .checked_sub(depth as usize + 1) + .ok_or_else(|| { + anyhow::anyhow!( + "Branch depth {} exceeds control stack depth {}", + depth, + self.control_stack.len() + ) + })?; + + let frame = &self.control_stack[frame_idx]; + let (target, is_loop) = match frame { + ControlFrame::Loop { start_block, .. } => (*start_block, true), + _ => (frame.end_block(), false), + }; + + Ok((target, is_loop, frame_idx)) + } + /// Start a new block (create and switch to it). pub(super) fn start_block(&mut self, block_id: BlockId) { self.current_block = block_id; @@ -504,6 +764,192 @@ impl IrBuilder { terminator: IrTerminator::Unreachable, }); } + + /// Start a new reachable block: creates the block and clears `dead_code`. + /// + /// Use this instead of `start_block` whenever the new block is a real join point + /// reachable from live code (e.g., after If/Else/End, or after BrIf fallthrough). + pub(super) fn start_real_block(&mut self, block_id: BlockId) { + self.dead_code = false; + self.start_block(block_id); + } + + /// Record a forward branch to a non-loop frame. + /// + /// Saves `(current_block, local_vars_snapshot)` in the target frame's `branch_incoming`. + /// No-op if `dead_code` is set (unreachable branches are not phi predecessors). + /// + /// `frame_idx` is the index into `self.control_stack`. + pub(super) fn record_forward_branch(&mut self, frame_idx: usize) { + if self.dead_code { + return; + } + let pred_block = self.current_block; + let locals_snap = self.local_vars.clone(); + self.control_stack[frame_idx] + .branch_incoming_mut() + .push((pred_block, locals_snap)); + } + + /// Record a backward branch to a loop frame (adds to `phi_patches`). + /// + /// For each loop phi var, records `(phi_var, current_block, current_local_value)`. + /// No-op if `dead_code` is set. + /// + /// `frame_idx` is the index into `self.control_stack` for the Loop frame. + pub(super) fn record_loop_back_branch(&mut self, frame_idx: usize) { + if self.dead_code { + return; + } + let pred_block = self.current_block; + // Clone to avoid borrow conflict (local_vars is also in self) + let phi_vars = self.control_stack[frame_idx].loop_phi_vars().to_vec(); + for (local_idx, &phi_var) in phi_vars.iter().enumerate() { + let src_var = self.local_vars[local_idx]; + self.phi_patches.push((phi_var, pred_block, src_var)); + } + } + + /// Insert SSA phi nodes at a join block for locals with differing predecessor values. + /// + /// For each local index, if any predecessor provides a different VarId for that local, + /// a `IrInstr::Phi` node is inserted at the beginning of `join_block` and + /// `self.local_vars[i]` is updated to the phi dest. + /// + /// Phis are inserted in local-index order at the very start of the block's instruction + /// list, before any instructions already in the block. + pub(super) fn insert_phis_at_join( + &mut self, + join_block: BlockId, + predecessors: &[(BlockId, Vec)], + ) -> Result<()> { + let num_locals = self.local_vars.len(); + if predecessors.is_empty() || num_locals == 0 { + return Ok(()); + } + + // Collect phis to insert: allocate dest vars before touching self.blocks. + let mut phi_instrs: Vec = Vec::new(); + let mut new_locals = self.local_vars.clone(); + + for local_idx in 0..num_locals { + let first_var = predecessors[0].1[local_idx]; + let all_same = predecessors + .iter() + .all(|(_, locals)| locals[local_idx] == first_var); + if !all_same { + // Use new_pre_alloc_var because the phi dest is inserted into a + // non-current block — we can't go through emit_def here. + let (dest_id, dest_use) = self.new_pre_alloc_var(); + let srcs: Vec<(BlockId, VarId)> = predecessors + .iter() + .map(|(bid, locals)| (*bid, locals[local_idx].var_id())) + .collect(); + new_locals[local_idx] = dest_use; + phi_instrs.push(IrInstr::Phi { + dest: dest_id, + srcs, + }); + } else { + // All predecessors agree on this local — no phi needed, but we still + // update local_vars to the canonical value. This ensures correctness + // when arriving from dead code (where local_vars may be stale). + new_locals[local_idx] = first_var; + } + } + + self.local_vars = new_locals; + + if !phi_instrs.is_empty() { + let block = self + .blocks + .iter_mut() + .find(|b| b.id == join_block) + .ok_or_else(|| { + anyhow::anyhow!("join block {:?} not found in blocks", join_block) + })?; + let old = std::mem::take(&mut block.instructions); + block.instructions = phi_instrs; + block.instructions.extend(old); + } + Ok(()) + } + + /// Emit phi instructions for a loop frame into its header block. + /// + /// Called at `End` of a Loop frame (after `pop_control`). Inserts `IrInstr::Phi` + /// at the start of the loop header (`start_block`) for each local. Sources come from: + /// 1. The pre-loop predecessor (`pre_loop_block`, `locals_at_entry`). + /// 2. All backward branches recorded in `self.phi_patches` for this loop's phi vars. + /// + /// Consumes the relevant entries from `self.phi_patches`. + /// Trivial phis (all sources are the same var, or the only non-self source) are left + /// for the `lower_phis` pass to eliminate. + pub(super) fn emit_loop_phis(&mut self, frame: &ControlFrame) { + let (start_block, pre_loop_block, loop_phi_vars, locals_at_entry) = match frame { + ControlFrame::Loop { + start_block, + pre_loop_block, + loop_phi_vars, + locals_at_entry, + .. + } => ( + *start_block, + *pre_loop_block, + loop_phi_vars, + locals_at_entry, + ), + _ => return, + }; + + let num_locals = loop_phi_vars.len(); + if num_locals == 0 { + return; + } + + let mut phi_srcs: Vec> = vec![Vec::new(); num_locals]; + + // Entry from before the loop (pre_loop_block is always present for Loop frames) + for (local_idx, phi_src) in phi_srcs.iter_mut().enumerate() { + phi_src.push((pre_loop_block, locals_at_entry[local_idx].var_id())); + } + + // Backward branch sources from phi_patches + for &(phi_dest, pred_block, src_var) in &self.phi_patches { + if let Some(local_idx) = loop_phi_vars.iter().position(|v| *v == phi_dest) { + phi_srcs[local_idx].push((pred_block, src_var.var_id())); + } + } + + // Consume the processed patches + self.phi_patches + .retain(|&(phi_dest, _, _)| !loop_phi_vars.contains(&phi_dest)); + + // Build phi instructions and prepend to loop header + let mut phi_instrs: Vec = Vec::new(); + for (local_idx, &phi_var) in loop_phi_vars.iter().enumerate() { + let srcs = std::mem::take(&mut phi_srcs[local_idx]); + phi_instrs.push(IrInstr::Phi { + dest: phi_var.var_id(), + srcs, + }); + } + + let block = self + .blocks + .iter_mut() + .find(|b| b.id == start_block) + .unwrap_or_else(|| { + panic!( + "Loop header block {:?} not found when emitting loop phis. \ + This indicates a bug in the IR builder's block or control frame management.", + start_block + ); + }); + let old = std::mem::take(&mut block.instructions); + block.instructions = phi_instrs; + block.instructions.extend(old); + } } impl Default for IrBuilder { diff --git a/crates/herkos/src/ir/builder/translate.rs b/crates/herkos/src/ir/builder/translate.rs index de96acf..d6bcbce 100644 --- a/crates/herkos/src/ir/builder/translate.rs +++ b/crates/herkos/src/ir/builder/translate.rs @@ -1,7 +1,118 @@ -//! Operator translation - converts WebAssembly bytecode to SSA IR instructions. +//! Operator translation — converts WebAssembly bytecode to SSA IR instructions. //! -//! This module contains the `translate_operator` method and related emit helpers +//! This module contains [`IrBuilder::translate_operator`] and the emit helpers //! that form the core of the Wasm-to-IR conversion logic. +//! +//! ## SSA and Phi-Node Tracking +//! +//! WebAssembly locals are mutable, but the IR is in SSA form: every variable is +//! defined exactly once. To reconcile this, every `local.set`/`local.tee` +//! allocates a **fresh variable** and updates `self.local_vars[i]` to point to +//! it. Reads via `local.get` emit a copy of the current `local_vars[i]` so that +//! the copy's definition is stable even if the local is overwritten later. +//! +//! When control flow merges (at the exit of a `block`, `loop`, or `if`/`else`), +//! different predecessors may have written different variables to the same local. +//! The IR resolves this with **phi nodes** (`IrInstr::Phi`), inserted at the join +//! block, that select the right value depending on which predecessor ran. +//! +//! ### How phi predecessors are tracked +//! +//! Three mechanisms accumulate the predecessor information needed to build phi +//! nodes: +//! +//! 1. **`ControlFrame::locals_at_entry`** — a snapshot of `self.local_vars` taken +//! when the frame is pushed. Used as the "implicit else" predecessor for +//! `if`-without-`else`, and as the loop-entry predecessor for loop phis. +//! +//! 2. **`ControlFrame::branch_incoming`** — filled by `record_forward_branch()` +//! whenever a `br`/`br_if`/`br_table` jumps *forward* to the frame's +//! `end_block`. Each entry is `(predecessor_block_id, local_vars_snapshot)`. +//! Loop frames are exempt: their backward branches go into `phi_patches` instead. +//! +//! 3. **`IrBuilder::phi_patches`** — filled by `record_loop_back_branch()` for +//! backward edges (`br` inside a loop). Consumed by `emit_loop_phis()` when +//! the loop's `End` is processed. +//! +//! ### Protocol for branch instructions (`Br`, `BrIf`, `BrTable`) +//! +//! For every branch: +//! 1. Determine whether the target frame is a `Loop` (backward) or not (forward). +//! 2. Call the appropriate record method *before* terminating the block, so the +//! snapshot captures the local state at the branch point. +//! 3. Terminate the block with the appropriate `IrTerminator`. +//! 4. Mark subsequent code as dead (`dead_code = true`) for unconditional branches. +//! +//! ### Protocol for join points (`End` of Block / If / Else / Loop) +//! +//! When an `End` is processed: +//! 1. Collect all predecessor snapshots: fall-through (current block, if reachable) +//! + `frame.branch_incoming` for forward targets, or `phi_patches` for loops. +//! 2. Terminate the current block (jump to `end_block`). +//! 3. Activate `end_block` with `start_real_block` (clears `dead_code`). +//! 4. Call `insert_phis_at_join` to insert `IrInstr::Phi` for every local whose +//! value differs across predecessors, and update `self.local_vars` to the phi +//! dest vars. +//! +//! After all operators are translated, `lower_phis::lower` converts these phi +//! nodes into ordinary predecessor-block assignments before the optimizer runs. +//! +//! ### End-to-end tracing example +//! +//! Consider an `if`/`else` that writes to a local: +//! +//! ```wasm +//! ;; (func (param $cond i32) (param $x i32) (result i32) +//! ;; local.get $cond +//! ;; if +//! ;; i32.const 10 local.set $x ;; then: $x ← 10 +//! ;; else +//! ;; i32.const 20 local.set $x ;; else: $x ← 20 +//! ;; end +//! ;; local.get $x) ;; result = chosen value +//! ``` +//! +//! Step-by-step state of `local_vars` and the tracking structures: +//! +//! ```text +//! ── entry ────────────────────────────────────────────────────────── +//! local_vars = [v_cond, v_x] +//! +//! ── Operator::If ─────────────────────────────────────────────────── +//! push_control(If): locals_at_entry = [v_cond, v_x] ← snapshot +//! BranchIf(v_cv, if_true=block_then, if_false=block_else) +//! start_real_block(block_then) +//! +//! ── then-branch ───────────────────────────────────────────────────── +//! local_vars = [v_cond, v_x] (same as entry; then branch starts from snapshot) +//! local.set $x → v_xt = Assign(10); local_vars = [v_cond, v_xt] +//! +//! ── Operator::Else ────────────────────────────────────────────────── +//! then_pred_info = (block_then, [v_cond, v_xt]) ← saved for phi use at End +//! terminate block_then: Jump → block_join +//! local_vars restored to [v_cond, v_x] ← locals_at_entry +//! start_real_block(block_else) +//! +//! ── else-branch ───────────────────────────────────────────────────── +//! local_vars = [v_cond, v_x] (restored; else branch is independent of then) +//! local.set $x → v_xe = Assign(20); local_vars = [v_cond, v_xe] +//! +//! ── Operator::End (Else frame) ────────────────────────────────────── +//! preds = [ +//! (block_then, [v_cond, v_xt]), ← from then_pred_info +//! (block_else, [v_cond, v_xe]), ← else fall-through (current block) +//! ] +//! terminate block_else: Jump → block_join +//! start_real_block(block_join) +//! insert_phis_at_join(block_join, preds): +//! $cond: v_cond == v_cond → no phi needed +//! $x: v_xt != v_xe → v_phi = Phi[(block_then, v_xt), (block_else, v_xe)] +//! local_vars = [v_cond, v_phi] +//! +//! ── block_join ────────────────────────────────────────────────────── +//! local.get $x → Assign(v_phi) ← reads the phi result +//! return +//! ``` use super::super::types::*; use super::core::IrBuilder; @@ -14,39 +125,31 @@ impl IrBuilder { match op { // Constants Operator::I32Const { value } => { - let dest = self.new_var(); - self.emit(IrInstr::Const { - dest, - value: IrValue::I32(*value), - }); - self.value_stack.push(dest); + let v = IrValue::I32(*value); + let def = self.new_var(); + let use_v = self.emit_def(def, |d| IrInstr::Const { dest: d, value: v }); + self.value_stack.push(use_v); } Operator::I64Const { value } => { - let dest = self.new_var(); - self.emit(IrInstr::Const { - dest, - value: IrValue::I64(*value), - }); - self.value_stack.push(dest); + let v = IrValue::I64(*value); + let def = self.new_var(); + let use_v = self.emit_def(def, |d| IrInstr::Const { dest: d, value: v }); + self.value_stack.push(use_v); } Operator::F32Const { value } => { - let dest = self.new_var(); - self.emit(IrInstr::Const { - dest, - value: IrValue::F32(f32::from_bits(value.bits())), - }); - self.value_stack.push(dest); + let v = IrValue::F32(f32::from_bits(value.bits())); + let def = self.new_var(); + let use_v = self.emit_def(def, |d| IrInstr::Const { dest: d, value: v }); + self.value_stack.push(use_v); } Operator::F64Const { value } => { - let dest = self.new_var(); - self.emit(IrInstr::Const { - dest, - value: IrValue::F64(f64::from_bits(value.bits())), - }); - self.value_stack.push(dest); + let v = IrValue::F64(f64::from_bits(value.bits())); + let def = self.new_var(); + let use_v = self.emit_def(def, |d| IrInstr::Const { dest: d, value: v }); + self.value_stack.push(use_v); } // Local variable access @@ -58,59 +161,74 @@ impl IrBuilder { .ok_or_else(|| { anyhow::anyhow!("local.get: local index {} out of range", local_index) })?; - // Emit a copy rather than pushing the local's VarId directly. - // If we push the local's VarId, a later local.tee/local.set that + // Emit a copy rather than pushing the local's UseVar directly. + // If we push the local's UseVar, a later local.tee/local.set that // overwrites the same local will corrupt any already-pushed reference // to it, because the backend emits sequential mutable assignments. // A fresh variable captures the value at this point in time. - let dest = self.new_var(); - self.emit(IrInstr::Assign { dest, src }); - self.value_stack.push(dest); + let def = self.new_var(); + let use_v = self.emit_def(def, |d| IrInstr::Assign { + dest: d, + src: src.var_id(), + }); + self.value_stack.push(use_v); } Operator::LocalSet { local_index } => { + let idx = *local_index as usize; // Pop value and assign to local let value = self .value_stack .pop() .ok_or_else(|| anyhow::anyhow!("Stack underflow for local.set"))?; - let dest = self - .local_vars - .get(*local_index as usize) - .copied() - .ok_or_else(|| { - anyhow::anyhow!("local.set: local index {} out of range", local_index) - })?; - self.emit(IrInstr::Assign { dest, src: value }); + if idx >= self.local_vars.len() { + bail!("local.set: local index {} out of range", local_index); + } + + // Allocate a fresh dest to satisfy SSA single-definition rule. + let def = self.new_var(); + let use_v = self.emit_def(def, |d| IrInstr::Assign { + dest: d, + src: value.var_id(), + }); + // Update the local mapping so subsequent reads see the new value. + self.local_vars[idx] = use_v; } Operator::LocalTee { local_index } => { + let idx = *local_index as usize; // Like LocalSet but keeps value on stack let value = self .value_stack .last() + .copied() .ok_or_else(|| anyhow::anyhow!("Stack underflow for local.tee"))?; - let dest = self - .local_vars - .get(*local_index as usize) - .copied() - .ok_or_else(|| { - anyhow::anyhow!("local.tee: local index {} out of range", local_index) - })?; - self.emit(IrInstr::Assign { dest, src: *value }); - // Value stays on stack (we already have it via .last()) + if idx >= self.local_vars.len() { + bail!("local.tee: local index {} out of range", local_index); + } + + // Allocate a fresh dest to satisfy SSA single-definition rule. + let def = self.new_var(); + let use_v = self.emit_def(def, |d| IrInstr::Assign { + dest: d, + src: value.var_id(), + }); + // Update the local mapping so subsequent reads see the new value. + self.local_vars[idx] = use_v; + // Value stays on stack (already there via .last()) } // Global variable access Operator::GlobalGet { global_index } => { - let dest = self.new_var(); - self.emit(IrInstr::GlobalGet { - dest, - index: GlobalIdx::new(*global_index as usize), + let idx = GlobalIdx::new(*global_index as usize); + let def = self.new_var(); + let use_v = self.emit_def(def, |d| IrInstr::GlobalGet { + dest: d, + index: idx, }); - self.value_stack.push(dest); + self.value_stack.push(use_v); } Operator::GlobalSet { global_index } => { @@ -118,9 +236,9 @@ impl IrBuilder { .value_stack .pop() .ok_or_else(|| anyhow::anyhow!("Stack underflow for global.set"))?; - self.emit(IrInstr::GlobalSet { + self.emit_void(IrInstr::GlobalSet { index: GlobalIdx::new(*global_index as usize), - value, + value: value.var_id(), }); } @@ -281,6 +399,7 @@ impl IrBuilder { // the already-terminated block's control flow. let dead_block = self.new_block(); self.start_block(dead_block); + self.dead_code = true; } // End (end of function or block) @@ -289,112 +408,194 @@ impl IrBuilder { // End of function - treat as implicit return self.emit_return()?; } else { - // End of block/loop/if/else let frame = self.pop_control()?; - // Check if this is an If frame - if frame.kind == super::core::ControlKind::If { - if let Some(else_block) = frame.else_block { - // === STEP 1: Finalize the THEN branch === - // At this point, we've finished executing all instructions in the if's then block. - // If the if has a result type (e.g., "if i32 ... end"), any result value - // is now on top of the value_stack, and we need to assign it to result_var - // so it can be passed to the join point. - - // Then branch: assign result if needed (only if value is on stack) - if let Some(result_var) = frame.result_var { + // Extract result_var before consuming frame in the match below. + let result_var = frame.result_var(); + + // Emit loop phi instructions if this is a Loop frame. + // Called here (before the match consumes `frame`) because emit_loop_phis + // needs a reference to the frame. It is a no-op for non-Loop frames. + self.emit_loop_phis(&frame); + + match frame { + super::core::ControlFrame::If { + else_block, + end_block, + result_var: rv, + mut branch_incoming, + locals_at_entry, + .. + } => { + // === IF without ELSE === + // The then-branch just finished. We create an implicit empty else + // block and insert phi nodes at the join point. + + // Collect predecessors of end_block for phi computation. + // 1. Fall-through from then-body (if reachable) + // 2. Any `br` inside then-body that targeted end_block + // 3. Implicit else block (with pre-if locals = locals_at_entry) + self.push_predecessor_if_live(&mut branch_incoming); + + // Assign result if needed (then-branch fall-through) + if let Some(result_var) = rv { if let Some(stack_value) = self.value_stack.pop() { - self.emit(IrInstr::Assign { - dest: result_var, - src: stack_value, + self.emit_void(IrInstr::Assign { + dest: result_var.var_id(), + src: stack_value.var_id(), }); } - // If stack is empty, then branch ended with br/return (unreachable after) } - // === STEP 2: Terminate the THEN branch with a forward jump === - // Jump to the end block (the join point after the if-else). - // This merges both the then and else branches back together. - self.terminate(IrTerminator::Jump { - target: frame.end_block, - }); - - // === STEP 3: Create the ELSE block === - // Even if the source WebAssembly had NO explicit "else" clause, - // we MUST create one in the IR because: - // - WebAssembly's `if` always has two branches (true/false) - // - The IR needs an explicit control flow graph with both paths - // - An implicit else (no code written) becomes an empty else block - // - // This is a fundamental design choice: the IR makes ALL control flow explicit. + // Terminate then-branch (if reachable) + self.terminate_if_live(IrTerminator::Jump { target: end_block }); + + // Create implicit else block (empty; just jumps to end). + // The else_block is always reachable (false-branch of BranchIf), + // but we don't need to clear dead_code for it — it has no user + // instructions; we just terminate it directly. self.start_block(else_block); + self.terminate(IrTerminator::Jump { target: end_block }); + // The implicit else carries the pre-if local state. + branch_incoming.push((else_block, locals_at_entry.clone())); + + // Restore local_vars to pre-if state before computing phis + // (preds already captured the necessary snapshots above). + self.local_vars = locals_at_entry; + + // Start the join block (always reachable — else_block always jumps here) + self.start_real_block(end_block); + + // Insert phi nodes for locals with differing predecessor values. + // If no live predecessors, mark as dead code. + if branch_incoming.is_empty() { + self.dead_code = true; + } else { + self.insert_phis_at_join(end_block, &branch_incoming)?; + } + } - // === STEP 4: Else block body (empty in this case) === - // Since the source Wasm had no explicit "else" clause, the else block - // has no instructions. It just falls through to the join point. - // We represent this as a single jump to the end block. - self.terminate(IrTerminator::Jump { - target: frame.end_block, - }); - - // === STEP 5: Continue in the END block (join point) === - // After both then and else branches have jumped here, - // future instructions execute in this end block. - self.start_block(frame.end_block); - } else { - // Should not happen - If always has else_block - bail!("If frame missing else_block"); + super::core::ControlFrame::Else { + end_block, + result_var: rv, + branch_incoming, + then_pred_info, + .. + } => { + // === IF-ELSE END === + // The else-branch just finished. Insert phis at the join point + // using then-pred and else-pred info saved during Operator::Else. + + // Collect predecessors of end_block: + // 1. then-branch fall-through (saved as then_pred_info in Else frame) + // 2. else-branch fall-through (current block, if reachable) + // 3. Any `br` from either branch targeting end_block (branch_incoming) + let mut preds: Vec<(BlockId, Vec)> = Vec::new(); + if let Some((then_block, then_locals)) = then_pred_info { + preds.push((then_block, then_locals)); + } + self.push_predecessor_if_live(&mut preds); + preds.extend(branch_incoming.clone()); + + // Assign result if needed (else-branch fall-through) + if let Some(result_var) = rv { + if let Some(stack_value) = self.value_stack.pop() { + self.emit_void(IrInstr::Assign { + dest: result_var.var_id(), + src: stack_value.var_id(), + }); + } + } + + // Terminate else-branch (if reachable) + self.terminate_if_live(IrTerminator::Jump { target: end_block }); + + // Start join block + self.start_real_block(end_block); + + if preds.is_empty() { + self.dead_code = true; + } else { + self.insert_phis_at_join(end_block, &preds)?; + } } - } else { - // === This handles Block, Loop, and Else constructs (NOT If) === - // These are simpler than If: they have no branching, just linear control flow. - - // === STEP 1: Capture the block's result value (if any) === - // If this block/loop/else has a result type (e.g., "block i32 ... end"), - // the result value should be on top of the value_stack when we exit. - // We assign it to result_var so it can be used at the join point. - if let Some(result_var) = frame.result_var { - if let Some(stack_value) = self.value_stack.pop() { - // Normal case: block fell through with a result value - self.emit(IrInstr::Assign { - dest: result_var, - src: stack_value, - }); + + super::core::ControlFrame::Loop { + end_block, + result_var: rv, + mut branch_incoming, + .. + } => { + // === LOOP END === + // Loop phi instructions were already emitted into the loop header + // by the emit_loop_phis call above. Fall through to end_block. + + // Collect fall-through predecessor BEFORE switching blocks. + self.push_predecessor_if_live(&mut branch_incoming); + + // Assign result if needed (loop fall-through) + if let Some(result_var) = rv { + if let Some(stack_value) = self.value_stack.pop() { + self.emit_void(IrInstr::Assign { + dest: result_var.var_id(), + src: stack_value.var_id(), + }); + } + } + + // Terminate loop body fall-through (if reachable) + self.terminate_if_live(IrTerminator::Jump { target: end_block }); + + // Start the loop's exit block + self.start_real_block(end_block); + + // Insert phis at end_block for locals with differing exit values + if branch_incoming.is_empty() { + self.dead_code = true; + } else { + self.insert_phis_at_join(end_block, &branch_incoming)?; } - // WHY IS EMPTY STACK NOT AN ERROR? - // ──────────────────────────────── - // If stack is empty here, it means this block ended with a branch - // (br/br_if/br_table) or return instruction. These terminators: - // 1. Consume the value from the stack before jumping/returning - // 2. Jump away, making all subsequent code unreachable - // - // So even though result_var exists, it won't be used (the code - // after this block is unreachable via the normal path). - // This is NOT an error—it's valid control flow to have dead code - // after a terminating instruction. - // - // Example: - // block i32 - // i32.const 5 - // br 0 ◄─── Consumes the 5, jumps, stack becomes empty - // end ◄─── Stack is empty here, but that's OK } - // === STEP 2: Terminate this block with a forward jump === - // Jump to the end block (the join point after this control structure). - // This is the normal exit path (only reached if no br/return interrupted us). - self.terminate(IrTerminator::Jump { - target: frame.end_block, - }); + super::core::ControlFrame::Block { + end_block, + result_var: rv, + mut branch_incoming, + .. + } => { + // === BLOCK END === + // Collect fall-through predecessor BEFORE switching blocks. + self.push_predecessor_if_live(&mut branch_incoming); + + // Assign result if needed (block fall-through) + if let Some(result_var) = rv { + if let Some(stack_value) = self.value_stack.pop() { + // Normal case: block fell through with a result value + self.emit_void(IrInstr::Assign { + dest: result_var.var_id(), + src: stack_value.var_id(), + }); + } + // Empty stack: block ended with br/return (dead code after) + } - // === STEP 3: Continue in the END block (join point) === - // All paths (normal fall-through, branches into this block) meet here. - self.start_block(frame.end_block); + // Terminate block fall-through (if reachable) + self.terminate_if_live(IrTerminator::Jump { target: end_block }); + + // Start join block + self.start_real_block(end_block); + + if branch_incoming.is_empty() { + self.dead_code = true; + } else { + self.insert_phis_at_join(end_block, &branch_incoming)?; + } + } } - // If block has result type, push result var onto stack - if let Some(result_var) = frame.result_var { - self.value_stack.push(result_var); + // If the control structure produced a result, push it onto the value stack. + if let Some(rv) = result_var { + self.value_stack.push(rv); } } } @@ -545,9 +746,9 @@ impl IrBuilder { // === Memory size and grow === Operator::MemorySize { mem: 0, .. } => { - let dest = self.new_var(); - self.emit(IrInstr::MemorySize { dest }); - self.value_stack.push(dest); + let def = self.new_var(); + let use_v = self.emit_def(def, |d| IrInstr::MemorySize { dest: d }); + self.value_stack.push(use_v); } Operator::MemoryGrow { mem: 0, .. } => { @@ -555,9 +756,12 @@ impl IrBuilder { .value_stack .pop() .ok_or_else(|| anyhow::anyhow!("Stack underflow for memory.grow"))?; - let dest = self.new_var(); - self.emit(IrInstr::MemoryGrow { dest, delta }); - self.value_stack.push(dest); + let def = self.new_var(); + let use_v = self.emit_def(def, |d| IrInstr::MemoryGrow { + dest: d, + delta: delta.var_id(), + }); + self.value_stack.push(use_v); } // Control flow @@ -577,30 +781,9 @@ impl IrBuilder { // This is where we merge back together after the block. let end_block = self.new_block(); - // === Reuse the current block as the loop start target === - // This is KEY DIFFERENCE from Loop: - // Block: start_block = current block (we stay in it, no jump needed) - // Loop: start_block = newly created block (we jump to it) - // - // Why? Because blocks execute sequentially with no backward jumps. - // We don't need to set up a loop header; we just keep building - // instructions in the current block. - let start_block = self.current_block; - // === STEP 1: Push control frame === - // Record this block's control structure: - // - kind=Block: marks this as a block (for br/br_if dispatch) - // - start_block=current_block: where we are now (no backward jumps) - // - end_block=end_block: where br jumps (exit the block) - // - else_block=None: blocks don't have else branches (that's just If) - // - result_type: the block's result type (if any) - self.push_control( - super::core::ControlKind::Block, - start_block, - end_block, - None, - result_type, - ); + // Blocks have no backward jumps — br targets end_block (forward exit). + self.push_block(end_block, result_type); } Operator::Loop { blockty } => { @@ -632,25 +815,20 @@ impl IrBuilder { target: loop_header, }); - // === STEP 2: Begin codegen in the loop header block === + // === STEP 2: Push control frame BEFORE switching blocks === + // push_loop captures self.current_block as pre_loop_block (the block + // that jumps into the loop). It must be called while current_block still + // points to the pre-loop block, before start_block changes it. + // + // push_loop also pre-allocates phi vars for all locals and updates + // self.local_vars to point to them, so all code inside the loop body + // reads/writes through the phi vars from the start. + self.push_loop(loop_header, end_block, result_type); + + // === STEP 3: Begin codegen in the loop header block === // This block is the entry point to the loop and the target of backward // branches (via "br" inside the loop body). self.start_block(loop_header); - - // === STEP 3: Push control frame === - // Record this loop's control structure: - // - kind=Loop: marks this as a loop (for br/br_if dispatch) - // - start_block=loop_header: where backward br's jump - // - end_block=end_block: where forward br's jump (exit) - // - else_block=None: loops don't have else branches (that's just If) - // - result_type: the loop's result type (if any) - self.push_control( - super::core::ControlKind::Loop, - loop_header, - end_block, - None, - result_type, - ); } Operator::If { blockty } => { @@ -669,7 +847,8 @@ impl IrBuilder { let condition = self .value_stack .pop() - .ok_or_else(|| anyhow::anyhow!("Stack underflow for if condition"))?; + .ok_or_else(|| anyhow::anyhow!("Stack underflow for if condition"))? + .var_id(); // === STEP 2: Pre-allocate all three blocks === // We create all blocks upfront so we can reference them in the BranchIf. @@ -692,8 +871,9 @@ impl IrBuilder { }); // === STEP 4: Start building the THEN branch === - // Activate the then_block so subsequent instructions are emitted there. - self.start_block(then_block); + // Activate the then_block. This is always reachable (the BranchIf true path), + // so use start_real_block to clear dead_code. + self.start_real_block(then_block); // === STEP 5: Push If control frame === // Record this if's control structure for later resolution: @@ -702,13 +882,7 @@ impl IrBuilder { // - end_block=end_block: where br 1 jumps (out of if/else) // - else_block=Some(else_block): deferred; we'll activate it when we see Else or End // - result_type: the if's result type (if any) - self.push_control( - super::core::ControlKind::If, - then_block, - end_block, - Some(else_block), - result_type, - ); + self.push_if(else_block, end_block, result_type); // === NOTE: Deferred activation === // The else_block is NOT activated yet. It's stored in the control frame. @@ -718,66 +892,144 @@ impl IrBuilder { } Operator::Else => { - // Pop if frame + // === Transition from then-branch to else-branch === + // + // At this point we are at the end of the then-branch body. We need to: + // 1. Finalize the then-branch (assign result, emit jump to end_block). + // 2. Save the then-branch's fall-through predecessor info so it can + // be included in the phi computation at end_block later. + // 3. Restore local_vars to the pre-if snapshot so the else-branch + // starts with the same local state that the then-branch started with. + // 4. Activate the pre-allocated else_block and push an Else frame. + // + // Why restore local_vars? + // The then-branch may have modified locals (local.set). The else-branch + // must start with the *pre-if* locals, not the then-branch's modified ones. + // This is captured in `if_frame.locals_at_entry`. + // + // Why save then_pred_info instead of calling record_forward_branch? + // At Else time, end_block hasn't been activated yet, and the Else frame + // doesn't exist yet. We stash the then-fall-through as `then_pred_info` + // in the new Else frame; the End handler reads it from there. + // (Any `br 0` inside the then-body went through record_forward_branch + // normally and is already in `if_frame.branch_incoming`.) + let if_frame = self.pop_control().context("else without matching if")?; - if if_frame.kind != super::core::ControlKind::If { + let super::core::ControlFrame::If { + else_block, + end_block: if_end_block, + result_var, + locals_at_entry, + branch_incoming, + .. + } = if_frame + else { bail!("else without matching if"); - } + }; - // Then branch: assign result if needed - let result_var = if_frame.result_var; + // Step 1a: Save then-branch fall-through as a phi predecessor for end_block. + // Only recorded if the then-branch body is reachable (not dead code after + // an unconditional `br` or `return` inside the then-branch). + let then_pred_info = if !self.dead_code { + Some((self.current_block, self.local_vars.clone())) + } else { + None + }; + + // Step 1b: Assign the result variable from the then-branch value (if typed). if let Some(result_var) = result_var { - let stack_value = self.value_stack.pop().ok_or_else(|| { - anyhow::anyhow!("Stack underflow for then result in else") - })?; - self.emit(IrInstr::Assign { - dest: result_var, - src: stack_value, - }); + if let Some(stack_value) = self.value_stack.pop() { + self.emit_void(IrInstr::Assign { + dest: result_var.var_id(), + src: stack_value.var_id(), + }); + } } - // Current block (then branch) jumps to end - self.terminate(IrTerminator::Jump { - target: if_frame.end_block, + // Step 1c: Terminate the then-branch with a jump to end_block. + self.terminate_if_live(IrTerminator::Jump { + target: if_end_block, }); - // Use the pre-allocated else block - let else_block = if_frame - .else_block - .expect("If frame should have else_block"); - self.start_block(else_block); - - // Push else frame (same end block, same result_var, no else_block needed) - // We manually create the frame to preserve result_var - self.control_stack.push(super::core::ControlFrame { - kind: super::core::ControlKind::Else, - start_block: else_block, - end_block: if_frame.end_block, - else_block: None, - result_type: if_frame.result_type, + // Step 3: Restore local_vars to the pre-if snapshot. + // The else-branch must see the same locals that entered the if. + self.local_vars = locals_at_entry.clone(); + + // Step 4: Activate the else block (always reachable: false path of BranchIf). + self.start_real_block(else_block); + + // Push Else frame. Transfer `branch_incoming` from the If frame so that + // any `br 0` emitted inside the then-branch is still tracked as a predecessor + // of end_block when End is processed. + self.control_stack.push(super::core::ControlFrame::Else { + end_block: if_end_block, result_var, + branch_incoming, // then-body forward branches + then_pred_info, // then fall-through predecessor }); } Operator::Br { relative_depth } => { - let target = self.get_branch_target(*relative_depth)?; + // === Unconditional branch === + // + // "br N" jumps to the Nth enclosing control structure: + // - Loop frame → backward branch to start_block (re-enter the loop) + // - Other frame → forward branch to end_block (exit the block) + // + // Before terminating the block we snapshot `local_vars` and record it + // as a phi predecessor for the target frame. This snapshot is used by + // `insert_phis_at_join` (or `emit_loop_phis`) to build the phi sources. + let (target, is_loop, frame_idx) = self.resolve_branch_info(*relative_depth)?; + + // Record snapshot *before* terminating so local_vars is still valid. + if is_loop { + // Backward branch: store (phi_var, current_block, src_var) in phi_patches. + // Consumed by emit_loop_phis when the loop's End is processed. + self.record_loop_back_branch(frame_idx); + } else { + // Forward branch: push (current_block, local_vars) into branch_incoming. + // Consumed by insert_phis_at_join when the target frame's End is processed. + self.record_forward_branch(frame_idx); + } + self.terminate(IrTerminator::Jump { target }); - // Create unreachable continuation block - let unreachable_block = self.new_block(); - self.start_block(unreachable_block); + // Everything after an unconditional branch is unreachable. + // We still need a block to absorb any subsequent instructions + // (e.g. the End of the enclosing block) without corrupting the + // already-terminated block. + let dead_block = self.new_block(); + self.start_block(dead_block); + self.dead_code = true; } Operator::BrIf { relative_depth } => { + // === Conditional branch === + // + // "br_if N" pops an i32 condition. If nonzero, branches to the Nth + // enclosing frame's target; otherwise falls through to the next instruction. + // + // Phi tracking: we record the *taken* path as a predecessor of the target + // frame (same as unconditional Br). The fall-through path continues in a + // new block and does NOT need recording here — the current `local_vars` + // state carries forward naturally into the continuation block. let condition = self .value_stack .pop() - .ok_or_else(|| anyhow::anyhow!("Stack underflow for br_if"))?; + .ok_or_else(|| anyhow::anyhow!("Stack underflow for br_if"))? + .var_id(); - let target = self.get_branch_target(*relative_depth)?; + let (target, is_loop, frame_idx) = self.resolve_branch_info(*relative_depth)?; - // Create continuation block (fallthrough) + // Record the taken branch as a phi predecessor (snapshot before termination). + if is_loop { + self.record_loop_back_branch(frame_idx); + } else { + self.record_forward_branch(frame_idx); + } + + // The fall-through block is always reachable (the false path of BranchIf). let continue_block = self.new_block(); self.terminate(IrTerminator::BranchIf { @@ -786,24 +1038,54 @@ impl IrBuilder { if_false: continue_block, }); - // Continue building in continuation block - self.start_block(continue_block); + // start_real_block clears dead_code — the fallthrough is always live. + self.start_real_block(continue_block); } Operator::BrTable { targets } => { + // === Jump table === + // + // "br_table [d0 d1 ... dN] default" pops an index and branches to depth + // d_index if index ≤ N, otherwise to `default`. + // + // Phi tracking: each distinct target frame must receive a predecessor + // snapshot. Multiple table entries may resolve to the *same* frame + // (same depth → same frame_idx), so we deduplicate by frame_idx using + // `recorded` to avoid recording the same block twice for the same phi. let index = self .value_stack .pop() - .ok_or_else(|| anyhow::anyhow!("Stack underflow for br_table"))?; + .ok_or_else(|| anyhow::anyhow!("Stack underflow for br_table"))? + .var_id(); - // Convert targets to BlockIds let target_depths: Vec = targets.targets().collect::, _>>()?; + let default_depth = targets.default(); + + // Collect all branch targets and record phi predecessors, deduplicating by frame_idx + // (multiple table entries may point to the same target frame). + let mut recorded: std::collections::HashSet = + std::collections::HashSet::new(); + for depth in target_depths + .iter() + .copied() + .chain(std::iter::once(default_depth)) + { + let (_, is_loop, frame_idx) = self.resolve_branch_info(depth)?; + if recorded.insert(frame_idx) { + if is_loop { + self.record_loop_back_branch(frame_idx); + } else { + self.record_forward_branch(frame_idx); + } + } + } + let target_blocks: Vec = target_depths .iter() .map(|depth| self.get_branch_target(*depth)) .collect::>>()?; - let default = self.get_branch_target(targets.default())?; + let default = self.get_branch_target(default_depth)?; self.terminate(IrTerminator::BranchTable { index, @@ -811,9 +1093,10 @@ impl IrBuilder { default, }); - // Create unreachable continuation block - let unreachable_block = self.new_block(); - self.start_block(unreachable_block); + // Everything after br_table is unreachable (same as Br). + let dead_block = self.new_block(); + self.start_block(dead_block); + self.dead_code = true; } Operator::Call { function_index } => { @@ -826,7 +1109,14 @@ impl IrBuilder { let args = self.pop_call_args(param_count, &format!("call to func_{}", func_idx))?; - let dest = callee_return_type.map(|_| self.new_var()); + // For optional-result calls we use new_pre_alloc_var: the dest is + // defined by the call instruction itself, not via emit_def. + let (dest_id, dest_use) = if callee_return_type.is_some() { + let (id, u) = self.new_pre_alloc_var(); + (Some(id), Some(u)) + } else { + (None, None) + }; // Check if this is a call to an imported function or a local function if func_idx < self.num_imported_functions { @@ -837,8 +1127,8 @@ impl IrBuilder { anyhow::anyhow!("Call: import index {} out of range", import_idx) })?; - self.emit(IrInstr::CallImport { - dest, + self.emit_void(IrInstr::CallImport { + dest: dest_id, import_idx: ImportIdx::new(import_idx), module_name, func_name, @@ -847,15 +1137,15 @@ impl IrBuilder { } else { // Call to local function - convert to local index let local_func_idx = func_idx - self.num_imported_functions; - self.emit(IrInstr::Call { - dest, + self.emit_void(IrInstr::Call { + dest: dest_id, func_idx: LocalFuncIdx::new(local_func_idx), args, }); } - if let Some(d) = dest { - self.value_stack.push(d); + if let Some(u) = dest_use { + self.value_stack.push(u); } } @@ -873,9 +1163,13 @@ impl IrBuilder { })?; // Pop table element index (on top of stack) - let table_idx_var = self.value_stack.pop().ok_or_else(|| { - anyhow::anyhow!("Stack underflow for call_indirect table index") - })?; + let table_idx_var = self + .value_stack + .pop() + .ok_or_else(|| { + anyhow::anyhow!("Stack underflow for call_indirect table index") + })? + .var_id(); // Pop arguments let args = self.pop_call_args( @@ -883,16 +1177,21 @@ impl IrBuilder { &format!("call_indirect type {}", type_idx_usize), )?; - let dest = callee_return_type.map(|_| self.new_var()); - self.emit(IrInstr::CallIndirect { - dest, + let (dest_id, dest_use) = if callee_return_type.is_some() { + let (id, u) = self.new_pre_alloc_var(); + (Some(id), Some(u)) + } else { + (None, None) + }; + self.emit_void(IrInstr::CallIndirect { + dest: dest_id, type_idx: TypeIdx::new(*type_index as usize), table_idx: table_idx_var, args, }); - if let Some(d) = dest { - self.value_stack.push(d); + if let Some(u) = dest_use { + self.value_stack.push(u); } } @@ -901,6 +1200,7 @@ impl IrBuilder { // Create unreachable continuation block (dead code follows) let unreachable_block = self.new_block(); self.start_block(unreachable_block); + self.dead_code = true; } Operator::Select => { @@ -919,14 +1219,14 @@ impl IrBuilder { .value_stack .pop() .ok_or_else(|| anyhow::anyhow!("stack underflow in Select (val1)"))?; - let dest = self.new_var(); - self.emit(IrInstr::Select { - dest, - val1, - val2, - condition, + let def = self.new_var(); + let use_v = self.emit_def(def, |d| IrInstr::Select { + dest: d, + val1: val1.var_id(), + val2: val2.var_id(), + condition: condition.var_id(), }); - self.value_stack.push(dest); + self.value_stack.push(use_v); } // === Bulk memory operations === @@ -947,7 +1247,11 @@ impl IrBuilder { .value_stack .pop() .ok_or_else(|| anyhow::anyhow!("Stack underflow for memory.copy (dst)"))?; - self.emit(IrInstr::MemoryCopy { dst, src, len }); + self.emit_void(IrInstr::MemoryCopy { + dst: dst.var_id(), + src: src.var_id(), + len: len.var_id(), + }); } _ => bail!("Unsupported operator: {:?}", op), @@ -964,7 +1268,8 @@ impl IrBuilder { Some( self.value_stack .pop() - .ok_or_else(|| anyhow::anyhow!("stack underflow in return"))?, + .ok_or_else(|| anyhow::anyhow!("stack underflow in return"))? + .var_id(), ) }; self.terminate(IrTerminator::Return { value }); @@ -986,9 +1291,13 @@ impl IrBuilder { .pop() .ok_or_else(|| anyhow::anyhow!("stack underflow in binop (lhs)"))?; let dest = self.new_var(); - - self.emit(IrInstr::BinOp { dest, op, lhs, rhs }); - self.value_stack.push(dest); + let use_v = self.emit_def(dest, |v| IrInstr::BinOp { + dest: v, + op, + lhs: lhs.var_id(), + rhs: rhs.var_id(), + }); + self.value_stack.push(use_v); Ok(()) } @@ -1004,9 +1313,12 @@ impl IrBuilder { .pop() .ok_or_else(|| anyhow::anyhow!("stack underflow in unop (operand)"))?; let dest = self.new_var(); - - self.emit(IrInstr::UnOp { dest, op, operand }); - self.value_stack.push(dest); + let use_v = self.emit_def(dest, |v| IrInstr::UnOp { + dest: v, + op, + operand: operand.var_id(), + }); + self.value_stack.push(use_v); Ok(()) } @@ -1035,17 +1347,15 @@ impl IrBuilder { .pop() .ok_or_else(|| anyhow::anyhow!("stack underflow in load (addr)"))?; let dest = self.new_var(); - - self.emit(IrInstr::Load { - dest, + let use_v = self.emit_def(dest, |v| IrInstr::Load { + dest: v, ty, - addr, + addr: addr.var_id(), offset: offset as u32, width, sign, }); - - self.value_stack.push(dest); + self.value_stack.push(use_v); Ok(()) } @@ -1063,9 +1373,14 @@ impl IrBuilder { } let mut args = Vec::with_capacity(param_count); for _ in 0..param_count { - args.push(self.value_stack.pop().ok_or_else(|| { - anyhow::anyhow!("stack underflow collecting {} arguments", context) - })?); + args.push( + self.value_stack + .pop() + .ok_or_else(|| { + anyhow::anyhow!("stack underflow collecting {} arguments", context) + })? + .var_id(), + ); } args.reverse(); Ok(args) @@ -1092,10 +1407,10 @@ impl IrBuilder { .pop() .ok_or_else(|| anyhow::anyhow!("stack underflow in store (addr)"))?; - self.emit(IrInstr::Store { + self.emit_void(IrInstr::Store { ty, - addr, - value, + addr: addr.var_id(), + value: value.var_id(), offset: offset as u32, width, }); diff --git a/crates/herkos/src/ir/lower_phis.rs b/crates/herkos/src/ir/lower_phis.rs new file mode 100644 index 0000000..ee81849 --- /dev/null +++ b/crates/herkos/src/ir/lower_phis.rs @@ -0,0 +1,425 @@ +//! SSA phi-node lowering. +//! +//! ## Overview +//! +//! This pass converts `IrInstr::Phi` nodes — which are SSA-form join-point +//! selectors — into ordinary `IrInstr::Assign` instructions placed at the end +//! of predecessor blocks (just before their terminators). After this pass, no +//! `IrInstr::Phi` instructions remain in the IR. +//! +//! This pass **must run before all optimizer passes** and **before codegen**. +//! It is a phase transition (SSA destruction), not an optimization. +//! +//! ## Algorithm +//! +//! For each function: +//! +//! 1. **Prune stale phi sources**: dead_blocks may have removed predecessor +//! blocks. Remove any `(pred_block, _)` entry in a Phi's `srcs` whose +//! `pred_block` no longer exists in `func.blocks`. +//! +//! 2. **Simplify trivial phis**: A phi is trivial if: +//! - It has no sources → dead (replace with self-assign, will be removed by +//! dead_instrs if unused). +//! - Ignoring self-references (`src == dest`), all remaining sources resolve +//! to the same single variable. +//! +//! In both cases, replace `Phi { dest, srcs }` with `Assign { dest, src }`. +//! Repeat until no more trivial phis can be simplified. +//! +//! 3. **Lower non-trivial phis**: For each remaining `Phi { dest, srcs }`: +//! - In each predecessor block, insert `Assign { dest, src }` just before +//! the block's terminator. +//! - Remove the `Phi` instruction from the join block. +//! +//! ## Example +//! +//! Given an `if/else` that produces a value, the SSA IR contains a phi at the +//! merge block: +//! +//! ```text +//! block0 (entry): +//! v0 = i32.const 1 +//! v1 = i32.const 2 +//! br_if v_cond → block1, block2 +//! +//! block1 (then): +//! br → block3 +//! +//! block2 (else): +//! br → block3 +//! +//! block3 (merge): +//! v2 = phi [(block1, v0), (block2, v1)] ← SSA phi +//! ...use v2... +//! ``` +//! +//! After lowering, the phi is removed and each predecessor gets an assignment: +//! +//! ```text +//! block1 (then): +//! v2 = v0 ← inserted before terminator +//! br → block3 +//! +//! block2 (else): +//! v2 = v1 ← inserted before terminator +//! br → block3 +//! +//! block3 (merge): +//! ...use v2... ← phi gone; v2 already set +//! ``` +//! +//! ## Why predecessor assignments? +//! +//! A phi at a join point conceptually says "take the value from whichever +//! predecessor we came from". In the generated Rust state machine, the predecessor +//! block's code runs immediately before transitioning to the join block, so +//! assigning there is equivalent to selecting based on the taken path. + +use crate::ir::{BlockId, IrFunction, IrInstr, ModuleInfo, VarId}; +use std::collections::HashSet; + +/// Lower all `IrInstr::Phi` nodes in `module_info`, returning a [`super::LoweredModuleInfo`]. +/// +/// After this call, no `IrInstr::Phi` instructions remain in any function. +/// The returned [`super::LoweredModuleInfo`] can be passed to the optimizer and codegen. +pub fn lower(module_info: ModuleInfo) -> super::LoweredModuleInfo { + let mut module_info = module_info; + for func in &mut module_info.ir_functions { + lower_func(func); + debug_assert!( + func.blocks + .iter() + .flat_map(|b| &b.instructions) + .all(|i| !matches!(i, IrInstr::Phi { .. })), + "lower_phis: phi instructions remain after lowering — bug in lower_func" + ); + } + super::LoweredModuleInfo(module_info) +} + +/// Lower all `IrInstr::Phi` nodes in a single function. +fn lower_func(func: &mut IrFunction) { + // Collect the set of live block IDs present in the IR right now. + // Note: lower_phis runs before the optimizer, so no optimizer dead-block + // elimination has happened yet. This pruning handles blocks that were never + // emitted (e.g. code after `unreachable` in the IR builder) — their block + // IDs may still appear as phi sources even though the blocks were dropped. + let live_blocks: HashSet = func.blocks.iter().map(|b| b.id).collect(); + + // Step 1: Prune phi sources that refer to removed (dead) predecessor blocks. + for block in &mut func.blocks { + for instr in &mut block.instructions { + if let IrInstr::Phi { srcs, .. } = instr { + srcs.retain(|(pred_id, _)| live_blocks.contains(pred_id)); + } + } + } + + // Step 2: Simplify trivial phis to Assign, iterating to fixpoint. + // + // A phi is trivial when — after removing self-references — all remaining + // sources point to the same VarId. We iterate because simplifying one phi + // may allow another that referenced it to become trivial. + // + // Example — loop-invariant variable (self-reference only): + // + // v1 = phi [(block0, v0), (block2, v1)] + // ^^^^^^^^^^^^^^^^^^ pre-loop value + // ^^^^^^^^^^^ back-edge (self-ref, ignored) + // + // → all non-self sources resolve to v0 → trivial + // → simplified to: v1 = v0 + // + // Example — chain: simplifying v2 unlocks v3: + // + // v2 = phi [(block0, v0), (block1, v0)] ← both sources same → trivial + // v3 = phi [(block0, v2), (block1, v2)] ← after v2→v0: both v0 → trivial + // + // pass 1: v2 → Assign { dest: v2, src: v0 } + // pass 2: v3 → Assign { dest: v3, src: v0 } + loop { + let mut changed = false; + for block in &mut func.blocks { + for instr in &mut block.instructions { + if let IrInstr::Phi { dest, srcs } = instr { + let phi_dest = *dest; + + // Collect unique non-self sources + let non_self: Vec = srcs + .iter() + .map(|(_, v)| *v) + .filter(|&v| v != phi_dest) + .collect::>() + .into_iter() + .collect(); + + let trivial_src = if non_self.is_empty() { + // All sources are self-references — phi is dead. + // Replace with an assign from itself (a no-op) so the + // dead_instrs pass can remove it if unused. + Some(phi_dest) + } else if non_self.len() == 1 { + // All non-self sources agree on one value. + Some(non_self[0]) + } else { + None + }; + + if let Some(src) = trivial_src { + *instr = IrInstr::Assign { + dest: phi_dest, + src, + }; + changed = true; + } + } + } + } + + if !changed { + break; + } + + // After simplifying a phi, propagate the change: replace uses of the + // former phi's dest with its now-known single source in the same block. + // (Full global propagation is handled by copy_prop; we just do the local + // fixpoint simplification of other phis in the same block here.) + for block in &mut func.blocks { + // Collect Assign(dest, src) replacements from this iteration's simplifications. + let replacements: Vec<(VarId, VarId)> = block + .instructions + .iter() + .filter_map(|i| { + if let IrInstr::Assign { dest, src } = i { + // Only propagate trivial-phi-turned-assigns where src != dest + if *dest != *src { + Some((*dest, *src)) + } else { + None + } + } else { + None + } + }) + .collect(); + + for (old, new) in replacements { + for instr in &mut block.instructions { + replace_phi_src(instr, old, new); + } + } + } + } + + // Step 3: Lower non-trivial phis to predecessor-block assignments. + // + // We collect all phi nodes first, then mutate blocks. + // `phi_assignments` maps `(dest, pred_block) -> src`. + // `phi_locations` maps `block_id -> [phi_dest]` so we know which phis to remove. + let mut phi_assignments: Vec<(BlockId, IrInstr)> = Vec::new(); + let mut phi_block_dests: Vec<(BlockId, VarId)> = Vec::new(); + + for block in &func.blocks { + for instr in &block.instructions { + if let IrInstr::Phi { dest, srcs } = instr { + phi_block_dests.push((block.id, *dest)); + for (pred_block, src) in srcs { + phi_assignments.push(( + *pred_block, + IrInstr::Assign { + dest: *dest, + src: *src, + }, + )); + } + } + } + } + + // Insert assignments into predecessor blocks (before the terminator). + for (pred_id, assign) in phi_assignments { + if let Some(block) = func.blocks.iter_mut().find(|b| b.id == pred_id) { + // Insert just before the terminator (i.e., at the end of the instruction list). + block.instructions.push(assign); + } + } + + // Remove phi instructions from their join blocks. + let phi_dests: HashSet = phi_block_dests.iter().map(|(_, d)| *d).collect(); + for block in &mut func.blocks { + block + .instructions + .retain(|i| !matches!(i, IrInstr::Phi { dest, .. } if phi_dests.contains(dest))); + } +} + +/// Replace every read-occurrence of `old` with `new` in `instr`. +/// Used during trivial-phi simplification to propagate the resolved source. +fn replace_phi_src(instr: &mut IrInstr, old: VarId, new: VarId) { + let sub = |v: &mut VarId| { + if *v == old { + *v = new; + } + }; + match instr { + IrInstr::Phi { srcs, .. } => { + for (_, src) in srcs { + sub(src); + } + } + IrInstr::Assign { src, .. } => sub(src), + // Other instruction kinds don't appear before phi lowering completes + // within the trivial-phi loop, so only phi/assign need handling here. + _ => {} + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::ir::{BlockId, IrBlock, IrFunction, IrTerminator, IrValue, TypeIdx, VarId}; + + fn make_module(blocks: Vec) -> ModuleInfo { + ModuleInfo { + has_memory: false, + has_memory_import: false, + max_pages: 0, + initial_pages: 0, + table_initial: 0, + table_max: 0, + element_segments: Vec::new(), + globals: Vec::new(), + data_segments: Vec::new(), + func_exports: Vec::new(), + type_signatures: Vec::new(), + canonical_type: Vec::new(), + func_imports: Vec::new(), + imported_globals: Vec::new(), + ir_functions: vec![IrFunction { + params: vec![], + locals: vec![], + blocks, + entry_block: BlockId(0), + return_type: None, + type_idx: TypeIdx::new(0), + needs_host: false, + }], + wasm_version: 1, + } + } + + /// A trivial phi where all non-self sources agree: phi(v0, v0) → Assign(dest, v0). + #[test] + fn test_trivial_phi_same_source() { + // block_0: v0 = const 1; jump block_1 + // block_1: v1 = phi((block_0, v0), (block_0, v0)); return v1 + let block0 = IrBlock { + id: BlockId(0), + instructions: vec![IrInstr::Const { + dest: VarId(0), + value: IrValue::I32(1), + }], + terminator: IrTerminator::Jump { target: BlockId(1) }, + }; + let block1 = IrBlock { + id: BlockId(1), + instructions: vec![IrInstr::Phi { + dest: VarId(1), + srcs: vec![(BlockId(0), VarId(0)), (BlockId(0), VarId(0))], + }], + terminator: IrTerminator::Return { + value: Some(VarId(1)), + }, + }; + + let module = make_module(vec![block0, block1]); + let lowered = lower(module); + + // phi should be simplified to Assign + let instrs = &lowered.ir_functions[0].blocks[1].instructions; + assert_eq!(instrs.len(), 1); + assert!(matches!( + instrs[0], + IrInstr::Assign { + dest: VarId(1), + src: VarId(0) + } + )); + } + + /// A non-trivial phi with two different sources gets lowered to predecessor assignments. + #[test] + fn test_non_trivial_phi_lowering() { + // block_0: v0=1; br_if block_1 else block_2 + // block_1: v1=2; jump block_3 + // block_2: v2=3; jump block_3 + // block_3: v3=phi((block_1,v1),(block_2,v2)); return v3 + let block0 = IrBlock { + id: BlockId(0), + instructions: vec![IrInstr::Const { + dest: VarId(0), + value: IrValue::I32(0), + }], + terminator: IrTerminator::BranchIf { + condition: VarId(0), + if_true: BlockId(1), + if_false: BlockId(2), + }, + }; + let block1 = IrBlock { + id: BlockId(1), + instructions: vec![IrInstr::Const { + dest: VarId(1), + value: IrValue::I32(2), + }], + terminator: IrTerminator::Jump { target: BlockId(3) }, + }; + let block2 = IrBlock { + id: BlockId(2), + instructions: vec![IrInstr::Const { + dest: VarId(2), + value: IrValue::I32(3), + }], + terminator: IrTerminator::Jump { target: BlockId(3) }, + }; + let block3 = IrBlock { + id: BlockId(3), + instructions: vec![IrInstr::Phi { + dest: VarId(3), + srcs: vec![(BlockId(1), VarId(1)), (BlockId(2), VarId(2))], + }], + terminator: IrTerminator::Return { + value: Some(VarId(3)), + }, + }; + + let module = make_module(vec![block0, block1, block2, block3]); + let lowered = lower(module); + let func = &lowered.ir_functions[0]; + + // No phi in block3 after lowering + assert!(!func.blocks[3] + .instructions + .iter() + .any(|i| matches!(i, IrInstr::Phi { .. }))); + + // block1 should have an Assign v3 = v1 appended + assert!(func.blocks[1].instructions.iter().any(|i| matches!( + i, + IrInstr::Assign { + dest: VarId(3), + src: VarId(1) + } + ))); + + // block2 should have an Assign v3 = v2 appended + assert!(func.blocks[2].instructions.iter().any(|i| matches!( + i, + IrInstr::Assign { + dest: VarId(3), + src: VarId(2) + } + ))); + } +} diff --git a/crates/herkos/src/ir/mod.rs b/crates/herkos/src/ir/mod.rs index e70694f..694c185 100644 --- a/crates/herkos/src/ir/mod.rs +++ b/crates/herkos/src/ir/mod.rs @@ -7,9 +7,33 @@ //! It includes: //! - **Per-function IR** ([`IrFunction`], [`IrBlock`], [`IrInstr`]): SSA-form IR for function bodies //! - **Module-level IR** ([`ModuleInfo`] and related types): Module structure and metadata +//! - **[`LoweredModuleInfo`]**: Post-SSA-destruction wrapper; no `IrInstr::Phi` nodes remain mod types; pub use types::*; pub mod builder; pub use builder::{build_module_info, ModuleContext}; + +pub mod lower_phis; + +/// [`ModuleInfo`] with all `IrInstr::Phi` nodes lowered to `IrInstr::Assign`. +/// +/// Constructed exclusively by [`lower_phis::lower`]. Signals the phase +/// boundary between SSA IR (with phi nodes) and post-SSA IR (without). +/// After this point, no optimizer pass or codegen module will encounter +/// `IrInstr::Phi` in any function body. +pub struct LoweredModuleInfo(ModuleInfo); + +impl std::ops::Deref for LoweredModuleInfo { + type Target = ModuleInfo; + fn deref(&self) -> &ModuleInfo { + &self.0 + } +} + +impl std::ops::DerefMut for LoweredModuleInfo { + fn deref_mut(&mut self) -> &mut ModuleInfo { + &mut self.0 + } +} diff --git a/crates/herkos/src/ir/types.rs b/crates/herkos/src/ir/types.rs index 02bf3ab..22158e6 100644 --- a/crates/herkos/src/ir/types.rs +++ b/crates/herkos/src/ir/types.rs @@ -12,6 +12,44 @@ use std::fmt; #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] pub struct VarId(pub u32); +/// One-time-use definition token for an SSA variable. +/// +/// Returned by [`IrBuilder::new_var()`] and consumed by [`IrBuilder::emit_def()`]. +/// Non-`Copy`/non-`Clone` so that the borrow checker enforces single-definition: +/// attempting to emit the same variable twice is a compile-time error. +/// +/// ```compile_fail +/// let v = builder.new_var(); +/// let _ = builder.emit_def(v, |id| IrInstr::Const { dest: id, value: IrValue::I32(1) }); +/// let _ = builder.emit_def(v, |id| IrInstr::Const { dest: id, value: IrValue::I32(2) }); // error: use of moved value +/// ``` +#[must_use = "DefVar must be emitted exactly once via emit_def or emit_phi_def"] +#[derive(Debug)] +pub struct DefVar(pub(super) VarId); + +impl DefVar { + /// Consume this token and return the underlying [`VarId`]. + /// Used internally by emit methods to build [`IrInstr`] with the correct dest. + pub(super) fn into_var_id(self) -> VarId { + self.0 + } +} + +/// Multi-use read token for an already-defined SSA variable. +/// +/// Obtained from [`IrBuilder::emit_def()`] or [`IrBuilder::emit_phi_def()`]. +/// `Copy + Clone` because a variable may be read any number of times. +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] +pub struct UseVar(pub(super) VarId); + +impl UseVar { + /// Return the underlying [`VarId`]. + /// Used internally when building [`IrInstr`] source fields. + pub(super) fn var_id(self) -> VarId { + self.0 + } +} + /// Generic index type with a phantom tag to distinguish different index spaces. #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] pub struct Idx { @@ -315,6 +353,17 @@ pub enum IrInstr { val2: VarId, condition: VarId, }, + + /// SSA phi node: at a join point, select the reaching definition based on which + /// predecessor block was taken. `srcs` maps predecessor BlockId to the VarId that + /// holds the value of the local at the end of that predecessor. + /// + /// Phi nodes are inserted during IR construction and lowered (converted to Assign + /// instructions in predecessor blocks) before codegen. + Phi { + dest: VarId, + srcs: Vec<(BlockId, VarId)>, + }, } /// Block terminator — how control flow exits a basic block. diff --git a/crates/herkos/src/lib.rs b/crates/herkos/src/lib.rs index efa298a..d540971 100644 --- a/crates/herkos/src/lib.rs +++ b/crates/herkos/src/lib.rs @@ -14,7 +14,7 @@ pub use anyhow::{Context, Result}; use backend::SafeBackend; use codegen::CodeGenerator; use ir::builder::build_module_info; -use ir::ModuleInfo; +use ir::{lower_phis, LoweredModuleInfo}; use optimizer::optimize_ir; use parser::parse_wasm; @@ -65,6 +65,11 @@ pub fn transpile(wasm_bytes: &[u8], options: &TranspileOptions) -> Result Result Result { +fn generate_rust_code(module_info: &LoweredModuleInfo) -> Result { let backend = SafeBackend::new(); let codegen = CodeGenerator::new(&backend); diff --git a/crates/herkos/src/optimizer/mod.rs b/crates/herkos/src/optimizer/mod.rs index fb666b1..cccb988 100644 --- a/crates/herkos/src/optimizer/mod.rs +++ b/crates/herkos/src/optimizer/mod.rs @@ -6,14 +6,17 @@ //! Each optimization is a self-contained sub-module. The top-level //! [`optimize_ir`] function runs all passes in order. -use crate::ir::ModuleInfo; +use crate::ir::LoweredModuleInfo; use anyhow::Result; // ── Passes ─────────────────────────────────────────────────────────────────── mod dead_blocks; /// Optimizes the IR representation by running all passes in order. -pub fn optimize_ir(module_info: ModuleInfo) -> Result { +/// +/// Expects a [`LoweredModuleInfo`] — i.e. phi nodes have already been lowered +/// by [`crate::ir::lower_phis::lower`] before calling this function. +pub fn optimize_ir(module_info: LoweredModuleInfo) -> Result { let mut module_info = module_info; for func in &mut module_info.ir_functions { dead_blocks::eliminate(func)?; @@ -71,7 +74,7 @@ mod tests { ..Default::default() }; - let result = super::optimize_ir(module).unwrap(); + let result = super::optimize_ir(crate::ir::lower_phis::lower(module)).unwrap(); assert_eq!( result.ir_functions[0].blocks.len(), 1,