updates to lua program to furhter support new constructs and correct report errors.

This commit is contained in:
ed
2026-08-15 19:52:56 -04:00
parent 8a825a59c7
commit a16c727db2
13 changed files with 592 additions and 181 deletions
+38 -11
View File
@@ -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
+2 -2
View File
@@ -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_<name>` 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
+1 -1
View File
@@ -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_<name>) 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
+2 -2
View File
@@ -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
+6 -6
View File
@@ -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_<name>) 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_<name>`,
--- Pass entry. For each source that declares at least one tape atom,
--- emit two files in `<out_root>/`: `<basename>.atoms.sourcemap.txt` (per-word call-site map) and `<basename>.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 `<ctx.out_root>/gdb_tape_atoms_runtime.gdb`.
+4 -3
View File
@@ -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_<name>` 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_<name>` 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 `<name>[]` 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
+93 -24
View File
@@ -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]
+147 -67
View File
@@ -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_(<name>) [atom_info(<binds>, <reads>, <writes>)] { <body> }`
--- @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_(<name>, <abuilder>, { <body> })` — 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_<name>` 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_<type> only. Never the C data struct (V3_S4 has a pad field).
local REG_ALLOC_FIELDS = {
Reg_V3_S4 = { "x", "y", "z" },
Reg_P3_S4 = { "x", "y", "z" },
Reg_V3_S2 = { "x", "y", "z" },
}
local parse_reg_use_schema_body
local function fields_for_reg_type(type_name, type_registry)
local reg_name = "Reg_" .. type_name
local entry = type_registry and type_registry[reg_name]
if entry and entry.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
+287 -63
View File
@@ -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 "<none>"
local next_ident = next_label_tc and (next_label_tc.ident .. "(" .. (next_label_tc.label_name or "?") .. ")") or "<no following label>"
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 },