Corrections, flatting nested branches (lua metaprogram)

This commit is contained in:
2026-07-11 10:24:34 -04:00
parent 1ffad6cf98
commit 2b00956862
8 changed files with 571 additions and 549 deletions
+110 -73
View File
@@ -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,
+75 -85
View File
@@ -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
+27 -32
View File
@@ -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
+5 -3
View File
@@ -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
View File
@@ -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