From 2b009568623d5d63c10c3dfd3e5eb42b2149a19f Mon Sep 17 00:00:00 2001 From: Ed_ Date: Sat, 11 Jul 2026 10:24:34 -0400 Subject: [PATCH] Corrections, flatting nested branches (lua metaprogram) --- code/duffle/gen/duffle.offsets.h | 2 +- code/duffle/gp.h | 4 +- code/gte_hello/gen/gte_hello.offsets.h | 29 ++ scripts/duffle.lua | 183 ++++--- scripts/passes/annotation.lua | 160 +++--- scripts/passes/components.lua | 59 +-- scripts/passes/offsets.lua | 8 +- scripts/passes/static_analysis.lua | 675 ++++++++++++------------- 8 files changed, 571 insertions(+), 549 deletions(-) create mode 100644 code/gte_hello/gen/gte_hello.offsets.h diff --git a/code/duffle/gen/duffle.offsets.h b/code/duffle/gen/duffle.offsets.h index 12ccce8..eabc7da 100644 --- a/code/duffle/gen/duffle.offsets.h +++ b/code/duffle/gen/duffle.offsets.h @@ -1,5 +1,5 @@ // Auto-generated by ps1_meta.lua (passes/offsets.lua) — DO NOT EDIT -// Source: code\duffle\lottes_tape.h +// Source: C:\projects\Pikuma\ps1\code\duffle\lottes_tape.h #pragma once #pragma region lottes_tape diff --git a/code/duffle/gp.h b/code/duffle/gp.h index 24281a2..6fc1b66 100644 --- a/code/duffle/gp.h +++ b/code/duffle/gp.h @@ -419,7 +419,7 @@ typedef Struct_(Poly_F4) { }; }; -/* ---------- Poly_G3 (Gouraud Triangle; 6 words) ---------- */ +/* ---------- Poly_G3 (Gouraud Triangle; 7 words) ---------- */ typedef Struct_(Poly_G3) { U4 tag; RGB8 c0; B1 code; V2_S2 p0; RGB8 c1; B1 pad1; @@ -427,7 +427,7 @@ typedef Struct_(Poly_G3) { V2_S2 p2; }; -/* ---------- Poly_G4 (Gouraud Quad; 5 words in the demo's interleaved layout) ---------- */ +/* ---------- Poly_G4 (Gouraud Quad; 9 words) ---------- */ typedef Struct_(Poly_G4) { U4 tag; RGB8 c0; B1 code; V2_S2 p0; RGB8 c1; B1 pad1; diff --git a/code/gte_hello/gen/gte_hello.offsets.h b/code/gte_hello/gen/gte_hello.offsets.h new file mode 100644 index 0000000..e75de50 --- /dev/null +++ b/code/gte_hello/gen/gte_hello.offsets.h @@ -0,0 +1,29 @@ +// Auto-generated by ps1_meta.lua (passes/offsets.lua) — DO NOT EDIT +// Source: C:\projects\Pikuma\ps1\code\gte_hello\hello_gte_tape.c +#pragma once + +#pragma region hello_gte_tape + + +// --- atom: cube_g4_face (87 words) --- + +#define _atom_offset_cull_cube_g4_face_exit 48 +#define _atom_offset_bounds_chk_cube_g4_face_exit 12 + +enum { + atom_offset_cull_cube_g4_face_exit = _atom_offset_cull_cube_g4_face_exit, + atom_offset_bounds_chk_cube_g4_face_exit = _atom_offset_bounds_chk_cube_g4_face_exit, +}; + +// --- atom: floor_f3_face (66 words) --- + +#define _atom_offset_culling_floor_f3_face_exit 29 +#define _atom_offset_bounds_chk_floor_f3_face_exit 13 + +enum { + atom_offset_culling_floor_f3_face_exit = _atom_offset_culling_floor_f3_face_exit, + atom_offset_bounds_chk_floor_f3_face_exit = _atom_offset_bounds_chk_floor_f3_face_exit, +}; + +#pragma endregion hello_gte_tape + diff --git a/scripts/duffle.lua b/scripts/duffle.lua index 06dada7..5499385 100644 --- a/scripts/duffle.lua +++ b/scripts/duffle.lua @@ -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: #\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_, ...` 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_` 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_` 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_, ...` 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_` (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, diff --git a/scripts/passes/annotation.lua b/scripts/passes/annotation.lua index 23fbd70..8352a9c 100644 --- a/scripts/passes/annotation.lua +++ b/scripts/passes/annotation.lua @@ -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: - 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: - if not ident then - pos = pos + 1 - elseif ident == "typedef" then - -- scan: typedef Struct_() { } - 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_() { } + 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: - 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_() - 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_() { - if brace then - local _, after_brace = read_braces(source, brace) - -- scan: MipsAtom_() { } - 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_() + 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_() { + if brace then + local _, after_brace = read_braces(source, brace) + -- scan: MipsAtom_() { } + 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: - 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_() - local name, _ = read_alnum_ident(inner, 1) - - local entry, new_pos = parse_atom_info_call(source, name, after_paren, line_of) - -- scan: MipsAtom_() atom_info(, , ) - 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_() atom_info(...) { - if brace then - local _, after_brace = read_braces(source, brace) - -- scan: MipsAtom_() atom_info(...) { } - 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: + 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_() + local name, _ = read_alnum_ident(inner, 1) + + local entry, new_pos = parse_atom_info_call(source, name, after_paren, line_of) + -- scan: MipsAtom_() atom_info(, , ) + 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_() atom_info(...) { + if brace then + local _, after_brace = read_braces(source, brace) + -- scan: MipsAtom_() atom_info(...) { } + pos = after_brace + end + + ::continue:: end return annots end diff --git a/scripts/passes/components.lua b/scripts/passes/components.lua index c6ac104..30f7912 100644 --- a/scripts/passes/components.lua +++ b/scripts/passes/components.lua @@ -455,40 +455,35 @@ local function find_component_atoms(source) local ident, after_ident = duffle.read_ident(source, pos) -- scan: 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: () + local name, body = parse_atomcomp_inner(inner) + -- scan: () OR (, { }) + 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: () { + local comp, new_pos = make_bare_component(source, name, pos, after_paren, line_of, args, comment) + -- scan: () { } + 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: () - local name, body = parse_atomcomp_inner(inner) - -- scan: () OR (, { }) - 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: () { - local comp, new_pos = make_bare_component(source, name, pos, after_paren, line_of, args, comment) - -- scan: () { } - if comp then out[#out + 1] = comp end - pos = new_pos - else - -- Function form: body was inside the parens. - -- scan: (, { }) - 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: (, { }) + out[#out + 1] = make_proc_component(name, body, pos, line_of, args, comment) + pos = after_paren end + + ::continue:: end return out end diff --git a/scripts/passes/offsets.lua b/scripts/passes/offsets.lua index 2e9022c..2eaae2e 100644 --- a/scripts/passes/offsets.lua +++ b/scripts/passes/offsets.lua @@ -335,9 +335,9 @@ local function find_atoms(source_text) local ident, after = duffle.read_ident(source_text, pos) -- scan: - 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_() { } 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 diff --git a/scripts/passes/static_analysis.lua b/scripts/passes/static_analysis.lua index bfd72ac..3838926 100644 --- a/scripts/passes/static_analysis.lua +++ b/scripts/passes/static_analysis.lua @@ -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: (, { }) + 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: () + if name == "" then return nil, open_paren + 1 end + local brace = duffle.scan_to_char(source_text, "{", after_paren) + -- scan: () { + if not brace then return nil, open_paren + 1 end + local body, after_brace = duffle.read_braces(source_text, brace) + -- scan: () { } + 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: 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: () - - 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: (, { }) - -- 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: () - if name == "" then - pos = open_paren + 1 - else - local brace = duffle.scan_to_char(source_text, "{", after_paren) - -- scan: () { - if brace then - local body, after_brace = duffle.read_braces(source_text, brace) - -- scan: () { } - 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: () + + 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, -> 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: ... -> 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_ 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_` 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_` 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_ 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: - 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 - 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_() - local name = duffle.trim(inner) - local brace = duffle.scan_to_char(source_text, "{", after_paren) - -- scan: typedef Struct_() { - if not brace then - pos = open_paren + 1 - else - local body, after_brace = duffle.read_braces(source_text, brace) - -- scan: typedef Struct_() { } - 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: + 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 + 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_() + local name = duffle.trim(inner) + local brace = duffle.scan_to_char(source_text, "{", after_paren) + -- scan: typedef Struct_() { + if not brace then pos = open_paren + 1; goto continue end + + local body, after_brace = duffle.read_braces(source_text, brace) + -- scan: typedef Struct_() { } + 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 = 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() OR atom_writes() + 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: - 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_() - 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_() - 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_() atom_info(, , ) - 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 = 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() OR atom_writes() - 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: + 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_() + 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_() + 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_() atom_info(, , ) + 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