| 1 | // Copyright (c) 2026 Alexander Medvednikov. All rights reserved. |
| 2 | // Use of this source code is governed by an MIT license |
| 3 | // that can be found in the LICENSE file. |
| 4 | |
| 5 | module optimize |
| 6 | |
| 7 | import v2.ssa |
| 8 | |
| 9 | fn dead_code_elimination(mut m ssa.Module) bool { |
| 10 | mut any_changed := false |
| 11 | mut changed := true |
| 12 | for changed { |
| 13 | changed = false |
| 14 | for fi in 0 .. m.funcs.len { |
| 15 | // Dead store elimination: find stores to local allocas that are never read |
| 16 | dead_stores := find_dead_stores(m, &m.funcs[fi]) |
| 17 | |
| 18 | for blk_id in m.funcs[fi].blocks { |
| 19 | // First pass: find dead instructions (don't modify yet) |
| 20 | mut dead_instrs := []int{} |
| 21 | for val_id in m.blocks[blk_id].instrs { |
| 22 | val := m.values[val_id] |
| 23 | if val.kind == .instruction { |
| 24 | instr := m.instrs[val.index] |
| 25 | // Check if this is a dead store |
| 26 | if instr.op == .store && val_id in dead_stores { |
| 27 | dead_instrs << val_id |
| 28 | continue |
| 29 | } |
| 30 | side_effects := instr.op in [.store, .call, .call_indirect, .call_sret, |
| 31 | .ret, .br, .jmp, .switch_, .unreachable, .assign, .fence, .cmpxchg, |
| 32 | .atomicrmw, .go_call, .spawn_call] |
| 33 | if !side_effects && val.uses.len == 0 { |
| 34 | dead_instrs << val_id |
| 35 | } |
| 36 | } |
| 37 | } |
| 38 | |
| 39 | // Only rebuild the instruction list if we found dead instructions |
| 40 | if dead_instrs.len > 0 { |
| 41 | // Remove uses from dead instructions |
| 42 | for val_id in dead_instrs { |
| 43 | instr := m.instrs[m.values[val_id].index] |
| 44 | for op_id in instr.operands { |
| 45 | remove_use(mut m, op_id, val_id) |
| 46 | } |
| 47 | } |
| 48 | |
| 49 | // Build set for O(1) lookup |
| 50 | mut dead_set := map[int]bool{} |
| 51 | for d in dead_instrs { |
| 52 | dead_set[d] = true |
| 53 | } |
| 54 | |
| 55 | // Filter out dead instructions |
| 56 | mut new_instrs := []int{cap: m.blocks[blk_id].instrs.len} |
| 57 | for val_id in m.blocks[blk_id].instrs { |
| 58 | if !dead_set[val_id] { |
| 59 | new_instrs << val_id |
| 60 | } |
| 61 | } |
| 62 | // Avoid m.blocks[X].instrs = ... -- chained field assign broken in ARM64 self-hosted |
| 63 | mut dce_blk := m.blocks[blk_id] |
| 64 | dce_blk.instrs = new_instrs |
| 65 | m.blocks[blk_id] = dce_blk |
| 66 | changed = true |
| 67 | any_changed = true |
| 68 | } |
| 69 | } |
| 70 | } |
| 71 | } |
| 72 | return any_changed |
| 73 | } |
| 74 | |
| 75 | fn remove_use(mut m ssa.Module, val_id int, user_id int) { |
| 76 | if val_id >= m.values.len { |
| 77 | return |
| 78 | } |
| 79 | mut val := &m.values[val_id] |
| 80 | // Remove all occurrences (handles instructions that use same value multiple times) |
| 81 | // Iterate in reverse to safely delete while iterating |
| 82 | for i := val.uses.len - 1; i >= 0; i-- { |
| 83 | if val.uses[i] == user_id { |
| 84 | val.uses.delete(i) |
| 85 | } |
| 86 | } |
| 87 | } |
| 88 | |
| 89 | // Find stores to local allocas that are never read (dead stores). |
| 90 | // A store is dead if: |
| 91 | // 1. It stores to a local alloca |
| 92 | // 2. The alloca is ONLY used by stores (no loads, GEPs, calls, or other uses) |
| 93 | // |
| 94 | // We must be conservative: if the alloca pointer escapes (used by GEP, passed to |
| 95 | // a function, etc.), we cannot eliminate stores because the value might be read |
| 96 | // through the escaped pointer. |
| 97 | fn find_dead_stores(m &ssa.Module, func &ssa.Function) map[int]bool { |
| 98 | mut dead_stores := map[int]bool{} |
| 99 | |
| 100 | // Find all local allocas |
| 101 | mut allocas := map[int]bool{} |
| 102 | mut alloca_stores := map[int][]int{} // alloca_id -> list of store val_ids |
| 103 | mut alloca_escapes := map[int]bool{} // alloca_id -> true if pointer escapes |
| 104 | |
| 105 | for blk_id in func.blocks { |
| 106 | for val_id in m.blocks[blk_id].instrs { |
| 107 | val := m.values[val_id] |
| 108 | if val.kind != .instruction { |
| 109 | continue |
| 110 | } |
| 111 | instr := m.instrs[val.index] |
| 112 | |
| 113 | if instr.op == .alloca || instr.op == .heap_alloc { |
| 114 | allocas[val_id] = true |
| 115 | } |
| 116 | } |
| 117 | } |
| 118 | |
| 119 | // Check how each alloca is used |
| 120 | for alloca_id, _ in allocas { |
| 121 | alloca_val := m.values[alloca_id] |
| 122 | for use_id in alloca_val.uses { |
| 123 | use_val := m.values[use_id] |
| 124 | if use_val.kind != .instruction { |
| 125 | // Non-instruction use - alloca escapes |
| 126 | alloca_escapes[alloca_id] = true |
| 127 | continue |
| 128 | } |
| 129 | use_instr := m.instrs[use_val.index] |
| 130 | match use_instr.op { |
| 131 | .store { |
| 132 | // Check if alloca is the destination (operand[1]), not the value being stored |
| 133 | if use_instr.operands.len > 1 && use_instr.operands[1] == alloca_id { |
| 134 | alloca_stores[alloca_id] << use_id |
| 135 | } else { |
| 136 | // Alloca is being stored somewhere - it escapes |
| 137 | alloca_escapes[alloca_id] = true |
| 138 | } |
| 139 | } |
| 140 | .load { |
| 141 | // Alloca is being read - stores are not dead |
| 142 | alloca_escapes[alloca_id] = true |
| 143 | } |
| 144 | else { |
| 145 | // Any other use (GEP, call, etc.) - alloca escapes |
| 146 | alloca_escapes[alloca_id] = true |
| 147 | } |
| 148 | } |
| 149 | } |
| 150 | } |
| 151 | |
| 152 | // Mark stores to non-escaping allocas as dead |
| 153 | for alloca_id, store_ids in alloca_stores { |
| 154 | if !alloca_escapes[alloca_id] { |
| 155 | for i := 0; i < store_ids.len; i++ { |
| 156 | store_id := store_ids[i] |
| 157 | dead_stores[store_id] = true |
| 158 | } |
| 159 | } |
| 160 | } |
| 161 | |
| 162 | return dead_stores |
| 163 | } |
| 164 | |