26 changed files with 1540 additions and 1250 deletions
+2
View File
@@ -160,6 +160,8 @@
* ----------------------------------------------------------------------------*/
#define atom_bind(binds_struct) /* atom_bind(binds_struct) */
#define Binds_(type) (tmpl(Binds,type)) // TODO(Ed): Do we want to use this?
/* ============================================================================
* atom_label / atom_offset — branch target machinery
*
+7 -7
View File
@@ -228,21 +228,21 @@ WORD_COUNT(mac_gte_op_cross_v3s4, 13)
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) \
#define mac_gte_load_tri_verts(vbase, v0, v1, v2) \
shift_lleft(R_AT, v0, v3s2_byteoff) \
, add_u_self(R_AT, vbase) \
, load_word(R_V0, R_AT, O_(V3_S2,x)) \
, load_word(R_V1, R_AT, O_(V3_S2,z)) \
, LdSlot_ 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) \
, shift_lleft(R_AT, v1, v3s2_byteoff) \
, add_u_self(R_AT, vbase) \
, load_word(R_V0, R_AT, O_(V3_S2,x)) \
, load_word(R_V1, R_AT, O_(V3_S2,z)) \
, LdSlot_ 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) \
, shift_lleft(R_AT, v2, v3s2_byteoff) \
, add_u_self(R_AT, vbase) \
, load_word(R_V0, R_AT, O_(V3_S2,x)) \
, load_word(R_V1, R_AT, O_(V3_S2,z)) \
, LdSlot_ gte_mv_to_data_r(R_V0, C2_VXY2) \
+1 -1
View File
@@ -33,7 +33,7 @@ enum {
atom_offset_example_atom_proc_skip = _atom_offset_example_atom_proc_skip,
};
// --- atom: build_normalize_v3s4 (63 words) ---
// --- atom: normalize_v3s4 (62 words) ---
#define _atom_offset_aligned_done_srav_path 3
#define _atom_offset_srav_path_aligned_done 4
+17 -18
View File
@@ -63,10 +63,10 @@ FI_ Slice_MipsCode ac_gte_store_f3(AtomBuilder_R ab, U4 r_primitive_cursor) atom
})
/* Words: 18; Translates indices to vertex addresses and pushes them to GTE */
I_ Slice_MipsCode ac_gte_load_tri_verts(AtomBuilder_R ab, U4 r_vert_base, U4 r_v0, U4 r_v1, U4 r_v2) atom_dbg_skip MipsAtomComp_Proc_(ab, {
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)), LdSlot_ 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)), LdSlot_ 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)), LdSlot_ gte_mv_to_data_r(R_V0, C2_VXY2), gte_mv_to_data_r(R_V1, C2_VZ2),
I_ Slice_MipsCode ac_gte_load_tri_verts(AtomBuilder_R ab, Reg vbase, Reg v0, Reg v1, Reg v2) atom_dbg_skip MipsAtomComp_Proc_(ab, {
shift_lleft(R_AT, v0, v3s2_byteoff), add_u_self(R_AT, vbase), load_word(R_V0, R_AT, O_(V3_S2,x)), load_word(R_V1, R_AT, O_(V3_S2,z)), LdSlot_ gte_mv_to_data_r(R_V0, C2_VXY0), gte_mv_to_data_r(R_V1, C2_VZ0),
shift_lleft(R_AT, v1, v3s2_byteoff), add_u_self(R_AT, vbase), load_word(R_V0, R_AT, O_(V3_S2,x)), load_word(R_V1, R_AT, O_(V3_S2,z)), LdSlot_ gte_mv_to_data_r(R_V0, C2_VXY1), gte_mv_to_data_r(R_V1, C2_VZ1),
shift_lleft(R_AT, v2, v3s2_byteoff), add_u_self(R_AT, vbase), load_word(R_V0, R_AT, O_(V3_S2,x)), load_word(R_V1, R_AT, O_(V3_S2,z)), LdSlot_ 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
@@ -267,28 +267,28 @@ internal S2 const gte_normalize_sqr_tbl[192] align_(2) = {
0x0820, 0x081c, 0x0818, 0x0814, 0x0810, 0x080c, 0x0808, 0x0804,
};
typedef Struct_(Binds_NormalizeV3S4) {
typedef Struct_(Binds_normalize_v3s4) {
U2 src_offset; /* offset of src V3_S4 within the BIOS scratchpad */
U2 dst_offset; /* offset of dst V3_S4 within the BIOS scratchpad */
};
typedef Struct_(RegUse_build_normalize_v3s4) {
typedef Struct_(RegUse_normalize_v3s4) {
union { Reg_(V3_S4) res, src; };
union { Reg t0, src_ptr, mac2; };
union { Reg t1, dst_ptr; };
union { Reg t2, dst_offset, mac1, v_sqr_aligned; };
union { Reg t3, src_offset, btarget, shift_count, sqrtbl_index; };
union { Reg t4, mac3, v_sqr_sum, half_shift_tmp, inv_len; };
union { Reg t5, lzcr, half_shift; };
union { Reg r0, src_ptr, mac2; };
union { Reg r1, dst_ptr; };
union { Reg r2, dst_offset, mac1, v_sqr_aligned; };
union { Reg r3, src_offset, btarget, shift_count, sqrtbl_index; };
union { Reg r4, mac3, v_sqr_sum, scale_exp, srav_shift; };
union { Reg r5, lzcr, inv_len; };
};
/* ─── Full normalize (all 4 stages inline) ───
* Generic 4-stage GTE normalize (SQR → sum+LZCR → align+sqrtbl → GPF+srav). */
internal MipsAtom* build_normalize_v3s4(AtomArena_R aa, RegUse_build_normalize_v3s4 r)
internal MipsAtom* normalize_v3s4(AtomArena_R aa, RegUse_normalize_v3s4 r)
MipsAtom_Proc_(aa, {
load_half(r.src_offset, R_TapePtr, O_(Binds_NormalizeV3S4, src_offset)),
load_half(r.dst_offset, R_TapePtr, O_(Binds_NormalizeV3S4, dst_offset)),
load_half(r.src_offset, R_TapePtr, O_(Binds_normalize_v3s4, src_offset)),
load_half(r.dst_offset, R_TapePtr, O_(Binds_normalize_v3s4, dst_offset)),
LdSlot_ add_u(r.src_ptr, R_ScratchBase, r.src_offset),
LdSlot_ add_u(r.dst_ptr, R_ScratchBase, r.dst_offset),
LdSlot_ add_ui_self(R_TapePtr, S_(Binds_NormalizeV3S4)),
LdSlot_ add_ui_self(R_TapePtr, S_(Binds_normalize_v3s4)),
mac_load_v3s4(r.src, r.src_ptr, 0),
@@ -313,7 +313,6 @@ MipsAtom_Proc_(aa, {
sub_s(r.shift_count, r.shift_count, r.lzcr),
shift_aright_var(r.v_sqr_aligned, r.v_sqr_aligned, r.shift_count),
atom_label(aligned_done)
or_u(r.half_shift, r.half_shift_tmp, 0),
add_si( r.v_sqr_aligned, r.v_sqr_aligned, -64),
shift_lleft(r.v_sqr_aligned, r.v_sqr_aligned, 1),
mac_load_word_imm(r.sqrtbl_index, & gte_normalize_sqr_tbl), add_u_self(r.sqrtbl_index, r.v_sqr_aligned),
@@ -326,7 +325,7 @@ MipsAtom_Proc_(aa, {
GteDelay_ load_word(R_AtomJmp, R_TapePtr, 0), LdSlot_ // ac_yield: word 1
GteDelay_ add_ui_self( R_TapePtr, S_(MipsCode)) // ac_yield: word 2
),
mac_shift_aright_var_v3s4_self(r.res, r.half_shift),
mac_shift_aright_var_v3s4_self(r.res, r.srav_shift),
mac_store_v3s4(r.res, r.dst_ptr, 0),
jump_reg(R_AtomJmp), BdSlot_ nop // ac_yield: word 3-4
+14 -8
View File
@@ -99,8 +99,8 @@ enum {
R_Atom7 = R_T7,
R_Atom8 = R_T8,
R_Atom9 = R_T9,
R_Atom10 = R_V0, // Tend to be used with gte DMAs
R_Atom11 = R_V1, // Tend to be used with gte DMAs
R_Atom10 = R_V0, // Tend to be used with gte moves
R_Atom11 = R_V1, // Tend to be used with gte moves
R_Atom12 = R_A0,
R_Atom13 = R_A1,
R_Atom14 = R_A2,
@@ -163,6 +163,12 @@ typedef Slice_(MipsAtom);
// Used for trivial mappings from one atom component proc to the command of a more baser (meant for type-mapping)
#define MipsAtomComp_ProcMap_(ab, base_command) atom_dbg_skip MipsAtomComp_Proc_(ab, {base_command })
// WIP: Atoms Assocated closely with each other to form a tape procedure. (Maybe also a phase in a procedure/pipeline?)
#define AtomBundle_(name) Struct_(tmpl(AtomBundle,name))
#define AtomBundle_Len(name) S_(tmpl(AtomBundle,name))/S_(MipsAtom*)
#define AtomBundleEntry_(bundle,entry) tmpl(bundle,entry)
/* Line-table anchor: gcc only adds a file to the .debug_line file table when the contains line-numbered content.
Files containing only atoms and atom components.
Place `ATOM_FILE_LINE_MARKER();` once at file scope in any `.atom.c` that defines atoms.
@@ -242,9 +248,13 @@ FI_ TapeBuilder tb_make(Slice mem) { return (TapeBuilder){ u4_(mem.ptr), mem.len
FI_ void tb_emit(TapeBuilder* tb, MipsAtom* atom) { u4_r(tb->ptr)[tb->used] = u4_(atom); ++ tb->used; }
FI_ void tb_data(TapeBuilder* tb, U4 data) { u4_r(tb->ptr)[tb->used] = u4_(data); ++ tb->used; }
#define tb_emit_(atom) tb_emit(& tb, atom)
#define tb_data_(field, data) tb_data(& tb, u4_(data))
FI_ void tb_emit_bundle(TapeBuilder_R tb, Slice_MipsAtom atoms) { mem_copy(u4_(tb->ptr), u4_(atoms.ptr), S_slice(atoms)); tb->used += atoms.len; }
FI_ void tb_bind(TapeBuilder* tb, Slice data) { mem_copy(tb->ptr + tb->used * S_(MipsCode), u4_(data.ptr), data.len); tb->used += data.len / S_(MipsCode); }
#define tb_bind_(tb,type,...) tb_bind(tb, (Slice){ (B1*)(& (type){__VA_ARGS__}), S_(type) }); static_assert(S_(type) % S_(MipsCode) == 0)
// NOTE(Ed): Wip still ideating convention. Possibly will never use a composite.
#define tb_emit_wbind_(tb,atom,...) tb_emit(tb,atom); tb_bind_(tb,tmpl(Binds,atom),__VA_ARGS__)
#define tb_emit_wbind2_(tb,atom,type,...) tb_emit(tb,atom); tb_bind_(tb,type,__VA_ARGS__)
FI_ Tape tb_end (TapeBuilder* tb) { tb_emit(tb,tape_exit); return (Tape){ C_(U4*,tb->ptr), tb->used }; }
FI_ Tape tb_slice(TapeBuilder tb) { return (Tape){ C_(U4*,tb.ptr), tb.used }; }
@@ -377,10 +387,6 @@ FI_ Reg regfile__alloc_helper(A2_U2 file, Reg r_id) {
}
return result;
}
/* regfile_alloc picks the next free GPR from regfile_alloc_order.
* The table is the first-fit allocation order: T0..T7, V0..V1, A0..A3,
* S0..S7, T8..T9. The 24 entries leave room for the tape program to use
* any of them while R0, R1, R26-R31 remain reserved. */
I_ Reg regfile_alloc(RegFile_R rf) {
Reg allocated = 0;
for index_iter(U4, r_id, R_V0, <, R_T9) {
+2 -9
View File
@@ -4,10 +4,7 @@
# include "math.h"
#endif
/* PSX button bit positions — 1:1 with PSX-SPX docs at docs/psx-spx/docs/controllersandmemorycards.md:405-421.
* Wire is active-low (0 = pressed).
* The decoder atom computes buttons = (~raw_buttons) & 0xFFFF;
* active-low-to-active-high inversion is applied bit-by-bit. */
// PSX button bit positions: PSX-SPX docs/psx-spx/docs/controllersandmemorycards.md:405-421.
typedef Enum_(U2, PadBtns) {
Bit_(Pad_Select, 0),
Bit_(Pad_L3, 1),
@@ -62,11 +59,7 @@ typedef Enum_(U4, PadStatus) {
PadStatus_Invalid,
};
/* Distinct from the game-facing PadStatus enum: PadRawStatus_Ok and PadRawStatus_Timeout are raw BIOS values;
* PadStatus_* are game-facing post-decode states. PadUnknownId_Sentinel is written by the decoder
* when the controller id does not match any known controller type.
* PadAxisCentered_Word: Four-byte 0x80 pattern used to clear / center
* four byte axes at PadState.left_x through PadState.right_y. */
// Distinct from the game-facing PadStatus enum: PadRawStatus_Ok and PadRawStatus_Timeout are raw BIOS values
typedef Enum_(U1, PadRawStatus) {
PadRawStatus_Ok = 0x00,
PadRawStatus_Timeout = 0xFF,
+1 -1
View File
@@ -104,7 +104,7 @@ void gte_matrix_set_translation(MT3_S2S4* mat) asm("SetTransMatrix");
// Einheit, Metrication to unit vector. "Normalization", not Orthogonal "Normal, Normalis". Directionalization.
// RGA(Lengyel): Normalize the bulk of a zero-weight direction. This is not finite-point unitization (which forces w=1).
S4 normalize_v3s4(V3_S4* v0, V3_S4* v1) asm("VectorNormal");
S4 psy_normalize_v3s4(V3_S4* v0, V3_S4* v1) asm("VectorNormal");
// RGA(Lengyel): Apply the matrix expansion of a rigid transformation.
// Motor antiproduct is equivalent for unitized points; LA form is what GTE consumes.
+3 -3
View File
@@ -26,7 +26,7 @@ enum {
atom_offset_end_low_exit_stick = _atom_offset_end_low_exit_stick,
};
// --- atom: pad_input_cam (40 words) ---
// --- atom: pad_input_cam (39 words) ---
#define _atom_offset_left_x_exit_left_x 3
#define _atom_offset_right_x_exit_right_x 3
@@ -44,7 +44,7 @@ enum {
atom_offset_circle_z_exit_circle_z = _atom_offset_circle_z_exit_circle_z,
};
// --- atom: cube_g4_face (75 words) ---
// --- atom: cube_g4_face (73 words) ---
#define _atom_offset_cull_cube_g4_face_exit 41
#define _atom_offset_bounds_chk_cube_g4_face_exit 24
@@ -54,7 +54,7 @@ enum {
atom_offset_bounds_chk_cube_g4_face_exit = _atom_offset_bounds_chk_cube_g4_face_exit,
};
// --- atom: floor_f3_face (58 words) ---
// --- atom: floor_f3_face (56 words) ---
#define _atom_offset_culling_floor_f3_face_exit 25
#define _atom_offset_bounds_chk_floor_f3_face_exit 16
+76 -142
View File
@@ -92,11 +92,6 @@ MipsAtomComp_Proc_(ab, {
#pragma endregion MACs
#pragma region Atom Procs
// Modular Atoms
#define AtomBundle_(name) Struct_(tmpl(AtomBundle,name))
#define AtomBundle_Len(name) S_(tmpl(AtomBundle,name))/S_(MipsAtom*)
#define AtomBundleEntry_(bundle,entry) tmpl(bundle,entry)
#pragma region resolve_look_at
/* ─── resolve_look_at bundle chain atoms ──────────────────────────── */
@@ -108,7 +103,7 @@ typedef AtomBundle_(resolve_look_at) { MipsAtom
*normalize_right_ux,
*cross_to_up,
*normalize_up_uy,
*pop_mv_trans;
*populate_mt3s4s2;
};
typedef Struct_(ResolveLookAtScratch) {
@@ -123,71 +118,56 @@ typedef Struct_(ResolveLookAtScratch) {
V3_S4 up_in;
};
/* Binds_ResolveLookAtSub — what the C side pushes onto the tape before input_and_sub.
* The scratchpad base is no longer pushed because R_ScratchBase (= R_SP) is a tape carrier
* preserved across atoms; the atom body reads 0x1F800000 directly from R_SP. */
typedef Struct_(Binds_ResolveLookAt) {
MT3_S2S4* look_at;
P3_S4* eye;
P3_S4* target;
V3_S4* up_in;
};
typedef Struct_(Binds_ResolveLookAtSub) {
P3_S4* target;
P3_S4* eye;
V3_S4* up_in;
};
typedef Struct_(RegUse_resolve_look_at_input_and_sub) {
Reg target; Reg eye; Reg up_in;
Reg t0; Reg t1; Reg t2; Reg t3; Reg t4;
Reg target_ptr;
Reg eye_ptr;
Reg up_in_ptr;
union { Reg_(V3_S4) r012, up_in, eye; };
union { Reg_(V3_S4) r345, target, fwd; };
};
/* Atom 0 in the bundle: input_and_sub. Stages C-side inputs into the scratchpad and computes fwd = target - eye. */
internal MipsAtom* AtomBundleEntry_(resolve_look_at,input_and_sub)(AtomArena_R aa, RegUse_resolve_look_at_input_and_sub r)
atom_info(atom_bind(Binds_ResolveLookAtSub)) MipsAtom_Proc_(aa, {
load_word(r.target, R_TapePtr, O_(Binds_ResolveLookAtSub,target)),
load_word(r.eye, R_TapePtr, O_(Binds_ResolveLookAtSub,eye)),
load_word(r.up_in, R_TapePtr, O_(Binds_ResolveLookAtSub,up_in)),
load_word(r.target_ptr, R_TapePtr, O_(Binds_ResolveLookAtSub,target)),
load_word(r.eye_ptr, R_TapePtr, O_(Binds_ResolveLookAtSub,eye)),
load_word(r.up_in_ptr, R_TapePtr, O_(Binds_ResolveLookAtSub,up_in)),
LdSlot_ add_ui_self( R_TapePtr, S_(Binds_ResolveLookAtSub)),
/* Stage up_in.x/y/z into the scratchpad. R_ScratchBase = R_SP = 0x1F800000. */
mac_load_word_v3( r.t0, r.t1, r.t2, r.up_in, 0), LdSlot_
mac_store_word_v3(r.t0, r.t1, r.t2, R_ScratchBase, O_(ResolveLookAtScratch,up_in)),
mac_load_v3s4( r.up_in, r.up_in_ptr, 0), LdSlot_
mac_store_v3s4(r.up_in, R_ScratchBase, O_(ResolveLookAtScratch,up_in)),
// Stage eye.x/y/z into the scratchpad (atom 6 reads these for the translation column).
mac_load_word_v3( r.t0, r.t1, r.t2, r.eye, 0), LdSlot_
mac_store_word_v3(r.t0, r.t1, r.t2, R_ScratchBase, O_(ResolveLookAtScratch,eye)),
mac_load_v3s4( r.eye, r.eye_ptr, 0), LdSlot_
mac_store_v3s4(r.eye, R_ScratchBase, O_(ResolveLookAtScratch,eye)),
/* Compute fwd = target - eye. */
// mac_load_p3s4(t3, R_AT, t4, r.eye, 0),
mac_load_word_v3(r.t3, R_AT, r.t4, r.target, 0), LdSlot_
mac_sub_s_v3_self(
r.t3, R_AT, r.t4,
r.t0, r.t1, r.t2),
mac_store_word_v3(r.t3, R_AT, r.t4, R_ScratchBase, O_(ResolveLookAtScratch,fwd)),
mac_load_v3s4( r.target, r.target_ptr, 0), LdSlot_
mac_sub_v3s4_self(r.fwd, r.eye),
mac_store_v3s4( r.fwd, R_ScratchBase, O_(ResolveLookAtScratch,fwd)),
mac_yield()
})
typedef Struct_(Binds_ResolveLookAtPopMvTrans) {
U4 look_at; /* MT3_S2S4* — destination matrix address */
typedef Struct_(Binds_ResolveLookAt_PopulateMT3S4S2) {
MT3_S2S4* look_at; /* MT3_S2S4* — destination matrix address */
};
typedef Struct_(RegUse_resolve_look_at__pop_mv_trans) {
typedef Struct_(RegUse_resolve_look_at_populate_mt3s4s2) {
Reg look_at;
Reg_(V3_S4) row; /* populate phase: load ux/uy/uz */
union { Reg ux, v_x; } t6; /* populate addr (canonical) → matrix_vector v_x */
union { Reg uy, v_y; } t7; /* populate uy → matrix_vector v_y */
union { Reg uz, v_z; } t8; /* populate uz → matrix_vector v_z */
Reg eye; /* matrix_vector phase: load -eye */
Reg_(V3_S4) row; /* populate phase: load ux/uy/uz */
union { Reg r0, ux, vx; }; /* populate addr → matrix_vector v_x */
union { Reg r1, uy, vy; }; /* populate uy → matrix_vector v_y */
union { Reg r2, uz, vz; }; /* populate uz → matrix_vector v_z */
};
/* Atom 6 (fused): write look_at->m[][] from ux/uy/uz as packed S2 (populate),
/* write look_at->m[][] from ux/uy/uz as packed S2 (populate),
* ctc2 RT chain into C2[0..4] (matrix_vector), MVMVA RT*(-eye)>>12, store off
* directly to look_at->t[] (trans_matrix). Replaces the previous 3 separate atoms
* (populate + matrix_vector + trans_matrix).
*
* MT3_S2S4 { A3x3_S2 m; A3_S4 t; }
* m[][] is S2 packed (9 × 2 = 18 bytes at offset 0)
* t[0..2] is S4 (3 × 4 = 12 bytes at offset 18)
* directly to look_at->t[] (trans_matrix).
*
* C11 ApplyMatrixLV semantics (gte.atom.c ac_apply_matrix_lv; libgte reference):
* 1. ctc2 RT matrix (5 ctc2s to C2[0..4])
@@ -197,82 +177,51 @@ typedef Struct_(RegUse_resolve_look_at__pop_mv_trans) {
* 4. mtc2 to IR1/2/3, nop2, MVMVA pass2 (sf=1, mx=0, v=3, cv=3)
* 5. mfc2 MACs → off
* 6. store off to look_at->t[] (skip scratch.eye intermediate)
*
* GPR codes (assigned by resolve_look_at_init):
* r_scratch : R_ResolveScratch (R_T4 carrier)
* r_look_at : ralloc() — also serves as the off-dst in the trans_matrix phase
* r_row : V3_S4, reused for ux/uy/uz loads in populate phase
* r_eye : ralloc() — &scratch.eye, used for -eye load in matrix_vector phase
* r_v_x/v_y/v_z : ralloc() — populate scratch addrs (ux/uy/uz), reused as
* ctc2 transfer + MVMVA -eye temp in matrix_vector phase
* (v_x/v_y/v_z alias ux/uy/uz via the union; lifetime ends for ux/uy/uz after
* populate's mac_load_v3s4, so reusing for v.x/v.y/v.z is safe)
* Pool cost: 1 carrier + 1 look_at + 3 row + 1 eye + 3 aliased = 9 GPRs
*
* Net word savings vs the previous 3-atom flow: ~15 words + 2 mac_yields + 1 tape pop.
* - 2 mac_yields (trans_matrix's + matrix_vector's) → fused into one yield
* - 1 redundant tb_data (look_at was pushed 2x; now once)
* - mac_trans_mt3s3s4 (6 words) → replaced by direct mac_store_v3s4
* - mac_store_v3s4 to scratch.eye (3 words intermediate) → eliminated
* - add_si for r_off_ptr (2 words) → eliminated
* - mac_store_v3s4 zero-store of t[] (3 words) → eliminated (matrix_vector writes
* off directly; no consumer needed the zero first)
* - 1 set_gte_mt3s2s4 ctc2 chain (13 baked words) → eliminated (matrix_vector
* has its own ctc2 RT chain; cube rendering atoms reload C2 state themselves)
*/
internal MipsAtom* resolve_look_at__pop_mv_trans(AtomArena_R aa,
RegUse_resolve_look_at__pop_mv_trans r
) MipsAtom_Proc_(aa, {
internal MipsAtom* AtomBundleEntry_(resolve_look_at,populate_mt3s4s2)(AtomArena_R aa, RegUse_resolve_look_at_populate_mt3s4s2 r)
atom_info(atom_bind(Binds_ResolveLookAt_PopulateMT3S4S2)) MipsAtom_Proc_(aa, {
/* --- Tape pop: look_at pointer --- */
load_word(r.look_at, R_TapePtr, O_(Binds_ResolveLookAtPopMvTrans,look_at)),
LdSlot_ add_ui_self(R_TapePtr, S_(Binds_ResolveLookAtPopMvTrans)),
load_word(r.look_at, R_TapePtr, O_(Binds_ResolveLookAt_PopulateMT3S4S2,look_at)),
LdSlot_ add_ui_self( R_TapePtr, S_(Binds_ResolveLookAt_PopulateMT3S4S2)),
/* --- Scratch addresses for ux/uy/uz/eye (populate phase; t6/t7/t8 alias ux/uy/uz).
* R_ScratchBase (= R_SP) holds 0x1F800000; no per-atom bake is required because
* R_SP is a tape carrier preserved across atoms. --- */
add_si(r.t6.ux, R_ScratchBase, O_(ResolveLookAtScratch, ux)), LdSlot_
add_si(r.t7.uy, R_ScratchBase, O_(ResolveLookAtScratch, uy)),
add_si(r.t8.uz, R_ScratchBase, O_(ResolveLookAtScratch, uz)),
add_si(r.ux, R_ScratchBase, O_(ResolveLookAtScratch, ux)), LdSlot_
add_si(r.uy, R_ScratchBase, O_(ResolveLookAtScratch, uy)),
add_si(r.uz, R_ScratchBase, O_(ResolveLookAtScratch, uz)),
add_si(r.eye, R_ScratchBase, O_(ResolveLookAtScratch, eye)),
/* --- POPULATE phase: write look_at->m[][] from ux/uy/uz as packed S2 --- */
mac_load_v3s4(r.row, r.t6.ux, 0), LdSlot_ mac_store_v3s2(r.row, r.look_at, O_(MT3_S2S4, m[0])),
mac_load_v3s4(r.row, r.t7.uy, 0), LdSlot_ mac_store_v3s2(r.row, r.look_at, O_(MT3_S2S4, m[1])),
mac_load_v3s4(r.row, r.t8.uz, 0), LdSlot_ mac_store_v3s2(r.row, r.look_at, O_(MT3_S2S4, m[2])),
/* write look_at->m[][] from ux/uy/uz as packed S2 */
mac_load_v3s4(r.row, r.ux, 0), LdSlot_ mac_store_v3s2(r.row, r.look_at, O_(MT3_S2S4, m[0])),
mac_load_v3s4(r.row, r.uy, 0), LdSlot_ mac_store_v3s2(r.row, r.look_at, O_(MT3_S2S4, m[1])),
mac_load_v3s4(r.row, r.uz, 0), LdSlot_ mac_store_v3s2(r.row, r.look_at, O_(MT3_S2S4, m[2])),
/* --- MATRIX-VECTOR phase: ctc2 RT chain + MVMVA RT*(-eye)>>12 --- */
/* RT packing (per libgte ApplyMatrixLV convention; see gte.h:217-220 +
* atom_6b_disasm_comparison.md:28-32):
* C2[0] = (RT12<<16)|RT11 ← ctc2 RT11 from m[0][0..1] packed word
* C2[1] = (RT21<<16)|RT13 ← ctc2 RT12 from m[0][2..3] packed word
* C2[2] = (RT23<<16)|RT22 ← ctc2 RT13 from m[1][1..2] packed word
* C2[3] = (RT32<<16)|RT31 ← ctc2 RT21 from m[2][0..1] packed word
* C2[4] = (RT33<<16)|junk ← ctc2 RT22 from m[2][2] (half)
* Each ctc2 writes a WHOLE 32-bit C2 slot; the "macro name" identifies
* which C2 register, not which 16-bit half. */
load_word( r.t6.v_x, r.look_at, O_(MT3_S2S4, m[0][0])), /* RT11|RT12 */ LdSlot_
load_word( r.t7.v_y, r.look_at, O_(MT3_S2S4, m[0][2])), /* RT13|RT21 */ LdSlot_ gte_mv_to_ctrl_r(r.t6.v_x, gte_cr_RT11),
load_word( r.t8.v_z, r.look_at, O_(MT3_S2S4, m[1][1])), /* RT22|RT23 */ LdSlot_ gte_mv_to_ctrl_r(r.t7.v_y, gte_cr_RT12),
load_word( r.t6.v_x, r.look_at, O_(MT3_S2S4, m[2][0])), /* RT31|RT32 */ LdSlot_ gte_mv_to_ctrl_r(r.t8.v_z, gte_cr_RT13),
load_half_u(r.t7.v_y, r.look_at, O_(MT3_S2S4, m[2][2])), /* RT33 */ LdSlot_ gte_mv_to_ctrl_r(r.t6.v_x, gte_cr_RT21),
/* pos = -eye. The three loads also retire the last CTC2. */ gte_mv_to_ctrl_r(r.t7.v_y, gte_cr_RT22),
GteDelay_ mac_load_word_v3(r.t6.v_x, r.t7.v_y, r.t8.v_z, r.eye, 0), LdSlot_
mac_sub_s_v3(r.t6.v_x, r.t7.v_y, r.t8.v_z, R_0, R_0, R_0, r.t6.v_x, r.t7.v_y, r.t8.v_z),
/* mtc2 pos (as S16) to IR1/2/3. The GTE takes low 16 bits. pos fits in S16. For negative pos, the 32-bit sign-extended value's low 16 bits = correct S16. */
gte_mv_to_data_r(r.t6.v_x, C2_IR1),
gte_mv_to_data_r(r.t7.v_y, C2_IR2),
gte_mv_to_data_r(r.t8.v_z, C2_IR3),
/* ctc2 RT chain + MVMVA RT * (-eye) >> 12 */
/* C2[0] = (RT12<<16)|RT11 ← ctc2 RT11 from m[0][0..1]
* C2[1] = (RT21<<16)|RT13 ← ctc2 RT12 from m[0][2..3]
* C2[2] = (RT23<<16)|RT22 ← ctc2 RT13 from m[1][1..2]
* C2[3] = (RT32<<16)|RT31 ← ctc2 RT21 from m[2][0..1]
* C2[4] = (RT33<<16)|junk ← ctc2 RT22 from m[2][2] (half) */
load_word( r.vx, r.look_at, O_(MT3_S2S4, m[0][0])), /* RT11|RT12 */ LdSlot_
load_word( r.vy, r.look_at, O_(MT3_S2S4, m[0][2])), /* RT13|RT21 */ LdSlot_ gte_mv_to_ctrl_r(r.vx, gte_cr_RT11),
load_word( r.vz, r.look_at, O_(MT3_S2S4, m[1][1])), /* RT22|RT23 */ LdSlot_ gte_mv_to_ctrl_r(r.vy, gte_cr_RT12),
load_word( r.vx, r.look_at, O_(MT3_S2S4, m[2][0])), /* RT31|RT32 */ LdSlot_ gte_mv_to_ctrl_r(r.vz, gte_cr_RT13),
load_half_u(r.vy, r.look_at, O_(MT3_S2S4, m[2][2])), /* RT33 */ LdSlot_ gte_mv_to_ctrl_r(r.vx, gte_cr_RT21),
GteDelay_ mac_load_word_v3(r.vx, r.vy, r.vz, r.eye, 0), LdSlot_
mac_sub_s_v3(r.vx, r.vy, r.vz, R_0, R_0, R_0, r.vx, r.vy, r.vz),
gte_mv_to_data_r(r.vx, C2_IR1),
gte_mv_to_data_r(r.vy, C2_IR2),
gte_mv_to_data_r(r.vz, C2_IR3),
GteDelay_ nop2,
/* MVMVA pass 2 — C11 ApplyMatrixLV command.
* sf=1, mx=0 (RT), v=3 (IR), cv=3. Reads RT × IR >> 12. */
gte_cmdw_mvmva_c11_pass2, GteDelay_ nop,
mac_gte_mv_from_data_r_mac123(r.t6.v_x, r.t7.v_y, r.t8.v_z), GteDelay_ nop,
/* MVMVA pass 2 — C11 ApplyMatrixLV command. sf=1, mx=0 (RT), v=3 (IR), cv=3. Reads RT × IR >> 12. */
gte_cmdw_mvmva_c11_pass2, GteDelay_ load_word(R_AtomJmp, R_TapePtr, 0), // ac_yield: word 1
mac_gte_mv_from_data_r_mac123(r.vx, r.vy, r.vz), GteDelay_ add_ui_self( R_TapePtr, S_(MipsCode)), // ac_yield: word 2
/* --- TRANS-MATRIX phase: store off directly to look_at->t[] (skip scratch.eye intermediate) --- */
mac_store_word_v3(r.t6.v_x, r.t7.v_y, r.t8.v_z, r.look_at, O_(MT3_S2S4, t)),
/* store off directly to look_at->t[] (skip scratch.eye intermediate) */
mac_store_word_v3(r.vx, r.vy, r.vz, r.look_at, O_(MT3_S2S4, t)),
mac_yield()
jump_reg(R_AtomJmp), BdSlot_ nop, // ac_yield: word 3-4
})
#pragma endregion resolve_look_at
@@ -339,23 +288,6 @@ internal MipsAtom_(screen_env_init) atom_info(atom_phase(screen_init)
mac_yield(),
};
/* gp_screen_init's GPR setup. Tests the mixed user-pinning + auto-reg pattern:
* - R_IO_BaseAddr = R_T4 (user-pinned via atom_reg; pre-existing)
* - R_GP1_Offset = R_T2 (user-pinned via atom_reg; NEW -- for GPIO_PORT1_OFFSET)
* - R_ScreenX = R_T5 (user-pinned via atom_reg; used as a transfer and GTE setup reg)
* - R_GpTmp = auto-allocated by the lua pass and used for several GPU transfers;
* the C preprocessor resolves it to the chosen free pool GPR.
*
* For gp_screen_init, the auto-reg pool exclusions are:
* user_pinned (from the corpus register_alias_registry) : R_T0..R_T7 (all 8 user-pinned across hello_camera.atom.c)
* body-parsed physical registers : aliases resolve through the registry;
* the body uses R_ScreenX, not raw R_T5
* source_pool after both subtractions : {R_V0, R_V1} only
* R_GpTmp gets R_V0 (the first-fit choice). Its repeated GPU-transfer use proves that the
* auto-reg allocation is active while the R_ScreenX references prove the pinned alias is used.
* R_TapePtr (R_T9), R_AtomJmp (R_T8), R_AT are excluded from the POOL by construction in
* passes/auto_reg.lua -- see the "obvious exclusions" comment block at the top of that file.
*/
enum {
R_IO_BaseAddr = R_T4 atom_reg, /* Caller-pinned: IO_BASE_ADDR = 0x1F800000 */
R_GP1_Offset = R_T2 atom_reg, /* Caller-pinned: GPIO_PORT1_OFFSET = 0x10 */
@@ -522,7 +454,7 @@ internal MipsAtom_(pad_input_cam) atom_info(atom_bind(Binds_PadInputCam)
load_word(R_T1, R_Cam, O_(Camera,pos.x)),
// D-pad Left → cam.pos.x -= 50. and_i fulfills BD-slot for load on R_Cam.
LdSlot_ and_i(R_T3, R_T0, Pad_Left), branch_le_zero(R_T3, atom_offset(left_x, exit_left_x)), BdSlot_ mac_yield_load(), LdSlot_
LdSlot_ and_i(R_T3, R_T0, Pad_Left), branch_le_zero(R_T3, atom_offset(left_x, exit_left_x)), BdSlot_ nop,
add_si(R_T1, R_T1, -50), store_word(R_T1, R_Cam, O_(Camera,pos.x)),
atom_label(exit_left_x)
@@ -544,16 +476,16 @@ internal MipsAtom_(pad_input_cam) atom_info(atom_bind(Binds_PadInputCam)
/* D-pad Cross → cam.pos.z -= 50. Load pos.z BEFORE the andi. */
load_word(R_T1, R_Cam, O_(Camera,pos.z)), LdSlot_
and_i(R_T3, R_T0, Pad_Cross), branch_le_zero(R_T3, atom_offset(cross_z, exit_cross_z)), BdSlot_ nop,
and_i(R_T3, R_T0, Pad_Cross), branch_le_zero(R_T3, atom_offset(cross_z, exit_cross_z)), BdSlot_ load_word(R_AtomJmp, R_TapePtr, 0), LdSlot_ // ac_yield: word 1
add_si(R_T1, R_T1, -50), store_word(R_T1, R_Cam, O_(Camera,pos.z)),
atom_label(exit_cross_z)
/* D-pad Circle → cam.pos.z += 50. Reuses R_T1 from Cross. */
and_i(R_T3, R_T0, Pad_Circle), branch_le_zero(R_T3, atom_offset(circle_z, exit_circle_z)), BdSlot_ nop,
and_i(R_T3, R_T0, Pad_Circle), branch_le_zero(R_T3, atom_offset(circle_z, exit_circle_z)), BdSlot_ add_ui_self(R_TapePtr, S_(MipsCode)), // ac_yield: word 2
add_si(R_T1, R_T1, 50), store_word(R_T1, R_Cam, O_(Camera,pos.z)),
atom_label(exit_circle_z)
mac_yield_tail(),
jump_reg(R_AtomJmp), BdSlot_ nop // ac_yield: word 3-4
};
enum {
@@ -596,11 +528,13 @@ 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)),
LdSlot_ mac_gte_load_tri_verts(R_VertBase, R_T0, R_T1, R_T2), GteDelay_ load_half_u(R_T3, R_FaceCursor, 3 * S_(S2)), LdSlot_
GteDelay_ nop, gte_cmdw_rotate_translate_perspective_triple,
LdSlot_ mac_gte_load_tri_verts(R_VertBase, R_T0, R_T1, R_T2),
GteDelay_ load_half_u(R_T3, R_FaceCursor, 3 * S_(S2)), LdSlot_
GteDelay_ load_word(R_AtomJmp, R_TapePtr, 0), LdSlot_ //ac_yield: word 2,
gte_cmdw_rotate_translate_perspective_triple,
gte_cmdw_nclip,
gte_mv_from_data_r(R_T0, C2_MAC0), GteDelay_ nop,
gte_mv_from_data_r(R_T0, C2_MAC0), GteDelay_ add_ui_self(R_TapePtr, S_(MipsCode)), // ac_yield: word 1
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 —
@@ -632,7 +566,7 @@ MipsAtom_(cube_g4_face) atom_info(atom_phase(cube_g4),
atom_label(cube_g4_face_exit)
add_ui_self(R_PrimCursor, S_(Poly_G4)), /* 9 words = Poly_G4 */
add_ui_self(R_FaceCursor, S_(S2) * 4), /* 4 × S2 = 8 bytes */
mac_yield()
jump_reg(R_AtomJmp), BdSlot_ nop // ac_yield: word 3-4
};
typedef Struct_(Binds_FloorTri) {
@@ -662,13 +596,13 @@ MipsAtom_(floor_f3_face) atom_info(atom_phase(floor_f3)
, atom_writes(R_PrimCursor, R_FaceCursor)
) {
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
mac_gte_load_tri_verts(R_VertBase, R_T0, R_T1, R_T2), GteDelay_ nop2,
gte_cmdw_rotate_translate_perspective_triple, // 2 nops retire the final cpu -> gte writes before RTPT
gte_cmdw_nclip,
/* Culling (Branch forward if Backface) */
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.
gte_mv_from_data_r(R_T0, C2_MAC0), GteDelay_ load_word(R_AtomJmp, R_TapePtr, 0), // ac_yield: word 1
branch_le_zero(R_T0, atom_offset(culling, floor_f3_face_exit)), BdSlot_ add_ui_self(R_TapePtr, S_(MipsCode)), // ac_yield: word 2
/* Format Primitive */
mac_gte_store_f3(R_PrimCursor),
@@ -678,7 +612,7 @@ MipsAtom_(floor_f3_face) atom_info(atom_phase(floor_f3)
/* Bounds Check OTZ < 2048 (Branch forward to skip insertion) */
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,
branch_equal(R_AT, R_0, atom_offset(bounds_chk, floor_f3_face_exit)), BdSlot_ nop,
mac_format_f3_color(R_PrimCursor, 0xFF, 0xFF, 0xFF), // RGB-form (R=FF, G=FF, B=FF = white)
mac_insert_ot_tag(R_OtBase, R_PrimCursor, S_(Poly_F3)), /* Insert into Ordering Table Linked List */
add_ui_self(R_PrimCursor, S_(Poly_F3)), /* Advance Prim Cursor (5 words) */
@@ -689,7 +623,7 @@ MipsAtom_(floor_f3_face) atom_info(atom_phase(floor_f3)
/* Advance Input Cursor & Yield (Both branch targets land here) */
atom_label(floor_f3_face_exit)
add_ui_self(R_FaceCursor, S_(S2) * 4), /* Advance Face Cursor (4 * S2 = 8 bytes) */
mac_yield()
jump_reg(R_AtomJmp), BdSlot_ nop // ac_yield: word 3-4
};
typedef Struct_(Binds_SyncPrimitiveArena) { U4 used; U4 cursor; };
+63 -81
View File
@@ -80,9 +80,6 @@ typedef Struct_(SMemory) {
PadBiosRaw pad_raw[2];
PadState pad[2];
// TODO(Ed): We don't need this we can just cast at any point an address to a desired view of scratchpad, we have the address.
U4_V scratchpad; // d-cache
U1 ct_init_atom_mem[CT_InitAtomMem_Size];
MipsAtom* normalize_v3s4;
MipsAtom* gte_cross_v3s4;
@@ -114,10 +111,10 @@ I_ void resolve_look_at_c11(MT3_S2S4* look_at, P3_S4* eye, P3_S4* target, V3_S4*
V3_S4 pos, off;
forward = target[0]; sub_v3s4(& forward, eye[0]); // RGA(Lengyel): Affine point - point = zero-weight direction.
normalize_v3s4(& forward, & uz); // RGA(Lengyel): Normalize the direction bulk. Not finite-point unitization.
psy_normalize_v3s4(& forward, & uz); // RGA(Lengyel): Normalize the direction bulk. Not finite-point unitization.
cross_v3s4(& uz, up_in, & right); normalize_v3s4(& right, & ux); // RGA(Lengyel): Complement(Wedge(forward, up_in)) -> right axis.
cross_v3s4(& uz, & ux, & up); normalize_v3s4(& up, & uy); // RGA(Lengyel): Complement(Wedge(forward, right)) -> up axis.
cross_v3s4(& uz, up_in, & right); psy_normalize_v3s4(& right, & ux); // RGA(Lengyel): Complement(Wedge(forward, up_in)) -> right axis.
cross_v3s4(& uz, & ux, & up); psy_normalize_v3s4(& up, & uy); // RGA(Lengyel): Complement(Wedge(forward, right)) -> up axis.
// RGA(Lengyel): matrix expansion of the world-to-camera rotation (basis rows).
look_at->m[0][0] = ux.x; look_at->m[0][1] = ux.y; look_at->m[0][2] = ux.z;
@@ -149,15 +146,15 @@ internal void compile_init_atoms(void) {
});
regfile_reset(& rf);
smem.normalize_v3s4 = build_normalize_v3s4(& ab,
RegUse_(build_normalize_v3s4) {
smem.normalize_v3s4 = normalize_v3s4(& ab,
RegUse_(normalize_v3s4) {
.res = ralloc_v3(),
.t0 = ralloc(),
.t1 = ralloc(),
.t2 = ralloc(),
.t3 = ralloc(),
.t4 = ralloc(),
.t5 = ralloc(),
.r0 = ralloc(),
.r1 = ralloc(),
.r2 = ralloc(),
.r3 = ralloc(),
.r4 = ralloc(),
.r5 = ralloc(),
});
regfile_reset(& rf);
@@ -167,25 +164,19 @@ internal void compile_init_atoms(void) {
}
internal void compile_resolve_look_at(void) {
AtomArena ab = atomarena_make(slice_ut_arr(smem.resolve_look_at_mem));
AtomBundle_resolve_look_at_R bundle = C_(void*, smem.resolve_look_at_bundle);
/* R_ScratchBase (= R_SP) is a tape carrier preserved across atoms; no carrier
* pin is needed in the regfile. The standard 24-register pool is sufficient. */
AtomArena ab = atomarena_make(slice_ut_arr(smem.resolve_look_at_mem));
RegFile rf = regfile(regfile_abi_mask);
#define ralloc() regfile_alloc(& rf)
#define ralloc_v3() { ralloc(), ralloc(), ralloc() }
bundle->input_and_sub = AtomBundleEntry_(resolve_look_at, input_and_sub)(& ab,
RegUse_(resolve_look_at_input_and_sub) {
.target = ralloc(),
.eye = ralloc(),
.up_in = ralloc(),
.t0 = ralloc(),
.t1 = ralloc(),
.t2 = ralloc(),
.t3 = ralloc(),
.t4 = ralloc(),
.target_ptr = ralloc(),
.eye_ptr = ralloc(),
.up_in_ptr = ralloc(),
.up_in = ralloc_v3(),
.r012 = ralloc_v3(),
.r345 = {ralloc(), R_AT, ralloc() },
});
regfile_reset(& rf);
@@ -195,62 +186,58 @@ internal void compile_resolve_look_at(void) {
bundle->cross_to_up = smem.gte_cross_v3s4;
bundle->normalize_up_uy = smem.normalize_v3s4;
bundle->pop_mv_trans = resolve_look_at__pop_mv_trans(& ab,
RegUse_(resolve_look_at__pop_mv_trans){
bundle->populate_mt3s4s2 = AtomBundleEntry_(resolve_look_at,populate_mt3s4s2)(& ab,
RegUse_(resolve_look_at_populate_mt3s4s2){
.look_at = ralloc(),
.eye = ralloc(),
.row = ralloc_v3(),
.t6 = ralloc(),
.t7 = ralloc(),
.t8 = ralloc(),
.r0 = ralloc(),
.r1 = ralloc(),
.r2 = ralloc(),
});
/* Sanity check: arena didn't overflow. */
assert(ab.used <= ResolveLookAtArena_Size);
assert(ab.used <= ResolveLookAtArena_Size); // Sanity check: arena didn't overflow.
#undef ralloc
}
/* Emit the resolve_look_at bundle into the tape. Called once per frame from update(). */
I_ void resolve_look_at(TapeBuilder_R tb
, MT3_S2S4* look_at
, P3_S4* eye
, P3_S4* target
, V3_S4* up_in
){
// Emit the resolve_look_at bundle into the tape. Called once per frame from update().
I_ void resolve_look_at(TapeBuilder_R tb, MT3_S2S4* look_at, P3_S4* eye, P3_S4* target, V3_S4* up_in) {
/* Typed view of the scratchpad for field-address arithmetic. */
ResolveLookAtScratch* sp = C_scratch(ResolveLookAtScratch*);
AtomBundle_resolve_look_at_R bundle = C_(void*, smem.resolve_look_at_bundle);
tb_emit(tb, bundle->input_and_sub); tb_bind_(tb, Binds_ResolveLookAtSub,
.target = target,
.eye = eye,
.up_in = up_in,
);
tb_emit(tb, bundle->normalize_fwd_uz); tb_bind_(tb, Binds_normalize_v3s4,
.src_offset = O_(ResolveLookAtScratch,fwd),
.dst_offset = O_(ResolveLookAtScratch,uz),
);
tb_emit(tb, bundle->cross_to_right); tb_bind_(tb, Binds_gte_cross_v3s4,
.src_a = & sp->uz,
.src_b = & sp->up_in,
.out = & sp->right,
);
tb_emit(tb, bundle->normalize_right_ux); tb_bind_(tb, Binds_normalize_v3s4,
.src_offset = O_(ResolveLookAtScratch,right),
.dst_offset = O_(ResolveLookAtScratch,ux),
);
tb_emit(tb, bundle->cross_to_up); tb_bind_(tb, Binds_gte_cross_v3s4,
.src_a = & sp->uz,
.src_b = & sp->ux,
.out = & sp->up,
);
tb_emit(tb, bundle->normalize_up_uy); tb_bind_(tb, Binds_normalize_v3s4,
.src_offset = O_(ResolveLookAtScratch,up),
.dst_offset = O_(ResolveLookAtScratch,uy),
);
tb_emit(tb, bundle->populate_mt3s4s2); tb_bind_(tb, Binds_ResolveLookAt_PopulateMT3S4S2,
.look_at = look_at,
);
}
FI_ void camera_look_at(TapeBuilder_R tb, Camera* c, P3_S4* target, V3_S4* up_in) { resolve_look_at(tb, & c->look_at, & c->pos, target, up_in); }
tb_emit(tb, bundle->input_and_sub); {
tb_data(tb, u4_(target));
tb_data(tb, u4_(eye));
tb_data(tb, u4_(up_in));
}
tb_emit(tb, bundle->normalize_fwd_uz); {
tb_data(tb, u4_(O_(ResolveLookAtScratch, fwd) | (O_(ResolveLookAtScratch, uz) << 16)));
}
tb_emit(tb, bundle->cross_to_right); {
tb_data(tb, u4_(& sp->uz));
tb_data(tb, u4_(& sp->up_in));
tb_data(tb, u4_(& sp->right));
}
tb_emit(tb, bundle->normalize_right_ux); {
tb_data(tb, u4_(O_(ResolveLookAtScratch, right) | (O_(ResolveLookAtScratch, ux) << 16)));
}
tb_emit(tb, bundle->cross_to_up); {
tb_data(tb, u4_(& sp->uz));
tb_data(tb, u4_(& sp->ux));
tb_data(tb, u4_(& sp->up));
}
tb_emit(tb, bundle->normalize_up_uy); {
tb_data(tb, u4_(O_(ResolveLookAtScratch, up) | (O_(ResolveLookAtScratch, uy) << 16)));
}
tb_emit(tb, bundle->pop_mv_trans); {
tb_data(tb, u4_(look_at));
}
}
GCC_OPTIMIZATION_DISABLE
void update(PrimitiveArena* pa, U4* ordering_buf)
{
TapeBuilder tb = tb_make(slice_ut_arr(smem.MemTape));
@@ -260,15 +247,15 @@ void update(PrimitiveArena* pa, U4* ordering_buf)
tb.used = 0; tb_scope_run(& tb) {
// Grab latest state from bios.
tb_emit_(pad_bios_snapshot);
tb_data_(raw, & smem.pad_raw[0]);
tb_data_(state, & smem.pad[0]);
tb_data(& tb, u4_(& smem.pad_raw[0]));
tb_data(& tb, u4_(& smem.pad[0]));
// tb_emit_(pad_bios_snapshot);
// tb_data_(raw, & smem.pad_raw[1]);
// tb_data_(state, & smem.pad[1]);
tb_emit_(pad_input_cam);
tb_data_(state, & smem.pad[0]);
tb_data_(cam, & smem.cam);
tb_data(& tb, u4_(& smem.pad[0]));
tb_data(& tb, u4_(& smem.cam));
// tb_emit_(pad_input_cube_rotation);
// tb_data_(state, & smem.pad[0]);
@@ -301,7 +288,7 @@ void update(PrimitiveArena* pa, U4* ordering_buf)
if (use_c11_path == false)
{
tb.used = 0; tb_scope_run(& tb) {
resolve_look_at(& tb, & smem.cam.look_at, & smem.cam.pos, & smem.cube.pos, & v3s4(0, -fp_one, 0));
camera_look_at(& tb, & smem.cam, & smem.cube.pos, & v3s4(0, -fp_one, 0));
}
}
@@ -384,7 +371,6 @@ void update(PrimitiveArena* pa, U4* ordering_buf)
// smem.floor.rot.y += 5;
}
}
GCC_OPTIMIZATION_ENABLE
void render(void) {
}
@@ -401,12 +387,9 @@ void gp_display_frame(DoubleBuffer* screen_buf, S4* active_buf_id, U4* ordering_
active_buf_id[0] = ! active_buf_id[0]; // Swap current buffer
}
GCC_OPTIMIZATION_DISABLE
int main(void)
{
smem = (SMemory){0};
// TODO(Ed): remove this field we don't need it in smem.
smem.scratchpad = C_(U4_V, Scratchpad_Loc);
// smem.primitives.used = 0;
// smem.active_buf_id = 0;
smem.cam.pos = v3s4(500, -1000, -1500);
@@ -451,4 +434,3 @@ int main(void)
};
return 0;
}
GCC_OPTIMIZATION_ENABLE
+4 -6
View File
@@ -31,9 +31,8 @@ local M = {} ---@type DuffleExport
--- @field binds_by_name table<string, BindsEntry>
--- @field atoms_by_name table<AtomName, AtomEntry>
--- @field atom_infos AtomInfoEntry[]
--- @field components table<string, ComponentDef>
--- @field components table<string, Component>
--- @field component_atom_infos AtomInfoEntry[]|nil
--- @field component_body_index table<string, ComponentBodyEntry>
--- @field tape_chains table<string, TapeChain>|nil
--- @field source_order SourceFile[]
--- @field collisions CorpusCollision[]
@@ -70,7 +69,6 @@ function M.corpus_view(ctx)
atom_infos = corpus.atom_infos or {},
components = corpus.components or {},
component_atom_infos = corpus.component_atom_infos or {},
component_body_index = corpus.component_body_index or {},
tape_chains = corpus.tape_chains or {},
source_order = corpus.source_order or {},
collisions = corpus.collisions or {},
@@ -80,12 +78,12 @@ end
--- @param rules CheckRule[]
--- @param phase string
--- @param item AtomEntry|SourceFile
--- @param pipe_ctx PipeCtx
--- @param findings CheckFinding[]
--- @param pipe_ctx PassScratch
--- @param findings Finding[]
--- @return nil
function M.run_check_rules(rules, phase, item, pipe_ctx, findings)
for _, rule in ipairs(rules) do ---@type integer, CheckRule
local fn = rule[phase] ---@type (fun(item: AtomEntry|SourceFile, pipe_ctx: PipeCtx, findings: CheckFinding[]): nil)|nil
local fn = rule[phase] ---@type (fun(item: AtomEntry|SourceFile, pipe_ctx: PassScratch, findings: Finding[]): nil)|nil
if fn then fn(item, pipe_ctx, findings) end
end
end
+69 -69
View File
@@ -11,20 +11,9 @@ for k, v in pairs(isa) do M[k] = v end ---@type string, any
-- Shared, memoized helpers: a single emitted-word event stream that every downstream pass reads from,
-- built once from the pre-tokenized bodies.
--- @class ComponentBodyEntry
--- @field body_tokens BodyToken[]
--- @field body_off integer -- byte offset of body[1] in `source`
--- @field line_of fun(pos:integer):integer -- byte-offset → 1-based line number in `source`
--- @field source string -- absolute path of the source containing the declaration
--- @field declaration integer -- 1-based line number of the MipsAtomComp_(ac_X) declaration
--- @field kind string -- "comp_bare" | "comp_proc"
--- @field arg_names string[]|nil
--- @field sub_map table<string, string>|nil -- bag: formal -> substituted operand
--- @class EmissionWalkCtx
--- @field component_index table<string, ComponentBodyEntry>
--- @field components table<string, Component>
--- @field word_counts WordCounts
--- @field components table<string, ComponentDef>
--- @field reg_use_schema RegUseSchema|nil
--- @field reg_use_param string|nil
--- @field atom_name AtomName|nil
@@ -39,10 +28,12 @@ for k, v in pairs(isa) do M[k] = v end ---@type string, any
--- @field atom_name AtomName|nil
--- @field schema_name string|nil
-- Finding: see ps1_meta.lua
--- @class DuffleEmit
-- The cross-source component-body index is owned by the corpus (`corpus.component_body_index`, populated by `passes/components.lua`).
-- The cross-source component row is owned by the corpus (`corpus.components`, populated by `passes/components.lua`).
-- Consumers (`passes/static_analysis.lua`, `passes/emission_model.lua`) read it directly; per-pass memoization helpers stay out of scope.
-- ASCII byte constants used by split_call_args (kept local to keep Section 8 self-contained).
@@ -119,18 +110,18 @@ local E_MAC_PREFIX_LEN = 4 ---@type integer
--- * Known `mac_X(...)` calls: Recursively expand the indexed component body, including nested components. Every event from the expansion carries:
--- - `source` / `line` = the COMPONENT'S source path + the line of the token within the component body (i.e. "definition site").
--- - `call_source` / `call_line` = the ROOT atom's source path + call-site line, PRESERVED across recursion so nested events still point at the original root.
--- * Unknown `mac_X` (not in `component_index`): fall back to `word_counts[ident]` if present; otherwise emit one opaque event so the cycle budget accounts for the word.
--- * Unknown `mac_X` (not in `components`): fall back to `word_counts[ident]` if present; otherwise emit one opaque event so the cycle budget accounts for the word.
--- * Marker Tokens (`atom_label(...)` / `atom_offset(...)`): Zero events (they are pure metaprogram hints).
---
--- Cycle protection: a per-expansion `visiting` set tracks components currently on the expansion stack;
--- a re-entry produces a deterministic `{kind = "cycle", ...}` error and aborts that branch (does NOT hang, does NOT recurse).
--- a re-entry produces a deterministic `{check = "cycle", ...}` error and aborts that branch (does NOT hang, does NOT recurse).
---
--- Pure: reads `body_entry` / `component_index` / `word_counts`. Memoization is the caller's responsibility.
--- Pure: reads `body_entry` / `components` / `word_counts`. Memoization is the caller's responsibility.
--- Callers wanting `word_events` / `word_event_errors` precomputed for many atoms should memoize them per atom.
--- @param body_entry ComponentBodyEntry
--- @param component_index table<string, ComponentBodyEntry>
--- @param body_entry Component
--- @param components table<string, Component>
--- @param word_counts WordCounts
--- @return WordEvent[], EmitError[]
--- @return WordEvent[], Finding[]
-- ════════════════════════════════════════════════════════════════════════════
-- Section 11: project_emission (per-atom emission projection)
@@ -141,7 +132,7 @@ local E_MAC_PREFIX_LEN = 4 ---@type integer
-- The items stream is the single ordered source of truth; `word_events` and `markers` are dense views over it.
--
-- The helper below operates on a body string (not a body_entry) so the pass can call it without depending on the older SourceScan / body_off conventions.
-- component_index argument is reserved for recursive component expansion.
-- `components` is the recursive expansion map (`corpus.components`).
-- word_counts table is authored-metadata + current-component count table.
--- @class EmissionProjection
@@ -149,8 +140,8 @@ local E_MAC_PREFIX_LEN = 4 ---@type integer
--- @field word_events WordEvent[]
--- @field markers EmissionMarker[]
--- @field invocations InvocationRecord[] -- dense view of items where kind == "invoke_begin"|"invoke_end"
--- @field errors EmitError[]
--- @field warnings EmitWarning[]
--- @field errors Finding[]
--- @field warnings Finding[]
--- @class InvocationRecord
--- Lives at `atom.paths.invocations[*]`. Constructed once at the single invocation-construction site
@@ -171,7 +162,7 @@ local E_MAC_PREFIX_LEN = 4 ---@type integer
--- @field end_word integer -- 1-based items index of the `invoke_end` item (set by `emit_invoke_end`)
--- @field word_count integer -- Number of `word` items emitted between `start_word` and `end_word` (inclusive)
--- @field debug_skip boolean -- `debug_skip` stamp; true iff `corpus.components[name].debug_skip` is true at construction. Always boolean (never `nil`).
--- @field errors EmitError[]
--- @field errors Finding[]
-- Internal recursive walker. The items stream holds every emitted event in order; `word_events`, `markers`,
-- `invocations`, `errors`, `warnings` are dense views / side outputs appended alongside.
@@ -187,9 +178,9 @@ local E_MAC_PREFIX_LEN = 4 ---@type integer
-- * Unknown uncounted macros emit one opaque word + one warning. Unknown metadata-backed macros (entry in `word_counts`) emit the declared word count, no warning.
-- * Cycle detection uses an active DFS stack (`visiting`); a cycle appends a construction error to BOTH the projection errors and the cycle invocation's own errors,
-- then breaks out without recursing (the cycle entry still receives an invocation ID + paired `invoke_begin` / `invoke_end` items, so the boundary invariant is preserved).
-- * Component declared-count mismatch (declared vs. measured) is a construction error (kind = "count_mismatch"); recorded on the invocation record and pass-level errors list.
-- * Component declared-count mismatch (declared vs. measured) is a construction error (check = "count_mismatch"); recorded on the invocation record and pass-level errors list.
-- * Final boundary check: if any invocation is still open at end of walk, surface a "unbalanced" construction error.
--- @param root_body_entry ComponentBodyEntry
--- @param root_body_entry Component
--- @param ctx_table EmissionWalkCtx
--- @return EmissionProjection
local function _project_emission_inner(root_body_entry, ctx_table)
@@ -197,8 +188,8 @@ local function _project_emission_inner(root_body_entry, ctx_table)
local word_events = {} ---@type WordEvent[]
local markers = {} ---@type EmissionMarker[]
local invocations = {} ---@type InvocationRecord[]
local errors = {} ---@type EmitError[]
local warnings = {} ---@type EmitWarning[]
local errors = {} ---@type Finding[]
local warnings = {} ---@type Finding[]
local word_idx = 0 ---@type integer
local invocation_stack = {} ---@type InvocationRecord[] -- stack of currently-open invocation records
@@ -282,7 +273,8 @@ local function _project_emission_inner(root_body_entry, ctx_table)
gpr_keys[pos] = key
if unresolved then
errors[#errors + 1] = {
kind = "reguse_unresolved",
kind = "error",
check = "reguse_unresolved",
line = line,
msg = string.format("RegUse operand %q does not resolve in schema %q",
effective, (reg_use_schema and reg_use_schema.name) or "?"),
@@ -294,7 +286,8 @@ local function _project_emission_inner(root_body_entry, ctx_table)
for _, wpos in ipairs(row.writes) do ---@type integer, integer
if wpos == pos then
errors[#errors + 1] = {
kind = "reguse_const_write",
kind = "error",
check = "reguse_const_write",
line = line,
msg = string.format("RegUse slot %q is Reg const; %s writes it",
slot, encoder),
@@ -541,15 +534,14 @@ local function _project_emission_inner(root_body_entry, ctx_table)
-- The stamp is resolved from the `corpus.components[name]` registry (passed in via `ctx_table.components` by `emission_model.run`),
-- Unmarked components stamp `false` (not `nil`) so consumers can dispatch on the boolean without nil checks.
--
-- The walker has already found the component body in `ctx_table.component_index[component_name]`, so the matching entry MUST exist in `ctx_table.components[component_name]`
-- (both registries are populated from the same source by the components pass).
-- The walker has already found the component body in `ctx_table.components[component_name]`.
-- A missing entry is a corpus-plumbing bug; we fail loudly here rather than silently stamp `false` and mask the regression.
local components = ctx_table.components ---@type table<string, ComponentDef>|nil
local component_def = components and components[component_name] or nil ---@type ComponentDef|nil
local components = ctx_table.components ---@type table<string, Component>|nil
local component_def = components and components[component_name] or nil ---@type Component|nil
if not component_def then
error("duffle.emit_invoke_begin: component " .. string.format("%q", component_name)
.. " is present in `component_index` (the walker matched a `mac_" .. component_name .. "()` call) but absent from `components` (the canonical corpus.components registry). "
.. "This is a corpus-plumbing bug — the components pass must populate corpus.components[name] for every component it puts in corpus.component_body_index[name]. "
.. " is present in the walk (the walker matched a `mac_" .. component_name .. "()` call) but absent from `components` (the canonical corpus.components registry). "
.. "This is a corpus-plumbing bug — the components pass must populate corpus.components[name] for every expanded component. "
.. "The emission pass refuses to silently stamp `debug_skip = false` for a missing registry entry."
, 0
)
@@ -621,9 +613,10 @@ local function _project_emission_inner(root_body_entry, ctx_table)
local canon = M.gte_canon(ident) ---@type string
if canon ~= ident and wc and wc[canon] then return wc[canon] end
warnings[#warnings + 1] = {
kind = "uncounted",
kind = "warning",
check = "uncounted",
line = tok_line,
msg = string.format("project_emission: opaque word emitted for %q (no entry in word_counts or component_index)",
msg = string.format("project_emission: opaque word emitted for %q (no entry in word_counts or components)",
ident),
}
return 1
@@ -634,19 +627,19 @@ local function _project_emission_inner(root_body_entry, ctx_table)
-- walk_root_call_text: Outermost `mac_X(...)` token text (preserved across recursion).
-- walk_immediate_call_text: IMMEDIATE outer `mac_X(...)` token text for words emitted in this body — nil for the root atom body.
-- Two trackers are propagated as separate parameters so words deep inside nested expansions correctly identify both their immediate call site and the outermost call site.
--- @param body_entry ComponentBodyEntry
--- @param body_entry Component
--- @param walk_parent_inv_id integer
--- @param walk_root_call_text string|nil
--- @param walk_immediate_call_text string|nil
--- @param sub_map table<string, string>|nil
--- @return nil
local function walk_body_entry(body_entry, walk_parent_inv_id,
walk_root_call_text, walk_immediate_call_text)
walk_root_call_text, walk_immediate_call_text, sub_map)
local tokens = body_entry.body_tokens or {} ---@type BodyToken[]
local body_off = body_entry.body_off or 0 ---@type integer
local line_of = body_entry.line_of or M.LineIndex("") ---@type LineIndexFn
local def_source = body_entry.source or "" ---@type string
local def_line = body_entry.declaration or 0 ---@type integer
local sub_map = body_entry.sub_map ---@type table<string, string>|nil
local def_line = body_entry.line or 0 ---@type integer
-- Per-token dispatch: each matched branch returns; only the fall-through
-- "opaque word" emit handles direct encoders + mac_X-without-component.
--- @param bt BodyToken
@@ -716,7 +709,7 @@ local function _project_emission_inner(root_body_entry, ctx_table)
end
if ident:sub(1, 4) == "mac_" then
local bare = ident:sub(5) ---@type string
local comp = ctx_table.component_index[bare] ---@type ComponentBodyEntry|nil
local comp = ctx_table.components[bare] ---@type Component|nil
if comp then
local invocation_root_call_text = walk_root_call_text or tok ---@type string
if ctx_table.visiting[bare] then
@@ -724,8 +717,9 @@ local function _project_emission_inner(root_body_entry, ctx_table)
local inv = emit_invoke_begin(comp.kind or "comp_bare", bare, tok, invocation_root_call_text, def_source, tok_line) ---@type InvocationRecord
inv.parent_id = walk_parent_inv_id
inv.call_text = tok
local err = { ---@type EmitError
kind = "cycle",
local err = { ---@type Finding
kind = "error",
check = "cycle",
msg = string.format("project_emission: component cycle detected: %q", bare),
source = def_source,
line = tok_line,
@@ -741,12 +735,12 @@ local function _project_emission_inner(root_body_entry, ctx_table)
inv.parent_id = walk_parent_inv_id
inv.call_text = tok
inv.def_path = comp.source
inv.def_line = comp.declaration
inv.def_line = comp.line
-- Propagate trackers into the recursive walk:
-- immediate_call_text = this call's tok (the IMMEDIATE outer call for words emitted in this body)
-- root_call_text = the OUTERMOST call (immutable across the recursion)
local formal_names = ctx_table.component_index[bare] ---@type string[]|nil
and ctx_table.component_index[bare].arg_names
-- child_map = per-invocation formal substitution; stays on the walk stack
local formal_names = comp.arg_names ---@type string[]|nil
local child_map = nil ---@type table<string, string>|nil
if formal_names then
child_map = {}
@@ -754,17 +748,11 @@ local function _project_emission_inner(root_body_entry, ctx_table)
child_map[fname] = apply_sub(sub_map, args[i])
end
end
walk_body_entry({
body_tokens = comp.body_tokens or {},
body_off = comp.body_off or 0,
line_of = comp.line_of,
source = comp.source,
declaration = comp.declaration,
sub_map = child_map,
},
walk_body_entry(comp,
inv.id,
invocation_root_call_text,
tok)
tok,
child_map)
ctx_table.visiting[bare] = nil
emit_invoke_end(inv)
-- Count `word` items inside [start_word, end_word].
@@ -780,8 +768,9 @@ local function _project_emission_inner(root_body_entry, ctx_table)
-- We compare against the measured word count.
local declared = ctx_table.word_counts["mac_" .. bare] ---@type integer|nil
if declared and wc_inside ~= declared then
local err = { ---@type EmitError
kind = "count_mismatch",
local err = { ---@type Finding
kind = "error",
check = "count_mismatch",
msg = string.format("project_emission: mac_%s declared=%d measured=%d", bare, declared, wc_inside),
source = def_source,
line = tok_line,
@@ -791,7 +780,7 @@ local function _project_emission_inner(root_body_entry, ctx_table)
end
return
end
-- mac_X NOT in component_index: fall through to opaque emit.
-- mac_X NOT in components: fall through to opaque emit.
end
-- Direct encoder, or mac_X-without-component: resolve count + emit n words.
-- Resolve_count may emit a warning if the count is unresolved.
@@ -816,13 +805,14 @@ local function _project_emission_inner(root_body_entry, ctx_table)
-- Walk first; the pass caller stamps the root call site for direct words after the projection returns.
-- For nested words the def_path / def_line already point at the component source and MUST be preserved (the stamping helper checks for that).
walk_body_entry(root_body_entry, 0, nil, nil)
walk_body_entry(root_body_entry, 0, nil, nil, nil)
-- Boundary check: every invoke_begin must have a matching invoke_end.
-- If anything is still open, surface a hard error.
if #invocation_stack > 0 then
errors[#errors + 1] = {
kind = "unbalanced",
kind = "error",
check = "unbalanced",
msg = string.format("project_emission: invocation boundaries not balanced (%d unclosed invocation(s) at end of walk)", #invocation_stack),
}
end
@@ -850,7 +840,7 @@ end
--- * `mac_X(...)` calls: emit `invoke_begin` (zero-width), recurse into the component body, emit `invoke_end` (zero-width).
--- The component body's words land between the begin/end pair; one invocation record is allocated per call (monotonic ID per atom).
--- * Unknown uncounted macros emit 1 opaque word + one warning per occurrence.
--- * Tokens whose count cannot be resolved (e.g. `mac_unknown` not in word_counts and not in component_index) surface one
--- * Tokens whose count cannot be resolved (e.g. `mac_unknown` not in word_counts and not in components) surface one
--- warning; cycle + count-mismatch + boundary violations are construction errors on `pass.errors`.
---
--- Every emitted `word` carries: `i` (0-based word index), `encoder`, `args` (top-level args), `def_path`, `def_line`,
@@ -860,15 +850,15 @@ end
--- for the open invocation stack at that word.
---
--- @param body_text string -- the raw atom body string
--- @param component_index table<string, ComponentBodyEntry>
--- @param component_index table<string, Component>
--- @param word_counts WordCounts
--- @param components table<string, ComponentDef>
--- @param components table<string, Component>
--- `invocation.debug_skip`. A missing or non-table `components` raises a fail-loud error rather than silently falling back.
--- @return EmissionProjection
--- @param reg_use_ctx RegUseCtx|nil
function M.project_emission(body_text, component_index, word_counts, components, reg_use_ctx)
-- The recursive walk delegates to `_project_emission_inner` so component bodies (which arrive as
-- `{body_tokens, body_off, line_of, source, declaration}` records from `corpus.component_body_index`)
-- The recursive walk delegates to `_project_emission_inner` so component bodies
-- (the `corpus.components` row: body_tokens / body_off / line_of / source / line)
-- re-enter the same walker with the same shared output state.
--
-- The walker is body-relative: it builds `line_of` from `body_text` and stamps body-relative line numbers (1..N)
@@ -898,17 +888,17 @@ function M.project_emission(body_text, component_index, word_counts, components,
end
local tokens = M.tokenize_body(body_text) ---@type BodyToken[]
local comps = components or component_index or {} ---@type table<string, Component>
return _project_emission_inner({
body_tokens = tokens,
body_off = 0,
line_of = M.LineIndex(body_text),
source = "",
declaration = 0,
line = 0,
},
{
component_index = component_index or {},
components = comps,
word_counts = word_counts or {},
components = components,
reg_use_schema = reg_use_ctx and reg_use_ctx.reg_use_schema,
reg_use_param = reg_use_ctx and reg_use_ctx.reg_use_param,
atom_name = reg_use_ctx and reg_use_ctx.atom_name,
@@ -1051,6 +1041,16 @@ function M.find_atom_proc_decl_for(source, before_pos, mips_atom_ptr_len)
if source:sub(next_pos, next_pos) == "(" then
local inner, after_paren = M.read_parens(source, next_pos) ---@type string|nil, integer
if inner then
if ident == "AtomBundleEntry_" then
local tmpl = M.split_top_level_commas(inner) ---@type string[]
if #tmpl ~= 2 then return nil end
local name = M.trim(tmpl[1]) .. "_" .. M.trim(tmpl[2]) ---@type string
local formals_pos = M.skip_ws_and_cmt(source, after_paren) ---@type integer
if source:sub(formals_pos, formals_pos) ~= "(" then return nil end
local real_inner, after_real = M.read_parens(source, formals_pos) ---@type string|nil, integer
if not real_inner then return nil end
return name, real_inner, name, after_real
end
return ident, inner, ident, after_paren
end
end
+44
View File
@@ -88,7 +88,14 @@
--- @field visibility_kind string
--- @field evidence HardwareRelationEvidence
--- @class GprRole
--- @field name string
--- @field pool boolean
--- @field optional boolean
--- @field carrier boolean
--- @class DuffleIsa
--- @field GPR_ROLE table<string, GprRole>
--- @field TAPE_ATOM_MACROS table<string, TapeAtomMacroRow>
--- @field DELAY_MARKERS table<string, boolean>
--- @field INSTRUCTION table<string, InstructionRow>
@@ -111,6 +118,43 @@ local M = {} ---@type DuffleIsa
-- Section 7: domain tables
-- ════════════════════════════════════════════════════════════════════════════
-- One GprRole row per name. Construction order is the auto_reg pool order,
-- then R_AT, then the three carriers. Index by name into M.GPR_ROLE.
--- @type table<string, GprRole>
M.GPR_ROLE = {
{ name = "R_V0", pool = true, optional = true, carrier = false },
{ name = "R_V1", pool = true, optional = true, carrier = false },
{ name = "R_T0", pool = true, optional = true, carrier = false },
{ name = "R_T1", pool = true, optional = true, carrier = false },
{ name = "R_T2", pool = true, optional = true, carrier = false },
{ name = "R_T3", pool = true, optional = true, carrier = false },
{ name = "R_T4", pool = true, optional = true, carrier = false },
{ name = "R_T5", pool = true, optional = true, carrier = false },
{ name = "R_T6", pool = true, optional = true, carrier = false },
{ name = "R_T7", pool = true, optional = true, carrier = false },
{ name = "R_A0", pool = true, optional = true, carrier = false },
{ name = "R_A1", pool = true, optional = true, carrier = false },
{ name = "R_A2", pool = true, optional = true, carrier = false },
{ name = "R_A3", pool = true, optional = true, carrier = false },
{ name = "R_S0", pool = true, optional = true, carrier = false },
{ name = "R_S1", pool = true, optional = true, carrier = false },
{ name = "R_S2", pool = true, optional = true, carrier = false },
{ name = "R_S3", pool = true, optional = true, carrier = false },
{ name = "R_S4", pool = true, optional = true, carrier = false },
{ name = "R_S5", pool = true, optional = true, carrier = false },
{ name = "R_S6", pool = true, optional = true, carrier = false },
{ name = "R_S7", pool = true, optional = true, carrier = false },
{ name = "R_T8", pool = true, optional = true, carrier = false },
{ name = "R_T9", pool = true, optional = true, carrier = false },
{ name = "R_AT", pool = false, optional = true, carrier = false },
{ name = "R_TapePtr", pool = false, optional = true, carrier = true },
{ name = "R_AtomJmp", pool = false, optional = true, carrier = true },
{ name = "R_ScratchBase", pool = false, optional = true, carrier = true },
}
for _, row in ipairs(M.GPR_ROLE) do ---@type integer, GprRole
M.GPR_ROLE[row.name] = row
end
-- atom_info sub-calls: atom_bind, atom_reads, atom_writes, atom_view, atom_reg_types, atom_ctx, atom_phase.
--- @type table<string, TapeAtomMacroRow>
M.TAPE_ATOM_MACROS = {
+59
View File
@@ -59,6 +59,10 @@
--- @field unity_root Path
--- @field project_root Path
--- @class ExactResolveOptions
--- @field sources string[]
--- @field project_root Path
--- @alias LineIndexFn fun(query_pos: integer): integer
--- @class DuffleScan
@@ -914,6 +918,61 @@ function M.resolve_source_corpus(options)
}
end
--- Exact `--source` corpus. No include expansion. Paths stay as given (normalized).
--- Same return shape as resolve_source_corpus.
--- @param options ExactResolveOptions
--- @return Corpus
function M.resolve_exact_sources(options)
if type(options) ~= "table" then error("resolve_exact_sources requires options", 2) end
if type(options.project_root) ~= "string" or options.project_root == "" then
error("resolve_exact_sources requires options.project_root", 2)
end
local sources = options.sources ---@type string[]|nil
if type(sources) ~= "table" then error("resolve_exact_sources requires options.sources", 2) end
local project_root = options.project_root ---@type Path
local code_root = M.normalize_path(project_root .. "/code") ---@type Path
local source_order = {} ---@type SourceFile[]
local sources_by_path = {} ---@type table<Path, SourceFile>
local resolver = { ---@type SourceResolver
resolved = {},
skipped = {},
shadowed = {},
}
for _, input_path in ipairs(sources) do ---@type integer, string
local path = M.normalize_path(input_path) ---@type string
M.canonical_path_key(path)
local source = { ---@type SourceFile
path = path,
text = M.read_file(path),
dir = M.dirname(path),
basename = M.basename_no_ext(path),
}
source_order[#source_order + 1] = source
local key = M.canonical_path_key(path) ---@type string
if not sources_by_path[key] then sources_by_path[key] = source end
resolver.resolved[#resolver.resolved + 1] = {
include_path = path,
include_text = nil,
root_source = path,
root_line = 1,
candidate_a = path,
candidate_b = nil,
selected_path = path,
disposition = "exact",
}
end
return {
unity_root = nil,
project_root = project_root,
code_root = code_root,
source_order = source_order,
sources_by_path = sources_by_path,
sources_by_dir = M.group_sources_by_dir(source_order),
resolver = resolver,
}
end
-- Split a brace-body into top-level comma-separated tokens. Honors nested parens/braces/brackets and skips strings/comments.
-- Splits at top-level NEWLINES and SEMICOLONS too, AND emits a token break after a top-level comment/string.
-- Pure-comment / pure-string chunks contribute 0 words.
+36 -58
View File
@@ -21,16 +21,8 @@ local duffle = dofile(_bootstrap_dir .. "../duffle_paths.lua")
-- Type declarations
-- ════════════════════════════════════════════════════════════════════════════
-- SourceFile, PassCtx, PassResult, PassShared, Corpus: see ps1_meta.lua
-- SourceScan, AtomEntry, BindsEntry, RegTypeDefault, AtomViewEntry: see scan_source.lua
--- @class AtomAnnotation
--- @field atom_name string -- Atom name (scan.atom_infos row)
--- @field info_line integer -- Source line of the atom_info call
--- @field binds string|nil -- Binds_X name if any
--- @field reads string[] -- R_* names (read targets)
--- @field writes string[] -- R_* names (write targets)
--- @field errors string[]|nil -- Parse-time errors from scan_source (atom_info body malformed)
-- SourceFile, PassCtx, PassResult, PassShared, Corpus, Finding: see ps1_meta.lua
-- SourceScan, AtomEntry, AtomInfoEntry, BindsEntry, RegTypeDefault, AtomViewEntry: see scan_source.lua
--- @class RegTypeOccurrence
--- @field reg string
@@ -38,44 +30,30 @@ local duffle = dofile(_bootstrap_dir .. "../duffle_paths.lua")
--- @field source_line integer
--- @class Findings
--- @field errors PassFinding[]
--- @field warnings PassFinding[]
--- @field info PassFinding[]
--- @field errors Finding[]
--- @field warnings Finding[]
--- @field info Finding[]
--- @class PipeCtx
--- @field atom_index table<string, AtomEntry> -- raw_name or name -> scan.atoms row (kind atom/atom_proc)
--- @field binds_index table<string, BindsEntry>
--- @field annot_counts table<string, integer> -- bag: atom name -> annotation count
--- @field types table<string, RegTypeDefault>
--- @field atom_views table<string, AtomViewEntry>
--- @field seen_defaults table<string, integer> -- bag: register ident -> occurrence count
--- @field seen_field table<string, integer> -- bag: leftover field-count slot
--- @field _scan SourceScan
--- @field word_counts WordCounts|nil
--- @field register_alias_registry table<string, AliasEntry>|nil
--- @field type_name_registry table<string, TypeNameEntry>|nil
--- @field type_occurrences RegTypeOccurrence[]|nil
--- @field atom_infos_list AtomInfoEntry[]|nil
--- @field binds_list BindsEntry[]|nil
-- PassScratch: see ps1_meta.lua
--- @class AnnotatedResult
--- @field atoms AtomEntry[]
--- @field annots AtomAnnotation[]
--- @field annots AtomInfoEntry[]
--- @field macros MacroEntry[]
--- @field binds BindsEntry[]
--- @field errors PassFinding[]
--- @field warnings PassFinding[]
--- @field info PassFinding[]
--- @field errors Finding[]
--- @field warnings Finding[]
--- @field info Finding[]
--- @field source string|nil
--- @class CheckRule
--- @field per_annot (fun(item: AtomAnnotation, pipe_ctx: PipeCtx, findings: Findings): nil)|nil
--- @field per_annot (fun(item: AtomInfoEntry, pipe_ctx: PassScratch, findings: Findings): nil)|nil
--- @class SourceScan
--- @field type_occurrences RegTypeOccurrence[]|nil
--- @class AnnotationPass
--- @field validate fun(ctx: PassCtx, src: SourceFile, corpus_pipe_ctx: PipeCtx|nil): AnnotatedResult
--- @field validate fun(ctx: PassCtx, src: SourceFile, corpus_pipe_ctx: PassScratch|nil): AnnotatedResult
--- @field run fun(ctx: PassCtx): PassResult
-- ════════════════════════════════════════════════════════════════════════════
@@ -85,8 +63,8 @@ local duffle = dofile(_bootstrap_dir .. "../duffle_paths.lua")
--- `macro_word_drift` writes errors[] for missing or mismatched metadata and info[] for a match.
--- Check: Every annotated atom must have a matching MipsAtom_(name) declaration.
--- @param info AtomAnnotation
--- @param pipe_ctx PipeCtx
--- @param info AtomInfoEntry
--- @param pipe_ctx PassScratch
--- @param findings Findings
--- @return nil
local function check_atom_decl_exists(info, pipe_ctx, findings)
@@ -100,8 +78,8 @@ end
--- Check: Every atom may have AT MOST ONE annotation.
--- Post-loop: Needs full-corpus `annot_counts` from pipe_ctx.
--- @param _item AtomAnnotation|nil
--- @param pipe_ctx PipeCtx
--- @param _item AtomInfoEntry|nil
--- @param pipe_ctx PassScratch
--- @param findings Findings
--- @return nil
local function check_unique_annotation(_item, pipe_ctx, findings)
@@ -117,8 +95,8 @@ end
--- Check: BIND atoms must reference a real Binds_* struct.
--- I keep this as a warning so the annotation pass can report the common test-fixture case; `check_abi_handoff` in static analysis supplies the build-stopping error.
--- @param info AtomAnnotation
--- @param pipe_ctx PipeCtx
--- @param info AtomInfoEntry
--- @param pipe_ctx PassScratch
--- @param findings Findings
--- @return nil
local function check_binds_struct_exists(info, pipe_ctx, findings)
@@ -135,7 +113,7 @@ end
--- Check: TAPE_WORDS(mac_X, N) ↔ WORD_COUNT(mac_X, N) drift.
--- Three outcomes: missing (error), mismatch (error), match (info).
--- @param m MacroEntry
--- @param pipe_ctx PipeCtx
--- @param pipe_ctx PassScratch
--- @param findings Findings
--- @return nil
local function check_macro_word_drift(m, pipe_ctx, findings)
@@ -164,7 +142,7 @@ end
--- Check: atom_dbg_reg_default(R_X, <type>) targets an alias in `pipe_ctx.register_alias_registry` and a type in `pipe_ctx.type_name_registry`.
--- Pointer depth remains bounded to 0 or 1, and duplicate defaults remain errors.
--- @param _src SourceFile -- unused (kept for the per_source shape)
--- @param pipe_ctx PipeCtx
--- @param pipe_ctx PassScratch
--- @param findings Findings
--- @return nil
local function check_semantic_reg_defaults(_src, pipe_ctx, findings)
@@ -215,7 +193,7 @@ end
--- Check: atom_reg_types(R_X, <type>) entries target an alias in `pipe_ctx.register_alias_registry` and a type in `pipe_ctx.type_name_registry`.
--- A bare `atom_reg` marker opts the `R_<n>` alias into GPR identity; references to R_T0..R_T3 require the same explicit marker.
--- @param _src SourceFile
--- @param pipe_ctx PipeCtx
--- @param pipe_ctx PassScratch
--- @param findings Findings
--- @return nil
local function check_atom_reg_types(_src, pipe_ctx, findings)
@@ -247,7 +225,7 @@ end
--- Check: atom_view(Binds_X) entries reference a Binds_* struct with at least one field.
--- @param _src SourceFile
--- @param pipe_ctx PipeCtx
--- @param pipe_ctx PassScratch
--- @param findings Findings
--- @return nil
local function check_atom_view_layout(_src, pipe_ctx, findings)
@@ -277,7 +255,7 @@ end
--- Check: Binds_* structs require unique field names because atom_view uses those names for typed-field lookup in gdb.
--- @param _src SourceFile
--- @param pipe_ctx PipeCtx
--- @param pipe_ctx PassScratch
--- @param findings Findings
--- @return nil
local function check_binds_no_duplicate_fields(_src, pipe_ctx, findings)
@@ -310,7 +288,7 @@ end
--- 6. unsupported target_kind -> marker precedes an unrelated declaration
--- Valid markers stamp `debug_skip` on whole-atom, bare-component, and proc-component declaration records in scan_source.lua.
--- @param marker DebugSkipMarker
--- @param _pipe_ctx PipeCtx -- Unused; kept for consistency with per_annot
--- @param _pipe_ctx PassScratch -- Unused; kept for consistency with per_annot
--- @param findings Findings
--- @return nil
local function check_skip_marker(marker, _pipe_ctx, findings)
@@ -368,7 +346,7 @@ end
--- Warn when a source references an unregistered alias.
--- When a source uses an unregistered R_X, this check emits one pass-level info entry for that source and directs C-ABI register names to explicit alias registration.
--- @param _src SourceFile
--- @param pipe_ctx PipeCtx
--- @param pipe_ctx PassScratch
--- @param findings Findings
--- @return nil
local function check_wave_context_migration(_src, pipe_ctx, findings)
@@ -425,9 +403,9 @@ local CHECK_RULES = { ---@type CheckRule[]
--- Builds one pass-wide pipe_ctx from the merged `corpus.*` registries and source-ordered `corpus.atom_infos`; per-source declarations and bodies remain in `src.scan`.
--- The module ownership contract above requires callers to construct `ctx.shared.corpus` through `build_ctx`; the error message below enforces that gate.
--- @param ctx PassCtx
--- @return PipeCtx
--- @return PassScratch
local function build_corpus_pipe_ctx(ctx)
local view = duffle.corpus_view(ctx) ---@type PipeCtx
local view = duffle.corpus_view(ctx) ---@type PassScratch
local annot_counts = {} ---@type table<string, integer> -- bag: atom name -> annotation count
for _, info in ipairs(view.atom_infos) do ---@type integer, AtomInfoEntry
if info and info.atom_name then
@@ -443,7 +421,7 @@ end
--- Validate one source against its pre-scanned SourceScan payload + the corpus-wide pipe_ctx.
--- @param ctx PassCtx
--- @param src SourceFile
--- @param corpus_pipe_ctx PipeCtx|nil -- Built once per pass from corpus registries; nil builds the same projection here.
--- @param corpus_pipe_ctx PassScratch|nil -- Built once per pass from corpus registries; nil builds the same projection here.
--- @return AnnotatedResult
local function validate(ctx, src, corpus_pipe_ctx)
corpus_pipe_ctx = corpus_pipe_ctx or build_corpus_pipe_ctx(ctx)
@@ -453,7 +431,7 @@ local function validate(ctx, src, corpus_pipe_ctx)
local seen_defaults = {}; for reg, _ in pairs (scan.types or {}) do seen_defaults[reg] = (seen_defaults[reg] or 0) + 1 end ---@type table<string, integer> -- bag: register ident -> occurrence count
local atom_infos_list = {}; for _, ai in ipairs(scan.atom_infos or {}) do atom_infos_list[#atom_infos_list + 1] = ai end ---@type AtomInfoEntry[]
local pipe_ctx = { ---@type PipeCtx
local pipe_ctx = { ---@type PassScratch
atom_index = {},
binds_index = {},
annot_counts = corpus_pipe_ctx.annot_counts,
@@ -548,12 +526,12 @@ M.validate = validate
--- @return PassResult
function M.run(ctx)
local outputs = {} ---@type PassOutputEntry[]
local errors = {} ---@type PassFinding[]
local warnings = {} ---@type PassFinding[]
local errors = {} ---@type Finding[]
local warnings = {} ---@type Finding[]
-- Build the shared pipe_ctx once for this run; every validate() call sees the same cross-source registries.
-- The corpus owns the canonical cross-source registries; per-source scans retain body / declaration ownership.
local corpus_pipe_ctx = build_corpus_pipe_ctx(ctx) ---@type PipeCtx
local corpus_pipe_ctx = build_corpus_pipe_ctx(ctx) ---@type PassScratch
local corpus = ctx.shared.corpus ---@type Corpus
-- Group `corpus.sources_by_dir` by module, validate every source in each bucket, and emit one errors.h per directory.
@@ -562,17 +540,17 @@ function M.run(ctx)
for dir, dir_sources in pairs(by_dir) do ---@type string, SourceFile[]
local dir_basename = dir:match("([^/\\]+)$") or dir ---@type string
local dir_atoms = 0 ---@type integer
local dir_errors = {} ---@type PassFinding[]
local dir_warnings = {} ---@type PassFinding[]
local dir_errors = {} ---@type Finding[]
local dir_warnings = {} ---@type Finding[]
for _, src in ipairs(dir_sources) do ---@type integer, SourceFile
local result = validate(ctx, src, corpus_pipe_ctx) ---@type AnnotatedResult
result.source = src.path -- tag for downstream rendering
dir_atoms = dir_atoms + #result.atoms
for _, e in ipairs(result.errors) do ---@type integer, PassFinding
for _, e in ipairs(result.errors) do ---@type integer, Finding
dir_errors[#dir_errors + 1] = { line = e.line, msg = e.msg, source = src.path }
errors [#errors + 1] = { line = e.line, msg = e.msg }
end
for _, w in ipairs(result.warnings) do ---@type integer, PassFinding
for _, w in ipairs(result.warnings) do ---@type integer, Finding
dir_warnings[#dir_warnings + 1] = { line = w.line, msg = w.msg }
warnings [#warnings + 1] = { line = w.line, msg = w.msg }
end
+2 -2
View File
@@ -595,8 +595,8 @@ end
--- @return PassResult
function M.run(ctx)
local outputs = {} ---@type PassOutputEntry[]
local errors = {} ---@type PassFinding[]
local warnings = {} ---@type PassFinding[]
local errors = {} ---@type Finding[]
local warnings = {} ---@type Finding[]
local corpus = ctx.shared and ctx.shared.corpus ---@type Corpus|nil
if type(corpus) ~= "table" or type(corpus.source_order) ~= "table" then
+15 -17
View File
@@ -28,8 +28,8 @@
--- @class AutoRegResult
--- @field outputs AutoRegOutput[]
--- @field errors PassFinding[]
--- @field warnings PassFinding[]
--- @field errors Finding[]
--- @field warnings Finding[]
--- @class AutoRegPass
--- @field run fun(ctx: PassCtx): AutoRegResult
@@ -37,6 +37,7 @@
local _bootstrap_dir = debug.getinfo(1, "S").source:match("^@?(.*[/\\])") or "./" ---@type string
local duffle = dofile(_bootstrap_dir .. "../duffle_paths.lua") ---@type DuffleExport
local isa = require("duffle_isa") ---@type DuffleIsa
--- ════════════════════════════════════════════════════════════════════════════
--- THE GPR ALLOCATION POOL — what's allocatable, and (more importantly) WHY
@@ -54,15 +55,12 @@ local duffle = dofile(_bootstrap_dir .. "../duffle_paths.lua")
--- R_K0/K1 (codes 26-27) — Kernel / interrupt handler reserves. Never touched by user code.
--- R_GP/SP/FP/RA (codes 28-31) — R_SP/R_FP/R_RA are tape-runtime carriers between tape_enter and tape_exit; R_GP stays the host global pointer.
---
local POOL = { ---@type GprIdent[]
"R_V0", "R_V1",
"R_T0", "R_T1", "R_T2", "R_T3",
"R_T4", "R_T5", "R_T6", "R_T7",
"R_A0", "R_A1", "R_A2", "R_A3",
"R_S0", "R_S1", "R_S2", "R_S3",
"R_S4", "R_S5", "R_S6", "R_S7",
"R_T8", "R_T9",
}
local POOL = {} ---@type GprIdent[]
for _, row in ipairs(isa.GPR_ROLE) do ---@type integer, GprRole
if row.pool then
POOL[#POOL + 1] = row.name
end
end
-- Map from integer MIPS GPR code (the `code` field on AliasEntry) to the physical GPR ident in POOL.
-- The standard MIPS O32 ABI register numbering matches mips.h's R_*_Code #defines (mips.h).
@@ -94,7 +92,7 @@ end
--- @param phase_label string
--- @param decls table<string, string> -- bag: auto-reg symbol -> decl payload
--- @return GprAllocMap
--- @return PassFinding[]
--- @return Finding[]
local function allocate_phase(phase_label, decls)
-- Deep-copy POOL into a fresh sequence table. The original `table.unpack and table.unpack(POOL) or { unpack(POOL) }`
-- idiom wraps the unpacked values in a single inner table under LuaJIT 5.1 (`table.unpack` is nil; the `or` returns one value),
@@ -102,7 +100,7 @@ local function allocate_phase(phase_label, decls)
local pool = {} ---@type GprIdent[]
for i = 1, #POOL do pool[i] = POOL[i] end ---@type integer
local result = {} ---@type GprAllocMap
local errors = {} ---@type PassFinding[]
local errors = {} ---@type Finding[]
for _, sym in ipairs(stable_sort_keys(decls)) do ---@type integer, string
local next_gpr = table.remove(pool, 1) ---@type GprIdent|nil
if not next_gpr then
@@ -226,8 +224,8 @@ local M = {} ---@type AutoRegPass
--- @return AutoRegResult
function M.run(ctx)
local outputs = {} ---@type AutoRegOutput[]
local errors = {} ---@type PassFinding[]
local warnings = {} ---@type PassFinding[]
local errors = {} ---@type Finding[]
local warnings = {} ---@type Finding[]
local corpus = ctx.shared and ctx.shared.corpus ---@type Corpus|nil
if type(corpus) ~= "table" then
@@ -244,12 +242,12 @@ function M.run(ctx)
-- 1. Allocate phase pools first (phase declarations take precedence over per-atom declarations).
local phase_allocations = {} ---@type table<string, GprAllocMap> -- bag: phase_label -> alloc map
for phase_label, decls in pairs(corpus.phase_auto_regs or {}) do ---@type string, table<string, string>
local mapping, errs = allocate_phase(phase_label, decls) ---@type GprAllocMap, PassFinding[]
local mapping, errs = allocate_phase(phase_label, decls) ---@type GprAllocMap, Finding[]
for sym, gpr in pairs(mapping) do ---@type string, GprIdent
phase_allocations[phase_label] = phase_allocations[phase_label] or {}
phase_allocations[phase_label][sym] = gpr
end
for _, e in ipairs(errs) do ---@type integer, PassFinding
for _, e in ipairs(errs) do ---@type integer, Finding
errors[#errors + 1] = e
end
end
+23 -54
View File
@@ -1,6 +1,6 @@
--- passes/components.lua — Component-macro header generator.
---
--- Ownership: `corpus.word_counts`, `corpus.components`, and `corpus.component_body_index`.
--- Ownership: `corpus.word_counts` and `corpus.components`.
--- Scanner owns `declaration_comment` and `debug_skip` on each declaration record; this pass projects both forward.
---
--- Reads the pre-scanned SourceScan payload from `duffle.scan_source` for `MipsAtomComp_(ac_X)` and `MipsAtomComp_Proc_(ac_X, { body })` declarations (kind="comp_bare" / "comp_proc"),
@@ -56,7 +56,6 @@ local MACS_FILENAME = "macs.h" ---@type string
-- SourceScan, AtomEntry, CorpusCollision, CollisionSite: see scan_source.lua
-- BodyToken: see emission_model.lua
-- WordCounts: see word_count_eval.lua
-- ComponentBodyEntry: see duffle_emit.lua
-- InstructionRow, GteCommandRow: see duffle_isa.lua
--- @class Component
@@ -70,6 +69,11 @@ local MACS_FILENAME = "macs.h" ---@type string
--- @field comment string|nil -- Scanner-owned `declaration_comment`; the components pass reads it from the scanner record
--- @field kind string -- "comp_bare" | "comp_proc" (atom_proc is NOT a component — see `project_components`)
--- @field debug_skip boolean -- Mirror of `a.debug_skip` (scanner-owned); true iff a bare `atom_dbg_skip` marker immediately preceded the declaration
--- @field path string|nil -- Slash-normalized source path (collision sites)
--- @field source string|nil -- Absolute source path (emit)
--- @field line_of (fun(pos: integer): integer)|nil
--- @field cycle_cost integer|nil -- From metadata[c.name]; nil when the body was not costed
--- @field gp0_contrib integer|nil -- From metadata[c.name]; nil when the body was not costed
--- @class ComponentMeta
--- @field cycle_cost integer
@@ -800,16 +804,7 @@ local function update_canonical_word_counts(corpus, components, counts)
end
end
--- @class ComponentDef
--- @field name string -- Bare name (without ac_/mac_ prefix)
--- @field line integer -- Definition source line (line of `MipsAtomComp_(ac_X)` / `MipsAtomComp_Proc_(ac_X, ...)`)
--- @field path string -- Absolute source path of the definition
--- @field kind string -- "comp_bare" | "comp_proc" (atom_proc is NOT a component)
--- @field debug_skip boolean -- Mirror of the scanner-owned `a.debug_skip`; consumers read this directly
--- @field cycle_cost integer|nil -- From metadata[c.name]; nil when the body was not costed
--- @field gp0_contrib integer|nil -- From metadata[c.name]; nil when the body was not costed
--- (internal) Populate `corpus.components` with this source's components-by-name map.
--- (internal) Populate `corpus.components` with this source's one component row per bare name.
--- First declaration wins; later declarations of the same bare name are dropped and recorded as a collision via `corpus.collisions` (kind = "component").
--- The pass does NOT write to `ctx.shared.components`.
--- No parallel skip map is built here; consumers that need the per-component skip state read `corpus.components[name].debug_skip` directly.
@@ -818,28 +813,29 @@ end
--- @param src SourceFile
--- @param components Component[]
--- @param metadata ComponentMetaMap
--- @param scan SourceScan
--- @return nil
local function update_canonical_components(corpus, src, components, metadata)
local function update_canonical_components(corpus, src, components, metadata, scan)
local rel_path = src.path:gsub("\\", "/") ---@type string
local line_of = scan and scan.line_of ---@type (fun(pos: integer): integer)|nil
for _, c in ipairs(components) do ---@type integer, Component
-- Keyed by bare name (e.g. `yield`, `load_tri_indices`).
-- The atoms_source_map pass looks up components by bare name from the corpus;
-- `mac_` prefix lives at the call-site identifier and is stripped before lookup.
local m = metadata and metadata[c.name] or nil ---@type ComponentMeta|nil
if corpus.components[c.name] == nil then
corpus.components[c.name] = {
name = c.name,
line = c.line,
path = rel_path,
kind = c.kind or "comp_bare",
debug_skip = c.debug_skip == true,
cycle_cost = m and m.cycle_cost or nil,
gp0_contrib = m and m.gp0_contrib or nil,
}
c.path = rel_path
c.source = src.path
c.line_of = line_of
c.kind = c.kind or "comp_bare"
c.debug_skip = c.debug_skip == true
c.cycle_cost = m and m.cycle_cost or nil
c.gp0_contrib = m and m.gp0_contrib or nil
corpus.components[c.name] = c
else
-- A second declaration of the same bare name: record a typed collision so static-analysis + the report can surface it.
-- Identical-shape declarations (same path + line) reuse the first-wins entry without a collision record.
local existing = corpus.components[c.name] ---@type ComponentDef
local existing = corpus.components[c.name] ---@type Component
if existing.path ~= rel_path or existing.line ~= c.line then
local kind = c.kind or "comp_bare" ---@type string
local first_kind = existing.kind or "comp_bare" ---@type string
@@ -856,37 +852,12 @@ local function update_canonical_components(corpus, src, components, metadata)
end
end
--- (internal) Populate `corpus.component_body_index` with this source's body index entries.
--- First declaration wins; later declarations are dropped (no separate collision record: the components collision is already surfaced by `update_canonical_components`).
--- The pass writes to `corpus.component_body_index` only (the corpus owns this projection).
--- @param corpus Corpus
--- @param src SourceFile
--- @param components Component[]
--- @param scan SourceScan
--- @return nil
local function update_canonical_component_body_index(corpus, src, components, scan)
local line_of = scan and scan.line_of ---@type (fun(pos: integer): integer)|nil
for _, c in ipairs(components) do ---@type integer, Component
if corpus.component_body_index[c.name] == nil then
corpus.component_body_index[c.name] = {
body_tokens = c.body_tokens,
body_off = c.body_off,
line_of = line_of,
source = src.path,
declaration = c.line,
kind = c.kind,
arg_names = c.arg_names,
}
end
end
end
--- @param ctx PassCtx
--- @return PassResult
function M.run(ctx)
local outputs = {} ---@type MacsOutput[]
local errors = {} ---@type PassFinding[]
local warnings = {} ---@type PassFinding[]
local errors = {} ---@type Finding[]
local warnings = {} ---@type Finding[]
-- Corpus ownership gate.
local corpus = ctx.shared and ctx.shared.corpus ---@type Corpus|nil
@@ -904,8 +875,7 @@ function M.run(ctx)
-- Projection ownership:
-- * `corpus.word_counts["mac_"..name]` — current component count
-- * `corpus.components[name]` — bare-name component definition
-- * `corpus.component_body_index[name]` — body / line_of / source index
-- * `corpus.components[name]` — one row: body, line_of, source, cost
-- The pass writes to the corpus only; consumers read from the corpus directly.
-- Per-directory aggregation: every source in the same directory contributes to one `gen/macs.h`.
@@ -937,8 +907,7 @@ function M.run(ctx)
for _, src in ipairs(sources) do ---@type integer, SourceFile
local per_source = project_components(src.text, src.scan) or {} ---@type Component[]
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)
update_canonical_components(corpus, src, per_source, metadata_per_source[src], src.scan)
end
end
end
+1 -1
View File
@@ -450,7 +450,7 @@ local DEFAULT_BASENAME = "hello_gte" ---@type string
--- DieSchema / FORM_WRITERS / atom-table records live on this file.
--- SourceFile, Path, AtomName: see duffle.lua.
--- Corpus, PassCtx, PassResult, PassFlags, PassShared, PassFinding: see ps1_meta.lua.
--- Corpus, PassCtx, PassResult, PassFlags, PassShared, Finding: see ps1_meta.lua.
--- AtomEntry, AliasEntry, TypeNameEntry, TypeField, AtomInfoEntry, BindsEntry,
--- AtomViewEntry, AtomCtxEntry, AtomPhaseGroup, SourceScan, RegTypeOverride: see scan_source.lua.
--- AtomPaths, WordEvent, BodyToken: see emission_model.lua.
+32 -29
View File
@@ -1,7 +1,7 @@
--- passes/emission_model.lua: Per-atom emission projection.
---
--- The `emission-model` pass owns `atom.paths`, the canonical per-atom mutable surface for atoms and raw atoms with bodies in `ctx.shared.corpus.source_order`.
--- For each atom, the pass invokes `duffle.project_emission(body_text, component_index, word_counts, components)`.
--- For each atom, the pass invokes `duffle.project_emission(body_text, components, word_counts, components)`.
--- It stores the ordered `items` stream plus the dense `word_events` / `markers` / `invocations` views on `atom.paths`.
---
--- Public boundary:
@@ -92,17 +92,7 @@
--- @field consuming_encoder string|nil
--- @field consuming_arg_pos integer|nil
--- @class EmitError
--- @field kind string
--- @field line integer|nil
--- @field msg string
--- @field source string|nil
--- @field schema_name string|nil
--- @class EmitWarning
--- @field kind string
--- @field line integer|nil
--- @field msg string
-- Finding: see ps1_meta.lua
--- @class AtomPaths
--- @field tokens BodyToken[]
@@ -111,8 +101,8 @@
--- @field word_events WordEvent[]
--- @field markers EmissionMarker[]
--- @field invocations InvocationRecord[]
--- @field errors EmitError[]
--- @field warnings EmitWarning[]
--- @field errors Finding[]
--- @field warnings Finding[]
--- @class EmissionModelPass
--- @field run fun(ctx: PassCtx): PassResult
@@ -136,7 +126,7 @@ local duffle = dofile(_bootstrap_dir .. "../duffle_paths.lua")
-- * 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.
-- * INNER invocations (`inv.parent_id ~= 0`) receive physical `call_line` values directly from the COMPONENT's `line_of` in the walker.
-- Recursive descent forwards that closure through `corpus.component_body_index[name].line_of`; those values arrive physical and remain unchanged.
-- Recursive descent forwards that closure through `corpus.components[name].line_of`; those values arrive physical and remain unchanged.
--
-- After this function, every `inv.call_line` is physical. DWARF and provenance output read it directly.
-- The word-event loop forwards the already-physical `outer_inv.call_line` into `we.call_line` for words inside an invocation.
@@ -155,7 +145,7 @@ local function stamp_root_provenance(projection, atom_record, src, corpus)
-- 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 ---@type integer
local component_index = corpus.component_body_index or {} ---@type table<string, ComponentBodyEntry>
local components = corpus.components or {} ---@type table<string, Component>
local word_items = {} ---@type EmissionItem[]
for _, item in ipairs(projection.items) do ---@type integer, EmissionItem
@@ -176,7 +166,7 @@ local function stamp_root_provenance(projection, atom_record, src, corpus)
local inner_id = ids[#ids] ---@type integer
local inner_inv = inner_id and projection.invocations[inner_id] ---@type InvocationRecord|nil
if inner_inv then
local component = component_index[inner_inv.component_name] ---@type ComponentBodyEntry|nil
local component = components[inner_inv.component_name] ---@type Component|nil
if component and component.line_of then
-- Walker used `comp.line_of`, which is the source's physical LineIndex. item.line is already physical.
return item.line or 0
@@ -257,13 +247,13 @@ end
local function project_atom(atom_record, src, corpus)
local body = atom_record.body or "" ---@type string
local wc = corpus.word_counts or {} ---@type WordCounts
local cbi = corpus.component_body_index or {} ---@type table<string, ComponentBodyEntry>
local comps = corpus.components or {} ---@type table<string, Component>
local schema = nil ---@type RegUseSchema|nil
if atom_record.reg_use_schema_name then
schema = corpus.reg_use_schemas and corpus.reg_use_schemas[atom_record.reg_use_schema_name]
end
-- That construction site stamps `invocation.debug_skip` while appending each record to `proj.invocations`.
local proj = duffle.project_emission(body, cbi, wc, corpus.components, { ---@type EmissionProjection
local proj = duffle.project_emission(body, comps, wc, comps, { ---@type EmissionProjection
reg_use_schema = schema,
reg_use_param = atom_record.reg_use_param_name,
atom_name = atom_record.name,
@@ -271,13 +261,22 @@ local function project_atom(atom_record, src, corpus)
})
if atom_record.reg_use_schema_name and not schema then
proj.errors[#proj.errors + 1] = {
kind = "reguse_missing_schema",
kind = "error",
check = "reguse_missing_schema",
msg = string.format("RegUse schema %q is missing", atom_record.reg_use_schema_name),
schema_name = atom_record.reg_use_schema_name,
}
end
for _, err in ipairs(corpus.reg_use_errors or {}) do ---@type integer, EmitError
for _, err in ipairs(corpus.reg_use_errors or {}) do ---@type integer, RegUseError
if err.schema_name == atom_record.reg_use_schema_name then
proj.errors[#proj.errors + 1] = err
proj.errors[#proj.errors + 1] = {
kind = "error",
check = err.kind,
line = err.line or err.source_line or 0,
msg = err.msg or "",
source = err.source or err.source_file,
schema_name = err.schema_name,
}
end
end
local paths = { ---@type AtomPaths
@@ -303,8 +302,8 @@ end
--- @return PassResult
function M.run(ctx)
local outputs = {} ---@type PassOutputEntry[]
local errors = {} ---@type EmitError[]
local warnings = {} ---@type EmitWarning[]
local errors = {} ---@type Finding[]
local warnings = {} ---@type Finding[]
local corpus = ctx and ctx.shared and ctx.shared.corpus ---@type Corpus|nil
if type(corpus) ~= "table" then error("emission_model: ctx.shared.corpus is required (canonical projection)", 0) end
@@ -322,18 +321,22 @@ function M.run(ctx)
return
end
local proj = project_atom(atom, src, corpus) ---@type EmissionProjection
for _, e in ipairs(proj.errors) do ---@type integer, EmitError
-- Preserve `kind` (cycle / count_mismatch / unbalanced) so readers dispatch on the diagnostic class and leave the message string as display text.
for _, e in ipairs(proj.errors) do ---@type integer, Finding
-- Finding.kind is severity. Finding.check holds the diagnostic code
-- (cycle / count_mismatch / unbalanced / reguse_*).
errors[#errors + 1] = {
kind = e.kind,
kind = "error",
check = e.check,
line = e.line,
msg = e.msg,
source = e.source or src.path,
schema_name = e.schema_name,
}
end
for _, w in ipairs(proj.warnings) do ---@type integer, EmitWarning
for _, w in ipairs(proj.warnings) do ---@type integer, Finding
warnings[#warnings + 1] = {
kind = w.kind,
kind = "warning",
check = w.check,
line = w.line,
msg = w.msg,
}
+4 -4
View File
@@ -137,7 +137,7 @@ end
--- missing `consuming_encoder` -> ERROR. A lone top-level `atom_offset` is not a branch.
--- @param labels table<string, integer>
--- @param branches OffsetBranch[]
--- @param errors PassFinding[]
--- @param errors Finding[]
--- @return BranchOffset[]
local function compute_offsets(labels, branches, errors)
local results = {} ---@type BranchOffset[]
@@ -259,7 +259,7 @@ local M = {} ---@type OffsetsPass
--- @param ctx PassCtx
--- @param dir string
--- @param sources SourceFile[]
--- @param errors PassFinding[]
--- @param errors Finding[]
--- @return string|nil
local function process_directory(ctx, dir, sources, errors)
local atoms_data = {} ---@type AtomData[]
@@ -297,8 +297,8 @@ end
--- @return PassResult
function M.run(ctx)
local outputs = {} ---@type OffsetOutput[]
local errors = {} ---@type PassFinding[]
local warnings = {} ---@type PassFinding[]
local errors = {} ---@type Finding[]
local warnings = {} ---@type Finding[]
local corpus = ctx.shared and ctx.shared.corpus ---@type Corpus|nil
if type(corpus) ~= "table" then
+70 -42
View File
@@ -54,36 +54,17 @@ local PASS_NAME = "report" ---@type string
-- ════════════════════════════════════════════════════════════════════════════
-- SourceFile: see duffle.lua
-- PassCtx, PassResult, PassFinding, PassOutputEntry: see ps1_meta.lua
-- PassCtx, PassResult, Finding, PassOutputEntry: see ps1_meta.lua
-- AtomEntry, SourceScan, BindsEntry, AtomInfoEntry, AliasEntry,
-- AtomPhaseGroup, AtomViewEntry, AtomCtxEntry, CorpusCollision: see scan_source.lua
-- CheckFinding, AtomAnalysis: see static_analysis.lua
-- AtomAnalysis: see static_analysis.lua
-- WordCounts: see word_count_eval.lua
-- ComponentBodyEntry: see duffle_emit.lua
-- Component: see components.lua
-- AtomPaths, WordEvent: see emission_model.lua
-- GprAllocMap: see auto_reg.lua
-- Shapes produced by `passes/annotation.lua`'s `M.validate()`.
--- @class AnnotEntry
--- @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
--- @class BindsField
--- @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
-- Binds_* rows are BindsEntry (scan_source.lua). Report reads src.scan.binds.
--- @class MacroEntry
--- @field name string -- Macro name (e.g. "WORD_COUNT(my_macro, 4)")
@@ -93,12 +74,12 @@ local PASS_NAME = "report" ---@type string
--- @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 annots AtomInfoEntry[] -- Annotation entries
--- @field macros MacroEntry[] -- Macro word-count declarations
--- @field binds BindsStruct[] -- Binds_* struct declarations
--- @field errors PassFinding[] -- Errors from validation
--- @field warnings PassFinding[] -- Warnings from validation
--- @field info PassFinding[] -- Info summary (not rendered here)
--- @field binds BindsEntry[] -- Binds_* struct declarations
--- @field errors Finding[] -- Errors from validation
--- @field warnings Finding[] -- Warnings from validation
--- @field info Finding[] -- Info summary (not rendered here)
--- @class ModuleEntry
--- @field dir string -- Absolute directory path
@@ -196,7 +177,7 @@ local PASS_NAME = "report" ---@type string
--- @field sources SourceFile[]
--- @field decls AtomEntry[]
--- @field schemas RegUseSchema[]
--- @field findings CheckFinding[]
--- @field findings Finding[]
--- @field corpus Corpus
--- @class SectionRenderer
@@ -425,10 +406,10 @@ end
--- @return nil
local function render_section_components(add, view)
local rows = {} ---@type ComponentReportRow[]
local index = (view.corpus and view.corpus.component_body_index) or {} ---@type table<string, ComponentBodyEntry>
local index = (view.corpus and view.corpus.components) or {} ---@type table<string, Component>
for _, a in ipairs(view.decls) do ---@type integer, AtomEntry
if a.kind == "comp_bare" or a.kind == "comp_proc" then
local idx = index[a.name] or {} ---@type ComponentBodyEntry
local idx = index[a.name] or {} ---@type Component
local args = idx.arg_names or {} ---@type string[]
rows[#rows + 1] = {
name = a.name,
@@ -563,6 +544,50 @@ local function render_section_binds(add, view)
if not wrote then add("_(none)_"); add("") end
end
--- @param add fun(s: string): nil
--- @param view ModuleView
--- @return nil
local function render_section_atom_bundles(add, view)
local bundles = (view.corpus and view.corpus.atom_bundles) or {} ---@type table<string, AtomBundle>
local names = {} ---@type string[]
for name, bundle in pairs(bundles) do ---@type string, AtomBundle
if path_in_module(bundle.path, view) then
names[#names + 1] = name
end
end
if #names == 0 then add("_(none)_"); add(""); return end
table.sort(names)
for _, name in ipairs(names) do ---@type integer, string
local bundle = bundles[name] ---@type AtomBundle
local entries = bundle.entries or {} ---@type table<string, string>
add(string.format("### %s", name))
for _, slot in ipairs(bundle.slots or {}) do ---@type integer, string
local ident = entries[slot] ---@type string|nil
if ident then
add(string.format("- `%s` `%s`", slot, ident))
else
add(string.format("- `%s`", slot))
end
end
add("")
end
end
--- @param add fun(s: string): nil
--- @param view ModuleView
--- @return nil
local function render_section_tape_emits(add, view)
local wrote = false ---@type boolean
for _, src in ipairs(view.sources or {}) do ---@type integer, SourceFile
for _, emit in ipairs((src.scan and src.scan.tape_emits) or {}) do ---@type integer, TapeEmit
wrote = true
add(string.format("- `%s` `%s`", emit.name or "?", emit.binds or ""))
end
end
if not wrote then add("_(none)_"); add(""); return end
add("")
end
--- @param add fun(s: string): nil
--- @param view ModuleView
--- @return nil
@@ -698,8 +723,8 @@ end
--- @param view ModuleView
--- @return nil
local function render_section_findings(add, view)
local by_atom = {} ---@type table<string, CheckFinding[]>
for _, f in ipairs(view.findings or {}) do ---@type integer, CheckFinding
local by_atom = {} ---@type table<string, Finding[]>
for _, f in ipairs(view.findings or {}) do ---@type integer, Finding
local key = f.atom or "?" ---@type string
by_atom[key] = by_atom[key] or {}
by_atom[key][#by_atom[key] + 1] = f
@@ -707,11 +732,11 @@ local function render_section_findings(add, view)
if next(by_atom) == nil then add("_(none)_"); add(""); return end
local seen = {} ---@type table<string, boolean> -- bag: atom name already emitted
--- @param name string
--- @param fs CheckFinding[]
--- @param fs Finding[]
--- @return nil
local function emit(name, fs)
add("### " .. name)
for _, f in ipairs(fs) do ---@type integer, CheckFinding
for _, f in ipairs(fs) do ---@type integer, Finding
local msg = f.msg or "" ---@type string
local slot = slot_suffix(f.gpr_key or f.producer_destination) ---@type string|nil
if slot and not msg:find("(slot ", 1, true) then
@@ -766,11 +791,12 @@ local HIDDEN_UNLESS_WRITTEN = { ---@type table<string, boolean> -- bag: GPR key
R_AT = true, R_TapePtr = true, R_AtomJmp = true,
}
local PHYSICAL_GPR = { ---@type table<string, boolean> -- bag: physical GPR alias -> true
R_T0 = true, R_T1 = true, R_T2 = true, R_T3 = true,
R_T4 = true, R_T5 = true, R_T6 = true, R_T7 = true,
R_V0 = true, R_V1 = true,
}
local PHYSICAL_GPR = {} ---@type table<string, boolean> -- bag: physical GPR alias -> true
for _, row in ipairs(duffle.GPR_ROLE) do ---@type integer, GprRole
if row.pool then
PHYSICAL_GPR[row.name] = true
end
end
--- @param atom AtomEntry
--- @param key string
@@ -913,6 +939,8 @@ local SECTION_RENDERERS = { ---@type SectionRenderer[]
{ header = "## Annotations", render = render_section_annotations },
{ header = "## Component annotations", render = render_section_component_annotations },
{ header = "## Binds_* structs", render = render_section_binds },
{ header = "## Atom bundles", render = render_section_atom_bundles },
{ header = "## Tape emits", render = render_section_tape_emits },
{ header = "## Phases / views / ctx", render = render_section_phases },
{ header = "## Register aliases", render = render_section_aliases },
{ header = "## Auto-reg", render = render_section_autoreg },
@@ -945,7 +973,7 @@ local function render_module_meta_report(view)
n_macros = n_macros + #((src.scan and src.scan.macros) or {})
end
local n_err, n_warn, n_info = 0, 0, 0 ---@type integer, integer, integer
for _, f in ipairs(view.findings or {}) do ---@type integer, CheckFinding
for _, f in ipairs(view.findings or {}) do ---@type integer, Finding
if f.kind == "error" then n_err = n_err + 1
elseif f.kind == "warning" then n_warn = n_warn + 1
else n_info = n_info + 1
@@ -1087,7 +1115,7 @@ function M.run(ctx)
n_macros = n_macros + #((src.scan and src.scan.macros) or {})
end
local n_err, n_warn, n_info = 0, 0, 0 ---@type integer, integer, integer
for _, f in ipairs(view.findings or {}) do ---@type integer, CheckFinding
for _, f in ipairs(view.findings or {}) do ---@type integer, Finding
if f.kind == "error" then n_err = n_err + 1
elseif f.kind == "warning" then n_warn = n_warn + 1
else n_info = n_info + 1
+203 -1
View File
@@ -118,6 +118,7 @@ local parse_enum_int_literal ---@type fun(text: string, start: integer): (intege
--- @field ctx_atom string|nil
--- @field phase string|nil
--- @field info_line integer
--- @field errors string[]|nil -- parse-time atom_info errors
--- @class BindsEntry
--- @field line integer
@@ -126,6 +127,13 @@ local parse_enum_int_literal ---@type fun(text: string, start: integer): (intege
--- @field body string
--- @field bytes integer|nil
--- @class AtomBundle
--- @field name string
--- @field slots string[] -- typedef order
--- @field line integer
--- @field path string
--- @field entries table<string, string>|nil -- slot → B_E when an AtomBundleEntry_ proc exists
--- @class AliasEntry
--- @field name string
--- @field code integer
@@ -181,6 +189,14 @@ local parse_enum_int_literal ---@type fun(text: string, start: integer): (intege
--- @class TapeChain
--- @field [integer] string -- ordered atom names in one tb_emit chain
--- @class TapeEmit
--- @field name string
--- @field binds string|nil
--- @field line integer
--- @field path string
--- @field slot string|nil
--- @field data_words integer|nil
--- @class RegUseView
--- @field names string[]
--- @field lanes boolean
@@ -207,6 +223,8 @@ local parse_enum_int_literal ---@type fun(text: string, start: integer): (intege
--- @field reg_use_schemas table<string, RegUseSchema>
--- @field reg_use_errors RegUseError[]
--- @field tape_chains TapeChain[]
--- @field tape_emits TapeEmit[]
--- @field atom_bundles table<string, AtomBundle>
--- @field _source_file string|nil
--- @field _code_macros table<string, integer>|nil -- bag
--- @field _code_macro_bodies table<string, string>|nil -- bag
@@ -2438,11 +2456,76 @@ local function parse_typedef_array(source, pos, id2_end, line_of, out, after_typ
return semi and (semi + 1) or after_paren
end
--- Parse `MipsAtom *slot, …;` from an AtomBundle_ typedef body.
--- Ignores the MipsAtom type token. Slot name is the ident after `*`.
--- @param body string
--- @return string[]
local function parse_atom_bundle_slots(body)
local slots = {} ---@type string[]
local pos = 1 ---@type integer
while pos <= #body do
pos = duffle.skip_ws_and_cmt(body, pos)
if pos > #body then break end
local b = body:byte(pos) ---@type integer
if b == BYTE_SEMI then
break
elseif b == BYTE_COMMA then
pos = pos + 1
elseif b == BYTE_STAR then
local after_star = duffle.skip_ws_and_cmt(body, pos + 1) ---@type integer
local ident, ident_end = duffle.read_ident(body, after_star) ---@type string|nil, integer
if ident then
slots[#slots + 1] = ident
pos = ident_end
else
pos = pos + 1
end
else
local ident, ident_end = duffle.read_ident(body, pos) ---@type string|nil, integer
if ident then
pos = ident_end
else
pos = pos + 1
end
end
end
return slots
end
-- Shape 5: `typedef AtomBundle_(<name>) { MipsAtom *slot, … };`
--- @param source string
--- @param pos integer
--- @param id2_end integer
--- @param line_of fun(pos: integer): integer
--- @param out SourceScan
--- @param after_typedef integer
--- @return integer
local function parse_typedef_atom_bundle(source, pos, id2_end, line_of, out, after_typedef)
local inner, after_paren, open_paren = read_parens_after(source, id2_end, id2_end) ---@type string|nil, integer, integer
if not inner then return id2_end end
local name = duffle.trim(inner) ---@type string
local body, after_brace = find_body_braces(source, after_paren, open_paren + 1) ---@type string|nil, integer
if not body then return after_brace end
if name ~= "" and out.atom_bundles[name] == nil then
local bundle = { ---@type AtomBundle
name = name,
slots = parse_atom_bundle_slots(body),
line = line_of(pos),
path = out._source_file or "",
}
out.atom_bundles[name] = bundle
end
attach_debug_skip_marker(out, "unrelated")
return after_brace
end
local TYPE_FORMS = { ---@type table<string, fun(source: string, pos: integer, id2_end: integer, line_of: fun(pos: integer): integer, out: SourceScan, after_typedef: integer): integer>
Struct_ = parse_typedef_struct,
Enum_ = parse_typedef_enum,
TSet_ = parse_typedef_tset,
Array_ = parse_typedef_array,
AtomBundle_ = parse_typedef_atom_bundle,
}
--- Parse: `typedef` declarations.
@@ -2826,6 +2909,23 @@ local function parse_addrs_assign(source, pos, ident_end, line_of, out)
return rhs
end
--- @param out SourceScan
--- @param name string
--- @param last string
--- @param line integer
--- @return nil
local function push_tape_emit(out, name, last, line)
out.tape_emits = out.tape_emits or {}
out.tape_emits[#out.tape_emits + 1] = { ---@type TapeEmit
name = name,
binds = nil,
line = line,
path = out._source_file or "",
slot = last:find("->", 1, true) and name or nil,
data_words = 0,
}
end
--- @param source string
--- @param pos integer
--- @param ident_end integer
@@ -2836,10 +2936,12 @@ local function parse_tb_emit_(source, pos, ident_end, line_of, out)
local after = duffle.skip_ws_and_cmt(source, ident_end) ---@type integer
if source:sub(after, after) ~= "(" then return ident_end end
local inner, after_p = duffle.read_parens(source, after) ---@type string|nil, integer
local name = duffle.trim(inner or ""):match("^([%w_]+)") ---@type string
local last = duffle.trim(inner or "") ---@type string
local name = last:match("^([%w_]+)") ---@type string
if name then
out._chain = out._chain or {}
out._chain[#out._chain + 1] = name
push_tape_emit(out, name, last, line_of(pos))
end
return after_p or (after + 1)
end
@@ -2864,6 +2966,49 @@ local function parse_tb_emit(source, pos, ident_end, line_of, out)
if name then
out._chain = out._chain or {}
out._chain[#out._chain + 1] = name
push_tape_emit(out, name, last, line_of(pos))
end
return after_p or (after + 1)
end
--- @param source string
--- @param pos integer
--- @param ident_end integer
--- @param line_of fun(pos: integer): integer
--- @param out SourceScan
--- @return integer
local function parse_tb_bind_(source, pos, ident_end, line_of, out)
local after = duffle.skip_ws_and_cmt(source, ident_end) ---@type integer
if source:sub(after, after) ~= "(" then return ident_end end
local inner, after_p = duffle.read_parens(source, after) ---@type string|nil, integer
local args = duffle.split_top_level_commas(inner or "") ---@type string[]
local typ = duffle.trim(args[2] or "") ---@type string
if typ ~= "" then
local emits = out.tape_emits or {} ---@type TapeEmit[]
for i = #emits, 1, -1 do ---@type integer
if emits[i].binds == nil then
emits[i].binds = typ
break
end
end
end
return after_p or (after + 1)
end
--- @param source string
--- @param pos integer
--- @param ident_end integer
--- @param line_of fun(pos: integer): integer
--- @param out SourceScan
--- @return integer
local function parse_tb_data(source, pos, ident_end, line_of, out)
local after = duffle.skip_ws_and_cmt(source, ident_end) ---@type integer
if source:sub(after, after) ~= "(" then return ident_end end
local _, after_p = duffle.read_parens(source, after) ---@type string|nil, integer
local emits = out.tape_emits or {} ---@type TapeEmit[]
local last = emits[#emits] ---@type TapeEmit|nil
if last then
last.data_words = (last.data_words or 0) + 1
end
return after_p or (after + 1)
end
@@ -2871,6 +3016,8 @@ end
local C_STMT_PARSERS = { ---@type table<string, fun(source: string, pos: integer, ident_end: integer, line_of: fun(pos: integer): integer, out: SourceScan): integer>
tb_emit_ = parse_tb_emit_,
tb_emit = parse_tb_emit,
tb_bind_ = parse_tb_bind_,
tb_data = parse_tb_data,
addrs = parse_addrs_assign,
}
@@ -2923,6 +3070,8 @@ local function scan_source(source, source_file, code_macros, code_macro_bodies)
atoms = {},
raw_atoms = {},
binds = {},
atom_bundles = {},
tape_emits = {},
atom_infos = {},
component_atom_infos = {},
macros = {},
@@ -3237,6 +3386,8 @@ local function merge_corpus_registries(corpus)
corpus.reg_use_schemas = corpus.reg_use_schemas or {}
corpus.reg_use_errors = corpus.reg_use_errors or {}
corpus.tape_chains = corpus.tape_chains or {}
corpus.atom_bundles = corpus.atom_bundles or {}
corpus.tape_emits = corpus.tape_emits or {}
-- Replace the existing corpus collections with empty tables so a re-run on the same corpus produces identical state (deterministic merge).
-- This is safe because M.run is the only writer to these tables within a single orchestrator invocation.
@@ -3254,6 +3405,8 @@ local function merge_corpus_registries(corpus)
"reg_use_schemas",
"reg_use_errors",
"tape_chains",
"atom_bundles",
"tape_emits",
}) do
corpus[key] = {}
end
@@ -3361,6 +3514,55 @@ local function merge_corpus_registries(corpus)
for _, chain in ipairs(scan.tape_chains or {}) do ---@type integer, TapeChain
corpus.tape_chains[#corpus.tape_chains + 1] = chain
end
for _, emit in ipairs(scan.tape_emits or {}) do ---@type integer, TapeEmit
corpus.tape_emits[#corpus.tape_emits + 1] = emit
end
-- atom_bundles: keyed by typedef name. First-wins per name (like components).
for name, bundle in pairs(scan.atom_bundles or {}) do ---@type string, AtomBundle
if corpus.atom_bundles[name] == nil then
corpus.atom_bundles[name] = bundle
end
end
end
end
-- Join slot roles to AtomBundleEntry_ identities after catalogs and atoms exist.
-- Do not invent a catalog from an entry that has no typedef.
for _, bundle in pairs(corpus.atom_bundles) do ---@type string, AtomBundle
local entries = {} ---@type table<string, string>
for _, slot in ipairs(bundle.slots or {}) do ---@type integer, string
local atom_name = bundle.name .. "_" .. slot ---@type string
if corpus.atoms_by_name[atom_name] then
entries[slot] = atom_name
end
end
if next(entries) then
bundle.entries = entries
end
end
-- Resolve tb_emit names: atom first, else unique catalog slot → entries[slot].
-- Ambiguous slot or missing entry: leave the raw ident (no A/B later).
for _, emit in ipairs(corpus.tape_emits) do ---@type integer, TapeEmit
if corpus.atoms_by_name[emit.name] == nil then
local slot = emit.slot or emit.name ---@type string
local hit = nil ---@type AtomBundle|nil
local n = 0 ---@type integer
for _, bundle in pairs(corpus.atom_bundles) do ---@type string, AtomBundle
for _, s in ipairs(bundle.slots or {}) do ---@type integer, string
if s == slot then
n = n + 1
hit = bundle
break
end
end
end
local ident = hit and hit.entries and hit.entries[slot] ---@type string|nil
if n == 1 and ident then
emit.name = ident
emit.slot = slot
end
end
end
end
File diff suppressed because it is too large Load Diff
+2 -2
View File
@@ -4,13 +4,13 @@
--- 1. **Public utility** `M.count_token_words(token, wc)`: Used by `passes/offsets.lua`, `passes/annotation.lua`, and other passes.
--- 2. **Pass entry** `M.run(ctx)`: Loads the authored `word_count.metadata.h` into `ctx.shared.corpus.word_counts` for downstream passes.
--- The generated `.macs.h` files are OUTPUT artifacts and are NOT inputs to this pass;
--- Current component counts are owned by `passes/components.lua` (which populates `corpus.word_counts` and `corpus.component_body_index`
--- Current component counts are owned by `passes/components.lua` (which populates `corpus.word_counts` and `corpus.components`
--- AFTER computing each current count from the just-built body + `corpus.word_counts`).
---
--- **Canonical contract**:
--- * `ctx.shared.corpus.word_counts` is the count table.
--- * `corpus.word_counts` is the sole count table. Consumers read `corpus.word_counts` directly.
--- * `ctx.shared.components` and `ctx.shared.component_body_index` are NOT created by this pass (projections only).
--- * `ctx.shared.components` is NOT created by this pass (projections only).
--- * No `.macs.h` recursive discovery (no `scan_dir`, no scan cache, no `_invalidate_scan_cache`).
---
--- **Conventions**: tabs (1/level), EmmyLua annotations, no regex,
+55 -62
View File
@@ -76,8 +76,9 @@ local PASS_FLAG_DISPATCH_KEY = "__pass__" ---@type string
--- @field atom_ctxs table<AtomName, AtomCtxEntry>
--- @field atom_phases table<string, AtomPhaseGroup>
--- @field word_counts WordCounts
--- @field components table<string, ComponentDef>
--- @field component_body_index table<string, ComponentBodyEntry>
--- @field components table<string, Component>
--- @field atom_bundles table<string, AtomBundle>|nil
--- @field tape_emits TapeEmit[]|nil
--- @field collisions CorpusCollision[]
--- @field resolver SourceResolver
--- @field component_atom_infos AtomInfoEntry[]|nil
@@ -104,9 +105,43 @@ local PASS_FLAG_DISPATCH_KEY = "__pass__" ---@type string
--- @field flags PassFlags -- CLI flags + per-pass stash
--- @field verbose boolean -- If true, log diagnostic info
--- @class PassFinding
--- @field line integer -- Source line (or 0 for pass-level)
--- @field msg string -- Finding message
--- CheckName: see static_analysis.lua. AtomName: see duffle.lua.
--- @class Finding
--- @field line integer
--- @field msg string
--- @field kind string|nil -- error | warning | info
--- @field atom AtomName|nil
--- @field check CheckName|nil
--- @field source string|nil -- optional; emit/reguse path
--- @field schema_name string|nil -- optional; emit/reguse
--- @class PassScratch
--- @field corpus Corpus|nil
--- @field info_by_atom table<string, AtomInfoEntry>|nil
--- @field binds_index table<string, BindsEntry>|nil
--- @field atom_index table<string, AtomEntry>|nil
--- @field annot_counts table<string, integer>|nil -- bag
--- @field types table<string, RegTypeDefault>|nil
--- @field atom_views table<string, AtomViewEntry>|nil
--- @field seen_defaults table<string, integer>|nil -- bag
--- @field seen_field table<string, integer>|nil -- bag
--- @field _scan SourceScan|nil
--- @field word_counts WordCounts|nil
--- @field register_alias_registry table<string, AliasEntry>|nil
--- @field type_name_registry table<string, TypeNameEntry>|nil
--- @field type_occurrences RegTypeOccurrence[]|nil
--- @field atom_infos_list AtomInfoEntry[]|nil
--- @field binds_list BindsEntry[]|nil
--- @field unknown_seen table<string, integer>|nil -- bag
--- @field atoms AtomEntry[]|nil
--- @field components_by_name table<string, Component>|nil
--- @field atoms_by_name table<string, AtomEntry>|nil
--- @field tape_chains table<string, string[]>|nil
--- @field source_order SourceFile[]|nil
--- @field component_atom_infos AtomInfoEntry[]|nil
--- @field atom_infos_all AtomInfoEntry[]|nil
--- @field gte_cr_alias_groups GteCrAliasGroup[]|nil
--- @field line_for_word_event (fun(ev: WordEvent): integer)|nil
--- @class PassOutputEntry
--- @field kind string
@@ -114,9 +149,9 @@ local PASS_FLAG_DISPATCH_KEY = "__pass__" ---@type string
--- @class PassResult
--- @field outputs PassOutputEntry[]
--- @field errors PassFinding[] -- Build-stops (per-pass kind policy)
--- @field warnings PassFinding[] -- Informational
--- @field info CheckFinding[]|nil -- static_analysis only
--- @field errors Finding[] -- Build-stops (per-pass kind policy)
--- @field warnings Finding[] -- Informational
--- @field info Finding[]|nil -- static_analysis only
--- @class ParsedArgs
--- @field requested_set string[] -- Pass names to run (explicit --all expanded)
@@ -340,8 +375,7 @@ COMMON_FLAGS:
--help Show this help and exit
EXIT CODES:
0 All requested passes succeeded
1 Validation errors found
0 Ran. Findings print on stderr and in the report; they do not fail the process.
2 Metaprogram internal error
EXAMPLES:
@@ -604,56 +638,15 @@ local function build_ctx(args)
end
resolution = resolved
else
local source_order = {} ---@type SourceFile[]
local sources_by_path = {} ---@type table<Path, SourceFile>
local resolver = { ---@type SourceResolver
resolved = {},
skipped = {},
shadowed = {},
}
for _, input_path in ipairs(args.sources) do ---@type integer, string
local path = duffle.normalize_path(input_path) ---@type string
local key_ok, key_or_error = pcall(duffle.canonical_path_key, path) ---@type boolean, string
if not key_ok then
error("ps1_meta: invalid --source " .. input_path .. ": " .. tostring(key_or_error), 0)
end
local file = io.open(path, "r") ---@type file*|nil
if not file then
io.stderr:write("ps1_meta: cannot open --source " .. input_path .. "\n")
local ok_exact, exact = pcall(duffle.resolve_exact_sources, { ---@type boolean, Corpus|string
sources = args.sources,
project_root = project_root,
})
if not ok_exact then
io.stderr:write("ps1_meta: cannot resolve --source: " .. tostring(exact) .. "\n")
os.exit(EXIT_INTERNAL_ERROR)
end
local text = file:read("*a") ---@type string
file:close()
local source = { ---@type SourceFile
path = path,
text = text,
dir = duffle.dirname(path),
basename = duffle.basename_no_ext(path),
}
source_order[#source_order + 1] = source
local key = key_or_error ---@type string
if not sources_by_path[key] then sources_by_path[key] = source end
resolver.resolved[#resolver.resolved + 1] = {
include_path = path,
include_text = nil,
root_source = nil,
root_line = nil,
candidate_a = path,
candidate_b = nil,
selected_path = path,
disposition = "exact",
}
end
resolution = {
unity_root = nil,
project_root = project_root,
code_root = duffle.normalize_path(project_root .. "/code"),
source_order = source_order,
sources_by_path = sources_by_path,
sources_by_dir = duffle.group_sources_by_dir(source_order),
resolver = resolver,
}
resolution = exact
end
local corpus = { ---@type Corpus
@@ -673,7 +666,8 @@ local function build_ctx(args)
atom_phases = {},
word_counts = {},
components = {},
component_body_index = {},
atom_bundles = {},
tape_emits = {},
collisions = {},
resolver = resolution.resolver,
}
@@ -791,7 +785,7 @@ end
local function report_validation_errors(pass_name, pass, result)
local has_errors = result.errors and #result.errors > 0 ---@type boolean
if not has_errors then return false end
for _, e in ipairs(result.errors) do ---@type integer, PassFinding
for _, e in ipairs(result.errors) do ---@type integer, Finding
io.stderr:write(string.format("[%s] line %d: %s\n", pass_name, e.line or 0, e.msg or ""))
end
return PASS_KIND_STOP_ON_ERROR[pass.kind] == true
@@ -825,8 +819,7 @@ local function main(argv)
local requested = args.requested_set ---@type string[]
local closed = topo_sort(PASSES, requested) ---@type string[]
local had_errors = dispatch_passes(ctx, closed) ---@type boolean
if had_errors then os.exit(EXIT_VALIDATION_ERRORS) end
dispatch_passes(ctx, closed)
end)
if not ok then