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