mirror of
https://github.com/Ed94/pikuma_ps1.git
synced 2026-08-07 08:08:49 +00:00
Compare commits
6
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
02658d3609 | ||
|
|
dbc459b7e0 | ||
|
|
a704341fc6 | ||
|
|
7421b32fd7 | ||
|
|
e2eb74be19 | ||
|
|
338f1fe46e |
@@ -17,3 +17,5 @@ toolchain/PSn00bSDK
|
||||
.vscode/settings.json
|
||||
toolchain/lfs
|
||||
toolchain/lpeg
|
||||
|
||||
scratch
|
||||
|
||||
Vendored
-1
@@ -138,7 +138,6 @@
|
||||
"monitor reset shellhalt",
|
||||
"load build/hello_gte.dwarf-injected.elf",
|
||||
"source scripts/gdb/gdb_tape_atoms.gdb",
|
||||
"source build/gen/hello_gte.gdbinit",
|
||||
"tbreak main",
|
||||
"continue"
|
||||
]
|
||||
|
||||
@@ -98,11 +98,11 @@ WORD_COUNT(mac_format_f3_color, 3)
|
||||
/* atom_dbg_skip */
|
||||
/* Words: 3; Stores the 3 transformed (V2_S2 screen) vertices to the F3.
|
||||
* PIPELINE: post-RTPT (SXY0=v0.screen, SXY1=v1.screen, SXY2=v2.screen). */
|
||||
#define mac_gte_store_f3_post_rtpt(...) \
|
||||
#define mac_gte_store_f3(...) \
|
||||
gte_sw(C2_SXY0, R_PrimCursor, O_(Poly_F3,p0)) \
|
||||
, gte_sw(C2_SXY1, R_PrimCursor, O_(Poly_F3,p1)) \
|
||||
, gte_sw(C2_SXY2, R_PrimCursor, O_(Poly_F3,p2))
|
||||
WORD_COUNT(mac_gte_store_f3_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) \
|
||||
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
|
||||
* G4 triangle portion to p0/p1/p2.
|
||||
* PIPELINE: post-RTPT, pre-RTPS (SXY0=v0.screen, SXY1=v1.screen, SXY2=v2.screen).
|
||||
* MUST be called BEFORE V3-RTPS, otherwise SXY0/1/2
|
||||
* get overwritten with v3 (RTPS writes only to SXY2, but to keep the
|
||||
* three registers aligned with v0/v1/v2 you must store before RTPS).
|
||||
* 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(...) \
|
||||
* MUST be called BEFORE V3-RTPS, otherwise SXY0/1/2 get overwritten with v3
|
||||
* (RTPS writes only to SXY2, but to keep the three registers aligned with v0/v1/v2 you must store before RTPS). */
|
||||
#define mac_gte_store_g4_p012(...) \
|
||||
gte_sw(C2_SXY0, R_PrimCursor, O_(Poly_G4,p0)) \
|
||||
, gte_sw(C2_SXY1, R_PrimCursor, O_(Poly_G4,p1)) \
|
||||
, gte_sw(C2_SXY2, R_PrimCursor, O_(Poly_G4,p2))
|
||||
WORD_COUNT(mac_gte_store_g4_p012_post_rtpt_pre_rtps, 3)
|
||||
WORD_COUNT(mac_gte_store_g4_p012, 3)
|
||||
|
||||
/* atom_dbg_skip */
|
||||
/* Words: 1; Stores the V3 screen coord to the G4's p3 slot.
|
||||
* PIPELINE: post-RTPS (SXY2 holds v3.screen because RTPS writes its
|
||||
* single-vertex result to SXY2; SXY0 still holds v0.screen from the
|
||||
* earlier RTPT — DO NOT read SXY0 here, that's the bug this name
|
||||
* prevents).
|
||||
* PIPELINE: post-RTPS (SXY2 holds v3.screen because RTPS writes its single-vertex result to SXY2;
|
||||
* SXY0 still holds v0.screen from the earlier RTPT.
|
||||
*/
|
||||
#define mac_gte_store_g4_p3_post_rtps(...) \
|
||||
#define mac_gte_store_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)
|
||||
|
||||
|
||||
@@ -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.
|
||||
* 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_SXY1, R_PrimCursor, O_(Poly_F3,p1)),
|
||||
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
|
||||
* G4 triangle portion to p0/p1/p2.
|
||||
* PIPELINE: post-RTPT, pre-RTPS (SXY0=v0.screen, SXY1=v1.screen, SXY2=v2.screen).
|
||||
* MUST be called BEFORE V3-RTPS, otherwise SXY0/1/2
|
||||
* get overwritten with v3 (RTPS writes only to SXY2, but to keep the
|
||||
* three registers aligned with v0/v1/v2 you must store before RTPS).
|
||||
* 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) {
|
||||
* MUST be called BEFORE V3-RTPS, otherwise SXY0/1/2 get overwritten with v3
|
||||
* (RTPS writes only to SXY2, but to keep the three registers aligned with v0/v1/v2 you must store before RTPS). */
|
||||
atom_dbg_skip MipsAtomComp_(ac_gte_store_g4_p012) {
|
||||
gte_sw(C2_SXY0, R_PrimCursor, O_(Poly_G4,p0)),
|
||||
gte_sw(C2_SXY1, R_PrimCursor, O_(Poly_G4,p1)),
|
||||
gte_sw(C2_SXY2, R_PrimCursor, O_(Poly_G4,p2)),
|
||||
};
|
||||
|
||||
/* Words: 1; Stores the V3 screen coord to the G4's p3 slot.
|
||||
* PIPELINE: post-RTPS (SXY2 holds v3.screen because RTPS writes its
|
||||
* single-vertex result to SXY2; SXY0 still holds v0.screen from the
|
||||
* earlier RTPT — DO NOT read SXY0 here, that's the bug this name
|
||||
* prevents).
|
||||
* PIPELINE: post-RTPS (SXY2 holds v3.screen because RTPS writes its single-vertex result to SXY2;
|
||||
* SXY0 still holds v0.screen from the earlier RTPT.
|
||||
*/
|
||||
atom_dbg_skip MipsAtomComp_(ac_gte_store_g4_p3_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
|
||||
|
||||
|
||||
@@ -5,10 +5,10 @@
|
||||
#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_bounds_chk_cube_g4_face_exit 12
|
||||
#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,
|
||||
@@ -18,7 +18,7 @@ enum {
|
||||
// --- atom: floor_f3_face (58 words) ---
|
||||
|
||||
#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 {
|
||||
atom_offset_culling_floor_f3_face_exit = _atom_offset_culling_floor_f3_face_exit,
|
||||
@@ -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/word_count.metadata.h"
|
||||
|
||||
# include "gen/gte_hello.offsets.h"
|
||||
# include "gen/hello_gte.offsets.h"
|
||||
#include "hello_gte.h"
|
||||
|
||||
#include "hello_gte_tape.c"
|
||||
#include "hello_gte.tape.c"
|
||||
|
||||
typedef U4 OrderingTable_Buffer[OrderingTbl_Len];
|
||||
typedef Array_(OrderingTable_Buffer, 2);
|
||||
@@ -4,7 +4,7 @@
|
||||
# include "duffle/atom_dsl.h"
|
||||
# include "duffle/lottes_tape.h"
|
||||
# include "duffle/word_count.metadata.h"
|
||||
# include "gen/gte_hello.offsets.h"
|
||||
# include "gen/hello_gte.offsets.h"
|
||||
# include "hello_gte.h"
|
||||
#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)),
|
||||
|
||||
mac_gte_load_tri_verts(R_T0, R_T1, R_T2),
|
||||
nop2, gte_cmdw_rotate_translate_perspective_triple,
|
||||
nop2, gte_cmdw_nclip,
|
||||
nop2, gte_cmdw_rotate_translate_perspective_triple, // required cpu -> gte delay slot
|
||||
gte_cmdw_nclip,
|
||||
|
||||
nop2, gte_mv_from_data_r(R_T0, C2_MAC0),
|
||||
nop,
|
||||
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),
|
||||
mac_gte_store_g4_p012_post_rtpt_pre_rtps(),
|
||||
|
||||
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(),
|
||||
// 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 */
|
||||
@@ -103,11 +102,11 @@ MipsAtom_(rbind_floor_f3_face) atom_info(atom_bind(Binds_FloorTri), atom_phase(f
|
||||
mac_yield()
|
||||
};
|
||||
|
||||
atom_dbg_skip
|
||||
// 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_FaceCursr)
|
||||
, 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),
|
||||
@@ -118,8 +117,7 @@ MipsAtom_(floor_f3_face) atom_info(atom_phase(floor_f3)
|
||||
gte_mv_from_data_r(R_T0, C2_MAC0),
|
||||
nop, branch_le_zero(R_T0, atom_offset(culling, floor_f3_face_exit)), nop, // required gte -> cpu load-delay slot.
|
||||
/* Format Primitive */
|
||||
mac_format_f3_color(0xFF, 0xFF, 0xFF), // RGB-form (R=FF, G=FF, B=FF = white)
|
||||
mac_gte_store_f3_post_rtpt(),
|
||||
mac_gte_store_f3(),
|
||||
|
||||
/* Calculate Depth */
|
||||
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),
|
||||
set_lt_u( R_AT, R_T1, R_AT),
|
||||
branch_equal(R_AT, R_0, atom_offset(bounds_chk, floor_f3_face_exit)), nop,
|
||||
/* Insert into Ordering Table Linked List */
|
||||
mac_insert_ot_tag_f3(),
|
||||
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.
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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)
|
||||
@@ -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
@@ -350,10 +350,10 @@ function build-graphis_hello {
|
||||
}
|
||||
# build-graphis_hello
|
||||
|
||||
function build-gte_hello {
|
||||
function build-hello_gte {
|
||||
$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_atom_metadata = join-path $path_duffle 'word_count.metadata.h'
|
||||
$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...
|
||||
function Send-ToEmulator { param( [string]$exePath )
|
||||
|
||||
+17
-38
@@ -1180,10 +1180,9 @@ M.GTE_COMMAND_INPUTS = {
|
||||
-- * "mac_result" : generic MAC output (nclip, op, mvmva)
|
||||
--
|
||||
-- Consumers:
|
||||
-- * passes/static_analysis.lua::analyze_hardware_relations (the walker reads this after a GTE command to update
|
||||
-- `forward_state.post_command_roles` for `gte_result_position`).
|
||||
-- * 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 result-position check.
|
||||
-- * 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`).
|
||||
-- * passes/static_analysis.lua::check_gte_role_mismatch (per-atom CHECK_RULES reader; renders role mismatches).
|
||||
-- This table is consumed by the hardware-relation analyzer and the gte_role_mismatch check.
|
||||
M.GTE_COMMAND_OUTPUTS = {
|
||||
-- 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.
|
||||
@@ -1293,32 +1292,8 @@ M.GTE_COMMAND_LATCH_WINDOWS = {
|
||||
},
|
||||
}
|
||||
|
||||
-- GTE component result contracts (immutable; keyed by bare component name).
|
||||
--
|
||||
-- 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",
|
||||
},
|
||||
}
|
||||
-- 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.
|
||||
|
||||
-- Operand-class table for the COP2->GPR load-delay check.
|
||||
--
|
||||
@@ -1447,6 +1422,7 @@ M.GP0_CMD_BY_SHAPE = {
|
||||
["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.
|
||||
-- (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
|
||||
@@ -1455,10 +1431,10 @@ M.GP0_MACRO_CONTRIB = {
|
||||
["mac_format_f3_color"] = 1,
|
||||
["mac_format_g3_color"] = 3,
|
||||
["mac_format_g4_color"] = 4,
|
||||
["mac_gte_store_f3_post_rtpt"] = 3,
|
||||
["mac_gte_store_g3_post_rtpt"] = 3,
|
||||
["mac_gte_store_g4_p012_post_rtpt_pre_rtps"] = 3,
|
||||
["mac_gte_store_g4_p3_post_rtps"] = 1,
|
||||
["mac_gte_store_f3"] = 3,
|
||||
["mac_gte_store_g3"] = 3,
|
||||
["mac_gte_store_g4_p012"] = 3,
|
||||
["mac_gte_store_g4_p3"] = 1,
|
||||
["mac_insert_ot_tag_f3"] = 1,
|
||||
["mac_insert_ot_tag_g4"] = 1,
|
||||
}
|
||||
@@ -1583,6 +1559,8 @@ M.INSTRUCTION_LATENCY = {
|
||||
["gte_load_v1"] = 2,
|
||||
["gte_load_v2"] = 2,
|
||||
["gte_load_v0v1v2"] = 6,
|
||||
|
||||
-- TODO(Ed): REMOVE THIS HARDCODE, THIS SHOULD BE RESOLVED AUTOMATICALLY
|
||||
-- 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"] = 0,
|
||||
@@ -1591,12 +1569,13 @@ M.INSTRUCTION_LATENCY = {
|
||||
["mac_format_g4_color"] = 12, -- 4 x mac_pack_color_word
|
||||
["mac_load_tri_indices"] = 3, -- 3 x lhu
|
||||
["mac_gte_load_tri_verts"] = 18, -- 3 x {sll, addu, lw, lw, mtc2, mtc2}
|
||||
["mac_gte_store_f3_post_rtpt"] = 3,
|
||||
["mac_gte_store_g3_post_rtpt"] = 3,
|
||||
["mac_gte_store_g4_p012_post_rtpt_pre_rtps"] = 3,
|
||||
["mac_gte_store_g4_p3_post_rtps"] = 1,
|
||||
["mac_gte_store_f3"] = 3,
|
||||
["mac_gte_store_g3"] = 3,
|
||||
["mac_gte_store_g4_p012"] = 3,
|
||||
["mac_gte_store_g4_p3"] = 1,
|
||||
["mac_insert_ot_tag_f3"] = 11, -- 11 .word slots in the macro body
|
||||
["mac_insert_ot_tag_g4"] = 11,
|
||||
|
||||
-- Annotation markers (emit no code; pure metaprogram hints)
|
||||
["atom_label"] = 0,
|
||||
["atom_offset"] = 0,
|
||||
|
||||
@@ -3,16 +3,16 @@
|
||||
# Wrapper for the tape-atom step-debug helpers.
|
||||
# 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`
|
||||
# (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.
|
||||
#
|
||||
# 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.
|
||||
|
||||
# ── 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
|
||||
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."
|
||||
end
|
||||
document tape_atoms
|
||||
@@ -21,35 +21,35 @@ document tape_atoms
|
||||
end
|
||||
|
||||
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
|
||||
document break_atom
|
||||
Set a breakpoint at the start of tape atom <name>. STUB state.
|
||||
end
|
||||
|
||||
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
|
||||
document step_atom
|
||||
Resume execution until the next atom boundary. STUB state.
|
||||
end
|
||||
|
||||
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
|
||||
document next_atom
|
||||
Alias for step_atom. STUB state.
|
||||
end
|
||||
|
||||
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
|
||||
document where_in_atom
|
||||
Report current atom name, .rodata addr, word offset, and source line (if known). STUB state.
|
||||
end
|
||||
|
||||
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
|
||||
document stepi_inside_atom
|
||||
One MIPS-instruction step, then where_in_atom. STUB state.
|
||||
@@ -89,17 +89,17 @@ document wave_ctx
|
||||
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).
|
||||
# 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...
|
||||
# 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
|
||||
|
||||
# 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
|
||||
echo [gdb_tape_atoms] Runtime sourced successfully (9 commands now have real implementations).
|
||||
|
||||
@@ -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.
|
||||
--- 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.
|
||||
-- `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 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:
|
||||
-- * 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
|
||||
|
||||
-- ════════════════════════════════════════════════════════════════════════════
|
||||
-- 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
|
||||
-- ════════════════════════════════════════════════════════════════════════════
|
||||
@@ -683,11 +642,6 @@ function M.run(ctx)
|
||||
warnings [#warnings + 1] = { line = w.line, msg = w.msg }
|
||||
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
|
||||
|
||||
return { outputs = outputs, errors = errors, warnings = warnings }
|
||||
|
||||
@@ -4,20 +4,18 @@
|
||||
--- `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>`).
|
||||
---
|
||||
--- Inputs from `atom.paths`: the ordered `items` stream, dense `word_events`, `invocations` views. Outputs: one
|
||||
--- `WORD N LINE L TEXT T` line per emitted `.word`, plus the per-word provenance form that DWARF synthesis consumes.
|
||||
--- Inputs from `atom.paths`: the ordered `items` stream, dense `word_events`, `invocations` views. Outputs:
|
||||
--- 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`):
|
||||
--- 1. **Sourcemap.txt form** — `<out_root>/<basename>.atoms.sourcemap.txt`. Format-version-tagged for forward-compat.
|
||||
--- Lives in `<out_root>/` (build/gen). Mirrors the convention used by `annotation.lua`
|
||||
--- (`<out_root>/<basename>.errors.h`) and `static_analysis.lua` (`<out_root>/<basename>.static_analysis.txt`).
|
||||
--- Two output forms:
|
||||
--- 1. Markdown form: Handled by `passes/report.lua` (writes `<module>.atoms.md`).
|
||||
--- The render functions `render_source_map` + `render_provenance` are exported for `report.lua` to call directly.
|
||||
--- 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
|
||||
--- from `nm`, the 9 user commands are static `define ... end` blocks. Emitted when `ctx.flags.gdb_runtime` is true
|
||||
--- AND `ctx.flags.elf_path` points to an existing ELF. Useful for `gdb-multiarch --without-python` users
|
||||
--- (the common case on Windows MinGW builds) — `source <path>` loads it with no Python / Tcl / Guile required.
|
||||
--- 2. `gdb_tape_atoms_runtime.gdb`: Post-link opt-in (`ctx.flags.gdb_runtime`),
|
||||
--- so the gdb wrapper script + the generated runtime script share the same canonical location.
|
||||
--- Triggered by `--post-link` or `--gdb-runtime`.
|
||||
---
|
||||
--- **Output format** (sourcemap.txt form):
|
||||
--- Output forma (sourcemap.txt form):
|
||||
--- ```
|
||||
--- # FORMAT_VERSION 1
|
||||
--- # 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.
|
||||
---
|
||||
--- **Conventions:** tabs (1/level), EmmyLua annotations, no regex, Lua 5.3 compatible.
|
||||
|
||||
-- ════════════════════════════════════════════════════════════════════════════
|
||||
-- Module-scope requires + package.path setup
|
||||
@@ -458,7 +454,22 @@ local function emit_gdb_runtime(ctx)
|
||||
-- Confirmation line for the source operator.
|
||||
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.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))
|
||||
@@ -470,10 +481,62 @@ end
|
||||
|
||||
local M = {}
|
||||
|
||||
--- 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`.
|
||||
-- Expose the pure render functions so `report.lua` and the focused tests can call them directly without triggering the file-emit path.
|
||||
M.render_source_map = render_source_map
|
||||
M.render_provenance = render_provenance
|
||||
|
||||
--- 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
|
||||
--- @return PassResult
|
||||
function M.run(ctx)
|
||||
@@ -495,34 +558,8 @@ function M.run(ctx)
|
||||
}
|
||||
end
|
||||
|
||||
-- Always emit the text form (per-source).
|
||||
for _, src in ipairs(corpus.source_order) do
|
||||
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
|
||||
-- atoms.sourcemap.txt + atoms.provenance.txt content moved to report.lua via `<module>.atoms.md` markdown file.
|
||||
-- This pass emits only the post-link gdb_runtime artifact (see emit_gdb_runtime below).
|
||||
|
||||
-- Optionally emit the gdb-runtime form (post-link, one file per build).
|
||||
if ctx.flags and ctx.flags.gdb_runtime then
|
||||
|
||||
@@ -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_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).
|
||||
-- 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).
|
||||
@@ -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.
|
||||
-- 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 = {
|
||||
["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,
|
||||
}
|
||||
|
||||
|
||||
+380
-257
@@ -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.
|
||||
--- 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
|
||||
@@ -18,8 +15,7 @@
|
||||
-- Resolve `arg[0]` to an absolute-ish script directory so that `require("duffle")` resolves against `scripts/` regardless of CWD.
|
||||
-- Bootstrap: see `ps1_meta.lua` for the rationale.
|
||||
-- Bootstrap: load `scripts/duffle_paths.lua` (sets package.path + package.cpath).
|
||||
-- Uses `debug.getinfo` to find this file's own directory, so it works
|
||||
-- both standalone and when require'd from the orchestrator.
|
||||
-- Uses `debug.getinfo` to find this file's own directory, so it works both standalone and when require'd from the orchestrator.
|
||||
-- Bootstrap: load `duffle_paths.lua` via `debug.getinfo(1, "S").source` (works both standalone + when require'd).
|
||||
-- duffle_paths.lua sets package.path then returns `require("duffle")` at the bottom, so the dofile value IS the duffle module.
|
||||
local _bootstrap_dir = debug.getinfo(1, "S").source:match("^@?(.*[/\\])") or "./"
|
||||
@@ -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.
|
||||
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
|
||||
-- ════════════════════════════════════════════════════════════════════════════
|
||||
@@ -44,8 +45,7 @@ local SECTION_HEADER_MACROS = "── Macro word-count declarations ───
|
||||
local SECTION_HEADER_ERRORS = "── Errors ──────────────────────────────────────────────"
|
||||
local SECTION_HEADER_WARNINGS = "── Warnings ────────────────────────────────────────────"
|
||||
|
||||
-- Lua pattern that captures the basename (last path segment) of a
|
||||
-- forward- or back-slash separated path.
|
||||
-- Lua pattern that captures the basename (last path segment) of a forward- or back-slash separated path.
|
||||
local BASENAME_PATTERN = "([^/\\]+)$"
|
||||
|
||||
-- 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
|
||||
end
|
||||
|
||||
--- (internal) Format a single annotation entry as one rendered line.
|
||||
--- @param a AnnotEntry
|
||||
--- @param src_name string
|
||||
-- ════════════════════════════════════════════════════════════════════════════
|
||||
-- Markdown renderers (consolidated-report-files refactor, 2026-07-26)
|
||||
-- ════════════════════════════════════════════════════════════════════════════
|
||||
|
||||
--- 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
|
||||
local function format_annot_line(a, src_name)
|
||||
if a.error then
|
||||
return string.format(" ✗ line %d %s [ERROR: %s] [%s]", a.line, a.macro or "?", a.error, src_name)
|
||||
local function render_project_summary(all_results)
|
||||
local lines = {
|
||||
"# 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
|
||||
local line = string.format(" ● line %d %s [%s]", a.line, a.name, src_name)
|
||||
if a.binds then line = line .. " binds=" .. a.binds end
|
||||
if #a.reads > 0 then line = line .. " reads={" .. table.concat(a.reads, ",") .. "}" end
|
||||
if #a.writes > 0 then line = line .. " writes={" .. table.concat(a.writes, ",") .. "}" end
|
||||
return line
|
||||
lines[#lines + 1] = string.format(
|
||||
"| **TOTAL** | %d | %d | %d | %d | %d | %d | %d | %d |",
|
||||
totals.atoms, totals.annots, totals.binds, totals.macros,
|
||||
totals.findings, totals.errors, totals.warnings, totals.info)
|
||||
return table.concat(lines, "\n") .. "\n"
|
||||
end
|
||||
|
||||
--- (internal) Tally totals across all results in a module.
|
||||
--- @param results AnnotationResult[]
|
||||
--- @return integer, integer, integer, integer, integer, integer
|
||||
local function tally_module_totals(results)
|
||||
local total_atoms, total_annots, total_binds, total_macros = 0, 0, 0, 0
|
||||
local total_errors, total_warnings = 0, 0
|
||||
for _, r in ipairs(results) do
|
||||
total_atoms = total_atoms + #r.atoms
|
||||
total_annots = total_annots + #r.annots
|
||||
total_binds = total_binds + #r.binds
|
||||
total_macros = total_macros + #r.macros
|
||||
total_errors = total_errors + #r.errors
|
||||
total_warnings = total_warnings + #r.warnings
|
||||
--- Render the per-module verbose source-map markdown (`build/<module>.atoms.md`).
|
||||
--- Per-source sub-section, per-atom stanza with sourcemap + provenance rows.
|
||||
--- Pulls sourcemap + provenance from `atoms_source_map` (no second source walk).
|
||||
--- @param dir string
|
||||
--- @param dir_sources SourceFile[]
|
||||
--- @param wc table<string, integer>
|
||||
--- @return string
|
||||
local function render_module_atoms_md(dir, dir_sources, wc)
|
||||
local dir_basename = source_basename(dir)
|
||||
local lines = {
|
||||
"# " .. dir_basename .. " — atoms (verbose source map)",
|
||||
"> Per-word call-site + provenance. Auto-generated.",
|
||||
"",
|
||||
}
|
||||
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
|
||||
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
|
||||
|
||||
-- (internal) Section renderer: per-source atom declarations.
|
||||
local function render_module_atoms_section(add, results)
|
||||
add(SECTION_HEADER_ATOMS)
|
||||
for _, r in ipairs(results) do
|
||||
--- Render the consolidated per-module markdown (`build/<module>.atom_meta_report.md`).
|
||||
--- Aggregates annotation + static-analysis content across all sources in `dir`.
|
||||
--- Annotations come from re-running `annotation.validate()` per source (the existing pattern);
|
||||
--- static-analysis comes from `corpus.static_analysis_results[dir_basename]` (populated by `static_analysis.lua` — no second corpus_pipe_ctx build).
|
||||
--- @param dir string
|
||||
--- @param dir_sources SourceFile[]
|
||||
--- @param 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)
|
||||
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
|
||||
add("")
|
||||
end
|
||||
|
||||
-- (internal) Section renderer: per-source annotation entries.
|
||||
local function render_module_annots_section(add, results)
|
||||
add(SECTION_HEADER_ANNOTS)
|
||||
for _, r in ipairs(results) do
|
||||
-- Annotations
|
||||
add("## Annotations"); add("")
|
||||
if #annot_results == 0 then
|
||||
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)
|
||||
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
|
||||
add("")
|
||||
end
|
||||
|
||||
-- (internal) Section renderer: per-source Binds_* struct declarations.
|
||||
local function render_module_binds_section(add, results)
|
||||
add(SECTION_HEADER_BINDS)
|
||||
for _, r in ipairs(results) do
|
||||
-- Binds_* structs
|
||||
add("## Binds_* structs"); add("")
|
||||
if #annot_results == 0 then
|
||||
add("_(none)_")
|
||||
else
|
||||
for _, r in ipairs(annot_results) do
|
||||
local src_name = source_basename(r.source)
|
||||
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
|
||||
add(string.format(" +%2d: %s", f.offset, f.name))
|
||||
end
|
||||
end
|
||||
add(string.format("- `+%d %s`", f.offset, f.name))
|
||||
end
|
||||
add("")
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
-- (internal) Section renderer: per-source macro word-count declarations.
|
||||
local function render_module_macros_section(add, results)
|
||||
add(SECTION_HEADER_MACROS)
|
||||
for _, r in ipairs(results) do
|
||||
-- Macro decls
|
||||
add("## Macro word-count declarations"); add("")
|
||||
if #annot_results == 0 then
|
||||
add("_(none)_")
|
||||
else
|
||||
add("| source | line | macro declaration |")
|
||||
add("|--------|------|-------------------|")
|
||||
for _, r in ipairs(annot_results) do
|
||||
local src_name = source_basename(r.source)
|
||||
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
|
||||
add("")
|
||||
end
|
||||
|
||||
-- (internal) Section renderer: per-source errors (one-line + "(none)" if empty).
|
||||
local function render_module_errors_section(add, results, total_errors)
|
||||
add(SECTION_HEADER_ERRORS)
|
||||
if total_errors == 0 then
|
||||
add(" (none)")
|
||||
-- Findings by atom (static-analysis)
|
||||
add("## Static analysis — findings by atom"); add("")
|
||||
local by_atom = {}
|
||||
for _, f in ipairs(sa_results.findings or {}) 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)_")
|
||||
else
|
||||
for _, r in ipairs(results) do
|
||||
local src_name = source_basename(r.source)
|
||||
for _, e in ipairs(r.errors) do
|
||||
add(string.format(" ✗ line %d %s [%s]", e.line, e.msg, src_name))
|
||||
end
|
||||
end
|
||||
for _, a in ipairs(sa_results.atoms or {}) do
|
||||
local fs = by_atom[a.name]
|
||||
if fs then
|
||||
add(string.format("### %s", a.name))
|
||||
for _, f in ipairs(fs) do
|
||||
add(string.format("- `[%s] %s`", f.check, f.msg))
|
||||
end
|
||||
add("")
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
-- (internal) Section renderer: per-source warnings (one-line + "(none)" if empty).
|
||||
local function render_module_warnings_section(add, results, total_warnings)
|
||||
add(SECTION_HEADER_WARNINGS)
|
||||
if total_warnings == 0 then
|
||||
add(" (none)")
|
||||
-- Errors / Warnings / Info
|
||||
local function add_findings(label, entries)
|
||||
add(string.format("## %s", label))
|
||||
if #entries == 0 then
|
||||
add("_(none)_")
|
||||
else
|
||||
for _, r in ipairs(results) do
|
||||
local src_name = source_basename(r.source)
|
||||
for _, w in ipairs(r.warnings) do
|
||||
add(string.format(" ⚠ line %d %s [%s]", w.line, w.msg, src_name))
|
||||
end
|
||||
for _, e in ipairs(entries) do
|
||||
add(string.format("- line %d %s", e.line, e.msg))
|
||||
end
|
||||
end
|
||||
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
|
||||
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"
|
||||
end
|
||||
|
||||
-- ════════════════════════════════════════════════════════════════════════════
|
||||
-- Per-project summary
|
||||
-- REPORT_RENDERERS — data-driven report dispatch (one row per file kind)
|
||||
-- ════════════════════════════════════════════════════════════════════════════
|
||||
|
||||
--- Render the per-project summary (`build/gen/annotation_validation.txt`).
|
||||
--- Aggregates totals across all sources; lists per-source error counts if any source has errors.
|
||||
--- @param all_results AnnotationResult[]
|
||||
--- @return string
|
||||
local function render_project_report(all_results)
|
||||
local lines = {}
|
||||
local function add(s) lines[#lines + 1] = s end
|
||||
|
||||
local total_atoms, total_annots, total_macros, total_binds = 0, 0, 0, 0
|
||||
local total_errors, total_warnings = 0, 0
|
||||
for _, r in ipairs(all_results) do
|
||||
total_atoms = total_atoms + #r.atoms
|
||||
total_annots = total_annots + #r.annots
|
||||
total_macros = total_macros + #r.macros
|
||||
total_binds = total_binds + #r.binds
|
||||
total_errors = total_errors + #r.errors
|
||||
total_warnings = total_warnings + #r.warnings
|
||||
end
|
||||
|
||||
add(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 = {}
|
||||
-- `once = true` means render once at the project level (not per-module).
|
||||
-- `basename(dir_basename)` yields the file's basename for that kind.
|
||||
-- `gather(ctx, dir, dir_sources [, all_modules])` returns the rendered string.
|
||||
local REPORT_RENDERERS = {
|
||||
{
|
||||
name = "atom_meta_report",
|
||||
ext = "md",
|
||||
basename = function(dir_basename) return dir_basename .. ".atom_meta_report" end,
|
||||
once = false,
|
||||
gather = function(ctx, dir, dir_sources)
|
||||
-- Annotations: re-run `annotation.validate()` per source (the existing pattern).
|
||||
local annot_results = {}
|
||||
for _, src in ipairs(dir_sources) do
|
||||
if src.scan then
|
||||
local result = annotation.validate(ctx, src, nil)
|
||||
result.source = src.path -- tag for downstream rendering
|
||||
module_results[#module_results + 1] = result
|
||||
all_results[#all_results + 1] = result
|
||||
local r = annotation.validate(ctx, src, nil)
|
||||
r.source = src.path
|
||||
annot_results[#annot_results + 1] = r
|
||||
end
|
||||
end
|
||||
return module_results, all_results
|
||||
end
|
||||
|
||||
--- (internal) Does this module's results contain anything worth emitting?
|
||||
--- @param module_results AnnotationResult[]
|
||||
--- @return boolean
|
||||
local function module_has_content(module_results)
|
||||
for _, r in ipairs(module_results) do
|
||||
if #r.atoms > 0 or #r.annots > 0 or #r.binds > 0
|
||||
or #r.macros > 0 or #r.errors > 0 or #r.warnings > 0 then
|
||||
return true
|
||||
end
|
||||
end
|
||||
return false
|
||||
end
|
||||
|
||||
--- (internal) Log a debug message if `_G[DEBUG_FLAG]` is truthy.
|
||||
--- @param fmt string
|
||||
local function debug_log(fmt, ...)
|
||||
if _G[DEBUG_FLAG] then
|
||||
io.stderr:write(string.format("[%s] " .. fmt, PASS_NAME, ...))
|
||||
end
|
||||
end
|
||||
-- Static-analysis: read stashed projection (no re-validate).
|
||||
local dir_basename = dir:match("([^/\\]+)$") or dir
|
||||
local sa_results = (ctx.shared.corpus.static_analysis_results or {})[dir_basename] or {}
|
||||
return render_module_meta_report(dir, dir_sources, annot_results, sa_results)
|
||||
end,
|
||||
},
|
||||
{
|
||||
name = "atoms",
|
||||
ext = "md",
|
||||
basename = function(dir_basename) return dir_basename .. ".atoms" end,
|
||||
once = false,
|
||||
gather = function(ctx, dir, dir_sources)
|
||||
return render_module_atoms_md(dir, dir_sources,
|
||||
ctx.shared.corpus.word_counts or {})
|
||||
end,
|
||||
},
|
||||
{
|
||||
name = "summary",
|
||||
ext = "md",
|
||||
basename = function(_dir_basename) return "atom_meta_report.summary" end,
|
||||
once = true,
|
||||
gather = function(_ctx, _dir, _dir_sources, all_modules)
|
||||
return render_project_summary(all_modules)
|
||||
end,
|
||||
},
|
||||
}
|
||||
|
||||
-- ════════════════════════════════════════════════════════════════════════════
|
||||
-- M — module exports
|
||||
-- M — public pass surface
|
||||
-- ════════════════════════════════════════════════════════════════════════════
|
||||
|
||||
local M = {}
|
||||
|
||||
--- Run the report pass.
|
||||
--- Renders one `<dir_basename>.annotations.txt` per source-directory that has content, plus the project-wide `annotation_validation.txt` summary.
|
||||
--- 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).
|
||||
--- Reads `corpus.static_analysis_results` (added in Phase 1) to populate per-module findings without re-running validate().
|
||||
--- @param ctx PassCtx
|
||||
--- @return PassResult
|
||||
function M.run(ctx)
|
||||
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 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
|
||||
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
|
||||
local module_results, all_results = lookup_module_results(ctx, dir_sources)
|
||||
for _, r in ipairs(all_results) do
|
||||
all_results_for_summary[#all_results_for_summary + 1] = r
|
||||
end
|
||||
|
||||
if module_has_content(module_results) then
|
||||
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
|
||||
-- Per-renderer dispatch for the per-module renderers (once = false).
|
||||
for _, renderer in ipairs(REPORT_RENDERERS) do
|
||||
if not renderer.once then
|
||||
local body = renderer.gather(ctx, dir, dir_sources)
|
||||
local out_path = out_root_effective .. "/" .. renderer.basename(dir_basename) .. "." .. renderer.ext
|
||||
duffle.write_file(out_path, body)
|
||||
outputs[#outputs + 1] = { kind = renderer.name, path = out_path }
|
||||
end
|
||||
end
|
||||
|
||||
if #all_results_for_summary > 0 then
|
||||
local summary_path = ctx.out_root .. "/annotation_validation.txt"
|
||||
duffle.write_file(summary_path, render_project_report(all_results_for_summary))
|
||||
outputs[#outputs + 1] = { summary_txt = summary_path }
|
||||
-- For the summary, compute per-module totals once (re-validating annotations per source — same pattern as the meta_report renderer).
|
||||
local annot_results = {}
|
||||
for _, src in ipairs(dir_sources) do
|
||||
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
|
||||
|
||||
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
|
||||
|
||||
return M
|
||||
|
||||
+114
-271
@@ -39,17 +39,19 @@
|
||||
--- 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.
|
||||
---
|
||||
--- 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:
|
||||
--- `["static-analysis"] = {
|
||||
--- module = "passes.static_analysis",
|
||||
--- kind = "diagnostic",
|
||||
--- 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.
|
||||
---
|
||||
--- **Conventions**: tabs (1/level), EmmyLua annotations, no regex, Lua 5.3 compatible.
|
||||
|
||||
-- ════════════════════════════════════════════════════════════════════════════
|
||||
-- Module-scope requires + package.path setup
|
||||
@@ -148,6 +150,38 @@ local OUTPUT_EXTENSION = ".static_analysis.txt"
|
||||
--- @field findings Finding[] -- findings for this atom
|
||||
--- @field total_cycles integer -- sum of token cycle costs
|
||||
|
||||
-- ════════════════════════════════════════════════════════════════════════════
|
||||
-- Per-word-event helpers
|
||||
-- ════════════════════════════════════════════════════════════════════════════
|
||||
|
||||
-- Pick the source-line field that best represents "where in the user's source file is this word?".
|
||||
--
|
||||
-- `word_events` (populated by `passes/emission_model.lua::stamp_root_provenance`) carry four line fields:
|
||||
-- * `call_line` — physical line in the ROOT atom's source (the line of the `mac_X(...)` call site that triggered this emission, or `body_line` for direct words in the atom body)
|
||||
-- * `body_line` — physical line in the body containing the emitted word (the atom body for direct words; the component body for words expanded inside `mac_X(...)`)
|
||||
-- * `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
|
||||
-- ════════════════════════════════════════════════════════════════════════════
|
||||
@@ -551,11 +585,12 @@ local function append_cu2_finding(atom, event, forward, transition,
|
||||
local event_ident = event.encoder or event.ident or "?"
|
||||
local policy = duffle.CU2_TRANSITION_POLICY or {}
|
||||
local evidence = policy.evidence or {}
|
||||
local event_line = line_for_word_event(event)
|
||||
atom.paths.hazards[#atom.paths.hazards + 1] = {
|
||||
check = "transfer_hazards",
|
||||
kind = kind,
|
||||
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 "",
|
||||
relation_id = "mtc0_cu2_visibility",
|
||||
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 target = transition.target_state
|
||||
local event_line = line_for_word_event(event)
|
||||
if target == "unknown" then
|
||||
append_cu2_finding(atom, event, forward, transition, gap, "info", "unknown",
|
||||
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
|
||||
)
|
||||
)
|
||||
@@ -635,7 +671,7 @@ local function consume_cu2_transition(atom, event, ev_word, forward)
|
||||
local verb = target == "enabled" and "enable" or "disable"
|
||||
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)"
|
||||
, atom.name, event.body_line or event.line or event.def_line or 0
|
||||
, atom.name, event_line
|
||||
, verb, gap, transition.required
|
||||
)
|
||||
)
|
||||
@@ -653,7 +689,7 @@ local function consume_cu2_transition(atom, event, ev_word, forward)
|
||||
string.format(
|
||||
"%s at line %d: COP2 unavailable after SR.CU2 was disabled"
|
||||
.. " (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))
|
||||
forward.cu2_state = "disabled"
|
||||
end
|
||||
@@ -708,7 +744,7 @@ local function analyze_hardware_relations(atom)
|
||||
|
||||
for _, ev in ipairs(events) do
|
||||
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_args = ev.args or {}
|
||||
-- `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. ──
|
||||
-- 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 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
|
||||
|
||||
-- ─────────────────────────────────────────────────────────────────────────
|
||||
-- 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`.
|
||||
-- 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.
|
||||
-- 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.
|
||||
-- ─────────────────────────────────────────────────────────────────────────
|
||||
|
||||
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
|
||||
if not forward or not forward.post_command_roles then return end
|
||||
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`.
|
||||
-- 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
|
||||
@@ -1047,11 +1056,12 @@ local function check_gte_result_position(atom, _pipe_ctx, findings)
|
||||
-- This is a semantic mismatch.
|
||||
if reg ~= latest_screen_xy_entry.command_register
|
||||
and (reg == "C2_SXY0" or reg == "C2_SXY1") then
|
||||
local ev_line = line_for_word_event(ev)
|
||||
findings[#findings + 1] = {
|
||||
check = "gte_result_position",
|
||||
check = "gte_role_mismatch",
|
||||
kind = "warning",
|
||||
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 "",
|
||||
relation_id = "result_role_mismatch",
|
||||
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_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)"
|
||||
, atom.name, ev.body_line or ev.line or ev.def_line or 0
|
||||
, atom.name, ev_line
|
||||
, reg, latest_screen_xy_entry.command
|
||||
, latest_screen_xy_entry.role
|
||||
, reg),
|
||||
@@ -1084,6 +1094,13 @@ end
|
||||
-- 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.
|
||||
--
|
||||
-- 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.
|
||||
-- ─────────────────────────────────────────────────────────────────────────
|
||||
|
||||
@@ -1091,6 +1108,9 @@ local function check_hazard_nop_use(atom, _pipe_ctx, findings)
|
||||
local forward = atom.paths and atom.paths.forward_state
|
||||
local events = atom.paths.word_events or {}
|
||||
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 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_args = ev.args or {}
|
||||
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.
|
||||
if ev_ident == "nop" and prev_ev ~= nil then
|
||||
-- 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_args = prev_ev.args or {}
|
||||
local bd_policies = duffle.CONTROL_TRANSFER_DELAY_SLOT_POLICIES or {}
|
||||
local is_bd_slot = false
|
||||
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
|
||||
local is_bd_slot = bd_policies[prev_ident] ~= nil
|
||||
if not is_bd_slot then
|
||||
-- Find a pending modeled relation that this nop would retire.
|
||||
local retired = nil
|
||||
@@ -1157,7 +1173,7 @@ local function check_hazard_nop_use(atom, _pipe_ctx, findings)
|
||||
check = "hazard_nop_use",
|
||||
kind = "info",
|
||||
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 "",
|
||||
nop_classification = "modeled-required",
|
||||
nop_word_index = ev_word,
|
||||
@@ -1165,7 +1181,7 @@ local function check_hazard_nop_use(atom, _pipe_ctx, findings)
|
||||
producer_destination = retired.destination,
|
||||
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)"
|
||||
, 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
|
||||
@@ -1173,16 +1189,16 @@ local function check_hazard_nop_use(atom, _pipe_ctx, findings)
|
||||
local slot_kind = "plain"
|
||||
findings[#findings + 1] = {
|
||||
check = "hazard_nop_use",
|
||||
kind = "warning",
|
||||
kind = "info",
|
||||
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 "",
|
||||
nop_classification = "modeled-redundant",
|
||||
nop_word_index = ev_word,
|
||||
retired_relation = nil,
|
||||
slot_kind = slot_kind,
|
||||
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
|
||||
@@ -1254,6 +1270,9 @@ end
|
||||
-- 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.
|
||||
--
|
||||
-- `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
|
||||
-- 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.
|
||||
@@ -1262,6 +1281,9 @@ end
|
||||
local function check_control_transfer_delay_slot_use(atom, pipe_ctx, findings)
|
||||
local events = atom.paths.word_events or {}
|
||||
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 {}
|
||||
for event_idx, event in ipairs(events) do
|
||||
-- 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_ident = slot and (slot.encoder or slot.ident) or "<missing>"
|
||||
if slot == nil or (slot.encoder or slot.ident) == "nop" then
|
||||
-- Each word event carries `body_line` as the physical source line.
|
||||
-- Use `body_line`, then `def_line`, then 0.
|
||||
local ev_line = event.body_line or event.line or event.def_line or 0
|
||||
-- Prefer `call_line` (the line of the `mac_X(...)` call site in the atom body) so the rendered
|
||||
-- finding points at the user's source, not at the vendored component body.
|
||||
local ev_line = line_for_word_event(event)
|
||||
findings[#findings + 1] = {
|
||||
atom = atom.name,
|
||||
line = ev_line,
|
||||
@@ -1303,8 +1325,17 @@ end
|
||||
--- Empty bodies are not currently flagged — runtime infrastructure atoms like
|
||||
--- `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.
|
||||
---
|
||||
--- 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.
|
||||
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:
|
||||
-- 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.
|
||||
@@ -1935,7 +1966,7 @@ end
|
||||
local CHECK_RULES = {
|
||||
{ name = "transfer_hazards", per_atom = check_transfer_hazards },
|
||||
{ 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 = "control_transfer_delay_slot_use",per_atom = check_control_transfer_delay_slot_use},
|
||||
{ name = "mac_yield_uniformity", per_atom = check_mac_yield_uniformity },
|
||||
@@ -2179,203 +2210,6 @@ local function validate(ctx, src, corpus_pipe_ctx)
|
||||
}
|
||||
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
|
||||
-- ════════════════════════════════════════════════════════════════════════════
|
||||
@@ -2431,21 +2265,30 @@ function M.run(ctx)
|
||||
for _, s in ipairs(result.summaries or {}) do dir_summaries[#dir_summaries + 1] = s end
|
||||
end
|
||||
|
||||
-- Skip directories with zero atoms. A directory with only headers / no MipsAtom_ is "nothing to report".
|
||||
if #all_atoms == 0 then
|
||||
-- Still aggregate errors/warnings/info so orchestrator sees them, but don't write a file.
|
||||
-- Stash per-module results on the corpus for `report.lua` to consume.
|
||||
-- Avoids re-running validate() in the report pass + avoids rebuilding corpus_pipe_ctx.
|
||||
-- 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 _, w in ipairs(dir_warnings) do warnings[#warnings + 1] = w end
|
||||
for _, i_ in ipairs(dir_info) do info[#info + 1] = i_ end
|
||||
else
|
||||
local out_path = emit_module_static_analysis_txt(ctx, dir, dir_sources, all_atoms, all_findings, dir_errors, dir_warnings, dir_info, dir_summaries)
|
||||
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
|
||||
for _, i_ in ipairs(dir_info) do info [#info + 1] = i_ end
|
||||
-- (No per-dir emit: per-module findings are stashed on `corpus.static_analysis_results` above.
|
||||
-- `report.lua` reads that projection to render `<module>.atom_meta_report.md` without re-running validate().)
|
||||
end
|
||||
|
||||
-- Result exposes at least {outputs, errors, warnings, info}.
|
||||
|
||||
@@ -2,17 +2,14 @@
|
||||
---
|
||||
--- Dispatches to pass modules under `scripts/passes/`, resolving dependencies topologically (Kahn's algorithm + cycle detection).
|
||||
---
|
||||
--- **Architecture**:
|
||||
--- - **PASSES table** — declarative dep graph (data, not code).
|
||||
--- - **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**.
|
||||
--- Architecture:
|
||||
--- - PASSES table: Declarative dep graph (data, not code).
|
||||
--- - FLAG_HANDLERS table: Maps CLI flags to handlers.
|
||||
--- - 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`).
|
||||
--- 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.
|
||||
---
|
||||
--- **Conventions**: tabs (1/level), EmmyLua annotations, no regex,
|
||||
--- Lua 5.3 compatible.
|
||||
---
|
||||
-- ════════════════════════════════════════════════════════════════════════════
|
||||
-- Module-scope requires + package.path setup
|
||||
-- ════════════════════════════════════════════════════════════════════════════
|
||||
@@ -160,7 +157,7 @@ local PASSES = {
|
||||
report = {
|
||||
module = "passes.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" },
|
||||
},
|
||||
}
|
||||
@@ -206,7 +203,8 @@ end
|
||||
|
||||
-- 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.
|
||||
-- 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 = {
|
||||
|
||||
Reference in New Issue
Block a user