703 lines
19 KiB
V
703 lines
19 KiB
V
// 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 ssa
|
||
|
||
import v2.types
|
||
|
||
pub struct TargetData {
|
||
pub:
|
||
ptr_size int
|
||
endian_little bool
|
||
}
|
||
|
||
@[heap]
|
||
pub struct Module {
|
||
pub mut:
|
||
name string
|
||
target TargetData
|
||
type_store TypeStore
|
||
// For parallel SSA workers: shared reference to main module's type_store.
|
||
// When non-nil, type operations use this instead of the local type_store.
|
||
shared_type_store &TypeStore = unsafe { nil }
|
||
|
||
// Type checker environment (optional, for backends that need rich type info)
|
||
env &types.Environment = unsafe { nil }
|
||
|
||
// Arenas
|
||
values []Value
|
||
instrs []Instruction
|
||
blocks []BasicBlock
|
||
funcs []Function
|
||
globals []GlobalVar
|
||
|
||
// C struct names: TypeID -> C struct name (e.g., 114 -> "stat" for struct stat)
|
||
// Used by the C gen to emit `typedef struct <name> Struct_N;` instead of
|
||
// generating a custom struct definition that would have the wrong memory layout.
|
||
c_struct_names map[int]string
|
||
// C structs marked with @[typedef] – already a C typedef, not a struct tag.
|
||
c_typedef_structs map[int]bool
|
||
|
||
// Constant cache: (type, name) -> ValueID for deduplication
|
||
const_cache map[string]ValueID
|
||
}
|
||
|
||
pub fn Module.new(name string) &Module {
|
||
mut m := &Module{
|
||
name: name
|
||
type_store: TypeStore.new()
|
||
}
|
||
// Pre-allocate arenas to avoid repeated reallocation during SSA building.
|
||
// A typical hello.v compilation needs ~158K values, ~134K instrs, ~10K blocks, ~1.8K funcs.
|
||
// Pre-allocating avoids ARM64 backend issues with array growth reallocation.
|
||
m.values = []Value{cap: 262144}
|
||
m.instrs = []Instruction{cap: 262144}
|
||
m.blocks = []BasicBlock{cap: 16384}
|
||
m.funcs = []Function{cap: 4096}
|
||
m.globals = []GlobalVar{cap: 2048}
|
||
// Reserve ID 0 to represent "null" or "invalid", avoiding collisions
|
||
// with map lookups returning 0.
|
||
m.values << Value{
|
||
kind: .unknown
|
||
id: 0
|
||
}
|
||
return m
|
||
}
|
||
|
||
// release_outer_arenas_after_mir_lower releases SSA arena buffers that MIR has
|
||
// copied by value. Nested slices (value uses, instruction operands, block edges,
|
||
// function params/blocks) are intentionally not freed because MIR shallow-shares
|
||
// them after lowering.
|
||
pub fn (mut m Module) release_outer_arenas_after_mir_lower() {
|
||
unsafe {
|
||
m.values.free()
|
||
m.instrs.free()
|
||
m.blocks.free()
|
||
m.funcs.free()
|
||
}
|
||
m.values = []Value{}
|
||
m.instrs = []Instruction{}
|
||
m.blocks = []BasicBlock{}
|
||
m.funcs = []Function{}
|
||
}
|
||
|
||
pub fn (mut m Module) new_function(name string, ret TypeID, params []TypeID) int {
|
||
// Check if function already exists (avoid duplicates from multiple files)
|
||
for i, f in m.funcs {
|
||
if f.name == name {
|
||
return i
|
||
}
|
||
}
|
||
id := m.funcs.len
|
||
m.funcs << Function{
|
||
id: id
|
||
name: name
|
||
typ: ret
|
||
}
|
||
return id
|
||
}
|
||
|
||
pub fn (mut m Module) add_block(func_id int, name string) BlockID {
|
||
id := m.blocks.len
|
||
// FIX: Sanitize block names for C labels (replace . with _)
|
||
safe_name := name.replace('.', '_')
|
||
unique_name := '${safe_name}_${id}'
|
||
|
||
// Store 'id' (index in blocks arena) in the Value
|
||
val_id := m.add_value_node(.basic_block, 0, unique_name, id)
|
||
|
||
m.blocks << BasicBlock{
|
||
id: id
|
||
val_id: val_id
|
||
name: unique_name
|
||
parent: func_id
|
||
}
|
||
// Avoid m.funcs[func_id].blocks << id -- chained broken in ARM64 self-hosted
|
||
mut f := m.funcs[func_id]
|
||
f.blocks << id
|
||
f.is_prototype = false
|
||
m.funcs[func_id] = f
|
||
return id
|
||
}
|
||
|
||
// Updated to accept 'index'
|
||
pub fn (mut m Module) add_value_node(kind ValueKind, typ TypeID, name string, index int) ValueID {
|
||
id := m.values.len
|
||
m.values << Value{
|
||
id: id
|
||
kind: kind
|
||
typ: typ
|
||
name: name
|
||
index: index
|
||
}
|
||
return id
|
||
}
|
||
|
||
// --- Helpers to avoid chained struct-array mutations (broken in ARM64 self-hosted) ---
|
||
|
||
pub fn (mut m Module) func_add_param(func_id int, param_val ValueID) {
|
||
mut f := m.funcs[func_id]
|
||
f.params << param_val
|
||
m.funcs[func_id] = f
|
||
}
|
||
|
||
pub fn (mut m Module) func_set_c_extern(func_id int, val bool) {
|
||
mut f := m.funcs[func_id]
|
||
f.is_c_extern = val
|
||
m.funcs[func_id] = f
|
||
}
|
||
|
||
pub fn (mut m Module) func_set_prototype(func_id int, val bool) {
|
||
mut f := m.funcs[func_id]
|
||
f.is_prototype = val
|
||
m.funcs[func_id] = f
|
||
}
|
||
|
||
pub fn (mut m Module) func_clear_blocks(func_id int) {
|
||
mut f := m.funcs[func_id]
|
||
f.blocks.clear()
|
||
m.funcs[func_id] = f
|
||
}
|
||
|
||
pub fn (mut m Module) func_clear_params(func_id int) {
|
||
mut f := m.funcs[func_id]
|
||
f.params.clear()
|
||
m.funcs[func_id] = f
|
||
}
|
||
|
||
pub fn (mut m Module) func_set_params(func_id int, params []ValueID) {
|
||
mut f := m.funcs[func_id]
|
||
f.params = params
|
||
m.funcs[func_id] = f
|
||
}
|
||
|
||
pub fn (mut m Module) block_add_succ(from int, to int) {
|
||
mut blk := m.blocks[from]
|
||
blk.succs << to
|
||
m.blocks[from] = blk
|
||
}
|
||
|
||
pub fn (mut m Module) block_add_pred(to int, from int) {
|
||
mut blk := m.blocks[to]
|
||
blk.preds << from
|
||
m.blocks[to] = blk
|
||
}
|
||
|
||
pub fn (mut m Module) nop_instr(val_idx int) {
|
||
mut instr := m.instrs[val_idx]
|
||
instr.op = .bitcast
|
||
instr.operands = []
|
||
m.instrs[val_idx] = instr
|
||
}
|
||
|
||
// Get or create a constant value, reusing existing ones when possible.
|
||
// This maintains SSA's immutability principle by avoiding duplicate constants.
|
||
pub fn (mut m Module) get_or_add_const(typ TypeID, name string) ValueID {
|
||
key := '${typ}:${name}'
|
||
if existing := map_get_value_id(m.const_cache, key) {
|
||
return existing
|
||
}
|
||
id := m.add_value_node(.constant, typ, name, 0)
|
||
m.const_cache[key] = id
|
||
return id
|
||
}
|
||
|
||
pub fn (m Module) get_block_from_val(val_id int) int {
|
||
return m.values[val_id].index
|
||
}
|
||
|
||
pub fn (mut m Module) add_instr(op OpCode, block BlockID, typ TypeID, operands []ValueID) ValueID {
|
||
// 1. Save Instruction Index
|
||
instr_idx := m.instrs.len
|
||
|
||
instr := Instruction{
|
||
op: op
|
||
block: block
|
||
typ: typ
|
||
operands: operands
|
||
}
|
||
m.instrs << instr
|
||
|
||
// 2. Pass instr_idx to Value
|
||
val_id := m.add_value_node(.instruction, typ, '', instr_idx)
|
||
|
||
// 3. Link Block — read whole struct, modify, write back (chained broken in ARM64)
|
||
mut blk := m.blocks[block]
|
||
blk.instrs << val_id
|
||
m.blocks[block] = blk
|
||
|
||
// 4. Update Def-Use — same pattern
|
||
for op_id in operands {
|
||
if op_id < m.values.len {
|
||
mut v := m.values[op_id]
|
||
v.uses << val_id
|
||
m.values[op_id] = v
|
||
}
|
||
}
|
||
|
||
return val_id
|
||
}
|
||
|
||
pub fn (mut m Module) add_global(name string, typ TypeID, is_const bool) int {
|
||
return m.add_global_with_value(name, typ, is_const, 0)
|
||
}
|
||
|
||
pub fn (mut m Module) add_global_with_value(name string, typ TypeID, is_const bool, initial_value i64) int {
|
||
id := m.globals.len
|
||
g := GlobalVar{
|
||
name: name
|
||
typ: typ
|
||
linkage: .private // Local global, not external
|
||
is_constant: is_const
|
||
initial_value: initial_value
|
||
}
|
||
m.globals << g
|
||
|
||
// FIX: The Value representing a global is a POINTER to the data
|
||
ptr_typ := m.type_store.get_ptr(typ)
|
||
return m.add_value_node(.global, ptr_typ, name, id)
|
||
}
|
||
|
||
pub fn (mut m Module) add_global_with_data(name string, elem_type TypeID, is_const bool, data []u8) int {
|
||
id := m.globals.len
|
||
g := GlobalVar{
|
||
name: name
|
||
typ: elem_type
|
||
linkage: .private
|
||
is_constant: is_const
|
||
initial_data: data
|
||
}
|
||
m.globals << g
|
||
|
||
// The Value is a POINTER to element data (for direct indexing)
|
||
ptr_typ := m.type_store.get_ptr(elem_type)
|
||
return m.add_value_node(.global, ptr_typ, name, id)
|
||
}
|
||
|
||
// add_external_global adds an external global variable (defined outside this module)
|
||
// Returns the ValueID for the global pointer
|
||
pub fn (mut m Module) add_external_global(name string, typ TypeID) ValueID {
|
||
// Check if this external global already exists
|
||
for v in m.values {
|
||
if v.kind == .global && v.name == name {
|
||
return v.id
|
||
}
|
||
}
|
||
|
||
// Create a new external global
|
||
id := m.globals.len
|
||
g := GlobalVar{
|
||
name: name
|
||
typ: typ
|
||
linkage: .external
|
||
}
|
||
m.globals << g
|
||
|
||
// The Value representing a global is a POINTER to the data
|
||
ptr_typ := m.type_store.get_ptr(typ)
|
||
return m.add_value_node(.global, ptr_typ, name, id)
|
||
}
|
||
|
||
pub fn (mut m Module) add_instr_front(op OpCode, block BlockID, typ TypeID, operands []ValueID) ValueID {
|
||
instr_idx := m.instrs.len
|
||
instr := Instruction{
|
||
op: op
|
||
block: block
|
||
typ: typ
|
||
operands: operands
|
||
}
|
||
m.instrs << instr
|
||
val_id := m.add_value_node(.instruction, typ, '', instr_idx)
|
||
|
||
// Prepend to block instructions — read whole struct, modify, write back (chained broken in ARM64)
|
||
mut blk := m.blocks[block]
|
||
blk.instrs.prepend(val_id)
|
||
m.blocks[block] = blk
|
||
|
||
// Update Def-Use — same pattern
|
||
for op_id in operands {
|
||
if op_id < m.values.len {
|
||
mut v := m.values[op_id]
|
||
v.uses << val_id
|
||
m.values[op_id] = v
|
||
}
|
||
}
|
||
return val_id
|
||
}
|
||
|
||
// append_phi_operands appends a (val, block_val) pair to a phi instruction's operands.
|
||
// Append (val, block_val) pair to phi instruction operands.
|
||
// Avoid m.instrs[idx].operands << x — chained append broken in ARM64 self-hosted.
|
||
pub fn (mut m Module) append_phi_operands(instr_idx int, val int, block_val int) {
|
||
// Read whole struct, modify, write back (chained broken in ARM64)
|
||
mut instr := m.instrs[instr_idx]
|
||
instr.operands << val
|
||
instr.operands << block_val
|
||
m.instrs[instr_idx] = instr
|
||
}
|
||
|
||
pub fn (mut m Module) replace_uses(old_val int, new_val int) {
|
||
// Copy uses, because we modify instr operands which might change things
|
||
uses := m.values[old_val].uses.clone()
|
||
for use_id in uses {
|
||
use_val := m.values[use_id]
|
||
if use_val.kind == .instruction {
|
||
// Read whole instr, modify operands, write back (chained broken in ARM64)
|
||
mut instr := m.instrs[use_val.index]
|
||
mut replaced := false
|
||
for i in 0 .. instr.operands.len {
|
||
if instr.operands[i] == old_val {
|
||
instr.operands[i] = new_val
|
||
replaced = true
|
||
}
|
||
}
|
||
if replaced {
|
||
m.instrs[use_val.index] = instr
|
||
}
|
||
// Only add to uses list once per user, even if used multiple times
|
||
if replaced && use_id !in m.values[new_val].uses {
|
||
mut nv := m.values[new_val]
|
||
nv.uses << use_id
|
||
m.values[new_val] = nv
|
||
}
|
||
}
|
||
}
|
||
mut ov := m.values[old_val]
|
||
ov.uses = []
|
||
m.values[old_val] = ov
|
||
}
|
||
|
||
fn dfs(mut m Module, blk int, mut visited map[int]bool, mut rpo []int) {
|
||
visited[blk] = true
|
||
for s in m.blocks[blk].succs {
|
||
if !visited[s] {
|
||
dfs(mut m, s, mut visited, mut rpo)
|
||
}
|
||
}
|
||
rpo << blk
|
||
}
|
||
|
||
fn (mut m Module) get_rpo(func Function) []int {
|
||
mut visited := map[int]bool{}
|
||
mut rpo := []int{}
|
||
dfs(mut m, func.blocks[0], mut visited, mut rpo)
|
||
// rpo.reverse_inplace()
|
||
rpo = rpo.reverse()
|
||
return rpo
|
||
}
|
||
|
||
// new_worker_module creates a lightweight Module for parallel SSA building.
|
||
// Each worker gets its own copy of type_store (COW from main).
|
||
// Has its own values/instrs/blocks arenas starting from empty.
|
||
pub fn (mut m Module) new_worker_module() &Module {
|
||
// Explicitly clone funcs and globals to avoid COW races between threads.
|
||
mut wf := []Function{cap: m.funcs.len}
|
||
for f in m.funcs {
|
||
wf << f
|
||
}
|
||
mut wg := []GlobalVar{cap: m.globals.len}
|
||
for g in m.globals {
|
||
wg << g
|
||
}
|
||
// Deep-clone TypeStore to avoid COW races on types[] and cache map.
|
||
mut wts := TypeStore{}
|
||
wts.types = []Type{cap: m.type_store.types.len}
|
||
for t in m.type_store.types {
|
||
wts.types << t
|
||
}
|
||
wts.cache = m.type_store.cache.clone()
|
||
|
||
mut w := &Module{
|
||
name: m.name
|
||
shared_type_store: unsafe { nil }
|
||
type_store: wts
|
||
env: m.env
|
||
funcs: wf
|
||
globals: wg
|
||
}
|
||
// Seed worker with main module's values so that ValueIDs from Phases 1-3
|
||
// (constants, globals, func_refs, params) are valid in the worker.
|
||
w.values = []Value{cap: m.values.len + 32768}
|
||
for v in m.values {
|
||
w.values << v
|
||
}
|
||
// Also seed instrs and blocks (typically empty after Phases 1-3, but just in case).
|
||
w.instrs = []Instruction{cap: m.instrs.len + 32768}
|
||
for instr in m.instrs {
|
||
w.instrs << instr
|
||
}
|
||
w.blocks = []BasicBlock{cap: m.blocks.len + 2048}
|
||
for blk in m.blocks {
|
||
w.blocks << blk
|
||
}
|
||
return w
|
||
}
|
||
|
||
// FuncSSAData holds the SSA data produced by a worker for a single function.
|
||
pub struct FuncSSAData {
|
||
pub:
|
||
func_idx int // Index into main module's funcs[]
|
||
blocks []BlockID // Worker-local block IDs
|
||
params []ValueID // Worker-local param ValueIDs
|
||
}
|
||
|
||
// merge_worker_module merges a worker's SSA arenas into the main module.
|
||
// Workers are seeded with the main module's values/instrs/blocks from Phases 1-3.
|
||
// seed_values/seed_instrs/seed_blocks/seed_funcs are the lengths of the seed data.
|
||
// Only data beyond the seed is new and needs to be merged with ID remapping.
|
||
// func_data contains (func_idx, blocks, params) for updating main funcs[].
|
||
pub fn (mut m Module) merge_worker_module(w &Module, func_data []FuncSSAData, seed_values int, seed_instrs int, seed_blocks int, seed_types int, seed_funcs int) {
|
||
// Build type remapping: worker type IDs >= seed_types may differ from main.
|
||
// For each new worker type, find or create equivalent in main.
|
||
mut type_remap := []TypeID{len: w.type_store.types.len, init: 0}
|
||
// Seed types map to themselves (identity)
|
||
for ti := 0; ti < seed_types && ti < type_remap.len; ti++ {
|
||
type_remap[ti] = ti
|
||
}
|
||
// Map new worker types to main types
|
||
for ti := seed_types; ti < w.type_store.types.len; ti++ {
|
||
wt := w.type_store.types[ti]
|
||
// Generate cache key matching TypeStore conventions
|
||
mut cache_key := ''
|
||
match wt.kind {
|
||
.int_t {
|
||
cache_key = if wt.is_unsigned { 'u${wt.width}' } else { 'i${wt.width}' }
|
||
}
|
||
.float_t {
|
||
cache_key = 'f${wt.width}'
|
||
}
|
||
.ptr_t {
|
||
// Remap the elem_type first
|
||
remapped_elem := if int(wt.elem_type) < type_remap.len {
|
||
type_remap[int(wt.elem_type)]
|
||
} else {
|
||
wt.elem_type
|
||
}
|
||
cache_key = 'p${remapped_elem}'
|
||
}
|
||
.array_t {
|
||
remapped_elem := if int(wt.elem_type) < type_remap.len {
|
||
type_remap[int(wt.elem_type)]
|
||
} else {
|
||
wt.elem_type
|
||
}
|
||
cache_key = 'a${remapped_elem}_${wt.len}'
|
||
}
|
||
else {}
|
||
}
|
||
|
||
// Try cache lookup in main
|
||
if cache_key.len > 0 {
|
||
if existing := map_get_type_id(m.type_store.cache, cache_key) {
|
||
type_remap[ti] = existing
|
||
continue
|
||
}
|
||
}
|
||
// Register new type in main with remapped field types
|
||
remapped_ret := if int(wt.ret_type) < type_remap.len && int(wt.ret_type) >= seed_types {
|
||
type_remap[int(wt.ret_type)]
|
||
} else {
|
||
wt.ret_type
|
||
}
|
||
remapped_elem2 := if int(wt.elem_type) < type_remap.len && int(wt.elem_type) >= seed_types {
|
||
type_remap[int(wt.elem_type)]
|
||
} else {
|
||
wt.elem_type
|
||
}
|
||
mut new_type := Type{
|
||
kind: wt.kind
|
||
width: wt.width
|
||
len: wt.len
|
||
is_c_struct: wt.is_c_struct
|
||
is_union: wt.is_union
|
||
is_unsigned: wt.is_unsigned
|
||
ret_type: remapped_ret
|
||
elem_type: remapped_elem2
|
||
}
|
||
if wt.fields.len > 0 {
|
||
mut new_fields := []TypeID{cap: wt.fields.len}
|
||
for f in wt.fields {
|
||
new_fields << if int(f) < type_remap.len && int(f) >= seed_types {
|
||
type_remap[int(f)]
|
||
} else {
|
||
f
|
||
}
|
||
}
|
||
new_type = Type{
|
||
...new_type
|
||
fields: new_fields
|
||
field_names: wt.field_names
|
||
}
|
||
}
|
||
if wt.params.len > 0 {
|
||
mut new_params := []TypeID{cap: wt.params.len}
|
||
for p in wt.params {
|
||
new_params << if int(p) < type_remap.len && int(p) >= seed_types {
|
||
type_remap[int(p)]
|
||
} else {
|
||
p
|
||
}
|
||
}
|
||
new_type = Type{
|
||
...new_type
|
||
params: new_params
|
||
}
|
||
}
|
||
new_id := m.type_store.register(new_type)
|
||
if cache_key.len > 0 {
|
||
m.type_store.cache[cache_key] = new_id
|
||
}
|
||
type_remap[ti] = new_id
|
||
}
|
||
|
||
// Remapping offsets: worker IDs in [seed..seed+N) → main IDs in [main.len..main.len+N).
|
||
// For seed IDs (< seed_values), no remapping needed — they're the same in both.
|
||
value_off := m.values.len - seed_values
|
||
instr_off := m.instrs.len - seed_instrs
|
||
block_off := m.blocks.len - seed_blocks
|
||
|
||
// Append worker values beyond seed
|
||
for wi := seed_values; wi < w.values.len; wi++ {
|
||
wv := w.values[wi]
|
||
mut new_index := wv.index
|
||
if wv.kind == .instruction {
|
||
new_index += instr_off
|
||
} else if wv.kind == .basic_block {
|
||
new_index += block_off
|
||
}
|
||
// Remap uses — only IDs >= seed need remapping
|
||
mut new_uses := []ValueID{cap: wv.uses.len}
|
||
for u in wv.uses {
|
||
new_uses << if u >= seed_values { u + value_off } else { u }
|
||
}
|
||
// Remap type
|
||
new_typ := if int(wv.typ) >= seed_types && int(wv.typ) < type_remap.len {
|
||
type_remap[int(wv.typ)]
|
||
} else {
|
||
wv.typ
|
||
}
|
||
m.values << Value{
|
||
id: wi + value_off
|
||
kind: wv.kind
|
||
typ: new_typ
|
||
name: wv.name
|
||
index: new_index
|
||
uses: new_uses
|
||
}
|
||
}
|
||
|
||
// Append worker instructions beyond seed
|
||
for ii := seed_instrs; ii < w.instrs.len; ii++ {
|
||
instr := w.instrs[ii]
|
||
mut new_ops := []ValueID{cap: instr.operands.len}
|
||
for op in instr.operands {
|
||
new_ops << if op >= seed_values { op + value_off } else { op }
|
||
}
|
||
// Remap type
|
||
new_typ := if int(instr.typ) >= seed_types && int(instr.typ) < type_remap.len {
|
||
type_remap[int(instr.typ)]
|
||
} else {
|
||
instr.typ
|
||
}
|
||
m.instrs << Instruction{
|
||
op: instr.op
|
||
block: if instr.block >= seed_blocks {
|
||
instr.block + block_off
|
||
} else {
|
||
instr.block
|
||
}
|
||
typ: new_typ
|
||
operands: new_ops
|
||
pos: instr.pos
|
||
atomic_ord: instr.atomic_ord
|
||
inline: instr.inline
|
||
}
|
||
}
|
||
|
||
// Append worker blocks beyond seed
|
||
for bi := seed_blocks; bi < w.blocks.len; bi++ {
|
||
blk := w.blocks[bi]
|
||
mut new_instrs := []ValueID{cap: blk.instrs.len}
|
||
for vi in blk.instrs {
|
||
new_instrs << if vi >= seed_values { vi + value_off } else { vi }
|
||
}
|
||
mut new_preds := []BlockID{cap: blk.preds.len}
|
||
for p in blk.preds {
|
||
new_preds << if p >= seed_blocks { p + block_off } else { p }
|
||
}
|
||
mut new_succs := []BlockID{cap: blk.succs.len}
|
||
for s in blk.succs {
|
||
new_succs << if s >= seed_blocks { s + block_off } else { s }
|
||
}
|
||
m.blocks << BasicBlock{
|
||
id: blk.id + block_off
|
||
val_id: if blk.val_id >= seed_values { blk.val_id + value_off } else { blk.val_id }
|
||
name: blk.name
|
||
parent: blk.parent
|
||
instrs: new_instrs
|
||
preds: new_preds
|
||
succs: new_succs
|
||
}
|
||
}
|
||
|
||
// Update main module's funcs[] with blocks and params from worker
|
||
for fd in func_data {
|
||
if fd.func_idx >= m.funcs.len {
|
||
continue
|
||
}
|
||
mut f := m.funcs[fd.func_idx]
|
||
// Skip if function was already built by a previous worker's merge.
|
||
// Without this guard, duplicate blocks from multiple workers get appended,
|
||
// leading to mismatched parameter/value IDs and crashes.
|
||
if f.blocks.len > 0 {
|
||
continue
|
||
}
|
||
for blk_id in fd.blocks {
|
||
f.blocks << if blk_id >= seed_blocks { blk_id + block_off } else { blk_id }
|
||
}
|
||
mut new_params := []ValueID{cap: fd.params.len}
|
||
for p in fd.params {
|
||
new_params << if p >= seed_values { p + value_off } else { p }
|
||
}
|
||
f.params = new_params
|
||
m.funcs[fd.func_idx] = f
|
||
}
|
||
|
||
// Also add any new functions created by workers (stubs like wyhash, array_eq).
|
||
for fi := seed_funcs; fi < w.funcs.len; fi++ {
|
||
wfunc := w.funcs[fi]
|
||
mut already_added := false
|
||
for existing in m.funcs {
|
||
if existing.name == wfunc.name {
|
||
already_added = true
|
||
break
|
||
}
|
||
}
|
||
if already_added {
|
||
continue
|
||
}
|
||
mut new_blocks := []BlockID{cap: wfunc.blocks.len}
|
||
for blk_id in wfunc.blocks {
|
||
new_blocks << if blk_id >= seed_blocks { blk_id + block_off } else { blk_id }
|
||
}
|
||
mut new_params := []ValueID{cap: wfunc.params.len}
|
||
for p in wfunc.params {
|
||
new_params << if p >= seed_values { p + value_off } else { p }
|
||
}
|
||
// Remap function return type
|
||
new_typ := if int(wfunc.typ) >= seed_types && int(wfunc.typ) < type_remap.len {
|
||
type_remap[int(wfunc.typ)]
|
||
} else {
|
||
wfunc.typ
|
||
}
|
||
m.funcs << Function{
|
||
id: m.funcs.len
|
||
name: wfunc.name
|
||
typ: new_typ
|
||
blocks: new_blocks
|
||
params: new_params
|
||
}
|
||
}
|
||
}
|