final pass on metaprogram

This commit is contained in:
2026-07-11 19:53:12 -04:00
parent 277af1c901
commit 9f7a4a00ce
9 changed files with 741 additions and 611 deletions
+101 -80
View File
@@ -14,7 +14,7 @@
--- `["static-analysis"] = { module = "passes.static_analysis", kind = "validation", deps = {"word-counts", "components"},
--- out = { { kind = "report", path_template = "<out_root>/<basename>.static_analysis.txt" } } }`
---
--- **Conventions**: tabs (1/level), EmmyLua annotations, no regex, Lua 5.3 compatible. See `lua.md` in the ps1-ai styleguides.
--- **Conventions**: tabs (1/level), EmmyLua annotations, no regex, Lua 5.3 compatible.
-- ════════════════════════════════════════════════════════════════════════════
-- Module-scope requires + package.path setup
@@ -114,25 +114,6 @@ local OUTPUT_EXTENSION = ".static_analysis.txt"
--- @field findings Finding[] -- findings for this atom
--- @field total_cycles integer -- sum of token cycle costs
-- ════════════════════════════════════════════════════════════════════════════
-- Source scanning — delegated to duffle.scan_source (ps1_meta.lua pre-scans)
-- ════════════════════════════════════════════════════════════════════════════
--
-- The orchestrator calls duffle.scan_source once per source and stashes the fat SourceScan in src.scan.
-- validate() below reads from src.scan — no source walking in this pass.
-- ════════════════════════════════════════════════════════════════════════════
-- Body tokenizer (top-level comma splitter + per-token classification)
-- ════════════════════════════════════════════════════════════════════════════
--- Build a map: `body_relative_char_offset` -> `body_relative_line`.
--- 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.
---
-- NOTE: `tokenize_body` and `build_body_line_index` moved to `duffle.lua` as shared
-- memoized utilities (`duffle.tokenize_body`, `duffle.build_body_line_index`).
-- The local copies were deleted; all callers now use the duffle versions.
-- ════════════════════════════════════════════════════════════════════════════
-- classify_tokens — per-token classification (the plex's pre-computed data layer)
-- ════════════════════════════════════════════════════════════════════════════
@@ -156,16 +137,30 @@ local OUTPUT_EXTENSION = ".static_analysis.txt"
-- Checks that need "how many nops before token i" use `tok_class.nop_prefix` instead of walking backwards.
--- @class TokClass
--- @field ident string -- leading identifier
--- @field nop_words integer -- 0/1/2
--- @field nop_prefix integer -- consecutive nop words before this token
--- @field is_yield boolean
--- @field is_atom_label boolean
--- @field label_name string|nil -- for atom_label(name)
--- @field is_branch boolean
--- @field branch_label string|false|nil -- for branch_*(..., atom_offset(F, label))
--- @field is_load_word boolean
--- @field is_store_word boolean
--- @field ident string -- leading identifier
--- @field nop_words integer -- 0/1/2
--- @field nop_prefix integer -- consecutive nop words before this token
--- @field is_yield boolean
--- @field is_atom_label boolean
--- @field label_name string|nil -- for atom_label(name)
--- @field is_branch boolean
--- @field branch_label string|false|nil -- for branch_*(..., atom_offset(F, label))
--- @field is_load_word boolean
--- @field is_store_word boolean
--- @field mac_format_shape string|nil -- "f3" / "g4" etc. for mac_format_X_color; nil otherwise
--- @field is_gte_store boolean -- ident matches `mac_gte_store_<shape>`
--- @field is_ot_tag boolean -- ident matches `mac_insert_ot_tag_<shape>`
--- @field writes_r_prim_cursor boolean -- store_word targeting R_PrimCursor
--- @field reads_r_tape_ptr boolean -- any token referencing R_TapePtr
--- @field o_arg1 string|nil -- first arg of O_(<a>, <b>) captures; nil for non-O_ tokens
--- @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
-- Patterns for O_(<arg1>, <arg2>) and S_(<arg>) captures. UNANCHORED — the substring can appear
-- anywhere in the token (e.g., `load_word(R_T0, R_TapePtr, O_(Binds_X, field))` matches at position ~24).
-- The binds_name match is deferred to check_abi_handoff (which compares tc.o_arg1 == atom.info.binds).
local O_PATTERN = "O_%(([%w_]+),%s*([%w_]+)%s*%)"
local S_PATTERN = "S_%(([%w_]+)%s*%)"
local function classify_tokens(tokens)
local n = #tokens
@@ -186,6 +181,16 @@ local function classify_tokens(tokens)
local is_load_word = ident == "load_word"
local is_store_word = ident == "store_word"
-- Per-check pre-computes (R3 lift). Each pre-compute eliminates one per-token regex/string-find
-- call from check_abi_handoff / check_gpu_portstore_shape.
local mac_format_shape = nil
local is_gte_store = false
local is_ot_tag = false
local writes_r_prim_cursor = false
local reads_r_tape_ptr = false
local o_arg1, o_arg2 = nil, nil
local s_arg1 = nil
if ident == "atom_label" then
is_atom_label = true
label_name = tok:match("^atom_label%s*%(%s*([%w_]+)%s*%)")
@@ -194,17 +199,40 @@ local function classify_tokens(tokens)
branch_label = tok:match("atom_offset%s*%([^,]+,%s*([%w_]+)%s*%)") or false
end
-- mac_format_X_color / mac_gte_store_<shape> / mac_insert_ot_tag_<shape> (used by check_gpu_portstore_shape).
local shape = ident:match("^mac_format_([%w_]+)_color$")
if shape then mac_format_shape = shape end
if ident:match("^mac_gte_store_[%w_]+$") then is_gte_store = true end
if ident:match("^mac_insert_ot_tag_[%w_]+$") then is_ot_tag = true end
-- O_(<arg1>, <arg2>) / S_(<arg>) captures (used by check_abi_handoff).
-- Cheap pattern match — anchored, fails fast on non-matching tokens.
o_arg1, o_arg2 = tok:match(O_PATTERN)
if not o_arg1 then s_arg1 = tok:match(S_PATTERN) end
-- R_TapePtr + R_PrimCursor references (used by check_abi_handoff / check_gpu_portstore_shape).
if tok:find("R_TapePtr", 1, true) then reads_r_tape_ptr = true end
if is_store_word and tok:find("R_PrimCursor", 1, true) then writes_r_prim_cursor = true end
tc[tok_idx] = {
ident = ident,
nop_words = nop_words,
nop_prefix = nop_run,
is_yield = is_yield,
is_atom_label = is_atom_label,
label_name = label_name,
is_branch = is_branch,
branch_label = branch_label,
is_load_word = is_load_word,
is_store_word = is_store_word,
ident = ident,
nop_words = nop_words,
nop_prefix = nop_run,
is_yield = is_yield,
is_atom_label = is_atom_label,
label_name = label_name,
is_branch = is_branch,
branch_label = branch_label,
is_load_word = is_load_word,
is_store_word = is_store_word,
mac_format_shape = mac_format_shape,
is_gte_store = is_gte_store,
is_ot_tag = is_ot_tag,
writes_r_prim_cursor = writes_r_prim_cursor,
reads_r_tape_ptr = reads_r_tape_ptr,
o_arg1 = o_arg1,
o_arg2 = o_arg2,
s_arg1 = s_arg1,
}
-- Advance the nop run for the NEXT token.
if nop_words > 0 then
@@ -406,29 +434,28 @@ local function check_abi_handoff(atom, pipe_ctx, findings)
local tc = atom.paths.tok_class
local found_field_set = {}
local found_advance = false
local bind_re = "O_%(" .. binds_name .. ",%s*([%w_]+)%s*%)"
for tok_idx, t in ipairs(tokens) do
local tok = t.tok
if tc[tok_idx].is_load_word 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 = atom.line + line_in_body[t.rel]
findings[#findings + 1] = {
atom = atom.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>)",
atom.name, body_line, binds_name, binds_name),
}
end
-- Reads from tc_entry fields pre-computed by classify_tokens (R3 lift). Eliminates 3 per-token
-- string-find/match calls (R_TapePtr + O_(binds_name,...) + bind_re) → 3 O(1) field reads.
for tok_idx = 1, #tokens do
local tc_entry = tc[tok_idx]
-- scan: load_word(R_*, R_TapePtr, O_(<Binds_X>, <field>))
if tc_entry.is_load_word and tc_entry.reads_r_tape_ptr and tc_entry.o_arg1 == binds_name then
local field = tc_entry.o_arg2
if field then
found_field_set[field] = true
else
local body_line = atom.line + line_in_body[tokens[tok_idx].rel]
findings[#findings + 1] = {
atom = atom.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>)",
atom.name, body_line, binds_name, binds_name),
}
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>))
-- scan: add_ui_self(R_TapePtr, S_(<Binds_X>))
if tc_entry.reads_r_tape_ptr and tc_entry.s_arg1 == binds_name then
found_advance = true
end
end
@@ -468,7 +495,6 @@ 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.
---
--- Applies only to `kind = "atom"` (baked atoms). Components don't emit full primitives.
--- Stage 2: signature uniformized to `(atom, pipe_ctx, findings)` — pipe_ctx is ignored here.
local function check_gpu_portstore_shape(atom, pipe_ctx, findings)
if atom.kind ~= "atom" then return end
local tokens = atom.paths.tokens
@@ -479,31 +505,30 @@ local function check_gpu_portstore_shape(atom, pipe_ctx, findings)
local contrib = 0
local saw_format = false
local saw_prim_write = false
for tok_idx, t in ipairs(tokens) do
local tok = t.tok
local ident = tc[tok_idx].ident
-- Match `mac_format_<shape>_color(...)` and strip `_color`
-- to get the bare shape suffix (f3 / g4 / etc).
local shape = ident:match("^mac_format_([%w_]+)_color$")
-- Reads from tc_entry fields pre-computed by classify_tokens (R3 lift). Eliminates 4 per-token
-- string matches (mac_format_X_color + mac_gte_store_<shape> + mac_insert_ot_tag_<shape> + R_PrimCursor)
for tok_idx = 1, #tokens do
local tc_entry = tc[tok_idx]
local shape = tc_entry.mac_format_shape
if shape and duffle.GP0_CMD_BY_SHAPE[shape] then
if not cmd_byte then
cmd_byte = duffle.GP0_CMD_BY_SHAPE[shape]
cmd_line = atom.line + line_in_body[t.rel]
cmd_line = atom.line + line_in_body[tokens[tok_idx].rel]
end
saw_format = true
local contrib_key = "mac_format_" .. shape .. "_color"
local n = duffle.GP0_MACRO_CONTRIB[contrib_key]
local n = duffle.GP0_MACRO_CONTRIB["mac_format_" .. shape .. "_color"]
if n then contrib = contrib + n end
end
if ident:match("^mac_gte_store_[%w_]+$") then
local n = duffle.GP0_MACRO_CONTRIB[ident]
if tc_entry.is_gte_store then
local n = duffle.GP0_MACRO_CONTRIB[tc_entry.ident]
if n then contrib = contrib + n end
end
if ident:match("^mac_insert_ot_tag_[%w_]+$") then
local n = duffle.GP0_MACRO_CONTRIB[ident]
if tc_entry.is_ot_tag then
local n = duffle.GP0_MACRO_CONTRIB[tc_entry.ident]
if n then contrib = contrib + n end
end
if tc[tok_idx].is_store_word and tok:find("R_PrimCursor", 1, true) then
if tc_entry.writes_r_prim_cursor then
saw_prim_write = true
end
end
@@ -536,10 +561,6 @@ end
-- Check #5: per-atom cycle budget (uses analyze_atom_paths's unknown_macros)
-- ════════════════════════════════════════════════════════════════════════════
-- NOTE: `token_cycles`, `find_atom_labels`, `find_branch_targets` were removed
-- when `classify_tokens` (the pre-computed per-token classification) replaced them.
-- The classification lives on `atom.paths.tok_class`; analyze_atom_paths reads it.
--- Walk all paths through an atom body and return per-path cycle sums.
--- Builds a tiny CFG: each token has a "next" pointer; branches have two (fall-through + taken).
--- The BD-slot nop after a branch is absorbed into the branch's cost (MIPS-accurate: BD slot always runs),