22 changed files with 1586 additions and 796 deletions
+2
View File
@@ -17,3 +17,5 @@ toolchain/PSn00bSDK
.vscode/settings.json .vscode/settings.json
toolchain/lfs toolchain/lfs
toolchain/lpeg toolchain/lpeg
scratch
-1
View File
@@ -138,7 +138,6 @@
"monitor reset shellhalt", "monitor reset shellhalt",
"load build/hello_gte.dwarf-injected.elf", "load build/hello_gte.dwarf-injected.elf",
"source scripts/gdb/gdb_tape_atoms.gdb", "source scripts/gdb/gdb_tape_atoms.gdb",
"source build/gen/hello_gte.gdbinit",
"tbreak main", "tbreak main",
"continue" "continue"
] ]
+10 -15
View File
@@ -98,11 +98,11 @@ WORD_COUNT(mac_format_f3_color, 3)
/* atom_dbg_skip */ /* atom_dbg_skip */
/* Words: 3; Stores the 3 transformed (V2_S2 screen) vertices to the F3. /* Words: 3; Stores the 3 transformed (V2_S2 screen) vertices to the F3.
* PIPELINE: post-RTPT (SXY0=v0.screen, SXY1=v1.screen, SXY2=v2.screen). */ * PIPELINE: post-RTPT (SXY0=v0.screen, SXY1=v1.screen, SXY2=v2.screen). */
#define mac_gte_store_f3_post_rtpt(...) \ #define mac_gte_store_f3(...) \
gte_sw(C2_SXY0, R_PrimCursor, O_(Poly_F3,p0)) \ gte_sw(C2_SXY0, R_PrimCursor, O_(Poly_F3,p0)) \
, gte_sw(C2_SXY1, R_PrimCursor, O_(Poly_F3,p1)) \ , gte_sw(C2_SXY1, R_PrimCursor, O_(Poly_F3,p1)) \
, gte_sw(C2_SXY2, R_PrimCursor, O_(Poly_F3,p2)) , gte_sw(C2_SXY2, R_PrimCursor, O_(Poly_F3,p2))
WORD_COUNT(mac_gte_store_f3_post_rtpt, 3) WORD_COUNT(mac_gte_store_f3, 3)
#define mac_format_g4_color(r0, g0, b0, r1, g1, b1, r2, g2, b2, r3, g3, b3) \ #define mac_format_g4_color(r0, g0, b0, r1, g1, b1, r2, g2, b2, r3, g3, b3) \
mac_pack_color_word(O_(Poly_G4,c0), gp0_cmd_poly_g4, r0,g0,b0) \ mac_pack_color_word(O_(Poly_G4,c0), gp0_cmd_poly_g4, r0,g0,b0) \
@@ -115,25 +115,20 @@ WORD_COUNT(mac_format_g4_color, 12)
/* Words: 3; Stores the 3 transformed (V2_S2 screen) vertices of the /* Words: 3; Stores the 3 transformed (V2_S2 screen) vertices of the
* G4 triangle portion to p0/p1/p2. * G4 triangle portion to p0/p1/p2.
* PIPELINE: post-RTPT, pre-RTPS (SXY0=v0.screen, SXY1=v1.screen, SXY2=v2.screen). * PIPELINE: post-RTPT, pre-RTPS (SXY0=v0.screen, SXY1=v1.screen, SXY2=v2.screen).
* MUST be called BEFORE V3-RTPS, otherwise SXY0/1/2 * MUST be called BEFORE V3-RTPS, otherwise SXY0/1/2 get overwritten with v3
* get overwritten with v3 (RTPS writes only to SXY2, but to keep the * (RTPS writes only to SXY2, but to keep the three registers aligned with v0/v1/v2 you must store before RTPS). */
* three registers aligned with v0/v1/v2 you must store before RTPS). #define mac_gte_store_g4_p012(...) \
* The macro name declares the pipeline position; check #6 (GTE state-
* machine validation) verifies the call site matches the declaration. */
#define mac_gte_store_g4_p012_post_rtpt_pre_rtps(...) \
gte_sw(C2_SXY0, R_PrimCursor, O_(Poly_G4,p0)) \ gte_sw(C2_SXY0, R_PrimCursor, O_(Poly_G4,p0)) \
, gte_sw(C2_SXY1, R_PrimCursor, O_(Poly_G4,p1)) \ , gte_sw(C2_SXY1, R_PrimCursor, O_(Poly_G4,p1)) \
, gte_sw(C2_SXY2, R_PrimCursor, O_(Poly_G4,p2)) , gte_sw(C2_SXY2, R_PrimCursor, O_(Poly_G4,p2))
WORD_COUNT(mac_gte_store_g4_p012_post_rtpt_pre_rtps, 3) WORD_COUNT(mac_gte_store_g4_p012, 3)
/* atom_dbg_skip */ /* atom_dbg_skip */
/* Words: 1; Stores the V3 screen coord to the G4's p3 slot. /* Words: 1; Stores the V3 screen coord to the G4's p3 slot.
* PIPELINE: post-RTPS (SXY2 holds v3.screen because RTPS writes its * PIPELINE: post-RTPS (SXY2 holds v3.screen because RTPS writes its single-vertex result to SXY2;
* single-vertex result to SXY2; SXY0 still holds v0.screen from the * SXY0 still holds v0.screen from the earlier RTPT.
* earlier RTPT — DO NOT read SXY0 here, that's the bug this name
* prevents).
*/ */
#define mac_gte_store_g4_p3_post_rtps(...) \ #define mac_gte_store_g4_p3(...) \
gte_sw(C2_SXY2, R_PrimCursor, O_(Poly_G4,p3)) gte_sw(C2_SXY2, R_PrimCursor, O_(Poly_G4,p3))
WORD_COUNT(mac_gte_store_g4_p3_post_rtps, 1) WORD_COUNT(mac_gte_store_g4_p3, 1)
+7 -12
View File
@@ -170,7 +170,7 @@ atom_dbg_skip MipsAtomComp_Proc_(ac_format_f3_color, { mac_pack_color_word(O_(Po
/* Words: 3; Stores the 3 transformed (V2_S2 screen) vertices to the F3. /* Words: 3; Stores the 3 transformed (V2_S2 screen) vertices to the F3.
* PIPELINE: post-RTPT (SXY0=v0.screen, SXY1=v1.screen, SXY2=v2.screen). */ * PIPELINE: post-RTPT (SXY0=v0.screen, SXY1=v1.screen, SXY2=v2.screen). */
atom_dbg_skip MipsAtomComp_(ac_gte_store_f3_post_rtpt) { atom_dbg_skip MipsAtomComp_(ac_gte_store_f3) {
gte_sw(C2_SXY0, R_PrimCursor, O_(Poly_F3,p0)), gte_sw(C2_SXY0, R_PrimCursor, O_(Poly_F3,p0)),
gte_sw(C2_SXY1, R_PrimCursor, O_(Poly_F3,p1)), gte_sw(C2_SXY1, R_PrimCursor, O_(Poly_F3,p1)),
gte_sw(C2_SXY2, R_PrimCursor, O_(Poly_F3,p2)), gte_sw(C2_SXY2, R_PrimCursor, O_(Poly_F3,p2)),
@@ -193,24 +193,19 @@ MipsAtomComp_Proc_(ac_format_g4_color, {
/* Words: 3; Stores the 3 transformed (V2_S2 screen) vertices of the /* Words: 3; Stores the 3 transformed (V2_S2 screen) vertices of the
* G4 triangle portion to p0/p1/p2. * G4 triangle portion to p0/p1/p2.
* PIPELINE: post-RTPT, pre-RTPS (SXY0=v0.screen, SXY1=v1.screen, SXY2=v2.screen). * PIPELINE: post-RTPT, pre-RTPS (SXY0=v0.screen, SXY1=v1.screen, SXY2=v2.screen).
* MUST be called BEFORE V3-RTPS, otherwise SXY0/1/2 * MUST be called BEFORE V3-RTPS, otherwise SXY0/1/2 get overwritten with v3
* get overwritten with v3 (RTPS writes only to SXY2, but to keep the * (RTPS writes only to SXY2, but to keep the three registers aligned with v0/v1/v2 you must store before RTPS). */
* three registers aligned with v0/v1/v2 you must store before RTPS). atom_dbg_skip MipsAtomComp_(ac_gte_store_g4_p012) {
* The macro name declares the pipeline position; check #6 (GTE state-
* machine validation) verifies the call site matches the declaration. */
atom_dbg_skip MipsAtomComp_(ac_gte_store_g4_p012_post_rtpt_pre_rtps) {
gte_sw(C2_SXY0, R_PrimCursor, O_(Poly_G4,p0)), gte_sw(C2_SXY0, R_PrimCursor, O_(Poly_G4,p0)),
gte_sw(C2_SXY1, R_PrimCursor, O_(Poly_G4,p1)), gte_sw(C2_SXY1, R_PrimCursor, O_(Poly_G4,p1)),
gte_sw(C2_SXY2, R_PrimCursor, O_(Poly_G4,p2)), gte_sw(C2_SXY2, R_PrimCursor, O_(Poly_G4,p2)),
}; };
/* Words: 1; Stores the V3 screen coord to the G4's p3 slot. /* Words: 1; Stores the V3 screen coord to the G4's p3 slot.
* PIPELINE: post-RTPS (SXY2 holds v3.screen because RTPS writes its * PIPELINE: post-RTPS (SXY2 holds v3.screen because RTPS writes its single-vertex result to SXY2;
* single-vertex result to SXY2; SXY0 still holds v0.screen from the * SXY0 still holds v0.screen from the earlier RTPT.
* earlier RTPT — DO NOT read SXY0 here, that's the bug this name
* prevents).
*/ */
atom_dbg_skip MipsAtomComp_(ac_gte_store_g4_p3_post_rtps) { gte_sw(C2_SXY2, R_PrimCursor, O_(Poly_G4,p3)) }; atom_dbg_skip MipsAtomComp_(ac_gte_store_g4_p3) { gte_sw(C2_SXY2, R_PrimCursor, O_(Poly_G4,p3)) };
#pragma endregion Macro Atom Components #pragma endregion Macro Atom Components
@@ -5,10 +5,10 @@
#pragma region hello_gte_tape #pragma region hello_gte_tape
// --- atom: cube_g4_face (87 words) --- // --- atom: cube_g4_face (77 words) ---
#define _atom_offset_cull_cube_g4_face_exit 48 #define _atom_offset_cull_cube_g4_face_exit 42
#define _atom_offset_bounds_chk_cube_g4_face_exit 12 #define _atom_offset_bounds_chk_cube_g4_face_exit 24
enum { enum {
atom_offset_cull_cube_g4_face_exit = _atom_offset_cull_cube_g4_face_exit, atom_offset_cull_cube_g4_face_exit = _atom_offset_cull_cube_g4_face_exit,
@@ -18,7 +18,7 @@ enum {
// --- atom: floor_f3_face (58 words) --- // --- atom: floor_f3_face (58 words) ---
#define _atom_offset_culling_floor_f3_face_exit 25 #define _atom_offset_culling_floor_f3_face_exit 25
#define _atom_offset_bounds_chk_floor_f3_face_exit 13 #define _atom_offset_bounds_chk_floor_f3_face_exit 16
enum { enum {
atom_offset_culling_floor_f3_face_exit = _atom_offset_culling_floor_f3_face_exit, atom_offset_culling_floor_f3_face_exit = _atom_offset_culling_floor_f3_face_exit,
+29
View File
@@ -0,0 +1,29 @@
// Auto-generated by ps1_meta.lua (passes/offsets.lua) — DO NOT EDIT
// Source: C:\projects\Pikuma\ps1\code\hello_gte\hello_gte.tape.c
#pragma once
#pragma region hello_gte.tape
// --- atom: cube_g4_face (77 words) ---
#define _atom_offset_cull_cube_g4_face_exit 42
#define _atom_offset_bounds_chk_cube_g4_face_exit 24
enum {
atom_offset_cull_cube_g4_face_exit = _atom_offset_cull_cube_g4_face_exit,
atom_offset_bounds_chk_cube_g4_face_exit = _atom_offset_bounds_chk_cube_g4_face_exit,
};
// --- atom: floor_f3_face (58 words) ---
#define _atom_offset_culling_floor_f3_face_exit 25
#define _atom_offset_bounds_chk_floor_f3_face_exit 16
enum {
atom_offset_culling_floor_f3_face_exit = _atom_offset_culling_floor_f3_face_exit,
atom_offset_bounds_chk_floor_f3_face_exit = _atom_offset_bounds_chk_floor_f3_face_exit,
};
#pragma endregion hello_gte.tape
@@ -20,10 +20,10 @@
#include "duffle/lottes_tape.h" #include "duffle/lottes_tape.h"
#include "duffle/word_count.metadata.h" #include "duffle/word_count.metadata.h"
# include "gen/gte_hello.offsets.h" # include "gen/hello_gte.offsets.h"
#include "hello_gte.h" #include "hello_gte.h"
#include "hello_gte_tape.c" #include "hello_gte.tape.c"
typedef U4 OrderingTable_Buffer[OrderingTbl_Len]; typedef U4 OrderingTable_Buffer[OrderingTbl_Len];
typedef Array_(OrderingTable_Buffer, 2); typedef Array_(OrderingTable_Buffer, 2);
@@ -4,7 +4,7 @@
# include "duffle/atom_dsl.h" # include "duffle/atom_dsl.h"
# include "duffle/lottes_tape.h" # include "duffle/lottes_tape.h"
# include "duffle/word_count.metadata.h" # include "duffle/word_count.metadata.h"
# include "gen/gte_hello.offsets.h" # include "gen/hello_gte.offsets.h"
# include "hello_gte.h" # include "hello_gte.h"
#endif #endif
@@ -47,35 +47,34 @@ MipsAtom_(cube_g4_face) atom_info(atom_phase(cube_g4),
load_half_u(R_T3, R_FaceCursor, 3 * S_(S2)), load_half_u(R_T3, R_FaceCursor, 3 * S_(S2)),
mac_gte_load_tri_verts(R_T0, R_T1, R_T2), mac_gte_load_tri_verts(R_T0, R_T1, R_T2),
nop2, gte_cmdw_rotate_translate_perspective_triple, nop2, gte_cmdw_rotate_translate_perspective_triple, // required cpu -> gte delay slot
nop2, gte_cmdw_nclip, gte_cmdw_nclip,
nop2, gte_mv_from_data_r(R_T0, C2_MAC0), gte_mv_from_data_r(R_T0, C2_MAC0), nop,
nop,
branch_le_zero(R_T0, atom_offset(cull, cube_g4_face_exit)), nop, branch_le_zero(R_T0, atom_offset(cull, cube_g4_face_exit)), nop,
store_word(R_0, R_PrimCursor, O_(Poly_G4, tag)), store_word(R_0, R_PrimCursor, O_(Poly_G4, tag)),
shift_lleft(R_AT, R_T3, v3s2_byteoff), add_u(R_AT, R_AT, R_VertBase),
load_word(R_V0, R_AT, O_(V3_S2, x)), load_word(R_V1, R_AT, O_(V3_S2, z)),
gte_mv_to_data_r(R_V0, C2_VXY0), gte_mv_to_data_r(R_V1, C2_VZ0),
mac_gte_store_g4_p012(),
gte_cmdw_rotate_translate_perspective_single,
mac_gte_store_g4_p3(),
gte_cmdw_avg_sort_z4,
gte_mv_from_data_r(R_T1, C2_OTZ),
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, cube_g4_face_exit)), nop,
mac_insert_ot_tag_g4(),
mac_format_g4_color( mac_format_g4_color(
/* c0 magenta */ 0xFF, 0x00, 0xFF, /* c0 magenta */ 0xFF, 0x00, 0xFF,
/* c1 yellow */ 0xFF, 0xFF, 0x00, /* c1 yellow */ 0xFF, 0xFF, 0x00,
/* c2 cyan */ 0x00, 0xFF, 0xFF, /* c2 cyan */ 0x00, 0xFF, 0xFF,
/* c3 green */ 0x00, 0xFF, 0x00), /* c3 green */ 0x00, 0xFF, 0x00),
mac_gte_store_g4_p012_post_rtpt_pre_rtps(), // end: branch(bounds_chk)
// end: branch(cull)
shift_lleft(R_AT, R_T3, v3s2_byteoff), add_u(R_AT, R_AT, R_VertBase),
load_word(R_V0, R_AT, O_(V3_S2, x)), load_word(R_V1, R_AT, O_(V3_S2, z)),
gte_mv_to_data_r(R_V0, C2_VXY0), gte_mv_to_data_r(R_V1, C2_VZ0),
nop2, gte_cmdw_rotate_translate_perspective_single,
mac_gte_store_g4_p3_post_rtps(),
nop2, gte_cmdw_avg_sort_z4,
nop2, gte_mv_from_data_r(R_T1, C2_OTZ),
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, cube_g4_face_exit)), nop,
mac_insert_ot_tag_g4(),
atom_label(cube_g4_face_exit) atom_label(cube_g4_face_exit)
add_ui_self(R_PrimCursor, S_(Poly_G4)), /* 9 words = Poly_G4 */ add_ui_self(R_PrimCursor, S_(Poly_G4)), /* 9 words = Poly_G4 */
@@ -103,11 +102,11 @@ MipsAtom_(rbind_floor_f3_face) atom_info(atom_bind(Binds_FloorTri), atom_phase(f
mac_yield() mac_yield()
}; };
atom_dbg_skip // atom_dbg_skip
internal internal
MipsAtom_(floor_f3_face) atom_info(atom_phase(floor_f3) MipsAtom_(floor_f3_face) atom_info(atom_phase(floor_f3)
, atom_reads( R_PrimCursor, R_FaceCursor, R_VertBase, R_OtBase) , atom_reads( R_PrimCursor, R_FaceCursor, R_VertBase, R_OtBase)
, atom_writes(R_PrimCursor, R_FaceCursr) , atom_writes(R_PrimCursor, R_FaceCursor)
) { ) {
mac_load_tri_indices( R_T0, R_T1, R_T2), mac_load_tri_indices( R_T0, R_T1, R_T2),
mac_gte_load_tri_verts(R_T0, R_T1, R_T2), mac_gte_load_tri_verts(R_T0, R_T1, R_T2),
@@ -118,8 +117,7 @@ MipsAtom_(floor_f3_face) atom_info(atom_phase(floor_f3)
gte_mv_from_data_r(R_T0, C2_MAC0), 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. nop, branch_le_zero(R_T0, atom_offset(culling, floor_f3_face_exit)), nop, // required gte -> cpu load-delay slot.
/* Format Primitive */ /* Format Primitive */
mac_format_f3_color(0xFF, 0xFF, 0xFF), // RGB-form (R=FF, G=FF, B=FF = white) mac_gte_store_f3(),
mac_gte_store_f3_post_rtpt(),
/* Calculate Depth */ /* Calculate Depth */
gte_avg_sort_z3, gte_avg_sort_z3,
@@ -128,8 +126,8 @@ MipsAtom_(floor_f3_face) atom_info(atom_phase(floor_f3)
add_ui( R_AT, R_0, OrderingTbl_Len), add_ui( R_AT, R_0, OrderingTbl_Len),
set_lt_u( R_AT, R_T1, R_AT), 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)), nop,
/* Insert into Ordering Table Linked List */ mac_format_f3_color(0xFF, 0xFF, 0xFF), // RGB-form (R=FF, G=FF, B=FF = white)
mac_insert_ot_tag_f3(), mac_insert_ot_tag_f3(), /* Insert into Ordering Table Linked List */
add_ui_self(R_PrimCursor, S_(Poly_F3)), /* Advance Prim Cursor (5 words) */ add_ui_self(R_PrimCursor, S_(Poly_F3)), /* Advance Prim Cursor (5 words) */
// Note(Ed): No bounds checking, should be checked before atom runs. // Note(Ed): No bounds checking, should be checked before atom runs.
@@ -0,0 +1,29 @@
// Auto-generated by ps1_meta.lua (passes/offsets.lua) — DO NOT EDIT
// Source: C:\projects\Pikuma\ps1\code\hello_joypad\hello_joypad.tape.c
#pragma once
#pragma region hello_joypad.tape
// --- atom: cube_g4_face (77 words) ---
#define _atom_offset_cull_cube_g4_face_exit 42
#define _atom_offset_bounds_chk_cube_g4_face_exit 24
enum {
atom_offset_cull_cube_g4_face_exit = _atom_offset_cull_cube_g4_face_exit,
atom_offset_bounds_chk_cube_g4_face_exit = _atom_offset_bounds_chk_cube_g4_face_exit,
};
// --- atom: floor_f3_face (58 words) ---
#define _atom_offset_culling_floor_f3_face_exit 25
#define _atom_offset_bounds_chk_floor_f3_face_exit 16
enum {
atom_offset_culling_floor_f3_face_exit = _atom_offset_culling_floor_f3_face_exit,
atom_offset_bounds_chk_floor_f3_face_exit = _atom_offset_bounds_chk_floor_f3_face_exit,
};
#pragma endregion hello_joypad.tape
+430
View File
@@ -0,0 +1,430 @@
#include <stdio.h>
#include <stdlib.h>
#include <assert.h>
// #include "libgpu.h"
// #include "libetc.h"
// #include "libgte.h"
#include "duffle/dsl.h"
#include "duffle/memory.h"
#include "duffle/math.h"
#include "duffle/gcc_asm.h"
#include "duffle/mips.h"
#include "duffle/gp.h"
#include "duffle/gte.h"
# include "duffle/gen/duffle.macs.h"
# include "duffle/gen/duffle.offsets.h"
#include "duffle/atom_dsl.h"
#include "duffle/lottes_tape.h"
#include "duffle/word_count.metadata.h"
# include "gen/hello_joypad.offsets.h"
#include "hello_joypad.h"
#include "hello_joypad.tape.c"
typedef U4 OrderingTable_Buffer[OrderingTbl_Len];
typedef Array_(OrderingTable_Buffer, 2);
typedef B1 PrimitiveBuffer[PrimitiveBuff_Len];
typedef Array_(PrimitiveBuffer, 2);
typedef Struct_(PrimitiveArena) {
A2_PrimitiveBuffer buf;
U4 used;
};
#define Cube_num_verts 8
typedef Array_(V3_S2, Cube_num_verts);
#define Cube_num_faces 6
typedef Array_(V4_S2, Cube_num_faces);
I_ void ent_cube128_init(A8_V3_S2* verts, A6_V4_S2* faces) {
LP_ A8_V3_S2 baked_verts = (A8_V3_S2) {
{ -128, -128, -128 },
{ 128, -128, -128 },
{ 128, -128, 128 },
{ -128, -128, 128 },
{ -128, 128, -128 },
{ 128, 128, -128 },
{ 128, 128, 128 },
{ -128, 128, 128 }
};
LP_ A6_V4_S2 baked_faces = (A6_V4_S2) {
{ 3, 2, 0, 1 },
{ 0, 1, 4, 5 },
{ 4, 5, 7, 6 },
{ 1, 2, 5, 6 },
{ 2, 3, 6, 7 },
{ 3, 0, 7, 4 },
};
mem_copy(u4_(verts), u4_(& baked_verts), S_(A8_V3_S2) );
mem_copy(u4_(faces), u4_(& baked_faces), S_(A6_V4_S2) );
return;
}
typedef Struct_(Ent_Cube) {
V3_S4 accel;
V3_S4 vel;
V3_S4 pos;
V3_S4 scale;
V3_S2 rot;
A8_V3_S2 verts;
A6_V4_S2 faces;
};
#define Floor_num_verts 4
typedef Array_(V3_S2, Floor_num_verts);
#define Floor_num_faces 2
typedef Array_(V3_S2, Floor_num_faces);
I_ void ent_floor_init(A4_V3_S2* verts, A2_V3_S2* faces) {
LP_ A4_V3_S2 baked_verts = (A4_V3_S2) {
{ -900, 0, -900 },
{ -900, 0, 900 },
{ 900, 0, -900 },
{ 900, 0, 900 },
};
LP_ A2_V3_S2 baked_faces = (A2_V3_S2) {
{ 0, 1, 2 },
{ 1, 3, 2 },
};
mem_copy(u4_(verts), u4_(& baked_verts), S_(A4_V3_S2));
mem_copy(u4_(faces), u4_(& baked_faces), S_(A2_V3_S2));
};
typedef Struct_(Ent_Floor) {
V3_S4 accel;
V3_S4 pos;
V3_S4 scale;
V3_S2 rot;
A4_V3_S2 verts;
A2_V3_S2 faces;
};
enum {
Scratchpad_Len = 1024,
MemTape_Len = 512,
};
typedef Struct_(SMemory) {
U4 MemTape[MemTape_Len];
DoubleBuffer screen_buf;
A2_OrderingTable_Buffer ordering_tbl;
PrimitiveArena primitives;
S4 active_buf_id;
M3_S2 tform_world;
Ent_Cube cube;
Ent_Floor floor;
U4_V scratchpad; // d-cache
};
global SMemory smem;
extern SMemory smem;
// TODO(Ed):
FI_ U4* spad_warm(MipsAtom atom) {
return nullptr;
}
I_ B1* prim__alloc(U4 type_width, Str8 type_name) {
gknown PrimitiveArena* pa = & smem.primitives;
gknown B1* buf = (B1*) r_(smem.primitives.buf)[smem.active_buf_id];
assert(pa->used + type_width < PrimitiveBuff_Len);
B1* next = buf + pa->used;
pa->used += type_width;
return next;
}
#define prim_alloc(type) (type*)prim__alloc(S_(type), slit( stringify(type)))
void gp_screen_init_c11(DoubleBuffer* screen_buf, S4* active_buf_id)
{
reset_graph(0);
// Set the current initial buffer
active_buf_id[0] = 0;
// Just setting env data, not interacting with console hw.
// First buffer area
displayenv_init(& r_(screen_buf->display)[0], 0, 0, ScreenRes_X, ScreenRes_Y);
drawenv_init (& r_(screen_buf->draw )[0], 0, ScreenRes_Y, ScreenRes_X, ScreenRes_Y);
// Second buffer area
displayenv_init(& r_(screen_buf->display)[1], 0, ScreenRes_Y, ScreenRes_X, ScreenRes_Y);
drawenv_init (& r_(screen_buf->draw )[1], 0, 0, ScreenRes_X, ScreenRes_Y);
// Set the back/drawing buffer
screen_buf->draw[0].enable_auto_clear = true;
screen_buf->draw[1].enable_auto_clear = true;
// Set the background clear color
screen_buf->draw[0].initial_bg_color = rgb8( .r = 7, .g = 7, .b = 7 );
screen_buf->draw[1].initial_bg_color = rgb8( .r = 7, .g = 7, .b = 7 );
// screen_buf->draw[1].initial_bg_color = rgb8( .r = 47, .g = 13, .b = 0 );
displayenv_put(& r_(screen_buf->display)[ active_buf_id[0] ]);
drawenv_put (& r_(screen_buf->draw )[ active_buf_id[0] ]);
// Initialize and setup the GTE geometry offsets
geom_init();
// NOTE: geom_set_offset/geom_set_screen are kept as-is (the libgte versions
// are known to be broken in this PSYQ 4.7 build — see report 2026-07-09).
// The user's research wants the C-side non-tape reference to work as a
// known-good baseline for comparison against the tape.
geom_set_offset(ScreenRes_CenterX, ScreenRes_CenterY);
geom_set_screen(ScreenZ);
set_display_enabled(1); // gp_DisplayEnabled
}
void gp_display_frame(DoubleBuffer* screen_buf, S4* active_buf_id, U4* ordering_buf, PrimitiveArena* pa) {
draw_sync(0);
vsync(0);
displayenv_put(& r_(screen_buf->display)[active_buf_id[0] ]);
drawenv_put (& r_(screen_buf->draw) [active_buf_id[0] ]);
{
draw_orderingtbl(ordering_buf + OrderingTbl_Len - 1);
pa->used = 0;
}
active_buf_id[0] = ! active_buf_id[0]; // Swap current buffer
}
void render(void) {
}
GCC_OPTIMIZATION_DISABLE
void update(PrimitiveArena* pa, U4* ordering_buf)
{
orderingtbl_clear_reverse(ordering_buf, OrderingTbl_Len);
// Update the position based on acceleration and velocity
gknown V3_S4_R pos = & smem.cube.pos;
gknown V3_S4_R vel = & smem.cube.vel;
gknown V3_S4_R acc = & smem.cube.accel;
add_v3s4(vel, acc[0]);
add_v3s4_fp(pos, vel[0]);
// vel->x += acc->x;
// vel->y += acc->y;
// vel->z += acc->z;
// pos->x += vel->x;
// pos->y += vel->y;
// pos->z += vel->z;
if (pos->y + 150 > smem.floor.pos.y) vel->y *= -1;
// Prep
S4 nclip = 0;
S4 orderingtbl_z = 0;
A2_S2 p; //???
S4 flag; //????
TapeBuilder tb = tb_make(slice_ut_arr(smem.MemTape));
// Draw Cube
if (0)
{
m3s2_rotation (& smem.cube.rot, & smem.tform_world);
m3s2_translation(& smem.tform_world, & smem.cube.pos);
m3s2_scale (& smem.tform_world, & smem.cube.scale);
// gte_matrix_set_rotation (& smem.tform_world);
gte_matrix_set_translation(& smem.tform_world);
for (U4 face_id = 0; face_id < Cube_num_faces; face_id += 1)
{
Poly_G4* quad = prim_alloc(Poly_G4); set_poly_g4(quad);
quad->c0 = rgb8(255, 0, 255);
quad->c1 = rgb8(255, 255, 0);
quad->c2 = rgb8( 0, 255, 255);
quad->c3 = rgb8( 0, 255, 0);
V4_S2* face = & smem.cube.faces[face_id];
V3_S2* p0 = & smem.cube.verts[face->x];
V3_S2* p1 = & smem.cube.verts[face->y];
V3_S2* p2 = & smem.cube.verts[face->z];
V3_S2* p3 = & smem.cube.verts[face->w];
nclip = rtp_avg_nclip_a4_v3s2(
p0, p1, p2, p3,
& quad->p0, & quad->p1, & quad->p2, & quad->p3,
& p, & orderingtbl_z, & flag
);
if (nclip <= 0) {
continue;
}
if ((orderingtbl_z > 0) && (orderingtbl_z < OrderingTbl_Len)) {
orderingtbl_add_primitive(ordering_buf[orderingtbl_z], quad);
}
}
// smem.cube.rot.x += 6;
// smem.cube.rot.y += 8;
// smem.cube.rot.z += 12;
smem.cube.rot.y += 30;
}
// Draw cube (tape method) - two triangles per face
if (1)
{
m3s2_rotation (& smem.cube.rot, & smem.tform_world);
m3s2_translation(& smem.tform_world, & smem.cube.pos);
m3s2_scale (& smem.tform_world, & smem.cube.scale);
gte_matrix_set_rotation (& smem.tform_world);
gte_matrix_set_translation(& smem.tform_world);
U4 prim_base = u4_(pa->buf[smem.active_buf_id]);
U4 prim_cursor = prim_base + pa->used;
tb.used = 0; tb_scope(& tb) {
tb_emit(& tb, rbind_cube_g4_face);
tb_data(& tb, prim_cursor);
tb_data(& tb, u4_(smem.cube.faces));
tb_data(& tb, u4_(smem.cube.verts));
tb_data(& tb, u4_(ordering_buf));
for (U4 i = 0; i < Cube_num_faces; i++) {
// Two triangles per quad face: (x,y,z) and (x,z,w)
tb_emit(& tb, cube_g4_face);
}
tb_emit(& tb, sync_primitive_arena);
tb_data(& tb, u4_(& pa->used));
tb_data(& tb, prim_base);
}
tape_run(tb_slice(tb));
smem.cube.rot.y += 30;
}
// Draw Floor
if (0)
{
m3s2_rotation (& smem.floor.rot, & smem.tform_world);
m3s2_translation(& smem.tform_world, & smem.floor.pos);
m3s2_scale (& smem.tform_world, & smem.floor.scale);
gte_matrix_set_rotation (& smem.tform_world);
gte_matrix_set_translation(& smem.tform_world);
for (U4 face_id = 0; face_id < Floor_num_faces; face_id += 1)
{
Poly_F3* tri = prim_alloc(Poly_F3); set_poly_f3(tri);
tri->color = rgb8(255, 255, 255);
V3_S2* face = & smem.floor.faces[face_id];
register V3_S2* p0 rgcc(R_T4) = & smem.floor.verts[face->x];
register V3_S2* p1 rgcc(R_T5) = & smem.floor.verts[face->y];
register V3_S2* p2 rgcc(R_T6) = & smem.floor.verts[face->z];
gte_load_v0(p0, R_T4);
/*
asm volatile( ".word " "%0" ", %1" : :
"i"(((op_lwc2 & OPCODE_MASK) << OPCODE_SHIFT) | ((R_T4 & REG_MASK) << RS_SHIFT) | ((gte_in_v0_xy & REG_MASK) << RT_SHIFT) | (0 & IMM_MASK)),
"i"(((op_lwc2 & OPCODE_MASK) << OPCODE_SHIFT) | ((R_T4 & REG_MASK) << RS_SHIFT) | ((gte_in_v0_z & REG_MASK) << RT_SHIFT) | (GTE_Z_Offset & IMM_MASK)),
"r"(p0) :
"$2", "$8", "$9", "$31", "memory"
);
*/
gte_load_v1(p1, R_T5);
gte_load_v2(p2, R_T6);
gte_rtpt();
gte_nclip();
gte_stotz(& nclip);
// nclip = rtp_avg_nclip_a3_v3s2(p0, p1, p2
// , & tri->p0, & tri->p1, & tri->p2
// , & p, & orderingtbl_z, & flag
// );
// if (nclip <= 0) {
// continue;
// }
if (nclip > 0 ) {
gte_stsxy3(& tri->p0, & tri->p1, & tri->p2);
gte_avsz3();
gte_stotz(& orderingtbl_z);
if ((orderingtbl_z > 0) && (orderingtbl_z < OrderingTbl_Len)) {
orderingtbl_add_primitive(ordering_buf[orderingtbl_z], tri);
}
}
}
smem.floor.rot.y += 5;
}
// Draw floor tape method
if (1)
{
m3s2_rotation (& smem.floor.rot, & smem.tform_world);
m3s2_translation(& smem.tform_world, & smem.floor.pos);
m3s2_scale (& smem.tform_world, & smem.floor.scale);
U4 prim_base = u4_(pa->buf[smem.active_buf_id]);
U4 prim_cursor = prim_base + pa->used;
// TODO(Ed): We should do a bounds check beforehand to confirm pa can hold all tris?
// The tape atoms in-flight should not need to care.
// Prepare the tape. (Push protocol to tape)
tb.used = 0; tb_scope(& tb) {
tb_emit(& tb, set_gte_world);
tb_data(& tb, u4_(& smem.tform_world));
tb_emit(& tb, rbind_floor_f3_face);
// TODO(Ed): Just use a single context struct ref
tb_data(& tb, prim_cursor);
tb_data(& tb, u4_(smem.floor.faces));
tb_data(& tb, u4_(smem.floor.verts));
tb_data(& tb, u4_(ordering_buf));
for (U4 i = 0; i < Floor_num_faces; i++) {
tb_emit(& tb, floor_f3_face);
}
// After floor_f3_face iterations complete, the primitive arena's used counter needs updating.
tb_emit(& tb, sync_primitive_arena);
tb_data(& tb, u4_(& pa->used));
tb_data(& tb, prim_base);
}
tape_run(tb_slice(tb));// Fire off the tape.
// C-side state (pa->used) has already been updated by the tape!
smem.floor.rot.y += 5;
}
// --- TAPE DIAGNOSTICS ---
if (0)
{
LP_ U4 mem_temp_tape[512]; FArena tape_arena; farena_init(& tape_arena, slice_ut_arr(mem_temp_tape));
TapeBuilder tb = tb_make_old(& tape_arena); tb_scope(& tb) {
// Skip set_gte_world atom for diagnostics to isolate the triangle loop
for (U4 i = 0; i < Floor_num_faces; i++) {
// tb_emit(& tb, code_diag_yield);
// tb_emit(& tb, code_diag_color);
// tb_emit(& tb, code_diag_gte);
}
}
B1* prim_cursor = (B1*)r_(pa->buf)[smem.active_buf_id] + pa->used;
tape_run(tb_slice(tb));
pa->used = (U4)prim_cursor - (U4)r_(pa->buf)[smem.active_buf_id];
}
}
GCC_OPTIMIZATION_ENABLE
int main(void)
{
smem = (SMemory){0};
smem.scratchpad = C_(U4_V, 0x1F800000);
smem.primitives.used = 0;
ent_cube128_init(& smem.cube.verts, & smem.cube.faces); {
Ent_Cube* cube = & smem.cube;
cube->rot = v3s2(0, 0, 0);
// cube->pos = v3s4(0, 0, 900);
cube->scale = v3s4_fp_one();
cube->accel = v3s4(0, 1, 0);
cube->pos = v3s4(0, -400, 1800);
}
ent_floor_init(& smem.floor.verts, & smem.floor.faces); {
Ent_Floor* floor = & smem.floor;
floor->rot = v3s2(0, 0, 0);
floor->pos = v3s4(0, 450, 1800);
floor->scale = v3s4_fp_one();
}
// gknown gp_screen_init();
gp_screen_init_c11(& smem.screen_buf, & smem.active_buf_id);
while (1) {
gknown S4* active_buf_id = & smem.active_buf_id;
gknown U4* ordering_buf = r_(smem.ordering_tbl)[active_buf_id[0]];
gknown PrimitiveArena* pa = & smem.primitives;
update(pa, ordering_buf);
render();
gp_display_frame(& smem.screen_buf, active_buf_id, ordering_buf, pa);
};
return 0;
}
+119
View File
@@ -0,0 +1,119 @@
#ifdef INTELLISENSE_DIRECTIVES
# pragma once
# include "duffle/dsl.h"
# include "duffle/math.h"
# include "duffle/gp.h"
#endif
enum {
PrimitiveBuff_Len = 4096,
OrderingTbl_Len = 2048
};
typedef Struct_(DrawEnv_Packed) { U4 tag; U4 code[15]; };
typedef Struct_(DrawEnv) {
Rect_S2 clip_area;
A2_S2 drawing_offset;
Rect_S2 texture_window;
S2 texture_page;
B1 flag_dither;
B1 flag_draw_on_display;
B1 enable_auto_clear;
RGB8 initial_bg_color;
DrawEnv_Packed dr_env; // reserved
};
typedef Struct_(DisplayEnv) {
Rect_S2 display_area;
Rect_S2 screen;
B1 vinterlace;
B1 color24;
B1 pad0;
B1 pad1;
};
typedef Array_(DrawEnv, 2);
typedef Array_(DisplayEnv, 2);
typedef Struct_(DoubleBuffer) {
A2_DrawEnv draw;
A2_DisplayEnv display;
};
#define ScreenRes_X 320
#define ScreenRes_Y 240
#define ScreenZ 320
#define ScreenRes_CenterX (ScreenRes_X >> 1)
#define ScreenRes_CenterY (ScreenRes_Y >> 1)
DisplayEnv* displayenv_init(DisplayEnv* env, S4 x, S4 y, S4 w, S4 h) __asm__("SetDefDispEnv");
DrawEnv* drawenv_init (DrawEnv* env, S4 x, S4 y, S4 w, S4 h) __asm__("SetDefDrawEnv");
DisplayEnv* displayenv_put(DisplayEnv* env) __asm__("PutDispEnv");
DrawEnv* drawenv_put (DrawEnv* env) __asm__("PutDrawEnv");
U4 geom_init(void) __asm__("InitGeom");
void geom_set_offset(U4 x, U4 y) __asm__("SetGeomOffset");
void geom_set_screen(U4 h) __asm__("SetGeomScreen");
U4* orderingtbl_clear_reverse(U4* ot, U4 len) __asm__("ClearOTagR");
U4 reset_graph(U4 mode) __asm__("ResetGraph");
void set_display_enabled(U4 mask) __asm__("SetDispMask");
U4 draw_sync(U4 mode) __asm__("DrawSync");
U4 vsync(U4 mode) __asm__("VSync");
void draw_orderingtbl(U4* buf) __asm__("DrawOTag");
typedef Struct_(Tile) {
U4 tag;
RGB8 color;
B1 code;
Rect_S2 rect;
};
/*
Linear Algebra
*/
M3_S2* m3s2_rotation (V3_S2* vec, M3_S2* mat) __asm__("RotMatrix");
M3_S2* m3s2_translation(M3_S2* mat, V3_S4* vec) __asm__("TransMatrix");
M3_S2* m3s2_scale (M3_S2* mat, V3_S4* vec) __asm__("ScaleMatrix");
// Rotation, Translation, Perspective
S4 rtp_v3s2_raw(V3_S2* vec, S4* xy, S4* pp, S4* flag) __asm__("RotTransPers");
FI_ S4 rtp_v3s2(V3_S2* vec, V2_S2* xy, A2_S2* pp, S4* flag) { return rtp_v3s2_raw(vec, C_(S4*R_, & xy->x), C_(S4*R_, pp), r_(flag)); }
S4 rtp_avg_nclip_a3_v3s2_raw(V3_S2* v0, V3_S2* v1, V3_S2* v2, S4* xy1, S4* xy2, S4* xy3, S4* pp, S4* otz, S4* flag) __asm__("RotAverageNclip3");
FI_ S4 rtp_avg_nclip_a3_v3s2(
V3_S2* v0, V3_S2* v1, V3_S2* v2,
V2_S2* xy0, V2_S2* xy1, V2_S2* xy2,
A2_S2* pp, S4* otz, S4* flag
){
return rtp_avg_nclip_a3_v3s2_raw(
v0, v1, v2,
C_(S4*R_, xy0), C_(S4*R_, xy1), C_(S4*R_, xy2),
C_(S4*R_, pp), C_(S4*R_, otz), C_(S4*R_, flag)
);
}
S4 rtp_avg_nclip_a4_v3s2_raw(V3_S2* v0, V3_S2* v1, V3_S2* v2, V3_S2* v3, S4* xy1, S4* xy2, S4* xy3, S4* xy4, S4* pp, S4* otz, S4* flag) __asm__("RotAverageNclip4");
FI_ S4 rtp_avg_nclip_a4_v3s2(
V3_S2* v0, V3_S2* v1, V3_S2* v2, V3_S2* v3,
V2_S2* xy0, V2_S2* xy1, V2_S2* xy2, V2_S2* xy3,
A2_S2* pp, S4* otz, S4* flag
){
return rtp_avg_nclip_a4_v3s2_raw(
v0, v1, v2, v3,
C_(S4*R_, xy0), C_(S4*R_, xy1), C_(S4*R_, xy2), C_(S4*R_, xy3),
C_(S4*R_, pp), C_(S4*R_, otz), C_(S4*R_, flag)
);
}
void gte_matrix_set_rotation (M3_S2* mat) __asm__("SetRotMatrix");
void gte_matrix_set_translation(M3_S2* mat) __asm__("SetTransMatrix");
enum {
fp_one = (1 << 12),
};
#define v3s4_fp_one() v3s4(fp_one, fp_one, fp_one)
+154
View File
@@ -0,0 +1,154 @@
#ifdef INTELLISENSE_DIRECTIVES
# include "duffle/gen/duffle.macs.h"
# include "duffle/gen/duffle.offsets.h"
# include "duffle/atom_dsl.h"
# include "duffle/lottes_tape.h"
# include "duffle/word_count.metadata.h"
# include "gen/hello_joypad.offsets.h"
# include "hello_joypad.h"
#endif
#pragma region MACs (Mips Atom components)
#pragma endregion MACs
#pragma region Baked Atoms
typedef Struct_(Binds_CubeTri) {
U4 PrimCursor;
V4_S2* FaceCursor;
V3_S2* VertBase;
U4* OtBase;
};
internal MipsAtom_(rbind_cube_g4_face) atom_info(atom_bind(Binds_CubeTri), atom_phase(cube_g4)
, atom_reads(R_TapePtr)
, atom_writes(R_PrimCursor, R_FaceCursor, R_VertBase, R_OtBase)
){
/* Pop 4 arguments from the tape directly into the workspace registers */
load_word(R_PrimCursor, R_TapePtr, O_(Binds_CubeTri,PrimCursor)),
load_word(R_FaceCursor, R_TapePtr, O_(Binds_CubeTri,FaceCursor)),
load_word(R_VertBase, R_TapePtr, O_(Binds_CubeTri,VertBase)),
load_word(R_OtBase, R_TapePtr, O_(Binds_CubeTri,OtBase)),
add_ui_self( R_TapePtr, S_(Binds_CubeTri)),
mac_yield()
};
// cube_g4_face — Draw one cube face (Gouraud-shaded quad) via the GTE tape pipeline
internal
MipsAtom_(cube_g4_face) atom_info(atom_phase(cube_g4),
atom_reads( R_PrimCursor, R_FaceCursor, R_VertBase, R_OtBase),
atom_writes(R_PrimCursor, R_FaceCursor)
){
load_half_u(R_T0, R_FaceCursor, 0 * S_(S2)),
load_half_u(R_T1, R_FaceCursor, 1 * S_(S2)),
load_half_u(R_T2, R_FaceCursor, 2 * S_(S2)),
load_half_u(R_T3, R_FaceCursor, 3 * S_(S2)),
mac_gte_load_tri_verts(R_T0, R_T1, R_T2),
nop2, gte_cmdw_rotate_translate_perspective_triple, // required cpu -> gte delay slot
gte_cmdw_nclip,
gte_mv_from_data_r(R_T0, C2_MAC0), nop,
branch_le_zero(R_T0, atom_offset(cull, cube_g4_face_exit)), nop,
store_word(R_0, R_PrimCursor, O_(Poly_G4, tag)),
shift_lleft(R_AT, R_T3, v3s2_byteoff), add_u(R_AT, R_AT, R_VertBase),
load_word(R_V0, R_AT, O_(V3_S2, x)), load_word(R_V1, R_AT, O_(V3_S2, z)),
gte_mv_to_data_r(R_V0, C2_VXY0), gte_mv_to_data_r(R_V1, C2_VZ0),
mac_gte_store_g4_p012(),
gte_cmdw_rotate_translate_perspective_single,
mac_gte_store_g4_p3(),
gte_cmdw_avg_sort_z4,
gte_mv_from_data_r(R_T1, C2_OTZ),
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, cube_g4_face_exit)), nop,
mac_insert_ot_tag_g4(),
mac_format_g4_color(
/* c0 magenta */ 0xFF, 0x00, 0xFF,
/* c1 yellow */ 0xFF, 0xFF, 0x00,
/* c2 cyan */ 0x00, 0xFF, 0xFF,
/* c3 green */ 0x00, 0xFF, 0x00),
// end: branch(bounds_chk)
// end: branch(cull)
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()
};
typedef Struct_(Binds_FloorTri) {
U4 PrimCursor;
V3_S2* FaceCursor;
V3_S2* VertBase;
U4* OtBase;
};
internal
MipsAtom_(rbind_floor_f3_face) atom_info(atom_bind(Binds_FloorTri), atom_phase(floor_f3)
, atom_reads(R_TapePtr)
, atom_writes(R_PrimCursor, R_FaceCursor, R_VertBase, R_OtBase)
){
/* Pop 4 arguments from the tape directly into the workspace registers */
load_word(R_PrimCursor, R_TapePtr, O_(Binds_FloorTri,PrimCursor)),
load_word(R_FaceCursor, R_TapePtr, O_(Binds_FloorTri,FaceCursor)),
load_word(R_VertBase, R_TapePtr, O_(Binds_FloorTri,VertBase)),
load_word(R_OtBase, R_TapePtr, O_(Binds_FloorTri,OtBase)),
add_ui_self( R_TapePtr, S_(Binds_FloorTri)),
mac_yield()
};
// atom_dbg_skip
internal
MipsAtom_(floor_f3_face) atom_info(atom_phase(floor_f3)
, atom_reads( R_PrimCursor, R_FaceCursor, R_VertBase, R_OtBase)
, atom_writes(R_PrimCursor, R_FaceCursor)
) {
mac_load_tri_indices( R_T0, R_T1, R_T2),
mac_gte_load_tri_verts(R_T0, R_T1, R_T2),
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.
/* Format Primitive */
mac_gte_store_f3(),
/* Calculate Depth */
gte_avg_sort_z3,
gte_mv_from_data_r(R_T1, C2_OTZ),
/* 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,
mac_format_f3_color(0xFF, 0xFF, 0xFF), // RGB-form (R=FF, G=FF, B=FF = white)
mac_insert_ot_tag_f3(), /* Insert into Ordering Table Linked List */
add_ui_self(R_PrimCursor, S_(Poly_F3)), /* Advance Prim Cursor (5 words) */
// Note(Ed): No bounds checking, should be checked before atom runs.
/* 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()
};
typedef Struct_(Binds_SyncPrimitiveArena) { U4 used; U4 cursor; };
internal MipsAtom_(sync_primitive_arena) atom_info(atom_bind(Binds_SyncPrimitiveArena)
, atom_reads( R_TapePtr, R_PrimCursor)
, atom_writes(R_TapePtr)
){
load_word(R_AT, R_TapePtr, O_(Binds_SyncPrimitiveArena,used)),
load_word(R_T0, R_TapePtr, O_(Binds_SyncPrimitiveArena,cursor)),
add_ui_self( R_TapePtr, S_(Binds_SyncPrimitiveArena)),
/* Calculate byte offset and store directly back to RAM */
sub_u( R_T0, R_PrimCursor, R_T0), // R_T0 = R_PrimCursor - binds.cursor
store_word(R_T0, R_AT, 0), // R_AT[0] = R_T0
mac_yield()
};
#pragma endregion Baked Atoms
+107 -3
View File
@@ -350,10 +350,10 @@ function build-graphis_hello {
} }
# build-graphis_hello # build-graphis_hello
function build-gte_hello { function build-hello_gte {
$includes += @() $includes += @()
$path_module = join-path $path_code 'gte_hello' $path_module = join-path $path_code 'hello_gte'
$path_duffle = join-path $path_code 'duffle' $path_duffle = join-path $path_code 'duffle'
$path_atom_metadata = join-path $path_duffle 'word_count.metadata.h' $path_atom_metadata = join-path $path_duffle 'word_count.metadata.h'
$path_build_gen = join-path $path_build 'gen' $path_build_gen = join-path $path_build 'gen'
@@ -458,8 +458,112 @@ function build-gte_hello {
} }
} }
} }
build-gte_hello # build-hello_gte
function build-hello_joypad {
$includes += @()
$path_module = join-path $path_code 'hello_joypad'
$path_duffle = join-path $path_code 'duffle'
$path_atom_metadata = join-path $path_duffle 'word_count.metadata.h'
$path_build_gen = join-path $path_build 'gen'
$src_c = join-path $path_module 'hello_joypad.c'
ps1-meta -unity_root $src_c -metadata $path_atom_metadata -out_root $path_build_gen
$assemble_args = @()
$assemble_args += $f_debug
$assemble_args += $f_optimize_none
$assemble_args += ($f_include + $path_code)
$src_asm_crt = join-path $path_nugget_common 'crt0/crt0.s'
$module_asm_crt = join-path $path_build 'crt0.o'
assemble-unit $src_asm_crt $module_asm_crt $includes $assemble_args
$module_c = join-path $path_build 'hello_joypad_c.o'
$compile_args = @()
$compile_args += $f_debug
$compile_args += $f_optimize_none
# $compile_args += $f_optimize_intrinsics
# $compile_args += $f_optimize_size
# $compile_args += $f_optimize_debug
$compile_args += ($f_include + $path_code)
compile-unit $src_c $module_c $includes $compile_args
$elf = join-path $path_build 'hello_joypad.elf'
$exe = join-path $path_build 'hello_joypad.ps-exe'
$link_args = @()
$link_args += $f_debug
# $link_args += $f_optimize_size
$link_modules = @(
$module_asm_crt,
$module_c
)
link-modules $link_modules $elf $link_args
make-binary $elf $exe
# Post-link: gdb-runtime + dwarf-injection in a single Lua invocation (one luajit cold start).
ps1-meta -unity_root $src_c -metadata $path_atom_metadata -out_root $path_build_gen -passes @('--post-link') ` -extra_args @('--elf', $elf)
$dwarfLineBin = join-path $path_build_gen 'hello_joypad.dwarf_line.bin'
$dwarfArangesBin = join-path $path_build_gen 'hello_joypad.dwarf_aranges.bin'
$dwarfRnglistsBin = join-path $path_build_gen 'hello_joypad.dwarf_rnglists.bin'
$injectElf = join-path $path_build 'hello_joypad.dwarf-injected.elf'
if ((Test-Path $dwarfLineBin) -and (Test-Path $dwarfArangesBin) -and (Test-Path $dwarfRnglistsBin))
{
Write-Host "[build] DWARF-injecting $elf -> $injectElf"
Copy-Item -LiteralPath $elf -Destination $injectElf -Force
# Objcopy call: 3x --update-section for (line, aranges, rnglists).
$f_args = @(
"--update-section=.debug_line=$dwarfLineBin",
"--update-section=.debug_aranges=$dwarfArangesBin",
"--update-section=.debug_rnglists=$dwarfRnglistsBin"
)
& $Objcopy @f_args $injectElf 2>&1 | Out-Null
if ($LASTEXITCODE -ne 0) {
Write-Warning "[build] objcopy F' splice failed (exit $LASTEXITCODE); removing $injectElf"
Remove-Item -LiteralPath $injectElf -ErrorAction SilentlyContinue
return;
}
$dwarfInfoBin = join-path $path_build_gen 'hello_joypad.dwarf_info.bin'
$dwarfAbbrevBin = join-path $path_build_gen 'hello_joypad.dwarf_abbrev.bin'
$dwarfStrBin = join-path $path_build_gen 'hello_joypad.dwarf_str.bin'
$dwarfLocBin = join-path $path_build_gen 'hello_joypad.dwarf_loc.bin'
$dwarfLoclistsBin = join-path $path_build_gen 'hello_joypad.dwarf_loclists.bin'
$g_args = @(
"--update-section=.debug_info=$dwarfInfoBin",
"--update-section=.debug_abbrev=$dwarfAbbrevBin",
"--update-section=.debug_str=$dwarfStrBin",
"--add-section=.debug_loc=$dwarfLocBin",
"--add-section=.debug_loclists=$dwarfLoclistsBin"
)
& $Objcopy @g_args $injectElf 2>&1 | Out-Null
if ($LASTEXITCODE -ne 0) {
Write-Warning "[build] objcopy G' splice failed (exit $LASTEXITCODE); removing $injectElf"
Remove-Item -LiteralPath $injectElf -ErrorAction SilentlyContinue
return;
}
# Baked atoms execute from RAM but are emitted as C data arrays, so their ELF sections lack SHF_EXECINSTR.
# GDB discards line rows for non-code sections. Mark only the debug-copy sections executable.
# The original ELF and PS-EXE remain byte/flag unchanged.
& $Objcopy `
--set-section-flags ".rodata=alloc,load,readonly,code,contents" `
--set-section-flags ".data=alloc,load,data,code,contents" `
$injectElf 2>&1 | Out-Null
if ($LASTEXITCODE -ne 0) {
Write-Warning "[build] atom-section flag update failed (exit $LASTEXITCODE); removing $injectElf"
Remove-Item -LiteralPath $injectElf -ErrorAction SilentlyContinue
}
else {
Write-Host "[build] DWARF-injected ELF: $injectElf"
}
}
}
build-hello_joypad
# NO idea if this works yet... # NO idea if this works yet...
function Send-ToEmulator { param( [string]$exePath ) function Send-ToEmulator { param( [string]$exePath )
+17 -38
View File
@@ -1180,10 +1180,9 @@ M.GTE_COMMAND_INPUTS = {
-- * "mac_result" : generic MAC output (nclip, op, mvmva) -- * "mac_result" : generic MAC output (nclip, op, mvmva)
-- --
-- Consumers: -- Consumers:
-- * passes/static_analysis.lua::analyze_hardware_relations (the walker reads this after a GTE command to update -- * passes/static_analysis.lua::analyze_hardware_relations (the walker reads this after a GTE command to update `forward_state.post_command_roles` for `gte_role_mismatch`).
-- `forward_state.post_command_roles` for `gte_result_position`). -- * passes/static_analysis.lua::check_gte_role_mismatch (per-atom CHECK_RULES reader; renders role mismatches).
-- * passes/static_analysis.lua::check_gte_result_position (per-atom CHECK_RULES reader; renders role mismatches). -- This table is consumed by the hardware-relation analyzer and the gte_role_mismatch check.
-- This table is consumed by the hardware-relation analyzer and result-position check.
M.GTE_COMMAND_OUTPUTS = { M.GTE_COMMAND_OUTPUTS = {
-- RTPS: writes one screen coordinate (the perspective-divide result) into C2_SXY2. -- RTPS: writes one screen coordinate (the perspective-divide result) into C2_SXY2.
-- The FIFO side effects leave SXY0 / SXY1 untouched, so `latest_screen_xy` is C2_SXY2. -- The FIFO side effects leave SXY0 / SXY1 untouched, so `latest_screen_xy` is C2_SXY2.
@@ -1293,32 +1292,8 @@ M.GTE_COMMAND_LATCH_WINDOWS = {
}, },
} }
-- GTE component result contracts (immutable; keyed by bare component name). -- GTE component result contracts were removed: the `_post_<cmd>` naming convention was a soft convention
-- -- (the user did not want it formalized via static-analysis enforcement). A proper `atom_info` directive for ordering semantics is a future TODO.
-- Register-role claims that the `_post_<cmd>` suffix alone cannot infer live here.
-- The bare name (the component name stripped of the `_post_<cmd>` suffix) is the key; the row carries the expected
-- command, the expected role, and the expected C2 register.
--
-- Known rows:
-- * `gte_store_g4_p3_post_rtps`: post-RTPS polygon-emit slot reads the newest projected screen coordinate from C2_SXY2.
-- C2_SXY0 is wrong (C2_SXY0 is an older FIFO entry, never the newest post-RTPS result).
--
-- Unknown `_post_<cmd>` components (a `<name>_post_<cmd>`-suffixed component whose bare `<name>` is not a row key) emit one
-- `table_gap` info finding so downstream consumers can detect when the contract table is incomplete for an authored atom body.
--
-- Consumers:
-- * passes/static_analysis.lua::check_gte_result_position (renders result-position findings).
-- * passes/static_analysis.lua::emit_table_gap_warning (called once per atom body; surfaces the missing-row diagnostic).
-- This table is consumed by the result-position check.
M.GTE_COMPONENT_RESULT_CONTRACTS = {
-- Post-RTPS g4 p3 store contract: writes the latest screen XY (C2_SXY2) into the primitive's p3 slot.
-- Reading from C2_SXY0 is a semantic mismatch — C2_SXY0 is the oldest post-RTPS SXY, not the newest one.
["gte_store_g4_p3_post_rtps"] = {
command = "gte_cmdw_rtps",
role = "latest_screen_xy",
register = "C2_SXY2",
},
}
-- Operand-class table for the COP2->GPR load-delay check. -- Operand-class table for the COP2->GPR load-delay check.
-- --
@@ -1447,6 +1422,7 @@ M.GP0_CMD_BY_SHAPE = {
["g4"] = 0x38, ["gt4"] = 0x3C, ["g4"] = 0x38, ["gt4"] = 0x3C,
} }
-- TODO(Ed): REMOVE THIS HARDCODE, THIS SHOULD BE RESOLVED AUTOMATICALLY
-- Per-macro prim-buffer contribution: how many 32-bit words each macro writes to the primitive being built in main RAM. -- Per-macro prim-buffer contribution: how many 32-bit words each macro writes to the primitive being built in main RAM.
-- (This counts RAM-side prim-buffer words, not .text instruction words.) -- (This counts RAM-side prim-buffer words, not .text instruction words.)
-- The sum across `mac_format_X_color` + `mac_gte_store_X_post_*` + `mac_insert_ot_tag_X` calls in an atom body must equal -- The sum across `mac_format_X_color` + `mac_gte_store_X_post_*` + `mac_insert_ot_tag_X` calls in an atom body must equal
@@ -1455,10 +1431,10 @@ M.GP0_MACRO_CONTRIB = {
["mac_format_f3_color"] = 1, ["mac_format_f3_color"] = 1,
["mac_format_g3_color"] = 3, ["mac_format_g3_color"] = 3,
["mac_format_g4_color"] = 4, ["mac_format_g4_color"] = 4,
["mac_gte_store_f3_post_rtpt"] = 3, ["mac_gte_store_f3"] = 3,
["mac_gte_store_g3_post_rtpt"] = 3, ["mac_gte_store_g3"] = 3,
["mac_gte_store_g4_p012_post_rtpt_pre_rtps"] = 3, ["mac_gte_store_g4_p012"] = 3,
["mac_gte_store_g4_p3_post_rtps"] = 1, ["mac_gte_store_g4_p3"] = 1,
["mac_insert_ot_tag_f3"] = 1, ["mac_insert_ot_tag_f3"] = 1,
["mac_insert_ot_tag_g4"] = 1, ["mac_insert_ot_tag_g4"] = 1,
} }
@@ -1583,6 +1559,8 @@ M.INSTRUCTION_LATENCY = {
["gte_load_v1"] = 2, ["gte_load_v1"] = 2,
["gte_load_v2"] = 2, ["gte_load_v2"] = 2,
["gte_load_v0v1v2"] = 6, ["gte_load_v0v1v2"] = 6,
-- TODO(Ed): REMOVE THIS HARDCODE, THIS SHOULD BE RESOLVED AUTOMATICALLY
-- mac_* helpers (cycle cost = sum of the expanded instructions) -- mac_* helpers (cycle cost = sum of the expanded instructions)
-- mac_yield transfers control; cycle budget is 0 (the next atom absorbs the cost). -- mac_yield transfers control; cycle budget is 0 (the next atom absorbs the cost).
["mac_yield"] = 0, ["mac_yield"] = 0,
@@ -1591,12 +1569,13 @@ M.INSTRUCTION_LATENCY = {
["mac_format_g4_color"] = 12, -- 4 x mac_pack_color_word ["mac_format_g4_color"] = 12, -- 4 x mac_pack_color_word
["mac_load_tri_indices"] = 3, -- 3 x lhu ["mac_load_tri_indices"] = 3, -- 3 x lhu
["mac_gte_load_tri_verts"] = 18, -- 3 x {sll, addu, lw, lw, mtc2, mtc2} ["mac_gte_load_tri_verts"] = 18, -- 3 x {sll, addu, lw, lw, mtc2, mtc2}
["mac_gte_store_f3_post_rtpt"] = 3, ["mac_gte_store_f3"] = 3,
["mac_gte_store_g3_post_rtpt"] = 3, ["mac_gte_store_g3"] = 3,
["mac_gte_store_g4_p012_post_rtpt_pre_rtps"] = 3, ["mac_gte_store_g4_p012"] = 3,
["mac_gte_store_g4_p3_post_rtps"] = 1, ["mac_gte_store_g4_p3"] = 1,
["mac_insert_ot_tag_f3"] = 11, -- 11 .word slots in the macro body ["mac_insert_ot_tag_f3"] = 11, -- 11 .word slots in the macro body
["mac_insert_ot_tag_g4"] = 11, ["mac_insert_ot_tag_g4"] = 11,
-- Annotation markers (emit no code; pure metaprogram hints) -- Annotation markers (emit no code; pure metaprogram hints)
["atom_label"] = 0, ["atom_label"] = 0,
["atom_offset"] = 0, ["atom_offset"] = 0,
+13 -13
View File
@@ -3,16 +3,16 @@
# Wrapper for the tape-atom step-debug helpers. # Wrapper for the tape-atom step-debug helpers.
# The 9 user commands are defined here as STUBS (degraded-state messages). # The 9 user commands are defined here as STUBS (degraded-state messages).
# The real implementations + the per-atom data tables are emitted by `passes/atoms_source_map.lua` # The real implementations + the per-atom data tables are emitted by `passes/atoms_source_map.lua`
# (post-link invocation: `ps1_meta.lua --atoms-source-map --gdb-runtime --elf <elf>`) into `build/gen/gdb_tape_atoms_runtime.gdb`. # (post-link invocation: `ps1_meta.lua --atoms-source-map --gdb-runtime --elf <elf>`) into `build/gdb_tape_atoms_runtime.gdb`.
# Sourcing that file RE-DEFINES the commands with real implementations. # Sourcing that file RE-DEFINES the commands with real implementations.
# #
# If `build/gen/gdb_tape_atoms_runtime.gdb` is missing or stale, the stubs remain (E1: no source map). # If `build/gdb_tape_atoms_runtime.gdb` is missing or stale, the stubs remain (E1: no source map).
# The user just needs to re-run `build_psyq.ps1` to regenerate. # The user just needs to re-run `build_psyq.ps1` to regenerate.
# ── Stub commands (defined here so they're always present, even if the runtime file is missing). The runtime file overrides these if sourced. ── # ?? Stub commands (defined here so they're always present, even if the runtime file is missing). The runtime file overrides these if sourced. ??
define tape_atoms define tape_atoms
echo "[gdb_tape_atoms] STUB: runtime file build/gen/gdb_tape_atoms_runtime.gdb not found." echo "[gdb_tape_atoms] STUB: runtime file build/gdb_tape_atoms_runtime.gdb not found."
echo "[gdb_tape_atoms] STUB: run .\\build_psyq.ps1 to regenerate, then re-source this file." echo "[gdb_tape_atoms] STUB: run .\\build_psyq.ps1 to regenerate, then re-source this file."
end end
document tape_atoms document tape_atoms
@@ -21,35 +21,35 @@ document tape_atoms
end end
define break_atom define break_atom
echo "[gdb_tape_atoms] STUB: build/gen/gdb_tape_atoms_runtime.gdb not sourced. Run build_psyq.ps1." echo "[gdb_tape_atoms] STUB: build/gdb_tape_atoms_runtime.gdb not sourced. Run build_psyq.ps1."
end end
document break_atom document break_atom
Set a breakpoint at the start of tape atom <name>. STUB state. Set a breakpoint at the start of tape atom <name>. STUB state.
end end
define step_atom define step_atom
echo "[gdb_tape_atoms] STUB: build/gen/gdb_tape_atoms_runtime.gdb not sourced. Run build_psyq.ps1." echo "[gdb_tape_atoms] STUB: build/gdb_tape_atoms_runtime.gdb not sourced. Run build_psyq.ps1."
end end
document step_atom document step_atom
Resume execution until the next atom boundary. STUB state. Resume execution until the next atom boundary. STUB state.
end end
define next_atom define next_atom
echo "[gdb_tape_atoms] STUB: build/gen/gdb_tape_atoms_runtime.gdb not sourced. Run build_psyq.ps1." echo "[gdb_tape_atoms] STUB: build/gdb_tape_atoms_runtime.gdb not sourced. Run build_psyq.ps1."
end end
document next_atom document next_atom
Alias for step_atom. STUB state. Alias for step_atom. STUB state.
end end
define where_in_atom define where_in_atom
echo "[gdb_tape_atoms] STUB: build/gen/gdb_tape_atoms_runtime.gdb not sourced. Run build_psyq.ps1." echo "[gdb_tape_atoms] STUB: build/gdb_tape_atoms_runtime.gdb not sourced. Run build_psyq.ps1."
end end
document where_in_atom document where_in_atom
Report current atom name, .rodata addr, word offset, and source line (if known). STUB state. Report current atom name, .rodata addr, word offset, and source line (if known). STUB state.
end end
define stepi_inside_atom define stepi_inside_atom
echo "[gdb_tape_atoms] STUB: build/gen/gdb_tape_atoms_runtime.gdb not sourced. Run build_psyq.ps1." echo "[gdb_tape_atoms] STUB: build/gdb_tape_atoms_runtime.gdb not sourced. Run build_psyq.ps1."
end end
document stepi_inside_atom document stepi_inside_atom
One MIPS-instruction step, then where_in_atom. STUB state. One MIPS-instruction step, then where_in_atom. STUB state.
@@ -89,17 +89,17 @@ document wave_ctx
end end
# ── Source the runtime file (re-defines commands with real impls + data). ── # ?? Source the runtime file (re-defines commands with real impls + data). ??
# Try to source from project-root-relative path first (the typical case). # Try to source from project-root-relative path first (the typical case).
# If the user is in a different CWD, the source will fail and stubs remain. # If the user is in a different CWD, the source will fail and stubs remain.
# The runtime file path is computed relative to the ELF's source map convention (build/gen/gdb_tape_atoms_runtime.gdb). # The runtime file path is computed relative to the ELF's source map convention (build/gdb_tape_atoms_runtime.gdb).
echo [gdb_tape_atoms] Wrapper loaded. Sourcing runtime file... echo [gdb_tape_atoms] Wrapper loaded. Sourcing runtime file...
# Suppress the "Redefine command" prompts that would otherwise appear when the runtime file overrides the 9 stub commands defined above. # Suppress the "Redefine command" prompts that would otherwise appear when the runtime file overrides the 9 stub commands defined above.
# The runtime's `define` blocks are intended to overwrite there's no ambiguity to confirm. # The runtime's `define` blocks are intended to overwrite ? there's no ambiguity to confirm.
set confirm off set confirm off
# Source the runtime file (re-defines commands with real impls + data). # Source the runtime file (re-defines commands with real impls + data).
source build/gen/gdb_tape_atoms_runtime.gdb source build/gdb_tape_atoms_runtime.gdb
set confirm on set confirm on
echo [gdb_tape_atoms] Runtime sourced successfully (9 commands now have real implementations). echo [gdb_tape_atoms] Runtime sourced successfully (9 commands now have real implementations).
-46
View File
@@ -7,18 +7,11 @@
--- ---
--- Ownership: the canonical `ctx.shared.corpus` supplies cross-source registries, while each `src.scan` supplies its source's declarations and bodies. --- Ownership: the canonical `ctx.shared.corpus` supplies cross-source registries, while each `src.scan` supplies its source's declarations and bodies.
--- A context without `ctx.shared.corpus` is rejected with an explicit canonical-corpus message. --- A context without `ctx.shared.corpus` is rejected with an explicit canonical-corpus message.
---
--- Writes `<ctx.out_root>/<dir_basename>.errors.h` once per module, with `#error` directives for findings that the C compile surfaces.
--- `passes/report.lua` renders annotations.txt from `corpus.sources_by_dir`, re-validating each source through `M.validate()`.
---
--- **Conventions**: tabs (1/level), EmmyLua annotations, no regex, Lua 5.3 compatible.
-- Bootstrap follows the entry scripts; `scripts/duffle_paths.lua` sets package.path and package.cpath. See `ps1_meta.lua` for the rationale. -- Bootstrap follows the entry scripts; `scripts/duffle_paths.lua` sets package.path and package.cpath. See `ps1_meta.lua` for the rationale.
-- `debug.getinfo(1, "S").source` locates this file for standalone and orchestrated runs, then `duffle_paths.lua` returns the loaded `duffle` module. -- `debug.getinfo(1, "S").source` locates this file for standalone and orchestrated runs, then `duffle_paths.lua` returns the loaded `duffle` module.
local _bootstrap_dir = debug.getinfo(1, "S").source:match("^@?(.*[/\\])") or "./" local _bootstrap_dir = debug.getinfo(1, "S").source:match("^@?(.*[/\\])") or "./"
local duffle = dofile(_bootstrap_dir .. "../duffle_paths.lua") local duffle = dofile(_bootstrap_dir .. "../duffle_paths.lua")
local write_file = duffle.write_file
local ensure_dir = duffle.ensure_dir
-- The annotation pass reads the source-derived registries from scan_source: -- The annotation pass reads the source-derived registries from scan_source:
-- * pipe_ctx.register_alias_registry — for atom_dbg_reg_default(R_X, ...) and atom_reg_types(R_X, ...) member-identity checks -- * pipe_ctx.register_alias_registry — for atom_dbg_reg_default(R_X, ...) and atom_reg_types(R_X, ...) member-identity checks
@@ -605,40 +598,6 @@ local function validate(ctx, src, corpus_pipe_ctx)
} }
end end
-- ════════════════════════════════════════════════════════════════════════════
-- Per-DIRECTORY (per-module) output: errors.h + annotations.txt
-- ════════════════════════════════════════════════════════════════════════════
--- Render `<dir_basename>.errors.h` with `#error` directives for every error found across all sources in the directory.
--- Empty directories (no errors, no atoms) produce no file.
local function emit_module_errors_h(ctx, dir_basename, atoms_count, errors, sources)
if atoms_count == 0 and #errors == 0 then
return nil
end
local out_path = ctx.out_root .. "/" .. dir_basename .. ".errors.h"
local lines = {
"// Auto-generated by ps1_meta.lua (passes/annotation.lua) — DO NOT EDIT",
string.format("// Module: %s Sources: %d", dir_basename, #sources),
"#pragma once",
"",
}
if #errors == 0 then
lines[#lines + 1] = "// annotation pass OK"
else
for _, e in ipairs(errors) do
local src_tag = ""
if e.source then
local src_name = e.source:match("([^/\\]+)$") or e.source
src_tag = src_name .. ": "
end
lines[#lines + 1] = string.format('#error "%s%s (line %d)"', src_tag, e.msg, e.line)
end
end
ensure_dir(ctx.out_root)
write_file(out_path, table.concat(lines, "\n") .. "\n")
return out_path
end
-- ════════════════════════════════════════════════════════════════════════════ -- ════════════════════════════════════════════════════════════════════════════
-- M.run — orchestrator entry -- M.run — orchestrator entry
-- ════════════════════════════════════════════════════════════════════════════ -- ════════════════════════════════════════════════════════════════════════════
@@ -683,11 +642,6 @@ function M.run(ctx)
warnings [#warnings + 1] = { line = w.line, msg = w.msg } warnings [#warnings + 1] = { line = w.line, msg = w.msg }
end end
end end
local err_path = emit_module_errors_h(ctx, dir_basename, dir_atoms, dir_errors, dir_sources)
if err_path then
table.insert(outputs, { errors_h = err_path })
end
end end
return { outputs = outputs, errors = errors, warnings = warnings } return { outputs = outputs, errors = errors, warnings = warnings }
+83 -46
View File
@@ -4,20 +4,18 @@
--- `passes/dwarf_injection.lua` (synthesizes DW_TAG_inlined_subroutine + per-word line program rows) and the gdb-runtime --- `passes/dwarf_injection.lua` (synthesizes DW_TAG_inlined_subroutine + per-word line program rows) and the gdb-runtime
--- wrapper at `scripts/gdb/gdb_tape_atoms.gdb` (loads the source map via `source <path>`). --- wrapper at `scripts/gdb/gdb_tape_atoms.gdb` (loads the source map via `source <path>`).
--- ---
--- Inputs from `atom.paths`: the ordered `items` stream, dense `word_events`, `invocations` views. Outputs: one --- Inputs from `atom.paths`: the ordered `items` stream, dense `word_events`, `invocations` views. Outputs:
--- `WORD N LINE L TEXT T` line per emitted `.word`, plus the per-word provenance form that DWARF synthesis consumes. --- one `WORD N LINE L TEXT T` line per emitted `.word`, plus the per-word provenance form that DWARF synthesis consumes.
--- ---
--- **Two output forms** (per the workspace's per-emission-form pattern from `guide_metaprogram_ssdl.md`): --- Two output forms:
--- 1. **Sourcemap.txt form** — `<out_root>/<basename>.atoms.sourcemap.txt`. Format-version-tagged for forward-compat. --- 1. Markdown form: Handled by `passes/report.lua` (writes `<module>.atoms.md`).
--- Lives in `<out_root>/` (build/gen). Mirrors the convention used by `annotation.lua` --- The render functions `render_source_map` + `render_provenance` are exported for `report.lua` to call directly.
--- (`<out_root>/<basename>.errors.h`) and `static_analysis.lua` (`<out_root>/<basename>.static_analysis.txt`).
--- Compile artifacts (`*.macs.h`, `*.offsets.h`) stay in `<source_dir>/gen/`. --- Compile artifacts (`*.macs.h`, `*.offsets.h`) stay in `<source_dir>/gen/`.
--- 2. **gdb-runtime form** — `<ctx.out_root>/gdb_tape_atoms_runtime.gdb`. A pure gdb command script — addresses come --- 2. `gdb_tape_atoms_runtime.gdb`: Post-link opt-in (`ctx.flags.gdb_runtime`),
--- from `nm`, the 9 user commands are static `define ... end` blocks. Emitted when `ctx.flags.gdb_runtime` is true --- so the gdb wrapper script + the generated runtime script share the same canonical location.
--- AND `ctx.flags.elf_path` points to an existing ELF. Useful for `gdb-multiarch --without-python` users --- Triggered by `--post-link` or `--gdb-runtime`.
--- (the common case on Windows MinGW builds) — `source <path>` loads it with no Python / Tcl / Guile required.
--- ---
--- **Output format** (sourcemap.txt form): --- Output forma (sourcemap.txt form):
--- ``` --- ```
--- # FORMAT_VERSION 1 --- # FORMAT_VERSION 1
--- # auto-generated by ps1_meta.lua (passes/atoms_source_map.lua) — DO NOT EDIT --- # auto-generated by ps1_meta.lua (passes/atoms_source_map.lua) — DO NOT EDIT
@@ -32,8 +30,6 @@
--- ``` --- ```
--- ---
--- Marker records are zero-width in `atom.paths.items`, so they emit no WORD rows in the dense word view. --- Marker records are zero-width in `atom.paths.items`, so they emit no WORD rows in the dense word view.
---
--- **Conventions:** tabs (1/level), EmmyLua annotations, no regex, Lua 5.3 compatible.
-- ════════════════════════════════════════════════════════════════════════════ -- ════════════════════════════════════════════════════════════════════════════
-- Module-scope requires + package.path setup -- Module-scope requires + package.path setup
@@ -458,7 +454,22 @@ local function emit_gdb_runtime(ctx)
-- Confirmation line for the source operator. -- Confirmation line for the source operator.
lines[#lines + 1] = 'printf "[gdb_tape_atoms] runtime loaded %d atoms from %s\\n", $__atom_count, $__elf_path' lines[#lines + 1] = 'printf "[gdb_tape_atoms] runtime loaded %d atoms from %s\\n", $__atom_count, $__elf_path'
local out_path = ctx.out_root .. "/gdb_tape_atoms_runtime.gdb" local out_path
-- Move out of `<out_root>/gdb_tape_atoms_runtime.gdb` to `<out_root>/../gdb_tape_atoms_runtime.gdb` when the conventional `<out_root>` is `<build>/gen`
-- (any equivalent spelling — relative, absolute backslash, absolute forward-slash, trailing-separator variants).
-- This puts the gdb runtime alongside the ELF at `build/` rather than under the report subdir.
local function ends_with_gen_dir(p)
if type(p) ~= "string" then return false end
return p:match("[/\\]gen[/\\]?$") ~= nil or p == "build/gen" or p == "build\\gen"
end
if ends_with_gen_dir(ctx.out_root) then
-- Strip the trailing `/gen` segment, then write the runtime script under `build/`.
-- e.g. "C:/projects/Pikuma/ps1/build/gen" -> "C:/projects/Pikuma/ps1/build".
local parent = ctx.out_root:gsub("[/\\]gen[/\\]?$", "")
out_path = parent .. "/gdb_tape_atoms_runtime.gdb"
else
out_path = ctx.out_root .. "/gdb_tape_atoms_runtime.gdb"
end
duffle.ensure_dir(duffle.dirname(out_path)) duffle.ensure_dir(duffle.dirname(out_path))
duffle.write_file_lf(out_path, table.concat(lines, "\n") .. "\n") duffle.write_file_lf(out_path, table.concat(lines, "\n") .. "\n")
-- io.stderr:write(string.format("[atoms_source_map] wrote %s (%d atoms)\n", out_path, #matched)) -- io.stderr:write(string.format("[atoms_source_map] wrote %s (%d atoms)\n", out_path, #matched))
@@ -470,10 +481,62 @@ end
local M = {} local M = {}
--- Pass entry. For each source that declares at least one `MipsAtom_(name)` / `MipsCode code_<name>`, emit two files -- Expose the pure render functions so `report.lua` and the focused tests can call them directly without triggering the file-emit path.
--- in `<out_root>/`: `<basename>.atoms.sourcemap.txt` (per-word call-site map) and `<basename>.atoms.provenance.txt` M.render_source_map = render_source_map
--- (per-word definition + body line, resolved via the outermost `mac_X(...)` invocation). When `ctx.flags.gdb_runtime` M.render_provenance = render_provenance
--- is true and `ctx.flags.elf_path` exists, also emit the post-link gdb script `<ctx.out_root>/gdb_tape_atoms_runtime.gdb`.
--- Render ONE atom's sourcemap stanza.
--- @param atom table -- atom record (must have `atom.paths` populated)
--- @return string
function M.render_atom_source_map(atom)
assert(type(atom) == "table", "render_atom_source_map: atom must be a table")
assert(type(atom.paths) == "table", "render_atom_source_map: atom.paths must be a table")
local entries, total = canonical_word_entries(atom)
local lines = {}
lines[#lines + 1] = string.format("ATOM %s %d", (atom.raw_name or atom.name), total)
for _, entry in ipairs(entries) do
lines[#lines + 1] = string.format("WORD %d LINE %d TEXT %s",
entry.pos, entry.line, entry.text)
end
lines[#lines + 1] = "ENDATOM"
return table.concat(lines, "\n") .. "\n"
end
--- Render ONE atom's provenance stanza — no per-file format header, no enumeration of other atoms.
---
--- `rel_path` is the source path (forward-slashes) embedded in every `CALL` line.
--- The .md caller (report.lua) is expected to derive this once per `## <source>` heading and pass it down for each atom in that source.
--- @param atom table -- atom record (must have `atom.paths` populated)
--- @param wc table -- identity alias of `corpus.word_counts`
--- @param rel_path string -- source path (forward-slashes) for `CALL` fields
--- @return string
function M.render_atom_provenance(atom, wc, rel_path)
assert(type(atom) == "table", "render_atom_provenance: atom must be a table")
assert(type(atom.paths) == "table", "render_atom_provenance: atom.paths must be a table")
assert(type(rel_path) == "string", "render_atom_provenance: rel_path must be a string")
local entries, total = canonical_word_entries(atom)
local lines = {}
lines[#lines + 1] = string.format("ATOM %s %d", (atom.raw_name or atom.name), total)
for _, entry in ipairs(entries) do
local inv = entry.invocation
local macro_count = inv and wc and wc["mac_" .. inv.component_name]
if inv and macro_count ~= nil then
lines[#lines + 1] = string.format(
'WORD %d CALL %s:%d MACRO %s "%s:%d" BODY %d',
entry.pos, rel_path, entry.line, inv.component_name,
inv.def_path or "", inv.def_line or 0, entry.body_line)
else
lines[#lines + 1] = string.format(
"WORD %d CALL %s:%d RAW", entry.pos, rel_path, entry.line)
end
end
return table.concat(lines, "\n") .. "\n"
end
--- Pass entry. For each source that declares at least one `MipsAtom_(name)` / `MipsCode code_<name>`,
--- emit two files in `<out_root>/`: `<basename>.atoms.sourcemap.txt` (per-word call-site map) and `<basename>.atoms.provenance.txt`
--- (per-word definition + body line, resolved via the outermost `mac_X(...)` invocation).
--- When `ctx.flags.gdb_runtime` is true and `ctx.flags.elf_path` exists, also emit the post-link gdb script `<ctx.out_root>/gdb_tape_atoms_runtime.gdb`.
--- @param ctx PassCtx --- @param ctx PassCtx
--- @return PassResult --- @return PassResult
function M.run(ctx) function M.run(ctx)
@@ -495,34 +558,8 @@ function M.run(ctx)
} }
end end
-- Always emit the text form (per-source). -- atoms.sourcemap.txt + atoms.provenance.txt content moved to report.lua via `<module>.atoms.md` markdown file.
for _, src in ipairs(corpus.source_order) do -- This pass emits only the post-link gdb_runtime artifact (see emit_gdb_runtime below).
local has_projection = false
for _, atom in ipairs((src.scan or {}).atoms or {}) do
if (atom.kind == "atom" or atom.kind == "raw_atom") and atom.paths then
has_projection = true; break
end
end
if not has_projection then
for _, atom in ipairs((src.scan or {}).raw_atoms or {}) do
if atom.paths then has_projection = true; break end
end
end
if has_projection then
local basename = duffle.basename_no_ext(src.path)
-- (1) atoms.sourcemap.txt — format-1 per-word call-site map.
local sourcemap_path = ctx.out_root .. "/" .. basename .. ".atoms.sourcemap.txt"
local sourcemap_body = render_source_map(src)
-- (2) atoms.provenance.txt — format-1 per-word definition/body map.
local prov_path = ctx.out_root .. "/" .. basename .. ".atoms.provenance.txt"
local prov_body = render_provenance(src, wc)
duffle.ensure_dir(duffle.dirname(sourcemap_path))
duffle.write_file_lf(sourcemap_path, sourcemap_body)
duffle.write_file_lf(prov_path, prov_body)
outputs[#outputs + 1] = { kind = "report", path = sourcemap_path }
outputs[#outputs + 1] = { kind = "report", path = prov_path }
end
end
-- Optionally emit the gdb-runtime form (post-link, one file per build). -- Optionally emit the gdb-runtime form (post-link, one file per build).
if ctx.flags and ctx.flags.gdb_runtime then if ctx.flags and ctx.flags.gdb_runtime then
+3 -1
View File
@@ -153,6 +153,7 @@ local DW_AT_inline = 0x20 -- DWARF5 §7.7.1: DW_AT_inline (used by a
local DW_AT_decl_file = 0x3A -- DWARF5 §7.7.1: DW_AT_decl_file (1-based file index into the CU's file table) local DW_AT_decl_file = 0x3A -- DWARF5 §7.7.1: DW_AT_decl_file (1-based file index into the CU's file table)
local DW_AT_decl_line = 0x3B -- DWARF5 §7.7.1: DW_AT_decl_line local DW_AT_decl_line = 0x3B -- DWARF5 §7.7.1: DW_AT_decl_line
-- TODO(Ed): Remove this HARDCODE
-- File index lookup table for the existing main line unit (Unit 2). -- File index lookup table for the existing main line unit (Unit 2).
-- Provenance paths come back with mixed slashes; we normalize to basename and look up against the line unit's actual file table. -- Provenance paths come back with mixed slashes; we normalize to basename and look up against the line unit's actual file table.
-- Current scope has two provenance basenames: hello_gte_tape.c (the atom's call site) and lottes_tape.h (the component definition). -- Current scope has two provenance basenames: hello_gte_tape.c (the atom's call site) and lottes_tape.h (the component definition).
@@ -162,7 +163,8 @@ local DW_AT_decl_line = 0x3B -- DWARF5 §7.7.1: DW_AT_decl_line
-- headers that are pure macros/typedefs (dsl.h, memory.h, math.h, mips.h, gp.h, gte.h, etc.) never appear. -- headers that are pure macros/typedefs (dsl.h, memory.h, math.h, mips.h, gp.h, gte.h, etc.) never appear.
-- lottes_tape.h is the FIRST include that emits line entries (MipsAtomComp_ declarations), so it is the FIRST entry after the primary file. -- lottes_tape.h is the FIRST include that emits line entries (MipsAtomComp_ declarations), so it is the FIRST entry after the primary file.
local PROVENANCE_BASENAME_TO_FILE_INDEX = { local PROVENANCE_BASENAME_TO_FILE_INDEX = {
["hello_gte_tape.c"] = ATOM_SOURCE_FILE_INDEX, -- = 11 ["hello_joypad.tape.c"] = ATOM_SOURCE_FILE_INDEX, -- = 11
["hello_gte.tape.c"] = ATOM_SOURCE_FILE_INDEX, -- = 11
["lottes_tape.h"] = 2, ["lottes_tape.h"] = 2,
} }
+380 -257
View File
@@ -7,9 +7,6 @@
--- ---
--- The annotation pass emits `errors.h` files per module and the canonical `corpus.sources_by_dir` projection groups sources by directory. --- The annotation pass emits `errors.h` files per module and the canonical `corpus.sources_by_dir` projection groups sources by directory.
--- This pass iterates the canonical dir projection directly and re-validates each source via `annotation.validate()` to get the detailed per-source results. --- This pass iterates the canonical dir projection directly and re-validates each source via `annotation.validate()` to get the detailed per-source results.
---
--- **Conventions**: tabs (1/level), EmmyLua annotations, no regex,
--- Lua 5.3 compatible.
-- ════════════════════════════════════════════════════════════════════════════ -- ════════════════════════════════════════════════════════════════════════════
-- Module-scope requires + package.path setup -- Module-scope requires + package.path setup
@@ -18,8 +15,7 @@
-- Resolve `arg[0]` to an absolute-ish script directory so that `require("duffle")` resolves against `scripts/` regardless of CWD. -- Resolve `arg[0]` to an absolute-ish script directory so that `require("duffle")` resolves against `scripts/` regardless of CWD.
-- Bootstrap: see `ps1_meta.lua` for the rationale. -- Bootstrap: see `ps1_meta.lua` for the rationale.
-- Bootstrap: load `scripts/duffle_paths.lua` (sets package.path + package.cpath). -- Bootstrap: load `scripts/duffle_paths.lua` (sets package.path + package.cpath).
-- Uses `debug.getinfo` to find this file's own directory, so it works -- Uses `debug.getinfo` to find this file's own directory, so it works both standalone and when require'd from the orchestrator.
-- both standalone and when require'd from the orchestrator.
-- Bootstrap: load `duffle_paths.lua` via `debug.getinfo(1, "S").source` (works both standalone + when require'd). -- Bootstrap: load `duffle_paths.lua` via `debug.getinfo(1, "S").source` (works both standalone + when require'd).
-- duffle_paths.lua sets package.path then returns `require("duffle")` at the bottom, so the dofile value IS the duffle module. -- duffle_paths.lua sets package.path then returns `require("duffle")` at the bottom, so the dofile value IS the duffle module.
local _bootstrap_dir = debug.getinfo(1, "S").source:match("^@?(.*[/\\])") or "./" local _bootstrap_dir = debug.getinfo(1, "S").source:match("^@?(.*[/\\])") or "./"
@@ -30,6 +26,11 @@ local duffle = dofile(_bootstrap_dir .. "../duffle_paths.lua")
-- that the report pass renders into the per-module `<dir_basename>.annotations.txt` output. -- that the report pass renders into the per-module `<dir_basename>.annotations.txt` output.
local annotation = dofile(_bootstrap_dir .. "annotation.lua") local annotation = dofile(_bootstrap_dir .. "annotation.lua")
-- Load atoms_source_map for the `render_source_map` / `render_provenance` module functions (used by `render_module_atoms_md` to produce `<module>.atoms.md` without re-walking source tokens).
-- The pass itself emits no per-source files anymore; we only consume the two pure renderers here.
-- Defined BEFORE the renderer functions below so their upvalues resolve to this local (not the global `atoms_source_map`, which is nil).
local atoms_source_map = dofile(_bootstrap_dir .. "atoms_source_map.lua")
-- ════════════════════════════════════════════════════════════════════════════ -- ════════════════════════════════════════════════════════════════════════════
-- Constants -- Constants
-- ════════════════════════════════════════════════════════════════════════════ -- ════════════════════════════════════════════════════════════════════════════
@@ -44,8 +45,7 @@ local SECTION_HEADER_MACROS = "── Macro word-count declarations ───
local SECTION_HEADER_ERRORS = "── Errors ──────────────────────────────────────────────" local SECTION_HEADER_ERRORS = "── Errors ──────────────────────────────────────────────"
local SECTION_HEADER_WARNINGS = "── Warnings ────────────────────────────────────────────" local SECTION_HEADER_WARNINGS = "── Warnings ────────────────────────────────────────────"
-- Lua pattern that captures the basename (last path segment) of a -- Lua pattern that captures the basename (last path segment) of a forward- or back-slash separated path.
-- forward- or back-slash separated path.
local BASENAME_PATTERN = "([^/\\]+)$" local BASENAME_PATTERN = "([^/\\]+)$"
-- Debug flag name — set to truthy in `_G` to enable verbose logging. -- Debug flag name — set to truthy in `_G` to enable verbose logging.
@@ -148,328 +148,451 @@ local function source_basename(path)
return path:match(BASENAME_PATTERN) or path return path:match(BASENAME_PATTERN) or path
end end
--- (internal) Format a single annotation entry as one rendered line. -- ════════════════════════════════════════════════════════════════════════════
--- @param a AnnotEntry -- Markdown renderers (consolidated-report-files refactor, 2026-07-26)
--- @param src_name string -- ════════════════════════════════════════════════════════════════════════════
--- Render the thin project-wide summary (`build/atom_meta_report.summary.md`).
--- @param all_results { module:string, atoms:integer, annots:integer, binds:integer,
--- macros:integer, findings:integer, errors:integer,
--- warnings:integer, info:integer }[]
--- @return string --- @return string
local function format_annot_line(a, src_name) local function render_project_summary(all_results)
if a.error then local lines = {
return string.format(" ✗ line %d %s [ERROR: %s] [%s]", a.line, a.macro or "?", a.error, src_name) "# Project summary",
"> Auto-generated by ps1_meta.lua (passes/report.lua).",
"",
"| module | atoms | annots | binds | macros | findings | errors | warnings | info |",
"|--------|-------|--------|-------|--------|----------|--------|----------|------|",
}
local totals = { atoms = 0, annots = 0, binds = 0, macros = 0,
findings = 0, errors = 0, warnings = 0, info = 0 }
for _, e in ipairs(all_results) do
lines[#lines + 1] = string.format(
"| %s | %d | %d | %d | %d | %d | %d | %d | %d |",
e.module, e.atoms, e.annots, e.binds, e.macros,
e.findings, e.errors, e.warnings, e.info)
totals.atoms = totals.atoms + e.atoms
totals.annots = totals.annots + e.annots
totals.binds = totals.binds + e.binds
totals.macros = totals.macros + e.macros
totals.findings = totals.findings + e.findings
totals.errors = totals.errors + e.errors
totals.warnings = totals.warnings + e.warnings
totals.info = totals.info + e.info
end end
local line = string.format(" ● line %d %s [%s]", a.line, a.name, src_name) lines[#lines + 1] = string.format(
if a.binds then line = line .. " binds=" .. a.binds end "| **TOTAL** | %d | %d | %d | %d | %d | %d | %d | %d |",
if #a.reads > 0 then line = line .. " reads={" .. table.concat(a.reads, ",") .. "}" end totals.atoms, totals.annots, totals.binds, totals.macros,
if #a.writes > 0 then line = line .. " writes={" .. table.concat(a.writes, ",") .. "}" end totals.findings, totals.errors, totals.warnings, totals.info)
return line return table.concat(lines, "\n") .. "\n"
end end
--- (internal) Tally totals across all results in a module. --- Render the per-module verbose source-map markdown (`build/<module>.atoms.md`).
--- @param results AnnotationResult[] --- Per-source sub-section, per-atom stanza with sourcemap + provenance rows.
--- @return integer, integer, integer, integer, integer, integer --- Pulls sourcemap + provenance from `atoms_source_map` (no second source walk).
local function tally_module_totals(results) --- @param dir string
local total_atoms, total_annots, total_binds, total_macros = 0, 0, 0, 0 --- @param dir_sources SourceFile[]
local total_errors, total_warnings = 0, 0 --- @param wc table<string, integer>
for _, r in ipairs(results) do --- @return string
total_atoms = total_atoms + #r.atoms local function render_module_atoms_md(dir, dir_sources, wc)
total_annots = total_annots + #r.annots local dir_basename = source_basename(dir)
total_binds = total_binds + #r.binds local lines = {
total_macros = total_macros + #r.macros "# " .. dir_basename .. " — atoms (verbose source map)",
total_errors = total_errors + #r.errors "> Per-word call-site + provenance. Auto-generated.",
total_warnings = total_warnings + #r.warnings "",
}
for _, src in ipairs(dir_sources) do
local src_name = source_basename(src.path)
lines[#lines + 1] = "## " .. src_name
lines[#lines + 1] = ""
-- For each atom with a projection, render its sourcemap + provenance.
local atoms_list = {}
for _, atom in ipairs((src.scan or {}).atoms or {}) do
if atom.paths then atoms_list[#atoms_list + 1] = atom end
end end
return total_atoms, total_annots, total_binds, total_macros, total_errors, total_warnings for _, atom in ipairs((src.scan or {}).raw_atoms or {}) do
if atom.paths then atoms_list[#atoms_list + 1] = atom end
end
if #atoms_list == 0 then
lines[#lines + 1] = "_(no atom projections)_"
lines[#lines + 1] = ""
else
-- Per-source forward-slash path (same one `emit_atom_stanza` / `emit_provenance_stanza` would derive;
-- computed once per `## <source>` heading and reused by each atom's `WORD N CALL ...` field).
local rel_path = src.path:gsub("\\\\", "/")
for _, atom in ipairs(atoms_list) do
lines[#lines + 1] = string.format(
"### atom: %s (line %d, %d words)",
atom.name, atom.line or 0, #(atom.paths.items or {}))
lines[#lines + 1] = ""
lines[#lines + 1] = "**Sourcemap** — per-word call site:"
lines[#lines + 1] = "```"
-- Per-atom invariant: call the per-atom renderers, NOT the per-source ones.
-- The per-source renderers enumerate every atom in `src`;
-- calling them in a per-atom loop would repeat the whole source under every `### atom:` heading.
lines[#lines + 1] = atoms_source_map.render_atom_source_map(atom):gsub("\n+$", "")
lines[#lines + 1] = "```"
lines[#lines + 1] = ""
lines[#lines + 1] = "**Provenance** — per-word definition + body:"
lines[#lines + 1] = "```"
lines[#lines + 1] = atoms_source_map.render_atom_provenance(atom, wc, rel_path):gsub("\n+$", "")
lines[#lines + 1] = "```"
lines[#lines + 1] = ""
end
end
end
return table.concat(lines, "\n") .. "\n"
end end
-- (internal) Section renderer: per-source atom declarations. --- Render the consolidated per-module markdown (`build/<module>.atom_meta_report.md`).
local function render_module_atoms_section(add, results) --- Aggregates annotation + static-analysis content across all sources in `dir`.
add(SECTION_HEADER_ATOMS) --- Annotations come from re-running `annotation.validate()` per source (the existing pattern);
for _, r in ipairs(results) do --- static-analysis comes from `corpus.static_analysis_results[dir_basename]` (populated by `static_analysis.lua` — no second corpus_pipe_ctx build).
--- @param dir string
--- @param dir_sources SourceFile[]
--- @param annot_results AnnotationResult[]
--- @param sa_results table -- corpus.static_analysis_results[dir_basename]
--- @return string
local function render_module_meta_report(dir, dir_sources, annot_results, sa_results)
local dir_basename = source_basename(dir)
local lines = {
"# " .. dir_basename .. " — atom meta report",
"> Auto-generated by ps1_meta.lua (passes/report.lua). Do not edit.",
"",
}
local function add(s) lines[#lines + 1] = s end
-- Module summary table.
local n_atoms = 0
local n_annot = 0
local n_binds = 0
local n_macros = 0
local n_bare, n_proc = 0, 0
for _, r in ipairs(annot_results) do
n_atoms = n_atoms + #r.atoms
n_annot = n_annot + #r.annots
n_binds = n_binds + #r.binds
n_macros = n_macros + #r.macros
end
for _, a in ipairs(sa_results.atoms or {}) do
if a.kind == "comp_bare" then n_bare = n_bare + 1
elseif a.kind == "comp_proc" then n_proc = n_proc + 1
end
end
add("## Module summary"); add("")
add("| metric | value |"); add("|--------|-------|")
add(string.format("| sources | %d |", #dir_sources))
add(string.format("| atoms | %d (atoms: %d, comp_bare: %d, comp_proc: %d) |",
#(sa_results.atoms or {}),
#(sa_results.atoms or {}) - n_bare - n_proc, n_bare, n_proc))
add(string.format("| annotations | %d |", n_annot))
add(string.format("| binds structs | %d |", n_binds))
add(string.format("| macro decls | %d |", n_macros))
add(string.format("| findings | %d (errors: %d, warnings: %d, info: %d) |",
#(sa_results.findings or {}),
#(sa_results.errors or {}),
#(sa_results.warnings or {}),
#(sa_results.info or {})))
add("")
-- Sources
add("## Sources"); add("")
for _, s in ipairs(dir_sources) do add("- `" .. s.path .. "`") end
add("")
-- Atoms (annotation)
add("## Atoms"); add("")
add("| kind | name | source | line |"); add("|------|------|--------|------|")
for _, r in ipairs(annot_results) do
local src_name = source_basename(r.source) local src_name = source_basename(r.source)
for _, a in ipairs(r.atoms) do for _, a in ipairs(r.atoms) do
add(string.format(" MipsAtom_(%s) line %d [%s]", a.name, a.line, src_name)) add(string.format("| atom | %s | %s | %d |", a.name, src_name, a.line))
end end
end end
add("") add("")
end
-- (internal) Section renderer: per-source annotation entries. -- Annotations
local function render_module_annots_section(add, results) add("## Annotations"); add("")
add(SECTION_HEADER_ANNOTS) if #annot_results == 0 then
for _, r in ipairs(results) do add("_(none)_")
else
add("| source | line | name | binds | reads | writes |")
add("|--------|------|------|-------|-------|--------|")
for _, r in ipairs(annot_results) do
local src_name = source_basename(r.source) local src_name = source_basename(r.source)
for _, a in ipairs(r.annots) do for _, a in ipairs(r.annots) do
add(format_annot_line(a, src_name)) local binds = a.binds or ""
local reads = (#a.reads > 0 and table.concat(a.reads, ",")) or ""
local writes = (#a.writes > 0 and table.concat(a.writes, ",")) or ""
add(string.format("| %s | %d | %s | %s | %s | %s |",
src_name, a.line, a.name, binds, reads, writes))
end
end end
end end
add("") add("")
end
-- (internal) Section renderer: per-source Binds_* struct declarations. -- Binds_* structs
local function render_module_binds_section(add, results) add("## Binds_* structs"); add("")
add(SECTION_HEADER_BINDS) if #annot_results == 0 then
for _, r in ipairs(results) do add("_(none)_")
else
for _, r in ipairs(annot_results) do
local src_name = source_basename(r.source) local src_name = source_basename(r.source)
for _, b in ipairs(r.binds) do for _, b in ipairs(r.binds) do
add(string.format(" %s line %d %d bytes [%s]", b.name, b.line, b.bytes, src_name)) add(string.format("### %s (%s:%d, %d bytes)",
b.name, src_name, b.line, b.bytes))
for _, f in ipairs(b.fields) do for _, f in ipairs(b.fields) do
add(string.format(" +%2d: %s", f.offset, f.name)) add(string.format("- `+%d %s`", f.offset, f.name))
end
end
end end
add("") add("")
end end
end
end
-- (internal) Section renderer: per-source macro word-count declarations. -- Macro decls
local function render_module_macros_section(add, results) add("## Macro word-count declarations"); add("")
add(SECTION_HEADER_MACROS) if #annot_results == 0 then
for _, r in ipairs(results) do add("_(none)_")
else
add("| source | line | macro declaration |")
add("|--------|------|-------------------|")
for _, r in ipairs(annot_results) do
local src_name = source_basename(r.source) local src_name = source_basename(r.source)
for _, m in ipairs(r.macros) do for _, m in ipairs(r.macros) do
add(string.format(" %s line %d words=%d [%s]", m.name, m.line, m.words, src_name)) add(string.format("| %s | %d | %s |",
src_name, m.line, m.name))
end
end end
end end
add("") add("")
end
-- (internal) Section renderer: per-source errors (one-line + "(none)" if empty). -- Findings by atom (static-analysis)
local function render_module_errors_section(add, results, total_errors) add("## Static analysis — findings by atom"); add("")
add(SECTION_HEADER_ERRORS) local by_atom = {}
if total_errors == 0 then for _, f in ipairs(sa_results.findings or {}) do
add(" (none)") by_atom[f.atom] = by_atom[f.atom] or {}
by_atom[f.atom][#by_atom[f.atom] + 1] = f
end
if next(by_atom) == nil then
add("_(no findings)_")
else else
for _, r in ipairs(results) do for _, a in ipairs(sa_results.atoms or {}) do
local src_name = source_basename(r.source) local fs = by_atom[a.name]
for _, e in ipairs(r.errors) do if fs then
add(string.format(" ✗ line %d %s [%s]", e.line, e.msg, src_name)) add(string.format("### %s", a.name))
end for _, f in ipairs(fs) do
end add(string.format("- `[%s] %s`", f.check, f.msg))
end end
add("") add("")
end end
end
end
-- (internal) Section renderer: per-source warnings (one-line + "(none)" if empty). -- Errors / Warnings / Info
local function render_module_warnings_section(add, results, total_warnings) local function add_findings(label, entries)
add(SECTION_HEADER_WARNINGS) add(string.format("## %s", label))
if total_warnings == 0 then if #entries == 0 then
add(" (none)") add("_(none)_")
else else
for _, r in ipairs(results) do for _, e in ipairs(entries) do
local src_name = source_basename(r.source) add(string.format("- line %d %s", e.line, e.msg))
for _, w in ipairs(r.warnings) do
add(string.format(" ⚠ line %d %s [%s]", w.line, w.msg, src_name))
end
end end
end end
add("") add("")
end
-- ════════════════════════════════════════════════════════════════════════════
-- SECTION_RENDERERS — data-driven section dispatch (the plex pattern)
-- ════════════════════════════════════════════════════════════════════════════
--
-- Each entry maps a section to its (header, render_fn). The render_fn signature:
-- render_fn(add, results, totals)
-- add -- the `add(line)` closure from the surrounding report renderer
-- results -- AnnotationResult[] (per-source results)
-- totals -- {atoms, annots, binds, macros, errors, warnings} counts
--
-- Sections that need to render "(none)" vs iterate use totals.errors / totals.warnings;
-- other sections ignore the totals arg.
-- Adding a new section = 1 row here + 1 render_<thing>_section function.
local SECTION_RENDERERS = {
{ header = SECTION_HEADER_ATOMS, render = render_module_atoms_section },
{ header = SECTION_HEADER_ANNOTS, render = render_module_annots_section },
{ header = SECTION_HEADER_BINDS, render = render_module_binds_section },
{ header = SECTION_HEADER_MACROS, render = render_module_macros_section },
{ header = SECTION_HEADER_ERRORS, render = function(add, results, totals) return render_module_errors_section(add, results, totals.errors) end },
{ header = SECTION_HEADER_WARNINGS, render = function(add, results, totals) return render_module_warnings_section(add, results, totals.warnings) end },
}
--- Render the per-MODULE annotation report (one `<dir_basename>.annotations.txt`).
--- @param dir string -- module directory path
--- @param sources SourceFile[] -- sources in this module
--- @param results AnnotationResult[] -- per-source validate() results
--- @return string -- the rendered report text
local function render_module_report(dir, sources, results)
local lines = {}
local function add(s) lines[#lines + 1] = s end
add(RULE_THICK)
add("ANNOTATION PASS — module " .. source_basename(dir))
add(RULE_THICK)
add(string.format("Sources: %d", #sources))
for _, s in ipairs(sources) do add(" " .. s.path) end
add("")
local total_atoms, total_annots, total_binds, total_macros, total_errors, total_warnings = tally_module_totals(results)
add(string.format("Atoms: %d Annotations: %d Binds structs: %d Macro decls: %d",
total_atoms, total_annots, total_binds, total_macros))
add("")
-- Bundle the totals so the section renderers don't need separate parameter lists.
-- Errors/warnings sections need their total count to decide "(none)" vs iterate.
-- Sections without totals (atoms/annots/binds/macros) ignore this arg.
local totals = {
atoms = total_atoms, annots = total_annots, binds = total_binds,
macros = total_macros, errors = total_errors, warnings = total_warnings,
}
-- THE per-section dispatch. ONE loop over SECTION_RENDERERS.
-- Each renderer writes its header + content via the `add` closure (pre-bound above).
-- Adding a new section = 1 row here + 1 render_<thing>_section function.
for _, section in ipairs(SECTION_RENDERERS) do
section.render(add, results, totals)
end end
add_findings("Errors", sa_results.errors or {})
add_findings("Warnings", sa_results.warnings or {})
add_findings("Info", sa_results.info or {})
-- Per-atom cycle counts (path-aware)
add("## Per-atom cycle counts (path-aware, best case, no stalls)"); add("")
add("| atom | source | min | max | branches | paths | notes |")
add("|------|--------|-----|-----|----------|-------|-------|")
local sorted = {}
for _, a in ipairs(sa_results.atoms or {}) do sorted[#sorted + 1] = a end
table.sort(sorted, function(x, y)
return ((x.paths or {}).cycles_max or 0) > ((y.paths or {}).cycles_max or 0)
end)
for _, a in ipairs(sorted) do
local p = a.paths or {}
local src_name = a.source_path and source_basename(a.source_path) or ""
local notes = ""
if p.has_loops then notes = notes .. " [loop!]" end
if p.unknown_macros and #p.unknown_macros > 0 then
notes = notes .. " [unknown: " .. table.concat(p.unknown_macros, ", ") .. "]"
end
add(string.format("| %s | %s | %d | %d | %d | %d | %s |",
a.name, src_name,
p.cycles_min or 0, p.cycles_max or 0,
p.branches or 0, p.paths or 0, notes))
end
add("")
-- Per-source scan summary
add("## Per-source scan summary"); add("")
for _, src in ipairs(dir_sources) do
local src_atoms = {}
for _, a in ipairs(sa_results.atoms or {}) do
if a.source_path == src.path then src_atoms[#src_atoms + 1] = a end
end
if #src_atoms > 0 then
local mn, mx = math.huge, -1
for _, a in ipairs(src_atoms) do
local p = a.paths or {}
if (p.cycles_min or 0) < mn then mn = p.cycles_min or 0 end
if (p.cycles_max or 0) > mx then mx = p.cycles_max or 0 end
end
local path_str
if mx > 0 then
path_str = string.format(" cycles=%d..%d", mn, mx)
else
path_str = string.format(" %d cycles", mn)
end
add(string.format("- `%s` — %d atom%s%s",
src.basename, #src_atoms,
#src_atoms == 1 and "" or "s", path_str))
end
end
add("")
return table.concat(lines, "\n") .. "\n" return table.concat(lines, "\n") .. "\n"
end end
-- ════════════════════════════════════════════════════════════════════════════ -- ════════════════════════════════════════════════════════════════════════════
-- Per-project summary -- REPORT_RENDERERS — data-driven report dispatch (one row per file kind)
-- ════════════════════════════════════════════════════════════════════════════ -- ════════════════════════════════════════════════════════════════════════════
-- `once = true` means render once at the project level (not per-module).
--- Render the per-project summary (`build/gen/annotation_validation.txt`). -- `basename(dir_basename)` yields the file's basename for that kind.
--- Aggregates totals across all sources; lists per-source error counts if any source has errors. -- `gather(ctx, dir, dir_sources [, all_modules])` returns the rendered string.
--- @param all_results AnnotationResult[] local REPORT_RENDERERS = {
--- @return string {
local function render_project_report(all_results) name = "atom_meta_report",
local lines = {} ext = "md",
local function add(s) lines[#lines + 1] = s end basename = function(dir_basename) return dir_basename .. ".atom_meta_report" end,
once = false,
local total_atoms, total_annots, total_macros, total_binds = 0, 0, 0, 0 gather = function(ctx, dir, dir_sources)
local total_errors, total_warnings = 0, 0 -- Annotations: re-run `annotation.validate()` per source (the existing pattern).
for _, r in ipairs(all_results) do local annot_results = {}
total_atoms = total_atoms + #r.atoms
total_annots = total_annots + #r.annots
total_macros = total_macros + #r.macros
total_binds = total_binds + #r.binds
total_errors = total_errors + #r.errors
total_warnings = total_warnings + #r.warnings
end
add(RULE_THICK)
add("ANNOTATION VALIDATION — project summary")
add(RULE_THICK)
add("")
add(string.format("Atoms: %d", total_atoms))
add(string.format("Annotations: %d", total_annots))
add(string.format("Macros: %d", total_macros))
add(string.format("Binds: %d", total_binds))
add("")
add(string.format("Errors: %d", total_errors))
add(string.format("Warnings: %d", total_warnings))
add("")
if total_errors > 0 then
add("Per-source error counts:")
for _, r in ipairs(all_results) do
if #r.errors > 0 then
local src_name = source_basename(r.source)
add(string.format(" %s : %d error(s)", src_name, #r.errors))
end
end
add("")
end
return table.concat(lines, "\n") .. "\n"
end
-- ════════════════════════════════════════════════════════════════════════════
-- Orchestration helpers
-- ════════════════════════════════════════════════════════════════════════════
--- (internal) Re-validate every source in a directory against the canonical corpus projection.
--- Calls `annotation.validate()` per source to produce the per-source AnnotationResult (atoms / annots / macros / binds / errors / warnings)
--- that the report renderer consumes. Eeach report pass run is reproducible from the corpus.
--- Returns the list of module results + the flat list of all results (for the project-wide summary).
--- @param ctx PassCtx
--- @param dir_sources SourceFile[]
--- @return AnnotationResult[], AnnotationResult[]
local function lookup_module_results(ctx, dir_sources)
local module_results = {}
local all_results = {}
for _, src in ipairs(dir_sources) do for _, src in ipairs(dir_sources) do
if src.scan then if src.scan then
local result = annotation.validate(ctx, src, nil) local r = annotation.validate(ctx, src, nil)
result.source = src.path -- tag for downstream rendering r.source = src.path
module_results[#module_results + 1] = result annot_results[#annot_results + 1] = r
all_results[#all_results + 1] = result
end end
end end
return module_results, all_results -- Static-analysis: read stashed projection (no re-validate).
end local dir_basename = dir:match("([^/\\]+)$") or dir
local sa_results = (ctx.shared.corpus.static_analysis_results or {})[dir_basename] or {}
--- (internal) Does this module's results contain anything worth emitting? return render_module_meta_report(dir, dir_sources, annot_results, sa_results)
--- @param module_results AnnotationResult[] end,
--- @return boolean },
local function module_has_content(module_results) {
for _, r in ipairs(module_results) do name = "atoms",
if #r.atoms > 0 or #r.annots > 0 or #r.binds > 0 ext = "md",
or #r.macros > 0 or #r.errors > 0 or #r.warnings > 0 then basename = function(dir_basename) return dir_basename .. ".atoms" end,
return true once = false,
end gather = function(ctx, dir, dir_sources)
end return render_module_atoms_md(dir, dir_sources,
return false ctx.shared.corpus.word_counts or {})
end end,
},
--- (internal) Log a debug message if `_G[DEBUG_FLAG]` is truthy. {
--- @param fmt string name = "summary",
local function debug_log(fmt, ...) ext = "md",
if _G[DEBUG_FLAG] then basename = function(_dir_basename) return "atom_meta_report.summary" end,
io.stderr:write(string.format("[%s] " .. fmt, PASS_NAME, ...)) once = true,
end gather = function(_ctx, _dir, _dir_sources, all_modules)
end return render_project_summary(all_modules)
end,
},
}
-- ════════════════════════════════════════════════════════════════════════════ -- ════════════════════════════════════════════════════════════════════════════
-- M — module exports -- M — public pass surface
-- ════════════════════════════════════════════════════════════════════════════ -- ════════════════════════════════════════════════════════════════════════════
local M = {} local M = {}
--- Run the report pass. --- Run the report pass. Emits 1 `atom_meta_report.summary.md` per build + 2 `atom_meta_report.md` + 2 `atoms.md` files per module (duffle + gte_hello).
--- Renders one `<dir_basename>.annotations.txt` per source-directory that has content, plus the project-wide `annotation_validation.txt` summary. --- Reads `corpus.static_analysis_results` (added in Phase 1) to populate per-module findings without re-running validate().
--- @param ctx PassCtx --- @param ctx PassCtx
--- @return PassResult --- @return PassResult
function M.run(ctx) function M.run(ctx)
local outputs = {} local outputs = {}
local errors = {}
local warnings = {}
-- Module grouping comes from `corpus.sources_by_dir` (the canonical projection).
-- Iterate it directly; no private cache, no per-pass stash.
local corpus = ctx.shared and ctx.shared.corpus local corpus = ctx.shared and ctx.shared.corpus
local by_dir = (corpus and corpus.sources_by_dir) or {} local by_dir = (corpus and corpus.sources_by_dir) or {}
duffle.ensure_dir(ctx.out_root) -- `out_path_root`: when the conventional `out_root` is `build/gen` (any spelling — relative, absolute, separator variants).
-- Write the md files to `build/` (parent of `gen/`) instead of nested under `gen/`.
-- Mirrors the `gdb_tape_atoms_runtime.gdb` relocation.
local function ends_with_gen(p)
return type(p) == "string" and (p:match("[/\\]gen[/\\]?$") ~= nil
or p == "build/gen" or p == "build\\gen")
end
local out_root_effective = ends_with_gen(ctx.out_root)
and ctx.out_root:gsub("[/\\]gen[/\\]?$", "")
or ctx.out_root
duffle.ensure_dir(out_root_effective)
-- Aggregator for the project-wide `once = true` summary renderer.
local all_modules = {}
local all_results_for_summary = {}
for dir, dir_sources in pairs(by_dir) do for dir, dir_sources in pairs(by_dir) do
local dir_basename = dir:match("([^/\\]+)$") or dir local dir_basename = dir:match("([^/\\]+)$") or dir
debug_log("dir=%s basename=%s sources=%d\n", dir, dir_basename, #dir_sources)
if #dir_sources > 0 then -- Per-renderer dispatch for the per-module renderers (once = false).
local module_results, all_results = lookup_module_results(ctx, dir_sources) for _, renderer in ipairs(REPORT_RENDERERS) do
for _, r in ipairs(all_results) do if not renderer.once then
all_results_for_summary[#all_results_for_summary + 1] = r local body = renderer.gather(ctx, dir, dir_sources)
end local out_path = out_root_effective .. "/" .. renderer.basename(dir_basename) .. "." .. renderer.ext
duffle.write_file(out_path, body)
if module_has_content(module_results) then outputs[#outputs + 1] = { kind = renderer.name, path = out_path }
local out_path = ctx.out_root .. "/" .. dir_basename .. ".annotations.txt"
duffle.write_file(out_path, render_module_report(dir, dir_sources, module_results))
outputs[#outputs + 1] = { annotations_txt = out_path }
else
debug_log(" -> no content; skipping\n")
end
end end
end end
if #all_results_for_summary > 0 then -- For the summary, compute per-module totals once (re-validating annotations per source — same pattern as the meta_report renderer).
local summary_path = ctx.out_root .. "/annotation_validation.txt" local annot_results = {}
duffle.write_file(summary_path, render_project_report(all_results_for_summary)) for _, src in ipairs(dir_sources) do
outputs[#outputs + 1] = { summary_txt = summary_path } if src.scan then
local r = annotation.validate(ctx, src, nil)
r.source = src.path
annot_results[#annot_results + 1] = r
end
end
local n_annot, n_binds, n_macros = 0, 0, 0
for _, r in ipairs(annot_results) do
n_annot = n_annot + #r.annots
n_binds = n_binds + #r.binds
n_macros = n_macros + #r.macros
end
local sa_results = (corpus.static_analysis_results or {})[dir_basename] or {}
all_modules[#all_modules + 1] = {
module = dir_basename,
atoms = #(sa_results.atoms or {}),
annots = n_annot,
binds = n_binds,
macros = n_macros,
findings = #(sa_results.findings or {}),
errors = #(sa_results.errors or {}),
warnings = #(sa_results.warnings or {}),
info = #(sa_results.info or {}),
}
end end
return { outputs = outputs, errors = errors, warnings = warnings } -- Project-wide renderer (once = true): write the summary file.
for _, renderer in ipairs(REPORT_RENDERERS) do
if renderer.once then
local body = renderer.gather(ctx, nil, nil, all_modules)
local out_path = out_root_effective .. "/" .. renderer.basename("") .. "." .. renderer.ext
duffle.write_file(out_path, body)
outputs[#outputs + 1] = { kind = renderer.name, path = out_path }
end
end
return { outputs = outputs, errors = {}, warnings = {} }
end end
return M return M
+114 -271
View File
@@ -39,17 +39,19 @@
--- The report header includes `Info: N` alongside Findings / Errors / Warnings, and a dedicated --- The report header includes `Info: N` alongside Findings / Errors / Warnings, and a dedicated
--- `── Info` section renders finding-level info between `── Warnings` and the per-atom cycle counts. --- `── Info` section renders finding-level info between `── Warnings` and the per-atom cycle counts.
--- ---
--- The structural handshake checks (`mac_yield_uniformity`, `hazard_nop_use`, `control_transfer_delay_slot_use`) skip atoms/components with `debug_skip == true`.
--- The `atom_dbg_skip` marker designates runtime-helper declarations whose structure is fixed by the tape runtime (e.g. `tape_exit`, `ac_yield`).
--- Flagging them as "missing mac_yield" or "BD slot is redundant" is signal noise, not a logic failure.
--- Other checks (transfer_hazards, gpu_portstore_shape, abi_handoff, enum_alias_membership, …) still apply to debug_skip declarations because real hazards / typos can still surface in them.
---
--- The orchestrator (`ps1_meta.lua`) wires this module in via the PASSES table: --- The orchestrator (`ps1_meta.lua`) wires this module in via the PASSES table:
--- `["static-analysis"] = { --- `["static-analysis"] = {
--- module = "passes.static_analysis", --- module = "passes.static_analysis",
--- kind = "diagnostic", --- kind = "diagnostic",
--- deps = {"word-counts", "components"}, --- deps = {"word-counts", "components"},
--- out = { { kind = "report", path_template = "<out_root>/<basename>.static_analysis.txt" } }
--- } --- }
--- `kind = "diagnostic"` keeps every finding visible in the report; the orchestrator does not exit non-zero on static-analysis errors. --- `kind = "diagnostic"` keeps every finding visible in the projection; the orchestrator does not exit non-zero on static-analysis errors.
--- Annotation and header-output validation remain build-stopping. --- Annotation and header-output validation remain build-stopping.
---
--- **Conventions**: tabs (1/level), EmmyLua annotations, no regex, Lua 5.3 compatible.
-- ════════════════════════════════════════════════════════════════════════════ -- ════════════════════════════════════════════════════════════════════════════
-- Module-scope requires + package.path setup -- Module-scope requires + package.path setup
@@ -148,6 +150,38 @@ local OUTPUT_EXTENSION = ".static_analysis.txt"
--- @field findings Finding[] -- findings for this atom --- @field findings Finding[] -- findings for this atom
--- @field total_cycles integer -- sum of token cycle costs --- @field total_cycles integer -- sum of token cycle costs
-- ════════════════════════════════════════════════════════════════════════════
-- Per-word-event helpers
-- ════════════════════════════════════════════════════════════════════════════
-- Pick the source-line field that best represents "where in the user's source file is this word?".
--
-- `word_events` (populated by `passes/emission_model.lua::stamp_root_provenance`) carry four line fields:
-- * `call_line` — physical line in the ROOT atom's source (the line of the `mac_X(...)` call site that triggered this emission, or `body_line` for direct words in the atom body)
-- * `body_line` — physical line in the body containing the emitted word (the atom body for direct words; the component body for words expanded inside `mac_X(...)`)
-- * `def_line` — line of the COMPONENT's declaration in its source file (only meaningful for words emitted inside a component expansion)
-- * `line` — body-relative line in the source text (not a physical source line; rarely useful in rendered findings)
--
-- For component-expanded words (e.g. the BD-slot nop of `jump_reg(R_AtomJmp)` inside `mac_yield()`),
-- `body_line` points into the COMPONENT's source file (e.g. `lottes_tape.h:110` for `ac_yield`'s body).
-- The user editing their atom body expects the line to point at THEIR source — i.e. the line where `mac_yield()`
-- was called (e.g. `hello_gte_tape.c:35`). That line is `call_line`.
--
-- For direct words in the atom body (no invocation wrapping them), `call_line == body_line` already,
-- so `call_line` works for both cases.
local function line_for_word_event(ev)
if ev == nil then return 0 end
return ev.call_line or ev.body_line or ev.line or ev.def_line or 0
end
-- True iff the given atom/component declaration has the bare `atom_dbg_skip` marker.
-- Used by the structural handshake checks (`mac_yield_uniformity`, `hazard_nop_use`,
-- `control_transfer_delay_slot_use`) to exempt runtime-helper declarations (`tape_exit`, `ac_yield`,
-- and the `ac_*` macro components) from findings whose contract they intentionally don't satisfy.
local function is_runtime_helper(atom)
return atom and atom.debug_skip == true
end
-- ════════════════════════════════════════════════════════════════════════════ -- ════════════════════════════════════════════════════════════════════════════
-- classify_tokens — per-token classification -- classify_tokens — per-token classification
-- ════════════════════════════════════════════════════════════════════════════ -- ════════════════════════════════════════════════════════════════════════════
@@ -551,11 +585,12 @@ local function append_cu2_finding(atom, event, forward, transition,
local event_ident = event.encoder or event.ident or "?" local event_ident = event.encoder or event.ident or "?"
local policy = duffle.CU2_TRANSITION_POLICY or {} local policy = duffle.CU2_TRANSITION_POLICY or {}
local evidence = policy.evidence or {} local evidence = policy.evidence or {}
local event_line = line_for_word_event(event)
atom.paths.hazards[#atom.paths.hazards + 1] = { atom.paths.hazards[#atom.paths.hazards + 1] = {
check = "transfer_hazards", check = "transfer_hazards",
kind = kind, kind = kind,
atom = atom.name, atom = atom.name,
line = event.body_line or event.line or event.def_line or 0, line = event_line,
source = event.def_path or event.source or "", source = event.def_path or event.source or "",
relation_id = "mtc0_cu2_visibility", relation_id = "mtc0_cu2_visibility",
semantic = "MTC0", semantic = "MTC0",
@@ -619,10 +654,11 @@ local function consume_cu2_transition(atom, event, ev_word, forward)
local gap = ev_word - transition.producer_word - 1 local gap = ev_word - transition.producer_word - 1
local target = transition.target_state local target = transition.target_state
local event_line = line_for_word_event(event)
if target == "unknown" then if target == "unknown" then
append_cu2_finding(atom, event, forward, transition, gap, "info", "unknown", append_cu2_finding(atom, event, forward, transition, gap, "info", "unknown",
string.format("%s at line %d uses COP2 after an MTC0 Status write whose CU2 value is unknown (gap=%d, configured boundary=%d)" string.format("%s at line %d uses COP2 after an MTC0 Status write whose CU2 value is unknown (gap=%d, configured boundary=%d)"
, atom.name, event.body_line or event.line or event.def_line or 0 , atom.name, event_line
, gap, transition.required , gap, transition.required
) )
) )
@@ -635,7 +671,7 @@ local function consume_cu2_transition(atom, event, ev_word, forward)
local verb = target == "enabled" and "enable" or "disable" local verb = target == "enabled" and "enable" or "disable"
append_cu2_finding(atom, event, forward, transition, gap, "warning", "conservative", append_cu2_finding(atom, event, forward, transition, gap, "warning", "conservative",
string.format("%s at line %d uses COP2 before the SR.CU2 %s transition has settled (gap=%d, required=%d; timing is conservative)" string.format("%s at line %d uses COP2 before the SR.CU2 %s transition has settled (gap=%d, required=%d; timing is conservative)"
, atom.name, event.body_line or event.line or event.def_line or 0 , atom.name, event_line
, verb, gap, transition.required , verb, gap, transition.required
) )
) )
@@ -653,7 +689,7 @@ local function consume_cu2_transition(atom, event, ev_word, forward)
string.format( string.format(
"%s at line %d: COP2 unavailable after SR.CU2 was disabled" "%s at line %d: COP2 unavailable after SR.CU2 was disabled"
.. " (gap=%d, required=%d)", .. " (gap=%d, required=%d)",
atom.name, event.body_line or event.line or event.def_line or 0, atom.name, event_line,
gap, transition.required)) gap, transition.required))
forward.cu2_state = "disabled" forward.cu2_state = "disabled"
end end
@@ -708,7 +744,7 @@ local function analyze_hardware_relations(atom)
for _, ev in ipairs(events) do for _, ev in ipairs(events) do
local ev_ident = ev.encoder or ev.ident or "?" local ev_ident = ev.encoder or ev.ident or "?"
local ev_line = ev.body_line or ev.line or ev.def_line or 0 local ev_line = line_for_word_event(ev)
local ev_source = ev.def_path or ev.source or "" local ev_source = ev.def_path or ev.source or ""
local ev_args = ev.args or {} local ev_args = ev.args or {}
-- `word_events` use `i` as the 0-based word index across the entire expansion. -- `word_events` use `i` as the 0-based word index across the entire expansion.
@@ -867,7 +903,7 @@ local function analyze_hardware_relations(atom)
-- ── 4. Update semantic role state and stage post-command latch relations. ── -- ── 4. Update semantic role state and stage post-command latch relations. ──
-- A GTE command emits outputs with semantic roles (latest_screen_xy, otz, latest_color, etc.) per `duffle.GTE_COMMAND_OUTPUTS`. -- A GTE command emits outputs with semantic roles (latest_screen_xy, otz, latest_color, etc.) per `duffle.GTE_COMMAND_OUTPUTS`.
-- The walker records these on `forward_state.post_command_roles[<register>]` so the `gte_result_position` reader can later detect a reader that picks the wrong register. -- The walker records these on `forward_state.post_command_roles[<register>]` so the `gte_role_mismatch` reader can later detect a reader that picks the wrong register.
-- --
-- The walker also stages POST-COMMAND LATCH relations (kind = "command_latch_input"): a subsequent MTC2/CTC2 overwrite of a latched output before the measured boundary is a hazard. -- The walker also stages POST-COMMAND LATCH relations (kind = "command_latch_input"): a subsequent MTC2/CTC2 overwrite of a latched output before the measured boundary is a hazard.
-- The relation kind is intentionally separate from the preceding MTC2 → command relation (`MTC2` / `CTC2` / `LWC2`). -- The relation kind is intentionally separate from the preceding MTC2 → command relation (`MTC2` / `CTC2` / `LWC2`).
@@ -980,7 +1016,7 @@ local function check_gte_input_latch(atom, _pipe_ctx, findings)
end end
-- ───────────────────────────────────────────────────────────────────────── -- ─────────────────────────────────────────────────────────────────────────
-- Check #1e: gte_result_position (READER for forward_state semantic roles). -- Check #1e: gte_role_mismatch (READER for forward_state semantic roles).
-- --
-- A GTE command emits outputs with semantic roles (latest_screen_xy, otz, latest_color, etc.) per `duffle.GTE_COMMAND_OUTPUTS`. -- A GTE command emits outputs with semantic roles (latest_screen_xy, otz, latest_color, etc.) per `duffle.GTE_COMMAND_OUTPUTS`.
-- The forward walker records `forward_state.post_command_roles[<register>]` after each command. -- The forward walker records `forward_state.post_command_roles[<register>]` after each command.
@@ -988,43 +1024,16 @@ end
-- A subsequent MFC2 (or any encoder that reads a C2 register) that picks the WRONG register for the active role emits a `result_role_mismatch` warning. -- A subsequent MFC2 (or any encoder that reads a C2 register) that picks the WRONG register for the active role emits a `result_role_mismatch` warning.
-- For example, reading `C2_SXY0` after RTPS is wrong: the `latest_screen_xy` role is `C2_SXY2`. -- For example, reading `C2_SXY0` after RTPS is wrong: the `latest_screen_xy` role is `C2_SXY2`.
-- --
-- Note: the OLD `gte_result_position` check also emitted table-gap info findings for `_post_<cmd>` components missing a row in `duffle.GTE_COMPONENT_RESULT_CONTRACTS`. That table-gap check was based on the `_post_<cmd>` NAMING convention rather than hardware truth, and was removed (the user did not want naming to encode ordering semantics; a proper `atom_info` directive for ordering semantics is a future TODO).
--
-- The first `transfer_hazards` reader comment above records the projection contract. -- The first `transfer_hazards` reader comment above records the projection contract.
-- ───────────────────────────────────────────────────────────────────────── -- ─────────────────────────────────────────────────────────────────────────
local function check_gte_result_position(atom, _pipe_ctx, findings) local function check_gte_role_mismatch(atom, _pipe_ctx, findings)
local forward = atom.paths and atom.paths.forward_state local forward = atom.paths and atom.paths.forward_state
if not forward or not forward.post_command_roles then return end if not forward or not forward.post_command_roles then return end
local events = atom.paths.word_events or {} local events = atom.paths.word_events or {}
-- Build a set of known _post_<cmd> component names whose contract rows we have to verify
-- (table-gap detection: a missing row key is itself an info finding).
-- The names are the BODY-LEVEL component calls that appear in atom body text;
-- The walker doesn't expose body tokens to the reader, so we scan the events' root_call_text.
local contracts = duffle.GTE_COMPONENT_RESULT_CONTRACTS or {}
local component_names_seen = {}
for _, ev in ipairs(events) do
local root_call = ev.root_call_text or ev.call_text or ""
local name = root_call:match("^([%w_]+)") or ""
if name:find("_post_") then component_names_seen[name] = true end
end
for component_name in pairs(component_names_seen) do
-- Strip any trailing parenthesized argument list / whitespace.
local bare = component_name:match("^([%w_]+)") or component_name
if contracts[bare] == nil then
findings[#findings + 1] = {
check = "gte_result_position",
kind = "info",
atom = atom.name,
line = 0,
source = "",
relation_id = "table_gap",
component_name = bare,
msg = string.format("%s: component %q has no GTE_COMPONENT_RESULT_CONTRACTS row (unknown _post_<cmd> contract)"
, atom.name, bare),
}
end
end
-- For each word event whose encoder is `gte_mv_from_data_r`, look up the register being read in `forward_state.post_command_roles`. -- For each word event whose encoder is `gte_mv_from_data_r`, look up the register being read in `forward_state.post_command_roles`.
-- If a role is set, the reader's register must match the role's register (the registered "latest_<role>" target). -- If a role is set, the reader's register must match the role's register (the registered "latest_<role>" target).
for _, ev in ipairs(events) do for _, ev in ipairs(events) do
@@ -1047,11 +1056,12 @@ local function check_gte_result_position(atom, _pipe_ctx, findings)
-- This is a semantic mismatch. -- This is a semantic mismatch.
if reg ~= latest_screen_xy_entry.command_register if reg ~= latest_screen_xy_entry.command_register
and (reg == "C2_SXY0" or reg == "C2_SXY1") then and (reg == "C2_SXY0" or reg == "C2_SXY1") then
local ev_line = line_for_word_event(ev)
findings[#findings + 1] = { findings[#findings + 1] = {
check = "gte_result_position", check = "gte_role_mismatch",
kind = "warning", kind = "warning",
atom = atom.name, atom = atom.name,
line = ev.body_line or ev.line or ev.def_line or 0, line = ev_line,
source = ev.def_path or ev.source or "", source = ev.def_path or ev.source or "",
relation_id = "result_role_mismatch", relation_id = "result_role_mismatch",
semantic = "result_position", semantic = "result_position",
@@ -1062,7 +1072,7 @@ local function check_gte_result_position(atom, _pipe_ctx, findings)
producer_word = latest_screen_xy_entry.producer_word, producer_word = latest_screen_xy_entry.producer_word,
producer_line = latest_screen_xy_entry.producer_line, producer_line = latest_screen_xy_entry.producer_line,
msg = string.format("%s at line %d: reading %s after %s but the %s role is C2_SXY2 (not %s)" msg = string.format("%s at line %d: reading %s after %s but the %s role is C2_SXY2 (not %s)"
, atom.name, ev.body_line or ev.line or ev.def_line or 0 , atom.name, ev_line
, reg, latest_screen_xy_entry.command , reg, latest_screen_xy_entry.command
, latest_screen_xy_entry.role , latest_screen_xy_entry.role
, reg), , reg),
@@ -1084,6 +1094,13 @@ end
-- Branch/jump delay-slot NOPs belong to `control_transfer_delay_slot_use`, so this check leaves them unclassified. -- Branch/jump delay-slot NOPs belong to `control_transfer_delay_slot_use`, so this check leaves them unclassified.
-- The fixed `mac_yield()` handshake (`jump_reg(R_AtomJmp), nop`) is preserved as suppressed. -- The fixed `mac_yield()` handshake (`jump_reg(R_AtomJmp), nop`) is preserved as suppressed.
-- --
-- Both classifications emit at `info` severity: `modeled-required` documents the model boundary and `modeled-redundant`
-- is a soft observation ("you have a redundant nop; consider replacing it").
-- Neither is a logic failure, so neither rises to `warning`.
--
-- `atom_dbg_skip` runtime helpers (`tape_exit`, `ac_yield`, the `ac_*` macro components) are exempt:
-- their structural nops are part of the fixed handshake and not author choices.
--
-- The first `transfer_hazards` reader comment above records the projection contract. -- The first `transfer_hazards` reader comment above records the projection contract.
-- ───────────────────────────────────────────────────────────────────────── -- ─────────────────────────────────────────────────────────────────────────
@@ -1091,6 +1108,9 @@ local function check_hazard_nop_use(atom, _pipe_ctx, findings)
local forward = atom.paths and atom.paths.forward_state local forward = atom.paths and atom.paths.forward_state
local events = atom.paths.word_events or {} local events = atom.paths.word_events or {}
if not events or #events == 0 then return end if not events or #events == 0 then return end
-- Runtime-helper atoms / components (e.g. tape_exit, ac_yield) carry `debug_skip = true` from the bare
-- `atom_dbg_skip` marker; their structural nops are part of the fixed handshake and not author choices.
if is_runtime_helper(atom) then return end
-- The walker does not currently snapshot the pending state per event; we replay the same forward walk cheaply here. -- The walker does not currently snapshot the pending state per event; we replay the same forward walk cheaply here.
-- The replay is observation-only (no staging); the only output is one finding per non-BD-slot nop with its classification. -- The replay is observation-only (no staging); the only output is one finding per non-BD-slot nop with its classification.
@@ -1100,20 +1120,16 @@ local function check_hazard_nop_use(atom, _pipe_ctx, findings)
local ev_ident = ev.encoder or "" local ev_ident = ev.encoder or ""
local ev_args = ev.args or {} local ev_args = ev.args or {}
local ev_word = ev.i or 0 local ev_word = ev.i or 0
local ev_line = line_for_word_event(ev)
-- Classify the nop BEFORE its event is applied to the pending state. -- Classify the nop BEFORE its event is applied to the pending state.
if ev_ident == "nop" and prev_ev ~= nil then if ev_ident == "nop" and prev_ev ~= nil then
-- Skip BD-slot nops: they are exclusively owned by control_transfer_delay_slot_use. -- Skip BD-slot nops: they are exclusively owned by control_transfer_delay_slot_use.
-- Every BD-slot nop is structural; this check never reports on it.
-- (The earlier `if not suppressed then is_bd_slot = true end` form inverted the suppression — the `mac_yield()` handshake's `jump_reg(R_AtomJmp)` was incorrectly flagged.)
local prev_ident = prev_ev.encoder or "" local prev_ident = prev_ev.encoder or ""
local prev_args = prev_ev.args or {}
local bd_policies = duffle.CONTROL_TRANSFER_DELAY_SLOT_POLICIES or {} local bd_policies = duffle.CONTROL_TRANSFER_DELAY_SLOT_POLICIES or {}
local is_bd_slot = false local is_bd_slot = bd_policies[prev_ident] ~= nil
local policy = bd_policies[prev_ident]
if policy then
local arg1 = prev_args[1]
local suppressed = policy.suppress_arg1 and policy.suppress_arg1[arg1] or nil
if not suppressed then is_bd_slot = true end
end
if not is_bd_slot then if not is_bd_slot then
-- Find a pending modeled relation that this nop would retire. -- Find a pending modeled relation that this nop would retire.
local retired = nil local retired = nil
@@ -1157,7 +1173,7 @@ local function check_hazard_nop_use(atom, _pipe_ctx, findings)
check = "hazard_nop_use", check = "hazard_nop_use",
kind = "info", kind = "info",
atom = atom.name, atom = atom.name,
line = ev.body_line or ev.line or ev.def_line or 0, line = ev_line,
source = ev.def_path or ev.source or "", source = ev.def_path or ev.source or "",
nop_classification = "modeled-required", nop_classification = "modeled-required",
nop_word_index = ev_word, nop_word_index = ev_word,
@@ -1165,7 +1181,7 @@ local function check_hazard_nop_use(atom, _pipe_ctx, findings)
producer_destination = retired.destination, producer_destination = retired.destination,
consumer_token = would_be_consumer or "<would-be-consumer>", consumer_token = would_be_consumer or "<would-be-consumer>",
msg = string.format("%s at line %d: nop at word %d is modeled-required (retires %s for %s)" msg = string.format("%s at line %d: nop at word %d is modeled-required (retires %s for %s)"
, atom.name, ev.body_line or ev.line or ev.def_line or 0, ev_word, retired.relation.id, retired.destination , atom.name, ev_line, ev_word, retired.relation.id, retired.destination
), ),
} }
else else
@@ -1173,16 +1189,16 @@ local function check_hazard_nop_use(atom, _pipe_ctx, findings)
local slot_kind = "plain" local slot_kind = "plain"
findings[#findings + 1] = { findings[#findings + 1] = {
check = "hazard_nop_use", check = "hazard_nop_use",
kind = "warning", kind = "info",
atom = atom.name, atom = atom.name,
line = ev.body_line or ev.line or ev.def_line or 0, line = ev_line,
source = ev.def_path or ev.source or "", source = ev.def_path or ev.source or "",
nop_classification = "modeled-redundant", nop_classification = "modeled-redundant",
nop_word_index = ev_word, nop_word_index = ev_word,
retired_relation = nil, retired_relation = nil,
slot_kind = slot_kind, slot_kind = slot_kind,
msg = string.format("%s at line %d: nop at word %d is modeled-redundant (no pending modeled relation)" msg = string.format("%s at line %d: nop at word %d is modeled-redundant (no pending modeled relation)"
, atom.name, ev.body_line or ev.line or ev.def_line or 0, ev_word , atom.name, ev_line, ev_word
), ),
} }
end end
@@ -1254,6 +1270,9 @@ end
-- Suppress the finding when `policy.suppress_arg1[first_arg]` is non-nil. -- Suppress the finding when `policy.suppress_arg1[first_arg]` is non-nil.
-- The only current suppression is `jump_reg(R_AtomJmp)`, the fixed `mac_yield()` handshake. -- The only current suppression is `jump_reg(R_AtomJmp)`, the fixed `mac_yield()` handshake.
-- --
-- `atom_dbg_skip` runtime helpers (`tape_exit`, `ac_yield`, the `ac_*` macro components) are exempt:
-- their BD slots are part of the fixed handshake (`jump_reg(rret_addr), nop` for tape_exit, `jump_reg(R_AtomJmp), nop` for ac_yield).
--
-- `pipe_ctx` is unused; the uniform `(atom, pipe_ctx, findings)` signature is preserved so the check plugs into -- `pipe_ctx` is unused; the uniform `(atom, pipe_ctx, findings)` signature is preserved so the check plugs into
-- the existing CHECK_RULES dispatch without modifying the per-atom loop or analyze_atom_paths. -- the existing CHECK_RULES dispatch without modifying the per-atom loop or analyze_atom_paths.
-- `passes/emission_model` already normalizes `nop2` to two `nop` events and `atom_label` to zero events, so no special-case branching is needed for either. -- `passes/emission_model` already normalizes `nop2` to two `nop` events and `atom_label` to zero events, so no special-case branching is needed for either.
@@ -1262,6 +1281,9 @@ end
local function check_control_transfer_delay_slot_use(atom, pipe_ctx, findings) local function check_control_transfer_delay_slot_use(atom, pipe_ctx, findings)
local events = atom.paths.word_events or {} local events = atom.paths.word_events or {}
if not events or #events == 0 then return end if not events or #events == 0 then return end
-- Runtime-helper atoms / components (e.g. tape_exit, ac_yield) carry `debug_skip = true` from the bare
-- `atom_dbg_skip` marker; their structural BD slots are part of the fixed handshake.
if is_runtime_helper(atom) then return end
local policies = duffle.CONTROL_TRANSFER_DELAY_SLOT_POLICIES or {} local policies = duffle.CONTROL_TRANSFER_DELAY_SLOT_POLICIES or {}
for event_idx, event in ipairs(events) do for event_idx, event in ipairs(events) do
-- Canonical word_events use `encoder` as the leading identifier of the emitting token). -- Canonical word_events use `encoder` as the leading identifier of the emitting token).
@@ -1276,9 +1298,9 @@ local function check_control_transfer_delay_slot_use(atom, pipe_ctx, findings)
local slot = events[event_idx + 1] local slot = events[event_idx + 1]
local slot_ident = slot and (slot.encoder or slot.ident) or "<missing>" local slot_ident = slot and (slot.encoder or slot.ident) or "<missing>"
if slot == nil or (slot.encoder or slot.ident) == "nop" then if slot == nil or (slot.encoder or slot.ident) == "nop" then
-- Each word event carries `body_line` as the physical source line. -- Prefer `call_line` (the line of the `mac_X(...)` call site in the atom body) so the rendered
-- Use `body_line`, then `def_line`, then 0. -- finding points at the user's source, not at the vendored component body.
local ev_line = event.body_line or event.line or event.def_line or 0 local ev_line = line_for_word_event(event)
findings[#findings + 1] = { findings[#findings + 1] = {
atom = atom.name, atom = atom.name,
line = ev_line, line = ev_line,
@@ -1303,8 +1325,17 @@ end
--- Empty bodies are not currently flagged — runtime infrastructure atoms like --- Empty bodies are not currently flagged — runtime infrastructure atoms like
--- `MipsAtom_(yield) { mac_yield() }` and `MipsAtom_(tape_exit) { jump_reg(rret_addr), nop }` --- `MipsAtom_(yield) { mac_yield() }` and `MipsAtom_(tape_exit) { jump_reg(rret_addr), nop }`
--- are valid as-is; mac_yield at the end is the contract. --- are valid as-is; mac_yield at the end is the contract.
---
--- Runtime helpers carrying the bare `atom_dbg_skip` marker (`tape_exit`, `ac_yield`, the `ac_*` macro components) are exempt:
--- they intentionally do not follow the standard "1 yield at the end" contract. `tape_exit` performs its own `jump_reg(rret_addr),
--- nop` to return from the tape runner; `ac_yield` IS the `mac_yield()` implementation.
--- Flagging them as "missing mac_yield" is signal noise, not a logic failure.
---
--- Uses the standard `(atom, pipe_ctx, findings)` signature; `pipe_ctx` is unused. --- Uses the standard `(atom, pipe_ctx, findings)` signature; `pipe_ctx` is unused.
local function check_mac_yield_uniformity(atom, pipe_ctx, findings) local function check_mac_yield_uniformity(atom, pipe_ctx, findings)
-- Runtime-helper atoms / components (e.g. tape_exit, ac_yield) carry `debug_skip = true` from the bare
-- `atom_dbg_skip` marker; they intentionally break the "1 yield at the end" contract.
if is_runtime_helper(atom) then return end
-- Per-kind semantics: -- Per-kind semantics:
-- MipsAtom_ (baked atom): exactly 1 mac_yield at the end of the body. Control transfer is the atom's job. -- MipsAtom_ (baked atom): exactly 1 mac_yield at the end of the body. Control transfer is the atom's job.
-- MipsAtomComp_ (bare static-array component): ZERO mac_yield. -- MipsAtomComp_ (bare static-array component): ZERO mac_yield.
@@ -1935,7 +1966,7 @@ end
local CHECK_RULES = { local CHECK_RULES = {
{ name = "transfer_hazards", per_atom = check_transfer_hazards }, { name = "transfer_hazards", per_atom = check_transfer_hazards },
{ name = "gte_input_latch", per_atom = check_gte_input_latch }, { name = "gte_input_latch", per_atom = check_gte_input_latch },
{ name = "gte_result_position", per_atom = check_gte_result_position }, { name = "gte_role_mismatch", per_atom = check_gte_role_mismatch },
{ name = "hazard_nop_use", per_atom = check_hazard_nop_use }, { name = "hazard_nop_use", per_atom = check_hazard_nop_use },
{ name = "control_transfer_delay_slot_use",per_atom = check_control_transfer_delay_slot_use}, { name = "control_transfer_delay_slot_use",per_atom = check_control_transfer_delay_slot_use},
{ name = "mac_yield_uniformity", per_atom = check_mac_yield_uniformity }, { name = "mac_yield_uniformity", per_atom = check_mac_yield_uniformity },
@@ -2179,203 +2210,6 @@ local function validate(ctx, src, corpus_pipe_ctx)
} }
end end
-- ════════════════════════════════════════════════════════════════════════════
-- Per-directory output: build/gen/<dir_basename>.static_analysis.txt
-- ════════════════════════════════════════════════════════════════════════════
--- Per-directory emit. Aggregates atoms + findings across every source in `dir_sources`
--- and writes a single report to `<out_root>/<dir_basename>.static_analysis.txt`.
--- Called only when at least one atom was found (the caller in M.run handles the skip).
---
--- `info` is finding-level info only (kind == "info" findings); the scanned/cycles summary rows
--- live in `summaries` and are rendered as trailing summary lines after `Module findings:`.
local function emit_module_static_analysis_txt(ctx, dir, dir_sources, atoms, findings, errors, warnings, info, summaries)
-- Module basename = last component of `dir` ("code/duffle" -> "duffle").
local dir_basename = dir:match("([^/\\]+)$") or dir
local out_path = ctx.out_root .. "/" .. dir_basename .. ".static_analysis.txt"
duffle.ensure_dir(ctx.out_root)
local lines = {}
local function add(s) lines[#lines + 1] = s end
add("========================================================")
add("STATIC ANALYSIS PASS -- module " .. dir_basename)
add("========================================================")
add(string.format("Sources: %d", #dir_sources))
for _, s in ipairs(dir_sources) do
add(" " .. s.path)
end
add("")
-- Tally atoms by kind for the header summary
local n_atoms, n_bare, n_proc = 0, 0, 0
for _, a in ipairs(atoms) do
n_atoms = n_atoms + 1
if a.kind == "comp_bare" then n_bare = n_bare + 1
elseif a.kind == "comp_proc" then n_proc = n_proc + 1
end
end
local header_atoms = string.format("Atoms: %d", n_atoms)
if n_bare > 0 or n_proc > 0 then
header_atoms = header_atoms .. string.format(" (atoms: %d, comp_bare: %d, comp_proc: %d)",
n_atoms - n_bare - n_proc, n_bare, n_proc)
end
-- Header carries the per-severity counts; info is its own column, not a warning.
-- (`Info: N` is the byte-asserted field that the focused test matches; do not collapse it into Warnings.)
add(string.format("%s Findings: %d Errors: %d Warnings: %d Info: %d",
header_atoms, #findings, #errors, #warnings, #info))
add("")
-- Group findings by atom (with source prefix when multi-source module)
local multi_source = #dir_sources > 1
local by_atom = {}
for _, f in ipairs(findings) do
by_atom[f.atom] = by_atom[f.atom] or {}
by_atom[f.atom][#by_atom[f.atom] + 1] = f
end
if next(by_atom) == nil then
add(" (no findings -- every atom passed all checks)")
else
add("── Findings by atom ─────────────────────────────────────")
for _, a in ipairs(atoms) do
local fs = by_atom[a.name]
if fs then
local label = a.name
if multi_source and a.source_path then
label = string.format("%s (%s)", a.name, a.source_path:match("([^/\\]+)$") or a.source_path)
end
add(string.format(" %s line %d", label, a.line))
for _, f in ipairs(fs) do
add(string.format(" [%s] %s", f.check, f.msg))
end
end
end
end
add("")
add("── Errors ──────────────────────────────────────────────")
if #errors == 0 then add(" (none)") end
for _, e in ipairs(errors) do
add(string.format(" X line %d %s", e.line, e.msg))
end
add("")
add("── Warnings ────────────────────────────────────────────")
if #warnings == 0 then add(" (none)") end
for _, w in ipairs(warnings) do
add(string.format(" ! line %d %s", w.line, w.msg))
end
-- Finding-level Info section.
-- Rendered between Warnings and the per-atom cycle table so the next `── ` line after `── Info` is the per-atom cycle counts section;
-- the trailing scan/cycle summary rows (rendered after Module findings) stay outside this section.
add("")
add("── Info ────────────────────────────────────────────────")
if #info == 0 then add(" (none)") end
for _, i_ in ipairs(info) do
add(string.format(" i line %d %s", i_.line, i_.msg))
end
-- Per-atom cycle counts (path-aware). For each atom:
-- min = shortest path through the body (earliest exit)
-- max = longest path through the body (full fall-through)
-- br = number of branch instructions
-- paths = number of distinct paths reached
-- Both min and max are best-case (no stalls); BD-slot nops are absorbed into branch costs (MIPS semantics).
add("")
add("── Per-atom cycle counts (path-aware, best case, no stalls) ─")
if #atoms == 0 then
add(" (no atoms)")
else
-- Sort atoms by max cycles descending for quick scanning.
local sorted = {}
for _, a in ipairs(atoms) do sorted[#sorted + 1] = a end
table.sort(sorted, function(x, y) return ((x.paths or {}).cycles_max or 0) > ((y.paths or {}).cycles_max or 0) end)
for _, a in ipairs(sorted) do
local p = a.paths or {}
local br_count = p.branches or 0
local path_count = p.paths or 0
local loops_tag = p.has_loops and " [loop!]" or ""
local unknown_tag = ""
if p.unknown_macros and #p.unknown_macros > 0 then
unknown_tag = string.format(" [unknown: %s]",
table.concat(p.unknown_macros, ", "))
end
local name_label = a.name
if multi_source and a.source_path then
name_label = string.format("%s (%s)", a.name, a.source_path:match("([^/\\]+)$") or a.source_path)
end
if br_count > 0 then
add(string.format(" %-44s min=%4d max=%4d br=%d paths=%d (line %d)%s%s",
name_label, p.cycles_min or 0, p.cycles_max or 0, br_count, path_count,
a.line, loops_tag, unknown_tag))
else
add(string.format(" %-44s %4d cycles (line %d, no branches)%s%s",
name_label, p.cycles_min or 0, a.line, loops_tag, unknown_tag))
end
end
end
add("")
add("── Per-source scan summary ──────────────────────────────")
-- One line per source that contributed atoms.
-- The line includes the source basename + per-source atom count + (if path-aware cycle data is present) the min..max cycle range.
-- Sources with 0 atoms are skipped (they're just header files that declared no MipsAtom_ — they're already listed in the module's "Sources:" section above).
for _, src in ipairs(dir_sources) do
local src_atoms = {}
for _, a in ipairs(atoms) do
if a.source_path == src.path then
src_atoms[#src_atoms + 1] = a
end
end
if #src_atoms == 0 then
goto continue
end
local atom_count = #src_atoms
local mn, mx = math.huge, -1
for _, a in ipairs(src_atoms) do
local p = a.paths or {}
if (p.cycles_min or 0) < mn then mn = p.cycles_min or 0 end
if (p.cycles_max or 0) > mx then mx = p.cycles_max or 0 end
end
local path_str
if mx > 0 then
path_str = string.format(" cycles=%d..%d", mn, mx)
else
path_str = string.format(" %d cycles", mn)
end
add(string.format(" %-30s %d atom%s%s",
src.basename, atom_count,
atom_count == 1 and "" or "s",
path_str))
::continue::
end
-- Module-level findings summary (across all sources).
-- Info has its own count; it remains separate from warnings.
local total_errs = #errors
local total_warns = #warnings
local total_infos = #info
add("")
add(string.format("Module findings: %d error(s), %d warning(s), %d info", total_errs, total_warns, total_infos))
-- Per-source "scanned:" / "cycles:" summary lines (each line includes the source basename for traceability).
-- These are kept SEPARATE from the finding-level Info section above so the report's Info section is signal-only
-- (true findings), not a mix of findings + rollups.
-- The downstream test (`test_control_transfer_delay_slot.lua`)
-- asserts that the Info section contains NEITHER `scanned:` NOR `cycles:` lines.
if summaries and #summaries > 0 then
add("")
for _, s in ipairs(summaries) do
add(string.format(" %s", s.msg))
end
end
duffle.write_file(out_path, table.concat(lines, "\n") .. "\n")
return out_path
end
-- ════════════════════════════════════════════════════════════════════════════ -- ════════════════════════════════════════════════════════════════════════════
-- M.run — orchestrator entry -- M.run — orchestrator entry
-- ════════════════════════════════════════════════════════════════════════════ -- ════════════════════════════════════════════════════════════════════════════
@@ -2431,21 +2265,30 @@ function M.run(ctx)
for _, s in ipairs(result.summaries or {}) do dir_summaries[#dir_summaries + 1] = s end for _, s in ipairs(result.summaries or {}) do dir_summaries[#dir_summaries + 1] = s end
end end
-- Skip directories with zero atoms. A directory with only headers / no MipsAtom_ is "nothing to report". -- Stash per-module results on the corpus for `report.lua` to consume.
if #all_atoms == 0 then -- Avoids re-running validate() in the report pass + avoids rebuilding corpus_pipe_ctx.
-- Still aggregate errors/warnings/info so orchestrator sees them, but don't write a file. -- Pattern matches `corpus.atoms_by_name` / `corpus.word_counts` / `corpus.components`
-- (one writer: `static_analysis.lua`; one reader: `report.lua`).
-- Module basename = last component of `dir` ("code/duffle" -> "duffle").
local dir_basename = dir:match("([^/\\]+)$") or dir
corpus.static_analysis_results = corpus.static_analysis_results or {}
corpus.static_analysis_results[dir_basename] = {
atoms = all_atoms,
findings = all_findings,
errors = dir_errors,
warnings = dir_warnings,
info = dir_info,
summaries = dir_summaries,
sources = dir_sources,
}
-- Aggregate per-dir errors/warnings/info into the orchestrator totals.
-- Hoisted out of any per-dir file-emit so `report.lua` can drop the on-disk file emitter without losing the cross-module rollup.
for _, e in ipairs(dir_errors) do errors [#errors + 1] = e end for _, e in ipairs(dir_errors) do errors [#errors + 1] = e end
for _, w in ipairs(dir_warnings) do warnings[#warnings + 1] = w end for _, w in ipairs(dir_warnings) do warnings[#warnings + 1] = w end
for _, i_ in ipairs(dir_info) do info[#info + 1] = i_ end for _, i_ in ipairs(dir_info) do info [#info + 1] = i_ end
else -- (No per-dir emit: per-module findings are stashed on `corpus.static_analysis_results` above.
local out_path = emit_module_static_analysis_txt(ctx, dir, dir_sources, all_atoms, all_findings, dir_errors, dir_warnings, dir_info, dir_summaries) -- `report.lua` reads that projection to render `<module>.atom_meta_report.md` without re-running validate().)
if out_path then
table.insert(outputs, { static_analysis_txt = out_path })
end
for _, e in ipairs(dir_errors) do errors [#errors + 1] = e end
for _, w in ipairs(dir_warnings) do warnings[#warnings + 1] = w end
for _, i_ in ipairs(dir_info) do info[#info + 1] = i_ end
end
end end
-- Result exposes at least {outputs, errors, warnings, info}. -- Result exposes at least {outputs, errors, warnings, info}.
+7 -9
View File
@@ -2,17 +2,14 @@
--- ---
--- Dispatches to pass modules under `scripts/passes/`, resolving dependencies topologically (Kahn's algorithm + cycle detection). --- Dispatches to pass modules under `scripts/passes/`, resolving dependencies topologically (Kahn's algorithm + cycle detection).
--- ---
--- **Architecture**: --- Architecture:
--- - **PASSES table** — declarative dep graph (data, not code). --- - PASSES table: Declarative dep graph (data, not code).
--- - **FLAG_HANDLERS table** — maps CLI flags to handlers. --- - FLAG_HANDLERS table: Maps CLI flags to handlers.
--- - **parse_args****build_ctx** (resolves unity/direct includes or exact sources; no semantic scanning) → **topo_sort****dispatch_passes**. --- - parse_args → build_ctx (resolves unity/direct includes or exact sources) → topo_sort → dispatch_passes.
--- - The first pass in the dep graph is `scan-source` (see `passes/scan_source.lua`). --- - The first pass in the dep graph is `scan-source` (see `passes/scan_source.lua`).
--- It calls `duffle.scan_source` once per source to produce the fat `SourceScan` payload, which is attached to each `src.scan`. --- It calls `duffle.scan_source` once per source to produce the fat `SourceScan` payload, which is attached to each `src.scan`.
--- Every other pass that reads source structure depends on `scan-source` and consumes `src.scan` as a read-only. --- Every other pass that reads source structure depends on `scan-source` and consumes `src.scan` as a read-only.
--- ---
--- **Conventions**: tabs (1/level), EmmyLua annotations, no regex,
--- Lua 5.3 compatible.
---
-- ════════════════════════════════════════════════════════════════════════════ -- ════════════════════════════════════════════════════════════════════════════
-- Module-scope requires + package.path setup -- Module-scope requires + package.path setup
-- ════════════════════════════════════════════════════════════════════════════ -- ════════════════════════════════════════════════════════════════════════════
@@ -160,7 +157,7 @@ local PASSES = {
report = { report = {
module = "passes.report", module = "passes.report",
kind = "report", kind = "report",
deps = {"annotation", "static-analysis"}, deps = {"annotation", "static-analysis", "atoms-source-map"}, -- +atoms-source-map (consolidated-report-files refactor, 2026-07-26)
groups = { "pre-link" }, groups = { "pre-link" },
}, },
} }
@@ -206,7 +203,8 @@ end
-- Pass-kind taxonomy: Which kinds stop the build on errors? -- Pass-kind taxonomy: Which kinds stop the build on errors?
-- --
-- Report severity is independent from process exit policy. A "diagnostic" pass still writes every `error`/`warning` finding into its report file, -- Report severity is independent from process exit policy.
-- A "diagnostic" pass still writes every `error`/`warning` finding into its report file,
-- but `report_validation_errors` returns early for non-stopping kinds, so nothing is printed to stderr and the orchestrator does not exit non-zero. -- but `report_validation_errors` returns early for non-stopping kinds, so nothing is printed to stderr and the orchestrator does not exit non-zero.
-- Adding a new pass kind requires listing it here explicitly; an unknown kind must not silently fall back to "true". -- Adding a new pass kind requires listing it here explicitly; an unknown kind must not silently fall back to "true".
local PASS_KIND_STOP_ON_ERROR = { local PASS_KIND_STOP_ON_ERROR = {