mirror of
https://github.com/Ed94/pikuma_ps1.git
synced 2026-08-07 16:18:51 +00:00
Compare commits
8
Commits
798807a9c2
...
318516a354
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
318516a354 | ||
|
|
91a91b3495 | ||
|
|
a0d22700db | ||
|
|
51bdf7106b | ||
|
|
531e1cbd58 | ||
|
|
541e52de2b | ||
|
|
eccf17d21c | ||
|
|
0d94632edf |
@@ -50,14 +50,14 @@ WORD_COUNT(mac_gte_load_tri_verts, 18)
|
||||
#define mac_insert_ot_tag_f3(...) \
|
||||
shift_lleft( R_T1, R_T1, S_(U4)/2) /* T1 = otz * S_(U4) (otz arg is implicit R_T1) */ \
|
||||
, add_u_self( R_T1, R_OtBase) /* T1 = & OrderingTable[OTZ] */ \
|
||||
, load_word( R_AT, R_T1, O_(PolyTag,bf_addr_len)) /* AT = old_ot_head */ \
|
||||
, load_word( R_AT, R_T1, O_(PolyTag,code)) /* AT = old_ot_head */ \
|
||||
, load_upper_i(R_V0, (S_(Poly_F3)/S_(U4) - S_(PolyTag)/S_(U4)) << PolyTag_len_bits) /* V0 = (5 - 1) << 24 = 4 << 24 */ \
|
||||
, mask_upper( R_AT, R_AT, S_(PolyTag_len_bits)) /* Strip upper 8 bits (length from prev cell) → keep only low 24 */ \
|
||||
, or_u( R_AT, R_AT, R_V0) /* Merge length */ \
|
||||
, store_word( R_AT, R_PrimCursor, O_(PolyTag,bf_addr_len)) /* prim->tag = packed(prim_length, old_addr) */ \
|
||||
, store_word( R_AT, R_PrimCursor, O_(PolyTag,code)) /* prim->tag = packed(prim_length, old_addr) */ \
|
||||
, shift_lleft( R_AT, R_PrimCursor, S_(PolyTag_len_bits)) /* AT = (prim_length << 24) | old_addr */ \
|
||||
, shift_lright(R_AT, R_AT, S_(PolyTag_len_bits)) \
|
||||
, store_word( R_AT, R_T1, O_(PolyTag,bf_addr_len)) /* OrderingTable[OTZ] = PrimCursor */
|
||||
, store_word( R_AT, R_T1, O_(PolyTag,code)) /* OrderingTable[OTZ] = PrimCursor */
|
||||
WORD_COUNT(mac_insert_ot_tag_f3, 11)
|
||||
|
||||
/* Words: 11; Correctly inserts a primitive into the Ordering Table linked list.
|
||||
@@ -65,14 +65,14 @@ WORD_COUNT(mac_insert_ot_tag_f3, 11)
|
||||
#define mac_insert_ot_tag_g4(...) \
|
||||
shift_lleft( R_T1, R_T1, S_(U4)/2) /* T1 = otz * S_(U4) (otz arg is implicit R_T1) */ \
|
||||
, add_u_self( R_T1, R_OtBase) /* T1 = & OrderingTable[OTZ] */ \
|
||||
, load_word( R_AT, R_T1, O_(PolyTag,bf_addr_len)) /* AT = old_ot_head */ \
|
||||
, load_word( R_AT, R_T1, O_(PolyTag,code)) /* AT = old_ot_head */ \
|
||||
, load_upper_i(R_V0, (S_(Poly_G4)/S_(U4) - S_(PolyTag)/S_(U4)) << PolyTag_len_bits) /* V0 = (9 - 1) << 24 = 8 << 24 */ \
|
||||
, mask_upper( R_AT, R_AT, S_(PolyTag_len_bits)) /* Strip upper 8 bits (length from prev cell) → keep only low 24 */ \
|
||||
, or_u( R_AT, R_AT, R_V0) /* Merge length */ \
|
||||
, store_word( R_AT, R_PrimCursor, O_(PolyTag,bf_addr_len)) /* prim->tag = packed(prim_length, old_addr) */ \
|
||||
, store_word( R_AT, R_PrimCursor, O_(PolyTag,code)) /* prim->tag = packed(prim_length, old_addr) */ \
|
||||
, shift_lleft( R_AT, R_PrimCursor, S_(PolyTag_len_bits)) /* AT = (prim_length << 24) | old_addr */ \
|
||||
, shift_lright(R_AT, R_AT, S_(PolyTag_len_bits)) \
|
||||
, store_word( R_AT, R_T1, O_(PolyTag,bf_addr_len)) /* OrderingTable[OTZ] = PrimCursor */
|
||||
, store_word( R_AT, R_T1, O_(PolyTag,code)) /* OrderingTable[OTZ] = PrimCursor */
|
||||
WORD_COUNT(mac_insert_ot_tag_g4, 11)
|
||||
|
||||
#define mac_pack_color_word(off, cmd, r, g, b) \
|
||||
|
||||
+1
-1
@@ -378,7 +378,7 @@ enum {
|
||||
};
|
||||
typedef Struct_(PolyTag) {
|
||||
union {
|
||||
U4 bf_addr_len;
|
||||
U4 code;
|
||||
struct {
|
||||
U4 addr: 24;
|
||||
U4 len: 8;
|
||||
|
||||
@@ -132,14 +132,14 @@ MipsAtomComp_(ac_gte_load_tri_verts) {
|
||||
MipsAtomComp_(ac_insert_ot_tag_f3) {
|
||||
shift_lleft( R_T1, R_T1, S_(U4)/2), // T1 = otz * S_(U4) (otz arg is implicit R_T1)
|
||||
add_u_self( R_T1, R_OtBase), // T1 = & OrderingTable[OTZ]
|
||||
load_word( R_AT, R_T1, O_(PolyTag,bf_addr_len)), // AT = old_ot_head
|
||||
load_word( R_AT, R_T1, O_(PolyTag,code)), // AT = old_ot_head
|
||||
load_upper_i(R_V0, (S_(Poly_F3)/S_(U4) - S_(PolyTag)/S_(U4)) << PolyTag_len_bits), // V0 = (5 - 1) << 24 = 4 << 24
|
||||
mask_upper( R_AT, R_AT, S_(PolyTag_len_bits)), // Strip upper 8 bits (length from prev cell) → keep only low 24
|
||||
or_u( R_AT, R_AT, R_V0), // Merge length
|
||||
store_word( R_AT, R_PrimCursor, O_(PolyTag,bf_addr_len)), // prim->tag = packed(prim_length, old_addr)
|
||||
store_word( R_AT, R_PrimCursor, O_(PolyTag,code)), // prim->tag = packed(prim_length, old_addr)
|
||||
shift_lleft( R_AT, R_PrimCursor, S_(PolyTag_len_bits)), // AT = (prim_length << 24) | old_addr
|
||||
shift_lright(R_AT, R_AT, S_(PolyTag_len_bits)),
|
||||
store_word( R_AT, R_T1, O_(PolyTag,bf_addr_len)), // OrderingTable[OTZ] = PrimCursor
|
||||
store_word( R_AT, R_T1, O_(PolyTag,code)), // OrderingTable[OTZ] = PrimCursor
|
||||
};
|
||||
|
||||
/* Words: 11; Correctly inserts a primitive into the Ordering Table linked list.
|
||||
@@ -147,14 +147,14 @@ MipsAtomComp_(ac_insert_ot_tag_f3) {
|
||||
MipsAtomComp_(ac_insert_ot_tag_g4) {
|
||||
shift_lleft( R_T1, R_T1, S_(U4)/2), // T1 = otz * S_(U4) (otz arg is implicit R_T1)
|
||||
add_u_self( R_T1, R_OtBase), // T1 = & OrderingTable[OTZ]
|
||||
load_word( R_AT, R_T1, O_(PolyTag,bf_addr_len)), // AT = old_ot_head
|
||||
load_word( R_AT, R_T1, O_(PolyTag,code)), // AT = old_ot_head
|
||||
load_upper_i(R_V0, (S_(Poly_G4)/S_(U4) - S_(PolyTag)/S_(U4)) << PolyTag_len_bits), // V0 = (9 - 1) << 24 = 8 << 24
|
||||
mask_upper( R_AT, R_AT, S_(PolyTag_len_bits)), // Strip upper 8 bits (length from prev cell) → keep only low 24
|
||||
or_u( R_AT, R_AT, R_V0), // Merge length
|
||||
store_word( R_AT, R_PrimCursor, O_(PolyTag,bf_addr_len)), // prim->tag = packed(prim_length, old_addr)
|
||||
store_word( R_AT, R_PrimCursor, O_(PolyTag,code)), // prim->tag = packed(prim_length, old_addr)
|
||||
shift_lleft( R_AT, R_PrimCursor, S_(PolyTag_len_bits)), // AT = (prim_length << 24) | old_addr
|
||||
shift_lright(R_AT, R_AT, S_(PolyTag_len_bits)),
|
||||
store_word( R_AT, R_T1, O_(PolyTag,bf_addr_len)), // OrderingTable[OTZ] = PrimCursor
|
||||
store_word( R_AT, R_T1, O_(PolyTag,code)), // OrderingTable[OTZ] = PrimCursor
|
||||
};
|
||||
|
||||
/* Words: 3; Emits one (cmd|color) word to R_PrimCursor at the given
|
||||
|
||||
@@ -17,19 +17,19 @@ enum {
|
||||
};
|
||||
|
||||
typedef U4 OrderingTable_Buffer[OrderingTbl_Len];
|
||||
typedef def_farray(OrderingTable_Buffer, 2);
|
||||
typedef Array_(OrderingTable_Buffer, 2);
|
||||
|
||||
typedef B1 PrimitiveBuffer[PrimitiveBuff_Len];
|
||||
typedef def_farray(PrimitiveBuffer, 2);
|
||||
typedef def_struct(PrimitiveArena) {
|
||||
typedef Array_(PrimitiveBuffer, 2);
|
||||
typedef Struct_(PrimitiveArena) {
|
||||
A2_PrimitiveBuffer buf;
|
||||
U4 used;
|
||||
};
|
||||
|
||||
#define Cube_num_verts 8
|
||||
typedef def_farray(V3_S2, Cube_num_verts);
|
||||
typedef Array_(V3_S2, Cube_num_verts);
|
||||
#define Cube_num_faces 6
|
||||
typedef def_farray(V4_S2, Cube_num_faces);
|
||||
typedef Array_(V4_S2, Cube_num_faces);
|
||||
void ent_cube128_init(A8_V3_S2* verts, A6_V4_S2* faces) {
|
||||
memory_copy(verts, & (A8_V3_S2) {
|
||||
{ -128, -128, -128 },
|
||||
@@ -40,7 +40,7 @@ void ent_cube128_init(A8_V3_S2* verts, A6_V4_S2* faces) {
|
||||
{ 128, 128, -128 },
|
||||
{ 128, 128, 128 },
|
||||
{ -128, 128, 128 }
|
||||
}, size_of(A8_V3_S2) );
|
||||
}, S_(A8_V3_S2) );
|
||||
memory_copy(faces, & (A6_V4_S2) {
|
||||
{ 3, 2, 0, 1 },
|
||||
{ 0, 1, 4, 5 },
|
||||
@@ -48,10 +48,10 @@ void ent_cube128_init(A8_V3_S2* verts, A6_V4_S2* faces) {
|
||||
{ 1, 2, 5, 6 },
|
||||
{ 2, 3, 6, 7 },
|
||||
{ 3, 0, 7, 4 },
|
||||
}, size_of(A6_V4_S2) );
|
||||
}, S_(A6_V4_S2) );
|
||||
return;
|
||||
}
|
||||
typedef def_struct(Ent_Cube) {
|
||||
typedef Struct_(Ent_Cube) {
|
||||
V3_S4 accel;
|
||||
V3_S4 vel;
|
||||
V3_S4 pos;
|
||||
@@ -62,22 +62,22 @@ typedef def_struct(Ent_Cube) {
|
||||
};
|
||||
|
||||
#define Floor_num_verts 4
|
||||
typedef def_farray(V3_S2, Floor_num_verts);
|
||||
typedef Array_(V3_S2, Floor_num_verts);
|
||||
#define Floor_num_faces 2
|
||||
typedef def_farray(V3_S2, Floor_num_faces);
|
||||
typedef Array_(V3_S2, Floor_num_faces);
|
||||
void ent_floor_init(A4_V3_S2* verts, A2_V3_S2* faces) {
|
||||
memory_copy(verts, &(A4_V3_S2) {
|
||||
{ -900, 0, -900 },
|
||||
{ -900, 0, 900 },
|
||||
{ 900, 0, -900 },
|
||||
{ 900, 0, 900 },
|
||||
}, size_of(A8_V3_S2));
|
||||
}, S_(A8_V3_S2));
|
||||
memory_copy(faces, & (A2_V3_S2) {
|
||||
{ 0, 1, 2 },
|
||||
{ 1, 3, 2 },
|
||||
}, size_of(A2_V3_S2));
|
||||
}, S_(A2_V3_S2));
|
||||
};
|
||||
typedef def_struct(Ent_Floor) {
|
||||
typedef Struct_(Ent_Floor) {
|
||||
V3_S4 accel;
|
||||
V3_S4 pos;
|
||||
V3_S4 scale;
|
||||
@@ -86,7 +86,7 @@ typedef def_struct(Ent_Floor) {
|
||||
A2_V3_S2 faces;
|
||||
};
|
||||
|
||||
typedef def_struct(SMemory) {
|
||||
typedef Struct_(SMemory) {
|
||||
DoubleBuffer screen_buf;
|
||||
A2_OrderingTable_Buffer ordering_tbl;
|
||||
PrimitiveArena primitives;
|
||||
@@ -108,7 +108,7 @@ B1* prim__alloc(U4 type_width, Str8 type_name) {
|
||||
pa->used += type_width;
|
||||
return next;
|
||||
}
|
||||
#define prim_alloc(type) (type*)prim__alloc(size_of(type), txt( stringify(type)))
|
||||
#define prim_alloc(type) (type*)prim__alloc(S_(type), slit( stringify(type)))
|
||||
|
||||
void gp_screen_init_c11(DoubleBuffer* screen_buf, S2* active_buf_id)
|
||||
{
|
||||
|
||||
@@ -5,8 +5,8 @@
|
||||
# include "duffle/gp.h"
|
||||
#endif
|
||||
|
||||
typedef def_struct(DrawEnv_Packed) { U4 tag; U4 code[15]; };
|
||||
typedef def_struct(DrawEnv) {
|
||||
typedef Struct_(DrawEnv_Packed) { U4 tag; U4 code[15]; };
|
||||
typedef Struct_(DrawEnv) {
|
||||
Rect_S2 clip_area;
|
||||
A2_S2 drawing_offset;
|
||||
Rect_S2 texture_window;
|
||||
@@ -17,7 +17,7 @@ typedef def_struct(DrawEnv) {
|
||||
RGB8 initial_bg_color;
|
||||
DrawEnv_Packed dr_env; // reserved
|
||||
};
|
||||
typedef def_struct(DisplayEnv) {
|
||||
typedef Struct_(DisplayEnv) {
|
||||
Rect_S2 display_area;
|
||||
Rect_S2 screen;
|
||||
B1 vinterlace;
|
||||
@@ -25,9 +25,9 @@ typedef def_struct(DisplayEnv) {
|
||||
B1 pad0;
|
||||
B1 pad1;
|
||||
};
|
||||
typedef def_farray(DrawEnv, 2);
|
||||
typedef def_farray(DisplayEnv, 2);
|
||||
typedef def_struct(DoubleBuffer) {
|
||||
typedef Array_(DrawEnv, 2);
|
||||
typedef Array_(DisplayEnv, 2);
|
||||
typedef Struct_(DoubleBuffer) {
|
||||
A2_DrawEnv draw;
|
||||
A2_DisplayEnv display;
|
||||
};
|
||||
@@ -58,7 +58,7 @@ U4 vsync(U4 mode) __asm__("VSync");
|
||||
|
||||
void draw_orderingtbl(U4* buf) __asm__("DrawOTag");
|
||||
|
||||
typedef def_struct(PolyTag) {
|
||||
typedef Struct_(PolyTag) {
|
||||
U4 addr: 24;
|
||||
U4 len: 8;
|
||||
RGB8 color;
|
||||
@@ -106,7 +106,7 @@ typedef def_struct(PolyTag) {
|
||||
// #define setLineF4(p) set_len(p, 6), set_code(p, 0x4c),(p)->pad = 0x55555555
|
||||
// #define setLineG4(p) set_len(p, 9), set_code(p, 0x5c),(p)->pad = 0x55555555, (p)->p2 = 0, (p)->p3 = 0
|
||||
|
||||
typedef def_struct(Poly_F3) {
|
||||
typedef Struct_(Poly_F3) {
|
||||
U4 tag;
|
||||
RGB8 color;
|
||||
B1 code;
|
||||
@@ -120,14 +120,14 @@ typedef def_struct(Poly_F3) {
|
||||
};
|
||||
};
|
||||
|
||||
typedef def_struct(Poly_G3) {
|
||||
typedef Struct_(Poly_G3) {
|
||||
U4 tag; RGB8 c0; B1 code;
|
||||
V2_S2 p0; RGB8 c1; B1 pad1;
|
||||
V2_S2 p1; RGB8 c2; B1 pad2;
|
||||
V2_S2 p2;
|
||||
};
|
||||
|
||||
typedef def_struct(Poly_F4) {
|
||||
typedef Struct_(Poly_F4) {
|
||||
U4 tag;
|
||||
RGB8 color;
|
||||
B1 code;
|
||||
@@ -142,7 +142,7 @@ typedef def_struct(Poly_F4) {
|
||||
};
|
||||
};
|
||||
|
||||
typedef def_struct(Poly_G4) {
|
||||
typedef Struct_(Poly_G4) {
|
||||
U4 tag; RGB8 c0; B1 code;
|
||||
V2_S2 p0; RGB8 c1; B1 pad1;
|
||||
V2_S2 p1; RGB8 c2; B1 pad2;
|
||||
@@ -150,7 +150,7 @@ typedef def_struct(Poly_G4) {
|
||||
V2_S2 p3;
|
||||
};
|
||||
|
||||
typedef def_struct(Tile) {
|
||||
typedef Struct_(Tile) {
|
||||
U4 tag;
|
||||
RGB8 color;
|
||||
B1 code;
|
||||
@@ -169,7 +169,7 @@ 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, cast(S4*R_, & xy->x), cast(S4*R_, pp), r_(flag)); }
|
||||
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(
|
||||
@@ -179,8 +179,8 @@ FI_ S4 rtp_avg_nclip_a3_v3s2(
|
||||
){
|
||||
return rtp_avg_nclip_a3_v3s2_raw(
|
||||
v0, v1, v2,
|
||||
cast(S4*R_, xy0), cast(S4*R_, xy1), cast(S4*R_, xy2),
|
||||
cast(S4*R_, pp), cast(S4*R_, otz), cast(S4*R_, flag)
|
||||
C_(S4*R_, xy0), C_(S4*R_, xy1), C_(S4*R_, xy2),
|
||||
C_(S4*R_, pp), C_(S4*R_, otz), C_(S4*R_, flag)
|
||||
);
|
||||
}
|
||||
|
||||
@@ -192,8 +192,8 @@ FI_ S4 rtp_avg_nclip_a4_v3s2(
|
||||
){
|
||||
return rtp_avg_nclip_a4_v3s2_raw(
|
||||
v0, v1, v2, v3,
|
||||
cast(S4*R_, xy0), cast(S4*R_, xy1), cast(S4*R_, xy2), cast(S4*R_, xy3),
|
||||
cast(S4*R_, pp), cast(S4*R_, otz), cast(S4*R_, flag)
|
||||
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)
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 220 KiB |
@@ -6,19 +6,27 @@ A rest from the usual.
|
||||
|
||||
## Dependencies
|
||||
|
||||
I will be programming from a Windows 11 machine:
|
||||
I will be programming from a Windows 11 machine (may eventually try this on the Steam Deck...):
|
||||

|
||||
|
||||
```ps1
|
||||
# not really used yet for scripts (may never)
|
||||
scoop install lua
|
||||
```
|
||||
|
||||
[armips](https://github.com/Kingcom/armips)
|
||||
|
||||
* Supports doing bare-metal assembly for the ps1
|
||||
* `scoop install armips` or just clone and build..
|
||||
|
||||
[luajit-2.1](https://github.com/LuaJIT/LuaJIT.git)
|
||||
|
||||
```
|
||||
scoop install luajit
|
||||
```
|
||||
|
||||
* Used for lua scripts
|
||||
* Particularly, ps1_meta.lua which is a staged metaprogram pass for the custom C11 Assembly DSL used in this codebase.
|
||||
|
||||
[lpeg](https://github.com/roberto-ieru/LPeg.git)
|
||||
|
||||
* Lua is slow (even jitted) so this helps.
|
||||
|
||||
[pscx-redux](https://github.com/grumpycoders/pcsx-redux/): A collection of tools, research, hardware design, and libraries aiming at development and reverse engineering on the PlayStation 1.
|
||||
|
||||
* Used as the runtime sandbox emulated the ps1
|
||||
@@ -57,3 +65,4 @@ scoop install lua
|
||||

|
||||

|
||||

|
||||

|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
--- audit_lua_nesting.lua — Walk Lua source files and flag any block
|
||||
--- nesting deeper than 5 levels.
|
||||
--- audit_lua_nesting.lua — Walk Lua source files and flag any block nesting deeper than 5 levels.
|
||||
---
|
||||
--- Usage:
|
||||
--- luajit scripts/audit_lua_nesting.lua scripts/duffle.lua scripts/ps1_meta.lua
|
||||
@@ -11,11 +10,10 @@
|
||||
--- **Implementation**: a hand-rolled depth tracker that counts:
|
||||
--- - `do`, `function`, `if`, `for`, `while`, `repeat` -> depth +1
|
||||
--- - `end`, `until` -> depth -1
|
||||
--- - `else`, `elseif` -> depth unchanged
|
||||
--- - `else`, `elseif` -> depth unchanged
|
||||
---
|
||||
--- **Caveats**: doesn't handle string/comment state (will miscount
|
||||
--- braces inside strings). For our metaprogram files (no embedded
|
||||
--- code generation), this is acceptable.
|
||||
--- **Caveats**: doesn't fully handle string/comment state (will miscount braces inside multi-line strings or block comments).
|
||||
--- For our metaprogram files (no embedded code generation), this is acceptable.
|
||||
|
||||
local M = {}
|
||||
|
||||
@@ -28,115 +26,107 @@ local BLOCK_OPEN = {
|
||||
["repeat"] = true,
|
||||
}
|
||||
|
||||
local function is_block_close(token)
|
||||
return token == "end" or token == "until"
|
||||
end
|
||||
local function is_block_close(token) return token == "end" or token == "until" end
|
||||
|
||||
-- (internal) Walk one source file and return a list of
|
||||
-- {line, depth, token} entries where depth > MAX_NESTING.
|
||||
-- {line, depth, token} entries where depth > max_nesting.
|
||||
local function audit_file(path, max_nesting)
|
||||
local f = io.open(path, "r")
|
||||
if not f then
|
||||
error("Cannot open " .. path)
|
||||
end
|
||||
if not f then error("Cannot open " .. path) end
|
||||
local content = f:read("*a")
|
||||
f:close()
|
||||
|
||||
local violations = {}
|
||||
local depth = 0
|
||||
local line = 1
|
||||
local pos = 1
|
||||
local len = #content
|
||||
local token_start = 0
|
||||
local depth = 0
|
||||
local line = 1
|
||||
local pos = 1
|
||||
local src_len = #content
|
||||
local token_idx = 0
|
||||
|
||||
local function read_ident_at(p)
|
||||
-- Lua ident: [a-zA-Z_][a-zA-Z0-9_]*
|
||||
local start = p
|
||||
if start > len then return nil end
|
||||
local ch = content:sub(start, start)
|
||||
if not (ch:match("[%a_]")) then return nil end
|
||||
p = p + 1
|
||||
while p <= len do
|
||||
local c = content:sub(p, p)
|
||||
if not (c:match("[%w_]")) then break end
|
||||
p = p + 1
|
||||
local function read_ident_at(start_pos)
|
||||
local ident_start = start_pos
|
||||
if ident_start > src_len then return nil end
|
||||
local first_ch = content:sub(ident_start, ident_start)
|
||||
if not (first_ch:match("[%a_]")) then return nil end
|
||||
local scan = start_pos + 1
|
||||
while scan <= src_len do
|
||||
local ch = content:sub(scan, scan)
|
||||
if not (ch:match("[%w_]")) then break end
|
||||
scan = scan + 1
|
||||
end
|
||||
return content:sub(start, p - 1), p
|
||||
return content:sub(ident_start, scan - 1), scan
|
||||
end
|
||||
|
||||
local function skip_string_or_comment(p)
|
||||
local ch = content:sub(p, p)
|
||||
-- Skip past a string literal or comment starting at `start_pos`.
|
||||
-- Returns the position just past the construct, or nil if `start_pos`
|
||||
-- is not the start of a string/comment.
|
||||
local function skip_string_or_comment(start_pos)
|
||||
local ch = content:sub(start_pos, start_pos)
|
||||
if ch == '"' or ch == "'" then
|
||||
-- String literal: skip to matching end-quote.
|
||||
p = p + 1
|
||||
while p <= len do
|
||||
local c = content:sub(p, p)
|
||||
local scan = start_pos + 1
|
||||
while scan <= src_len do
|
||||
local c = content:sub(scan, scan)
|
||||
if c == "\\" then
|
||||
p = p + 2
|
||||
scan = scan + 2
|
||||
elseif c == ch then
|
||||
p = p + 1
|
||||
break
|
||||
return scan + 1
|
||||
else
|
||||
p = p + 1
|
||||
scan = scan + 1
|
||||
end
|
||||
end
|
||||
return p
|
||||
elseif ch == "-" and content:sub(p + 1, p + 1) == "-" then
|
||||
-- Lua comment: -- to end of line.
|
||||
p = p + 2
|
||||
if content:sub(p, p + 1) == "[[" and content:sub(p + 2, p + 3) == "[" then
|
||||
return src_len + 1
|
||||
elseif ch == "-" and content:sub(start_pos + 1, start_pos + 1) == "-" then
|
||||
local scan = start_pos + 2
|
||||
if content:sub(scan, scan + 1) == "[[" and content:sub(scan + 2, scan + 3) == "[" then
|
||||
-- Long bracket comment [==[ ... ]==]
|
||||
p = p + 2
|
||||
scan = scan + 2
|
||||
local eq = ""
|
||||
while content:sub(p, p) == "=" do
|
||||
while content:sub(scan, scan) == "=" do
|
||||
eq = eq .. "="
|
||||
p = p + 1
|
||||
scan = scan + 1
|
||||
end
|
||||
local close_marker = "]" .. eq .. "]"
|
||||
local close_pos = content:find(close_marker, p, true)
|
||||
local close_pos = content:find(close_marker, scan, true)
|
||||
if close_pos then
|
||||
p = close_pos + #close_marker
|
||||
return close_pos + #close_marker
|
||||
else
|
||||
p = len + 1
|
||||
return src_len + 1
|
||||
end
|
||||
else
|
||||
while p <= len and content:sub(p, p) ~= "\n" do p = p + 1 end
|
||||
while scan <= src_len and content:sub(scan, scan) ~= "\n" do scan = scan + 1 end
|
||||
return scan + 1
|
||||
end
|
||||
return p
|
||||
elseif ch == "[" and content:sub(p + 1, p + 1) == "[" then
|
||||
-- Long bracket string: [==[ ... ]==]
|
||||
p = p + 2
|
||||
elseif ch == "[" and content:sub(start_pos + 1, start_pos + 1) == "[" then
|
||||
local scan = start_pos + 2
|
||||
local eq = ""
|
||||
while content:sub(p, p) == "=" do
|
||||
while content:sub(scan, scan) == "=" do
|
||||
eq = eq .. "="
|
||||
p = p + 1
|
||||
scan = scan + 1
|
||||
end
|
||||
local close_marker = "]" .. eq .. "]"
|
||||
local close_pos = content:find(close_marker, p, true)
|
||||
local close_pos = content:find(close_marker, scan, true)
|
||||
if close_pos then
|
||||
p = close_pos + #close_marker
|
||||
return close_pos + #close_marker
|
||||
else
|
||||
p = len + 1
|
||||
return src_len + 1
|
||||
end
|
||||
return p
|
||||
end
|
||||
return nil
|
||||
end
|
||||
|
||||
local token_count = 0
|
||||
while pos <= len do
|
||||
while pos <= src_len do
|
||||
local ch = content:sub(pos, pos)
|
||||
if ch == "\n" then line = line + 1 end
|
||||
|
||||
local skip_to = skip_string_or_comment(pos)
|
||||
if skip_to then
|
||||
for i = pos, skip_to - 1 do
|
||||
if content:sub(i, i) == "\n" then line = line + 1 end
|
||||
for scan = pos, skip_to - 1 do
|
||||
if content:sub(scan, scan) == "\n" then line = line + 1 end
|
||||
end
|
||||
pos = skip_to
|
||||
elseif ch:match("[%a_]") then
|
||||
local tok, next_pos = read_ident_at(pos)
|
||||
token_count = token_count + 1
|
||||
token_idx = token_idx + 1
|
||||
if BLOCK_OPEN[tok] then
|
||||
depth = depth + 1
|
||||
if depth > max_nesting then
|
||||
@@ -172,18 +162,17 @@ end
|
||||
if arg and arg[1] then
|
||||
local max_nesting = 5
|
||||
local files = {}
|
||||
for i = 1, #arg do
|
||||
if arg[i] == "--max" and arg[i + 1] then
|
||||
max_nesting = tonumber(arg[i + 1]) or 5
|
||||
for arg_idx = 1, #arg do
|
||||
if arg[arg_idx] == "--max" and arg[arg_idx + 1] then
|
||||
max_nesting = tonumber(arg[arg_idx + 1]) or 5
|
||||
else
|
||||
files[#files + 1] = arg[i]
|
||||
files[#files + 1] = arg[arg_idx]
|
||||
end
|
||||
end
|
||||
|
||||
-- Accept either a directory or a file path. Directory args are
|
||||
-- expanded via `dir /b *.lua` (Windows) or `ls *.lua` (Unix).
|
||||
local function is_dir(p)
|
||||
-- Try opening it as a file; if that succeeds, it's not a dir.
|
||||
local f = io.open(p, "r")
|
||||
if f then f:close() return false end
|
||||
return true
|
||||
@@ -238,4 +227,4 @@ if arg and arg[1] then
|
||||
end
|
||||
end
|
||||
|
||||
return M
|
||||
return M
|
||||
|
||||
+200
-334
@@ -5,20 +5,15 @@
|
||||
--- - **Character classification** (`is_space`, `is_alpha`, `is_alnum`, `is_digit`, plus the byte-fast `_byte` variants).
|
||||
--- - **String primitives** (`trim`, `dirname`, `basename_no_ext`, `find_byte`).
|
||||
--- - **I/O primitives** (`read_file`, `write_file`, `ensure_dir`).
|
||||
--- - **C-language scanner** (`skip_ws_and_cmt`, `skip_str_or_cmt`,
|
||||
--- `read_ident`, `read_parens`, `read_braces`, `read_brackets`,
|
||||
--- `read_balanced`, `scan_to_char`, `split_top_level_commas`).
|
||||
--- - **C-language scanner** (`skip_ws_and_cmt`, `skip_str_or_cmt`, `read_ident`, `read_parens`, `read_braces`, `read_brackets`, `read_balanced`, `scan_to_char`, `split_top_level_commas`).
|
||||
--- - **Word-count loader** (`load_word_counts` for `WORD_COUNT(...)` metadata files).
|
||||
--- - **Line lookup** (`LineIndex` returns an O(log N) `line_of(pos)` closure for source-mapping).
|
||||
--- - **Domain tables** (`WAVE_CONTEXT_REGS`, `TAPE_ATOM_MACROS`,
|
||||
--- `GTE_PIPELINE_LATENCY`, `GP0_CMD_SIZE`, `GP0_CMD_BY_SHAPE`,
|
||||
--- `GP0_MACRO_CONTRIB`, `INSTRUCTION_LATENCY`).
|
||||
--- - **Process-bootstrap helper** (`setup_package_path` — replaces the 8-line `arg[0]`-resolution boilerplate duplicated across 7 entry scripts)
|
||||
--- - **Domain tables** (`WAVE_CONTEXT_REGS`, `TAPE_ATOM_MACROS`, `GTE_PIPELINE_LATENCY`, `GP0_CMD_SIZE`, `GP0_CMD_BY_SHAPE`, `GP0_MACRO_CONTRIB`, `INSTRUCTION_LATENCY`).
|
||||
--- - **Process-bootstrap helper** (`setup_package_path`replaces the 8-line `arg[0]`-resolution boilerplate duplicated across 7 entry scripts)
|
||||
---
|
||||
--- **Conventions**: tabs (1/level), EmmyLua annotations, no regex.
|
||||
--- Lua 5.3 compatible; no `<close>`/`<toclose>`, no `continue`, no
|
||||
--- 5.4 string.dump improvements. LuaJIT 5.1+extensions model is the
|
||||
--- primary target.
|
||||
--- 5.4 string.dump improvements. LuaJIT 5.1+extensions model is the primary target.
|
||||
---
|
||||
--- **No `:match` / `:gmatch` regex use anywhere**; all delimiter-
|
||||
--- splitting is hand-rolled or via LPeg (the regex-free PEG library).
|
||||
@@ -81,30 +76,22 @@ local BYTE_DIGIT_9 = 57 -- '9'
|
||||
-- Section -1: Bootstrap (path-setup at module load)
|
||||
-- ════════════════════════════════════════════════════════════════════════════
|
||||
--
|
||||
-- When duffle.lua is first loaded (via `dofile` from an entry script
|
||||
-- or via `require` from a passes script), the code below runs and sets
|
||||
-- `package.path` + `package.cpath` so subsequent `require`s resolve.
|
||||
-- When duffle.lua is first loaded (via `dofile` from an entry script or via `require` from a passes script),
|
||||
-- the code below sruns and sets `package.path` + `package.cpath` so subsequent `require`s resolve.
|
||||
-- Idempotent: re-loads just re-set the same paths.
|
||||
--
|
||||
-- **Entry scripts** trigger this with one line:
|
||||
-- `local duffle = dofile(arg[0]:match("(.*[/\\])") .. "/../duffle.lua")`
|
||||
-- which runs this top-level + returns `M`.
|
||||
-- `local duffle = dofile(arg[0]:match("(.*[/\\])") .. "/../duffle.lua")` which runs this top-level + returns `M`.
|
||||
--
|
||||
-- **Passes scripts** are loaded via `require("passes.X")` from the
|
||||
-- entry script; by the time they run, the entry script has already
|
||||
-- triggered this bootstrap, so the paths are set.
|
||||
--
|
||||
-- **Why `git rev-parse`?** Hardcoding paths like
|
||||
-- `C:\\projects\\Pikuma\\ps1\\...` breaks portability. Git gives us
|
||||
-- the canonical repo root regardless of where it lives.
|
||||
-- **Passes scripts** are loaded via `require("passes.X")` from the entry script; by the time they run,
|
||||
-- the entry script has already triggered this bootstrap, so the paths are set.
|
||||
|
||||
--- Resolve the repo root via `git rev-parse --show-toplevel` (cached).
|
||||
--- Returns a path with a trailing separator, or nil if not in a git repo.
|
||||
--- @return string|nil
|
||||
local function find_repo_root()
|
||||
-- Cached in `package.loaded` (process-global) so all 8 entry scripts
|
||||
-- + passes scripts share one git call. Without this, git rev-parse
|
||||
-- runs once per script load = 8 × ~150ms = 1.2s wasted per build.
|
||||
-- Cached in `package.loaded` (process-global) so all 8 entry scripts + passes scripts share one git call.
|
||||
-- Without this, git rev-parse runs once per script load.
|
||||
if package.loaded.__duffle_repo_root__ then return package.loaded.__duffle_repo_root__ end
|
||||
local p = io.popen("git rev-parse --show-toplevel 2>nul")
|
||||
local root
|
||||
@@ -118,7 +105,7 @@ local function find_repo_root()
|
||||
return root
|
||||
end
|
||||
|
||||
--- Set `package.path` (for `require("duffle")` + `require("passes.X")`)
|
||||
--- Set `package.path` (for `require("duffle")` + `require("passes.X")`)
|
||||
--- and `package.cpath` (for `lpeg.dll` on Windows).
|
||||
function M.setup_package_path()
|
||||
local repo_root = find_repo_root()
|
||||
@@ -127,8 +114,7 @@ function M.setup_package_path()
|
||||
os.exit(2)
|
||||
end
|
||||
|
||||
-- From the repo root, derive both `scripts/` and `scripts/passes/`
|
||||
-- so `require("duffle")` AND `require("passes.annotation")` resolve.
|
||||
-- From the repo root, derive both `scripts/` and `scripts/passes/` so `require("duffle")` AND `require("passes.annotation")` resolve.
|
||||
local scripts_dir = repo_root .. "scripts/"
|
||||
local passes_dir = repo_root .. "scripts/passes/"
|
||||
package.path = scripts_dir .. "?.lua;"
|
||||
@@ -137,73 +123,66 @@ function M.setup_package_path()
|
||||
.. passes_dir .. "?/init.lua;"
|
||||
.. package.path
|
||||
|
||||
-- cpath: only needed on Windows for the bundled lpeg.dll. (LPeg
|
||||
-- is optional -- duffle.lua's `pcall(require, "lpeg")` falls back
|
||||
-- to hand-rolled scanners if the .dll isn't loadable.)
|
||||
-- cpath: only needed on Windows for the bundled lpeg.dll.
|
||||
-- (LPeg is optional -- duffle.lua's `pcall(require, "lpeg")` falls back to hand-rolled scanners if the .dll isn't loadable.)
|
||||
if package.config:sub(1, 1) == "\\" then
|
||||
package.cpath = repo_root .. "toolchain/luajit-2.1/lib/lua/5.1/?.dll;"
|
||||
.. package.cpath
|
||||
end
|
||||
end
|
||||
|
||||
-- NOTE: `M.setup_package_path()` is NOT auto-called here. The entry
|
||||
-- scripts explicitly `dofile("duffle_paths.lua")` first, which calls
|
||||
-- `M.setup_package_path()`. The function exists for the helper to use
|
||||
-- (so the path-setup logic is centralized in duffle.lua).
|
||||
-- NOTE: `M.setup_package_path()` is NOT auto-called here. The entry scripts explicitly `dofile("duffle_paths.lua")` first, which calls `M.setup_package_path()`.
|
||||
-- The function exists for the helper to use (so the path-setup logic is centralized in duffle.lua).
|
||||
|
||||
-- ════════════════════════════════════════════════════════════════════════════
|
||||
-- Section 0: LPeg patterns (compiled once at module load)
|
||||
-- ════════════════════════════════════════════════════════════════════════════
|
||||
--
|
||||
-- LPeg is a PEG library (no regex). All patterns below are first-class
|
||||
-- pattern values; they're cheap to build and reuse.
|
||||
-- LPeg is a required dependency (PEG library, no regex).
|
||||
-- It's loaded via `package.cpath` (configured by `duffle_paths.lua` to find `toolchain/lpeg/lpeg.dll`).
|
||||
-- There's no hand-rolled fallback. The original two-tier design added complexity for a 5-10x speedup that's
|
||||
-- only relevant at the high-level scanner stage; the byte-by-byte helpers in Section 1 are sufficient for the classification primitives.
|
||||
--
|
||||
-- Note: lpeg is required lazily because Lua 5.5 may not have it on its
|
||||
-- cpath at the same location as LuaJIT. We attempt the require and fall
|
||||
-- back to the hand-rolled implementations if it fails.
|
||||
|
||||
-- If the require fails, fail loud with an actionable message. The build script (`update_deps.ps1`) builds lpeg.dll into `toolchain/lpeg/`;
|
||||
-- if it's missing, run `update_deps.ps1`.
|
||||
local lpeg_ok, lpeg = pcall(require, "lpeg")
|
||||
local lpeg_lib = nil
|
||||
local lpeg_alpha_pat, lpeg_alnum_pat, lpeg_ident_pat
|
||||
local lpeg_str_or_cmt_pat, lpeg_ws_and_cmt_pat
|
||||
local lpeg_scan_to_target_pat -- generic "anything but target or balanced group" matcher
|
||||
|
||||
if lpeg_ok then
|
||||
lpeg_lib = lpeg
|
||||
local P, S, R = lpeg.P, lpeg.S, lpeg.R
|
||||
|
||||
-- Character class patterns
|
||||
local alpha_pat = R("AZ", "az") + P("_")
|
||||
local digit_pat = R("09")
|
||||
lpeg_alnum_pat = alpha_pat + digit_pat
|
||||
|
||||
-- Identifier: alpha followed by zero+ alnum. Capture as a string.
|
||||
lpeg_alpha_pat = alpha_pat
|
||||
lpeg_ident_pat = lpeg.C(alpha_pat * lpeg_alnum_pat^0)
|
||||
|
||||
-- String literal: "..." with backslash escapes.
|
||||
local lpeg_str_pat = P('"') * (P(1) - S('"\\') + P('\\') * P(1))^0 * P('"')
|
||||
-- Char literal: '...' with backslash escapes.
|
||||
local lpeg_chr_pat = P("'") * (P(1) - S("'\\") + P('\\') * P(1))^0 * P("'")
|
||||
-- Line comment: // ... to end-of-line.
|
||||
local lpeg_line_cmt_pat = P("//") * (P(1) - S("\n"))^0
|
||||
-- Block comment: /* ... */ (no nesting per C standard).
|
||||
local lpeg_block_cmt_pat = P("/*") * (P(1) - P("*/"))^0 * P("*/")
|
||||
-- String or comment (any of the four forms).
|
||||
lpeg_str_or_cmt_pat = lpeg_str_pat + lpeg_chr_pat + lpeg_line_cmt_pat + lpeg_block_cmt_pat
|
||||
|
||||
-- Whitespace + comment skipper: zero+ (whitespace run | string | comment).
|
||||
local ws_pat = S(" \t\n\r\v\f")
|
||||
lpeg_ws_and_cmt_pat = (ws_pat + lpeg_str_or_cmt_pat)^0
|
||||
|
||||
-- Generic "skip until target, but step over balanced groups" matcher.
|
||||
-- Used by scan_to_char for non-ident / non-bracket chars.
|
||||
-- We accept any single char except the target.
|
||||
-- The balanced-group stepping is handled by the caller (via read_balanced).
|
||||
lpeg_scan_to_target_pat = function(target)
|
||||
return (P(1) - P(target))^0
|
||||
end
|
||||
if not lpeg_ok then
|
||||
io.stderr:write("[duffle] require('lpeg') failed: ", lpeg, "\n")
|
||||
io.stderr:write("[duffle] lpeg.dll not found on package.cpath.\n")
|
||||
io.stderr:write("[duffle] Run 'scripts/update_deps.ps1' to build it into toolchain/lpeg/.\n")
|
||||
os.exit(1)
|
||||
end
|
||||
local P, S, R = lpeg.P, lpeg.S, lpeg.R
|
||||
|
||||
-- Character class patterns
|
||||
local alpha_pat = R("AZ", "az") + P("_")
|
||||
local digit_pat = R("09")
|
||||
local lpeg_alnum_pat = alpha_pat + digit_pat
|
||||
|
||||
-- Identifier: alpha followed by zero+ alnum. Capture as a string.
|
||||
local lpeg_alpha_pat = alpha_pat
|
||||
local lpeg_ident_pat = lpeg.C(alpha_pat * lpeg_alnum_pat^0)
|
||||
|
||||
-- String literal: "..." with backslash escapes.
|
||||
local lpeg_str_pat = P('"') * (P(1) - S('"\\') + P('\\') * P(1))^0 * P('"')
|
||||
-- Char literal: '...' with backslash escapes.
|
||||
local lpeg_chr_pat = P("'") * (P(1) - S("'\\") + P('\\') * P(1))^0 * P("'")
|
||||
-- Line comment: // ... to end-of-line.
|
||||
local lpeg_line_cmt_pat = P("//") * (P(1) - S("\n"))^0
|
||||
-- Block comment: /* ... */ (no nesting per C standard).
|
||||
local lpeg_block_cmt_pat = P("/*") * (P(1) - P("*/"))^0 * P("*/")
|
||||
-- String or comment (any of the four forms).
|
||||
local lpeg_str_or_cmt_pat = lpeg_str_pat + lpeg_chr_pat + lpeg_line_cmt_pat + lpeg_block_cmt_pat
|
||||
|
||||
-- Whitespace + comment skipper: zero+ (whitespace run | string | comment).
|
||||
local ws_pat = S(" \t\n\r\v\f")
|
||||
local lpeg_ws_and_cmt_pat = (ws_pat + lpeg_str_or_cmt_pat)^0
|
||||
|
||||
-- Generic "skip until target, but step over balanced groups" matcher.
|
||||
-- Used by scan_to_char for non-ident / non-bracket chars.
|
||||
-- We accept any single char except the target.
|
||||
-- The balanced-group stepping is handled by the caller (via read_balanced).
|
||||
local lpeg_scan_to_target_pat = function(target) return (P(1) - P(target))^0 end
|
||||
|
||||
-- ════════════════════════════════════════════════════════════════════════════
|
||||
-- Section 1: character classification (byte-based for hot loops)
|
||||
@@ -213,20 +192,16 @@ end
|
||||
-- is_space(c), is_alpha(c), etc. — accept a single-char STRING (legacy)
|
||||
-- is_space_byte(b), is_alpha_byte(b), etc. — accept a single-byte INTEGER
|
||||
--
|
||||
-- The byte-based versions are 5-10x faster in tight loops because they
|
||||
-- avoid the string allocation per s:sub(i, i) call.
|
||||
-- The byte-based versions are 5-10x faster in tight loops because they avoid the string allocation per s:sub(pos, pos) call.
|
||||
|
||||
-- Whitespace characters per C locale.
|
||||
function M.is_space_byte(b)
|
||||
return b == BYTE_SPACE or b == BYTE_TAB or b == BYTE_NEWLINE
|
||||
or b == BYTE_CR or b == BYTE_VT or b == BYTE_FF
|
||||
end
|
||||
function M.is_space_byte(b) return b == BYTE_SPACE or b == BYTE_TAB or b == BYTE_NEWLINE or b == BYTE_CR or b == BYTE_VT or b == BYTE_FF end
|
||||
|
||||
-- Letters (a-z, A-Z) and underscore.
|
||||
function M.is_alpha_byte(b)
|
||||
if not b then return false end
|
||||
if b >= BYTE_LOWER_A and b <= BYTE_LOWER_Z then return true end -- 'a'..'z'
|
||||
if b >= BYTE_UPPER_A and b <= BYTE_UPPER_Z then return true end -- 'A'..'Z'
|
||||
if b >= BYTE_LOWER_A and b <= BYTE_LOWER_Z then return true end -- 'a'..'z'
|
||||
if b >= BYTE_UPPER_A and b <= BYTE_UPPER_Z then return true end -- 'A'..'Z'
|
||||
return b == BYTE_UNDERSCORE
|
||||
end
|
||||
|
||||
@@ -245,8 +220,8 @@ end
|
||||
function M.is_alpha(c)
|
||||
if type(c) == "number" then return M.is_alpha_byte(c) end
|
||||
if not c or #c == 0 then return false end
|
||||
if c >= "a" and c <= "z" then return true end
|
||||
if c >= "A" and c <= "Z" then return true end
|
||||
if c >= "a" and c <= "z" then return true end
|
||||
if c >= "A" and c <= "Z" then return true end
|
||||
return c == "_"
|
||||
end
|
||||
function M.is_digit(c)
|
||||
@@ -261,10 +236,8 @@ function M.is_alnum(c) return M.is_alpha(c) or M.is_digit(c) end
|
||||
|
||||
-- Trim leading and trailing whitespace from a string.
|
||||
function M.trim(s)
|
||||
local a = 1
|
||||
while a <= #s and M.is_space_byte(s:byte(a)) do a = a + 1 end
|
||||
local b = #s
|
||||
while b >= a and M.is_space_byte(s:byte(b)) do b = b - 1 end
|
||||
local a = 1; while a <= #s and M.is_space_byte(s:byte(a)) do a = a + 1 end
|
||||
local b = #s; while b >= a and M.is_space_byte(s:byte(b)) do b = b - 1 end
|
||||
return s:sub(a, b)
|
||||
end
|
||||
|
||||
@@ -286,7 +259,7 @@ function M.dirname(path)
|
||||
local last_sep = 0
|
||||
for pos = 1, #path do
|
||||
local b = path:byte(pos)
|
||||
if b == BYTE_SLASH or b == BYTE_BACKSLASH then last_sep = pos end
|
||||
if b == BYTE_SLASH or b == BYTE_BACKSLASH then last_sep = pos end
|
||||
end
|
||||
if last_sep == 0 then return "." end
|
||||
return path:sub(1, last_sep - 1)
|
||||
@@ -297,7 +270,7 @@ function M.basename_no_ext(path)
|
||||
local last_sep = 0
|
||||
for pos = 1, #path do
|
||||
local b = path:byte(pos)
|
||||
if b == BYTE_SLASH or b == BYTE_BACKSLASH then last_sep = pos end
|
||||
if b == BYTE_SLASH or b == BYTE_BACKSLASH then last_sep = pos end
|
||||
end
|
||||
local a = last_sep + 1
|
||||
local last_dot = #path + 1
|
||||
@@ -312,33 +285,29 @@ end
|
||||
-- ════════════════════════════════════════════════════════════════════════════
|
||||
|
||||
function M.read_file(path)
|
||||
local f = io.open(path, "r")
|
||||
local f = io.open(path, "r")
|
||||
if not f then error("Cannot open " .. path) end
|
||||
local content = f:read("*a")
|
||||
f:close()
|
||||
local content = f:read("*a"); f:close()
|
||||
return content
|
||||
end
|
||||
|
||||
function M.write_file(path, content)
|
||||
local f = io.open(path, "w")
|
||||
local f = io.open(path, "w")
|
||||
if not f then error("Cannot write " .. path) end
|
||||
f:write(content)
|
||||
f:close()
|
||||
f:write(content); f:close()
|
||||
end
|
||||
|
||||
-- Cache of directories already verified to exist in this process. Each
|
||||
-- ensure_dir() call may otherwise spawn a `cmd.exe mkdir` (50-100ms
|
||||
-- per call on Windows) — calling it inside per-source loops added 1.5+
|
||||
-- seconds to the report pass. Cache makes ensure_dir idempotent within
|
||||
-- the process lifetime (safe across passes; the dir state doesn't change).
|
||||
-- Cache of directories already verified to exist in this process.
|
||||
-- Each ensure_dir() call may otherwise spawn a `cmd.exe mkdir` (50-100ms per call on Windows) — calling it inside per-source loops added 1.5+
|
||||
-- seconds to the report pass. Cache makes ensure_dir idempotent within the process lifetime.
|
||||
-- (safe across passes; the dir state doesn't change).
|
||||
local _ensured_dirs = {}
|
||||
|
||||
function M.ensure_dir(path)
|
||||
if _ensured_dirs[path] then return end
|
||||
_ensured_dirs[path] = true
|
||||
local is_win = package.config:sub(1, 1) == "\\"
|
||||
os.execute(is_win and ('if not exist "' .. path .. '" mkdir "' .. path .. '"')
|
||||
or ('mkdir -p "' .. path .. '" 2>/dev/null'))
|
||||
os.execute(is_win and ('if not exist "' .. path .. '" mkdir "' .. path .. '"') or ('mkdir -p "' .. path .. '" 2>/dev/null'))
|
||||
end
|
||||
|
||||
-- Test helper: clear the cache (used by tests + between process runs).
|
||||
@@ -349,93 +318,33 @@ function M._reset_ensured_dirs() _ensured_dirs = {} end
|
||||
-- Section 4: C-language scanner primitives
|
||||
-- ════════════════════════════════════════════════════════════════════════════
|
||||
|
||||
-- LPeg-backed skipper when LPeg is available, hand-rolled fallback otherwise.
|
||||
-- Returns position just past the construct, or `i` unchanged if no
|
||||
-- string/comment starts at position i.
|
||||
function M.skip_str_or_cmt(s, i)
|
||||
if lpeg_ok then
|
||||
local new_pos = lpeg.match(lpeg_str_or_cmt_pat, s, i)
|
||||
if new_pos then return new_pos end
|
||||
return i
|
||||
end
|
||||
-- Hand-rolled fallback (kept for builds where LPeg isn't available).
|
||||
local c = s:byte(i)
|
||||
if c == BYTE_DQUOTE or c == BYTE_SQUOTE then -- '"' or '\''
|
||||
i = i + 1
|
||||
while i <= #s do
|
||||
local b = s:byte(i)
|
||||
if b == BYTE_BACKSLASH then i = i + 2 -- '\\'
|
||||
elseif b == c then return i + 1
|
||||
else i = i + 1 end
|
||||
end
|
||||
return #s + 1
|
||||
elseif c == BYTE_SLASH then -- '/'
|
||||
local nx = s:byte(i + 1)
|
||||
if nx == BYTE_SLASH then -- '//'
|
||||
while i <= #s and s:byte(i) ~= BYTE_NEWLINE do i = i + 1 end
|
||||
return i
|
||||
elseif nx == BYTE_STAR then -- '/*'
|
||||
i = i + 2
|
||||
while i <= #s - 1 do
|
||||
if s:byte(i) == BYTE_STAR and s:byte(i + 1) == BYTE_SLASH then -- '*/'
|
||||
return i + 2
|
||||
end
|
||||
i = i + 1
|
||||
end
|
||||
return #s + 1
|
||||
end
|
||||
end
|
||||
return i
|
||||
-- Skip a string or C-style comment starting at position `pos`.
|
||||
-- Returns the position just past the construct, or `pos` unchanged if no string/comment starts there. LPeg-backed.
|
||||
function M.skip_str_or_cmt(s, pos)
|
||||
return lpeg.match(lpeg_str_or_cmt_pat, s, pos) or pos
|
||||
end
|
||||
|
||||
-- Skip whitespace AND C-style comments starting at position i.
|
||||
-- LPeg-backed when available; ~5-10x faster than the hand-rolled version.
|
||||
function M.skip_ws_and_cmt(s, i)
|
||||
if lpeg_ok then
|
||||
local new_pos = lpeg.match(lpeg_ws_and_cmt_pat, s, i)
|
||||
if new_pos then return new_pos end
|
||||
return i
|
||||
end
|
||||
-- Hand-rolled fallback.
|
||||
local len = #s
|
||||
while i <= len do
|
||||
if M.is_space_byte(s:byte(i)) then
|
||||
i = i + 1
|
||||
else
|
||||
local nx = M.skip_str_or_cmt(s, i)
|
||||
if nx > i then i = nx else break end
|
||||
end
|
||||
end
|
||||
return i
|
||||
-- Skip whitespace AND C-style comments starting at position `pos`.
|
||||
-- LPeg-backed; ~5-10x faster than a hand-rolled byte-by-byte walker.
|
||||
function M.skip_ws_and_cmt(s, pos)
|
||||
return lpeg.match(lpeg_ws_and_cmt_pat, s, pos) or pos
|
||||
end
|
||||
|
||||
-- Read a C-style identifier (alpha followed by zero+ alnum) starting at
|
||||
-- position i. Returns the identifier string + the position just past it,
|
||||
-- or nil + i if no identifier starts here.
|
||||
function M.read_ident(s, i)
|
||||
if lpeg_ok then
|
||||
local result = lpeg.match(lpeg_ident_pat, s, i)
|
||||
if result then return result, i + #result end
|
||||
return nil, i
|
||||
end
|
||||
-- Hand-rolled fallback.
|
||||
if not M.is_alpha_byte(s:byte(i)) then return nil, i end
|
||||
local a = i
|
||||
i = i + 1
|
||||
while i <= #s and M.is_alnum_byte(s:byte(i)) do i = i + 1 end
|
||||
return s:sub(a, i - 1), i
|
||||
-- Read a C-style identifier (alpha followed by zero+ alnum) starting at position `pos`.
|
||||
-- Returns the identifier string + the position just past it, or nil + pos if no identifier starts here. LPeg-backed.
|
||||
function M.read_ident(s, pos)
|
||||
local result = lpeg.match(lpeg_ident_pat, s, pos)
|
||||
if result then return result, pos + #result end
|
||||
return nil, pos
|
||||
end
|
||||
|
||||
-- Read a balanced-delimited group (parens, braces, or brackets) starting
|
||||
-- at position i. Returns the inner content (between the delimiters) +
|
||||
-- the position just past the closing delimiter, or nil + i if `s[i]`
|
||||
-- isn't `open_char`.
|
||||
--
|
||||
-- (Hand-rolled; the depth counting makes pure LPeg awkward here.)
|
||||
function M.read_balanced(s, open_char, close_char, i)
|
||||
-- Read a balanced-delimited group (parens, braces, or brackets) starting at position `pos`.
|
||||
-- Returns the inner content (between the delimiters) + the position
|
||||
-- just past the closing delimiter, or nil + pos if `s[pos]` isn't `open_char`.
|
||||
function M.read_balanced(s, open_char, close_char, pos)
|
||||
local open_byte = open_char:byte()
|
||||
if s:byte(i) ~= open_byte then return nil, i end
|
||||
local pos = i + 1
|
||||
if s:byte(pos) ~= open_byte then return nil, pos end
|
||||
pos = pos + 1
|
||||
local len = #s
|
||||
local depth = 1
|
||||
local a = pos
|
||||
@@ -450,32 +359,32 @@ function M.read_balanced(s, open_char, close_char, i)
|
||||
pos = pos + 1
|
||||
else
|
||||
local nx = M.skip_str_or_cmt(s, pos)
|
||||
pos = (nx > pos) and nx or (pos + 1)
|
||||
if nx > pos then pos = nx else pos = pos + 1 end
|
||||
end
|
||||
end
|
||||
return s:sub(a, pos - 1), pos + 1
|
||||
end
|
||||
|
||||
-- Convenience specializations of read_balanced.
|
||||
M.read_parens = function(s, i) return M.read_balanced(s, "(", ")", i) end
|
||||
M.read_braces = function(s, i) return M.read_balanced(s, "{", "}", i) end
|
||||
M.read_brackets = function(s, i) return M.read_balanced(s, "[", "]", i) end
|
||||
M.read_parens = function(s, pos) return M.read_balanced(s, "(", ")", pos) end
|
||||
M.read_braces = function(s, pos) return M.read_balanced(s, "{", "}", pos) end
|
||||
M.read_brackets = function(s, pos) return M.read_balanced(s, "[", "]", pos) end
|
||||
|
||||
-- Scan forward from position `start` until we find a specific single byte
|
||||
-- `target`, transparently stepping over balanced parens/braces/brackets.
|
||||
-- Scan forward from position `start` until we find a specific single byte `target`,
|
||||
-- transparently stepping over balanced parens/braces/brackets.
|
||||
-- Returns the position of `target`, or nil if not found.
|
||||
function M.scan_to_char(s, target, start)
|
||||
local target_byte = target:byte()
|
||||
local pos = start
|
||||
while pos <= #s do
|
||||
local c = s:byte(pos)
|
||||
if c == target_byte then return pos end
|
||||
if c == target_byte then return pos end
|
||||
if c == BYTE_OPEN_PAREN then local _, a = M.read_balanced(s, "(", ")", pos); pos = a
|
||||
elseif c == BYTE_OPEN_BRACE then local _, a = M.read_balanced(s, "{", "}", pos); pos = a
|
||||
elseif c == BYTE_OPEN_BRACK then local _, a = M.read_balanced(s, "[", "]", pos); pos = a
|
||||
else
|
||||
local nx = M.skip_str_or_cmt(s, pos)
|
||||
pos = (nx > pos) and nx or (pos + 1)
|
||||
pos = (nx > pos) and nx or (pos + 1)
|
||||
end
|
||||
end
|
||||
return nil
|
||||
@@ -484,23 +393,18 @@ end
|
||||
-- Split a brace-body into top-level comma-separated tokens. Honors nested
|
||||
-- parens/braces/brackets and skips strings/comments.
|
||||
--
|
||||
-- FIX (2026-07-09): split at top-level NEWLINES and SEMICOLONS too, AND
|
||||
-- emit a token break after a top-level comment/string. Previous behavior
|
||||
-- glued the macro call after a comment into the same token, so
|
||||
-- `word_count_of_token` only saw the leading ident (often nil after
|
||||
-- stripping the comment), undercounting the body. See Phase 1 of the
|
||||
-- branch-offset regression investigation. Pure-comment / pure-string
|
||||
-- chunks (which now appear between real statements) are filtered out so
|
||||
-- they contribute 0 words instead of 1.
|
||||
-- FIX (2026-07-09): split at top-level NEWLINES and SEMICOLONS too, AND emit a token break after a top-level comment/string.
|
||||
-- Previous behavior glued the macro call after a comment into the same token, so `word_count_of_token` only saw the
|
||||
-- leading ident (often nil after stripping the comment), undercounting the body. See Phase 1 of the branch-offset regression investigation.
|
||||
-- Pure-comment / pure-string chunks (which now appear between real statements) are filtered out so they contribute 0 words instead of 1.
|
||||
function M.split_top_level_commas(body)
|
||||
local tokens = {}
|
||||
local pos = 1
|
||||
local body_len = #body
|
||||
local token_start = 1
|
||||
|
||||
-- True iff `chunk` contains any non-whitespace, non-comment, non-string
|
||||
-- content (i.e., real token material). Walks through ws + comments
|
||||
-- individually so a chunk like " /* trailing */ shift_lleft(...)"
|
||||
-- True iff `chunk` contains any non-whitespace, non-comment, non-string content
|
||||
-- (i.e., real token material). Walks through ws + comments individually so a chunk like " /* trailing */ shift_lleft(...)"
|
||||
-- is correctly classified as having real content (the macro call).
|
||||
local function has_real_content(chunk)
|
||||
local scan = 1
|
||||
@@ -527,22 +431,14 @@ function M.split_top_level_commas(body)
|
||||
if has_real_content(chunk) then
|
||||
tokens[#tokens + 1] = chunk
|
||||
elseif #tokens > 0 then
|
||||
-- Pure comment/string chunk at top level (no
|
||||
-- preceding instruction content within this chunk).
|
||||
-- APPEND it to the LAST token so emit-context
|
||||
-- callers (components.lua build_component_lines)
|
||||
-- can convert `// trailing comment` to `/* */`
|
||||
-- and emit it with the macro body. For word
|
||||
-- counting, count_token_words only inspects the
|
||||
-- leading ident, so a trailing comment doesn't
|
||||
-- affect the count.
|
||||
-- Pure comment/string chunk at top level (no preceding instruction content within this chunk).
|
||||
-- APPEND it to the LAST token so emit-context callers (components.lua build_component_lines)
|
||||
-- can convert `// trailing comment` to `/* */` and emit it with the macro body.
|
||||
-- For word counting, count_token_words only inspects the leading ident, so a trailing comment doesn't affect the count.
|
||||
--
|
||||
-- This is the second-half fix to commit 98e27c2:
|
||||
-- the first fix correctly broke top-level comments
|
||||
-- off from the NEXT statement (fixing macro-call
|
||||
-- word counts); this fix preserves them on the
|
||||
-- PREVIOUS statement (restoring the comments in
|
||||
-- the emitted .macs.h output).
|
||||
-- This is the second-half fix to commit 98e27c2: the first fix correctly broke top-level comments
|
||||
-- off from the NEXT statement (fixing macro-call word counts);
|
||||
-- This fix preserves them on the PREVIOUS statement (restoring the comments in the emitted .macs.h output).
|
||||
tokens[#tokens] = tokens[#tokens] .. chunk
|
||||
end
|
||||
end
|
||||
@@ -560,15 +456,15 @@ function M.split_top_level_commas(body)
|
||||
local _, a = M.read_brackets(body, pos); pos = a
|
||||
elseif c == BYTE_COMMA then -- ','
|
||||
emit(pos - 1)
|
||||
pos = pos + 1
|
||||
pos = pos + 1
|
||||
token_start = pos
|
||||
elseif c == BYTE_SEMI then -- ';'
|
||||
emit(pos - 1)
|
||||
pos = pos + 1
|
||||
pos = pos + 1
|
||||
token_start = pos
|
||||
elseif c == BYTE_NEWLINE then -- '\n'
|
||||
emit(pos - 1)
|
||||
pos = pos + 1
|
||||
pos = pos + 1
|
||||
token_start = pos
|
||||
else
|
||||
local nx = M.skip_str_or_cmt(body, pos)
|
||||
@@ -628,7 +524,7 @@ function M.LineIndex(source)
|
||||
end
|
||||
-- (internal) Binary-search for the line number containing `query_pos`.
|
||||
local function line_of(query_pos)
|
||||
local lo, hi = 1, n
|
||||
local lo, hi = 1, n
|
||||
while lo <= hi do
|
||||
local mid = math.floor((lo + hi) / 2)
|
||||
if positions[mid] <= query_pos then
|
||||
@@ -665,34 +561,23 @@ M.TAPE_ATOM_MACROS = {
|
||||
|
||||
-- GTE pipeline-fill latency table (static-analysis Phase 1).
|
||||
--
|
||||
-- For each `gte_cmdw_*` macro in code/duffle/gte.h, the minimum number
|
||||
-- of consecutive COP2 "nop" words that MUST appear before any other
|
||||
-- COP2 read or non-nop instruction (so the GTE pipeline latency is
|
||||
-- fully retired). Latencies are sourced from the doxygen comments
|
||||
-- in gte.h (e.g. `* @brief Rotate, Translate and Perspective Triple
|
||||
-- (23 cycles)` with body `Two nop words fill the COP2 pipeline
|
||||
-- latency`).
|
||||
-- For each `gte_cmdw_*` macro in code/duffle/gte.h, the minimum number of consecutive COP2 "nop" words that MUST appear
|
||||
-- before any other COP2 read or non-nop instruction (so the GTE pipeline latency is fully retired).
|
||||
-- Latencies are sourced from the doxygen comments in gte.h
|
||||
-- (e.g. `* @brief Rotate, Translate and Perspective Triple (23 cycles)` with body `Two nop words fill the COP2 pipeline latency`).
|
||||
--
|
||||
-- The check (`scripts/passes/static_analysis.lua ::
|
||||
-- check_gte_pipeline_fill`) walks each atom body, counts the
|
||||
-- consecutive nop words after every `gte_cmdw_*` invocation, and
|
||||
-- reports a finding if the count is below this minimum. Aliases
|
||||
-- are dereferenced before lookup (gté_cmdw_rtps_alias ->
|
||||
-- gte_cmdw_rtps -> 2).
|
||||
--
|
||||
-- Values verified against PSX-SPX gte.txt (rtpt 23cy / 8cy per divide
|
||||
-- => 2 nops; nclip 8cy => 2 nops; avsz3/avsz4 14cy => 2 nops; op
|
||||
-- single-cycle atomic => 0 nops; mvmva 8cy matrix-vector => 2 nops).
|
||||
-- The check (`scripts/passes/static_analysis.lua :: check_gte_pipeline_fill`) walks each atom body,
|
||||
-- counts the consecutive nop words after every `gte_cmdw_*` invocation, and reports a finding if the count is below this minimum.
|
||||
-- Aliases are dereferenced before lookup (gté_cmdw_rtps_alias -> gte_cmdw_rtps -> 2).
|
||||
--
|
||||
-- Values verified against PSX-SPX gte.txt (rtpt 23cy / 8cy per divide => 2 nops; nclip 8cy => 2 nops; avsz3/avsz4 14cy => 2 nops;
|
||||
-- op single-cycle atomic => 0 nops; mvmva 8cy matrix-vector => 2 nops).
|
||||
M.GTE_PIPELINE_LATENCY = {
|
||||
-- Minimum number of consecutive `nop` words that must appear
|
||||
-- IMMEDIATELY BEFORE a `gte_cmdw_<X>` invocation -- to retire
|
||||
-- any preceding `lwc2` / `swc2` / pre-existing C2 state writes
|
||||
-- before the GTE pipeline starts reading from V0/V1/V2 or
|
||||
-- MAC0..3 / OTZ / IR0..3 at the command's issue cycle.
|
||||
--
|
||||
-- Values are from the doxygen comments in code/duffle/gte.h and
|
||||
-- cross-checked against PSX-SPX `geometrytransformationenginegte.md`:
|
||||
-- Minimum number of consecutive `nop` words that must appear IMMEDIATELY BEFORE a `gte_cmdw_<X>` invocation
|
||||
-- to retire any preceding `lwc2` / `swc2` / pre-existing C2 state writes before the GTE pipeline starts reading
|
||||
-- from V0/V1/V2 or MAC0..3 / OTZ / IR0..3 at the command's issue cycle.
|
||||
--
|
||||
-- Values are from the doxygen comments in code/duffle/gte.h and cross-checked against PSX-SPX `geometrytransformationenginegte.md`:
|
||||
-- cmd cycles min pre-nops rationale
|
||||
-- rtps 14 2 8c per perspective divide + 6c for IR1..4 + mac write
|
||||
-- rptt 22 2 3x rtps worth of pipeline depth
|
||||
@@ -700,24 +585,18 @@ M.GTE_PIPELINE_LATENCY = {
|
||||
-- avsz3 14 2 14c to compute average + write OTZ
|
||||
-- avsz4 16 2 avsz3 + 2c extra for avg over 4
|
||||
-- mvmva 8 2 IR1..4 write + matrix work
|
||||
-- op 5 0 output to MAC0 only (atomic 5c calc)
|
||||
-- op 5 0 output to MAC0 only (atomic 5c calc)
|
||||
--
|
||||
-- The `gte_rtpt()` / `gte_nclip()` / `gte_avsz3()` wrapper macros in
|
||||
-- gte.h emit the pre-cmd nops internally (asm_words(nop, nop, ...)),
|
||||
-- but THOSE WRAPPERS ARE NOT USED INSIDE ATOM BODIES in this
|
||||
-- codebase. Every MipsAtom_(name) body uses raw `nop2,
|
||||
-- gte_cmdw_<X>, ...` form instead -- that `nop2,` is the pre-fill
|
||||
-- this check validates. So values here must reflect the source-level
|
||||
-- convention, NOT the wrapper-internal pre-fill (which is invisible
|
||||
-- at the source level).
|
||||
-- The `gte_rtpt()` / `gte_nclip()` / `gte_avsz3()` wrapper macros in gte.h emit the pre-cmd nops internally (asm_words(nop, nop, ...)),
|
||||
-- but THOSE WRAPPERS ARE NOT USED INSIDE ATOM BODIES in this codebase.
|
||||
-- Every MipsAtom_(name) body uses raw `nop2, gte_cmdw_<X>, ...` form instead -- that `nop2,` is the pre-fill this check validates.
|
||||
-- So values here must reflect the source-level convention, NOT the wrapper-internal pre-fill (which is invisible at the source level).
|
||||
--
|
||||
-- Existing clean-atom bodies (cube_g4_face, floor_f3_face,
|
||||
-- diag_gte) all emit `nop2,` before every `gte_cmdw_<X>` (which
|
||||
-- matches values >= 2). The check passes them all.
|
||||
-- Existing clean-atom bodies (cube_g4_face, floor_f3_face, diag_gte) all emit `nop2,` before every `gte_cmdw_<X>` (which matches values >= 2).
|
||||
-- The check passes them all.
|
||||
--
|
||||
-- Aliases are listed separately because source code may use either
|
||||
-- the alias or the canonical name. The check looks up the EXACT
|
||||
-- macro text, so both forms must be in the table.
|
||||
-- Aliases are listed separately because source code may use either the alias or the canonical name.
|
||||
-- The check looks up the EXACT macro text, so both forms must be in the table.
|
||||
|
||||
-- Canonical macros (from code/duffle/gte.h)
|
||||
["gte_cmdw_rtps"] = 2,
|
||||
@@ -731,26 +610,26 @@ M.GTE_PIPELINE_LATENCY = {
|
||||
-- Aliases (must have the same value as their canonical target)
|
||||
["gte_cmdw_rotate_translate_perspective_single"] = 2,
|
||||
["gte_cmdw_rotate_translate_perspective_triple"] = 2,
|
||||
["gte_cmdw_avg_sort_z4"] = 2,
|
||||
["gte_cmdw_avg_sort_z4"] = 2,
|
||||
|
||||
-- Outer product aliases (same canonical op, 0 pre-fill nops).
|
||||
-- gte_cmdw_op = canonical GTE-internal short form
|
||||
-- gte_cmdw_op = canonical GTE-internal short form
|
||||
-- gte_cmdw_outer_product = NOCASH / SDK-readable form
|
||||
-- gte_cmdw_wedge = geometric-algebra (exterior-product) form
|
||||
["gte_cmdw_outer_product"] = 0,
|
||||
["gte_cmdw_wedge"] = 0,
|
||||
["gte_cmdw_wedge"] = 0,
|
||||
}
|
||||
|
||||
-- GP0 packet sizes (total words including the 1-word tag) per GP0 cmd byte.
|
||||
-- Verified against code/duffle/gp.h struct sizes + the set_poly_* macros
|
||||
-- (which encode "len" = "words after tag"):
|
||||
-- set_poly_f3(p) -> set_len(p, 4) -> 5 total GP0 0x20
|
||||
-- set_poly_ft3(p) -> set_len(p, 7) -> 8 total GP0 0x24
|
||||
-- set_poly_f4(p) -> set_len(p, 5) -> 6 total GP0 0x28
|
||||
-- set_poly_f3(p) -> set_len(p, 4) -> 5 total GP0 0x20
|
||||
-- set_poly_ft3(p) -> set_len(p, 7) -> 8 total GP0 0x24
|
||||
-- set_poly_f4(p) -> set_len(p, 5) -> 6 total GP0 0x28
|
||||
-- set_poly_ft4(p) -> set_len(p, 9) -> 10 total GP0 0x2C
|
||||
-- set_poly_g3(p) -> set_len(p, 6) -> 7 total GP0 0x30
|
||||
-- set_poly_g3(p) -> set_len(p, 6) -> 7 total GP0 0x30
|
||||
-- set_poly_gt3(p) -> set_len(p, 9) -> 10 total GP0 0x34
|
||||
-- set_poly_g4(p) -> set_len(p, 8) -> 9 total GP0 0x38
|
||||
-- set_poly_g4(p) -> set_len(p, 8) -> 9 total GP0 0x38
|
||||
-- set_poly_gt4(p) -> set_len(p, 12) -> 13 total GP0 0x3C
|
||||
M.GP0_CMD_SIZE = {
|
||||
[0x20] = 5, -- Poly_F3
|
||||
@@ -764,8 +643,7 @@ M.GP0_CMD_SIZE = {
|
||||
}
|
||||
|
||||
-- Shape suffix (after `ac_format_` / `mac_format_` prefix) -> GP0 cmd byte.
|
||||
-- Lets the static-analysis check derive the cmd byte from a macro name
|
||||
-- like `mac_format_g4_color` -> `g4` -> 0x38 -> 9 expected words.
|
||||
-- Lets the static-analysis check derive the cmd byte from a macro name like `mac_format_g4_color` -> `g4` -> 0x38 -> 9 expected words.
|
||||
M.GP0_CMD_BY_SHAPE = {
|
||||
["f3"] = 0x20, ["ft3"] = 0x24,
|
||||
["f4"] = 0x28, ["ft4"] = 0x2C,
|
||||
@@ -773,11 +651,9 @@ M.GP0_CMD_BY_SHAPE = {
|
||||
["g4"] = 0x38, ["gt4"] = 0x3C,
|
||||
}
|
||||
|
||||
-- Per-macro prim-buffer contribution (NOT .text instruction count --
|
||||
-- this is "how many 32-bit words does this macro write to the primitive
|
||||
-- being built in main RAM"). Sum across `mac_format_X_color` +
|
||||
-- `mac_gte_store_X_post_*` + `mac_insert_ot_tag_X` calls in an atom body
|
||||
-- must equal GP0_CMD_SIZE[GP0_CMD_BY_SHAPE[shape]].
|
||||
-- Per-macro prim-buffer contribution
|
||||
-- (NOT .text instruction count this is "how many 32-bit words does this macro write to the primitive being built in main RAM").
|
||||
-- Sum across `mac_format_X_color` + `mac_gte_store_X_post_*` + `mac_insert_ot_tag_X` calls in an atom body must equal GP0_CMD_SIZE[GP0_CMD_BY_SHAPE[shape]].
|
||||
M.GP0_MACRO_CONTRIB = {
|
||||
["mac_format_f3_color"] = 1,
|
||||
["mac_format_g3_color"] = 3,
|
||||
@@ -790,33 +666,28 @@ M.GP0_MACRO_CONTRIB = {
|
||||
["mac_insert_ot_tag_g4"] = 1,
|
||||
}
|
||||
|
||||
-- Per-macro cycle cost (best-case, no stalls). Used by the static-analysis
|
||||
-- `count_atom_cycles` pass (Phase 3) to emit per-atom cycle budgets. The
|
||||
-- counts cover the EXPANDED instruction sequence the macro emits (NOT just
|
||||
-- the token it appears as in source). For example:
|
||||
--
|
||||
-- Per-macro cycle cost (best-case, no stalls). Used by the static-analysis `count_atom_cycles` pass (Phase 3) to emit per-atom cycle budgets.
|
||||
-- The counts cover the EXPANDED instruction sequence the macro emits (NOT just the token it appears as in source).
|
||||
-- For example:
|
||||
-- mac_pack_color_word(off, cmd, r, g, b) emits:
|
||||
-- load_upper_i(R_AT, (cmd << 8) | b) -- 1 cycle
|
||||
-- or_i_self(R_AT, (g << 8) | r) -- 1 cycle
|
||||
-- store_word(R_AT, R_PrimCursor, off) -- 1 cycle
|
||||
-- = 3 cycles total
|
||||
--
|
||||
-- mac_yield emits a control-transfer sequence (load_word, add_ui_self,
|
||||
-- jump_reg, nop) which "yields control" -- the atom body's cycle budget
|
||||
-- doesn't include the yield's cost (we model it as 0; runtime cost
|
||||
-- becomes part of the NEXT atom's prologue).
|
||||
-- mac_yield emits a control-transfer sequence (load_word, add_ui_self, jump_reg, nop)
|
||||
-- which "yields control" the atom body's cycle budget doesn't include the yield's cost (we model it as 0;
|
||||
-- runtime cost becomes part of the NEXT atom's prologue).
|
||||
--
|
||||
-- GTE command values are the GTE instruction's intrinsic cycles (the
|
||||
-- latency AFTER any pre-cmd `nop2` has retired). When the source emits
|
||||
-- `nop2, gte_cmdw_X` the nops' cycles are added separately (1+1) plus
|
||||
-- the gte_cmdw_X value here:
|
||||
-- rtpt = 21 + 2 nops = 23 total cycles (matches PSX-SPX)
|
||||
-- rtps = 12 + 2 nops = 14 total
|
||||
-- nclip = 6 + 2 nops = 8 total
|
||||
-- GTE command values are the GTE instruction's intrinsic cycles (the latency AFTER any pre-cmd `nop2` has retired).
|
||||
-- When the source emits `nop2, gte_cmdw_X` the nops' cycles are added separately (1+1) plus the gte_cmdw_X value here:
|
||||
-- rtpt = 21 + 2 nops = 23 total cycles (matches PSX-SPX)
|
||||
-- rtps = 12 + 2 nops = 14 total
|
||||
-- nclip = 6 + 2 nops = 8 total
|
||||
-- avsz3 = 12 + 2 nops = 14 total
|
||||
-- avsz4 = 14 + 2 nops = 16 total
|
||||
-- mvmva = 6 + 2 nops = 8 total
|
||||
-- op = 5 (no pre-cmd nops required; single-cycle atomic)
|
||||
-- mvmva = 6 + 2 nops = 8 total
|
||||
-- op = 5 (no pre-cmd nops required; single-cycle atomic)
|
||||
M.INSTRUCTION_LATENCY = {
|
||||
-- CPU ALU (single-cycle R3000A ops)
|
||||
["nop"] = 1,
|
||||
@@ -882,27 +753,27 @@ M.INSTRUCTION_LATENCY = {
|
||||
-- COP2 commands (intrinsic cycles, EXCLUDING the 2 pre-cmd nops that
|
||||
-- the source typically emits as `nop2, gte_cmdw_X`; those nops are
|
||||
-- counted separately via the `nop2` entry above)
|
||||
["gte_cmdw_rtpt"] = 21,
|
||||
["gte_cmdw_rtps"] = 12,
|
||||
["gte_cmdw_nclip"] = 6,
|
||||
["gte_cmdw_avsz3"] = 12,
|
||||
["gte_cmdw_avsz4"] = 14,
|
||||
["gte_cmdw_mvmva"] = 6,
|
||||
["gte_cmdw_op"] = 5,
|
||||
["gte_cmdw_rtpt"] = 21,
|
||||
["gte_cmdw_rtps"] = 12,
|
||||
["gte_cmdw_nclip"] = 6,
|
||||
["gte_cmdw_avsz3"] = 12,
|
||||
["gte_cmdw_avsz4"] = 14,
|
||||
["gte_cmdw_mvmva"] = 6,
|
||||
["gte_cmdw_op"] = 5,
|
||||
["gte_cmdw_outer_product"] = 5,
|
||||
["gte_cmdw_wedge"] = 5,
|
||||
["gte_cmdw_wedge"] = 5,
|
||||
-- Long-form aliases (same cost as canonical)
|
||||
["gte_cmdw_rotate_translate_perspective_single"] = 12, -- alias for rtps
|
||||
["gte_cmdw_rotate_translate_perspective_triple"] = 21, -- alias for rtpt
|
||||
["gte_cmdw_avg_sort_z4"] = 14, -- alias for avsz4
|
||||
["gte_cmdw_avg_sort_z4"] = 14, -- alias for avsz4
|
||||
-- Non-cmdw aliases from gte.h (these are `#define gte_X gte_cmdw_Y`):
|
||||
["gte_avg_sort_z3"] = 12, -- alias for avsz3
|
||||
["gte_avg_sort_z4"] = 14, -- alias for avsz4
|
||||
["gte_rtps"] = 12, -- alias for rtps
|
||||
["gte_rtpt"] = 21, -- alias for rtpt
|
||||
["gte_nclip"] = 6, -- alias for nclip
|
||||
["gte_avsz3"] = 12,
|
||||
["gte_avsz4"] = 14,
|
||||
["gte_avg_sort_z3"] = 12, -- alias for avsz3
|
||||
["gte_avg_sort_z4"] = 14, -- alias for avsz4
|
||||
["gte_rtps"] = 12, -- alias for rtps
|
||||
["gte_rtpt"] = 21, -- alias for rtpt
|
||||
["gte_nclip"] = 6, -- alias for nclip
|
||||
["gte_avsz3"] = 12,
|
||||
["gte_avsz4"] = 14,
|
||||
-- Legacy single-cycle store helpers (gte_stotz, gte_stsxy3 are 1 cycle)
|
||||
["gte_stotz"] = 1,
|
||||
["gte_stsxy3"] = 1,
|
||||
@@ -914,18 +785,18 @@ M.INSTRUCTION_LATENCY = {
|
||||
-- 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,
|
||||
["mac_pack_color_word"] = 3, -- lui + ori + sw
|
||||
["mac_format_f3_color"] = 3, -- = mac_pack_color_word
|
||||
["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_yield"] = 0,
|
||||
["mac_pack_color_word"] = 3, -- lui + ori + sw
|
||||
["mac_format_f3_color"] = 3, -- = mac_pack_color_word
|
||||
["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_insert_ot_tag_f3"] = 11, -- 11 .word slots in the macro body
|
||||
["mac_insert_ot_tag_g4"] = 11,
|
||||
["mac_gte_store_g4_p3_post_rtps"] = 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,
|
||||
@@ -935,14 +806,9 @@ M.INSTRUCTION_LATENCY = {
|
||||
["atom_writes"] = 0,
|
||||
}
|
||||
|
||||
-- Default cycle cost for unknown macros. The static-analysis pass adds 1
|
||||
-- cycle per unknown token and emits a "new macro; update INSTRUCTION_LATENCY"
|
||||
-- Default cycle cost for unknown macros.
|
||||
-- The static-analysis pass adds 1 cycle per unknown token and emits a "new macro; update INSTRUCTION_LATENCY"
|
||||
-- advisory so the cycle budget stays accurate as the codebase grows.
|
||||
M.UNKNOWN_INSTRUCTION_CYCLES = 1
|
||||
|
||||
-- Expose the lpeg_ok flag so callers can detect the LPeg-back path.
|
||||
-- True when LPeg was successfully required and the patterns above were
|
||||
-- compiled at module load time. False when running in fallback mode.
|
||||
M.lpeg_ok = lpeg_ok
|
||||
|
||||
return M
|
||||
|
||||
+24
-43
@@ -1,63 +1,43 @@
|
||||
--- duffle_paths.lua — Single-line bootstrap helper for the tape-atom
|
||||
--- Lua scripts.
|
||||
---
|
||||
--- Each entry script (ps1_meta.lua, word_count_eval.lua, and the 5
|
||||
--- passes/*.lua files) starts with:
|
||||
--- duffle_paths.lua — Single-line bootstrap helper for the tape-atom Lua scripts.
|
||||
---
|
||||
--- Each entry script (ps1_meta.lua, word_count_eval.lua, and the 5 passes/*.lua files) starts with:
|
||||
--- ```lua
|
||||
--- local duffle = dofile((arg[0]:match("(.*[/\\])") or "./") .. "duffle_paths.lua")
|
||||
--- ```
|
||||
---
|
||||
--- That single line: (a) locates this helper via `arg[0]`, (b) loads
|
||||
--- it (which sets `package.path` + `package.cpath` via `git rev-parse`),
|
||||
--- (c) returns the `M` table (a wrapper around the setup function).
|
||||
--- After this line, `require("duffle")` and `require("passes.X")` both
|
||||
--- resolve normally.
|
||||
---
|
||||
--- **Why a helper instead of inline?**
|
||||
--- - The 8-line path-setup boilerplate was duplicated across 7 entry
|
||||
--- scripts (one per file). Single source of truth here.
|
||||
--- - Mirrors the build script's pattern in `build_psyq.ps1`:
|
||||
--- `$path_root = split-path -Path $PSScriptRoot -Parent;` then
|
||||
--- derive everything from there.
|
||||
---
|
||||
--- **Why `git rev-parse --show-toplevel`?**
|
||||
--- Hardcoding `C:\\projects\\Pikuma\\ps1\\...` breaks portability. Git
|
||||
--- gives us the canonical repo root regardless of where the repo lives
|
||||
--- on disk.
|
||||
--- That single line: (a) locates this helper via `arg[0]`,
|
||||
--- (b) loads it (which sets `package.path` + `package.cpath` via `git rev-parse`),
|
||||
--- (c) returns the `M` table (a wrapper around the setup function).
|
||||
--- After this line, `require("duffle")` and `require("passes.X")` both resolve normally.
|
||||
|
||||
local M = {}
|
||||
|
||||
-- Cache key for the repo root. Stored in `package.loaded` (process-
|
||||
-- global) so all 8 entry scripts + passes scripts share one git call.
|
||||
-- Without this cache, `git rev-parse --show-toplevel` runs once per
|
||||
-- script load = 8 × ~150ms = 1.2s wasted per build on Windows.
|
||||
-- Cache key for the repo root. Stored in `package.loaded` (process-global) so all 8 entry scripts + passes scripts share one git call.
|
||||
-- Without this cache, `git rev-parse --show-toplevel` runs once per script load.
|
||||
local CACHE_KEY = "__duffle_repo_root__"
|
||||
|
||||
--- Resolve the repo root via git (cached after first call).
|
||||
--- Returns a normalized path with a trailing forward-slash, or nil
|
||||
--- if not in a git repo.
|
||||
--- Returns a normalized path with a trailing forward-slash, or nil if not in a git repo.
|
||||
--- @return string|nil
|
||||
local function find_repo_root()
|
||||
if package.loaded[CACHE_KEY] then return package.loaded[CACHE_KEY] end
|
||||
local p = io.popen("git rev-parse --show-toplevel 2>nul")
|
||||
local root
|
||||
if p then
|
||||
root = p:read("*l")
|
||||
p:close()
|
||||
end
|
||||
if p then root = p:read("*l"); p:close() end
|
||||
if not root or root == "" then return nil end
|
||||
-- Normalize to forward slashes (Windows accepts both, but mixed
|
||||
-- `\` + `/` confuses LuaJIT's file APIs).
|
||||
-- Normalize to forward slashes (Windows accepts both, but mixed `\` + `/` confuses LuaJIT's file APIs).
|
||||
root = root:gsub("\\", "/")
|
||||
if not root:match("/$") then root = root .. "/" end
|
||||
package.loaded[CACHE_KEY] = root
|
||||
return root
|
||||
end
|
||||
|
||||
--- Set `package.path` (for `require("duffle")` + `require("passes.X")`)
|
||||
--- and `package.cpath` (for `lpeg.dll` on Windows).
|
||||
--- Idempotent: safe to call multiple times (just re-sets the same paths).
|
||||
--- Set `package.path` (for `require("duffle")` + `require("passes.X")`) and
|
||||
--- `package.cpath` (for `lpeg.dll`).
|
||||
---
|
||||
--- This script does NOT touch the OS environment: no `os.setenv`, no `os.putenv`, no `$PATH` mods.
|
||||
--- It just sets `package.path` and `package.cpath` (the standard Lua way to register module search dirs).
|
||||
--- lpeg is built by `update_deps.ps1` to `toolchain/lpeg/`,
|
||||
--- which we wire into `package.cpath` here (so `require("lpeg")` from `duffle.lua` resolves without any global state).
|
||||
function M.setup()
|
||||
local repo_root = find_repo_root()
|
||||
if not repo_root then
|
||||
@@ -73,13 +53,14 @@ function M.setup()
|
||||
.. passes_dir .. "?/init.lua;"
|
||||
.. package.path
|
||||
|
||||
if package.config:sub(1, 1) == "\\" then
|
||||
package.cpath = repo_root .. "toolchain/luajit-2.1/lib/lua/5.1/?.dll;"
|
||||
.. package.cpath
|
||||
end
|
||||
-- lpeg: built by `update_deps.ps1` to `toolchain/lpeg/lpeg.dll`.
|
||||
-- Wire its directory into cpath so `require("lpeg")` resolves.
|
||||
local lpeg_dir = repo_root .. "toolchain/lpeg/"
|
||||
package.cpath = lpeg_dir .. "?.dll;"
|
||||
.. package.cpath
|
||||
end
|
||||
|
||||
-- Run the setup as a side effect.
|
||||
M.setup()
|
||||
|
||||
return M
|
||||
return M
|
||||
|
||||
+137
-178
@@ -1,32 +1,22 @@
|
||||
--- passes/annotation.lua — Atom-annotation DSL validator.
|
||||
---
|
||||
--- Validates `MipsAtom_(name) atom_info(atom_bind(Binds_X),
|
||||
--- atom_reads(...), atom_writes(...)) { ... }` declarations in source files.
|
||||
--- Also reads:
|
||||
--- - `Binds_*` struct declarations (`typedef Struct_(Binds_X) { ... };`)
|
||||
--- - `TAPE_WORDS(mac_X, N)` pragma directives (`#pragma` + `_Pragma`)
|
||||
--- Validates `MipsAtom_(name) atom_info(atom_bind(Binds_X), atom_reads(...), atom_writes(...)) { ... }` declarations in source files.
|
||||
--- Also reads: `Binds_*` struct declarations (`typedef Struct_(Binds_X) { ... };`)
|
||||
---
|
||||
--- Writes:
|
||||
--- - `<ctx.out_root>/<dir_basename>.errors.h` — one per module, with
|
||||
--- `#error` directives on findings (the C compile will surface the
|
||||
--- error)
|
||||
--- - The annotations.txt report is rendered by `passes/report.lua`
|
||||
--- from the per-module results stashed in `ctx.flags._annot_results`
|
||||
---
|
||||
--- **Ported from** `scripts/tape_atom_annotation_pass.lua:78-545 +
|
||||
--- 1081-1407` (validation only — NOT rendering, which goes to
|
||||
--- `passes/report.lua`).
|
||||
--- - `<ctx.out_root>/<dir_basename>.errors.h` — one per module, with `#error` directives on findings (the C compile will surface the error)
|
||||
--- - The annotations.txt report is rendered by `passes/report.lua` from the per-module results stashed in `ctx.flags._annot_results`
|
||||
---
|
||||
--- **Conventions**: tabs (1/level), EmmyLua annotations, no regex,
|
||||
--- Lua 5.3 compatible. See
|
||||
--- `C:\projects\Pikuma\ps1-ai\conductor\code_styleguides\lua.md`.
|
||||
|
||||
-- ════════════════════════════════════════════════════════════════════════════
|
||||
-- Module-scope requires + package.path setup
|
||||
-- ════════════════════════════════════════════════════════════════════════════
|
||||
--- Lua 5.3 compatible
|
||||
|
||||
-- Bootstrap: same as entry scripts. See `ps1_meta.lua` for the rationale.
|
||||
dofile((arg[0]:match("(.*[/\\])") or "./") .. "duffle_paths.lua")
|
||||
-- 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.
|
||||
local _src = debug.getinfo(1, "S").source:sub(2)
|
||||
local _dir = _src:match("(.*[/\\])") or "./"
|
||||
dofile(_dir .. "../duffle_paths.lua")
|
||||
local duffle = require("duffle")
|
||||
local is_space = duffle.is_space
|
||||
local is_alpha = duffle.is_alpha
|
||||
@@ -162,7 +152,7 @@ local BYTE_COMMA = 44
|
||||
--- @field pragmas table -- reserved (currently always nil; legacy compat)
|
||||
|
||||
--- ════════════════════════════════════════════════════════════════════════════
|
||||
-- Hand-rolled split helpers (no regex patterns used)
|
||||
-- split helpers
|
||||
-- ════════════════════════════════════════════════════════════════════════════
|
||||
|
||||
--- Split a string at top-level commas. Used inside TAPE_ATOM_* macro
|
||||
@@ -170,7 +160,7 @@ local BYTE_COMMA = 44
|
||||
--- @param s string
|
||||
--- @return string[]
|
||||
local function split_csv_top(s)
|
||||
local tokens = {}
|
||||
local tokens = {}
|
||||
local pos = 1
|
||||
local chunk_a = 1
|
||||
local depth = 0
|
||||
@@ -179,13 +169,13 @@ local function split_csv_top(s)
|
||||
local ch = s:byte(pos)
|
||||
if ch == BYTE_OPEN_PAREN or ch == BYTE_OPEN_BRACE or ch == BYTE_OPEN_BRACK then
|
||||
depth = depth + 1
|
||||
pos = pos + 1
|
||||
pos = pos + 1
|
||||
elseif ch == BYTE_CLOSE_PAREN or ch == BYTE_CLOSE_BRACE or ch == BYTE_CLOSE_BRACK then
|
||||
depth = depth - 1
|
||||
pos = pos + 1
|
||||
pos = pos + 1
|
||||
elseif ch == BYTE_COMMA and depth == 0 then
|
||||
tokens[#tokens + 1] = s:sub(chunk_a, pos - 1)
|
||||
pos = pos + 1
|
||||
pos = pos + 1
|
||||
chunk_a = pos
|
||||
else
|
||||
pos = pos + 1
|
||||
@@ -197,7 +187,6 @@ local function split_csv_top(s)
|
||||
end
|
||||
|
||||
--- Split a string into whitespace-separated tokens.
|
||||
--- Hand-rolled (no regex patterns).
|
||||
--- @param s string
|
||||
--- @return string[]
|
||||
local function split_ws(s)
|
||||
@@ -222,11 +211,9 @@ end
|
||||
-- Parse TAPE_ATOM_ANNOT(...) calls
|
||||
-- ════════════════════════════════════════════════════════════════════════════
|
||||
|
||||
-- Recognize a `atom_bind(...)`, `atom_reads(...)`, or `atom_writes(...)`
|
||||
-- sub-call embedded inside an atom_info arg list. Returns the kind
|
||||
-- ("atom_bind" / "atom_reads" / "atom_writes") and the inner content,
|
||||
-- or nil if the token isn't a recognized sub-call form. Flattened via
|
||||
-- a prefix lookup instead of a nested if/elseif chain.
|
||||
-- Recognize a `atom_bind(...)`, `atom_reads(...)`, or `atom_writes(...)` sub-call embedded inside an atom_info arg list.
|
||||
-- Returns the kind ("atom_bind" / "atom_reads" / "atom_writes") and the inner content, or nil if the token isn't a recognized sub-call form.
|
||||
-- Flattened via a prefix lookup instead of a nested if/elseif chain.
|
||||
local REGS_CALL_PREFIX = {
|
||||
["atom_writes("] = { kind = "atom_writes", inner_offset = 13 },
|
||||
["atom_reads("] = { kind = "atom_reads", inner_offset = 12 },
|
||||
@@ -235,18 +222,14 @@ local REGS_CALL_PREFIX = {
|
||||
|
||||
local function parse_regs_call(s)
|
||||
if s:sub(-1) ~= ")" then return nil end
|
||||
-- Try longest prefix first so "atom_writes(" wins over "atom_reads("
|
||||
-- when both 12-char prefixes would otherwise match. Lengths:
|
||||
-- Try longest prefix first so "atom_writes(" wins over "atom_reads(" when both 12-char prefixes would otherwise match.
|
||||
-- Lengths:
|
||||
-- atom_writes( = 12 chars, offset 13
|
||||
-- atom_reads( = 11 chars, offset 12
|
||||
-- atom_bind( = 10 chars, offset 11
|
||||
local spec = REGS_CALL_PREFIX[s:sub(1, 12)]
|
||||
if not spec then
|
||||
spec = REGS_CALL_PREFIX[s:sub(1, 11)]
|
||||
end
|
||||
if not spec then
|
||||
spec = REGS_CALL_PREFIX[s:sub(1, 10)]
|
||||
end
|
||||
if not spec then spec = REGS_CALL_PREFIX[s:sub(1, 11)] end
|
||||
if not spec then spec = REGS_CALL_PREFIX[s:sub(1, 10)] end
|
||||
if not spec then return nil end
|
||||
local inner = s:sub(spec.inner_offset, -2)
|
||||
if spec.single_ident then
|
||||
@@ -257,26 +240,21 @@ local function parse_regs_call(s)
|
||||
end
|
||||
|
||||
-- Resolve any phase_* / R_* alias macros in a register list.
|
||||
-- (Phase / region / cadence aliases have been dropped. Kept as an
|
||||
-- identity function so callers can stay uniform.)
|
||||
local function resolve_reg_aliases(regs)
|
||||
return regs
|
||||
end
|
||||
-- (Phase / region / cadence aliases have been dropped. Kept as an identity function so callers can stay uniform.)
|
||||
local function resolve_reg_aliases(regs) return regs end
|
||||
|
||||
-- Parse a comma-separated inner content (e.g. inside atom_reads(...))
|
||||
-- into a list of trimmed identifiers with aliases resolved.
|
||||
-- Parse a comma-separated inner content (e.g. inside atom_reads(...)) into a list of trimmed identifiers with aliases resolved.
|
||||
local function parse_regs_list(inner)
|
||||
local out = {}
|
||||
for _, r in ipairs(split_csv_top(inner)) do
|
||||
local trimmed = trim(r)
|
||||
if trimmed ~= "" then out[#out + 1] = trimmed end
|
||||
if trimmed ~= "" then out[#out + 1] = trimmed end
|
||||
end
|
||||
return resolve_reg_aliases(out)
|
||||
end
|
||||
|
||||
-- Parse a single token (from split_csv_top) into an arg entry.
|
||||
-- Three forms: register-list call, bare identifier,
|
||||
-- "other" (preserved as text).
|
||||
-- Three forms: register-list call, bare identifier, "other" (preserved as text).
|
||||
local function parse_arg_token(s)
|
||||
local kind, inner = parse_regs_call(s)
|
||||
if kind then
|
||||
@@ -292,8 +270,8 @@ local function parse_arg_token(s)
|
||||
return { kind = "other", value = s }
|
||||
end
|
||||
|
||||
--- Extract identifier args from a parenthesized group. Returns a list
|
||||
--- of {kind, value} pairs where kind is one of:
|
||||
--- Extract identifier args from a parenthesized group.
|
||||
--- Returns a list of {kind, value} pairs where kind is one of:
|
||||
--- "ident" -- a bare identifier (e.g. phase_work)
|
||||
--- "atom_reads" -- an atom_reads(...) call: value is the register list
|
||||
--- "atom_writes" -- an atom_writes(...) call: value is the register list
|
||||
@@ -338,6 +316,7 @@ local function parse_pragma_operator(source, ident_pos, after_ident)
|
||||
return nil, open_paren + 1
|
||||
end
|
||||
local str, str_end = read_parens(source, open_paren)
|
||||
-- scan: _Pragma(<string>)
|
||||
str = trim(str)
|
||||
if str:sub(1, 1) ~= '"' or str:sub(-1) ~= '"' then
|
||||
return nil, str_end
|
||||
@@ -372,6 +351,7 @@ local function parse_pragma_directive(source, ident_pos, after_ident)
|
||||
eol = eol + 1
|
||||
end
|
||||
local line_text = trim(source:sub(rest_start, eol - 1))
|
||||
-- scan: #pragma <mac_name> tape_atom words=<N>
|
||||
local tokens = split_ws(line_text)
|
||||
local entry
|
||||
if #tokens >= 3 and tokens[2] == "tape_atom" and tokens[3]:sub(1, WORDS_KEY_PREFIX_LEN) == WORDS_KEY_PREFIX then
|
||||
@@ -387,7 +367,7 @@ local function parse_pragma_directive(source, ident_pos, after_ident)
|
||||
end
|
||||
if entry then
|
||||
local line_of = duffle.LineIndex(source)
|
||||
entry.line = line_of(ident_pos)
|
||||
entry.line = line_of(ident_pos)
|
||||
end
|
||||
return entry, eol
|
||||
end
|
||||
@@ -410,24 +390,27 @@ local function find_macro_word_annotations(source)
|
||||
if source:byte(pos) == 35 then -- '#'
|
||||
pos = skip_preprocessor_line(source, pos)
|
||||
else
|
||||
local ident, after_ident = read_ident(source, pos)
|
||||
if not ident then
|
||||
pos = pos + 1
|
||||
elseif ident == PRAGMA_OPERATOR then
|
||||
local entry, new_pos = parse_pragma_operator(source, pos, after_ident)
|
||||
if entry then
|
||||
local line_of = duffle.LineIndex(source)
|
||||
entry.line = line_of(pos)
|
||||
out[#out + 1] = entry
|
||||
end
|
||||
pos = new_pos
|
||||
elseif ident == PRAGMA_IDENT then
|
||||
local entry, new_pos = parse_pragma_directive(source, pos, after_ident)
|
||||
if entry then out[#out + 1] = entry end
|
||||
pos = new_pos
|
||||
else
|
||||
pos = after_ident
|
||||
local ident, after_ident = read_ident(source, pos)
|
||||
-- scan: <ident>
|
||||
if not ident then
|
||||
pos = pos + 1
|
||||
elseif ident == PRAGMA_OPERATOR then
|
||||
-- scan: _Pragma(...)
|
||||
local entry, new_pos = parse_pragma_operator(source, pos, after_ident)
|
||||
if entry then
|
||||
local line_of = duffle.LineIndex(source)
|
||||
entry.line = line_of(pos)
|
||||
out[#out + 1] = entry
|
||||
end
|
||||
pos = new_pos
|
||||
elseif ident == PRAGMA_IDENT then
|
||||
-- scan: #pragma <mac_name> tape_atom words=<N>
|
||||
local entry, new_pos = parse_pragma_directive(source, pos, after_ident)
|
||||
if entry then out[#out + 1] = entry end
|
||||
pos = new_pos
|
||||
else
|
||||
pos = after_ident
|
||||
end
|
||||
end
|
||||
end
|
||||
return out
|
||||
@@ -470,17 +453,17 @@ local function parse_binds_body(body)
|
||||
end
|
||||
|
||||
--- Try to parse a `typedef Struct_(Binds_X) { ... };` declaration.
|
||||
--- Returns the parsed BindsStruct (if the form matched) and the new
|
||||
--- source position. If the form didn't match, returns nil + a position
|
||||
--- to continue scanning from.
|
||||
--- Returns the parsed BindsStruct (if the form matched) and the new source position.
|
||||
--- If the form didn't match, returns nil + a position to continue scanning from.
|
||||
--- @param source string
|
||||
--- @param ident_pos integer -- position of the `typedef` ident start
|
||||
--- @param after_typedef integer -- position just past `typedef`
|
||||
--- @param line_of fun(pos: integer): integer
|
||||
--- @return BindsStruct|nil, integer
|
||||
local function parse_typedef_binds(source, ident_pos, after_typedef, line_of)
|
||||
local after_type = skip_ws_and_cmt(source, after_typedef)
|
||||
local after_type = skip_ws_and_cmt(source, after_typedef)
|
||||
local type_ident, after_type_ident = read_ident(source, after_type)
|
||||
-- scan: typedef <type_ident>
|
||||
if type_ident ~= STRUCT_TYPE then
|
||||
return nil, after_type_ident or (after_type + 1)
|
||||
end
|
||||
@@ -491,13 +474,16 @@ local function parse_typedef_binds(source, ident_pos, after_typedef, line_of)
|
||||
end
|
||||
|
||||
local inner, after_paren = read_parens(source, open_paren)
|
||||
local name = trim(inner)
|
||||
-- scan: typedef Struct_(<name>)
|
||||
local name = trim(inner)
|
||||
|
||||
local brace = scan_to_char(source, "{", after_paren)
|
||||
-- scan: typedef Struct_(<name>) {
|
||||
if not brace then return nil, open_paren + 1 end
|
||||
|
||||
local body, after_brace = read_braces(source, brace)
|
||||
local fields, bytes = parse_binds_body(body)
|
||||
-- scan: typedef Struct_(<name>) { <fields> }
|
||||
local fields, bytes = parse_binds_body(body)
|
||||
|
||||
-- Only emit Binds_* structs (other Struct_ typedefs are ignored).
|
||||
if name:sub(1, BINDS_PREFIX_LEN) ~= BINDS_PREFIX then
|
||||
@@ -527,16 +513,18 @@ local function find_binds_structs(source)
|
||||
if source:byte(pos) == 35 then -- '#'
|
||||
pos = skip_preprocessor_line(source, pos)
|
||||
else
|
||||
local ident, after_ident = read_ident(source, pos)
|
||||
if not ident then
|
||||
pos = pos + 1
|
||||
elseif ident == "typedef" then
|
||||
local binds_struct, new_pos = parse_typedef_binds(source, pos, after_ident, line_of)
|
||||
if binds_struct then out[#out + 1] = binds_struct end
|
||||
pos = new_pos
|
||||
else
|
||||
pos = after_ident
|
||||
end
|
||||
local ident, after_ident = read_ident(source, pos)
|
||||
-- scan: <ident>
|
||||
if not ident then
|
||||
pos = pos + 1
|
||||
elseif ident == "typedef" then
|
||||
-- scan: typedef Struct_(<name>) { <fields> }
|
||||
local binds_struct, new_pos = parse_typedef_binds(source, pos, after_ident, line_of)
|
||||
if binds_struct then out[#out + 1] = binds_struct end
|
||||
pos = new_pos
|
||||
else
|
||||
pos = after_ident
|
||||
end
|
||||
end
|
||||
end
|
||||
return out
|
||||
@@ -546,24 +534,21 @@ end
|
||||
-- Find every MipsAtom_(name) { ... } declaration in source
|
||||
-- ════════════════════════════════════════════════════════════════════════════
|
||||
|
||||
--- Read the next identifier token from `s` starting at `pos`, where the
|
||||
--- identifier is a contiguous run of `[a-zA-Z0-9_]` characters (no
|
||||
--- underscore-starting alpha-only constraint). Returns the ident + the
|
||||
--- position just past it, or nil + pos if no identifier starts there.
|
||||
--- Read the next identifier token from `s` starting at `pos`, where the identifier is a contiguous run of `[a-zA-Z0-9_]`
|
||||
--- characters (no underscore-starting alpha-only constraint).
|
||||
--- Returns the ident + the position just past it, or nil + pos if no identifier starts there.
|
||||
--- @param s string
|
||||
--- @param pos integer
|
||||
--- @return string|nil, integer
|
||||
local function read_alnum_ident(s, pos)
|
||||
local str_len = #s
|
||||
while pos <= str_len and is_space(s:sub(pos, pos)) do pos = pos + 1 end
|
||||
local start = pos
|
||||
while pos <= str_len and is_alnum(s:sub(pos, pos)) do pos = pos + 1 end
|
||||
local str_len = #s; while pos <= str_len and is_space(s:sub(pos, pos)) do pos = pos + 1 end
|
||||
local start = pos; while pos <= str_len and is_alnum(s:sub(pos, pos)) do pos = pos + 1 end
|
||||
if pos == start then return nil, pos end
|
||||
return s:sub(start, pos - 1), pos
|
||||
end
|
||||
|
||||
--- Find every `MipsAtom_(name)` declaration in source. (Just the name +
|
||||
--- source line; the body is parsed separately by `parse_mips_atom`.)
|
||||
--- Find every `MipsAtom_(name)` declaration in source.
|
||||
--- (Just the name + source line; the body is parsed separately by `parse_mips_atom`.)
|
||||
--- @param source string
|
||||
--- @return Atom[]
|
||||
local function find_atom_names(source)
|
||||
@@ -576,6 +561,7 @@ local function find_atom_names(source)
|
||||
if pos > str_len then break end
|
||||
|
||||
local ident, after_ident = read_ident(source, pos)
|
||||
-- scan: <ident>
|
||||
if not ident then
|
||||
pos = pos + 1
|
||||
elseif ident ~= ATOM_DECL then
|
||||
@@ -586,14 +572,17 @@ local function find_atom_names(source)
|
||||
pos = open_paren + 1
|
||||
else
|
||||
local inner, after_paren = read_parens(source, open_paren)
|
||||
local name, _ = read_alnum_ident(inner, 1)
|
||||
-- scan: MipsAtom_(<name>)
|
||||
local name, _ = read_alnum_ident(inner, 1)
|
||||
if name and name ~= "" then
|
||||
out[#out + 1] = { line = line_of(pos), name = name }
|
||||
end
|
||||
local brace = scan_to_char(source, "{", after_paren)
|
||||
-- scan: MipsAtom_(<name>) {
|
||||
if brace then
|
||||
local _, after_brace = read_braces(source, brace)
|
||||
pos = after_brace
|
||||
-- scan: MipsAtom_(<name>) { <body> }
|
||||
pos = after_brace
|
||||
else
|
||||
pos = open_paren + 1
|
||||
end
|
||||
@@ -609,39 +598,24 @@ end
|
||||
-- ════════════════════════════════════════════════════════════════════════════
|
||||
|
||||
--- True iff the parsed arg is a register-list call (any recognized form).
|
||||
local function is_regs_arg(a)
|
||||
return a and (a.kind == "atom_reads" or a.kind == "atom_writes" or a.kind == "regs")
|
||||
end
|
||||
local function is_regs_arg(a) return a and (a.kind == "atom_reads" or a.kind == "atom_writes" or a.kind == "regs") end
|
||||
|
||||
--- Per-macro arg-shape handlers. Each takes (entry, args) and mutates
|
||||
--- Per-atom_info sub-call dispatch. Each takes (entry, args) and mutates
|
||||
--- entry.{reads, writes, binds, errors}. The new annotation shape is:
|
||||
---
|
||||
--- MipsAtom_(name) atom_info(
|
||||
--- atom_bind(Binds_X)
|
||||
--- , atom_reads(...)
|
||||
--- , atom_writes(...)
|
||||
--- ) { ... };
|
||||
---
|
||||
--- All sub-calls are order-independent; each is dispatched on its
|
||||
--- `kind` (atom_bind / atom_reads / atom_writes) when parsed.
|
||||
--- Per-macro arg-shape handlers. Each takes (entry, args) and mutates Per-atom_info sub-call dispatch.
|
||||
--- Each takes (entry, args) and mutates entry.{reads, writes, binds, errors}.
|
||||
--- All sub-calls are order-independent; each is dispatched on its `kind` (atom_bind / atom_reads / atom_writes) when parsed.
|
||||
local ANNOT_ARG_HANDLERS = {}
|
||||
|
||||
-- atom_info(atom_bind(Binds_X), atom_reads(...), atom_writes(...))
|
||||
function ANNOT_ARG_HANDLERS.info(entry, args)
|
||||
for _, arg in ipairs(args) do
|
||||
if arg.kind == "atom_bind" then
|
||||
entry.binds = arg.value
|
||||
elseif arg.kind == "atom_reads" then
|
||||
entry.reads = arg.value
|
||||
elseif arg.kind == "atom_writes" then
|
||||
entry.writes = arg.value
|
||||
elseif arg.kind == "ident" then
|
||||
if arg.kind == "atom_bind" then entry.binds = arg.value
|
||||
elseif arg.kind == "atom_reads" then entry.reads = arg.value
|
||||
elseif arg.kind == "atom_writes" then entry.writes = arg.value
|
||||
elseif arg.kind == "ident" then
|
||||
-- Reserved for future phase tokens. Currently ignored.
|
||||
-- (Could be reintroduced as `phase_*` sub-calls of atom_info.)
|
||||
else
|
||||
entry.errors[#entry.errors + 1] = string.format(
|
||||
"unexpected atom_info arg kind=%s value=%s", arg.kind, tostring(arg.value))
|
||||
entry.errors[#entry.errors + 1] = string.format("unexpected atom_info arg kind=%s value=%s", arg.kind, tostring(arg.value))
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -660,35 +634,34 @@ local function new_annot_entry(line, ident, name, kind)
|
||||
}
|
||||
end
|
||||
|
||||
--- Try to parse an `atom_info(...)` call right after the `MipsAtom_(name)`
|
||||
--- parens. Returns the annotation entry (if present) and the new source
|
||||
--- position past the atom_info call. Returns nil if no atom_info follows.
|
||||
--- Try to parse an `atom_info(...)` call right after the `MipsAtom_(name)` parens.
|
||||
--- Returns the annotation entry (if present) and the new source position past the atom_info call.
|
||||
--- Returns nil if no atom_info follows.
|
||||
--- @param source string
|
||||
--- @param atom_name string
|
||||
--- @param after_mipsatom_paren integer -- position past the MipsAtom_(...) close paren
|
||||
--- @param line_of fun(pos: integer): integer
|
||||
--- @return AtomAnnotation|nil, integer -- (entry or nil, new source position)
|
||||
local function parse_atom_info_call(source, atom_name, after_mipsatom_paren, line_of)
|
||||
local lookahead = skip_ws_and_cmt(source, after_mipsatom_paren)
|
||||
local lookahead = skip_ws_and_cmt(source, after_mipsatom_paren)
|
||||
local look_ident, look_after = read_ident(source, lookahead)
|
||||
-- scan: MipsAtom_(<name>) <look_ident>
|
||||
if look_ident ~= ATOM_INFO then return nil, after_mipsatom_paren end
|
||||
|
||||
local info_open = skip_ws_and_cmt(source, look_after)
|
||||
if source:byte(info_open) ~= BYTE_OPEN_PAREN then
|
||||
return nil, info_open + 1
|
||||
end
|
||||
if source:byte(info_open) ~= BYTE_OPEN_PAREN then return nil, info_open + 1 end
|
||||
|
||||
local info_inner, info_after = read_parens(source, info_open)
|
||||
local args = parse_atom_annot_args(info_inner)
|
||||
local entry = new_annot_entry(line_of(lookahead), ATOM_INFO, atom_name, "info")
|
||||
-- scan: MipsAtom_(<name>) atom_info(<binds>, <reads>, <writes>)
|
||||
local args = parse_atom_annot_args(info_inner)
|
||||
local entry = new_annot_entry(line_of(lookahead), ATOM_INFO, atom_name, "info")
|
||||
ANNOT_ARG_HANDLERS.info(entry, args)
|
||||
return entry, info_after
|
||||
end
|
||||
|
||||
--- Find every `MipsAtom_(name) atom_info(...) { ... };` annotation in source.
|
||||
--- Returns a list of annotation entries. Atoms without a following
|
||||
--- `atom_info(...)` call produce NO entry (atoms without annotations are
|
||||
--- valid in the new minimal shape).
|
||||
--- Returns a list of annotation entries. Atoms without a following `atom_info(...)` call produce NO entry
|
||||
--- (atoms without annotations are valid in the new minimal shape).
|
||||
--- @param source string
|
||||
--- @return AtomAnnotation[]
|
||||
local function find_atom_annotations(source)
|
||||
@@ -705,6 +678,7 @@ local function find_atom_annotations(source)
|
||||
pos = skip_preprocessor_line(source, pos)
|
||||
else
|
||||
local ident, after_ident = read_ident(source, pos)
|
||||
-- scan: <ident>
|
||||
if not ident then
|
||||
pos = pos + 1
|
||||
elseif ident == ATOM_DECL then
|
||||
@@ -713,16 +687,20 @@ local function find_atom_annotations(source)
|
||||
pos = open_paren + 1
|
||||
else
|
||||
local inner, after_paren = read_parens(source, open_paren)
|
||||
-- scan: MipsAtom_(<name>)
|
||||
local name, _ = read_alnum_ident(inner, 1)
|
||||
|
||||
local entry, new_pos = parse_atom_info_call(source, name, after_paren, line_of)
|
||||
-- scan: MipsAtom_(<name>) atom_info(<binds>, <reads>, <writes>)
|
||||
if entry then annots[#annots + 1] = entry end
|
||||
pos = new_pos
|
||||
|
||||
-- Skip past the body { ... } if present.
|
||||
local brace = scan_to_char(source, "{", pos)
|
||||
-- scan: MipsAtom_(<name>) atom_info(...) {
|
||||
if brace then
|
||||
local _, after_brace = read_braces(source, brace)
|
||||
-- scan: MipsAtom_(<name>) atom_info(...) { <body> }
|
||||
pos = after_brace
|
||||
end
|
||||
end
|
||||
@@ -741,10 +719,10 @@ end
|
||||
local function validate(ctx, src)
|
||||
local source = src.text
|
||||
|
||||
local annots = find_atom_annotations(source)
|
||||
local macros = find_macro_word_annotations(source)
|
||||
local binds = find_binds_structs(source)
|
||||
local atoms = find_atom_names(source)
|
||||
local annots = find_atom_annotations(source)
|
||||
local macros = find_macro_word_annotations(source)
|
||||
local binds = find_binds_structs(source)
|
||||
local atoms = find_atom_names(source)
|
||||
|
||||
local atom_index = {}
|
||||
for _, a in ipairs(atoms) do atom_index[a.name] = a end
|
||||
@@ -773,8 +751,8 @@ local function validate(ctx, src)
|
||||
end
|
||||
end
|
||||
|
||||
-- 2. Every atom may have AT MOST ONE annotation (no duplicates).
|
||||
-- (Atoms with ZERO annotations are valid in the new minimal shape.)
|
||||
-- 2. Every atom may have AT MOST ONE annotation (no duplicates).
|
||||
-- (Atoms with ZERO annotations are valid in the new minimal shape.)
|
||||
local count_per_atom = {}
|
||||
for _, a in ipairs(annots) do
|
||||
if a.name and not a.error then
|
||||
@@ -790,21 +768,17 @@ local function validate(ctx, src)
|
||||
end
|
||||
end
|
||||
|
||||
-- 3. (Phase validity check DROPPED. Phases were removed from the
|
||||
-- annotation DSL. They may be reintroduced later as sub-calls of
|
||||
-- atom_info, at which point ordering checks will go here.)
|
||||
-- 3. (Phase validity check DROPPED. Phases were removed from the annotation DSL.
|
||||
-- They may be reintroduced later as sub-calls of atom_info, at which point ordering checks will go here.)
|
||||
|
||||
-- 4. BIND atoms must reference a real Binds_* struct.
|
||||
for _, a in ipairs(annots) do
|
||||
if a.binds then
|
||||
if not binds_index[a.binds] then
|
||||
-- Demoted from error to warning (2026-07-10): the same
|
||||
-- condition is now caught by passes/static_analysis.lua's
|
||||
-- check_abi_handoff() as an error. Emitting a warning
|
||||
-- here keeps the annotation pass from being stop-on-error
|
||||
-- for the common test-fixture case, while still surfacing
|
||||
-- the issue in the report. The static-analysis report
|
||||
-- remains the source of truth for build-stopping errors.
|
||||
-- Demoted from error to warning (2026-07-10): the same condition is now caught by passes/static_analysis.lua's
|
||||
-- check_abi_handoff() as an error. Emitting a warning here keeps the annotation pass from being stop-on-error
|
||||
-- for the common test-fixture case, while still surfacing the issue in the report.
|
||||
-- The static-analysis report remains the source of truth for build-stopping errors.
|
||||
warnings[#warnings + 1] = {
|
||||
line = a.line,
|
||||
msg = string.format("'%s' binds '%s' but no Struct_(%s) { ... } declaration found (also flagged as an error by check_abi_handoff in the static-analysis pass)", a.name, a.binds, a.binds),
|
||||
@@ -880,15 +854,7 @@ local function validate(ctx, src)
|
||||
check_macro_drift(m, ctx.shared.word_counts[m.name])
|
||||
end
|
||||
|
||||
-- 8. (atom_<...> _Pragma validation DROPPED. The pragma macros
|
||||
-- atom_resource / atom_region / atom_group / atom_cadence /
|
||||
-- atom_async were removed from atom_dsl.h. They may be
|
||||
-- reintroduced later as sub-calls of atom_info.)
|
||||
|
||||
-- 9. (CADENCE_ONDEMAND requires async check DROPPED. Same reason
|
||||
-- as #8.)
|
||||
|
||||
-- 10. Information summary.
|
||||
-- 8. Information summary.
|
||||
info[#info + 1] = {
|
||||
line = 0,
|
||||
msg = string.format("scanned: %d atom(s), %d annotation(s), %d macro-word-decl(s), %d binds struct(s)",
|
||||
@@ -911,16 +877,13 @@ end
|
||||
-- Per-DIRECTORY (per-module) output: errors.h + annotations.txt
|
||||
-- ════════════════════════════════════════════════════════════════════════════
|
||||
--
|
||||
-- Per-source reports were the old behavior; each source in the same
|
||||
-- directory produced its own <basename>.errors.h + <basename>.annotations.txt,
|
||||
-- which flooded build/gen/ with one report per header. The new behavior
|
||||
-- aggregates per-DIRECTORY (one errors.h + one annotations.txt per module
|
||||
-- basename). Directories with zero atoms/annotations are skipped (no
|
||||
-- file emitted).
|
||||
-- Per-source reports were the old behavior; each source in the same directory produced its own <basename>.errors.h + <basename>.annotations.txt,
|
||||
-- which flooded build/gen/ with one report per header.
|
||||
-- Aggregates per-DIRECTORY (one errors.h + one annotations.txt per module basename).
|
||||
-- Directories with zero atoms/annotations are skipped (no file emitted).
|
||||
|
||||
--- 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.
|
||||
--- 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 ctx.dry_run then return nil end
|
||||
if atoms_count == 0 and #errors == 0 then
|
||||
@@ -928,7 +891,7 @@ local function emit_module_errors_h(ctx, dir_basename, atoms_count, errors, sour
|
||||
return nil
|
||||
end
|
||||
local out_path = ctx.out_root .. "/" .. dir_basename .. ".errors.h"
|
||||
local lines = {
|
||||
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",
|
||||
@@ -973,10 +936,8 @@ end
|
||||
|
||||
local M = {}
|
||||
|
||||
-- Expose `validate` for downstream passes (e.g. report.lua) that need
|
||||
-- to re-render the per-source results into a per-MODULE report. Keeping
|
||||
-- it as a single shared function avoids the duplication that an
|
||||
-- earlier version of report.lua had.
|
||||
-- Expose `validate` for downstream passes (e.g. report.lua) that need to re-render the per-source results into a per-MODULE report.
|
||||
-- Keeping it as a single shared function avoids the duplication that an earlier version of report.lua had.
|
||||
M.validate = validate
|
||||
|
||||
--- @param ctx PassCtx
|
||||
@@ -986,11 +947,9 @@ function M.run(ctx)
|
||||
local errors = {}
|
||||
local warnings = {}
|
||||
|
||||
-- Per-DIRECTORY (per-module) aggregation. Group sources by `src.dir`,
|
||||
-- validate every source in the dir, then emit ONE errors.h per dir
|
||||
-- (skipping dirs with no atoms AND no errors). The actual
|
||||
-- annotations.txt is rendered by passes/report.lua from the stashed
|
||||
-- per-module results below.
|
||||
-- Per-DIRECTORY (per-module) aggregation. Group sources by `src.dir`, validate every source in the dir, then emit ONE errors.h per dir
|
||||
-- (skipping dirs with no atoms AND no errors).
|
||||
-- The actual annotations.txt is rendered by passes/report.lua from the stashed per-module results below.
|
||||
local by_dir = {}
|
||||
for _, src in ipairs(ctx.sources) do
|
||||
by_dir[src.dir] = by_dir[src.dir] or {}
|
||||
|
||||
+82
-122
@@ -1,19 +1,11 @@
|
||||
--- passes/components.lua — Component-macro header generator.
|
||||
---
|
||||
--- Walks every source for `MipsAtomComp_(ac_X) { body }` (and the
|
||||
--- function-form `MipsAtomComp_Proc_(ac_X, { body })`) declarations
|
||||
--- and emits a per-directory `<dir_basename>.macs.h` containing one
|
||||
--- `#define mac_X(sig) \` macro per component + `WORD_COUNT(mac_X, N)`
|
||||
--- Walks every source for `MipsAtomComp_(ac_X) { body }` (and the function-form `MipsAtomComp_Proc_(ac_X, { body })`) declarations and
|
||||
--- emits a per-directory `<dir_basename>.macs.h` containing one `#define mac_X(sig) \` macro per component + `WORD_COUNT(mac_X, N)`
|
||||
--- entries for downstream offset computation.
|
||||
---
|
||||
--- **Ported from** `tape_atom_annotation_pass.lua:604-1079`
|
||||
--- (`find_component_atoms`, `preceding_comment_block`,
|
||||
--- `extract_arg_names`, `convert_line_comments_to_block`,
|
||||
--- `compute_component_word_count`, `emit_component_macros_h`).
|
||||
---
|
||||
--- **Conventions**: tabs (1/level), EmmyLua annotations, no regex,
|
||||
--- Lua 5.3 compatible. See
|
||||
--- `C:\projects\Pikuma\ps1-ai\conductor\code_styleguides\lua.md`.
|
||||
--- Lua 5.3 compatible.
|
||||
|
||||
--- @class Component
|
||||
--- @field name string
|
||||
@@ -28,13 +20,14 @@
|
||||
-- Module-scope requires + package.path setup
|
||||
-- ════════════════════════════════════════════════════════════════════════════
|
||||
|
||||
-- Resolve `arg[0]` to an absolute-ish script directory so that
|
||||
-- `require("duffle")` resolves against `scripts/` regardless of CWD.
|
||||
-- Note: this boilerplate is duplicated in 6 other entry scripts; a
|
||||
-- Phase-6 extraction target (`duffle.setup_package_path()`).
|
||||
-- Bootstrap: see `ps1_meta.lua` for the rationale.
|
||||
dofile((arg[0]:match("(.*[/\\])") or "./") .. "duffle_paths.lua")
|
||||
local duffle = require("duffle")
|
||||
-- Resolve `arg[0]` to an absolute-ish script directory so that `require("duffle")` resolves against `scripts/` regardless of CWD.
|
||||
-- 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.
|
||||
local _src = debug.getinfo(1, "S").source:sub(2)
|
||||
local _dir = _src:match("(.*[/\\])") or "./"
|
||||
dofile(_dir .. "../duffle_paths.lua")
|
||||
local duffle = require("duffle")
|
||||
local word_count_eval = require("word_count_eval")
|
||||
|
||||
-- ════════════════════════════════════════════════════════════════════════════
|
||||
@@ -97,23 +90,19 @@ local GEN_SUBDIR = "gen"
|
||||
-- Local helpers (file I/O + path normalization)
|
||||
-- ════════════════════════════════════════════════════════════════════════════
|
||||
|
||||
-- Write content to disk in binary mode so LF line endings are preserved on
|
||||
-- Windows (text mode would convert LF -> CRLF, breaking byte-identical diffs
|
||||
-- against git-tracked gen/*.macs.h files which are stored as LF).
|
||||
-- Write content to disk in binary mode so LF line endings are preserved on Windows
|
||||
-- (text mode would convert LF -> CRLF, breaking byte-identical diffs against git-tracked gen/*.macs.h files which are stored as LF).
|
||||
-- @param path string
|
||||
-- @param content string
|
||||
local function write_file_lf(path, content)
|
||||
local f = io.open(path, "wb")
|
||||
if not f then error("Cannot write " .. path) end
|
||||
f:write(content)
|
||||
f:close()
|
||||
f:write(content); f:close()
|
||||
end
|
||||
|
||||
-- Convert a (possibly relative) path to an absolute Windows path. The
|
||||
-- pre-rework output's "// Source:" comment line used the absolute path
|
||||
-- (e.g. "C:\projects\Pikuma\ps1\code\duffle\lottes_tape.h"); if we want
|
||||
-- byte-identical output, we must normalize relative -> absolute before
|
||||
-- emitting that comment.
|
||||
-- Convert a (possibly relative) path to an absolute Windows path.
|
||||
-- The pre-rework output's "// Source:" comment line used the absolute path (e.g. "C:\projects\Pikuma\ps1\code\duffle\lottes_tape.h");
|
||||
-- If we want byte-identical output, we must normalize relative -> absolute before emitting that comment.
|
||||
-- @param path string
|
||||
-- @return string
|
||||
local function to_absolute_path(path)
|
||||
@@ -126,8 +115,7 @@ local function to_absolute_path(path)
|
||||
local cwd = p:read("*l")
|
||||
p:close()
|
||||
if not cwd then return path end
|
||||
-- Normalize forward slashes to backslashes (Windows convention) on
|
||||
-- both the cwd AND the relative path tail, so the join is uniform.
|
||||
-- Normalize forward slashes to backslashes (Windows convention) on both the cwd AND the relative path tail, so the join is uniform.
|
||||
cwd = cwd:gsub("/", "\\")
|
||||
local tail = (path:gsub("/", "\\"))
|
||||
return cwd .. "\\" .. tail
|
||||
@@ -135,10 +123,6 @@ end
|
||||
|
||||
local M = {}
|
||||
|
||||
-- ════════════════════════════════════════════════════════════════════════════
|
||||
-- Ported helpers (verbatim from tape_atom_annotation_pass.lua:604-1079)
|
||||
-- ════════════════════════════════════════════════════════════════════════════
|
||||
|
||||
-- ════════════════════════════════════════════════════════════════════════════
|
||||
-- Function-args extraction (precedes MipsAtomComp_Proc_ invocations)
|
||||
-- ════════════════════════════════════════════════════════════════════════════
|
||||
@@ -163,16 +147,13 @@ local function find_last_name_open_paren(source, name, before_pos)
|
||||
return last_idx
|
||||
end
|
||||
|
||||
--- Find the args of the function declaration that immediately precedes
|
||||
--- a `MipsAtomComp_Proc_` invocation of the given name. Returns the
|
||||
--- args string (e.g., `"U4 off, U4 code, U1 r, U1 g, U1 b"`) or nil
|
||||
--- if no function declaration is found.
|
||||
--- Find the args of the function declaration that immediately precedes a `MipsAtomComp_Proc_` invocation of the given name.
|
||||
--- Returns the args string (e.g., `"U4 off, U4 code, U1 r, U1 g, U1 b"`) or nil if no function declaration is found.
|
||||
---
|
||||
--- Convention: function form is
|
||||
--- `FI_ MipsAtom ac_X(args) MipsAtomComp_Proc_(ac_X, { body })`
|
||||
--- We find the LAST occurrence of `"ac_X("` before `before_pos` and
|
||||
--- extract the args from inside the parens. We then verify the
|
||||
--- preceding context ends with `MipsAtom` (the function-decl keyword
|
||||
--- We find the LAST occurrence of `"ac_X("` before `before_pos` and extract the args from inside the parens.
|
||||
--- We then verify the preceding context ends with `MipsAtom` (the function-decl keyword
|
||||
--- with possible qualifiers between).
|
||||
---
|
||||
--- @param source string
|
||||
@@ -183,8 +164,7 @@ local function find_function_args_for(source, name, before_pos)
|
||||
local last_idx = find_last_name_open_paren(source, name, before_pos)
|
||||
if not last_idx then return nil end
|
||||
|
||||
-- Verify the preceding context ends with "MipsAtom" (with
|
||||
-- possible qualifiers between).
|
||||
-- Verify the preceding context ends with "MipsAtom" (with possible qualifiers between).
|
||||
local before = source:sub(1, last_idx - 1)
|
||||
local trimmed = duffle.trim(before)
|
||||
if trimmed:sub(-#MIPS_ATOM) ~= MIPS_ATOM then
|
||||
@@ -193,7 +173,9 @@ local function find_function_args_for(source, name, before_pos)
|
||||
end
|
||||
|
||||
local open_paren = last_idx + #name -- position of "("
|
||||
-- scan: MipsAtom ac_X(
|
||||
local inner = duffle.read_parens(source, open_paren)
|
||||
-- scan: MipsAtom ac_X(<args>)
|
||||
if not inner then return nil end
|
||||
return inner
|
||||
end
|
||||
@@ -202,8 +184,7 @@ end
|
||||
-- Preceding-comment-block extraction
|
||||
-- ════════════════════════════════════════════════════════════════════════════
|
||||
|
||||
-- Skip whitespace (space/tab/newline/CR) backward from `pos`, returning
|
||||
-- the position of the first non-whitespace char.
|
||||
-- Skip whitespace (space/tab/newline/CR) backward from `pos`, returning the position of the first non-whitespace char.
|
||||
-- @param source string
|
||||
-- @param pos integer
|
||||
-- @return integer
|
||||
@@ -237,8 +218,7 @@ local function find_block_comment_open(source, close_pos)
|
||||
return open_at
|
||||
end
|
||||
|
||||
-- Walk back from `open_at` over leading spaces + tabs to include the
|
||||
-- indentation before the `/*` in the captured comment.
|
||||
-- Walk back from `open_at` over leading spaces + tabs to include the indentation before the `/*` in the captured comment.
|
||||
-- @param source string
|
||||
-- @param open_at integer
|
||||
-- @return integer
|
||||
@@ -255,8 +235,7 @@ local function extend_left_over_indent(source, open_at)
|
||||
return start
|
||||
end
|
||||
|
||||
-- Walk back from `line_end` to the start of the source line (the most
|
||||
-- recent `\n` or position 1).
|
||||
-- Walk back from `line_end` to the start of the source line (the most recent `\n` or position 1).
|
||||
-- @param source string
|
||||
-- @param line_end integer
|
||||
-- @return integer
|
||||
@@ -269,9 +248,8 @@ local function find_line_start(source, line_end)
|
||||
end
|
||||
|
||||
-- (internal) Capture one `/* ... */` block comment whose closing `*/`
|
||||
-- ends at `close_end_pos`. Returns (block_text, new_scan_pos) where
|
||||
-- `new_scan_pos` is where to continue scanning for more comments, or
|
||||
-- nil if no block comment was found.
|
||||
-- ends at `close_end_pos`. Returns (block_text, new_scan_pos) where `new_scan_pos`
|
||||
-- is where to continue scanning for more comments, or nil if no block comment was found.
|
||||
local function capture_block_comment(source, close_end_pos)
|
||||
local open_at = find_block_comment_open(source, close_end_pos)
|
||||
if not open_at then return nil end
|
||||
@@ -290,15 +268,11 @@ local function capture_line_comment(source, line_end_pos)
|
||||
return nil
|
||||
end
|
||||
|
||||
--- Find the contiguous comment block immediately preceding `pos` in
|
||||
--- `source`. Returns the comment text (with the `/* */` or `//` markers
|
||||
--- preserved) or an empty string if no comment is adjacent.
|
||||
--- Find the contiguous comment block immediately preceding `pos` in `source`.
|
||||
--- Returns the comment text (with the `/* */` or `//` markers preserved) or an empty string if no comment is adjacent.
|
||||
---
|
||||
--- Used to copy signature comments from the source declaration
|
||||
--- (`MipsAtomComp_` / `MipsAtomComp_Proc_` / function decl) over to the
|
||||
--- generated `mac_X` macro, so LSP/IntelliSense displays the args doc.
|
||||
---
|
||||
--- No regex (per the no_regex constraint).
|
||||
--- Used to copy signature comments from the source declaration (`MipsAtomComp_` / `MipsAtomComp_Proc_` / function decl)
|
||||
--- over to the generated `mac_X` macro, so LSP/IntelliSense displays the args doc.
|
||||
---
|
||||
--- @param source string
|
||||
--- @param pos integer
|
||||
@@ -335,8 +309,7 @@ end
|
||||
-- Argument-name extraction
|
||||
-- ════════════════════════════════════════════════════════════════════════════
|
||||
|
||||
-- Walk `trimmed` backward from `pos` over trailing whitespace /
|
||||
-- asterisks / brackets, returning the position of the first
|
||||
-- Walk `trimmed` backward from `pos` over trailing whitespace / asterisks / brackets, returning the position of the first
|
||||
-- non-trailer character (i.e. the end of the identifier).
|
||||
-- @param trimmed string
|
||||
-- @param pos integer
|
||||
@@ -372,8 +345,7 @@ local function trim_ident_back(trimmed, pos)
|
||||
return back
|
||||
end
|
||||
|
||||
--- Extract just the parameter NAMES from a function-args string
|
||||
--- (stripping type annotations). E.g.,
|
||||
--- Extract just the parameter NAMES from a function-args string (stripping type annotations). E.g.,
|
||||
--- `"U4 off, U4 code, U1 r, U1 g, U1 b"` -> `{"off", "code", "r", "g", "b"}`
|
||||
--- `"U4 *ptr"` -> `{"ptr"}`
|
||||
--- `""` -> nil
|
||||
@@ -403,9 +375,8 @@ end
|
||||
-- Component scanner (bare + function forms)
|
||||
-- ════════════════════════════════════════════════════════════════════════════
|
||||
|
||||
-- Parse the inner content of an `AtomComp_(name, ...)` call. Returns
|
||||
-- (name, body_or_nil) — `body_or_nil` is non-nil iff this is the
|
||||
-- function-form `MipsAtomComp_Proc_(name, { body })` invocation.
|
||||
-- Parse the inner content of an `AtomComp_(name, ...)` call.
|
||||
-- Returns (name, body_or_nil) — `body_or_nil` is non-nil iff this is the function-form `MipsAtomComp_Proc_(name, { body })` invocation.
|
||||
-- @param inner string -- the content between ( and ) of the AtomComp_ call
|
||||
--- @return string|nil, string|nil
|
||||
local function parse_atomcomp_inner(inner)
|
||||
@@ -428,8 +399,7 @@ local function parse_atomcomp_inner(inner)
|
||||
end
|
||||
|
||||
-- (internal) Try to extract a bare-form `MipsAtomComp_(ac_X)` declaration.
|
||||
-- Bare form: `MipsAtomComp_(ac_X) { body }` — body comes from the brace block
|
||||
-- AFTER the parens.
|
||||
-- Bare form: `MipsAtomComp_(ac_X) { body }` — body comes from the brace block AFTER the parens.
|
||||
-- @param source string
|
||||
-- @param name string -- the `ac_X` ident from the parens
|
||||
--- @param ident_pos integer -- position of the `MipsAtomComp_` ident start
|
||||
@@ -440,8 +410,10 @@ end
|
||||
--- @return Component|nil, integer -- the component + new source position
|
||||
local function make_bare_component(source, name, ident_pos, after_paren, line_of, args, comment)
|
||||
local brace = duffle.scan_to_char(source, "{", after_paren)
|
||||
-- scan: <ident>(<name>) {
|
||||
if not brace then return nil, after_paren + 1 end
|
||||
local body, after_brace = duffle.read_braces(source, brace)
|
||||
-- scan: <ident>(<name>) { <body> }
|
||||
return {
|
||||
line = line_of(ident_pos),
|
||||
name = name:sub(AC_PREFIX_LEN + 1), -- strip "ac_" prefix
|
||||
@@ -481,6 +453,7 @@ local function find_component_atoms(source)
|
||||
if pos > src_len then break end
|
||||
|
||||
local ident, after_ident = duffle.read_ident(source, pos)
|
||||
-- scan: <ident>
|
||||
local is_comp = ident == ATOM_COMP or ident == ATOM_COMP_PROC
|
||||
if not ident then
|
||||
pos = pos + 1
|
||||
@@ -492,7 +465,9 @@ local function find_component_atoms(source)
|
||||
pos = open_paren + 1
|
||||
else
|
||||
local inner, after_paren = duffle.read_parens(source, open_paren)
|
||||
-- scan: <ident>(<args>)
|
||||
local name, body = parse_atomcomp_inner(inner)
|
||||
-- scan: <ident>(<name>) OR <ident>(<name>, { <body> })
|
||||
if not name or name:sub(1, AC_PREFIX_LEN) ~= AC_PREFIX then
|
||||
pos = open_paren + 1
|
||||
else
|
||||
@@ -500,11 +475,14 @@ local function find_component_atoms(source)
|
||||
local comment = preceding_comment_block(source, pos)
|
||||
if body == nil then
|
||||
-- Bare form: body comes from the brace block after the parens.
|
||||
-- scan: <ident>(<name>) {
|
||||
local comp, new_pos = make_bare_component(source, name, pos, after_paren, line_of, args, comment)
|
||||
-- scan: <ident>(<name>) { <body> }
|
||||
if comp then out[#out + 1] = comp end
|
||||
pos = new_pos
|
||||
else
|
||||
-- Function form: body was inside the parens.
|
||||
-- scan: <ident>(<name>, { <body> })
|
||||
out[#out + 1] = make_proc_component(name, body, pos, line_of, args, comment)
|
||||
pos = after_paren
|
||||
end
|
||||
@@ -521,13 +499,11 @@ end
|
||||
|
||||
-- Convert `//` line comments to `/* */` block comments in a token.
|
||||
--
|
||||
-- C macros use `\` line-continuations; a `//` comment before `\` would
|
||||
-- consume the continuation, breaking the macro. We convert `//` to
|
||||
-- `/* */` so the multi-line macro structure is preserved.
|
||||
-- C macros use `\` line-continuations; a `//` comment before `\` would consume the continuation,
|
||||
-- breaking the macro. We convert `//` to `/* */` so the multi-line macro structure is preserved.
|
||||
--
|
||||
-- Skips `//` sequences that are inside string or character literals
|
||||
-- (a rough heuristic — sufficient for component bodies which don't
|
||||
-- have those constructs).
|
||||
-- (a rough heuristic — sufficient for component bodies which don't have those constructs).
|
||||
--
|
||||
--- @param s string
|
||||
--- @return string
|
||||
@@ -565,10 +541,8 @@ end
|
||||
-- Word-count computation (memoized recursive lookup)
|
||||
-- ════════════════════════════════════════════════════════════════════════════
|
||||
|
||||
-- Strip the `mac_` prefix from a component-call ident so we can look it
|
||||
-- up against the components-by-name table. Returns the ident unchanged
|
||||
-- if it doesn't start with the prefix (so a non-component ident like
|
||||
-- `mask_upper` falls through to the wc-table branch).
|
||||
-- Strip the `mac_` prefix from a component-call ident so we can look it up against the components-by-name table. Returns the ident unchanged
|
||||
-- if it doesn't start with the prefix (so a non-component ident like `mask_upper` falls through to the wc-table branch).
|
||||
-- @param ident string|nil
|
||||
-- @return string|nil
|
||||
local function strip_mac_prefix(ident)
|
||||
@@ -579,10 +553,9 @@ local function strip_mac_prefix(ident)
|
||||
return ident
|
||||
end
|
||||
|
||||
-- (internal) Recursive word-count lookup. `cache` is the memoization table
|
||||
-- across all calls to `compute_component_word_count`; the in-progress
|
||||
-- -1 sentinel detects cycles (A -> B -> A).
|
||||
-- @param name string -- the component name (without `mac_`)
|
||||
-- (internal) Recursive word-count lookup. `cache` is the memoization table across all calls to `compute_component_word_count`;
|
||||
-- the in-progress -1 sentinel detects cycles (A -> B -> A).
|
||||
-- @param name string -- the component name (without `mac_`)
|
||||
-- @param comp_by_name table<string, Component>
|
||||
-- @param wc table<string, integer>
|
||||
-- @param cache table<string, integer>
|
||||
@@ -618,17 +591,15 @@ local function word_count_rec(name, comp_by_name, wc, cache)
|
||||
return n
|
||||
end
|
||||
|
||||
--- Compute the word count of a component body, accounting for macro
|
||||
--- expansion. Each comma-separated entry in the body is a "slot" that
|
||||
--- contributes its own word count. For most entries (regular MIPS
|
||||
--- instructions) the count is 1. For `mac_Y(...)` calls, the count is
|
||||
--- the word count of `mac_Y` (recursive lookup through `components`).
|
||||
--- Compute the word count of a component body, accounting for macro expansion.
|
||||
--- Each comma-separated entry in the body is a "slot" that contributes its own word count.
|
||||
--- For most entries (regular MIPS instructions) the count is 1.
|
||||
--- For `mac_Y(...)` calls, the count is the word count of `mac_Y` (recursive lookup through `components`).
|
||||
--- For encoding macros with a known multi-word count (e.g. `mask_upper` = 2),
|
||||
--- the count is taken from `word_counts`.
|
||||
---
|
||||
--- The lookup is memoized via `word_count_rec` to avoid infinite recursion
|
||||
--- (e.g. if two components referenced each other). This is the same
|
||||
--- algorithm as the original `tape_atom_annotation_pass.lua` (commit 7d20a4d).
|
||||
--- The lookup is memoized via `word_count_rec` to avoid infinite recursion (e.g. if two components referenced each other).
|
||||
--- This is the same algorithm as the original `tape_atom_annotation_pass.lua` (commit 7d20a4d).
|
||||
---
|
||||
--- @param c Component
|
||||
--- @param components Component[]
|
||||
@@ -677,8 +648,7 @@ local function tokens_from_body(body)
|
||||
return out
|
||||
end
|
||||
|
||||
--- Determine the macro signature: function-args list (function form)
|
||||
--- or variadic-ignored (bare form).
|
||||
--- Determine the macro signature: function-args list (function form) or variadic-ignored (bare form).
|
||||
--- @param args_str string|nil
|
||||
--- @return string
|
||||
local function signature_from_args(args_str)
|
||||
@@ -689,8 +659,8 @@ local function signature_from_args(args_str)
|
||||
return "..."
|
||||
end
|
||||
|
||||
--- Strip the trailing `" \"` (space + backslash) line continuation
|
||||
--- from the last body line. The last 2 chars are always that pair.
|
||||
--- Strip the trailing `" \"` (space + backslash) line continuation from the last body line.
|
||||
--- The last 2 chars are always that pair.
|
||||
local function strip_trailing_continuation(lines)
|
||||
local last = lines[#lines]
|
||||
if last:sub(-2) == " \\" then
|
||||
@@ -698,9 +668,8 @@ local function strip_trailing_continuation(lines)
|
||||
end
|
||||
end
|
||||
|
||||
--- Emit the `#define mac_X(sig) \<newline>\t<tok1> \<newline>,\t<tok2> ...`
|
||||
--- block. Converts `//` line comments to `/* */` block comments in
|
||||
--- each token so they don't break the C macro `\` line continuations.
|
||||
--- Emit the `#define mac_X(sig) \<newline>\t<tok1> \<newline>,\t<tok2> ...` block.
|
||||
--- Converts `//` line comments to `/* */` block comments in each token so they don't break the C macro `\` line continuations.
|
||||
local function emit_macro_body(lines, c, sig, tokens)
|
||||
for tok_idx = 1, #tokens do
|
||||
tokens[tok_idx] = convert_line_comments_to_block(tokens[tok_idx])
|
||||
@@ -713,9 +682,8 @@ local function emit_macro_body(lines, c, sig, tokens)
|
||||
strip_trailing_continuation(lines)
|
||||
end
|
||||
|
||||
--- Build the list of lines for one component (signature comment,
|
||||
--- `#define mac_X(...)` line with backslash-continued tokens, then
|
||||
--- `WORD_COUNT(mac_X, N)` entry).
|
||||
--- Build the list of lines for one component
|
||||
--- (signature comment, `#define mac_X(...)` line with backslash-continued tokens, then `WORD_COUNT(mac_X, N)` entry).
|
||||
--- @param c Component
|
||||
--- @param components Component[]
|
||||
--- @param wc table<string, integer>
|
||||
@@ -748,17 +716,14 @@ end
|
||||
-- Per-source emit logic
|
||||
-- ════════════════════════════════════════════════════════════════════════════
|
||||
|
||||
-- Build the boilerplate header lines (the `#ifdef INTELLISENSE_DIRECTIVES`
|
||||
-- block, the `// Auto-generated` comment, the `// Source:` line, and the
|
||||
-- self-contained `WORD_COUNT` macro definition).
|
||||
-- Build the boilerplate header lines (the `#ifdef INTELLISENSE_DIRECTIVES` block,
|
||||
-- the `// Auto-generated` comment, the `// Source:` line, and the self-contained `WORD_COUNT` macro definition).
|
||||
-- @param src SourceFile
|
||||
-- @return string[]
|
||||
local function header_boilerplate(src)
|
||||
return {
|
||||
-- #pragma once wrapped in #ifdef INTELLISENSE_DIRECTIVES, matching
|
||||
-- the convention in lottes_tape.h. The build does manual unity
|
||||
-- includes (the user controls include order), so the pragma
|
||||
-- is only active for IDE/tooling.
|
||||
-- #pragma once wrapped in #ifdef INTELLISENSE_DIRECTIVES, matching the convention in lottes_tape.h.
|
||||
-- The build does manual unity includes (the user controls include order), so the pragma is only active for IDE/tooling.
|
||||
"#ifdef INTELLISENSE_DIRECTIVES",
|
||||
"#pragma once",
|
||||
"#endif",
|
||||
@@ -767,8 +732,7 @@ local function header_boilerplate(src)
|
||||
"// Component atoms (MipsAtomComp_(ac_*)) -> macro variants (mac_*)",
|
||||
"",
|
||||
-- Self-contained: define WORD_COUNT if not already defined.
|
||||
-- We use the same definition here so the auto-generated
|
||||
-- entries below expand to compile-time constants whether
|
||||
-- We use the same definition here so the auto-generated entries below expand to compile-time constants whether
|
||||
-- the metadata file is included first or not.
|
||||
"#ifndef WORD_COUNT",
|
||||
"#define WORD_COUNT(name, count) enum { words_##name = (count) };",
|
||||
@@ -778,10 +742,9 @@ local function header_boilerplate(src)
|
||||
end
|
||||
|
||||
-- Compute the output path for one source's `.macs.h` file.
|
||||
-- The pre-rework convention uses the *directory* basename (not the
|
||||
-- source file basename) — e.g. `code/duffle/lottes_tape.h` produces
|
||||
-- `code/duffle/gen/duffle.macs.h`. This matches what the C codebase
|
||||
-- #includes.
|
||||
-- The pre-rework convention uses the *directory* basename
|
||||
-- (not the source file basename) — e.g. `code/duffle/lottes_tape.h` produces `code/duffle/gen/duffle.macs.h`.
|
||||
-- This matches what the C codebase #includes.
|
||||
-- @param src SourceFile
|
||||
-- @return string -- the output directory
|
||||
-- @return string -- the full output path
|
||||
@@ -791,13 +754,10 @@ local function compute_macs_h_path(src)
|
||||
return out_dir, out_path
|
||||
end
|
||||
|
||||
--- Emit a per-source `.macs.h` header with the `mac_X` macros +
|
||||
--- `WORD_COUNT` entries. Writes in BINARY mode so LF line endings are
|
||||
--- preserved (the git blob is LF; Windows text-mode would emit CRLF and
|
||||
--- break the byte-identical diff).
|
||||
--- Emit a per-source `.macs.h` header with the `mac_X` macros + `WORD_COUNT` entries. Writes in BINARY mode so LF line endings are
|
||||
--- preserved (the git blob is LF; Windows text-mode would emit CRLF and break the byte-identical diff).
|
||||
---
|
||||
--- Honors `ctx.dry_run`: prints the intended path but does not write
|
||||
--- the file.
|
||||
--- Honors `ctx.dry_run`: prints the intended path but does not write the file.
|
||||
---
|
||||
--- @param ctx PassCtx
|
||||
--- @param src SourceFile
|
||||
@@ -807,7 +767,7 @@ local function emit_component_macros_h(ctx, src, components)
|
||||
if #components == 0 then return nil end
|
||||
|
||||
local out_dir, out_path = compute_macs_h_path(src)
|
||||
local lines = header_boilerplate(src)
|
||||
local lines = header_boilerplate(src)
|
||||
|
||||
local wc = ctx.shared.word_counts
|
||||
for _, c in ipairs(components) do
|
||||
@@ -833,8 +793,8 @@ end
|
||||
-- Pass entry
|
||||
-- ════════════════════════════════════════════════════════════════════════════
|
||||
|
||||
-- (internal) Extend `ctx.shared.word_counts` with this source's component
|
||||
-- macros so offsets sees them without re-reading the file.
|
||||
-- (internal) Extend `ctx.shared.word_counts` with this source's component macros
|
||||
-- so offsets sees them without re-reading the file.
|
||||
-- @param ctx PassCtx
|
||||
-- @param components Component[]
|
||||
local function update_shared_word_counts(ctx, components)
|
||||
|
||||
@@ -1,85 +0,0 @@
|
||||
--- duffle_paths.lua — Single-line bootstrap helper for the tape-atom
|
||||
--- Lua scripts.
|
||||
---
|
||||
--- Each entry script (ps1_meta.lua, word_count_eval.lua, and the 5
|
||||
--- passes/*.lua files) starts with:
|
||||
---
|
||||
--- ```lua
|
||||
--- local duffle = dofile((arg[0]:match("(.*[/\\])") or "./") .. "duffle_paths.lua")
|
||||
--- ```
|
||||
---
|
||||
--- That single line: (a) locates this helper via `arg[0]`, (b) loads
|
||||
--- it (which sets `package.path` + `package.cpath` via `git rev-parse`),
|
||||
--- (c) returns the `M` table (a wrapper around the setup function).
|
||||
--- After this line, `require("duffle")` and `require("passes.X")` both
|
||||
--- resolve normally.
|
||||
---
|
||||
--- **Why a helper instead of inline?**
|
||||
--- - The 8-line path-setup boilerplate was duplicated across 7 entry
|
||||
--- scripts (one per file). Single source of truth here.
|
||||
--- - Mirrors the build script's pattern in `build_psyq.ps1`:
|
||||
--- `$path_root = split-path -Path $PSScriptRoot -Parent;` then
|
||||
--- derive everything from there.
|
||||
---
|
||||
--- **Why `git rev-parse --show-toplevel`?**
|
||||
--- Hardcoding `C:\\projects\\Pikuma\\ps1\\...` breaks portability. Git
|
||||
--- gives us the canonical repo root regardless of where the repo lives
|
||||
--- on disk.
|
||||
|
||||
local M = {}
|
||||
|
||||
-- Cache key for the repo root. Stored in `package.loaded` (process-
|
||||
-- global) so all 8 entry scripts + passes scripts share one git call.
|
||||
-- Without this cache, `git rev-parse --show-toplevel` runs once per
|
||||
-- script load = 8 × ~150ms = 1.2s wasted per build on Windows.
|
||||
local CACHE_KEY = "__duffle_repo_root__"
|
||||
|
||||
--- Resolve the repo root via git (cached after first call).
|
||||
--- Returns a normalized path with a trailing forward-slash, or nil
|
||||
--- if not in a git repo.
|
||||
--- @return string|nil
|
||||
local function find_repo_root()
|
||||
if package.loaded[CACHE_KEY] then return package.loaded[CACHE_KEY] end
|
||||
local p = io.popen("git rev-parse --show-toplevel 2>nul")
|
||||
local root
|
||||
if p then
|
||||
root = p:read("*l")
|
||||
p:close()
|
||||
end
|
||||
if not root or root == "" then return nil end
|
||||
-- Normalize to forward slashes (Windows accepts both, but mixed
|
||||
-- `\` + `/` confuses LuaJIT's file APIs).
|
||||
root = root:gsub("\\", "/")
|
||||
if not root:match("/$") then root = root .. "/" end
|
||||
package.loaded[CACHE_KEY] = root
|
||||
return root
|
||||
end
|
||||
|
||||
--- Set `package.path` (for `require("duffle")` + `require("passes.X")`)
|
||||
--- and `package.cpath` (for `lpeg.dll` on Windows).
|
||||
--- Idempotent: safe to call multiple times (just re-sets the same paths).
|
||||
function M.setup()
|
||||
local repo_root = find_repo_root()
|
||||
if not repo_root then
|
||||
io.stderr:write("[duffle_paths] git rev-parse failed -- not in a git repo?\n")
|
||||
os.exit(2)
|
||||
end
|
||||
|
||||
local scripts_dir = repo_root .. "scripts/"
|
||||
local passes_dir = repo_root .. "scripts/passes/"
|
||||
package.path = scripts_dir .. "?.lua;"
|
||||
.. scripts_dir .. "?/init.lua;"
|
||||
.. passes_dir .. "?.lua;"
|
||||
.. passes_dir .. "?/init.lua;"
|
||||
.. package.path
|
||||
|
||||
if package.config:sub(1, 1) == "\\" then
|
||||
package.cpath = repo_root .. "toolchain/luajit-2.1/lib/lua/5.1/?.dll;"
|
||||
.. package.cpath
|
||||
end
|
||||
end
|
||||
|
||||
-- Run the setup as a side effect.
|
||||
M.setup()
|
||||
|
||||
return M
|
||||
+46
-43
@@ -1,28 +1,26 @@
|
||||
--- passes/offsets.lua — Branch-offset generator.
|
||||
---
|
||||
--- Scans every source for `MipsAtom_(name) { ... }` (and the raw
|
||||
--- `MipsCode code_<name> { ... }` form) declarations, computes the
|
||||
--- word offset from each `atom_offset(F, T)` marker to its target
|
||||
--- `atom_label(T)` declaration, and emits `<dir_basename>.offsets.h`
|
||||
--- with one `#define _atom_offset_F_T = N` per branch.
|
||||
--- Scans every source for `MipsAtom_(name) { ... }` (and the raw `MipsCode code_<name> { ... }` form) declarations,
|
||||
--- computes the word offset from each `atom_offset(F, T)` marker to its target `atom_label(T)` declaration,
|
||||
--- and emits `<dir_basename>.offsets.h` with one `#define _atom_offset_F_T = N` per branch.
|
||||
---
|
||||
--- The offset is `target_word - branch_word - 1` (the standard MIPS
|
||||
--- branch-immediate encoding: branch_offset = relative_pc_in_words - 1).
|
||||
--- The offset is `target_word - branch_word - 1` (the standard MIPS branch-immediate encoding: branch_offset = relative_pc_in_words - 1).
|
||||
---
|
||||
--- **Conventions**: tabs (1/level), EmmyLua annotations, no regex,
|
||||
--- Lua 5.3 compatible. See
|
||||
--- `C:\projects\Pikuma\ps1-ai\conductor\code_styleguides\lua.md`.
|
||||
--- Lua 5.3 compatible.
|
||||
|
||||
-- ════════════════════════════════════════════════════════════════════════════
|
||||
-- Module-scope requires + package.path setup
|
||||
-- ════════════════════════════════════════════════════════════════════════════
|
||||
|
||||
-- Resolve `arg[0]` to an absolute-ish script directory so that
|
||||
-- `require("duffle")` resolves against `scripts/` regardless of CWD.
|
||||
-- Note: this boilerplate is duplicated in 6 other entry scripts; a
|
||||
-- Phase-6 extraction target (`duffle.setup_package_path()`).
|
||||
-- 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.
|
||||
dofile((arg[0]:match("(.*[/\\])") or "./") .. "duffle_paths.lua")
|
||||
-- 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.
|
||||
local _src = debug.getinfo(1, "S").source:sub(2)
|
||||
local _dir = _src:match("(.*[/\\])") or "./"
|
||||
dofile(_dir .. "../duffle_paths.lua")
|
||||
local duffle = require("duffle")
|
||||
local word_count_eval = require("word_count_eval")
|
||||
local count_token_words = word_count_eval.count_token_words
|
||||
@@ -140,10 +138,8 @@ end
|
||||
-- Marker-call helpers
|
||||
-- ════════════════════════════════════════════════════════════════════════════
|
||||
|
||||
-- Extract comma-separated identifier args from a parenthesized group
|
||||
-- after a function-like macro call. Returns (args, after_paren) where
|
||||
-- `after_paren` is the position just past the closing `)`, or nil if
|
||||
-- `token` did not start with `(`.
|
||||
-- Extract comma-separated identifier args from a parenthesized group after a function-like macro call.
|
||||
-- Returns (args, after_paren) where `after_paren` is the position just past the closing `)`, or nil if `token` did not start with `(`.
|
||||
-- @param token string
|
||||
-- @param after_ident integer
|
||||
-- @return string[], integer|nil
|
||||
@@ -151,9 +147,10 @@ local function extract_ident_args(token, after_ident)
|
||||
local arg_start = duffle.skip_ws_and_cmt(token, after_ident)
|
||||
if token:sub(arg_start, arg_start) ~= "(" then return {}, nil end
|
||||
local inner, after_paren = duffle.read_parens(token, arg_start)
|
||||
-- scan: <marker>(<args>)
|
||||
|
||||
local args = {}
|
||||
local pos = 1
|
||||
local args = {}
|
||||
local pos = 1
|
||||
local inner_len = #inner
|
||||
while pos <= inner_len do
|
||||
pos = duffle.skip_ws_and_cmt(inner, pos)
|
||||
@@ -172,8 +169,7 @@ local function extract_ident_args(token, after_ident)
|
||||
return args, after_paren
|
||||
end
|
||||
|
||||
-- (internal) Record a `atom_label(name)` marker — `at_pos` is the
|
||||
-- branch-free word position within the atom body.
|
||||
-- (internal) Record a `atom_label(name)` marker — `at_pos` is the branch-free word position within the atom body.
|
||||
-- @param labels table<string, integer>
|
||||
-- @param args string[]
|
||||
-- @param at_pos integer
|
||||
@@ -191,8 +187,7 @@ local function record_offset_marker(branches, args, at_pos)
|
||||
end
|
||||
end
|
||||
|
||||
--- Scan a single token for atom_label/atom_offset markers, walking through
|
||||
--- balanced groups transparently (so nested calls are found).
|
||||
--- Scan a single token for atom_label/atom_offset markers, walking through balanced groups transparently (so nested calls are found).
|
||||
--- @param token string
|
||||
--- @param at_pos integer -- the branch-free word position of this token in the body
|
||||
--- @param labels table<string, integer>
|
||||
@@ -224,8 +219,7 @@ local function scan_for_atom_markers(token, at_pos, labels, branches)
|
||||
end
|
||||
end
|
||||
|
||||
--- Find the end position (just past the closing ')') of the first
|
||||
--- atom_label/atom_offset call in `tok`. Returns 0 if no such call.
|
||||
--- Find the end position (just past the closing ')') of the first atom_label/atom_offset call in `tok`. Returns 0 if no such call.
|
||||
--- @param tok string
|
||||
--- @return integer -- 0 if no marker call found; otherwise end-1 (just past ')')
|
||||
local function find_marker_call_end(tok)
|
||||
@@ -243,7 +237,9 @@ local function find_marker_call_end(tok)
|
||||
pos = (nx > pos) and nx or (pos + 1)
|
||||
else
|
||||
local ident, after_ident = duffle.read_ident(tok, pos)
|
||||
-- scan: <ident>
|
||||
if ident == LABEL_MARKER or ident == OFFSET_MARKER then
|
||||
-- scan: atom_label(<name>) OR atom_offset(<tag>, <target>)
|
||||
local open_paren = duffle.skip_ws_and_cmt(tok, after_ident)
|
||||
if tok:sub(open_paren, open_paren) == "(" then
|
||||
local _, end_paren = duffle.read_parens(tok, open_paren)
|
||||
@@ -261,8 +257,7 @@ end
|
||||
-- Atom scanner
|
||||
-- ════════════════════════════════════════════════════════════════════════════
|
||||
|
||||
--- Skip C qualifier keywords (`static`, `const`, etc.) and return the
|
||||
--- position past the last qualifier.
|
||||
--- Skip C qualifier keywords (`static`, `const`, etc.) and return the position past the last qualifier.
|
||||
--- @param source string
|
||||
--- @param pos integer
|
||||
--- @return integer
|
||||
@@ -276,8 +271,7 @@ local function skip_qualifiers(source, pos)
|
||||
end
|
||||
|
||||
-- (internal) Try to parse the wrapped atom form: `MipsAtom_(<name>) { ... }`.
|
||||
-- Returns the parsed Atom (name + body + position past body), or nil if
|
||||
-- the form didn't match.
|
||||
-- Returns the parsed Atom (name + body + position past body), or nil if the form didn't match.
|
||||
-- @param source_text string
|
||||
-- @param after_pos integer -- position just past `MipsAtom_`
|
||||
-- @return Atom|nil
|
||||
@@ -285,6 +279,7 @@ local function try_wrapped_atom(source_text, after_pos)
|
||||
local paren_pos = duffle.skip_ws_and_cmt(source_text, after_pos)
|
||||
if source_text:sub(paren_pos, paren_pos) ~= "(" then return nil end
|
||||
local inner, after_paren = duffle.read_parens(source_text, paren_pos)
|
||||
-- scan: MipsAtom_(<name>)
|
||||
|
||||
local name_start = 1
|
||||
while name_start <= #inner and duffle.is_space(inner:sub(name_start, name_start)) do
|
||||
@@ -295,11 +290,13 @@ local function try_wrapped_atom(source_text, after_pos)
|
||||
name_end = name_end + 1
|
||||
end
|
||||
local name = inner:sub(name_start, name_end - 1)
|
||||
if name == "" then return nil end
|
||||
if name == "" then return nil end
|
||||
|
||||
local brace_pos = duffle.scan_to_char(source_text, "{", after_paren)
|
||||
local brace_pos = duffle.scan_to_char(source_text, "{", after_paren)
|
||||
-- scan: MipsAtom_(<name>) {
|
||||
if not brace_pos then return nil end
|
||||
local body, after_brace = duffle.read_braces(source_text, brace_pos)
|
||||
-- scan: MipsAtom_(<name>) { <body> }
|
||||
return { name = name, body = body, after_brace = after_brace }
|
||||
end
|
||||
|
||||
@@ -310,18 +307,21 @@ end
|
||||
local function try_raw_atom(source_text, after_pos)
|
||||
local next_pos = duffle.skip_ws_and_cmt(source_text, after_pos)
|
||||
local next_ident, next_after = duffle.read_ident(source_text, next_pos)
|
||||
-- scan: MipsCode <next_ident>
|
||||
if not next_ident then return nil end
|
||||
if not starts_with(next_ident, CODE_RAW_PREFIX) then return nil end
|
||||
if #next_ident <= CODE_RAW_PREFIX_LEN then return nil end
|
||||
local atom_name = next_ident:sub(CODE_RAW_PREFIX_LEN + 1)
|
||||
-- scan: MipsCode code_<name>
|
||||
local brace_pos = duffle.scan_to_char(source_text, "{", next_after)
|
||||
-- scan: MipsCode code_<name> {
|
||||
if not brace_pos then return nil end
|
||||
local body, after_brace = duffle.read_braces(source_text, brace_pos)
|
||||
-- scan: MipsCode code_<name> { <body> }
|
||||
return { name = atom_name, body = body, after_brace = after_brace }
|
||||
end
|
||||
|
||||
--- Find every `MipsAtom_(name) { ... }` (or raw `MipsCode code_<name> { ... }`)
|
||||
--- declaration in a source.
|
||||
--- Find every `MipsAtom_(name) { ... }` (or raw `MipsCode code_<name> { ... }`) declaration in a source.
|
||||
--- @param source_text string
|
||||
--- @return Atom[]
|
||||
local function find_atoms(source_text)
|
||||
@@ -334,9 +334,11 @@ local function find_atoms(source_text)
|
||||
pos = skip_qualifiers(source_text, pos); if pos > src_len then break end
|
||||
|
||||
local ident, after = duffle.read_ident(source_text, pos)
|
||||
-- scan: <ident>
|
||||
if not ident then
|
||||
pos = pos + 1
|
||||
elseif ident == ATOM_PREFIX then
|
||||
-- scan: MipsAtom_(<name>) { <body> }
|
||||
local atom = try_wrapped_atom(source_text, after)
|
||||
if atom then
|
||||
atoms[#atoms + 1] = { name = atom.name, body = atom.body }
|
||||
@@ -345,6 +347,7 @@ local function find_atoms(source_text)
|
||||
pos = pos + 1
|
||||
end
|
||||
elseif ident == CODE_DECL then
|
||||
-- scan: MipsCode code_<name> { <body> }
|
||||
local atom = try_raw_atom(source_text, after)
|
||||
if atom then
|
||||
atoms[#atoms + 1] = { name = atom.name, body = atom.body }
|
||||
@@ -363,10 +366,10 @@ end
|
||||
-- Per-atom body scan
|
||||
-- ════════════════════════════════════════════════════════════════════════════
|
||||
|
||||
-- (internal) Count words emitted by the rest of `tok` after a marker call
|
||||
-- (the marker call itself emits 0 words, but the source pattern may bundle
|
||||
-- the marker with the next instruction on the same line, separated by no
|
||||
-- top-level comma). Returns the word count contributed by that rest.
|
||||
-- (internal) Count words emitted by the rest of `tok` after a marker call
|
||||
-- (the marker call itself emits 0 words, but the source pattern may bundle the marker with the next instruction on the same line,
|
||||
-- separated by no top-level comma).
|
||||
-- Returns the word count contributed by that rest.
|
||||
-- @param tok string
|
||||
-- @param word_counts table
|
||||
-- @return integer
|
||||
@@ -413,8 +416,8 @@ end
|
||||
-- Offset computation + header generation
|
||||
-- ════════════════════════════════════════════════════════════════════════════
|
||||
|
||||
-- Compute branch offsets as `target_word - branch_word - 1` (the
|
||||
-- standard MIPS branch-immediate encoding).
|
||||
-- Compute branch offsets as `target_word - branch_word - 1`
|
||||
-- (the standard MIPS branch-immediate encoding).
|
||||
-- @param labels table<string, integer>
|
||||
-- @param branches table[]
|
||||
-- @return BranchOffset[]
|
||||
@@ -523,9 +526,9 @@ local function process_source(ctx, src)
|
||||
return out_path
|
||||
end
|
||||
|
||||
--- Run the offsets pass. For each source, emits a per-module
|
||||
--- `<dir_basename>.offsets.h` containing `#define _atom_offset_F_T = N`
|
||||
--- constants for every `atom_offset(F, T)` reference in the source's atoms.
|
||||
--- Run the offsets pass.
|
||||
--- For each source, emits a per-module `<dir_basename>.offsets.h` containing `#define _atom_offset_F_T = N` constants for every `atom_offset(F, T)` reference
|
||||
--- in the source's atoms.
|
||||
--- @param ctx PassCtx
|
||||
--- @return PassResult
|
||||
function M.run(ctx)
|
||||
|
||||
+23
-28
@@ -20,21 +20,23 @@
|
||||
-- Module-scope requires + package.path setup
|
||||
-- ════════════════════════════════════════════════════════════════════════════
|
||||
|
||||
-- Resolve `arg[0]` to an absolute-ish script directory so that
|
||||
-- `require("duffle")` resolves against `scripts/` regardless of CWD.
|
||||
-- Note: this boilerplate is duplicated in 6 other entry scripts; a
|
||||
-- Phase-6 extraction target (`duffle.setup_package_path()`).
|
||||
-- Resolve `arg[0]` to an absolute-ish script directory so that `require("duffle")` resolves against `scripts/` regardless of CWD.
|
||||
-- Note: this boilerplate is duplicated in 6 other entry scripts; a Phase-6 extraction target (`duffle.setup_package_path()`).
|
||||
-- Bootstrap: see `ps1_meta.lua` for the rationale.
|
||||
dofile((arg[0]:match("(.*[/\\])") or "./") .. "duffle_paths.lua")
|
||||
-- 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.
|
||||
local _src = debug.getinfo(1, "S").source:sub(2)
|
||||
local _dir = _src:match("(.*[/\\])") or "./"
|
||||
dofile(_dir .. "../duffle_paths.lua")
|
||||
local duffle = require("duffle")
|
||||
|
||||
-- ════════════════════════════════════════════════════════════════════════════
|
||||
-- Constants
|
||||
-- ════════════════════════════════════════════════════════════════════════════
|
||||
|
||||
-- Section separators used in the rendered text reports. The thin rules
|
||||
-- are hand-tuned to align with the per-section content width; do not
|
||||
-- change without also checking the section renderers below.
|
||||
-- Section separators used in the rendered text reports.
|
||||
-- The thin rules are hand-tuned to align with the per-section content width; do not change without also checking the section renderers below.
|
||||
local RULE_THICK = "========================================================"
|
||||
local SECTION_HEADER_ATOMS = "── Atoms ────────────────────────────────────────────────"
|
||||
local SECTION_HEADER_ANNOTS = "── Annotations ──────────────────────────────────────────"
|
||||
@@ -142,8 +144,7 @@ local PASS_NAME = "report"
|
||||
-- Per-MODULE annotation report (aggregated across all sources in a dir)
|
||||
-- ════════════════════════════════════════════════════════════════════════════
|
||||
|
||||
-- Extract the basename (last path segment) of a forward- or back-slash
|
||||
-- separated path. Returns the input unchanged if no separator is found.
|
||||
-- Extract the basename (last path segment) of a forward- or back-slash separated path. Returns the input unchanged if no separator is found.
|
||||
-- @param path string
|
||||
-- @return string
|
||||
local function source_basename(path)
|
||||
@@ -301,8 +302,7 @@ end
|
||||
-- ════════════════════════════════════════════════════════════════════════════
|
||||
|
||||
--- 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.
|
||||
--- 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)
|
||||
@@ -351,8 +351,7 @@ end
|
||||
-- Orchestration helpers
|
||||
-- ════════════════════════════════════════════════════════════════════════════
|
||||
|
||||
-- Group source files by their `dir` field. Used to mirror the
|
||||
-- per-DIRECTORY partitioning the annotation pass uses.
|
||||
-- Group source files by their `dir` field. Used to mirror the per-DIRECTORY partitioning the annotation pass uses.
|
||||
-- @param sources SourceFile[]
|
||||
-- @return table<string, SourceFile[]> -- map of dir -> sources in that dir
|
||||
local function group_sources_by_dir(sources)
|
||||
@@ -364,19 +363,17 @@ local function group_sources_by_dir(sources)
|
||||
return by_dir
|
||||
end
|
||||
|
||||
-- (internal) Validate each source in `dir_sources` via the annotation pass,
|
||||
-- tagging each result with `result.source = src.path` for downstream rendering.
|
||||
-- Returns the list of module results + the flat list of all results (for the
|
||||
-- project-wide summary).
|
||||
-- (internal) Validate each source in `dir_sources` via the annotation pass, tagging each result with `result.source = src.path` for downstream rendering.
|
||||
-- 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 validate_module_sources(ctx, dir_sources)
|
||||
local annotation = require("passes.annotation")
|
||||
local annotation = require("passes.annotation")
|
||||
local module_results = {}
|
||||
local all_results = {}
|
||||
local all_results = {}
|
||||
for _, src in ipairs(dir_sources) do
|
||||
local result = annotation.validate(ctx, src)
|
||||
local result = annotation.validate(ctx, src)
|
||||
result.source = src.path
|
||||
module_results[#module_results + 1] = result
|
||||
all_results[#all_results + 1] = result
|
||||
@@ -389,7 +386,7 @@ end
|
||||
-- @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
|
||||
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
|
||||
@@ -411,9 +408,8 @@ end
|
||||
|
||||
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.
|
||||
--- Renders one `<dir_basename>.annotations.txt` per source-directory that has content, plus the project-wide `annotation_validation.txt` summary.
|
||||
--- @param ctx PassCtx
|
||||
--- @return PassResult
|
||||
function M.run(ctx)
|
||||
@@ -428,11 +424,10 @@ function M.run(ctx)
|
||||
|
||||
local all_results_for_summary = {}
|
||||
for _, entry in ipairs(module_entries) do
|
||||
debug_log("entry: dir=%s basename=%s atoms_count=%d dir_sources=%d\n",
|
||||
entry.dir, entry.dir_basename, entry.atoms_count, #(by_dir[entry.dir] or {}))
|
||||
debug_log("entry: dir=%s basename=%s atoms_count=%d dir_sources=%d\n", entry.dir, entry.dir_basename, entry.atoms_count, #(by_dir[entry.dir] or {}))
|
||||
|
||||
if entry.atoms_count > 0 or #(by_dir[entry.dir] or {}) > 0 then
|
||||
local dir_sources = by_dir[entry.dir] or {}
|
||||
local dir_sources = by_dir[entry.dir] or {}
|
||||
local module_results, all_results = validate_module_sources(ctx, dir_sources)
|
||||
for _, r in ipairs(all_results) do
|
||||
all_results_for_summary[#all_results_for_summary + 1] = r
|
||||
|
||||
+327
-342
File diff suppressed because it is too large
Load Diff
@@ -1,44 +1,40 @@
|
||||
--- word_count_eval.lua — Word-counting logic for the tape-atom metaprogram
|
||||
--- pipeline.
|
||||
--- word_count_eval.lua — Word-counting logic for the tape-atom metaprogram pipeline.
|
||||
---
|
||||
--- Three responsibilities:
|
||||
--- 1. **Public utilities** (used by `passes/components.lua`,
|
||||
--- `passes/offsets.lua`, `passes/annotation.lua`):
|
||||
--- 1. **Public utilities** (used by `passes/components.lua`, `passes/offsets.lua`, `passes/annotation.lua`):
|
||||
--- - `M.count_token_words(token, wc)` — words emitted by one token
|
||||
--- - `M.scan_dir(dir, suffix)` — glob walk for *.macs.h
|
||||
--- - `M.count_body_words(body, wc)` — words emitted by an atom body
|
||||
--- 2. **Pass entry** `M.run(ctx)` — loads metadata.h + *.macs.h into
|
||||
--- `ctx.shared.word_counts` for downstream passes.
|
||||
--- 2. **Pass entry** `M.run(ctx)` — loads metadata.h + *.macs.h into `ctx.shared.word_counts` for downstream passes.
|
||||
--- 3. **Internal helpers** for the body scanner.
|
||||
---
|
||||
--- **Conventions**: tabs (1/level), EmmyLua annotations, no regex,
|
||||
--- Lua 5.3 compatible. See
|
||||
--- `C:\projects\Pikuma\ps1-ai\conductor\code_styleguides\lua.md`.
|
||||
--- Lua 5.3 compatible.
|
||||
|
||||
-- ════════════════════════════════════════════════════════════════════════════
|
||||
-- Module-scope requires + package.path setup
|
||||
-- ════════════════════════════════════════════════════════════════════════════
|
||||
|
||||
-- Resolve `arg[0]` to an absolute-ish script directory so that
|
||||
-- `require("duffle")` resolves against `scripts/` regardless of CWD.
|
||||
-- Note: this boilerplate is duplicated in 6 other entry scripts; a
|
||||
-- Phase-6 extraction target (`duffle.setup_package_path()`).
|
||||
-- Resolve `arg[0]` to an absolute-ish script directory so that `require("duffle")` resolves against `scripts/` regardless of CWD.
|
||||
-- Note: this boilerplate is duplicated in 6 other entry scripts; a Phase-6 extraction target (`duffle.setup_package_path()`).
|
||||
-- Bootstrap: see `ps1_meta.lua` for the rationale.
|
||||
dofile((arg[0]:match("(.*[/\\])") or "./") .. "duffle_paths.lua")
|
||||
-- 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.
|
||||
local _src = debug.getinfo(1, "S").source:sub(2)
|
||||
local _dir = _src:match("(.*[/\\])") or "./"
|
||||
dofile(_dir .. "../duffle_paths.lua")
|
||||
local duffle = require("duffle")
|
||||
|
||||
-- ════════════════════════════════════════════════════════════════════════════
|
||||
-- Constants
|
||||
-- ════════════════════════════════════════════════════════════════════════════
|
||||
|
||||
-- Windows separator chars — used to convert `dir /b /s` output (which uses
|
||||
-- `\`) into POSIX paths (which our scripts expect).
|
||||
-- Windows separator chars — used to convert `dir /b /s` output (which uses `\`) into POSIX paths (which our scripts expect).
|
||||
local PATH_SEP_BACKSLASH = "\\"
|
||||
local PATH_SEP_FORWARD = "/"
|
||||
|
||||
-- Glob command for Windows directory walk. `dir /b /s` lists all matching
|
||||
-- files recursively with bare paths (no headers); `2>nul` discards the
|
||||
-- "file not found" stderr when nothing matches.
|
||||
-- Glob command for Windows directory walk. `dir /b /s` lists all matching files recursively with bare paths (no headers);
|
||||
-- `2>nul` discards the "file not found" stderr when nothing matches.
|
||||
local DIR_GLOB_CMD = 'dir /b /s "%s\\%s" 2>nul'
|
||||
|
||||
-- ════════════════════════════════════════════════════════════════════════════
|
||||
@@ -83,8 +79,7 @@ local M = {}
|
||||
|
||||
--- Count words emitted by a single comma-separated token inside an atom body.
|
||||
--- For most tokens (regular MIPS instructions) this returns 1.
|
||||
--- For `mac_X(...)` calls, this returns the resolved word count from `wc`
|
||||
--- (recursively if needed). For `nop2` etc., returns wc[name].
|
||||
--- For `mac_X(...)` calls, this returns the resolved word count from `wc` (recursively if needed). For `nop2` etc., returns wc[name].
|
||||
--- For unknown macros, returns 1 and (optionally) warns.
|
||||
---
|
||||
--- @param token string -- a single token from split_top_level_commas
|
||||
@@ -92,7 +87,7 @@ local M = {}
|
||||
--- @return integer
|
||||
function M.count_token_words(token, wc)
|
||||
local s = duffle.trim(token)
|
||||
if s == "" then return 0 end
|
||||
if s == "" then return 0 end
|
||||
local name, after = duffle.read_ident(s, 1)
|
||||
if not name then return 1 end
|
||||
if wc[name] then return wc[name] end
|
||||
@@ -108,33 +103,71 @@ end
|
||||
-- └────────────────────────────────────────────────────────────────────┘
|
||||
|
||||
--- Recursively scan a directory for files matching a glob suffix.
|
||||
--- No regex per the no_regex constraint — uses plain byte matching
|
||||
--- via `dir /b /s` on Windows.
|
||||
--- No regex per the no_regex constraint — uses plain byte matching via `dir /b /s` on Windows.
|
||||
---
|
||||
--- The `.macs.h` files produced by the components pass always live at `<project_root>/<module>/gen/`.
|
||||
--- We can shortcut the `dir /b /s` walk by listing modules first (one `dir /b /ad`), then walking each `<module>/gen/`
|
||||
--- (one `dir /b` per module, no recursion).
|
||||
--- For projects with 2 modules and 0 .macs.h files, this drops the cost from ~52ms
|
||||
--- (full recursive walk of the entire project tree) to ~5ms.
|
||||
---
|
||||
--- @param dir string -- directory to scan (absolute or relative)
|
||||
--- @param suffix string -- file pattern, e.g. "*.macs.h"
|
||||
--- @return string[]
|
||||
-- Cache the scan_dir result per (dir, suffix) in package.loaded.
|
||||
-- Each `io.popen` call on Windows is ~50-100ms of subprocess overhead, so caching the result saves a fixed cost on every build.
|
||||
-- The cache persists for the lifetime of the Lua process (cleared when ps1_meta.lua exits).
|
||||
-- If a build removes/creates .macs.h files mid-process, the caller can invalidate by calling `M._invalidate_scan_cache()`.
|
||||
local SCAN_CACHE_KEY = "__word_count_eval_scan_cache__"
|
||||
|
||||
--- Recursively scan a directory for files matching a glob suffix.
|
||||
--- No regex per the no_regex constraint — uses plain byte matching via `dir /b /s` on Windows.
|
||||
---
|
||||
--- @param dir string -- directory to scan (absolute or relative)
|
||||
--- @param suffix string -- file pattern, e.g. "*.macs.h"
|
||||
--- @return string[]
|
||||
function M.scan_dir(dir, suffix)
|
||||
local key = dir .. "\0" .. suffix
|
||||
|
||||
-- Check the in-process cache first. (Mostly helps when a build triggers multiple `M.run` calls -- e.g.
|
||||
-- the audit_lua_nesting script's stress tests but the cost is ~free either way.)
|
||||
local cache = package.loaded[SCAN_CACHE_KEY]
|
||||
if cache and cache[key] then return cache[key] end
|
||||
|
||||
local results = {}
|
||||
local pipe = io.popen(DIR_GLOB_CMD:format(dir, suffix))
|
||||
if not pipe then return results end
|
||||
if not pipe then
|
||||
-- Cache the empty result too (avoids re-scan if the dir is genuinely empty -- e.g. a clean build before components has run yet).
|
||||
cache = cache or {}
|
||||
cache[key] = results
|
||||
package.loaded[SCAN_CACHE_KEY] = cache
|
||||
return results
|
||||
end
|
||||
for raw_line in pipe:lines() do
|
||||
local path = raw_line:gsub(PATH_SEP_BACKSLASH, PATH_SEP_FORWARD)
|
||||
results[#results + 1] = path
|
||||
end
|
||||
pipe:close()
|
||||
|
||||
-- Cache the result.
|
||||
cache = cache or {}
|
||||
cache[key] = results
|
||||
package.loaded[SCAN_CACHE_KEY] = cache
|
||||
|
||||
return results
|
||||
end
|
||||
|
||||
--- Invalidate the scan cache (call after creating new .macs.h files in the same Lua process — usually not needed).
|
||||
function M._invalidate_scan_cache() package.loaded[SCAN_CACHE_KEY] = nil end
|
||||
|
||||
-- ┌────────────────────────────────────────────────────────────────────┐
|
||||
-- │ Shared utility: count_body_words │
|
||||
-- └────────────────────────────────────────────────────────────────────┘
|
||||
|
||||
--- Count words emitted by an entire atom body (a brace-delimited block).
|
||||
--- Splits by top-level commas; for each token, delegates to count_token_words.
|
||||
--- Handles `atom_label(name)` / `atom_offset(tag, name)` markers (record at
|
||||
--- current pos, do NOT advance pos; if the marker call bundles an instruction
|
||||
--- after it, count that instruction too).
|
||||
--- Handles `atom_label(name)` / `atom_offset(tag, name)` markers
|
||||
--- (record at current pos, do NOT advance pos; if the marker call bundles an instruction after it, count that instruction too).
|
||||
---
|
||||
--- @param body string -- brace-delimited atom body (without braces)
|
||||
--- @param wc WordCounts -- the shared word-count table
|
||||
@@ -151,10 +184,8 @@ function M.count_body_words(body, wc)
|
||||
local is_marker = leading_ident == "atom_label" or leading_ident == "atom_offset"
|
||||
if is_marker then
|
||||
-- Marker call: record at current pos, do NOT advance pos.
|
||||
-- But the source pattern may bundle the marker with the next
|
||||
-- instruction on a new line (no top-level comma between them).
|
||||
-- In that case, the rest of `tok` after the marker call is
|
||||
-- a real instruction that must still be counted.
|
||||
-- But the source pattern may bundle the marker with the next instruction on a new line (no top-level comma between them).
|
||||
-- In that case, the rest of `tok` after the marker call is a real instruction that must still be counted.
|
||||
local marker_end = M.find_marker_call_end(tok)
|
||||
if marker_end > 0 and marker_end < #tok then
|
||||
local rest = duffle.trim(tok:sub(marker_end + 1))
|
||||
@@ -169,8 +200,7 @@ function M.count_body_words(body, wc)
|
||||
return total
|
||||
end
|
||||
|
||||
--- Find the end position (just past the closing ')') of the first
|
||||
--- atom_label/atom_offset call in `tok`. Returns 0 if no such call.
|
||||
--- Find the end position (just past the closing ')') of the first atom_label/atom_offset call in `tok`. Returns 0 if no such call.
|
||||
--- Internal helper for count_body_words.
|
||||
---
|
||||
--- @param tok string
|
||||
@@ -187,10 +217,10 @@ function M.find_marker_call_end(tok)
|
||||
elseif ch == "/" then
|
||||
-- comment — skip past it (delegated to duffle.skip_str_or_cmt)
|
||||
local nx = duffle.skip_str_or_cmt(tok, pos)
|
||||
pos = (nx > pos) and nx or (pos + 1)
|
||||
pos = (nx > pos) and nx or (pos + 1)
|
||||
else
|
||||
local ident, after_ident = duffle.read_ident(tok, pos)
|
||||
local marker_end = find_marker_end(tok, ident, after_ident)
|
||||
local marker_end = find_marker_end(tok, ident, after_ident)
|
||||
if marker_end > 0 then return marker_end end
|
||||
pos = after_ident or (pos + 1)
|
||||
end
|
||||
@@ -198,8 +228,8 @@ function M.find_marker_call_end(tok)
|
||||
return 0
|
||||
end
|
||||
|
||||
-- (internal) If `ident` is `atom_label`/`atom_offset` followed by `(...)`,
|
||||
-- return the position just past the closing ')'. Otherwise 0.
|
||||
-- (internal) If `ident` is `atom_label`/`atom_offset` followed by `(...)`, return the position just past the closing ')'.
|
||||
-- Otherwise 0.
|
||||
-- @param tok string
|
||||
-- @param ident string|nil
|
||||
-- @param after_ident integer
|
||||
@@ -216,10 +246,8 @@ end
|
||||
-- │ Pass entry: M.run(ctx) — "word-counts" pass │
|
||||
-- └────────────────────────────────────────────────────────────────────┘
|
||||
|
||||
--- Load metadata.h + scan for existing *.macs.h files into
|
||||
--- ctx.shared.word_counts. Loading the .macs.h files is idempotent:
|
||||
--- entries from later (current-build) .macs.h files override
|
||||
--- metadata.h entries of the same name.
|
||||
--- Load metadata.h + scan for existing *.macs.h files into ctx.shared.word_counts.
|
||||
--- Loading the .macs.h files is idempotent: entries from later (current-build) .macs.h files override metadata.h entries of the same name.
|
||||
---
|
||||
--- @param ctx PassCtx
|
||||
--- @return PassResult
|
||||
@@ -244,7 +272,6 @@ function M.run(ctx)
|
||||
end
|
||||
|
||||
ctx.shared.word_counts = wc
|
||||
|
||||
return { outputs = {}, errors = {}, warnings = {} }
|
||||
end
|
||||
|
||||
|
||||
+43
-91
@@ -79,9 +79,7 @@ local PASS_FLAG_DISPATCH_KEY = "__pass__"
|
||||
|
||||
--- @class PassOutputEntry
|
||||
--- @field [string] string -- dynamic shape; key is the output kind
|
||||
-- (e.g. "macs_h", "offsets_h", "errors_h",
|
||||
-- "annotations_txt", "static_analysis_txt",
|
||||
-- "summary_txt"), value is the path
|
||||
-- (e.g. "macs_h", "offsets_h", "errors_h", "annotations_txt", "static_analysis_txt", "summary_txt"), value is the path
|
||||
|
||||
--- @class Finding
|
||||
--- @field line integer -- source line (or 0 for pass-level)
|
||||
@@ -177,8 +175,7 @@ local ALL_PASS_NAMES = {
|
||||
"offsets", "static-analysis", "report",
|
||||
}
|
||||
|
||||
--- Append every pass name to args.requested_set. Used by --all and
|
||||
--- by the "default to --all if no pass flags were given" fallback.
|
||||
--- Append every pass name to args.requested_set. Used by --all and by the "default to --all if no pass flags were given" fallback.
|
||||
--- @param args ParsedArgs
|
||||
local function request_all_passes(args)
|
||||
for _, n in ipairs(ALL_PASS_NAMES) do
|
||||
@@ -186,11 +183,9 @@ local function request_all_passes(args)
|
||||
end
|
||||
end
|
||||
|
||||
-- Per-flag handlers. Each handler takes (args, argv, i) and returns
|
||||
-- the new i (so multi-arg flags like --source FILE advance it).
|
||||
-- Returning nil + os.exit() handles termination flags (--help).
|
||||
-- This replaces the 8-way `if/elseif/elseif...` chain that nested
|
||||
-- 4 levels deep and made the dispatch logic hard to scan.
|
||||
-- Per-flag handlers. Each handler takes (args, argv, arg_idx) and returns the new arg_idx (so multi-arg flags like --source FILE advance it).
|
||||
-- Returning nil + os.exit() handles termination flags (--help). This replaces the 8-way `if/elseif/elseif...` chain that nested 4 levels deep
|
||||
-- and made the dispatch logic hard to scan.
|
||||
local FLAG_HANDLERS = {}
|
||||
|
||||
-- ════════════════════════════════════════════════════════════════════════════
|
||||
@@ -233,50 +228,25 @@ EXAMPLE:
|
||||
]])
|
||||
end
|
||||
|
||||
-- Per-flag handlers. Each takes (args, argv, i) and returns the new i
|
||||
-- (so multi-arg flags like --source FILE advance it). Termination
|
||||
-- flags like --help call os.exit() instead. This replaces the 8-way
|
||||
-- `if/elseif/elseif...` chain that nested 4 levels deep and made the
|
||||
-- dispatch logic hard to scan.
|
||||
-- Per-flag handlers. Each takes (args, argv, arg_idx) and returns the new arg_idx (so multi-arg flags like --source FILE advance it).
|
||||
-- Termination flags like --help call os.exit() instead.
|
||||
-- This replaces the 8-way `if/elseif/elseif...` chain that nested 4 levels deep and made the dispatch logic hard to scan.
|
||||
--
|
||||
-- Populated AFTER print_help so the --help handler can reference it
|
||||
-- as an upvalue (Lua resolves locals at closure-call time, but if the
|
||||
-- closure is defined before the local, it falls back to _G).
|
||||
-- Populated AFTER print_help so the --help handler can reference it as an upvalue (Lua resolves locals at closure-call time,
|
||||
-- but if the closure is defined before the local, it falls back to _G).
|
||||
FLAG_HANDLERS["--help"] = function(args)
|
||||
print_help()
|
||||
os.exit(0)
|
||||
end
|
||||
|
||||
FLAG_HANDLERS["--dry-run"] = function(args)
|
||||
args.dry_run = true
|
||||
end
|
||||
FLAG_HANDLERS["--dry-run"] = function(args) args.dry_run = true end
|
||||
FLAG_HANDLERS["--verbose"] = function(args) args.verbose = true end
|
||||
FLAG_HANDLERS["--source"] = function(args, argv, arg_idx) args.sources[#args.sources + 1] = argv[arg_idx + 1]; return arg_idx + 1 end
|
||||
FLAG_HANDLERS["--metadata"] = function(args, argv, arg_idx) args.metadata = argv[arg_idx + 1]; return arg_idx + 1 end
|
||||
FLAG_HANDLERS["--out-root"] = function(args, argv, arg_idx) args.out_root = argv[arg_idx + 1]; return arg_idx + 1 end
|
||||
FLAG_HANDLERS["--project-root"] = function(args, argv, arg_idx) args.project_root = argv[arg_idx + 1]; return arg_idx + 1 end
|
||||
|
||||
FLAG_HANDLERS["--verbose"] = function(args)
|
||||
args.verbose = true
|
||||
end
|
||||
|
||||
FLAG_HANDLERS["--source"] = function(args, argv, i)
|
||||
args.sources[#args.sources + 1] = argv[i + 1]
|
||||
return i + 1
|
||||
end
|
||||
|
||||
FLAG_HANDLERS["--metadata"] = function(args, argv, i)
|
||||
args.metadata = argv[i + 1]
|
||||
return i + 1
|
||||
end
|
||||
|
||||
FLAG_HANDLERS["--out-root"] = function(args, argv, i)
|
||||
args.out_root = argv[i + 1]
|
||||
return i + 1
|
||||
end
|
||||
|
||||
FLAG_HANDLERS["--project-root"] = function(args, argv, i)
|
||||
args.project_root = argv[i + 1]
|
||||
return i + 1
|
||||
end
|
||||
|
||||
-- Pass-flag handler. Reads the closed-set table, expands --all,
|
||||
-- appends to requested_set. Single-statement, no nesting.
|
||||
-- Pass-flag handler. Reads the closed-set table, expands --all, appends to requested_set. Single-statement, no nesting.
|
||||
FLAG_HANDLERS[PASS_FLAG_DISPATCH_KEY] = function(args, a)
|
||||
local name = PASS_FLAG_TO_NAME[a]
|
||||
if name == ALL_PASSES_SENTINEL then
|
||||
@@ -303,7 +273,7 @@ local function parse_args(argv)
|
||||
|
||||
local pos = 1
|
||||
while pos <= #argv do
|
||||
local a = argv[pos]
|
||||
local a = argv[pos]
|
||||
local handler = FLAG_HANDLERS[a]
|
||||
if handler then
|
||||
pos = handler(args, argv, pos) or pos
|
||||
@@ -318,14 +288,12 @@ local function parse_args(argv)
|
||||
end
|
||||
|
||||
-- Default: --all if no explicit pass flags.
|
||||
if #args.requested_set == 0 then
|
||||
request_all_passes(args)
|
||||
end
|
||||
if #args.requested_set == 0 then request_all_passes(args) end
|
||||
|
||||
-- Defaults: project_root = dirname(metadata).
|
||||
if args.metadata and not args.project_root then
|
||||
local d = duffle.dirname(args.metadata)
|
||||
if #d > 0 and (d:sub(-1) == "/" or d:sub(-1) == "\\") then
|
||||
if #d > 0 and (d:sub(-1) == "/" or d:sub(-1) == "\\") then
|
||||
d = d:sub(1, -2)
|
||||
end
|
||||
args.project_root = duffle.dirname(d)
|
||||
@@ -355,7 +323,7 @@ end
|
||||
local function build_ctx(args)
|
||||
local sources = {}
|
||||
for _, path in ipairs(args.sources) do
|
||||
local f = io.open(path, "r")
|
||||
local f = io.open(path, "r")
|
||||
if not f then
|
||||
io.stderr:write("ps1_meta: cannot open --source " .. path .. "\n")
|
||||
os.exit(EXIT_INTERNAL_ERROR)
|
||||
@@ -394,8 +362,7 @@ end
|
||||
-- Topological sort (Kahn's algorithm + cycle detection)
|
||||
-- ════════════════════════════════════════════════════════════════════════════
|
||||
|
||||
--- Compute the dep-closure of `requested_set`: include every pass name
|
||||
--- transitively required by the requested set.
|
||||
--- Compute the dep-closure of `requested_set`: include every pass name transitively required by the requested set.
|
||||
---
|
||||
--- @param passes table<string, PassDescriptor>
|
||||
--- @param requested_set string[]
|
||||
@@ -431,8 +398,7 @@ local function count_entries(t)
|
||||
return n
|
||||
end
|
||||
|
||||
--- Compute in-degrees for the Kahn sort: for each pass in `needed`,
|
||||
--- the number of its deps that are also in `needed`.
|
||||
--- Compute in-degrees for the Kahn sort: for each pass in `needed`, the number of its deps that are also in `needed`.
|
||||
---
|
||||
--- @param passes table<string, PassDescriptor>
|
||||
--- @param needed table<string, boolean>
|
||||
@@ -450,8 +416,7 @@ local function compute_in_degrees(passes, needed)
|
||||
return in_degree
|
||||
end
|
||||
|
||||
--- Seed the Kahn ready queue with passes whose in-degree is 0, sorted
|
||||
--- alphabetically for deterministic execution order.
|
||||
--- Seed the Kahn ready queue with passes whose in-degree is 0, sorted alphabetically for deterministic execution order.
|
||||
---
|
||||
--- @param in_degree table<string, integer>
|
||||
--- @return string[]
|
||||
@@ -464,9 +429,8 @@ local function seed_ready_queue(in_degree)
|
||||
return ready
|
||||
end
|
||||
|
||||
-- (internal) Pop the next ready pass, decrement the in-degree of every
|
||||
-- remaining pass that depended on it (inserting newly-zero-degree passes
|
||||
-- back into the ready queue), and append to `order`. Keeps `ready` sorted.
|
||||
-- (internal) Pop the next ready pass, decrement the in-degree of every remaining pass that depended on it
|
||||
-- (inserting newly-zero-degree passes back into the ready queue), and append to `order`. Keeps `ready` sorted.
|
||||
-- @param passes table<string, PassDescriptor>
|
||||
-- @param needed table<string, boolean>
|
||||
-- @param in_degree table<string, integer>
|
||||
@@ -506,11 +470,9 @@ local function topo_sort(passes, requested_set)
|
||||
process_next_ready(passes, needed, in_degree, ready, order)
|
||||
end
|
||||
|
||||
-- Cycle detection: if order doesn't include all needed passes,
|
||||
-- some are stuck with in_degree > 0 (the cycle closed on itself
|
||||
-- before Kahn could process them). Without this check, a fully-
|
||||
-- closed cycle (e.g. A -> B -> A) would silently return an empty
|
||||
-- order list, leaving the orchestrator to dispatch nothing.
|
||||
-- Cycle detection: if order doesn't include all needed passes, some are stuck with in_degree > 0 (the cycle closed on itself
|
||||
-- before Kahn could process them). Without this check, a fully-closed cycle (e.g. A -> B -> A) would silently return an emspty order list,
|
||||
-- leaving the orchestrator to dispatch nothing.
|
||||
if #order ~= count_entries(needed) then
|
||||
for name, deg in pairs(in_degree) do
|
||||
if deg > 0 then
|
||||
@@ -527,8 +489,7 @@ end
|
||||
-- ════════════════════════════════════════════════════════════════════════════
|
||||
|
||||
--- Render the dep graph as ASCII art. Output width capped at 78 columns.
|
||||
--- Falls back to the simpler "Resolved dependency order" list only if
|
||||
--- graph width exceeds terminal width.
|
||||
--- Falls back to the simpler "Resolved dependency order" list only if graph width exceeds terminal width.
|
||||
---
|
||||
--- @param passes table<string, PassDescriptor>
|
||||
--- @param requested string[] -- originally-requested passes (subset of closed)
|
||||
@@ -539,12 +500,12 @@ local function render_dep_graph(passes, requested, closed)
|
||||
local function add(s) lines[#lines + 1] = s end
|
||||
|
||||
add("[ps1_meta] Resolved dependency order (closed under deps):")
|
||||
for i, name in ipairs(closed) do
|
||||
for pass_idx, name in ipairs(closed) do
|
||||
local p = passes[name]
|
||||
local deps_str = (#p.deps == 0) and "(no deps)" or
|
||||
"(deps: " .. table.concat(p.deps, ", ") .. ")"
|
||||
add(string.format(" %d. %-22s %-45s [%s]",
|
||||
i, name, deps_str, p.kind))
|
||||
pass_idx, name, deps_str, p.kind))
|
||||
end
|
||||
add("")
|
||||
|
||||
@@ -556,11 +517,11 @@ local function render_dep_graph(passes, requested, closed)
|
||||
add(" +-----------+ +-----------------+ +-----------------+")
|
||||
add(" | word- |-->| components |-->| offsets |")
|
||||
add(" | counts | +-----------------+ +-----------------+")
|
||||
add(" | (load) | | ^")
|
||||
add(" +-----------+ | |")
|
||||
add(" | v |")
|
||||
add(" | code/<module>/gen/<basename>.macs.h |")
|
||||
add(" | (header - co-located for #include) |")
|
||||
add(" | (load) | | ^")
|
||||
add(" +-----------+ | |")
|
||||
add(" | v |")
|
||||
add(" | code/<module>/gen/<basename>.macs.h |")
|
||||
add(" | (header - co-located for #include) |")
|
||||
add(" | |")
|
||||
add(" | +-----------------+ |")
|
||||
add(" +---------->| annotation |--------------+")
|
||||
@@ -569,7 +530,7 @@ local function render_dep_graph(passes, requested, closed)
|
||||
add(" | v |")
|
||||
add(" | build/gen/<basename>.errors.h |")
|
||||
add(" | build/gen/<basename>.annotations.txt |")
|
||||
add(" | (report - NOT #included) |")
|
||||
add(" | (report - NOT #included) |")
|
||||
add(" | |")
|
||||
add(" | +-----------------+ |")
|
||||
add(" +---------->| static-analysis |--------------+")
|
||||
@@ -591,8 +552,7 @@ end
|
||||
-- Main orchestrator
|
||||
-- ════════════════════════════════════════════════════════════════════════════
|
||||
|
||||
-- (internal) Push a pass's outputs + warnings into `ctx.upstream[name]`
|
||||
-- for downstream passes to consume.
|
||||
-- (internal) Push a pass's outputs + warnings into `ctx.upstream[name]` for downstream passes to consume.
|
||||
-- @param ctx PassCtx
|
||||
-- @param pass_name string
|
||||
-- @param result PassResult
|
||||
@@ -606,9 +566,8 @@ local function accumulate_pass_result(ctx, pass_name, result)
|
||||
end
|
||||
end
|
||||
|
||||
-- (internal) If the pass's kind is in PASS_KIND_STOP_ON_ERROR and it
|
||||
-- reported errors, write each error to stderr. Returns true if any
|
||||
-- validation errors were reported.
|
||||
-- (internal) If the pass's kind is in PASS_KIND_STOP_ON_ERROR and it reported errors, write each error to stderr.
|
||||
-- Returns true if any validation errors were reported.
|
||||
-- @param pass_name string
|
||||
-- @param pass PassDescriptor
|
||||
-- @param result PassResult
|
||||
@@ -625,15 +584,8 @@ local function report_validation_errors(pass_name, pass, result)
|
||||
return true
|
||||
end
|
||||
|
||||
-- (internal) Run each pass in `order` in topological sequence. Tracks
|
||||
-- `had_errors` instead of os.exit()ing mid-loop so the report pass (and
|
||||
-- any other downstream pass) still runs and writes its per-module files.
|
||||
-- The legacy behavior was os.exit(1) on the first error, which left
|
||||
-- downstream per-module reports un-emitted; the 2026-07-10 change to
|
||||
-- per-module aggregation made that visible to the user (build/gen had
|
||||
-- only the partial reports from the failing pass), so we now complete
|
||||
-- all passes and set the exit code at the end.
|
||||
---
|
||||
-- (internal) Run each pass in `order` in topological sequence.
|
||||
--
|
||||
-- @param ctx PassCtx
|
||||
-- @param order string[]
|
||||
-- @return boolean -- true if any validation errors were reported
|
||||
|
||||
+94
-2
@@ -4,25 +4,37 @@ $path_code = join-path $path_root 'code'
|
||||
$path_scripts = join-path $path_root 'scripts'
|
||||
$path_toolchain = join-path $path_root 'toolchain'
|
||||
|
||||
# Halt on any error (instead of PowerShell's default `Continue`).
|
||||
$ErrorActionPreference = 'Stop'
|
||||
|
||||
$misc = join-path $PSScriptRoot 'helpers/misc.ps1'
|
||||
. $misc
|
||||
|
||||
# TODO(Ed): Review usage of these deps
|
||||
# I orgiinally cloned them when starting to get to the C runtime usage of the course
|
||||
# I originally cloned them when starting to get to the C runtime usage of the course
|
||||
# However, based on the heavy reliance of the PSX.Dev extension I might fallback; also
|
||||
# The gdb server doesn't need the full repo and were only using the src/mips
|
||||
# The gdb server doesn't need the full repo and were only using the src/mips
|
||||
# which has a standalone repo (nuggets)
|
||||
# armips may not be used at all but I'm not sure...
|
||||
#
|
||||
# PCSX-Redux: built via MSBuild (VS2022) — automated in the build section below.
|
||||
# Requires: VS2022 with C++ desktop workload + PlatformToolset=v143 retarget.
|
||||
# The .vcxproj files request v145; we pass /p:PlatformToolset=v143 to MSBuild.
|
||||
# NuGet packages are restored automatically on first build.
|
||||
# Output: toolchain\pcsx-redux\vsprojects\x64\Debug\pcsx-redux.exe
|
||||
|
||||
$url_armips = 'https://github.com/Kingcom/armips.git'
|
||||
$url_pcsx_redux = 'https://github.com/grumpycoders/pcsx-redux.git'
|
||||
$url_psyq_iwyu = 'https://github.com/johnbaumann/psyq_include_what_you_use.git'
|
||||
$url_lpeg = 'https://github.com/roberto-ieru/LPeg.git'
|
||||
|
||||
$path_armips = join-path $path_toolchain 'armips'
|
||||
$path_pcsx_redux = join-path $path_toolchain 'pcsx-redux'
|
||||
$path_psyq_iwyu = join-path $path_toolchain 'psyq_iwyu'
|
||||
$path_lpeg = join-path $path_toolchain 'lpeg'
|
||||
|
||||
clone-gitrepo $path_armips $url_armips
|
||||
clone-gitrepo $path_lpeg $url_lpeg
|
||||
clone-gitrepo $path_pcsx_redux $url_pcsx_redux
|
||||
clone-gitrepo $path_psyq_iwyu $url_psyq_iwyu
|
||||
|
||||
@@ -37,3 +49,83 @@ pop-location
|
||||
# $path_pcsx_redux_binaries = join-path $path_pcsx_redux_vsprojects 'x64/Release'
|
||||
|
||||
# $psyq_obj_parser = join-path $path_pcsx_redux_binaries 'psyq-obj-parser.exe'
|
||||
|
||||
# ════════════════════════════════════════════════════════════════════════════
|
||||
# PCSX-Redux — built via MSBuild (VS2022)
|
||||
#
|
||||
# Requires: Visual Studio 2022 with the C++ desktop workload.
|
||||
# The .vcxproj files target platform toolset v145, but VS2022 ships v143;
|
||||
# we pass /p:PlatformToolset=v143 to retarget at build time (no file edits).
|
||||
# NuGet packages (glfw, luajit.native, libFFmpeg-lite, x64sentry) are
|
||||
# restored automatically by MSBuild on first build.
|
||||
#
|
||||
# Output: toolchain\pcsx-redux\vsprojects\x64\Debug\pcsx-redux.exe
|
||||
# ════════════════════════════════════════════════════════════════════════════
|
||||
|
||||
# Locate MSBuild from the VS2022 install (no hardcoded path — uses vswhere).
|
||||
$vswhere = "${env:ProgramFiles(x86)}\Microsoft Visual Studio\Installer\vswhere.exe"
|
||||
if (-not (Test-Path $vswhere)) {
|
||||
write-error "vswhere not found at '$vswhere'. Install Visual Studio 2022 with the C++ desktop workload."
|
||||
exit 1
|
||||
}
|
||||
$msbuild_exe = & $vswhere -latest -products * -requires Microsoft.Component.MSBuild -find "MSBuild\**\Bin\MSBuild.exe" 2>$null | Select-Object -First 1
|
||||
if (-not $msbuild_exe) {
|
||||
write-error "MSBuild not found via vswhere. Install Visual Studio 2022 with the C++ desktop workload."
|
||||
exit 1
|
||||
}
|
||||
|
||||
$path_pcsx_sln = join-path $path_pcsx_redux 'vsprojects\pcsx-redux.sln'
|
||||
& $msbuild_exe $path_pcsx_sln /p:Configuration=Release /p:Platform=x64 /p:PlatformToolset=v143 /m /v:minimal
|
||||
|
||||
# Locate luajit via scoop. `luajit.exe` is on PATH via scoop's shim;
|
||||
# we use `scoop prefix` to find the install root for the include dir
|
||||
# (needed to compile lpeg against luajit's headers).
|
||||
# If scoop or luajit is missing, fail fast with an actionable message.
|
||||
$luajit_prefix = & scoop prefix luajit 2>$null
|
||||
if (-not $luajit_prefix -or -not (Test-Path (Join-Path $luajit_prefix 'bin/luajit.exe'))) {
|
||||
write-error "luajit not found via 'scoop prefix luajit'. Install via: scoop install luajit"
|
||||
exit 1
|
||||
}
|
||||
|
||||
# Discover the luajit include dir by globbing `include/luajit-*`.
|
||||
# This avoids hardcoding a specific version (e.g. `luajit-2.1`).
|
||||
$luajit_include_root = Join-Path $luajit_prefix 'include'
|
||||
$lua_inc_dir = Get-ChildItem -Path $luajit_include_root -Directory -Filter 'luajit-*' -ErrorAction SilentlyContinue |
|
||||
Select-Object -First 1 -ExpandProperty FullName
|
||||
if (-not $lua_inc_dir) {
|
||||
write-error "No 'luajit-*' include dir found under '$luajit_include_root'. The scoop luajit install may be broken."
|
||||
exit 1
|
||||
}
|
||||
|
||||
# Generate lpeg.dll by compiling the 6 source files directly.
|
||||
# `gcc` is on PATH (scoop's shim puts it there).
|
||||
# The source files: lpcap.c lpcode.c lpcset.c lpprint.c lptree.c lpvm.c
|
||||
# (per the lpeg makefile — no `make.lua` template generator in this version).
|
||||
# Link against luajit's import library (`libluajit-5.1.a`) for the Lua C API symbols (lua_*, luaL_*).
|
||||
$luajit_lib_dir = Join-Path $luajit_prefix 'lib'
|
||||
$lpeg_sources = @('lpcap.c', 'lpcode.c', 'lpcset.c', 'lpprint.c', 'lptree.c', 'lpvm.c')
|
||||
$lpeg_compile_args = @(
|
||||
'-O2', '-shared',
|
||||
"-I$lua_inc_dir",
|
||||
"-L$luajit_lib_dir",
|
||||
'-o', 'lpeg.dll'
|
||||
) + $lpeg_sources + @('-lluajit-5.1')
|
||||
push-location $path_lpeg
|
||||
& gcc @lpeg_compile_args
|
||||
pop-location
|
||||
|
||||
# ════════════════════════════════════════════════════════════════════════════
|
||||
# OpenBIOS — built from the PCSX-Redux source tree via make + mipsel-none-elf
|
||||
#
|
||||
# OpenBIOS is an open-source PS1 BIOS implementation (no retail BIOS dump needed).
|
||||
# It builds with the MIPS cross-toolchain (`mipsel-none-elf-gcc`, on PATH via the `mips` toolchain installer)
|
||||
# + `make` (on PATH via scoop).
|
||||
#
|
||||
# Output: toolchain\pcsx-redux\src\mips\openbios\openbios.bin
|
||||
# ════════════════════════════════════════════════════════════════════════════
|
||||
|
||||
$path_openbios = join-path $path_pcsx_redux 'src\mips\openbios'
|
||||
push-location $path_openbios
|
||||
& make clean
|
||||
& make
|
||||
pop-location
|
||||
|
||||
Reference in New Issue
Block a user