diff --git a/code/duffle/atom_dsl.h b/code/duffle/dsl.atom.h similarity index 96% rename from code/duffle/atom_dsl.h rename to code/duffle/dsl.atom.h index 248f85a..092515b 100644 --- a/code/duffle/atom_dsl.h +++ b/code/duffle/dsl.atom.h @@ -1,5 +1,5 @@ /* - * atom_dsl.h + * dsl.atom.h * ============================================================================ * * ATOM DSL: Annotation layer for tape atoms (lottes_tape.h). @@ -57,7 +57,6 @@ #ifdef INTELLISENSE_DIRECTIVES #pragma once -// #include #endif /* ============================================================================ @@ -148,12 +147,12 @@ * ... body ... * atom_label(bounds_chk) ← another anchor * - * atom_offset(culling, bounds_chk) ← resolved by gen/.offsets.h + * atom_offset(culling, bounds_chk) ← resolved by gen/offsets.h * - * The metaprogram generates gen/atom_offsets.h with one #define with the offset value per atom_offset(F, T) call. + * The metaprogram generates gen/offsets.h with one #define with the offset value per atom_offset(F, T) call. * The preprocessor then expands the call to the right immediate value. * - * If gen/atom_offsets.h is stale (or atom_label(name) is undefined), `atom_offset_F_T` becomes an undefined macro and the C build fails. + * If gen/offsets.h is stale (or atom_label(name) is undefined), `atom_offset_F_T` becomes an undefined macro and the C build fails. * ============================================================================*/ #define atom_offset(F, T) atom_offset_ ## F ## _ ## T // atom_label is a pure annotation for the metaprogram's offset calculations. diff --git a/code/duffle/dsl.h b/code/duffle/dsl.h index b1095fa..1ed754b 100644 --- a/code/duffle/dsl.h +++ b/code/duffle/dsl.h @@ -43,7 +43,9 @@ #define R_ restrict #define V_ volatile -// Fictional, used for intiution. + +#pragma region Fictional //, used for intiution + #define EUB_ restrict // Execute Unit Bound: Data is siloed in the ALU Register File. The Load/Store Unit is bypassed. (Route to Execution Unit. Keep in registers) #define ISO_ restrict // Isolated Provenance: Alternative to Exu_. Guarantees electrical memory isolation, // unlocking the compiler’s ability to safely pack data across multiple parallel SIMD lanes (vectorization). @@ -67,7 +69,8 @@ #define latch_load_anchor(ptr) //__atomic_load_n(ptr, ooo_anchor_) #define latch_store_drain(ptr, val) //__atomic_store_n(ptr, val, ooo_drain_) #define pulse_xchg_weld(ptr, val) //__atomic_exchange_n(ptr, val, ooo_weld_) -//end of: Fictional. + +#pragma endreigon Fictional // R_ (restrict) establishes an "Eigen" or "Proprius" mapping. diff --git a/code/duffle/gen/duffle.macs.h b/code/duffle/gen/duffle.macs.h deleted file mode 100644 index 45a6fcf..0000000 --- a/code/duffle/gen/duffle.macs.h +++ /dev/null @@ -1,150 +0,0 @@ -#ifdef INTELLISENSE_DIRECTIVES -#pragma once -#endif -// Auto-generated by ps1_meta.lua — DO NOT EDIT -// Source: C:\projects\Pikuma\ps1\code\duffle\lottes_tape.h -// Component atoms (MipsAtomComp_(ac_*)) -> macro variants (mac_*) - -#ifndef WORD_COUNT -#define WORD_COUNT(name, count) enum { words_##name = (count) }; -#endif - -/* atom_dbg_skip */ -/* --------------------------------------------------------------------------- - * MACRO ATOM Components (Reusable Assembly Components) - * These do NOT yield. They are expanded inline inside Tape Atoms. - * ---------------------------------------------------------------------------*/ -// The 'Yield' sequence for Tape Atoms (mac_yield). -// - mac_yield() is the safe default for atom-endings: 4 words, BD-slot of jr is mandatory nop. -// - mac_yield_load() + mac_yield_tail(): -// - unconditional branch: mac_yield_load fills the branch's BD-slot (replaces a nop); -// - mac_yield_tail runs at the branch target (does NOT re-load R_AtomJmp). -#define mac_yield(...) \ - load_word(R_AtomJmp, R_TapePtr, 0) \ -, add_ui_self( R_TapePtr, S_(MipsCode)) \ -, jump_reg( R_AtomJmp) \ -, nop -WORD_COUNT(mac_yield, 4) - -/* atom_dbg_skip */ -#define mac_yield_load(...) \ - load_word(R_AtomJmp, R_TapePtr, 0) -WORD_COUNT(mac_yield_load, 1) - -/* atom_dbg_skip */ -#define mac_yield_tail(...) \ - add_ui_self(R_TapePtr, S_(MipsCode)) \ -, jump_reg( R_AtomJmp) \ -, nop -WORD_COUNT(mac_yield_tail, 3) - -/* atom_dbg_skip */ -/* Words: 3; Loads 3 S2 indices from the face array */ -#define mac_load_tri_indices(...) \ - load_half_u(R_T0, R_FaceCursor, 0 * S_(S2)) \ -, load_half_u(R_T1, R_FaceCursor, 1 * S_(S2)) \ -, load_half_u(R_T2, R_FaceCursor, 2 * S_(S2)) -WORD_COUNT(mac_load_tri_indices, 3) - -/* atom_dbg_skip */ -/* Words: 18; Translates indices to vertex addresses and pushes them to GTE */ -#define mac_gte_load_tri_verts(...) \ - shift_lleft(R_AT, R_T0, v3s2_byteoff) \ -, add_u_self(R_AT, R_VertBase) \ -, load_word(R_V0, R_AT, O_(V3_S2,x)) \ -, load_word(R_V1, R_AT, O_(V3_S2,z)) \ -, gte_mv_to_data_r(R_V0, C2_VXY0) \ -, gte_mv_to_data_r(R_V1, C2_VZ0) \ -, shift_lleft(R_AT, R_T1, v3s2_byteoff) \ -, add_u_self(R_AT, R_VertBase) \ -, load_word(R_V0, R_AT, O_(V3_S2,x)) \ -, load_word(R_V1, R_AT, O_(V3_S2,z)) \ -, gte_mv_to_data_r(R_V0, C2_VXY1) \ -, gte_mv_to_data_r(R_V1, C2_VZ1) \ -, shift_lleft(R_AT, R_T2, v3s2_byteoff) \ -, add_u_self(R_AT, R_VertBase) \ -, load_word(R_V0, R_AT, O_(V3_S2,x)) \ -, load_word(R_V1, R_AT, O_(V3_S2,z)) \ -, gte_mv_to_data_r(R_V0, C2_VXY2) \ -, gte_mv_to_data_r(R_V1, C2_VZ2) -WORD_COUNT(mac_gte_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_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,code)) /* 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,code)) /* 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,code)) /* OrderingTable[OTZ] = PrimCursor */ -WORD_COUNT(mac_insert_ot_tag_f3, 11) - -/* 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_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,code)) /* 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,code)) /* 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,code)) /* OrderingTable[OTZ] = PrimCursor */ -WORD_COUNT(mac_insert_ot_tag_g4, 11) - -/* atom_dbg_skip */ -#define mac_pack_color_word(off, cmd, r, g, b) \ - load_upper_i(R_AT, (cmd) << 8 | (b)) \ -, or_i_self( R_AT, ((g) << 8) | (r)) \ -, store_word( R_AT, R_PrimCursor, (off)) -WORD_COUNT(mac_pack_color_word, 3) - -/* atom_dbg_skip */ -#define mac_format_f3_color(r, g, b) \ - mac_pack_color_word(O_(Poly_F3,color), gp0_cmd_poly_f3, r, g, b) -WORD_COUNT(mac_format_f3_color, 3) - -/* atom_dbg_skip */ -/* 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). */ -#define mac_gte_store_f3(...) \ - gte_sw(C2_SXY0, R_PrimCursor, O_(Poly_F3,p0)) \ -, gte_sw(C2_SXY1, R_PrimCursor, O_(Poly_F3,p1)) \ -, gte_sw(C2_SXY2, R_PrimCursor, O_(Poly_F3,p2)) -WORD_COUNT(mac_gte_store_f3, 3) - -#define mac_format_g4_color(r0, g0, b0, r1, g1, b1, r2, g2, b2, r3, g3, b3) \ - 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) \ -, mac_pack_color_word(O_(Poly_G4,c2), 0, r2,g2,b2) \ -, mac_pack_color_word(O_(Poly_G4,c3), 0, r3,g3,b3) -WORD_COUNT(mac_format_g4_color, 12) - -/* atom_dbg_skip */ -/* Words: 3; Stores the 3 transformed (V2_S2 screen) vertices of the - * G4 triangle portion to p0/p1/p2. - * PIPELINE: post-RTPT, pre-RTPS (SXY0=v0.screen, SXY1=v1.screen, SXY2=v2.screen). - * MUST be called BEFORE V3-RTPS, otherwise SXY0/1/2 get overwritten with v3 - * (RTPS writes only to SXY2, but to keep the three registers aligned with v0/v1/v2 you must store before RTPS). */ -#define mac_gte_store_g4_p012(...) \ - gte_sw(C2_SXY0, R_PrimCursor, O_(Poly_G4,p0)) \ -, gte_sw(C2_SXY1, R_PrimCursor, O_(Poly_G4,p1)) \ -, gte_sw(C2_SXY2, R_PrimCursor, O_(Poly_G4,p2)) -WORD_COUNT(mac_gte_store_g4_p012, 3) - -/* atom_dbg_skip */ -/* Words: 1; Stores the V3 screen coord to the G4's p3 slot. - * PIPELINE: post-RTPS (SXY2 holds v3.screen because RTPS writes its single-vertex result to SXY2; - * SXY0 still holds v0.screen from the earlier RTPT. - */ -#define mac_gte_store_g4_p3(...) \ - gte_sw(C2_SXY2, R_PrimCursor, O_(Poly_G4,p3)) -WORD_COUNT(mac_gte_store_g4_p3, 1) - diff --git a/code/duffle/gen/duffle.offsets.h b/code/duffle/gen/duffle.offsets.h deleted file mode 100644 index eabc7da..0000000 --- a/code/duffle/gen/duffle.offsets.h +++ /dev/null @@ -1,9 +0,0 @@ -// Auto-generated by ps1_meta.lua (passes/offsets.lua) — DO NOT EDIT -// Source: C:\projects\Pikuma\ps1\code\duffle\lottes_tape.h -#pragma once - -#pragma region lottes_tape - - -#pragma endregion lottes_tape - diff --git a/code/duffle/gen/macs.h b/code/duffle/gen/macs.h new file mode 100644 index 0000000..3c33ef1 --- /dev/null +++ b/code/duffle/gen/macs.h @@ -0,0 +1,183 @@ +#ifdef INTELLISENSE_DIRECTIVES +#pragma once +#endif +// Auto-generated by ps1_meta.lua — DO NOT EDIT +// Directory: C:\projects\Pikuma\ps1\code\duffle/ +// source: C:\projects\Pikuma\ps1\code\duffle\word_count.metadata.h +// source: C:\projects\Pikuma\ps1\code\duffle\dsl.h +// source: C:\projects\Pikuma\ps1\code\duffle\memory.h +// source: C:\projects\Pikuma\ps1\code\duffle\math.h +// source: C:\projects\Pikuma\ps1\code\duffle\gcc_asm.h +// source: C:\projects\Pikuma\ps1\code\duffle\mips.h +// source: C:\projects\Pikuma\ps1\code\duffle\gp.h +// source: C:\projects\Pikuma\ps1\code\duffle\gte.h +// source: C:\projects\Pikuma\ps1\code\duffle\pad.h +// source: C:\projects\Pikuma\ps1\code\duffle\dsl.atom.h +// source: C:\projects\Pikuma\ps1\code\duffle\lottes_tape.h +// source: C:\projects\Pikuma\ps1\code\duffle\psyq.h +// source: C:\projects\Pikuma\ps1\code\duffle\math.atom.c +// source: C:\projects\Pikuma\ps1\code\duffle\mips.atom.c +// source: C:\projects\Pikuma\ps1\code\duffle\gte.atom.c +// source: C:\projects\Pikuma\ps1\code\duffle\gp.atom.c +// source: C:\projects\Pikuma\ps1\code\duffle\psyq.atom.c +// Component atoms (MipsAtomComp_(ac_*)) -> macro variants (mac_*) + +#ifndef WORD_COUNT +#define WORD_COUNT(name, count) enum { words_##name = (count) }; +#endif + +/* atom_dbg_skip */ +/* --------------------------------------------------------------------------- + * MACRO ATOM Components (Reusable Assembly Components) + * These do NOT yield. They are expanded inline inside Tape Atoms. + * ---------------------------------------------------------------------------*/ +// The 'Yield' sequence for Tape Atoms (mac_yield). +// - mac_yield() is the safe default for atom-endings: 4 words, BD-slot of jr is mandatory nop. +// - mac_yield_load() + mac_yield_tail(): +// - unconditional branch: mac_yield_load fills the branch's BD-slot (replaces a nop); +// - mac_yield_tail runs at the branch target (does NOT re-load R_AtomJmp). +#define mac_yield(...) \ + load_word(R_AtomJmp, R_TapePtr, 0) \ +, add_ui_self( R_TapePtr, S_(MipsCode)) \ +, jump_reg( R_AtomJmp) \ +, nop +WORD_COUNT(mac_yield, 4) + +/* atom_dbg_skip */ +#define mac_yield_load(...) \ + load_word(R_AtomJmp, R_TapePtr, 0) +WORD_COUNT(mac_yield_load, 1) + +/* atom_dbg_skip */ +#define mac_yield_tail(...) \ + add_ui_self(R_TapePtr, S_(MipsCode)) \ +, jump_reg( R_AtomJmp) \ +, nop +WORD_COUNT(mac_yield_tail, 3) + +/* atom_dbg_skip */ +#define mac_load_v2s2(rs_x, rs_y, r_base, offset) \ + load_half( rs_x, r_base, O_(V3_S2,x)) \ +, load_half( rs_y, r_base, O_(V3_S2,y)) +WORD_COUNT(mac_load_v2s2, 2) + +/* atom_dbg_skip */ +#define mac_store_v2s2(rt_x, rt_y, base, offset) \ + store_half(rt_x, base, offset + O_(V2_S2,x)) \ +, store_half(rt_y, base, offset + O_(V2_S2,y)) +WORD_COUNT(mac_store_v2s2, 2) + +/* atom_dbg_skip */ +#define mac_store_rects2(rt_x, rt_y, rt_width, rt_height, base, offset) \ + store_half(rt_x, base, offset + O_(Rect_S2,x)) \ +, store_half(rt_y, base, offset + O_(Rect_S2,y)) \ +, store_half(rt_width, base, offset + O_(Rect_S2,width)) \ +, store_half(rt_height, base, offset + O_(Rect_S2,height)) +WORD_COUNT(mac_store_rects2, 4) + +/* atom_dbg_skip */ +#define mac_load_tri_indices(r_face_cusor, r_i0, r_i1, r_i2) \ + load_half_u(r_i0, r_face_cusor, 0 * S_(S2)) \ +, load_half_u(r_i1, r_face_cusor, 1 * S_(S2)) \ +, load_half_u(r_i2, r_face_cusor, 2 * S_(S2)) +WORD_COUNT(mac_load_tri_indices, 3) + +/* atom_dbg_skip */ +#define mac_gte_store_f3(r_primitive_cursor) \ + gte_sw(C2_SXY0, r_primitive_cursor, O_(Poly_F3,p0)) \ +, gte_sw(C2_SXY1, r_primitive_cursor, O_(Poly_F3,p1)) \ +, gte_sw(C2_SXY2, r_primitive_cursor, O_(Poly_F3,p2)) +WORD_COUNT(mac_gte_store_f3, 3) + +/* atom_dbg_skip */ +#define mac_gte_load_tri_verts(r_vert_base, r_v0, r_v1, r_v2) \ + shift_lleft(R_AT, r_v0, v3s2_byteoff) \ +, add_u_self(R_AT, r_vert_base) \ +, load_word(R_V0, R_AT, O_(V3_S2,x)) \ +, load_word(R_V1, R_AT, O_(V3_S2,z)) \ +, gte_mv_to_data_r(R_V0, C2_VXY0) \ +, gte_mv_to_data_r(R_V1, C2_VZ0) \ +, shift_lleft(R_AT, r_v1, v3s2_byteoff) \ +, add_u_self(R_AT, r_vert_base) \ +, load_word(R_V0, R_AT, O_(V3_S2,x)) \ +, load_word(R_V1, R_AT, O_(V3_S2,z)) \ +, gte_mv_to_data_r(R_V0, C2_VXY1) \ +, gte_mv_to_data_r(R_V1, C2_VZ1) \ +, shift_lleft(R_AT, r_v2, v3s2_byteoff) \ +, add_u_self(R_AT, r_vert_base) \ +, load_word(R_V0, R_AT, O_(V3_S2,x)) \ +, load_word(R_V1, R_AT, O_(V3_S2,z)) \ +, gte_mv_to_data_r(R_V0, C2_VXY2) \ +, gte_mv_to_data_r(R_V1, C2_VZ2) +WORD_COUNT(mac_gte_load_tri_verts, 18) + +/* atom_dbg_skip */ +#define mac_gte_store_g4_p012(r_primitive_cursor) \ + gte_sw(C2_SXY0, r_primitive_cursor, O_(Poly_G4,p0)) \ +, gte_sw(C2_SXY1, r_primitive_cursor, O_(Poly_G4,p1)) \ +, gte_sw(C2_SXY2, r_primitive_cursor, O_(Poly_G4,p2)) +WORD_COUNT(mac_gte_store_g4_p012, 3) + +/* atom_dbg_skip */ +#define mac_gte_store_g4_p3(r_primitive_cursor) \ + gte_sw(C2_SXY2, r_primitive_cursor, O_(Poly_G4,p3)) +WORD_COUNT(mac_gte_store_g4_p3, 1) + +#define mac_gcmd_push(cmd, reg_transfer, reg_base, port) \ + load_upper_i(reg_transfer, cmd >> 16) \ +, or_i_self( reg_transfer, cmd & 0xFFFF) \ +, store_word( reg_transfer, reg_base, port) +WORD_COUNT(mac_gcmd_push, 3) + +/* atom_dbg_skip */ +#define mac_store_rgb8(rr, rg, rb, base, offset) \ + store_byte(rr, base, offset + O_(RGB8,r)) \ +, store_byte(rg, base, offset + O_(RGB8,g)) \ +, store_byte(rb, base, offset + O_(RGB8,b)) +WORD_COUNT(mac_store_rgb8, 3) + +/* atom_dbg_skip */ +#define mac_pack_color_word(r_base, off, cmd, r, g, b) \ + load_upper_i(R_AT, (cmd) << 8 | (b)) \ +, or_i_self( R_AT, ((g) << 8) | (r)) \ +, store_word( R_AT, r_base, (off)) +WORD_COUNT(mac_pack_color_word, 3) + +/* atom_dbg_skip */ +#define mac_format_f3_color(r_base, r, g, b) \ + mac_pack_color_word(r_base, O_(Poly_F3,color), gp0_cmd_poly_f3, r, g, b) +WORD_COUNT(mac_format_f3_color, 3) + +#define mac_format_g4_color(r_prim_cursor, r0, g0, b0, r1, g1, b1, r2, g2, b2, r3, g3, b3) \ + mac_pack_color_word(r_prim_cursor, O_(Poly_G4,c0), gp0_cmd_poly_g4, r0,g0,b0) \ +, mac_pack_color_word(r_prim_cursor, O_(Poly_G4,c1), 0, r1,g1,b1) \ +, mac_pack_color_word(r_prim_cursor, O_(Poly_G4,c2), 0, r2,g2,b2) \ +, mac_pack_color_word(r_prim_cursor, O_(Poly_G4,c3), 0, r3,g3,b3) +WORD_COUNT(mac_format_g4_color, 12) + +#define mac_insert_ot_tag_f3(r_ot_base, r_prim_cursor) \ + 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_ot_base) /* T1 = & OrderingTable[OTZ] */ \ +, load_word( R_AT, R_T1, O_(PolyTag,code)) /* 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_prim_cursor, O_(PolyTag,code)) /* prim->tag = packed(prim_length, old_addr) */ \ +, shift_lleft( R_AT, r_prim_cursor, 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,code)) /* OrderingTable[OTZ] = PrimCursor */ +WORD_COUNT(mac_insert_ot_tag_f3, 11) + +#define mac_insert_ot_tag_g4(r_ot_base, r_prim_cursor) \ + 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_ot_base) /* T1 = & OrderingTable[OTZ] */ \ +, load_word( R_AT, R_T1, O_(PolyTag,code)) /* 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_prim_cursor, O_(PolyTag,code)) /* prim->tag = packed(prim_length, old_addr) */ \ +, shift_lleft( R_AT, r_prim_cursor, 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,code)) /* OrderingTable[OTZ] = PrimCursor */ +WORD_COUNT(mac_insert_ot_tag_g4, 11) + diff --git a/code/duffle/gen/offsets.h b/code/duffle/gen/offsets.h new file mode 100644 index 0000000..667aaea --- /dev/null +++ b/code/duffle/gen/offsets.h @@ -0,0 +1,26 @@ +// Auto-generated by ps1_meta.lua (passes/offsets.lua) — DO NOT EDIT +// Directory: C:\projects\Pikuma\ps1\code\duffle\ +// source: C:\projects\Pikuma\ps1\code\duffle\word_count.metadata.h +// source: C:\projects\Pikuma\ps1\code\duffle\dsl.h +// source: C:\projects\Pikuma\ps1\code\duffle\memory.h +// source: C:\projects\Pikuma\ps1\code\duffle\math.h +// source: C:\projects\Pikuma\ps1\code\duffle\gcc_asm.h +// source: C:\projects\Pikuma\ps1\code\duffle\mips.h +// source: C:\projects\Pikuma\ps1\code\duffle\gp.h +// source: C:\projects\Pikuma\ps1\code\duffle\gte.h +// source: C:\projects\Pikuma\ps1\code\duffle\pad.h +// source: C:\projects\Pikuma\ps1\code\duffle\dsl.atom.h +// source: C:\projects\Pikuma\ps1\code\duffle\lottes_tape.h +// source: C:\projects\Pikuma\ps1\code\duffle\psyq.h +// source: C:\projects\Pikuma\ps1\code\duffle\math.atom.c +// source: C:\projects\Pikuma\ps1\code\duffle\mips.atom.c +// source: C:\projects\Pikuma\ps1\code\duffle\gte.atom.c +// source: C:\projects\Pikuma\ps1\code\duffle\gp.atom.c +// source: C:\projects\Pikuma\ps1\code\duffle\psyq.atom.c +#pragma once + +#pragma region duffle + + +#pragma endregion duffle + diff --git a/code/duffle/gp.atom.c b/code/duffle/gp.atom.c new file mode 100644 index 0000000..91892e4 --- /dev/null +++ b/code/duffle/gp.atom.c @@ -0,0 +1,82 @@ +#ifdef INTELLISENSE_DIRECTIVES +# include "dsl.h" +# include "gp.h" +# include "lottes_tape.h" +#endif + +ATOM_FILE_DEBUGGER_LINE_MARKER(gp_atom_c); + +#pragma region MACs (Mips Atom Components) + +FI_ Slice_MipsCode ac_gcmd_push(U4 cmd, U4 reg_transfer, U4 reg_base, U2 port) +MipsAtomComp_Proc_(ac_gcmd_push, { + load_upper_i(reg_transfer, cmd >> 16), + or_i_self( reg_transfer, cmd & 0xFFFF), + store_word( reg_transfer, reg_base, port), +}) + +FI_ Slice_MipsCode ac_store_rgb8(U1 rr, U1 rg, U1 rb, U4 base, U4 offset) atom_dbg_skip MipsAtomComp_Proc_(ac_store_rgb8, { + store_byte(rr, base, offset + O_(RGB8,r)), + store_byte(rg, base, offset + O_(RGB8,g)), + store_byte(rb, base, offset + O_(RGB8,b)), +}) + +/* Words: 3; Emits one (cmd|color) word to R_PrimCursor at the given + * byte offset. Internal helper used by the *_format_*_color macros. */ +FI_ Slice_MipsCode ac_pack_color_word(U4 r_base, U4 off, U4 cmd, U1 r, U1 g, U1 b) +atom_dbg_skip MipsAtomComp_Proc_(ac_pack_color_word, { + load_upper_i(R_AT, (cmd) << 8 | (b)), + or_i_self( R_AT, ((g) << 8) | (r)), + store_word( R_AT, r_base, (off)), +}) + +/* 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). */ +FI_ Slice_MipsCode ac_format_f3_color(U4 r_base, U1 r, U1 g, U1 b) +atom_dbg_skip MipsAtomComp_Proc_(ac_format_f3_color, { mac_pack_color_word(r_base, O_(Poly_F3,color), gp0_cmd_poly_f3, r, g, b) }) + +/* 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_ Slice_MipsCode ac_format_g4_color(U4 r_prim_cursor, + 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(r_prim_cursor, O_(Poly_G4,c0), gp0_cmd_poly_g4, r0,g0,b0), + mac_pack_color_word(r_prim_cursor, O_(Poly_G4,c1), 0, r1,g1,b1), + mac_pack_color_word(r_prim_cursor, O_(Poly_G4,c2), 0, r2,g2,b2), + mac_pack_color_word(r_prim_cursor, O_(Poly_G4,c3), 0, r3,g3,b3), +}) + +/* 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. */ +I_ Slice_MipsCode ac_insert_ot_tag_f3(U4 r_ot_base, U4 r_prim_cursor) MipsAtomComp_Proc_(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_ot_base), // T1 = & OrderingTable[OTZ] + load_word( R_AT, R_T1, O_(PolyTag,code)), // 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_prim_cursor, O_(PolyTag,code)), // prim->tag = packed(prim_length, old_addr) + shift_lleft( R_AT, r_prim_cursor, 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,code)), // OrderingTable[OTZ] = PrimCursor +}) + +/* 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. */ +I_ Slice_MipsCode ac_insert_ot_tag_g4(U4 r_ot_base, U4 r_prim_cursor) MipsAtomComp_Proc_(ac_insert_ot_tag_g4, { + 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_ot_base), // T1 = & OrderingTable[OTZ] + load_word( R_AT, R_T1, O_(PolyTag,code)), // 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_prim_cursor, O_(PolyTag,code)), // prim->tag = packed(prim_length, old_addr) + shift_lleft( R_AT, r_prim_cursor, 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,code)), // OrderingTable[OTZ] = PrimCursor +}) + +#pragma endregion MACs (Mips Atom Components) diff --git a/code/duffle/gp.tape.c b/code/duffle/gp.tape.c deleted file mode 100644 index e69de29..0000000 diff --git a/code/duffle/gte.atom.c b/code/duffle/gte.atom.c new file mode 100644 index 0000000..eb131ce --- /dev/null +++ b/code/duffle/gte.atom.c @@ -0,0 +1,76 @@ +#ifdef INTELLISENSE_DIRECTIVES +# include "gen/macs.h" +# include "gen/offsets.h" +# include "gte.h" +# include "gp.h" +# include "lottes_tape.h" +#endif + +ATOM_FILE_DEBUGGER_LINE_MARKER(gte_atom_c); + +#pragma region MACs (Mips Atom Components) + +/* Words: 3; Loads 3 S2 indices from the face array */ +FI_ Slice_MipsCode ac_load_tri_indices(U4 r_face_cusor, U4 r_i0, U4 r_i1, U4 r_i2) atom_dbg_skip MipsAtomComp_Proc_(ac_load_tri_indices, { + load_half_u(r_i0, r_face_cusor, 0 * S_(S2)), + load_half_u(r_i1, r_face_cusor, 1 * S_(S2)), + load_half_u(r_i2, r_face_cusor, 2 * S_(S2)), +}) + +/* 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). */ +FI_ Slice_MipsCode ac_gte_store_f3(U4 r_primitive_cursor) atom_dbg_skip MipsAtomComp_Proc_(ac_gte_store_f3, { + gte_sw(C2_SXY0, r_primitive_cursor, O_(Poly_F3,p0)), + gte_sw(C2_SXY1, r_primitive_cursor, O_(Poly_F3,p1)), + gte_sw(C2_SXY2, r_primitive_cursor, O_(Poly_F3,p2)), +}) + +/* Words: 18; Translates indices to vertex addresses and pushes them to GTE */ +I_ Slice_MipsCode ac_gte_load_tri_verts(U4 r_vert_base, U4 r_v0, U4 r_v1, U4 r_v2) atom_dbg_skip MipsAtomComp_Proc_(ac_gte_load_tri_verts, { + shift_lleft(R_AT, r_v0, v3s2_byteoff), add_u_self(R_AT, r_vert_base), load_word(R_V0, R_AT, O_(V3_S2,x)), load_word(R_V1, R_AT, O_(V3_S2,z)), gte_mv_to_data_r(R_V0, C2_VXY0), gte_mv_to_data_r(R_V1, C2_VZ0), + shift_lleft(R_AT, r_v1, v3s2_byteoff), add_u_self(R_AT, r_vert_base), load_word(R_V0, R_AT, O_(V3_S2,x)), load_word(R_V1, R_AT, O_(V3_S2,z)), gte_mv_to_data_r(R_V0, C2_VXY1), gte_mv_to_data_r(R_V1, C2_VZ1), + shift_lleft(R_AT, r_v2, v3s2_byteoff), add_u_self(R_AT, r_vert_base), load_word(R_V0, R_AT, O_(V3_S2,x)), load_word(R_V1, R_AT, O_(V3_S2,z)), gte_mv_to_data_r(R_V0, C2_VXY2), gte_mv_to_data_r(R_V1, C2_VZ2), +}) + +/* Words: 3; Stores the 3 transformed (V2_S2 screen) vertices of the + * G4 triangle portion to p0/p1/p2. + * PIPELINE: post-RTPT, pre-RTPS (SXY0=v0.screen, SXY1=v1.screen, SXY2=v2.screen). + * MUST be called BEFORE V3-RTPS, otherwise SXY0/1/2 get overwritten with v3 + * (RTPS writes only to SXY2, but to keep the three registers aligned with v0/v1/v2 you must store before RTPS). */ +FI_ Slice_MipsCode ac_gte_store_g4_p012(U4 r_primitive_cursor) atom_dbg_skip MipsAtomComp_Proc_(ac_gte_store_g4_p012, { + gte_sw(C2_SXY0, r_primitive_cursor, O_(Poly_G4,p0)), + gte_sw(C2_SXY1, r_primitive_cursor, O_(Poly_G4,p1)), + gte_sw(C2_SXY2, r_primitive_cursor, O_(Poly_G4,p2)), +}) + +/* Words: 1; Stores the V3 screen coord to the G4's p3 slot. + * PIPELINE: post-RTPS (SXY2 holds v3.screen because RTPS writes its single-vertex result to SXY2; + * SXY0 still holds v0.screen from the earlier RTPT. + */ +FI_ Slice_MipsCode ac_gte_store_g4_p3(U4 r_primitive_cursor) atom_dbg_skip MipsAtomComp_Proc_(ac_gte_store_g4_p3, { gte_sw(C2_SXY2, r_primitive_cursor, O_(Poly_G4,p3)) }) + +#pragma endregion MACs (Mips Atom Components) + +#pragma region Bsked Atoms + +typedef Struct_(Binds_SetGteWorld) { + M3_S2* transform; +}; +internal MipsAtom_(set_gte_world) atom_info( + atom_bind(Binds_SetGteWorld) + , atom_reads(R_TapePtr) +){ + /* Pop matrix address from tape into R_T3 ($11) */ + load_word(R_T3, R_TapePtr, O_(Binds_SetGteWorld,transform)), + add_ui_self( R_TapePtr, S_(Binds_SetGteWorld)), + /* Load 3x3 Rotation + 3x1 Translation from R_T3 into GTE CONTROL Regs (ctc2) */ + load_word(R_T0, R_T3, 0), load_word(R_T1, R_T3, 4), + gte_mv_to_ctrl_r(R_T0, gte_cr_RT11), gte_mv_to_ctrl_r(R_T1, gte_cr_RT12), + load_word(R_T0, R_T3, 8), load_word(R_T1, R_T3, 12), load_word(R_T2, R_T3, 16), + gte_mv_to_ctrl_r(R_T0, gte_cr_RT13), gte_mv_to_ctrl_r(R_T1, gte_cr_RT21), gte_mv_to_ctrl_r(R_T2, gte_cr_RT22), + load_word(R_T0, R_T3, 20), load_word(R_T1, R_T3, 24), load_word(R_T2, R_T3, 28), + gte_mv_to_ctrl_r(R_T0, gte_cr_TRX), gte_mv_to_ctrl_r(R_T1, gte_cr_TRY), gte_mv_to_ctrl_r(R_T2, gte_cr_TRZ), + mac_yield() +}; + +#pragma endregion Baked Atoms diff --git a/code/duffle/gte.tape.c b/code/duffle/gte.tape.c deleted file mode 100644 index e69de29..0000000 diff --git a/code/duffle/lottes_tape.h b/code/duffle/lottes_tape.h index 581d4da..36dae7d 100644 --- a/code/duffle/lottes_tape.h +++ b/code/duffle/lottes_tape.h @@ -1,13 +1,14 @@ #ifdef INTELLISENSE_DIRECTIVES # pragma once +# include "gen/macs.h" +# include "gen/offsets.h" + # include "dsl.h" # include "gcc_asm.h" # include "mips.h" # include "gte.h" # include "memory.h" -# include "atom_dsl.h" -# include "gen/duffle.macs.h" -# include "gen/duffle.offsets.h" +# include "dsl.atom.h" #endif typedef U4 const MipsCode; // Underlying type to mips asm words. @@ -28,7 +29,19 @@ typedef U4 const MipsAtom; // Underlying type to an array of mips asm words that // FI_ Slice_MipsCode 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 (/gen//.macs.h) are included manually by the unity build. +/* Line-table anchor: gcc only adds a file to the .debug_line file table when the + file contains line-numbered content. Files containing only: + - `MipsAtomComp_` static-array declarations, or + - `MipsAtomComp_Proc_` (force-inline) function bodies whose line info gets + attributed to the call site at the include point are otherwise omitted from the file table, + which breaks the DWARF injection when it tries to resolve atom-component provenance paths. + + Place `ATOM_FILE_LINE_MARKER();` once at file scope in any `.atom.c` that defines atoms. + The macro expands to a file-scope `internal U4 const` declaration keeps the file in the line table. + The constant is in `.rodata` and unreferenced; the linker may eliminate it. + The two-level concat + `__LINE__` suffix makes the identifier unique per call site + (the identifier embeds the source line, so duplicates across `#include`d files don't collide). */ +#define ATOM_FILE_DEBUGGER_LINE_MARKER(file_name) internal U4 const tmpl(atom_file_debugger_line_marker,file_name) = 0 /* Register aliases */ enum { @@ -59,15 +72,11 @@ enum { R_TScratch6 = R_T6, R_TScratch7 = R_T7, R_TScratch8 = R_T8, - R_TScratch10 = R_V0, - R_TScratch11 = R_V1, + R_TScratch10 = R_V0, // Tend to be used with gte DMAs + R_TScratch11 = R_V1, // Tend to be used with gte DMAs // Note(Ed): We can technically clobber these, but don't unless we hit a bottleneck. - // R_TScratch12 = R_A0, - // R_TScratch13 = R_A1, - // R_TScratch14 = R_A3, - // TODO(Ed): Review S0-S7, they are technically avaialble, we just have to snapshot them at the ABI boundary. - // TODO(Ed): This is technically a waste of cycles for most work? so maybe only do this for expensive atoms on-demand or atom phases. -// TODO(Ed): Sort out the other available registers... (Not sure how much is left avail) + // A 0-2 + // S 0-7 }; #pragma region Tape Drive @@ -79,7 +88,6 @@ typedef Slice_(MipsAtom); typedef Slice_MipsAtom Tape; /* The 'Exit' Atom */ atom_dbg_skip MipsAtom_(tape_exit) { jump_reg(rret_addr), nop }; -//TODO(Ed): Do we backup R_S0-7 here? Have it in a heavier tape run as a opt-in? Same with V0-1 and A0-3? /* Generalized Tape Engine Runner */ FI_ void tape_run(Tape tape) { register U4* tape_ptr rgcc(R_TapePtr) = u4_r(tape.ptr); asm volatile( asm_words( @@ -97,6 +105,26 @@ FI_ void tape_run(Tape tape) { register U4* tape_ptr rgcc(R_TapePtr) = u4_r(tape clb_mem_drain ); } +/* Fully Clobbered Tape */ +FI_ void tape_run_a02_s07(Tape tape) { register U4* tape_ptr rgcc(R_TapePtr) = u4_r(tape.ptr); asm volatile( + asm_words( + load_word( R_AtomJmp, R_TapePtr, 0) /* Bootstrap the first jump */ + , add_ui_self(R_TapePtr, S_(MipsAtom)) /* Advance tape */ + , call_reg( R_AtomJmp) /* jalr $t9 */ + , nop /* Branch delay slot */ + ) + asm_rpins, r_use(tape_ptr) + asm_clobber: + rlit(R_AT), + rlit(R_V0), rlit(R_V1), rlit(R_A0), rlit(R_A1), rlit(R_A2), + rlit(R_T0), rlit(R_T1), rlit(R_T2), rlit(R_T3), rlit(R_T4), + rlit(R_T5), rlit(R_T6), rlit(R_T7), rlit(R_T8), + rlit(R_S0), rlit(R_S1), rlit(R_S2), rlit(R_S3), rlit(R_S4), + rlit(R_S5), rlit(R_S6), rlit(R_S7), + clb_mem_drain +); } + +// Procedural authoring of tapes: typedef Relative_(FArena) Struct_(TapeBuilder) { U4 ptr; U4 capacity; U4 used; }; FI_ void tb_init(TapeBuilder* tb, FArena* arena) { tb->ptr = arena->start; tb->used = 0; } FI_ TapeBuilder tb_make_old( FArena* arena) { return (TapeBuilder){ arena->start, 0 }; } @@ -113,7 +141,6 @@ FI_ Tape tb_slice(TapeBuilder tb) { return (Tape){ C_(U4 FI_ void tb_scope_run_end(TapeBuilder* tb) { tb_emit(tb,tape_exit); tape_run(tb_slice(tb[0])); } #define tb_scope_run(tb) for(U4 tbs_once=0;tbs_once==0;++tbs_once,tb_scope_run_end(tb)) - #pragma endregion Tape Drive #pragma region Macro Mips Atom Components @@ -143,118 +170,10 @@ atom_dbg_skip MipsAtomComp_(ac_yield_tail) { jump_reg( R_AtomJmp), nop, }; -enum { - R_PrimCursor = R_T7 atom_reg atom_type(U4*), /* VRAM output cursor (primitive buffer) */ - R_FaceCursor = R_T4 atom_reg atom_type(V4_S2*), /* Cube face-index cursor (V4_S2*); floor context switches to V3_S2* via atom_phase */ - R_VertBase = R_T5 atom_reg atom_type(V3_S2*), /* Base address of the vertex array */ - R_OtBase = R_T6 atom_reg atom_type(U4*), /* Base address of the Ordering Table */ -#define R_PrimCursor_Code R_T7_Code -#define R_FaceCursor_Code R_T4_Code -#define R_VertBase_Code R_T5_Code -#define R_OtBase_Code R_T6_Code -}; - -/* Words: 3; Loads 3 S2 indices from the face array */ -atom_dbg_skip MipsAtomComp_(ac_load_tri_indices) { - load_half_u(R_T0, R_FaceCursor, 0 * S_(S2)), - load_half_u(R_T1, R_FaceCursor, 1 * S_(S2)), - load_half_u(R_T2, R_FaceCursor, 2 * S_(S2)), -}; - -/* Words: 18; Translates indices to vertex addresses and pushes them to GTE */ -atom_dbg_skip MipsAtomComp_(ac_gte_load_tri_verts) { - shift_lleft(R_AT, R_T0, v3s2_byteoff), add_u_self(R_AT, R_VertBase), load_word(R_V0, R_AT, O_(V3_S2,x)), load_word(R_V1, R_AT, O_(V3_S2,z)), gte_mv_to_data_r(R_V0, C2_VXY0), gte_mv_to_data_r(R_V1, C2_VZ0), - shift_lleft(R_AT, R_T1, v3s2_byteoff), add_u_self(R_AT, R_VertBase), load_word(R_V0, R_AT, O_(V3_S2,x)), load_word(R_V1, R_AT, O_(V3_S2,z)), gte_mv_to_data_r(R_V0, C2_VXY1), gte_mv_to_data_r(R_V1, C2_VZ1), - shift_lleft(R_AT, R_T2, v3s2_byteoff), add_u_self(R_AT, R_VertBase), load_word(R_V0, R_AT, O_(V3_S2,x)), load_word(R_V1, R_AT, O_(V3_S2,z)), gte_mv_to_data_r(R_V0, C2_VXY2), gte_mv_to_data_r(R_V1, C2_VZ2), -}; - -/* 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,code)), // 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,code)), // 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,code)), // OrderingTable[OTZ] = PrimCursor -}; - -/* 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. */ -MipsAtomComp_(ac_insert_ot_tag_g4) { - 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,code)), // 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,code)), // 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,code)), // OrderingTable[OTZ] = PrimCursor -}; - -/* Words: 3; Emits one (cmd|color) word to R_PrimCursor at the given - * byte offset. Internal helper used by the *_format_*_color macros. */ -FI_ Slice_MipsCode ac_pack_color_word(U4 off, U4 cmd, U1 r, U1 g, U1 b) -atom_dbg_skip MipsAtomComp_Proc_(ac_pack_color_word, { - load_upper_i(R_AT, (cmd) << 8 | (b)), - or_i_self( R_AT, ((g) << 8) | (r)), - store_word( R_AT, R_PrimCursor, (off)), -}) - -/* 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). */ -FI_ Slice_MipsCode ac_format_f3_color(U1 r, U1 g, U1 b) -atom_dbg_skip 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). */ -atom_dbg_skip MipsAtomComp_(ac_gte_store_f3) { - gte_sw(C2_SXY0, R_PrimCursor, O_(Poly_F3,p0)), - gte_sw(C2_SXY1, R_PrimCursor, O_(Poly_F3,p1)), - gte_sw(C2_SXY2, R_PrimCursor, O_(Poly_F3,p2)), -}; - -/* 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_ Slice_MipsCode 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), - mac_pack_color_word(O_(Poly_G4,c2), 0, r2,g2,b2), - mac_pack_color_word(O_(Poly_G4,c3), 0, r3,g3,b3), -}) - -/* Words: 3; Stores the 3 transformed (V2_S2 screen) vertices of the - * G4 triangle portion to p0/p1/p2. - * PIPELINE: post-RTPT, pre-RTPS (SXY0=v0.screen, SXY1=v1.screen, SXY2=v2.screen). - * MUST be called BEFORE V3-RTPS, otherwise SXY0/1/2 get overwritten with v3 - * (RTPS writes only to SXY2, but to keep the three registers aligned with v0/v1/v2 you must store before RTPS). */ -atom_dbg_skip MipsAtomComp_(ac_gte_store_g4_p012) { - gte_sw(C2_SXY0, R_PrimCursor, O_(Poly_G4,p0)), - gte_sw(C2_SXY1, R_PrimCursor, O_(Poly_G4,p1)), - gte_sw(C2_SXY2, R_PrimCursor, O_(Poly_G4,p2)), -}; - -/* Words: 1; Stores the V3 screen coord to the G4's p3 slot. - * PIPELINE: post-RTPS (SXY2 holds v3.screen because RTPS writes its single-vertex result to SXY2; - * SXY0 still holds v0.screen from the earlier RTPT. - */ -atom_dbg_skip MipsAtomComp_(ac_gte_store_g4_p3) { gte_sw(C2_SXY2, R_PrimCursor, O_(Poly_G4,p3)) }; - #pragma endregion Macro Atom Components #pragma region Mips Atom Builder -// This allows for runtime procedural authoring of mips atoms. +// This helps with runtime procedural authoring of mips atoms. typedef Struct_(FMipsAtom512) { U4 data[512]; U4 used; }; @@ -277,57 +196,9 @@ FI_ void atombuilder_end(MipsAtomBuilder_R ab) { } #define mipsatom_from_builder(ab) (Slice_MipsCode){ab.start, ab.used} - #pragma endregion Mips Atom Builder #pragma region Baked Mips Atoms // These atoms are resolved at compile time and are (usually) statically linked readonly data. -enum { - bios_flushcache = 0x44, - bios_table_addr = 0xA0, -}; - -/* 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 - * 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), nop, // jalr $t0, $ra, 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(), -}; - -typedef Struct_(Binds_SetGteWorld) { - M3_S2* transform; -}; -internal MipsAtom_(set_gte_world) atom_info( - atom_bind(Binds_SetGteWorld) - , atom_reads(R_TapePtr) -){ - /* Pop matrix address from tape into R_T3 ($11) */ - load_word(R_T3, R_TapePtr, O_(Binds_SetGteWorld,transform)), - add_ui_self( R_TapePtr, S_(Binds_SetGteWorld)), - /* Load 3x3 Rotation + 3x1 Translation from R_T3 into GTE CONTROL Regs (ctc2) */ - load_word(R_T0, R_T3, 0), load_word(R_T1, R_T3, 4), - gte_mv_to_ctrl_r(R_T0, gte_cr_RT11), gte_mv_to_ctrl_r(R_T1, gte_cr_RT12), - load_word(R_T0, R_T3, 8), load_word(R_T1, R_T3, 12), load_word(R_T2, R_T3, 16), - gte_mv_to_ctrl_r(R_T0, gte_cr_RT13), gte_mv_to_ctrl_r(R_T1, gte_cr_RT21), gte_mv_to_ctrl_r(R_T2, gte_cr_RT22), - load_word(R_T0, R_T3, 20), load_word(R_T1, R_T3, 24), load_word(R_T2, R_T3, 28), - gte_mv_to_ctrl_r(R_T0, gte_cr_TRX), gte_mv_to_ctrl_r(R_T1, gte_cr_TRY), gte_mv_to_ctrl_r(R_T2, gte_cr_TRZ), - mac_yield() -}; - #pragma endregion Baked Mips Atoms diff --git a/code/duffle/math.atom.c b/code/duffle/math.atom.c new file mode 100644 index 0000000..c01fe66 --- /dev/null +++ b/code/duffle/math.atom.c @@ -0,0 +1,29 @@ +#ifdef INTELLISENSE_DIRECTIVES +# include "gen/macs.h" +# include "gen/offsets.h" +# include "math.h" +# include "lottes_tape.h" +#endif + +ATOM_FILE_DEBUGGER_LINE_MARKER(math_atom_c); + +#pragma region MACs (Mips Atom Component) + +FI_ Slice_MipsCode ac_load_v2s2(U4 rs_x, U4 rs_y, U4 r_base, U4 offset) atom_dbg_skip MipsAtomComp_Proc_(ac_load_v2s2, { + load_half( rs_x, r_base, O_(V3_S2,x)), + load_half( rs_y, r_base, O_(V3_S2,y)), +}) + +FI_ Slice_MipsCode ac_store_v2s2(U4 rt_x, U4 rt_y, U4 base, U4 offset) atom_dbg_skip MipsAtomComp_Proc_(ac_store_v2s2, { + store_half(rt_x, base, offset + O_(V2_S2,x)), + store_half(rt_y, base, offset + O_(V2_S2,y)), +}) + +FI_ Slice_MipsCode ac_store_rects2(U4 rt_x, U4 rt_y, U4 rt_width, U4 rt_height, U4 base, U4 offset) atom_dbg_skip MipsAtomComp_Proc_(ac_store_rects2, { + store_half(rt_x, base, offset + O_(Rect_S2,x)), + store_half(rt_y, base, offset + O_(Rect_S2,y)), + store_half(rt_width, base, offset + O_(Rect_S2,width)), + store_half(rt_height, base, offset + O_(Rect_S2,height)), +}) + +#pragma endregion MACs (Mips Atom Component) diff --git a/code/duffle/math.h b/code/duffle/math.h index 5fcec87..09fff5e 100644 --- a/code/duffle/math.h +++ b/code/duffle/math.h @@ -34,10 +34,10 @@ typedef Struct_(V4_S4) { S4 x; S4 y; S4 z; S4 w; }; typedef Struct_(R2_S2) { V2_S2 p0; V2_S2 p1; }; typedef Struct_(R2_S4) { V2_S4 p0; V2_S4 p1; }; -typedef Struct_(Rect_S2) { S2 x; S2 y; S2 width; S2 height; }; -typedef Struct_(Rect_S4) { S4 x; S4 y; S4 width; S4 height; }; +typedef Struct_(Rect_S2) { S2 x; S2 y; S2 width; S2 height; }; +typedef Struct_(Rect_S4) { S4 x; S4 y; S4 width; S4 height; }; -typedef Struct_(M3_S2) { A3x3_S2 m; A3_S4 t; }; +typedef Struct_(M3_S2) { A3x3_S2 m; A3_S4 t; }; typedef Array_(V2_S2, 2); typedef Array_(V2_S2, 3); @@ -61,10 +61,5 @@ FI_ void add_a3s4_fp(A3_S4_R out_a, A3_S4 b) { (out_a[0])[2] += b[2] >> 1; } -FI_ void add_v3s4(V3_S4_R out_a, V3_S4 b) { - add_a3s4(pcast(A3_S4_R, out_a), pcast(A3_S4, b)); -} - -FI_ void add_v3s4_fp(V3_S4_R out_a, V3_S4 b) { - add_a3s4_fp(pcast(A3_S4_R, out_a), pcast(A3_S4, b)); -} +FI_ void add_v3s4 (V3_S4_R out_a, V3_S4 b) { add_a3s4 (pcast(A3_S4_R, out_a), pcast(A3_S4, b)); } +FI_ void add_v3s4_fp(V3_S4_R out_a, V3_S4 b) { add_a3s4_fp(pcast(A3_S4_R, out_a), pcast(A3_S4, b)); } diff --git a/code/duffle/mips.atom.c b/code/duffle/mips.atom.c new file mode 100644 index 0000000..d7bfc89 --- /dev/null +++ b/code/duffle/mips.atom.c @@ -0,0 +1,38 @@ +#ifdef INTELLISENSE_DIRECTIVES +# include "gen/macs.h" +# include "gen/offsets.h" +# include "lottes_tape.h" +#endif + +ATOM_FILE_DEBUGGER_LINE_MARKER(mips_atom_c); + +#pragma region Baked Atoms + +enum { + bios_flushcache = 0x44, + bios_table_addr = 0xA0, +}; + +/* 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 + * 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), nop, // jalr $t0, $ra, 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(), +}; + +#pragma endregion Baked Atoms diff --git a/code/duffle/pad.atom.c b/code/duffle/pad.atom.c new file mode 100644 index 0000000..12688a8 --- /dev/null +++ b/code/duffle/pad.atom.c @@ -0,0 +1,8 @@ +#ifdef INTELLISSENSE_DIRECTIVES +# include "gen/macs.h" +# include "gen/offsets.h" +#endif + +ATOM_FILE_DEBUGGER_LINE_MARKER(pad_atom_c); + + diff --git a/code/duffle/pad.tape.c b/code/duffle/pad.tape.c deleted file mode 100644 index e69de29..0000000 diff --git a/code/duffle/psyq.atom.c b/code/duffle/psyq.atom.c new file mode 100644 index 0000000..b4dced3 --- /dev/null +++ b/code/duffle/psyq.atom.c @@ -0,0 +1,7 @@ +#ifdef INTELLISENSE_DIRECTIVES +# include "gen/macs.h" +# include "gen/offsets.h" +# include "psyq.h" +#endif + +ATOM_FILE_DEBUGGER_LINE_MARKER(pysq_atom_c); diff --git a/code/hello_joypad/psyq.h b/code/duffle/psyq.h similarity index 97% rename from code/hello_joypad/psyq.h rename to code/duffle/psyq.h index c94a00f..da885e6 100644 --- a/code/hello_joypad/psyq.h +++ b/code/duffle/psyq.h @@ -1,8 +1,8 @@ #ifdef INTELLISENSE_DIRECTIVES # pragma once -# include "duffle/dsl.h" -# include "duffle/math.h" -# include "duffle/gp.h" +# include "dsl.h" +# include "math.h" +# include "gp.h" #endif typedef Struct_(DrawEnv_Packed) { U4 tag; U4 code[15]; }; diff --git a/code/duffle/word_count.metadata.h b/code/duffle/word_count.metadata.h index 2229268..68ee122 100644 --- a/code/duffle/word_count.metadata.h +++ b/code/duffle/word_count.metadata.h @@ -6,7 +6,7 @@ // One line per macro that appears in your atom sources. // // This file is encoding-macros-only. -// The auto-generated component macros (mac_X) live in duffle/gen/.macs.h (included separately by the unity build). +// The auto-generated component macros (mac_X) live in the source directory's own gen/macs.h (per-directory aggregation; included separately by the unity build). // The unity build should include THIS file and the .macs.h file in the same TU, with both wrapped // (or the include guard order handled) to avoid WORD_COUNT redeclaration. // diff --git a/code/hello_gte/hello_gte.c b/code/hello_gte/hello_gte.c index 36de40e..de0346d 100644 --- a/code/hello_gte/hello_gte.c +++ b/code/hello_gte/hello_gte.c @@ -14,13 +14,13 @@ #include "duffle/gp.h" #include "duffle/gte.h" -# include "duffle/gen/duffle.macs.h" -# include "duffle/gen/duffle.offsets.h" +# include "duffle/gen/macs.h" +# include "duffle/gen/offsets.h" #include "duffle/atom_dsl.h" #include "duffle/lottes_tape.h" #include "duffle/word_count.metadata.h" -# include "gen/hello_gte.offsets.h" +# include "gen/offsets.h" #include "hello_gte.h" #include "hello_gte.tape.c" diff --git a/code/hello_gte/hello_gte.tape.c b/code/hello_gte/hello_gte.tape.c index e10d6ec..8aae1d0 100644 --- a/code/hello_gte/hello_gte.tape.c +++ b/code/hello_gte/hello_gte.tape.c @@ -1,10 +1,10 @@ #ifdef INTELLISENSE_DIRECTIVES -# include "duffle/gen/duffle.macs.h" -# include "duffle/gen/duffle.offsets.h" +# include "duffle/gen/macs.h" +# include "duffle/gen/offsets.h" # include "duffle/atom_dsl.h" # include "duffle/lottes_tape.h" # include "duffle/word_count.metadata.h" -# include "gen/hello_gte.offsets.h" +# include "gen/offsets.h" # include "hello_gte.h" #endif diff --git a/code/hello_joypad/gen/hello_joypad.macs.h b/code/hello_joypad/gen/macs.h similarity index 69% rename from code/hello_joypad/gen/hello_joypad.macs.h rename to code/hello_joypad/gen/macs.h index 137868b..350f01b 100644 --- a/code/hello_joypad/gen/hello_joypad.macs.h +++ b/code/hello_joypad/gen/macs.h @@ -2,53 +2,23 @@ #pragma once #endif // Auto-generated by ps1_meta.lua — DO NOT EDIT -// Source: C:\projects\Pikuma\ps1\code\hello_joypad\hello_joypad.tape.c +// Directory: C:\projects\Pikuma\ps1\code\hello_joypad/ +// source: C:\projects\Pikuma\ps1\code\hello_joypad\hello_joypad.c +// source: C:\projects\Pikuma\ps1\code\hello_joypad\hello_joypad.h +// source: C:\projects\Pikuma\ps1\code\hello_joypad\hello_joypad.atom.c // Component atoms (MipsAtomComp_(ac_*)) -> macro variants (mac_*) #ifndef WORD_COUNT #define WORD_COUNT(name, count) enum { words_##name = (count) }; #endif -/* atom_dbg_skip */ -#define mac_load_v2s2(rs_x, rs_y, r_base, offset) \ - load_half( rs_x, r_base, O_(V3_S2,x)) \ -, load_half( rs_y, r_base, O_(V3_S2,y)) -WORD_COUNT(mac_load_v2s2, 2) - -/* atom_dbg_skip */ -#define mac_store_v2s2(rt_x, rt_y, base, offset) \ - store_half(rt_x, base, offset + O_(V2_S2,x)) \ -, store_half(rt_y, base, offset + O_(V2_S2,y)) -WORD_COUNT(mac_store_v2s2, 2) - -/* atom_dbg_skip */ -#define mac_store_rects2(rt_x, rt_y, rt_width, rt_height, base, offset) \ - store_half(rt_x, base, offset + O_(Rect_S2,x)) \ -, store_half(rt_y, base, offset + O_(Rect_S2,y)) \ -, store_half(rt_width, base, offset + O_(Rect_S2,width)) \ -, store_half(rt_height, base, offset + O_(Rect_S2,height)) -WORD_COUNT(mac_store_rects2, 4) - -/* atom_dbg_skip */ -#define mac_store_rgb8(rr, rg, rb, base, offset) \ - store_byte(rr, base, offset + O_(DrawEnv,initial_bg_color.r)) \ -, store_byte(rg, base, offset + O_(DrawEnv,initial_bg_color.g)) \ -, store_byte(rb, base, offset + O_(DrawEnv,initial_bg_color.b)) -WORD_COUNT(mac_store_rgb8, 3) - -#define mac_gcmd_push(cmd, reg_transfer, reg_base, port) \ - load_upper_i(reg_transfer, cmd >> 16) \ -, or_i_self( reg_transfer, cmd & 0xFFFF) \ -, store_word( reg_transfer, reg_base, port) -WORD_COUNT(mac_gcmd_push, 3) - #define mac_put_disp_env(reg_transfer, reg_base, port) \ mac_gcmd_push(gp0_word_draw_area_top_left_origin, reg_transfer, reg_base, port) \ , mac_gcmd_push(gp0_word_draw_area_bottom_right_320x240, reg_transfer, reg_base, port) \ , mac_gcmd_push(gp0_word_set_mask_bit(), reg_transfer, reg_base, port) \ , mac_gcmd_push(gp0_word_draw_area_top_left_origin, reg_transfer, reg_base, port) \ , mac_gcmd_push(gp0_word_draw_area_bottom_right_320x240, reg_transfer, reg_base, port) -WORD_COUNT(mac_put_disp_env, 15) +WORD_COUNT(mac_put_disp_env, 5) #define mac_put_draw_env(reg_transfer, reg_base, port) \ mac_gcmd_push(gp0_dr_env_tag, reg_transfer, reg_base, port) /* tag (length=15 << 24, addr=0) — packet header for the DR_ENV sequence. The GPU needs this to recognize the next 15 words as a DR_ENV packet and trigger the isbg auto-clear. */ \ @@ -67,5 +37,5 @@ WORD_COUNT(mac_put_disp_env, 15) , mac_gcmd_push(gp0_word_set_texture_window(), reg_transfer, reg_base, port) /* code[13..14] Padding (NOP) — completes the 16-word packet. */ \ , mac_gcmd_push(gp0_word_nop(), reg_transfer, reg_base, port) \ , mac_gcmd_push(gp0_word_nop(), reg_transfer, reg_base, port) -WORD_COUNT(mac_put_draw_env, 48) +WORD_COUNT(mac_put_draw_env, 16) diff --git a/code/hello_joypad/gen/hello_joypad.offsets.h b/code/hello_joypad/gen/offsets.h similarity index 90% rename from code/hello_joypad/gen/hello_joypad.offsets.h rename to code/hello_joypad/gen/offsets.h index 58aec02..b08d1fc 100644 --- a/code/hello_joypad/gen/hello_joypad.offsets.h +++ b/code/hello_joypad/gen/offsets.h @@ -1,8 +1,11 @@ // Auto-generated by ps1_meta.lua (passes/offsets.lua) — DO NOT EDIT -// Source: C:\projects\Pikuma\ps1\code\hello_joypad\hello_joypad.tape.c +// Directory: C:\projects\Pikuma\ps1\code\hello_joypad\ +// source: C:\projects\Pikuma\ps1\code\hello_joypad\hello_joypad.c +// source: C:\projects\Pikuma\ps1\code\hello_joypad\hello_joypad.h +// source: C:\projects\Pikuma\ps1\code\hello_joypad\hello_joypad.atom.c #pragma once -#pragma region hello_joypad.tape +#pragma region hello_joypad // --- atom: cube_g4_face (76 words) --- @@ -69,5 +72,5 @@ enum { atom_offset_end_low_exit_stick = _atom_offset_end_low_exit_stick, }; -#pragma endregion hello_joypad.tape +#pragma endregion hello_joypad diff --git a/code/hello_joypad/hello_joypad.tape.c b/code/hello_joypad/hello_joypad.atom.c similarity index 93% rename from code/hello_joypad/hello_joypad.tape.c rename to code/hello_joypad/hello_joypad.atom.c index 624f3ec..9eece99 100644 --- a/code/hello_joypad/hello_joypad.tape.c +++ b/code/hello_joypad/hello_joypad.atom.c @@ -1,51 +1,29 @@ #ifdef INTELLISENSE_DIRECTIVES -# include "duffle/gen/duffle.macs.h" -# include "duffle/gen/duffle.offsets.h" -# include "duffle/atom_dsl.h" +# pragma once +# include "duffle/gen/macs.h" +# include "duffle/gen/offsets.h" +# include "duffle/dsl.atom.h" # include "duffle/lottes_tape.h" # include "duffle/mips.h" # include "duffle/gte.h" # include "duffle/gp.h" # include "duffle/pad.h" # include "duffle/word_count.metadata.h" -# include "psyq.h" -# include "gen/hello_joypad.offsets.h" -# include "gen/hello_joypad.macs.h" +# include "duffle/psyq.h" +# include "duffle/math.atom.c" +# include "duffle/mips.atom.c" +# include "duffle/gte.atom.c" +# include "duffle/gp.atom.c" +# include "duffle/psyq.atom.c" +# include "gen/offsets.h" +# include "gen/macs.h" # include "hello_joypad.h" #endif +ATOM_FILE_DEBUGGER_LINE_MARKER(hello_joypad_atom_c); + #pragma region MACs (Mips Atom components) -FI_ Slice_MipsCode ac_load_v2s2(U4 rs_x, U4 rs_y, U4 r_base, U4 offset) atom_dbg_skip MipsAtomComp_Proc_(ac_load_v2s2, { - load_half( rs_x, r_base, O_(V3_S2,x)), - load_half( rs_y, r_base, O_(V3_S2,y)), -}) - -FI_ Slice_MipsCode ac_store_v2s2(U4 rt_x, U4 rt_y, U4 base, U4 offset) atom_dbg_skip MipsAtomComp_Proc_(ac_store_v2s2, { - store_half(rt_x, base, offset + O_(V2_S2,x)), - store_half(rt_y, base, offset + O_(V2_S2,y)), -}) - -FI_ Slice_MipsCode ac_store_rects2(U4 rt_x, U4 rt_y, U4 rt_width, U4 rt_height, U4 base, U4 offset) atom_dbg_skip MipsAtomComp_Proc_(ac_store_rects2, { - store_half(rt_x, base, offset + O_(Rect_S2,x)), - store_half(rt_y, base, offset + O_(Rect_S2,y)), - store_half(rt_width, base, offset + O_(Rect_S2,width)), - store_half(rt_height, base, offset + O_(Rect_S2,height)), -}) - -FI_ Slice_MipsCode ac_store_rgb8(U1 rr, U1 rg, U1 rb, U4 base, U4 offset) atom_dbg_skip MipsAtomComp_Proc_(ac_store_rgb8, { - store_byte(rr, base, offset + O_(DrawEnv,initial_bg_color.r)), - store_byte(rg, base, offset + O_(DrawEnv,initial_bg_color.g)), - store_byte(rb, base, offset + O_(DrawEnv,initial_bg_color.b)), -}) - -FI_ Slice_MipsCode ac_gcmd_push(U4 cmd, U4 reg_transfer, U4 reg_base, U2 port) -MipsAtomComp_Proc_(ac_gcmd_push, { - load_upper_i(reg_transfer, cmd >> 16), - or_i_self( reg_transfer, cmd & 0xFFFF), - store_word( reg_transfer, reg_base, port), -}) - FI_ Slice_MipsCode ac_put_disp_env(U4 reg_transfer, U4 reg_base, U2 port) MipsAtomComp_Proc_(ac_put_disp_env, { // Emits 5 GP0 commands for buffer 0 (display_area = (0,0,320,240)). @@ -202,6 +180,17 @@ internal MipsAtom_(gp_screen_init) atom_info(atom_phase(screen_init), atom_reads mac_yield(), }; +enum { + R_PrimCursor = R_T7 atom_reg atom_type(U4*), /* VRAM output cursor (primitive buffer) */ + R_FaceCursor = R_T4 atom_reg atom_type(V4_S2*), /* Cube face-index cursor (V4_S2*); floor context switches to V3_S2* via atom_phase */ + R_VertBase = R_T5 atom_reg atom_type(V3_S2*), /* Base address of the vertex array */ + R_OtBase = R_T6 atom_reg atom_type(U4*), /* Base address of the Ordering Table */ +#define R_PrimCursor_Code R_T7_Code +#define R_FaceCursor_Code R_T4_Code +#define R_VertBase_Code R_T5_Code +#define R_OtBase_Code R_T6_Code +}; + typedef Struct_(Binds_CubeTri) { U4 PrimCursor; V4_S2* FaceCursor; @@ -232,23 +221,23 @@ MipsAtom_(cube_g4_face) atom_info(atom_phase(cube_g4), load_half_u(R_T2, R_FaceCursor, 2 * S_(S2)), load_half_u(R_T3, R_FaceCursor, 3 * S_(S2)), - mac_gte_load_tri_verts(R_T0, R_T1, R_T2), + mac_gte_load_tri_verts(R_VertBase, R_T0, R_T1, R_T2), nop2, gte_cmdw_rotate_translate_perspective_triple, // required cpu -> gte delay slot gte_cmdw_nclip, gte_mv_from_data_r(R_T0, C2_MAC0), nop, branch_le_zero(R_T0, atom_offset(cull, cube_g4_face_exit)), /* BD-slot: write the prim tag (R_0=0; overwrites the legacy tag word in the prim_buffer). - * If branch IS taken (face culled), the body is skipped and this 0-tag is stranded — harmless - * because the OT entry that points to this prim is created later, only on the body path. */ + * If branch IS taken (face culled), the body is skipped and this 0-tag is stranded — + * harmless because the OT entry that points to this prim is created later, only on the body path. */ store_word(R_0, R_PrimCursor, O_(Poly_G4, tag)), shift_lleft(R_AT, R_T3, v3s2_byteoff), add_u(R_AT, R_AT, R_VertBase), load_word(R_V0, R_AT, O_(V3_S2, x)), load_word(R_V1, R_AT, O_(V3_S2, z)), gte_mv_to_data_r(R_V0, C2_VXY0), gte_mv_to_data_r(R_V1, C2_VZ0), - mac_gte_store_g4_p012(), + mac_gte_store_g4_p012(R_PrimCursor), gte_cmdw_rotate_translate_perspective_single, - mac_gte_store_g4_p3(), + mac_gte_store_g4_p3(R_PrimCursor), gte_cmdw_avg_sort_z4, gte_mv_from_data_r(R_T1, C2_OTZ), @@ -256,8 +245,8 @@ MipsAtom_(cube_g4_face) atom_info(atom_phase(cube_g4), set_lt_u( R_AT, R_T1, R_AT), branch_equal(R_AT, R_0, atom_offset(bounds_chk, cube_g4_face_exit)), nop, - mac_insert_ot_tag_g4(), - mac_format_g4_color( + mac_insert_ot_tag_g4(R_OtBase, R_PrimCursor), + mac_format_g4_color(R_PrimCursor, /* c0 magenta */ 0xFF, 0x00, 0xFF, /* c1 yellow */ 0xFF, 0xFF, 0x00, /* c2 cyan */ 0x00, 0xFF, 0xFF, @@ -297,8 +286,8 @@ MipsAtom_(floor_f3_face) atom_info(atom_phase(floor_f3) , atom_reads( R_PrimCursor, R_FaceCursor, R_VertBase, R_OtBase) , atom_writes(R_PrimCursor, R_FaceCursor) ) { - mac_load_tri_indices( R_T0, R_T1, R_T2), - mac_gte_load_tri_verts(R_T0, R_T1, R_T2), + mac_load_tri_indices( R_FaceCursor, R_T0, R_T1, R_T2), + mac_gte_load_tri_verts(R_VertBase, R_T0, R_T1, R_T2), nop2, gte_cmdw_rotate_translate_perspective_triple, // 2 nops retire the final cpu -> gte writes before RTPT gte_cmdw_nclip, @@ -306,7 +295,7 @@ MipsAtom_(floor_f3_face) atom_info(atom_phase(floor_f3) gte_mv_from_data_r(R_T0, C2_MAC0), nop, branch_le_zero(R_T0, atom_offset(culling, floor_f3_face_exit)), nop, // required gte -> cpu load-delay slot. /* Format Primitive */ - mac_gte_store_f3(), + mac_gte_store_f3(R_PrimCursor), /* Calculate Depth */ gte_avg_sort_z3, @@ -315,8 +304,8 @@ MipsAtom_(floor_f3_face) atom_info(atom_phase(floor_f3) add_ui( R_AT, R_0, OrderingTbl_Len), set_lt_u( R_AT, R_T1, R_AT), branch_equal(R_AT, R_0, atom_offset(bounds_chk, floor_f3_face_exit)), nop, - mac_format_f3_color(0xFF, 0xFF, 0xFF), // RGB-form (R=FF, G=FF, B=FF = white) - mac_insert_ot_tag_f3(), /* Insert into Ordering Table Linked List */ + mac_format_f3_color(R_PrimCursor, 0xFF, 0xFF, 0xFF), // RGB-form (R=FF, G=FF, B=FF = white) + mac_insert_ot_tag_f3(R_OtBase, R_PrimCursor), /* Insert into Ordering Table Linked List */ add_ui_self(R_PrimCursor, S_(Poly_F3)), /* Advance Prim Cursor (5 words) */ // Note(Ed): No bounds checking, should be checked before atom runs. // end: branch(bounds_chk) diff --git a/code/hello_joypad/hello_joypad.c b/code/hello_joypad/hello_joypad.c index 666c182..a0c65fa 100644 --- a/code/hello_joypad/hello_joypad.c +++ b/code/hello_joypad/hello_joypad.c @@ -1,9 +1,17 @@ +#pragma region Vendors #include #include #include // #include "libgpu.h" // #include "libetc.h" // #include "libgte.h" +#pragma endregion Vendors + +#pragma region Duffle Headers +# include "duffle/gen/macs.h" +# include "duffle/gen/offsets.h" + +#include "duffle/word_count.metadata.h" #include "duffle/dsl.h" #include "duffle/memory.h" @@ -15,96 +23,30 @@ #include "duffle/gte.h" #include "duffle/pad.h" -# include "duffle/gen/duffle.macs.h" -# include "duffle/gen/duffle.offsets.h" -#include "duffle/atom_dsl.h" +#include "duffle/dsl.atom.h" #include "duffle/lottes_tape.h" -#include "duffle/word_count.metadata.h" -#include "psyq.h" +#include "duffle/psyq.h" +#pragma endregion Duffle Headers + +#pragma region Duffle TUs +#include "duffle/math.atom.c" +#include "duffle/mips.atom.c" +#include "duffle/gte.atom.c" +#include "duffle/gp.atom.c" +#include "duffle/psyq.atom.c" +#pragma endregion Duffle TUs + +#pragma region Joypade Headers +# include "gen/macs.h" +# include "gen/offsets.h" -# include "gen/hello_joypad.macs.h" -# include "gen/hello_joypad.offsets.h" #include "hello_joypad.h" +#pragma region Joypad Headers -#include "psyq.c" -#include "hello_joypad.tape.c" - - -typedef U4 OrderingTable_Buffer[OrderingTbl_Len]; -typedef Array_(OrderingTable_Buffer, 2); - -typedef B1 PrimitiveBuffer[PrimitiveBuff_Len]; -typedef Array_(PrimitiveBuffer, 2); -typedef Struct_(PrimitiveArena) { - A2_PrimitiveBuffer buf; - U4 used; -}; - -#define Cube_num_verts 8 -typedef Array_(V3_S2, Cube_num_verts); -#define Cube_num_faces 6 -typedef Array_(V4_S2, Cube_num_faces); -I_ void ent_cube128_init(A8_V3_S2* verts, A6_V4_S2* faces) { - LP_ A8_V3_S2 baked_verts = (A8_V3_S2) { - { -128, -128, -128 }, - { 128, -128, -128 }, - { 128, -128, 128 }, - { -128, -128, 128 }, - { -128, 128, -128 }, - { 128, 128, -128 }, - { 128, 128, 128 }, - { -128, 128, 128 } - }; - LP_ A6_V4_S2 baked_faces = (A6_V4_S2) { - { 3, 2, 0, 1 }, - { 0, 1, 4, 5 }, - { 4, 5, 7, 6 }, - { 1, 2, 5, 6 }, - { 2, 3, 6, 7 }, - { 3, 0, 7, 4 }, - }; - mem_copy(u4_(verts), u4_(& baked_verts), S_(A8_V3_S2) ); - mem_copy(u4_(faces), u4_(& baked_faces), S_(A6_V4_S2) ); - return; -} -typedef Struct_(Ent_Cube) { - V3_S4 accel; - V3_S4 vel; - V3_S4 pos; - V3_S4 scale; - V3_S2 rot; - A8_V3_S2 verts; - A6_V4_S2 faces; -}; - -#define Floor_num_verts 4 -typedef Array_(V3_S2, Floor_num_verts); -#define Floor_num_faces 2 -typedef Array_(V3_S2, Floor_num_faces); -I_ void ent_floor_init(A4_V3_S2* verts, A2_V3_S2* faces) { - LP_ A4_V3_S2 baked_verts = (A4_V3_S2) { - { -900, 0, -900 }, - { -900, 0, 900 }, - { 900, 0, -900 }, - { 900, 0, 900 }, - }; - LP_ A2_V3_S2 baked_faces = (A2_V3_S2) { - { 0, 1, 2 }, - { 1, 3, 2 }, - }; - mem_copy(u4_(verts), u4_(& baked_verts), S_(A4_V3_S2)); - mem_copy(u4_(faces), u4_(& baked_faces), S_(A2_V3_S2)); -}; -typedef Struct_(Ent_Floor) { - V3_S4 accel; - V3_S4 pos; - V3_S4 scale; - V3_S2 rot; - A4_V3_S2 verts; - A2_V3_S2 faces; -}; - +#pragma region Hello Joypad TUs +#include "hello_joypad.atom.c" +#pragma endregion Hello Joypad TUs enum { Scratchpad_Len = 1024, @@ -212,50 +154,6 @@ NI_ void pad_bios_init_start(PadBiosRaw* raw0, PadBiosRaw* raw1) ); } -void gp_screen_init_c11(DoubleBuffer* screen_buf, S4* active_buf_id) -{ - reset_graph(0); - - // Set the current initial buffer - active_buf_id[0] = 0; - - // Just setting env data, not interacting with console hw. - // First buffer area - displayenv_init(& r_(screen_buf->display)[0], 0, 0, ScreenRes_X, ScreenRes_Y); - drawenv_init (& r_(screen_buf->draw )[0], 0, ScreenRes_Y, ScreenRes_X, ScreenRes_Y); - // Second buffer area - displayenv_init(& r_(screen_buf->display)[1], 0, ScreenRes_Y, ScreenRes_X, ScreenRes_Y); - drawenv_init (& r_(screen_buf->draw )[1], 0, 0, ScreenRes_X, ScreenRes_Y); - // Set the back/drawing buffer - screen_buf->draw[0].enable_auto_clear = true; - screen_buf->draw[1].enable_auto_clear = true; - // Set the background clear color - screen_buf->draw[0].initial_bg_color = rgb8( .r = 7, .g = 7, .b = 7 ); - screen_buf->draw[1].initial_bg_color = rgb8( .r = 7, .g = 7, .b = 7 ); - // screen_buf->draw[1].initial_bg_color = rgb8( .r = 47, .g = 13, .b = 0 ); - displayenv_put(& r_(screen_buf->display)[ active_buf_id[0] ]); - drawenv_put (& r_(screen_buf->draw )[ active_buf_id[0] ]); - - // Initialize and setup the GTE geometry offsets - geom_init(); - geom_set_offset(ScreenRes_CenterX, ScreenRes_CenterY); - geom_set_screen(ScreenZ); - - set_display_enabled(1); // gp_DisplayEnabled -} - -void gp_display_frame(DoubleBuffer* screen_buf, S4* active_buf_id, U4* ordering_buf, PrimitiveArena* pa) { - draw_sync(0); - vsync(0); - displayenv_put(& r_(screen_buf->display)[active_buf_id[0] ]); - drawenv_put (& r_(screen_buf->draw) [active_buf_id[0] ]); - { - draw_orderingtbl(ordering_buf + OrderingTbl_Len - 1); - pa->used = 0; - } - active_buf_id[0] = ! active_buf_id[0]; // Swap current buffer -} - GCC_OPTIMIZATION_DISABLE void update(PrimitiveArena* pa, U4* ordering_buf) { @@ -484,6 +382,19 @@ GCC_OPTIMIZATION_ENABLE void render(void) { } +void gp_display_frame(DoubleBuffer* screen_buf, S4* active_buf_id, U4* ordering_buf, PrimitiveArena* pa) { + draw_sync(0); + vsync(0); + displayenv_put(& r_(screen_buf->display)[active_buf_id[0] ]); + drawenv_put (& r_(screen_buf->draw) [active_buf_id[0] ]); + { + draw_orderingtbl(ordering_buf + OrderingTbl_Len - 1); + pa->used = 0; + } + active_buf_id[0] = ! active_buf_id[0]; // Swap current buffer +} + +GCC_OPTIMIZATION_DISABLE int main(void) { smem = (SMemory){0}; @@ -527,3 +438,4 @@ int main(void) }; return 0; } +GCC_OPTIMIZATION_ENABLE diff --git a/code/hello_joypad/hello_joypad.h b/code/hello_joypad/hello_joypad.h index d44e6be..8c3f9ce 100644 --- a/code/hello_joypad/hello_joypad.h +++ b/code/hello_joypad/hello_joypad.h @@ -26,3 +26,77 @@ enum { }; #define v3s4_fp_one() v3s4(fp_one, fp_one, fp_one) + +typedef U4 OrderingTable_Buffer[OrderingTbl_Len]; +typedef Array_(OrderingTable_Buffer, 2); + +typedef B1 PrimitiveBuffer[PrimitiveBuff_Len]; +typedef Array_(PrimitiveBuffer, 2); +typedef Struct_(PrimitiveArena) { + A2_PrimitiveBuffer buf; + U4 used; +}; + +#define Cube_num_verts 8 +typedef Array_(V3_S2, Cube_num_verts); +#define Cube_num_faces 6 +typedef Array_(V4_S2, Cube_num_faces); +I_ void ent_cube128_init(A8_V3_S2* verts, A6_V4_S2* faces) { + LP_ A8_V3_S2 baked_verts = (A8_V3_S2) { + { -128, -128, -128 }, + { 128, -128, -128 }, + { 128, -128, 128 }, + { -128, -128, 128 }, + { -128, 128, -128 }, + { 128, 128, -128 }, + { 128, 128, 128 }, + { -128, 128, 128 } + }; + LP_ A6_V4_S2 baked_faces = (A6_V4_S2) { + { 3, 2, 0, 1 }, + { 0, 1, 4, 5 }, + { 4, 5, 7, 6 }, + { 1, 2, 5, 6 }, + { 2, 3, 6, 7 }, + { 3, 0, 7, 4 }, + }; + mem_copy(u4_(verts), u4_(& baked_verts), S_(A8_V3_S2) ); + mem_copy(u4_(faces), u4_(& baked_faces), S_(A6_V4_S2) ); + return; +} +typedef Struct_(Ent_Cube) { + V3_S4 accel; + V3_S4 vel; + V3_S4 pos; + V3_S4 scale; + V3_S2 rot; + A8_V3_S2 verts; + A6_V4_S2 faces; +}; + +#define Floor_num_verts 4 +typedef Array_(V3_S2, Floor_num_verts); +#define Floor_num_faces 2 +typedef Array_(V3_S2, Floor_num_faces); +I_ void ent_floor_init(A4_V3_S2* verts, A2_V3_S2* faces) { + LP_ A4_V3_S2 baked_verts = (A4_V3_S2) { + { -900, 0, -900 }, + { -900, 0, 900 }, + { 900, 0, -900 }, + { 900, 0, 900 }, + }; + LP_ A2_V3_S2 baked_faces = (A2_V3_S2) { + { 0, 1, 2 }, + { 1, 3, 2 }, + }; + mem_copy(u4_(verts), u4_(& baked_verts), S_(A4_V3_S2)); + mem_copy(u4_(faces), u4_(& baked_faces), S_(A2_V3_S2)); +}; +typedef Struct_(Ent_Floor) { + V3_S4 accel; + V3_S4 pos; + V3_S4 scale; + V3_S2 rot; + A4_V3_S2 verts; + A2_V3_S2 faces; +}; diff --git a/code/hello_joypad/psyq.c b/code/hello_joypad/psyq.c deleted file mode 100644 index 0bf105d..0000000 --- a/code/hello_joypad/psyq.c +++ /dev/null @@ -1,3 +0,0 @@ -#ifdef INTELLISENSE_DIRECTIVES -# include "psyq.h" -#endif diff --git a/scripts/passes/components.lua b/scripts/passes/components.lua index 03097f5..87c7085 100644 --- a/scripts/passes/components.lua +++ b/scripts/passes/components.lua @@ -6,10 +6,9 @@ --- Reads the pre-scanned SourceScan payload from `duffle.scan_source` for `MipsAtomComp_(ac_X)` and `MipsAtomComp_Proc_(ac_X, { body })` declarations, --- then resolves the function-args string from the preceding `FI_ Slice_MipsCode ac_X(...)` declaration via a backward walk. --- ---- Emits one `.macs.h` per source with `#define mac_X(sig) \` macros plus `WORD_COUNT(mac_X, N)` entries for downstream offset computation. ---- ---- **Conventions**: tabs (1/level), EmmyLua annotations, no regex, ---- Lua 5.3 compatible. +--- Emits one `gen/macs.h` per *immediate source directory* with `#define mac_X(sig) \` macros plus `WORD_COUNT(mac_X, N)` entries for downstream offset computation. +--- All sources inside the same directory contribute to the same file (per-directory aggregation). +--- The directory itself is the namespace, so the filename does not repeat the module name. -- ════════════════════════════════════════════════════════════════════════════ -- Module-scope requires + package.path setup @@ -41,43 +40,44 @@ local MAC_PREFIX_LEN = 4 local BYTE_NEWLINE = 10 local BYTE_SLASH = 47 --- Source dir basename used as the output `.macs.h` filename. +-- Output gen subdirectory + filename (per-directory aggregation; the directory name is the namespace). local GEN_SUBDIR = "gen" +local MACS_FILENAME = "macs.h" -- ════════════════════════════════════════════════════════════════════════════ -- Type declarations -- ════════════════════════════════════════════════════════════════════════════ --- @class SourceFile ---- @field path string -- absolute path to the source file ---- @field text string -- the full source text ---- @field dir string -- the directory containing the source ---- @field basename string -- filename without extension ---- @field scan table -- pre-scanned SourceScan payload (from duffle.scan_source) +--- @field path string -- Absolute path to the source file +--- @field text string -- Full source text +--- @field dir string -- Directory containing the source +--- @field basename string -- Filename without extension +--- @field scan table -- Pre-scanned SourceScan payload (from duffle.scan_source) --- @class PassCtx ---- @field sources SourceFile[] -- all source files in the build ---- @field metadata_path string -- path to word_count.metadata.h ---- @field shared table -- cross-pass shared state ---- @field out_root string -- output root (e.g. "build/gen") ---- @field project_root string -- project root (e.g. "code/") ---- @field upstream table -- per-pass upstream outputs +--- @field sources SourceFile[] -- All source files in the build +--- @field metadata_path string -- Path to word_count.metadata.h +--- @field shared table -- Cross-pass shared state +--- @field out_root string -- Output root (e.g. "build/gen") +--- @field project_root string -- Project root (e.g. "code/") +--- @field upstream table -- Per-pass upstream outputs --- @field flags table -- CLI flags ---- @field verbose boolean -- log diagnostic info +--- @field verbose boolean -- Log diagnostic info --- @class PassResult --- @field outputs table[] -- {kind=, path=} entries describing emit files ---- @field errors table[] -- {line=, msg=} entries; build-stops ---- @field warnings table[] -- {line=, msg=} entries; build-succeeds +--- @field errors table[] -- {line=, msg=} entries; build-stops +--- @field warnings table[] -- {line=, msg=} entries; build-succeeds --- @class Component ---- @field name string -- atom name (without `ac_` prefix) ---- @field body string -- brace-delimited body (without the braces) ---- @field args string|nil -- function-args string (function form only) ---- @field line integer -- source line of the declaration ---- @field comment string|nil -- scanner-owned `declaration_comment`; the components pass reads it from the scanner record +--- @field name string -- Atom name (without `ac_` prefix) +--- @field body string -- Brace-delimited body (without the braces) +--- @field args string|nil -- Function-args string (function form only) +--- @field line integer -- Source line of the declaration +--- @field comment string|nil -- Scanner-owned `declaration_comment`; the components pass reads it from the scanner record --- @field kind string -- "comp_bare" | "comp_proc" ---- @field debug_skip boolean -- mirror of `a.debug_skip` (scanner-owned); true iff a bare `atom_dbg_skip` marker immediately preceded the declaration +--- @field debug_skip boolean -- Mirror of `a.debug_skip` (scanner-owned); true iff a bare `atom_dbg_skip` marker immediately preceded the declaration -- ════════════════════════════════════════════════════════════════════════════ -- Local helpers (file I/O + path normalization) @@ -231,7 +231,6 @@ end -- -- Skips `//` sequences that are inside string or character literals -- (a rough heuristic — sufficient for component bodies which don't have those constructs). --- --- @param s string --- @return string local function convert_line_comments_to_block(s) @@ -300,7 +299,7 @@ local function word_count_rec(name, comp_by_name, wc, cache) local trimmed = t.tok if trimmed ~= "" then local lookup = strip_mac_prefix(duffle.read_ident(trimmed, 1)) - if lookup and comp_by_name[lookup] then + if lookup and comp_by_name[lookup] then -- It's a `mac_X(...)` call. Recurse. n = n + word_count_rec(lookup, comp_by_name, wc, cache) elseif lookup and wc and wc[lookup] then @@ -350,7 +349,6 @@ end --- (internal) Recursive cycle-cost derivation. Sum `latency[ident]` per emitted instruction in the component body, --- recursing through nested `mac_*` calls (so `mac_format_g4_color`'s cost = 4 × `mac_pack_color_word`'s cost). ---- --- Special rule: `mac_yield`'s cost = 0 (per `lottes_tape.h:125-130` "the runtime cost lands in the next atom's prologue"). --- @param name string -- component bare name (e.g. "yield", "pack_color_word") --- @param comp_by_name table @@ -394,7 +392,6 @@ end --- (internal) Recursive GP0 prim-buffer contribution. Count `store_word` / `store_half` / `store_byte` --- calls in the component body that target `R_PrimCursor` (these are the --- RAM-side prim-buffer words the macro contributes), recursing through nested `mac_*` calls. ---- --- Only `R_PrimCursor`-targeting stores count. Stores targeting other registers (e.g. `R_OtBase`, heap pointers) are not prim-buffer contributions. --- @param name string --- @param comp_by_name table @@ -410,9 +407,9 @@ local function gp0_contrib_rec(name, comp_by_name, cache) local tokens = cc.body_tokens for _, t in ipairs(tokens) do local trimmed = t.tok - if trimmed ~= "" then + if trimmed ~= "" then local ident = duffle.read_ident(trimmed, 1) - if ident and ident:sub(1, MAC_PREFIX_LEN) == MAC_PREFIX then + if ident and ident:sub(1, MAC_PREFIX_LEN) == MAC_PREFIX then -- Nested `mac_X(...)` call: recurse. local nested = ident:sub(MAC_PREFIX_LEN + 1) n = n + gp0_contrib_rec(nested, comp_by_name, cache) @@ -460,8 +457,8 @@ end --- @param s string --- @return string[] local function split_comment_lines(s) - local out = {} - local pos = 1 + local out = {} + local pos = 1 local s_len = #s while pos <= s_len do local nl = s:find("\n", pos, true) @@ -536,9 +533,9 @@ local function build_component_lines(c, counts) local tokens = duffle.split_top_level_commas(c.body) for i = 1, #tokens do tokens[i] = duffle.trim(tokens[i]) end - local sig = signature_from_args(c.args) + local sig = signature_from_args(c.args) -- Direct lookup against the per-source precomputed `counts` table (built once by count_all_components). - local n = counts[c.name] + local n = counts[c.name] if n > 0 then emit_macro_body(lines, c, sig, tokens) @@ -557,9 +554,15 @@ end --- Build the boilerplate header lines (the `#ifdef INTELLISENSE_DIRECTIVES` block, --- the `// Auto-generated` comment, the `// Source:` line, and the self-contained `WORD_COUNT` macro definition). ---- @param src SourceFile +--- @param dir string -- the absolute source directory +--- @param sources SourceFile[] -- sources contributing to this directory (for the header comment) --- @return string[] -local function header_boilerplate(src) +local function header_boilerplate(dir, sources) + local source_lines = { "// Directory: " .. duffle.to_absolute_path(dir) .. "/" } + for _, src in ipairs(sources) do + source_lines[#source_lines + 1] = "// source: " .. duffle.to_absolute_path(src.path) + end + local source_blob = table.concat(source_lines, "\n") return { -- #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. @@ -567,7 +570,7 @@ local function header_boilerplate(src) "#pragma once", "#endif", "// Auto-generated by ps1_meta.lua — DO NOT EDIT", - "// Source: " .. duffle.to_absolute_path(src.path), + source_blob, "// Component atoms (MipsAtomComp_(ac_*)) -> macro variants (mac_*)", "", -- Self-contained: define WORD_COUNT if not already defined. @@ -580,30 +583,30 @@ local function header_boilerplate(src) } end ---- Compute the output path for one source's `.macs.h` file. ---- The pre-rework convention uses the *directory* basename (not the source file basename) ---- e.g. `code/duffle/lottes_tape.h` produces `code/duffle/gen/duffle.macs.h`. ---- This matches what the C codebase #includes. ---- @param src SourceFile +--- Compute the per-directory output path for `.macs.h`. +--- e.g. any source in `code/duffle/` produces `code/duffle/gen/macs.h` regardless of source filename. +--- The directory name is the namespace; the filename does not repeat it. +--- @param dir string -- the absolute source directory --- @return string -- the output directory --- @return string -- the full output path -local function compute_macs_h_path(src) - local out_dir = src.dir .. "/" .. GEN_SUBDIR - local out_path = out_dir .. "/" .. duffle.basename_no_ext(src.dir) .. ".macs.h" +local function compute_macs_h_path(dir) + local out_dir = dir .. "/" .. GEN_SUBDIR + local out_path = out_dir .. "/" .. MACS_FILENAME return out_dir, out_path end ---- Emit a per-source `.macs.h` header with the `mac_X` macros + `WORD_COUNT` entries. +--- Emit a per-directory `.macs.h` header with the aggregated `mac_X` macros + `WORD_COUNT` entries. --- Writes in BINARY mode so LF line endings are preserved (the git blob is LF; Windows text-mode would emit CRLF and break the byte-identical diff). --- @param ctx PassCtx ---- @param src SourceFile ---- @param components Component[] +--- @param dir string -- the absolute source directory +--- @param sources SourceFile[] -- sources contributing to this directory (for the header comment) +--- @param components Component[] -- aggregated components from all sources in this directory --- @param counts table -- precomputed word counts (from count_all_components) --- @return string|nil -- path to the written file (nil if no components) -local function emit_component_macros_h(ctx, src, components, counts) +local function emit_component_macros_h(ctx, dir, sources, components, counts) if #components == 0 then return nil end - local out_dir, out_path = compute_macs_h_path(src) - local lines = header_boilerplate(src) + local out_dir, out_path = compute_macs_h_path(dir) + local lines = header_boilerplate(dir, sources) for _, c in ipairs(components) do for _, l in ipairs(build_component_lines(c, counts)) do @@ -675,7 +678,7 @@ local function update_canonical_components(corpus, src, components, metadata) -- Identical-shape declarations (same path + line) reuse the first-wins entry without a collision record. local existing = corpus.components[c.name] if existing.path ~= rel_path or existing.line ~= c.line then - local kind = c.kind or "comp_bare" + local kind = c.kind or "comp_bare" local first_kind = existing.kind or "comp_bare" corpus.collisions[#corpus.collisions + 1] = { kind = "component", @@ -740,24 +743,39 @@ function M.run(ctx) -- * `corpus.component_body_index[name]` — body / line_of / source index -- The pass writes to the corpus only; consumers read from the corpus directly. - for _, src in ipairs(corpus.source_order) do - -- project_components reads from src.scan + does backward lookups on src.text - local components = project_components(src.text, src.scan) - if #components > 0 then - -- Compute all component word counts once per source. - -- Use `corpus.word_counts` so the recursive lookup sees both authored-metadata entries - -- (loaded by word_count_eval.run) AND same-source component entries (populated earlier in this loop by `update_canonical_word_counts`). - local counts = count_all_components(components, corpus.word_counts) - -- Derive cycle_cost + gp0_contrib from the original `MipsAtomComp_` body tokens - -- (NOT from the generated `mac_*` variants — those are written to disk above). - local metadata = compute_components_metadata(components, duffle.INSTRUCTION_LATENCY) - local macs_path = emit_component_macros_h(ctx, src, components, counts) + -- Per-directory aggregation: every source in the same directory contributes to one `gen/macs.h`. + -- The directory itself is the namespace. `corpus.sources_by_dir` preserves source-order within each bucket (matches `corpus.source_order`). + local sources_by_dir = corpus.sources_by_dir or duffle.group_sources_by_dir(corpus.source_order) + for dir, sources in pairs(sources_by_dir) do + -- Aggregate components from every source in this directory. + -- `project_components` returns nil for sources with no `MipsAtomComp_` declarations; we skip those. + local aggregated_components = {} + local metadata_per_source = {} + for _, src in ipairs(sources) do + local per_source = project_components(src.text, src.scan) or {} + for _, c in ipairs(per_source) do + aggregated_components[#aggregated_components + 1] = c + end + if #per_source > 0 then + metadata_per_source[src] = compute_components_metadata(per_source, duffle.INSTRUCTION_LATENCY) + end + end + if #aggregated_components > 0 then + -- Compute word counts across the aggregated set. `corpus.word_counts` carries the + -- same-source + prior-directory entries so the recursive lookup sees both. + local counts = count_all_components(aggregated_components, corpus.word_counts) + local macs_path = emit_component_macros_h(ctx, dir, sources, aggregated_components, counts) if macs_path then outputs[#outputs + 1] = { macs_h = macs_path } - -- Populate the projections AFTER disk emission (so the byte-identical `.macs.h` contract is preserved before any current-count mutation). - update_canonical_word_counts(corpus, components, counts) - update_canonical_components(corpus, src, components, metadata) - update_canonical_component_body_index(corpus, src, components, src.scan) + -- Populate the projections AFTER disk emission (byte-identical `.macs.h` contract). + update_canonical_word_counts(corpus, aggregated_components, counts) + for _, src in ipairs(sources) do + local per_source = project_components(src.text, src.scan) or {} + if #per_source > 0 then + update_canonical_components(corpus, src, per_source, metadata_per_source[src]) + update_canonical_component_body_index(corpus, src, per_source, src.scan) + end + end end end end diff --git a/scripts/passes/dwarf_injection.lua b/scripts/passes/dwarf_injection.lua index acac4b8..7ffddb6 100644 --- a/scripts/passes/dwarf_injection.lua +++ b/scripts/passes/dwarf_injection.lua @@ -73,8 +73,8 @@ local DW_RLE_start_length = DWARF5_RNGLISTS.start_length -- File-index lookup for the existing main line unit (Unit 2). -- Populated at pass start by `init_file_index_lookup(elf_path)` from the runtime ELF (see `elf_dwarf.read_line_unit_file_table`). -local _file_index_by_basename = nil -- [basename] = 1-based line-table file index -local _file_path_by_index = nil -- [1-based index] = full source path (diagnostics / future consumers) +local _file_index_by_basename = nil -- [basename] = 1-based line-table file index +local _file_path_by_index = nil -- [1-based index] = full source path (diagnostics / future consumers) local _default_atom_source_index = nil -- any valid index used in opaque-row fallbacks -- RR_ debug-visible variables come from the merged register_alias_registry filtered to aliases whose code is a valid MIPS GPR 0..31 @@ -107,8 +107,8 @@ local ABBREV_TYPED_VIEW_POINTER = 0x6E -- 110: DW_TAG_pointer_type no children -- DWARF5 §7.7.3 loclist opcodes. local DW_LLE_end_of_list = 0x00 local DW_LLE_start_length = 0x08 -local DW_OP_reg0 = 0x50 -- base reg op; regN = 0x50 + N -local DW_OP_breg0 = 0x70 -- base breg op; bregN = 0x70 + N (SLEB offset) +local DW_OP_reg0 = 0x50 -- base reg op; regN = 0x50 + N +local DW_OP_breg0 = 0x70 -- base breg op; bregN = 0x70 + N (SLEB offset) local DW_OP_piece = 0x93 local MIPS_LOAD_DELAY_BYTES = 0x08 -- 1 load word + 1 BD-slot word @@ -184,15 +184,17 @@ end --- Resolve an absolute provenance path to the line-unit file index used by the emitting line program. --- Normalizes mixed `/` and `\` separators to a basename and looks it up against the runtime-computed file table populated by `init_file_index_lookup`. --- ---- Fails loudly on an unknown provenance basename: adding a new component source file will produce a clear error message naming the missing basename and listing the .debug_line file table contents, ---- so the user can either confirm the gcc include order, the unity-root, or the `.debug_line` file table contents. ---- Silent fallback would mask the new-file case by misattributing component rows to an arbitrary source file. +--- Returns 0 (the DWARF `set_file(0)` "no file change" sentinel) when the basename is not in the file table. +--- This is a normal occurrence: the compiler only adds a file to the `.debug_line` file table when the file has line-numbered content (i.e., code). +--- Files containing only static-array data (e.g. `MipsAtomComp_` declarations in `gp.atom.c`, `psyq.atom.c`, `pad.atom.c` — the OT-tag inserts, etc.) produce no line numbers, +--- so gcc omits them from the file table. +--- The DWARF emitter then keeps the previous line-program file state instead of pointing at a file that has no entries to walk. +--- A stderr warning is emitted per-miss so the user can audit which files the compiler dropped. --- @param path string -- absolute provenance path (mixed slashes accepted) ---- @return integer -- 1-based line-unit file index +--- @return integer -- 1-based line-unit file index, or 0 on miss (DWARF no-change sentinel) local function resolve_provenance_file_index(path) if _file_index_by_basename == nil then - error("[dwarf_injection] resolve_provenance_file_index called before init_file_index_lookup. " - .. "Is M.run being entered correctly (with --elf)?") + error("[dwarf_injection] resolve_provenance_file_index called before init_file_index_lookup. Is M.run being entered correctly (with --elf)?") end if path == nil or path == "" then error("[dwarf_injection] resolve_provenance_file_index: empty path") @@ -201,19 +203,17 @@ local function resolve_provenance_file_index(path) local normalized = path:gsub("\\", "/") -- Take the last path component (the basename). local basename = normalized:match("([^/]+)$") or normalized - local idx = _file_index_by_basename[basename] + local idx = _file_index_by_basename[basename] if idx ~= nil then return idx end -- Last-resort exact-path match (handles paths that don't reduce to a known basename). for i, p in pairs(_file_path_by_index) do if p and p:gsub("\\", "/") == normalized then return i end end - -- Build an error message listing the known basenames for fast diagnostics. - local known = {} - for k in pairs(_file_index_by_basename) do known[#known + 1] = k end - table.sort(known) - error(string.format("[dwarf_injection] resolve_provenance_file_index: unknown provenance basename '%s' (from '%s'). " - .. "Known basenames in the .debug_line file table (%d): %s" - , basename, path, #known, table.concat(known, ", "))) + -- File is in the corpus but gcc omitted it from the .debug_line file table (data-only content). + -- Return 0 = DWARF `set_file(0)` no-change sentinel so the line program keeps its prior file state. + io.stderr:write(string.format("[dwarf_injection] line-table miss: '%s' (basename '%s') not in .debug_line file table; " + .. "falling back to set_file(0)\n", path, basename)) + return 0 end local DW_FORM_addr = 0x01 @@ -228,7 +228,6 @@ local DW_FORM_sec_offset = 0x17 -- 4-byte section-relative offset (into .d -- DW_OP_reg0 + DW_OP_piece are declared above (lines 114-116) alongside the other DWARF5 §7.7.3 loclist opcodes. - local DW_ATE_unsigned = 0x07 -- DWARF5 §7.8.1: DW_ATE_unsigned (used for U4 base type) -- (DW_LANG_Mips_Assembler = 0x8001 was used in the, but we want this CU to look like a C TU so VSCode's Variables pane treats it as code.) @@ -448,9 +447,9 @@ end --- Statement-state rules: --- * A marked whole atom emits one opaque is_stmt=false range row and no nested component rows; its subprogram symbol/range remains available. --- * Per-row policy at every other PC: ---- - Call-site row of any invocation's first word: is_stmt = true (unconditional; `want_call = true`). +--- - Call-site row of any invocation's first word: is_stmt = true (unconditional; `want_call = true`). --- - Body row of any invocation (first or subsequent): is_stmt = not inv.debug_skip (`want_body = not inv.debug_skip`). ---- - RAW word (no containing invocation): is_stmt = true (unconditional). +--- - RAW word (no containing invocation): is_stmt = true (unconditional). --- * The previous per-word `marked_idx` ancestor walk and the GDB 12 zero-instruction-prologue duplicate row at atom entry are DELETED; the new --- first-word emission IS the entry statement. --- * Whole-atom suppression wins over component markers; no nested inversion. @@ -612,10 +611,8 @@ local function build_atom_sequence(atom) -- NOT anc.body_lines[1] (= the line of the first WORD, which is wrong when the outer's body starts with a nested call). for ai, anc in ipairs(entry_1_ancestry) do assert(anc.body_lines, "missing body_lines: emitter did not run emission-model") - assert(anc.body_lines[1] ~= nil - , "dwarf_injection: body_lines[1] missing on first-word entry for inv=" .. tostring(anc.component_name)) - assert(anc.call_path and anc.call_path ~= "" - , "dwarf_injection: inv.call_path is missing on invocation " .. tostring(anc.component_name) .. "; emitter did not run emission-model.") + assert(anc.body_lines[1] ~= nil, "dwarf_injection: body_lines[1] missing on first-word entry for inv=" .. tostring(anc.component_name)) + assert(anc.call_path and anc.call_path ~= "", "dwarf_injection: inv.call_path is missing on invocation " .. tostring(anc.component_name) .. "; emitter did not run emission-model.") emit_row(resolve_provenance_file_index(anc.call_path), anc.call_line, true) local is_outermost = (ai == 1) if not (is_outermost and anc.debug_skip) then @@ -640,8 +637,7 @@ local function build_atom_sequence(atom) -- all OTHER ancestors emit body_lines[1] with is_stmt = not debug_skip. -- -- This re-emits the outer ancestor's call-site + body rows at the inner's first word PC - -- for debugger context: source-level stepping now shows the outer body line - -- (not the inner body line) when stepping into the inner. PROBLEM B fix. + -- for debugger context: source-level stepping now shows the outer body line (not the inner body line) when stepping into the inner. PROBLEM B fix. -- The body_lines[1] row references body_first_line_of[anc.id] (= the body's first content line in the parent's source), -- NOT anc.body_lines[1] (= the line of the first WORD, which is wrong when the outer's body starts with a nested call: -- gdb 12.1 picks the displayed line as the LAST row at the same PC in byte-stream order, @@ -649,12 +645,9 @@ local function build_atom_sequence(atom) local ancestry = ancestry_idx[idx] for ai, anc in ipairs(ancestry) do assert(anc.body_lines, "missing body_lines: emitter did not run emission-model") - assert(anc.body_lines[1] ~= nil - , string.format("missing body_lines[1] for inv=%s start_pos=%d len=%d", - anc.component_name, anc.start_pos, #(anc.body_lines or {}))) - assert(anc.call_path and anc.call_path ~= "" - , "dwarf_injection: inv.call_path is missing on invocation " .. tostring(anc.component_name) .. "; emitter did not run emission-model.") - emit_row(resolve_provenance_file_index(anc.call_path), anc.call_line, true) + assert(anc.body_lines[1] ~= nil, string.format("missing body_lines[1] for inv=%s start_pos=%d len=%d", anc.component_name, anc.start_pos, #(anc.body_lines or {}))) + assert(anc.call_path and anc.call_path ~= "", "dwarf_injection: inv.call_path is missing on invocation " .. tostring(anc.component_name) .. "; emitter did not run emission-model.") + emit_row(resolve_provenance_file_index(anc.call_path), anc.call_line, true) local is_outermost = (ai == 1) if not (is_outermost and anc.debug_skip) then emit_row(resolve_provenance_file_index(anc.def_path), body_first_line_of[anc.id] or anc.body_lines[1], not anc.debug_skip) @@ -669,10 +662,8 @@ local function build_atom_sequence(atom) -- Marked invocations emit non-statement body rows at every body word; unmarked invocations emit statement body rows. assert(inv.body_lines, "missing body_lines: emitter did not run emission-model") local words_into = idx - inv.start_pos - assert(inv.body_lines[words_into] ~= nil - , string.format("missing body_lines[%d] for inv=%s start_pos=%d len=%d idx=%d", - words_into, inv.component_name, inv.start_pos, #(inv.body_lines or {}), idx)) - emit_row(resolve_provenance_file_index(inv.def_path), inv.body_lines[words_into], not inv.debug_skip) + assert(inv.body_lines[words_into] ~= nil, string.format("missing body_lines[%d] for inv=%s start_pos=%d len=%d idx=%d", words_into, inv.component_name, inv.start_pos, #(inv.body_lines or {}), idx)) + emit_row(resolve_provenance_file_index(inv.def_path), inv.body_lines[words_into], not inv.debug_skip) else -- RAW word: single call-site row, always a statement target (the word itself is unmarked). emit_row(call_file_idx, entry.line, true) @@ -712,8 +703,8 @@ end --- `{comp_name, call_file, call_line, comp_file, comp_line, start_pos, end_pos, body_lines, debug_skip}`. `body_lines[k]` --- is the k-th word's source line within the component body. --- ---- @param corpus table -- the corpus from `ctx.shared.corpus` ---- @param addrs table -- ELF symbols keyed by atom name from `elf_dwarf.read_nm` +--- @param corpus table -- the corpus from `ctx.shared.corpus` +--- @param addrs table -- ELF symbols keyed by atom name from `elf_dwarf.read_nm` --- @return table[] -- list of {name, addr, size_bytes, words, entries, invocations, debug_skip?} local function build_atom_table(corpus, addrs) -- Cross-ref: keep only atoms present in BOTH the nm symbol table AND `corpus.atoms_by_name`. Output is sorted by ascending addr. @@ -730,8 +721,8 @@ local function build_atom_table(corpus, addrs) local word_events = paths.word_events or {} local invocations_proj = paths.invocations or {} -- Build the dense entries list from `word_events`. - -- `word_events[i].i` = the 0-based `.word` position - -- `call_line` = the root atom's physical source line for that word (stamped by emission_model) + -- `word_events[i].i` = the 0-based `.word` position + -- `call_line` = the root atom's physical source line for that word (stamped by emission_model) local entries = {} for idx, ev in ipairs(word_events) do entries[#entries + 1] = { @@ -773,9 +764,8 @@ local function build_atom_table(corpus, addrs) local out = {} -- Walk every source's atom list (which preserves source order + per-source src_path). - -- Cross-ref with the nm symbol table; atoms absent from `addrs` are skipped (an atom - -- declared in source but not emitted as a symbol is a metaprogram or atom-info bug, not - -- a source-correlation bug — emit_no_emit would catch it upstream). + -- Cross-ref with the nm symbol table; atoms absent from `addrs` are skipped + -- (an atom declared in source but not emitted as a symbol is a metaprogram or atom-info bug, not a source-correlation bug — emit_no_emit would catch it upstream). for _, src in ipairs((corpus and corpus.source_order) or {}) do local src_path = src.path or "" for _, atom_rec in ipairs(((src.scan or {}).atoms) or {}) do @@ -843,8 +833,7 @@ end --- (no `atom_reg` opt-in) are silently skipped — the resulting rbind record will be incomplete and the atom will fail to bind a usable piece chain. --- This is intentional: silently falling back to a hardcoded GPR would mask the missing opt-in. --- ---- Pre-tokenized: `body_tokens` is the scan-source pass's pre-split list of top-level ---- statements (each entry is a single `load_*` call or other statement). +--- Pre-tokenized: `body_tokens` is the scan-source pass's pre-split list of top-level statements (each entry is a single `load_*` call or other statement). --- @param body_tokens table[] -- the atom's pre-tokenized body statements (from atom.body_tokens) --- @param binds_name string -- expected Binds_X name (skip pairs with mismatching binds) --- @param registries table -- merged registries from collect_per_source_registries @@ -856,7 +845,7 @@ local function parse_body_load_pairs(body_tokens, binds_name, registries) -- The captured ident is `kind`; `inner` holds the parens body for arg parsing. local load_pattern = "^(load_word|load_half|load_half_u|load_byte|load_byte_u|gte_lw|gte_lwc2)%s*%((.*)%)$" for _, t in ipairs(body_tokens or {}) do - local tok = duffle.trim(t.tok or "") + local tok = duffle.trim(t.tok or "") local kind, inner = tok:match(load_pattern) if kind then local args = duffle.split_top_level_commas(inner) @@ -882,9 +871,7 @@ local function parse_body_load_pairs(body_tokens, binds_name, registries) end --- Collect every rbind atom + the matching Binds_X struct + (reg, field) pairs. ---- --- Inputs come from the dep-closed `scan-source` pass (the per-source `src.scan` payload is preserved on each `corpus.source_order` entry). ---- --- Returns: --- rbind_atoms = {[atom_name] = {binds, fields, regs, byte_size, info_line}} --- rbind_structs = {[binds_name] = {byte_size, fields, atom_names}} @@ -929,7 +916,7 @@ local function parse_rbind_atoms(corpus, atom_table, registries) local body_tokens_by_atom = {} for _, src in ipairs((corpus and corpus.source_order) or {}) do local scan = src.scan - if scan then + if scan then for _, atom in ipairs(scan.atoms or {}) do body_tokens_by_atom[atom.name] = atom.body_tokens end @@ -948,8 +935,8 @@ local function parse_rbind_atoms(corpus, atom_table, registries) for atom_name, ai in pairs(ai_by_atom) do if ai.binds then - local struct = rbind_structs[ai.binds] - local body_toks = body_tokens_by_atom[atom_name] + local struct = rbind_structs[ai.binds] + local body_toks = body_tokens_by_atom[atom_name] if struct and body_toks then local pairs = parse_body_load_pairs(body_toks, ai.binds, registries) if #pairs > 0 then @@ -984,7 +971,8 @@ end --- (the final unit, referenced by the main CU's DW_AT_stmt_list). --- --- This builder extends the main compilation unit. ---- A detached synthetic line unit has no DW_AT_stmt_list referencing it, so gdb ignored it (a previous experiment); byte 13 is the first special opcode, not the extended-opcode marker. +--- A detached synthetic line unit has no DW_AT_stmt_list referencing it, so gdb ignored it (a previous experiment); +--- byte 13 is the first special opcode, not the extended-opcode marker. --- The existing final unit already contains hello_gte_tape.c as file index 11 and ends with a valid end_sequence. --- We preserve its bytes, append independent atom sequences, and increase only that unit's DWARF32 unit_length. --- @param existing string -- existing section bytes, byte-for-byte @@ -995,9 +983,7 @@ local function build_dwarf_line_section(existing, atom_table) -- Build the sequences. local sequences = {} - for _, atom in ipairs(atom_table) do - sequences[#sequences + 1] = build_atom_sequence(atom) - end + for _, atom in ipairs(atom_table) do sequences[#sequences + 1] = build_atom_sequence(atom) end local appended = table.concat(sequences) -- Walk DWARF32 line units and retain the final unit's bounds. @@ -1005,7 +991,7 @@ local function build_dwarf_line_section(existing, atom_table) local unit_pos, last_pos, last_length, last_end = 0, nil, nil, nil while unit_pos < #existing do if unit_pos + 4 > #existing then return existing end - local unit_length = elf_dwarf.read_u32_le(existing, unit_pos) + local unit_length = elf_dwarf.read_u32_le(existing, unit_pos) if unit_length == elf_dwarf.ELF32.dw_dwarf32_terminator then return existing end local unit_end_excl = unit_pos + 4 + unit_length if unit_end_excl > #existing then return existing end @@ -1531,13 +1517,12 @@ end --- DW_AT_location = piece-chain (DW_FORM_exprloc) --- DW_AT_type = ref4 → structure_type DIE --- ---- This function does NOT emit the final 0 byte (root terminator). build_debug_info_section splices bytes ahead of the root terminator ---- and preserves existing DIE bytes exactly. +--- This function does NOT emit the final 0 byte (root terminator). +--- build_debug_info_section splices bytes ahead of the root terminator and preserves existing DIE bytes exactly. --- --- ref4 basis: DW_FORM_ref4 is CU-relative (offset from the first byte of the CU header). --- Our inserted DIEs live in the main CU, so every ref4 = (target section offset) - main_cu_offset. --- Per-die section offsets are tracked via the running `next_offset` cursor (= section offset of the NEXT byte to emit). ---- --- @param main_cu_offset integer -- 0-based section offset of the main CU's unit_length field --- @param main_cu_end_excl integer -- 0-based section offset of the first byte AFTER the main CU --- @param atom_table table[] -- atoms (with atom.rbind set if rbind; atom.invocations set if mac_X(...) calls) @@ -1657,6 +1642,8 @@ local function build_inserted_children(main_cu_offset, main_cu_end_excl, atom_ta -- -- The table is small + explicit — the prototype principle treats the typed-view struct layout as data, not derived state. local STRUCT_MEMBER_TABLE = { + -- TODO(Ed): This hardcoding is brittle... + -- TODO(Ed): Better to just have a table for the fundamental types in duffle/dsl.h, we can derive the rest via typedef parsing... -- 2-element signed short vector (rare; placeholder for future use). V2_S2 = { byte_size = 4, members = { { name = "x", offset = 0, byte_size = 2 }, @@ -1909,12 +1896,13 @@ local function build_inserted_children(main_cu_offset, main_cu_end_excl, atom_ta end local atom_view = (registries.atom_views or {})[atom.name] -- Build the atom-name lookup table once (cheap; O(atom_table)) so step (b) and step (d) can resolve rbind_atom names. + -- TODO(Ed): Bad assignment? local atom_by_name = atom_by_name or (function() local m = {}; for _, a in ipairs(atom_table) do if a.name then m[a.name] = a end end; return m end)() -- step (b) inputs: this atom's `atom_ctx()` (resolved from the registries' atom_ctxs) local this_ctx = registries.atom_ctxs and registries.atom_ctxs[atom.name] if this_ctx and this_ctx.rbind_atom then local rbind = atom_by_name_global[this_ctx.rbind_atom] - if rbind and rbind.rbind and rbind.rbind.fields then + if rbind and rbind.rbind and rbind.rbind.fields then atom_view_ctx_fields = {} for _, f in ipairs(rbind.rbind.fields) do atom_view_ctx_fields[f.name] = f end if rbind.rbind.regs then @@ -1934,11 +1922,11 @@ local function build_inserted_children(main_cu_offset, main_cu_end_excl, atom_ta end if my_phase_label then local group = (registries.atom_phases or {})[my_phase_label] - if group and group.atoms then + if group and group.atoms then for _, group_atom_name in ipairs(group.atoms) do if group_atom_name ~= atom.name then local cand = atom_by_name_global[group_atom_name] - if cand and cand.rbind and cand.rbind.fields then + if cand and cand.rbind and cand.rbind.fields then atom_view_phase_fields = {} for _, f in ipairs(cand.rbind.fields) do atom_view_phase_fields[f.name] = f end if cand.rbind.regs then @@ -1959,7 +1947,7 @@ local function build_inserted_children(main_cu_offset, main_cu_end_excl, atom_ta -- (a) per-atom callsite atom_type(R_X, ): most specific; user explicit override for THIS atom only. function(r_name, alias_code) local override = atom_view and atom_view.reg_type_overrides and atom_view.reg_type_overrides[r_name] - if override and override.pointer_depth and override.pointer_depth > 0 then + if override and override.pointer_depth and override.pointer_depth > 0 then return type_chain_offsets[override.type_name .. "|" .. override.pointer_depth] end end, @@ -1967,7 +1955,7 @@ local function build_inserted_children(main_cu_offset, main_cu_end_excl, atom_ta function(r_name, alias_code) local ctx_field_name = reg_to_field_ctx and reg_to_field_ctx[alias_code] local ctx_f = ctx_field_name and atom_view_ctx_fields and atom_view_ctx_fields[ctx_field_name] - if ctx_f and ctx_f.pointer_depth and ctx_f.pointer_depth > 0 then + if ctx_f and ctx_f.pointer_depth and ctx_f.pointer_depth > 0 then return type_chain_offsets[ctx_f.type_name .. "|" .. ctx_f.pointer_depth] end end, @@ -1975,7 +1963,7 @@ local function build_inserted_children(main_cu_offset, main_cu_end_excl, atom_ta function(r_name, alias_code) local field_name = reg_to_field[alias_code] local f = field_name and field_type_by_name[field_name] - if f and f.pointer_depth and f.pointer_depth > 0 then + if f and f.pointer_depth and f.pointer_depth > 0 then return type_chain_offsets[f.type_name .. "|" .. f.pointer_depth] end end, @@ -1983,12 +1971,13 @@ local function build_inserted_children(main_cu_offset, main_cu_end_excl, atom_ta function(r_name, alias_code) local phase_field_name = reg_to_field_phase and reg_to_field_phase[alias_code] local phase_f = phase_field_name and atom_view_phase_fields and atom_view_phase_fields[phase_field_name] - if phase_f and phase_f.pointer_depth and phase_f.pointer_depth > 0 then + if phase_f and phase_f.pointer_depth and phase_f.pointer_depth > 0 then return type_chain_offsets[phase_f.type_name .. "|" .. phase_f.pointer_depth] end end, -- (e) enum-site atom_type() default on the registry entry. function(r_name, alias_code) + -- TODO(Ed): Bad definition? if alias and alias.default_type and alias.default_depth and alias.default_depth > 0 then return type_chain_offsets[alias.default_type .. "|" .. alias.default_depth] end @@ -1997,8 +1986,8 @@ local function build_inserted_children(main_cu_offset, main_cu_end_excl, atom_ta -- Iterate `by_alias` in sorted order; Lua's pairs() is non-deterministic, so sorting ensures byte-identical DWARF output across builds. for _, r_name in ipairs(by_alias_order) do - local alias = by_alias[r_name] - local rr_name = "RR_" .. strip_r_prefix(r_name) + local alias = by_alias[r_name] + local rr_name = "RR_" .. strip_r_prefix(r_name) local alias_code = alias.code emit(uleb128(ABBREV_VARIABLE)) emit(rr_name .. "\0") -- DW_FORM_string (DW_AT_name) @@ -2020,7 +2009,7 @@ local function build_inserted_children(main_cu_offset, main_cu_end_excl, atom_ta -- Two PC ranges cover every field: [atom.addr, last_load+8) describes each field as tape memory (DW_OP_bregN + offset) piece, -- and [last_load+8, atom.end) describes each field as a GPR (DW_OP_regN) piece. if atom.rbind then - local binds_name = atom.rbind.binds + local binds_name = atom.rbind.binds local loclists_offset = loclists_offsets[atom.name] or 0 emit(uleb128(ABBREV_BIND_VAR_LOCLIST)) emit("bind_args\0") -- DW_FORM_string (DW_AT_name) @@ -2073,7 +2062,7 @@ end --- --- Fails safely by returning existing sections unchanged if the table walker can't find the table terminator (malformed input). --- ---- @param existing string -- existing .debug_abbrev bytes, byte-for-byte +--- @param existing string -- existing .debug_abbrev bytes, byte-for-byte --- @param main_abbrev_offset integer -- 0-based offset into `existing` of the main CU's abbrev table --- @return string, integer -- (new_abbrev_bytes, offset_where_duplicate_table_starts = #existing) local function build_debug_abbrev_section(existing, main_abbrev_offset) @@ -2091,7 +2080,7 @@ local function build_debug_abbrev_section(existing, main_abbrev_offset) end --- Build the new .debug_str: existing strings + new strings appended. ---- @param existing string -- existing .debug_str bytes, byte-for-byte +--- @param existing string -- existing .debug_str bytes, byte-for-byte --- @param atom_table table[] --- @param registries table -- merged registries from collect_per_source_registries --- @return string -- existing bytes plus the deterministic appended strings @@ -2101,7 +2090,6 @@ local function build_debug_str_section(existing, atom_table, registries) end --- Build the new .debug_info: SPLICE inserted DIEs into the MAIN CU as children. ---- --- This implementation: --- 1. Builds the inserted-children bytes (base_type, struct_types, subprograms with their RR_* + bind_args children) via build_inserted_children. --- 2. Patches the main CU's `unit_length` field to account for the inserted bytes. @@ -2111,14 +2099,14 @@ end --- --- The crt CU (everything before main_cu_start) is preserved. ---- @param existing string -- existing .debug_info section bytes ---- @param main_cu_start integer -- 0-based offset of the main CU's unit_length field ---- @param main_cu_end_excl integer -- 0-based offset of the first byte AFTER the main CU +--- @param existing string -- existing .debug_info section bytes +--- @param main_cu_start integer -- 0-based offset of the main CU's unit_length field +--- @param main_cu_end_excl integer -- 0-based offset of the first byte AFTER the main CU --- @param new_abbrev_offset integer -- 0-based offset into the new .debug_abbrev of the duplicate main table ---- @param atom_table table[] ---- @param rbind_structs table -- {[binds_name] = {bytes, fields, atom_names}} ---- @param loclists_offsets table -- {[atom_name] = section-relative offset} ---- @param registries table -- merged registries from collect_per_source_registries +--- @param atom_table table[] +--- @param rbind_structs table -- {[binds_name] = {bytes, fields, atom_names}} +--- @param loclists_offsets table -- {[atom_name] = section-relative offset} +--- @param registries table -- merged registries from collect_per_source_registries --- @return string -- the rebuilt .debug_info bytes local function build_debug_info_section(existing, main_cu_start, main_cu_end_excl, new_abbrev_offset, atom_table, rbind_structs, loclists_offsets, registries) -- 1) Build the inserted children bytes (just before the main CU's root terminator). @@ -2176,13 +2164,13 @@ local SECTION_WRITERS = { -- Write a list of `{name, data}` section records to disk via SECTION_WRITERS. -- @param results table[] -- list of `{name=, data=}` records to write -- @param ctx PassCtx --- @param basename string -- output file basename (e.g. "hello_gte") --- @return table -- list of {name_bin = path} entries to append to M.run's outputs +-- @param basename string -- output file basename (e.g. "hello_gte") +-- @return table -- list of {name_bin = path} entries to append to M.run's outputs local function write_sections(results, ctx, basename) local outputs = {} for _, r in ipairs(results) do local path = SECTION_WRITERS[r.name](ctx.out_root, basename) - local f = io.open(path, "wb") + local f = io.open(path, "wb") if not f then io.stderr:write(string.format("[dwarf_injection] failed to open %s for write\n", path)) else @@ -2209,7 +2197,7 @@ function M.run(ctx) end -- Guard: --elf is required. - local elf_path = ctx.flags and ctx.flags.elf_path + local elf_path = ctx.flags and ctx.flags.elf_path if not elf_path or elf_path == "" then io.stderr:write("[dwarf_injection] --elf flag missing\n") return { outputs = {}, errors = {}, warnings = {} } @@ -2220,7 +2208,8 @@ function M.run(ctx) -- Read the existing DWARF sections directly (no subprocess; io.open + manual ELF32 section-header walk). -- We need all 8 sections: .debug_line / .debug_aranges / .debug_rnglists get extended (additional rows appended to the existing unit), - -- and .debug_info / .debug_abbrev / .debug_str / .debug_loc / .debug_loclists get spliced (the main CU's unit_length is patched; no new compile unit is appended; .debug_loc/.debug_loclists may not exist in the source ELF so we add-section them on splice). + -- and .debug_info / .debug_abbrev / .debug_str / .debug_loc / .debug_loclists get spliced + -- (the main CU's unit_length is patched; no new compile unit is appended; .debug_loc/.debug_loclists may not exist in the source ELF so we add-section them on splice). -- The per-section dispatch is inlined in the writers loop below. local existing_sections = elf_dwarf.read_elf_sections(elf_path, { ".debug_line", ".debug_aranges", ".debug_rnglists", @@ -2235,12 +2224,11 @@ function M.run(ctx) init_file_index_lookup(elf_path) -- Skip state lives in `corpus.atoms_by_name[*].debug_skip` (whole-atom) and `atom.paths.invocations[*].debug_skip` (per-invocation). -- `corpus` is the sole canonical source projection. - local corpus = (ctx.shared and ctx.shared.corpus) or {} + local corpus = (ctx.shared and ctx.shared.corpus) or {} local registries = collect_per_source_registries(corpus) - -- Read nm symbols (the ONLY disk-side input to the atom table) and join - -- them against `corpus.atoms_by_name` + `atom.paths` for word rows + invocation ancestry. + -- Read nm symbols (the ONLY disk-side input to the atom table) and join them against `corpus.atoms_by_name` + `atom.paths` for word rows + invocation ancestry. -- Disk source-map/provenance text is not consulted (those are diagnostic artifacts; semantic inputs are in memory). - local addrs = elf_dwarf.read_nm(ctx.flags.elf_path) + local addrs = elf_dwarf.read_nm(ctx.flags.elf_path) local atom_table = build_atom_table(corpus, addrs) -- Detect rbind atoms + index Binds_* struct fields from the corpus. @@ -2264,7 +2252,7 @@ function M.run(ctx) duffle.ensure_dir(ctx.out_root) -- Step 0: layout validation. Bail out safely if the .debug_info layout doesn't match what we expect (crt CU + DWARF5 main CU + final 0 byte). - -- A layout mismatch means the gcc emission changed; the safest response is to leave existing sections unchanged and emit no synthetic data, so the build's debug-info step never silently produces broken DWARF. + -- A layout mismatch means the gcc emission changed; the safest response is to leave existing sections unchanged and emit no synthetic data, so the build's debug-info step never silently produces broken DWARF. local existing_info = existing_sections[".debug_info"] or "" local existing_abbrev = existing_sections[".debug_abbrev"] or "" local main_cu_start, main_cu_end_excl, main_abbrev_offset = find_main_cu_layout(existing_info) @@ -2299,7 +2287,7 @@ function M.run(ctx) local new_info = build_debug_info_section(existing_info, main_cu_start, main_cu_end_excl, new_abbrev_offset, atom_table, rbind_structs, loclists_offsets, registries) -- Step 2b: rebuild .debug_str now that we know which RR_ entries get emitted. - -- This aligns with build_debug_info_section's by_alias loop. + -- This aligns with build_debug_info_section's by_alias loop. local new_str = build_debug_str_section(existing_sections[".debug_str"] or "", atom_table, registries) -- Step 3-5: independent sections. diff --git a/scripts/passes/emission_model.lua b/scripts/passes/emission_model.lua index 0905e49..4081b2c 100644 --- a/scripts/passes/emission_model.lua +++ b/scripts/passes/emission_model.lua @@ -41,7 +41,6 @@ local duffle = dofile(_bootstrap_dir .. "../duffle_paths.lua") -- Convert the recursive walk's body-relative line numbers into physical source lines once. -- The walker builds `line_of` from `body_text` and stamps body-relative line numbers (1..N) into `item.line` and `invocation.call_line`. -- This function converts those values to physical source lines at the close site with the forwarded source `line_of` closure. --- -- `call_line` discipline: -- * ROOT invocations (`inv.parent_id == 0`) receive body-relative `call_line` values directly from `M.LineIndex(body_text)` in the walker. -- The source `line_of` closure supplies physical lines at the close site, so this function converts each root value exactly once. @@ -59,10 +58,9 @@ local function stamp_root_provenance(projection, atom_record, src, corpus) -- `root_body_line` is the physical source line of the ATOM HEADER byte containing the opening `{`; that byte is one byte BEFORE `atom_record.body_off`. -- The walker assigns line 2 to the body's first content line because line 1 is the trailing `\n` after `{`. Body-text line k therefore maps to `root_body_line + (k - 1)`. -- `body_off - 1` points at the opening `{`, whose line index identifies the header line. `body_off` points after `{` and would shift every word row forward by one line. - local root_body_line = root_line_of(atom_record.body_off - 1) - or atom_record.line or 0 + local root_body_line = root_line_of(atom_record.body_off - 1) or atom_record.line or 0 local component_index = corpus.component_body_index or {} - local word_items = {} + local word_items = {} for _, item in ipairs(projection.items) do if item.kind == "word" then word_items[#word_items + 1] = item end @@ -102,7 +100,7 @@ local function stamp_root_provenance(projection, atom_record, src, corpus) end -- Normalize `inv.call_line` to a physical source line. - -- * ROOT invocations (`parent_id == 0`) carry body-relative `call_line` values from `M.LineIndex(body_text)`; convert them once with `root_body_line`. + -- * ROOT invocations (`parent_id == 0`) carry body-relative `call_line` values from `M.LineIndex(body_text)`; convert them once with `root_body_line`. -- * INNER invocations (`parent_id ~= 0`) carry physical `call_line` values from the component's `line_of`; retain them unchanged. for _, inv in ipairs(projection.invocations) do if inv.parent_id == 0 then @@ -114,14 +112,14 @@ local function stamp_root_provenance(projection, atom_record, src, corpus) -- `atoms_source_map` and `dwarf_injection` read `inv.body_lines[k]` directly from the invocation record created here. -- Component words already carry physical `item.line` values from the walker's COMPONENT line index, so `body_line_for` returns them unchanged. for _, inv in ipairs(projection.invocations) do - local sw = inv.start_word - local ew = inv.end_word + local sw = inv.start_word + local ew = inv.end_word local bls = {} for i = sw, ew do local it = projection.items and projection.items[i] - if it and it.kind == "word" then + if it and it.kind == "word" then local fake_event = { invocation_ids = { inv.id } } - bls[#bls + 1] = body_line_for(fake_event, it) or 0 + bls[#bls + 1] = body_line_for(fake_event, it) or 0 end end inv.body_lines = bls diff --git a/scripts/passes/offsets.lua b/scripts/passes/offsets.lua index c9746b4..ae295d7 100644 --- a/scripts/passes/offsets.lua +++ b/scripts/passes/offsets.lua @@ -3,12 +3,11 @@ --- Reads the pre-scanned SourceScan payload (produced once upstream by `duffle.scan_source`) --- for `MipsAtom_(name)` and `MipsCode code_` declarations, computes the word offset --- from each `atom_offset(F, T)` marker to its target `atom_label(T)` declaration, and emits ---- `.offsets.h` with one `#define _atom_offset_F_T = N` per branch. +--- `gen/offsets.h` with one `#define _atom_offset_F_T = N` per branch. +--- Per-directory aggregation: every source in the same directory contributes to the same `gen/offsets.h`. +--- The directory itself is the namespace; the filename does not repeat the module name. --- --- The offset is `target_word - branch_word - 1` (the standard MIPS branch-immediate encoding: branch_offset = relative_pc_in_words - 1). ---- ---- **Conventions**: tabs (1/level), EmmyLua annotations, no regex, ---- Lua 5.3 compatible. -- ════════════════════════════════════════════════════════════════════════════ -- Module-scope requires + package.path setup @@ -16,12 +15,11 @@ -- Bootstrap: same as entry scripts. See `ps1_meta.lua` for the rationale. -- Bootstrap: load `scripts/duffle_paths.lua` (sets package.path + package.cpath). --- Uses `debug.getinfo` to find this file's own directory, so it works --- both standalone and when require'd from the orchestrator. +-- Uses `debug.getinfo` to find this file's own directory, so it works both standalone and when require'd from the orchestrator. -- Bootstrap: load `duffle_paths.lua` via `debug.getinfo(1, "S").source` (works both standalone + when require'd). -- duffle_paths.lua sets package.path then returns `require("duffle")` at the bottom, so the dofile value IS the duffle module. local _bootstrap_dir = debug.getinfo(1, "S").source:match("^@?(.*[/\\])") or "./" -local duffle = dofile(_bootstrap_dir .. "../duffle_paths.lua") +local duffle = dofile(_bootstrap_dir .. "../duffle_paths.lua") -- ════════════════════════════════════════════════════════════════════════════ -- Constants @@ -39,35 +37,35 @@ local OFFSET_MACRO_COL = 44 -- ════════════════════════════════════════════════════════════════════════════ --- @class SourceFile ---- @field path string -- absolute path to the source file ---- @field text string -- the full source text ---- @field dir string -- the directory containing the source ---- @field basename string -- filename without extension ---- @field scan table -- pre-scanned SourceScan payload (from duffle.scan_source) +--- @field path string -- Absolute path to the source file +--- @field text string -- Full source text +--- @field dir string -- Directory containing the source +--- @field basename string -- Filename without extension +--- @field scan table -- Pre-scanned SourceScan payload (from duffle.scan_source) --- @class PassCtx ---- @field shared table -- cross-pass shared state ---- @field shared.corpus table -- canonical corpus projection +--- @field shared table -- Cross-pass shared state +--- @field shared.corpus table -- Corpus projection --- @field shared.word_counts table ---- @field out_root string -- output root (e.g. "build/gen") +--- @field out_root string -- Output root (e.g. "build/gen") --- @class PassResult --- @field outputs table[] -- {kind=, path=} entries describing emit files ---- @field errors table[] -- {line=, msg=} entries; build-stops ---- @field warnings table[] -- {line=, msg=} entries; build-succeeds +--- @field errors table[] -- {line=, msg=} entries; build-stops +--- @field warnings table[] -- {line=, msg=} entries; build-succeeds --- @class BranchOffset ---- @field tag string -- the marker tag (e.g. "F" in `atom_offset(F, T)`) ---- @field target string -- the target label name (e.g. "T" in `atom_offset(F, T)`) ---- @field branch_word integer -- branch word position within the atom body ---- @field offset integer -- computed per consuming instruction (see `compute_offsets`) ---- @field consuming_encoder string|nil -- the instruction consuming the offset (e.g. "branch_le_zero", "jump", "call_addr") +--- @field tag string -- Marker tag (e.g. "F" in `atom_offset(F, T)`) +--- @field target string -- Target label name (e.g. "T" in `atom_offset(F, T)`) +--- @field branch_word integer -- Branch word position within the atom body +--- @field offset integer -- Computed per consuming instruction (see `compute_offsets`) +--- @field consuming_encoder string|nil -- Instruction consuming the offset (e.g. "branch_le_zero", "jump", "call_addr") --- @field consuming_arg_pos integer|nil -- 1-based arg position within the consuming instruction's arg list --- @class AtomData ---- @field name string -- atom name ---- @field total_words integer -- total word count of the atom body ---- @field offsets BranchOffset[] -- per-branch offset list +--- @field name string -- Atom name +--- @field total_words integer -- Total word count of the atom body +--- @field offsets BranchOffset[] -- Per-branch offset list -- ════════════════════════════════════════════════════════════════════════════ -- Canonical marker projection @@ -99,7 +97,7 @@ local function project_markers(markers) local state = { labels = {}, branches = {} } for _, marker in ipairs(markers or {}) do local project = MARKER_PROJECTORS[marker.kind] - if project then project(state, marker) end + if project then project(state, marker) end end return state.labels, state.branches end @@ -109,7 +107,6 @@ end -- ════════════════════════════════════════════════════════════════════════════ --- Compute branch offsets per consuming instruction. ---- --- Disposition table: --- `branch_*` -> relative offset: `target_word - branch_word - 1` (MIPS branch-immediate encoding). --- `jump` / `call_addr` -> same value as `branch_*` (a relative word offset). @@ -121,7 +118,7 @@ end --- --- Top-level `atom_offset(F, T)` markers (where the marker is the entire token — `consuming_encoder` == nil) default to `branch_*` behavior (relative offset). --- This preserves backward compatibility for any top-level marker that may exist outside a control-transfer instruction. ---- @param labels table +--- @param labels table --- @param branches table[] --- @return BranchOffset[] local function compute_offsets(labels, branches) @@ -195,44 +192,48 @@ local function emit_atom_offsets(add, atom) add("") end ---- Generate the per-source .offsets.h header. ---- @param source_path string ---- @param atoms_data AtomData[] +--- Generate the per-directory .offsets.h header. +--- @param dir string -- the absolute source directory +--- @param sources table[] -- sources contributing to this directory (for the header comment) +--- @param atoms_data AtomData[] --- @return string -local function generate_header(source_path, atoms_data) - local basename = duffle.basename_no_ext(source_path) +local function generate_header(dir, sources, atoms_data) + local dir_basename = duffle.basename_no_ext(dir) local lines = {} local function add(s) lines[#lines + 1] = s end add("// Auto-generated by ps1_meta.lua (passes/offsets.lua) — DO NOT EDIT") - add("// Source: " .. source_path) + add("// Directory: " .. dir:gsub("/", "\\") .. "\\") + for _, src in ipairs(sources) do + add("// source: " .. src.path:gsub("/", "\\")) + end add("#pragma once") add("") - add("#pragma region " .. basename) + add("#pragma region " .. dir_basename) add("") add("") for _, atom in ipairs(atoms_data) do emit_atom_offsets(add, atom) end - add("#pragma endregion " .. basename) + add("#pragma endregion " .. dir_basename) add("") return table.concat(lines, "\n") .. "\n" end local M = {} ---- (internal) Process one source: render offsets from canonical atom paths. +--- (internal) Aggregate atoms from every source in one directory, render the per-directory `offsets.h`. --- Returns the offsets_h path if a header was written, or nil. ---- @param ctx PassCtx ---- @param src SourceFile +--- @param ctx PassCtx +--- @param dir string -- the absolute source directory +--- @param sources SourceFile[] -- sources in this directory --- @return string|nil -- the offsets_h path -local function process_source(ctx, src) +local function process_directory(ctx, dir, sources) local atoms_data = {} - local scan = src.scan or {} local function append_atom(atom) - local paths = atom and atom.paths + local paths = atom and atom.paths if not paths then return end local labels, branches = project_markers(paths.markers) atoms_data[#atoms_data + 1] = { @@ -242,19 +243,22 @@ local function process_source(ctx, src) } end - for _, atom in ipairs(scan.atoms or {}) do append_atom(atom) end - for _, atom in ipairs(scan.raw_atoms or {}) do append_atom(atom) end + for _, src in ipairs(sources) do + local scan = src.scan or {} + for _, atom in ipairs(scan.atoms or {}) do append_atom(atom) end + for _, atom in ipairs(scan.raw_atoms or {}) do append_atom(atom) end + end if #atoms_data == 0 then return nil end - local out_path = src.dir .. "/gen/" .. duffle.basename_no_ext(src.dir) .. ".offsets.h" + local out_path = dir .. "/gen/offsets.h" duffle.ensure_dir(duffle.dirname(out_path)) - duffle.write_file(out_path, generate_header(src.path:gsub("/", "\\"), atoms_data)) + duffle.write_file(out_path, generate_header(dir, sources, atoms_data)) return out_path end --- Run the offsets pass. ---- For each canonical source, emits a per-module `.offsets.h` ---- containing constants for every marker recorded in atom.paths. +--- For each canonical source-directory, emits a per-directory `gen/offsets.h` +--- containing constants for every marker recorded in atom.paths across every source in that directory. --- @param ctx PassCtx --- @return PassResult function M.run(ctx) @@ -263,12 +267,17 @@ function M.run(ctx) local warnings = {} local corpus = ctx.shared and ctx.shared.corpus - if type(corpus) ~= "table" or type(corpus.source_order) ~= "table" then - error("offsets.run requires ctx.shared.corpus.source_order (canonical corpus).", 0) + if type(corpus) ~= "table" then + error("offsets.run requires ctx.shared.corpus", 0) + end + if type(corpus.source_order) ~= "table" then + error("offsets.run requires ctx.shared.corpus.source_order.", 0) end - for _, src in ipairs(corpus.source_order) do - local out_path = process_source(ctx, src) + -- Per-directory aggregation: every source in the same directory contributes to one `gen/offsets.h`. + local sources_by_dir = corpus.sources_by_dir or duffle.group_sources_by_dir(corpus.source_order) + for dir, sources in pairs(sources_by_dir) do + local out_path = process_directory(ctx, dir, sources) if out_path then outputs[#outputs + 1] = { offsets_h = out_path } end diff --git a/scripts/passes/report.lua b/scripts/passes/report.lua index 7b08298..eceb0dd 100644 --- a/scripts/passes/report.lua +++ b/scripts/passes/report.lua @@ -1,22 +1,21 @@ ---- passes/report.lua — Per-MODULE annotation report renderer + ---- project-wide summary writer. +--- passes/report.lua — Per-MODULE annotation report renderer + project-wide summary writer. --- --- Two output files per build: --- - `build/gen/.annotations.txt` — one per source-directory containing atoms; aggregates across all sources in the directory. --- - `build/gen/annotation_validation.txt` — the project summary. --- --- The annotation pass emits `errors.h` files per module and the canonical `corpus.sources_by_dir` projection groups sources by directory. ---- This pass iterates the canonical dir projection directly and re-validates each source via `annotation.validate()` to get the detailed per-source results. +--- This pass iterates the dir projection directly and re-validates each source via `annotation.validate()` to get the detailed per-source results. -- ════════════════════════════════════════════════════════════════════════════ -- Module-scope requires + package.path setup -- ════════════════════════════════════════════════════════════════════════════ -- Resolve `arg[0]` to an absolute-ish script directory so that `require("duffle")` resolves against `scripts/` regardless of CWD. --- Bootstrap: see `ps1_meta.lua` for the rationale. --- Bootstrap: load `scripts/duffle_paths.lua` (sets package.path + package.cpath). +-- Bootstrap: See `ps1_meta.lua` for the rationale. +-- Bootstrap: Load `scripts/duffle_paths.lua` (sets package.path + package.cpath). -- Uses `debug.getinfo` to find this file's own directory, so it works both standalone and when require'd from the orchestrator. --- Bootstrap: load `duffle_paths.lua` via `debug.getinfo(1, "S").source` (works both standalone + when require'd). +-- Bootstrap: Load `duffle_paths.lua` via `debug.getinfo(1, "S").source` (works both standalone + when require'd). -- duffle_paths.lua sets package.path then returns `require("duffle")` at the bottom, so the dofile value IS the duffle module. local _bootstrap_dir = debug.getinfo(1, "S").source:match("^@?(.*[/\\])") or "./" local duffle = dofile(_bootstrap_dir .. "../duffle_paths.lua") @@ -37,7 +36,7 @@ local atoms_source_map = dofile(_bootstrap_dir .. "atoms_source_map.lua") -- Section separators used in the rendered text reports. -- The thin rules are hand-tuned to align with the per-section content width; do not change without also checking the section renderers below. -local RULE_THICK = "========================================================" +local RULE_THICK = "========================================================" local SECTION_HEADER_ATOMS = "── Atoms ────────────────────────────────────────────────" local SECTION_HEADER_ANNOTS = "── Annotations ──────────────────────────────────────────" local SECTION_HEADER_BINDS = "── Binds_* structs ──────────────────────────────────────" @@ -59,83 +58,83 @@ local PASS_NAME = "report" -- ════════════════════════════════════════════════════════════════════════════ --- @class SourceFile ---- @field path string -- absolute path to the source file ---- @field text string -- the full source text ---- @field dir string -- the directory containing the source ---- @field basename string -- filename without extension +--- @field path string -- Absolute path to the source file +--- @field text string -- Full source text +--- @field dir string -- Directory containing the source +--- @field basename string -- Filename without extension --- @class PassCtx ---- @field sources SourceFile[] -- all source files in the build ---- @field metadata_path string -- path to word_count.metadata.h ---- @field shared table -- cross-pass shared state ---- @field out_root string -- output root (e.g. "build/gen") ---- @field project_root string -- project root (e.g. "code/") ---- @field upstream table -- per-pass upstream outputs +--- @field sources SourceFile[] -- All source files in the build +--- @field metadata_path string -- Path to word_count.metadata.h +--- @field shared table -- Cross-pass shared state +--- @field out_root string -- Output root (e.g. "build/gen") +--- @field project_root string -- Project root (e.g. "code/") +--- @field upstream table -- Per-pass upstream outputs --- @field flags table -- CLI flags + per-pass stash ---- @field verbose boolean -- if true, log diagnostic info +--- @field verbose boolean -- If true, log diagnostic info --- @class PassResult --- @field outputs table[] -- {kind=, path=} entries describing emit files ---- @field errors table[] -- {line=, msg=} entries; build-stops ---- @field warnings table[] -- {line=, msg=} entries; build-succeeds +--- @field errors table[] -- {line=, msg=} entries; build-stops +--- @field warnings table[] -- {line=, msg=} entries; build-succeeds -- Shapes produced by `passes/annotation.lua`'s `M.validate()`. --- @class AtomEntry ---- @field name string -- atom name (e.g. "cube_g4_face") ---- @field line integer -- source line of the atom declaration +--- @field name string -- Atom name (e.g. "cube_g4_face") +--- @field line integer -- Source line of the atom declaration --- @class AnnotEntry ---- @field line integer -- source line ---- @field macro string -- the macro name (e.g. "atom_reads") ---- @field name string -- the atom name (if a `name(...)` was given) ---- @field kind string -- "atom_info" | "atom_bind" | ... +--- @field line integer -- Source line +--- @field macro string -- Macro name (e.g. "atom_reads") +--- @field name string -- Atom name (if a `name(...)` was given) +--- @field kind string -- "atom_info" | "atom_bind" | ... --- @field binds string|nil -- Binds_X name if any --- @field reads string[] -- R_* names (read targets) --- @field writes string[] -- R_* names (write targets) ---- @field error string|nil -- error message if annotation was malformed +--- @field error string|nil -- Error message if annotation was malformed --- @class BindsField ---- @field name string -- field name ---- @field offset integer -- byte offset within the Binds_X struct +--- @field name string -- Field name +--- @field offset integer -- Byte offset within the Binds_X struct --- @class BindsStruct ---- @field name string -- struct name (e.g. "Binds_Floor") ---- @field line integer -- source line of the typedef ---- @field bytes integer -- total byte size ---- @field fields BindsField[] -- the field list +--- @field name string -- Struct name (e.g. "Binds_Floor") +--- @field line integer -- Source line of the typedef +--- @field bytes integer -- Total byte size +--- @field fields BindsField[] -- The field list --- @class MacroEntry ---- @field name string -- macro name (e.g. "WORD_COUNT(my_macro, 4)") ---- @field line integer -- source line ---- @field words integer -- declared word count +--- @field name string -- Macro name (e.g. "WORD_COUNT(my_macro, 4)") +--- @field line integer -- Source line +--- @field words integer -- Declared word count --- @class Finding ---- @field line integer -- source line ---- @field msg string -- finding message +--- @field line integer -- Source line +--- @field msg string -- Finding message --- @class AnnotationResult ---- @field source string -- set by this pass; original source path ---- @field atoms AtomEntry[] -- atom declarations in this source ---- @field annots AnnotEntry[] -- annotation entries ---- @field macros MacroEntry[] -- macro word-count declarations +--- @field source string -- Set by this pass; original source path +--- @field atoms AtomEntry[] -- Atom declarations in this source +--- @field annots AnnotEntry[] -- Annotation entries +--- @field macros MacroEntry[] -- Macro word-count declarations --- @field binds BindsStruct[] -- Binds_* struct declarations ---- @field errors Finding[] -- errors from validation ---- @field warnings Finding[] -- warnings from validation ---- @field info table -- info summary (not rendered here) +--- @field errors Finding[] -- Errors from validation +--- @field warnings Finding[] -- Warnings from validation +--- @field info table -- Info summary (not rendered here) --- @class ModuleEntry ---- @field dir string -- absolute directory path ---- @field dir_basename string -- basename (e.g. "duffle", "gte_hello") ---- @field atoms_count integer -- pre-counted atoms for filtering +--- @field dir string -- Absolute directory path +--- @field dir_basename string -- Basename (e.g. "duffle", "gte_hello") +--- @field atoms_count integer -- Pre-counted atoms for filtering --- @class ModuleReport ---- @field dir string -- module directory ---- @field sources SourceFile[] -- sources in this module ---- @field results AnnotationResult[] -- per-source validate() results +--- @field dir string -- Module directory +--- @field sources SourceFile[] -- Sources in this module +--- @field results AnnotationResult[] -- Per-source validate() results --- @class ProjectReport ---- @field results AnnotationResult[] -- all per-source results +--- @field results AnnotationResult[] -- All per-source results -- ════════════════════════════════════════════════════════════════════════════ -- Per-MODULE annotation report (aggregated across all sources in a dir) @@ -153,9 +152,16 @@ end -- ════════════════════════════════════════════════════════════════════════════ --- Render the thin project-wide summary (`build/atom_meta_report.summary.md`). ---- @param all_results { module:string, atoms:integer, annots:integer, binds:integer, ---- macros:integer, findings:integer, errors:integer, ---- warnings:integer, info:integer }[] +--- @param all_results { +--- module:string, +--- atoms:integer, +--- annots:integer, +--- binds:integer, +--- macros:integer, +--- findings:integer, +--- errors:integer, +--- warnings:integer, +--- info:integer }[] --- @return string local function render_project_summary(all_results) local lines = { @@ -165,13 +171,10 @@ local function render_project_summary(all_results) "| module | atoms | annots | binds | macros | findings | errors | warnings | info |", "|--------|-------|--------|-------|--------|----------|--------|----------|------|", } - local totals = { atoms = 0, annots = 0, binds = 0, macros = 0, - findings = 0, errors = 0, warnings = 0, info = 0 } + local totals = { atoms = 0, annots = 0, binds = 0, macros = 0, findings = 0, errors = 0, warnings = 0, info = 0 } for _, e in ipairs(all_results) do - lines[#lines + 1] = string.format( - "| %s | %d | %d | %d | %d | %d | %d | %d | %d |", - e.module, e.atoms, e.annots, e.binds, e.macros, - e.findings, e.errors, e.warnings, e.info) + lines[#lines + 1] = string.format("| %s | %d | %d | %d | %d | %d | %d | %d | %d |" + , e.module, e.atoms, e.annots, e.binds, e.macros, e.findings, e.errors, e.warnings, e.info) totals.atoms = totals.atoms + e.atoms totals.annots = totals.annots + e.annots totals.binds = totals.binds + e.binds @@ -181,23 +184,21 @@ local function render_project_summary(all_results) totals.warnings = totals.warnings + e.warnings totals.info = totals.info + e.info end - lines[#lines + 1] = string.format( - "| **TOTAL** | %d | %d | %d | %d | %d | %d | %d | %d |", - totals.atoms, totals.annots, totals.binds, totals.macros, - totals.findings, totals.errors, totals.warnings, totals.info) + lines[#lines + 1] = string.format("| **TOTAL** | %d | %d | %d | %d | %d | %d | %d | %d |" + , totals.atoms, totals.annots, totals.binds, totals.macros, totals.findings, totals.errors, totals.warnings, totals.info) return table.concat(lines, "\n") .. "\n" end --- Render the per-module verbose source-map markdown (`build/.atoms.md`). --- Per-source sub-section, per-atom stanza with sourcemap + provenance rows. --- Pulls sourcemap + provenance from `atoms_source_map` (no second source walk). ---- @param dir string +--- @param dir string --- @param dir_sources SourceFile[] ---- @param wc table +--- @param wc table --- @return string local function render_module_atoms_md(dir, dir_sources, wc) local dir_basename = source_basename(dir) - local lines = { + local lines = { "# " .. dir_basename .. " — atoms (verbose source map)", "> Per-word call-site + provenance. Auto-generated.", "", @@ -249,14 +250,14 @@ end --- Aggregates annotation + static-analysis content across all sources in `dir`. --- Annotations come from re-running `annotation.validate()` per source (the existing pattern); --- static-analysis comes from `corpus.static_analysis_results[dir_basename]` (populated by `static_analysis.lua` — no second corpus_pipe_ctx build). ---- @param dir string ---- @param dir_sources SourceFile[] +--- @param dir string +--- @param dir_sources SourceFile[] --- @param annot_results AnnotationResult[] ---- @param sa_results table -- corpus.static_analysis_results[dir_basename] +--- @param sa_results table -- corpus.static_analysis_results[dir_basename] --- @return string local function render_module_meta_report(dir, dir_sources, annot_results, sa_results) local dir_basename = source_basename(dir) - local lines = { + local lines = { "# " .. dir_basename .. " — atom meta report", "> Auto-generated by ps1_meta.lua (passes/report.lua). Do not edit.", "", @@ -326,8 +327,8 @@ local function render_module_meta_report(dir, dir_sources, annot_results, sa_res local binds = a.binds or "—" local reads = (#a.reads > 0 and table.concat(a.reads, ",")) or "—" local writes = (#a.writes > 0 and table.concat(a.writes, ",")) or "—" - add(string.format("| %s | %d | %s | %s | %s | %s |", - src_name, a.line, a.name, binds, reads, writes)) + add(string.format("| %s | %d | %s | %s | %s | %s |" + , src_name, a.line, a.name, binds, reads, writes)) end end end @@ -418,7 +419,7 @@ local function render_module_meta_report(dir, dir_sources, annot_results, sa_res for _, a in ipairs(sorted) do local p = a.paths or {} local src_name = a.source_path and source_basename(a.source_path) or "" - local notes = "" + local notes = "" if p.has_loops then notes = notes .. " [loop!]" end if p.unknown_macros and #p.unknown_macros > 0 then notes = notes .. " [unknown: " .. table.concat(p.unknown_macros, ", ") .. "]" @@ -426,7 +427,7 @@ local function render_module_meta_report(dir, dir_sources, annot_results, sa_res add(string.format("| %s | %s | %d | %d | %d | %d | %s |", a.name, src_name, p.cycles_min or 0, p.cycles_max or 0, - p.branches or 0, p.paths or 0, notes)) + p.branches or 0, p.paths or 0, notes)) end add("") diff --git a/scripts/passes/scan_source.lua b/scripts/passes/scan_source.lua index b571a2d..3765b43 100644 --- a/scripts/passes/scan_source.lua +++ b/scripts/passes/scan_source.lua @@ -2,7 +2,6 @@ --- --- Single source-walk pass that produces the fat `SourceScan` payload consumed by all downstream passes. Walks each corpus source record once, --- extracting every construct type the metaprograms need: ---- --- MipsAtom_ (kind = "atom", with optional atom_info inner) --- MipsAtomComp_ (kind = "comp_bare") --- MipsAtomComp_Proc_ (kind = "comp_proc", body inside last {}) @@ -14,8 +13,6 @@ --- The result is attached to each `src.scan` so downstream passes can read from `src.scan.atoms` / `src.scan.binds` / etc. without re-walking the source. --- This is the first pass in the dep graph (no deps). --- Every other pass that reads source structure depends on this one — see `ps1_meta.lua :: PASSES`. ---- ---- **Conventions**: tabs (1/level), EmmyLua annotations, no regex, Lua 5.3 compatible -- Bootstrap: same as entry scripts. See `ps1_meta.lua` for the rationale. -- Bootstrap: load `scripts/duffle_paths.lua` (sets package.path + package.cpath). @@ -23,7 +20,7 @@ -- Bootstrap: load `duffle_paths.lua` via `debug.getinfo(1, "S").source` (works both standalone + when required). -- duffle_paths.lua sets package.path then returns `require("duffle")` at the bottom, so the dofile value IS the duffle module. local _bootstrap_dir = debug.getinfo(1, "S").source:match("^@?(.*[/\\])") or "./" -local duffle = dofile(_bootstrap_dir .. "../duffle_paths.lua") +local duffle = dofile(_bootstrap_dir .. "../duffle_paths.lua") -- Forward declarations for helpers used by earlier parsers (parse_enum_body_fields needs parse_enum_int_literal; -- parse_typedef_binds needs duffle.find_byte). @@ -50,12 +47,12 @@ local parse_enum_int_literal --- @field line_of fun(pos: integer): integer -- shared LineIndex closure --- @class DebugSkipMarker ---- @field marker_kind string -- exact marker ident read from source. Only "atom_dbg_skip" (bare) is positive; any other ident reaches the unrelated fallback and is never associated with a declaration. ---- @field marker_line integer -- line of the marker ident start ---- @field marker_pos integer -- byte position of the marker ident start (the comment walker anchors here) +--- @field marker_kind string -- Exact marker ident read from source. Only "atom_dbg_skip" (bare) is positive; any other ident reaches the unrelated fallback and is never associated with a declaration. +--- @field marker_line integer -- Line of the marker ident start +--- @field marker_pos integer -- Byte position of the marker ident start (the comment walker anchors here) --- @field is_bare boolean -- true iff marker_kind == "atom_dbg_skip" AND has_parens == false (the only positive form) --- @field has_parens boolean -- true iff a `(...)` follows the marker ident (diagnostic-only) ---- @field args string|nil -- trimmed args inside the `(...)` (nil when has_parens is false) +--- @field args string|nil -- Trimmed args inside the `(...)` (nil when has_parens is false) --- @field pending boolean -- true while awaiting the following declaration --- @field superseded_by_marker_line integer|nil -- set when a newer marker bumped this one out of the pending slot --- @field target_kind string|nil -- "atom" | "comp_bare" | "comp_proc" | "unrelated" once observed (nil if no declaration ever followed) @@ -71,15 +68,15 @@ local parse_enum_int_literal --- @field reg string -- "R_T0" --- @field type_name string --- @field pointer_depth integer ---- @field source_line integer -- line of the call site (callsite or enum-site) +--- @field source_line integer -- Line of the call site (callsite or enum-site) --- @class AtomCtxEntry ---- @field rbind_atom string -- the rbind atom ident that this consumer should propagate types from +--- @field rbind_atom string -- The rbind atom ident that this consumer should propagate types from --- @field info_line integer ---- @field source string -- absolute path of the source file +--- @field source string -- Absolute path of the source file --- @class AtomPhaseGroup ---- @field atoms string[] -- atom names tagged with this phase label (source-order) +--- @field atoms string[] -- Atom names tagged with this phase label (source-order) --- @class AtomViewEntry --- @field atom_name string -- e.g. "red_cube_g4_face" @@ -88,11 +85,11 @@ local parse_enum_int_literal --- @field info_line integer -- line of the atom_info call --- @class SourceFile ---- @field path string -- absolute path to the source file ---- @field text string -- the full source text ---- @field dir string -- the directory containing the source ---- @field basename string -- filename without extension ---- @field scan table -- pre-scanned SourceScan payload (set by this pass) +--- @field path string -- Absolute path to the source file +--- @field text string -- Full source text +--- @field dir string -- Directory containing the source +--- @field basename string -- Filename without extension +--- @field scan table -- Pre-scanned SourceScan payload (set by this pass) --- @class PassCtx --- @field sources SourceFile[] @@ -111,15 +108,15 @@ local parse_enum_int_literal --- @class AtomEntry --- @field line integer ---- @field name string -- atom name (for components: without ac_ prefix) ---- @field body string -- brace-delimited body (without the braces) ---- @field body_off integer -- char offset of body[1] in source ---- @field kind string -- "atom" | "comp_bare" | "comp_proc" | "raw_atom" ---- @field raw_name string -- un-stripped name (for components: with ac_ prefix) ---- @field ident_pos integer -- position of the MipsAtom_/MipsAtomComp_ ident start ---- @field after_paren integer -- position past the closing paren ---- @field debug_skip boolean -- true when an `atom_dbg_skip` bare marker immediately precedes this declaration (sole-owner stamp; see push_debug_skip_marker) ---- @field declaration_comment string|nil -- populated by the scanner (backward walk past the marker, captures contiguous `/* */` or `//` block) +--- @field name string -- Atom name (for components: without ac_ prefix) +--- @field body string -- Brace-delimited body (without the braces) +--- @field body_off integer -- Char offset of body[1] in source +--- @field kind string -- "atom" | "comp_bare" | "comp_proc" | "raw_atom" +--- @field raw_name string -- Un-stripped name (for components: with ac_ prefix) +--- @field ident_pos integer -- Position of the MipsAtom_/MipsAtomComp_ ident start +--- @field after_paren integer -- Position past the closing paren +--- @field debug_skip boolean -- true when an `atom_dbg_skip` bare marker immediately precedes this declaration (sole-owner stamp; see push_debug_skip_marker) +--- @field declaration_comment string|nil -- Populated by the scanner (backward walk past the marker, captures contiguous `/* */` or `//` block) -- ════════════════════════════════════════════════════════════════════════════ -- Local helpers (shared by per-form parsers) @@ -139,10 +136,10 @@ local QUALIFIER_KEYWORDS = { local AC_PREFIX = "ac_" local AC_PREFIX_LEN = 3 --- Strip the "ac_" prefix from a component name. --- Returns the input unchanged if it doesn't start with the prefix. --- @param raw_name string --- @return string +--- Strip the "ac_" prefix from a component name. +--- Returns the input unchanged if it doesn't start with the prefix. +--- @param raw_name string +--- @return string local function strip_ac_prefix(raw_name) if #raw_name > AC_PREFIX_LEN and raw_name:sub(1, AC_PREFIX_LEN) == AC_PREFIX then return raw_name:sub(AC_PREFIX_LEN + 1) @@ -156,7 +153,7 @@ end local function push_debug_skip_marker(out, marker) local markers = out.debug_skip_markers local prior = markers[#markers] - if prior and prior.pending then + if prior and prior.pending then prior.pending = false prior.superseded_by_marker_line = marker.marker_line end @@ -178,25 +175,25 @@ end -- Returns (body, after_brace, body_off) on success, or (nil, fallback_pos) on no brace. -- `fallback_pos` defaults to `after_paren + 1` (the common "advance by 1" case). local function find_body_braces(source, after_paren, fallback) - local brace = duffle.scan_to_char(source, "{", after_paren) + local brace = duffle.scan_to_char(source, "{", after_paren) if not brace then return nil, fallback or (after_paren + 1) end local body, after_brace = duffle.read_braces(source, brace) return body, after_brace, brace + 1 end --- Walk backward from `start_pos` capturing contiguous `/* */` block(s) and --- `//` line(s) that immediately precede it. The caller (preceding_declaration_comment) --- supplies `start_pos` so the walker does not need to detect marker shape or prelude layout. --- The scanner already knows the marker_pos + decl ident_pos and threads that knowledge forward. --- --- The walker captures: --- - Block comment close `*/` followed by walking back to `/*`. --- - `//` line comments (the line containing the current non-ws position starts with `//`). --- It stops at the first non-ws char that does not begin a comment block or line. --- Empty string if no comment is adjacent. --- @param source string --- @param start_pos integer -- exclusive upper bound for the captured block --- @return string +--- Walk backward from `start_pos` capturing contiguous `/* */` block(s) and +--- `//` line(s) that immediately precede it. The caller (preceding_declaration_comment) +--- supplies `start_pos` so the walker does not need to detect marker shape or prelude layout. +--- The scanner already knows the marker_pos + decl ident_pos and threads that knowledge forward. +--- +--- The walker captures: +--- - Block comment close `*/` followed by walking back to `/*`. +--- - `//` line comments (the line containing the current non-ws position starts with `//`). +--- It stops at the first non-ws char that does not begin a comment block or line. +--- Empty string if no comment is adjacent. +--- @param source string +--- @param start_pos integer -- exclusive upper bound for the captured block +--- @return string local function preceding_comment_walk_backward(source, start_pos) local pieces = {} local scan_pos = start_pos @@ -249,13 +246,13 @@ local function preceding_comment_walk_backward(source, start_pos) return table.concat(pieces, "\n") end --- Resolve the start position for the declaration-comment walk. --- When a debug-skip marker is pending, the walker must start from the position immediately before the marker ident --- (so it walks backward past the marker text and any `FI_ MipsAtom ac_X(args)` proc-prelude layout — neither of which is visible if we start from the declaration ident_pos). --- When no marker is pending, the walker starts from the declaration ident_pos directly. --- @param pending_marker DebugSkipMarker|nil --- @param ident_pos integer -- declaration ident position --- @return integer +--- Resolve the start position for the declaration-comment walk. +--- When a debug-skip marker is pending, the walker must start from the position immediately before the marker ident +--- (so it walks backward past the marker text and any `FI_ MipsAtom ac_X(args)` proc-prelude layout — neither of which is visible if we start from the declaration ident_pos). +--- When no marker is pending, the walker starts from the declaration ident_pos directly. +--- @param pending_marker DebugSkipMarker|nil +--- @param ident_pos integer -- declaration ident position +--- @return integer local function comment_walk_start(pending_marker, ident_pos) if pending_marker then return pending_marker.marker_pos - 1 @@ -263,16 +260,16 @@ local function comment_walk_start(pending_marker, ident_pos) return ident_pos - 1 end --- Attach the pending marker to the next declaration. --- The declaration form disambiguates whole atoms from components; the resolved `debug_skip` is stamped directly on the declaration record --- (sole-owner discipline; see push_debug_skip_marker). --- --- A marker is POSITIVE (stamps `debug_skip = true` on the declaration) iff: --- marker_kind == "atom_dbg_skip" AND is_bare == true --- Any other spelling or shape (parenthesized form, legacy name) is recorded as a raw marker for annotation validation but never stamps `debug_skip`. --- @param out SourceScan --- @param target_kind string|nil -- "atom" | "comp_bare" | "comp_proc" | "unrelated" once observed --- @return boolean|nil -- true iff the marker is the positive bare form +--- Attach the pending marker to the next declaration. +--- The declaration form disambiguates whole atoms from components; the resolved `debug_skip` is stamped directly on the declaration record +--- (sole-owner discipline; see push_debug_skip_marker). +--- +--- A marker is POSITIVE (stamps `debug_skip = true` on the declaration) iff: +--- marker_kind == "atom_dbg_skip" AND is_bare == true +--- Any other spelling or shape (parenthesized form, legacy name) is recorded as a raw marker for annotation validation but never stamps `debug_skip`. +--- @param out SourceScan +--- @param target_kind string|nil -- "atom" | "comp_bare" | "comp_proc" | "unrelated" once observed +--- @return boolean|nil -- true iff the marker is the positive bare form local function attach_debug_skip_marker(out, target_kind) local markers = out.debug_skip_markers local marker = markers[#markers] @@ -390,13 +387,13 @@ local function walk_body_fields(body, build_field) while body_pos <= body_len do body_pos = duffle.skip_ws_and_cmt(body, body_pos) if body_pos > body_len then break end - local first, first_end = duffle.read_ident(body, body_pos) + local first, first_end = duffle.read_ident(body, body_pos) if not first then body_pos = body_pos + 1 else - local after_first = duffle.skip_ws_and_cmt(body, first_end) + local after_first = duffle.skip_ws_and_cmt(body, first_end) local result, new_pos = build_field(first, first_end, after_first) - if result then fields[#fields + 1] = result end + if result then fields[#fields + 1] = result end body_pos = new_pos or first_end -- Skip a single trailing `,` or `;`. if body_pos <= body_len and (body:sub(body_pos, body_pos) == "," or body:sub(body_pos, body_pos) == ";") then @@ -442,7 +439,7 @@ local function parse_enum_body_fields(body) local value local new_pos if body:sub(after_name, after_name) == "=" then - local val_pos = duffle.skip_ws_and_cmt(body, after_name + 1) + local val_pos = duffle.skip_ws_and_cmt(body, after_name + 1) local v, end_pos = parse_enum_int_literal(body, val_pos) if v ~= nil then value = v @@ -461,16 +458,16 @@ end -- Returns a positive integer byte_size when the chain bottoms out at a builtin, or nil if the chain is broken, exceeds TYPE_CHAIN_MAX_DEPTH, or contains a cycle. local function resolve_typedef_byte_size(type_name, type_name_registry, visited, depth) if depth > TYPE_CHAIN_MAX_DEPTH then return nil end - if visited[type_name] then return nil end + if visited[type_name] then return nil end visited[type_name] = true -- Check the builtin primitive map FIRST. -- This handles undeclared builtin idents (e.g. `__UINT32_TYPE__` appears as underlying_type in `typedef __UINT32_TYPE__ TSet_(V4_S2);` -- even though the fixture never declares `__UINT32_TYPE__` itself). local builtin = BUILTIN_BYTE_SIZES[type_name] - if builtin ~= nil then return builtin end + if builtin ~= nil then return builtin end - local entry = type_name_registry[type_name] + local entry = type_name_registry[type_name] if not entry then return nil end -- Confident: this entry was already resolved by the propagation pass (e.g., a builtin or a struct whose fields are all resolved). @@ -519,9 +516,9 @@ local function propagate_type_sizes(out) for name, entry in pairs(reg) do if entry.byte_size == nil then local resolved = resolve_typedef_byte_size(name, reg, {}, 1) - if resolved ~= nil then + if resolved ~= nil then entry.byte_size = resolved - any_change = true + any_change = true end end end @@ -828,7 +825,7 @@ end --- Parse a decimal/negative-decimal/hex integer literal starting at byte position `start`. --- Returns (value, end_pos) on success, or (nil, start) on failure / no match. --- Accepts: 12, -1, 0, 0x10, 0X1F, -0x10. ---- @param text string +--- @param text string --- @param start integer --- @return integer|nil, integer --- Implementation note: this is a plain assignment (not `local function`) @@ -952,9 +949,9 @@ end --- Always saves the raw RHS text into `code_macro_bodies` (for cross-source fallback during chain resolution), --- then (if resolvable) stores the resolved integer code into `code_macros` keyed by the macro name. --- `directive_start` points at the `#` byte. The function is silent on non-matching directives, the caller skips the line in any case. ---- @param source string +--- @param source string --- @param directive_start integer -- byte position of `#` ---- @param code_macros table -- out._code_macros / ctx.shared._code_macros +--- @param code_macros table -- out._code_macros / ctx.shared._code_macros --- @param code_macro_bodies table -- out._code_macro_bodies / ctx.shared._code_macro_bodies local function try_extract_code_macro(source, directive_start, code_macros, code_macro_bodies) local rest = duffle.skip_ws_and_cmt(source, directive_start + 1) @@ -985,8 +982,8 @@ end --- Populates `code_macros` with resolved integer codes AND `code_macro_bodies` with raw RHS text --- (used by the chain walker as cross-source fallback during pass 1b in `M.run`); ignores everything else. --- Used by `M.run` pass 1a to build the cross-source `_code_macros` + `_code_macro_bodies` registries before pass 1b resolves chains. ---- @param source string ---- @param code_macros table +--- @param source string +--- @param code_macros table --- @param code_macro_bodies table local function scan_source_pre_pass(source, code_macros, code_macro_bodies) local pos = 1 @@ -1043,7 +1040,7 @@ local function parse_enum_atom_type_default(body, pos) if pos > #body then return nil, 0, pos end -- Bare `atom_type` word with word-bounding on both sides. local ident, ident_end = duffle.read_ident(body, pos) - if ident ~= "atom_type" then return nil, 0, pos end + if ident ~= "atom_type" then return nil, 0, pos end if pos > 1 then local prev = body:byte(pos - 1) if duffle.is_alnum_byte(prev) then return nil, 0, pos end @@ -1054,19 +1051,19 @@ local function parse_enum_atom_type_default(body, pos) end -- Expect `( ... )` immediately after. local open_pos = duffle.skip_ws_and_cmt(body, ident_end) - if open_pos > #body or body:sub(open_pos, open_pos) ~= "(" then return nil, 0, pos end + if open_pos > #body or body:sub(open_pos, open_pos) ~= "(" then return nil, 0, pos end local inner, after_close = duffle.read_parens(body, open_pos) -- Reject any trailing tokens past the close paren other than comma / close-brace (next enum entry / end of enum). local residue = duffle.skip_ws_and_cmt(body, after_close) - if residue <= #body then + if residue <= #body then local rbyte = body:byte(residue) if rbyte ~= BYTE_COMMA and rbyte ~= BYTE_CLOSE_BRACE then return nil, 0, pos end end -- Parse the type chain inside the parens (e.g. `V4_S2*` -> ("V4_S2", 1)). - local type_name, depth, after_chain = parse_type_chain(inner, 1) + local type_name, depth, after_chain = parse_type_chain(inner, 1) if not type_name then return nil, 0, pos end local end_check = duffle.skip_ws_and_cmt(inner, after_chain) - if end_check <= #inner then return nil, 0, pos end + if end_check <= #inner then return nil, 0, pos end return type_name, depth, duffle.skip_ws_and_cmt(body, after_close) end @@ -1098,11 +1095,11 @@ end --- --- Diagnostic-only path: a following `(...)` is recorded as an invalid parenthesized-form marker so the annotation rule can emit a precise "parenthesized form" diagnostic. --- The parenthesized form stays diagnostic; the bare form alone carries the runtime stamp. ---- @param source string ---- @param pos integer +--- @param source string +--- @param pos integer --- @param ident_end integer ---- @param line_of fun(pos: integer): integer ---- @param out SourceScan +--- @param line_of fun(pos: integer): integer +--- @param out SourceScan --- @return integer -- source cursor position to resume from local function parse_dbg_skip_marker(source, pos, ident_end, line_of, out) local marker_kind = source:sub(pos, ident_end - 1) @@ -1134,15 +1131,15 @@ end -- Parse `atom_dbg_reg_default(R_X, ...)`; -- the second argument may be a `Type` or `Type*`/`Type**` chain. Records in `out.types[R_X]`. local function parse_atom_dbg_reg_default(source, pos, ident_end, line_of, out) - local inner, after_paren = read_parens_after(source, ident_end) + local inner, after_paren = read_parens_after(source, ident_end) if not inner then return after_paren end local args = duffle.split_top_level_commas(inner) - if #args < 1 then + if #args < 1 then -- Annotation pass surfaces this; we still consume the marker. return after_paren end - local reg_name = duffle.trim(args[1]) - local type_part = args[2] or "void" + local reg_name = duffle.trim(args[1]) + local type_part = args[2] or "void" local type_name, depth = parse_type_chain(type_part, 1) if not type_name then type_name, depth = duffle.trim(type_part), 0 end out.types[reg_name] = { @@ -1161,23 +1158,23 @@ local function parse_atom_dbg_reg_default(source, pos, ident_end, line_of, out) end --- Parse: `MipsAtom_() [atom_info(, , )] { }` ---- @param source string ---- @param pos integer +--- @param source string +--- @param pos integer --- @param ident_end integer ---- @param line_of fun(pos: integer): integer ---- @param out SourceScan +--- @param line_of fun(pos: integer): integer +--- @param out SourceScan --- @return integer local function parse_mips_atom(source, pos, ident_end, line_of, out) - local inner, after_paren, open_paren = read_parens_after(source, ident_end) + local inner, after_paren, open_paren = read_parens_after(source, ident_end) if not inner then return after_paren end local raw_name = duffle.read_ident(inner, 1) -- Lookahead for atom_info(...) between `)` and `{`. Captures sub-calls; updates brace search start. - local brace_search_pos = after_paren - local lookahead = duffle.skip_ws_and_cmt(source, after_paren) + local brace_search_pos = after_paren + local lookahead = duffle.skip_ws_and_cmt(source, after_paren) local look_ident, look_end = duffle.read_ident(source, lookahead) - if look_ident == "atom_info" then + if look_ident == "atom_info" then local info_open = duffle.skip_ws_and_cmt(source, look_end) if source:sub(info_open, info_open) == "(" then local info_inner, info_after = duffle.read_parens(source, info_open) @@ -1221,7 +1218,7 @@ local function parse_mips_atom(source, pos, ident_end, line_of, out) end end - local body, after_brace, body_off = find_body_braces(source, brace_search_pos, open_paren + 1) + local body, after_brace, body_off = find_body_braces(source, brace_search_pos, open_paren + 1) if not body then return after_brace end if raw_name and raw_name ~= "" then register_atom(out, "atom", line_of(pos), raw_name, body, body_off, raw_name, pos, after_paren, source) @@ -1231,20 +1228,20 @@ local function parse_mips_atom(source, pos, ident_end, line_of, out) end --- Parse: `MipsAtomComp_() { }` ---- @param source string ---- @param pos integer +--- @param source string +--- @param pos integer --- @param ident_end integer ---- @param line_of fun(pos: integer): integer ---- @param out SourceScan +--- @param line_of fun(pos: integer): integer +--- @param out SourceScan --- @return integer local function parse_mips_atom_comp(source, pos, ident_end, line_of, out) - local inner, after_paren, open_paren = read_parens_after(source, ident_end) + local inner, after_paren, open_paren = read_parens_after(source, ident_end) if not inner then return after_paren end local raw_name = duffle.read_ident(inner, 1) if not raw_name then return open_paren + 1 end - local body, after_brace, body_off = find_body_braces(source, after_paren, open_paren + 1) + local body, after_brace, body_off = find_body_braces(source, after_paren, open_paren + 1) if not body then return after_brace end local name = strip_ac_prefix(raw_name) register_atom(out, "comp_bare", line_of(pos), name, body, body_off, raw_name, pos, after_paren, source) @@ -1253,14 +1250,14 @@ local function parse_mips_atom_comp(source, pos, ident_end, line_of, out) end --- Parse: `MipsAtomComp_Proc_(, { })` — body is inside the LAST `{` in args. ---- @param source string ---- @param pos integer +--- @param source string +--- @param pos integer --- @param ident_end integer ---- @param line_of fun(pos: integer): integer ---- @param out SourceScan +--- @param line_of fun(pos: integer): integer +--- @param out SourceScan --- @return integer local function parse_mips_atom_comp_proc(source, pos, ident_end, line_of, out) - local inner, after_paren, open_paren = read_parens_after(source, ident_end) + local inner, after_paren, open_paren = read_parens_after(source, ident_end) if not inner then return after_paren end -- Find the LAST `{` in inner (the body brace, not any potential embedded braces in expressions). @@ -1277,21 +1274,20 @@ local function parse_mips_atom_comp_proc(source, pos, ident_end, line_of, out) if close_pos > #inner + 1 then return after_paren end local raw_name = inner:match("^%s*([%w_]+)") or "?" - local name = strip_ac_prefix(raw_name) + local name = strip_ac_prefix(raw_name) -- Position of body[1] in source = open_paren + 1 (start of inner) + last_brace_pos + 1 (past '{'). local body_off = open_paren + 2 + last_brace_pos - register_atom(out, "comp_proc", line_of(pos), name, body, body_off, raw_name, pos, after_paren, source) return after_paren end --- Parse: `MipsCode code_ { }` (raw atom form — offsets pass only). ---- @param source string ---- @param pos integer +--- @param source string +--- @param pos integer --- @param ident_end integer ---- @param line_of fun(pos: integer): integer ---- @param out SourceScan +--- @param line_of fun(pos: integer): integer +--- @param out SourceScan --- @return integer local function parse_mips_code(source, pos, ident_end, line_of, out) local next_pos = duffle.skip_ws_and_cmt(source, ident_end) @@ -1300,8 +1296,8 @@ local function parse_mips_code(source, pos, ident_end, line_of, out) return ident_end end - local atom_name = next_ident:sub(6) - local body, after_brace, body_off = find_body_braces(source, next_after, ident_end) + local atom_name = next_ident:sub(6) + local body, after_brace, body_off = find_body_braces(source, next_after, ident_end) if not body then return after_brace end register_raw_atom(out, line_of(pos), atom_name, body, body_off, atom_name, pos) @@ -1320,13 +1316,13 @@ end --- pointer_depth = 0 --- } -- byte_size + per-field offset/byte_size set by the propagation pass. --- Also populates `out.binds[]` IFF `name:sub(1, 6) == "Binds_"`. ---- @param body string ---- @param name string ---- @param pos integer +--- @param body string +--- @param name string +--- @param pos integer --- @param line_of fun(pos: integer): integer ---- @param out SourceScan +--- @param out SourceScan local function register_struct_type(body, name, pos, line_of, out) - local fields = parse_struct_body_fields(body) + local fields = parse_struct_body_fields(body) local source_pos = line_of(pos) out.type_name_registry[name] = { name = name, @@ -1352,11 +1348,11 @@ end --- Register an Enum_ entry in type_name_registry. --- Local helper for parse_typedef_binds. Captures the underlying type (1st arg of `Enum_(, )`) and the body fields. --- @param underlying string ---- @param name string ---- @param body string ---- @param pos integer ---- @param line_of fun(pos: integer): integer ---- @param out SourceScan +--- @param name string +--- @param body string +--- @param pos integer +--- @param line_of fun(pos: integer): integer +--- @param out SourceScan local function register_enum_type(underlying, name, body, pos, line_of, out) local fields = parse_enum_body_fields(body) out.type_name_registry[name] = { @@ -1375,18 +1371,18 @@ end --- Captures the underlying type ident (LHS of `typedef ;`) and exposes it through the registry. --- The propagation pass follows the underlying_type chain to resolve byte_size. --- @param underlying string ---- @param name string ---- @param pos integer ---- @param line_of fun(pos: integer): integer ---- @param out SourceScan +--- @param name string +--- @param pos integer +--- @param line_of fun(pos: integer): integer +--- @param out SourceScan local function register_typedef_alias(underlying, name, pos, line_of, out) out.type_name_registry[name] = { - name = name, - kind = "typedef", + name = name, + kind = "typedef", underlying_type = underlying, - source_line = line_of(pos), - source_file = out._source_file, - pointer_depth = 0, + source_line = line_of(pos), + source_file = out._source_file, + pointer_depth = 0, } end @@ -1396,29 +1392,29 @@ end --- 1. `typedef Struct_() { } ;` adds to type_name_registry (kind="struct"). --- Binds_* aliases also land in out.binds[]. --- 2. `typedef Enum_(, ) { } ;` ---- adds to type_name_registry (kind="enum"). +--- Adds to type_name_registry (kind="enum"). --- 3. `typedef ;` simple typedef alias. --- Adds to type_name_registry (kind="typedef"). --- 4. `typedef TSet_();` duffle TSet_ convention. --- Strips TSet_ wrapper; adds to type_name_registry (kind="typedef") with underlying_type=. --- --- All four shapes also attach an "unrelated" debug-skip marker (the existing behavior — typedef declarations don't carry atom_dbg_skip). ---- @param source string ---- @param pos integer +--- @param source string +--- @param pos integer --- @param ident_end integer ---- @param line_of fun(pos: integer): integer ---- @param out SourceScan ---- @return integer +--- @param line_of fun(pos: integer): integer +--- @param out SourceScan +--- @return integer local function parse_typedef_binds(source, pos, ident_end, line_of, out) - local after_typedef = duffle.skip_ws_and_cmt(source, ident_end) - local id2, id2_end = duffle.read_ident(source, after_typedef) + local after_typedef = duffle.skip_ws_and_cmt(source, ident_end) + local id2, id2_end = duffle.read_ident(source, after_typedef) if not id2 then return ident_end end -- ── Shape 1: `typedef Struct_() { } ;` ──────────── if id2 == "Struct_" then - local inner, after_paren, open_paren = read_parens_after(source, id2_end, id2_end) + local inner, after_paren, open_paren = read_parens_after(source, id2_end, id2_end) if not inner then return id2_end end - local name = duffle.trim(inner) + local name = duffle.trim(inner) local body, after_brace = find_body_braces(source, after_paren, open_paren + 1) if not body then return after_brace end @@ -1428,7 +1424,7 @@ local function parse_typedef_binds(source, pos, ident_end, line_of, out) -- ── Shape 2: `typedef Enum_(, ) { } ;` elseif id2 == "Enum_" then - local inner, after_paren, open_paren = read_parens_after(source, id2_end, id2_end) + local inner, after_paren, open_paren = read_parens_after(source, id2_end, id2_end) if not inner then return id2_end end -- Split `inner` on the first top-level comma into (, ). local args = duffle.split_top_level_commas(inner) @@ -1436,7 +1432,7 @@ local function parse_typedef_binds(source, pos, ident_end, line_of, out) local underlying = duffle.trim(args[1]) local name = duffle.trim(args[2]) - local body, after_brace = find_body_braces(source, after_paren, open_paren + 1) + local body, after_brace = find_body_braces(source, after_paren, open_paren + 1) if not body then return after_brace end register_enum_type(underlying, name, body, pos, line_of, out) attach_debug_skip_marker(out, "unrelated") @@ -1469,11 +1465,10 @@ local function parse_typedef_binds(source, pos, ident_end, line_of, out) -- Shape 4 (TSet_ at id2 position): no preceding underlying span. if id2 == "TSet_" then - local inner, after_paren = read_parens_after(source, id2_end, id2_end) + local inner, after_paren = read_parens_after(source, id2_end, id2_end) if not inner then return id2_end end local tset_name = duffle.trim(inner) - -- Empty underlying span is acceptable; the TSet_ wrapper itself - -- encodes the alias identity (per the duffle TSet_ convention). + -- Empty underlying span is acceptable; the TSet_ wrapper itself encodes the alias identity (per the duffle TSet_ convention). register_typedef_alias("", tset_name, pos, line_of, out) attach_debug_skip_marker(out, "unrelated") return after_paren @@ -1491,13 +1486,13 @@ local function parse_typedef_binds(source, pos, ident_end, line_of, out) while scan < semi_pos do scan = duffle.skip_ws_and_cmt(source, scan) if scan >= semi_pos then break end - local id, id_end = duffle.read_ident(source, scan) + local id, id_end = duffle.read_ident(source, scan) if not id then scan = scan + 1 elseif id == "TSet_" then -- Shape 4 (TSet_ at non-id2 position): grab the parenthesized argument. local inner, after_paren = read_parens_after(source, id_end, id_end) - if inner then + if inner then tset_arg = duffle.trim(inner) tset_arg_end = after_paren tset_pos = scan @@ -1957,7 +1952,7 @@ local function merge_named_with_sites(registry, name, new_entry, site, collision registry[name].sites = { site } return end - local existing = registry[name] + local existing = registry[name] local new_shape = shape_fn(new_entry) local old_shape = shape_fn(existing) if new_shape == old_shape and new_shape ~= "" then diff --git a/scripts/passes/static_analysis.lua b/scripts/passes/static_analysis.lua index 458b524..846b36b 100644 --- a/scripts/passes/static_analysis.lua +++ b/scripts/passes/static_analysis.lua @@ -1,20 +1,17 @@ --- passes/static_analysis.lua — Per-atom static-analysis checks. ---- ---- Ownership: `ctx.shared.corpus` is the canonical merged registry; per-source fallback synthesis is rejected. +--- Ownership: `ctx.shared.corpus` canonical merged registry; per-source fallback synthesis is rejected. --- `atom.paths` supplies the emitted and analysis projections consumed by this pass. --- --- Per-atom rules: --- 1. transfer_hazards: A single forward walker (`analyze_hardware_relations`) reads `atom.paths.word_events` once per atom. ---- For each emitted word event it (a) inspects pending CPU/COP0/COP2/GTE relations against the event as CONSUMER +--- For each emitted word event it (a) inspects pending CPU / COP0 / COP2 / GTE relations against the event as CONSUMER --- (recording a hazard on `atom.paths.hazards` when the producer→consumer gap is below the required retire-slot count), ---- (b) applies the event's GPR value effects (`duffle.INSTRUCTION_GPR_EFFECTS`) to `atom.paths.forward_state.gpr_values`, +--- (b) Applies the event's GPR value effects (`duffle.INSTRUCTION_GPR_EFFECTS`) to `atom.paths.forward_state.gpr_values`, --- applies bounded constant propagation, and stages matching relation rows as PRODUCERS (with `destination_match` filters, e.g. for the IRGB fan-out). --- The `transfer_hazards` CHECK_RULES reader projects `atom.paths.hazards` into per-atom findings. ---- The reader does NOT re-walk source; this is the per-check purity contract. ---- The walker runs once per atom before the per-atom dispatch; the reader runs inside the same dispatch. +--- The reader does NOT re-walk source. The walker runs once per atom before the per-atom dispatch; the reader runs inside the same dispatch. --- 2. control_transfer_delay_slot_use: For every emitted branch/jump/call encoder in `duffle.CONTROL_TRANSFER_DELAY_SLOT_POLICIES` ---- (the six `branch_*` encoders plus `jump` / `jump_reg` / `jump_link` / `call_reg` / `call_addr`), ---- inspect the next emitted event in `atom.paths.word_events`. +--- (the six `branch_*` encoders plus `jump` / `jump_reg` / `jump_link` / `call_reg` / `call_addr`), inspect the next emitted event in `atom.paths.word_events`. --- Emit an `info`-severity finding when the successor is `nop` or absent (the next emitted word IS the hardware delay slot). --- `jump_reg(R_AtomJmp)` is suppressed by policy (the fixed `mac_yield()` handshake). --- `nop2` needs no special case: emission-model emits two `nop` events for it, so the first expansion is the hardware delay slot. @@ -101,10 +98,10 @@ local OUTPUT_EXTENSION = ".static_analysis.txt" -- ════════════════════════════════════════════════════════════════════════════ --- @class SourceFile ---- @field path string -- absolute path to the source file ---- @field text string -- the full source text ---- @field dir string -- the directory containing the source ---- @field basename string -- filename without extension +--- @field path string -- Absolute path to the source file +--- @field text string -- Full source text +--- @field dir string -- Directory containing the source +--- @field basename string -- Filename without extension --- @class PassCtx --- @field sources SourceFile[] @@ -121,43 +118,42 @@ local OUTPUT_EXTENSION = ".static_analysis.txt" --- @field outputs table[] --- @field errors table[] --- @field warnings table[] ---- @field info table[] -- finding-level info (kind == "info"); distinct from per-source scanned/cycles summary rows +--- @field info table[] -- Finding-level info (kind == "info"); distinct from per-source scanned/cycles summary rows --- @alias AtomName string -- lower_snake_case atom nameMacroName string -- lower_snake_case macro identifier --- @alias CheckName string -- "transfer_hazards" | "control_transfer_delay_slot_use" | "mac_yield_uniformity" | "yield_load_tail_pairing" | "abi_handoff" | "gpu_portstore_shape" | "per_atom_cycle_budget" | "enum_alias_membership" | "atom_type_consistency" | "binds_no_substruct_deref" --- @class AtomBody ---- @field line integer -- source line of the atom declaration ---- @field name AtomName -- atom name (e.g. "cube_g4_face") ---- @field body string -- the brace-delimited body (without the braces) +--- @field line integer -- Source line of the atom declaration +--- @field name AtomName -- Atom name (e.g. "cube_g4_face") +--- @field body string -- Brace-delimited body (without the braces) --- @field body_off integer -- char offset of body[1] in source --- @field kind string -- "atom" | "comp_bare" | "comp_proc" --- @class Token ---- @field tok string -- the raw token text (trimmed) ---- @field line integer -- source line of the token's start ---- @field ident string|nil -- the leading ident of the token (if any) +--- @field tok string -- Raw token text (trimmed) +--- @field line integer -- Source line of the token's start +--- @field ident string|nil -- Leading ident of the token (if any) --- @field kind string -- "n_words" | "mac_yield" | "gte_cmdw" | "mac_format" | "mac_gte_store" | "mac_insert_ot_tag" | "atom_label" | "atom_offset" | "other" --- @class Finding ---- @field line integer -- source line of the finding ---- @field atom AtomName -- the atom this finding is for (or "") ---- @field check CheckName -- the check identifier +--- @field line integer -- Source line of the finding +--- @field atom AtomName -- Atom this finding is for (or "") +--- @field check CheckName -- Check identifier --- @field kind string -- "error" | "warning" | "info" ---- @field msg string -- the finding message +--- @field msg string -- Finding message --- @class AtomAnalysis --- @field atom AtomBody ---- @field tokens Token[] -- the tokens in the atom body, annotated ---- @field findings Finding[] -- findings for this atom ---- @field total_cycles integer -- sum of token cycle costs +--- @field tokens Token[] -- Tokens in the atom body, annotated +--- @field findings Finding[] -- Findings for this atom +--- @field total_cycles integer -- Sum of token cycle costs -- ════════════════════════════════════════════════════════════════════════════ -- Per-word-event helpers -- ════════════════════════════════════════════════════════════════════════════ -- Pick the source-line field that best represents "where in the user's source file is this word?". --- -- `word_events` (populated by `passes/emission_model.lua::stamp_root_provenance`) carry four line fields: -- * `call_line` — physical line in the ROOT atom's source (the line of the `mac_X(...)` call site that triggered this emission, or `body_line` for direct words in the atom body) -- * `body_line` — physical line in the body containing the emitted word (the atom body for direct words; the component body for words expanded inside `mac_X(...)`) @@ -169,8 +165,7 @@ local OUTPUT_EXTENSION = ".static_analysis.txt" -- The user editing their atom body expects the line to point at THEIR source — i.e. the line where `mac_yield()` -- was called (e.g. `hello_gte_tape.c:35`). That line is `call_line`. -- --- For direct words in the atom body (no invocation wrapping them), `call_line == body_line` already, --- so `call_line` works for both cases. +-- For direct words in the atom body (no invocation wrapping them), `call_line == body_line` already, so `call_line` works for both cases. local function line_for_word_event(ev) if ev == nil then return 0 end return ev.call_line or ev.body_line or ev.line or ev.def_line or 0 @@ -193,46 +188,44 @@ end -- -- The classification is stored on `atom.paths.tok_class` as an array indexed by token index (1..#tokens). -- Each entry has: --- ident — the leading identifier (e.g. "load_word", "gte_cmdw_rtpt", "nop", "mac_yield") --- nop_words — 0 / 1 / 2 (for "nop" / "nop2" / anything else) --- nop_prefix — consecutive nop words ending just BEFORE this token (forward-pass pre-compute; --- makes preceding-nop lookup O(N)) --- is_yield — true if this token is `mac_yield` or `mac_yield(...)` --- is_atom_label — true if this token is `atom_label(name)`; label_name has the name --- is_branch — true if this token is `branch_*(...)` OR an unconditional-jump-with-offset (`jump(off)` / `call_addr(off)`); branch_label has the target label or false +-- ident — the leading identifier (e.g. "load_word", "gte_cmdw_rtpt", "nop", "mac_yield") +-- nop_words — 0 / 1 / 2 (for "nop" / "nop2" / anything else) +-- nop_prefix — consecutive nop words ending just BEFORE this token (forward-pass pre-compute; makes preceding-nop lookup O(N)) +-- is_yield — true if this token is `mac_yield` or `mac_yield(...)` +-- is_atom_label — true if this token is `atom_label(name)`; label_name has the name +-- is_branch — true if this token is `branch_*(...)` OR an unconditional-jump-with-offset (`jump(off)` / `call_addr(off)`); branch_label has the target label or false -- is_unconditional_jump — true if this token is `jump` or `call_addr` (BD slot + single successor — taken only; no fall-through). -- Mutually exclusive with the conditional-branch semantics; combined with `is_branch` above. --- is_terminal_jump — true if this token is `jump_reg` / `call_reg` / `jump_link` (transfers control OUT of the current atom; the `mac_yield()` handshake ends in `jump_reg(R_AtomJmp), nop`). --- No offset field — `atom_offset` is invalid here. Terminates the current path in the CFG. --- is_load — true if this token starts with any of: load_word, load_half, load_half_u, load_byte, --- load_byte_u, gte_lw, gte_lwc2. These all have MIPS load-delay semantics (the --- destination register is volatile for 1 word after the load). --- is_store_word — true if this token starts with `store_word(` +-- is_terminal_jump — true if this token is `jump_reg` / `call_reg` / `jump_link` (transfers control OUT of the current atom; the `mac_yield()` handshake ends in `jump_reg(R_AtomJmp), nop`). +-- No offset field — `atom_offset` is invalid here. Terminates the current path in the CFG. +-- is_load — true if this token starts with any of: load_word, load_half, load_half_u, load_byte, +-- load_byte_u, gte_lw, gte_lwc2. These all have MIPS load-delay semantics (the destination register is volatile for 1 word after the load). +-- is_store_word — true if this token starts with `store_word(` -- -- Checks that need the leading ident use `tok_class.ident` instead of re-matching the token string. -- Checks that need "how many nops before token i" use `tok_class.nop_prefix` instead of walking backwards. --- @class TokClass ---- @field ident string -- leading identifier ---- @field nop_words integer -- 0/1/2 ---- @field nop_prefix integer -- consecutive nop words before this token +--- @field ident string -- lLading identifier +--- @field nop_words integer -- 0 / 1 / 2 +--- @field nop_prefix integer -- Consecutive nop words before this token --- @field is_yield boolean --- @field is_atom_label boolean ---- @field label_name string|nil -- for atom_label(name) ---- @field is_branch boolean -- conditional branch OR unconditional-jump-with-offset ---- @field is_unconditional_jump boolean -- `jump` / `call_addr` only ---- @field is_terminal_jump boolean -- `jump_reg` / `call_reg` / `jump_link` only ---- @field branch_label string|false|nil -- for branch_*(..., atom_offset(F, label)) OR jump/call_addr ---- @field is_load boolean -- load_word | load_half | load_half_u | load_byte | load_byte_u | gte_lw | gte_lwc2 +--- @field label_name string|nil -- For atom_label(name) +--- @field is_branch boolean -- Conditional branch OR unconditional-jump-with-offset +--- @field is_unconditional_jump boolean -- `jump` / `call_addr` only +--- @field is_terminal_jump boolean -- `jump_reg` / `call_reg` / `jump_link` only +--- @field branch_label string|false|nil -- For branch_*(..., atom_offset(F, label)) OR jump/call_addr +--- @field is_load boolean -- load_word | load_half | load_half_u | load_byte | load_byte_u | gte_lw | gte_lwc2 --- @field is_store_word boolean ---- @field mac_format_shape string|nil -- "f3" / "g4" etc. for mac_format_X_color; nil otherwise ---- @field is_gte_store boolean -- ident matches `mac_gte_store_` ---- @field is_ot_tag boolean -- ident matches `mac_insert_ot_tag_` ---- @field writes_r_prim_cursor boolean -- store_word targeting R_PrimCursor ---- @field reads_r_tape_ptr boolean -- any token referencing R_TapePtr ---- @field o_arg1 string|nil -- first arg of O_(, ) captures; nil for non-O_ tokens ---- @field o_arg2 string|nil -- second arg of O_(, ) captures ---- @field s_arg1 string|nil -- arg of S_() captures; nil for non-S_ tokens +--- @field mac_format_shape string|nil -- "f3" / "g4" etc. for mac_format_X_color; nil otherwise +--- @field is_gte_store boolean -- Ident matches `mac_gte_store_` +--- @field is_ot_tag boolean -- Ident matches `mac_insert_ot_tag_` +--- @field writes_r_prim_cursor boolean -- store_word targeting R_PrimCursor +--- @field reads_r_tape_ptr boolean -- Any token referencing R_TapePtr +--- @field o_arg1 string|nil -- First arg of O_(, ) captures; nil for non-O_ tokens +--- @field o_arg2 string|nil -- Second arg of O_(, ) captures +--- @field s_arg1 string|nil -- Arg of S_() captures; nil for non-S_ tokens -- The set of MIPS instruction idents that have a load-delay slot. -- Per MIPS I R3000A: `lw`, `lh`, `lhu`, `lb`, `lbu`, `lwc2` (gte_lw). @@ -343,7 +336,7 @@ local function classify_tokens(tokens) is_atom_label = is_atom_label, label_name = label_name, is_branch = is_branch, - is_unconditional_jump = is_unconditional_jump, + is_unconditional_jump = is_unconditional_jump, is_terminal_jump = is_terminal_jump, branch_label = branch_label, is_load = is_load, @@ -401,15 +394,13 @@ end -- therefore counts ONLY words strictly between the producer and the consumer. -- ───────────────────────────────────────────────────────────────────────── --- True iff `consumer_event` is a GTE command (gte_cmdw_* or one of the human-readable aliases --- mapped in `duffle.GTE_COMMAND_ALIASES`). Used by the LWC2 retirement-regime dispatch in the --- forward walker: a GTE-command consumer can read the LWC2 result in the very next slot (the GTE --- pipeline latches the LWC2 data); any other consumer must observe the standard MIPS load delay --- (gap >= 1). +-- True iff `consumer_event` is a GTE command (gte_cmdw_* or one of the human-readable aliases mapped in `duffle.GTE_COMMAND_ALIASES`). +-- Used by the LWC2 retirement-regime dispatch in the forward walker: a GTE-command consumer can read the LWC2 result in the very next slot +-- (the GTE pipeline latches the LWC2 data); any other consumer must observe the standard MIPS load delay (gap >= 1). local function is_gte_command(consumer_event) local tok = consumer_event.encoder or consumer_event.ident or "" if tok:sub(1, 9) == "gte_cmdw_" then return true end - local aliases = duffle.GTE_COMMAND_ALIASES or {} + local aliases = duffle.GTE_COMMAND_ALIASES or {} return aliases[tok] ~= nil end @@ -422,7 +413,7 @@ local function is_cop2_consumer_of(consumer_event, destination, producer_rel) -- not used by MTC2/CTC2 today because the "consumer" is a GTE command and its reads are not operand positions.) local args = consumer_event.args or {} for _, pos in ipairs(args) do - if pos == destination then return true end + if pos == destination then return true end end -- Match via the command's input set: the consumer encoder resolves to a `gte_cmdw_*` -- short form whose `duffle.GTE_COMMAND_INPUTS` entry includes the destination (or a fan-out target). @@ -718,8 +709,8 @@ local function consume_cu2_transition(atom, event, ev_word, forward) local transition = forward.cu2_transition if not transition then return end - local gap = ev_word - transition.producer_word - 1 - local target = transition.target_state + local gap = ev_word - transition.producer_word - 1 + local target = transition.target_state local event_line = line_for_word_event(event) if target == "unknown" then append_cu2_finding(atom, event, forward, transition, gap, "info", "unknown", @@ -752,8 +743,7 @@ local function consume_cu2_transition(atom, event, ev_word, forward) else append_cu2_finding(atom, event, forward, transition, gap, "error", "exact", - string.format( - "%s at line %d: COP2 unavailable after SR.CU2 was disabled" + string.format("%s at line %d: COP2 unavailable after SR.CU2 was disabled" .. " (gap=%d, required=%d)", atom.name, event_line, gap, transition.required)) @@ -831,8 +821,8 @@ local function analyze_hardware_relations(atom) local is_match = false if semantic == "MTC2" or semantic == "CTC2" or semantic == "LWC2_to_GTE" or semantic == "LWC2_to_other" then -- Consumer is a GTE command whose input set contains the producer's COP2 destination (or a fan-out target). - -- LWC2_to_GTE — GTE-command consumer: gap = 0 OK (the pipeline latches the LWC2 result). - -- LWC2_to_other — non-GTE consumer: standard load delay applies. + -- LWC2_to_GTE — GTE-command consumer: gap = 0 OK (the pipeline latches the LWC2 result). + -- LWC2_to_other — non-GTE consumer: standard load delay applies. if relation.id == "lwc2_to_gte_command" then is_match = is_gte_command(ev) and is_cop2_consumer_of(ev, prod.destination, relation) elseif relation.id == "lwc2_to_other_consumer" then @@ -1266,21 +1256,26 @@ local function check_hazard_nop_use(atom, _pipe_ctx, findings) else -- Track the slot_kind so the BD-separation case can assert the mac_yield handshake is still suppressed. local slot_kind = "plain" - -- MIPS load-delay slot: a `load_*` wrote a register in the previous slot, and the result - -- is unavailable for 1 cycle. This `nop` is structurally required; classifying it as - -- `modeled-required` is the correct signal (removing it would make the following - -- instruction read the OLD value of the loaded register, a load-use hazard). The - -- `load_delay_violations` check (Concern 3) catches the actual read-side error; here - -- we suppress the `modeled-redundant` misclassification. + -- MIPS load-delay slot: a `load_*` wrote a register in the previous slot, and the result is unavailable for 1 cycle. + -- This `nop` is structurally required; classifying it as `modeled-required` is the correct signal + -- (removing it would make the following instruction read the OLD value of the loaded register, a load-use hazard). + -- The `load_delay_violations` check (Concern 3) catches the actual read-side error; here we suppress the `modeled-redundant` misclassification. -- The set of load instructions mirrors the LOAD_INSTRUCTION_IDENTS in `check_load_delay_slots`. - local load_idents = { load_word = true, load_half = true, load_half_u = true, - load_byte = true, load_byte_u = true, gte_lw = true, gte_lwc2 = true } + local load_idents = { + load_word = true, + load_half = true, + load_half_u = true, + load_byte = true, + load_byte_u = true, + gte_lw = true, + gte_lwc2 = true + } local is_load_delay = load_idents[prev_ident] == true - if is_load_delay then + if is_load_delay then -- Determine the destination register from the load's `writes` field. - local prev_writes = gpr_effects[prev_ident] and gpr_effects[prev_ident].writes or {} - local prev_args = prev_ev.args or {} - local load_dest = prev_writes[1] and prev_args[prev_writes[1]] or "" + local prev_writes = gpr_effects[prev_ident] and gpr_effects[prev_ident].writes or {} + local prev_args = prev_ev.args or {} + local load_dest = prev_writes[1] and prev_args[prev_writes[1]] or "" findings[#findings + 1] = { check = "hazard_nop_use", kind = "info", @@ -1447,7 +1442,7 @@ end local function check_load_delay_slots(atom, pipe_ctx, findings) if atom.kind ~= "atom" then return end local events = atom.paths.word_events or {} - if #events == 0 then return end + if #events == 0 then return end if is_runtime_helper(atom) then return end local gpr_effects = duffle.INSTRUCTION_GPR_EFFECTS or {} @@ -2349,20 +2344,20 @@ end -- This is the plex pattern: the iteration is in ONE place (validate), the variation is in DATA (this table). local CHECK_RULES = { - { name = "transfer_hazards", per_atom = check_transfer_hazards }, - { name = "gte_input_latch", per_atom = check_gte_input_latch }, - { name = "gte_role_mismatch", per_atom = check_gte_role_mismatch }, - { name = "hazard_nop_use", per_atom = check_hazard_nop_use }, - { name = "control_transfer_delay_slot_use",per_atom = check_control_transfer_delay_slot_use}, - { name = "load_delay_violation", per_atom = check_load_delay_slots }, - { name = "mac_yield_uniformity", per_atom = check_mac_yield_uniformity }, - { name = "yield_load_tail_pairing", per_atom = check_yield_load_tail_pairing }, - { name = "abi_handoff", per_atom = check_abi_handoff }, - { name = "gpu_portstore_shape", per_atom = check_gpu_portstore_shape }, - { name = "per_atom_cycle_budget", per_atom = check_per_atom_cycle_budget }, - { name = "enum_alias_membership", per_source = check_enum_alias_membership }, - { name = "atom_type_consistency", per_source = check_atom_type_consistency }, - { name = "binds_no_substruct_deref", per_source = check_binds_no_substruct_deref }, + { name = "transfer_hazards", per_atom = check_transfer_hazards }, + { name = "gte_input_latch", per_atom = check_gte_input_latch }, + { name = "gte_role_mismatch", per_atom = check_gte_role_mismatch }, + { name = "hazard_nop_use", per_atom = check_hazard_nop_use }, + { name = "control_transfer_delay_slot_use", per_atom = check_control_transfer_delay_slot_use }, + { name = "load_delay_violation", per_atom = check_load_delay_slots }, + { name = "mac_yield_uniformity", per_atom = check_mac_yield_uniformity }, + { name = "yield_load_tail_pairing", per_atom = check_yield_load_tail_pairing }, + { name = "abi_handoff", per_atom = check_abi_handoff }, + { name = "gpu_portstore_shape", per_atom = check_gpu_portstore_shape }, + { name = "per_atom_cycle_budget", per_atom = check_per_atom_cycle_budget }, + { name = "enum_alias_membership", per_source = check_enum_alias_membership }, + { name = "atom_type_consistency", per_source = check_atom_type_consistency }, + { name = "binds_no_substruct_deref", per_source = check_binds_no_substruct_deref }, } -- ════════════════════════════════════════════════════════════════════════════ @@ -2521,28 +2516,27 @@ local function validate(ctx, src, corpus_pipe_ctx) -- Hazard readers (transfer_hazards) populate `f.check`, `f.relation_id`, `f.semantic`, `f.direction`, `f.producer_destination`, `f.gap`, `f.required`, `f.evidence_confidence`, etc.; -- Copying them through keeps the per-severity bucket schema compatible with the renderer while making the diagnostic payload queryable. local payload = { - line = f.line, - msg = f.msg, - check = f.check, - atom = f.atom, - source = f.source, - relation_id = f.relation_id, - semantic = f.semantic, - direction = f.direction, + line = f.line, + msg = f.msg, + check = f.check, + atom = f.atom, + source = f.source, + relation_id = f.relation_id, + semantic = f.semantic, + direction = f.direction, producer_destination = f.producer_destination, - producer_word = f.producer_word, - producer_line = f.producer_line, - producer_source = f.producer_source, - consumer_word = f.consumer_word, - consumer_token = f.consumer_token, - gap = f.gap, - required = f.required, - evidence_confidence = f.evidence_confidence, - evidence_source = f.evidence_source, + producer_word = f.producer_word, + producer_line = f.producer_line, + producer_source = f.producer_source, + consumer_word = f.consumer_word, + consumer_token = f.consumer_token, + gap = f.gap, + required = f.required, + evidence_confidence = f.evidence_confidence, + evidence_source = f.evidence_source, } - -- Preserve relation fields such as target_state and status_register, - -- status_value, and future policy metadata) without making the binner - -- another semantic walker. + -- Preserve relation fields such as target_state and status_register status_value, + -- and future policy metadata) without making the binner another semantic walker. for key, value in pairs(f) do if payload[key] == nil then payload[key] = value end end @@ -2624,7 +2618,7 @@ function M.run(ctx) -- Build the corpus-wide pipe_ctx ONCE per pass run. -- The pipe_ctx is shared across every validate() invocation in this M.run so cross-source visibility is constant. local corpus_pipe_ctx = build_corpus_pipe_ctx(ctx) - local corpus = ctx.shared.corpus + local corpus = ctx.shared.corpus -- Aggregate per-DIRECTORY (per-module). -- One static_analysis.txt per source-directory, emitted only if the directory contains at least one atom. @@ -2676,8 +2670,8 @@ function M.run(ctx) -- Aggregate per-dir errors/warnings/info into the orchestrator totals. -- Hoisted out of any per-dir file-emit so `report.lua` can drop the on-disk file emitter without losing the cross-module rollup. - for _, e in ipairs(dir_errors) do errors [#errors + 1] = e end - for _, w in ipairs(dir_warnings) do warnings[#warnings + 1] = w end + for _, e in ipairs(dir_errors) do errors [#errors + 1] = e end + for _, w in ipairs(dir_warnings) do warnings[#warnings + 1] = w end for _, i_ in ipairs(dir_info) do info [#info + 1] = i_ end -- (No per-dir emit: per-module findings are stashed on `corpus.static_analysis_results` above. -- `report.lua` reads that projection to render `.atom_meta_report.md` without re-running validate().) diff --git a/scripts/passes/word_count_eval.lua b/scripts/passes/word_count_eval.lua index f1a271d..aebd8af 100644 --- a/scripts/passes/word_count_eval.lua +++ b/scripts/passes/word_count_eval.lua @@ -93,7 +93,7 @@ end --- Load the authored `word_count.metadata.h` into `ctx.shared.corpus.word_counts`. --- Generated `.macs.h` files are OUTPUT artifacts and are NOT scanned as inputs. --- Current component counts are computed and inserted by `passes/components.lua` ---- after the components pass iterates `corpus.source_order` and writes each source's `.macs.h` file. +--- after the components pass iterates `corpus.source_order` and writes each source-directory's `gen/macs.h` file. --- --- Contract: --- * `ctx.shared.corpus` MUST exist (canonical corpus ownership). diff --git a/scripts/ps1_meta.lua b/scripts/ps1_meta.lua index aff09e8..35dc8bf 100644 --- a/scripts/ps1_meta.lua +++ b/scripts/ps1_meta.lua @@ -269,9 +269,9 @@ PASS_FLAGS: Or pick any subset: --scan-source Scan sources into the fat SourceScan payload --word-counts Load metadata.h + scan for existing .macs.h - --components Generate /gen/.macs.h + --components Generate /gen/macs.h (per-directory aggregation) --validate Run atom annotation DSL validation - --offsets Generate /gen/.offsets.h + --offsets Generate /gen/offsets.h (per-directory aggregation) --atoms-source-map Generate .atoms.sourcemap.txt per source --dwarf-injection [opt-in] Select the post-link dwarf-injection pass + set the opt-in flag. Requires --elf. --static-analysis Static analysis: GTE pipeline-fill, mac_yield, ABI handoff, cycle budget diff --git a/scripts/update_deps.ps1 b/scripts/update_deps.ps1 index 58145e7..d3711a0 100644 --- a/scripts/update_deps.ps1 +++ b/scripts/update_deps.ps1 @@ -56,7 +56,7 @@ if (-not $msbuild_exe) { } $path_pcsx_sln = join-path $path_pcsx_redux 'vsprojects\pcsx-redux.sln' -& $msbuild_exe $path_pcsx_sln /p:Configuration=Release /p:Platform=x64 /p:PlatformToolset=v143 /m /v:minimal +& $msbuild_exe $path_pcsx_sln /p:Configuration=Release /p:Platform=x64 /p:PlatformToolset=v143 /m /v:minimal # Locate luajit via scoop. `luajit.exe` is on PATH via scoop's shim; # we use `scoop prefix` to find the install root for the include dir (needed to compile lpeg against luajit's headers). @@ -70,8 +70,8 @@ if (-not $luajit_prefix -or -not (Test-Path (Join-Path $luajit_prefix 'bin/luaji # Discover the luajit include dir by globbing `include/luajit-*`. # This avoids hardcoding a specific version (e.g. `luajit-2.1`). $luajit_include_root = Join-Path $luajit_prefix 'include' -$lua_inc_dir = Get-ChildItem -Path $luajit_include_root -Directory -Filter 'luajit-*' -ErrorAction SilentlyContinue | - Select-Object -First 1 -ExpandProperty FullName +$lua_inc_dir = Get-ChildItem -Path $luajit_include_root -Directory -Filter 'luajit-*' -ErrorAction SilentlyContinue | +Select-Object -First 1 -ExpandProperty FullName if (-not $lua_inc_dir) { write-error "No 'luajit-*' include dir found under '$luajit_include_root'. The scoop luajit install may be broken." exit 1 @@ -90,7 +90,7 @@ $lpeg_compile_args = @( '-o', 'lpeg.dll' ) + $lpeg_sources + @('-lluajit-5.1') push-location $path_lpeg -& gcc @lpeg_compile_args +& gcc @lpeg_compile_args pop-location # ════════════════════════════════════════════════════════════════════════════ @@ -101,8 +101,8 @@ pop-location $path_lfs = join-path $path_toolchain 'lfs' verify-path $path_lfs -$lfs_src = join-path $path_pcsx_redux 'third_party\luafilesystem\src\lfs.c' -$lfs_dll = join-path $path_lfs 'lfs.dll' +$lfs_src = join-path $path_pcsx_redux 'third_party\luafilesystem\src\lfs.c' +$lfs_dll = join-path $path_lfs 'lfs.dll' $lfs_dll_import = join-path $luajit_lib_dir 'libluajit-5.1.dll.a' & gcc -O2 -shared "-I$lua_inc_dir" -o $lfs_dll $lfs_src $lfs_dll_import