diff --git a/scripts/duffle.lua b/scripts/duffle.lua index 4813245..e19ecca 100644 --- a/scripts/duffle.lua +++ b/scripts/duffle.lua @@ -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 diff --git a/scripts/passes/components.lua b/scripts/passes/components.lua index c11519a..3ef8ea7 100644 --- a/scripts/passes/components.lua +++ b/scripts/passes/components.lua @@ -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_)` 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_` aliases inside atom bodies. -- `MipsAtom_Proc_` (kind="atom_proc") is an ATOM (ends with `mac_yield()`); it gets emitted via - -- `tb_emit(tb, code_)` (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_` 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 --- @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 diff --git a/scripts/passes/offsets.lua b/scripts/passes/offsets.lua index 1982272..c11ad3c 100644 --- a/scripts/passes/offsets.lua +++ b/scripts/passes/offsets.lua @@ -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_` 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. --- diff --git a/scripts/passes/report.lua b/scripts/passes/report.lua index 7a55b53..deff700 100644 --- a/scripts/passes/report.lua +++ b/scripts/passes/report.lua @@ -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/.atom_meta_report.md`). diff --git a/scripts/passes/scan_source.lua b/scripts/passes/scan_source.lua index 88e33e5..65b08df 100644 --- a/scripts/passes/scan_source.lua +++ b/scripts/passes/scan_source.lua @@ -416,31 +416,51 @@ local function walk_body_fields(body, build_field) return fields end --- Parse the ` ;` declarations from a Struct_ body. +-- Parse the ` [, ...];` 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_(, ) { }` 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_() [atom_info(, , )] { }` --- @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_ 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 = {} } diff --git a/scripts/passes/static_analysis.lua b/scripts/passes/static_analysis.lua index 273cd7e..565085e 100644 --- a/scripts/passes/static_analysis.lua +++ b/scripts/passes/static_analysis.lua @@ -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_(, ) / S_() 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_ + mac_insert_ot_tag_ + 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.