Initial project import

This commit is contained in:
Agila Forge Importer committed 2026-07-13 15:20:32 +00:00
commit 23f2d29e32
11957 files changed
+1754577

No files matched your search

+153
View File
@@ -0,0 +1,153 @@
module optimize
import v3.ssa
fn remove_unreachable_blocks(mut m ssa.Module) {
build_cfg(mut m)
for fi in 0 .. m.funcs.len {
if m.funcs[fi].blocks.len == 0 {
continue
}
mut reachable := map[int]bool{}
mut q := []int{}
q << m.funcs[fi].blocks[0]
reachable[m.funcs[fi].blocks[0]] = true
for q.len > 0 {
curr := q.last()
q.delete_last()
for succ in m.blocks[curr].succs {
if !reachable[succ] {
reachable[succ] = true
q << succ
}
}
}
mut new_blocks := []int{}
for blk in m.funcs[fi].blocks {
if reachable[blk] {
new_blocks << blk
}
}
mut func := m.funcs[fi]
func.blocks = new_blocks
m.funcs[fi] = func
}
}
fn merge_blocks(mut m ssa.Module) {
mut changed := true
mut first := true
for changed {
changed = false
if first {
build_cfg(mut m)
first = false
}
for fi in 0 .. m.funcs.len {
mut merged := map[int]bool{}
for blk_id in m.funcs[fi].blocks {
if merged[blk_id] {
continue
}
if m.blocks[blk_id].instrs.len == 0 {
continue
}
last_val := m.blocks[blk_id].instrs.last()
last_instr := m.instrs[m.values[last_val].index]
if last_instr.op != .jmp || last_instr.operands.len < 1 {
continue
}
target_id := int(last_instr.operands[0])
if target_id < 0 || target_id >= m.blocks.len || target_id == blk_id {
continue
}
if m.blocks[target_id].preds.len != 1 || m.blocks[target_id].preds[0] != blk_id {
continue
}
// Phi-safety: don't merge into a block that holds phi nodes. Its phis
// reference predecessors by block id; collapsing the edge would leave
// dangling predecessor operands.
mut target_has_phi := false
for vid in m.blocks[target_id].instrs {
if vid > 0 && vid < m.values.len && m.values[vid].kind == .instruction {
if m.instrs[m.values[vid].index].op == .phi {
target_has_phi = true
break
}
}
}
if target_has_phi {
continue
}
// Merge: remove jmp from A, append B's instrs (and reparent them).
mut blk := m.blocks[blk_id]
blk.instrs.delete_last()
for moved_val in m.blocks[target_id].instrs {
blk.instrs << moved_val
if moved_val > 0 && moved_val < m.values.len
&& m.values[moved_val].kind == .instruction {
instr_idx := m.values[moved_val].index
if instr_idx >= 0 && instr_idx < m.instrs.len {
mut instr := m.instrs[instr_idx]
instr.block = blk_id
m.instrs[instr_idx] = instr
}
}
}
m.blocks[blk_id] = blk
// B's successors now have A as a predecessor instead of B. Rewrite any
// phi predecessor operands that named B (raw block id) to name A.
for succ_id in m.blocks[target_id].succs {
if succ_id < 0 || succ_id >= m.blocks.len {
continue
}
for iv in m.blocks[succ_id].instrs {
if iv <= 0 || iv >= m.values.len || m.values[iv].kind != .instruction {
continue
}
idx := m.values[iv].index
if m.instrs[idx].op != .phi {
continue
}
mut phi_ins := m.instrs[idx]
mut phi_modified := false
for i := 1; i < phi_ins.operands.len; i += 2 {
if int(phi_ins.operands[i]) == target_id {
phi_ins.operands[i] = ssa.ValueID(blk_id)
phi_modified = true
}
}
if phi_modified {
m.instrs[idx] = phi_ins
}
}
}
merged[target_id] = true
changed = true
}
if merged.len > 0 {
mut new_blks := []int{}
for b in m.funcs[fi].blocks {
if !merged[b] {
new_blks << b
}
}
mut func := m.funcs[fi]
func.blocks = new_blks
m.funcs[fi] = func
}
}
if changed {
build_cfg(mut m)
}
}
}
+95
View File
@@ -0,0 +1,95 @@
module optimize
import v3.ssa
fn arr_contains(arr []int, val int) bool {
for v in arr {
if v == val {
return true
}
}
return false
}
fn build_cfg(mut m ssa.Module) {
n_blocks := m.blocks.len
n_values := m.values.len
for bi in 0 .. n_blocks {
mut blk := m.blocks[bi]
blk.succs = []
blk.preds = []
m.blocks[bi] = blk
}
for fi in 0 .. m.funcs.len {
for blk_id in m.funcs[fi].blocks {
blk := m.blocks[blk_id]
if blk.instrs.len == 0 {
continue
}
term_val_id := blk.instrs.last()
if term_val_id < 0 || term_val_id >= n_values {
continue
}
term_val := m.values[term_val_id]
if term_val.kind != .instruction {
continue
}
term := m.instrs[term_val.index]
match term.op {
.br {
if term.operands.len >= 3 {
for oi in 1 .. 3 {
s := int(term.operands[oi])
if s >= 0 && s < n_blocks && !arr_contains(m.blocks[blk_id].succs, s) {
mut b := m.blocks[blk_id]
b.succs << s
m.blocks[blk_id] = b
}
}
}
}
.jmp {
if term.operands.len >= 1 {
s := int(term.operands[0])
if s >= 0 && s < n_blocks {
mut b := m.blocks[blk_id]
b.succs << s
m.blocks[blk_id] = b
}
}
}
.switch_ {
// switch_ cond, default_blk, [case_val, blk]...
if term.operands.len >= 2 {
s := int(term.operands[1])
if s >= 0 && s < n_blocks && !arr_contains(m.blocks[blk_id].succs, s) {
mut b := m.blocks[blk_id]
b.succs << s
m.blocks[blk_id] = b
}
for oi := 3; oi < term.operands.len; oi += 2 {
cs := int(term.operands[oi])
if cs >= 0 && cs < n_blocks && !arr_contains(m.blocks[blk_id].succs, cs) {
mut b := m.blocks[blk_id]
b.succs << cs
m.blocks[blk_id] = b
}
}
}
}
else {}
}
for s in m.blocks[blk_id].succs {
if s >= 0 && s < n_blocks && !arr_contains(m.blocks[s].preds, blk_id) {
mut b := m.blocks[s]
b.preds << blk_id
m.blocks[s] = b
}
}
}
}
}
+139
View File
@@ -0,0 +1,139 @@
module optimize
import v3.ssa
fn dead_code_elimination(mut m ssa.Module) {
mut changed := true
for changed {
changed = false
for fi in 0 .. m.funcs.len {
dead_stores := find_dead_stores(m, m.funcs[fi])
for blk_id in m.funcs[fi].blocks {
mut dead_instrs := map[int]bool{}
for val_id in m.blocks[blk_id].instrs {
val := m.values[val_id]
if val.kind != .instruction {
continue
}
instr := m.instrs[val.index]
if instr.op == .store && val_id in dead_stores {
dead_instrs[val_id] = true
continue
}
has_side_effects := instr.op in [.store, .call, .call_indirect, .call_sret,
.ret, .br, .jmp, .switch_, .unreachable, .assign, .fence, .cmpxchg,
.atomicrmw, .go_call, .spawn_call]
if !has_side_effects && val.uses.len == 0 {
dead_instrs[val_id] = true
}
}
if dead_instrs.len > 0 {
for val_id, _ in dead_instrs {
instr := m.instrs[m.values[val_id].index]
for op_id in instr.value_operands() {
remove_use(mut m, op_id, val_id)
}
}
mut new_instrs := []int{cap: m.blocks[blk_id].instrs.len}
for val_id in m.blocks[blk_id].instrs {
if val_id !in dead_instrs {
new_instrs << val_id
}
}
mut blk := m.blocks[blk_id]
blk.instrs = new_instrs
m.blocks[blk_id] = blk
changed = true
}
}
}
}
}
fn remove_use(mut m ssa.Module, val_id int, user_id int) {
if val_id < 0 || val_id >= m.values.len {
return
}
mut val := m.values[val_id]
for i := val.uses.len - 1; i >= 0; i-- {
if val.uses[i] == user_id {
val.uses.delete(i)
}
}
m.values[val_id] = val
}
fn find_dead_stores(m &ssa.Module, func ssa.Function) map[int]bool {
mut dead_stores := map[int]bool{}
mut allocas := map[int]bool{}
mut alloca_stores := map[int][]int{}
mut alloca_escapes := map[int]bool{}
for blk_id in func.blocks {
for val_id in m.blocks[blk_id].instrs {
val := m.values[val_id]
if val.kind != .instruction {
continue
}
op := m.instrs[val.index].op
if op == .alloca || op == .heap_alloc {
allocas[val_id] = true
}
}
}
for alloca_id, _ in allocas {
for use_id in m.values[alloca_id].uses {
if use_id < 0 || use_id >= m.values.len {
continue
}
val := m.values[use_id]
if val.kind != .instruction {
alloca_escapes[alloca_id] = true
continue
}
instr := m.instrs[val.index]
match instr.op {
.store {
if instr.operands.len > 1 && instr.operands[1] == alloca_id {
alloca_stores[alloca_id] << use_id
} else {
alloca_escapes[alloca_id] = true
}
}
.load {}
else {
alloca_escapes[alloca_id] = true
}
}
}
}
// An alloca with no loads and no escapes → all stores to it are dead
for alloca_id, _ in allocas {
if alloca_id in alloca_escapes {
continue
}
mut has_load := false
for use_id in m.values[alloca_id].uses {
if use_id >= 0 && use_id < m.values.len && m.values[use_id].kind == .instruction {
if m.instrs[m.values[use_id].index].op == .load {
has_load = true
break
}
}
}
if !has_load {
if store_ids := alloca_stores[alloca_id] {
for store_id in store_ids {
dead_stores[store_id] = true
}
}
}
}
return dead_stores
}
+395
View File
@@ -0,0 +1,395 @@
module optimize
import v3.ssa
// CfgData holds successor/predecessor lists indexed by block id. v3 branch and
// phi operands store raw block ids, so this is built straight from the CFG that
// build_cfg() already wrote into m.blocks.
struct CfgData {
mut:
succs [][]int
preds [][]int
}
// cfg_data_from_module snapshots the successor/predecessor sets from m.blocks.
// build_cfg(mut m) must have been called first.
fn cfg_data_from_module(m &ssa.Module) CfgData {
n_blocks := m.blocks.len
mut cfg := CfgData{
succs: [][]int{len: n_blocks}
preds: [][]int{len: n_blocks}
}
for bi in 0 .. n_blocks {
mut s := []int{cap: m.blocks[bi].succs.len}
for x in m.blocks[bi].succs {
s << int(x)
}
cfg.succs[bi] = s
mut p := []int{cap: m.blocks[bi].preds.len}
for x in m.blocks[bi].preds {
p << int(x)
}
cfg.preds[bi] = p
}
return cfg
}
// --- Dominators (Lengauer-Tarjan) ---
struct LTContext {
mut:
parent []int
semi []int
vertex []int
bucket [][]int
dfnum []int
ancestor []int
label []int
idom []int
dom_tree [][]int
n int
}
struct DfsFrame {
mut:
node int
succ_idx int
}
// DomInfo holds dominator results in flat arrays indexed by block id.
struct DomInfo {
mut:
idom []int
dom_tree [][]int
}
// compute_dominators runs Lengauer-Tarjan for every function and also writes the
// idom / dom_tree results back into m.blocks for the verifier's dominance checks.
fn compute_dominators(mut m ssa.Module, cfg &CfgData) DomInfo {
max_id := m.blocks.len
mut ctx := LTContext{
parent: []int{len: max_id, init: -1}
semi: []int{len: max_id, init: -1}
vertex: []int{len: max_id + 1, init: -1}
bucket: [][]int{len: max_id}
dfnum: []int{len: max_id, init: 0}
ancestor: []int{len: max_id, init: -1}
label: []int{len: max_id, init: -1}
idom: []int{len: max_id, init: -1}
dom_tree: [][]int{len: max_id}
n: 0
}
for fi in 0 .. m.funcs.len {
func := m.funcs[fi]
if func.blocks.len == 0 {
continue
}
mut valid := true
n_func_blocks := func.blocks.len
for fbi in 0 .. n_func_blocks {
blk_id := func.blocks[fbi]
if blk_id < 0 || blk_id >= max_id {
valid = false
break
}
for s in cfg.succs[blk_id] {
if s < 0 || s >= max_id {
valid = false
break
}
}
if !valid {
break
}
for p in cfg.preds[blk_id] {
if p < 0 || p >= max_id {
valid = false
break
}
}
if !valid {
break
}
}
if !valid {
continue
}
ctx.n = 0
for fbi2 in 0 .. n_func_blocks {
blk_id := func.blocks[fbi2]
ctx.parent[blk_id] = -1
ctx.semi[blk_id] = blk_id
ctx.vertex[blk_id] = -1
ctx.bucket[blk_id] = []
ctx.dfnum[blk_id] = 0
ctx.ancestor[blk_id] = -1
ctx.label[blk_id] = blk_id
ctx.idom[blk_id] = -1
ctx.dom_tree[blk_id] = []
}
entry := func.blocks[0]
lt_dfs(entry, cfg, mut ctx)
for i := ctx.n; i >= 2; i-- {
if i >= ctx.vertex.len {
continue
}
w := ctx.vertex[i]
if w < 0 || w >= max_id {
continue
}
for p in cfg.preds[w] {
if p < 0 || p >= max_id || ctx.dfnum[p] == 0 {
continue
}
u := ctx.eval(p)
if u < 0 || u >= max_id {
continue
}
semi_u := ctx.semi[u]
semi_w := ctx.semi[w]
if semi_u < 0 || semi_u >= max_id || semi_w < 0 || semi_w >= max_id {
continue
}
if ctx.dfnum[semi_u] < ctx.dfnum[semi_w] {
ctx.semi[w] = ctx.semi[u]
}
}
semi_w2 := ctx.semi[w]
if semi_w2 >= 0 && semi_w2 < max_id {
mut b := ctx.bucket[semi_w2]
b << w
ctx.bucket[semi_w2] = b
}
ctx.link(ctx.parent[w], w)
parent_w := ctx.parent[w]
if parent_w < 0 || parent_w >= max_id {
continue
}
for v in ctx.bucket[parent_w] {
if v < 0 || v >= max_id {
continue
}
u := ctx.eval(v)
if u < 0 || u >= max_id {
continue
}
if ctx.semi[u] == ctx.semi[v] {
ctx.idom[v] = parent_w
} else {
ctx.idom[v] = u
}
}
ctx.bucket[parent_w] = []
}
for i := 2; i <= ctx.n; i++ {
if i >= ctx.vertex.len {
continue
}
w := ctx.vertex[i]
if w < 0 || w >= max_id {
continue
}
semi_w := ctx.semi[w]
if semi_w < 0 || semi_w >= max_id {
continue
}
dfnum_semi_w := ctx.dfnum[semi_w]
if dfnum_semi_w < 0 || dfnum_semi_w >= ctx.vertex.len {
continue
}
target := ctx.vertex[dfnum_semi_w]
if target < 0 {
continue
}
if ctx.idom[w] != target {
idom_w := ctx.idom[w]
if idom_w >= 0 && idom_w < max_id {
next_idom := ctx.idom[idom_w]
if next_idom >= 0 {
ctx.idom[w] = next_idom
}
}
}
}
ctx.idom[entry] = entry
for fbi4 in 0 .. n_func_blocks {
blk_id := func.blocks[fbi4]
idom := ctx.idom[blk_id]
if idom != -1 && idom != blk_id {
if idom >= 0 && idom < max_id {
mut t := ctx.dom_tree[idom]
t << blk_id
ctx.dom_tree[idom] = t
}
}
}
}
// Write idom / dom_tree back into the blocks for verifier dominance checks.
for bi in 0 .. max_id {
mut blk := m.blocks[bi]
blk.idom = ctx.idom[bi]
blk.dom_tree = []ssa.BlockID{cap: ctx.dom_tree[bi].len}
for c in ctx.dom_tree[bi] {
blk.dom_tree << ssa.BlockID(c)
}
m.blocks[bi] = blk
}
return DomInfo{
idom: ctx.idom
dom_tree: ctx.dom_tree
}
}
fn lt_dfs(root int, cfg &CfgData, mut ctx LTContext) {
if root < 0 || root >= ctx.dfnum.len {
return
}
n_total_blocks := cfg.succs.len
mut stack := []DfsFrame{}
ctx.n++
if ctx.n >= ctx.vertex.len {
return
}
ctx.dfnum[root] = ctx.n
ctx.vertex[ctx.n] = root
stack << DfsFrame{
node: root
}
for stack.len > 0 {
top := stack.len - 1
node := stack[top].node
if node < 0 || node >= n_total_blocks {
stack.pop()
continue
}
n_succs := cfg.succs[node].len
si := stack[top].succ_idx
if si < n_succs {
mut frame := stack[top]
frame.succ_idx++
stack[top] = frame
w := cfg.succs[node][si]
if w >= 0 && w < ctx.dfnum.len && ctx.dfnum[w] == 0 {
ctx.parent[w] = node
ctx.n++
if ctx.n >= ctx.vertex.len {
return
}
ctx.dfnum[w] = ctx.n
ctx.vertex[ctx.n] = w
stack << DfsFrame{
node: w
}
}
} else {
stack.pop()
}
}
}
fn (mut ctx LTContext) compress(v int) {
if v < 0 || v >= ctx.ancestor.len {
return
}
mut chain := []int{}
mut cur := v
for cur >= 0 && cur < ctx.ancestor.len {
av := ctx.ancestor[cur]
if av < 0 || av >= ctx.ancestor.len {
break
}
if ctx.ancestor[av] == -1 {
break
}
chain << cur
cur = av
}
for ci := chain.len - 1; ci >= 0; ci-- {
node := chain[ci]
if node < 0 || node >= ctx.ancestor.len {
continue
}
anc := ctx.ancestor[node]
if anc < 0 || anc >= ctx.label.len || node >= ctx.label.len {
continue
}
label_anc := ctx.label[anc]
label_node := ctx.label[node]
if label_anc < 0 || label_anc >= ctx.semi.len || label_node < 0
|| label_node >= ctx.semi.len {
continue
}
semi_label_anc := ctx.semi[label_anc]
semi_label_node := ctx.semi[label_node]
if semi_label_anc < 0 || semi_label_anc >= ctx.dfnum.len || semi_label_node < 0
|| semi_label_node >= ctx.dfnum.len {
continue
}
if ctx.dfnum[semi_label_anc] < ctx.dfnum[semi_label_node] {
ctx.label[node] = ctx.label[anc]
}
aav := ctx.ancestor[anc]
if aav >= 0 {
ctx.ancestor[node] = aav
}
}
}
fn (mut ctx LTContext) eval(v int) int {
if v < 0 || v >= ctx.ancestor.len {
if v >= 0 && v < ctx.label.len {
return ctx.label[v]
}
return 0
}
if ctx.ancestor[v] == -1 {
return ctx.label[v]
}
ctx.compress(v)
av := ctx.ancestor[v]
if av < 0 || av >= ctx.label.len {
if v < ctx.label.len {
return ctx.label[v]
}
return 0
}
if v >= ctx.label.len {
return 0
}
label_av := ctx.label[av]
label_v := ctx.label[v]
if label_av < 0 || label_av >= ctx.semi.len || label_v < 0 || label_v >= ctx.semi.len {
return label_v
}
semi_lav := ctx.semi[label_av]
semi_lv := ctx.semi[label_v]
if semi_lav < 0 || semi_lav >= ctx.dfnum.len || semi_lv < 0 || semi_lv >= ctx.dfnum.len {
return label_v
}
if ctx.dfnum[semi_lav] >= ctx.dfnum[semi_lv] {
return label_v
}
return label_av
}
fn (mut ctx LTContext) link(v int, w int) {
ctx.ancestor[w] = v
}
+308
View File
@@ -0,0 +1,308 @@
module optimize
import v3.ssa
fn constant_fold(mut m ssa.Module) bool {
mut changed := false
for fi in 0 .. m.funcs.len {
for blk_id in m.funcs[fi].blocks {
for val_id in m.blocks[blk_id].instrs {
if m.values[val_id].kind != .instruction {
continue
}
instr := m.instrs[m.values[val_id].index]
if instr.operands.len != 2 {
continue
}
lhs := m.values[instr.operands[0]]
rhs := m.values[instr.operands[1]]
// Skip undef values - can't fold/simplify with undefined.
if lhs.kind == .constant && lhs.name == 'undef' {
continue
}
if rhs.kind == .constant && rhs.name == 'undef' {
continue
}
// Algebraic simplifications first (work even with non-constant operands):
// x+0, x*1, x*0, x-x, x^x, x&x, x|x, x<<0, x*2 -> x<<1, etc.
repl, needs_zero := try_algebraic_simplify(m, val_id, instr, lhs, rhs)
if repl >= 0 || repl == -2 {
if repl == -2 {
// x * 2 -> x << 1
other_id := if lhs.kind == .constant {
instr.operands[1]
} else {
instr.operands[0]
}
typ := m.values[val_id].typ
one_val := m.get_or_add_const(typ, '1')
mut shl_instr := m.instrs[m.values[val_id].index]
shl_instr.op = .shl
shl_instr.operands = [other_id, one_val]
m.instrs[m.values[val_id].index] = shl_instr
changed = true
continue
} else if needs_zero {
// x * 0, x & 0, x - x, x ^ x -> 0
typ := m.values[val_id].typ
zero_val := m.get_or_add_const(typ, '0')
m.replace_uses(val_id, zero_val)
} else {
m.replace_uses(val_id, repl)
}
changed = true
continue
}
if lhs.kind != .constant || rhs.kind != .constant {
continue
}
l_int := lhs.name.i64()
r_int := rhs.name.i64()
mut result := i64(0)
mut folded := false
match instr.op {
.add {
result = l_int + r_int
folded = true
}
.sub {
result = l_int - r_int
folded = true
}
.mul {
result = l_int * r_int
folded = true
}
.sdiv {
if r_int != 0 {
result = l_int / r_int
folded = true
}
}
.srem {
if r_int != 0 {
result = l_int % r_int
folded = true
}
}
.udiv {
if r_int != 0 {
result = i64(u64(l_int) / u64(r_int))
folded = true
}
}
.urem {
if r_int != 0 {
result = i64(u64(l_int) % u64(r_int))
folded = true
}
}
.and_ {
result = l_int & r_int
folded = true
}
.or_ {
result = l_int | r_int
folded = true
}
.xor {
result = l_int ^ r_int
folded = true
}
.shl {
if r_int >= 0 && r_int < 64 {
result = i64(u64(l_int) << u64(r_int))
folded = true
}
}
.ashr {
if r_int >= 0 && r_int < 64 {
result = l_int >> u64(r_int)
folded = true
}
}
.lshr {
if r_int >= 0 && r_int < 64 {
result = i64(u64(l_int) >> u64(r_int))
folded = true
}
}
.eq {
result = if l_int == r_int { 1 } else { 0 }
folded = true
}
.ne {
result = if l_int != r_int { 1 } else { 0 }
folded = true
}
.lt {
result = if l_int < r_int { 1 } else { 0 }
folded = true
}
.gt {
result = if l_int > r_int { 1 } else { 0 }
folded = true
}
.le {
result = if l_int <= r_int { 1 } else { 0 }
folded = true
}
.ge {
result = if l_int >= r_int { 1 } else { 0 }
folded = true
}
.ult {
result = if u64(l_int) < u64(r_int) { 1 } else { 0 }
folded = true
}
.ugt {
result = if u64(l_int) > u64(r_int) { 1 } else { 0 }
folded = true
}
.ule {
result = if u64(l_int) <= u64(r_int) { 1 } else { 0 }
folded = true
}
.uge {
result = if u64(l_int) >= u64(r_int) { 1 } else { 0 }
folded = true
}
else {}
}
if folded {
typ := m.values[val_id].typ
const_val := m.get_or_add_const(typ, '${result}')
m.replace_uses(val_id, const_val)
changed = true
}
}
}
}
return changed
}
// try_algebraic_simplify implements identity/strength-reduction simplifications:
// x+0=x, x*1=x, x*0=0, x-x=0, x^x=0, x&x=x, x|x=x, x<<0=x, x*2=x<<1, x/1=x.
// Returns (replacement_id, needs_zero):
// repl >= 0 -> replace all uses of val_id with `repl`
// repl == -2 -> rewrite as `x << 1` (caller builds the shift)
// needs_zero == true -> replace all uses with a fresh zero constant
// repl == -1 -> no simplification
fn try_algebraic_simplify(_m &ssa.Module, val_id int, instr ssa.Instruction, lhs ssa.Value, rhs ssa.Value) (int, bool) {
lhs_id := instr.operands[0]
rhs_id := instr.operands[1]
// Same-operand simplifications (x op x). Integer ops only.
if lhs_id == rhs_id {
match instr.op {
.sub, .xor {
return val_id, true // x - x = 0, x ^ x = 0
}
.and_, .or_ {
return lhs_id, false // x & x = x, x | x = x
}
else {}
}
}
mut const_val := i64(0)
mut const_is_rhs := false
mut other_id := 0
if lhs.kind == .constant && lhs.name != 'undef' {
const_val = lhs.name.i64()
const_is_rhs = false
other_id = instr.operands[1]
} else if rhs.kind == .constant && rhs.name != 'undef' {
const_val = rhs.name.i64()
const_is_rhs = true
other_id = instr.operands[0]
} else {
return -1, false
}
match instr.op {
.add {
if const_val == 0 {
return other_id, false // x + 0 = x
}
}
.sub {
if const_is_rhs && const_val == 0 {
return other_id, false // x - 0 = x
}
}
.mul {
if const_val == 0 {
return val_id, true // x * 0 = 0
}
if const_val == 1 {
return other_id, false // x * 1 = x
}
if const_val == 2 {
return -2, false // x * 2 = x << 1
}
}
.sdiv, .udiv {
if const_is_rhs && const_val == 1 {
return other_id, false // x / 1 = x
}
}
.and_ {
if const_val == 0 {
return val_id, true // x & 0 = 0
}
}
.or_ {
if const_val == 0 {
return other_id, false // x | 0 = x
}
}
.xor {
if const_val == 0 {
return other_id, false // x ^ 0 = x
}
}
.shl, .ashr, .lshr {
if const_is_rhs && const_val == 0 {
return other_id, false // x << 0 = x, x >> 0 = x
}
}
else {}
}
return -1, false
}
fn branch_fold(mut m ssa.Module) bool {
mut changed := false
for fi in 0 .. m.funcs.len {
for blk_id in m.funcs[fi].blocks {
if m.blocks[blk_id].instrs.len == 0 {
continue
}
term_val_id := m.blocks[blk_id].instrs.last()
term := m.instrs[m.values[term_val_id].index]
if term.op == .br && term.operands.len >= 3 {
cond_val := m.values[term.operands[0]]
if cond_val.kind == .constant {
cond_int := cond_val.name.i64()
target := if cond_int != 0 { term.operands[1] } else { term.operands[2] }
mut jmp_instr := m.instrs[m.values[term_val_id].index]
jmp_instr.op = .jmp
mut new_ops := []ssa.ValueID{}
new_ops << target
jmp_instr.operands = new_ops
m.instrs[m.values[term_val_id].index] = jmp_instr
changed = true
}
}
}
}
return changed
}
+374
View File
@@ -0,0 +1,374 @@
module optimize
import v3.ssa
// --- Mem2Reg (promote allocas to SSA values + phi nodes) ---
// Flat arrays indexed by value_id / block_id. Phi predecessor operands store
// raw block ids, matching the rest of the v3 SSA (and the arm64 backend's
// emit_phi_edge_copies, which reads operand[odd] as a block id).
struct Mem2RegCtx {
mut:
defs [][]int // alloc_id -> blocks storing to it
uses [][]int // alloc_id -> blocks loading from it
phi_placements [][]int // block_id -> alloc_ids needing a phi here
phi_vals [][]int // block_id -> parallel phi value ids
stacks [][]int // alloc_id -> reaching-definition stack
is_promotable []bool
phi_blocks []int
}
fn array2d_append(mut arr [][]int, idx int, val int) {
mut inner := arr[idx]
inner << val
arr[idx] = inner
}
// promote_memory_to_register promotes scalar allocas to SSA registers, inserting
// phi nodes at dominance frontiers and renaming load/store chains.
fn promote_memory_to_register(mut m ssa.Module, dom DomInfo, cfg &CfgData) {
n_values := m.values.len
n_blocks := m.blocks.len
mut ctx := Mem2RegCtx{
defs: [][]int{len: n_values}
uses: [][]int{len: n_values}
phi_placements: [][]int{len: n_blocks}
phi_vals: [][]int{len: n_blocks}
stacks: [][]int{len: n_values}
is_promotable: []bool{len: n_values}
phi_blocks: []int{}
}
mut df := [][]int{len: n_blocks}
mut df_blocks := []int{}
mut visited := []bool{len: n_blocks}
mut has_phi := []bool{len: n_blocks}
for fi in 0 .. m.funcs.len {
mut promotable := []int{}
func := m.funcs[fi]
for blk_id in func.blocks {
blk := m.blocks[blk_id]
for val_id in blk.instrs {
val := m.values[val_id]
if val.kind != .instruction {
continue
}
instr := m.instrs[val.index]
if instr.op == .alloca {
if is_promotable(m, val_id) {
promotable << val_id
ctx.is_promotable[val_id] = true
}
}
if instr.op == .store {
ptr := instr.operands[1]
if ptr < n_values && blk_id !in ctx.defs[ptr] {
array2d_append(mut ctx.defs, ptr, blk_id)
}
} else if instr.op == .load {
ptr := instr.operands[0]
if ptr < n_values && blk_id !in ctx.uses[ptr] {
array2d_append(mut ctx.uses, ptr, blk_id)
}
}
}
}
compute_dominance_frontier_flat(m, fi, &dom, cfg, mut df, mut df_blocks)
// Insert phis at iterated dominance frontiers of each promotable alloca.
for alloc_id in promotable {
mut worklist := ctx.defs[alloc_id].clone()
mut visited_list := []int{}
mut has_phi_list := []int{}
for worklist.len > 0 {
b := worklist.pop()
if b < 0 || b >= n_blocks {
continue
}
for d in df[b] {
if !has_phi[d] {
array2d_append(mut ctx.phi_placements, d, alloc_id)
if d !in ctx.phi_blocks {
ctx.phi_blocks << d
}
has_phi[d] = true
has_phi_list << d
if !visited[d] {
visited[d] = true
visited_list << d
worklist << d
}
}
}
}
for d in visited_list {
visited[d] = false
}
for d in has_phi_list {
has_phi[d] = false
}
}
// Materialize the phi instructions (empty operands, filled during rename).
for blk_id in ctx.phi_blocks {
for alloc_id in ctx.phi_placements[blk_id] {
alloc_val := m.values[alloc_id]
alloc_typ := m.type_store.types[alloc_val.typ]
typ := alloc_typ.elem_type
phi_val := m.add_instr_front(.phi, blk_id, typ, [])
mut pv := m.values[phi_val]
pv.name = '${alloc_val.name}.phi_${blk_id}'
m.values[phi_val] = pv
array2d_append(mut ctx.phi_vals, blk_id, phi_val)
}
}
// Rename: walk the dominator tree, threading reaching definitions.
if func.blocks.len > 0 {
rename_blocks(mut m, func.blocks[0], mut ctx, dom, cfg)
}
// Reset per-function state.
for alloc_id in promotable {
ctx.defs[alloc_id] = []
ctx.uses[alloc_id] = []
ctx.stacks[alloc_id] = []
ctx.is_promotable[alloc_id] = false
}
for pb in ctx.phi_blocks {
ctx.phi_placements[pb] = []
ctx.phi_vals[pb] = []
}
ctx.phi_blocks = []
for d in df_blocks {
df[d] = []
}
df_blocks = []
}
}
// is_promotable returns true if every use of the alloca is a direct load/store,
// i.e. the pointer never escapes. Array- and pointer-backed slots stay in memory.
fn is_promotable(m &ssa.Module, alloc_id int) bool {
if alloc_id > 0 && alloc_id < m.values.len {
alloc_typ_id := m.values[alloc_id].typ
if alloc_typ_id > 0 && alloc_typ_id < m.type_store.types.len {
alloc_typ := m.type_store.types[alloc_typ_id]
if alloc_typ.kind == .ptr_t {
elem_typ_id := alloc_typ.elem_type
if elem_typ_id > 0 && elem_typ_id < m.type_store.types.len {
elem_typ := m.type_store.types[elem_typ_id]
// Keep pointer-, array- and aggregate-backed slots in memory.
// Promoting them to SSA values would require the backend to
// thread by-value aggregates through phi copies, which the v3
// arm64 lowering does not yet handle reliably.
if elem_typ.kind in [.ptr_t, .array_t, .struct_t] {
return false
}
}
}
}
}
for u in m.values[alloc_id].uses {
if u >= m.values.len {
continue
}
user := m.values[u]
if user.kind != .instruction {
return false
}
instr := m.instrs[user.index]
match instr.op {
.load {
if instr.operands.len == 0 || instr.operands[0] != alloc_id {
return false
}
}
.store {
if instr.operands.len < 2 || instr.operands[1] != alloc_id {
return false
}
}
else {
return false
}
}
}
return true
}
fn compute_dominance_frontier_flat(m &ssa.Module, func_idx int, dom &DomInfo, cfg &CfgData, mut df [][]int, mut df_blocks []int) {
func := m.funcs[func_idx]
for blk_id in func.blocks {
if blk_id < 0 || blk_id >= m.blocks.len {
continue
}
num_preds := cfg.preds[blk_id].len
if num_preds >= 2 {
for pi in 0 .. num_preds {
mut runner := cfg.preds[blk_id][pi]
idom := dom.idom[blk_id]
for runner != -1 && runner != idom {
if runner < 0 || runner >= m.blocks.len {
break
}
if blk_id !in df[runner] {
array2d_append(mut df, runner, blk_id)
if runner !in df_blocks {
df_blocks << runner
}
}
if runner == dom.idom[runner] {
break
}
runner = dom.idom[runner]
}
}
}
}
}
struct RenameFrame {
mut:
blk_id int
child_idx int
pushed_allocs []int
processed bool
}
fn rename_blocks(mut m ssa.Module, root_blk int, mut ctx Mem2RegCtx, dom DomInfo, cfg &CfgData) {
mut work := []RenameFrame{}
work << RenameFrame{
blk_id: root_blk
}
mut visited := []bool{len: m.blocks.len}
for work.len > 0 {
fi := work.len - 1
blk_id := work[fi].blk_id
if !work[fi].processed {
if blk_id >= 0 && blk_id < visited.len && visited[blk_id] {
work.pop()
continue
}
if blk_id >= 0 && blk_id < visited.len {
visited[blk_id] = true
}
mut frame2 := work[fi]
frame2.processed = true
work[fi] = frame2
// 1. Push phis defined in this block.
if blk_id < ctx.phi_placements.len && ctx.phi_placements[blk_id].len > 0 {
n_phi := ctx.phi_placements[blk_id].len
for pai in 0 .. n_phi {
alloc_id := ctx.phi_placements[blk_id][pai]
if pai < ctx.phi_vals[blk_id].len {
phi_val_id := ctx.phi_vals[blk_id][pai]
mut new_stack := ctx.stacks[alloc_id].clone()
new_stack << phi_val_id
ctx.stacks[alloc_id] = new_stack
mut wf := work[fi]
wf.pushed_allocs << alloc_id
work[fi] = wf
}
}
}
// 2. Process instructions: rewrite loads, record store values, nop them.
mut instrs_to_nop := []int{}
blk2 := m.blocks[blk_id]
for val_id in blk2.instrs {
val := m.values[val_id]
if val.kind != .instruction {
continue
}
instr := m.instrs[val.index]
if instr.op == .store {
ptr := instr.operands[1]
if ptr < ctx.is_promotable.len && ctx.is_promotable[ptr] {
mut new_stack := ctx.stacks[ptr].clone()
new_stack << instr.operands[0]
ctx.stacks[ptr] = new_stack
mut wf := work[fi]
wf.pushed_allocs << ptr
work[fi] = wf
instrs_to_nop << val_id
}
} else if instr.op == .load {
ptr := instr.operands[0]
if ptr < ctx.is_promotable.len && ctx.is_promotable[ptr] {
stack := ctx.stacks[ptr]
mut repl := 0
if stack.len > 0 {
repl = stack.last()
} else {
repl = m.get_or_add_const(val.typ, 'undef')
}
m.replace_uses(val_id, repl)
instrs_to_nop << val_id
}
} else if instr.op == .alloca {
if val_id < ctx.is_promotable.len && ctx.is_promotable[val_id] {
instrs_to_nop << val_id
}
}
}
for vid in instrs_to_nop {
vid_val := m.values[vid]
mut nop_instr := m.instrs[vid_val.index]
nop_instr.op = .bitcast
nop_instr.operands = []
m.instrs[vid_val.index] = nop_instr
}
// 3. Fill phi operands in CFG successors (raw block id as predecessor).
for succ_id in cfg.succs[blk_id] {
if succ_id < ctx.phi_placements.len && ctx.phi_placements[succ_id].len > 0 {
n_succ_phi := ctx.phi_placements[succ_id].len
for spai in 0 .. n_succ_phi {
alloc_id := ctx.phi_placements[succ_id][spai]
if spai < ctx.phi_vals[succ_id].len {
vid := ctx.phi_vals[succ_id][spai]
phi_v := m.values[vid]
phi_val := if ctx.stacks[alloc_id].len > 0 {
ctx.stacks[alloc_id].last()
} else {
alloc_v := m.values[alloc_id]
alloc_typ := m.type_store.types[alloc_v.typ]
m.get_or_add_const(alloc_typ.elem_type, 'undef')
}
m.append_phi_operands(phi_v.index, phi_val, blk_id)
}
}
}
}
}
// 4. Recurse into dominator-tree children.
n_children := dom.dom_tree[blk_id].len
child_idx := work[fi].child_idx
if child_idx < n_children {
child := dom.dom_tree[blk_id][child_idx]
mut frame := work[fi]
frame.child_idx++
work[fi] = frame
work << RenameFrame{
blk_id: child
}
} else {
// 5. Pop reaching definitions pushed in this block.
pushed := work[fi].pushed_allocs.clone()
work.pop()
for i := pushed.len - 1; i >= 0; i-- {
mut s := ctx.stacks[pushed[i]].clone()
s.pop()
ctx.stacks[pushed[i]] = s
}
}
}
}
+145
View File
@@ -0,0 +1,145 @@
module optimize
import os
import v3.ssa
pub struct OptimizeOptions {
pub:
mem2reg bool // promote scalar allocas to SSA values + phi nodes
eliminate_phis bool // lower phis to `assign` copies (for backends without phi)
verify_each_pass bool // run the structured verifier after every structural pass
strict_verify bool // treat historically-noncritical verifier findings as fatal
}
// optimize runs the default, backend-safe optimization pipeline. Structural SSA
// construction (mem2reg / phi elimination) is opt-in via the environment so the
// proven arm64 lowering path is unchanged unless explicitly requested:
// V3_MEM2REG=1 enable alloca promotion + phi insertion
// V3_PHI_ELIM=1 additionally lower phis to assign copies
// V3_VERIFY=1 structured verify after each pass (V3_VERIFY_STRICT=1 = fatal)
pub fn optimize(mut m ssa.Module) {
mem2reg := os.getenv('V3_MEM2REG') != ''
optimize_with_options(mut m, OptimizeOptions{
mem2reg: mem2reg
// The arm64 backend's native phi resolution is incomplete (no copies on
// conditional edges, no parallel-copy sequencing), so phis introduced by
// mem2reg must be lowered to assign copies for correct codegen.
eliminate_phis: mem2reg || os.getenv('V3_PHI_ELIM') != ''
verify_each_pass: os.getenv('V3_VERIFY') != ''
strict_verify: os.getenv('V3_VERIFY_STRICT') != ''
})
}
pub fn optimize_with_options(mut m ssa.Module, opts OptimizeOptions) {
rebuild_use_lists(mut m)
build_cfg(mut m)
verify_ssa(m, 'initial normalization')
verify_pipeline_checkpoint(m, opts, 'input')
constant_fold(mut m)
rebuild_use_lists(mut m)
branch_fold(mut m)
rebuild_use_lists(mut m)
build_cfg(mut m)
// Branch folding can drop a phi block's predecessor edge; keep phis consistent.
prune_phi_operands(mut m)
rebuild_use_lists(mut m)
if opts.mem2reg {
// Normalize the CFG *before* SSA construction so that every phi
// predecessor (a reachable CFG predecessor) stays present in the
// function afterwards. Block-removing passes must not run once phis
// exist, or their predecessor operands would dangle.
dead_code_elimination(mut m)
rebuild_use_lists(mut m)
build_cfg(mut m)
remove_unreachable_blocks(mut m)
merge_blocks(mut m)
rebuild_use_lists(mut m)
build_cfg(mut m)
// Structural SSA construction: dominators -> mem2reg -> simplify phis.
cfg := cfg_data_from_module(m)
dom := compute_dominators(mut m, &cfg)
verify_pipeline_checkpoint(m, opts, 'compute_dominators')
promote_memory_to_register(mut m, dom, &cfg)
rebuild_use_lists(mut m)
verify_pipeline_checkpoint(m, opts, 'mem2reg')
simplify_phi_nodes(mut m)
rebuild_use_lists(mut m)
build_cfg(mut m)
verify_pipeline_checkpoint(m, opts, 'simplify_phi')
}
// Phi elimination lowers phis to assign copies for backends that cannot
// resolve phis natively. Runs whenever requested (input phis from the builder
// or a worker merge may exist even without mem2reg).
if opts.eliminate_phis {
eliminate_phi_nodes(mut m)
rebuild_use_lists(mut m)
build_cfg(mut m)
verify_pipeline_checkpoint(m, opts, 'eliminate_phi')
}
dead_code_elimination(mut m)
rebuild_use_lists(mut m)
build_cfg(mut m)
remove_unreachable_blocks(mut m)
if !opts.mem2reg {
// Without SSA construction, block-merging is phi-aware and safe to run.
merge_blocks(mut m)
}
rebuild_use_lists(mut m)
build_cfg(mut m)
// Final phi-consistency pass: any phi still present must match the final CFG.
prune_phi_operands(mut m)
rebuild_use_lists(mut m)
build_cfg(mut m)
verify_ssa(m, 'optimization')
verify_pipeline_checkpoint(m, opts, 'final')
}
fn verify_pipeline_checkpoint(m &ssa.Module, opts OptimizeOptions, pass_name string) {
if opts.verify_each_pass || opts.strict_verify {
verify_and_panic_with_options(m, pass_name, VerifyPanicOptions{
allow_noncritical: !opts.strict_verify
})
}
}
fn rebuild_use_lists(mut m ssa.Module) {
for vi in 0 .. m.values.len {
mut val := m.values[vi]
val.uses = []
m.values[vi] = val
}
for fi in 0 .. m.funcs.len {
for blk_id in m.funcs[fi].blocks {
if blk_id < 0 || blk_id >= m.blocks.len {
continue
}
for val_id in m.blocks[blk_id].instrs {
if val_id <= 0 || val_id >= m.values.len || m.values[val_id].kind != .instruction {
continue
}
instr_idx := m.values[val_id].index
if instr_idx < 0 || instr_idx >= m.instrs.len {
continue
}
instr := m.instrs[instr_idx]
for op_id in instr.value_operands() {
if op_id >= 0 && op_id < m.values.len && val_id !in m.values[op_id].uses {
mut op_val := m.values[op_id]
op_val.uses << val_id
m.values[op_id] = op_val
}
}
}
}
}
}
+445
View File
@@ -0,0 +1,445 @@
module optimize
import v3.ssa
// --- Simplify trivial phi nodes ---
// A phi is trivial if all its non-self operands resolve to a single value, or if
// it has no uses at all. Trivial phis are replaced and marked dead (nop bitcast).
fn simplify_phi_nodes(mut m ssa.Module) bool {
mut any_changed := false
mut changed := true
for changed {
changed = false
for fi in 0 .. m.funcs.len {
func := m.funcs[fi]
for blk_id in func.blocks {
blk := m.blocks[blk_id]
for val_id in blk.instrs {
val := m.values[val_id]
if val.kind != .instruction {
continue
}
instr := m.instrs[val.index]
if instr.op != .phi {
continue
}
// Dead phi: no uses -> remove.
if val.uses.len == 0 {
for i := 0; i < instr.operands.len; i += 2 {
op_val := instr.operands[i]
if op_val != val_id {
remove_use(mut m, op_val, val_id)
}
}
mut dead_phi := m.instrs[val.index]
dead_phi.op = .bitcast
dead_phi.operands = []
m.instrs[val.index] = dead_phi
changed = true
any_changed = true
continue
}
// Trivial phi: all non-self value operands identical.
mut unique_val := -1
mut is_trivial := true
for i := 0; i < instr.operands.len; i += 2 {
op_val := instr.operands[i]
if op_val == val_id {
continue
}
if unique_val == -1 {
unique_val = op_val
} else if unique_val != op_val {
is_trivial = false
break
}
}
if is_trivial && unique_val != -1 {
m.replace_uses(val_id, unique_val)
mut triv_phi := m.instrs[val.index]
triv_phi.op = .bitcast
triv_phi.operands = []
m.instrs[val.index] = triv_phi
changed = true
any_changed = true
}
}
}
}
}
return any_changed
}
// prune_phi_operands drops phi operand pairs whose predecessor block is no longer
// an actual predecessor (e.g. after branch folding / unreachable-block removal),
// keeping phis consistent with the current CFG. A phi reduced to a single distinct
// incoming value is replaced by that value. Requires up-to-date preds + uses.
fn prune_phi_operands(mut m ssa.Module) {
for fi in 0 .. m.funcs.len {
for blk_id in m.funcs[fi].blocks {
if blk_id < 0 || blk_id >= m.blocks.len {
continue
}
preds := m.blocks[blk_id].preds
for val_id in m.blocks[blk_id].instrs {
if val_id <= 0 || val_id >= m.values.len || m.values[val_id].kind != .instruction {
continue
}
idx := m.values[val_id].index
if m.instrs[idx].op != .phi {
continue
}
ops := m.instrs[idx].operands
mut kept := []ssa.ValueID{}
mut distinct := []ssa.ValueID{}
for i := 0; i + 1 < ops.len; i += 2 {
pred := int(ops[i + 1])
if pred in preds {
kept << ops[i]
kept << ops[i + 1]
if ops[i] !in distinct {
distinct << ops[i]
}
}
}
if kept.len == ops.len && distinct.len > 1 {
continue // unchanged, still a real phi
}
if distinct.len == 1 {
// Trivial phi -> its single incoming value.
m.replace_uses(val_id, distinct[0])
mut nop := m.instrs[idx]
nop.op = .bitcast
nop.operands = []
m.instrs[idx] = nop
} else if distinct.len == 0 {
mut nop := m.instrs[idx]
nop.op = .bitcast
nop.operands = []
m.instrs[idx] = nop
} else {
mut pruned := m.instrs[idx]
pruned.operands = kept
m.instrs[idx] = pruned
}
}
}
}
}
// --- Critical edge splitting ---
// Inserts an empty block on every edge whose source has multiple successors and
// whose target has multiple predecessors, so phi-elimination copies have a unique
// home. Operands here are raw block ids (v3 convention).
fn split_critical_edges(mut m ssa.Module) {
build_cfg(mut m)
for fi in 0 .. m.funcs.len {
mut edges_to_split := [][]int{}
func := m.funcs[fi]
for blk_id in func.blocks {
if blk_id < 0 || blk_id >= m.blocks.len {
continue
}
if m.blocks[blk_id].succs.len > 1 {
for succ_id in m.blocks[blk_id].succs {
if succ_id < 0 || succ_id >= m.blocks.len {
continue
}
if m.blocks[succ_id].preds.len > 1 {
edges_to_split << [int(blk_id), int(succ_id)]
}
}
}
}
for edge in edges_to_split {
pred_id := edge[0]
succ_id := edge[1]
if pred_id < 0 || pred_id >= m.blocks.len || succ_id < 0 || succ_id >= m.blocks.len {
continue
}
split_blk := m.add_block(fi, 'split_${pred_id}_${succ_id}')
m.add_instr(.jmp, split_blk, 0, [ssa.ValueID(succ_id)])
// Re-point predecessor terminator from succ to the split block.
pred_blk := m.blocks[pred_id]
if pred_blk.instrs.len > 0 {
term_val_id := pred_blk.instrs[pred_blk.instrs.len - 1]
if term_val_id >= 0 && term_val_id < m.values.len {
idx := m.values[term_val_id].index
mut term := m.instrs[idx]
for i in 0 .. term.operands.len {
if int(term.operands[i]) == succ_id {
term.operands[i] = ssa.ValueID(split_blk)
}
}
m.instrs[idx] = term
}
}
// Re-point phi predecessors in succ from pred to the split block.
for vid in m.blocks[succ_id].instrs {
if vid < 0 || vid >= m.values.len || m.values[vid].kind != .instruction {
continue
}
pidx := m.values[vid].index
if m.instrs[pidx].op != .phi {
continue
}
mut phi := m.instrs[pidx]
for i := 1; i < phi.operands.len; i += 2 {
if int(phi.operands[i]) == pred_id {
phi.operands[i] = ssa.ValueID(split_blk)
}
}
m.instrs[pidx] = phi
}
}
}
build_cfg(mut m)
}
// eliminate_phi_nodes lowers phis to `assign` copies placed in predecessor blocks,
// sequencing the per-predecessor parallel copies (Briggs) to respect dependencies.
fn eliminate_phi_nodes(mut m ssa.Module) {
split_critical_edges(mut m)
n_blocks := m.blocks.len
mut pred_copy_dests := [][]int{len: n_blocks}
mut pred_copy_srcs := [][]int{len: n_blocks}
for fi in 0 .. m.funcs.len {
func := m.funcs[fi]
mut pred_copy_blocks := []int{}
for blk_id in func.blocks {
if blk_id < 0 || blk_id >= m.blocks.len {
continue
}
for val_id in m.blocks[blk_id].instrs {
if val_id < 0 || val_id >= m.values.len {
continue
}
val := m.values[val_id]
if val.kind != .instruction {
continue
}
instr := m.instrs[val.index]
if instr.op != .phi {
continue
}
for i := 0; i + 1 < instr.operands.len; i += 2 {
val_in := instr.operands[i]
pred_blk_idx := int(instr.operands[i + 1])
if pred_blk_idx < 0 || pred_blk_idx >= n_blocks {
continue
}
if pred_copy_dests[pred_blk_idx].len == 0 {
pred_copy_blocks << pred_blk_idx
}
array2d_append(mut pred_copy_dests, pred_blk_idx, val_id)
array2d_append(mut pred_copy_srcs, pred_blk_idx, val_in)
}
}
}
for pred_blk in pred_copy_blocks {
resolve_parallel_copies(mut m, pred_blk, pred_copy_dests[pred_blk],
pred_copy_srcs[pred_blk])
}
for pred_blk in pred_copy_blocks {
pred_copy_dests[pred_blk] = []
pred_copy_srcs[pred_blk] = []
}
// Remove phi instructions (nop them).
for blk_id in func.blocks {
if blk_id < 0 || blk_id >= m.blocks.len {
continue
}
for val_id in m.blocks[blk_id].instrs {
if val_id < 0 || val_id >= m.values.len {
continue
}
val := m.values[val_id]
if val.kind != .instruction {
continue
}
idx := val.index
if m.instrs[idx].op == .phi {
mut cleanup := m.instrs[idx]
cleanup.op = .bitcast
cleanup.operands = []
m.instrs[idx] = cleanup
}
}
}
}
}
// resolve_parallel_copies sequences a set of parallel copies dest[i] = src[i]
// using Briggs' worklist algorithm, breaking cycles with a temp.
fn resolve_parallel_copies(mut m ssa.Module, blk_id int, dests []int, srcs []int) {
if dests.len == 0 {
return
}
mut p_dest := []int{}
mut p_src := []int{}
for ci in 0 .. dests.len {
if dests[ci] != srcs[ci] {
p_dest << dests[ci]
p_src << srcs[ci]
}
}
np := p_dest.len
if np == 0 {
return
}
mut max_id := m.values.len
for i in 0 .. np {
if p_dest[i] >= max_id {
max_id = p_dest[i] + 1
}
if p_src[i] >= max_id {
max_id = p_src[i] + 1
}
}
mut src_ref_count := []int{len: max_id + np + 4}
for i in 0 .. np {
src_ref_count[p_src[i]]++
}
mut alive := []int{len: np}
for i in 0 .. np {
alive[i] = 1
}
mut worklist := []int{}
for i in 0 .. np {
if src_ref_count[p_dest[i]] == 0 {
worklist << i
}
}
mut s_dest := []int{}
mut s_src := []int{}
mut remaining := np
for remaining > 0 {
if worklist.len > 0 {
idx := worklist.pop()
if idx < 0 || idx >= np || alive[idx] == 0 {
continue
}
alive[idx] = 0
remaining--
d := p_dest[idx]
s := p_src[idx]
s_dest << d
s_src << s
if src_ref_count[s] > 0 {
src_ref_count[s]--
}
if src_ref_count[s] == 0 {
for j in 0 .. np {
if alive[j] == 1 && p_dest[j] == s {
worklist << j
}
}
}
continue
}
// Cycle: break it by copying one source into a temp.
mut ci := -1
for k in 0 .. np {
if alive[k] == 1 {
ci = k
break
}
}
if ci < 0 {
break
}
cycle_src := p_src[ci]
mut typ := 0
if cycle_src >= 0 && cycle_src < m.values.len {
typ = m.values[cycle_src].typ
}
if typ == 0 && p_dest[ci] >= 0 && p_dest[ci] < m.values.len {
typ = m.values[p_dest[ci]].typ
}
temp := insert_temp_in_block(mut m, blk_id, cycle_src, typ)
for temp >= src_ref_count.len {
src_ref_count << 0
}
src_ref_count[temp] = 0
for i in 0 .. np {
if alive[i] == 1 && p_src[i] == cycle_src {
p_src[i] = temp
src_ref_count[temp]++
}
}
src_ref_count[cycle_src] = 0
for j in 0 .. np {
if alive[j] == 1 && p_dest[j] == cycle_src {
worklist << j
}
}
}
for si in 0 .. s_dest.len {
insert_copy_in_block(mut m, blk_id, s_dest[si], s_src[si])
}
}
fn insert_temp_in_block(mut m ssa.Module, blk_id int, src int, typ int) int {
m.instrs << ssa.Instruction{
op: .bitcast
block: blk_id
typ: typ
operands: [ssa.ValueID(src)]
}
temp_id := m.add_value(.instruction, typ, 'phi_tmp_${m.values.len}', m.instrs.len - 1)
insert_before_terminator(mut m, blk_id, temp_id)
return temp_id
}
fn insert_copy_in_block(mut m ssa.Module, blk_id int, dest int, src int) {
typ := m.values[dest].typ
m.instrs << ssa.Instruction{
op: .assign
block: blk_id
typ: typ
operands: [ssa.ValueID(dest), ssa.ValueID(src)]
}
val_id := m.add_value(.instruction, typ, 'copy', m.instrs.len - 1)
insert_before_terminator(mut m, blk_id, val_id)
}
fn insert_before_terminator(mut m ssa.Module, blk_id int, new_val int) {
mut blk := m.blocks[blk_id]
mut insert_idx := blk.instrs.len
if insert_idx > 0 {
last_val_id := blk.instrs[blk.instrs.len - 1]
last_val := m.values[last_val_id]
last_instr := m.instrs[last_val.index]
if last_instr.op in [.ret, .br, .jmp, .switch_, .unreachable] {
insert_idx = blk.instrs.len - 1
}
}
mut new_instrs := []ssa.ValueID{cap: blk.instrs.len + 1}
for ii in 0 .. insert_idx {
new_instrs << blk.instrs[ii]
}
new_instrs << ssa.ValueID(new_val)
for ii in insert_idx .. blk.instrs.len {
new_instrs << blk.instrs[ii]
}
blk.instrs = new_instrs
m.blocks[blk_id] = blk
}
+260
View File
@@ -0,0 +1,260 @@
module optimize
import v3.ssa
fn verify_ssa(m &ssa.Module, stage string) {
verify_module_indexes(m, stage)
verify_functions(m, stage)
verify_uses(m, stage)
}
fn verify_module_indexes(m &ssa.Module, stage string) {
for vi in 0 .. m.values.len {
if m.values[vi].id != vi {
verify_fail(stage, 'value ${vi} has id ${m.values[vi].id}')
}
if m.values[vi].kind == .instruction {
instr_idx := m.values[vi].index
if instr_idx < 0 || instr_idx >= m.instrs.len {
verify_fail(stage,
'instruction value ${vi} has invalid instruction index ${instr_idx}')
}
}
}
for bi in 0 .. m.blocks.len {
if m.blocks[bi].id != bi {
verify_fail(stage, 'block ${bi} has id ${m.blocks[bi].id}')
}
}
for fi in 0 .. m.funcs.len {
if m.funcs[fi].id != fi {
verify_fail(stage, 'function ${fi} has id ${m.funcs[fi].id}')
}
}
}
fn verify_functions(m &ssa.Module, stage string) {
for fi in 0 .. m.funcs.len {
func := m.funcs[fi]
mut func_blocks := map[int]bool{}
for blk_id in func.blocks {
if !verify_block_id(m, stage, blk_id, fi, 'function block list') {
continue
}
if func_blocks[blk_id] {
verify_fail(stage, 'function ${fi} contains duplicate block ${blk_id}')
}
func_blocks[blk_id] = true
}
for blk_id in func.blocks {
if !verify_block_id(m, stage, blk_id, fi, 'function block list') {
continue
}
blk := m.blocks[blk_id]
if blk.parent != fi {
verify_fail(stage, 'block ${blk_id} parent is ${blk.parent}, expected ${fi}')
}
verify_block_instrs(m, stage, fi, blk_id, func_blocks)
verify_cfg_edges(m, stage, fi, blk_id, func_blocks)
}
}
}
fn verify_block_instrs(m &ssa.Module, stage string, fi int, blk_id int, func_blocks map[int]bool) {
blk := m.blocks[blk_id]
if blk.instrs.len == 0 {
return
}
for ii, val_id in blk.instrs {
if val_id <= 0 || val_id >= m.values.len {
verify_fail(stage, 'block ${blk_id} contains invalid instruction value ${val_id}')
}
val := m.values[val_id]
if val.kind != .instruction {
verify_fail(stage, 'block ${blk_id} contains non-instruction value ${val_id}')
}
if val.index < 0 || val.index >= m.instrs.len {
verify_fail(stage,
'instruction value ${val_id} has invalid instruction index ${val.index}')
}
instr := m.instrs[val.index]
if instr.block != blk_id {
verify_fail(stage,
'instruction value ${val_id} is in block ${blk_id} but records block ${instr.block}')
}
verify_instruction_operands(m, stage, fi, blk_id, val_id, instr, func_blocks,
ii == blk.instrs.len - 1)
}
}
fn verify_instruction_operands(m &ssa.Module, stage string, fi int, blk_id int, val_id int, instr ssa.Instruction, func_blocks map[int]bool, is_final bool) {
for op_id in instr.value_operands() {
if op_id <= 0 || op_id >= m.values.len {
verify_fail(stage,
'instruction value ${val_id} in block ${blk_id} uses invalid value ${op_id}')
}
if m.values[op_id].kind == .unknown {
verify_fail(stage,
'instruction value ${val_id} in block ${blk_id} uses unknown value ${op_id}')
}
}
match instr.op {
.ret {
if instr.operands.len > 1 {
verify_fail(stage,
'ret value ${val_id} in block ${blk_id} has ${instr.operands.len} operands')
}
}
.jmp {
if instr.operands.len != 1 {
verify_fail(stage,
'jmp value ${val_id} in block ${blk_id} has ${instr.operands.len} operands')
} else if is_final {
verify_target_block(m, stage, fi, blk_id, int(instr.operands[0]), func_blocks,
'jmp')
}
}
.unreachable {
if instr.operands.len != 0 {
verify_fail(stage,
'unreachable value ${val_id} in block ${blk_id} has ${instr.operands.len} operands')
}
}
.br {
if instr.operands.len != 3 {
verify_fail(stage,
'br value ${val_id} in block ${blk_id} has ${instr.operands.len} operands')
} else if is_final {
verify_target_block(m, stage, fi, blk_id, int(instr.operands[1]), func_blocks,
'br true')
verify_target_block(m, stage, fi, blk_id, int(instr.operands[2]), func_blocks,
'br false')
}
}
.phi {
if instr.operands.len == 0 || instr.operands.len % 2 != 0 {
verify_fail(stage,
'phi value ${val_id} in block ${blk_id} has ${instr.operands.len} operands')
}
for oi := 1; oi < instr.operands.len; oi += 2 {
verify_target_block(m, stage, fi, blk_id, int(instr.operands[oi]), func_blocks,
'phi predecessor')
}
}
else {}
}
}
fn verify_target_block(m &ssa.Module, stage string, fi int, blk_id int, target_id int, func_blocks map[int]bool, label string) {
if !verify_block_id(m, stage, target_id, fi, '${label} target from block ${blk_id}') {
return
}
if !func_blocks[target_id] {
verify_fail(stage,
'${label} target ${target_id} from block ${blk_id} is outside function ${fi}')
}
}
fn verify_cfg_edges(m &ssa.Module, stage string, fi int, blk_id int, func_blocks map[int]bool) {
blk := m.blocks[blk_id]
mut expected_succs := []int{}
if blk.instrs.len > 0 {
term_val_id := blk.instrs.last()
term := m.instrs[m.values[term_val_id].index]
match term.op {
.jmp {
if term.operands.len == 1 {
expected_succs << int(term.operands[0])
}
}
.br {
if term.operands.len == 3 {
expected_succs << int(term.operands[1])
if term.operands[2] != term.operands[1] {
expected_succs << int(term.operands[2])
}
}
}
else {}
}
}
verify_same_blocks(stage, 'successors for block ${blk_id}', expected_succs, blk.succs)
for succ in blk.succs {
if !verify_block_id(m, stage, succ, fi, 'successor of block ${blk_id}') {
continue
}
if !func_blocks[succ] {
verify_fail(stage, 'successor ${succ} from block ${blk_id} is outside function ${fi}')
}
if !arr_contains(m.blocks[succ].preds, blk_id) {
verify_fail(stage, 'successor ${succ} is missing predecessor ${blk_id}')
}
}
for pred in blk.preds {
if !verify_block_id(m, stage, pred, fi, 'predecessor of block ${blk_id}') {
continue
}
if !func_blocks[pred] {
verify_fail(stage, 'predecessor ${pred} of block ${blk_id} is outside function ${fi}')
}
if !arr_contains(m.blocks[pred].succs, blk_id) {
verify_fail(stage, 'predecessor ${pred} is missing successor ${blk_id}')
}
}
}
fn verify_uses(m &ssa.Module, stage string) {
mut expected_uses := map[int][]int{}
for func in m.funcs {
for blk_id in func.blocks {
if blk_id < 0 || blk_id >= m.blocks.len {
continue
}
for val_id in m.blocks[blk_id].instrs {
if val_id <= 0 || val_id >= m.values.len || m.values[val_id].kind != .instruction {
continue
}
instr := m.instrs[m.values[val_id].index]
for op_id in instr.value_operands() {
if op_id > 0 && op_id < m.values.len {
if !arr_contains(expected_uses[op_id], val_id) {
expected_uses[op_id] << val_id
}
}
}
}
}
}
for vi in 0 .. m.values.len {
verify_same_blocks(stage, 'uses for value ${vi}', expected_uses[vi], m.values[vi].uses)
}
}
fn verify_block_id(m &ssa.Module, stage string, blk_id int, fi int, context string) bool {
if blk_id < 0 || blk_id >= m.blocks.len {
verify_fail(stage, '${context} references invalid block ${blk_id} in function ${fi}')
return false
}
return true
}
fn verify_same_blocks(stage string, label string, expected []int, actual []int) {
if expected.len != actual.len {
verify_fail(stage, '${label}: expected ${expected}, got ${actual}')
}
for v in expected {
if !arr_contains(actual, v) {
verify_fail(stage, '${label}: expected ${expected}, got ${actual}')
}
}
for v in actual {
if !arr_contains(expected, v) {
verify_fail(stage, '${label}: expected ${expected}, got ${actual}')
}
}
}
fn verify_fail(stage string, msg string) {
panic('SSA verifier failed after ${stage}: ${msg}')
}
+789
View File
@@ -0,0 +1,789 @@
module optimize
import v3.ssa
// VerifyError is a structured SSA verification failure. Unlike verify_ssa()
// (which panics on the first problem), the structured API collects every error so
// callers can decide which are fatal.
pub struct VerifyError {
pub:
msg string
func_id int = -1
func_name string
block_id int = -1
val_id int = -1
}
pub fn (e VerifyError) str() string {
mut s := 'SSA verify error'
if e.func_id >= 0 {
s += ' in func ${e.func_id}'
if e.func_name != '' {
s += ' (${e.func_name})'
}
}
if e.block_id >= 0 {
s += ' block ${e.block_id}'
}
if e.val_id >= 0 {
s += ' value ${e.val_id}'
}
return '${s}: ${e.msg}'
}
pub struct VerifyPanicOptions {
pub:
allow_noncritical bool
}
// verify performs comprehensive validation of SSA invariants and returns every
// error found (empty slice == valid). v3 stores branch/phi/switch targets as raw
// block ids, so block operands are validated as block ids, not block values.
pub fn verify(m &ssa.Module) []VerifyError {
mut errors := []VerifyError{}
for fi in 0 .. m.funcs.len {
errors << verify_function(m, m.funcs[fi])
}
errors << verify_use_def_chains(m)
return errors
}
// verify_and_panic runs verification and panics on critical errors only.
pub fn verify_and_panic(m &ssa.Module, pass_name string) {
verify_and_panic_with_options(m, pass_name, VerifyPanicOptions{
allow_noncritical: true
})
}
// verify_and_panic_with_options runs verification with an explicit warning policy.
// Declaration-only functions (C externs / prototypes) are always accepted.
pub fn verify_and_panic_with_options(m &ssa.Module, pass_name string, opts VerifyPanicOptions) {
errors := verify(m)
if errors.len == 0 {
return
}
mut msg := 'SSA verification failed after ${pass_name}:\n'
mut critical := []VerifyError{}
mut warnings := 0
for err in errors {
if is_accepted_declaration_verify_error(m, err) {
continue
}
if opts.allow_noncritical && is_legacy_noncritical_verify_error(err) {
warnings++
} else {
critical << err
}
}
for err in critical {
msg += ' ${err.msg}\n'
}
if warnings > 0 {
msg += ' (${warnings} non-critical warnings suppressed)\n'
}
if critical.len > 0 {
panic(msg)
}
}
fn is_accepted_declaration_verify_error(m &ssa.Module, err VerifyError) bool {
if err.msg == 'function has no blocks' {
if err.func_id < 0 || err.func_id >= m.funcs.len {
return false
}
func := m.funcs[err.func_id]
return func.is_c_extern || func.is_prototype
}
return false
}
fn is_legacy_noncritical_verify_error(err VerifyError) bool {
// Dominance / use-def / phi mismatches are commonly transient between passes.
if err.msg.contains('does not dominate') || err.msg.contains('uses list')
|| err.msg.contains('phi') || err.msg.contains('block mismatch') {
return true
}
// Structural-normalization artifacts emitted by the builder before the
// optimizer cleans them up (empty/unreachable blocks, dead code that follows
// a terminator within the same block). These are tolerated by the lenient
// verify_ssa() and removed by remove_unreachable_blocks / DCE.
if err.msg.contains('has no instructions') || err.msg.contains('not at end of block')
|| err.msg.contains('multiple terminators')
|| err.msg.contains('does not end with terminator') {
return true
}
// Type-precision findings. The v3 builder does not maintain strict SSA typing
// on every load/store/binary operand (e.g. aggregates loaded by value), and
// the backends tolerate it, so these are advisory rather than fatal.
return err.msg.contains('should be pointer type') || err.msg.contains('mismatched types')
|| err.msg.contains('result type should be pointer')
}
fn verify_function(m &ssa.Module, func ssa.Function) []VerifyError {
mut errors := []VerifyError{}
if func.blocks.len == 0 {
errors << VerifyError{
msg: 'function has no blocks'
func_id: func.id
}
return errors
}
for blk_id in func.blocks {
if blk_id < 0 || blk_id >= m.blocks.len {
errors << VerifyError{
msg: 'invalid block id ${blk_id}'
func_id: func.id
}
continue
}
errors << verify_block(m, func, blk_id)
}
errors << verify_dominance(m, func)
return errors
}
fn verify_block(m &ssa.Module, func ssa.Function, blk_id int) []VerifyError {
mut errors := []VerifyError{}
blk := m.blocks[blk_id]
if blk.parent != func.id {
errors << VerifyError{
msg: 'block parent mismatch: expected ${func.id}, got ${blk.parent}'
func_id: func.id
block_id: blk_id
}
}
if blk.instrs.len == 0 {
errors << VerifyError{
msg: 'block has no instructions (missing terminator)'
func_id: func.id
block_id: blk_id
}
return errors
}
mut seen_terminator := false
mut seen_non_phi := false
for i, val_id in blk.instrs {
if val_id < 0 || val_id >= m.values.len {
errors << VerifyError{
msg: 'invalid value id ${val_id} in instruction list'
func_id: func.id
block_id: blk_id
}
continue
}
val := m.values[val_id]
if val.kind != .instruction {
continue
}
if val.index < 0 || val.index >= m.instrs.len {
errors << VerifyError{
msg: 'value ${val_id} has invalid instruction index ${val.index}'
func_id: func.id
block_id: blk_id
val_id: val_id
}
continue
}
instr := m.instrs[val.index]
if instr.block != blk_id {
errors << VerifyError{
msg: 'instruction block mismatch: expected ${blk_id}, got ${instr.block}'
func_id: func.id
block_id: blk_id
val_id: val_id
}
}
if instr.op == .phi {
if seen_non_phi {
errors << VerifyError{
msg: 'phi node after non-phi instruction'
func_id: func.id
block_id: blk_id
val_id: val_id
}
}
} else {
seen_non_phi = true
}
is_terminator := instr.op in [.ret, .br, .jmp, .switch_, .unreachable]
if is_terminator {
if seen_terminator {
errors << VerifyError{
msg: 'multiple terminators in block'
func_id: func.id
func_name: func.name
block_id: blk_id
val_id: val_id
}
}
if i != blk.instrs.len - 1 {
errors << VerifyError{
msg: 'terminator not at end of block'
func_id: func.id
func_name: func.name
block_id: blk_id
val_id: val_id
}
}
seen_terminator = true
}
errors << verify_instruction(m, func.id, blk_id, val_id, instr)
}
if !seen_terminator {
errors << VerifyError{
msg: 'block does not end with terminator'
func_id: func.id
block_id: blk_id
}
}
errors << verify_cfg_consistency(m, func.id, blk_id)
return errors
}
fn verify_instruction(m &ssa.Module, func_id int, blk_id int, val_id int, instr ssa.Instruction) []VerifyError {
mut errors := []VerifyError{}
// Only value operands are validated as value ids; block ids (branch/phi/switch
// targets) are validated by the op-specific checks below.
for i, op_id in instr.value_operands() {
if op_id < 0 || op_id >= m.values.len {
errors << VerifyError{
msg: 'operand ${i} has invalid value id ${op_id}'
func_id: func_id
block_id: blk_id
val_id: val_id
}
}
}
match instr.op {
.add, .sub, .mul, .sdiv, .udiv, .srem, .urem {
errors << verify_binary_op(m, func_id, blk_id, val_id, instr)
}
.fadd, .fsub, .fmul, .fdiv, .frem {
errors << verify_binary_op(m, func_id, blk_id, val_id, instr)
}
.shl, .lshr, .ashr, .and_, .or_, .xor {
errors << verify_binary_op(m, func_id, blk_id, val_id, instr)
}
.lt, .gt, .le, .ge, .eq, .ne, .ult, .ugt, .ule, .uge {
errors << verify_binary_op(m, func_id, blk_id, val_id, instr)
}
.load {
errors << verify_load(m, func_id, blk_id, val_id, instr)
}
.store {
errors << verify_store(m, func_id, blk_id, val_id, instr)
}
.alloca, .heap_alloc {
if instr.typ > 0 && instr.typ < m.type_store.types.len {
typ := m.type_store.types[instr.typ]
if typ.kind != .ptr_t {
errors << VerifyError{
msg: '${instr.op} result type should be pointer, got ${typ.kind}'
func_id: func_id
block_id: blk_id
val_id: val_id
}
}
}
}
.phi {
errors << verify_phi(m, func_id, blk_id, val_id, instr)
}
.br {
errors << verify_branch(m, func_id, blk_id, val_id, instr)
}
.jmp {
errors << verify_jump(m, func_id, blk_id, val_id, instr)
}
.switch_ {
errors << verify_switch(m, func_id, blk_id, val_id, instr)
}
.ret {
if instr.operands.len > 1 {
errors << VerifyError{
msg: 'ret has ${instr.operands.len} operands, expected 0 or 1'
func_id: func_id
block_id: blk_id
val_id: val_id
}
}
}
.call, .call_indirect, .call_sret, .go_call, .spawn_call {
if instr.operands.len == 0 {
errors << VerifyError{
msg: '${instr.op} has no operands'
func_id: func_id
block_id: blk_id
val_id: val_id
}
}
}
.select {
if instr.operands.len != 3 {
errors << VerifyError{
msg: 'select has ${instr.operands.len} operands, expected 3'
func_id: func_id
block_id: blk_id
val_id: val_id
}
}
}
.assign {
if instr.operands.len != 2 {
errors << VerifyError{
msg: 'assign has ${instr.operands.len} operands, expected 2'
func_id: func_id
block_id: blk_id
val_id: val_id
}
}
}
else {}
}
return errors
}
fn verify_binary_op(m &ssa.Module, func_id int, blk_id int, val_id int, instr ssa.Instruction) []VerifyError {
mut errors := []VerifyError{}
if instr.operands.len != 2 {
errors << VerifyError{
msg: 'binary op has ${instr.operands.len} operands, expected 2'
func_id: func_id
block_id: blk_id
val_id: val_id
}
return errors
}
op0 := instr.operands[0]
op1 := instr.operands[1]
if op0 < m.values.len && op1 < m.values.len {
t0 := m.values[op0].typ
t1 := m.values[op1].typ
if t0 != t1 && t0 != 0 && t1 != 0 {
if t0 < m.type_store.types.len && t1 < m.type_store.types.len {
k0 := m.type_store.types[t0].kind
k1 := m.type_store.types[t1].kind
// Allow common mixed-kind binary patterns (different widths, ptr
// arithmetic, struct/nil comparisons, int/float coercions).
if k0 == .int_t && k1 == .int_t {
return errors
}
if k0 == .float_t && k1 == .float_t {
return errors
}
if (k0 == .int_t && k1 == .float_t) || (k0 == .float_t && k1 == .int_t) {
return errors
}
if (k0 == .ptr_t && k1 == .int_t) || (k0 == .int_t && k1 == .ptr_t) {
return errors
}
if k0 == .ptr_t && k1 == .ptr_t {
return errors
}
if (k0 == .ptr_t && k1 == .struct_t) || (k0 == .struct_t && k1 == .ptr_t) {
return errors
}
if k0 == .struct_t && k1 == .struct_t {
return errors
}
if (k0 == .struct_t && k1 == .int_t) || (k0 == .int_t && k1 == .struct_t) {
return errors
}
errors << VerifyError{
msg: 'binary op operands have mismatched types: ${t0} (${k0}) vs ${t1} (${k1})'
func_id: func_id
block_id: blk_id
val_id: val_id
}
}
}
}
return errors
}
fn verify_load(m &ssa.Module, func_id int, blk_id int, val_id int, instr ssa.Instruction) []VerifyError {
mut errors := []VerifyError{}
if instr.operands.len != 1 {
errors << VerifyError{
msg: 'load has ${instr.operands.len} operands, expected 1 (pointer)'
func_id: func_id
block_id: blk_id
val_id: val_id
}
return errors
}
ptr_id := instr.operands[0]
if ptr_id < m.values.len {
ptr_typ_id := m.values[ptr_id].typ
if ptr_typ_id > 0 && ptr_typ_id < m.type_store.types.len {
if m.type_store.types[ptr_typ_id].kind != .ptr_t {
errors << VerifyError{
msg: 'load operand should be pointer type, got ${m.type_store.types[ptr_typ_id].kind}'
func_id: func_id
block_id: blk_id
val_id: val_id
}
}
}
}
return errors
}
fn verify_store(m &ssa.Module, func_id int, blk_id int, val_id int, instr ssa.Instruction) []VerifyError {
mut errors := []VerifyError{}
if instr.operands.len != 2 {
errors << VerifyError{
msg: 'store has ${instr.operands.len} operands, expected 2 (value, pointer)'
func_id: func_id
block_id: blk_id
val_id: val_id
}
return errors
}
ptr_id := instr.operands[1]
if ptr_id < m.values.len {
ptr_typ_id := m.values[ptr_id].typ
if ptr_typ_id > 0 && ptr_typ_id < m.type_store.types.len {
if m.type_store.types[ptr_typ_id].kind != .ptr_t {
errors << VerifyError{
msg: 'store destination should be pointer type, got ${m.type_store.types[ptr_typ_id].kind}'
func_id: func_id
block_id: blk_id
val_id: val_id
}
}
}
}
return errors
}
fn verify_phi(m &ssa.Module, func_id int, blk_id int, val_id int, instr ssa.Instruction) []VerifyError {
mut errors := []VerifyError{}
if instr.operands.len % 2 != 0 {
errors << VerifyError{
msg: 'phi has odd number of operands (${instr.operands.len}), expected (value, block) pairs'
func_id: func_id
block_id: blk_id
val_id: val_id
}
return errors
}
if instr.operands.len == 0 {
errors << VerifyError{
msg: 'phi has no operands'
func_id: func_id
block_id: blk_id
val_id: val_id
}
return errors
}
blk := m.blocks[blk_id]
for i := 0; i < instr.operands.len; i += 2 {
val_in := instr.operands[i]
pred_blk_id := int(instr.operands[i + 1])
if val_in < 0 || val_in >= m.values.len {
errors << VerifyError{
msg: 'phi operand ${i} has invalid value id ${val_in}'
func_id: func_id
block_id: blk_id
val_id: val_id
}
}
if pred_blk_id < 0 || pred_blk_id >= m.blocks.len {
errors << VerifyError{
msg: 'phi operand ${i + 1} has invalid block id ${pred_blk_id}'
func_id: func_id
block_id: blk_id
val_id: val_id
}
} else if blk.preds.len > 0 && pred_blk_id !in blk.preds {
errors << VerifyError{
msg: 'phi references block ${pred_blk_id} which is not a predecessor'
func_id: func_id
block_id: blk_id
val_id: val_id
}
}
}
if blk.preds.len > 0 {
actual_pairs := instr.operands.len / 2
if actual_pairs != blk.preds.len {
errors << VerifyError{
msg: 'phi has ${actual_pairs} operand pairs but block has ${blk.preds.len} predecessors'
func_id: func_id
block_id: blk_id
val_id: val_id
}
}
}
return errors
}
fn verify_branch(m &ssa.Module, func_id int, blk_id int, val_id int, instr ssa.Instruction) []VerifyError {
mut errors := []VerifyError{}
if instr.operands.len != 3 {
errors << VerifyError{
msg: 'br has ${instr.operands.len} operands, expected 3 (cond, true_blk, false_blk)'
func_id: func_id
block_id: blk_id
val_id: val_id
}
return errors
}
for i in [1, 2] {
b := int(instr.operands[i])
if b < 0 || b >= m.blocks.len {
errors << VerifyError{
msg: 'br operand ${i} has invalid block id ${b}'
func_id: func_id
block_id: blk_id
val_id: val_id
}
}
}
return errors
}
fn verify_jump(m &ssa.Module, func_id int, blk_id int, val_id int, instr ssa.Instruction) []VerifyError {
mut errors := []VerifyError{}
if instr.operands.len != 1 {
errors << VerifyError{
msg: 'jmp has ${instr.operands.len} operands, expected 1 (target_blk)'
func_id: func_id
block_id: blk_id
val_id: val_id
}
return errors
}
b := int(instr.operands[0])
if b < 0 || b >= m.blocks.len {
errors << VerifyError{
msg: 'jmp operand has invalid block id ${b}'
func_id: func_id
block_id: blk_id
val_id: val_id
}
}
return errors
}
fn verify_switch(m &ssa.Module, func_id int, blk_id int, val_id int, instr ssa.Instruction) []VerifyError {
mut errors := []VerifyError{}
if instr.operands.len < 2 {
errors << VerifyError{
msg: 'switch has ${instr.operands.len} operands, expected at least 2 (cond, default)'
func_id: func_id
block_id: blk_id
val_id: val_id
}
return errors
}
def_blk := int(instr.operands[1])
if def_blk < 0 || def_blk >= m.blocks.len {
errors << VerifyError{
msg: 'switch default has invalid block id ${def_blk}'
func_id: func_id
block_id: blk_id
val_id: val_id
}
}
remaining := instr.operands.len - 2
if remaining % 2 != 0 {
errors << VerifyError{
msg: 'switch has odd number of case operands (${remaining})'
func_id: func_id
block_id: blk_id
val_id: val_id
}
}
for i := 3; i < instr.operands.len; i += 2 {
b := int(instr.operands[i])
if b < 0 || b >= m.blocks.len {
errors << VerifyError{
msg: 'switch case has invalid block id ${b}'
func_id: func_id
block_id: blk_id
val_id: val_id
}
}
}
return errors
}
fn verify_cfg_consistency(m &ssa.Module, func_id int, blk_id int) []VerifyError {
mut errors := []VerifyError{}
blk := m.blocks[blk_id]
if blk.succs.len == 0 && blk.preds.len == 0 {
return errors
}
for succ_id in blk.succs {
if succ_id < 0 || succ_id >= m.blocks.len {
errors << VerifyError{
msg: 'invalid successor block id ${succ_id}'
func_id: func_id
block_id: blk_id
}
continue
}
if blk_id !in m.blocks[succ_id].preds {
errors << VerifyError{
msg: 'block ${blk_id} has successor ${succ_id} but is not in its predecessors'
func_id: func_id
block_id: blk_id
}
}
}
for pred_id in blk.preds {
if pred_id < 0 || pred_id >= m.blocks.len {
errors << VerifyError{
msg: 'invalid predecessor block id ${pred_id}'
func_id: func_id
block_id: blk_id
}
continue
}
if blk_id !in m.blocks[pred_id].succs {
errors << VerifyError{
msg: 'block ${blk_id} has predecessor ${pred_id} but is not in its successors'
func_id: func_id
block_id: blk_id
}
}
}
return errors
}
fn verify_dominance(m &ssa.Module, func ssa.Function) []VerifyError {
mut errors := []VerifyError{}
mut def_block := map[int]int{}
for param_id in func.params {
if func.blocks.len > 0 {
def_block[param_id] = func.blocks[0]
}
}
for blk_id in func.blocks {
if blk_id >= m.blocks.len {
continue
}
for val_id in m.blocks[blk_id].instrs {
def_block[val_id] = blk_id
}
}
entry_block := func.blocks[0]
dominators_computed := func.blocks.len == 1 || m.blocks[entry_block].dom_tree.len > 0
if !dominators_computed {
return errors
}
for blk_id in func.blocks {
if blk_id >= m.blocks.len {
continue
}
for val_id in m.blocks[blk_id].instrs {
if val_id >= m.values.len || m.values[val_id].kind != .instruction {
continue
}
if m.values[val_id].index >= m.instrs.len {
continue
}
instr := m.instrs[m.values[val_id].index]
// Phi operands legitimately come from predecessors, not dominators.
if instr.op == .phi {
continue
}
for i, op_id in instr.value_operands() {
if op_id >= m.values.len {
continue
}
op_val := m.values[op_id]
if op_val.kind in [.basic_block, .constant, .global, .string_literal,
.c_string_literal, .func_ref, .argument] {
continue
}
if def_blk := def_block[op_id] {
if !dominates(m, func, def_blk, blk_id) {
errors << VerifyError{
msg: 'operand ${i} (value ${op_id}) defined in block ${def_blk} does not dominate use in block ${blk_id}'
func_id: func.id
func_name: func.name
block_id: blk_id
val_id: val_id
}
}
}
}
}
}
return errors
}
fn dominates(m &ssa.Module, _func ssa.Function, a int, b int) bool {
if a == b {
return true
}
mut curr := b
for {
if curr < 0 || curr >= m.blocks.len {
return false
}
idom := m.blocks[curr].idom
if idom < 0 {
return false
}
if idom == curr {
return a == curr
}
if idom == a {
return true
}
curr = idom
}
return false
}
fn verify_use_def_chains(m &ssa.Module) []VerifyError {
mut errors := []VerifyError{}
mut expected_uses := map[int]map[int]bool{}
for fi in 0 .. m.funcs.len {
for blk_id in m.funcs[fi].blocks {
if blk_id >= m.blocks.len {
continue
}
for val_id in m.blocks[blk_id].instrs {
if val_id >= m.values.len || m.values[val_id].kind != .instruction {
continue
}
if m.values[val_id].index >= m.instrs.len {
continue
}
instr := m.instrs[m.values[val_id].index]
for op_id in instr.value_operands() {
if op_id >= 0 && op_id < m.values.len {
if op_id !in expected_uses {
expected_uses[op_id] = map[int]bool{}
}
expected_uses[op_id][val_id] = true
}
}
}
}
}
for val_id, users in expected_uses {
if val_id >= m.values.len {
continue
}
actual := m.values[val_id].uses
for user_id, _ in users {
if user_id !in actual {
errors << VerifyError{
msg: 'value ${val_id} is used by ${user_id} but ${user_id} is not in uses list'
val_id: val_id
}
}
}
}
return errors
}