mirror of
https://github.com/Ed94/pikuma_ps1.git
synced 2026-08-25 02:20:33 +00:00
wip: going over all codepaths.
This commit is contained in:
@@ -115,7 +115,7 @@ end
|
||||
--- Post-loop: Needs full-corpus `annot_counts` from pipe_ctx.
|
||||
--- @param pipe_ctx PipeCtx
|
||||
--- @param findings Findings
|
||||
local function check_unique_annotation(pipe_ctx, findings)
|
||||
local function check_unique_annotation(_item, pipe_ctx, findings)
|
||||
for name, n in pairs(pipe_ctx.annot_counts) do
|
||||
if n > 1 then
|
||||
findings.errors[#findings.errors + 1] = {
|
||||
@@ -147,7 +147,8 @@ end
|
||||
--- @param m MacroEntry
|
||||
--- @param wc table<string, integer> -- Shared word-count table (from ctx.shared.word_counts)
|
||||
--- @param findings Findings
|
||||
local function check_macro_word_drift(m, wc, findings)
|
||||
local function check_macro_word_drift(m, pipe_ctx, findings)
|
||||
local wc = (pipe_ctx and pipe_ctx.word_counts) or {}
|
||||
local declared = wc[m.name]
|
||||
if not declared then
|
||||
findings.errors[#findings.errors + 1] = {
|
||||
@@ -429,40 +430,17 @@ local CHECK_RULES = {
|
||||
--- @param ctx PassCtx
|
||||
--- @return PipeCtx
|
||||
local function build_corpus_pipe_ctx(ctx)
|
||||
local corpus = ctx.shared and ctx.shared.corpus
|
||||
if not corpus then
|
||||
error("annotation requires ctx.shared.corpus "
|
||||
.. "(the canonical corpus is the source of truth; "
|
||||
.. "no per-source fallback is supported)", 0)
|
||||
end
|
||||
|
||||
-- `corpus.atom_infos` preserves source order and duplicates; I precompute counts here for `check_unique_annotation` and the per-source checks.
|
||||
local view = duffle.corpus_view(ctx)
|
||||
local annot_counts = {}
|
||||
for _, info in ipairs(corpus.atom_infos or {}) do
|
||||
for _, info in ipairs(view.atom_infos) do
|
||||
if info and info.atom_name then
|
||||
annot_counts[info.atom_name] = (annot_counts[info.atom_name] or 0) + 1
|
||||
end
|
||||
end
|
||||
|
||||
-- Every consumer of these fields observes mutations via the canonical corpus without independently mutable registry construction.
|
||||
return {
|
||||
-- Cross-source lookup tables from corpus.
|
||||
register_alias_registry = corpus.register_alias_registry or {},
|
||||
type_name_registry = corpus.type_name_registry or {},
|
||||
atom_views = corpus.atom_views or {},
|
||||
atom_ctxs = corpus.atom_ctxs or {},
|
||||
atom_phases = corpus.atom_phases or {},
|
||||
binds_by_name = corpus.binds_by_name or {},
|
||||
atoms_by_name = corpus.atoms_by_name or {},
|
||||
-- Corpus-wide ordered list of atom_info records (source-order + duplicates).
|
||||
atom_infos_list = corpus.atom_infos or {},
|
||||
-- Corpus-wide annotation count aggregation (post-rule consumes this).
|
||||
annot_counts = annot_counts,
|
||||
-- Corpus-wide collisions (recorded by scan_source.merge_corpus_registries).
|
||||
collisions = corpus.collisions or {},
|
||||
-- `check_macro_word_drift` reads `corpus.word_counts`, populated by word_count_eval.run.
|
||||
word_counts = corpus.word_counts or {},
|
||||
}
|
||||
view.annot_counts = annot_counts
|
||||
view.atom_infos_list = view.atom_infos
|
||||
view.word_counts = ctx.shared.corpus.word_counts or {}
|
||||
return view
|
||||
end
|
||||
|
||||
--- Validate one source against its pre-scanned SourceScan payload + the corpus-wide pipe_ctx.
|
||||
@@ -536,38 +514,28 @@ local function validate(ctx, src, corpus_pipe_ctx)
|
||||
|
||||
-- THE per-annotation pipeline. ONE loop. CHECK_RULES dispatches per_annot rules.
|
||||
for _, a in ipairs(annots) do
|
||||
for _, rule in ipairs(CHECK_RULES) do
|
||||
if rule.per_annot then rule.per_annot(a, pipe_ctx, findings) end
|
||||
end
|
||||
duffle.run_check_rules(CHECK_RULES, "per_annot", a, pipe_ctx, findings)
|
||||
end
|
||||
|
||||
-- Post-loop rules (one-shot checks that need full-corpus aggregation in pipe_ctx).
|
||||
for _, rule in ipairs(CHECK_RULES) do
|
||||
if rule.post then rule.post(pipe_ctx, findings) end
|
||||
end
|
||||
duffle.run_check_rules(CHECK_RULES, "post", nil, pipe_ctx, findings)
|
||||
|
||||
-- scan_source records each marker in scan.debug_skip_markers; this loop validates each record independently and emits at most one error per marker.
|
||||
-- Valid markers stamp `debug_skip = true` on the following atom or component declaration, which downstream consumers read directly.
|
||||
local skip_markers = scan.debug_skip_markers or {}
|
||||
for _, marker in ipairs(skip_markers) do
|
||||
for _, rule in ipairs(CHECK_RULES) do
|
||||
if rule.per_skip_marker then rule.per_skip_marker(marker, pipe_ctx, findings) end
|
||||
end
|
||||
duffle.run_check_rules(CHECK_RULES, "per_skip_marker", marker, pipe_ctx, findings)
|
||||
end
|
||||
|
||||
-- Per-macro rules (TAPE_WORDS vs WORD_COUNT drift).
|
||||
local wc = corpus_pipe_ctx.word_counts
|
||||
pipe_ctx.word_counts = corpus_pipe_ctx.word_counts
|
||||
for _, m in ipairs(scan.macros) do
|
||||
for _, rule in ipairs(CHECK_RULES) do
|
||||
if rule.per_macro then rule.per_macro(m, wc, findings) end
|
||||
end
|
||||
duffle.run_check_rules(CHECK_RULES, "per_macro", m, pipe_ctx, findings)
|
||||
end
|
||||
|
||||
-- Per-source rules (reg defaults, atom_view layout, compute-register type overrides, Binds_* field uniqueness).
|
||||
-- Each per_source rule sees the full scan payload via pipe_ctx.
|
||||
for _, rule in ipairs(CHECK_RULES) do
|
||||
if rule.per_source then rule.per_source(src, pipe_ctx, findings) end
|
||||
end
|
||||
duffle.run_check_rules(CHECK_RULES, "per_source", src, pipe_ctx, findings)
|
||||
|
||||
-- Information summary (always emitted).
|
||||
findings.info[#findings.info + 1] = {
|
||||
|
||||
@@ -372,8 +372,10 @@ local function cycle_cost_rec(name, comp_by_name, latency, cache)
|
||||
local nested = ident:sub(MAC_PREFIX_LEN + 1)
|
||||
n = n + cycle_cost_rec(nested, comp_by_name, latency, cache)
|
||||
else
|
||||
-- Leaf instruction or pseudo-macro. Look up in INSTRUCTION_LATENCY; default 1.
|
||||
n = n + (latency[ident] or 1)
|
||||
-- Leaf instruction or pseudo-macro.
|
||||
local isa = duffle.instr(ident)
|
||||
local gte = duffle.gte(ident)
|
||||
n = n + ((isa and isa.cycles) or (gte and gte.cycles) or latency[ident] or 1)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
+191
-172
@@ -1319,159 +1319,49 @@ local function parse_atom_info_after_decl(source, after_paren, raw_name, line_of
|
||||
return info_after
|
||||
end
|
||||
|
||||
--- Parse: `MipsAtom_(<name>) [atom_info(<binds>, <reads>, <writes>)] { <body> }`
|
||||
--- @param source string
|
||||
--- @param pos integer
|
||||
--- @param ident_end integer
|
||||
--- @param line_of fun(pos: integer): integer
|
||||
--- @param out SourceScan
|
||||
--- @return integer
|
||||
local function parse_mips_atom(source, pos, ident_end, line_of, out)
|
||||
local inner, after_paren, open_paren = read_parens_after(source, ident_end)
|
||||
if not inner then return after_paren end
|
||||
local DECL_FORMS = {
|
||||
MipsAtom_ = {
|
||||
kind = "atom", name = "paren_ident", body = "braces_after",
|
||||
info_dest = "atom_infos", strip = false,
|
||||
},
|
||||
MipsAtom_Proc_ = {
|
||||
kind = "atom_proc", name = "backward_atom_proc", body = "last_brace_in_args",
|
||||
info_dest = "atom_infos", strip = false, after = "reguse_hook",
|
||||
},
|
||||
MipsAtomComp_ = {
|
||||
kind = "comp_bare", name = "paren_ident", body = "braces_after",
|
||||
info_dest = "component_atom_infos", strip = "ac_",
|
||||
},
|
||||
MipsAtomComp_Proc_ = {
|
||||
kind = "comp_proc", name = "backward_fi", body = "last_brace_in_args",
|
||||
info_dest = nil, strip = "ac_",
|
||||
},
|
||||
MipsAtomComp_ProcMap_ = {
|
||||
kind = "comp_proc", name = "backward_fi", body = "comma_arg_2",
|
||||
info_dest = nil, strip = "ac_", after = "map_command_hook",
|
||||
},
|
||||
}
|
||||
|
||||
local raw_name = duffle.read_ident(inner, 1)
|
||||
|
||||
-- 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
|
||||
if raw_name and raw_name ~= "" then
|
||||
register_atom(out, "atom", line_of(pos), raw_name, body, body_off, raw_name, pos, after_paren, source)
|
||||
end
|
||||
|
||||
return after_brace
|
||||
end
|
||||
|
||||
--- Parse: `MipsAtomComp_(<name>) { <body> }`
|
||||
--- @param source string
|
||||
--- @param pos integer
|
||||
--- @param ident_end integer
|
||||
--- @param line_of fun(pos: integer): integer
|
||||
--- @param out SourceScan
|
||||
--- @return integer
|
||||
local function parse_mips_atom_comp(source, pos, ident_end, line_of, out)
|
||||
local inner, after_paren, open_paren = read_parens_after(source, ident_end)
|
||||
if not inner then return after_paren end
|
||||
|
||||
local raw_name = duffle.read_ident(inner, 1)
|
||||
if not raw_name then return open_paren + 1 end
|
||||
|
||||
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)
|
||||
|
||||
return after_brace
|
||||
end
|
||||
|
||||
--- Parse: `MipsAtomComp_Proc_(<name>, { <body> })` — body is inside the LAST `{` in args.
|
||||
--- @param source string
|
||||
--- @param pos integer
|
||||
--- @param ident_end integer
|
||||
--- @param line_of fun(pos: integer): integer
|
||||
--- @param out SourceScan
|
||||
--- @return integer
|
||||
local function parse_mips_atom_comp_proc(source, pos, ident_end, line_of, out)
|
||||
local inner, after_paren, open_paren = read_parens_after(source, ident_end)
|
||||
if not inner then return after_paren end
|
||||
|
||||
-- Find the LAST `{` in inner (the body brace, not any potential embedded braces in expressions).
|
||||
local function last_brace_body(inner, open_paren)
|
||||
local last_brace_pos = nil
|
||||
for search_pos = #inner, 1, -1 do
|
||||
if inner:sub(search_pos, search_pos) == "{" then last_brace_pos = search_pos; break end
|
||||
if inner:sub(search_pos, search_pos) == "{" then
|
||||
last_brace_pos = search_pos
|
||||
break
|
||||
end
|
||||
end
|
||||
if not last_brace_pos then return after_paren end
|
||||
|
||||
-- Use duffle.read_braces to find the matching close brace.
|
||||
-- Uses `read_balanced` for delimiter-depth tracking.
|
||||
-- If close_pos is past the end of inner, the brace didn't match (malformed input); skip.
|
||||
if not last_brace_pos then return nil end
|
||||
local body, close_pos = duffle.read_braces(inner, last_brace_pos)
|
||||
if close_pos > #inner + 1 then return after_paren end
|
||||
|
||||
-- The component name is derived from the preceding function declaration
|
||||
-- (`FI_ Slice_MipsCode ac_X(...)`), not from the first macro arg (which
|
||||
-- is now `ab`). The backward walk finds the function decl before open_paren.
|
||||
local raw_name = duffle.find_function_decl_for(source, open_paren, SLICE_MIPS_CODE_LEN)
|
||||
if not raw_name then raw_name = "?" end
|
||||
local name = strip_ac_prefix(raw_name)
|
||||
-- Position of body[1] in source = open_paren + 1 (start of inner) + last_brace_pos + 1 (past '{').
|
||||
local body_off = open_paren + 2 + last_brace_pos
|
||||
register_atom(out, "comp_proc", line_of(pos), name, body, body_off, raw_name, pos, after_paren, source)
|
||||
|
||||
return after_paren
|
||||
if close_pos > #inner + 1 then return nil end
|
||||
return body, open_paren + 2 + last_brace_pos
|
||||
end
|
||||
|
||||
--- Parse: `MipsAtomComp_ProcMap_(ab, command)` — body is the one command (second arg).
|
||||
--- Reuses the proc name walk. Kind is `comp_proc`. The C expansion wraps
|
||||
--- `atom_dbg_skip MipsAtomComp_Proc_(ab, {command })`; source-as-written is the map.
|
||||
--- @param source string
|
||||
--- @param pos integer
|
||||
--- @param ident_end integer
|
||||
--- @param line_of fun(pos: integer): integer
|
||||
--- @param out SourceScan
|
||||
--- @return integer
|
||||
local function parse_mips_atom_comp_proc_map(source, pos, ident_end, line_of, out)
|
||||
local inner, after_paren, open_paren = read_parens_after(source, ident_end)
|
||||
if not inner then return after_paren end
|
||||
local args = duffle.split_top_level_commas(inner)
|
||||
if #args < 2 then return after_paren end
|
||||
local command = duffle.trim(args[2])
|
||||
if command == "" then return after_paren end
|
||||
local raw_name = duffle.find_function_decl_for(source, open_paren, SLICE_MIPS_CODE_LEN)
|
||||
if not raw_name then raw_name = "?" end
|
||||
local name = strip_ac_prefix(raw_name)
|
||||
local body_off = open_paren + 1 + (inner:find(command, 1, true) or 1) - 1
|
||||
register_atom(out, "comp_proc", line_of(pos), name, command, body_off, raw_name, pos, after_paren, source)
|
||||
local function reguse_hook(source, pos, line_of, out, extras)
|
||||
local entry = out.atoms[#out.atoms]
|
||||
entry.map_command = command
|
||||
return after_paren
|
||||
end
|
||||
|
||||
--- 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
|
||||
--- @param line_of fun(pos: integer): integer
|
||||
--- @param out SourceScan
|
||||
--- @return integer
|
||||
local function parse_mips_atom_proc(source, pos, ident_end, line_of, out)
|
||||
local inner, after_paren, open_paren = read_parens_after(source, ident_end)
|
||||
if not inner then return after_paren end
|
||||
|
||||
-- Find the LAST `{` in inner (the body brace, not any potential embedded braces in expressions).
|
||||
local last_brace_pos = nil
|
||||
for search_pos = #inner, 1, -1 do
|
||||
if inner:sub(search_pos, search_pos) == "{" then last_brace_pos = search_pos; break end
|
||||
end
|
||||
if not last_brace_pos then return after_paren end
|
||||
|
||||
-- Use duffle.read_braces to find the matching close brace.
|
||||
-- Uses `read_balanced` for delimiter-depth tracking.
|
||||
-- If close_pos is past the end of inner, the brace didn't match (malformed input); skip.
|
||||
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 the preceding function ident as written
|
||||
-- (`internal MipsAtom* X(...)`). The first macro arg is the arena.
|
||||
local raw_name, args_inner, func_ident, after_func_paren =
|
||||
duffle.find_atom_proc_decl_for(source, open_paren, MIPS_ATOM_PTR_LEN)
|
||||
if not raw_name then raw_name = "?" end
|
||||
local name = strip_ac_prefix(raw_name)
|
||||
if after_func_paren then
|
||||
parse_atom_info_after_decl(source, after_func_paren, name, line_of, out, out.atom_infos)
|
||||
else
|
||||
parse_atom_info_after_decl(source, pos, name, line_of, out, out.atom_infos)
|
||||
end
|
||||
local reg_use_schema_name = nil
|
||||
local reg_use_param_name = nil
|
||||
if args_inner then
|
||||
local arg_tokens = duffle.split_top_level_commas(args_inner)
|
||||
if not entry then return end
|
||||
local reg_use_schema_name, reg_use_param_name
|
||||
if extras.args_inner then
|
||||
local arg_tokens = duffle.split_top_level_commas(extras.args_inner)
|
||||
for _, tok in ipairs(arg_tokens) do
|
||||
local trimmed = duffle.trim(tok)
|
||||
local schema_suffix, param = trimmed:match("RegUse_([%w_]+)%s+([%w_]+)$")
|
||||
@@ -1489,26 +1379,100 @@ local function parse_mips_atom_proc(source, pos, ident_end, line_of, out)
|
||||
end
|
||||
end
|
||||
end
|
||||
-- Position of body[1] in source = open_paren + 1 (start of inner) + last_brace_pos + 1 (past '{').
|
||||
local body_off = open_paren + 2 + last_brace_pos
|
||||
register_atom(out, "atom_proc", line_of(pos), name, body, body_off, raw_name, pos, after_paren, source)
|
||||
|
||||
local entry = out.atoms[#out.atoms]
|
||||
entry.reg_use_schema_name = reg_use_schema_name
|
||||
entry.reg_use_param_name = reg_use_param_name
|
||||
if reg_use_schema_name and func_ident then
|
||||
local expected = "RegUse_" .. func_ident
|
||||
if reg_use_schema_name and extras.func_ident then
|
||||
local expected = "RegUse_" .. extras.func_ident
|
||||
if reg_use_schema_name ~= expected then
|
||||
out.reg_use_errors[#out.reg_use_errors + 1] = {
|
||||
kind = "reguse_name_mismatch",
|
||||
schema_name = reg_use_schema_name,
|
||||
func_ident = func_ident,
|
||||
func_ident = extras.func_ident,
|
||||
source_line = line_of(pos),
|
||||
}
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
return after_paren
|
||||
local function parse_decl_form(source, pos, ident_end, line_of, out)
|
||||
local ident = duffle.read_ident(source, pos)
|
||||
local form = ident and DECL_FORMS[ident]
|
||||
if not form then return ident_end end
|
||||
|
||||
local inner, after_paren, open_paren = read_parens_after(source, ident_end)
|
||||
if not inner then return after_paren end
|
||||
|
||||
local extras = {}
|
||||
local raw_name
|
||||
if form.name == "paren_ident" then
|
||||
raw_name = duffle.read_ident(inner, 1)
|
||||
if form.strip and not raw_name then return open_paren + 1 end
|
||||
elseif form.name == "backward_fi" then
|
||||
raw_name = duffle.find_function_decl_for(source, open_paren, SLICE_MIPS_CODE_LEN)
|
||||
elseif form.name == "backward_atom_proc" then
|
||||
raw_name, extras.args_inner, extras.func_ident, extras.after_func_paren =
|
||||
duffle.find_atom_proc_decl_for(source, open_paren, MIPS_ATOM_PTR_LEN)
|
||||
end
|
||||
if form.name == "paren_ident" and form.strip and not raw_name then
|
||||
return open_paren + 1
|
||||
end
|
||||
if not raw_name then raw_name = "?" end
|
||||
local name = form.strip and strip_ac_prefix(raw_name) or raw_name
|
||||
|
||||
if form.info_dest == "component_atom_infos" then
|
||||
out.component_atom_infos = out.component_atom_infos or {}
|
||||
end
|
||||
local info_dest = form.info_dest and out[form.info_dest]
|
||||
|
||||
local body, body_off, resume
|
||||
if form.body == "braces_after" then
|
||||
local brace_search = after_paren
|
||||
if info_dest then
|
||||
brace_search = parse_atom_info_after_decl(
|
||||
source, after_paren, name, line_of, out, info_dest)
|
||||
end
|
||||
local after_brace
|
||||
body, after_brace, body_off = find_body_braces(source, brace_search, open_paren + 1)
|
||||
if not body then return after_brace end
|
||||
resume = after_brace
|
||||
elseif form.body == "last_brace_in_args" then
|
||||
body, body_off = last_brace_body(inner, open_paren)
|
||||
if not body then return after_paren end
|
||||
resume = after_paren
|
||||
if form.info_dest then
|
||||
if extras.after_func_paren then
|
||||
parse_atom_info_after_decl(
|
||||
source, extras.after_func_paren, name, line_of, out, out.atom_infos)
|
||||
else
|
||||
parse_atom_info_after_decl(source, pos, name, line_of, out, out.atom_infos)
|
||||
end
|
||||
end
|
||||
elseif form.body == "comma_arg_2" then
|
||||
local args = duffle.split_top_level_commas(inner)
|
||||
if #args < 2 then return after_paren end
|
||||
body = duffle.trim(args[2])
|
||||
if body == "" then return after_paren end
|
||||
body_off = open_paren + 1 + (inner:find(body, 1, true) or 1) - 1
|
||||
resume = after_paren
|
||||
else
|
||||
return after_paren
|
||||
end
|
||||
|
||||
local skip_register = form.name == "paren_ident" and not form.strip
|
||||
and (raw_name == "?" or raw_name == "")
|
||||
if not skip_register then
|
||||
register_atom(out, form.kind, line_of(pos), name, body, body_off,
|
||||
raw_name, pos, after_paren, source)
|
||||
end
|
||||
|
||||
if form.after == "reguse_hook" then
|
||||
reguse_hook(source, pos, line_of, out, extras)
|
||||
elseif form.after == "map_command_hook" then
|
||||
local entry = out.atoms[#out.atoms]
|
||||
if entry then entry.map_command = body end
|
||||
end
|
||||
|
||||
return resume
|
||||
end
|
||||
|
||||
--- Parse: `MipsCode code_<name> { <body> }` (raw atom form — offsets pass only).
|
||||
@@ -2219,11 +2183,11 @@ end
|
||||
-- Adding a new construct = 1 row here + 1 parser function above.
|
||||
|
||||
local DECL_PARSERS = {
|
||||
MipsAtom_ = parse_mips_atom,
|
||||
MipsAtom_Proc_ = parse_mips_atom_proc,
|
||||
MipsAtomComp_ = parse_mips_atom_comp,
|
||||
MipsAtomComp_Proc_ = parse_mips_atom_comp_proc,
|
||||
MipsAtomComp_ProcMap_ = parse_mips_atom_comp_proc_map,
|
||||
MipsAtom_ = parse_decl_form,
|
||||
MipsAtom_Proc_ = parse_decl_form,
|
||||
MipsAtomComp_ = parse_decl_form,
|
||||
MipsAtomComp_Proc_ = parse_decl_form,
|
||||
MipsAtomComp_ProcMap_ = parse_decl_form,
|
||||
-- `atom_dbg_skip` is the only debug-skip parser entry. Every other
|
||||
-- identifier follows the ordinary unrelated-token path; there is no alias.
|
||||
atom_dbg_skip = parse_dbg_skip_marker,
|
||||
@@ -2243,17 +2207,11 @@ local DECL_PARSERS = {
|
||||
-- Only the bare `atom_dbg_skip` marker reaches `parse_dbg_skip_marker`.
|
||||
-- Unknown identifiers follow the same unrelated-token path as every other unsupported source token.
|
||||
|
||||
local TAPE_SKIP_MACROS = {
|
||||
MipsAtom_ = true,
|
||||
MipsAtom_Proc_ = true,
|
||||
MipsAtomComp_ = true,
|
||||
MipsAtomComp_Proc_ = true,
|
||||
MipsAtomComp_ProcMap_ = true,
|
||||
Struct_ = true,
|
||||
Enum_ = true,
|
||||
}
|
||||
local function tape_skip_ident(ident)
|
||||
return ident and (DECL_FORMS[ident] or ident == "Struct_" or ident == "Enum_")
|
||||
end
|
||||
|
||||
local function collect_addrs_assigns(text)
|
||||
local function collect_addrs_assigns_REMOVED(text)
|
||||
local addrs = {}
|
||||
local pos = 1
|
||||
local n = #text
|
||||
@@ -2337,7 +2295,7 @@ local function scan_tape_chains(source)
|
||||
pos = duffle.skip_ws_and_cmt(source, pos)
|
||||
if pos > n then break end
|
||||
local ident, ident_end = duffle.read_ident(source, pos)
|
||||
if ident and TAPE_SKIP_MACROS[ident] then
|
||||
if tape_skip_ident(ident) then
|
||||
local after = duffle.skip_ws_and_cmt(source, ident_end)
|
||||
if source:sub(after, after) == "(" then
|
||||
local _, after_p = duffle.read_parens(source, after)
|
||||
@@ -2418,6 +2376,10 @@ local function scan_source(source, source_file, code_macros, code_macro_bodies)
|
||||
-- See `propagate_type_sizes()` below.
|
||||
type_name_registry = {},
|
||||
reg_use_schemas = {},
|
||||
tape_chains = {},
|
||||
_addrs = {},
|
||||
_chain = nil,
|
||||
_brace_depth = 0,
|
||||
reg_use_errors = {},
|
||||
-- Shared `R_*_Code -> integer code` registry
|
||||
-- (passed in from M.run pass 1; same reference so preprocessor intercept writes are visible to the enum-value resolver).
|
||||
@@ -2452,6 +2414,48 @@ local function scan_source(source, source_file, code_macros, code_macro_bodies)
|
||||
local parser = DECL_PARSERS[ident]
|
||||
if parser then
|
||||
pos = parser(source, pos, ident_end, line_of, out)
|
||||
elseif ident == "addrs" then
|
||||
local after = duffle.skip_ws_and_cmt(source, ident_end)
|
||||
if source:sub(after, after) == "[" then
|
||||
local inner, after_br = duffle.read_brackets(source, after)
|
||||
local idx = inner and tonumber(duffle.trim(inner))
|
||||
after_br = duffle.skip_ws_and_cmt(source, after_br or after)
|
||||
if idx and source:sub(after_br, after_br) == "=" then
|
||||
local rhs = duffle.skip_ws_and_cmt(source, after_br + 1)
|
||||
local rhs_ident = duffle.read_ident(source, rhs)
|
||||
if rhs_ident then out._addrs[idx] = rhs_ident end
|
||||
pos = rhs
|
||||
else
|
||||
pos = after_br or (after + 1)
|
||||
end
|
||||
else
|
||||
pos = ident_end
|
||||
end
|
||||
elseif ident == "tb_emit_" or ident == "tb_emit" then
|
||||
local after = duffle.skip_ws_and_cmt(source, ident_end)
|
||||
if source:sub(after, after) == "(" then
|
||||
local inner, after_p = duffle.read_parens(source, after)
|
||||
local name
|
||||
if ident == "tb_emit_" then
|
||||
name = duffle.trim(inner or ""):match("^([%w_]+)")
|
||||
else
|
||||
local args = duffle.split_top_level_commas(inner or "")
|
||||
local last = duffle.trim(args[#args] or "")
|
||||
local idx = last:match("^addrs%s*%[%s*(%d+)%s*%]$")
|
||||
if idx then
|
||||
name = out._addrs[tonumber(idx)]
|
||||
else
|
||||
name = last:match("([%w_]+)$")
|
||||
end
|
||||
end
|
||||
if name then
|
||||
out._chain = out._chain or {}
|
||||
out._chain[#out._chain + 1] = name
|
||||
end
|
||||
pos = after_p or (after + 1)
|
||||
else
|
||||
pos = ident_end
|
||||
end
|
||||
else
|
||||
-- Unsupported identifiers follow the unrelated-token path. If a
|
||||
-- pending marker is still open, consume it so it cannot drift to a
|
||||
@@ -2470,12 +2474,22 @@ local function scan_source(source, source_file, code_macros, code_macro_bodies)
|
||||
else
|
||||
local markers = out.debug_skip_markers
|
||||
local marker = markers[#markers]
|
||||
local c = source:sub(pos, pos)
|
||||
if marker and marker.pending and marker.proc_prelude then
|
||||
local c = source:sub(pos, pos)
|
||||
if c == "{" or c == ";" then
|
||||
attach_debug_skip_marker(out, "unrelated")
|
||||
end
|
||||
end
|
||||
if c == "{" then
|
||||
out._brace_depth = out._brace_depth + 1
|
||||
elseif c == "}" then
|
||||
if out._brace_depth == 1 and out._chain and #out._chain > 0 then
|
||||
out.tape_chains[#out.tape_chains + 1] = out._chain
|
||||
end
|
||||
out._chain = nil
|
||||
out._brace_depth = out._brace_depth - 1
|
||||
if out._brace_depth < 0 then out._brace_depth = 0 end
|
||||
end
|
||||
pos = pos + 1
|
||||
end
|
||||
end
|
||||
@@ -2486,7 +2500,12 @@ local function scan_source(source, source_file, code_macros, code_macro_bodies)
|
||||
-- Runs AFTER the source walk so all typedef / Struct_ / Enum_ declarations have been parsed into `out.type_name_registry`.
|
||||
-- Mutates each entry's `byte_size` field in place; fields with pointer_depth > 0 already carry byte_size = 4 from parse time and are unaffected.
|
||||
propagate_type_sizes(out)
|
||||
out.tape_chains = scan_tape_chains(source)
|
||||
if out._chain and #out._chain > 0 then
|
||||
out.tape_chains[#out.tape_chains + 1] = out._chain
|
||||
end
|
||||
out._addrs = nil
|
||||
out._chain = nil
|
||||
out._brace_depth = nil
|
||||
|
||||
return out
|
||||
end
|
||||
|
||||
@@ -227,19 +227,7 @@ end
|
||||
--- @field o_arg2 string|nil -- Second arg of O_(<a>, <b>) captures
|
||||
--- @field s_arg1 string|nil -- Arg of S_(<a>) captures; nil for non-S_ tokens
|
||||
|
||||
-- The set of MIPS instruction idents that have a load-delay slot.
|
||||
-- Per MIPS I R3000A: `lw`, `lh`, `lhu`, `lb`, `lbu`, `lwc2` (gte_lw).
|
||||
-- Note: `lui` (load_upper_i) does NOT have a load delay on MIPS I — it's an ALU op, not a load.
|
||||
-- The `load_imm_*` macros are lui + ori sequences with no per-component load delay either.
|
||||
local LOAD_INSTRUCTION_IDENTS = {
|
||||
load_word = true,
|
||||
load_half = true,
|
||||
load_half_u = true,
|
||||
load_byte = true,
|
||||
load_byte_u = true,
|
||||
gte_lw = true,
|
||||
gte_lwc2 = true,
|
||||
}
|
||||
-- Load-delay idents are M.INSTRUCTION rows with kind == "load".
|
||||
|
||||
-- Patterns for O_(<arg1>, <arg2>) and S_(<arg>) captures.
|
||||
-- UNANCHORED, the substring can appea anywhere in the token (e.g., `load_word(R_T0, R_TapePtr, O_(Binds_X, field))` matches at position ~24).
|
||||
@@ -256,21 +244,6 @@ local BRANCH_PATTERN = "^branch_[%w_]+%s*%("
|
||||
-- The C preprocessor expands it BEFORE the metaprogram sees the source, but for source-level metadata consistency we still match it here and classify it as a branch_equal.
|
||||
-- This keeps `consuming_encoder` canonical for any downstream tooling that consults the metadata field.
|
||||
local JUMP_REL_PATTERN = "^jump_rel%s*%("
|
||||
local UNCOND_JUMP_PATTERNS = {
|
||||
"^%f[%w]jump%f[%W]",
|
||||
"^%f[%w]call_addr%f[%W]",
|
||||
}
|
||||
local TERMINAL_JUMP_PATTERNS = {
|
||||
"^%f[%w]jump_reg%f[%W]",
|
||||
"^%f[%w]call_reg%f[%W]",
|
||||
"^%f[%w]jump_link%f[%W]",
|
||||
}
|
||||
local function matches_any(tok, patterns)
|
||||
for i = 1, #patterns do
|
||||
if tok:match(patterns[i]) then return true end
|
||||
end
|
||||
return false
|
||||
end
|
||||
|
||||
local function classify_tokens(tokens)
|
||||
local n = #tokens
|
||||
@@ -303,7 +276,8 @@ local function classify_tokens(tokens)
|
||||
local is_unconditional_jump = false
|
||||
local is_terminal_jump = false
|
||||
local branch_label = nil
|
||||
local is_load = LOAD_INSTRUCTION_IDENTS[ident] == true
|
||||
local isa = duffle.instr(ident)
|
||||
local is_load = isa and isa.kind == "load"
|
||||
local is_store_word = ident == "store_word"
|
||||
|
||||
-- Per-check pre-computes (R3 lift).
|
||||
@@ -319,18 +293,18 @@ local function classify_tokens(tokens)
|
||||
if ident == "atom_label" then
|
||||
is_atom_label = true
|
||||
label_name = tok:match("^atom_label%s*%(%s*([%w_]+)%s*%)")
|
||||
elseif tok:match(BRANCH_PATTERN) or tok:match(JUMP_REL_PATTERN) then
|
||||
elseif (isa and isa.kind == "branch") or tok:match(BRANCH_PATTERN) or tok:match(JUMP_REL_PATTERN) then
|
||||
-- Conditional branch OR `jump_rel` (the within-atom-safe unconditional jump alias).
|
||||
-- Both encode a 16-bit signed relative word offset.
|
||||
is_branch = true
|
||||
branch_label = tok:match("atom_offset%s*%([^,]+,%s*([%w_]+)%s*%)") or false
|
||||
elseif matches_any(tok, UNCOND_JUMP_PATTERNS) then
|
||||
elseif ident == "jump" or ident == "call_addr" then
|
||||
-- Unconditional absolute jump / call: `jump(off)` / `call_addr(off)`.
|
||||
-- One immediate offset field; can carry an `atom_offset(F, T)` marker (the offsets pass dispatches on `consuming_encoder` — see `passes/offsets.lua::compute_offsets`).
|
||||
is_branch = true
|
||||
is_unconditional_jump = true
|
||||
branch_label = tok:match("atom_offset%s*%([^,]+,%s*([%w_]+)%s*%)") or false
|
||||
elseif matches_any(tok, TERMINAL_JUMP_PATTERNS) then
|
||||
elseif ident == "jump_reg" or ident == "call_reg" or ident == "jump_link" then
|
||||
-- Register-form jump / call: no offset field; `atom_offset` is invalid here (the offsets pass will error if one is supplied).
|
||||
-- Transfers control OUT of the current atom — the CFG treats this as a path terminator.
|
||||
is_terminal_jump = true
|
||||
@@ -427,8 +401,7 @@ end
|
||||
local function is_gte_command(consumer_event)
|
||||
local tok = consumer_event.encoder or consumer_event.ident or ""
|
||||
if tok:sub(1, 9) == "gte_cmdw_" then return true end
|
||||
local aliases = duffle.GTE_COMMAND_ALIASES or {}
|
||||
return aliases[tok] ~= nil
|
||||
return duffle.gte(tok) ~= nil
|
||||
end
|
||||
|
||||
-- True iff `consumer_word` falls inside the COP2 command's input set OR inside the producer's `fanout_to` set (for IRGB writes).
|
||||
@@ -443,12 +416,11 @@ local function is_cop2_consumer_of(consumer_event, destination, producer_rel)
|
||||
if pos == destination then return true end
|
||||
end
|
||||
-- Match via the command's input set: the consumer encoder resolves to a `gte_cmdw_*`
|
||||
-- short form whose `duffle.GTE_COMMAND_INPUTS` entry includes the destination (or a fan-out target).
|
||||
local aliases = duffle.GTE_COMMAND_ALIASES or {}
|
||||
local canonical = aliases[consumer_token] or consumer_token
|
||||
if canonical:sub(1, 9) == "gte_cmdw_" or aliases[consumer_token] then
|
||||
local inputs = duffle.GTE_COMMAND_INPUTS or {}
|
||||
local cmd_inputs = inputs[canonical]
|
||||
-- short form whose GTE_COMMAND.inputs includes the destination (or a fan-out target).
|
||||
local gte = duffle.gte(consumer_token)
|
||||
local canonical = duffle.gte_canon(consumer_token)
|
||||
if gte or canonical:sub(1, 9) == "gte_cmdw_" then
|
||||
local cmd_inputs = gte and gte.inputs
|
||||
if cmd_inputs then
|
||||
-- Direct hit.
|
||||
for _, in_reg in ipairs(cmd_inputs) do
|
||||
@@ -659,8 +631,11 @@ local function apply_gpr_effects(ev, forward_state)
|
||||
local ev_ident = ev.encoder or ev.ident
|
||||
local ev_args = ev.args or {}
|
||||
local gpr_values = forward_state.gpr_values
|
||||
local effects = duffle.INSTRUCTION_GPR_EFFECTS or {}
|
||||
local row = effects[ev_ident]
|
||||
local isa = duffle.instr(ev_ident)
|
||||
local row = isa and (isa.reads or isa.writes) and isa or nil
|
||||
if row == nil and duffle.gte(ev_ident) then
|
||||
row = { reads = {}, writes = {} }
|
||||
end
|
||||
if row == nil then
|
||||
for pos, operand in ipairs(ev_args) do
|
||||
local key = gpr_identity(ev, pos) or operand
|
||||
@@ -671,7 +646,7 @@ local function apply_gpr_effects(ev, forward_state)
|
||||
return
|
||||
end
|
||||
|
||||
local value_rule = (duffle.GPR_VALUE_RULES or {})[ev_ident]
|
||||
local value_rule = isa and isa.value
|
||||
local value = value_rule and evaluate_gpr_value_rule(value_rule, ev_args, gpr_values) or nil
|
||||
for _, position in ipairs(row.writes or {}) do
|
||||
local destination = gpr_identity(ev, position)
|
||||
@@ -688,8 +663,7 @@ end
|
||||
-- Look up the alias of a GTE command ident.
|
||||
-- Defaults to the input ident so unknown idents surface rather than silently inheriting a 0-cycle command input set.
|
||||
local function canonical_command(ident)
|
||||
local aliases = duffle.GTE_COMMAND_ALIASES or {}
|
||||
return aliases[ident] or ident
|
||||
return duffle.gte_canon(ident)
|
||||
end
|
||||
|
||||
-- True for a COP2/GTE use that can make a pending SR.CU2 transition observable.
|
||||
@@ -1041,8 +1015,8 @@ local function analyze_hardware_relations(atom)
|
||||
local canonical = canonical_command(ev_ident)
|
||||
if canonical:sub(1, 9) == "gte_cmdw_" then
|
||||
-- Update the post-command role state.
|
||||
local outputs = duffle.GTE_COMMAND_OUTPUTS or {}
|
||||
local cmd_outputs = outputs[canonical]
|
||||
local gte_row = duffle.gte(canonical)
|
||||
local cmd_outputs = gte_row and gte_row.outputs
|
||||
if cmd_outputs then
|
||||
for _, out in ipairs(cmd_outputs) do
|
||||
if out.register then
|
||||
@@ -1058,8 +1032,7 @@ local function analyze_hardware_relations(atom)
|
||||
end
|
||||
end
|
||||
-- Stage post-command latch relations for every measured output.
|
||||
local latch_table = duffle.GTE_COMMAND_LATCH_WINDOWS or {}
|
||||
local cmd_latches = latch_table[canonical]
|
||||
local cmd_latches = gte_row and gte_row.latch
|
||||
if cmd_latches then
|
||||
for _, latch in ipairs(cmd_latches) do
|
||||
if latch.register and latch.required then
|
||||
@@ -1237,7 +1210,6 @@ local function check_hazard_nop_use(atom, _pipe_ctx, findings)
|
||||
local forward = atom.paths and atom.paths.forward_state
|
||||
local events = atom.paths.word_events or {}
|
||||
-- GPR effects table used to resolve load destinations when classifying load-delay-slot nops.
|
||||
local gpr_effects = duffle.INSTRUCTION_GPR_EFFECTS or {}
|
||||
if not events or #events == 0 then return end
|
||||
-- Runtime-helper atoms / components (e.g. tape_exit, ac_yield) carry `debug_skip = true` from the bare
|
||||
-- `atom_dbg_skip` marker; their structural nops are part of the fixed handshake and not author choices.
|
||||
@@ -1260,8 +1232,10 @@ local function check_hazard_nop_use(atom, _pipe_ctx, findings)
|
||||
-- Every BD-slot nop is structural; this check never reports on it.
|
||||
-- (The earlier `if not suppressed then is_bd_slot = true end` form inverted the suppression - the `mac_yield()` handshake's `jump_reg(R_AtomJmp)` was incorrectly flagged.)
|
||||
local prev_ident = prev_ev.encoder or ""
|
||||
local bd_policies = duffle.CONTROL_TRANSFER_DELAY_SLOT_POLICIES or {}
|
||||
local is_bd_slot = bd_policies[prev_ident] ~= nil
|
||||
local prev_isa = duffle.instr(prev_ident)
|
||||
local is_bd_slot = prev_isa
|
||||
and (prev_isa.kind == "branch" or prev_isa.kind == "jump" or prev_isa.kind == "call")
|
||||
and prev_isa.delay_slot ~= false
|
||||
if not is_bd_slot then
|
||||
-- Find a pending modeled relation that this nop would retire.
|
||||
local retired = nil
|
||||
@@ -1280,11 +1254,10 @@ local function check_hazard_nop_use(atom, _pipe_ctx, findings)
|
||||
if f_word > ev_word then
|
||||
local f_ident = future_ev.encoder or future_ev.ident or ""
|
||||
local f_args = future_ev.args or {}
|
||||
local aliases = duffle.GTE_COMMAND_ALIASES or {}
|
||||
local canonical = aliases[f_ident] or f_ident
|
||||
local f_gte = duffle.gte(f_ident)
|
||||
local canonical = duffle.gte_canon(f_ident)
|
||||
if canonical:sub(1, 9) == "gte_cmdw_" then
|
||||
local inputs = duffle.GTE_COMMAND_INPUTS or {}
|
||||
local cmd_inputs = inputs[canonical]
|
||||
local cmd_inputs = f_gte and f_gte.inputs
|
||||
if cmd_inputs then
|
||||
for _, in_reg in ipairs(cmd_inputs) do
|
||||
if in_reg == retired.destination then
|
||||
@@ -1323,20 +1296,10 @@ local function check_hazard_nop_use(atom, _pipe_ctx, findings)
|
||||
-- This `nop` is structurally required; classifying it as `modeled-required` is the correct signal
|
||||
-- (removing it would make the following instruction read the OLD value of the loaded register, a load-use hazard).
|
||||
-- The `load_delay_violations` check (Concern 3) catches the actual read-side error; here we suppress the `modeled-redundant` misclassification.
|
||||
-- The set of load instructions mirrors the LOAD_INSTRUCTION_IDENTS in `check_load_delay_slots`.
|
||||
local load_idents = {
|
||||
load_word = true,
|
||||
load_half = true,
|
||||
load_half_u = true,
|
||||
load_byte = true,
|
||||
load_byte_u = true,
|
||||
gte_lw = true,
|
||||
gte_lwc2 = true
|
||||
}
|
||||
local is_load_delay = load_idents[prev_ident] == true
|
||||
local is_load_delay = prev_isa and prev_isa.kind == "load"
|
||||
if is_load_delay then
|
||||
-- Determine the destination register from the load's `writes` field.
|
||||
local prev_writes = gpr_effects[prev_ident] and gpr_effects[prev_ident].writes or {}
|
||||
local prev_writes = prev_isa.writes or {}
|
||||
local dest_pos = prev_writes[1]
|
||||
local load_dest = dest_pos and (gpr_identity(prev_ev, dest_pos) or (prev_ev.args or {})[dest_pos]) or "<load-destination>"
|
||||
local authored = dest_pos and (prev_ev.args or {})[dest_pos] or load_dest
|
||||
@@ -1383,8 +1346,7 @@ local function check_hazard_nop_use(atom, _pipe_ctx, findings)
|
||||
-- Update the pending snapshot for the next iteration.
|
||||
-- The replay is observation-only; we mirror the walker's staging
|
||||
-- behavior for MTC2 / CTC2 / LWC2 / MFC2 / CFC2 / MFC0 / command_latch.
|
||||
local aliases = duffle.GTE_COMMAND_ALIASES or {}
|
||||
local canonical = aliases[ev_ident] or ev_ident
|
||||
local canonical = duffle.gte_canon(ev_ident)
|
||||
if ev_ident == "gte_mv_to_data_r" or ev_ident == "gte_mv_to_ctrl_r" then
|
||||
local relations_table = duffle.HARDWARE_RELATIONS or {}
|
||||
for _, row in ipairs(relations_table) do
|
||||
@@ -1407,8 +1369,7 @@ local function check_hazard_nop_use(atom, _pipe_ctx, findings)
|
||||
end
|
||||
elseif canonical:sub(1, 9) == "gte_cmdw_" then
|
||||
-- Command: stage post-command latch relations (same as the walker).
|
||||
local latch_table = duffle.GTE_COMMAND_LATCH_WINDOWS or {}
|
||||
local cmd_latches = latch_table[canonical]
|
||||
local cmd_latches = (duffle.gte(canonical) or {}).latch
|
||||
if cmd_latches then
|
||||
for _, latch in ipairs(cmd_latches) do
|
||||
if latch.register and latch.required then
|
||||
@@ -1459,13 +1420,16 @@ local function check_control_transfer_delay_slot_use(atom, pipe_ctx, findings)
|
||||
-- Runtime-helper atoms / components (e.g. tape_exit, ac_yield) carry `debug_skip = true` from the bare
|
||||
-- `atom_dbg_skip` marker; their structural BD slots are part of the fixed handshake.
|
||||
if is_runtime_helper(atom) then return end
|
||||
local policies = duffle.CONTROL_TRANSFER_DELAY_SLOT_POLICIES or {}
|
||||
for event_idx, event in ipairs(events) do
|
||||
-- Canonical word_events use `encoder` as the leading identifier of the emitting token).
|
||||
-- Focused inputs may supply `ident` when constructing isolated events.
|
||||
local event_ident = event.encoder or event.ident
|
||||
local slot_ident_field = event.encoder and "encoder" or "ident"
|
||||
local policy = policies[event_ident]
|
||||
local event_isa = duffle.instr(event_ident)
|
||||
local policy = event_isa
|
||||
and (event_isa.kind == "branch" or event_isa.kind == "jump" or event_isa.kind == "call")
|
||||
and event_isa.delay_slot ~= false
|
||||
and { family = event_isa.kind, suppress_arg1 = event_isa.suppress_arg1 }
|
||||
if policy then
|
||||
local arg1 = event.args and event.args[1] or nil
|
||||
local suppressed = policy.suppress_arg1 and policy.suppress_arg1[arg1] or nil
|
||||
@@ -1518,7 +1482,6 @@ local function check_load_delay_slots(atom, pipe_ctx, findings)
|
||||
local events = p.word_events or {}
|
||||
if #events == 0 then return end
|
||||
|
||||
local gpr_effects = duffle.INSTRUCTION_GPR_EFFECTS or {}
|
||||
local read_positions = duffle.OPERAND_READ_POSITIONS or {}
|
||||
-- volatile_until[reg] = 1-based word_events index; the slot AFTER which the register is safe.
|
||||
-- `nil` means "not currently volatile".
|
||||
@@ -1530,7 +1493,7 @@ local function check_load_delay_slots(atom, pipe_ctx, findings)
|
||||
-- and for genuine RMW ops like `add rt, rs, rt` where position 1 IS both read+written) is not a "read" for load-delay purposes:
|
||||
-- The write shadows whatever value the register previously held. Only positions that are reads WITHOUT a co-occurring write to the same register count as net reads.
|
||||
local function net_reads(event_ident, args)
|
||||
local effect = gpr_effects[event_ident]
|
||||
local effect = duffle.instr(event_ident)
|
||||
local positions = read_positions[event_ident]
|
||||
if not positions then return {} end
|
||||
local writes_set = {}
|
||||
@@ -1547,7 +1510,8 @@ local function check_load_delay_slots(atom, pipe_ctx, findings)
|
||||
for event_idx, event in ipairs(events) do
|
||||
local event_ident = event.encoder or event.ident
|
||||
local args = event.args or {}
|
||||
local is_load = LOAD_INSTRUCTION_IDENTS[event_ident] == true
|
||||
local event_isa = duffle.instr(event_ident)
|
||||
local is_load = event_isa and event_isa.kind == "load"
|
||||
|
||||
-- (1) Is this event reading a register that's still volatile from a previous load?
|
||||
-- Skip the load instruction itself (the load's own argument list may "read" its destination via `OPERAND_READ_POSITIONS`:
|
||||
@@ -1577,7 +1541,7 @@ local function check_load_delay_slots(atom, pipe_ctx, findings)
|
||||
end
|
||||
|
||||
-- (2) Update the volatile set based on what this event writes.
|
||||
local effect = gpr_effects[event_ident]
|
||||
local effect = event_isa
|
||||
if effect and effect.writes then
|
||||
for _, pos in ipairs(effect.writes) do
|
||||
local reg = gpr_identity(event, pos)
|
||||
@@ -2168,7 +2132,9 @@ local function analyze_atom_paths(atom, pipe_ctx)
|
||||
unknown_set[ident] = true
|
||||
end
|
||||
else
|
||||
cost = duffle.INSTRUCTION_LATENCY[ident]
|
||||
local isa = duffle.instr(ident)
|
||||
local gte = duffle.gte(ident)
|
||||
cost = (isa and isa.cycles) or (gte and gte.cycles)
|
||||
if cost == nil then
|
||||
cost = duffle.UNKNOWN_INSTRUCTION_CYCLES
|
||||
unknown_set[ident] = true
|
||||
@@ -2929,12 +2895,12 @@ end
|
||||
-- from duffle.lua. Only fires on parseable integer literals; register names,
|
||||
-- O_(...) offsets, atom_offset(...) markers, and enum tokens are skipped.
|
||||
local function check_immediate_field_width(atom, pipe_ctx, findings)
|
||||
local widths = duffle.IMMEDIATE_FIELD_WIDTHS or {}
|
||||
local events = atom.paths and atom.paths.word_events or {}
|
||||
local line_for_word_event = pipe_ctx.line_for_word_event
|
||||
for _, ev in ipairs(events) do
|
||||
local ev_ident = ev.encoder or ev.ident or "?"
|
||||
local rules = widths[ev_ident]
|
||||
local isa = duffle.instr(ev_ident)
|
||||
local rules = isa and isa.imm
|
||||
if rules then
|
||||
local ev_args = ev.args or {}
|
||||
local ev_line = line_for_word_event and line_for_word_event(ev) or atom.line
|
||||
@@ -3040,7 +3006,7 @@ local function collect_gpr_traffic(tokens)
|
||||
and ident ~= "nop" and ident ~= "atom_label" and ident ~= "atom_offset"
|
||||
then
|
||||
local args = token_arg_list(tok)
|
||||
local fx = (duffle.INSTRUCTION_GPR_EFFECTS or {})[ident]
|
||||
local fx = duffle.instr(ident)
|
||||
if fx then
|
||||
for _, pos in ipairs(fx.reads or {}) do
|
||||
local g = arg_as_gpr(args[pos])
|
||||
@@ -3228,42 +3194,11 @@ local CHECK_RULES = {
|
||||
--- @param ctx PassCtx
|
||||
--- @return PipeCtx
|
||||
local function build_corpus_pipe_ctx(ctx)
|
||||
local corpus = ctx.shared and ctx.shared.corpus
|
||||
if not corpus then
|
||||
error("static_analysis requires ctx.shared.corpus "
|
||||
.. "(the canonical corpus is the source of truth; "
|
||||
.. "no per-source fallback is supported)", 0)
|
||||
end
|
||||
-- The pipe_ctx views REFERENCE the corpus tables directly (no copies).
|
||||
-- Every consumer observes mutations through the corpus tables directly.
|
||||
return {
|
||||
-- Cross-source lookup tables.
|
||||
register_alias_registry = corpus.register_alias_registry or {},
|
||||
type_name_registry = corpus.type_name_registry or {},
|
||||
atom_views = corpus.atom_views or {},
|
||||
atom_ctxs = corpus.atom_ctxs or {},
|
||||
atom_phases = corpus.atom_phases or {},
|
||||
binds_by_name = corpus.binds_by_name or {},
|
||||
atoms_by_name = corpus.atoms_by_name or {},
|
||||
-- Per-component metadata (cycle_cost + gp0_contrib) auto-derived from the original
|
||||
-- `MipsAtomComp_` body by `passes/components.lua::compute_components_metadata`.
|
||||
-- Keyed by bare name (e.g. `format_f3_color`, `gte_store_f3`); the `mac_` prefix at call sites is stripped before lookup.
|
||||
components_by_name = corpus.components or {},
|
||||
atoms_by_name = corpus.atoms_by_name or {},
|
||||
tape_chains = corpus.tape_chains or {},
|
||||
source_order = corpus.source_order or {},
|
||||
component_atom_infos = corpus.component_atom_infos or {},
|
||||
atom_infos = corpus.atom_infos or {},
|
||||
-- Corpus-wide ordered list of atom_info records (source-order + duplicates).
|
||||
atom_infos_list = corpus.atom_infos or {},
|
||||
-- Corpus-wide collisions (recorded by scan_source.merge_corpus_registries).
|
||||
collisions = corpus.collisions or {},
|
||||
-- GTE control-register alias groups (from `duffle.GTE_CR_ALIAS_GROUPS`).
|
||||
-- The three new per_atom checks (gte_cr_alias_writes, rtdiagonal_completeness,
|
||||
-- gte_cr_TR_naming) read from this view. `duffle` is exposed alongside so
|
||||
-- `find_alias_pair_for` can resolve alias → group without a separate registry.
|
||||
gte_cr_alias_groups = duffle.GTE_CR_ALIAS_GROUPS or {},
|
||||
}
|
||||
local view = duffle.corpus_view(ctx)
|
||||
view.components_by_name = view.components
|
||||
view.atom_infos_list = view.atom_infos
|
||||
view.gte_cr_alias_groups = duffle.GTE_CR_ALIAS_GROUPS or {}
|
||||
return view
|
||||
end
|
||||
|
||||
local function validate(ctx, src, corpus_pipe_ctx)
|
||||
@@ -3360,17 +3295,13 @@ local function validate(ctx, src, corpus_pipe_ctx)
|
||||
|
||||
-- Run all per-atom checks on this one atom via the CHECK_RULES data table.
|
||||
-- Adding a new check = 1 row in CHECK_RULES; this loop never needs editing.
|
||||
for _, rule in ipairs(CHECK_RULES) do
|
||||
if rule.per_atom then rule.per_atom(a, pipe_ctx, findings) end
|
||||
end
|
||||
duffle.run_check_rules(CHECK_RULES, "per_atom", a, pipe_ctx, findings)
|
||||
end
|
||||
|
||||
-- Per-source dispatch. Run once per source AFTER the per-atom loop;
|
||||
-- consults pipe_ctx's cross-atom registries (register_alias_registry, type_name_registry).
|
||||
-- Same CHECK_RULES table; no parallel dispatch table.
|
||||
for _, rule in ipairs(CHECK_RULES) do
|
||||
if rule.per_source then rule.per_source(src, pipe_ctx, findings) end
|
||||
end
|
||||
duffle.run_check_rules(CHECK_RULES, "per_source", src, pipe_ctx, findings)
|
||||
|
||||
-- Three-way severity binning: per-finding severity is set by the check via `f.kind`.
|
||||
-- "error" / "warning" / "info" are all distinct; info findings are NEVER folded into warnings.
|
||||
|
||||
Reference in New Issue
Block a user