diff --git a/code/duffle/gen/duffle.macs.h b/code/duffle/gen/duffle.macs.h
index 043c81c..8d24ce4 100644
--- a/code/duffle/gen/duffle.macs.h
+++ b/code/duffle/gen/duffle.macs.h
@@ -50,14 +50,32 @@ WORD_COUNT(mac_load_tri_verts, 18)
/* Words: 11; Correctly inserts a primitive into the Ordering Table linked list.
* Hardcoded for Poly_F3 (5 words). For Poly_G4, use ac_insert_ot_tag_g4. */
#define mac_insert_ot_tag_f3(...) \
- shift_lright(R_AT, R_AT, S_(polytag_len_bits))
-WORD_COUNT(mac_insert_ot_tag_f3, 1)
+ shift_lleft( R_T1, R_T1, S_(U4)/2) /* T1 = otz * S_(U4) (otz arg is implicit R_T1) */ \
+, add_u_self( R_T1, R_OtBase) /* T1 = & OrderingTable[OTZ] */ \
+, load_word( R_AT, R_T1, O_(PolyTag,bf_addr_len)) /* AT = old_ot_head */ \
+, load_upper_i(R_V0, (S_(Poly_F3)/S_(U4) - S_(PolyTag)/S_(U4)) << polytag_len_bits) /* V0 = (5 - 1) << 24 = 4 << 24 */ \
+, mask_upper( R_AT, R_AT, S_(polytag_len_bits)) /* Strip upper 8 bits (length from prev cell) → keep only low 24 */ \
+, or_u( R_AT, R_AT, R_V0) /* Merge length */ \
+, store_word( R_AT, R_PrimCursor, O_(PolyTag,bf_addr_len)) /* prim->tag = packed(prim_length, old_addr) */ \
+, shift_lleft( R_AT, R_PrimCursor, S_(polytag_len_bits)) /* AT = (prim_length << 24) | old_addr */ \
+, shift_lright(R_AT, R_AT, S_(polytag_len_bits)) \
+, store_word( R_AT, R_T1, O_(PolyTag,bf_addr_len)) /* OrderingTable[OTZ] = PrimCursor */
+WORD_COUNT(mac_insert_ot_tag_f3, 10)
/* Words: 11; Correctly inserts a primitive into the Ordering Table linked list.
* Hardcoded for Poly_G4 (9 words). For Poly_F3, use ac_insert_ot_tag_f3. */
#define mac_insert_ot_tag_g4(...) \
- shift_lright(R_AT, R_AT, S_(polytag_len_bits))
-WORD_COUNT(mac_insert_ot_tag_g4, 1)
+ shift_lleft( R_T1, R_T1, S_(U4)/2) /* T1 = otz * S_(U4) (otz arg is implicit R_T1) */ \
+, add_u_self( R_T1, R_OtBase) /* T1 = & OrderingTable[OTZ] */ \
+, load_word( R_AT, R_T1, O_(PolyTag,bf_addr_len)) /* AT = old_ot_head */ \
+, load_upper_i(R_V0, (S_(Poly_G4)/S_(U4) - S_(PolyTag)/S_(U4)) << polytag_len_bits) /* V0 = (9 - 1) << 24 = 8 << 24 */ \
+, mask_upper( R_AT, R_AT, S_(polytag_len_bits)) /* Strip upper 8 bits (length from prev cell) → keep only low 24 */ \
+, or_u( R_AT, R_AT, R_V0) /* Merge length */ \
+, store_word( R_AT, R_PrimCursor, O_(PolyTag,bf_addr_len)) /* prim->tag = packed(prim_length, old_addr) */ \
+, shift_lleft( R_AT, R_PrimCursor, S_(polytag_len_bits)) /* AT = (prim_length << 24) | old_addr */ \
+, shift_lright(R_AT, R_AT, S_(polytag_len_bits)) \
+, store_word( R_AT, R_T1, O_(PolyTag,bf_addr_len)) /* OrderingTable[OTZ] = PrimCursor */
+WORD_COUNT(mac_insert_ot_tag_g4, 10)
#define mac_pack_color_word(off, code, r, g, b) \
load_upper_i(R_AT, (code) << 8 | (b)) \
@@ -70,9 +88,7 @@ WORD_COUNT(mac_pack_color_word, 3)
WORD_COUNT(mac_format_f3_color, 1)
/* Words: 3; Stores the 3 transformed (V2_S2 screen) vertices to the F3.
- * PIPELINE: post-RTPT (SXY0=v0.screen, SXY1=v1.screen, SXY2=v2.screen).
- * The macro name declares the pipeline position; check #6 (GTE state-
- * machine validation) verifies the call site matches the declaration. */
+ * PIPELINE: post-RTPT (SXY0=v0.screen, SXY1=v1.screen, SXY2=v2.screen). */
#define mac_gte_store_f3_post_rtpt(...) \
gte_sw(C2_SXY0, R_PrimCursor, O_(Poly_F3,p0)) \
, gte_sw(C2_SXY1, R_PrimCursor, O_(Poly_F3,p1)) \
@@ -105,14 +121,7 @@ WORD_COUNT(mac_gte_store_g4_p012_post_rtpt_pre_rtps, 3)
* single-vertex result to SXY2; SXY0 still holds v0.screen from the
* earlier RTPT — DO NOT read SXY0 here, that's the bug this name
* prevents).
- * The macro name declares the pipeline position; check #6 (GTE state-
- * machine validation) verifies the call site matches the declaration.
- *
- * History: this macro was named `mac_gte_store_g4_p3` until 2026-07-09
- * when it was discovered to be reading C2_SXY0 (which held v0.screen)
- * instead of C2_SXY2 (which holds v3.screen after RTPS). The rename
- * encodes the pipeline position in the name so the next bug of this
- * class is impossible. */
+ */
#define mac_gte_store_g4_p3_post_rtps(...) \
gte_sw(C2_SXY2, R_PrimCursor, O_(Poly_G4,p3))
WORD_COUNT(mac_gte_store_g4_p3_post_rtps, 1)
diff --git a/code/duffle/lottes_tape.h b/code/duffle/lottes_tape.h
index f44f08b..ce0616e 100644
--- a/code/duffle/lottes_tape.h
+++ b/code/duffle/lottes_tape.h
@@ -28,9 +28,8 @@ typedef Slice_MipsCode MipsAtom;
// FI_ MipsAtom ac_X(args) { MipsCode ac_X[] align_(4) = { body }; return slice_from_array(MipsCode, ac_X); }
#define MipsAtomComp_Proc_(sym, ...) { MipsCode sym [] align_(4) = __VA_ARGS__; return slice_from_array(MipsCode, sym); }
-// Auto-generated component macros (build/gen/
/.components.h)
+// Auto-generated component macros (/gen//.macs.h)
// are included manually by the unity build. The metaprogram puts them
-// under ./build/gen/ (not in the code dirs) so the source tree stays clean.
/* Register aliases (moved up from the Tape Drive region below so that
* mac_yield's body and the Mips Atom Builder functions can reference
@@ -137,9 +136,9 @@ MipsAtomComp_(ac_load_tri_verts) {
/* Words: 11; Correctly inserts a primitive into the Ordering Table linked list.
* Hardcoded for Poly_F3 (5 words). For Poly_G4, use ac_insert_ot_tag_g4. */
MipsAtomComp_(ac_insert_ot_tag_f3) {
- shift_lleft( R_T1, R_T1, S_(U4)/2) /* T1 = otz * S_(U4) (otz arg is implicit R_T1) */
- , add_u_self( R_T1, R_OtBase) /* T1 = & OrderingTable[OTZ] */
- , load_word( R_AT, R_T1, O_(PolyTag,bf_addr_len)) /* AT = old_ot_head */
+ shift_lleft( R_T1, R_T1, S_(U4)/2) // T1 = otz * S_(U4) (otz arg is implicit R_T1)
+ , add_u_self( R_T1, R_OtBase) // T1 = & OrderingTable[OTZ]
+ , load_word( R_AT, R_T1, O_(PolyTag,bf_addr_len)) // AT = old_ot_head
, load_upper_i(R_V0, (S_(Poly_F3)/S_(U4) - S_(PolyTag)/S_(U4)) << polytag_len_bits) /* V0 = (5 - 1) << 24 = 4 << 24 */
, mask_upper( R_AT, R_AT, S_(polytag_len_bits)) /* Strip upper 8 bits (length from prev cell) → keep only low 24 */
, or_u( R_AT, R_AT, R_V0) /* Merge length */
@@ -177,17 +176,14 @@ MipsAtomComp_Proc_(ac_pack_color_word, {
/* Words: 3; Emits the F3 command+color word (cmd byte | BLUE | GREEN | RED)
* Args: _r, _g, _b are 8-bit RGB byte values (not raw 16-bit fields).
- * Migrated from hello_gte_tape.c; takes RGB form per the Phase 3
- * convention. */
+ * Migrated from hello_gte_tape.c; takes RGB form per the Phase 3 convention. */
FI_ MipsAtom ac_format_f3_color(U1 r, U1 g, U1 b)
MipsAtomComp_Proc_(ac_format_f3_color, {
mac_pack_color_word(O_(Poly_F3,color), gp0_cmd_poly_f3, r, g, b)
})
/* Words: 3; Stores the 3 transformed (V2_S2 screen) vertices to the F3.
- * PIPELINE: post-RTPT (SXY0=v0.screen, SXY1=v1.screen, SXY2=v2.screen).
- * The macro name declares the pipeline position; check #6 (GTE state-
- * machine validation) verifies the call site matches the declaration. */
+ * PIPELINE: post-RTPT (SXY0=v0.screen, SXY1=v1.screen, SXY2=v2.screen). */
MipsAtomComp_(ac_gte_store_f3_post_rtpt) {
gte_sw(C2_SXY0, R_PrimCursor, O_(Poly_F3,p0))
, gte_sw(C2_SXY1, R_PrimCursor, O_(Poly_F3,p1))
@@ -196,10 +192,11 @@ MipsAtomComp_(ac_gte_store_f3_post_rtpt) {
/* Words: 12; Emits the four (code|color) words of a Poly_G4.
* Args: rN,gN,bN are 8-bit RGB byte values for each of the 4 vertices. */
-FI_ MipsAtom ac_format_g4_color(U1 r0, U1 g0, U1 b0,
- U1 r1, U1 g1, U1 b1,
- U1 r2, U1 g2, U1 b2,
- U1 r3, U1 g3, U1 b3)
+FI_ MipsAtom ac_format_g4_color(
+ U1 r0, U1 g0, U1 b0,
+ U1 r1, U1 g1, U1 b1,
+ U1 r2, U1 g2, U1 b2,
+ U1 r3, U1 g3, U1 b3)
MipsAtomComp_Proc_(ac_format_g4_color, {
mac_pack_color_word(O_(Poly_G4,c0), gp0_cmd_poly_g4, r0,g0,b0)
, mac_pack_color_word(O_(Poly_G4,c1), 0, r1,g1,b1)
@@ -226,14 +223,7 @@ MipsAtomComp_(ac_gte_store_g4_p012_post_rtpt_pre_rtps) {
* single-vertex result to SXY2; SXY0 still holds v0.screen from the
* earlier RTPT — DO NOT read SXY0 here, that's the bug this name
* prevents).
- * The macro name declares the pipeline position; check #6 (GTE state-
- * machine validation) verifies the call site matches the declaration.
- *
- * History: this macro was named `mac_gte_store_g4_p3` until 2026-07-09
- * when it was discovered to be reading C2_SXY0 (which held v0.screen)
- * instead of C2_SXY2 (which holds v3.screen after RTPS). The rename
- * encodes the pipeline position in the name so the next bug of this
- * class is impossible. */
+ */
MipsAtomComp_(ac_gte_store_g4_p3_post_rtps) {
gte_sw(C2_SXY2, R_PrimCursor, O_(Poly_G4,p3))
};
@@ -277,26 +267,25 @@ enum {
};
/* Flushes the Instruction Cache (PSX A-function 0x44 via BIOS stub at 0xA0).
- *
* Sequence (per MIPS ABI; arguments in arg registers, RA pushed to stack):
* 1. sp -= 8; sw $ra, 4($sp) ; save RA
* 2. $a0 = bios_flushcache (arg0)
* 3. $t0 = bios_table_addr ; t0 = &BIOS A-function table
- * 4. jalr $t0, $ra ; call BIOS(flushcache)
- * nop ; branch delay slot
- * 5. lw $ra, 4($sp); jr $ra ; restore & return
+ * 4. jalr $t0, $ra ; call BIOS(flushcache)
+ * nop ; branch delay slot
+ * 5. lw $ra, 4($sp); jr $ra ; restore & return
* 6. sp += 8
*/
internal MipsAtom_(mips_flush_icache) {
- add_ui(rstack_ptr, rstack_ptr, -MipsStackAlignment) /* sp -= 8 */
- , store_word(rret_addr, rstack_ptr, S_(U4)) /* sw $ra, 4($sp) */
- , add_ui(rret_0, rdiscard, bios_flushcache) /* addiu $a0, $0, 0x44 */
- , add_ui(rtmp_0, rdiscard, bios_table_addr) /* addiu $t0, $0, 0xA0 */
- , jump_link(rtmp_0, rret_addr) /* jalr $t0, $ra */
- , nop /* BD slot */
- , load_word(rret_addr, rstack_ptr, S_(U4)) /* lw $ra, 4($sp) */
- , jump_reg(rret_addr) /* jr $ra */
- , add_ui(rstack_ptr, rstack_ptr, MipsStackAlignment) /* sp += 8 (BD) */
+ add_ui(rstack_ptr, rstack_ptr, -MipsStackAlignment) // sp -= 8
+ , store_word(rret_addr, rstack_ptr, S_(U4)) // sw $ra, 4($sp)
+ , add_ui(rret_0, rdiscard, bios_flushcache) // addiu $a0, $0, 0x44
+ , add_ui(rtmp_0, rdiscard, bios_table_addr) // addiu $t0, $0, 0xA0
+ , jump_link(rtmp_0, rret_addr) // jalr $t0, $ra
+ , nop // BD slot
+ , load_word(rret_addr, rstack_ptr, S_(U4)) // lw $ra, 4($sp)
+ , jump_reg(rret_addr) // jr $ra
+ , add_ui(rstack_ptr, rstack_ptr, MipsStackAlignment) // sp += 8 (BD)
, mac_yield()
};
diff --git a/code/gte_hello/gen/gte_hello.offsets.h b/code/gte_hello/gen/gte_hello.offsets.h
index 669ec58..7415184 100644
--- a/code/gte_hello/gen/gte_hello.offsets.h
+++ b/code/gte_hello/gen/gte_hello.offsets.h
@@ -5,9 +5,9 @@
#pragma region hello_gte_tape
-// --- atom: cube_g4_face (44 words) ---
+// --- atom: cube_g4_face (42 words) ---
-#define _atom_offset_cull_cube_g4_face_exit 25
+#define _atom_offset_cull_cube_g4_face_exit 26
#define _atom_offset_bounds_chk_cube_g4_face_exit 2
enum {
@@ -15,9 +15,9 @@ enum {
atom_offset_bounds_chk_cube_g4_face_exit = _atom_offset_bounds_chk_cube_g4_face_exit,
};
-// --- atom: floor_f3_face (30 words) ---
+// --- atom: floor_f3_face (47 words) ---
-#define _atom_offset_culling_floor_f3_face_exit 15
+#define _atom_offset_culling_floor_f3_face_exit 14
#define _atom_offset_bounds_chk_floor_f3_face_exit 3
enum {
diff --git a/code/gte_hello/hello_gte.c b/code/gte_hello/hello_gte.c
index c8fce37..75aa462 100644
--- a/code/gte_hello/hello_gte.c
+++ b/code/gte_hello/hello_gte.c
@@ -22,6 +22,7 @@
# include "tape_atom.metadata.h"
# include "gen/gte_hello.offsets.h"
#include "hello_gte.h"
+
#include "hello_gte_tape.c"
typedef U4 OrderingTable_Buffer[OrderingTbl_Len];
diff --git a/code/gte_hello/hello_gte_tape.c b/code/gte_hello/hello_gte_tape.c
index f8e4aff..07628b5 100644
--- a/code/gte_hello/hello_gte_tape.c
+++ b/code/gte_hello/hello_gte_tape.c
@@ -4,6 +4,7 @@
# include "duffle/atom_dsl.h"
# include "duffle/lottes_tape.h"
# include "tape_atom.metadata.h"
+# include "gen/gte_hello.offsets.h"
# include "hello_gte.h"
#endif
diff --git a/scripts/duffle.lua b/scripts/duffle.lua
index 717511b..5799c10 100644
--- a/scripts/duffle.lua
+++ b/scripts/duffle.lua
@@ -345,7 +345,10 @@ function M.split_top_level_commas(body)
token_start = i
else
local nx = M.skip_str_or_cmt(body, i)
- if nx > i then i = nx; token_start = nx else i = i + 1 end
+ -- Don't advance token_start on skip: a trailing `/* ... */` after
+ -- a macro call is part of the current entry's chunk, not the
+ -- start of the next one.
+ if nx > i then i = nx else i = i + 1 end
end
end
local last = body:sub(token_start)
diff --git a/scripts/tape_atom.offset_gen.meta.lua b/scripts/tape_atom.offset_gen.meta.lua
index e0cfbd1..eb18aa9 100644
--- a/scripts/tape_atom.offset_gen.meta.lua
+++ b/scripts/tape_atom.offset_gen.meta.lua
@@ -427,12 +427,34 @@ end
-- ============================================================
local function main(args)
- if #args < 2 then
- print("Usage: luajit gen_atom_offsets.lua [source2 ...]")
- os.exit(1)
- end
- local word_counts = load_word_counts(args[1])
- for i = 2, #args do process_source(args[i], word_counts) end
+ if #args < 2 then
+ print("Usage: luajit gen_atom_offsets.lua [source2 ...]")
+ os.exit(1)
+ end
+ local word_counts = load_word_counts(args[1])
+ for i = 2, #args do
+ local source_path = args[i]
+ -- Also load the auto-generated .macs.h for this source's
+ -- directory (if it exists). The metaprogram emits
+ -- WORD_COUNT(mac_X, N) entries there for the MipsAtomComp_
+ -- declarations in the source. Merging ensures the offset
+ -- computation has the right word counts (no "unknown macro"
+ -- warnings for the auto-generated components).
+ local source_dir = dirname(source_path)
+ -- dirname() keeps the trailing separator; basename_no_ext()
+ -- on a path with a trailing separator returns "". Strip it.
+ if #source_dir > 0 and (source_dir:sub(-1) == "/" or source_dir:sub(-1) == "\\") then
+ source_dir = source_dir:sub(1, -2)
+ end
+ local macs_path = source_dir .. "/gen/" .. basename_no_ext(source_dir) .. ".macs.h"
+ local ok, mc = pcall(load_word_counts, macs_path)
+ if ok then
+ for name, count in pairs(mc) do
+ word_counts[name] = count
+ end
+ end
+ process_source(source_path, word_counts)
+ end
end
main({...})
\ No newline at end of file
diff --git a/scripts/tape_atom_annotation_pass.lua b/scripts/tape_atom_annotation_pass.lua
index 26d6147..f411b52 100644
--- a/scripts/tape_atom_annotation_pass.lua
+++ b/scripts/tape_atom_annotation_pass.lua
@@ -36,8 +36,8 @@
local script_path = arg[0]
local last_sep = 0
for i = 1, #script_path do
- local c = script_path:sub(i, i)
- if c == "/" or c == "\\" then last_sep = i end
+ local c = script_path:sub(i, i)
+ if c == "/" or c == "\\" then last_sep = i end
end
local script_dir = last_sep == 0 and "./" or script_path:sub(1, last_sep)
package.path = script_dir .. "?.lua;" .. script_dir .. "?/init.lua;" .. package.path
@@ -87,48 +87,48 @@ local function is_wave_context_reg(n) return WAVE_CONTEXT_REGS[n] ~= nil end
-- Split a string at top-level commas. Used inside TAPE_ATOM_* macro
-- bodies where nested parens/braces/brackets are possible.
local function split_csv_top(s)
- local tokens = {}
- local i, start = 1, 1
- local depth = 0
- while i <= #s do
- local c = s:sub(i, i)
- if c == "(" or c == "{" or c == "[" then
- depth = depth + 1
- i = i + 1
- elseif c == ")" or c == "}" or c == "]" then
- depth = depth - 1
- i = i + 1
- elseif c == "," and depth == 0 then
- tokens[#tokens + 1] = s:sub(start, i - 1)
- i = i + 1
- start = i
- else
- i = i + 1
- end
- end
- local last = s:sub(start)
- if trim(last) ~= "" then tokens[#tokens + 1] = last end
- return tokens
+ local tokens = {}
+ local i, start = 1, 1
+ local depth = 0
+ while i <= #s do
+ local c = s:sub(i, i)
+ if c == "(" or c == "{" or c == "[" then
+ depth = depth + 1
+ i = i + 1
+ elseif c == ")" or c == "}" or c == "]" then
+ depth = depth - 1
+ i = i + 1
+ elseif c == "," and depth == 0 then
+ tokens[#tokens + 1] = s:sub(start, i - 1)
+ i = i + 1
+ start = i
+ else
+ i = i + 1
+ end
+ end
+ local last = s:sub(start)
+ if trim(last) ~= "" then tokens[#tokens + 1] = last end
+ return tokens
end
-- Split a string into whitespace-separated tokens.
-- The original used :gmatch("%S+"); replaced here with a hand-rolled
-- loop (faster + no regex).
local function split_ws(s)
- local tokens = {}
- local i, n = 1, 1
- local len = #s
- while i <= len do
- -- Skip whitespace.
- while i <= len and is_space(s:sub(i, i)) do i = i + 1 end
- if i > len then break end
- local start = i
- -- Take non-whitespace run.
- while i <= len and not is_space(s:sub(i, i)) do i = i + 1 end
- tokens[n] = s:sub(start, i - 1)
- n = n + 1
- end
- return tokens
+ local tokens = {}
+ local i, n = 1, 1
+ local len = #s
+ while i <= len do
+ -- Skip whitespace.
+ while i <= len and is_space(s:sub(i, i)) do i = i + 1 end
+ if i > len then break end
+ local start = i
+ -- Take non-whitespace run.
+ while i <= len and not is_space(s:sub(i, i)) do i = i + 1 end
+ tokens[n] = s:sub(start, i - 1)
+ n = n + 1
+ end
+ return tokens
end
-- ============================================================
@@ -152,50 +152,50 @@ end
-- "atom_writes"-- an atom_writes(...) call: value is the register list
-- "other" -- something we can't classify (preserved as text)
local function parse_atom_annot_args(inner)
- -- Split at top-level commas, respecting nested parens.
- local args = {}
- local tokens = split_csv_top(inner)
- for _, tok in ipairs(tokens) do
- local s = trim(tok)
- if s ~= "" then
- -- Detect register-list calls: atom_reads(...) / atom_writes(...) / tape_regs(...)
- local regs_kind = nil
- local regs_inner = nil
- if s:sub(-1) == ")" then
- if s:sub(1, 11) == "atom_reads(" then
- regs_kind = "atom_reads"
- regs_inner = s:sub(12, -2)
- elseif s:sub(1, 12) == "atom_writes(" then
- regs_kind = "atom_writes"
- regs_inner = s:sub(13, -2)
- end
- end
+ -- Split at top-level commas, respecting nested parens.
+ local args = {}
+ local tokens = split_csv_top(inner)
+ for _, tok in ipairs(tokens) do
+ local s = trim(tok)
+ if s ~= "" then
+ -- Detect register-list calls: atom_reads(...) / atom_writes(...) / tape_regs(...)
+ local regs_kind = nil
+ local regs_inner = nil
+ if s:sub(-1) == ")" then
+ if s:sub(1, 11) == "atom_reads(" then
+ regs_kind = "atom_reads"
+ regs_inner = s:sub(12, -2)
+ elseif s:sub(1, 12) == "atom_writes(" then
+ regs_kind = "atom_writes"
+ regs_inner = s:sub(13, -2)
+ end
+ end
- if regs_kind then
- local regs = {}
- for _, r in ipairs(split_csv_top(regs_inner)) do
- local trimmed = trim(r)
- if trimmed ~= "" then regs[#regs + 1] = trimmed end
- end
- -- Resolve any phase_* / R_* alias macros
- for i, r in ipairs(regs) do
- if MACRO_EXPANSION[r] then regs[i] = MACRO_EXPANSION[r] end
- end
- args[#args + 1] = {kind = regs_kind, value = regs}
- else
- -- Bare identifier (e.g. phase_work)
- local id, _ = read_ident(s, 1)
- if id and trim(s) == id then
- local v = id
- if MACRO_EXPANSION[v] then v = MACRO_EXPANSION[v] end
- args[#args + 1] = {kind = "ident", value = v}
- else
- args[#args + 1] = {kind = "other", value = s}
- end
- end
- end
- end
- return args
+ if regs_kind then
+ local regs = {}
+ for _, r in ipairs(split_csv_top(regs_inner)) do
+ local trimmed = trim(r)
+ if trimmed ~= "" then regs[#regs + 1] = trimmed end
+ end
+ -- Resolve any phase_* / R_* alias macros
+ for i, r in ipairs(regs) do
+ if MACRO_EXPANSION[r] then regs[i] = MACRO_EXPANSION[r] end
+ end
+ args[#args + 1] = {kind = regs_kind, value = regs}
+ else
+ -- Bare identifier (e.g. phase_work)
+ local id, _ = read_ident(s, 1)
+ if id and trim(s) == id then
+ local v = id
+ if MACRO_EXPANSION[v] then v = MACRO_EXPANSION[v] end
+ args[#args + 1] = {kind = "ident", value = v}
+ else
+ args[#args + 1] = {kind = "other", value = s}
+ end
+ end
+ end
+ end
+ return args
end
-- ============================================================
@@ -209,89 +209,89 @@ end
-- ============================================================
local function find_macro_word_annotations(source)
- local line_of = duffle.LineIndex(source)
- local out = {}
- local len = #source
- local i = 1
- while i <= len do
- i = skip_ws_and_cmt(source, i); if i > len then break end
- -- Skip preprocessor directives (lines starting with #).
- -- read_ident doesn't recognize '#' so without this guard we'd
- -- infinite-loop on `#ifdef` / `#pragma region` / etc.
- if source:sub(i, i) == "#" then
- local j = i
- while j <= len and source:sub(j, j) ~= "\n" do j = j + 1 end
- i = j + 1
- else
- local ident, after = read_ident(source, i)
- if not ident then
- i = i + 1
- elseif ident == "_Pragma" then
- local open = skip_ws_and_cmt(source, after)
- if source:sub(open, open) == "(" then
- local str, str_end = read_parens(source, open)
- -- str is a parenthesized string literal; strip parens, then quotes
- str = trim(str)
- if str:sub(1, 1) == '"' and str:sub(-1) == '"' then
- local inner = str:sub(2, -2)
- -- Expect " tape_atom words="
- local space = find_byte(inner, " ", 1)
- if space then
- local name = inner:sub(1, space - 1)
- local rest = inner:sub(space + 1)
- local eq = find_byte(rest, "=", 1)
- if eq then
- local key = trim(rest:sub(1, eq - 1))
- local val = trim(rest:sub(eq + 1))
- if key == "tape_atom words" then
- -- key is "tape_atom words"; val is ""
- local n = tonumber(val) or 0
- out[#out + 1] = {
- line = line_of(i),
- name = name,
- words = n,
- }
- elseif key == "words" then
- -- Tolerate "mac_X words=N" if someone writes it that way
- local n = tonumber(val) or 0
- out[#out + 1] = {
- line = line_of(i),
- name = name,
- words = n,
- }
- end
- end
- end
- end
- i = str_end
- else
- i = open + 1
- end
- elseif ident == "pragma" then
- -- Directive form: `#pragma mac_X tape_atom words=N`
- local rest_start = skip_ws_and_cmt(source, after)
- -- Read the rest of the line (no #pragma content spans lines)
- local j = rest_start
- while j <= len and source:sub(j, j) ~= "\n" do j = j + 1 end
- local line_text = trim(source:sub(rest_start, j - 1))
- -- tokenize (no :gmatch; use hand-rolled split_ws)
- local tokens = split_ws(line_text)
- if #tokens >= 3 and tokens[2] == "tape_atom" and tokens[3]:sub(1, 6) == "words=" then
- local name = tokens[1]
- local n = tonumber(tokens[3]:sub(7)) or 0
- out[#out + 1] = { line = line_of(i), name = name, words = n }
- elseif #tokens >= 2 and tokens[2]:sub(1, 6) == "words=" then
- local name = tokens[1]
- local n = tonumber(tokens[2]:sub(7)) or 0
- out[#out + 1] = { line = line_of(i), name = name, words = n }
- end
- i = j
- else
- i = after
- end
- end
- end
- return out
+ local line_of = duffle.LineIndex(source)
+ local out = {}
+ local len = #source
+ local i = 1
+ while i <= len do
+ i = skip_ws_and_cmt(source, i); if i > len then break end
+ -- Skip preprocessor directives (lines starting with #).
+ -- read_ident doesn't recognize '#' so without this guard we'd
+ -- infinite-loop on `#ifdef` / `#pragma region` / etc.
+ if source:sub(i, i) == "#" then
+ local j = i
+ while j <= len and source:sub(j, j) ~= "\n" do j = j + 1 end
+ i = j + 1
+ else
+ local ident, after = read_ident(source, i)
+ if not ident then
+ i = i + 1
+ elseif ident == "_Pragma" then
+ local open = skip_ws_and_cmt(source, after)
+ if source:sub(open, open) == "(" then
+ local str, str_end = read_parens(source, open)
+ -- str is a parenthesized string literal; strip parens, then quotes
+ str = trim(str)
+ if str:sub(1, 1) == '"' and str:sub(-1) == '"' then
+ local inner = str:sub(2, -2)
+ -- Expect " tape_atom words="
+ local space = find_byte(inner, " ", 1)
+ if space then
+ local name = inner:sub(1, space - 1)
+ local rest = inner:sub(space + 1)
+ local eq = find_byte(rest, "=", 1)
+ if eq then
+ local key = trim(rest:sub(1, eq - 1))
+ local val = trim(rest:sub(eq + 1))
+ if key == "tape_atom words" then
+ -- key is "tape_atom words"; val is ""
+ local n = tonumber(val) or 0
+ out[#out + 1] = {
+ line = line_of(i),
+ name = name,
+ words = n,
+ }
+ elseif key == "words" then
+ -- Tolerate "mac_X words=N" if someone writes it that way
+ local n = tonumber(val) or 0
+ out[#out + 1] = {
+ line = line_of(i),
+ name = name,
+ words = n,
+ }
+ end
+ end
+ end
+ end
+ i = str_end
+ else
+ i = open + 1
+ end
+ elseif ident == "pragma" then
+ -- Directive form: `#pragma mac_X tape_atom words=N`
+ local rest_start = skip_ws_and_cmt(source, after)
+ -- Read the rest of the line (no #pragma content spans lines)
+ local j = rest_start
+ while j <= len and source:sub(j, j) ~= "\n" do j = j + 1 end
+ local line_text = trim(source:sub(rest_start, j - 1))
+ -- tokenize (no :gmatch; use hand-rolled split_ws)
+ local tokens = split_ws(line_text)
+ if #tokens >= 3 and tokens[2] == "tape_atom" and tokens[3]:sub(1, 6) == "words=" then
+ local name = tokens[1]
+ local n = tonumber(tokens[3]:sub(7)) or 0
+ out[#out + 1] = { line = line_of(i), name = name, words = n }
+ elseif #tokens >= 2 and tokens[2]:sub(1, 6) == "words=" then
+ local name = tokens[1]
+ local n = tonumber(tokens[2]:sub(7)) or 0
+ out[#out + 1] = { line = line_of(i), name = name, words = n }
+ end
+ i = j
+ else
+ i = after
+ end
+ end
+ end
+ return out
end
-- ============================================================
@@ -324,139 +324,139 @@ end
-- Map: macro name → pragma key
local ATOM_ATTR_MACROS = {
- ["atom_resource"] = "resource",
- ["atom_region"] = "region",
- ["atom_group"] = "group",
- ["atom_cadence"] = "cadence",
- ["atom_async"] = "async",
+ ["atom_resource"] = "resource",
+ ["atom_region"] = "region",
+ ["atom_group"] = "group",
+ ["atom_cadence"] = "cadence",
+ ["atom_async"] = "async",
}
-- Parse macro form: `atom_(atom_name, value, ...)`.
-- Returns (true, entry, str_end) on success, (false) on no match.
-- The entry has shape { line, name, attrs = {key = value} }.
local function try_parse_atom_attr_macro(source, i, line_of)
- local ident, after = read_ident(source, i)
- if not ident then return false end
- local key = ATOM_ATTR_MACROS[ident]
- if not key then return false end
- local open = skip_ws_and_cmt(source, after)
- if source:sub(open, open) ~= "(" then return false end
+ local ident, after = read_ident(source, i)
+ if not ident then return false end
+ local key = ATOM_ATTR_MACROS[ident]
+ if not key then return false end
+ local open = skip_ws_and_cmt(source, after)
+ if source:sub(open, open) ~= "(" then return false end
- -- Read parenthesized arg list.
- local body, body_end = read_parens(source, open)
- -- First arg = atom_name
- local first, after_name = read_ident(body, 1)
- if not first then return false end
+ -- Read parenthesized arg list.
+ local body, body_end = read_parens(source, open)
+ -- First arg = atom_name
+ local first, after_name = read_ident(body, 1)
+ if not first then return false end
- -- Skip whitespace, expect ",", skip whitespace to reach value.
- local j = after_name
- while j <= #body and is_space(body:sub(j, j)) do j = j + 1 end
- if body:sub(j, j) ~= "," then return false end
- j = j + 1
- while j <= #body and is_space(body:sub(j, j)) do j = j + 1 end
+ -- Skip whitespace, expect ",", skip whitespace to reach value.
+ local j = after_name
+ while j <= #body and is_space(body:sub(j, j)) do j = j + 1 end
+ if body:sub(j, j) ~= "," then return false end
+ j = j + 1
+ while j <= #body and is_space(body:sub(j, j)) do j = j + 1 end
- -- Value parser. Accepts either:
- -- - string literal: "..." (preserves inner text verbatim)
- -- - bare identifier or macro name (resolved via MACRO_EXPANSION)
- local value
- if body:sub(j, j) == '"' then
- -- find matching closing quote (handle \" escapes)
- local k = j + 1
- while k <= #body do
- local c = body:sub(k, k)
- if c == "\\" then
- k = k + 2
- elseif c == '"' then
- break
- else
- k = k + 1
- end
- end
- if body:sub(k, k) ~= '"' then return false end
- value = body:sub(j + 1, k - 1)
- else
- local id2, after_id = read_ident(body, j)
- if not id2 then return false end
- value = id2
- if MACRO_EXPANSION[value] then value = MACRO_EXPANSION[value] end
- end
+ -- Value parser. Accepts either:
+ -- - string literal: "..." (preserves inner text verbatim)
+ -- - bare identifier or macro name (resolved via MACRO_EXPANSION)
+ local value
+ if body:sub(j, j) == '"' then
+ -- find matching closing quote (handle \" escapes)
+ local k = j + 1
+ while k <= #body do
+ local c = body:sub(k, k)
+ if c == "\\" then
+ k = k + 2
+ elseif c == '"' then
+ break
+ else
+ k = k + 1
+ end
+ end
+ if body:sub(k, k) ~= '"' then return false end
+ value = body:sub(j + 1, k - 1)
+ else
+ local id2, after_id = read_ident(body, j)
+ if not id2 then return false end
+ value = id2
+ if MACRO_EXPANSION[value] then value = MACRO_EXPANSION[value] end
+ end
- return true, {
- line = line_of(i),
- name = first,
- attrs = { [key] = value },
- }, body_end
+ return true, {
+ line = line_of(i),
+ name = first,
+ attrs = { [key] = value },
+ }, body_end
end
local function find_atom_pragmas(source)
- local line_of = duffle.LineIndex(source)
- local out = {}
- local len = #source
- local i = 1
- while i <= len do
- i = skip_ws_and_cmt(source, i); if i > len then break end
- if source:sub(i, i) == "#" then
- local j = i
- while j <= len and source:sub(j, j) ~= "\n" do j = j + 1 end
- i = j + 1
- else
- local got, entry, next_i = try_parse_atom_attr_macro(source, i, line_of)
- if got then
- out[#out + 1] = entry
- i = next_i
- else
- local ident, after = read_ident(source, i)
- if not ident then
- i = i + 1
- elseif ident == "_Pragma" then
- local open = skip_ws_and_cmt(source, after)
- if source:sub(open, open) == "(" then
- local str, str_end = read_parens(source, open)
- str = trim(str)
- if str:sub(1, 1) == '"' and str:sub(-1) == '"' then
- local inner = str:sub(2, -2)
- -- Expect: "atom = [= ...]"
- local sp1 = find_byte(inner, " ", 1)
- if sp1 and trim(inner:sub(1, sp1 - 1)) == "atom" then
- local rest = trim(inner:sub(sp1 + 1))
- local sp2 = find_byte(rest, " ", 1)
- if sp2 then
- local name = trim(rest:sub(1, sp2 - 1))
- local attrs_str = trim(rest:sub(sp2 + 1))
- local attrs = {}
- local got_any = false
- -- tokenize (no :gmatch; use hand-rolled split_ws)
- for _, pair in ipairs(split_ws(attrs_str)) do
- local eq = find_byte(pair, "=", 1)
- if eq then
- local k = trim(pair:sub(1, eq - 1))
- local v = trim(pair:sub(eq + 1))
- if MACRO_EXPANSION[v] then v = MACRO_EXPANSION[v] end
- attrs[k] = v
- got_any = true
- end
- end
- if got_any then
- out[#out + 1] = {
- line = line_of(i),
- name = name,
- attrs = attrs,
- }
- end
- end
- end
- end
- i = str_end
- else
- i = open + 1
- end
- else
- i = after
- end
- end
- end
- end
- return out
+ local line_of = duffle.LineIndex(source)
+ local out = {}
+ local len = #source
+ local i = 1
+ while i <= len do
+ i = skip_ws_and_cmt(source, i); if i > len then break end
+ if source:sub(i, i) == "#" then
+ local j = i
+ while j <= len and source:sub(j, j) ~= "\n" do j = j + 1 end
+ i = j + 1
+ else
+ local got, entry, next_i = try_parse_atom_attr_macro(source, i, line_of)
+ if got then
+ out[#out + 1] = entry
+ i = next_i
+ else
+ local ident, after = read_ident(source, i)
+ if not ident then
+ i = i + 1
+ elseif ident == "_Pragma" then
+ local open = skip_ws_and_cmt(source, after)
+ if source:sub(open, open) == "(" then
+ local str, str_end = read_parens(source, open)
+ str = trim(str)
+ if str:sub(1, 1) == '"' and str:sub(-1) == '"' then
+ local inner = str:sub(2, -2)
+ -- Expect: "atom = [= ...]"
+ local sp1 = find_byte(inner, " ", 1)
+ if sp1 and trim(inner:sub(1, sp1 - 1)) == "atom" then
+ local rest = trim(inner:sub(sp1 + 1))
+ local sp2 = find_byte(rest, " ", 1)
+ if sp2 then
+ local name = trim(rest:sub(1, sp2 - 1))
+ local attrs_str = trim(rest:sub(sp2 + 1))
+ local attrs = {}
+ local got_any = false
+ -- tokenize (no :gmatch; use hand-rolled split_ws)
+ for _, pair in ipairs(split_ws(attrs_str)) do
+ local eq = find_byte(pair, "=", 1)
+ if eq then
+ local k = trim(pair:sub(1, eq - 1))
+ local v = trim(pair:sub(eq + 1))
+ if MACRO_EXPANSION[v] then v = MACRO_EXPANSION[v] end
+ attrs[k] = v
+ got_any = true
+ end
+ end
+ if got_any then
+ out[#out + 1] = {
+ line = line_of(i),
+ name = name,
+ attrs = attrs,
+ }
+ end
+ end
+ end
+ end
+ i = str_end
+ else
+ i = open + 1
+ end
+ else
+ i = after
+ end
+ end
+ end
+ end
+ return out
end
@@ -468,78 +468,78 @@ end
-- ============================================================
local function find_binds_structs(source)
- local line_of = duffle.LineIndex(source)
- local out = {}
- local len = #source
- local i = 1
- while i <= len do
- i = skip_ws_and_cmt(source, i); if i > len then break end
- -- Skip preprocessor directives (lines starting with #).
- if source:sub(i, i) == "#" then
- local j = i
- while j <= len and source:sub(j, j) ~= "\n" do j = j + 1 end
- i = j + 1
- else
- local ident, after = read_ident(source, i)
- if not ident then
- i = i + 1
- elseif ident == "typedef" then
- local j = skip_ws_and_cmt(source, after)
- local id2, after2 = read_ident(source, j)
- if id2 ~= "Struct_" then
- i = after2 or (j + 1)
- elseif id2 == "Struct_" then
- local open = skip_ws_and_cmt(source, after2)
- if source:sub(open, open) == "(" then
- local inner, after_paren = read_parens(source, open)
- local name = trim(inner)
- local brace = scan_to_char(source, "{", after_paren)
- if brace then
- local body, after_brace = read_braces(source, brace)
- local fields = {}
- local byte_off = 0
- local k = 1
- while k <= #body do
- k = skip_ws_and_cmt(body, k); if k > #body then break end
- local tid, tafter = read_ident(body, k)
- if not tid then
- k = k + 1
- elseif tid == "U4" then
- local fid, fafter = read_ident(body, skip_ws_and_cmt(body, tafter))
- if fid then
- fields[#fields + 1] = { name = fid, offset = byte_off }
- byte_off = byte_off + 4
- end
- k = fafter or tafter + 1
- else
- k = tafter + 1
- end
- end
- -- Only emit Binds_* structs (skip FMipsAtom512, MipsAtomBuilder, etc.)
- if name:sub(1, 6) == "Binds_" then
- out[#out + 1] = {
- line = line_of(i),
- name = name,
- fields = fields,
- bytes = byte_off,
- }
- end
- i = after_brace
- else
- i = open + 1
- end
- else
- i = open + 1
- end
- else
- i = after2 or (j + 1)
- end
- else
- i = after
- end
- end
- end
- return out
+ local line_of = duffle.LineIndex(source)
+ local out = {}
+ local len = #source
+ local i = 1
+ while i <= len do
+ i = skip_ws_and_cmt(source, i); if i > len then break end
+ -- Skip preprocessor directives (lines starting with #).
+ if source:sub(i, i) == "#" then
+ local j = i
+ while j <= len and source:sub(j, j) ~= "\n" do j = j + 1 end
+ i = j + 1
+ else
+ local ident, after = read_ident(source, i)
+ if not ident then
+ i = i + 1
+ elseif ident == "typedef" then
+ local j = skip_ws_and_cmt(source, after)
+ local id2, after2 = read_ident(source, j)
+ if id2 ~= "Struct_" then
+ i = after2 or (j + 1)
+ elseif id2 == "Struct_" then
+ local open = skip_ws_and_cmt(source, after2)
+ if source:sub(open, open) == "(" then
+ local inner, after_paren = read_parens(source, open)
+ local name = trim(inner)
+ local brace = scan_to_char(source, "{", after_paren)
+ if brace then
+ local body, after_brace = read_braces(source, brace)
+ local fields = {}
+ local byte_off = 0
+ local k = 1
+ while k <= #body do
+ k = skip_ws_and_cmt(body, k); if k > #body then break end
+ local tid, tafter = read_ident(body, k)
+ if not tid then
+ k = k + 1
+ elseif tid == "U4" then
+ local fid, fafter = read_ident(body, skip_ws_and_cmt(body, tafter))
+ if fid then
+ fields[#fields + 1] = { name = fid, offset = byte_off }
+ byte_off = byte_off + 4
+ end
+ k = fafter or tafter + 1
+ else
+ k = tafter + 1
+ end
+ end
+ -- Only emit Binds_* structs (skip FMipsAtom512, MipsAtomBuilder, etc.)
+ if name:sub(1, 6) == "Binds_" then
+ out[#out + 1] = {
+ line = line_of(i),
+ name = name,
+ fields = fields,
+ bytes = byte_off,
+ }
+ end
+ i = after_brace
+ else
+ i = open + 1
+ end
+ else
+ i = open + 1
+ end
+ else
+ i = after2 or (j + 1)
+ end
+ else
+ i = after
+ end
+ end
+ end
+ return out
end
-- ============================================================
@@ -548,52 +548,52 @@ end
-- ============================================================
local function find_atom_names(source)
- local line_of = duffle.LineIndex(source)
- local out = {}
- local len = #source
- local i = 1
- while i <= len do
- i = skip_ws_and_cmt(source, i); if i > len then break end
- local ident, after = read_ident(source, i)
- if not ident then
- i = i + 1
- elseif ident == "MipsAtom_" then
- local open = skip_ws_and_cmt(source, after)
- if source:sub(open, open) == "(" then
- local inner, after_paren = read_parens(source, open)
- local a = 1
- while a <= #inner and is_space(inner:sub(a, a)) do a = a + 1 end
- local b = a
- while b <= #inner and is_alnum(inner:sub(b, b)) do b = b + 1 end
- local name = inner:sub(a, b - 1)
- if name ~= "" then
- out[#out + 1] = { line = line_of(i), name = name }
- end
- local brace = scan_to_char(source, "{", after_paren)
- if brace then
- local _, after_brace = read_braces(source, brace)
- i = after_brace
- else
- i = open + 1
- end
- else
- i = open + 1
- end
- else
- i = after
- end
- end
- return out
+ local line_of = duffle.LineIndex(source)
+ local out = {}
+ local len = #source
+ local i = 1
+ while i <= len do
+ i = skip_ws_and_cmt(source, i); if i > len then break end
+ local ident, after = read_ident(source, i)
+ if not ident then
+ i = i + 1
+ elseif ident == "MipsAtom_" then
+ local open = skip_ws_and_cmt(source, after)
+ if source:sub(open, open) == "(" then
+ local inner, after_paren = read_parens(source, open)
+ local a = 1
+ while a <= #inner and is_space(inner:sub(a, a)) do a = a + 1 end
+ local b = a
+ while b <= #inner and is_alnum(inner:sub(b, b)) do b = b + 1 end
+ local name = inner:sub(a, b - 1)
+ if name ~= "" then
+ out[#out + 1] = { line = line_of(i), name = name }
+ end
+ local brace = scan_to_char(source, "{", after_paren)
+ if brace then
+ local _, after_brace = read_braces(source, brace)
+ i = after_brace
+ else
+ i = open + 1
+ end
+ else
+ i = open + 1
+ end
+ else
+ i = after
+ end
+ end
+ return out
end
-- ============================================================
-- Find the args of the function declaration that immediately precedes
--- a MipsAtomComponent_ invocation of the given name. Returns the
+-- a MipsAtomComp_Proc_ invocation of the given name. Returns the
-- args string (e.g., "U4 off, U4 code, U1 r, U1 g, U1 b") or nil
-- if no function declaration is found.
--
-- Convention: function form is
--- FI_ MipsAtom ac_X(args) MipsAtomComponent_(ac_X, { body })
+-- FI_ MipsAtom ac_X(args) MipsAtomComp_Proc_(ac_X, { body })
-- We find the LAST occurrence of "ac_X(" before before_pos and
-- extract the args from inside the parens.
--
@@ -602,33 +602,33 @@ end
-- ============================================================
local function find_function_args_for(source, name, before_pos)
- local search = source:sub(1, before_pos)
- local name_paren = name .. "("
- local last_idx = nil
- local p = 1
- while true do
- local s = search:find(name_paren, p, true) -- plain (no regex)
- if not s then break end
- last_idx = s
- p = s + #name_paren
- end
- if not last_idx then return nil end
+ local search = source:sub(1, before_pos)
+ local name_paren = name .. "("
+ local last_idx = nil
+ local p = 1
+ while true do
+ local s = search:find(name_paren, p, true) -- plain (no regex)
+ if not s then break end
+ last_idx = s
+ p = s + #name_paren
+ end
+ if not last_idx then return nil end
- -- Verify the preceding context ends with "MipsAtom" (with
- -- possible qualifiers between). Check the last word is
- -- "MipsAtom" (or the trimmed before ends with that token).
- local before = search:sub(1, last_idx - 1)
- local trimmed = duffle.trim(before)
- if trimmed:sub(-#"MipsAtom") ~= "MipsAtom" then
- -- Preceding context is not a function declaration.
- -- This shouldn't happen with the convention, but guard anyway.
- return nil
- end
+ -- Verify the preceding context ends with "MipsAtom" (with
+ -- possible qualifiers between). Check the last word is
+ -- "MipsAtom" (or the trimmed before ends with that token).
+ local before = search:sub(1, last_idx - 1)
+ local trimmed = duffle.trim(before)
+ if trimmed:sub(-#"MipsAtom") ~= "MipsAtom" then
+ -- Preceding context is not a function declaration.
+ -- This shouldn't happen with the convention, but guard anyway.
+ return nil
+ end
- local open_paren = last_idx + #name -- position of "("
- local inner = read_parens(source, open_paren)
- if not inner then return nil end
- return inner
+ local open_paren = last_idx + #name -- position of "("
+ local inner = read_parens(source, open_paren)
+ if not inner then return nil end
+ return inner
end
-- Extract just the parameter NAMES from a function-args string
@@ -644,72 +644,72 @@ end
-- `source`. Returns the comment text (with the `/* */` or `//` markers
-- preserved) or an empty string if no comment is adjacent.
-- Used to copy signature comments from the source declaration
--- (`MipsAtomComponent_` / function decl) over to the generated
+-- (`MipsAtomComp_` / `MipsAtomComp_Proc_` / function decl) over to the generated
-- `mac_X` macro, so LSP/IntelliSense displays the args doc.
-- No regex (per the no_regex constraint).
-- ============================================================
local function preceding_comment_block(source, pos)
- local i = pos
- local pieces = {}
- while true do
- -- Skip whitespace
- local j = i - 1
- while j > 0 do
- local c = source:sub(j, j)
- if c == " " or c == "\t" or c == "\n" or c == "\r" then
- j = j - 1
- else
- break
- end
- end
- if j == 0 then break end
- -- Check for /* ... */ ending at j
- if j >= 2 and source:sub(j-1, j) == "*/" then
- local s = source:sub(1, j - 1)
- local last_open = nil
- for k = #s - 1, 1, -1 do
- if s:sub(k, k+1) == "/*" then
- last_open = k
- break
- end
- end
- if last_open then
- -- Include the leading whitespace+indentation before /*
- local block_start = last_open
- while block_start > 1 do
- local c = source:sub(block_start - 1, block_start - 1)
- if c == " " or c == "\t" then
- block_start = block_start - 1
- else
- break
- end
- end
- table.insert(pieces, 1, source:sub(block_start, j))
- i = block_start
- else
- break
- end
- -- Check for // comment ending at j (j is at end of line, j-1 is \n)
- elseif j >= 1 and (source:sub(j, j) == "\n" or source:sub(j, j) == "\r") then
- -- Walk back to the start of the line
- local line_start = j
- while line_start > 1 and source:sub(line_start-1, line_start-1) ~= "\n" do
- line_start = line_start - 1
- end
- local line = source:sub(line_start, j)
- if line:sub(1, 2) == "//" then
- table.insert(pieces, 1, line)
- i = line_start - 1
- else
- break
- end
- else
- break
- end
- end
- if #pieces == 0 then return "" end
- return table.concat(pieces, "\n")
+ local i = pos
+ local pieces = {}
+ while true do
+ -- Skip whitespace
+ local j = i - 1
+ while j > 0 do
+ local c = source:sub(j, j)
+ if c == " " or c == "\t" or c == "\n" or c == "\r" then
+ j = j - 1
+ else
+ break
+ end
+ end
+ if j == 0 then break end
+ -- Check for /* ... */ ending at j
+ if j >= 2 and source:sub(j-1, j) == "*/" then
+ local s = source:sub(1, j - 1)
+ local last_open = nil
+ for k = #s - 1, 1, -1 do
+ if s:sub(k, k+1) == "/*" then
+ last_open = k
+ break
+ end
+ end
+ if last_open then
+ -- Include the leading whitespace+indentation before /*
+ local block_start = last_open
+ while block_start > 1 do
+ local c = source:sub(block_start - 1, block_start - 1)
+ if c == " " or c == "\t" then
+ block_start = block_start - 1
+ else
+ break
+ end
+ end
+ table.insert(pieces, 1, source:sub(block_start, j))
+ i = block_start
+ else
+ break
+ end
+ -- Check for // comment ending at j (j is at end of line, j-1 is \n)
+ elseif j >= 1 and (source:sub(j, j) == "\n" or source:sub(j, j) == "\r") then
+ -- Walk back to the start of the line
+ local line_start = j
+ while line_start > 1 and source:sub(line_start-1, line_start-1) ~= "\n" do
+ line_start = line_start - 1
+ end
+ local line = source:sub(line_start, j)
+ if line:sub(1, 2) == "//" then
+ table.insert(pieces, 1, line)
+ i = line_start - 1
+ else
+ break
+ end
+ else
+ break
+ end
+ end
+ if #pieces == 0 then return "" end
+ return table.concat(pieces, "\n")
end
-- Extract just the parameter NAMES from a function-args string
@@ -721,48 +721,48 @@ end
-- ============================================================
local function extract_arg_names(args_str)
- if not args_str or args_str == "" then return nil end
- local names = {}
- local tokens = duffle.split_top_level_commas(args_str)
- for _, tok in ipairs(tokens) do
- local trimmed = duffle.trim(tok)
- if trimmed ~= "" then
- -- Walk backwards from end of trimmed arg, skipping
- -- trailing whitespace / asterisks / brackets.
- local i = #trimmed
- while i > 0 do
- local c = trimmed:sub(i, i)
- if c == " " or c == "\t" or c == "*" or c == "]" or c == "[" then
- i = i - 1
- else
- break
- end
- end
- -- Now find the end of the last identifier (the param name).
- local j = i
- while j > 0 do
- local c = trimmed:sub(j, j)
- if duffle.is_alnum(c) or c == "_" then
- j = j - 1
- else
- break
- end
- end
- local name = trimmed:sub(j + 1, i)
- if name ~= "" then
- names[#names + 1] = name
- end
- end
- end
- if #names == 0 then return nil end
- return names
+ if not args_str or args_str == "" then return nil end
+ local names = {}
+ local tokens = duffle.split_top_level_commas(args_str)
+ for _, tok in ipairs(tokens) do
+ local trimmed = duffle.trim(tok)
+ if trimmed ~= "" then
+ -- Walk backwards from end of trimmed arg, skipping
+ -- trailing whitespace / asterisks / brackets.
+ local i = #trimmed
+ while i > 0 do
+ local c = trimmed:sub(i, i)
+ if c == " " or c == "\t" or c == "*" or c == "]" or c == "[" then
+ i = i - 1
+ else
+ break
+ end
+ end
+ -- Now find the end of the last identifier (the param name).
+ local j = i
+ while j > 0 do
+ local c = trimmed:sub(j, j)
+ if duffle.is_alnum(c) or c == "_" then
+ j = j - 1
+ else
+ break
+ end
+ end
+ local name = trimmed:sub(j + 1, i)
+ if name ~= "" then
+ names[#names + 1] = name
+ end
+ end
+ end
+ if #names == 0 then return nil end
+ return names
end
-- ============================================================
--- Find every MipsAtomComponent_(ac_) { body } declaration in source.
+-- Find every MipsAtomComp_(ac_) { body } declaration in source.
-- Supports BOTH the bare form and the function form:
--- Bare: MipsAtomComponent_(ac_X) { body }
--- Function: MipsAtomComponent_(ac_X, { body }) (with a preceding
+-- Bare: MipsAtomComp_(ac_X) { body }
+-- Function: MipsAtomComp_Proc_(ac_X, { body }) (with a preceding
-- "FI_ MipsAtom ac_X(args)" function declaration)
-- Returns: {line, name, body, args} where args is the function-args
-- string (or nil for the bare form).
@@ -771,288 +771,331 @@ end
-- ============================================================
local function find_component_atoms(source)
- local line_of = duffle.LineIndex(source)
- local out = {}
- local len = #source
- local i = 1
- while i <= len do
- i = skip_ws_and_cmt(source, i); if i > len then break end
- local ident, after = read_ident(source, i)
- if not ident then
- i = i + 1
- elseif ident == "MipsAtomComp_Proc_"
- or ident == "MipsAtomComp_" then
- local open = skip_ws_and_cmt(source, after)
- if source:sub(open, open) == "(" then
- local inner, after_paren = read_parens(source, open)
- -- Parse args: 1 arg = bare form, 2 args = function form
- local tokens = duffle.split_top_level_commas(inner)
- local name, body = nil, nil
- if #tokens == 1 then
- name = duffle.trim(tokens[1])
- elseif #tokens == 2 then
- name = duffle.trim(tokens[1])
- local body_raw = duffle.trim(tokens[2])
- -- Strip leading { and trailing } if present
- if #body_raw >= 2
- and body_raw:sub(1, 1) == "{"
- and body_raw:sub(-1) == "}" then
- body = duffle.trim(body_raw:sub(2, -2))
- else
- body = body_raw
- end
- end
- if name and name:sub(1, 3) == "ac_" then
- -- Find the function args (preceding function decl).
- -- For the bare form this returns nil (no function).
- local args = find_function_args_for(source, name, open)
- -- Capture the preceding comment block (signature doc).
- -- Walk back from `i` (position of the identifier start)
- -- so the walk-back goes through whitespace+comment and
- -- stops AT the comment (not at the identifier chars).
- local comment = preceding_comment_block(source, i)
- if body == nil then
- -- Bare form: body is the brace block AFTER the parens.
- local brace = scan_to_char(source, "{", after_paren)
- if brace then
- local body_content, after_brace = read_braces(source, brace)
- out[#out + 1] = {
- line = line_of(i),
- name = name:sub(4), -- strip "ac_" prefix
- body = body_content,
- args = args,
- comment = comment,
- }
- i = after_brace
- else
- i = open + 1
- end
- else
- -- Function form: body is the second arg (already extracted).
- out[#out + 1] = {
- line = line_of(i),
- name = name:sub(4), -- strip "ac_" prefix
- body = body,
- args = args,
- comment = comment,
- }
- i = after_paren
- end
- else
- i = open + 1
- end
- else
- i = open + 1
- end
- else
- i = after
- end
- end
- return out
+ local line_of = duffle.LineIndex(source)
+ local out = {}
+ local len = #source
+ local i = 1
+ while i <= len do
+ i = skip_ws_and_cmt(source, i); if i > len then break end
+ local ident, after = read_ident(source, i)
+ if not ident then
+ i = i + 1
+ elseif ident == "MipsAtomComp_Proc_"
+ or ident == "MipsAtomComp_" then
+ local open = skip_ws_and_cmt(source, after)
+ if source:sub(open, open) == "(" then
+ local inner, after_paren = read_parens(source, open)
+ -- Parse args: 1 arg = bare form, 2 args = function form
+ local tokens = duffle.split_top_level_commas(inner)
+ local name, body = nil, nil
+ if #tokens == 1 then
+ name = duffle.trim(tokens[1])
+ elseif #tokens == 2 then
+ name = duffle.trim(tokens[1])
+ local body_raw = duffle.trim(tokens[2])
+ -- Strip leading { and trailing } if present
+ if #body_raw >= 2
+ and body_raw:sub(1, 1) == "{"
+ and body_raw:sub(-1) == "}" then
+ body = duffle.trim(body_raw:sub(2, -2))
+ else
+ body = body_raw
+ end
+ end
+ if name and name:sub(1, 3) == "ac_" then
+ -- Find the function args (preceding function decl).
+ -- For the bare form this returns nil (no function).
+ local args = find_function_args_for(source, name, open)
+ -- Capture the preceding comment block (signature doc).
+ -- Walk back from `i` (position of the identifier start)
+ -- so the walk-back goes through whitespace+comment and
+ -- stops AT the comment (not at the identifier chars).
+ local comment = preceding_comment_block(source, i)
+ if body == nil then
+ -- Bare form: body is the brace block AFTER the parens.
+ local brace = scan_to_char(source, "{", after_paren)
+ if brace then
+ local body_content, after_brace = read_braces(source, brace)
+ out[#out + 1] = {
+ line = line_of(i),
+ name = name:sub(4), -- strip "ac_" prefix
+ body = body_content,
+ args = args,
+ comment = comment,
+ }
+ i = after_brace
+ else
+ i = open + 1
+ end
+ else
+ -- Function form: body is the second arg (already extracted).
+ out[#out + 1] = {
+ line = line_of(i),
+ name = name:sub(4), -- strip "ac_" prefix
+ body = body,
+ args = args,
+ comment = comment,
+ }
+ i = after_paren
+ end
+ else
+ i = open + 1
+ end
+ else
+ i = open + 1
+ end
+ else
+ i = after
+ end
+ end
+ return out
end
-- ============================================================
-- Emit a per-directory generated header with mac_X(...) macros
--- derived from MipsAtomComponent_ declarations + auto word-counts.
+-- derived from MipsAtomComp_ declarations + auto word-counts.
-- Output: /gen/.components.h
-- ============================================================
+-- Convert `//` line comments to `/* */` block comments in a token.
+-- C macros use `\` line-continuations; a `//` comment before `\` would
+-- consume the continuation, breaking the macro. We convert `//` to
+-- `/* */` so the multi-line macro structure is preserved.
+-- Skips `//` sequences that are inside string or character literals
+-- (a rough heuristic — sufficient for component bodies which don't
+-- have those constructs).
+local function convert_line_comments_to_block(s)
+ local result = s
+ local i = 1
+ while i <= #result do
+ local c = result:byte(i)
+ if c == 47 and i + 1 <= #result and result:byte(i + 1) == 47 then
+ -- Found `//`. Find end of line.
+ local eol = i
+ while eol <= #result and result:byte(eol) ~= 10 do
+ eol = eol + 1
+ end
+ local before = result:sub(1, i - 1)
+ local comment = result:sub(i + 2, eol - 1) -- skip the `//`
+ local after
+ if eol <= #result and result:byte(eol) == 10 then
+ after = " */" .. result:sub(eol) -- keep the newline
+ else
+ after = " */"
+ end
+ result = before .. "/*" .. comment .. after
+ i = #before + 2 + #comment + 3 -- skip past converted comment
+ else
+ i = i + 1
+ end
+ end
+ return result
+end
+
local function emit_component_macros_h(source_path, components)
- if #components == 0 then return end
+ if #components == 0 then return end
- local dir = duffle.dirname(source_path)
- local dir_basename = duffle.basename_no_ext(dir)
- -- Components header stays in the source dir (used by the codebase).
- local out_dir = dir .. "/gen"
- local out_path = out_dir .. "/" .. dir_basename .. ".macs.h"
+ local dir = duffle.dirname(source_path)
+ local dir_basename = duffle.basename_no_ext(dir)
+ -- Components header stays in the source dir (used by the codebase).
+ local out_dir = dir .. "/gen"
+ local out_path = out_dir .. "/" .. dir_basename .. ".macs.h"
- local lines = {
- -- #pragma once wrapped in #ifdef INTELLISENSE_DIRECTIVES, matching
- -- the convention in lottes_tape.h. The build does manual unity
- -- includes (the user controls include order), so the pragma
- -- is only active for IDE/tooling.
- "#ifdef INTELLISENSE_DIRECTIVES",
- "#pragma once",
- "#endif",
- "// Auto-generated by tape_atom_annotation_pass.lua — DO NOT EDIT",
- "// Source: " .. source_path,
- "// Component atoms (MipsAtomComp_(ac_*)) -> macro variants (mac_*)",
- "// + auto word-counts (so tape_atom.metadata.h stays manual-only",
- "// for encoding macros).",
- "",
- -- Self-contained: define WORD_COUNT if not already defined.
- -- The metadata file (tape_atom.metadata.h) defines it as
- -- enum { words_##name = (count) };
- -- We use the same definition here so the auto-generated
- -- entries below expand to compile-time constants whether
- -- the metadata file is included first or not.
- "#ifndef WORD_COUNT",
- "#define WORD_COUNT(name, count) enum { words_##name = (count) };",
- "#endif",
- "",
- }
+ local lines = {
+ -- #pragma once wrapped in #ifdef INTELLISENSE_DIRECTIVES, matching
+ -- the convention in lottes_tape.h. The build does manual unity
+ -- includes (the user controls include order), so the pragma
+ -- is only active for IDE/tooling.
+ "#ifdef INTELLISENSE_DIRECTIVES",
+ "#pragma once",
+ "#endif",
+ "// Auto-generated by tape_atom_annotation_pass.lua — DO NOT EDIT",
+ "// Source: " .. source_path,
+ "// Component atoms (MipsAtomComp_(ac_*)) -> macro variants (mac_*)",
+ "// + auto word-counts (so tape_atom.metadata.h stays manual-only",
+ "// for encoding macros).",
+ "",
+ -- Self-contained: define WORD_COUNT if not already defined.
+ -- The metadata file (tape_atom.metadata.h) defines it as
+ -- enum { words_##name = (count) };
+ -- We use the same definition here so the auto-generated
+ -- entries below expand to compile-time constants whether
+ -- the metadata file is included first or not.
+ "#ifndef WORD_COUNT",
+ "#define WORD_COUNT(name, count) enum { words_##name = (count) };",
+ "#endif",
+ "",
+ }
- for _, c in ipairs(components) do
- -- Emit the signature comment (if any) above the macro.
- -- This is the same comment that preceded the MipsAtomComponent_
- -- declaration in the source; LSP/IntelliSense shows it on the
- -- generated mac_X macro.
- if c.comment and c.comment ~= "" then
- for line in (c.comment .. "\n"):gmatch("([^\n]*)\n") do
- lines[#lines + 1] = line
- end
- end
+ for _, c in ipairs(components) do
+ -- Emit the signature comment (if any) above the macro.
+ -- This is the same comment that preceded the MipsAtomComp_
+ -- declaration in the source; LSP/IntelliSense shows it on the
+ -- generated mac_X macro.
+ if c.comment and c.comment ~= "" then
+ for line in (c.comment .. "\n"):gmatch("([^\n]*)\n") do
+ lines[#lines + 1] = line
+ end
+ end
- -- Split body by top-level commas; filter empty tokens.
- local tokens = {}
- for _, t in ipairs(duffle.split_top_level_commas(c.body)) do
- local trimmed = duffle.trim(t)
- if trimmed ~= "" then tokens[#tokens + 1] = trimmed end
- end
- local n = #tokens
+ -- Split body by top-level commas; filter empty tokens.
+ local tokens = {}
+ for _, t in ipairs(duffle.split_top_level_commas(c.body)) do
+ local trimmed = duffle.trim(t)
+ if trimmed ~= "" then tokens[#tokens + 1] = trimmed end
+ end
+ local n = #tokens
- -- Determine the macro signature: with function args (function
- -- form) or variadic-ignored (bare form).
- local arg_names = extract_arg_names(c.args)
- local sig
- if arg_names and #arg_names > 0 then
- sig = table.concat(arg_names, ", ")
- else
- sig = "..."
- end
+ -- Determine the macro signature: with function args (function
+ -- form) or variadic-ignored (bare form).
+ local arg_names = extract_arg_names(c.args)
+ local sig
+ if arg_names and #arg_names > 0 then
+ sig = table.concat(arg_names, ", ")
+ else
+ sig = "..."
+ end
if n > 0 then
+ -- Convert `//` line comments to `/* */` block comments in each
+ -- token. C macros use `\` line-continuations; if a `//` comment
+ -- appears before a `\`, the rest of the line (including the
+ -- continuation) is consumed by the `//`, breaking the macro.
+ -- Converting to `/* */` preserves the macro structure.
+ for j = 1, n do
+ tokens[j] = convert_line_comments_to_block(tokens[j])
+ end
-- Emit the mac_() macro
lines[#lines + 1] = "#define mac_" .. c.name .. "(" .. sig .. ") \\"
lines[#lines + 1] = "\t" .. tokens[1] .. " \\"
for j = 2, n do
lines[#lines + 1] = ",\t" .. tokens[j] .. " \\"
end
- -- Strip the trailing line-continuation on the last body line.
- -- The last 2 chars are always " \" (space + backslash).
- -- No regex — just trim with string.sub.
- local last = lines[#lines]
- if last:sub(-2) == " \\" then
- lines[#lines] = last:sub(1, -3)
- end
- end
+ -- Strip the trailing line-continuation on the last body line.
+ -- The last 2 chars are always " \" (space + backslash).
+ -- No regex — just trim with string.sub.
+ local last = lines[#lines]
+ if last:sub(-2) == " \\" then
+ lines[#lines] = last:sub(1, -3)
+ end
+ end
- -- Emit the WORD_COUNT(mac_, N) entry.
- lines[#lines + 1] = "WORD_COUNT(mac_" .. c.name .. ", " .. n .. ")"
- lines[#lines + 1] = ""
- end
+ -- Emit the WORD_COUNT(mac_, N) entry.
+ lines[#lines + 1] = "WORD_COUNT(mac_" .. c.name .. ", " .. n .. ")"
+ lines[#lines + 1] = ""
+ end
- duffle.ensure_dir(out_dir)
- duffle.write_file(out_path, table.concat(lines, "\n") .. "\n")
- print(string.format(" -> %s", out_path))
+ duffle.ensure_dir(out_dir)
+ duffle.write_file(out_path, table.concat(lines, "\n") .. "\n")
+ print(string.format(" -> %s", out_path))
end
local function find_atom_annotations(source)
- local line_of = duffle.LineIndex(source)
- local annots = {}
- local len = #source
- local i = 1
- while i <= len do
- i = skip_ws_and_cmt(source, i); if i > len then break end
- -- Match the leading identifier of a TAPE_ATOM_* macro.
- local ident, after = read_ident(source, i)
- if not ident then
- i = i + 1
- elseif TAPE_ATOM_MACROS[ident] then
- local open = skip_ws_and_cmt(source, after)
- if source:sub(open, open) == "(" then
- local inner, after_paren = read_parens(source, open)
- local args = parse_atom_annot_args(inner)
- local macro_def = TAPE_ATOM_MACROS[ident]
+ local line_of = duffle.LineIndex(source)
+ local annots = {}
+ local len = #source
+ local i = 1
+ while i <= len do
+ i = skip_ws_and_cmt(source, i); if i > len then break end
+ -- Match the leading identifier of a TAPE_ATOM_* macro.
+ local ident, after = read_ident(source, i)
+ if not ident then
+ i = i + 1
+ elseif TAPE_ATOM_MACROS[ident] then
+ local open = skip_ws_and_cmt(source, after)
+ if source:sub(open, open) == "(" then
+ local inner, after_paren = read_parens(source, open)
+ local args = parse_atom_annot_args(inner)
+ local macro_def = TAPE_ATOM_MACROS[ident]
- if #args < 1 then
- annots[#annots + 1] = {
- line = line_of(i),
- macro = ident,
- kind = macro_def.kind,
- error = "missing atom name (first arg)",
- }
- else
- local name = args[1].value
- -- atom_bind: (name, Binds_Struct, writes)
- -- atom_annot: (name, phase, reads, writes)
- -- atom_setup / atom_commit: (name, reads)
- -- atom_init / atom_terminate: (name)
- local entry = {
- line = line_of(i),
- macro = ident,
- name = name,
- kind = macro_def.kind,
- binds = nil,
- phase = nil,
- reads = {},
- writes = {},
- errors = {},
- }
+ if #args < 1 then
+ annots[#annots + 1] = {
+ line = line_of(i),
+ macro = ident,
+ kind = macro_def.kind,
+ error = "missing atom name (first arg)",
+ }
+ else
+ local name = args[1].value
+ -- atom_bind: (name, Binds_Struct, writes)
+ -- atom_annot: (name, phase, reads, writes)
+ -- atom_setup / atom_commit: (name, reads)
+ -- atom_init / atom_terminate: (name)
+ local entry = {
+ line = line_of(i),
+ macro = ident,
+ name = name,
+ kind = macro_def.kind,
+ binds = nil,
+ phase = nil,
+ reads = {},
+ writes = {},
+ errors = {},
+ }
- -- Is this arg a register-list call (any of the recognized forms)?
- local function is_regs(a)
- return a and (a.kind == "atom_reads" or a.kind == "atom_writes" or a.kind == "regs")
- end
+ -- Is this arg a register-list call (any of the recognized forms)?
+ local function is_regs(a)
+ return a and (a.kind == "atom_reads" or a.kind == "atom_writes" or a.kind == "regs")
+ end
- if macro_def.binds then
- -- atom_bind(name, Binds_Struct, writes)
- if #args >= 2 and args[2].kind == "ident" then
- entry.binds = args[2].value
- end
- if #args >= 3 and is_regs(args[3]) then
- -- A bind writes the wave-context, so atom_writes(...) is the
- -- canonical form, but legacy regs(...) is also accepted.
- entry.writes = args[3].value
- end
- elseif ident == "atom_init" or ident == "atom_terminate" then
- -- (name) only, no reads/writes to extract
- elseif ident == "atom_setup" then
- -- atom_setup(name, reads)
- if #args >= 2 and is_regs(args[2]) then
- entry.reads = args[2].value
- end
- elseif ident == "atom_commit" then
- -- atom_commit(name, reads)
- if #args >= 2 and is_regs(args[2]) then
- entry.reads = args[2].value
- end
- elseif ident == "atom_annot" then
- -- atom_annot(name, phase, reads, writes)
- if #args >= 2 and args[2].kind == "ident" then
- entry.phase = MACRO_EXPANSION[args[2].value] or args[2].value
- end
- -- reads slot: atom_reads(...) is the canonical form;
- -- atom_writes(...) in the reads slot is a likely bug.
- if #args >= 3 and is_regs(args[3]) then
- if args[3].kind == "atom_writes" then
- entry.errors[#entry.errors + 1] =
- "reads slot has atom_writes — swap order?"
- end
- entry.reads = args[3].value
- end
- -- writes slot: atom_writes(...) is canonical;
- -- atom_reads(...) here is a likely bug.
- if #args >= 4 and is_regs(args[4]) then
- if args[4].kind == "atom_reads" then
- entry.errors[#entry.errors + 1] =
- "writes slot has atom_reads — swap order?"
- end
- entry.writes = args[4].value
- end
- end
+ if macro_def.binds then
+ -- atom_bind(name, Binds_Struct, writes)
+ if #args >= 2 and args[2].kind == "ident" then
+ entry.binds = args[2].value
+ end
+ if #args >= 3 and is_regs(args[3]) then
+ -- A bind writes the wave-context, so atom_writes(...) is the
+ -- canonical form, but legacy regs(...) is also accepted.
+ entry.writes = args[3].value
+ end
+ elseif ident == "atom_init" or ident == "atom_terminate" then
+ -- (name) only, no reads/writes to extract
+ elseif ident == "atom_setup" then
+ -- atom_setup(name, reads)
+ if #args >= 2 and is_regs(args[2]) then
+ entry.reads = args[2].value
+ end
+ elseif ident == "atom_commit" then
+ -- atom_commit(name, reads)
+ if #args >= 2 and is_regs(args[2]) then
+ entry.reads = args[2].value
+ end
+ elseif ident == "atom_annot" then
+ -- atom_annot(name, phase, reads, writes)
+ if #args >= 2 and args[2].kind == "ident" then
+ entry.phase = MACRO_EXPANSION[args[2].value] or args[2].value
+ end
+ -- reads slot: atom_reads(...) is the canonical form;
+ -- atom_writes(...) in the reads slot is a likely bug.
+ if #args >= 3 and is_regs(args[3]) then
+ if args[3].kind == "atom_writes" then
+ entry.errors[#entry.errors + 1] =
+ "reads slot has atom_writes — swap order?"
+ end
+ entry.reads = args[3].value
+ end
+ -- writes slot: atom_writes(...) is canonical;
+ -- atom_reads(...) here is a likely bug.
+ if #args >= 4 and is_regs(args[4]) then
+ if args[4].kind == "atom_reads" then
+ entry.errors[#entry.errors + 1] =
+ "writes slot has atom_reads — swap order?"
+ end
+ entry.writes = args[4].value
+ end
+ end
- annots[#annots + 1] = entry
- end
- i = after_paren
- else
- i = open + 1
- end
- else
- i = after
- end
- end
- return annots
+ annots[#annots + 1] = entry
+ end
+ i = after_paren
+ else
+ i = open + 1
+ end
+ else
+ i = after
+ end
+ end
+ return annots
end
-- ============================================================
@@ -1060,217 +1103,217 @@ end
-- ============================================================
local function validate(source_path, word_counts)
- local source = read_file(source_path)
+ local source = read_file(source_path)
- local annots = find_atom_annotations(source)
- local macros = find_macro_word_annotations(source)
- local pragmas = find_atom_pragmas(source)
- local binds = find_binds_structs(source)
- local atoms = find_atom_names(source)
+ local annots = find_atom_annotations(source)
+ local macros = find_macro_word_annotations(source)
+ local pragmas = find_atom_pragmas(source)
+ local binds = find_binds_structs(source)
+ local atoms = find_atom_names(source)
- -- Index for O(1) lookup
- local atom_index = {}
- for _, a in ipairs(atoms) do atom_index[a.name] = a end
+ -- Index for O(1) lookup
+ local atom_index = {}
+ for _, a in ipairs(atoms) do atom_index[a.name] = a end
- local binds_index = {}
- for _, b in ipairs(binds) do binds_index[b.name] = b end
+ local binds_index = {}
+ for _, b in ipairs(binds) do binds_index[b.name] = b end
- local errors = {}
- local warnings = {}
- local info = {}
+ local errors = {}
+ local warnings = {}
+ local info = {}
- -- 1. Every annotated atom must exist as a real MipsAtom_ declaration.
- for _, a in ipairs(annots) do
- if a.error then
- errors[#errors + 1] = {line = a.line, msg = a.error}
- elseif not atom_index[a.name] then
- errors[#errors + 1] = {
- line = a.line,
- msg = string.format("annotation for '%s' has no matching MipsAtom_(%s) { ... }", a.name, a.name)
- }
- end
- -- Per-entry parser errors (e.g. reads/writes slot mix-ups)
- if a.errors then
- for _, msg in ipairs(a.errors) do
- errors[#errors + 1] = {line = a.line, msg = string.format("'%s': %s", a.name, msg)}
- end
- end
- end
+ -- 1. Every annotated atom must exist as a real MipsAtom_ declaration.
+ for _, a in ipairs(annots) do
+ if a.error then
+ errors[#errors + 1] = {line = a.line, msg = a.error}
+ elseif not atom_index[a.name] then
+ errors[#errors + 1] = {
+ line = a.line,
+ msg = string.format("annotation for '%s' has no matching MipsAtom_(%s) { ... }", a.name, a.name)
+ }
+ end
+ -- Per-entry parser errors (e.g. reads/writes slot mix-ups)
+ if a.errors then
+ for _, msg in ipairs(a.errors) do
+ errors[#errors + 1] = {line = a.line, msg = string.format("'%s': %s", a.name, msg)}
+ end
+ end
+ end
- -- 2. Every atom must have exactly one annotation (no orphans, no duplicates).
- local count_per_atom = {}
- for _, a in ipairs(annots) do
- if a.name and not a.error then
- count_per_atom[a.name] = (count_per_atom[a.name] or 0) + 1
- end
- end
- for _, atom in ipairs(atoms) do
- local n = count_per_atom[atom.name] or 0
- if n == 0 then
- warnings[#warnings + 1] = {
- line = atom.line,
- msg = string.format("MipsAtom_(%s) has no TAPE_ATOM_* annotation", atom.name)
- }
- elseif n > 1 then
- errors[#errors + 1] = {
- line = atom.line,
- msg = string.format("MipsAtom_(%s) has %d annotations (expected 1)", atom.name, n)
- }
- end
- end
+ -- 2. Every atom must have exactly one annotation (no orphans, no duplicates).
+ local count_per_atom = {}
+ for _, a in ipairs(annots) do
+ if a.name and not a.error then
+ count_per_atom[a.name] = (count_per_atom[a.name] or 0) + 1
+ end
+ end
+ for _, atom in ipairs(atoms) do
+ local n = count_per_atom[atom.name] or 0
+ if n == 0 then
+ warnings[#warnings + 1] = {
+ line = atom.line,
+ msg = string.format("MipsAtom_(%s) has no TAPE_ATOM_* annotation", atom.name)
+ }
+ elseif n > 1 then
+ errors[#errors + 1] = {
+ line = atom.line,
+ msg = string.format("MipsAtom_(%s) has %d annotations (expected 1)", atom.name, n)
+ }
+ end
+ end
- -- 3. Phase validity.
- for _, a in ipairs(annots) do
- if a.name and not a.error and a.phase and not valid_phase(a.phase) then
- errors[#errors + 1] = {
- line = a.line,
- msg = string.format("'%s' has unknown phase '%s' (expected one of init/bind/setup/work/commit/terminate)", a.name, a.phase)
- }
- end
- end
+ -- 3. Phase validity.
+ for _, a in ipairs(annots) do
+ if a.name and not a.error and a.phase and not valid_phase(a.phase) then
+ errors[#errors + 1] = {
+ line = a.line,
+ msg = string.format("'%s' has unknown phase '%s' (expected one of init/bind/setup/work/commit/terminate)", a.name, a.phase)
+ }
+ end
+ end
- -- 4. BIND atoms must reference a real Binds_* struct.
- for _, a in ipairs(annots) do
- if a.binds then
- if not binds_index[a.binds] then
- errors[#errors + 1] = {
- line = a.line,
- msg = string.format("'%s' binds '%s' but no Struct_(%s) { ... } declaration found", a.name, a.binds, a.binds)
- }
- end
- end
- end
+ -- 4. BIND atoms must reference a real Binds_* struct.
+ for _, a in ipairs(annots) do
+ if a.binds then
+ if not binds_index[a.binds] then
+ errors[#errors + 1] = {
+ line = a.line,
+ msg = string.format("'%s' binds '%s' but no Struct_(%s) { ... } declaration found", a.name, a.binds, a.binds)
+ }
+ end
+ end
+ end
- -- 5. BIND writes must be wave-context registers that match Binds_ fields.
- for _, a in ipairs(annots) do
- if a.binds and binds_index[a.binds] then
- local bs = binds_index[a.binds]
- local field_names = {}
- for _, f in ipairs(bs.fields) do field_names[f.name] = true end
+ -- 5. BIND writes must be wave-context registers that match Binds_ fields.
+ for _, a in ipairs(annots) do
+ if a.binds and binds_index[a.binds] then
+ local bs = binds_index[a.binds]
+ local field_names = {}
+ for _, f in ipairs(bs.fields) do field_names[f.name] = true end
- -- Binds_ field names should match wave-context registers.
- -- Convention: field name == register name minus the "R_" prefix and
- -- "Cursor"/"Base" suffix mapped to canonical names.
- -- We accept either direct match (rare) or a simple "R_" prefix.
- for _, f in ipairs(bs.fields) do
- local candidate = "R_" .. f.name
- if not is_wave_context_reg(candidate) then
- warnings[#warnings + 1] = {
- line = bs.line,
- msg = string.format("%s field '%s' doesn't match a known wave-context register (candidate '%s')", a.binds, f.name, candidate)
- }
- end
- end
+ -- Binds_ field names should match wave-context registers.
+ -- Convention: field name == register name minus the "R_" prefix and
+ -- "Cursor"/"Base" suffix mapped to canonical names.
+ -- We accept either direct match (rare) or a simple "R_" prefix.
+ for _, f in ipairs(bs.fields) do
+ local candidate = "R_" .. f.name
+ if not is_wave_context_reg(candidate) then
+ warnings[#warnings + 1] = {
+ line = bs.line,
+ msg = string.format("%s field '%s' doesn't match a known wave-context register (candidate '%s')", a.binds, f.name, candidate)
+ }
+ end
+ end
- -- Bind writes must all be wave-context registers.
- for _, w in ipairs(a.writes) do
- if not is_wave_context_reg(w) then
- warnings[#warnings + 1] = {
- line = a.line,
- msg = string.format("%s writes '%s' which is not a known wave-context register", a.name, w)
- }
- end
- end
- end
- end
+ -- Bind writes must all be wave-context registers.
+ for _, w in ipairs(a.writes) do
+ if not is_wave_context_reg(w) then
+ warnings[#warnings + 1] = {
+ line = a.line,
+ msg = string.format("%s writes '%s' which is not a known wave-context register", a.name, w)
+ }
+ end
+ end
+ end
+ end
- -- 6. WORK reads should be a subset of BIND writes (the wave contract).
- -- We can't see tape emission sites here, but we can warn when a WORK
- -- atom reads a register that no BIND atom writes.
- for _, a in ipairs(annots) do
- if a.kind == "work" then
- for _, r in ipairs(a.reads) do
- if not is_wave_context_reg(r) and r ~= "R_TapePtr" then
- warnings[#warnings + 1] = {
- line = a.line,
- msg = string.format("work atom '%s' reads '%s' which is not a known wave-context register", a.name, r)
- }
- end
- end
- end
- end
+ -- 6. WORK reads should be a subset of BIND writes (the wave contract).
+ -- We can't see tape emission sites here, but we can warn when a WORK
+ -- atom reads a register that no BIND atom writes.
+ for _, a in ipairs(annots) do
+ if a.kind == "work" then
+ for _, r in ipairs(a.reads) do
+ if not is_wave_context_reg(r) and r ~= "R_TapePtr" then
+ warnings[#warnings + 1] = {
+ line = a.line,
+ msg = string.format("work atom '%s' reads '%s' which is not a known wave-context register", a.name, r)
+ }
+ end
+ end
+ end
+ end
- -- 7. TAPE_WORDS(mac_X, N) ↔ WORD_COUNT(mac_X, N) drift.
- for _, m in ipairs(macros) do
- local declared = word_counts[m.name]
- if not declared then
- errors[#errors + 1] = {
- line = m.line,
- msg = string.format("TAPE_WORDS(%s, %d) but '%s' is not in metadata.h", m.name, m.words, m.name)
- }
- elseif declared ~= m.words then
- errors[#errors + 1] = {
- line = m.line,
- msg = string.format("DRIFT: TAPE_WORDS(%s, %d) but metadata.h declares WORD_COUNT(%s, %d)", m.name, m.words, m.name, declared)
- }
- else
- info[#info + 1] = {
- line = m.line,
- msg = string.format("OK: %s = %d words", m.name, m.words)
- }
- end
- end
+ -- 7. TAPE_WORDS(mac_X, N) ↔ WORD_COUNT(mac_X, N) drift.
+ for _, m in ipairs(macros) do
+ local declared = word_counts[m.name]
+ if not declared then
+ errors[#errors + 1] = {
+ line = m.line,
+ msg = string.format("TAPE_WORDS(%s, %d) but '%s' is not in metadata.h", m.name, m.words, m.name)
+ }
+ elseif declared ~= m.words then
+ errors[#errors + 1] = {
+ line = m.line,
+ msg = string.format("DRIFT: TAPE_WORDS(%s, %d) but metadata.h declares WORD_COUNT(%s, %d)", m.name, m.words, m.name, declared)
+ }
+ else
+ info[#info + 1] = {
+ line = m.line,
+ msg = string.format("OK: %s = %d words", m.name, m.words)
+ }
+ end
+ end
- -- 8. atom_<...> _Pragma validation: resource/region/group/cadence/async
- for _, p in ipairs(pragmas) do
- -- Validate the atom name is real
- if not atom_index[p.name] then
- errors[#errors + 1] = {
- line = p.line,
- msg = string.format("pragma references unknown atom '%s'", p.name),
- }
- end
+ -- 8. atom_<...> _Pragma validation: resource/region/group/cadence/async
+ for _, p in ipairs(pragmas) do
+ -- Validate the atom name is real
+ if not atom_index[p.name] then
+ errors[#errors + 1] = {
+ line = p.line,
+ msg = string.format("pragma references unknown atom '%s'", p.name),
+ }
+ end
- -- Validate each key
- for k, v in pairs(p.attrs) do
- local spec = ATOM_PRAGMA_KINDS[k]
- if not spec then
- errors[#errors + 1] = {
- line = p.line,
- msg = string.format("'%s' has unknown pragma key '%s' (allowed: resource/region/group/cadence/async)", p.name, k),
- }
- elseif spec.allowed and not spec.allowed[v] then
- -- Build a human-readable allowed-set
- local allowed = {}
- for kk in pairs(spec.allowed) do allowed[#allowed + 1] = kk end
- table.sort(allowed)
- local allowed_str = table.concat(allowed, ", ")
- errors[#errors + 1] = {
- line = p.line,
- msg = string.format("'%s' pragma %s=%s but '%s' is not allowed (allowed: %s)", p.name, k, v, v, allowed_str),
- }
- end
- end
- end
+ -- Validate each key
+ for k, v in pairs(p.attrs) do
+ local spec = ATOM_PRAGMA_KINDS[k]
+ if not spec then
+ errors[#errors + 1] = {
+ line = p.line,
+ msg = string.format("'%s' has unknown pragma key '%s' (allowed: resource/region/group/cadence/async)", p.name, k),
+ }
+ elseif spec.allowed and not spec.allowed[v] then
+ -- Build a human-readable allowed-set
+ local allowed = {}
+ for kk in pairs(spec.allowed) do allowed[#allowed + 1] = kk end
+ table.sort(allowed)
+ local allowed_str = table.concat(allowed, ", ")
+ errors[#errors + 1] = {
+ line = p.line,
+ msg = string.format("'%s' pragma %s=%s but '%s' is not allowed (allowed: %s)", p.name, k, v, v, allowed_str),
+ }
+ end
+ end
+ end
- -- 9. cad_async requires CADENCE_ONDEMAND (or no cadence — default-frame atoms can still async).
- -- CADENCE_ONDEMAND requires async=true (otherwise the trigger is undefined).
- for _, p in ipairs(pragmas) do
- if p.attrs.cadence == "ondemand" and p.attrs.async ~= "true" then
- errors[#errors + 1] = {
- line = p.line,
- msg = string.format("'%s' is CADENCE_ONDEMAND but does not declare atom_async(true)", p.name),
- }
- end
- end
+ -- 9. cad_async requires CADENCE_ONDEMAND (or no cadence — default-frame atoms can still async).
+ -- CADENCE_ONDEMAND requires async=true (otherwise the trigger is undefined).
+ for _, p in ipairs(pragmas) do
+ if p.attrs.cadence == "ondemand" and p.attrs.async ~= "true" then
+ errors[#errors + 1] = {
+ line = p.line,
+ msg = string.format("'%s' is CADENCE_ONDEMAND but does not declare atom_async(true)", p.name),
+ }
+ end
+ end
- -- 10. Information summary.
- info[#info + 1] = {
- line = 0,
- msg = string.format("scanned: %d atom(s), %d annotation(s), %d pragma(s), %d macro-word-decl(s), %d binds struct(s)",
- #atoms, #annots, #pragmas, #macros, #binds)
- }
+ -- 10. Information summary.
+ info[#info + 1] = {
+ line = 0,
+ msg = string.format("scanned: %d atom(s), %d annotation(s), %d pragma(s), %d macro-word-decl(s), %d binds struct(s)",
+ #atoms, #annots, #pragmas, #macros, #binds)
+ }
- return {
- atoms = atoms,
- annots = annots,
- macros = macros,
- pragmas = pragmas,
- binds = binds,
- errors = errors,
- warnings = warnings,
- info = info,
- }
+ return {
+ atoms = atoms,
+ annots = annots,
+ macros = macros,
+ pragmas = pragmas,
+ binds = binds,
+ errors = errors,
+ warnings = warnings,
+ info = info,
+ }
end
-- ============================================================
@@ -1278,122 +1321,122 @@ end
-- ============================================================
local function render_source_report(source_path, result)
- local lines = {}
- local function add(s) lines[#lines + 1] = s end
+ local lines = {}
+ local function add(s) lines[#lines + 1] = s end
- add("========================================================")
- add("ANNOTATION PASS — " .. source_path)
- add("========================================================")
- add("")
- add(string.format("Atoms: %d Annotations: %d Pragmas: %d Binds structs: %d Macro decls: %d",
- #result.atoms, #result.annots, #result.pragmas and #result.pragmas or 0,
- #result.binds, #result.macros))
- add("")
+ add("========================================================")
+ add("ANNOTATION PASS — " .. source_path)
+ add("========================================================")
+ add("")
+ add(string.format("Atoms: %d Annotations: %d Pragmas: %d Binds structs: %d Macro decls: %d",
+ #result.atoms, #result.annots, #result.pragmas and #result.pragmas or 0,
+ #result.binds, #result.macros))
+ add("")
- add("── Atoms ────────────────────────────────────────────────")
- for _, a in ipairs(result.atoms) do
- add(string.format(" MipsAtom_(%s) line %d", a.name, a.line))
- end
- add("")
+ add("── Atoms ────────────────────────────────────────────────")
+ for _, a in ipairs(result.atoms) do
+ add(string.format(" MipsAtom_(%s) line %d", a.name, a.line))
+ end
+ add("")
- add("── Annotations ──────────────────────────────────────────")
- for _, a in ipairs(result.annots) do
- if a.error then
- add(string.format(" ✗ line %d %s [ERROR: %s]", a.line, a.macro or "?", a.error))
- else
- local line = string.format(" %s line %d %s phase=%s",
- a.kind == "work" and "●" or (a.kind == "bind" and "◆" or "○"),
- a.line, a.name, a.phase or a.kind)
- if a.binds then line = line .. " binds=" .. a.binds end
- if #a.reads > 0 then line = line .. " reads={" .. table.concat(a.reads, ",") .. "}" end
- if #a.writes > 0 then line = line .. " writes={" .. table.concat(a.writes, ",") .. "}" end
- add(line)
- end
- end
- add("")
+ add("── Annotations ──────────────────────────────────────────")
+ for _, a in ipairs(result.annots) do
+ if a.error then
+ add(string.format(" ✗ line %d %s [ERROR: %s]", a.line, a.macro or "?", a.error))
+ else
+ local line = string.format(" %s line %d %s phase=%s",
+ a.kind == "work" and "●" or (a.kind == "bind" and "◆" or "○"),
+ a.line, a.name, a.phase or a.kind)
+ if a.binds then line = line .. " binds=" .. a.binds end
+ if #a.reads > 0 then line = line .. " reads={" .. table.concat(a.reads, ",") .. "}" end
+ if #a.writes > 0 then line = line .. " writes={" .. table.concat(a.writes, ",") .. "}" end
+ add(line)
+ end
+ end
+ add("")
- add("── Binds_* structs ──────────────────────────────────────")
- for _, b in ipairs(result.binds) do
- add(string.format(" %s line %d %d bytes", b.name, b.line, b.bytes))
- for _, f in ipairs(b.fields) do
- add(string.format(" +%2d: %s", f.offset, f.name))
- end
- end
- add("")
+ add("── Binds_* structs ──────────────────────────────────────")
+ for _, b in ipairs(result.binds) do
+ add(string.format(" %s line %d %d bytes", b.name, b.line, b.bytes))
+ for _, f in ipairs(b.fields) do
+ add(string.format(" +%2d: %s", f.offset, f.name))
+ end
+ end
+ add("")
- add("── Macro word-count declarations ─────────────────────────")
- for _, m in ipairs(result.macros) do
- add(string.format(" %s line %d words=%d", m.name, m.line, m.words))
- end
- add("")
+ add("── Macro word-count declarations ─────────────────────────")
+ for _, m in ipairs(result.macros) do
+ add(string.format(" %s line %d words=%d", m.name, m.line, m.words))
+ end
+ add("")
- add("── Atom pragmas (resource / region / group / cadence / async) ─")
- if not result.pragmas or #result.pragmas == 0 then add(" (none)") end
- for _, p in ipairs(result.pragmas or {}) do
- local kvs = {}
- for k, v in pairs(p.attrs) do kvs[#kvs + 1] = k .. "=" .. v end
- table.sort(kvs)
- add(string.format(" ◇ line %d %s {%s}", p.line, p.name, table.concat(kvs, ", ")))
- end
- add("")
+ add("── Atom pragmas (resource / region / group / cadence / async) ─")
+ if not result.pragmas or #result.pragmas == 0 then add(" (none)") end
+ for _, p in ipairs(result.pragmas or {}) do
+ local kvs = {}
+ for k, v in pairs(p.attrs) do kvs[#kvs + 1] = k .. "=" .. v end
+ table.sort(kvs)
+ add(string.format(" ◇ line %d %s {%s}", p.line, p.name, table.concat(kvs, ", ")))
+ end
+ add("")
- add("── Errors ──────────────────────────────────────────────")
- if #result.errors == 0 then add(" (none)") end
- for _, e in ipairs(result.errors) do
- add(string.format(" ✗ line %d %s", e.line, e.msg))
- end
- add("")
+ add("── Errors ──────────────────────────────────────────────")
+ if #result.errors == 0 then add(" (none)") end
+ for _, e in ipairs(result.errors) do
+ add(string.format(" ✗ line %d %s", e.line, e.msg))
+ end
+ add("")
- add("── Warnings ────────────────────────────────────────────")
- if #result.warnings == 0 then add(" (none)") end
- for _, w in ipairs(result.warnings) do
- add(string.format(" ⚠ line %d %s", w.line, w.msg))
- end
- add("")
+ add("── Warnings ────────────────────────────────────────────")
+ if #result.warnings == 0 then add(" (none)") end
+ for _, w in ipairs(result.warnings) do
+ add(string.format(" ⚠ line %d %s", w.line, w.msg))
+ end
+ add("")
- return table.concat(lines, "\n") .. "\n"
+ return table.concat(lines, "\n") .. "\n"
end
local function render_project_report(all_results)
- local lines = {}
- local function add(s) lines[#lines + 1] = s end
+ local lines = {}
+ local function add(s) lines[#lines + 1] = s end
- local total_atoms, total_annots, total_macros, total_binds = 0, 0, 0, 0
- local total_errors, total_warnings = 0, 0
+ local total_atoms, total_annots, total_macros, total_binds = 0, 0, 0, 0
+ local total_errors, total_warnings = 0, 0
- for _, r in ipairs(all_results) do
- total_atoms = total_atoms + #r.atoms
- total_annots = total_annots + #r.annots
- total_macros = total_macros + #r.macros
- total_binds = total_binds + #r.binds
- total_errors = total_errors + #r.errors
- total_warnings = total_warnings + #r.warnings
- end
+ for _, r in ipairs(all_results) do
+ total_atoms = total_atoms + #r.atoms
+ total_annots = total_annots + #r.annots
+ total_macros = total_macros + #r.macros
+ total_binds = total_binds + #r.binds
+ total_errors = total_errors + #r.errors
+ total_warnings = total_warnings + #r.warnings
+ end
- add("========================================================")
- add("ANNOTATION VALIDATION — project summary")
- add("========================================================")
- add("")
- add(string.format("Atoms: %d", total_atoms))
- add(string.format("Annotations: %d", total_annots))
- add(string.format("Macros: %d", total_macros))
- add(string.format("Binds: %d", total_binds))
- add("")
- add(string.format("Errors: %d", total_errors))
- add(string.format("Warnings: %d", total_warnings))
- add("")
+ add("========================================================")
+ add("ANNOTATION VALIDATION — project summary")
+ add("========================================================")
+ add("")
+ add(string.format("Atoms: %d", total_atoms))
+ add(string.format("Annotations: %d", total_annots))
+ add(string.format("Macros: %d", total_macros))
+ add(string.format("Binds: %d", total_binds))
+ add("")
+ add(string.format("Errors: %d", total_errors))
+ add(string.format("Warnings: %d", total_warnings))
+ add("")
- if total_errors > 0 then
- add("Per-source error counts:")
- for _, r in ipairs(all_results) do
- if #r.errors > 0 then
- add(string.format(" %s : %d error(s)", r.source, #r.errors))
- end
- end
- add("")
- end
+ if total_errors > 0 then
+ add("Per-source error counts:")
+ for _, r in ipairs(all_results) do
+ if #r.errors > 0 then
+ add(string.format(" %s : %d error(s)", r.source, #r.errors))
+ end
+ end
+ add("")
+ end
- return table.concat(lines, "\n") .. "\n"
+ return table.concat(lines, "\n") .. "\n"
end
-- ============================================================
@@ -1401,97 +1444,97 @@ end
-- ============================================================
local function main(args)
- if #args < 2 then
- print("Usage: luajit tape_atom_annotation_pass.lua [source2 ...]")
- os.exit(1)
- end
- local word_counts = load_word_counts(args[1])
- local all_results = {}
+ if #args < 2 then
+ print("Usage: luajit tape_atom_annotation_pass.lua [source2 ...]")
+ os.exit(1)
+ end
+ local word_counts = load_word_counts(args[1])
+ local all_results = {}
- -- Collect every /gen directory we touch, so we can prune stale
- -- empty reports left over by previous runs. A file that USED to have atoms
- -- but doesn't any more (refactor / template removal) shouldn't keep its
- -- report polluting the source tree.
- local pruned_dirs = {}
+ -- Collect every /gen directory we touch, so we can prune stale
+ -- empty reports left over by previous runs. A file that USED to have atoms
+ -- but doesn't any more (refactor / template removal) shouldn't keep its
+ -- report polluting the source tree.
+ local pruned_dirs = {}
- for i = 2, #args do
- local source_path = args[i]
- local basename = basename_no_ext(source_path)
- local dir_basename = basename_no_ext(dirname(source_path))
- local out_dir = dirname(source_path) .. "/../../build/gen/" .. dir_basename
- local out_txt = out_dir .. "/" .. basename .. ".annotations.txt"
- local out_err = out_dir .. "/" .. basename .. ".errors.h"
+ for i = 2, #args do
+ local source_path = args[i]
+ local basename = basename_no_ext(source_path)
+ local dir_basename = basename_no_ext(dirname(source_path))
+ local out_dir = dirname(source_path) .. "/../../build/gen/" .. dir_basename
+ local out_txt = out_dir .. "/" .. basename .. ".annotations.txt"
+ local out_err = out_dir .. "/" .. basename .. ".errors.h"
- local result = validate(source_path, word_counts)
- result.source = source_path
+ local result = validate(source_path, word_counts)
+ result.source = source_path
- -- Decide whether this source contributes to the build at all. A source
- -- without atoms (e.g. mips.h, gte.h, gcc_asm.h) has nothing for the
- -- annotation DSL to validate — skip both report files. The previously
- -- emitted ones, if any, get pruned below.
- local has_atoms = #result.atoms > 0
+ -- Decide whether this source contributes to the build at all. A source
+ -- without atoms (e.g. mips.h, gte.h, gcc_asm.h) has nothing for the
+ -- annotation DSL to validate — skip both report files. The previously
+ -- emitted ones, if any, get pruned below.
+ local has_atoms = #result.atoms > 0
- print(string.format("[pass] %s%s", source_path,
- has_atoms and "" or " (no atoms - skip report)"))
+ -- print(string.format("[pass] %s%s", source_path,
+ -- has_atoms and "" or " (no atoms - skip report)"))
- if has_atoms then
- ensure_dir(out_dir)
- pruned_dirs[out_dir] = true
+ if has_atoms then
+ ensure_dir(out_dir)
+ pruned_dirs[out_dir] = true
- -- Per-source annotation report
- write_file(out_txt, render_source_report(source_path, result))
- print(string.format(" -> %s", out_txt))
+ -- Per-source annotation report
+ write_file(out_txt, render_source_report(source_path, result))
+ print(string.format(" -> %s", out_txt))
- -- gen/.errors.h with #error lines for any structural problems.
- -- The C build refuses to compile if any source produces errors via -include.
- local header_lines = {
- "// Auto-generated by tape_atom_annotation_pass.lua — DO NOT EDIT",
- "#pragma once",
- "",
- }
- for _, e in ipairs(result.errors) do
- header_lines[#header_lines + 1] =
- string.format('#error "annotation: %s (line %d)"', e.msg, e.line)
- end
- if #result.errors == 0 then
- header_lines[#header_lines + 1] = "// annotation pass OK"
- end
- write_file(out_err, table.concat(header_lines, "\n") .. "\n")
- print(string.format(" -> %s", out_err))
+ -- gen/.errors.h with #error lines for any structural problems.
+ -- The C build refuses to compile if any source produces errors via -include.
+ local header_lines = {
+ "// Auto-generated by tape_atom_annotation_pass.lua — DO NOT EDIT",
+ "#pragma once",
+ "",
+ }
+ for _, e in ipairs(result.errors) do
+ header_lines[#header_lines + 1] =
+ string.format('#error "annotation: %s (line %d)"', e.msg, e.line)
+ end
+ if #result.errors == 0 then
+ header_lines[#header_lines + 1] = "// annotation pass OK"
+ end
+ write_file(out_err, table.concat(header_lines, "\n") .. "\n")
+ print(string.format(" -> %s", out_err))
- all_results[#all_results + 1] = result
- else
- -- No atoms: best-effort delete stale report files from a prior build.
- -- We only remove the ones we know belong to this source (filename is
- -- derived from the source path), never anything else in the dir.
- for _, stale in ipairs({ out_txt, out_err }) do
- local f = io.open(stale, "r")
- if f then f:close(); os.remove(stale) end
- end
- end
+ all_results[#all_results + 1] = result
+ else
+ -- No atoms: best-effort delete stale report files from a prior build.
+ -- We only remove the ones we know belong to this source (filename is
+ -- derived from the source path), never anything else in the dir.
+ for _, stale in ipairs({ out_txt, out_err }) do
+ local f = io.open(stale, "r")
+ if f then f:close(); os.remove(stale) end
+ end
+ end
- -- Emit the per-directory component-macros header (gen/.components.h)
- -- if this source has any MipsAtomComponent_ declarations. Independent of
- -- has_atoms: a header can have components without having full atoms.
- -- (TODO: pass source text into validate() to avoid the double-read.)
- local source_text = duffle.read_file(source_path)
- local components = find_component_atoms(source_text)
- if #components > 0 then
- emit_component_macros_h(source_path, components)
- end
- end
+ -- Emit the per-directory component-macros header (gen/.components.h)
+ -- if this source has any MipsAtomComp_ declarations. Independent of
+ -- has_atoms: a header can have components without having full atoms.
+ -- (TODO: pass source text into validate() to avoid the double-read.)
+ local source_text = duffle.read_file(source_path)
+ local components = find_component_atoms(source_text)
+ if #components > 0 then
+ emit_component_macros_h(source_path, components)
+ end
+ end
- -- Write project-level summary. Goes to build/gen/ (reporting cruft),
- -- not the source dir.
- local summary_path = dirname(args[2]) .. "/../../build/gen/annotation_validation.txt"
- ensure_dir(dirname(summary_path))
- write_file(summary_path, render_project_report(all_results))
- print(string.format("[summary] %s\n", summary_path))
+ -- Write project-level summary. Goes to build/gen/ (reporting cruft),
+ -- not the source dir.
+ local summary_path = dirname(args[2]) .. "/../../build/gen/annotation_validation.txt"
+ ensure_dir(dirname(summary_path))
+ write_file(summary_path, render_project_report(all_results))
+ print(string.format("[summary] %s\n", summary_path))
- -- Exit code
- local total_errors = 0
- for _, r in ipairs(all_results) do total_errors = total_errors + #r.errors end
- if total_errors > 0 then os.exit(1) end
+ -- Exit code
+ local total_errors = 0
+ for _, r in ipairs(all_results) do total_errors = total_errors + #r.errors end
+ if total_errors > 0 then os.exit(1) end
end
main({...})
\ No newline at end of file