Initial project import
This commit is contained in:
commit
23f2d29e32
11957 files changed
+1754577
No files matched your search
File diff suppressed because it is too large.
Load diff
@@ -0,0 +1,11 @@
|
||||
module ssa
|
||||
|
||||
fn test_bench_runtime_stubs_include_macos_peak_rss_helper() {
|
||||
assert 'macos_peak_rss_kb' in bench_runtime_stub_names
|
||||
assert 'bench.macos_peak_rss_kb' in bench_runtime_stub_names
|
||||
assert 'macos_rss_kb' !in bench_runtime_stub_names
|
||||
assert 'bench.macos_rss_kb' !in bench_runtime_stub_names
|
||||
b := Builder{}
|
||||
assert b.skip_source_fn('macos_peak_rss_kb')
|
||||
assert b.skip_source_fn('bench.macos_peak_rss_kb')
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -0,0 +1,808 @@
|
||||
module ssa
|
||||
|
||||
import v3.token
|
||||
|
||||
pub type ValueID = int
|
||||
pub type TypeID = int
|
||||
pub type BlockID = int
|
||||
|
||||
pub enum OpCode {
|
||||
// Terminators
|
||||
ret
|
||||
br
|
||||
jmp
|
||||
switch_ // Multi-way branch: switch_ %val, default_block, [case_val, block]...
|
||||
unreachable
|
||||
// Binary (integer)
|
||||
add
|
||||
sub
|
||||
mul
|
||||
sdiv
|
||||
srem
|
||||
udiv
|
||||
urem
|
||||
// Binary (float)
|
||||
fadd
|
||||
fsub
|
||||
fmul
|
||||
fdiv
|
||||
frem
|
||||
// Bitwise
|
||||
shl
|
||||
ashr
|
||||
lshr
|
||||
and_
|
||||
or_
|
||||
xor
|
||||
// Memory
|
||||
alloca
|
||||
load
|
||||
store
|
||||
get_element_ptr
|
||||
heap_alloc // Heap allocate memory for a type (malloc+zero): returns ptr
|
||||
fence // Memory ordering barrier
|
||||
cmpxchg // Atomic compare-exchange
|
||||
atomicrmw // Atomic read-modify-write
|
||||
// Comparisons
|
||||
lt
|
||||
gt
|
||||
le
|
||||
ge
|
||||
ult
|
||||
ugt
|
||||
ule
|
||||
uge
|
||||
eq
|
||||
ne
|
||||
// Other
|
||||
call
|
||||
call_indirect // Indirect call through function pointer
|
||||
call_sret // Call with struct return (x8 indirect return on ARM64)
|
||||
neg
|
||||
trunc
|
||||
sext
|
||||
zext
|
||||
fptoui
|
||||
fptosi
|
||||
uitofp
|
||||
sitofp
|
||||
bitcast
|
||||
phi
|
||||
select
|
||||
assign // Copy op used during phi elimination
|
||||
inline_string_init // Build a string struct by value: (string){str, len}
|
||||
// Concurrency
|
||||
go_call // Launch goroutine: go_call fn_ref, args...
|
||||
spawn_call // Launch OS thread: spawn_call fn_ref, args...
|
||||
// Aggregate (struct/tuple) operations
|
||||
extractvalue
|
||||
insertvalue
|
||||
struct_init
|
||||
}
|
||||
|
||||
pub enum AtomicOrdering {
|
||||
not_atomic
|
||||
unordered
|
||||
monotonic
|
||||
acquire
|
||||
release
|
||||
acq_rel
|
||||
seq_cst
|
||||
}
|
||||
|
||||
pub enum InlineHint {
|
||||
none_ // No hint, let optimizer decide
|
||||
always // Always inline (e.g. V's [inline] attribute)
|
||||
never // Never inline (e.g. V's [noinline] attribute)
|
||||
hint // Suggest inlining (optimizer may ignore)
|
||||
}
|
||||
|
||||
pub enum TypeKind {
|
||||
void_t
|
||||
int_t
|
||||
float_t
|
||||
ptr_t
|
||||
array_t
|
||||
struct_t
|
||||
func_t
|
||||
label_t
|
||||
metadata_t
|
||||
}
|
||||
|
||||
pub struct Type {
|
||||
pub:
|
||||
kind TypeKind
|
||||
width int
|
||||
is_unsigned bool
|
||||
elem_type TypeID // For Ptr, Array
|
||||
len int // For Array
|
||||
fields []TypeID
|
||||
field_names []string
|
||||
params []TypeID
|
||||
ret_type TypeID
|
||||
is_c_struct bool // True for C interop structs (raw field names, typedef to C struct)
|
||||
is_union bool // True for union types (all fields overlap at offset 0)
|
||||
}
|
||||
|
||||
pub struct TypeStore {
|
||||
pub mut:
|
||||
types []Type
|
||||
cache map[string]TypeID
|
||||
}
|
||||
|
||||
const recursive_type_slot_size = 256
|
||||
|
||||
pub fn TypeStore.new() TypeStore {
|
||||
mut ts := TypeStore{
|
||||
cache: map[string]TypeID{}
|
||||
}
|
||||
ts.types << Type{
|
||||
kind: .void_t
|
||||
}
|
||||
return ts
|
||||
}
|
||||
|
||||
pub fn (mut ts TypeStore) get_int(width int) TypeID {
|
||||
key := 'i${width}'
|
||||
if id := ts.cache[key] {
|
||||
if id > 0 {
|
||||
return id
|
||||
}
|
||||
}
|
||||
id := ts.register(Type{ kind: .int_t, width: width })
|
||||
ts.cache[key] = id
|
||||
return id
|
||||
}
|
||||
|
||||
pub fn (mut ts TypeStore) get_uint(width int) TypeID {
|
||||
key := 'u${width}'
|
||||
if id := ts.cache[key] {
|
||||
if id > 0 {
|
||||
return id
|
||||
}
|
||||
}
|
||||
id := ts.register(Type{ kind: .int_t, width: width, is_unsigned: true })
|
||||
ts.cache[key] = id
|
||||
return id
|
||||
}
|
||||
|
||||
pub fn (mut ts TypeStore) get_float(width int) TypeID {
|
||||
key := 'f${width}'
|
||||
if id := ts.cache[key] {
|
||||
if id > 0 {
|
||||
return id
|
||||
}
|
||||
}
|
||||
id := ts.register(Type{ kind: .float_t, width: width })
|
||||
ts.cache[key] = id
|
||||
return id
|
||||
}
|
||||
|
||||
pub fn (mut ts TypeStore) get_ptr(elem TypeID) TypeID {
|
||||
key := 'p${elem}'
|
||||
if id := ts.cache[key] {
|
||||
if id > 0 {
|
||||
return id
|
||||
}
|
||||
}
|
||||
id := ts.register(Type{ kind: .ptr_t, elem_type: elem })
|
||||
ts.cache[key] = id
|
||||
return id
|
||||
}
|
||||
|
||||
// get_array returns the cached fixed-array type for (elem, length).
|
||||
pub fn (mut ts TypeStore) get_array(elem TypeID, length int) TypeID {
|
||||
key := 'a${elem}_${length}'
|
||||
if id := ts.cache[key] {
|
||||
if id > 0 {
|
||||
return id
|
||||
}
|
||||
}
|
||||
id := ts.register(Type{ kind: .array_t, elem_type: elem, len: length })
|
||||
ts.cache[key] = id
|
||||
return id
|
||||
}
|
||||
|
||||
// get_tuple returns a cached anonymous struct type holding the given element types.
|
||||
pub fn (mut ts TypeStore) get_tuple(elem_types []TypeID) TypeID {
|
||||
mut key := 'tuple'
|
||||
for t in elem_types {
|
||||
key += '_${t}'
|
||||
}
|
||||
if id := ts.cache[key] {
|
||||
if id > 0 {
|
||||
return id
|
||||
}
|
||||
}
|
||||
id := ts.register(Type{
|
||||
kind: .struct_t
|
||||
fields: elem_types
|
||||
})
|
||||
ts.cache[key] = id
|
||||
return id
|
||||
}
|
||||
|
||||
pub fn (mut ts TypeStore) register(t Type) TypeID {
|
||||
id := TypeID(ts.types.len)
|
||||
ts.types << t
|
||||
return id
|
||||
}
|
||||
|
||||
pub enum ValueKind {
|
||||
unknown
|
||||
constant
|
||||
argument
|
||||
global
|
||||
instruction
|
||||
basic_block
|
||||
string_literal // V string struct literal (by value)
|
||||
c_string_literal // C string literal (raw char pointer)
|
||||
func_ref
|
||||
}
|
||||
|
||||
pub struct Value {
|
||||
pub mut:
|
||||
id ValueID
|
||||
kind ValueKind
|
||||
typ TypeID
|
||||
name string
|
||||
index int
|
||||
uses []ValueID
|
||||
}
|
||||
|
||||
// ConstantData carries the typed payload of a constant value. It mirrors v2's
|
||||
// representation so backends can recover the original int/float/string value
|
||||
// instead of re-parsing Value.name.
|
||||
pub struct ConstantData {
|
||||
pub:
|
||||
int_val i64
|
||||
float_val f64
|
||||
str_val string
|
||||
}
|
||||
|
||||
pub struct Instruction {
|
||||
pub mut:
|
||||
op OpCode
|
||||
operands []ValueID
|
||||
block BlockID
|
||||
typ TypeID
|
||||
pos token.Pos
|
||||
atomic_ord AtomicOrdering
|
||||
inline InlineHint // Inline hint for call instructions
|
||||
}
|
||||
|
||||
pub struct BasicBlock {
|
||||
pub mut:
|
||||
id BlockID
|
||||
val_id ValueID // SSA value representing the block (0 in v3's raw-block-id model)
|
||||
name string
|
||||
parent int
|
||||
instrs []ValueID
|
||||
preds []BlockID
|
||||
succs []BlockID
|
||||
// Dominators
|
||||
idom BlockID
|
||||
dom_tree []BlockID
|
||||
}
|
||||
|
||||
pub enum CallConv {
|
||||
c_decl
|
||||
fast_call
|
||||
wasm_std
|
||||
}
|
||||
|
||||
pub enum Linkage {
|
||||
external
|
||||
private
|
||||
internal
|
||||
}
|
||||
|
||||
pub struct Function {
|
||||
pub mut:
|
||||
id int
|
||||
name string
|
||||
typ TypeID
|
||||
blocks []BlockID
|
||||
params []ValueID
|
||||
is_c_extern bool // C-language extern function (no V body)
|
||||
is_prototype bool // Registered declaration/signature whose body is not materialized yet
|
||||
linkage Linkage
|
||||
call_conv CallConv
|
||||
}
|
||||
|
||||
pub struct GlobalVar {
|
||||
pub mut:
|
||||
name string
|
||||
typ TypeID
|
||||
linkage Linkage
|
||||
alignment int
|
||||
is_constant bool
|
||||
initial_value i64 // For constants/enums, the initial integer value
|
||||
initial_data []u8 // For constant arrays: serialized element data
|
||||
}
|
||||
|
||||
pub struct TargetData {
|
||||
pub:
|
||||
ptr_size int = 8
|
||||
endian_little bool = true
|
||||
}
|
||||
|
||||
@[heap]
|
||||
pub struct Module {
|
||||
pub mut:
|
||||
name string
|
||||
target TargetData
|
||||
type_store TypeStore
|
||||
values []Value
|
||||
instrs []Instruction
|
||||
blocks []BasicBlock
|
||||
funcs []Function
|
||||
globals []GlobalVar
|
||||
// C struct names: TypeID -> C struct name (e.g. for `struct stat`). Used by
|
||||
// codegen to emit `typedef struct <name> ...;` and preserve the C layout.
|
||||
c_struct_names map[int]string
|
||||
// C structs marked @[typedef] — already a C typedef, not a struct tag.
|
||||
c_typedef_structs map[int]bool
|
||||
// Constant cache: "type:name" -> ValueID for deduplication.
|
||||
const_cache map[string]ValueID
|
||||
}
|
||||
|
||||
pub fn Module.new() &Module {
|
||||
mut m := &Module{
|
||||
type_store: TypeStore.new()
|
||||
c_struct_names: map[int]string{}
|
||||
c_typedef_structs: map[int]bool{}
|
||||
const_cache: map[string]ValueID{}
|
||||
}
|
||||
m.values << Value{
|
||||
kind: .unknown
|
||||
id: 0
|
||||
}
|
||||
return m
|
||||
}
|
||||
|
||||
pub fn (mut m Module) add_value(kind ValueKind, typ TypeID, name string, index int) ValueID {
|
||||
id := ValueID(m.values.len)
|
||||
m.values << Value{
|
||||
id: id
|
||||
kind: kind
|
||||
typ: typ
|
||||
name: name
|
||||
index: index
|
||||
}
|
||||
return id
|
||||
}
|
||||
|
||||
pub fn (mut m Module) add_instr(op OpCode, block BlockID, typ TypeID, operands []ValueID) ValueID {
|
||||
instr_idx := m.instrs.len
|
||||
m.instrs << Instruction{
|
||||
op: op
|
||||
block: block
|
||||
typ: typ
|
||||
operands: operands
|
||||
}
|
||||
val_id := m.add_value(.instruction, typ, '', instr_idx)
|
||||
mut blk := m.blocks[block]
|
||||
blk.instrs << val_id
|
||||
m.blocks[block] = blk
|
||||
for op_id in m.instrs[instr_idx].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
|
||||
}
|
||||
}
|
||||
return val_id
|
||||
}
|
||||
|
||||
// add_instr_front creates an instruction and prepends it to the block (used for
|
||||
// phi insertion by mem2reg, which requires phis at the top of a block).
|
||||
pub fn (mut m Module) add_instr_front(op OpCode, block BlockID, typ TypeID, operands []ValueID) ValueID {
|
||||
instr_idx := m.instrs.len
|
||||
m.instrs << Instruction{
|
||||
op: op
|
||||
block: block
|
||||
typ: typ
|
||||
operands: operands
|
||||
}
|
||||
val_id := m.add_value(.instruction, typ, '', instr_idx)
|
||||
mut blk := m.blocks[block]
|
||||
blk.instrs.prepend(val_id)
|
||||
m.blocks[block] = blk
|
||||
for op_id in m.instrs[instr_idx].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
|
||||
}
|
||||
}
|
||||
return val_id
|
||||
}
|
||||
|
||||
// append_phi_operands appends a (val, block_id) pair to a phi instruction.
|
||||
pub fn (mut m Module) append_phi_operands(instr_idx int, val ValueID, block_id BlockID) {
|
||||
mut instr := m.instrs[instr_idx]
|
||||
instr.operands << val
|
||||
instr.operands << block_id
|
||||
m.instrs[instr_idx] = instr
|
||||
if val > 0 && val < m.values.len {
|
||||
// keep use lists consistent: the phi value uses `val`
|
||||
}
|
||||
}
|
||||
|
||||
pub fn (mut m Module) add_block(func_id int, name string) BlockID {
|
||||
id := BlockID(m.blocks.len)
|
||||
unique := '${name}_${id}'
|
||||
m.blocks << BasicBlock{
|
||||
id: id
|
||||
name: unique
|
||||
parent: func_id
|
||||
}
|
||||
mut f := m.funcs[func_id]
|
||||
f.blocks << id
|
||||
f.is_prototype = false
|
||||
m.funcs[func_id] = f
|
||||
return id
|
||||
}
|
||||
|
||||
pub fn (mut m Module) new_function(name string, ret TypeID) int {
|
||||
for i, f in m.funcs {
|
||||
if f.name == name {
|
||||
return i
|
||||
}
|
||||
}
|
||||
id := m.funcs.len
|
||||
m.funcs << Function{
|
||||
id: id
|
||||
name: name
|
||||
typ: ret
|
||||
}
|
||||
return id
|
||||
}
|
||||
|
||||
// --- Safe mutation helpers (avoid chained struct-array mutations) ---
|
||||
|
||||
pub fn (mut m Module) func_add_param(func_id int, param_val ValueID) {
|
||||
mut f := m.funcs[func_id]
|
||||
f.params << param_val
|
||||
m.funcs[func_id] = f
|
||||
}
|
||||
|
||||
pub fn (mut m Module) func_set_c_extern(func_id int, val bool) {
|
||||
mut f := m.funcs[func_id]
|
||||
f.is_c_extern = val
|
||||
m.funcs[func_id] = f
|
||||
}
|
||||
|
||||
pub fn (mut m Module) func_set_prototype(func_id int, val bool) {
|
||||
mut f := m.funcs[func_id]
|
||||
f.is_prototype = val
|
||||
m.funcs[func_id] = f
|
||||
}
|
||||
|
||||
pub fn (mut m Module) block_add_succ(from BlockID, to BlockID) {
|
||||
mut blk := m.blocks[from]
|
||||
blk.succs << to
|
||||
m.blocks[from] = blk
|
||||
}
|
||||
|
||||
pub fn (mut m Module) block_add_pred(to BlockID, from BlockID) {
|
||||
mut blk := m.blocks[to]
|
||||
blk.preds << from
|
||||
m.blocks[to] = blk
|
||||
}
|
||||
|
||||
pub fn (mut m Module) add_global(name string, typ TypeID) ValueID {
|
||||
id := m.globals.len
|
||||
m.globals << GlobalVar{
|
||||
name: name
|
||||
typ: typ
|
||||
linkage: .private
|
||||
}
|
||||
ptr_typ := m.type_store.get_ptr(typ)
|
||||
return m.add_value(.global, ptr_typ, name, id)
|
||||
}
|
||||
|
||||
// add_global_with_data registers a private global initialized from raw bytes
|
||||
// (used for const arrays serialized to element data).
|
||||
pub fn (mut m Module) add_global_with_data(name string, elem_type TypeID, is_const bool, data []u8) ValueID {
|
||||
id := m.globals.len
|
||||
m.globals << GlobalVar{
|
||||
name: name
|
||||
typ: elem_type
|
||||
linkage: .private
|
||||
is_constant: is_const
|
||||
initial_data: data
|
||||
}
|
||||
ptr_typ := m.type_store.get_ptr(elem_type)
|
||||
return m.add_value(.global, ptr_typ, name, id)
|
||||
}
|
||||
|
||||
// add_external_global registers (or reuses) a global defined outside this module
|
||||
// (e.g. C runtime globals such as __stdoutp). Returns the global pointer value.
|
||||
pub fn (mut m Module) add_external_global(name string, typ TypeID) ValueID {
|
||||
for v in m.values {
|
||||
if v.kind == .global && v.name == name {
|
||||
return v.id
|
||||
}
|
||||
}
|
||||
id := m.globals.len
|
||||
m.globals << GlobalVar{
|
||||
name: name
|
||||
typ: typ
|
||||
linkage: .external
|
||||
}
|
||||
ptr_typ := m.type_store.get_ptr(typ)
|
||||
return m.add_value(.global, ptr_typ, name, id)
|
||||
}
|
||||
|
||||
pub fn (mut m Module) get_or_add_const(typ TypeID, name string) ValueID {
|
||||
key := '${typ}:${name}'
|
||||
if existing := m.const_cache[key] {
|
||||
if existing > 0 {
|
||||
return existing
|
||||
}
|
||||
}
|
||||
id := m.add_value(.constant, typ, name, 0)
|
||||
m.const_cache[key] = id
|
||||
return id
|
||||
}
|
||||
|
||||
// get_block_from_val converts a basic-block value operand to its block index.
|
||||
pub fn (m &Module) get_block_from_val(val_id int) int {
|
||||
return m.values[val_id].index
|
||||
}
|
||||
|
||||
// type_size returns the byte size for an SSA type on the current target.
|
||||
pub fn (m &Module) type_size(typ_id TypeID) int {
|
||||
mut visiting := []bool{len: m.type_store.types.len}
|
||||
mut cache := []int{len: m.type_store.types.len}
|
||||
return m.type_size_inner(typ_id, 0, mut visiting, mut cache)
|
||||
}
|
||||
|
||||
fn (m &Module) type_size_inner(typ_id TypeID, depth int, mut visiting []bool, mut cache []int) int {
|
||||
if typ_id <= 0 || typ_id >= m.type_store.types.len {
|
||||
return 0
|
||||
}
|
||||
if depth > 32 {
|
||||
return recursive_type_slot_size
|
||||
}
|
||||
if cache[typ_id] > 0 {
|
||||
return cache[typ_id]
|
||||
}
|
||||
typ := m.type_store.types[typ_id]
|
||||
if typ.width > 0 {
|
||||
size := (typ.width + 7) / 8
|
||||
cache[typ_id] = size
|
||||
return size
|
||||
}
|
||||
if typ.kind == .array_t {
|
||||
if visiting[typ_id] {
|
||||
return recursive_type_slot_size
|
||||
}
|
||||
visiting[typ_id] = true
|
||||
elem := m.type_size_inner(typ.elem_type, depth + 1, mut visiting, mut cache)
|
||||
visiting[typ_id] = false
|
||||
mut total := elem * typ.len
|
||||
if total <= 0 {
|
||||
total = 0
|
||||
}
|
||||
cache[typ_id] = total
|
||||
return total
|
||||
}
|
||||
if typ.elem_type > 0 && typ.fields.len == 0 {
|
||||
cache[typ_id] = 8
|
||||
return 8
|
||||
}
|
||||
if typ.fields.len == 0 {
|
||||
if typ.params.len > 0 || typ.ret_type > 0 {
|
||||
cache[typ_id] = 8
|
||||
return 8
|
||||
}
|
||||
return 0
|
||||
}
|
||||
if typ.fields.len > 256 {
|
||||
cache[typ_id] = 8
|
||||
return 8
|
||||
}
|
||||
if visiting[typ_id] {
|
||||
return recursive_type_slot_size
|
||||
}
|
||||
visiting[typ_id] = true
|
||||
mut total := 0
|
||||
if typ.is_union {
|
||||
// Unions: all fields overlap at offset 0; size is the largest field,
|
||||
// rounded up to the largest field alignment.
|
||||
mut max_size := 0
|
||||
mut max_align := 1
|
||||
for i in 0 .. typ.fields.len {
|
||||
field_typ := typ.fields[i]
|
||||
s := m.type_size_inner(field_typ, depth + 1, mut visiting, mut cache)
|
||||
if s > max_size {
|
||||
max_size = s
|
||||
}
|
||||
a := m.type_align_for_layout(field_typ)
|
||||
if a > max_align {
|
||||
max_align = a
|
||||
}
|
||||
}
|
||||
total = if max_align > 1 && max_size % max_align != 0 {
|
||||
(max_size + max_align - 1) & ~(max_align - 1)
|
||||
} else {
|
||||
max_size
|
||||
}
|
||||
} else {
|
||||
mut offset := 0
|
||||
mut max_align := 1
|
||||
for i in 0 .. typ.fields.len {
|
||||
field_typ := typ.fields[i]
|
||||
align := m.type_align_for_layout(field_typ)
|
||||
if align > max_align {
|
||||
max_align = align
|
||||
}
|
||||
if align > 1 && offset % align != 0 {
|
||||
offset = (offset + align - 1) & ~(align - 1)
|
||||
}
|
||||
offset += m.type_size_inner(field_typ, depth + 1, mut visiting, mut cache)
|
||||
}
|
||||
total = if max_align > 1 && offset % max_align != 0 {
|
||||
(offset + max_align - 1) & ~(max_align - 1)
|
||||
} else {
|
||||
offset
|
||||
}
|
||||
}
|
||||
visiting[typ_id] = false
|
||||
if total <= 0 {
|
||||
total = 8
|
||||
}
|
||||
cache[typ_id] = total
|
||||
return total
|
||||
}
|
||||
|
||||
// type_align returns the ABI alignment for an SSA type on the current target.
|
||||
pub fn (m &Module) type_align(typ_id TypeID) int {
|
||||
return m.type_align_for_layout(typ_id)
|
||||
}
|
||||
|
||||
fn (m &Module) type_align_for_layout(typ_id TypeID) int {
|
||||
return m.type_align_for_layout_inner(typ_id, 0)
|
||||
}
|
||||
|
||||
fn (m &Module) type_align_for_layout_inner(typ_id TypeID, depth int) int {
|
||||
if typ_id <= 0 || typ_id >= m.type_store.types.len {
|
||||
return 1
|
||||
}
|
||||
if depth > 16 {
|
||||
return 8
|
||||
}
|
||||
typ := m.type_store.types[typ_id]
|
||||
if typ.width > 0 {
|
||||
size := (typ.width + 7) / 8
|
||||
if size >= 8 {
|
||||
return 8
|
||||
}
|
||||
if size >= 4 {
|
||||
return 4
|
||||
}
|
||||
return 1
|
||||
}
|
||||
if typ.kind == .array_t {
|
||||
return m.type_align_for_layout_inner(typ.elem_type, depth + 1)
|
||||
}
|
||||
if typ.elem_type > 0 && typ.fields.len == 0 {
|
||||
return 8
|
||||
}
|
||||
if typ.fields.len > 0 {
|
||||
return 8
|
||||
}
|
||||
if typ.params.len > 0 || typ.ret_type > 0 {
|
||||
return 8
|
||||
}
|
||||
return 1
|
||||
}
|
||||
|
||||
pub fn (mut m Module) replace_uses(old_id ValueID, new_id ValueID) {
|
||||
if old_id <= 0 || old_id >= m.values.len {
|
||||
return
|
||||
}
|
||||
for user_id in m.values[old_id].uses {
|
||||
if user_id <= 0 || user_id >= m.values.len {
|
||||
continue
|
||||
}
|
||||
val := m.values[user_id]
|
||||
if val.kind != .instruction {
|
||||
continue
|
||||
}
|
||||
mut instr := m.instrs[val.index]
|
||||
for i in 0 .. instr.operands.len {
|
||||
if instr.operands[i] == old_id {
|
||||
instr.operands[i] = new_id
|
||||
}
|
||||
}
|
||||
m.instrs[val.index] = instr
|
||||
}
|
||||
}
|
||||
|
||||
pub fn (i &Instruction) value_operands() []ValueID {
|
||||
if i.op == .br {
|
||||
if i.operands.len > 0 {
|
||||
mut r := []ValueID{}
|
||||
r << i.operands[0]
|
||||
return r
|
||||
}
|
||||
return []ValueID{}
|
||||
}
|
||||
if i.op == .jmp {
|
||||
return []ValueID{}
|
||||
}
|
||||
if i.op == .switch_ {
|
||||
// switch_ %val, default_block, [case_val, block]...
|
||||
// Only %val and the case values are SSA values; blocks are raw block ids.
|
||||
mut r := []ValueID{}
|
||||
if i.operands.len > 0 {
|
||||
r << i.operands[0]
|
||||
}
|
||||
for oi := 2; oi < i.operands.len; oi += 2 {
|
||||
r << i.operands[oi]
|
||||
}
|
||||
return r
|
||||
}
|
||||
if i.op == .phi {
|
||||
mut r := []ValueID{}
|
||||
for oi := 0; oi < i.operands.len; oi += 2 {
|
||||
r << i.operands[oi]
|
||||
}
|
||||
return r
|
||||
}
|
||||
return i.operands
|
||||
}
|
||||
|
||||
// struct_field_offset returns the byte offset of a field in a struct type.
|
||||
pub fn (m &Module) struct_field_offset(typ_id TypeID, field_idx int) int {
|
||||
if typ_id <= 0 || typ_id >= m.type_store.types.len {
|
||||
return 0
|
||||
}
|
||||
typ := m.type_store.types[typ_id]
|
||||
if typ.kind != .struct_t {
|
||||
return 0
|
||||
}
|
||||
if typ.is_union {
|
||||
return 0
|
||||
}
|
||||
mut visiting := []bool{len: m.type_store.types.len}
|
||||
mut cache := []int{len: m.type_store.types.len}
|
||||
visiting[typ_id] = true
|
||||
mut offset := 0
|
||||
for i in 0 .. field_idx {
|
||||
if i >= typ.fields.len {
|
||||
break
|
||||
}
|
||||
align := m.type_align_for_layout(typ.fields[i])
|
||||
if align > 1 && offset % align != 0 {
|
||||
offset = (offset + align - 1) & ~(align - 1)
|
||||
}
|
||||
offset += m.type_size_inner(typ.fields[i], 1, mut visiting, mut cache)
|
||||
}
|
||||
if field_idx < typ.fields.len {
|
||||
align := m.type_align_for_layout(typ.fields[field_idx])
|
||||
if align > 1 && offset % align != 0 {
|
||||
offset = (offset + align - 1) & ~(align - 1)
|
||||
}
|
||||
}
|
||||
return offset
|
||||
}
|
||||
|
||||
// struct_field_size returns the byte size of a field in a struct type.
|
||||
pub fn (m &Module) struct_field_size(typ_id TypeID, field_idx int) int {
|
||||
if typ_id <= 0 || typ_id >= m.type_store.types.len {
|
||||
return 0
|
||||
}
|
||||
typ := m.type_store.types[typ_id]
|
||||
if typ.kind != .struct_t || field_idx < 0 || field_idx >= typ.fields.len {
|
||||
return 0
|
||||
}
|
||||
mut visiting := []bool{len: m.type_store.types.len}
|
||||
mut cache := []int{len: m.type_store.types.len}
|
||||
visiting[typ_id] = true
|
||||
return m.type_size_inner(typ.fields[field_idx], 1, mut visiting, mut cache)
|
||||
}
|
||||
@@ -0,0 +1,311 @@
|
||||
module ssa
|
||||
|
||||
// Parallel-SSA worker support, ported from v2's module.v and adapted to v3's
|
||||
// raw-block-id operand model: branch/phi/switch operands that name blocks are
|
||||
// remapped by the block offset, while ordinary value operands are remapped by
|
||||
// the value offset.
|
||||
|
||||
// FuncSSAData holds the SSA a worker produced for a single function.
|
||||
pub struct FuncSSAData {
|
||||
pub:
|
||||
func_idx int // index into the main module's funcs[]
|
||||
blocks []BlockID // worker-local block ids
|
||||
params []ValueID // worker-local param value ids
|
||||
}
|
||||
|
||||
// is_block_operand reports whether operand `idx` of this instruction names a
|
||||
// basic block (a raw block id) rather than an SSA value.
|
||||
pub fn (i &Instruction) is_block_operand(idx int) bool {
|
||||
return match i.op {
|
||||
.jmp { idx == 0 }
|
||||
.br { idx == 1 || idx == 2 }
|
||||
// switch_: cond, default_blk, [case_val, blk]... -> blocks at odd indices.
|
||||
.switch_ { idx % 2 == 1 }
|
||||
// phi: [val, blk]... -> blocks at odd indices.
|
||||
.phi { idx % 2 == 1 }
|
||||
else { false }
|
||||
}
|
||||
}
|
||||
|
||||
// new_worker_module creates a lightweight Module for parallel SSA building. The
|
||||
// worker gets a deep copy of the type store and is seeded with the main module's
|
||||
// values/instrs/blocks so ids produced in earlier (serial) phases stay valid.
|
||||
pub fn (mut m Module) new_worker_module() &Module {
|
||||
mut wf := []Function{cap: m.funcs.len}
|
||||
for f in m.funcs {
|
||||
wf << f
|
||||
}
|
||||
mut wg := []GlobalVar{cap: m.globals.len}
|
||||
for g in m.globals {
|
||||
wg << g
|
||||
}
|
||||
mut wts := TypeStore{
|
||||
cache: map[string]TypeID{}
|
||||
}
|
||||
wts.types = []Type{cap: m.type_store.types.len}
|
||||
for t in m.type_store.types {
|
||||
wts.types << t
|
||||
}
|
||||
for k, v in m.type_store.cache {
|
||||
wts.cache[k] = v
|
||||
}
|
||||
|
||||
mut w := &Module{
|
||||
name: m.name
|
||||
target: m.target
|
||||
type_store: wts
|
||||
funcs: wf
|
||||
globals: wg
|
||||
c_struct_names: map[int]string{}
|
||||
c_typedef_structs: map[int]bool{}
|
||||
const_cache: m.const_cache.clone()
|
||||
}
|
||||
w.values = []Value{cap: m.values.len + 1024}
|
||||
for v in m.values {
|
||||
w.values << v
|
||||
}
|
||||
w.instrs = []Instruction{cap: m.instrs.len + 1024}
|
||||
for instr in m.instrs {
|
||||
w.instrs << instr
|
||||
}
|
||||
w.blocks = []BasicBlock{cap: m.blocks.len + 256}
|
||||
for blk in m.blocks {
|
||||
w.blocks << blk
|
||||
}
|
||||
return w
|
||||
}
|
||||
|
||||
fn worker_type_cache_key(t Type, type_remap []TypeID) string {
|
||||
return match t.kind {
|
||||
.int_t {
|
||||
if t.is_unsigned {
|
||||
'u${t.width}'
|
||||
} else {
|
||||
'i${t.width}'
|
||||
}
|
||||
}
|
||||
.float_t {
|
||||
'f${t.width}'
|
||||
}
|
||||
.ptr_t {
|
||||
remapped := if int(t.elem_type) < type_remap.len {
|
||||
type_remap[int(t.elem_type)]
|
||||
} else {
|
||||
t.elem_type
|
||||
}
|
||||
'p${remapped}'
|
||||
}
|
||||
.array_t {
|
||||
remapped := if int(t.elem_type) < type_remap.len {
|
||||
type_remap[int(t.elem_type)]
|
||||
} else {
|
||||
t.elem_type
|
||||
}
|
||||
'a${remapped}_${t.len}'
|
||||
}
|
||||
else {
|
||||
''
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// merge_worker_module folds a worker's freshly-built SSA (everything beyond the
|
||||
// seed lengths) back into the main module, remapping type/value/instr/block/func
|
||||
// ids. func_data carries each built function's worker-local blocks/params.
|
||||
pub fn (mut m Module) merge_worker_module(w &Module, func_data []FuncSSAData, seed_values int, seed_instrs int, seed_blocks int, seed_types int, seed_funcs int) {
|
||||
// 1. Remap worker types >= seed_types into the main type store.
|
||||
mut type_remap := []TypeID{len: w.type_store.types.len, init: 0}
|
||||
for ti := 0; ti < seed_types && ti < type_remap.len; ti++ {
|
||||
type_remap[ti] = ti
|
||||
}
|
||||
for ti := seed_types; ti < w.type_store.types.len; ti++ {
|
||||
wt := w.type_store.types[ti]
|
||||
cache_key := worker_type_cache_key(wt, type_remap)
|
||||
if cache_key.len > 0 {
|
||||
if existing := m.type_store.cache[cache_key] {
|
||||
if existing > 0 {
|
||||
type_remap[ti] = existing
|
||||
continue
|
||||
}
|
||||
}
|
||||
}
|
||||
remap_t := fn (id TypeID, type_remap []TypeID, seed_types int) TypeID {
|
||||
if int(id) >= seed_types && int(id) < type_remap.len {
|
||||
return type_remap[int(id)]
|
||||
}
|
||||
return id
|
||||
}
|
||||
mut new_fields := []TypeID{cap: wt.fields.len}
|
||||
for f in wt.fields {
|
||||
new_fields << remap_t(f, type_remap, seed_types)
|
||||
}
|
||||
mut new_params := []TypeID{cap: wt.params.len}
|
||||
for p in wt.params {
|
||||
new_params << remap_t(p, type_remap, seed_types)
|
||||
}
|
||||
new_type := Type{
|
||||
kind: wt.kind
|
||||
width: wt.width
|
||||
is_unsigned: wt.is_unsigned
|
||||
elem_type: remap_t(wt.elem_type, type_remap, seed_types)
|
||||
len: wt.len
|
||||
fields: new_fields
|
||||
field_names: wt.field_names
|
||||
params: new_params
|
||||
ret_type: remap_t(wt.ret_type, type_remap, seed_types)
|
||||
is_c_struct: wt.is_c_struct
|
||||
is_union: wt.is_union
|
||||
}
|
||||
new_id := m.type_store.register(new_type)
|
||||
if cache_key.len > 0 {
|
||||
m.type_store.cache[cache_key] = new_id
|
||||
}
|
||||
type_remap[ti] = new_id
|
||||
}
|
||||
|
||||
value_off := m.values.len - seed_values
|
||||
instr_off := m.instrs.len - seed_instrs
|
||||
block_off := m.blocks.len - seed_blocks
|
||||
|
||||
remap_val := fn (id ValueID, seed_values int, value_off int) ValueID {
|
||||
return if id >= seed_values { id + value_off } else { id }
|
||||
}
|
||||
remap_blk := fn (id BlockID, seed_blocks int, block_off int) BlockID {
|
||||
return if id >= seed_blocks { id + block_off } else { id }
|
||||
}
|
||||
remap_typ := fn (id TypeID, seed_types int, type_remap []TypeID) TypeID {
|
||||
return if int(id) >= seed_types && int(id) < type_remap.len {
|
||||
type_remap[int(id)]
|
||||
} else {
|
||||
id
|
||||
}
|
||||
}
|
||||
|
||||
// 2. Append worker values beyond the seed.
|
||||
for wi := seed_values; wi < w.values.len; wi++ {
|
||||
wv := w.values[wi]
|
||||
mut new_index := wv.index
|
||||
if wv.kind == .instruction {
|
||||
new_index += instr_off
|
||||
} else if wv.kind == .basic_block {
|
||||
new_index += block_off
|
||||
}
|
||||
mut new_uses := []ValueID{cap: wv.uses.len}
|
||||
for u in wv.uses {
|
||||
new_uses << remap_val(u, seed_values, value_off)
|
||||
}
|
||||
m.values << Value{
|
||||
id: wi + value_off
|
||||
kind: wv.kind
|
||||
typ: remap_typ(wv.typ, seed_types, type_remap)
|
||||
name: wv.name
|
||||
index: new_index
|
||||
uses: new_uses
|
||||
}
|
||||
}
|
||||
|
||||
// 3. Append worker instructions beyond the seed, remapping operands by kind.
|
||||
for ii := seed_instrs; ii < w.instrs.len; ii++ {
|
||||
instr := w.instrs[ii]
|
||||
mut new_ops := []ValueID{cap: instr.operands.len}
|
||||
for oi in 0 .. instr.operands.len {
|
||||
op := instr.operands[oi]
|
||||
if instr.is_block_operand(oi) {
|
||||
new_ops << ValueID(remap_blk(BlockID(op), seed_blocks, block_off))
|
||||
} else {
|
||||
new_ops << remap_val(op, seed_values, value_off)
|
||||
}
|
||||
}
|
||||
m.instrs << Instruction{
|
||||
op: instr.op
|
||||
operands: new_ops
|
||||
block: remap_blk(instr.block, seed_blocks, block_off)
|
||||
typ: remap_typ(instr.typ, seed_types, type_remap)
|
||||
pos: instr.pos
|
||||
atomic_ord: instr.atomic_ord
|
||||
inline: instr.inline
|
||||
}
|
||||
}
|
||||
|
||||
// 4. Append worker blocks beyond the seed.
|
||||
for bi := seed_blocks; bi < w.blocks.len; bi++ {
|
||||
blk := w.blocks[bi]
|
||||
mut new_instrs := []ValueID{cap: blk.instrs.len}
|
||||
for vi in blk.instrs {
|
||||
new_instrs << remap_val(vi, seed_values, value_off)
|
||||
}
|
||||
mut new_preds := []BlockID{cap: blk.preds.len}
|
||||
for p in blk.preds {
|
||||
new_preds << remap_blk(p, seed_blocks, block_off)
|
||||
}
|
||||
mut new_succs := []BlockID{cap: blk.succs.len}
|
||||
for s in blk.succs {
|
||||
new_succs << remap_blk(s, seed_blocks, block_off)
|
||||
}
|
||||
m.blocks << BasicBlock{
|
||||
id: blk.id + block_off
|
||||
val_id: remap_val(blk.val_id, seed_values, value_off)
|
||||
name: blk.name
|
||||
parent: blk.parent
|
||||
instrs: new_instrs
|
||||
preds: new_preds
|
||||
succs: new_succs
|
||||
}
|
||||
}
|
||||
|
||||
// 5. Patch the built functions' blocks/params in the main module.
|
||||
for fd in func_data {
|
||||
if fd.func_idx >= m.funcs.len {
|
||||
continue
|
||||
}
|
||||
mut f := m.funcs[fd.func_idx]
|
||||
if f.blocks.len > 0 {
|
||||
continue // already merged by an earlier worker
|
||||
}
|
||||
mut new_blocks := []BlockID{cap: fd.blocks.len}
|
||||
for blk_id in fd.blocks {
|
||||
new_blocks << remap_blk(blk_id, seed_blocks, block_off)
|
||||
}
|
||||
f.blocks = new_blocks
|
||||
mut new_params := []ValueID{cap: fd.params.len}
|
||||
for p in fd.params {
|
||||
new_params << remap_val(p, seed_values, value_off)
|
||||
}
|
||||
f.params = new_params
|
||||
m.funcs[fd.func_idx] = f
|
||||
}
|
||||
|
||||
// 6. Append any brand-new functions a worker created (runtime stubs, etc.).
|
||||
for fi := seed_funcs; fi < w.funcs.len; fi++ {
|
||||
wfunc := w.funcs[fi]
|
||||
mut already := false
|
||||
for existing in m.funcs {
|
||||
if existing.name == wfunc.name {
|
||||
already = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if already {
|
||||
continue
|
||||
}
|
||||
mut new_blocks := []BlockID{cap: wfunc.blocks.len}
|
||||
for blk_id in wfunc.blocks {
|
||||
new_blocks << remap_blk(blk_id, seed_blocks, block_off)
|
||||
}
|
||||
mut new_params := []ValueID{cap: wfunc.params.len}
|
||||
for p in wfunc.params {
|
||||
new_params << remap_val(p, seed_values, value_off)
|
||||
}
|
||||
m.funcs << Function{
|
||||
id: m.funcs.len
|
||||
name: wfunc.name
|
||||
typ: remap_typ(wfunc.typ, seed_types, type_remap)
|
||||
blocks: new_blocks
|
||||
params: new_params
|
||||
is_c_extern: wfunc.is_c_extern
|
||||
is_prototype: wfunc.is_prototype
|
||||
linkage: wfunc.linkage
|
||||
call_conv: wfunc.call_conv
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user