v / vlib / v2 / ssa / optimize / dce.v
163 lines · 148 sloc · 4.6 KB · 9e3e02d5ea5b1cb300b5f5304cd821f47337b008
Raw
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
5module optimize
6
7import v2.ssa
8
9fn 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
75fn 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.
97fn 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