Codebase overhaul. Metaprogram proofread (part 2). Starting to get serious.

Need to rewrite the ps1 lua metaprogram sometime soonish. Getting too bloated... need to consolidate code paths.

In this push codebase structure is starting to get a bit more realized. Decided todo now to match Pikuma's linking module files vods beginning to reorganize its codebase as well.
Atoms & atom components are not in their on *.atom.c files. (Not calling it tape.c as I don't really bake tapes like that outside of the unity c file so far...)

The lua metaprogram has had additional features added to it yet again to avoid hardcoding module handling and supporting multiple atom files per-module.
Either after the camera or cd-rom section I'll be most likely pausing to fully refactor the metaprogram. Possibly as a full re-write to get the loc minimal.
This commit is contained in:
ed
2026-08-04 23:34:00 -04:00
parent 6441dbc23e
commit 748b58c5c5
37 changed files with 1265 additions and 1159 deletions
@@ -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 <stdint.h>
#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.
+5 -2
View File
@@ -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 compilers 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.
-150
View File
@@ -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)
-9
View File
@@ -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
+183
View File
@@ -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)
+26
View File
@@ -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
+82
View File
@@ -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)
View File
+76
View File
@@ -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
View File
+42 -171
View File
@@ -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 (<module>/gen/<dir>/<dir>.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
+29
View File
@@ -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)
+5 -10
View File
@@ -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)); }
+38
View File
@@ -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
+8
View File
@@ -0,0 +1,8 @@
#ifdef INTELLISSENSE_DIRECTIVES
# include "gen/macs.h"
# include "gen/offsets.h"
#endif
ATOM_FILE_DEBUGGER_LINE_MARKER(pad_atom_c);
View File
+7
View File
@@ -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);
@@ -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]; };
+1 -1
View File
@@ -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/<dir>.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.
//
+3 -3
View File
@@ -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"
+3 -3
View File
@@ -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
@@ -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)
@@ -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
@@ -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)
+41 -129
View File
@@ -1,9 +1,17 @@
#pragma region Vendors
#include <stdio.h>
#include <stdlib.h>
#include <assert.h>
// #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
+74
View File
@@ -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;
};
-3
View File
@@ -1,3 +0,0 @@
#ifdef INTELLISENSE_DIRECTIVES
# include "psyq.h"
#endif
+87 -69
View File
@@ -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 `<dir_basename>.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<string, 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<string, 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<string, Component>
@@ -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<string, Component>
@@ -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<string, integer> -- 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
+79 -91
View File
@@ -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_<R_Name> 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(<rbind_atom>)` (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, <T>): 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(<T>) 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_<R_Name> 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.
+7 -9
View File
@@ -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
+61 -52
View File
@@ -3,12 +3,11 @@
--- Reads the pre-scanned SourceScan payload (produced once upstream by `duffle.scan_source`)
--- for `MipsAtom_(name)` and `MipsCode code_<name>` declarations, computes the word offset
--- from each `atom_offset(F, T)` marker to its target `atom_label(T)` declaration, and emits
--- `<dir_basename>.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<string, integer>
--- @param labels table<string, integer>
--- @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 `<dir_basename>.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
+77 -76
View File
@@ -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/<dir_basename>.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<string, 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<string, 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/<module>.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<string, integer>
--- @param wc table<string, integer>
--- @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("")
+149 -154
View File
@@ -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, <type>...)`;
-- 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_(<name>) [atom_info(<binds>, <reads>, <writes>)] { <body> }`
--- @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_(<name>) { <body> }`
--- @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_(<name>, { <body> })` — 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_<name> { <body> }` (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_(<underlying>, <name>)`) 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 <type> <alias>;`) 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_(<name>) { <body> } <alias>;` adds to type_name_registry (kind="struct").
--- Binds_* aliases also land in out.binds[].
--- 2. `typedef Enum_(<underlying>, <name>) { <body> } <alias>;`
--- adds to type_name_registry (kind="enum").
--- Adds to type_name_registry (kind="enum").
--- 3. `typedef <type> <alias>;` simple typedef alias.
--- Adds to type_name_registry (kind="typedef").
--- 4. `typedef <type> TSet_(<name>);` duffle TSet_ convention.
--- Strips TSet_ wrapper; adds to type_name_registry (kind="typedef") with underlying_type=<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_(<name>) { <body> } <alias>;` ────────────
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_(<underlying>, <name>) { <body> } <alias>;`
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 (<underlying>, <name>).
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
+117 -123
View File
@@ -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_<shape>`
--- @field is_ot_tag boolean -- ident matches `mac_insert_ot_tag_<shape>`
--- @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_(<a>, <b>) captures; nil for non-O_ tokens
--- @field o_arg2 string|nil -- second arg of O_(<a>, <b>) captures
--- @field s_arg1 string|nil -- arg of S_(<a>) 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_<shape>`
--- @field is_ot_tag boolean -- Ident matches `mac_insert_ot_tag_<shape>`
--- @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_(<a>, <b>) captures; nil for non-O_ tokens
--- @field o_arg2 string|nil -- Second arg of O_(<a>, <b>) captures
--- @field s_arg1 string|nil -- Arg of S_(<a>) 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 "<load-destination>"
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 "<load-destination>"
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 `<module>.atom_meta_report.md` without re-running validate().)
+1 -1
View File
@@ -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 `<dir_basename>.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).
+2 -2
View File
@@ -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 <module>/gen/<basename>.macs.h
--components Generate <srcdir>/gen/macs.h (per-directory aggregation)
--validate Run atom annotation DSL validation
--offsets Generate <module>/gen/<basename>.offsets.h
--offsets Generate <srcdir>/gen/offsets.h (per-directory aggregation)
--atoms-source-map Generate <basename>.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
+6 -6
View File
@@ -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