mirror of
https://github.com/Ed94/pikuma_ps1.git
synced 2026-07-12 20:31:25 -07:00
Corrections, flatting nested branches (lua metaprogram)
This commit is contained in:
+110
-73
@@ -399,6 +399,19 @@ function M.scan_to_char(s, target, start)
|
||||
return nil
|
||||
end
|
||||
|
||||
-- If `s[pos]` is `#`, skip to the end of the preprocessor directive line (past the newline).
|
||||
-- Returns the position past the newline, or nil if `s[pos]` is not `#`.
|
||||
-- scan: #<directive>\n -> past the newline
|
||||
function M.skip_preprocessor_line(s, pos)
|
||||
if s:byte(pos) ~= 35 then return nil end -- '#'
|
||||
local scan = pos
|
||||
local len = #s
|
||||
while scan <= len and s:byte(scan) ~= BYTE_NEWLINE do
|
||||
scan = scan + 1
|
||||
end
|
||||
return scan + 1
|
||||
end
|
||||
|
||||
-- Split a brace-body into top-level comma-separated tokens. Honors nested
|
||||
-- parens/braces/brackets and skips strings/comments.
|
||||
--
|
||||
@@ -568,53 +581,59 @@ M.TAPE_ATOM_MACROS = {
|
||||
["atom_info"] = { kind = "info", binds = false },
|
||||
}
|
||||
|
||||
-- GTE pipeline-fill latency table (static-analysis Phase 1).
|
||||
-- GTE pipeline-fill latency table.
|
||||
--
|
||||
-- For each `gte_cmdw_*` macro in code/duffle/gte.h, the minimum number of consecutive COP2 "nop" words that MUST appear
|
||||
-- before any other COP2 read or non-nop instruction (so the GTE pipeline latency is fully retired).
|
||||
-- Latencies are sourced from the doxygen comments in gte.h
|
||||
-- (e.g. `* @brief Rotate, Translate and Perspective Triple (23 cycles)` with body `Two nop words fill the COP2 pipeline latency`).
|
||||
-- For each `gte_cmdw_*` macro in code/duffle/gte.h, the minimum number of consecutive COP2 "nop" words that MUST appear
|
||||
-- before the command issues so that any preceding `lwc2`/`swc2`/C2 state writes have retired before the GTE starts
|
||||
-- reading its input registers.
|
||||
--
|
||||
-- The check (`scripts/passes/static_analysis.lua :: check_gte_pipeline_fill`) walks each atom body,
|
||||
-- counts the consecutive nop words after every `gte_cmdw_*` invocation, and reports a finding if the count is below this minimum.
|
||||
-- Aliases are dereferenced before lookup (gté_cmdw_rtps_alias -> gte_cmdw_rtps -> 2).
|
||||
--
|
||||
-- Values verified against PSX-SPX gte.txt (rtpt 23cy / 8cy per divide => 2 nops; nclip 8cy => 2 nops; avsz3/avsz4 14cy => 2 nops;
|
||||
-- op single-cycle atomic => 0 nops; mvmva 8cy matrix-vector => 2 nops).
|
||||
-- The check (`scripts/passes/static_analysis.lua :: check_gte_pipeline_fill`) walks each atom body,
|
||||
-- counts the consecutive nop words before every `gte_cmdw_*` invocation, and reports a finding if the count is below this minimum.
|
||||
--
|
||||
-- PRE-FILL vs POST-FILL: this table models PRE-cmdw nops (retiring preceding C2 writes), NOT the post-cmdw input-latch
|
||||
-- window. The PSX-SPX pipeline timings doc (`docs/psx-spx/docs/gtepipelinetimings.md`) measures a DIFFERENT number:
|
||||
-- the smallest N nops between `cop2` and `mtc2` to a specific input register at which the write no longer affects
|
||||
-- the output. For nearly all instructions, inputs latch in the first 0-4 cycles — the GTE snapshots its input
|
||||
-- register file early and works from internal pipeline storage afterward. The documented total cycle count is
|
||||
-- NOT the "do not touch inputs" window; the actual read window is much shorter.
|
||||
--
|
||||
-- The `gte_rtpt()` / `gte_nclip()` wrapper macros in gte.h emit the pre-cmd nops internally (asm_words(nop, nop, ...)),
|
||||
-- but THOSE WRAPPERS ARE NOT USED INSIDE ATOM BODIES in this codebase.
|
||||
-- Every MipsAtom_(name) body uses raw `nop2, gte_cmdw_<X>, ...` form instead — that `nop2,` is the pre-fill this check validates.
|
||||
-- So values here reflect the source-level convention, NOT the wrapper-internal pre-fill.
|
||||
--
|
||||
-- Cycle counts from PSX-SPX `docs/psx-spx/docs/geometrytransformationenginegte.md`:
|
||||
-- cmd PSX-SPX cycles min pre-nops rationale
|
||||
-- rtps 15 2 8c per perspective divide + 6c for IR1..4 + mac write
|
||||
-- rtpt 23 2 3x rtps worth of pipeline depth (per-vertex pipeline fill)
|
||||
-- nclip 8 2 MAC0 write + 5c for sign computation
|
||||
-- avsz3 5 2 5c to compute average + write OTZ (all inputs latch at N=0)
|
||||
-- avsz4 6 2 avsz3 + 1c extra for 4th vertex
|
||||
-- mvmva 8 2 IR1..4 write + matrix work (8c regardless of mx/v/cv selection)
|
||||
-- op 6 0 cross product; output to IR1..3 only (atomic 6c calc, no pre-fill needed)
|
||||
--
|
||||
-- The pre-nop values (2 for most commands) are conservative: PSX-SPX pipeline timings show most inputs latch at N=0-1
|
||||
-- relative to a preceding mtc2, but 2 nops is the gte.h convention for retiring preceding lwc2/swc2 + C2 state.
|
||||
-- OP is set to 0 because it's a short atomic op with no input that needs a long retire window.
|
||||
--
|
||||
-- Aliases are listed separately because source code may use either the alias or the canonical name.
|
||||
M.GTE_PIPELINE_LATENCY = {
|
||||
-- Minimum number of consecutive `nop` words that must appear IMMEDIATELY BEFORE a `gte_cmdw_<X>` invocation
|
||||
-- to retire any preceding `lwc2` / `swc2` / pre-existing C2 state writes before the GTE pipeline starts reading
|
||||
-- Minimum number of consecutive `nop` words that must appear IMMEDIATELY BEFORE a `gte_cmdw_<X>` invocation
|
||||
-- to retire any preceding `lwc2` / `swc2` / pre-existing C2 state writes before the GTE pipeline starts reading
|
||||
-- from V0/V1/V2 or MAC0..3 / OTZ / IR0..3 at the command's issue cycle.
|
||||
--
|
||||
-- Values are from the doxygen comments in code/duffle/gte.h and cross-checked against PSX-SPX `geometrytransformationenginegte.md`:
|
||||
-- cmd cycles min pre-nops rationale
|
||||
-- rtps 14 2 8c per perspective divide + 6c for IR1..4 + mac write
|
||||
-- rptt 22 2 3x rtps worth of pipeline depth
|
||||
-- nclip 7 2 MAC0 write + 5c for sign
|
||||
-- avsz3 14 2 14c to compute average + write OTZ
|
||||
-- avsz4 16 2 avsz3 + 2c extra for avg over 4
|
||||
-- mvmva 8 2 IR1..4 write + matrix work
|
||||
-- op 5 0 output to MAC0 only (atomic 5c calc)
|
||||
--
|
||||
-- The `gte_rtpt()` / `gte_nclip()` / `gte_avsz3()` wrapper macros in gte.h emit the pre-cmd nops internally (asm_words(nop, nop, ...)),
|
||||
-- but THOSE WRAPPERS ARE NOT USED INSIDE ATOM BODIES in this codebase.
|
||||
-- Every MipsAtom_(name) body uses raw `nop2, gte_cmdw_<X>, ...` form instead -- that `nop2,` is the pre-fill this check validates.
|
||||
-- So values here must reflect the source-level convention, NOT the wrapper-internal pre-fill (which is invisible at the source level).
|
||||
--
|
||||
-- Existing clean-atom bodies (cube_g4_face, floor_f3_face, diag_gte) all emit `nop2,` before every `gte_cmdw_<X>` (which matches values >= 2).
|
||||
-- The check passes them all.
|
||||
--
|
||||
-- Aliases are listed separately because source code may use either the alias or the canonical name.
|
||||
-- The check looks up the EXACT macro text, so both forms must be in the table.
|
||||
-- Values are from the doxygen comments in code/duffle/gte.h and cross-checked against
|
||||
-- PSX-SPX `docs/psx-spx/docs/geometrytransformationenginegte.md` (cycle counts) and
|
||||
-- `docs/psx-spx/docs/gtepipelinetimings.md` (input-latch boundaries).
|
||||
|
||||
-- Canonical macros (from code/duffle/gte.h)
|
||||
["gte_cmdw_rtps"] = 2,
|
||||
["gte_cmdw_rtpt"] = 2,
|
||||
["gte_cmdw_nclip"] = 2,
|
||||
["gte_cmdw_op"] = 0,
|
||||
["gte_cmdw_mvmva"] = 2,
|
||||
["gte_cmdw_avsz3"] = 2,
|
||||
["gte_cmdw_avsz4"] = 2,
|
||||
["gte_cmdw_rtps"] = 2, -- RTPS: 15 cycles (PSX-SPX)
|
||||
["gte_cmdw_rtpt"] = 2, -- RTPT: 23 cycles (PSX-SPX)
|
||||
["gte_cmdw_nclip"] = 2, -- NCLIP: 8 cycles (PSX-SPX)
|
||||
["gte_cmdw_op"] = 0, -- OP: 6 cycles, atomic (PSX-SPX)
|
||||
["gte_cmdw_mvmva"] = 2, -- MVMVA: 8 cycles (PSX-SPX)
|
||||
["gte_cmdw_avsz3"] = 2, -- AVSZ3: 5 cycles (PSX-SPX)
|
||||
["gte_cmdw_avsz4"] = 2, -- AVSZ4: 6 cycles (PSX-SPX)
|
||||
|
||||
-- Aliases (must have the same value as their canonical target)
|
||||
["gte_cmdw_rotate_translate_perspective_single"] = 2,
|
||||
@@ -630,7 +649,18 @@ M.GTE_PIPELINE_LATENCY = {
|
||||
}
|
||||
|
||||
-- GP0 packet sizes (total words including the 1-word tag) per GP0 cmd byte.
|
||||
-- Verified against code/duffle/gp.h struct sizes + the set_poly_* macros
|
||||
-- Per PSX-SPX `docs/psx-spx/docs/graphicsprocessingunitgpu.md` §"GPU Render Polygon Commands":
|
||||
-- Each polygon command's word count = 1 (tag/cmd) + per-vertex (vertex + optional color + optional UV).
|
||||
-- F3: cmd + 3 vertices = 4 words; +1 tag = 5
|
||||
-- F4: cmd + 4 vertices = 5 words; +1 tag = 6
|
||||
-- G3: cmd + 3×(color + vertex) = 6 words; +1 tag = 7
|
||||
-- G4: cmd + 4×(color + vertex) = 8 words; +1 tag = 9
|
||||
-- FT3: cmd + tpage + clut + 3×(vertex + UV) = 7 words; +1 tag = 8
|
||||
-- FT4: cmd + tpage + clut + 4×(vertex + UV) = 9 words; +1 tag = 10
|
||||
-- GT3: cmd + tpage + clut + 3×(color + vertex + UV) = 9 words; +1 tag = 10
|
||||
-- GT4: cmd + tpage + clut + 4×(color + vertex + UV) = 12 words; +1 tag = 13
|
||||
--
|
||||
-- Cross-checked against code/duffle/gp.h struct sizes + the set_poly_* macros
|
||||
-- (which encode "len" = "words after tag"):
|
||||
-- set_poly_f3(p) -> set_len(p, 4) -> 5 total GP0 0x20
|
||||
-- set_poly_ft3(p) -> set_len(p, 7) -> 8 total GP0 0x24
|
||||
@@ -677,26 +707,33 @@ M.GP0_MACRO_CONTRIB = {
|
||||
|
||||
-- Per-macro cycle cost (best-case, no stalls). Used by the static-analysis pass to emit per-atom cycle budgets.
|
||||
-- The counts cover the EXPANDED instruction sequence the macro emits (NOT just the token it appears as in source).
|
||||
-- For example:
|
||||
-- For example:
|
||||
-- mac_pack_color_word(off, cmd, r, g, b) emits:
|
||||
-- load_upper_i(R_AT, (cmd << 8) | b) -- 1 cycle
|
||||
-- or_i_self(R_AT, (g << 8) | r) -- 1 cycle
|
||||
-- store_word(R_AT, R_PrimCursor, off) -- 1 cycle
|
||||
-- = 3 cycles total
|
||||
--
|
||||
-- mac_yield emits a control-transfer sequence (load_word, add_ui_self, jump_reg, nop)
|
||||
-- which "yields control" the atom body's cycle budget doesn't include the yield's cost (we model it as 0;
|
||||
-- mac_yield emits a control-transfer sequence (load_word, add_ui_self, jump_reg, nop)
|
||||
-- which "yields control" the atom body's cycle budget doesn't include the yield's cost (we model it as 0;
|
||||
-- runtime cost becomes part of the NEXT atom's prologue).
|
||||
--
|
||||
-- GTE command values are the GTE instruction's intrinsic cycles (the latency AFTER any pre-cmd `nop2` has retired).
|
||||
-- When the source emits `nop2, gte_cmdw_X` the nops' cycles are added separately (1+1) plus the gte_cmdw_X value here:
|
||||
-- rtpt = 21 + 2 nops = 23 total cycles (matches PSX-SPX)
|
||||
-- rtps = 12 + 2 nops = 14 total
|
||||
-- nclip = 6 + 2 nops = 8 total
|
||||
-- avsz3 = 12 + 2 nops = 14 total
|
||||
-- avsz4 = 14 + 2 nops = 16 total
|
||||
-- mvmva = 6 + 2 nops = 8 total
|
||||
-- op = 5 (no pre-cmd nops required; single-cycle atomic)
|
||||
-- rtpt = 23 + 2 nops = 25 total cycles (PSX-SPX says 23 cycles for the cmd itself; the nops are pre-fill)
|
||||
-- rtps = 15 + 2 nops = 17 total
|
||||
-- nclip = 8 + 2 nops = 10 total
|
||||
-- avsz3 = 5 + 2 nops = 7 total
|
||||
-- avsz4 = 6 + 2 nops = 8 total
|
||||
-- mvmva = 8 + 2 nops = 10 total
|
||||
-- op = 6 (no pre-cmd nops required; atomic)
|
||||
--
|
||||
-- Note: the "total" above is the pre-fill nops + the GTE intrinsic cycles. PSX-SPX documents the GTE
|
||||
-- intrinsic cycles as the total execution time of the command itself (rtpt=23, rtps=15, nclip=8, etc.).
|
||||
-- The pre-fill nops are a codebase convention for retiring preceding C2 writes, not part of the GTE's
|
||||
-- own execution time. See `docs/psx-spx/docs/geometrytransformationenginegte.md` for the canonical
|
||||
-- per-command cycle counts and `docs/psx-spx/docs/gtepipelinetimings.md` for the hardware-verified
|
||||
-- input-latch boundaries (which show most inputs are safe to clobber after just 0-4 cycles).
|
||||
M.INSTRUCTION_LATENCY = {
|
||||
-- CPU ALU (single-cycle R3000A ops)
|
||||
["nop"] = 1,
|
||||
@@ -759,30 +796,30 @@ M.INSTRUCTION_LATENCY = {
|
||||
["gte_mv_from_ctrl_r"] = 1,
|
||||
["gte_lw"] = 1, ["gte_lwc2"] = 1,
|
||||
["gte_sw"] = 1, ["gte_swc2"] = 1,
|
||||
-- COP2 commands (intrinsic cycles, EXCLUDING the 2 pre-cmd nops that
|
||||
-- the source typically emits as `nop2, gte_cmdw_X`; those nops are
|
||||
-- counted separately via the `nop2` entry above)
|
||||
["gte_cmdw_rtpt"] = 21,
|
||||
["gte_cmdw_rtps"] = 12,
|
||||
["gte_cmdw_nclip"] = 6,
|
||||
["gte_cmdw_avsz3"] = 12,
|
||||
["gte_cmdw_avsz4"] = 14,
|
||||
["gte_cmdw_mvmva"] = 6,
|
||||
["gte_cmdw_op"] = 5,
|
||||
["gte_cmdw_outer_product"] = 5,
|
||||
["gte_cmdw_wedge"] = 5,
|
||||
-- COP2 commands (intrinsic cycles per PSX-SPX, EXCLUDING the 2 pre-cmd nops that
|
||||
-- the source typically emits as `nop2, gte_cmdw_X`; those nops are counted
|
||||
-- separately via the `nop2` entry above)
|
||||
["gte_cmdw_rtpt"] = 23, -- RTPT: 23 cycles (PSX-SPX)
|
||||
["gte_cmdw_rtps"] = 15, -- RTPS: 15 cycles (PSX-SPX)
|
||||
["gte_cmdw_nclip"] = 8, -- NCLIP: 8 cycles (PSX-SPX)
|
||||
["gte_cmdw_avsz3"] = 5, -- AVSZ3: 5 cycles (PSX-SPX)
|
||||
["gte_cmdw_avsz4"] = 6, -- AVSZ4: 6 cycles (PSX-SPX)
|
||||
["gte_cmdw_mvmva"] = 8, -- MVMVA: 8 cycles (PSX-SPX)
|
||||
["gte_cmdw_op"] = 6, -- OP: 6 cycles (PSX-SPX)
|
||||
["gte_cmdw_outer_product"] = 6, -- alias for OP
|
||||
["gte_cmdw_wedge"] = 6, -- alias for OP
|
||||
-- Long-form aliases (same cost as canonical)
|
||||
["gte_cmdw_rotate_translate_perspective_single"] = 12, -- alias for rtps
|
||||
["gte_cmdw_rotate_translate_perspective_triple"] = 21, -- alias for rtpt
|
||||
["gte_cmdw_avg_sort_z4"] = 14, -- alias for avsz4
|
||||
["gte_cmdw_rotate_translate_perspective_single"] = 15, -- alias for rtps
|
||||
["gte_cmdw_rotate_translate_perspective_triple"] = 23, -- alias for rtpt
|
||||
["gte_cmdw_avg_sort_z4"] = 6, -- alias for avsz4
|
||||
-- Non-cmdw aliases from gte.h (these are `#define gte_X gte_cmdw_Y`):
|
||||
["gte_avg_sort_z3"] = 12, -- alias for avsz3
|
||||
["gte_avg_sort_z4"] = 14, -- alias for avsz4
|
||||
["gte_rtps"] = 12, -- alias for rtps
|
||||
["gte_rtpt"] = 21, -- alias for rtpt
|
||||
["gte_nclip"] = 6, -- alias for nclip
|
||||
["gte_avsz3"] = 12,
|
||||
["gte_avsz4"] = 14,
|
||||
["gte_avg_sort_z3"] = 5, -- alias for avsz3
|
||||
["gte_avg_sort_z4"] = 6, -- alias for avsz4
|
||||
["gte_rtps"] = 15, -- alias for rtps
|
||||
["gte_rtpt"] = 23, -- alias for rtpt
|
||||
["gte_nclip"] = 8, -- alias for nclip
|
||||
["gte_avsz3"] = 5,
|
||||
["gte_avsz4"] = 6,
|
||||
-- Legacy single-cycle store helpers (gte_stotz, gte_stsxy3 are 1 cycle)
|
||||
["gte_stotz"] = 1,
|
||||
["gte_stsxy3"] = 1,
|
||||
|
||||
@@ -291,15 +291,6 @@ end
|
||||
--- @param source string
|
||||
--- @param pos integer
|
||||
--- @return integer
|
||||
local function skip_preprocessor_line(source, pos)
|
||||
local str_len = #source
|
||||
local scan = pos
|
||||
while scan <= str_len and source:byte(scan) ~= BYTE_NEWLINE do
|
||||
scan = scan + 1
|
||||
end
|
||||
return scan + 1
|
||||
end
|
||||
|
||||
--- Parse `_Pragma("mac_X tape_atom words=N")` (operator form).
|
||||
--- @param source string
|
||||
--- @param ident_pos integer -- position of the `_Pragma` ident
|
||||
@@ -382,14 +373,17 @@ local function find_macro_word_annotations(source)
|
||||
if pos > str_len then break end
|
||||
|
||||
-- Skip preprocessor directives (lines starting with #).
|
||||
-- (_Pragma is an operator, not a directive — it doesn't start with #.)
|
||||
if source:byte(pos) == 35 then -- '#'
|
||||
pos = skip_preprocessor_line(source, pos)
|
||||
else
|
||||
pos = duffle.skip_preprocessor_line(source, pos)
|
||||
goto continue
|
||||
end
|
||||
|
||||
local ident, after_ident = read_ident(source, pos)
|
||||
-- scan: <ident>
|
||||
if not ident then
|
||||
pos = pos + 1
|
||||
elseif ident == PRAGMA_OPERATOR then
|
||||
if not ident then pos = pos + 1; goto continue end
|
||||
|
||||
if ident == PRAGMA_OPERATOR then
|
||||
-- scan: _Pragma(...)
|
||||
local entry, new_pos = parse_pragma_operator(source, pos, after_ident)
|
||||
if entry then
|
||||
@@ -406,7 +400,8 @@ local function find_macro_word_annotations(source)
|
||||
else
|
||||
pos = after_ident
|
||||
end
|
||||
end
|
||||
|
||||
::continue::
|
||||
end
|
||||
return out
|
||||
end
|
||||
@@ -506,21 +501,21 @@ local function find_binds_structs(source)
|
||||
if pos > str_len then break end
|
||||
|
||||
if source:byte(pos) == 35 then -- '#'
|
||||
pos = skip_preprocessor_line(source, pos)
|
||||
else
|
||||
pos = duffle.skip_preprocessor_line(source, pos)
|
||||
goto continue
|
||||
end
|
||||
|
||||
local ident, after_ident = read_ident(source, pos)
|
||||
-- scan: <ident>
|
||||
if not ident then
|
||||
pos = pos + 1
|
||||
elseif ident == "typedef" then
|
||||
-- scan: typedef Struct_(<name>) { <fields> }
|
||||
local binds_struct, new_pos = parse_typedef_binds(source, pos, after_ident, line_of)
|
||||
if binds_struct then out[#out + 1] = binds_struct end
|
||||
pos = new_pos
|
||||
else
|
||||
pos = after_ident
|
||||
end
|
||||
end
|
||||
if not ident then pos = pos + 1; goto continue end
|
||||
if ident ~= "typedef" then pos = after_ident; goto continue end
|
||||
|
||||
-- scan: typedef Struct_(<name>) { <fields> }
|
||||
local binds_struct, new_pos = parse_typedef_binds(source, pos, after_ident, line_of)
|
||||
if binds_struct then out[#out + 1] = binds_struct end
|
||||
pos = new_pos
|
||||
|
||||
::continue::
|
||||
end
|
||||
return out
|
||||
end
|
||||
@@ -557,32 +552,29 @@ local function find_atom_names(source)
|
||||
|
||||
local ident, after_ident = read_ident(source, pos)
|
||||
-- scan: <ident>
|
||||
if not ident then
|
||||
pos = pos + 1
|
||||
elseif ident ~= ATOM_DECL then
|
||||
pos = after_ident
|
||||
else
|
||||
local open_paren = skip_ws_and_cmt(source, after_ident)
|
||||
if source:byte(open_paren) ~= BYTE_OPEN_PAREN then
|
||||
pos = open_paren + 1
|
||||
else
|
||||
local inner, after_paren = read_parens(source, open_paren)
|
||||
-- scan: MipsAtom_(<name>)
|
||||
local name, _ = read_alnum_ident(inner, 1)
|
||||
if name and name ~= "" then
|
||||
out[#out + 1] = { line = line_of(pos), name = name }
|
||||
end
|
||||
local brace = scan_to_char(source, "{", after_paren)
|
||||
-- scan: MipsAtom_(<name>) {
|
||||
if brace then
|
||||
local _, after_brace = read_braces(source, brace)
|
||||
-- scan: MipsAtom_(<name>) { <body> }
|
||||
pos = after_brace
|
||||
else
|
||||
pos = open_paren + 1
|
||||
end
|
||||
end
|
||||
if not ident then pos = pos + 1; goto continue end
|
||||
if ident ~= ATOM_DECL then pos = after_ident; goto continue end
|
||||
|
||||
local open_paren = skip_ws_and_cmt(source, after_ident)
|
||||
if source:byte(open_paren) ~= BYTE_OPEN_PAREN then pos = open_paren + 1; goto continue end
|
||||
|
||||
local inner, after_paren = read_parens(source, open_paren)
|
||||
-- scan: MipsAtom_(<name>)
|
||||
local name, _ = read_alnum_ident(inner, 1)
|
||||
if name and name ~= "" then
|
||||
out[#out + 1] = { line = line_of(pos), name = name }
|
||||
end
|
||||
local brace = scan_to_char(source, "{", after_paren)
|
||||
-- scan: MipsAtom_(<name>) {
|
||||
if brace then
|
||||
local _, after_brace = read_braces(source, brace)
|
||||
-- scan: MipsAtom_(<name>) { <body> }
|
||||
pos = after_brace
|
||||
else
|
||||
pos = open_paren + 1
|
||||
end
|
||||
|
||||
::continue::
|
||||
end
|
||||
return out
|
||||
end
|
||||
@@ -668,39 +660,37 @@ local function find_atom_annotations(source)
|
||||
|
||||
-- Skip preprocessor directives (lines starting with #).
|
||||
if source:byte(pos) == 35 then -- '#'
|
||||
pos = skip_preprocessor_line(source, pos)
|
||||
else
|
||||
local ident, after_ident = read_ident(source, pos)
|
||||
-- scan: <ident>
|
||||
if not ident then
|
||||
pos = pos + 1
|
||||
elseif ident == ATOM_DECL then
|
||||
local open_paren = skip_ws_and_cmt(source, after_ident)
|
||||
if source:byte(open_paren) ~= BYTE_OPEN_PAREN then
|
||||
pos = open_paren + 1
|
||||
else
|
||||
local inner, after_paren = read_parens(source, open_paren)
|
||||
-- scan: MipsAtom_(<name>)
|
||||
local name, _ = read_alnum_ident(inner, 1)
|
||||
|
||||
local entry, new_pos = parse_atom_info_call(source, name, after_paren, line_of)
|
||||
-- scan: MipsAtom_(<name>) atom_info(<binds>, <reads>, <writes>)
|
||||
if entry then annots[#annots + 1] = entry end
|
||||
pos = new_pos
|
||||
|
||||
-- Skip past the body { ... } if present.
|
||||
local brace = scan_to_char(source, "{", pos)
|
||||
-- scan: MipsAtom_(<name>) atom_info(...) {
|
||||
if brace then
|
||||
local _, after_brace = read_braces(source, brace)
|
||||
-- scan: MipsAtom_(<name>) atom_info(...) { <body> }
|
||||
pos = after_brace
|
||||
end
|
||||
end
|
||||
else
|
||||
pos = after_ident
|
||||
end
|
||||
pos = duffle.skip_preprocessor_line(source, pos)
|
||||
goto continue
|
||||
end
|
||||
|
||||
local ident, after_ident = read_ident(source, pos)
|
||||
-- scan: <ident>
|
||||
if not ident then pos = pos + 1; goto continue end
|
||||
if ident ~= ATOM_DECL then pos = after_ident; goto continue end
|
||||
|
||||
local open_paren = skip_ws_and_cmt(source, after_ident)
|
||||
if source:byte(open_paren) ~= BYTE_OPEN_PAREN then pos = open_paren + 1; goto continue end
|
||||
|
||||
local inner, after_paren = read_parens(source, open_paren)
|
||||
-- scan: MipsAtom_(<name>)
|
||||
local name, _ = read_alnum_ident(inner, 1)
|
||||
|
||||
local entry, new_pos = parse_atom_info_call(source, name, after_paren, line_of)
|
||||
-- scan: MipsAtom_(<name>) atom_info(<binds>, <reads>, <writes>)
|
||||
if entry then annots[#annots + 1] = entry end
|
||||
pos = new_pos
|
||||
|
||||
-- Skip past the body { ... } if present.
|
||||
local brace = scan_to_char(source, "{", pos)
|
||||
-- scan: MipsAtom_(<name>) atom_info(...) {
|
||||
if brace then
|
||||
local _, after_brace = read_braces(source, brace)
|
||||
-- scan: MipsAtom_(<name>) atom_info(...) { <body> }
|
||||
pos = after_brace
|
||||
end
|
||||
|
||||
::continue::
|
||||
end
|
||||
return annots
|
||||
end
|
||||
|
||||
@@ -455,40 +455,35 @@ local function find_component_atoms(source)
|
||||
local ident, after_ident = duffle.read_ident(source, pos)
|
||||
-- scan: <ident>
|
||||
local is_comp = ident == ATOM_COMP or ident == ATOM_COMP_PROC
|
||||
if not ident then
|
||||
pos = pos + 1
|
||||
elseif not is_comp then
|
||||
pos = after_ident
|
||||
if not ident then pos = pos + 1; goto continue end
|
||||
if not is_comp then pos = after_ident; goto continue end
|
||||
|
||||
local open_paren = duffle.skip_ws_and_cmt(source, after_ident)
|
||||
if source:sub(open_paren, open_paren) ~= "(" then pos = open_paren + 1; goto continue end
|
||||
|
||||
local inner, after_paren = duffle.read_parens(source, open_paren)
|
||||
-- scan: <ident>(<args>)
|
||||
local name, body = parse_atomcomp_inner(inner)
|
||||
-- scan: <ident>(<name>) OR <ident>(<name>, { <body> })
|
||||
if not name or name:sub(1, AC_PREFIX_LEN) ~= AC_PREFIX then pos = open_paren + 1; goto continue end
|
||||
|
||||
local args = find_function_args_for(source, name, open_paren)
|
||||
local comment = preceding_comment_block(source, pos)
|
||||
if body == nil then
|
||||
-- Bare form: body comes from the brace block after the parens.
|
||||
-- scan: <ident>(<name>) {
|
||||
local comp, new_pos = make_bare_component(source, name, pos, after_paren, line_of, args, comment)
|
||||
-- scan: <ident>(<name>) { <body> }
|
||||
if comp then out[#out + 1] = comp end
|
||||
pos = new_pos
|
||||
else
|
||||
local open_paren = duffle.skip_ws_and_cmt(source, after_ident)
|
||||
if source:sub(open_paren, open_paren) ~= "(" then
|
||||
pos = open_paren + 1
|
||||
else
|
||||
local inner, after_paren = duffle.read_parens(source, open_paren)
|
||||
-- scan: <ident>(<args>)
|
||||
local name, body = parse_atomcomp_inner(inner)
|
||||
-- scan: <ident>(<name>) OR <ident>(<name>, { <body> })
|
||||
if not name or name:sub(1, AC_PREFIX_LEN) ~= AC_PREFIX then
|
||||
pos = open_paren + 1
|
||||
else
|
||||
local args = find_function_args_for(source, name, open_paren)
|
||||
local comment = preceding_comment_block(source, pos)
|
||||
if body == nil then
|
||||
-- Bare form: body comes from the brace block after the parens.
|
||||
-- scan: <ident>(<name>) {
|
||||
local comp, new_pos = make_bare_component(source, name, pos, after_paren, line_of, args, comment)
|
||||
-- scan: <ident>(<name>) { <body> }
|
||||
if comp then out[#out + 1] = comp end
|
||||
pos = new_pos
|
||||
else
|
||||
-- Function form: body was inside the parens.
|
||||
-- scan: <ident>(<name>, { <body> })
|
||||
out[#out + 1] = make_proc_component(name, body, pos, line_of, args, comment)
|
||||
pos = after_paren
|
||||
end
|
||||
end
|
||||
end
|
||||
-- Function form: body was inside the parens.
|
||||
-- scan: <ident>(<name>, { <body> })
|
||||
out[#out + 1] = make_proc_component(name, body, pos, line_of, args, comment)
|
||||
pos = after_paren
|
||||
end
|
||||
|
||||
::continue::
|
||||
end
|
||||
return out
|
||||
end
|
||||
|
||||
@@ -335,9 +335,9 @@ local function find_atoms(source_text)
|
||||
|
||||
local ident, after = duffle.read_ident(source_text, pos)
|
||||
-- scan: <ident>
|
||||
if not ident then
|
||||
pos = pos + 1
|
||||
elseif ident == ATOM_PREFIX then
|
||||
if not ident then pos = pos + 1; goto continue end
|
||||
|
||||
if ident == ATOM_PREFIX then
|
||||
-- scan: MipsAtom_(<name>) { <body> }
|
||||
local atom = try_wrapped_atom(source_text, after)
|
||||
if atom then
|
||||
@@ -358,6 +358,8 @@ local function find_atoms(source_text)
|
||||
else
|
||||
pos = after
|
||||
end
|
||||
|
||||
::continue::
|
||||
end
|
||||
return atoms
|
||||
end
|
||||
|
||||
+322
-353
@@ -118,6 +118,68 @@ local OUTPUT_EXTENSION = ".static_analysis.txt"
|
||||
-- Source walkers
|
||||
-- ════════════════════════════════════════════════════════════════════════════
|
||||
|
||||
-- Parse `MipsAtomComp_Proc_(name, { body })` — the body is inside the LAST `{ ... }` in the args.
|
||||
-- Returns (atom_entry, new_pos) or (nil, fallback_pos).
|
||||
local function parse_comp_proc_body(inner, line_of, pos, open_paren, after_paren)
|
||||
-- Find the last `{` in `inner`, then the matching `}`.
|
||||
local last_brace_pos
|
||||
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 nil, open_paren + 1 end
|
||||
-- Walk forward to find matching `}` honoring balanced ()/[] and strings.
|
||||
local depth = 1
|
||||
local inner_pos = last_brace_pos + 1
|
||||
while inner_pos <= #inner and depth > 0 do
|
||||
local c = inner:byte(inner_pos)
|
||||
if c == 123 then
|
||||
depth = depth + 1; inner_pos = inner_pos + 1
|
||||
elseif c == 125 then
|
||||
depth = depth - 1
|
||||
if depth == 0 then break end
|
||||
inner_pos = inner_pos + 1
|
||||
elseif c == 40 then
|
||||
local _, a = duffle.read_parens(inner, inner_pos); inner_pos = a
|
||||
elseif c == 91 then
|
||||
local _, a = duffle.read_brackets(inner, inner_pos); inner_pos = a
|
||||
elseif c == 34 or c == 39 then
|
||||
inner_pos = duffle.skip_str_or_cmt(inner, inner_pos) + 1
|
||||
else
|
||||
inner_pos = inner_pos + 1
|
||||
end
|
||||
end
|
||||
if depth ~= 0 then return nil, open_paren + 1 end
|
||||
-- scan: <ident>(<name>, { <body> })
|
||||
local name_match = inner:match("^%s*([%w_]+)")
|
||||
local name = name_match or "?"
|
||||
local body = inner:sub(last_brace_pos + 1, inner_pos - 1)
|
||||
local body_off = open_paren + 1 + last_brace_pos
|
||||
return {
|
||||
line = line_of(pos), name = name, body = body, body_off = body_off + 1, kind = "comp_proc",
|
||||
}, after_paren
|
||||
end
|
||||
|
||||
-- Parse `MipsAtom_(name) { body }` or `MipsAtomComp_(name) { body }` — body is the FIRST `{ ... }` after the parens.
|
||||
-- Returns (atom_entry, new_pos) or (nil, fallback_pos).
|
||||
local function parse_atom_or_comp_bare_body(inner, source_text, line_of, pos, open_paren, after_paren, kind)
|
||||
-- Extract the name from the first arg.
|
||||
local name_start = 1
|
||||
while name_start <= #inner and inner:sub(name_start, name_start):match("[%s]") do name_start = name_start + 1 end
|
||||
local name_end = name_start
|
||||
while name_end <= #inner and inner:sub(name_end, name_end):match("[%w_]") do name_end = name_end + 1 end
|
||||
local name = inner:sub(name_start, name_end - 1)
|
||||
-- scan: <ident>(<name>)
|
||||
if name == "" then return nil, open_paren + 1 end
|
||||
local brace = duffle.scan_to_char(source_text, "{", after_paren)
|
||||
-- scan: <ident>(<name>) {
|
||||
if not brace then return nil, open_paren + 1 end
|
||||
local body, after_brace = duffle.read_braces(source_text, brace)
|
||||
-- scan: <ident>(<name>) { <body> }
|
||||
return {
|
||||
line = line_of(pos), name = name, body = body, body_off = brace + 1, kind = kind,
|
||||
}, after_brace
|
||||
end
|
||||
|
||||
--- Walk source-as-written, return a list of `{line, name, body,
|
||||
--- body_off, kind}` for every:
|
||||
--- `MipsAtom_(name) { body };` -> kind = "atom" (baked atom)
|
||||
@@ -140,142 +202,50 @@ local OUTPUT_EXTENSION = ".static_analysis.txt"
|
||||
--- `body[1]` in `source_text`, used to compute per-token line numbers
|
||||
--- later.
|
||||
local function find_atom_bodies(source_text)
|
||||
local line_of = duffle.LineIndex(source_text)
|
||||
local out = {}
|
||||
local line_of = duffle.LineIndex(source_text)
|
||||
local out = {}
|
||||
local src_len = #source_text
|
||||
local pos = 1
|
||||
local pos = 1
|
||||
while pos <= src_len do
|
||||
pos = duffle.skip_ws_and_cmt(source_text, pos); if pos > src_len then break end
|
||||
-- Skip preprocessor directives (#define / #include / #pragma /
|
||||
-- etc). Otherwise the `#define MipsAtom_(sym) ...` definition
|
||||
-- in lottes_tape.h gets matched as an atom named "sym" and
|
||||
-- its `body` swallows the next real atom declaration via
|
||||
-- duffle.scan_to_char("{", ...).
|
||||
if source_text:sub(pos, pos) == "#" then
|
||||
local eol_pos = pos
|
||||
while eol_pos <= src_len and source_text:byte(eol_pos) ~= 10 do eol_pos = eol_pos + 1 end
|
||||
pos = eol_pos + 1
|
||||
else
|
||||
local ident, ident_end = duffle.read_ident(source_text, pos)
|
||||
pos = duffle.skip_ws_and_cmt(source_text, pos)
|
||||
if pos > src_len then break end
|
||||
|
||||
-- Skip preprocessor directives (#define / #include / #pragma / etc).
|
||||
local pp_pos = duffle.skip_preprocessor_line(source_text, pos)
|
||||
if pp_pos then pos = pp_pos; goto continue end
|
||||
|
||||
local ident, ident_end = duffle.read_ident(source_text, pos)
|
||||
-- scan: <ident>
|
||||
if not ident then
|
||||
pos = pos + 1
|
||||
elseif ident == "MipsAtom_"
|
||||
or ident == "MipsAtomComp_"
|
||||
or ident == "MipsAtomComp_Proc_" then
|
||||
-- Determine the kind from the exact ident (3 distinct macros,
|
||||
-- each with its own kind).
|
||||
local kind
|
||||
if ident == "MipsAtom_" then kind = "atom"
|
||||
elseif ident == "MipsAtomComp_" then kind = "comp_bare"
|
||||
else kind = "comp_proc"
|
||||
end
|
||||
|
||||
local open_paren = duffle.skip_ws_and_cmt(source_text, ident_end)
|
||||
if source_text:sub(open_paren, open_paren) ~= "(" then
|
||||
pos = open_paren + 1
|
||||
else
|
||||
local inner, after_paren = duffle.read_parens(source_text, open_paren)
|
||||
-- scan: <ident>(<args>)
|
||||
|
||||
if kind == "comp_proc" then
|
||||
-- MipsAtomComp_Proc_(sym, { body })
|
||||
-- The body is inside the LAST `{ ... }` in the args
|
||||
-- (the macro takes 2 args: sym name, then body in {}).
|
||||
-- Find the last `{` in `inner`, then the matching `}`.
|
||||
local last_brace_pos
|
||||
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
|
||||
pos = open_paren + 1
|
||||
else
|
||||
-- Walk forward to find matching `}` honoring balanced
|
||||
-- ()/[] and strings. We could call duffle.read_braces
|
||||
-- from last_brace_pos+1, but read_braces expects to start at
|
||||
-- the brace itself. Inline the walk for clarity.
|
||||
local depth = 1
|
||||
local inner_pos = last_brace_pos + 1
|
||||
while inner_pos <= #inner and depth > 0 do
|
||||
local c = inner:byte(inner_pos)
|
||||
if c == 123 then
|
||||
depth = depth + 1; inner_pos = inner_pos + 1
|
||||
elseif c == 125 then
|
||||
depth = depth - 1
|
||||
if depth == 0 then break end
|
||||
inner_pos = inner_pos + 1
|
||||
elseif c == 40 then
|
||||
local _, a = duffle.read_parens(inner, inner_pos); inner_pos = a
|
||||
elseif c == 91 then
|
||||
local _, a = duffle.read_brackets(inner, inner_pos); inner_pos = a
|
||||
elseif c == 34 or c == 39 then
|
||||
inner_pos = duffle.skip_str_or_cmt(inner, inner_pos) + 1
|
||||
else
|
||||
inner_pos = inner_pos + 1
|
||||
end
|
||||
end
|
||||
if depth ~= 0 then
|
||||
-- unmatched; bail
|
||||
pos = open_paren + 1
|
||||
else
|
||||
-- scan: <ident>(<name>, { <body> })
|
||||
-- First ident in `inner` is the comp name.
|
||||
local name_match = inner:match("^%s*([%w_]+)")
|
||||
local name = name_match or "?"
|
||||
local body = inner:sub(last_brace_pos + 1, inner_pos - 1)
|
||||
-- body_off in full source: position right after the
|
||||
-- LAST `{` in `inner`, which sits at `open_paren+1+last_brace_pos`
|
||||
-- (open_paren+1 = just inside the outer paren,
|
||||
-- +last_brace_pos = at the `{`).
|
||||
local body_off = open_paren + 1 + last_brace_pos
|
||||
out[#out + 1] = {
|
||||
line = line_of(pos),
|
||||
name = name,
|
||||
body = body,
|
||||
body_off = body_off + 1,
|
||||
kind = kind,
|
||||
}
|
||||
pos = after_paren
|
||||
end
|
||||
end
|
||||
else
|
||||
-- MipsAtom_(sym) { body }; OR
|
||||
-- MipsAtomComp_(sym) { body };
|
||||
-- name is the first arg, body is the FIRST { ... } after
|
||||
-- the paren.
|
||||
local name_start = 1
|
||||
while name_start <= #inner and inner:sub(name_start, name_start):match("[%s]") do name_start = name_start + 1 end
|
||||
local name_end = name_start
|
||||
while name_end <= #inner and inner:sub(name_end, name_end):match("[%w_]") do name_end = name_end + 1 end
|
||||
local name = inner:sub(name_start, name_end - 1)
|
||||
-- scan: <ident>(<name>)
|
||||
if name == "" then
|
||||
pos = open_paren + 1
|
||||
else
|
||||
local brace = duffle.scan_to_char(source_text, "{", after_paren)
|
||||
-- scan: <ident>(<name>) {
|
||||
if brace then
|
||||
local body, after_brace = duffle.read_braces(source_text, brace)
|
||||
-- scan: <ident>(<name>) { <body> }
|
||||
local body_off = brace + 1
|
||||
out[#out + 1] = {
|
||||
line = line_of(pos),
|
||||
name = name,
|
||||
body = body,
|
||||
body_off = body_off,
|
||||
kind = kind,
|
||||
}
|
||||
pos = after_brace
|
||||
else
|
||||
pos = open_paren + 1
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
else
|
||||
pos = ident_end
|
||||
pos = pos + 1; goto continue
|
||||
end
|
||||
end -- close the new preprocessor-skip else
|
||||
|
||||
local is_atom = ident == "MipsAtom_"
|
||||
local is_comp = ident == "MipsAtomComp_"
|
||||
local is_proc = ident == "MipsAtomComp_Proc_"
|
||||
if not is_atom and not is_comp and not is_proc then
|
||||
pos = ident_end; goto continue
|
||||
end
|
||||
|
||||
local kind = is_atom and "atom" or (is_comp and "comp_bare" or "comp_proc")
|
||||
local open_paren = duffle.skip_ws_and_cmt(source_text, ident_end)
|
||||
if source_text:sub(open_paren, open_paren) ~= "(" then
|
||||
pos = open_paren + 1; goto continue
|
||||
end
|
||||
|
||||
local inner, after_paren = duffle.read_parens(source_text, open_paren)
|
||||
-- scan: <ident>(<args>)
|
||||
|
||||
local entry, new_pos
|
||||
if is_proc then
|
||||
entry, new_pos = parse_comp_proc_body(inner, line_of, pos, open_paren, after_paren)
|
||||
else
|
||||
entry, new_pos = parse_atom_or_comp_bare_body(inner, source_text, line_of, pos, open_paren, after_paren, kind)
|
||||
end
|
||||
if entry then out[#out + 1] = entry end
|
||||
pos = new_pos
|
||||
|
||||
::continue::
|
||||
end
|
||||
return out
|
||||
end
|
||||
@@ -388,82 +358,74 @@ end
|
||||
-- Check #1: GTE pipeline-fill
|
||||
-- ════════════════════════════════════════════════════════════════════════════
|
||||
|
||||
-- Count consecutive nop words immediately BEFORE token index `ti` in the token list.
|
||||
-- Walks backwards from ti-1, accumulating nop_word_count, stopping at the first non-nop.
|
||||
-- scan: nop, nop, <non-nop> -> have = count of nop words before ti
|
||||
local function count_preceding_nops(tokens, ti)
|
||||
local have = 0
|
||||
local where_ti = ti - 1
|
||||
while where_ti >= 1 do
|
||||
local n = nop_word_count(tokens[where_ti].tok)
|
||||
if n == 0 then break end
|
||||
have = have + n
|
||||
where_ti = where_ti - 1
|
||||
end
|
||||
return have
|
||||
end
|
||||
|
||||
-- Check a single gte_cmdw_* token for pipeline-fill compliance. Emits a finding if the
|
||||
-- preceding nops are insufficient (error) or the macro isn't in the latency table (warning).
|
||||
-- scan: <nop>... <gte_cmdw_X> -> validate nop count vs GTE_PIPELINE_LATENCY[X]
|
||||
local function check_one_gte_cmdw(a, tok, tokens, ti, line_in_body, findings)
|
||||
local cmdw_full = tok:match("^(gte_cmdw_[%w_]+)%s*[,%)]") or tok:match("^(gte_cmdw_[%w_]+)%s*$")
|
||||
if not cmdw_full then return end
|
||||
|
||||
local variant = cmdw_full:match("^gte_cmdw_(.+)$")
|
||||
local need = duffle.GTE_PIPELINE_LATENCY[cmdw_full]
|
||||
local line = a.line + line_in_body[tokens[ti].rel]
|
||||
|
||||
if need == nil then
|
||||
-- alias or new gte_cmdw_<X> not yet in latency table
|
||||
findings[#findings + 1] = {
|
||||
atom = a.name,
|
||||
line = line,
|
||||
check = "gte_pipeline_fill",
|
||||
kind = "warning",
|
||||
msg = string.format(
|
||||
"%s at line %d uses `gte_cmdw_%s` but that macro is not in duffle.GTE_PIPELINE_LATENCY -- add a min_nops entry",
|
||||
a.name, line, variant),
|
||||
}
|
||||
elseif need > 0 then
|
||||
local have = count_preceding_nops(tokens, ti)
|
||||
if have < need then
|
||||
findings[#findings + 1] = {
|
||||
atom = a.name,
|
||||
line = line,
|
||||
check = "gte_pipeline_fill",
|
||||
kind = "error",
|
||||
msg = string.format(
|
||||
"%s at line %d needs %d nop word%s immediately BEFORE `gte_cmdw_%s`; only %d found",
|
||||
a.name, line, need, need == 1 and "" or "s", variant, have),
|
||||
}
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
--- Walk the token list. Whenever we hit a `gte_cmdw_<X>` token, count
|
||||
--- consecutive nop words starting at the next token. If count < the
|
||||
--- consecutive nop words immediately preceding it. If count < the
|
||||
--- minimum declared in `duffle.GTE_PIPELINE_LATENCY[X]`, record a finding.
|
||||
---
|
||||
--- Aliases (`gte_cmdw_rotate_translate_perspective_single` etc.) are
|
||||
--- resolved against the lookup table directly; if a macro name is not
|
||||
--- 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).
|
||||
local function check_gte_pipeline_fill(atoms, findings, line_of)
|
||||
-- Walk the token list. Whenever we hit a `gte_cmdw_<X>` token,
|
||||
-- count consecutive `nop` words IMMEDIATELY PRECEDING it (the
|
||||
-- source-level `nop2, gte_cmdw_X` idiom provides the pre-pipeline
|
||||
-- fill that gte.h's wrapper functions provide internally). If
|
||||
-- count < `duffle.GTE_PIPELINE_LATENCY[X]`, record a finding.
|
||||
--
|
||||
-- We count nops going backwards from the cmdw token, stopping at
|
||||
-- the first non-nop token. Tokens like `mem_share` or `port_write`
|
||||
-- (any non-nop) break the count. `gte_mv_to_data_r` (writes to
|
||||
-- C2_DR registers) are non-nops in this sense -- they count as
|
||||
-- "previous GTE state" but don't themselves count as pipeline
|
||||
-- fill.
|
||||
for _, a in ipairs(atoms) do
|
||||
local tokens = tokenize_body(a.body)
|
||||
local line_in_body = build_body_line_index(a.body)
|
||||
local tn = #tokens
|
||||
local ti = 1
|
||||
while ti <= tn do
|
||||
local tok = tokens[ti].tok
|
||||
local cmdw_full = tok:match("^(gte_cmdw_[%w_]+)%s*[,%)]") or tok:match("^(gte_cmdw_[%w_]+)%s*$")
|
||||
if cmdw_full then
|
||||
local variant = cmdw_full:match("^gte_cmdw_(.+)$")
|
||||
local need = duffle.GTE_PIPELINE_LATENCY[cmdw_full]
|
||||
if need == nil then
|
||||
-- alias or new gte_cmdw_<X> not yet in latency table
|
||||
local line = a.line + line_in_body[tokens[ti].rel]
|
||||
findings[#findings + 1] = {
|
||||
atom = a.name,
|
||||
line = line,
|
||||
check = "gte_pipeline_fill",
|
||||
kind = "warning",
|
||||
msg = string.format(
|
||||
"%s at line %d uses `gte_cmdw_%s` but that macro is not in duffle.GTE_PIPELINE_LATENCY -- add a min_nops entry",
|
||||
a.name, line, variant),
|
||||
}
|
||||
ti = ti + 1
|
||||
elseif need > 0 then
|
||||
-- Count consecutive nops immediately BEFORE the cmdw
|
||||
-- token. We walk tokens[ti - n] backwards, accumulating
|
||||
-- nop_word_count, stopping at the first non-nop.
|
||||
local have = 0
|
||||
local where_ti = ti - 1
|
||||
while where_ti >= 1 do
|
||||
local n = nop_word_count(tokens[where_ti].tok)
|
||||
if n == 0 then break end
|
||||
have = have + n
|
||||
where_ti = where_ti - 1
|
||||
end
|
||||
if have < need then
|
||||
local line = a.line + line_in_body[tokens[ti].rel]
|
||||
findings[#findings + 1] = {
|
||||
atom = a.name,
|
||||
line = line,
|
||||
check = "gte_pipeline_fill",
|
||||
kind = "error",
|
||||
msg = string.format(
|
||||
"%s at line %d needs %d nop word%s immediately BEFORE `gte_cmdw_%s`; only %d found",
|
||||
a.name, line, need, need == 1 and "" or "s", variant, have),
|
||||
}
|
||||
end
|
||||
ti = ti + 1
|
||||
else
|
||||
ti = ti + 1
|
||||
end
|
||||
else
|
||||
ti = ti + 1
|
||||
end
|
||||
check_one_gte_cmdw(a, tokens[ti].tok, tokens, ti, line_in_body, findings)
|
||||
ti = ti + 1
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -583,6 +545,31 @@ end
|
||||
-- Source walkers: Binds_* structs + per-atom atom_info
|
||||
-- ════════════════════════════════════════════════════════════════════════════
|
||||
|
||||
-- Parse the U4 fields from a Binds_X body. Returns (fields, byte_count).
|
||||
local function parse_binds_fields(body)
|
||||
local fields = {}
|
||||
local byte_off = 0
|
||||
local body_pos = 1
|
||||
while body_pos <= #body do
|
||||
body_pos = duffle.skip_ws_and_cmt(body, body_pos)
|
||||
if body_pos > #body then break end
|
||||
local type_ident, type_end = duffle.read_ident(body, body_pos)
|
||||
if not type_ident then
|
||||
body_pos = body_pos + 1
|
||||
elseif type_ident == "U4" then
|
||||
local field_ident, field_end = duffle.read_ident(body, duffle.skip_ws_and_cmt(body, type_end))
|
||||
if field_ident then
|
||||
fields[#fields + 1] = { name = field_ident, offset = byte_off }
|
||||
byte_off = byte_off + 4
|
||||
end
|
||||
body_pos = field_end or (type_end + 1)
|
||||
else
|
||||
body_pos = type_end + 1
|
||||
end
|
||||
end
|
||||
return fields, byte_off
|
||||
end
|
||||
|
||||
--- Walk source-as-written, return a list of `{line, name, fields, bytes}`
|
||||
--- for every `typedef Struct_(Binds_X) { ... };` declaration. Only U4
|
||||
--- fields are tracked (Binds_* are always word arrays in this codebase --
|
||||
@@ -591,75 +578,109 @@ end
|
||||
--- static-analysis; each pass re-walks source).
|
||||
local function find_binds_structs(source_text)
|
||||
local line_of = duffle.LineIndex(source_text)
|
||||
local out = {}
|
||||
local out = {}
|
||||
local src_len = #source_text
|
||||
local pos = 1
|
||||
local pos = 1
|
||||
while pos <= src_len do
|
||||
pos = duffle.skip_ws_and_cmt(source_text, pos); if pos > src_len then break end
|
||||
if source_text:sub(pos, pos) == "#" then
|
||||
local eol_pos = pos
|
||||
while eol_pos <= src_len and source_text:byte(eol_pos) ~= 10 do eol_pos = eol_pos + 1 end
|
||||
pos = eol_pos + 1
|
||||
else
|
||||
local ident, ident_end = duffle.read_ident(source_text, pos)
|
||||
-- scan: <ident>
|
||||
if not ident then
|
||||
pos = pos + 1
|
||||
elseif ident == "typedef" then
|
||||
local after_typedef = duffle.skip_ws_and_cmt(source_text, ident_end)
|
||||
local id2, id2_end = duffle.read_ident(source_text, after_typedef)
|
||||
-- scan: typedef <id2>
|
||||
if id2 ~= "Struct_" then
|
||||
pos = id2_end or (after_typedef + 1)
|
||||
else
|
||||
local open_paren = duffle.skip_ws_and_cmt(source_text, id2_end)
|
||||
if source_text:sub(open_paren, open_paren) ~= "(" then
|
||||
pos = open_paren + 1
|
||||
else
|
||||
local inner, after_paren = duffle.read_parens(source_text, open_paren)
|
||||
-- scan: typedef Struct_(<name>)
|
||||
local name = duffle.trim(inner)
|
||||
local brace = duffle.scan_to_char(source_text, "{", after_paren)
|
||||
-- scan: typedef Struct_(<name>) {
|
||||
if not brace then
|
||||
pos = open_paren + 1
|
||||
else
|
||||
local body, after_brace = duffle.read_braces(source_text, brace)
|
||||
-- scan: typedef Struct_(<name>) { <fields> }
|
||||
local fields = {}
|
||||
local byte_off = 0
|
||||
local body_pos = 1
|
||||
while body_pos <= #body do
|
||||
body_pos = duffle.skip_ws_and_cmt(body, body_pos); if body_pos > #body then break end
|
||||
local type_ident, type_end = duffle.read_ident(body, body_pos)
|
||||
if not type_ident then
|
||||
body_pos = body_pos + 1
|
||||
elseif type_ident == "U4" then
|
||||
local field_ident, field_end = duffle.read_ident(body, duffle.skip_ws_and_cmt(body, type_end))
|
||||
if field_ident then
|
||||
fields[#fields + 1] = { name = field_ident, offset = byte_off }
|
||||
byte_off = byte_off + 4
|
||||
end
|
||||
body_pos = field_end or (type_end + 1)
|
||||
else
|
||||
body_pos = type_end + 1
|
||||
end
|
||||
end
|
||||
if name:sub(1, 6) == "Binds_" then
|
||||
out[#out + 1] = { line = line_of(pos), name = name, fields = fields, bytes = byte_off }
|
||||
end
|
||||
pos = after_brace
|
||||
end
|
||||
end
|
||||
end
|
||||
else
|
||||
pos = ident_end
|
||||
end
|
||||
pos = duffle.skip_ws_and_cmt(source_text, pos)
|
||||
if pos > src_len then break end
|
||||
|
||||
local pp_pos = duffle.skip_preprocessor_line(source_text, pos)
|
||||
if pp_pos then pos = pp_pos; goto continue end
|
||||
|
||||
local ident, ident_end = duffle.read_ident(source_text, pos)
|
||||
-- scan: <ident>
|
||||
if not ident then pos = pos + 1; goto continue end
|
||||
if ident ~= "typedef" then pos = ident_end; goto continue end
|
||||
|
||||
local after_typedef = duffle.skip_ws_and_cmt(source_text, ident_end)
|
||||
local id2, id2_end = duffle.read_ident(source_text, after_typedef)
|
||||
-- scan: typedef <id2>
|
||||
if id2 ~= "Struct_" then pos = id2_end or (after_typedef + 1); goto continue end
|
||||
|
||||
local open_paren = duffle.skip_ws_and_cmt(source_text, id2_end)
|
||||
if source_text:sub(open_paren, open_paren) ~= "(" then pos = open_paren + 1; goto continue end
|
||||
|
||||
local inner, after_paren = duffle.read_parens(source_text, open_paren)
|
||||
-- scan: typedef Struct_(<name>)
|
||||
local name = duffle.trim(inner)
|
||||
local brace = duffle.scan_to_char(source_text, "{", after_paren)
|
||||
-- scan: typedef Struct_(<name>) {
|
||||
if not brace then pos = open_paren + 1; goto continue end
|
||||
|
||||
local body, after_brace = duffle.read_braces(source_text, brace)
|
||||
-- scan: typedef Struct_(<name>) { <fields> }
|
||||
local fields, byte_off = parse_binds_fields(body)
|
||||
if name:sub(1, 6) == "Binds_" then
|
||||
out[#out + 1] = { line = line_of(pos), name = name, fields = fields, bytes = byte_off }
|
||||
end
|
||||
pos = after_brace
|
||||
|
||||
::continue::
|
||||
end
|
||||
return out
|
||||
end
|
||||
|
||||
-- Parse the register list from inside `atom_reads(...)` or `atom_writes(...)`.
|
||||
local function parse_reg_list(sub_inner)
|
||||
local regs = {}
|
||||
local sub_inner_pos = 1
|
||||
while sub_inner_pos <= #sub_inner do
|
||||
sub_inner_pos = duffle.skip_ws_and_cmt(sub_inner, sub_inner_pos)
|
||||
if sub_inner_pos > #sub_inner then break end
|
||||
local reg_ident, reg_end = duffle.read_ident(sub_inner, sub_inner_pos)
|
||||
if reg_ident then
|
||||
regs[#regs + 1] = duffle.trim(reg_ident)
|
||||
sub_inner_pos = reg_end
|
||||
else
|
||||
sub_inner_pos = sub_inner_pos + 1
|
||||
end
|
||||
if sub_inner_pos > #sub_inner then break end
|
||||
if sub_inner:sub(sub_inner_pos, sub_inner_pos) == "," then sub_inner_pos = sub_inner_pos + 1 end
|
||||
end
|
||||
return regs
|
||||
end
|
||||
|
||||
-- Parse the sub-calls inside `atom_info(atom_bind(...), atom_reads(...), atom_writes(...))`.
|
||||
-- Returns (binds, reads, writes).
|
||||
local function parse_atom_info_subcalls(info_inner)
|
||||
local binds, reads, writes = nil, nil, nil
|
||||
local sub_pos = 1
|
||||
while sub_pos <= #info_inner do
|
||||
sub_pos = duffle.skip_ws_and_cmt(info_inner, sub_pos)
|
||||
if sub_pos > #info_inner then break end
|
||||
local sub_ident, sub_end = duffle.read_ident(info_inner, sub_pos)
|
||||
if not sub_ident then
|
||||
sub_pos = sub_pos + 1
|
||||
elseif sub_ident == "atom_bind" then
|
||||
local sub_open = duffle.skip_ws_and_cmt(info_inner, sub_end)
|
||||
if info_inner:sub(sub_open, sub_open) == "(" then
|
||||
local sub_inner, sub_after2 = duffle.read_parens(info_inner, sub_open)
|
||||
-- scan: atom_bind(<Binds_X>)
|
||||
binds = duffle.trim(sub_inner)
|
||||
sub_pos = sub_after2
|
||||
else
|
||||
sub_pos = sub_open + 1
|
||||
end
|
||||
elseif sub_ident == "atom_reads" or sub_ident == "atom_writes" then
|
||||
local kind = sub_ident
|
||||
local sub_open = duffle.skip_ws_and_cmt(info_inner, sub_end)
|
||||
if info_inner:sub(sub_open, sub_open) == "(" then
|
||||
local sub_inner, sub_after2 = duffle.read_parens(info_inner, sub_open)
|
||||
-- scan: atom_reads(<regs>) OR atom_writes(<regs>)
|
||||
local regs = parse_reg_list(sub_inner)
|
||||
if kind == "atom_reads" then reads = regs else writes = regs end
|
||||
sub_pos = sub_after2
|
||||
else
|
||||
sub_pos = sub_open + 1
|
||||
end
|
||||
else
|
||||
sub_pos = sub_end
|
||||
end
|
||||
end
|
||||
return binds, reads, writes
|
||||
end
|
||||
|
||||
--- For every `MipsAtom_(name)` followed by `atom_info(...)`, parse the
|
||||
--- atom_info's `atom_bind(Binds_X)`, `atom_reads(...)`, and
|
||||
--- `atom_writes(...)` sub-calls. Returns a list of
|
||||
@@ -667,103 +688,51 @@ end
|
||||
--- check_abi_handoff.
|
||||
local function find_atom_info(source_text)
|
||||
local line_of = duffle.LineIndex(source_text)
|
||||
local out = {}
|
||||
local out = {}
|
||||
local src_len = #source_text
|
||||
local pos = 1
|
||||
local pos = 1
|
||||
while pos <= src_len do
|
||||
pos = duffle.skip_ws_and_cmt(source_text, pos); if pos > src_len then break end
|
||||
if source_text:sub(pos, pos) == "#" then
|
||||
local eol_pos = pos
|
||||
while eol_pos <= src_len and source_text:byte(eol_pos) ~= 10 do eol_pos = eol_pos + 1 end
|
||||
pos = eol_pos + 1
|
||||
else
|
||||
local ident, ident_end = duffle.read_ident(source_text, pos)
|
||||
-- scan: <ident>
|
||||
if not ident then
|
||||
pos = pos + 1
|
||||
elseif ident == "MipsAtom_" then
|
||||
local open_paren = duffle.skip_ws_and_cmt(source_text, ident_end)
|
||||
if source_text:sub(open_paren, open_paren) ~= "(" then
|
||||
pos = open_paren + 1
|
||||
else
|
||||
local inner, after_paren = duffle.read_parens(source_text, open_paren)
|
||||
-- scan: MipsAtom_(<name>)
|
||||
local name_start = 1
|
||||
while name_start <= #inner and inner:sub(name_start, name_start):match("[%s]") do name_start = name_start + 1 end
|
||||
local name_end = name_start
|
||||
while name_end <= #inner and inner:sub(name_end, name_end):match("[%w_]") do name_end = name_end + 1 end
|
||||
local atom_name = inner:sub(name_start, name_end - 1)
|
||||
local lookahead = duffle.skip_ws_and_cmt(source_text, after_paren)
|
||||
local look_ident, look_end = duffle.read_ident(source_text, lookahead)
|
||||
-- scan: MipsAtom_(<name>) <look_ident>
|
||||
if look_ident == "atom_info" then
|
||||
local info_open = duffle.skip_ws_and_cmt(source_text, look_end)
|
||||
if source_text:sub(info_open, info_open) == "(" then
|
||||
local info_inner, info_after = duffle.read_parens(source_text, info_open)
|
||||
-- scan: MipsAtom_(<name>) atom_info(<binds>, <reads>, <writes>)
|
||||
local binds, reads, writes = nil, nil, nil
|
||||
local sub_pos = 1
|
||||
while sub_pos <= #info_inner do
|
||||
sub_pos = duffle.skip_ws_and_cmt(info_inner, sub_pos); if sub_pos > #info_inner then break end
|
||||
local sub_ident, sub_end = duffle.read_ident(info_inner, sub_pos)
|
||||
if not sub_ident then
|
||||
sub_pos = sub_pos + 1
|
||||
elseif sub_ident == "atom_bind" then
|
||||
local sub_open = duffle.skip_ws_and_cmt(info_inner, sub_end)
|
||||
if info_inner:sub(sub_open, sub_open) == "(" then
|
||||
local sub_inner, sub_after2 = duffle.read_parens(info_inner, sub_open)
|
||||
-- scan: atom_bind(<Binds_X>)
|
||||
binds = duffle.trim(sub_inner)
|
||||
sub_pos = sub_after2
|
||||
else
|
||||
sub_pos = sub_open + 1
|
||||
end
|
||||
elseif sub_ident == "atom_reads" or sub_ident == "atom_writes" then
|
||||
local kind = sub_ident
|
||||
local sub_open = duffle.skip_ws_and_cmt(info_inner, sub_end)
|
||||
if info_inner:sub(sub_open, sub_open) == "(" then
|
||||
local sub_inner, sub_after2 = duffle.read_parens(info_inner, sub_open)
|
||||
-- scan: atom_reads(<regs>) OR atom_writes(<regs>)
|
||||
local regs = {}
|
||||
local sub_inner_pos = 1
|
||||
while sub_inner_pos <= #sub_inner do
|
||||
sub_inner_pos = duffle.skip_ws_and_cmt(sub_inner, sub_inner_pos); if sub_inner_pos > #sub_inner then break end
|
||||
local reg_ident, reg_end = duffle.read_ident(sub_inner, sub_inner_pos)
|
||||
if reg_ident then
|
||||
regs[#regs + 1] = duffle.trim(reg_ident)
|
||||
sub_inner_pos = reg_end
|
||||
else
|
||||
sub_inner_pos = sub_inner_pos + 1
|
||||
end
|
||||
if sub_inner_pos > #sub_inner then break end
|
||||
if sub_inner:sub(sub_inner_pos, sub_inner_pos) == "," then sub_inner_pos = sub_inner_pos + 1 end
|
||||
end
|
||||
if kind == "atom_reads" then reads = regs else writes = regs end
|
||||
sub_pos = sub_after2
|
||||
else
|
||||
sub_pos = sub_open + 1
|
||||
end
|
||||
else
|
||||
sub_pos = sub_end
|
||||
end
|
||||
end
|
||||
out[#out + 1] = {
|
||||
atom_name = atom_name, binds = binds,
|
||||
reads = reads or {}, writes = writes or {},
|
||||
info_line = line_of(lookahead),
|
||||
}
|
||||
pos = info_after
|
||||
else
|
||||
pos = info_open + 1
|
||||
end
|
||||
else
|
||||
pos = after_paren
|
||||
end
|
||||
end
|
||||
else
|
||||
pos = ident_end
|
||||
end
|
||||
end
|
||||
pos = duffle.skip_ws_and_cmt(source_text, pos)
|
||||
if pos > src_len then break end
|
||||
|
||||
local pp_pos = duffle.skip_preprocessor_line(source_text, pos)
|
||||
if pp_pos then pos = pp_pos; goto continue end
|
||||
|
||||
local ident, ident_end = duffle.read_ident(source_text, pos)
|
||||
-- scan: <ident>
|
||||
if not ident then pos = pos + 1; goto continue end
|
||||
if ident ~= "MipsAtom_" then pos = ident_end; goto continue end
|
||||
|
||||
local open_paren = duffle.skip_ws_and_cmt(source_text, ident_end)
|
||||
if source_text:sub(open_paren, open_paren) ~= "(" then pos = open_paren + 1; goto continue end
|
||||
|
||||
local inner, after_paren = duffle.read_parens(source_text, open_paren)
|
||||
-- scan: MipsAtom_(<name>)
|
||||
local name_start = 1
|
||||
while name_start <= #inner and inner:sub(name_start, name_start):match("[%s]") do name_start = name_start + 1 end
|
||||
local name_end = name_start
|
||||
while name_end <= #inner and inner:sub(name_end, name_end):match("[%w_]") do name_end = name_end + 1 end
|
||||
local atom_name = inner:sub(name_start, name_end - 1)
|
||||
|
||||
local lookahead = duffle.skip_ws_and_cmt(source_text, after_paren)
|
||||
local look_ident, look_end = duffle.read_ident(source_text, lookahead)
|
||||
-- scan: MipsAtom_(<name>) <look_ident>
|
||||
if look_ident ~= "atom_info" then pos = after_paren; goto continue end
|
||||
|
||||
local info_open = duffle.skip_ws_and_cmt(source_text, look_end)
|
||||
if source_text:sub(info_open, info_open) ~= "(" then pos = info_open + 1; goto continue end
|
||||
|
||||
local info_inner, info_after = duffle.read_parens(source_text, info_open)
|
||||
-- scan: MipsAtom_(<name>) atom_info(<binds>, <reads>, <writes>)
|
||||
local binds, reads, writes = parse_atom_info_subcalls(info_inner)
|
||||
out[#out + 1] = {
|
||||
atom_name = atom_name, binds = binds,
|
||||
reads = reads or {}, writes = writes or {},
|
||||
info_line = line_of(lookahead),
|
||||
}
|
||||
pos = info_after
|
||||
|
||||
::continue::
|
||||
end
|
||||
return out
|
||||
end
|
||||
|
||||
Reference in New Issue
Block a user