Initial project import
This commit is contained in:
commit
23f2d29e32
11957 files changed
+1754577
No files matched your search
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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}')
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
Reference in new issue
Block a user