lua metaprogram improvmeents

This commit is contained in:
ed
2026-08-15 22:24:26 -04:00
parent 3301826f5c
commit 1a0d417649
6 changed files with 812 additions and 196 deletions
+8 -10
View File
@@ -1338,7 +1338,6 @@ M.GTE_CR_ALIAS_GROUPS = {
-- first must be written before second.
M.GTE_PACKED_SLOT_RELATIONS = {
{ slot = 2, first = "gte_cr_RT13", second = "gte_cr_RT22" },
{ slot = 4, first = "gte_cr_RT22", second = "gte_cr_RT33" },
}
-- Operand-class table for the COP2->GPR load-delay check.
@@ -2924,18 +2923,17 @@ end
-------------------------------------------------------------------------------
-- find_atom_proc_decl_for — backward walk for MipsAtom_Proc_ name extraction.
--
-- After the `sym` arg was dropped from MipsAtom_Proc_, the atom name is
-- derived from the preceding `MipsAtom* X_proc(args)` function declaration.
-- The atom name is the preceding `MipsAtom* ident(args)` function ident.
-- This function walks backward from `before_pos` to find it.
--
-- Returns (raw_name, args_inner) or (nil, nil).
-- raw_name — e.g. "normalize_v3s4" (the _proc suffix is stripped)
-- args_inner — e.g. "AtomArena_R aa, U4 r_scratch, ..."
-- Returns (raw_name, args_inner, func_ident, after_paren) or (nil, nil).
-- raw_name — the function ident as written
-- args_inner — e.g. "AtomArena_R aa, U4 r_scratch, ..."
-- after_paren — source position after the function `)`
--
-- The walk finds the LAST "MipsAtom*" before before_pos, then skips
-- whitespace + qualifiers (internal, I_, FI_, comments) until it finds an
-- ident followed by "(". That ident is the function name (with _proc suffix);
-- the suffix is stripped to get raw_name. The parens contents are the args.
-- ident followed by "(". That ident is the name. The parens contents are the args.
-------------------------------------------------------------------------------
function M.find_atom_proc_decl_for(source, before_pos, mips_atom_ptr_len)
local search_pos = 1
@@ -2980,9 +2978,9 @@ function M.find_atom_proc_decl_for(source, before_pos, mips_atom_ptr_len)
-- check if the next non-ws char after ident is "("
local next_pos = M.skip_ws_and_cmt(source, ident_end)
if source:sub(next_pos, next_pos) == "(" then
local inner = M.read_parens(source, next_pos)
local inner, after_paren = M.read_parens(source, next_pos)
if inner then
return ident, inner, ident
return ident, inner, ident, after_paren
end
end
-- ident not followed by "(" — it's a qualifier; skip it
+14 -3
View File
@@ -7,7 +7,7 @@
--- then resolves the function-args string from the preceding `FI_ Slice_MipsCode ac_X(...)` declaration via a backward walk.
---
--- `MipsAtom_Proc_(X, ab, { body })` declarations (kind="atom_proc") are ATOMS, not components, and are deliberately excluded —
--- atoms get emitted via `tb_emit(tb, code_<name>)` linker symbols, not inlined as `mac_*` macros.
--- the ELF symbol is the C ident. Raw `MipsCode code_*` is leftover, not the atom rule.
---
--- Emits one `gen/macs.h` per *immediate source directory* with `#define mac_X(sig) \` macros plus `WORD_COUNT(mac_X, N)` entries for downstream offset computation.
--- All sources inside the same directory contribute to the same file (per-directory aggregation).
@@ -187,7 +187,7 @@ local function project_components(source, scan)
-- Only `MipsAtomComp_(ac_X)` (kind="comp_bare") and `MipsAtomComp_Proc_(ac_X, ...)` (kind="comp_proc")
-- are COMPONENTS — they get inlined via `mac_<name>` aliases inside atom bodies.
-- `MipsAtom_Proc_` (kind="atom_proc") is an ATOM (ends with `mac_yield()`); it gets emitted via
-- `tb_emit(tb, code_<name>)` (linker symbol), NOT inlined as a macro. Including `atom_proc` here
-- `tb_emit` of the C ident, NOT inlined as a macro. Including `atom_proc` here
-- would incorrectly emit `mac_<name>` aliases for atoms, polluting `gen/macs.h`.
-- See `docs/duffle_dsl_primer.md` §"mac_* aliases" for the contract.
if a.kind == "comp_bare" or a.kind == "comp_proc" then
@@ -393,6 +393,10 @@ end
--- @param cache table<string, integer>
--- @return integer
local function gp0_contrib_rec(name, comp_by_name, cache)
if name:match("^insert_ot_tag") then
cache[name] = 0
return 0
end
if cache[name] ~= nil then return cache[name] end
cache[name] = -1
local cc = comp_by_name[name]
@@ -408,8 +412,15 @@ local function gp0_contrib_rec(name, comp_by_name, cache)
-- Nested `mac_X(...)` call: recurse.
local nested = ident:sub(MAC_PREFIX_LEN + 1)
n = n + gp0_contrib_rec(nested, comp_by_name, cache)
elseif ident == "gte_sw" then
n = n + 1
elseif ident == "store_word" or ident == "store_half" or ident == "store_byte" then
if trimmed:find("R_PrimCursor", 1, true) then
if trimmed:find("R_PrimCursor", 1, true)
or trimmed:find("O_(Poly_", 1, true)
or trimmed:find("r_prim_cursor", 1, true)
or trimmed:find("r_primitive_cursor", 1, true)
or trimmed:find("r_base", 1, true)
then
n = n + 1
end
end
+2 -1
View File
@@ -1,7 +1,8 @@
--- passes/offsets.lua — Branch-offset generator.
---
--- Reads the pre-scanned SourceScan payload (produced once upstream by `duffle.scan_source`)
--- for `MipsAtom_(name)` and `MipsCode code_<name>` declarations, computes the word offset
--- for `MipsAtom_(name)` and leftover `MipsCode code_*` declarations, computes the word offset
--- (ELF symbol is the C ident; raw `code_*` is leftover, not the atom rule)
--- from each `atom_offset(F, T)` marker to its target `atom_label(T)` declaration, and emits
--- `gen/offsets.h` with one `#define _atom_offset_F_T = N` per branch.
---
+129 -3
View File
@@ -419,6 +419,29 @@ local function render_section_annotations(add, view)
add("")
end
local function render_section_component_annotations(add, view)
local rows = {}
for _, src in ipairs(view.sources) do
for _, info in ipairs((src.scan and src.scan.component_atom_infos) or {}) do
rows[#rows + 1] = {
source = source_basename(src.path),
line = info.info_line or 0,
name = info.atom_name or "?",
reads = (#(info.reads or {}) > 0 and table.concat(info.reads, ",")) or "",
writes = (#(info.writes or {}) > 0 and table.concat(info.writes, ",")) or "",
}
end
end
if #rows == 0 then add("_(none)_"); add(""); return end
add("| source | line | name | reads | writes |")
add("|--------|------|------|-------|--------|")
for _, r in ipairs(rows) do
add(string.format("| %s | %d | %s | %s | %s |",
r.source, r.line, r.name, r.reads, r.writes))
end
add("")
end
local function render_section_binds(add, view)
local wrote = false
for _, src in ipairs(view.sources) do
@@ -610,21 +633,123 @@ local function render_section_relations(add, view)
if not wrote then add("_(none)_"); add("") end
end
local HIDDEN_UNLESS_WRITTEN = {
R_AT = true, R_TapePtr = true, R_AtomJmp = true,
}
local PHYSICAL_GPR = {
R_T0 = true, R_T1 = true, R_T2 = true, R_T3 = true,
R_T4 = true, R_T5 = true, R_T6 = true, R_T7 = true,
R_V0 = true, R_V1 = true,
}
local function encoder_wrote_key(atom, key)
for _, ev in ipairs((atom.paths and atom.paths.word_events) or {}) do
for _, dest in pairs(ev.gpr_keys or {}) do
if dest == key then return true end
end
end
return false
end
local function written_name_for(key, atom)
local slot = key:match("^reguse:.+:(.+)$")
if slot then
local param = atom.reg_use_param_name
if param and param ~= "" then return param .. "." .. slot end
return slot
end
return key
end
local function aliases_for_key(key, atom, view)
local slot = key:match("^reguse:.+:(.+)$")
if not slot then return "" end
local schema_name = atom.reg_use_schema_name
local schema = view.corpus and view.corpus.reg_use_schemas and view.corpus.reg_use_schemas[schema_name]
if not schema then return "" end
for _, s in ipairs(schema.slots or {}) do
if s.name == slot then
local names = {}
for _, alias in ipairs(s.aliases or {}) do
if alias ~= slot then names[#names + 1] = alias end
end
if #names == 0 then
if s.aliases and #s.aliases > 0 then return table.concat(s.aliases, ", ") end
return ""
end
return table.concat(names, ", ")
end
end
return ""
end
local function physical_for_key(key, atom, view)
if PHYSICAL_GPR[key] then return key end
local corpus = view.corpus or {}
local alias = (corpus.register_alias_registry or {})[key]
if type(alias) == "table" then
local phys = alias.physical or alias.gpr or alias.code_name
if type(phys) == "string" and PHYSICAL_GPR[phys] then return phys end
if type(alias.name) == "string" and PHYSICAL_GPR[alias.name] then return alias.name end
elseif type(alias) == "string" and PHYSICAL_GPR[alias] then
return alias
end
local atom_map = (corpus.atom_auto_regs or {})[atom.name]
if type(atom_map) == "table" then
local slot = key:match("^reguse:.+:(.+)$") or key
local bound = atom_map[slot] or atom_map["R_" .. slot]
if type(bound) == "string" and PHYSICAL_GPR[bound] then return bound end
end
return ""
end
local function last_relation_for(key, atom)
local last = nil
for _, rel in ipairs((atom.paths and atom.paths.relations) or {}) do
local dest = rel.destination or rel.producer_destination
if dest == key then last = rel end
end
if not last then return "" end
local sem = last.semantic or "?"
local a = last.producer_word
local b = last.consumer_word
if a and b then return string.format("%s w%s→%s", sem, tostring(a), tostring(b)) end
return sem
end
local function render_section_forward(add, view)
local wrote = false
for _, a in ipairs(view.decls) do
local gpr = a.paths and a.paths.forward_state and a.paths.forward_state.gpr_values
local keys = {}
for k in pairs(gpr or {}) do
if k ~= "R_0" then keys[#keys + 1] = k end
if k == "R_0" then
-- hidden
elseif HIDDEN_UNLESS_WRITTEN[k] and not encoder_wrote_key(a, k) then
-- hidden
else
keys[#keys + 1] = k
end
end
if #keys > 0 then
wrote = true
add("### " .. a.name)
add("| written | aliases | physical | lattice | last relation |")
add("|---|---|---|---|---|")
table.sort(keys)
for _, k in ipairs(keys) do
local slot = gpr[k]
add(string.format("- `%s` %s", k, (slot and slot.kind) or "unknown"))
local lattice = ""
if slot and slot.kind == "constant" then
lattice = tostring(slot.value)
end
add(string.format("| `%s` | %s | %s | %s | %s |",
written_name_for(k, a),
aliases_for_key(k, a, view),
physical_for_key(k, a, view),
lattice,
last_relation_for(k, a)))
end
add("")
end
@@ -637,6 +762,7 @@ local SECTION_RENDERERS = {
{ header = "## Components", render = render_section_components },
{ header = "## RegUse schemas", render = render_section_reguse },
{ header = "## Annotations", render = render_section_annotations },
{ header = "## Component annotations", render = render_section_component_annotations },
{ header = "## Binds_* structs", render = render_section_binds },
{ header = "## Phases / views / ctx", render = render_section_phases },
{ header = "## Register aliases", render = render_section_aliases },
@@ -644,7 +770,7 @@ local SECTION_RENDERERS = {
{ header = "## Collisions", render = render_section_collisions },
{ header = "## Findings", render = render_section_findings },
{ header = "## Relations", render = render_section_relations },
{ header = "## Forward GPR", render = render_section_forward },
{ header = "## GPR model", render = render_section_forward },
}
--- Render the consolidated per-module markdown (`build/<module>.atom_meta_report.md`).
+263 -69
View File
@@ -416,31 +416,51 @@ local function walk_body_fields(body, build_field)
return fields
end
-- Parse the `<type> <field>;` declarations from a Struct_ body.
-- Parse the `<type> <field>[, <field>...];` declarations from a Struct_ body.
-- After the type and `*` chain, keep reading `, ident` until `;`.
-- Same type, same pointer depth for every name on that list.
-- Returns the raw fields array with `{name, type_name, pointer_depth}` only (NO offset / byte_size).
-- The propagation pass `resolve_struct_field_sizes` walks each struct's fields AFTER type resolution and populates offset + byte_size in place.
-- Returns (fields). The aggregate byte_count is computed in the propagation pass (it depends on whether every field's type resolved).
local function parse_struct_body_fields(body)
return walk_body_fields(body, function(type_name, type_end, after_type)
-- Parse the trailing `*` chain to derive pointer_depth.
local depth, cursor = 0, after_type
while cursor <= #body and body:sub(cursor, cursor) == "*" do
depth = depth + 1
cursor = cursor + 1
cursor = duffle.skip_ws_and_cmt(body, cursor)
local fields = {}
local body_pos = 1
local body_len = #body
while body_pos <= body_len do
body_pos = duffle.skip_ws_and_cmt(body, body_pos)
if body_pos > body_len then break end
local type_name, type_end = duffle.read_ident(body, body_pos)
if not type_name then
body_pos = body_pos + 1
else
local depth, cursor = 0, duffle.skip_ws_and_cmt(body, type_end)
while cursor <= body_len and body:sub(cursor, cursor) == "*" do
depth = depth + 1
cursor = duffle.skip_ws_and_cmt(body, cursor + 1)
end
while cursor <= body_len do
local field_ident, field_end = duffle.read_ident(body, cursor)
if not field_ident then break end
fields[#fields + 1] = {
name = field_ident,
type_name = type_name,
pointer_depth = depth,
offset = nil,
byte_size = nil,
}
cursor = duffle.skip_ws_and_cmt(body, field_end)
if body:sub(cursor, cursor) == "," then
cursor = duffle.skip_ws_and_cmt(body, cursor + 1)
else
break
end
end
if cursor <= body_len and body:sub(cursor, cursor) == ";" then
cursor = cursor + 1
end
body_pos = cursor
end
-- Read the field ident immediately after the type chain.
local field_ident, field_end = duffle.read_ident(body, cursor)
if not field_ident then return nil, type_end + 1 end
return {
name = field_ident,
type_name = type_name,
pointer_depth = depth,
-- offset + byte_size filled by resolve_struct_field_sizes
offset = nil,
byte_size = nil,
}, field_end
end)
end
return fields
end
-- Parse the `Enum_(<underlying>, <name>) { <body> }` body for entries.
@@ -1299,33 +1319,6 @@ local function parse_atom_info_after_decl(source, after_paren, raw_name, line_of
return info_after
end
--- Parse `atom_info(...)` immediately before `before_pos` (the MipsAtom_Proc_ token).
local function parse_atom_info_before(source, before_pos, raw_name, line_of, out, dest)
local i = before_pos - 1
while i >= 1 and source:sub(i, i):match("%s") do i = i - 1 end
if source:sub(i, i) ~= ")" then return end
local depth = 0
local j = i
while j >= 1 do
local c = source:sub(j, j)
if c == ")" then
depth = depth + 1
elseif c == "(" then
depth = depth - 1
if depth == 0 then break end
end
j = j - 1
end
if j < 1 then return end
local k = j - 1
while k >= 1 and source:sub(k, k):match("%s") do k = k - 1 end
local ident_end = k
while k >= 1 and source:sub(k, k):match("[%w_]") do k = k - 1 end
local ident_start = k + 1
if source:sub(ident_start, ident_end) ~= "atom_info" then return end
parse_atom_info_after_decl(source, ident_start, raw_name, line_of, out, dest)
end
--- Parse: `MipsAtom_(<name>) [atom_info(<binds>, <reads>, <writes>)] { <body> }`
--- @param source string
--- @param pos integer
@@ -1466,10 +1459,15 @@ local function parse_mips_atom_proc(source, pos, ident_end, line_of, out)
-- The atom name is the preceding function ident as written
-- (`internal MipsAtom* X(...)`). The first macro arg is the arena.
local raw_name, args_inner, func_ident = duffle.find_atom_proc_decl_for(source, open_paren, MIPS_ATOM_PTR_LEN)
local raw_name, args_inner, func_ident, after_func_paren =
duffle.find_atom_proc_decl_for(source, open_paren, MIPS_ATOM_PTR_LEN)
if not raw_name then raw_name = "?" end
local name = strip_ac_prefix(raw_name)
parse_atom_info_before(source, pos, name, line_of, out, out.atom_infos)
if after_func_paren then
parse_atom_info_after_decl(source, after_func_paren, name, line_of, out, out.atom_infos)
else
parse_atom_info_after_decl(source, pos, name, line_of, out, out.atom_infos)
end
local reg_use_schema_name = nil
local reg_use_param_name = nil
if args_inner then
@@ -1617,18 +1615,18 @@ local function register_typedef_alias(underlying, name, pos, line_of, out)
}
end
-- Layout of Reg_<type> only. Never the C data struct (V3_S4 has a pad field).
local REG_ALLOC_FIELDS = {
Reg_V3_S4 = { "x", "y", "z" },
Reg_P3_S4 = { "x", "y", "z" },
Reg_V3_S2 = { "x", "y", "z" },
}
local parse_reg_use_schema_body
local function fields_for_reg_type(type_name, type_registry)
local reg_name = "Reg_" .. type_name
local entry = type_registry and type_registry[reg_name]
if entry and entry.fields and #entry.fields > 0 then
local names = {}
for _, field in ipairs(entry.fields) do
if field.name then names[#names + 1] = field.name end
end
if #names > 0 then return names end
end
if entry and entry.body and parse_reg_use_schema_body then
local schema = parse_reg_use_schema_body(entry.body, type_registry)
if schema and schema.slots then
@@ -1639,10 +1637,13 @@ local function fields_for_reg_type(type_name, type_registry)
if #names > 0 then return names end
end
end
return REG_ALLOC_FIELDS[reg_name]
return nil
end
parse_reg_use_schema_body = function(body, type_registry)
parse_reg_use_schema_body = function(body, type_registry, opts)
opts = opts or {}
local require_types = opts.require_types == true
local pending = false
local slots = {}
local alias_to_slot = {}
local slot_names = {}
@@ -1789,10 +1790,14 @@ parse_reg_use_schema_body = function(body, type_registry)
errors[#errors + 1] = { kind = "reguse_malformed" }
return nil, errors
end
typed_fields = fields_for_reg_type(duffle.trim(type_inner), type_registry)
local type_ident = duffle.trim(type_inner)
typed_fields = fields_for_reg_type(type_ident, type_registry)
if not typed_fields then
errors[#errors + 1] = { kind = "reguse_malformed" }
return nil, errors
if require_types then
errors[#errors + 1] = { kind = "reguse_unknown_reg_type", type_name = type_ident }
else
pending = true
end
end
after = duffle.skip_ws_and_cmt(body, after_paren)
end
@@ -1808,11 +1813,13 @@ parse_reg_use_schema_body = function(body, type_registry)
return nil, errors
end
for _, n in ipairs(names) do
if typed_fields then
for _, field in ipairs(typed_fields) do
local path = n .. "." .. field
if not add_alias(path, path) then return nil, errors end
if not add_slot(path, { path }, readonly) then return nil, errors end
if first == "Reg_" then
if typed_fields then
for _, field in ipairs(typed_fields) do
local path = n .. "." .. field
if not add_alias(path, path) then return nil, errors end
if not add_slot(path, { path }, readonly) then return nil, errors end
end
end
else
if not add_alias(n, n) then return nil, errors end
@@ -1827,10 +1834,15 @@ parse_reg_use_schema_body = function(body, type_registry)
::continue::
end
if #slots == 0 then
errors[#errors + 1] = { kind = "reguse_malformed" }
if pending and not require_types then
return { slots = slots, alias_to_slot = alias_to_slot, pending = true }, errors
end
if #errors == 0 then
errors[#errors + 1] = { kind = "reguse_malformed" }
end
return nil, errors
end
return { slots = slots, alias_to_slot = alias_to_slot }, errors
return { slots = slots, alias_to_slot = alias_to_slot, pending = pending }, errors
end
--- Parse: `typedef` declarations.
@@ -2231,6 +2243,137 @@ local DECL_PARSERS = {
-- Only the bare `atom_dbg_skip` marker reaches `parse_dbg_skip_marker`.
-- Unknown identifiers follow the same unrelated-token path as every other unsupported source token.
local TAPE_SKIP_MACROS = {
MipsAtom_ = true,
MipsAtom_Proc_ = true,
MipsAtomComp_ = true,
MipsAtomComp_Proc_ = true,
MipsAtomComp_ProcMap_ = true,
Struct_ = true,
Enum_ = true,
}
local function collect_addrs_assigns(text)
local addrs = {}
local pos = 1
local n = #text
while pos <= n do
pos = duffle.skip_ws_and_cmt(text, pos)
if pos > n then break end
local ident, ident_end = duffle.read_ident(text, pos)
if ident == "addrs" then
local after = duffle.skip_ws_and_cmt(text, ident_end)
if text:sub(after, after) == "[" then
local inner, after_br = duffle.read_brackets(text, after)
local idx = inner and tonumber(duffle.trim(inner))
after_br = duffle.skip_ws_and_cmt(text, after_br or after)
if idx and text:sub(after_br, after_br) == "=" then
local rhs = duffle.skip_ws_and_cmt(text, after_br + 1)
local rhs_ident = duffle.read_ident(text, rhs)
if rhs_ident then addrs[idx] = rhs_ident end
pos = rhs
else
pos = after_br or (after + 1)
end
else
pos = ident_end
end
elseif ident then
pos = ident_end
else
pos = pos + 1
end
end
return addrs
end
local function collect_tb_emits(body, addrs)
local names = {}
local pos = 1
local n = #body
while pos <= n do
pos = duffle.skip_ws_and_cmt(body, pos)
if pos > n then break end
local ident, ident_end = duffle.read_ident(body, pos)
if ident == "tb_emit_" or ident == "tb_emit" then
local after = duffle.skip_ws_and_cmt(body, ident_end)
if body:sub(after, after) == "(" then
local inner, after_p = duffle.read_parens(body, after)
local name
if ident == "tb_emit_" then
name = duffle.trim(inner or ""):match("^([%w_]+)")
else
local args = duffle.split_top_level_commas(inner or "")
local last = duffle.trim(args[#args] or "")
local idx = last:match("^addrs%s*%[%s*(%d+)%s*%]$")
if idx then
name = addrs[tonumber(idx)]
else
name = last:match("([%w_]+)$")
end
end
if name then names[#names + 1] = name end
pos = after_p or (after + 1)
else
pos = ident_end
end
elseif ident then
pos = ident_end
else
pos = pos + 1
end
end
return names
end
-- Linear appearance order of tb_emit / tb_emit_ in each C function body.
-- Commented-out emits are skipped by skip_ws_and_cmt. No C if/loop CFG.
local function scan_tape_chains(source)
local addrs = collect_addrs_assigns(source)
local chains = {}
local pos = 1
local n = #source
while pos <= n do
pos = duffle.skip_ws_and_cmt(source, pos)
if pos > n then break end
local ident, ident_end = duffle.read_ident(source, pos)
if ident and TAPE_SKIP_MACROS[ident] then
local after = duffle.skip_ws_and_cmt(source, ident_end)
if source:sub(after, after) == "(" then
local _, after_p = duffle.read_parens(source, after)
after = duffle.skip_ws_and_cmt(source, after_p or after)
end
if source:sub(after, after) == "{" then
local _, after_b = duffle.read_braces(source, after)
pos = after_b or (after + 1)
else
pos = after
end
elseif ident then
local after = duffle.skip_ws_and_cmt(source, ident_end)
if source:sub(after, after) == "(" then
local _, after_p = duffle.read_parens(source, after)
after = duffle.skip_ws_and_cmt(source, after_p or after)
if source:sub(after, after) == "{" then
local body, after_b = duffle.read_braces(source, after)
local names = collect_tb_emits(body or "", addrs)
if #names > 0 then
chains[#chains + 1] = names
end
pos = after_b or (after + 1)
else
pos = after
end
else
pos = ident_end
end
else
pos = pos + 1
end
end
return chains
end
-- ════════════════════════════════════════════════════════════════════════════
-- The single source walker
-- ════════════════════════════════════════════════════════════════════════════
@@ -2343,6 +2486,7 @@ local function scan_source(source, source_file, code_macros, code_macro_bodies)
-- Runs AFTER the source walk so all typedef / Struct_ / Enum_ declarations have been parsed into `out.type_name_registry`.
-- Mutates each entry's `byte_size` field in place; fields with pointer_depth > 0 already carry byte_size = 4 from parse time and are unaffected.
propagate_type_sizes(out)
out.tape_chains = scan_tape_chains(source)
return out
end
@@ -2511,6 +2655,7 @@ local function merge_corpus_registries(corpus)
corpus.collisions = corpus.collisions or {}
corpus.reg_use_schemas = corpus.reg_use_schemas or {}
corpus.reg_use_errors = corpus.reg_use_errors or {}
corpus.tape_chains = corpus.tape_chains or {}
-- Replace the existing corpus collections with empty tables so a re-run on the same corpus produces identical state (deterministic merge).
-- This is safe because M.run is the only writer to these tables within a single orchestrator invocation.
@@ -2518,6 +2663,7 @@ local function merge_corpus_registries(corpus)
"register_alias_registry", "type_name_registry", "binds_by_name",
"atoms_by_name", "atom_views", "atom_ctxs", "atom_phases",
"atom_infos", "component_atom_infos", "collisions", "reg_use_schemas", "reg_use_errors",
"tape_chains",
}) do
corpus[key] = {}
end
@@ -2622,6 +2768,53 @@ local function merge_corpus_registries(corpus)
for _, err in ipairs(scan.reg_use_errors or {}) do
corpus.reg_use_errors[#corpus.reg_use_errors + 1] = err
end
for _, chain in ipairs(scan.tape_chains or {}) do
corpus.tape_chains[#corpus.tape_chains + 1] = chain
end
end
end
end
local SCHEMA_BODY_ERROR = {
reguse_malformed = true,
reguse_unknown_reg_type = true,
reguse_duplicate_alias = true,
reguse_duplicate_slot = true,
reguse_const_reg_spelling = true,
reguse_mixed_const = true,
}
-- Re-parse every RegUse_* body against the merged type_name_registry.
-- Scan-time expansion still runs when Reg_T is in the same source.
-- Missing Reg_T after merge is reguse_unknown_reg_type, not a fallback table.
local function resolve_reg_use_schemas(corpus)
local kept = {}
for _, err in ipairs(corpus.reg_use_errors or {}) do
if not SCHEMA_BODY_ERROR[err.kind] then
kept[#kept + 1] = err
end
end
corpus.reg_use_errors = kept
for name, type_entry in pairs(corpus.type_name_registry or {}) do
if name:sub(1, 7) == "RegUse_" and type_entry.body then
local fresh, errs = parse_reg_use_schema_body(
type_entry.body, corpus.type_name_registry, { require_types = true })
if fresh then
fresh.name = name
local old = corpus.reg_use_schemas[name]
fresh.source_file = (old and old.source_file) or type_entry.source_file
fresh.source_line = (old and old.source_line) or type_entry.source_line
corpus.reg_use_schemas[name] = fresh
else
corpus.reg_use_schemas[name] = nil
end
for _, err in ipairs(errs or {}) do
err.schema_name = name
err.source_file = type_entry.source_file
err.source_line = type_entry.source_line
corpus.reg_use_errors[#corpus.reg_use_errors + 1] = err
end
end
end
end
@@ -2710,6 +2903,7 @@ function M.run(ctx)
-- Merge per-source scans into the corpus registries (see merge_corpus_registries for first-wins + collision discipline).
merge_corpus_registries(corpus)
resolve_reg_use_schemas(corpus)
-- code_macros and code_macro_bodies are function-local; the GC reclaims them on M.run return.
return { outputs = {}, errors = {}, warnings = {} }
+396 -110
View File
@@ -340,7 +340,9 @@ local function classify_tokens(tokens)
local shape = ident:match("^mac_format_([%w_]+)_color$")
if shape then mac_format_shape = shape end
if ident:match("^mac_gte_store_[%w_]+$") then is_gte_store = true end
if ident:match("^mac_insert_ot_tag_[%w_]+$") then is_ot_tag = true end
if ident == "mac_insert_ot_tag" or ident:match("^mac_insert_ot_tag_[%w_]+$") then
is_ot_tag = true
end
-- O_(<arg1>, <arg2>) / S_(<arg>) captures (used by check_abi_handoff).
-- Cheap pattern match — anchored, fails fast on non-matching tokens.
@@ -1782,17 +1784,59 @@ local function check_yield_load_tail_pairing(atom, _pipe_ctx, findings)
end
-- ── Rule 2: `mac_yield_tail()` is valid if every path that reaches it already ran `mac_yield_load()`.
-- MIPS delay slot always runs. Successors skip the BD token for control flow,
-- but the yield walk still counts that token as executed.
local function load_covers_tail(tail_idx)
for i = 1, tail_idx - 1 do
if tc[i].ident == "mac_yield_load" then
local prev_i = skip_delay(i - 1, -1)
local prev = prev_i and tc[prev_i] or nil
if prev and (prev.is_branch or prev.is_atom_label) then
return true
end
local labels = {}
for i = 1, n do
if tc[i].is_atom_label and tc[i].label_name then
labels[tc[i].label_name] = i
end
end
return false
local function is_load(idx)
return tc[idx] and tc[idx].ident == "mac_yield_load"
end
local reached_without = false
local reached_any = false
local path_n = 0
local MAX_PATHS = 64
local function dfs(idx, saw_load, visited)
if path_n >= MAX_PATHS then return end
if visited[idx] then return end
local vis = {}
for k, v in pairs(visited) do vis[k] = v end
vis[idx] = true
local saw = saw_load or is_load(idx)
if tc[idx].is_branch and idx + 1 <= n then
saw = saw or is_load(idx + 1)
end
if idx == tail_idx then
path_n = path_n + 1
reached_any = true
if not saw then reached_without = true end
return
end
if tc[idx].is_yield or tc[idx].is_terminal_jump then
return
end
if tc[idx].is_branch then
if not tc[idx].is_unconditional_jump and idx + 2 <= n then
dfs(idx + 2, saw, vis)
end
local label = tc[idx].branch_label
if label and labels[label] then
local dest = labels[label] + 1
if dest <= n then dfs(dest, saw, vis) end
end
return
end
if idx + 1 <= n then
dfs(idx + 1, saw, vis)
end
end
dfs(1, false, {})
if not reached_any then return true end
return not reached_without
end
for tok_idx = 1, n do
@@ -1951,6 +1995,7 @@ local function check_gpu_portstore_shape(atom, pipe_ctx, findings)
local contrib = 0
local saw_format = false
local saw_prim_write = false
local saw_tag = false
-- Reads from tc_entry fields pre-computed by classify_tokens (R3 lift).
-- Eliminates 4 per-token string matches (mac_format_X_color + mac_gte_store_<shape> + mac_insert_ot_tag_<shape> + R_PrimCursor)
@@ -1981,30 +2026,55 @@ local function check_gpu_portstore_shape(atom, pipe_ctx, findings)
local comp = pipe_ctx.components_by_name[bare]
local n = comp and comp.gp0_contrib
if n then contrib = contrib + n end
-- insert_ot_tag writes the packet tag. Count it once when the atom
-- has no raw O_(Poly_*, tag) store.
if not saw_tag then
contrib = contrib + 1
saw_tag = true
end
end
if tc_entry.writes_r_prim_cursor then
saw_prim_write = true
end
-- A raw store to O_(Poly_*, tag) is the packet tag word, counted once.
if tc_entry.o_arg1 and tc_entry.o_arg1:match("^Poly_") and tc_entry.o_arg2 == "tag" then
if tc_entry.is_store_word or tc_entry.ident == "gte_sw" then
if not saw_tag then
contrib = contrib + 1
saw_tag = true
end
end
end
end
-- Token-name gp0_contrib is 0 when bodies use gte_sw. Count expanded prim-buffer stores.
-- Token-name gp0_contrib is 0 when bodies use uncounted stores.
-- Once gte_sw is taught, prefer that sum. Do not also count every PrimCursor store.
if contrib == 0 then
local seen_field = {}
for _, ev in ipairs(atom.paths.word_events or {}) do
local enc = ev.encoder or ""
if enc == "store_word" or enc == "store_half" or enc == "store_byte" or enc == "gte_sw" then
local text = (ev.call_text or "") .. " " .. (ev.root_call_text or "")
local hit = text:find("R_PrimCursor", 1, true)
if not hit then
for _, arg in ipairs(ev.args or {}) do
if tostring(arg):find("R_PrimCursor", 1, true) then
hit = true
break
if text:find("insert_ot_tag", 1, true) then
-- OT list mutation, not a packet word.
else
local field = text:match("O_%(([^%)]+)%)") or text
if not seen_field[field] then
local hit = text:find("R_PrimCursor", 1, true)
if not hit then
for _, arg in ipairs(ev.args or {}) do
if tostring(arg):find("R_PrimCursor", 1, true) then
hit = true
break
end
end
end
if hit then
seen_field[field] = true
contrib = contrib + 1
end
end
end
if hit then
contrib = contrib + 1
end
end
end
end
@@ -2429,8 +2499,14 @@ local function check_binds_no_substruct_deref(_src, pipe_ctx, findings)
local body_line = a.line + (line_in_body[tokens[ti].rel] or 0)
local type_entry = resolve_type_with_fields(type_name, type_registry, 1)
-- PSYQ opaques such as DisplayEnv have no fields table. Do not invent the layout.
local skip_opaque = type_name == "DisplayEnv" and (not type_entry or not type_entry.fields)
local no_fields = not type_entry or not type_entry.fields or #type_entry.fields == 0
local raw_entry = type_registry[type_name]
local is_typedef_to_struct = raw_entry
and raw_entry.kind == "typedef"
and raw_entry.underlying_type
and type_registry[raw_entry.underlying_type]
and type_registry[raw_entry.underlying_type].fields
local skip_opaque = no_fields and not is_typedef_to_struct
if skip_opaque then
-- leave this token
elseif not type_entry or not type_entry.fields then
@@ -2681,36 +2757,45 @@ local function check_gte_cr_TR_naming(atom, _pipe_ctx, findings)
end
end
local function check_gte_cr_alias_writes_xatom(_src, pipe_ctx, findings)
local slot_state = {}
for _, atom in ipairs(pipe_ctx.atoms or {}) do
atom.paths = atom.paths or {}
atom.paths.forward_state = atom.paths.forward_state or {}
local outgoing = {}
for slot, prev in pairs(slot_state) do
outgoing[slot] = prev
end
for _, w in ipairs(ctrl_writes_in_atom(atom)) do
local group = find_alias_pair_for(w.alias, duffle)
if group then
local slot = group[1]
local prev = slot_state[slot]
if prev and prev.alias ~= w.alias and prev.atom ~= atom.name then
findings[#findings + 1] = {
atom = atom.name or "",
line = w.line,
check = "gte_cr_alias_writes_xatom",
kind = "warning",
msg = string.format(
"atom '%s' writes %s to C2[%d]; atom '%s' already wrote %s"
, atom.name or "", w.alias, slot, prev.atom, prev.alias),
}
local function check_gte_cr_alias_writes_xatom(src, pipe_ctx, findings)
-- Walk tape chains once (first source only). Atoms in no chain stay per-atom.
local first = pipe_ctx.source_order and pipe_ctx.source_order[1]
if first and src ~= first then return end
local atoms_by_name = pipe_ctx.atoms_by_name or {}
for _, chain in ipairs(pipe_ctx.tape_chains or {}) do
local slot_state = {}
for _, name in ipairs(chain) do
local atom = atoms_by_name[name]
if atom then
atom.paths = atom.paths or {}
atom.paths.forward_state = atom.paths.forward_state or {}
local outgoing = {}
for slot, prev in pairs(slot_state) do
outgoing[slot] = prev
end
slot_state[slot] = { alias = w.alias, atom = atom.name, line = w.line }
outgoing[slot] = slot_state[slot]
for _, w in ipairs(ctrl_writes_in_atom(atom)) do
local group = find_alias_pair_for(w.alias, duffle)
if group then
local slot = group[1]
local prev = slot_state[slot]
if prev and prev.alias ~= w.alias and prev.atom ~= atom.name then
findings[#findings + 1] = {
atom = atom.name or "",
line = w.line,
check = "gte_cr_alias_writes_xatom",
kind = "warning",
msg = string.format(
"atom '%s' writes %s to C2[%d]; atom '%s' already wrote %s"
, atom.name or "", w.alias, slot, prev.atom, prev.alias),
}
end
slot_state[slot] = { alias = w.alias, atom = atom.name, line = w.line }
outgoing[slot] = slot_state[slot]
end
end
atom.paths.forward_state.ctrl_writes_by_slot = outgoing
end
end
atom.paths.forward_state.ctrl_writes_by_slot = outgoing
end
end
@@ -2738,74 +2823,103 @@ local function check_gte_packed_writes(atom, _pipe_ctx, findings)
end
local function check_ctc2_chain_source_preservation(atom, _pipe_ctx, findings)
local live = {}
local function mark_live(src, alias)
if src and alias and alias:match("^gte_cr_RT") then
live[src] = true
end
-- Fire only when a load sits before a later RT ctc2 that still names that GPR.
-- A load after the last RT ctc2 and before the command is a legal reload.
local events = (atom.paths and atom.paths.word_events) or {}
local function event_src(ev)
if ev.gpr_keys and ev.gpr_keys[1] then return ev.gpr_keys[1] end
local src = ev.args and ev.args[1]
if type(src) == "string" then src = src:match("^[%w_.]+") end
return src
end
for _, ev in ipairs((atom.paths and atom.paths.word_events) or {}) do
local enc = ev.encoder or ""
if enc == "gte_mv_to_ctrl_r" then
local src = ev.args and ev.args[1]
if type(src) == "string" then src = src:match("[%w_]+") end
local alias = ev.args and ev.args[2]
if type(alias) ~= "string" or not alias:match("^gte_cr_") then
alias = ctrl_alias_from_text(ev.call_text)
end
mark_live(src, alias)
elseif enc == "load_word" then
local dest = ev.args and ev.args[1]
if type(dest) == "string" then dest = dest:match("[%w_]+") end
if dest and live[dest] == true then
live[dest] = "clobbered"
end
elseif enc:match("^gte_cmdw_") then
for gpr, state in pairs(live) do
if state == "clobbered" then
findings[#findings + 1] = {
atom = atom.name or "",
line = ev.line or atom.line,
check = "ctc2_chain_source_preservation",
kind = "warning",
msg = string.format(
"atom '%s' reloads %s after ctc2 into RT and before %s"
, atom.name or "", gpr, enc),
}
end
end
live = {}
local function event_alias(ev)
local alias = ev.args and ev.args[2]
if type(alias) ~= "string" or not alias:match("^gte_cr_") then
alias = ctrl_alias_from_text(ev.call_text)
end
return alias
end
if not next((atom.paths and atom.paths.word_events) or {}) then
local pending = {}
for _, t in ipairs((atom.paths and atom.paths.tokens) or {}) do
local tok = t.tok or ""
local ident = tok:match("^([%w_]+)") or ""
if ident == "gte_mv_to_ctrl_r" then
mark_live(tok:match("%(%s*([%w_]+)"), ctrl_alias_from_text(tok))
elseif ident == "load_word" then
local dest = tok:match("%(%s*([%w_]+)")
if dest and live[dest] == true then live[dest] = "clobbered" end
elseif ident:match("^gte_cmdw_") then
for gpr, state in pairs(live) do
if state == "clobbered" then
pending[#pending + 1] = { gpr = gpr, enc = ident }
if #events > 0 then
for i, ev in ipairs(events) do
local enc = ev.encoder or ""
if enc == "load_word" then
local dest = event_src(ev)
if dest then
local earlier = false
for j = i - 1, 1, -1 do
local prev = events[j]
local prev_enc = prev.encoder or ""
if prev_enc:match("^gte_cmdw_") then break end
if prev_enc == "gte_mv_to_ctrl_r" then
local prev_src = event_src(prev)
local prev_alias = event_alias(prev)
if prev_src == dest and prev_alias and prev_alias:match("^gte_cr_RT") then
earlier = true
break
end
end
end
if earlier then
for j = i + 1, #events do
local later = events[j]
local later_enc = later.encoder or ""
if later_enc:match("^gte_cmdw_") then
break
end
if later_enc == "gte_mv_to_ctrl_r" then
local later_src = event_src(later)
local later_alias = event_alias(later)
if later_src == dest and later_alias and later_alias:match("^gte_cr_RT") then
findings[#findings + 1] = {
atom = atom.name or "",
line = ev.line or atom.line,
check = "ctc2_chain_source_preservation",
kind = "warning",
msg = string.format(
"atom '%s' reloads %s before a later ctc2 that still names it"
, atom.name or "", dest),
}
break
end
end
end
end
end
live = {}
end
end
for _, p in ipairs(pending) do
findings[#findings + 1] = {
atom = atom.name or "",
line = atom.line,
check = "ctc2_chain_source_preservation",
kind = "warning",
msg = string.format(
"atom '%s' reloads %s after ctc2 into RT and before %s"
, atom.name or "", p.gpr, p.enc),
}
return
end
local tokens = (atom.paths and atom.paths.tokens) or {}
for i, t in ipairs(tokens) do
local tok = t.tok or ""
local ident = tok:match("^([%w_]+)") or ""
if ident == "load_word" then
local dest = tok:match("%(%s*([%w_]+)")
if dest then
for j = i + 1, #tokens do
local later = tokens[j].tok or ""
local later_ident = later:match("^([%w_]+)") or ""
if later_ident:match("^gte_cmdw_") then
break
end
if later_ident == "gte_mv_to_ctrl_r" then
local later_src = later:match("%(%s*([%w_]+)")
local later_alias = ctrl_alias_from_text(later)
if later_src == dest and later_alias and later_alias:match("^gte_cr_RT") then
findings[#findings + 1] = {
atom = atom.name or "",
line = atom.line,
check = "ctc2_chain_source_preservation",
kind = "warning",
msg = string.format(
"atom '%s' reloads %s before a later ctc2 that still names it"
, atom.name or "", dest),
}
break
end
end
end
end
end
end
end
@@ -2893,7 +3007,6 @@ local function check_immediate_field_width(atom, pipe_ctx, findings)
end
end
end
::continue_token::
end
end
end
@@ -2901,6 +3014,167 @@ end
end
end
local SCRATCH_GPRS = {
R_T0 = true, R_T1 = true, R_T2 = true, R_T3 = true,
R_AT = true, R_V0 = true, R_V1 = true,
}
local function token_arg_list(tok)
local inner = (tok or ""):match("%b()")
if not inner then return {} end
return duffle.split_top_level_commas(inner:sub(2, -2))
end
local function arg_as_gpr(arg)
arg = duffle.trim(arg or "")
return arg:match("^R_[%w_]+$")
end
local function collect_gpr_traffic(tokens)
local reads, writes = {}, {}
for _, t in ipairs(tokens or {}) do
local tok = t.tok or t
local ident = (tok or ""):match("^([%w_]+)") or ""
if ident:sub(1, 4) ~= "mac_"
and not (duffle.DELAY_MARKERS and duffle.DELAY_MARKERS[ident])
and ident ~= "nop" and ident ~= "atom_label" and ident ~= "atom_offset"
then
local args = token_arg_list(tok)
local fx = (duffle.INSTRUCTION_GPR_EFFECTS or {})[ident]
if fx then
for _, pos in ipairs(fx.reads or {}) do
local g = arg_as_gpr(args[pos])
if g then reads[g] = true end
end
for _, pos in ipairs(fx.writes or {}) do
local g = arg_as_gpr(args[pos])
if g then writes[g] = true end
end
else
for _, arg in ipairs(args) do
local g = arg_as_gpr(arg)
if g then
reads[g] = true
writes[g] = true
end
end
end
end
end
return reads, writes
end
local function gpr_set_from_list(list)
local s = {}
for _, name in ipairs(list or {}) do
if type(name) == "string" then s[name] = true end
end
return s
end
local function gpr_set_eq(a, b)
for k in pairs(a) do if not b[k] then return false end end
for k in pairs(b) do if not a[k] then return false end end
return true
end
local function gpr_set_keys(s)
local keys = {}
for k in pairs(s) do keys[#keys + 1] = k end
table.sort(keys)
return keys
end
local function check_atom_calls_inferred_traffic(atom, pipe_ctx, findings)
if atom.kind ~= "atom" and atom.kind ~= "atom_proc" then return end
if is_runtime_helper(atom) then return end
local info = pipe_ctx.info_by_atom and pipe_ctx.info_by_atom[atom.name]
if not info then return end
if #(info.reads or {}) == 0 and #(info.writes or {}) == 0 then return end
local tokens = (atom.paths and atom.paths.tokens) or atom.body_tokens
local reads, writes = collect_gpr_traffic(tokens)
for _, t in ipairs(tokens or {}) do
local ident = ((t.tok or t) or ""):match("^([%w_]+)") or ""
if ident:sub(1, 4) == "mac_" then
local bare = ident:sub(5)
local idx = pipe_ctx.component_body_index and pipe_ctx.component_body_index[bare]
local comp = (pipe_ctx.components_by_name or {})[bare]
or (pipe_ctx.atoms_by_name or {})[bare]
local body_toks = (idx and idx.body_tokens)
or (comp and (comp.body_tokens or (comp.paths and comp.paths.tokens)))
local cr, cw = collect_gpr_traffic(body_toks)
for k in pairs(cr) do reads[k] = true end
for k in pairs(cw) do writes[k] = true end
end
end
local decl_r = gpr_set_from_list(info.reads)
local decl_w = gpr_set_from_list(info.writes)
local function keep_inferred(inferred, declared)
local out = {}
for k in pairs(inferred) do
if k == "R_0" then
if declared[k] then out[k] = true end
elseif SCRATCH_GPRS[k] then
if declared[k] then out[k] = true end
else
out[k] = true
end
end
return out
end
reads = keep_inferred(reads, decl_r)
writes = keep_inferred(writes, decl_w)
if not gpr_set_eq(decl_r, reads) or not gpr_set_eq(decl_w, writes) then
findings[#findings + 1] = {
atom = atom.name,
line = info.info_line or atom.line,
check = "atom_calls_inferred_traffic",
kind = "warning",
msg = string.format(
"atom '%s' declared [%s]/[%s] != inferred [%s]/[%s]",
atom.name,
table.concat(gpr_set_keys(decl_r), ","),
table.concat(gpr_set_keys(decl_w), ","),
table.concat(gpr_set_keys(reads), ","),
table.concat(gpr_set_keys(writes), ",")),
}
end
end
local function check_component_self_consistency(src, pipe_ctx, findings)
local first = pipe_ctx.source_order and pipe_ctx.source_order[1]
if first and src ~= first then return end
local infos = pipe_ctx.component_atom_infos or {}
local atoms_by_name = pipe_ctx.atoms_by_name or {}
for _, ai in ipairs(infos) do
local name = ai.atom_name or ai.name
local atom = name and atoms_by_name[name]
if atom and not atom.debug_skip then
local tokens = (atom.paths and atom.paths.tokens) or atom.body_tokens
local reads, writes = collect_gpr_traffic(tokens)
local decl_r = gpr_set_from_list(ai.reads)
local decl_w = gpr_set_from_list(ai.writes)
if not gpr_set_eq(decl_r, reads) or not gpr_set_eq(decl_w, writes) then
findings[#findings + 1] = {
atom = name,
line = ai.info_line or atom.line,
check = "component_self_consistency",
kind = "warning",
msg = string.format(
"component '%s' atom_reads/atom_writes [%s]/[%s] != body [%s]/[%s]",
name,
table.concat(gpr_set_keys(decl_r), ","),
table.concat(gpr_set_keys(decl_w), ","),
table.concat(gpr_set_keys(reads), ","),
table.concat(gpr_set_keys(writes), ",")),
}
end
end
end
end
-- CHECK_RULES — data-driven check dispatch (Muratori: data over control flow)
-- ════════════════════════════════════════════════════════════════════════════
@@ -2937,6 +3211,8 @@ local CHECK_RULES = {
{ name = "enum_alias_membership", per_source = check_enum_alias_membership },
{ name = "atom_type_consistency", per_source = check_atom_type_consistency },
{ name = "binds_no_substruct_deref", per_source = check_binds_no_substruct_deref },
{ name = "component_self_consistency", per_source = check_component_self_consistency },
{ name = "atom_calls_inferred_traffic", per_atom = check_atom_calls_inferred_traffic },
}
-- ════════════════════════════════════════════════════════════════════════════
@@ -2973,6 +3249,11 @@ local function build_corpus_pipe_ctx(ctx)
-- `MipsAtomComp_` body by `passes/components.lua::compute_components_metadata`.
-- Keyed by bare name (e.g. `format_f3_color`, `gte_store_f3`); the `mac_` prefix at call sites is stripped before lookup.
components_by_name = corpus.components or {},
atoms_by_name = corpus.atoms_by_name or {},
tape_chains = corpus.tape_chains or {},
source_order = corpus.source_order or {},
component_atom_infos = corpus.component_atom_infos or {},
atom_infos = corpus.atom_infos or {},
-- Corpus-wide ordered list of atom_info records (source-order + duplicates).
atom_infos_list = corpus.atom_infos or {},
-- Corpus-wide collisions (recorded by scan_source.merge_corpus_registries).
@@ -3031,6 +3312,11 @@ local function validate(ctx, src, corpus_pipe_ctx)
type_name_registry = corpus_pipe_ctx.type_name_registry,
-- Per-component metadata (cycle_cost + gp0_contrib) auto-derived from the original `MipsAtomComp_` body by `passes/components.lua::compute_components_metadata`.
components_by_name = corpus_pipe_ctx.components_by_name,
atoms_by_name = corpus_pipe_ctx.atoms_by_name,
tape_chains = corpus_pipe_ctx.tape_chains,
source_order = corpus_pipe_ctx.source_order,
component_atom_infos = corpus_pipe_ctx.component_atom_infos,
atom_infos_all = corpus_pipe_ctx.atom_infos,
}
-- Shared cross-source component-body index is owned by the corpus (`corpus.component_body_index`, populated by `passes/components.lua`).
-- Per-atom checks consume the corpus-owned index directly.