diff --git a/.vscode/tape-atom-syntax/atomasm-psx-0.3.0.vsix b/.vscode/tape-atom-syntax/atomasm-psx-0.3.0.vsix index e42f4f3..94f0d96 100644 Binary files a/.vscode/tape-atom-syntax/atomasm-psx-0.3.0.vsix and b/.vscode/tape-atom-syntax/atomasm-psx-0.3.0.vsix differ diff --git a/.vscode/tape-atom-syntax/classifier.js b/.vscode/tape-atom-syntax/classifier.js index d4ad0a3..4db0fef 100644 --- a/.vscode/tape-atom-syntax/classifier.js +++ b/.vscode/tape-atom-syntax/classifier.js @@ -49,7 +49,7 @@ const DSL_KEYWORDS = new Set([ "u1_r", "u2_r", "u4_r", "u8_r", "u1_v", "u2_v", "u4_v", "u8_v", ]); -const DELAY_SLOT_KEYWORDS = new Set(["LdSlot_", "BdSlot_"]); +const DELAY_SLOT_KEYWORDS = new Set(["LdSlot_", "BdSlot_", "DmaSlot_", "GteDelay_"]); const CONTROL_FLOW_PREFIXES = /^(?:branch_|jump_|call_)/; diff --git a/.vscode/tape-atom-syntax/syntaxes/tape_atom.tmLanguage.json b/.vscode/tape-atom-syntax/syntaxes/tape_atom.tmLanguage.json index fa768a2..d42df06 100644 --- a/.vscode/tape-atom-syntax/syntaxes/tape_atom.tmLanguage.json +++ b/.vscode/tape-atom-syntax/syntaxes/tape_atom.tmLanguage.json @@ -56,7 +56,7 @@ "name": "support.function.duffle.annotation" }, "delay-slots": { - "match": "\\b(LdSlot_|BdSlot_)\\b", + "match": "\\b(LdSlot_|BdSlot_|DmaSlot_|GteDelay_)\\b", "name": "keyword.operator.duffle.delayslot" }, "types": { diff --git a/.vscode/tape-atom-syntax/test/classifier.test.js b/.vscode/tape-atom-syntax/test/classifier.test.js index 9a67fb3..39d4895 100644 --- a/.vscode/tape-atom-syntax/test/classifier.test.js +++ b/.vscode/tape-atom-syntax/test/classifier.test.js @@ -105,6 +105,16 @@ test("document-local declarations override an empty workspace index", () => { assert.equal(byText(result, "mac_new_component")[0].type, "tapeComponentInstruction"); }); +test("delay slot markers share the tapeDelaySlot token", () => { + const source = "LdSlot_ nop, BdSlot_ nop, DmaSlot_ nop2, GteDelay_ nop"; + const result = classifyDocument(source, "C:/x/code/duffle/gte.atom.c", createIndex()); + + assert.equal(byText(result, "LdSlot_")[0].type, "tapeDelaySlot"); + assert.equal(byText(result, "BdSlot_")[0].type, "tapeDelaySlot"); + assert.equal(byText(result, "DmaSlot_")[0].type, "tapeDelaySlot"); + assert.equal(byText(result, "GteDelay_")[0].type, "tapeDelaySlot"); +}); + test("classifier returns ordered non-overlapping spans and partial malformed output", () => { const source = "atom_reads(R_A /* broken"; const result = classifyDocument(source, "C:/x/code/test.atom.c", createIndex()); diff --git a/scripts/duffle.lua b/scripts/duffle.lua index 12c4730..4813245 100644 --- a/scripts/duffle.lua +++ b/scripts/duffle.lua @@ -1014,14 +1014,20 @@ end -- Section 7: domain tables -- ════════════════════════════════════════════════════════════════════════════ --- The annotation DSL has been reduced to a single annotation macro: atom_info(atom_bind(Binds_X), atom_reads(...), atom_writes(...)) --- All phase / region / cadence / async / resource / group tokens have been dropped. --- They may be reintroduced later as optional sub-calls of atom_info; --- For now, the parser only recognizes atom_info + its three sub-calls (atom_bind, atom_reads, atom_writes). +-- atom_info sub-calls: atom_bind, atom_reads, atom_writes, atom_view, atom_reg_types, atom_ctx, atom_phase. M.TAPE_ATOM_MACROS = { ["atom_info"] = { kind = "info", binds = false }, } +-- Empty C macros that prefix the next encoder. Zero words. +-- BdSlot_ nop is one nop word. The marker is not the BD instruction. +M.DELAY_MARKERS = { + ["GteDelay_"] = true, + ["LdSlot_"] = true, + ["BdSlot_"] = true, + ["DmaSlot_"] = true, +} + -- GTE command-alias resolution table. -- -- Maps every source-side GTE command macro to its canonical short ident. @@ -1328,6 +1334,13 @@ M.GTE_CR_ALIAS_GROUPS = { { 26, { "gte_cr_BBK", "gte_cr_H" } }, -- background B vs projection plane distance H } +-- Packed RT slots named by the gte.h packed-slot comment. +-- 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. -- Maps each emitting-token ident to the set of GPR operand positions it reads. -- Covers the current encoder vocabulary (`code/duffle/mips.h` + `code/duffle/gte.h`); add rows here as new encoders land. @@ -2455,6 +2468,15 @@ local function _project_emission_inner(root_body_entry, ctx_table) pos = (next_pos > pos) and next_pos or (pos + 1) goto continue_loop end + if M.DELAY_MARKERS[ident] then + local arg_pos = nil + if consuming_encoder and consuming_paren then + arg_pos = count_top_level_commas(tok, consuming_paren + 1, pos) + 1 + end + emit_marker("delay", ident, nil, tok_line, nil, nil, consuming_encoder, arg_pos) + pos = after + goto continue_loop + end if ident ~= "atom_label" and ident ~= "atom_offset" then -- Ordinary ident; nothing to emit, step past the ident only. pos = after @@ -2601,9 +2623,18 @@ local function _project_emission_inner(root_body_entry, ctx_table) local function process_token(bt) local tok = M.trim(bt.tok or "") if tok == "" then return end - local ident = M.read_ident(tok, 1) or "?" + local ident, after = M.read_ident(tok, 1) + if not ident then ident = "?" end local _, args = token_ident_and_args(tok) local tok_line = line_of(body_off + bt.rel) or 0 + if M.DELAY_MARKERS[ident] then + emit_marker("delay", ident, nil, tok_line) + local rest = M.trim(tok:sub(after or (#tok + 1))) + if rest ~= "" then + process_token({ tok = rest, rel = bt.rel }) + end + return + end -- embedded markers live only in non-marker tokens. -- Pass `ident` as the consuming instruction so `emit_embedded_markers` can compute each marker's arg position + record the consuming_encoder for the offsets pass. -- Canonicalize `jump_rel` to `branch_equal` (its preprocessor-expanded form) so the `consuming_encoder` metadata in marker records is canonical. @@ -2751,6 +2782,7 @@ end --- `nop2` is normalized to encoder `nop` (per the spec). --- * `atom_label(F)` markers: one `label` item with `name = "F"`, `word_index = current word_idx`; zero-width (does NOT advance word_idx). --- * `atom_offset(B, T)` markers: one `offset` item with `name = "B"`, `target = "T"`, `word_index = current word_idx`; zero-width. +--- * Delay markers (`GteDelay_` / `LdSlot_` / `BdSlot_` / `DmaSlot_`): one `delay` item; zero-width. The following encoder is the next token. --- * `mac_X(...)` calls: emit `invoke_begin` (zero-width), recurse into the component body, emit `invoke_end` (zero-width). --- The component body's words land between the begin/end pair; one invocation record is allocated per call (monotonic ID per atom). --- * Unknown uncounted macros emit 1 opaque word + one warning per occurrence. @@ -2950,12 +2982,7 @@ function M.find_atom_proc_decl_for(source, before_pos, mips_atom_ptr_len) if source:sub(next_pos, next_pos) == "(" then local inner = M.read_parens(source, next_pos) if inner then - local proc_suffix = "_proc" - local atom_name = ident - if #ident > #proc_suffix and ident:sub(-#proc_suffix) == proc_suffix then - atom_name = ident:sub(1, #ident - #proc_suffix) - end - return atom_name, inner, ident + return ident, inner, ident end end -- ident not followed by "(" — it's a qualifier; skip it diff --git a/scripts/elf_dwarf.lua b/scripts/elf_dwarf.lua index b645721..1f0483a 100644 --- a/scripts/elf_dwarf.lua +++ b/scripts/elf_dwarf.lua @@ -612,13 +612,13 @@ function M.read_elf_sections(elf_path, section_names) end --- Read ELF symbol addresses by walking the `.symtab` + `.strtab` sections directly (no `nm` subprocess). ---- Returns a map `{name -> {addr, size_bytes}}` for every `code_` symbol. +--- Returns a map `{name -> {addr, size_bytes}}` for every defined symbol. --- --- **Conventions:** --- - ELF32 symtab entry = 16 bytes (`st_name:4 + st_value:4 + st_size:4 + st_info:1 + st_other:1 + st_shndx:2`); offsets within each entry are zero-based wire offsets. --- - Direct Lua `string.byte`/`string.sub`/`string.find` boundaries receive `+ 1`. --- - We filter on STB_GLOBAL (high nibble of st_info = 1) to match `nm`'s default (external symbols only). STB_WEAK excluded. ---- - The `code_` prefix is stripped (MipsAtom_ macros emit bare atom names, no `code_` prefix). +--- - Keys are the ELF symbol names as written (the C ident). --- - `st_size > 0` filter excludes undefined/imported symbols. --- --- @param elf_path Path diff --git a/scripts/gdb/gdb_tape_atoms.gdb b/scripts/gdb/gdb_tape_atoms.gdb index 54f0dbe..d047efb 100644 --- a/scripts/gdb/gdb_tape_atoms.gdb +++ b/scripts/gdb/gdb_tape_atoms.gdb @@ -16,7 +16,7 @@ define tape_atoms echo "[gdb_tape_atoms] STUB: run .\\build_psyq.ps1 to regenerate, then re-source this file." end document tape_atoms - List every tape atom symbol in the loaded ELF (code_) with its .rodata address and word count. + List every tape atom symbol in the loaded ELF with its .rodata address and word count. STUB state: runtime file not sourced. Run build_psyq.ps1 to regenerate. end diff --git a/scripts/passes/annotation.lua b/scripts/passes/annotation.lua index 04ec968..895d6f4 100644 --- a/scripts/passes/annotation.lua +++ b/scripts/passes/annotation.lua @@ -477,8 +477,8 @@ local function validate(ctx, src, corpus_pipe_ctx) -- Project the pre-scanned atoms to the AtomEntry shape this pass needs. local atoms = {} for _, a in ipairs(scan.atoms) do - if a.kind == "atom" then - atoms[#atoms + 1] = { line = a.line, name = a.raw_name } + if a.kind == "atom" or a.kind == "atom_proc" then + atoms[#atoms + 1] = { line = a.line, name = a.raw_name or a.name } end end diff --git a/scripts/passes/atoms_source_map.lua b/scripts/passes/atoms_source_map.lua index 1518d3b..92f2d8b 100644 --- a/scripts/passes/atoms_source_map.lua +++ b/scripts/passes/atoms_source_map.lua @@ -261,12 +261,12 @@ local function append_gdb_commands(lines, matched) for _, a in ipairs(matched) do -- gdb 12.1 quirk: literals in printf args require an attached target. -- Use the per-atom convenience vars set above as printf args. - lines[#lines + 1] = string.format(' printf " code_%%-32s @ 0x%%08x %%4d words\\n", $__atom_name_%d, $__atom_addr_%d, $__atom_words_%d', + lines[#lines + 1] = string.format(' printf " %%-32s @ 0x%%08x %%4d words\\n", $__atom_name_%d, $__atom_addr_%d, $__atom_words_%d', a.idx, a.idx, a.idx) end lines[#lines + 1] = "end" lines[#lines + 1] = "document tape_atoms" - lines[#lines + 1] = " List every tape atom symbol in the loaded ELF (code_) with .rodata addr + word count." + lines[#lines + 1] = " List every tape atom symbol in the loaded ELF with .rodata addr + word count." lines[#lines + 1] = "end" lines[#lines + 1] = "" @@ -285,10 +285,10 @@ local function append_gdb_commands(lines, matched) for _, a in ipairs(matched) do lines[#lines + 1] = string.format("define break_atom_%s", a.name) lines[#lines + 1] = string.format(" break *$__atom_addr_%d", a.idx) - lines[#lines + 1] = string.format(' printf " Breakpoint set at code_%s (0x%%08x)\\n", $__atom_addr_%d', a.name, a.idx) + lines[#lines + 1] = string.format(' printf " Breakpoint set at %s (0x%%08x)\\n", $__atom_addr_%d', a.name, a.idx) lines[#lines + 1] = "end" lines[#lines + 1] = string.format("document break_atom_%s", a.name) - lines[#lines + 1] = string.format(" Set a breakpoint at code_%s.", a.name) + lines[#lines + 1] = string.format(" Set a breakpoint at %s.", a.name) lines[#lines + 1] = "end" lines[#lines + 1] = "" end @@ -323,7 +323,7 @@ local function append_gdb_commands(lines, matched) -- Precompute end_addr (gdb 12.1's expression evaluator chokes on `addr + words*4`). lines[#lines + 1] = string.format(" set $__end_%d = $__atom_addr_%d + $__atom_words_%d * 4", a.idx, a.idx, a.idx) lines[#lines + 1] = string.format(" if $__pc >= $__atom_addr_%d && $__pc < $__end_%d", a.idx, a.idx) - lines[#lines + 1] = string.format(' printf "atom: code_%%s\\n", $__atom_name_%d', a.idx) + lines[#lines + 1] = string.format(' printf "atom: %%s\\n", $__atom_name_%d', a.idx) lines[#lines + 1] = ' printf "addr: 0x%08x\\n", $__pc' lines[#lines + 1] = string.format(" set $__word = ($__pc - $__atom_addr_%d) / 4", a.idx) lines[#lines + 1] = string.format(' printf "word: %%d/%%d\\n", $__word, $__atom_words_%d', a.idx) @@ -541,7 +541,7 @@ function M.render_atom_provenance(atom, wc, rel_path) return table.concat(lines, "\n") .. "\n" end ---- Pass entry. For each source that declares at least one `MipsAtom_(name)` / `MipsCode code_`, +--- Pass entry. For each source that declares at least one tape atom, --- emit two files in `/`: `.atoms.sourcemap.txt` (per-word call-site map) and `.atoms.provenance.txt` --- (per-word definition + body line, resolved via the outermost `mac_X(...)` invocation). --- When `ctx.flags.gdb_runtime` is true and `ctx.flags.elf_path` exists, also emit the post-link gdb script `/gdb_tape_atoms_runtime.gdb`. diff --git a/scripts/passes/dwarf_injection.lua b/scripts/passes/dwarf_injection.lua index b76b4bb..a0171e2 100644 --- a/scripts/passes/dwarf_injection.lua +++ b/scripts/passes/dwarf_injection.lua @@ -2,7 +2,7 @@ --- --- Reads the post-link ELF directly (io.open; walks the ELF32 section header table to find --- `.debug_info` + `.debug_abbrev` + `.debug_str` + `.debug_line` + `.debug_aranges` + `.debug_rnglists`), ---- APPENDS synthetic DWARF line-program sequences for every `code_` atom, EXTENDS the `.debug_aranges` +--- APPENDS synthetic DWARF line-program sequences for every tape atom, EXTENDS the `.debug_aranges` --- and main-CU range tables with the atom ranges, and INSERTS synthetic atom/component DIE children into the --- existing main compilation unit in `.debug_info` (no second compilation unit). --- Per-atom `DW_TAG_subprogram` + per-register `DW_TAG_variable` entries make @@ -1344,7 +1344,7 @@ local function build_new_abbrev() attr( DW_AT_name, DW_FORM_string) .. attr(DW_AT_low_pc, DW_FORM_addr) .. attr(DW_AT_high_pc, DW_FORM_addr) - .. attr(DW_AT_linkage_name, DW_FORM_string)) -- equals DW_AT_name; lets gdb's symbol-table lookup resolve to our subprogram (not the gcc global `code_` const U4 array) + .. attr(DW_AT_linkage_name, DW_FORM_string)) -- equals DW_AT_name; gdb resolves the subprogram, not the gcc global array local abbrev_variable = abbrev(ABBREV_VARIABLE, DW_TAG_variable, false, -- DW_CHILDREN_no attr( DW_AT_name, DW_FORM_string) @@ -1857,7 +1857,7 @@ local function build_inserted_children(main_cu_offset, main_cu_end_excl, atom_ta end -- 4) Emit per-atom DW_TAG_subprograms (children of main CU). - -- Subprogram names match nm symbols without a `code_` prefix. + -- Subprogram names match the written C ident (the ELF symbol). -- The gcc global `[]` is a DW_TAG_variable without children; our subprogram has the wave-context var children. -- gdb's symbol resolution picks our subprogram (it has low_pc/high_pc + children) over the gcc global for function-context lookups. for _, atom in ipairs(atom_table) do @@ -2313,5 +2313,6 @@ end M.compute_loclists_offsets_for_test = compute_loclists_offsets M.build_debug_loclists_section_for_test = build_debug_loclists_section M.tape_piece_size_for_test = tape_piece_size +M.build_atom_table_for_test = build_atom_table return M diff --git a/scripts/passes/report.lua b/scripts/passes/report.lua index cabec9a..7a55b53 100644 --- a/scripts/passes/report.lua +++ b/scripts/passes/report.lua @@ -259,6 +259,27 @@ local function slot_suffix(key) return key:match("([^:]+)$") end +local function decl_names(view) + local names = {} + for _, a in ipairs(view.decls or {}) do + if a.name then names[a.name] = true end + end + return names +end + +local function path_in_module(path, view) + if type(path) ~= "string" or path == "" then return false end + local norm = path:gsub("\\", "/") + local dir = (view.dir or ""):gsub("\\", "/") + if dir ~= "" and (norm == dir or norm:sub(1, #dir + 1) == dir .. "/") then + return true + end + for _, src in ipairs(view.sources or {}) do + if (src.path or ""):gsub("\\", "/") == norm then return true end + end + return false +end + local function build_module_view(dir, dir_sources, corpus) local decls = {} for _, src in ipairs(dir_sources or {}) do @@ -352,7 +373,16 @@ local function render_section_reguse(add, view) end add("") end - local errors = (view.corpus and view.corpus.reg_use_errors) or {} + local bound = {} + for _, schema in ipairs(view.schemas or {}) do + if schema.name then bound[schema.name] = true end + end + local errors = {} + for _, err in ipairs((view.corpus and view.corpus.reg_use_errors) or {}) do + if bound[err.schema_name] or path_in_module(err.source_file, view) then + errors[#errors + 1] = err + end + end if #errors > 0 then wrote = true add("### parse errors") @@ -394,12 +424,8 @@ local function render_section_binds(add, view) for _, src in ipairs(view.sources) do for _, b in ipairs((src.scan and src.scan.binds) or {}) do wrote = true - local line = b.line or 0 - if src.scan.line_of and type(b.line) == "number" then - line = src.scan.line_of(b.line) or b.line - end add(string.format("### %s (%s:%s, %s bytes)", - b.name, source_basename(src.path), tostring(line), tostring(b.bytes or "—"))) + b.name, source_basename(src.path), tostring(b.line or 0), tostring(b.bytes or "—"))) for _, f in ipairs(b.fields or {}) do add(string.format("- `+%s %s`", tostring(f.offset or "?"), f.name or "?")) end @@ -411,46 +437,75 @@ end local function render_section_phases(add, view) local corpus = view.corpus or {} - local wrote = false + local names = decl_names(view) + local wrote = false for phase, entry in pairs(corpus.atom_phases or {}) do - wrote = true - add(string.format("- phase `%s`: %s", phase, table.concat(entry.atoms or {}, ", "))) + local here = {} + for _, atom_name in ipairs(entry.atoms or {}) do + if names[atom_name] then here[#here + 1] = atom_name end + end + if #here > 0 then + wrote = true + add(string.format("- phase `%s`: %s", phase, table.concat(here, ", "))) + end end for name, entry in pairs(corpus.atom_views or {}) do - wrote = true - add(string.format("- view `%s` binds `%s`", name, entry.binds_name or "—")) + if names[name] then + wrote = true + add(string.format("- view `%s` binds `%s`", name, entry.binds_name or "—")) + end end for name, entry in pairs(corpus.atom_ctxs or {}) do - wrote = true - add(string.format("- ctx `%s` rbind `%s`", name, entry.rbind_atom or "—")) + if names[name] then + wrote = true + add(string.format("- ctx `%s` rbind `%s`", name, entry.rbind_atom or "—")) + end end if not wrote then add("_(none)_") end add("") end local function render_section_aliases(add, view) - local reg = (view.corpus and view.corpus.register_alias_registry) or {} local names = {} - for name in pairs(reg) do names[#names + 1] = name end + local seen = {} + for _, src in ipairs(view.sources or {}) do + for name, entry in pairs((src.scan and src.scan.register_alias_registry) or {}) do + if not seen[name] then + seen[name] = entry + names[#names + 1] = name + end + end + end table.sort(names) if #names == 0 then add("_(none)_"); add(""); return end add("| alias | type |") add("|-------|------|") for _, name in ipairs(names) do - local e = reg[name] + local e = seen[name] add(string.format("| %s | %s |", name, (e and e.default_type) or "—")) end add("") end local function render_section_autoreg(add, view) - local corpus = view.corpus or {} + local allowed = decl_names(view) + for phase, entry in pairs((view.corpus and view.corpus.atom_phases) or {}) do + for _, atom_name in ipairs(entry.atoms or {}) do + if allowed[atom_name] then allowed[phase] = true end + end + end local wrote = false + local seen = {} local function dump(label, table_map) local scopes = {} - for scope in pairs(table_map or {}) do scopes[#scopes + 1] = scope end + for scope in pairs(table_map or {}) do + if allowed[scope] and not seen[label .. "\0" .. scope] then + scopes[#scopes + 1] = scope + end + end table.sort(scopes) for _, scope in ipairs(scopes) do + seen[label .. "\0" .. scope] = true wrote = true local syms = {} for sym, gpr in pairs(table_map[scope] or {}) do @@ -464,16 +519,28 @@ local function render_section_autoreg(add, view) add(string.format("- %s `%s`: %s", label, scope, table.concat(syms, ", "))) end end + local corpus = view.corpus or {} dump("atom", corpus.atom_auto_regs) dump("phase", corpus.phase_auto_regs) + for _, src in ipairs(view.sources or {}) do + dump("atom", src.scan and src.scan.atom_auto_regs) + dump("phase", src.scan and src.scan.phase_auto_regs) + end if not wrote then add("_(none)_") end add("") end local function render_section_collisions(add, view) - local cols = (view.corpus and view.corpus.collisions) or {} - if #cols == 0 then add("_(none)_"); add(""); return end - for _, c in ipairs(cols) do + local rows = {} + for _, c in ipairs((view.corpus and view.corpus.collisions) or {}) do + local first = c.first_site or {} + local other = c.conflicting_site or {} + if path_in_module(first.path, view) or path_in_module(other.path, view) then + rows[#rows + 1] = c + end + end + if #rows == 0 then add("_(none)_"); add(""); return end + for _, c in ipairs(rows) do local first = c.first_site or {} local other = c.conflicting_site or {} add(string.format("- `%s` `%s` first %s:%s conflict %s:%s", @@ -547,11 +614,13 @@ 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 - if gpr and next(gpr) ~= nil then + local keys = {} + for k in pairs(gpr or {}) do + if k ~= "R_0" then keys[#keys + 1] = k end + end + if #keys > 0 then wrote = true add("### " .. a.name) - local keys = {} - for k in pairs(gpr) do keys[#keys + 1] = k end table.sort(keys) for _, k in ipairs(keys) do local slot = gpr[k] diff --git a/scripts/passes/scan_source.lua b/scripts/passes/scan_source.lua index 93211f1..88e33e5 100644 --- a/scripts/passes/scan_source.lua +++ b/scripts/passes/scan_source.lua @@ -1252,6 +1252,80 @@ local function parse_atom_dbg_reg_default(source, pos, ident_end, line_of, out) return after_paren end +--- Lookahead for `atom_info(...)` after a declaration's closing paren. +--- Records into `dest` (atom_infos or component_atom_infos). Returns the position after the info, or after_paren if none. +local function parse_atom_info_after_decl(source, after_paren, raw_name, line_of, out, dest) + local lookahead = duffle.skip_ws_and_cmt(source, after_paren) + local look_ident, look_end = duffle.read_ident(source, lookahead) + if look_ident ~= "atom_info" then return after_paren end + local info_open = duffle.skip_ws_and_cmt(source, look_end) + if source:sub(info_open, info_open) ~= "(" then return after_paren end + local info_inner, info_after = duffle.read_parens(source, info_open) + if not info_inner then return after_paren end + local info_line = line_of(info_open) + local ai_binds, ai_reads, ai_writes, ai_view, ai_overrides, ai_ctx, ai_phase = scan_atom_info_subcalls(info_inner, info_line) + dest = dest or out.atom_infos + dest[#dest + 1] = { + atom_name = raw_name or "?", binds = ai_binds, + reads = ai_reads or {}, writes = ai_writes or {}, + view = ai_view, + reg_type_overrides = ai_overrides, + ctx_atom = ai_ctx, + phase = ai_phase, + info_line = line_of(lookahead), + } + if ai_view and raw_name then + out.atom_views[raw_name] = { + atom_name = raw_name, + binds_name = ai_view, + reg_type_overrides = ai_overrides, + info_line = line_of(lookahead), + } + elseif raw_name and ai_overrides then + out.atom_views[raw_name] = out.atom_views[raw_name] or { atom_name = raw_name, binds_name = nil, reg_type_overrides = nil, info_line = line_of(lookahead) } + out.atom_views[raw_name].reg_type_overrides = ai_overrides + end + if raw_name then + if ai_ctx then + out.atom_ctxs = out.atom_ctxs or {} + out.atom_ctxs[raw_name] = { rbind_atom = ai_ctx, info_line = line_of(lookahead), source = source } + end + if ai_phase then + out.atom_phases = out.atom_phases or {} + out.atom_phases[ai_phase] = out.atom_phases[ai_phase] or { atoms = {} } + out.atom_phases[ai_phase].atoms[#out.atom_phases[ai_phase].atoms + 1] = raw_name + end + end + 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 @@ -1265,53 +1339,8 @@ local function parse_mips_atom(source, pos, ident_end, line_of, out) local raw_name = duffle.read_ident(inner, 1) - -- Lookahead for atom_info(...) between `)` and `{`. Captures sub-calls; updates brace search start. - local brace_search_pos = after_paren - local lookahead = duffle.skip_ws_and_cmt(source, after_paren) - local look_ident, look_end = duffle.read_ident(source, lookahead) - if look_ident == "atom_info" then - local info_open = duffle.skip_ws_and_cmt(source, look_end) - if source:sub(info_open, info_open) == "(" then - local info_inner, info_after = duffle.read_parens(source, info_open) - -- info_line feeds the per-atom reg_type_overrides table. - local info_line = line_of(info_open) - local ai_binds, ai_reads, ai_writes, ai_view, ai_overrides, ai_ctx, ai_phase = scan_atom_info_subcalls(info_inner, info_line) - out.atom_infos[#out.atom_infos + 1] = { - atom_name = raw_name or "?", binds = ai_binds, - reads = ai_reads or {}, writes = ai_writes or {}, - view = ai_view, - reg_type_overrides = ai_overrides, - ctx_atom = ai_ctx, - phase = ai_phase, - info_line = line_of(lookahead), - } - if ai_view and raw_name then - out.atom_views[raw_name] = { - atom_name = raw_name, - binds_name = ai_view, - reg_type_overrides = ai_overrides, - info_line = line_of(lookahead), - } - elseif raw_name and ai_overrides then - -- Record per-atom overrides even without atom_view. - out.atom_views[raw_name] = out.atom_views[raw_name] or { atom_name = raw_name, binds_name = nil, reg_type_overrides = nil, info_line = line_of(lookahead) } - out.atom_views[raw_name].reg_type_overrides = ai_overrides - end - -- Project the per-atom atom_ctx / atom_phase declarations onto the global phase index. - if raw_name then - if ai_ctx then - out.atom_ctxs = out.atom_ctxs or {} - out.atom_ctxs[raw_name] = { rbind_atom = ai_ctx, info_line = line_of(lookahead), source = source } - end - if ai_phase then - out.atom_phases = out.atom_phases or {} - out.atom_phases[ai_phase] = out.atom_phases[ai_phase] or { atoms = {} } - out.atom_phases[ai_phase].atoms[#out.atom_phases[ai_phase].atoms + 1] = raw_name - end - end - brace_search_pos = info_after - end - end + -- Lookahead for atom_info(...) between `)` and `{`. + local brace_search_pos = parse_atom_info_after_decl(source, after_paren, raw_name, line_of, out, out.atom_infos) local body, after_brace, body_off = find_body_braces(source, brace_search_pos, open_paren + 1) if not body then return after_brace end @@ -1336,7 +1365,9 @@ local function parse_mips_atom_comp(source, pos, ident_end, line_of, out) local raw_name = duffle.read_ident(inner, 1) if not raw_name then return open_paren + 1 end - local body, after_brace, body_off = find_body_braces(source, after_paren, open_paren + 1) + out.component_atom_infos = out.component_atom_infos or {} + local brace_search_pos = parse_atom_info_after_decl(source, after_paren, strip_ac_prefix(raw_name), line_of, out, out.component_atom_infos) + local body, after_brace, body_off = find_body_braces(source, brace_search_pos, open_paren + 1) if not body then return after_brace end local name = strip_ac_prefix(raw_name) register_atom(out, "comp_bare", line_of(pos), name, body, body_off, raw_name, pos, after_paren, source) @@ -1407,15 +1438,9 @@ local function parse_mips_atom_comp_proc_map(source, pos, ident_end, line_of, ou return after_paren end ---- Parse: `MipsAtom_Proc_(, , { })` — body is inside the LAST `{` in args. ---- Per Task 12.10: full support for the runtime-proc atom form. Registers the atom ---- with kind `"atom_proc"` so offsets.lua / components.lua can emit ---- * `mac_` aliases in `gen/macs.h` (the components pass) ---- * `atom_offset__X__Y` defs in `gen/offsets.h` (the offsets pass) ---- The atom name is the FIRST ident of the args (the second arg `ab` is the ---- atom-builder, not the name). Unlike `MipsAtomComp_Proc_`, there is no `ac_` ---- prefix on the symbol — `MipsAtom_Proc_` is the runtime-proc wrapper, so the ---- symbol IS the bare atom name (e.g. `normalize_v3s4`, not `ac_normalize_v3s4`). +--- Parse: `MipsAtom_Proc_(aa, { body })` — body is inside the LAST `{` in args. +--- Kind is `atom_proc`. The name is the preceding function ident as written. +--- Offsets walk this kind. Components do not emit a `mac_*` alias for it. --- @param source string --- @param pos integer --- @param ident_end integer @@ -1439,13 +1464,12 @@ local function parse_mips_atom_proc(source, pos, ident_end, line_of, out) local body, close_pos = duffle.read_braces(inner, last_brace_pos) if close_pos > #inner + 1 then return after_paren end - -- The atom name is derived from the preceding function declaration - -- (`internal MipsAtom* X_proc(...)`), not from the first macro arg (which - -- is now `aa`). The backward walk finds the function decl before open_paren - -- and strips the `_proc` suffix. + -- 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) 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) local reg_use_schema_name = nil local reg_use_param_name = nil if args_inner then @@ -1593,7 +1617,32 @@ local function register_typedef_alias(underlying, name, pos, line_of, out) } end -local function parse_reg_use_schema_body(body) +-- 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.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 + local names = {} + for _, slot in ipairs(schema.slots) do + if slot.name then names[#names + 1] = slot.name end + end + if #names > 0 then return names end + end + end + return REG_ALLOC_FIELDS[reg_name] +end + +parse_reg_use_schema_body = function(body, type_registry) local slots = {} local alias_to_slot = {} local slot_names = {} @@ -1728,7 +1777,25 @@ local function parse_reg_use_schema_body(body) after_close = duffle.skip_ws_and_cmt(body, after_close) if body:sub(after_close, after_close) == ";" then after_close = after_close + 1 end pos = after_close - elseif first == "Reg" then + elseif first == "Reg" or first == "Reg_" then + local typed_fields = nil + if first == "Reg_" then + if body:sub(after, after) ~= "(" then + errors[#errors + 1] = { kind = "reguse_malformed" } + return nil, errors + end + local type_inner, after_paren = duffle.read_parens(body, after) + if not type_inner then + errors[#errors + 1] = { kind = "reguse_malformed" } + return nil, errors + end + typed_fields = fields_for_reg_type(duffle.trim(type_inner), type_registry) + if not typed_fields then + errors[#errors + 1] = { kind = "reguse_malformed" } + return nil, errors + end + after = duffle.skip_ws_and_cmt(body, after_paren) + end local readonly = false local maybe_const, maybe_end = duffle.read_ident(body, after) if maybe_const == "const" then @@ -1741,8 +1808,16 @@ local function parse_reg_use_schema_body(body) return nil, errors end for _, n in ipairs(names) do - if not add_alias(n, n) then return nil, errors end - if not add_slot(n, { n }, readonly) then return nil, errors end + 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 + else + if not add_alias(n, n) then return nil, errors end + if not add_slot(n, { n }, readonly) then return nil, errors end + end end pos = new_pos else @@ -1792,7 +1867,7 @@ local function parse_typedef_binds(source, pos, ident_end, line_of, out) if not body then return after_brace end register_struct_type(body, name, pos, line_of, out) if name:sub(1, 7) == "RegUse_" then - local schema, schema_errors = parse_reg_use_schema_body(body) + local schema, schema_errors = parse_reg_use_schema_body(body, out.type_name_registry) if schema then schema.name = name schema.source_file = out._source_file @@ -2174,6 +2249,7 @@ local function scan_source(source, source_file, code_macros, code_macro_bodies) raw_atoms = {}, binds = {}, atom_infos = {}, + component_atom_infos = {}, macros = {}, -- Raw marker evidence for annotation validation. The `debug_skip` boolean -- is stamped on the declaration record itself; the projection lives on AtomEntry.debug_skip. @@ -2429,6 +2505,7 @@ local function merge_corpus_registries(corpus) corpus.atom_ctxs = corpus.atom_ctxs or {} corpus.atom_phases = corpus.atom_phases or {} corpus.atom_infos = corpus.atom_infos or {} + corpus.component_atom_infos = corpus.component_atom_infos or {} corpus.atom_auto_regs = corpus.atom_auto_regs or {} corpus.phase_auto_regs = corpus.phase_auto_regs or {} corpus.collisions = corpus.collisions or {} @@ -2440,7 +2517,7 @@ local function merge_corpus_registries(corpus) for _, key in ipairs({ "register_alias_registry", "type_name_registry", "binds_by_name", "atoms_by_name", "atom_views", "atom_ctxs", "atom_phases", - "atom_infos", "collisions", "reg_use_schemas", "reg_use_errors", + "atom_infos", "component_atom_infos", "collisions", "reg_use_schemas", "reg_use_errors", }) do corpus[key] = {} end @@ -2533,6 +2610,9 @@ local function merge_corpus_registries(corpus) for _, info in ipairs(scan.atom_infos or {}) do corpus.atom_infos[#corpus.atom_infos + 1] = info end + for _, info in ipairs(scan.component_atom_infos or {}) do + corpus.component_atom_infos[#corpus.component_atom_infos + 1] = info + end for name, schema in pairs(scan.reg_use_schemas or {}) do if corpus.reg_use_schemas[name] == nil then diff --git a/scripts/passes/static_analysis.lua b/scripts/passes/static_analysis.lua index 97b6be2..273cd7e 100644 --- a/scripts/passes/static_analysis.lua +++ b/scripts/passes/static_analysis.lua @@ -279,6 +279,16 @@ local function classify_tokens(tokens) for tok_idx, t in ipairs(tokens) do local tok = t.tok local ident = tok:match("^([%w_]+)") or "?" + local is_delay_marker = false + local delay_marker = nil + if duffle.DELAY_MARKERS and duffle.DELAY_MARKERS[ident] then + is_delay_marker = true + delay_marker = ident + local rest = tok:match("^[%w_]+%s+(.*)$") + if rest and rest ~= "" then + ident = rest:match("^([%w_]+)") or ident + end + end local nop_words = 0 if ident == "nop" then nop_words = 1 elseif ident == "nop2" then nop_words = 2 end @@ -343,6 +353,8 @@ local function classify_tokens(tokens) tc[tok_idx] = { ident = ident, + is_delay_marker = is_delay_marker, + delay_marker = delay_marker, nop_words = nop_words, nop_prefix = nop_run, is_yield = is_yield, @@ -961,6 +973,7 @@ local function analyze_hardware_relations(atom) semantic = relation.semantic, producer_word = prod.word, consumer_word = ev_word, + destination = prod.destination, gap = gap, required = prod.required, satisfied = satisfied, @@ -1697,9 +1710,10 @@ end --- and the tape runtime would jump to garbage. --- --- Rules: ---- 1. Every `mac_yield_load()` must be in a branch BD-slot (the immediately preceding token must be a branch). ---- 2. Every `mac_yield_tail()` must be the first instruction after an `atom_label()`, AND ---- at least one branch targeting that label must have `mac_yield_load()` in its BD-slot. +--- 1. Every `mac_yield_load()` must be in a branch BD-slot, or sit between two `atom_label`s. +--- Delay-marker prefixes are skipped when reading prev/next tokens. +--- 2. `mac_yield_tail()` is valid if every path that reaches it has already executed a `mac_yield_load()`. +--- A load in a branch BD slot always runs. Later branches that target the tail label may carry `nop`. --- 3. `mac_yield_tail()` as the atom-end terminator (last token) is a WARNING, not an error --- (the safe default for atom-endings is `mac_yield()` which re-loads `R_AtomJmp`). --- @@ -1718,53 +1732,76 @@ local function check_yield_load_tail_pairing(atom, _pipe_ctx, findings) return atom.line + line_in_body[tokens[idx].rel] end - -- ── Rule 1: every `mac_yield_load()` must be in a branch BD-slot, OR sit between two `atom_label`s (natural fall-through load pattern). - -- When the pattern is satisfied, the check stays silent; only violations emit findings. + local function is_delay_only(c) + return c and duffle.DELAY_MARKERS and duffle.DELAY_MARKERS[c.ident] == true + end + + local function skip_delay(idx, step) + local i = idx + while i >= 1 and i <= n and is_delay_only(tc[i]) do + i = i + step + end + if i < 1 or i > n then return nil end + return i + end + + -- ── Rule 1: every `mac_yield_load()` must be in a branch BD-slot, OR sit between two `atom_label`s. for tok_idx = 1, n do local c = tc[tok_idx] if c.ident == "mac_yield_load" then - local prev_tc = (tok_idx >= 2) and tc[tok_idx - 1] or nil - -- Look for the next `atom_label()` token (skip `atom_offset` markers; check immediately-adjacent first). - local next_label_tc = (tok_idx + 1 <= n) and tc[tok_idx + 1] or nil - if next_label_tc and next_label_tc.ident ~= "atom_label" then - next_label_tc = nil - for j = tok_idx + 1, n do - local t = tc[j] - if t.ident == "atom_label" then - next_label_tc = t - break - end + local prev_i = skip_delay(tok_idx - 1, -1) + local prev_tc = prev_i and tc[prev_i] or nil + local next_label_tc = nil + local j = skip_delay(tok_idx + 1, 1) + while j do + local t = tc[j] + if t.ident == "atom_label" then + next_label_tc = t + break end + if t.ident ~= "atom_offset" then break end + j = skip_delay(j + 1, 1) end local natural_fallthrough = prev_tc and prev_tc.is_atom_label and next_label_tc ~= nil if not natural_fallthrough then - if tok_idx < 2 or not prev_tc.is_branch then + if not prev_tc or not prev_tc.is_branch then local prev_ident = prev_tc and (prev_tc.ident or "?") or "" local next_ident = next_label_tc and (next_label_tc.ident .. "(" .. (next_label_tc.label_name or "?") .. ")") or "" findings[#findings + 1] = { atom = atom.name, - line = tok_idx >= 2 and line_for(tok_idx) or atom.line, + line = prev_i and line_for(tok_idx) or atom.line, check = "yield_load_tail_pairing", kind = "error", msg = string.format( "%s at line %d has `mac_yield_load()` at word %d but the previous token is `%s`, not a branch — and the next `atom_label()` token is `%s` — `mac_yield_load()` must fill a branch BD-slot or sit between two `atom_label`s for the natural fall-through load." - , atom.name, tok_idx >= 2 and line_for(tok_idx) or atom.line, tok_idx, prev_ident, next_ident), + , atom.name, prev_i and line_for(tok_idx) or atom.line, tok_idx, prev_ident, next_ident), } end end end end - -- ── Rule 2: every `mac_yield_tail()` must be at a labeled target whose branch BD-slot is `mac_yield_load()`. + -- ── Rule 2: `mac_yield_tail()` is valid if every path that reaches it already ran `mac_yield_load()`. + 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 + end + end + return false + end + for tok_idx = 1, n do local c = tc[tok_idx] if c.ident ~= "mac_yield_tail" then goto continue end - -- The immediately preceding token must be an `atom_label()` (no instructions between them). - local prev_idx = tok_idx - 1 - if prev_idx < 1 or not tc[prev_idx].is_atom_label then + local prev_idx = skip_delay(tok_idx - 1, -1) + if not prev_idx or not tc[prev_idx].is_atom_label then if tok_idx == n then - -- Atom-ending case: last token is `mac_yield_tail()` without a preceding label. WARNING. findings[#findings + 1] = { atom = atom.name, line = line_for(tok_idx), @@ -1789,36 +1826,14 @@ local function check_yield_load_tail_pairing(atom, _pipe_ctx, findings) end local label_name = tc[prev_idx].label_name - -- Find at least one branch targeting `label_name` whose BD-slot is `mac_yield_load()`. - local found_pairing = false - for branch_idx = 1, n do - local bt = tc[branch_idx] - if bt.is_branch and bt.branch_label == label_name then - local bd_idx = branch_idx + 1 - local bd_tc = bd_idx <= n and tc[bd_idx] or nil - if bd_tc and bd_tc.ident == "mac_yield_load" then - found_pairing = true - else - findings[#findings + 1] = { - atom = atom.name, - line = line_for(branch_idx), - check = "yield_load_tail_pairing", - kind = "error", - msg = string.format( - "%s at line %d has `mac_yield_tail()` at label `%s` (word %d) but the branch targeting it (at word %d) has BD-slot `%s` instead of `mac_yield_load()`." - , atom.name, line_for(branch_idx), label_name, tok_idx, branch_idx, bd_tc and bd_tc.ident or "?"), - } - end - end - end - if not found_pairing then + if not load_covers_tail(tok_idx) then findings[#findings + 1] = { atom = atom.name, line = line_for(tok_idx), check = "yield_load_tail_pairing", kind = "error", msg = string.format( - "%s at line %d has `mac_yield_tail()` at label `%s` but no branch in the body targets this label with `mac_yield_load()` in its BD-slot — R_AtomJmp would not be loaded." + "%s at line %d has `mac_yield_tail()` at label `%s` but no path that reaches it has executed `mac_yield_load()` — R_AtomJmp would not be loaded." , atom.name, line_for(tok_idx), label_name), } end @@ -1972,6 +1987,28 @@ local function check_gpu_portstore_shape(atom, pipe_ctx, findings) end end + -- Token-name gp0_contrib is 0 when bodies use gte_sw. Count expanded prim-buffer stores. + if contrib == 0 then + 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 + end + end + end + if hit then + contrib = contrib + 1 + end + end + end + end + if not cmd_byte then if saw_prim_write and not saw_format then findings[#findings + 1] = { @@ -2048,7 +2085,9 @@ local function analyze_atom_paths(atom, pipe_ctx) local c = tc[tok_idx] local ident = c.ident local cost - if ident:sub(1, #"mac_") == "mac_" then + if duffle.DELAY_MARKERS and duffle.DELAY_MARKERS[ident] then + cost = 0 + elseif ident:sub(1, #"mac_") == "mac_" then -- `mac_*` token: lookup corpus.components[bare_name].cycle_cost. local bare = ident:sub(#"mac_" + 1) local comp = pipe_ctx.components_by_name and pipe_ctx.components_by_name[bare] @@ -2227,13 +2266,11 @@ end -- Per-source rule (called once per source via the CHECK_RULES dispatch). -- Signature matches the per_source shape established by check_semantic_reg_defaults. -- --- Severity: WARNING (build continues). --- The rule is intentionally permissive because the production `code/duffle/` and `code/gte_hello/` --- sources use R_* aliases in atom_reads / atom_writes that may not yet be opted in via the bare `atom_reg` marker. --- R_TapePtr / R_AtomJmp / R_PrimCursor / R_FaceCursor / R_VertBase / R_OtBase ARE opted in. --- Raw C-ABI aliases like R_T0..R_T3 require explicit opt-in; the prototype keeps wave-context registration explicit. --- no auto-include of wave-context; explicit opt-in only). --- Warnings keep the build green and report aliases that need explicit registration. +-- Physical GPRs are members. Opt-in atom_reg stays for aliases. +local function is_physical_gpr(reg) + return type(reg) == "string" and (reg:match("^R_T[0-7]$") ~= nil or reg:match("^R_V[01]$") ~= nil) +end + local function check_enum_alias_membership(_src, pipe_ctx, findings) local reg_registry = pipe_ctx.register_alias_registry or {} @@ -2271,7 +2308,7 @@ local function check_enum_alias_membership(_src, pipe_ctx, findings) end end for _, reg in ipairs(ai.reads or {}) do - if not reg_registry[reg] then + if not reg_registry[reg] and not is_physical_gpr(reg) then findings[#findings + 1] = { atom = atom_name, line = info_line, check = "enum_alias_membership", kind = "warning", @@ -2281,7 +2318,7 @@ local function check_enum_alias_membership(_src, pipe_ctx, findings) end end for _, reg in ipairs(ai.writes or {}) do - if not reg_registry[reg] then + if not reg_registry[reg] and not is_physical_gpr(reg) then findings[#findings + 1] = { atom = atom_name, line = info_line, check = "enum_alias_membership", kind = "warning", @@ -2352,6 +2389,18 @@ local function find_field_by_name(type_entry, field_name) return nil end +-- Walk typedef aliases to the struct that owns the fields table. Depth matches propagate_type_sizes. +local function resolve_type_with_fields(type_name, type_registry, depth) + if depth > 8 then return nil end + local entry = type_registry[type_name] + if not entry then return nil end + if entry.fields then return entry end + if entry.kind == "typedef" and entry.underlying_type and entry.underlying_type ~= "" then + return resolve_type_with_fields(entry.underlying_type, type_registry, depth + 1) + end + return entry +end + -- True iff a (field, type_registry) pair is a leaf scalar (safe to dereference as a tape-payload field). -- Pointer-to-X is always a leaf; non-pointer struct members fail the leaf test. local function is_field_leaf(field, type_registry) @@ -2379,8 +2428,12 @@ local function check_binds_no_substruct_deref(_src, pipe_ctx, findings) local field_name = tc_entry.o_arg2 local body_line = a.line + (line_in_body[tokens[ti].rel] or 0) - local type_entry = type_registry[type_name] - if not type_entry or not type_entry.fields then + 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) + if skip_opaque then + -- leave this token + elseif not type_entry or not type_entry.fields then findings[#findings + 1] = { atom = a.name, line = body_line, check = "binds_no_substruct_deref", kind = "warning", @@ -2450,6 +2503,44 @@ local function atom_body_token_source_line(atom, token, line_in_body) return (atom.line or 0) + body_line - 1 end +local function ctrl_alias_from_text(text) + return tostring(text or ""):match("gte_cr_[%w_]+") +end + +local function ctrl_writes_in_atom(atom) + local out = {} + for _, ev in ipairs((atom.paths and atom.paths.word_events) or {}) do + if (ev.encoder or "") == "gte_mv_to_ctrl_r" then + 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) or ctrl_alias_from_text(ev.root_call_text) + end + local src = ev.args and ev.args[1] + if type(src) == "string" then src = src:match("[%w_]+") end + if alias then + out[#out + 1] = { + alias = alias, + src = src, + line = ev.line or atom.line, + } + end + end + end + if #out == 0 then + for _, t in ipairs((atom.paths and atom.paths.tokens) or {}) do + local tok = t.tok or "" + if tok:match("^gte_mv_to_ctrl_r") then + local alias = ctrl_alias_from_text(tok) + local src = tok:match("%(%s*([%w_]+)") + if alias then + out[#out + 1] = { alias = alias, src = src, line = atom.line } + end + end + end + end + return out +end + -- Check #N: gte_cr_alias_writes -- Fires one warning per atom per alias-group when the atom body touches two -- distinct aliases from the same group. Aliases within a group write to the @@ -2590,6 +2681,135 @@ 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), + } + 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 + +local function check_gte_packed_writes(atom, _pipe_ctx, findings) + local writes = ctrl_writes_in_atom(atom) + local first_idx = {} + for i, w in ipairs(writes) do + if first_idx[w.alias] == nil then first_idx[w.alias] = i end + end + for _, rel in ipairs(duffle.GTE_PACKED_SLOT_RELATIONS or {}) do + local i1 = first_idx[rel.first] + local i2 = first_idx[rel.second] + if i1 and i2 and i2 < i1 then + findings[#findings + 1] = { + atom = atom.name or "", + line = writes[i2].line, + check = "gte_packed_writes", + kind = "warning", + msg = string.format( + "atom '%s' writes %s before %s on packed C2[%d]" + , atom.name or "", rel.second, rel.first, rel.slot), + } + end + end +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 + 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 = {} + end + 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 } + 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), + } + end + end +end + -- check_immediate_field_width — flags integer literals passed to instruction -- macros that exceed the immediate field width. Reads `IMMEDIATE_FIELD_WIDTHS` -- from duffle.lua. Only fires on parseable integer literals; register names, @@ -2670,12 +2890,13 @@ local function check_immediate_field_width(atom, pipe_ctx, findings) "%s: immediate %d at arg %d overflows %d-bit unsigned field (valid 0..%d)", ev_ident, value, rule.arg, width, field_max), } - end - end - end end end end + ::continue_token:: + end + end +end end end end @@ -2707,6 +2928,9 @@ local CHECK_RULES = { { name = "gpu_portstore_shape", per_atom = check_gpu_portstore_shape }, { name = "per_atom_cycle_budget", per_atom = check_per_atom_cycle_budget }, { name = "gte_cr_alias_writes", per_atom = check_gte_cr_alias_writes }, + { name = "gte_cr_alias_writes_xatom", per_source = check_gte_cr_alias_writes_xatom }, + { name = "gte_packed_writes", per_atom = check_gte_packed_writes }, + { name = "ctc2_chain_source_preservation", per_atom = check_ctc2_chain_source_preservation }, { name = "rtdiagonal_completeness", per_atom = check_rtdiagonal_completeness }, { name = "gte_cr_TR_naming", per_atom = check_gte_cr_TR_naming }, { name = "immediate_field_width", per_atom = check_immediate_field_width },