mirror of
https://github.com/Ed94/pikuma_ps1.git
synced 2026-08-25 02:20:33 +00:00
updates to lua program to furhter support new constructs and correct report errors.
This commit is contained in:
BIN
Binary file not shown.
+1
-1
@@ -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_)/;
|
||||
|
||||
|
||||
@@ -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": {
|
||||
|
||||
@@ -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());
|
||||
|
||||
+38
-11
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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`.
|
||||
|
||||
@@ -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
@@ -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
@@ -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
|
||||
|
||||
@@ -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 },
|
||||
|
||||
Reference in New Issue
Block a user