Initial project import
This commit is contained in:
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'
|
||||
}
|
||||
Reference in new issue
Block a user