mirror of
https://github.com/Ed94/pikuma_ps1.git
synced 2026-08-05 23:28:47 +00:00
preparing for a big refactor
This commit is contained in:
+285
-256
@@ -129,8 +129,20 @@ local OUTPUT_EXTENSION = ".static_analysis.txt"
|
|||||||
--- Used by the checks to convert per-token offsets in the body to lina numbers relative to the start of `body`.
|
--- Used by the checks to convert per-token offsets in the body to lina numbers relative to the start of `body`.
|
||||||
--- The atom's source-line of the body-start is added by the caller.
|
--- The atom's source-line of the body-start is added by the caller.
|
||||||
---
|
---
|
||||||
|
-- Memoization cache for build_body_line_index / tokenize_body.
|
||||||
|
-- Atom bodies are immutable for the duration of a validate() pass (they come from src.scan which is set once by scan-source),
|
||||||
|
-- so the same body string is safe to use as a cache key. Each call site (5 check_* functions + analyze_atom_paths)
|
||||||
|
-- was re-running the full O(body_len) scan on the SAME body. After memoization, the first call pays the O(body_len) cost,
|
||||||
|
-- every subsequent call on the same body returns the cached table in O(1).
|
||||||
|
--
|
||||||
|
-- (Future-proofing: if a caller ever mutates the returned tokens / line-index tables, the cache aliasing means those
|
||||||
|
-- mutations would leak across atoms. Current callers all read-only; safe today.)
|
||||||
|
local _body_line_index_cache = {}
|
||||||
|
local _tokenize_body_cache = {}
|
||||||
|
|
||||||
--- Simple line-counting: count `\n` chars from offset 1 up to the offset; that count + 1 is the line number (1-based).
|
--- Simple line-counting: count `\n` chars from offset 1 up to the offset; that count + 1 is the line number (1-based).
|
||||||
local function build_body_line_index(body)
|
local function build_body_line_index(body)
|
||||||
|
if _body_line_index_cache[body] ~= nil then return _body_line_index_cache[body] end
|
||||||
local index = {}
|
local index = {}
|
||||||
local len = #body
|
local len = #body
|
||||||
local newline_count = 0
|
local newline_count = 0
|
||||||
@@ -144,6 +156,7 @@ local function build_body_line_index(body)
|
|||||||
end
|
end
|
||||||
-- Offsets beyond the body still resolve to the final line
|
-- Offsets beyond the body still resolve to the final line
|
||||||
index[len + 1] = newline_count + 1
|
index[len + 1] = newline_count + 1
|
||||||
|
_body_line_index_cache[body] = index
|
||||||
return index
|
return index
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -171,6 +184,7 @@ end
|
|||||||
--- `body_rel_offset` is the char offset within `body` of the start of the token.
|
--- `body_rel_offset` is the char offset within `body` of the start of the token.
|
||||||
--- Callers add it to the atom's `body_off` to get an absolute source position for line tracking.
|
--- Callers add it to the atom's `body_off` to get an absolute source position for line tracking.
|
||||||
local function tokenize_body(body)
|
local function tokenize_body(body)
|
||||||
|
if _tokenize_body_cache[body] ~= nil then return _tokenize_body_cache[body] end
|
||||||
local out = {}
|
local out = {}
|
||||||
local len = #body
|
local len = #body
|
||||||
local rel = 1
|
local rel = 1
|
||||||
@@ -213,6 +227,7 @@ local function tokenize_body(body)
|
|||||||
end
|
end
|
||||||
rel = scan
|
rel = scan
|
||||||
end
|
end
|
||||||
|
_tokenize_body_cache[body] = out
|
||||||
return out
|
return out
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -277,16 +292,17 @@ end
|
|||||||
--- If count < the minimum declared in `duffle.GTE_PIPELINE_LATENCY[X]`, record a finding.
|
--- If count < the minimum declared in `duffle.GTE_PIPELINE_LATENCY[X]`, record a finding.
|
||||||
--- Aliases are resolved against the lookup table directly; if a macro name is not in the table, emit a soft warning
|
--- Aliases are resolved against the lookup table directly; if a macro name is not in the table, emit a soft warning
|
||||||
--- (the user might have added a new gte_cmdw_* but not updated duffle.lua).
|
--- (the user might have added a new gte_cmdw_* but not updated duffle.lua).
|
||||||
local function check_gte_pipeline_fill(atoms, findings, line_of)
|
--- Per-atom: walk this atom's tokens, check every `gte_cmdw_*` for pipeline-fill compliance.
|
||||||
for _, a in ipairs(atoms) do
|
--- Signature changed from `(atoms, findings)` to `(atom, findings)` in Stage 1B of the plex move:
|
||||||
local tokens = tokenize_body(a.body)
|
--- the per-atom iteration now lives in validate()'s single loop; each check is a per-atom predicate.
|
||||||
local line_in_body = build_body_line_index(a.body)
|
local function check_gte_pipeline_fill(atom, findings)
|
||||||
local tn = #tokens
|
local tokens = tokenize_body(atom.body)
|
||||||
local ti = 1
|
local line_in_body = build_body_line_index(atom.body)
|
||||||
while ti <= tn do
|
local tn = #tokens
|
||||||
check_one_gte_cmdw(a, tokens[ti].tok, tokens, ti, line_in_body, findings)
|
local ti = 1
|
||||||
ti = ti + 1
|
while ti <= tn do
|
||||||
end
|
check_one_gte_cmdw(atom, tokens[ti].tok, tokens, ti, line_in_body, findings)
|
||||||
|
ti = ti + 1
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -299,7 +315,7 @@ end
|
|||||||
---
|
---
|
||||||
--- Empty bodies are not currently flagged — runtime infrastructure atoms like `MipsAtom_(yield) { mac_yield() }`
|
--- Empty bodies are not currently flagged — runtime infrastructure atoms like `MipsAtom_(yield) { mac_yield() }`
|
||||||
--- and `MipsAtom_(tape_exit) { jump_reg(rret_addr), nop }` are valid as-is; mac_yield at the end is the contract.
|
--- and `MipsAtom_(tape_exit) { jump_reg(rret_addr), nop }` are valid as-is; mac_yield at the end is the contract.
|
||||||
local function check_mac_yield_uniformity(atoms, findings)
|
local function check_mac_yield_uniformity(atom, findings)
|
||||||
-- Per-kind semantics:
|
-- Per-kind semantics:
|
||||||
-- MipsAtom_ (baked atom): exactly 1 mac_yield at the end of the body. Control transfer is the atom's job.
|
-- MipsAtom_ (baked atom): exactly 1 mac_yield at the end of the body. Control transfer is the atom's job.
|
||||||
-- MipsAtomComp_ (bare static-array component): ZERO mac_yield.
|
-- MipsAtomComp_ (bare static-array component): ZERO mac_yield.
|
||||||
@@ -308,88 +324,86 @@ local function check_mac_yield_uniformity(atoms, findings)
|
|||||||
-- Same reasoning -- it's a function returning a MipsAtom slice, invoked from a parent atom.
|
-- Same reasoning -- it's a function returning a MipsAtom slice, invoked from a parent atom.
|
||||||
--
|
--
|
||||||
-- The GTE pipeline-fill check applies to all 3 kinds (see check_gte_pipeline_fill). Only the mac_yield rule branches on kind.
|
-- The GTE pipeline-fill check applies to all 3 kinds (see check_gte_pipeline_fill). Only the mac_yield rule branches on kind.
|
||||||
for _, a in ipairs(atoms) do
|
local tokens = tokenize_body(atom.body)
|
||||||
local tokens = tokenize_body(a.body)
|
local line_in_body = build_body_line_index(atom.body)
|
||||||
local line_in_body = build_body_line_index(a.body)
|
|
||||||
|
|
||||||
local count = 0
|
local count = 0
|
||||||
local last_idx = 0
|
local last_idx = 0
|
||||||
for tok_idx, t in ipairs(tokens) do
|
for tok_idx, t in ipairs(tokens) do
|
||||||
local tok = t.tok
|
local tok = t.tok
|
||||||
-- Match `mac_yield(...)` or just `mac_yield`. The bareword
|
-- Match `mac_yield(...)` or just `mac_yield`. The bareword
|
||||||
-- variant is rare in modern style but tolerated.
|
-- variant is rare in modern style but tolerated.
|
||||||
if tok:match("^mac_yield%s*%(") or tok == "mac_yield" then
|
if tok:match("^mac_yield%s*%(") or tok == "mac_yield" then
|
||||||
count = count + 1
|
count = count + 1
|
||||||
last_idx = tok_idx
|
last_idx = tok_idx
|
||||||
|
end
|
||||||
|
end
|
||||||
|
local function line_for(idx)
|
||||||
|
return atom.line + line_in_body[tokens[idx].rel]
|
||||||
|
end
|
||||||
|
|
||||||
|
if atom.kind == "atom" then
|
||||||
|
-- Baked atom: exactly 1 yield at the end.
|
||||||
|
if count == 0 then
|
||||||
|
findings[#findings + 1] = {
|
||||||
|
atom = atom.name,
|
||||||
|
line = atom.line,
|
||||||
|
check = "mac_yield_uniformity",
|
||||||
|
kind = "warning",
|
||||||
|
msg = string.format(
|
||||||
|
"%s at line %d has no `mac_yield()`; every atom must hand control to the next via mac_yield at end",
|
||||||
|
atom.name, atom.line),
|
||||||
|
}
|
||||||
|
elseif count > 1 then
|
||||||
|
findings[#findings + 1] = {
|
||||||
|
atom = atom.name,
|
||||||
|
line = line_for(last_idx),
|
||||||
|
check = "mac_yield_uniformity",
|
||||||
|
kind = "warning",
|
||||||
|
msg = string.format(
|
||||||
|
"%s at line %d has %d `mac_yield()` calls; exactly 1 is allowed",
|
||||||
|
atom.name, line_for(last_idx), count),
|
||||||
|
}
|
||||||
|
elseif last_idx < #tokens then
|
||||||
|
-- 1 call, but not the last token. We DON'T fail if the post-token is just `nop` or `nop2` or a branch with `, nop` delay slot.
|
||||||
|
-- It's the standard "yield, then BD nop" idiom.
|
||||||
|
local post_non_nop = false
|
||||||
|
for search_idx = last_idx + 1, #tokens do
|
||||||
|
local t = tokens[search_idx].tok
|
||||||
|
if t ~= "" and t ~= "nop" and t ~= "nop2"
|
||||||
|
and not t:match("%,%s*nop%)%s*$") then
|
||||||
|
post_non_nop = true
|
||||||
|
break
|
||||||
|
end
|
||||||
end
|
end
|
||||||
end
|
if post_non_nop then
|
||||||
local function line_for(idx)
|
|
||||||
return a.line + line_in_body[tokens[idx].rel]
|
|
||||||
end
|
|
||||||
|
|
||||||
if a.kind == "atom" then
|
|
||||||
-- Baked atom: exactly 1 yield at the end.
|
|
||||||
if count == 0 then
|
|
||||||
findings[#findings + 1] = {
|
findings[#findings + 1] = {
|
||||||
atom = a.name,
|
atom = atom.name,
|
||||||
line = a.line,
|
|
||||||
check = "mac_yield_uniformity",
|
|
||||||
kind = "warning",
|
|
||||||
msg = string.format(
|
|
||||||
"%s at line %d has no `mac_yield()`; every atom must hand control to the next via mac_yield at end",
|
|
||||||
a.name, a.line),
|
|
||||||
}
|
|
||||||
elseif count > 1 then
|
|
||||||
findings[#findings + 1] = {
|
|
||||||
atom = a.name,
|
|
||||||
line = line_for(last_idx),
|
line = line_for(last_idx),
|
||||||
check = "mac_yield_uniformity",
|
check = "mac_yield_uniformity",
|
||||||
kind = "warning",
|
kind = "warning",
|
||||||
msg = string.format(
|
msg = string.format(
|
||||||
"%s at line %d has %d `mac_yield()` calls; exactly 1 is allowed",
|
"%s at line %d has `mac_yield()` at token %d/%d; the yield must be the LAST non-nop token in the body",
|
||||||
a.name, line_for(last_idx), count),
|
atom.name, line_for(last_idx), last_idx, #tokens),
|
||||||
}
|
|
||||||
elseif last_idx < #tokens then
|
|
||||||
-- 1 call, but not the last token. We DON'T fail if the post-token is just `nop` or `nop2` or a branch with `, nop` delay slot.
|
|
||||||
-- It's the standard "yield, then BD nop" idiom.
|
|
||||||
local post_non_nop = false
|
|
||||||
for search_idx = last_idx + 1, #tokens do
|
|
||||||
local t = tokens[search_idx].tok
|
|
||||||
if t ~= "" and t ~= "nop" and t ~= "nop2"
|
|
||||||
and not t:match("%,%s*nop%)%s*$") then
|
|
||||||
post_non_nop = true
|
|
||||||
break
|
|
||||||
end
|
|
||||||
end
|
|
||||||
if post_non_nop then
|
|
||||||
findings[#findings + 1] = {
|
|
||||||
atom = a.name,
|
|
||||||
line = line_for(last_idx),
|
|
||||||
check = "mac_yield_uniformity",
|
|
||||||
kind = "warning",
|
|
||||||
msg = string.format(
|
|
||||||
"%s at line %d has `mac_yield()` at token %d/%d; the yield must be the LAST non-nop token in the body",
|
|
||||||
a.name, line_for(last_idx), last_idx, #tokens),
|
|
||||||
}
|
|
||||||
end
|
|
||||||
end
|
|
||||||
else
|
|
||||||
-- Component (comp_bare or comp_proc): ZERO yields.
|
|
||||||
-- The parent atom does the yield.
|
|
||||||
-- A yield inside a component would either be dead code (bare) or prematurely terminate the function (proc).
|
|
||||||
-- Both are bugs.
|
|
||||||
if count > 0 then
|
|
||||||
findings[#findings + 1] = {
|
|
||||||
atom = a.name,
|
|
||||||
line = line_for(last_idx),
|
|
||||||
check = "mac_yield_uniformity",
|
|
||||||
kind = "warning",
|
|
||||||
msg = string.format(
|
|
||||||
"%s at line %d is a %s component but has %d `mac_yield()` call(s); components must not yield (the parent atom does)",
|
|
||||||
a.name, line_for(last_idx), a.kind, count),
|
|
||||||
}
|
}
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
else
|
||||||
|
-- Component (comp_bare or comp_proc): ZERO yields.
|
||||||
|
-- The parent atom does the yield.
|
||||||
|
-- A yield inside a component would either be dead code (bare) or prematurely terminate the function (proc).
|
||||||
|
-- Both are bugs.
|
||||||
|
if count > 0 then
|
||||||
|
findings[#findings + 1] = {
|
||||||
|
atom = atom.name,
|
||||||
|
line = line_for(last_idx),
|
||||||
|
check = "mac_yield_uniformity",
|
||||||
|
kind = "warning",
|
||||||
|
msg = string.format(
|
||||||
|
"%s at line %d is a %s component but has %d `mac_yield()` call(s); components must not yield (the parent atom does)",
|
||||||
|
atom.name, line_for(last_idx), atom.kind, count),
|
||||||
|
}
|
||||||
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
-- ════════════════════════════════════════════════════════════════════════════
|
-- ════════════════════════════════════════════════════════════════════════════
|
||||||
@@ -407,77 +421,73 @@ end
|
|||||||
--- 1. Body MUST contain one `load_word(R_*, R_TapePtr, O_(Binds_X, field))` per field of Binds_X. Missing field = error.
|
--- 1. Body MUST contain one `load_word(R_*, R_TapePtr, O_(Binds_X, field))` per field of Binds_X. Missing field = error.
|
||||||
--- 2. Body MUST contain an `add_ui_self(R_TapePtr, S_(Binds_X))` (or equivalent advance by the struct's byte count). Missing = error.
|
--- 2. Body MUST contain an `add_ui_self(R_TapePtr, S_(Binds_X))` (or equivalent advance by the struct's byte count). Missing = error.
|
||||||
--- 3. atom_bind(Binds_X) where Binds_X doesn't exist = error.
|
--- 3. atom_bind(Binds_X) where Binds_X doesn't exist = error.
|
||||||
local function check_abi_handoff(atoms, atom_infos, binds_index, findings)
|
--- Per-atom: verify the atom body reads every field of its `Binds_X` from R_TapePtr and advances R_TapePtr by S_(Binds_X).
|
||||||
local info_by_atom = {}
|
--- Signature changed in Stage 1B: takes `(atom, pipe_ctx, findings)` where `pipe_ctx` carries the cross-atom
|
||||||
for _, info in ipairs(atom_infos) do
|
--- `info_by_atom` + `binds_index` tables (built once by validate() before the per-atom loop).
|
||||||
info_by_atom[info.atom_name] = info
|
--- Per-atom iteration now lives in validate(); this is a per-atom predicate.
|
||||||
|
local function check_abi_handoff(atom, pipe_ctx, findings)
|
||||||
|
local info = pipe_ctx.info_by_atom[atom.name]
|
||||||
|
if not info or not info.binds then return end
|
||||||
|
local binds_name = info.binds
|
||||||
|
local binds = pipe_ctx.binds_index[binds_name]
|
||||||
|
if not binds then
|
||||||
|
findings[#findings + 1] = {
|
||||||
|
atom = atom.name, line = atom.line,
|
||||||
|
check = "abi_handoff", kind = "error",
|
||||||
|
msg = string.format("%s at line %d has `atom_bind(%s)` but no `typedef Struct_(%s)` declaration found in source",
|
||||||
|
atom.name, atom.line, binds_name, binds_name),
|
||||||
|
}
|
||||||
|
return
|
||||||
end
|
end
|
||||||
|
local tokens = tokenize_body(atom.body)
|
||||||
for _, a in ipairs(atoms) do
|
local line_in_body = build_body_line_index(atom.body)
|
||||||
local info = info_by_atom[a.name]
|
local found_field_set = {}
|
||||||
if info and info.binds then
|
local found_advance = false
|
||||||
local binds_name = info.binds
|
local bind_re = "O_%(" .. binds_name .. ",%s*([%w_]+)%s*%)"
|
||||||
local binds = binds_index[binds_name]
|
for _, t in ipairs(tokens) do
|
||||||
if not binds then
|
local tok = t.tok
|
||||||
findings[#findings + 1] = {
|
if tok:match("^load_word%s*%(") then
|
||||||
atom = a.name, line = a.line,
|
if tok:find("R_TapePtr", 1, true) and tok:find("O_(" .. binds_name .. ",", 1, true) then
|
||||||
check = "abi_handoff", kind = "error",
|
local field = tok:match(bind_re)
|
||||||
msg = string.format("%s at line %d has `atom_bind(%s)` but no `typedef Struct_(%s)` declaration found in source",
|
-- scan: load_word(R_*, R_TapePtr, O_(<Binds_X>, <field>))
|
||||||
a.name, a.line, binds_name, binds_name),
|
if field then
|
||||||
}
|
found_field_set[field] = true
|
||||||
else
|
else
|
||||||
local tokens = tokenize_body(a.body)
|
local body_line = atom.line + line_in_body[t.rel]
|
||||||
local line_in_body = build_body_line_index(a.body)
|
|
||||||
local found_field_set = {}
|
|
||||||
local found_advance = false
|
|
||||||
local bind_re = "O_%(" .. binds_name .. ",%s*([%w_]+)%s*%)"
|
|
||||||
for _, t in ipairs(tokens) do
|
|
||||||
local tok = t.tok
|
|
||||||
if tok:match("^load_word%s*%(") then
|
|
||||||
if tok:find("R_TapePtr", 1, true) and tok:find("O_(" .. binds_name .. ",", 1, true) then
|
|
||||||
local field = tok:match(bind_re)
|
|
||||||
-- scan: load_word(R_*, R_TapePtr, O_(<Binds_X>, <field>))
|
|
||||||
if field then
|
|
||||||
found_field_set[field] = true
|
|
||||||
else
|
|
||||||
local body_line = a.line + line_in_body[t.rel]
|
|
||||||
findings[#findings + 1] = {
|
|
||||||
atom = a.name, line = body_line,
|
|
||||||
check = "abi_handoff", kind = "error",
|
|
||||||
msg = string.format("%s at line %d has load_word(R_TapePtr, O_(%s, <non-ident>)); expected O_(%s, <field>)",
|
|
||||||
a.name, body_line, binds_name, binds_name),
|
|
||||||
}
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
if tok:find("R_TapePtr", 1, true)
|
|
||||||
and tok:find("S_(" .. binds_name .. ")", 1, true) then
|
|
||||||
-- scan: add_ui_self(R_TapePtr, S_(<Binds_X>))
|
|
||||||
found_advance = true
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
for _, f in ipairs(binds.fields) do
|
|
||||||
if not found_field_set[f.name] then
|
|
||||||
findings[#findings + 1] = {
|
|
||||||
atom = a.name, line = a.line,
|
|
||||||
check = "abi_handoff", kind = "error",
|
|
||||||
msg = string.format("%s at line %d binds %s but never loads field `%s` from R_TapePtr (expected O_(%s, %s))",
|
|
||||||
a.name, a.line, binds_name, f.name, binds_name, f.name),
|
|
||||||
}
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
if not found_advance then
|
|
||||||
findings[#findings + 1] = {
|
findings[#findings + 1] = {
|
||||||
atom = a.name, line = a.line,
|
atom = atom.name, line = body_line,
|
||||||
check = "abi_handoff", kind = "error",
|
check = "abi_handoff", kind = "error",
|
||||||
msg = string.format("%s at line %d binds %s but never advances R_TapePtr by S_(%s) (= %d bytes / %d words)",
|
msg = string.format("%s at line %d has load_word(R_TapePtr, O_(%s, <non-ident>)); expected O_(%s, <field>)",
|
||||||
a.name, a.line, binds_name, binds_name, binds.bytes, binds.bytes / 4),
|
atom.name, body_line, binds_name, binds_name),
|
||||||
}
|
}
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
if tok:find("R_TapePtr", 1, true)
|
||||||
|
and tok:find("S_(" .. binds_name .. ")", 1, true) then
|
||||||
|
-- scan: add_ui_self(R_TapePtr, S_(<Binds_X>))
|
||||||
|
found_advance = true
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
for _, f in ipairs(binds.fields) do
|
||||||
|
if not found_field_set[f.name] then
|
||||||
|
findings[#findings + 1] = {
|
||||||
|
atom = atom.name, line = atom.line,
|
||||||
|
check = "abi_handoff", kind = "error",
|
||||||
|
msg = string.format("%s at line %d binds %s but never loads field `%s` from R_TapePtr (expected O_(%s, %s))",
|
||||||
|
atom.name, atom.line, binds_name, f.name, binds_name, f.name),
|
||||||
|
}
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
if not found_advance then
|
||||||
|
findings[#findings + 1] = {
|
||||||
|
atom = atom.name, line = atom.line,
|
||||||
|
check = "abi_handoff", kind = "error",
|
||||||
|
msg = string.format("%s at line %d binds %s but never advances R_TapePtr by S_(%s) (= %d bytes / %d words)",
|
||||||
|
atom.name, atom.line, binds_name, binds_name, binds.bytes, binds.bytes / 4),
|
||||||
|
}
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -495,68 +505,67 @@ end
|
|||||||
--- - Atoms containing a `mac_<name>(...)` call whose name is not in duffle.GP0_MACRO_CONTRIB emit a "new macro; update duffle.GP0_MACRO_CONTRIB" advisory.
|
--- - Atoms containing a `mac_<name>(...)` call whose name is not in duffle.GP0_MACRO_CONTRIB emit a "new macro; update duffle.GP0_MACRO_CONTRIB" advisory.
|
||||||
---
|
---
|
||||||
--- Applies only to `kind = "atom"` (baked atoms). Components don't emit full primitives.
|
--- Applies only to `kind = "atom"` (baked atoms). Components don't emit full primitives.
|
||||||
local function check_gpu_portstore_shape(atoms, findings)
|
--- Per-atom: detect which GP0 primitive the atom is emitting, sum the macro contributions,
|
||||||
for _, a in ipairs(atoms) do
|
--- compare to the expected packet size. Signature changed in Stage 1B: `(atom, findings)`.
|
||||||
if a.kind == "atom" then
|
local function check_gpu_portstore_shape(atom, findings)
|
||||||
local tokens = tokenize_body(a.body)
|
if atom.kind ~= "atom" then return end
|
||||||
local line_in_body = build_body_line_index(a.body)
|
local tokens = tokenize_body(atom.body)
|
||||||
local cmd_byte = nil
|
local line_in_body = build_body_line_index(atom.body)
|
||||||
local cmd_line = nil
|
local cmd_byte = nil
|
||||||
local contrib = 0
|
local cmd_line = nil
|
||||||
local saw_format = false
|
local contrib = 0
|
||||||
local saw_prim_write = false
|
local saw_format = false
|
||||||
for _, t in ipairs(tokens) do
|
local saw_prim_write = false
|
||||||
local tok = t.tok
|
for _, t in ipairs(tokens) do
|
||||||
-- Match `mac_format_<shape>_color(...)` and strip `_color`
|
local tok = t.tok
|
||||||
-- to get the bare shape suffix (f3 / g4 / etc).
|
-- Match `mac_format_<shape>_color(...)` and strip `_color`
|
||||||
local shape = tok:match("^mac_format_([%w_]+)_color%s*%(") or tok:match("^mac_format_([%w_]+)_color%s*$")
|
-- to get the bare shape suffix (f3 / g4 / etc).
|
||||||
if shape and duffle.GP0_CMD_BY_SHAPE[shape] then
|
local shape = tok:match("^mac_format_([%w_]+)_color%s*%(") or tok:match("^mac_format_([%w_]+)_color%s*$")
|
||||||
if not cmd_byte then
|
if shape and duffle.GP0_CMD_BY_SHAPE[shape] then
|
||||||
cmd_byte = duffle.GP0_CMD_BY_SHAPE[shape]
|
|
||||||
cmd_line = a.line + line_in_body[t.rel]
|
|
||||||
end
|
|
||||||
saw_format = true
|
|
||||||
local contrib_key = "mac_format_" .. shape .. "_color"
|
|
||||||
local n = duffle.GP0_MACRO_CONTRIB[contrib_key]
|
|
||||||
if n then contrib = contrib + n end
|
|
||||||
end
|
|
||||||
local gte_store = tok:match("^mac_gte_store_[%w_]+")
|
|
||||||
if gte_store then
|
|
||||||
local n = duffle.GP0_MACRO_CONTRIB[gte_store]
|
|
||||||
if n then contrib = contrib + n end
|
|
||||||
end
|
|
||||||
local ot_tag = tok:match("^mac_insert_ot_tag_([%w_]+)")
|
|
||||||
if ot_tag then
|
|
||||||
local n = duffle.GP0_MACRO_CONTRIB["mac_insert_ot_tag_" .. ot_tag]
|
|
||||||
if n then contrib = contrib + n end
|
|
||||||
end
|
|
||||||
if tok:match("^store_word%s*%(") and tok:find("R_PrimCursor", 1, true) then
|
|
||||||
saw_prim_write = true
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
if not cmd_byte then
|
if not cmd_byte then
|
||||||
if saw_prim_write and not saw_format then
|
cmd_byte = duffle.GP0_CMD_BY_SHAPE[shape]
|
||||||
findings[#findings + 1] = {
|
cmd_line = atom.line + line_in_body[t.rel]
|
||||||
atom = a.name, line = a.line,
|
|
||||||
check = "gpu_portstore_shape", kind = "warning",
|
|
||||||
msg = string.format("%s at line %d writes to R_PrimCursor via raw store_word(...)"
|
|
||||||
.. " but uses no `mac_format_*_color`; the cmd byte + word count cannot be auto-validated."
|
|
||||||
.. " Consider migrating to `mac_format_X_color` + `mac_gte_store_X_post_*` + `mac_insert_ot_tag_X`.",
|
|
||||||
a.name, a.line),
|
|
||||||
}
|
|
||||||
end
|
|
||||||
else
|
|
||||||
local expected = duffle.GP0_CMD_SIZE[cmd_byte]
|
|
||||||
if contrib ~= expected then
|
|
||||||
findings[#findings + 1] = {
|
|
||||||
atom = a.name, line = cmd_line or a.line,
|
|
||||||
check = "gpu_portstore_shape", kind = "error",
|
|
||||||
msg = string.format("%s at line %d emits GP0 0x%02X with %d prim word(s); expected %d (cmd 0x%02X total = %d)",
|
|
||||||
a.name, cmd_line or a.line, cmd_byte, contrib, expected, cmd_byte, expected),
|
|
||||||
}
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
|
saw_format = true
|
||||||
|
local contrib_key = "mac_format_" .. shape .. "_color"
|
||||||
|
local n = duffle.GP0_MACRO_CONTRIB[contrib_key]
|
||||||
|
if n then contrib = contrib + n end
|
||||||
|
end
|
||||||
|
local gte_store = tok:match("^mac_gte_store_[%w_]+")
|
||||||
|
if gte_store then
|
||||||
|
local n = duffle.GP0_MACRO_CONTRIB[gte_store]
|
||||||
|
if n then contrib = contrib + n end
|
||||||
|
end
|
||||||
|
local ot_tag = tok:match("^mac_insert_ot_tag_([%w_]+)")
|
||||||
|
if ot_tag then
|
||||||
|
local n = duffle.GP0_MACRO_CONTRIB["mac_insert_ot_tag_" .. ot_tag]
|
||||||
|
if n then contrib = contrib + n end
|
||||||
|
end
|
||||||
|
if tok:match("^store_word%s*%(") and tok:find("R_PrimCursor", 1, true) then
|
||||||
|
saw_prim_write = true
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
if not cmd_byte then
|
||||||
|
if saw_prim_write and not saw_format then
|
||||||
|
findings[#findings + 1] = {
|
||||||
|
atom = atom.name, line = atom.line,
|
||||||
|
check = "gpu_portstore_shape", kind = "warning",
|
||||||
|
msg = string.format("%s at line %d writes to R_PrimCursor via raw store_word(...)"
|
||||||
|
.. " but uses no `mac_format_*_color`; the cmd byte + word count cannot be auto-validated."
|
||||||
|
.. " Consider migrating to `mac_format_X_color` + `mac_gte_store_X_post_*` + `mac_insert_ot_tag_X`.",
|
||||||
|
atom.name, atom.line),
|
||||||
|
}
|
||||||
|
end
|
||||||
|
else
|
||||||
|
local expected = duffle.GP0_CMD_SIZE[cmd_byte]
|
||||||
|
if contrib ~= expected then
|
||||||
|
findings[#findings + 1] = {
|
||||||
|
atom = atom.name, line = cmd_line or atom.line,
|
||||||
|
check = "gpu_portstore_shape", kind = "error",
|
||||||
|
msg = string.format("%s at line %d emits GP0 0x%02X with %d prim word(s); expected %d (cmd 0x%02X total = %d)",
|
||||||
|
atom.name, cmd_line or atom.line, cmd_byte, contrib, expected, cmd_byte, expected),
|
||||||
|
}
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
@@ -758,42 +767,42 @@ local function analyze_atom_paths(atom)
|
|||||||
local branch_count = 0
|
local branch_count = 0
|
||||||
for _ in pairs(branches) do branch_count = branch_count + 1 end
|
for _ in pairs(branches) do branch_count = branch_count + 1 end
|
||||||
|
|
||||||
return {
|
-- Mutate the pre-allocated `atom.paths` slot in place (caller owns the table).
|
||||||
cycles_min = cycles_min,
|
-- Mega-struct move: a single source of truth for all per-atom path-analysis data,
|
||||||
cycles_max = cycles_max,
|
-- instead of returning a fresh table that would just get copied onto 5 atom fields.
|
||||||
branches = branch_count,
|
-- If a caller ever DIDN'T pre-allocate (legacy code path), fall back to a fresh slot.
|
||||||
paths = path_count,
|
local p = atom.paths or {}
|
||||||
has_loops = has_loops,
|
p.cycles_min = cycles_min
|
||||||
unknown_macros = unknown_list,
|
p.cycles_max = cycles_max
|
||||||
}
|
p.branches = branch_count
|
||||||
|
p.paths = path_count
|
||||||
|
p.has_loops = has_loops
|
||||||
|
p.unknown_macros = unknown_list
|
||||||
|
atom.paths = p
|
||||||
end
|
end
|
||||||
|
|
||||||
--- Per-source check that emits one finding per unknown macro seen
|
--- Per-source check that emits one finding per unknown macro seen
|
||||||
--- (deduplicated across atoms so the warning section doesn't get spammed with N copies of "macro X not in duffle.INSTRUCTION_LATENCY").
|
--- (deduplicated across atoms so the warning section doesn't get spammed with N copies of "macro X not in duffle.INSTRUCTION_LATENCY").
|
||||||
local function check_per_atom_cycle_budget(atoms, findings)
|
--- Reuses `analyze_atom_paths`'s per-atom unknown_macros discovery (it's the canonical place that walks tokens
|
||||||
local unknown_seen = {}
|
--- and computes per-token cycle costs). We just sort + emit.
|
||||||
for _, a in ipairs(atoms) do
|
--- Per-atom: emit one finding per unknown macro seen, deduplicated across atoms (so the warning
|
||||||
local tokens = tokenize_body(a.body)
|
--- section doesn't get spammed with N copies of "macro X not in duffle.INSTRUCTION_LATENCY").
|
||||||
local unknown_set = {}
|
--- Reuses `analyze_atom_paths`'s per-atom unknown_macros discovery (it's the canonical place that walks tokens
|
||||||
for _, t in ipairs(tokens) do
|
--- and computes per-token cycle costs). We just sort + emit.
|
||||||
local _, _, unknown = token_cycles(t.tok)
|
--- Signature changed in Stage 1B: `(atom, pipe_ctx, findings)` — the `unknown_seen` dedup table lives on
|
||||||
if unknown then
|
--- `pipe_ctx` so it persists across the per-atom loop in validate().
|
||||||
unknown_set[t.tok:match("^([%w_]+)") or "?"] = true
|
local function check_per_atom_cycle_budget(atom, pipe_ctx, findings)
|
||||||
end
|
local p = atom.paths or {}
|
||||||
end
|
for _, name in ipairs(p.unknown_macros or {}) do
|
||||||
local unknown_macros = {}
|
if not pipe_ctx.unknown_seen[name] then
|
||||||
for macro_name in pairs(unknown_set) do unknown_macros[#unknown_macros + 1] = macro_name end
|
pipe_ctx.unknown_seen[name] = atom.line
|
||||||
for _, name in ipairs(unknown_macros) do
|
findings[#findings + 1] = {
|
||||||
if not unknown_seen[name] then
|
atom = atom.name, line = atom.line,
|
||||||
unknown_seen[name] = a.line
|
check = "per_atom_cycle_budget", kind = "warning",
|
||||||
findings[#findings + 1] = {
|
msg = string.format("%s at line %d uses macro `%s` which is not in duffle.INSTRUCTION_LATENCY; "
|
||||||
atom = a.name, line = a.line,
|
.. "cycle count will be +%d per call (best-case). Add an entry to duffle.INSTRUCTION_LATENCY.",
|
||||||
check = "per_atom_cycle_budget", kind = "warning",
|
atom.name, atom.line, name, duffle.UNKNOWN_INSTRUCTION_CYCLES),
|
||||||
msg = string.format("%s at line %d uses macro `%s` which is not in duffle.INSTRUCTION_LATENCY; "
|
}
|
||||||
.. "cycle count will be +%d per call (best-case). Add an entry to duffle.INSTRUCTION_LATENCY.",
|
|
||||||
a.name, a.line, name, duffle.UNKNOWN_INSTRUCTION_CYCLES),
|
|
||||||
}
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
@@ -816,22 +825,42 @@ local function validate(ctx, src)
|
|||||||
binds_index[b.name] = b
|
binds_index[b.name] = b
|
||||||
end
|
end
|
||||||
|
|
||||||
local findings = {}
|
-- pipe_ctx: the cross-atom shared state for the per-atom pipeline (Fleury "expose structure").
|
||||||
check_gte_pipeline_fill(atoms, findings)
|
-- Pre-allocated here, mutated by each per-atom check call below. Replaces the per-check
|
||||||
check_mac_yield_uniformity(atoms, findings)
|
-- local tables that used to live inside each check_* function body.
|
||||||
check_abi_handoff(atoms, atom_infos, binds_index, findings)
|
-- info_by_atom — atom_name -> atom_info (built once; check_abi_handoff reads it)
|
||||||
check_gpu_portstore_shape(atoms, findings)
|
-- binds_index — Binds_X -> binds struct (built once; check_abi_handoff reads it)
|
||||||
check_per_atom_cycle_budget(atoms, findings)
|
-- unknown_seen — macro_name -> first atom line (accumulated across atoms; check_per_atom_cycle_budget dedups)
|
||||||
|
local info_by_atom = {}
|
||||||
|
for _, info in ipairs(atom_infos) do
|
||||||
|
info_by_atom[info.atom_name] = info
|
||||||
|
end
|
||||||
|
local pipe_ctx = {
|
||||||
|
info_by_atom = info_by_atom,
|
||||||
|
binds_index = binds_index,
|
||||||
|
unknown_seen = {},
|
||||||
|
}
|
||||||
|
|
||||||
-- Path-aware cycle-budget output: attach per-path cycle data to each atom.
|
-- THE per-atom pipeline. ONE iteration of atoms; the 5 check_* functions + analyze_atom_paths
|
||||||
|
-- all run here, sharing a single tokenize_body + build_body_line_index per body.
|
||||||
|
-- Plex move: every piece of state derived from an atom body lives on `atom.paths` (the per-atom mega-struct);
|
||||||
|
-- readers (analyze_atom_paths, the 5 checks, the renderers) all consume `atom.paths`, not the raw `atoms` list.
|
||||||
|
-- Stage 1B: each check_* now takes `(atom, ...)` instead of `(atoms, findings)` — no more single-atom `{a}` shim.
|
||||||
|
local findings = {}
|
||||||
for _, a in ipairs(atoms) do
|
for _, a in ipairs(atoms) do
|
||||||
local p = analyze_atom_paths(a)
|
a.paths = a.paths or {}
|
||||||
a.paths = p
|
a.paths.tokens = tokenize_body(a.body)
|
||||||
a.cycles_min = p.cycles_min
|
a.paths.line_in_body = build_body_line_index(a.body)
|
||||||
a.cycles_max = p.cycles_max
|
|
||||||
a.branch_count = p.branches
|
-- analyze_atom_paths fills the *cycles / branches / has_loops / unknown_macros* fields of a.paths.
|
||||||
a.unknown_macros = p.unknown_macros
|
analyze_atom_paths(a)
|
||||||
a.has_loops = p.has_loops
|
|
||||||
|
-- Run all 5 checks on this one atom. Each is now a per-atom predicate.
|
||||||
|
check_gte_pipeline_fill(a, findings)
|
||||||
|
check_mac_yield_uniformity(a, findings)
|
||||||
|
check_abi_handoff(a, pipe_ctx, findings)
|
||||||
|
check_gpu_portstore_shape(a, findings)
|
||||||
|
check_per_atom_cycle_budget(a, pipe_ctx, findings)
|
||||||
end
|
end
|
||||||
|
|
||||||
local errors = {}
|
local errors = {}
|
||||||
@@ -987,16 +1016,16 @@ local function emit_module_static_analysis_txt(ctx, dir, dir_sources, atoms, fin
|
|||||||
-- Sort atoms by max cycles descending for quick scanning.
|
-- Sort atoms by max cycles descending for quick scanning.
|
||||||
local sorted = {}
|
local sorted = {}
|
||||||
for _, a in ipairs(atoms) do sorted[#sorted + 1] = a end
|
for _, a in ipairs(atoms) do sorted[#sorted + 1] = a end
|
||||||
table.sort(sorted, function(x, y) return (x.cycles_max or 0) > (y.cycles_max or 0) end)
|
table.sort(sorted, function(x, y) return ((x.paths or {}).cycles_max or 0) > ((y.paths or {}).cycles_max or 0) end)
|
||||||
for _, a in ipairs(sorted) do
|
for _, a in ipairs(sorted) do
|
||||||
local p = a.paths or {}
|
local p = a.paths or {}
|
||||||
local br_count = p.branches or 0
|
local br_count = p.branches or 0
|
||||||
local path_count = p.paths or 0
|
local path_count = p.paths or 0
|
||||||
local loops_tag = p.has_loops and " [loop!]" or ""
|
local loops_tag = p.has_loops and " [loop!]" or ""
|
||||||
local unknown_tag = ""
|
local unknown_tag = ""
|
||||||
if a.unknown_macros and #a.unknown_macros > 0 then
|
if p.unknown_macros and #p.unknown_macros > 0 then
|
||||||
unknown_tag = string.format(" [unknown: %s]",
|
unknown_tag = string.format(" [unknown: %s]",
|
||||||
table.concat(a.unknown_macros, ", "))
|
table.concat(p.unknown_macros, ", "))
|
||||||
end
|
end
|
||||||
local name_label = a.name
|
local name_label = a.name
|
||||||
if multi_source and a.source_path then
|
if multi_source and a.source_path then
|
||||||
|
|||||||
Reference in New Issue
Block a user