Initial project import

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

No files matched your search

@@ -0,0 +1,165 @@
// Copyright (c) 2026 Alexander Medvednikov. All rights reserved.
// Use of this source code is governed by an MIT license
// that can be found in the LICENSE file.
module optimize
import v2.ssa
fn remove_unreachable_blocks(mut m ssa.Module) {
// Re-build CFG first
build_cfg(mut m)
for fi in 0 .. m.funcs.len {
if m.funcs[fi].blocks.len == 0 {
continue
}
// BFS/DFS from entry
mut reachable := map[int]bool{}
mut q := [m.funcs[fi].blocks[0]]
reachable[m.funcs[fi].blocks[0]] = true
for q.len > 0 {
curr := q[q.len - 1]
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
}
}
// Avoid m.funcs[fi].blocks = ... -- chained field assign broken in ARM64 self-hosted
mut func := m.funcs[fi]
func.blocks = new_blocks
m.funcs[fi] = func
}
}
fn merge_blocks(mut m ssa.Module) {
// If Block A jumps unconditionally to B, and B has only A as predecessor:
// 1. Move instructions from B to A
// 2. Update A's successors to B's successors
// 3. Remove B
// We need to be careful about iteration while modifying.
// Loop until no changes.
mut changed := true
mut first_iter := true
for changed {
changed = false
// Only rebuild CFG on first iteration or after actual changes
if first_iter {
build_cfg(mut m)
first_iter = false
}
for fi in 0 .. m.funcs.len {
// We iterate through blocks.
// If we merge A->B, we can't merge B->C in same pass easily.
mut merged := map[int]bool{}
for blk_id in m.funcs[fi].blocks {
if merged[blk_id] {
continue
}
// Check if unconditional jump
if m.blocks[blk_id].instrs.len > 0 {
last_val := m.blocks[blk_id].instrs[m.blocks[blk_id].instrs.len - 1]
last_instr := m.instrs[m.values[last_val].index]
if last_instr.op == .jmp {
target_val := last_instr.operands[0]
target_id := m.get_block_from_val(target_val)
// Check if target has phi nodes - don't merge if it does
mut has_phi := false
for vid in m.blocks[target_id].instrs {
if m.values[vid].kind == .instruction {
if m.instrs[m.values[vid].index].op == .phi {
has_phi = true
break
}
}
}
// Candidate: target_id (only if no phi nodes)
if target_id != blk_id && m.blocks[target_id].preds.len == 1
&& m.blocks[target_id].preds[0] == blk_id && !has_phi {
// MERGE: Remove JMP from A, then append B's instrs to A
// Read whole struct, modify, write back (chained broken in ARM64)
mut merge_blk := m.blocks[blk_id]
merge_blk.instrs.delete_last()
merge_blk.instrs << m.blocks[target_id].instrs
m.blocks[blk_id] = merge_blk
// Update instructions in B to point to A (for their 'block' field)?
// Not strictly needed if we just use the list.
// But we need to update Phis in successors of B?
// If B has successors, their Phis might refer to B.
// Since B is gone, they now refer to A.
for succ_id in m.blocks[target_id].succs {
n_succ_instrs := m.blocks[succ_id].instrs.len
for ivi in 0 .. n_succ_instrs {
iv := m.blocks[succ_id].instrs[ivi]
v := m.values[iv]
if v.kind != .instruction {
continue
}
ins := m.instrs[v.index]
if ins.op == .phi {
// Replace all occurrences (defensive - handles edge cases)
// i=1,3,5... are block references in phi [val0, blk0, val1, blk1, ...]
// Avoid m.instrs[X].operands[i] = ... -- chained broken in ARM64 self-hosted
mut phi_ops := ins.operands.clone()
mut phi_modified := false
for i := 1; i < phi_ops.len; i += 2 {
if phi_ops[i] == m.blocks[target_id].val_id {
phi_ops[i] = m.blocks[blk_id].val_id
phi_modified = true
}
}
if phi_modified {
mut phi_ins := m.instrs[v.index]
phi_ins.operands = phi_ops
m.instrs[v.index] = phi_ins
}
}
}
}
// Remove B from func
merged[target_id] = true
changed = true
}
}
}
}
// Filter out merged blocks
if merged.len > 0 {
mut new_blks := []int{}
for b in m.funcs[fi].blocks {
if !merged[b] {
new_blks << b
}
}
// Avoid m.funcs[fi].blocks = ... -- chained field assign broken in ARM64 self-hosted
mut func2 := m.funcs[fi]
func2.blocks = new_blks
m.funcs[fi] = func2
}
}
// Rebuild CFG for next iteration if we made changes
if changed {
build_cfg(mut m)
}
}
}
@@ -0,0 +1,176 @@
// Copyright (c) 2026 Alexander Medvednikov. All rights reserved.
// Use of this source code is governed by an MIT license
// that can be found in the LICENSE file.
module optimize
import v2.ssa
// --- CFG Construction ---
// Flat CFG data: succs and preds stored outside BasicBlock to avoid struct field access issues
// in v3 (ARM64-compiled binary). Accessing m.blocks[i].succs returns garbage for large structs.
struct CfgData {
mut:
succs [][]int
preds [][]int
}
// Helper: check if value is already in a small array (avoids map allocation)
fn arr_contains(arr []int, val int) bool {
for i in 0 .. arr.len {
if arr[i] == val {
return true
}
}
return false
}
pub fn build_cfg(mut m ssa.Module) CfgData {
n_blocks := m.blocks.len
n_values := m.values.len
n_funcs := m.funcs.len
n_instrs_total := m.instrs.len
mut cfg := CfgData{
succs: [][]int{len: n_blocks}
preds: [][]int{len: n_blocks}
}
for fi in 0 .. n_funcs {
func := m.funcs[fi]
n_func_blocks := func.blocks.len
for fbi2 in 0 .. n_func_blocks {
blk_id := func.blocks[fbi2]
blk := m.blocks[blk_id]
n_instrs := blk.instrs.len
if n_instrs == 0 {
continue
}
term_val_id := blk.instrs[n_instrs - 1]
if term_val_id < 0 || term_val_id >= n_values {
continue
}
term_val := m.values[term_val_id]
if term_val.index < 0 || term_val.index >= n_instrs_total {
continue
}
term := m.instrs[term_val.index]
n_operands := term.operands.len
match term.op {
.br {
if n_operands >= 3 {
op1 := term.operands[1]
op2 := term.operands[2]
mut s1 := -1
if op1 >= 0 {
if op1 < n_values {
v1 := m.values[op1]
s1 = v1.index
}
}
mut s2 := -1
if op2 >= 0 {
if op2 < n_values {
v2 := m.values[op2]
s2 = v2.index
}
}
if s1 >= 0 {
if s1 < n_blocks {
if !arr_contains(cfg.succs[blk_id], s1) {
cfg.succs[blk_id] << s1
}
}
}
if s2 >= 0 {
if s2 < n_blocks {
if !arr_contains(cfg.succs[blk_id], s2) {
cfg.succs[blk_id] << s2
}
}
}
}
}
.jmp {
if n_operands >= 1 {
op0 := term.operands[0]
mut s := -1
if op0 >= 0 {
if op0 < n_values {
v0 := m.values[op0]
s = v0.index
}
}
if s >= 0 {
if s < n_blocks {
cfg.succs[blk_id] << s
}
}
}
}
.switch_ {
if n_operands >= 2 {
op_def := term.operands[1]
mut s := -1
if op_def >= 0 {
if op_def < n_values {
vd := m.values[op_def]
s = vd.index
}
}
if s >= 0 {
if s < n_blocks {
if !arr_contains(cfg.succs[blk_id], s) {
cfg.succs[blk_id] << s
}
}
}
for i := 3; i < n_operands; i += 2 {
op_case := term.operands[i]
mut cs := -1
if op_case >= 0 {
if op_case < n_values {
vc := m.values[op_case]
cs = vc.index
}
}
if cs >= 0 {
if cs < n_blocks {
if !arr_contains(cfg.succs[blk_id], cs) {
cfg.succs[blk_id] << cs
}
}
}
}
}
}
else {}
}
// Build predecessors from succs
n_succs := cfg.succs[blk_id].len
for si in 0 .. n_succs {
s := cfg.succs[blk_id][si]
if s < 0 || s >= n_blocks {
continue
}
if !arr_contains(cfg.preds[s], blk_id) {
cfg.preds[s] << blk_id
}
}
}
}
// Also write back to m.blocks for compatibility with other passes
for bi in 0 .. n_blocks {
// Avoid m.blocks[X].succs/preds = ... -- chained field assign broken in ARM64 self-hosted
mut blk := m.blocks[bi]
blk.succs = cfg.succs[bi]
blk.preds = cfg.preds[bi]
m.blocks[bi] = blk
}
return cfg
}
@@ -0,0 +1,163 @@
// Copyright (c) 2026 Alexander Medvednikov. All rights reserved.
// Use of this source code is governed by an MIT license
// that can be found in the LICENSE file.
module optimize
import v2.ssa
fn dead_code_elimination(mut m ssa.Module) bool {
mut any_changed := false
mut changed := true
for changed {
changed = false
for fi in 0 .. m.funcs.len {
// Dead store elimination: find stores to local allocas that are never read
dead_stores := find_dead_stores(m, &m.funcs[fi])
for blk_id in m.funcs[fi].blocks {
// First pass: find dead instructions (don't modify yet)
mut dead_instrs := []int{}
for val_id in m.blocks[blk_id].instrs {
val := m.values[val_id]
if val.kind == .instruction {
instr := m.instrs[val.index]
// Check if this is a dead store
if instr.op == .store && val_id in dead_stores {
dead_instrs << val_id
continue
}
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 !side_effects && val.uses.len == 0 {
dead_instrs << val_id
}
}
}
// Only rebuild the instruction list if we found dead instructions
if dead_instrs.len > 0 {
// Remove uses from dead instructions
for val_id in dead_instrs {
instr := m.instrs[m.values[val_id].index]
for op_id in instr.operands {
remove_use(mut m, op_id, val_id)
}
}
// Build set for O(1) lookup
mut dead_set := map[int]bool{}
for d in dead_instrs {
dead_set[d] = true
}
// Filter out dead instructions
mut new_instrs := []int{cap: m.blocks[blk_id].instrs.len}
for val_id in m.blocks[blk_id].instrs {
if !dead_set[val_id] {
new_instrs << val_id
}
}
// Avoid m.blocks[X].instrs = ... -- chained field assign broken in ARM64 self-hosted
mut dce_blk := m.blocks[blk_id]
dce_blk.instrs = new_instrs
m.blocks[blk_id] = dce_blk
changed = true
any_changed = true
}
}
}
}
return any_changed
}
fn remove_use(mut m ssa.Module, val_id int, user_id int) {
if val_id >= m.values.len {
return
}
mut val := &m.values[val_id]
// Remove all occurrences (handles instructions that use same value multiple times)
// Iterate in reverse to safely delete while iterating
for i := val.uses.len - 1; i >= 0; i-- {
if val.uses[i] == user_id {
val.uses.delete(i)
}
}
}
// Find stores to local allocas that are never read (dead stores).
// A store is dead if:
// 1. It stores to a local alloca
// 2. The alloca is ONLY used by stores (no loads, GEPs, calls, or other uses)
//
// We must be conservative: if the alloca pointer escapes (used by GEP, passed to
// a function, etc.), we cannot eliminate stores because the value might be read
// through the escaped pointer.
fn find_dead_stores(m &ssa.Module, func &ssa.Function) map[int]bool {
mut dead_stores := map[int]bool{}
// Find all local allocas
mut allocas := map[int]bool{}
mut alloca_stores := map[int][]int{} // alloca_id -> list of store val_ids
mut alloca_escapes := map[int]bool{} // alloca_id -> true if pointer escapes
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
}
instr := m.instrs[val.index]
if instr.op == .alloca || instr.op == .heap_alloc {
allocas[val_id] = true
}
}
}
// Check how each alloca is used
for alloca_id, _ in allocas {
alloca_val := m.values[alloca_id]
for use_id in alloca_val.uses {
use_val := m.values[use_id]
if use_val.kind != .instruction {
// Non-instruction use - alloca escapes
alloca_escapes[alloca_id] = true
continue
}
use_instr := m.instrs[use_val.index]
match use_instr.op {
.store {
// Check if alloca is the destination (operand[1]), not the value being stored
if use_instr.operands.len > 1 && use_instr.operands[1] == alloca_id {
alloca_stores[alloca_id] << use_id
} else {
// Alloca is being stored somewhere - it escapes
alloca_escapes[alloca_id] = true
}
}
.load {
// Alloca is being read - stores are not dead
alloca_escapes[alloca_id] = true
}
else {
// Any other use (GEP, call, etc.) - alloca escapes
alloca_escapes[alloca_id] = true
}
}
}
}
// Mark stores to non-escaping allocas as dead
for alloca_id, store_ids in alloca_stores {
if !alloca_escapes[alloca_id] {
for i := 0; i < store_ids.len; i++ {
store_id := store_ids[i]
dead_stores[store_id] = true
}
}
}
return dead_stores
}
@@ -0,0 +1,374 @@
// Copyright (c) 2026 Alexander Medvednikov. All rights reserved.
// Use of this source code is governed by an MIT license
// that can be found in the LICENSE file.
module optimize
import v2.ssa
// --- Dominators (Lengauer-Tarjan) ---
struct LTContext {
mut:
parent []int // DFS tree parent
semi []int // Semidominator (BlockID)
vertex []int // Map DFS number -> BlockID
bucket [][]int // bucket[w] = set of vertices v s.t. semi[v] = w
dfnum []int // DFS number (0 means unvisited)
ancestor []int // DSU parent
label []int // DSU label (min semi in path)
idom []int // Immediate dominator (flat array, indexed by block ID)
dom_tree [][]int // Dom tree children (flat array, indexed by block ID)
n int // Counter
}
// DFS frame for iterative lt_dfs
struct DfsFrame {
mut:
node int
succ_idx int // next successor to process
}
// DomInfo holds dominator results in flat arrays to avoid struct field access issues
struct DomInfo {
mut:
idom []int // Immediate dominator, indexed by block ID
dom_tree [][]int // Dom tree children, indexed by block ID
}
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
}
// Validate that all block IDs and their successor/predecessor
// references are within bounds.
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
}
n_succs := cfg.succs[blk_id].len
for si in 0 .. n_succs {
s := cfg.succs[blk_id][si]
if s < 0 || s >= max_id {
valid = false
break
}
}
if !valid {
break
}
n_preds := cfg.preds[blk_id].len
for pi in 0 .. n_preds {
p := cfg.preds[blk_id][pi]
if p < 0 || p >= max_id {
valid = false
break
}
}
if !valid {
break
}
}
if !valid {
continue
}
// Reset context for this function (only reset what's needed)
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].clear()
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)
// Process in reverse DFS order (skip root)
for i := ctx.n; i >= 2; i-- {
if i >= ctx.vertex.len {
continue
}
w := ctx.vertex[i]
if w < 0 || w >= max_id {
continue
}
// 1. Calculate Semidominator - use cfg.preds instead of m.blocks[w].preds
n_w_preds := cfg.preds[w].len
for pi2 in 0 .. n_w_preds {
p := cfg.preds[w][pi2]
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]
}
}
// Add w to bucket of its semidominator
semi_w2 := ctx.semi[w]
if semi_w2 >= 0 && semi_w2 < max_id {
ctx.bucket[semi_w2] << w
}
// Link to parent in forest
ctx.link(ctx.parent[w], w)
// 2. Implicitly compute IDom
parent_w := ctx.parent[w]
if parent_w < 0 || parent_w >= max_id {
continue
}
// Drain bucket of parent
n_bucket := ctx.bucket[parent_w].len
for bvi in 0 .. n_bucket {
v := ctx.bucket[parent_w][bvi]
if v < 0 || v >= max_id {
continue
}
u := ctx.eval(v)
if u < 0 || u >= max_id || v >= max_id {
continue
}
if ctx.semi[u] == ctx.semi[v] {
ctx.idom[v] = parent_w
} else {
ctx.idom[v] = u // Deferred: idom[v] = idom[u]
}
}
ctx.bucket[parent_w].clear()
}
// 3. Explicitly compute IDom
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
// Build Dom Tree Children
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 {
ctx.dom_tree[idom] << blk_id
}
}
}
}
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{}
// Visit root
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
}
// Use cfg.succs flat array instead of m.blocks[node].succs
n_succs := cfg.succs[node].len
si := stack[top].succ_idx
if si < n_succs {
// Avoid stack[top].succ_idx++ -- chained field increment broken in ARM64 self-hosted
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,327 @@
// Copyright (c) 2026 Alexander Medvednikov. All rights reserved.
// Use of this source code is governed by an MIT license
// that can be found in the LICENSE file.
module optimize
import v2.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 {
// Iterate directly without cloning - we don't modify the array during iteration
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 {
lhs := m.values[instr.operands[0]]
rhs := m.values[instr.operands[1]]
// Skip undef values - can't fold with undefined
if lhs.kind == .constant && lhs.name == 'undef' {
continue
}
if rhs.kind == .constant && rhs.name == 'undef' {
continue
}
// Try algebraic simplifications first (even with non-constants)
// vfmt off
repl, needs_zero := try_algebraic_simplify(m, val_id, instr, lhs, rhs)
// vfmt on
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')
// Avoid m.instrs[X].field = ... -- chained field assign broken in ARM64 self-hosted
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 or x & 0 or x - x or x ^ x - reuse zero constant
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
}
// Constant folding - both operands must be constants
if lhs.kind == .constant && rhs.kind == .constant {
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
}
}
.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 {
// Arithmetic shift right preserves sign
result = l_int >> u64(r_int)
folded = true
}
}
.lshr {
if r_int >= 0 && r_int < 64 {
// Logical shift right treats as unsigned
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.str())
m.replace_uses(val_id, const_val)
changed = true
}
}
}
}
}
}
return changed
}
// Branch folding: simplify conditional branches with constant conditions
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[m.blocks[blk_id].instrs.len - 1]
term := m.instrs[m.values[term_val_id].index]
if term.op == .br {
// br cond, true_blk, false_blk
cond_val := m.values[term.operands[0]]
if cond_val.kind == .constant && cond_val.name != 'undef' {
cond_int := cond_val.name.i64()
// Replace with unconditional jump to the taken branch
target := if cond_int != 0 { term.operands[1] } else { term.operands[2] }
// Avoid m.instrs[X].field = ... -- chained field assign broken in ARM64 self-hosted
mut jmp_instr := m.instrs[m.values[term_val_id].index]
jmp_instr.op = .jmp
jmp_instr.operands = [target]
m.instrs[m.values[term_val_id].index] = jmp_instr
changed = true
}
}
}
}
return changed
}
// Algebraic simplifications: x+0=x, x*1=x, x*0=0, x-x=0, x^x=0, x&x=x, x|x=x, x*2=x<<1, etc.
// Returns (replacement_id, needs_zero) - if needs_zero is true, caller should create zero constant
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]
// Check for same-operand simplifications (x op x)
if lhs_id == rhs_id {
match instr.op {
.sub {
// x - x = 0
return val_id, true
}
.xor {
// x ^ x = 0
return val_id, true
}
.and_ {
// x & x = x
return lhs_id, false
}
.or_ {
// x | x = x
return lhs_id, false
}
else {}
}
}
// Check if either operand is a constant
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 {
// x + 0 = 0 + x = x
if const_val == 0 {
return other_id, false
}
}
.sub {
// x - 0 = x
if const_is_rhs && const_val == 0 {
return other_id, false
}
}
.mul {
// x * 0 = 0 * x = 0
if const_val == 0 {
return val_id, true // Signal caller to create zero
}
// x * 1 = 1 * x = x
if const_val == 1 {
return other_id, false
}
// x * 2 = x << 1, 2 * x = x << 1
if const_val == 2 {
return -2, false // Signal caller to create shift
}
}
.sdiv {
// x / 1 = x
if const_is_rhs && const_val == 1 {
return other_id, false
}
}
.and_ {
// x & 0 = 0 & x = 0
if const_val == 0 {
return val_id, true // Signal caller to create zero
}
}
.or_ {
// x | 0 = 0 | x = x
if const_val == 0 {
return other_id, false
}
}
.xor {
// x ^ 0 = 0 ^ x = x
if const_val == 0 {
return other_id, false
}
}
.shl, .ashr, .lshr {
// x << 0 = x >> 0 = x
if const_is_rhs && const_val == 0 {
return other_id, false
}
}
else {}
}
return -1, false
}
@@ -0,0 +1,616 @@
// Copyright (c) 2026 Alexander Medvednikov. All rights reserved.
// Use of this source code is governed by an MIT license
// that can be found in the LICENSE file.
module optimize
import v2.ssa
// --- Mem2Reg (Promote Allocas) ---
// All data structures use flat arrays indexed by value_id or block_id
// to avoid map operations which are extremely slow in the ARM64 backend.
struct Mem2RegCtx {
mut:
// Indexed by alloc_id (value_id): list of block_ids where this alloc is stored to
defs [][]int
// Indexed by alloc_id (value_id): list of block_ids where this alloc is loaded from
uses [][]int
// Indexed by block_id: list of alloc_ids that need phi nodes in this block
phi_placements [][]int
// Indexed by block_id: parallel to phi_placements, stores the phi value_ids
// phi_vals[blk_id][i] is the phi val_id for phi_placements[blk_id][i]
phi_vals [][]int
// Indexed by alloc_id (value_id): stack of current SSA values during renaming
stacks [][]int
// Track which alloc_ids have been initialized (for cleanup)
is_promotable []bool
// Track which block_ids have phi placements (for cleanup and iteration)
phi_blocks []int
// Deferred phi operands: flat list of [instr_idx, val, bvid, instr_idx, val, bvid, ...].
// Applied after rename to avoid m.instrs[idx].operands << x which is broken
// in ARM64 self-hosted binaries (chained array-element-field-append bug).
deferred_phi_ops []int
}
// Helper: append to an inner array of a [][]int with proper write-back.
// Direct `arr[idx] << val` is broken in ARM64 self-hosted binaries
// (chained array-element append copies the struct instead of modifying in-place).
fn array2d_append(mut arr [][]int, idx int, val int) {
mut inner := arr[idx]
inner << val
arr[idx] = inner
}
fn promote_memory_to_register(mut m ssa.Module, dom DomInfo, cfg &CfgData) {
n_values := m.values.len
n_blocks := m.blocks.len
// Pre-allocate flat arrays ONCE outside the function loop.
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{}
}
// Pre-allocate stack_counts array (for rename_recursive).
mut stack_counts := []int{len: n_values}
for sci in 0 .. n_values {
stack_counts[sci] = -1
}
// Dominance frontier: indexed by block_id
mut df := [][]int{len: n_blocks}
// Track which blocks have DF entries for cleanup
mut df_blocks := []int{}
// Per-alloc visited/has_phi bitsets (reused across allocs within a function)
mut visited := []bool{len: n_blocks}
mut has_phi := []bool{len: n_blocks}
mut total_allocas := 0
mut total_promoted := 0
mut total_not_promoted := 0
mut total_blocks_visited := 0
mut total_blocks_skipped := 0
mut total_blocks_in_funcs := 0
for fi in 0 .. m.funcs.len {
// 1. Analyze Allocas
mut promotable := []int{}
func := m.funcs[fi]
n_func_blocks := func.blocks.len
for fbi in 0 .. n_func_blocks {
blk_id := func.blocks[fbi]
blk := m.blocks[blk_id]
n_blk_instrs := blk.instrs.len
for ii in 0 .. n_blk_instrs {
val_id := blk.instrs[ii]
val := m.values[val_id]
instr := m.instrs[val.index]
if instr.op == .alloca {
total_allocas += 1
if is_promotable(m, val_id) {
promotable << val_id
ctx.is_promotable[val_id] = true
total_promoted += 1
} else {
total_not_promoted += 1
}
}
if instr.op == .store {
ptr := instr.operands[1]
// Avoid duplicate def entries for same block
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]
// Avoid duplicate use entries for same block
if ptr < n_values && blk_id !in ctx.uses[ptr] {
array2d_append(mut ctx.uses, ptr, blk_id)
}
}
}
}
// 2. Compute Dominance Frontier (flat array version)
compute_dominance_frontier_flat(m, fi, &dom, cfg, mut df, mut df_blocks)
// 3. Insert Phis (Dominance Frontier)
n_promotable := promotable.len
for pri in 0 .. n_promotable {
alloc_id := promotable[pri]
mut worklist := ctx.defs[alloc_id].clone()
// Track blocks we mark visited/has_phi for cleanup
mut visited_list := []int{}
mut has_phi_list := []int{}
for worklist.len > 0 {
b := worklist.pop()
if b < 0 || b >= n_blocks {
continue
}
n_df_b := df[b].len
for di in 0 .. n_df_b {
d := df[b][di]
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
}
}
}
}
// Reset visited/has_phi for this alloc
for vli in 0 .. visited_list.len {
visited[visited_list[vli]] = false
}
for hli in 0 .. has_phi_list.len {
has_phi[has_phi_list[hli]] = false
}
}
// Insert Phis
n_phi_blocks := ctx.phi_blocks.len
for pbi in 0 .. n_phi_blocks {
blk_id := ctx.phi_blocks[pbi]
n_phi_allocs := ctx.phi_placements[blk_id].len
for pai in 0 .. n_phi_allocs {
alloc_id := ctx.phi_placements[blk_id][pai]
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, [])
// Avoid m.values[phi_val].name = ... -- chained field assign broken in ARM64 self-hosted
mut pv := m.values[phi_val]
pv.name = '${alloc_val.name}.phi_${blk_id}'
m.values[phi_val] = pv
// Store phi_val_id in parallel array for direct lookup (no string matching)
array2d_append(mut ctx.phi_vals, blk_id, phi_val)
}
}
// 4. Rename Variables
func2 := m.funcs[fi]
if func2.blocks.len > 0 {
entry := func2.blocks[0]
total_blocks_in_funcs += func2.blocks.len
bv, bs :=
rename_recursive(mut m, entry, mut ctx, promotable, mut stack_counts, &dom, cfg)
total_blocks_visited += bv
total_blocks_skipped += bs
}
// 4b. Apply deferred phi operands.
// Builds complete operand lists for each phi and assigns them all at once.
// This avoids m.instrs[idx].operands << x which is broken in v3-compiled binaries
// (chained array-element-field-append copies the struct instead of modifying in-place).
if ctx.deferred_phi_ops.len > 0 {
n_deferred := ctx.deferred_phi_ops.len
n_phi_blocks3 := ctx.phi_blocks.len
mut n_applied := 0
mut n_verified := 0
mut n_mismatch := 0
for pbi3 in 0 .. n_phi_blocks3 {
pb_id := ctx.phi_blocks[pbi3]
n_phi_allocs3 := ctx.phi_vals[pb_id].len
for pai3 in 0 .. n_phi_allocs3 {
phi_vid := ctx.phi_vals[pb_id][pai3]
phi_v := m.values[phi_vid]
phi_instr_idx := phi_v.index
// Collect all operands for this phi from deferred list
mut ops := []int{}
mut dj := 0
for dj + 2 < n_deferred {
if ctx.deferred_phi_ops[dj] == phi_instr_idx {
ops << ctx.deferred_phi_ops[dj + 1]
ops << ctx.deferred_phi_ops[dj + 2]
}
dj += 3
}
if ops.len > 0 {
// Avoid m.instrs[X].operands = ops -- chained field assign broken in ARM64 self-hosted
mut phi_instr := m.instrs[phi_instr_idx]
phi_instr.operands = ops
m.instrs[phi_instr_idx] = phi_instr
n_applied += 1
// Verify the assignment was actually written
actual_len := m.instrs[phi_instr_idx].operands.len
if actual_len != ops.len {
n_mismatch += 1
if n_mismatch <= 5 {
eprintln(' DEFERRED MISMATCH: phi_vid=${phi_vid} instr=${phi_instr_idx} wanted=${ops.len} got=${actual_len}')
}
} else {
n_verified += 1
// Also verify first operand value
if ops.len >= 2 {
actual0 := m.instrs[phi_instr_idx].operands[0]
actual1 := m.instrs[phi_instr_idx].operands[1]
if actual0 != ops[0] || actual1 != ops[1] {
n_mismatch += 1
if n_mismatch <= 5 {
eprintln(' DEFERRED VALUE MISMATCH: phi_vid=${phi_vid} instr=${phi_instr_idx} wanted[0]=${ops[0]},${ops[1]} got=${actual0},${actual1}')
}
}
}
}
}
}
}
if n_mismatch > 0 {
eprintln(' DEFERRED phi ops: applied=${n_applied} verified=${n_verified} MISMATCH=${n_mismatch}')
}
ctx.deferred_phi_ops = []
}
// Update uses for phi operands (deferred from step 3)
n_phi_blocks4 := ctx.phi_blocks.len
for pbi4 in 0 .. n_phi_blocks4 {
pb_id := ctx.phi_blocks[pbi4]
n_phi_allocs4 := ctx.phi_vals[pb_id].len
for pai4 in 0 .. n_phi_allocs4 {
phi_vid := ctx.phi_vals[pb_id][pai4]
phi_v := m.values[phi_vid]
phi_instr_idx := phi_v.index
instr := m.instrs[phi_instr_idx]
for oi := 0; oi < instr.operands.len; oi += 2 {
val_op := instr.operands[oi]
if val_op < m.values.len && phi_vid !in m.values[val_op].uses {
// Read whole struct, modify, write back (chained broken in ARM64)
mut vop := m.values[val_op]
vop.uses << phi_vid
m.values[val_op] = vop
}
}
}
}
// 5. Cleanup for next function: reset only entries we touched
for cli in 0 .. promotable.len {
alloc_id := promotable[cli]
ctx.defs[alloc_id] = []
ctx.uses[alloc_id] = []
ctx.stacks[alloc_id] = []
ctx.is_promotable[alloc_id] = false
}
n_phi_blocks2 := ctx.phi_blocks.len
for cli2 in 0 .. n_phi_blocks2 {
ctx.phi_placements[ctx.phi_blocks[cli2]] = []
ctx.phi_vals[ctx.phi_blocks[cli2]] = []
}
ctx.phi_blocks = []
n_df_blocks := df_blocks.len
for cli3 in 0 .. n_df_blocks {
df[df_blocks[cli3]] = []
}
df_blocks = []
}
eprintln(' mem2reg stats: allocas=${total_allocas} promoted=${total_promoted} not_promoted=${total_not_promoted} blocks_total=${total_blocks_in_funcs} blocks_visited=${total_blocks_visited} blocks_skipped=${total_blocks_skipped}')
}
fn is_promotable(m &ssa.Module, alloc_id int) bool {
// Keep array-backed slots in memory. Promoting pointer-to-array allocas can
// lose correct addressing semantics for fixed-array literals/indexing.
if alloc_id > 0 && alloc_id < m.values.len {
alloc_val := m.values[alloc_id]
alloc_typ_id := alloc_val.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]
if elem_typ.kind == .ptr_t {
return false
}
if elem_typ.kind == .array_t {
return false
}
}
}
}
}
alloc_val2 := m.values[alloc_id]
uses := alloc_val2.uses
n_uses := uses.len
for ui in 0 .. n_uses {
u := uses[ui]
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 {
// Only safe if used as pointer (index 1)
if instr.operands.len < 2 || instr.operands[1] != alloc_id {
return false
}
}
else {
// Escape (GEP, Call, Phi, etc.)
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]
n_func_blocks := func.blocks.len
for bi in 0 .. n_func_blocks {
blk_id := func.blocks[bi]
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]
// Safety check: idom != -1
for runner != -1 && runner != idom {
if runner < 0 || runner >= m.blocks.len {
break
}
// Avoid duplicate entries in dominance frontier
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]
}
}
}
}
}
// RenameFrame holds the state for one level of the iterative dom tree walk.
struct RenameFrame {
mut:
blk_id int
child_idx int // next child to process in dom_tree
pushed_allocs []int // alloc_ids pushed in this block (for cleanup)
processed bool // whether steps 1-3 have been run for this block
}
fn rename_iterative(mut m ssa.Module, root_blk int, mut ctx Mem2RegCtx, _promotable []int, mut _stack_counts []int, dom &DomInfo, cfg &CfgData) (int, int) {
// Pre-build block_val_ids[] by scanning values for basic_block kind.
// This avoids m.blocks[blk_id].val_id which produces wrong results
// in ARM64-compiled binaries (large struct field access bug).
n_blks := m.blocks.len
mut block_val_ids := []int{len: n_blks}
n_vals := m.values.len
mut n_bb_found := 0
mut n_bb_valid := 0
for vi in 0 .. n_vals {
if m.values[vi].kind == .basic_block {
bid := m.values[vi].index
n_bb_found += 1
if bid >= 0 && bid < n_blks {
block_val_ids[bid] = vi
n_bb_valid += 1
}
}
}
// Verify: also check using m.blocks[bid].val_id approach and compare
mut n_match := 0
mut n_mismatch := 0
for bid in 0 .. n_blks {
bval := m.blocks[bid].val_id
if block_val_ids[bid] != bval {
n_mismatch += 1
if n_mismatch <= 3 {
eprintln(' mem2reg block_val_ids MISMATCH: bid=${bid} scan=${block_val_ids[bid]} blocks=${bval}')
}
} else {
n_match += 1
}
}
if n_mismatch > 0 {
eprintln(' mem2reg block_val_ids: bb_found=${n_bb_found} valid=${n_bb_valid} match=${n_match} mismatch=${n_mismatch}')
}
mut work := []RenameFrame{}
work << RenameFrame{
blk_id: root_blk
}
mut visited := []bool{len: m.blocks.len}
mut blocks_visited := 0
mut blocks_skipped := 0
for work.len > 0 {
fi := work.len - 1
blk_id := work[fi].blk_id
if !work[fi].processed {
// Cycle detection: skip blocks already visited in this traversal
if blk_id >= 0 && blk_id < visited.len && visited[blk_id] {
blocks_skipped += 1
work.pop()
continue
}
if blk_id >= 0 && blk_id < visited.len {
visited[blk_id] = true
}
blocks_visited += 1
// Steps 1-3: process this block (runs once per block)
// Avoid work[fi].processed = true — chained field assignment broken in ARM64 self-hosted.
mut frame2 := work[fi]
frame2.processed = true
work[fi] = frame2
// 1. Push Phis to stack (use phi_vals[] for direct lookup, no string matching)
if blk_id < ctx.phi_placements.len && ctx.phi_placements[blk_id].len > 0 {
n_phi_allocs := ctx.phi_placements[blk_id].len
for pai in 0 .. n_phi_allocs {
alloc_id := ctx.phi_placements[blk_id][pai]
// Direct lookup: phi_vals[blk_id][pai] is the phi for this alloc
if blk_id < ctx.phi_vals.len && pai < ctx.phi_vals[blk_id].len {
phi_val_id := ctx.phi_vals[blk_id][pai]
// Avoid ctx.stacks[X] << Y — chained append broken in ARM64 self-hosted.
mut new_stack := ctx.stacks[alloc_id].clone()
new_stack << phi_val_id
ctx.stacks[alloc_id] = new_stack
// Avoid work[fi].pushed_allocs = X — chained field assignment broken in ARM64.
mut wf := work[fi]
wf.pushed_allocs << alloc_id
work[fi] = wf
}
}
}
// 2. Process Instructions
mut instrs_to_nop := []int{}
blk2 := m.blocks[blk_id]
n_instrs2 := blk2.instrs.len
for ii in 0 .. n_instrs2 {
val_id := blk2.instrs[ii]
val := m.values[val_id]
instr := m.instrs[val.index]
if instr.op == .store {
ptr := instr.operands[1]
if ptr < ctx.is_promotable.len && ctx.is_promotable[ptr] {
// Avoid ctx.stacks[X] << Y — chained append broken in ARM64 self-hosted.
mut new_stack := ctx.stacks[ptr].clone()
new_stack << instr.operands[0]
ctx.stacks[ptr] = new_stack
// Avoid work[fi].pushed_allocs = X — chained field assignment broken in ARM64.
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 {
// Undef - reading uninitialized memory
res_type := val.typ
repl = m.get_or_add_const(res_type, '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 nopi in 0 .. instrs_to_nop.len {
vid := instrs_to_nop[nopi]
vid_val := m.values[vid]
// Avoid m.instrs[X].op/operands = ... -- chained field assign broken in ARM64 self-hosted
mut nop_instr := m.instrs[vid_val.index]
nop_instr.op = .bitcast
nop_instr.operands = []
m.instrs[vid_val.index] = nop_instr
}
// 3. Update Successor Phi Operands
n_succs := cfg.succs[blk_id].len
for si in 0 .. n_succs {
succ_id := cfg.succs[blk_id][si]
if succ_id < ctx.phi_placements.len && ctx.phi_placements[succ_id].len > 0 {
n_succ_phi_allocs := ctx.phi_placements[succ_id].len
for spai in 0 .. n_succ_phi_allocs {
alloc_id := ctx.phi_placements[succ_id][spai]
// Direct lookup: phi_vals[succ_id][spai] is the phi for this alloc
if succ_id < ctx.phi_vals.len && spai < ctx.phi_vals[succ_id].len {
vid := ctx.phi_vals[succ_id][spai]
phi_v := m.values[vid]
mut phi_val := if ctx.stacks[alloc_id].len > 0 {
ctx.stacks[alloc_id].last()
} else {
// Undef - reading uninitialized memory
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')
}
bvid := block_val_ids[blk_id]
// Defer phi operand append to avoid m.instrs[idx].operands << x
// which is broken in ARM64 self-hosted binaries.
ctx.deferred_phi_ops << phi_v.index
ctx.deferred_phi_ops << phi_val
ctx.deferred_phi_ops << bvid
// FIX: Update uses so DCE doesn't remove the value
// Avoid m.values[X].uses << Y — chained append broken in ARM64 self-hosted.
if phi_val < m.values.len {
if vid !in m.values[phi_val].uses {
// Read whole struct, modify, write back (chained broken in ARM64)
mut vv := m.values[phi_val]
vv.uses << vid
m.values[phi_val] = vv
}
}
}
}
}
}
}
// 4. Push next unvisited dom child
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]
// Avoid work[fi].child_idx++ — chained field increment broken in ARM64 self-hosted.
mut frame := work[fi]
frame.child_idx++
work[fi] = frame
work << RenameFrame{
blk_id: child
}
} else {
// 5. All children processed - pop stacks (cleanup)
pushed := work[fi].pushed_allocs.clone()
work.pop()
for i := pushed.len - 1; i >= 0; i-- {
// Avoid ctx.stacks[X].pop() — chained method broken in ARM64 self-hosted.
mut s := ctx.stacks[pushed[i]].clone()
s.pop()
ctx.stacks[pushed[i]] = s
}
}
}
return blocks_visited, blocks_skipped
}
fn rename_recursive(mut m ssa.Module, blk_id int, mut ctx Mem2RegCtx, promotable []int, mut stack_counts []int, dom &DomInfo, cfg &CfgData) (int, int) {
return rename_iterative(mut m, blk_id, mut ctx, promotable, mut stack_counts, dom, cfg)
}
@@ -0,0 +1,147 @@
// Copyright (c) 2026 Alexander Medvednikov. All rights reserved.
// Use of this source code is governed by an MIT license
// that can be found in the LICENSE file.
module optimize
import os
import v2.ssa
import time
pub struct OptimizeOptions {
pub:
verify_each_pass bool
strict_verify bool
}
// Optimize Module
pub fn optimize(mut m ssa.Module) {
strict_verify := os.getenv('V2_VERIFY_STRICT') != ''
optimize_with_options(mut m, OptimizeOptions{
verify_each_pass: os.getenv('V2_VERIFY') != ''
strict_verify: strict_verify
})
}
pub fn optimize_with_options(mut m ssa.Module, opts OptimizeOptions) {
mut t0 := time.ticks()
verify_pipeline_checkpoint(m, opts, 'input')
rebuild_use_lists(mut m)
// 1. Build Control Flow Graph (Predecessors)
cfg := build_cfg(mut m)
verify_pipeline_checkpoint(m, opts, 'build_cfg')
mut t1 := time.ticks()
eprintln(' opt: build_cfg: ${t1 - t0}ms')
t0 = t1
// 2. Compute Dominator Tree (Lengauer-Tarjan)
dom := compute_dominators(mut m, &cfg)
verify_pipeline_checkpoint(m, opts, 'compute_dominators')
t1 = time.ticks()
// Count dom tree stats
mut n_with_idom := 0
mut n_in_dom_tree := 0
mut n_no_idom := 0
mut n_idom_self := 0
mut n_idom_diff := 0
for bi in 0 .. dom.idom.len {
if dom.idom[bi] >= 0 {
n_with_idom += 1
if dom.idom[bi] == bi {
n_idom_self += 1
} else {
n_idom_diff += 1
}
} else {
n_no_idom += 1
}
if bi < dom.dom_tree.len {
n_in_dom_tree += dom.dom_tree[bi].len
}
}
eprintln(' opt: compute_dominators: ${t1 - t0}ms (with_idom=${n_with_idom} no_idom=${n_no_idom} self=${n_idom_self} diff=${n_idom_diff} dom_tree=${n_in_dom_tree})')
t0 = t1
// 3. Promote Memory to Register (Construct SSA / Phi Nodes)
promote_memory_to_register(mut m, dom, &cfg)
verify_pipeline_checkpoint(m, opts, 'promote_memory_to_register')
rebuild_use_lists(mut m)
t1 = time.ticks()
// Count remaining allocas
mut n_alloca := 0
for vi in 0 .. m.values.len {
val := m.values[vi]
if val.kind == .instruction {
idx := val.index
if idx >= 0 && idx < m.instrs.len {
if m.instrs[idx].op == .alloca {
n_alloca += 1
}
}
}
}
eprintln(' opt: mem2reg: ${t1 - t0}ms (remaining allocas: ${n_alloca})')
t0 = t1
// 4. Simplify trivial Phi Nodes
simplify_phi_nodes(mut m)
verify_pipeline_checkpoint(m, opts, 'simplify_phi_nodes')
rebuild_use_lists(mut m)
t1 = time.ticks()
eprintln(' opt: simplify_phi: ${t1 - t0}ms')
t0 = t1
// 5. Eliminate Phi Nodes (convert to copies in predecessor blocks)
eliminate_phi_nodes(mut m)
verify_pipeline_checkpoint(m, opts, 'eliminate_phi_nodes')
rebuild_use_lists(mut m)
t1 = time.ticks()
eprintln(' opt: eliminate_phi: ${t1 - t0}ms')
t0 = t1
// 6. Remove dead instructions after structural SSA rewrites are complete.
dead_code_elimination(mut m)
verify_pipeline_checkpoint(m, opts, 'dead_code_elimination')
t1 = time.ticks()
eprintln(' opt: dce: ${t1 - t0}ms')
}
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
}
for op_id in m.instrs[instr_idx].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
}
}
}
}
}
}
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
})
}
}
@@ -0,0 +1,661 @@
// Copyright (c) 2026 Alexander Medvednikov. All rights reserved.
// Use of this source code is governed by an MIT license
// that can be found in the LICENSE file.
module optimize
import v2.ssa
// --- Simplify Phi Nodes ---
// Remove trivial phi nodes where all operands are the same or self-referential.
// A phi is trivial if all its non-self operands resolve to the same value.
// This reduces unnecessary instructions before phi elimination.
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]
n_func_blocks := func.blocks.len
for fbi in 0 .. n_func_blocks {
blk_id := func.blocks[fbi]
blk := m.blocks[blk_id]
n_blk_instrs := blk.instrs.len
for ii in 0 .. n_blk_instrs {
val_id := blk.instrs[ii]
val := m.values[val_id]
if val.kind != .instruction {
continue
}
instr := m.instrs[val.index]
if instr.op != .phi {
continue
}
// Dead phi removal: phi with no uses can be removed
if val.uses.len == 0 {
// Remove uses from this phi's operands
for i := 0; i < instr.operands.len; i += 2 {
op_val := instr.operands[i]
if op_val != val_id { // Don't try to remove self-reference
remove_phi_use(mut m, op_val, val_id)
}
}
// Mark phi as dead
// Avoid m.instrs[X].field = ... -- chained field assign broken in ARM64 self-hosted
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
}
// Check if phi is trivial (all non-self operands are the same)
mut unique_val := -1
mut is_trivial := true
for i := 0; i < instr.operands.len; i += 2 {
op_val := instr.operands[i]
// Skip self-references (phi refers to itself)
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 trivial and we found a unique value, replace all uses
if is_trivial && unique_val != -1 {
// Replace all uses of this phi with the unique value
m.replace_uses(val_id, unique_val)
// Mark phi as dead (will be cleaned up by DCE or ignored)
// Avoid m.instrs[X].field = ... -- chained field assign broken in ARM64 self-hosted
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
}
// Helper to remove a use from a value's uses list (for dead phi removal)
fn remove_phi_use(mut m ssa.Module, val_id int, user_id int) {
if 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)
}
}
}
// get_block_val_id returns the value ID for a block, using a pre-built lookup table.
// This avoids m.blocks[bid].val_id which can return wrong values for the large
// BasicBlock struct (168 bytes) in ARM64-compiled binaries.
fn get_block_val_id(_m &ssa.Module, bid int, block_val_ids []int) int {
if bid >= 0 && bid < block_val_ids.len {
return block_val_ids[bid]
}
return 0
}
// build_block_val_ids scans values for basic_block kind and builds block_id → val_id mapping.
fn build_block_val_ids(m &ssa.Module) []int {
n_blks := m.blocks.len
mut block_val_ids := []int{len: n_blks}
n_vals := m.values.len
for vi in 0 .. n_vals {
if m.values[vi].kind == .basic_block {
bid := m.values[vi].index
if bid >= 0 && bid < n_blks {
block_val_ids[bid] = vi
}
}
}
return block_val_ids
}
// --- Critical Edge Splitting ---
fn split_critical_edges(mut m ssa.Module) {
mut cfg := build_cfg(mut m)
for fi in 0 .. m.funcs.len {
mut new_blocks := []ssa.BlockID{}
// Collect edges to split (can't modify while iterating)
mut edges_to_split := [][]ssa.BlockID{} // [pred_id, succ_id]
// Find all critical edges
func := m.funcs[fi]
func_blocks_len := func.blocks.len
for bi in 0 .. func_blocks_len {
blk_id := func.blocks[bi]
if blk_id < 0 || blk_id >= m.blocks.len {
continue
}
n_blk_succs := cfg.succs[blk_id].len
if n_blk_succs > 1 {
for si in 0 .. n_blk_succs {
succ_id := cfg.succs[blk_id][si]
if succ_id < 0 || succ_id >= m.blocks.len {
continue
}
if cfg.preds[succ_id].len > 1 {
edges_to_split << [blk_id, succ_id]
}
}
}
}
// Split each critical edge
n_edges := edges_to_split.len
for ei in 0 .. n_edges {
pred_id := edges_to_split[ei][0]
succ_id := edges_to_split[ei][1]
if pred_id < 0 || pred_id >= m.blocks.len || succ_id < 0 || succ_id >= m.blocks.len {
continue
}
// Create new intermediate block
func2 := m.funcs[fi]
split_blk := m.add_block(func2.id, 'split_${pred_id}_${succ_id}')
new_blocks << split_blk
// Re-build block_val_ids after adding the split block
// (new blocks get new val_ids that need to be in the table)
block_val_ids := build_block_val_ids(m)
succ_val := get_block_val_id(m, succ_id, block_val_ids)
// Add unconditional jump from split block to original successor
m.add_instr(.jmp, split_blk, 0, [succ_val])
// Update predecessor's terminator to jump to split block instead of successor
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 {
term_val := m.values[term_val_id]
mut term := &m.instrs[term_val.index]
old_succ_val := succ_val
new_succ_val := get_block_val_id(m, split_blk, block_val_ids)
// Replace ALL occurrences (handles switch with duplicate targets)
for i in 0 .. term.operands.len {
if term.operands[i] == old_succ_val {
term.operands[i] = new_succ_val
}
}
}
}
// Update phi nodes in successor to reference split block instead of pred
succ_blk2 := m.blocks[succ_id]
n_succ_instrs := succ_blk2.instrs.len
for vi in 0 .. n_succ_instrs {
val_id := succ_blk2.instrs[vi]
if val_id < 0 || val_id >= m.values.len {
continue
}
val := m.values[val_id]
if val.kind != .instruction {
continue
}
idx := val.index
if idx < 0 || idx >= m.instrs.len {
continue
}
mut instr := &m.instrs[idx]
if instr.op == .phi {
old_pred_val := get_block_val_id(m, pred_id, block_val_ids)
new_pred_val := get_block_val_id(m, split_blk, block_val_ids)
// Replace all occurrences (defensive - handles edge cases)
for i := 1; i < instr.operands.len; i += 2 {
if instr.operands[i] == old_pred_val {
instr.operands[i] = new_pred_val
}
}
}
}
}
}
// Rebuild CFG after splitting
cfg = build_cfg(mut m)
_ = cfg
}
fn eliminate_phi_nodes(mut m ssa.Module) {
// First split critical edges to ensure correct copy placement
split_critical_edges(mut m)
n_blocks := m.blocks.len
n_funcs := m.funcs.len
// Build reverse map: val_id → block_id for block-kind values.
// Scan values array for basic_block kind instead of using m.blocks[bid].val_id
// which returns wrong results in ARM64-compiled binaries (large struct field access bug).
mut val_to_block := []int{len: m.values.len, init: -1}
n_vals := m.values.len
for vi in 0 .. n_vals {
if m.values[vi].kind == .basic_block {
bid := m.values[vi].index
if bid >= 0 && bid < n_blocks {
val_to_block[vi] = bid
}
}
}
// Use flat parallel arrays indexed by block_id instead of [][]ParallelCopy.
// pred_copy_dests[blk_id] and pred_copy_srcs[blk_id] store the dest/src pairs.
mut pred_copy_dests := [][]int{len: n_blocks}
mut pred_copy_srcs := [][]int{len: n_blocks}
mut pred_copy_blocks := []int{}
// Pre-allocate shared src_ref_count array for resolve_parallel_copies_flat.
// Sized to m.values.len + headroom for temps. Reused across all calls.
mut src_ref_count := []int{len: m.values.len + 1024}
mut touched_ids := []int{cap: 256}
for fi in 0 .. n_funcs {
func := m.funcs[fi]
func_blocks_len2 := func.blocks.len
for bi2 in 0 .. func_blocks_len2 {
blk_id := func.blocks[bi2]
if blk_id < 0 || blk_id >= m.blocks.len {
continue
}
blk := m.blocks[blk_id]
n_instrs := blk.instrs.len
for ii in 0 .. n_instrs {
val_id := blk.instrs[ii]
if val_id < 0 || val_id >= m.values.len {
continue
}
val := m.values[val_id]
if val.kind != .instruction {
continue
}
idx := val.index
if idx < 0 || idx >= m.instrs.len {
continue
}
instr := m.instrs[idx]
if instr.op == .phi {
is_do_loop := func.name == 'do_loop'
if is_do_loop {
eprintln(' do_loop phi: val=${val_id} blk=${blk_id} nops=${instr.operands.len}')
}
// Phi operands: [val0, blk0, val1, blk1, ...]
for i := 0; i < instr.operands.len; i += 2 {
if i + 1 >= instr.operands.len {
break
}
val_in := instr.operands[i]
blk_val := instr.operands[i + 1]
if blk_val < 0 || blk_val >= m.values.len {
if is_do_loop {
eprintln(' op[${i}]: val_in=${val_in} blk_val=${blk_val} SKIP(oob)')
}
continue
}
// Use val_to_block reverse map instead of blk_v.index
pred_blk_idx := val_to_block[blk_val]
if is_do_loop {
blk_kind := m.values[blk_val].kind
blk_index := m.values[blk_val].index
eprintln(' op[${i}]: val_in=${val_in} blk_val=${blk_val} kind=${blk_kind} index=${blk_index} pred_blk_idx=${pred_blk_idx}')
}
if pred_blk_idx >= 0 && pred_blk_idx < n_blocks {
if pred_copy_dests[pred_blk_idx].len == 0 {
pred_copy_blocks << pred_blk_idx
}
mut pcd := pred_copy_dests[pred_blk_idx]
pcd << val_id
pred_copy_dests[pred_blk_idx] = pcd
mut pcs := pred_copy_srcs[pred_blk_idx]
pcs << val_in
pred_copy_srcs[pred_blk_idx] = pcs
}
}
}
}
}
// For each predecessor that has copies, resolve with Briggs algorithm
for pki in 0 .. pred_copy_blocks.len {
pred_blk := pred_copy_blocks[pki]
resolve_parallel_copies_flat(mut m, pred_blk, pred_copy_dests[pred_blk],
pred_copy_srcs[pred_blk], mut src_ref_count, mut touched_ids)
}
// Cleanup: clear pred_copies for blocks we touched
for pki2 in 0 .. pred_copy_blocks.len {
pred_copy_dests[pred_copy_blocks[pki2]] = []
pred_copy_srcs[pred_copy_blocks[pki2]] = []
}
pred_copy_blocks.clear()
// Remove phi instructions (mark as nop/bitcast with no operands)
func2 := m.funcs[fi]
func_blocks_len3 := func2.blocks.len
for bi3 in 0 .. func_blocks_len3 {
blk_id := func2.blocks[bi3]
if blk_id < 0 || blk_id >= m.blocks.len {
continue
}
blk := m.blocks[blk_id]
n_instrs3 := blk.instrs.len
for ii3 in 0 .. n_instrs3 {
val_id := blk.instrs[ii3]
if val_id < 0 || val_id >= m.values.len {
continue
}
val := m.values[val_id]
if val.kind != .instruction {
continue
}
idx := val.index
if idx < 0 || idx >= m.instrs.len {
continue
}
if m.instrs[idx].op == .phi {
// Avoid m.instrs[X].field = ... -- chained field assign broken in ARM64 self-hosted
mut cleanup_instr := m.instrs[idx]
cleanup_instr.op = .bitcast
cleanup_instr.operands = []
m.instrs[idx] = cleanup_instr
}
}
}
}
}
// resolve_parallel_copies_flat sequences a set of parallel copies (dest[i] = src[i])
// using the Briggs algorithm with worklist-based scheduling.
// A copy dest←src is "ready" when no other pending copy reads from dest.
// Uses caller-provided flat array src_ref_count[] indexed by value ID (no maps).
// touched_ids tracks which entries were modified so we can zero only those on cleanup.
fn resolve_parallel_copies_flat(mut m ssa.Module, blk_id int, dests []int, srcs []int, mut src_ref_count []int, mut touched_ids []int) {
if dests.len == 0 {
return
}
n := dests.len
// Build pending copies, filtering self-copies
mut p_dest := []int{cap: n}
mut p_src := []int{cap: n}
for ci in 0 .. n {
if dests[ci] != srcs[ci] {
p_dest << dests[ci]
p_src << srcs[ci]
}
}
np := p_dest.len
if np == 0 {
return
}
// Ensure src_ref_count is large enough for all value IDs we'll encounter.
// Check current max needed against array length.
mut need_len := m.values.len + np + 4
for i in 0 .. np {
if p_dest[i] >= need_len {
need_len = p_dest[i] + np + 4
}
if p_src[i] >= need_len {
need_len = p_src[i] + np + 4
}
}
if need_len > src_ref_count.len {
// Grow array
for _ in 0 .. need_len - src_ref_count.len {
src_ref_count << 0
}
}
// src_ref_count[val] = how many pending copies use val as source
// (array is pre-zeroed or cleaned from previous call via touched_ids)
touched_ids.clear()
for i in 0 .. np {
s := p_src[i]
if src_ref_count[s] == 0 {
touched_ids << s
}
src_ref_count[s] = src_ref_count[s] + 1
}
// Also track dests we'll read from src_ref_count
for i in 0 .. np {
d := p_dest[i]
if src_ref_count[d] == 0 {
// dest not in touched_ids yet but we read it; track for safety
// (it's already 0, but if cycle-breaking modifies it we need to clean it)
}
}
// Use int instead of bool for alive - ARM64 codegen may mishandle []bool{init: true}
// alive[i] = 1 if copy i is still pending, 0 if done
mut alive := []int{len: np}
for ai in 0 .. np {
alive[ai] = 1
}
// Worklist: use manual stack pointer instead of .delete() which may be buggy in ARM64
mut worklist := []int{len: np + np + 4}
mut wl_top := 0 // stack pointer: worklist[0..wl_top] are valid entries
for i in 0 .. np {
d := p_dest[i]
if src_ref_count[d] == 0 {
worklist[wl_top] = i
wl_top += 1
}
}
// Sequenced output copies
mut s_dest := []int{cap: np + 4}
mut s_src := []int{cap: np + 4}
mut remaining := np
for remaining > 0 {
if wl_top > 0 {
// Pop a ready copy from the worklist
wl_top -= 1
idx := worklist[wl_top]
if idx < 0 || idx >= np || alive[idx] == 0 {
continue
}
alive[idx] = 0
remaining -= 1
d := p_dest[idx]
s := p_src[idx]
s_dest << d
s_src << s
// Decrement ref count for the source
src_ref_count[s] = src_ref_count[s] - 1
if src_ref_count[s] == 0 {
// s is no longer used as source; any pending copy with dest==s is now ready
for j in 0 .. np {
if alive[j] == 1 && p_dest[j] == s {
// Grow worklist if needed
if wl_top >= worklist.len {
worklist << 0
}
worklist[wl_top] = j
wl_top += 1
}
}
}
continue
}
// Worklist empty but copies remain → cycle. Break it with 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)
// Grow src_ref_count if temp exceeds array bounds
if temp >= src_ref_count.len {
for _ in 0 .. temp + 1 - src_ref_count.len {
src_ref_count << 0
}
}
// Replace all references to cycle_src with temp in pending copies
src_ref_count[temp] = 0
touched_ids << temp
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[temp] + 1
}
}
src_ref_count[cycle_src] = 0
// Check if any copy whose dest was cycle_src is now ready
for j in 0 .. np {
if alive[j] == 1 && p_dest[j] == cycle_src {
if wl_top >= worklist.len {
worklist << 0
}
worklist[wl_top] = j
wl_top += 1
}
}
}
// Clean up src_ref_count: zero only the entries we touched
for ti in 0 .. touched_ids.len {
tid := touched_ids[ti]
if tid < src_ref_count.len {
src_ref_count[tid] = 0
}
}
// Emit the sequenced copies
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_node(.instruction, typ, 'phi_tmp_${m.values.len}', m.instrs.len - 1)
// Read whole struct, modify, write back (chained field assign broken in ARM64)
if src < m.values.len && temp_id !in m.values[src].uses {
mut sv := m.values[src]
sv.uses << temp_id
m.values[src] = sv
}
// Insert before terminator — build new instrs array
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 := []int{cap: blk.instrs.len + 1}
for ii in 0 .. insert_idx {
new_instrs << blk.instrs[ii]
}
new_instrs << temp_id
for ii in insert_idx .. blk.instrs.len {
new_instrs << blk.instrs[ii]
}
blk.instrs = new_instrs
m.blocks[blk_id] = blk
return temp_id
}
fn insert_copy_in_block(mut m ssa.Module, blk_id int, dest int, src int) {
dest_val := m.values[dest]
typ := dest_val.typ
m.instrs << ssa.Instruction{
op: .assign
block: blk_id
typ: typ
operands: [ssa.ValueID(dest), src]
}
val_id := m.add_value_node(.instruction, typ, 'copy', m.instrs.len - 1)
// Read whole struct, modify, write back (chained field assign broken in ARM64)
if dest < m.values.len && val_id !in m.values[dest].uses {
mut dv := m.values[dest]
dv.uses << val_id
m.values[dest] = dv
}
if src < m.values.len && val_id !in m.values[src].uses {
mut sv := m.values[src]
sv.uses << val_id
m.values[src] = sv
}
// Insert before terminator — build new instrs array
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 := []int{cap: blk.instrs.len + 1}
for ii in 0 .. insert_idx {
new_instrs << blk.instrs[ii]
}
new_instrs << val_id
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,183 @@
// Copyright (c) 2026 Alexander Medvednikov. All rights reserved.
// Use of this source code is governed by an MIT license
// that can be found in the LICENSE file.
// vtest build: macos
module optimize
import v2.ssa
struct TestParallelCopy {
dest int
src int
}
fn resolve_test_parallel_copies(mut m ssa.Module, blk_id int, copies []TestParallelCopy) {
mut dests := []int{cap: copies.len}
mut srcs := []int{cap: copies.len}
for copy in copies {
dests << copy.dest
srcs << copy.src
}
mut src_ref_count := []int{len: m.values.len + copies.len + 8}
mut touched_ids := []int{cap: copies.len + 4}
resolve_parallel_copies_flat(mut m, blk_id, dests, srcs, mut src_ref_count, mut touched_ids)
}
fn collect_parallel_assigns(m &ssa.Module, blk_id int) []TestParallelCopy {
mut assigns := []TestParallelCopy{}
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.op != .assign {
continue
}
assert instr.operands.len == 2
assert instr.operands[0] != 0
assert instr.operands[1] != 0
assigns << TestParallelCopy{
dest: instr.operands[0]
src: instr.operands[1]
}
}
return assigns
}
fn test_parallel_copy_cycle_materializes_temp_in_block() {
mut m := ssa.Module.new('test_cycle')
i64_t := m.type_store.get_int(64)
fn_id := m.new_function('f', 0, [])
blk_id := m.add_block(fn_id, 'entry')
c1 := m.get_or_add_const(i64_t, '1')
c2 := m.get_or_add_const(i64_t, '2')
a := m.add_instr(.add, blk_id, i64_t, [c1, c2])
b := m.add_instr(.sub, blk_id, i64_t, [a, c1])
m.add_instr(.ret, blk_id, 0, [])
resolve_test_parallel_copies(mut m, blk_id, [
TestParallelCopy{
dest: a
src: b
},
TestParallelCopy{
dest: b
src: a
},
])
mut temp_ids := []int{}
for val_id in m.blocks[blk_id].instrs {
if m.values[val_id].name.starts_with('phi_tmp_') {
temp_ids << val_id
}
}
assigns := collect_parallel_assigns(m, blk_id)
assert temp_ids.len > 0
assert assigns.len == 2
temp_id := temp_ids[0]
mut has_temp_src := false
for copy in assigns {
if copy.src == temp_id {
has_temp_src = true
}
}
assert has_temp_src
}
fn test_parallel_copy_acyclic_chain_does_not_create_temp() {
mut m := ssa.Module.new('test_chain')
i64_t := m.type_store.get_int(64)
fn_id := m.new_function('f', 0, [])
blk_id := m.add_block(fn_id, 'entry')
c1 := m.get_or_add_const(i64_t, '1')
c2 := m.get_or_add_const(i64_t, '2')
b := m.add_instr(.add, blk_id, i64_t, [c1, c2])
a := m.add_instr(.sub, blk_id, i64_t, [b, c1])
c := m.add_instr(.mul, blk_id, i64_t, [a, c2])
m.add_instr(.ret, blk_id, 0, [])
resolve_test_parallel_copies(mut m, blk_id, [
TestParallelCopy{
dest: a
src: b
},
TestParallelCopy{
dest: c
src: a
},
])
mut has_temp := false
for val_id in m.blocks[blk_id].instrs {
if m.values[val_id].name.starts_with('phi_tmp_') {
has_temp = true
}
}
assigns := collect_parallel_assigns(m, blk_id)
assert !has_temp
assert assigns.len == 2
assert assigns[0].dest == c
assert assigns[0].src == a
assert assigns[1].dest == a
assert assigns[1].src == b
}
fn test_eliminate_phi_nodes_cycle_assign_dests_are_materialized() {
mut m := ssa.Module.new('test_phi_elim_cycle')
i64_t := m.type_store.get_int(64)
fn_id := m.new_function('f', 0, [])
entry := m.add_block(fn_id, 'entry')
join := m.add_block(fn_id, 'join')
block_val_ids := build_block_val_ids(m)
join_val := get_block_val_id(m, join, block_val_ids)
m.add_instr(.jmp, entry, 0, [join_val])
entry_val := get_block_val_id(m, entry, block_val_ids)
zero := m.get_or_add_const(i64_t, '0')
one := m.get_or_add_const(i64_t, '1')
phi_a := m.add_instr(.phi, join, i64_t, [zero, entry_val])
phi_b := m.add_instr(.phi, join, i64_t, [phi_a, entry_val])
// Make it a true cycle: a <- b, b <- a.
m.instrs[m.values[phi_a].index].operands[0] = phi_b
m.add_instr(.ret, join, 0, [one])
eliminate_phi_nodes(mut m)
mut materialized := map[int]bool{}
for blk_id in m.funcs[fn_id].blocks {
for val_id in m.blocks[blk_id].instrs {
materialized[val_id] = true
}
}
mut saw_assign := false
mut saw_temp := false
for blk_id in m.funcs[fn_id].blocks {
for val_id in m.blocks[blk_id].instrs {
if m.values[val_id].name.starts_with('phi_tmp_') {
saw_temp = true
}
if m.values[val_id].kind != .instruction {
continue
}
instr := m.instrs[m.values[val_id].index]
if instr.op == .assign {
saw_assign = true
dest := instr.operands[0]
assert dest in materialized
}
}
}
assert saw_assign
assert saw_temp
}
@@ -0,0 +1,434 @@
// Copyright (c) 2026 Alexander Medvednikov. All rights reserved.
// Use of this source code is governed by an MIT license
// that can be found in the LICENSE file.
module optimize
import os
import v2.ssa
fn gate4_has_error(errors []VerifyError, op string, needle string) bool {
for err in errors {
if err.msg.contains(op) && err.msg.contains(needle) {
return true
}
}
return false
}
fn gate4_corrupt_uses_list_module() &ssa.Module {
mut m := ssa.Module.new('gate4_corrupt_uses_list')
i64_t := m.type_store.get_int(64)
func_id := m.new_function('main', i64_t, [])
entry := m.add_block(func_id, 'entry')
lhs := m.get_or_add_const(i64_t, '1')
rhs := m.get_or_add_const(i64_t, '2')
sum := m.add_instr(.add, entry, i64_t, [lhs, rhs])
m.add_instr(.ret, entry, 0, [sum])
mut lhs_val := m.values[lhs]
lhs_val.uses.clear()
m.values[lhs] = lhs_val
return m
}
fn test_verify_rejects_call_sret_without_target() {
mut m := ssa.Module.new('gate4_verify_call_sret')
i64_t := m.type_store.get_int(64)
func_id := m.new_function('caller', i64_t, [])
entry := m.add_block(func_id, 'entry')
m.add_instr(.call_sret, entry, i64_t, [])
zero := m.get_or_add_const(i64_t, '0')
m.add_instr(.ret, entry, 0, [zero])
errors := verify(m)
assert gate4_has_error(errors, 'call_sret', 'no operands'), 'expected call_sret without target to fail verification'
}
fn test_verify_rejects_concurrency_calls_without_target() {
for op in [ssa.OpCode.go_call, .spawn_call] {
mut m := ssa.Module.new('gate4_verify_concurrency_call')
func_id := m.new_function('caller', 0, [])
entry := m.add_block(func_id, 'entry')
m.add_instr(op, entry, 0, [])
m.add_instr(.ret, entry, 0, [])
errors := verify(m)
assert gate4_has_error(errors, op.str(), 'no operands'), 'expected ${op} without target to fail verification'
}
}
fn test_dce_preserves_global_initializer_store() {
mut m := ssa.Module.new('gate4_dce_global_store')
i64_t := m.type_store.get_int(64)
global_id := m.add_global('module__state', i64_t, false)
value := m.get_or_add_const(i64_t, '7')
func_id := m.new_function('_vinit', 0, [])
entry := m.add_block(func_id, 'entry')
store_id := m.add_instr(.store, entry, 0, [value, global_id])
m.add_instr(.ret, entry, 0, [])
changed := dead_code_elimination(mut m)
assert !changed
assert store_id in m.blocks[entry].instrs
assert m.values[global_id].uses.len == 1
assert m.values[global_id].uses[0] == store_id
}
fn test_dce_preserves_imported_const_initializer_store() {
mut m := ssa.Module.new('gate4_dce_imported_const_init')
i64_t := m.type_store.get_int(64)
global_id := m.add_global('dep__runtime_value', i64_t, false)
value := m.get_or_add_const(i64_t, '11')
func_id := m.new_function('dep____v_init_consts_dep', 0, [])
entry := m.add_block(func_id, 'entry')
store_id := m.add_instr(.store, entry, 0, [value, global_id])
m.add_instr(.ret, entry, 0, [])
changed := dead_code_elimination(mut m)
assert !changed
assert store_id in m.blocks[entry].instrs
assert m.values[global_id].uses.len == 1
assert m.values[global_id].uses[0] == store_id
}
fn test_optimize_can_verify_each_pipeline_checkpoint() {
mut m := ssa.Module.new('gate4_verify_each_pipeline_checkpoint')
i64_t := m.type_store.get_int(64)
func_id := m.new_function('main', i64_t, [])
entry := m.add_block(func_id, 'entry')
zero := m.get_or_add_const(i64_t, '0')
ret_id := m.add_instr(.ret, entry, 0, [zero])
optimize_with_options(mut m, OptimizeOptions{
verify_each_pass: true
})
assert ret_id in m.blocks[entry].instrs
assert verify(m).len == 0
}
fn test_optimize_can_verify_strict_each_pipeline_checkpoint() {
mut m := ssa.Module.new('gate4_strict_verify_each_pipeline_checkpoint')
i64_t := m.type_store.get_int(64)
func_id := m.new_function('main', i64_t, [])
entry := m.add_block(func_id, 'entry')
zero := m.get_or_add_const(i64_t, '0')
ret_id := m.add_instr(.ret, entry, 0, [zero])
optimize_with_options(mut m, OptimizeOptions{
strict_verify: true
})
assert ret_id in m.blocks[entry].instrs
assert verify(m).len == 0
}
fn test_optimize_runs_dead_code_elimination_pass() {
mut m := ssa.Module.new('gate4_optimize_runs_dce')
i64_t := m.type_store.get_int(64)
func_id := m.new_function('main', i64_t, [])
entry := m.add_block(func_id, 'entry')
lhs := m.get_or_add_const(i64_t, '20')
rhs := m.get_or_add_const(i64_t, '22')
dead_add := m.add_instr(.add, entry, i64_t, [lhs, rhs])
zero := m.get_or_add_const(i64_t, '0')
ret_id := m.add_instr(.ret, entry, 0, [zero])
optimize_with_options(mut m, OptimizeOptions{
verify_each_pass: true
})
assert dead_add !in m.blocks[entry].instrs
assert ret_id in m.blocks[entry].instrs
}
fn test_public_optimize_uses_v2_verify_pipeline_checkpoints() {
old_verify := os.getenv_opt('V2_VERIFY')
os.setenv('V2_VERIFY', '1', true)
defer {
if old := old_verify {
os.setenv('V2_VERIFY', old, true)
} else {
os.unsetenv('V2_VERIFY')
}
}
mut m := ssa.Module.new('gate4_public_optimize_v2_verify')
i64_t := m.type_store.get_int(64)
func_id := m.new_function('main', i64_t, [])
entry := m.add_block(func_id, 'entry')
lhs := m.get_or_add_const(i64_t, '100')
rhs := m.get_or_add_const(i64_t, '23')
dead_add := m.add_instr(.add, entry, i64_t, [lhs, rhs])
zero := m.get_or_add_const(i64_t, '0')
ret_id := m.add_instr(.ret, entry, 0, [zero])
optimize_with_options(mut m, OptimizeOptions{})
assert dead_add !in m.blocks[entry].instrs
assert ret_id in m.blocks[entry].instrs
assert verify(m).len == 0
}
fn test_public_optimize_uses_v2_verify_strict_pipeline_checkpoints() {
old_verify := os.getenv_opt('V2_VERIFY')
old_strict := os.getenv_opt('V2_VERIFY_STRICT')
os.unsetenv('V2_VERIFY')
os.setenv('V2_VERIFY_STRICT', '1', true)
defer {
if old := old_verify {
os.setenv('V2_VERIFY', old, true)
} else {
os.unsetenv('V2_VERIFY')
}
if old := old_strict {
os.setenv('V2_VERIFY_STRICT', old, true)
} else {
os.unsetenv('V2_VERIFY_STRICT')
}
}
mut m := ssa.Module.new('gate4_public_optimize_v2_verify_strict')
i64_t := m.type_store.get_int(64)
func_id := m.new_function('main', i64_t, [])
entry := m.add_block(func_id, 'entry')
zero := m.get_or_add_const(i64_t, '0')
ret_id := m.add_instr(.ret, entry, 0, [zero])
optimize(mut m)
assert ret_id in m.blocks[entry].instrs
assert verify(m).len == 0
}
fn test_optimize_rebuilds_uses_before_mem2reg_promotability() {
mut m := ssa.Module.new('gate4_mem2reg_rebuilds_escaped_alloca_uses')
i8_t := m.type_store.get_int(8)
ptr_i8_t := m.type_store.get_ptr(i8_t)
func_id := m.new_function('main', 0, [])
entry := m.add_block(func_id, 'entry')
alloca_id := m.add_instr(.alloca, entry, ptr_i8_t, [])
ten := m.get_or_add_const(i8_t, '10')
m.add_instr(.store, entry, 0, [ten, alloca_id])
callee := m.add_value_node(.unknown, 0, 'sink', 0)
m.add_instr(.call, entry, 0, [callee, alloca_id])
m.add_instr(.ret, entry, 0, [])
mut alloca_val := m.values[alloca_id]
alloca_val.uses = []
m.values[alloca_id] = alloca_val
optimize_with_options(mut m, OptimizeOptions{})
alloca_instr := m.instrs[m.values[alloca_id].index]
assert alloca_instr.op == .alloca
}
fn test_verify_and_panic_allows_c_extern_prototype_without_blocks() {
mut m := ssa.Module.new('gate4_c_extern_prototype')
func_id := m.new_function('C_puts', 0, [])
m.func_set_c_extern(func_id, true)
verify_and_panic(m, 'c extern prototype')
}
fn test_verify_and_panic_allows_marked_prototype_without_blocks() {
mut m := ssa.Module.new('gate4_marked_prototype')
func_id := m.new_function('registered_only', 0, [])
m.func_set_prototype(func_id, true)
verify_and_panic(m, 'marked prototype')
}
fn test_strict_verify_and_panic_allows_valid_declarations_without_blocks() {
mut m := ssa.Module.new('gate4_strict_valid_declarations')
c_func_id := m.new_function('C_puts', 0, [])
m.func_set_c_extern(c_func_id, true)
prototype_func_id := m.new_function('registered_only', 0, [])
m.func_set_prototype(prototype_func_id, true)
verify_and_panic_with_options(m, 'strict valid declarations', VerifyPanicOptions{
allow_noncritical: false
})
}
fn test_verify_and_panic_allows_uses_list_warning_by_default() {
mut m := gate4_corrupt_uses_list_module()
errors := verify(m)
assert gate4_has_error(errors, 'uses list', 'not in uses list'), 'expected uses list warning'
verify_and_panic(m, 'uses list default warning')
}
fn test_strict_verify_and_panic_rejects_uses_list_warning() {
tmp_dir := os.join_path(os.temp_dir(), 'v2_gate4_strict_verify_panic_${os.getpid()}')
os.rmdir_all(tmp_dir) or {}
os.mkdir_all(tmp_dir) or { panic(err) }
defer {
os.rmdir_all(tmp_dir) or {}
}
sample_path := os.join_path(tmp_dir, 'strict_verify_panic_sample.v')
os.write_file(sample_path, 'module main
import v2.ssa
import v2.ssa.optimize
fn main() {
mut m := ssa.Module.new("gate4_strict_uses_list")
i64_t := m.type_store.get_int(64)
func_id := m.new_function("main", i64_t, [])
entry := m.add_block(func_id, "entry")
lhs := m.get_or_add_const(i64_t, "1")
rhs := m.get_or_add_const(i64_t, "2")
sum := m.add_instr(.add, entry, i64_t, [lhs, rhs])
m.add_instr(.ret, entry, 0, [sum])
mut lhs_val := m.values[lhs]
lhs_val.uses.clear()
m.values[lhs] = lhs_val
optimize.verify_and_panic_with_options(m, "strict uses list", optimize.VerifyPanicOptions{
allow_noncritical: false
})
}
') or {
panic(err)
}
res := os.execute('${os.quoted_path(@VEXE)} run ${os.quoted_path(sample_path)}')
assert res.exit_code != 0
assert res.output.contains('uses list'), res.output
}
fn test_strict_optimize_rejects_uses_list_warning() {
tmp_dir := os.join_path(os.temp_dir(), 'v2_gate4_strict_optimize_panic_${os.getpid()}')
os.rmdir_all(tmp_dir) or {}
os.mkdir_all(tmp_dir) or { panic(err) }
defer {
os.rmdir_all(tmp_dir) or {}
}
sample_path := os.join_path(tmp_dir, 'strict_optimize_panic_sample.v')
os.write_file(sample_path, 'module main
import v2.ssa
import v2.ssa.optimize
fn main() {
mut m := ssa.Module.new("gate4_strict_optimize_uses_list")
i64_t := m.type_store.get_int(64)
func_id := m.new_function("main", i64_t, [])
entry := m.add_block(func_id, "entry")
lhs := m.get_or_add_const(i64_t, "1")
rhs := m.get_or_add_const(i64_t, "2")
sum := m.add_instr(.add, entry, i64_t, [lhs, rhs])
m.add_instr(.ret, entry, 0, [sum])
mut lhs_val := m.values[lhs]
lhs_val.uses.clear()
m.values[lhs] = lhs_val
optimize.optimize_with_options(mut m, optimize.OptimizeOptions{
strict_verify: true
})
}
') or {
panic(err)
}
res := os.execute('${os.quoted_path(@VEXE)} run ${os.quoted_path(sample_path)}')
assert res.exit_code != 0
assert res.output.contains('uses list'), res.output
assert res.output.contains('input'), res.output
}
fn test_verify_and_panic_rejects_v_function_without_blocks() {
tmp_dir := os.join_path(os.temp_dir(), 'v2_gate4_verify_panic_${os.getpid()}')
os.rmdir_all(tmp_dir) or {}
os.mkdir_all(tmp_dir) or { panic(err) }
defer {
os.rmdir_all(tmp_dir) or {}
}
sample_path := os.join_path(tmp_dir, 'verify_panic_sample.v')
os.write_file(sample_path, 'module main
import v2.ssa
import v2.ssa.optimize
fn main() {
mut m := ssa.Module.new("gate4_v_prototype")
m.new_function("v_decl_without_body", 0, [])
optimize.verify_and_panic(m, "v prototype negative")
}
') or {
panic(err)
}
res := os.execute('${os.quoted_path(@VEXE)} run ${os.quoted_path(sample_path)}')
assert res.exit_code != 0
assert res.output.contains('function has no blocks'), res.output
}
fn test_dce_preserves_call_sret_side_effect() {
mut m := ssa.Module.new('gate4_dce_call_sret')
i8_t := m.type_store.get_int(8)
i64_t := m.type_store.get_int(64)
ptr_t := m.type_store.get_ptr(i8_t)
result_t := m.type_store.register(ssa.Type{
kind: .struct_t
fields: [i64_t, i64_t]
})
callee := m.add_value_node(.func_ref, ptr_t, 'make_pair', 0)
arg := m.get_or_add_const(i64_t, '3')
func_id := m.new_function('caller', 0, [])
entry := m.add_block(func_id, 'entry')
call_id := m.add_instr(.call_sret, entry, result_t, [callee, arg])
m.add_instr(.ret, entry, 0, [])
changed := dead_code_elimination(mut m)
assert !changed
assert call_id in m.blocks[entry].instrs
assert m.values[callee].uses.len == 1
assert m.values[callee].uses[0] == call_id
}
fn test_dce_preserves_concurrency_call_side_effects() {
mut m := ssa.Module.new('gate4_dce_concurrency_calls')
i8_t := m.type_store.get_int(8)
ptr_t := m.type_store.get_ptr(i8_t)
go_callee := m.add_value_node(.func_ref, ptr_t, 'launch_go', 0)
spawn_callee := m.add_value_node(.func_ref, ptr_t, 'launch_spawn', 0)
func_id := m.new_function('caller', 0, [])
entry := m.add_block(func_id, 'entry')
go_id := m.add_instr(.go_call, entry, 0, [go_callee])
spawn_id := m.add_instr(.spawn_call, entry, 0, [spawn_callee])
m.add_instr(.ret, entry, 0, [])
changed := dead_code_elimination(mut m)
assert !changed
assert go_id in m.blocks[entry].instrs
assert spawn_id in m.blocks[entry].instrs
assert m.values[go_callee].uses[0] == go_id
assert m.values[spawn_callee].uses[0] == spawn_id
}
fn test_dce_preserves_cmpxchg_side_effect() {
mut m := ssa.Module.new('gate4_dce_cmpxchg')
i64_t := m.type_store.get_int(64)
global_id := m.add_global('module__atomic_state', i64_t, false)
expected := m.get_or_add_const(i64_t, '0')
desired := m.get_or_add_const(i64_t, '1')
func_id := m.new_function('update', 0, [])
entry := m.add_block(func_id, 'entry')
cmpxchg_id := m.add_instr(.cmpxchg, entry, i64_t, [global_id, expected, desired])
m.add_instr(.ret, entry, 0, [])
changed := dead_code_elimination(mut m)
assert !changed
assert cmpxchg_id in m.blocks[entry].instrs
assert m.values[global_id].uses[0] == cmpxchg_id
}
File diff suppressed because it is too large. Load diff
@@ -0,0 +1,219 @@
// Copyright (c) 2026 Alexander Medvednikov. All rights reserved.
// Use of this source code is governed by an MIT license
// that can be found in the LICENSE file.
// vtest build: macos
module optimize
import v2.ssa
fn test_verify_empty_function() {
mut m := ssa.Module.new('test')
func_id := m.new_function('test_fn', 0, [])
// Function with no blocks should fail
errors := verify(m)
assert errors.len > 0
assert errors[0].msg.contains('no blocks')
}
fn test_verify_missing_terminator() {
mut m := ssa.Module.new('test')
func_id := m.new_function('test_fn', 0, [])
blk_id := m.add_block(func_id, 'entry')
// Block with no instructions should fail (missing terminator)
errors := verify(m)
assert errors.len > 0
assert errors[0].msg.contains('no instructions') || errors[0].msg.contains('terminator')
}
fn test_verify_valid_function() {
mut m := ssa.Module.new('test')
i64_t := m.type_store.get_int(64)
func_id := m.new_function('test_fn', i64_t, [])
blk_id := m.add_block(func_id, 'entry')
// Create a constant
const_id := m.add_value_node(.constant, i64_t, 'const_42', 0)
// Add ret instruction with constant operand
m.add_instr(.ret, blk_id, 0, [const_id])
errors := verify(m)
assert errors.len == 0, 'expected no errors, got: ${errors}'
}
fn test_verify_binary_op_operand_count() {
mut m := ssa.Module.new('test')
i64_t := m.type_store.get_int(64)
func_id := m.new_function('test_fn', i64_t, [])
blk_id := m.add_block(func_id, 'entry')
// Create constants
const1 := m.add_value_node(.constant, i64_t, 'const_1', 0)
const2 := m.add_value_node(.constant, i64_t, 'const_2', 0)
const3 := m.add_value_node(.constant, i64_t, 'const_3', 0)
// Add binary op with wrong number of operands (3 instead of 2)
m.add_instr(.add, blk_id, i64_t, [const1, const2, const3])
m.add_instr(.ret, blk_id, 0, [])
errors := verify(m)
mut found_binary_error := false
for err in errors {
if err.msg.contains('binary op') && err.msg.contains('operands') {
found_binary_error = true
break
}
}
assert found_binary_error, 'expected binary op operand count error'
}
fn test_verify_load_non_pointer() {
mut m := ssa.Module.new('test')
i64_t := m.type_store.get_int(64)
func_id := m.new_function('test_fn', i64_t, [])
blk_id := m.add_block(func_id, 'entry')
// Create a non-pointer constant
const_id := m.add_value_node(.constant, i64_t, 'const_42', 0)
// Try to load from non-pointer (should error)
m.add_instr(.load, blk_id, i64_t, [const_id])
m.add_instr(.ret, blk_id, 0, [])
errors := verify(m)
mut found_load_error := false
for err in errors {
if err.msg.contains('load') && err.msg.contains('pointer') {
found_load_error = true
break
}
}
assert found_load_error, 'expected load non-pointer error'
}
fn test_verify_phi_odd_operands() {
mut m := ssa.Module.new('test')
i64_t := m.type_store.get_int(64)
func_id := m.new_function('test_fn', i64_t, [])
blk_id := m.add_block(func_id, 'entry')
// Create constant
const_id := m.add_value_node(.constant, i64_t, 'const_1', 0)
// Add phi with odd number of operands (should be pairs)
m.add_instr(.phi, blk_id, i64_t, [const_id]) // 1 operand instead of pairs
m.add_instr(.ret, blk_id, 0, [])
errors := verify(m)
mut found_phi_error := false
for err in errors {
if err.msg.contains('phi') && err.msg.contains('odd') {
found_phi_error = true
break
}
}
assert found_phi_error, 'expected phi odd operands error'
}
fn test_verify_branch_operand_count() {
mut m := ssa.Module.new('test')
i64_t := m.type_store.get_int(64)
func_id := m.new_function('test_fn', i64_t, [])
blk_id := m.add_block(func_id, 'entry')
// Create constant for condition
const_id := m.add_value_node(.constant, i64_t, 'cond', 0)
// Add br with wrong number of operands (1 instead of 3)
m.add_instr(.br, blk_id, 0, [const_id])
errors := verify(m)
mut found_br_error := false
for err in errors {
if err.msg.contains('br') && err.msg.contains('operands') {
found_br_error = true
break
}
}
assert found_br_error, 'expected br operand count error'
}
fn test_verify_jump_target_not_block() {
mut m := ssa.Module.new('test')
i64_t := m.type_store.get_int(64)
func_id := m.new_function('test_fn', i64_t, [])
blk_id := m.add_block(func_id, 'entry')
// Create constant (not a block)
const_id := m.add_value_node(.constant, i64_t, 'not_a_block', 0)
// Add jmp targeting non-block
m.add_instr(.jmp, blk_id, 0, [const_id])
errors := verify(m)
mut found_jmp_error := false
for err in errors {
if err.msg.contains('jmp') && err.msg.contains('basic_block') {
found_jmp_error = true
break
}
}
assert found_jmp_error, 'expected jmp target not block error'
}
fn test_verify_terminator_not_last() {
mut m := ssa.Module.new('test')
i64_t := m.type_store.get_int(64)
func_id := m.new_function('test_fn', i64_t, [])
blk_id := m.add_block(func_id, 'entry')
// Create block for jump target
target_blk := m.add_block(func_id, 'target')
target_val := m.blocks[target_blk].val_id
// Add terminator in middle, then another instruction
m.add_instr(.ret, blk_id, 0, [])
// Manually add another instruction after terminator
const_id := m.add_value_node(.constant, i64_t, 'const', 0)
m.add_instr(.add, blk_id, i64_t, [const_id, const_id])
// Add terminator to target block
m.add_instr(.ret, target_blk, 0, [])
errors := verify(m)
mut found_term_error := false
for err in errors {
if err.msg.contains('terminator') && err.msg.contains('end') {
found_term_error = true
break
}
}
assert found_term_error, 'expected terminator not at end error'
}
fn test_verify_use_def_chain() {
mut m := ssa.Module.new('test')
i64_t := m.type_store.get_int(64)
func_id := m.new_function('test_fn', i64_t, [])
blk_id := m.add_block(func_id, 'entry')
// Create constants and use them
const1 := m.add_value_node(.constant, i64_t, 'const_1', 0)
const2 := m.add_value_node(.constant, i64_t, 'const_2', 0)
// Add instruction using both constants
result := m.add_instr(.add, blk_id, i64_t, [const1, const2])
m.add_instr(.ret, blk_id, i64_t, [result])
// Verify use-def chains are correct
errors := verify(m)
assert errors.len == 0, 'expected no errors for valid use-def chain, got: ${errors}'
// Check that uses were registered
assert result in m.values[const1].uses, 'const1 should have result in uses'
assert result in m.values[const2].uses, 'const2 should have result in uses'
}