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:
@@ -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