mirror of
https://github.com/Ed94/pikuma_ps1.git
synced 2026-08-08 16:48:17 +00:00
Compare commits
5
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
27a5f8029f | ||
|
|
7a168137fc | ||
|
|
2ceb2f2a05 | ||
|
|
6103f47f05 | ||
|
|
c824c998eb |
+104
-370
@@ -2,207 +2,123 @@
|
|||||||
* atom_dsl.h
|
* atom_dsl.h
|
||||||
* ============================================================================
|
* ============================================================================
|
||||||
*
|
*
|
||||||
* ATOM DSL — annotation layer for tape atoms (lottes_tape.h).
|
* ATOM DSL: Annotation layer for tape atoms (lottes_tape.h).
|
||||||
*
|
*
|
||||||
* This header turns `__attribute__((annotate(...)))` and `_Pragma(...)` into
|
* WHAT THIS HEADER IS
|
||||||
* a small named DSL that the metaprogram can validate against.
|
* -------------------
|
||||||
*
|
* The metaprogram (scripts/passes/annotation.lua) reads source-as-written
|
||||||
* The C compiler treats every macro below as a no-op:
|
|
||||||
* - atom_init / atom_terminate / atom_bind / atom_setup / atom_commit /
|
|
||||||
* atom_annot all expand to `__attribute__((annotate("..."))) MipsAtom_(name)`
|
|
||||||
* — accepted by GCC (with -Wno-attributes), absent at runtime.
|
|
||||||
* - atom_resource / atom_region / atom_group / atom_cadence / atom_async
|
|
||||||
* expand to `_Pragma("...")` — accepted by any C11 preprocessor.
|
|
||||||
*
|
|
||||||
* The metaprogram (tape_atom_annotation_pass.lua) reads the source-as-written
|
|
||||||
* and validates:
|
* and validates:
|
||||||
* - every MipsAtom_ has one atom_*() annotation (no orphans)
|
* - atom_info(...) shape: up to three sub-calls (atom_bind(Binds_X),
|
||||||
* - phase is recognized (init/bind/setup/work/commit/terminate)
|
* atom_reads(...), atom_writes(...)) in any order. All optional.
|
||||||
* - reads/writes reference canonical wave-context registers
|
* (No phase token for now; phases may be reintroduced later.)
|
||||||
* - rbind atoms reference a real Binds_* struct declaration
|
* - rbind atoms (atom_info(..., atom_bind(Binds_X), ...)) reference a
|
||||||
* - word-counts in tapre metadata agree with the body's actual .word count
|
* real Binds_* struct declaration.
|
||||||
* - resource/region/group/cadence/async pragmas are spelled correctly and
|
* - wave-context positions only reference the canonical 4-register
|
||||||
* reference known enum values
|
* set: R_PrimCursor / R_FaceCursor / R_VertBase / R_OtBase.
|
||||||
|
* - atom word-counts in word_counts.metadata.h agree with the body's
|
||||||
|
* actual .word count.
|
||||||
|
*
|
||||||
|
* WHY A PURE MACRO (atom_info, atom_bind, atom_reads, atom_writes, atom_label)
|
||||||
|
* -----------------------------------------------------------------
|
||||||
|
* Each of these expands to a C comment or to nothing. The C preprocessor
|
||||||
|
* strips them to whitespace. The metaprogram reads the literal token from
|
||||||
|
* source-as-written, NOT from the preprocessed output. This means:
|
||||||
|
* - the C compiler does no work for them (no __attribute__, no
|
||||||
|
* _Pragma, no asm side-effects)
|
||||||
|
* - they can never silently drift from the metaprogram's view
|
||||||
|
* (the metaprogram re-reads the source on every build)
|
||||||
|
* - the annotation is invisible to the linker, debugger, and IDE
|
||||||
*
|
*
|
||||||
* ============================================================================
|
* ============================================================================
|
||||||
*
|
*
|
||||||
* PUTTING IT ON AN ATOM — the canonical pattern
|
* Usage:
|
||||||
*
|
* MipsAtom_(cube_tri) atom_info(
|
||||||
* _tape_resources_
|
* atom_reads (R_PrimCursor, R_FaceCursor, R_VertBase, R_OtBase)
|
||||||
* atom_resource(cube_tri, "model_ship_cube")
|
* , atom_writes(R_PrimCursor, R_FaceCursor)
|
||||||
* atom_region (cube_tri, PRIM_ARENA)
|
* ){
|
||||||
* atom_group (cube_tri, GROUP_RENDER_PRIMS)
|
|
||||||
* atom_cadence (cube_tri, CADENCE_FRAME)
|
|
||||||
*
|
|
||||||
* atom_annot(cube_tri, phase_work,
|
|
||||||
* tape_regs(R_PrimCursor, R_FaceCursor, R_VertBase, R_OtBase),
|
|
||||||
* tape_regs(R_PrimCursor, R_FaceCursor))
|
|
||||||
* internal MipsAtom_(cube_tri) {
|
|
||||||
* atom_label(culling),
|
* atom_label(culling),
|
||||||
* // ... atom body ...
|
* // ... atom body ...
|
||||||
|
* atom_offset(culling, bounds_chk) // branch target, validated
|
||||||
|
* // ... atom body ...
|
||||||
* atom_label(bounds_chk),
|
* atom_label(bounds_chk),
|
||||||
* };
|
* };
|
||||||
*
|
*
|
||||||
* atom_offset(culling, bounds_chk) // ← branch target, validated
|
|
||||||
*
|
*
|
||||||
* RBIND pattern — `Binds_*` is the contract
|
* Data Binding pattern -- atom_bind as a sub-call of atom_info
|
||||||
*
|
*
|
||||||
* // Wave-context register layout (declarative):
|
* // Wave-context register layout (declarative):
|
||||||
* typedef struct Binds_TrackFaceBatch {
|
* typedef Struct_(Binds_TrackFaceBatch) {
|
||||||
* U4 R_PrimCursor, R_FaceCursor,
|
* U4 PrimCursor;
|
||||||
* R_VertBase, R_OtBase;
|
* U4 FaceCursor;
|
||||||
* } Binds_TrackFaceBatch;
|
* U4 VertBase;
|
||||||
*
|
* U4 OtBase;
|
||||||
* atom_resource(rbind_track_face_batch, "track_face_batch_42")
|
* };
|
||||||
* atom_region (rbind_track_face_batch, HEAP_3D)
|
* MipsAtom_(rbind_track_face_batch) atom_info(
|
||||||
* atom_group (rbind_track_face_batch, GROUP_LOAD_FACES)
|
* atom_bind(Binds_TrackFaceBatch)
|
||||||
* atom_cadence (rbind_track_face_batch, CADENCE_ONDEMAND)
|
* , atom_writes(R_PrimCursor, R_FaceCursor, R_VertBase, R_OtBase)
|
||||||
* atom_async (rbind_track_face_batch, true)
|
* ){ ... };
|
||||||
*
|
|
||||||
* atom_bind(rbind_track_face_batch, Binds_TrackFaceBatch,
|
|
||||||
* tape_regs(R_PrimCursor, R_FaceCursor, R_VertBase, R_OtBase))
|
|
||||||
* internal MipsAtom_(rbind_track_face_batch) { ... };
|
|
||||||
*
|
*
|
||||||
* Annotation rules
|
* Annotation rules
|
||||||
* ----------------
|
* ----------------
|
||||||
* 1. Each MipsAtom_(name) needs EXACTLY ONE atom_*() macro on the line
|
* 1. atom_info(...) is OPTIONAL. Most atoms have no annotation.
|
||||||
* immediately above. No annotation = orphan (warning). Two annotations
|
* Atoms without atom_info are silently skipped by the metaprogram.
|
||||||
* on the same name = duplicate (error).
|
|
||||||
*
|
*
|
||||||
* 2. atom_init and atom_terminate take only the name.
|
* 2. If present, atom_info takes up to three sub-calls, all
|
||||||
|
* order-independent within the arg list:
|
||||||
|
* - atom_bind(Binds_X) (optional; only for rbind atoms)
|
||||||
|
* - atom_reads(...) (optional; wave-context registers)
|
||||||
|
* - atom_writes(...) (optional; wave-context registers)
|
||||||
*
|
*
|
||||||
* 3. atom_setup and atom_commit take name + reads.
|
* 3. atom_bind(Binds_X) pins the ABI-struct shape -- the metaprogram
|
||||||
|
* cross-references Binds_X against the
|
||||||
|
* `typedef struct Binds_X { ... } Binds_X;` declaration.
|
||||||
*
|
*
|
||||||
* 4. atom_bind takes name + Binds_* type + writes.
|
* 4. atom_reads(...) and atom_writes(...) args are wave-context
|
||||||
|
* registers: R_PrimCursor / R_FaceCursor / R_VertBase / R_OtBase.
|
||||||
|
* Closed set. GTE / SP / DMA / I/O state is declared in source
|
||||||
|
* comments, not in atom_reads/atom_writes.
|
||||||
*
|
*
|
||||||
* 5. atom_annot takes name + phase token + reads + writes.
|
* 5. atom_label(name) is an anchor -- the macro is empty in C; the
|
||||||
* Phase tokens: phase_init / phase_bind / phase_setup / phase_work /
|
* metaprogram records the marker at the current pos for offset
|
||||||
* phase_commit / phase_terminate.
|
* calculation.
|
||||||
*
|
*
|
||||||
* 6. Optional pragmas (atom_resource / atom_region / atom_group /
|
* 6. atom_offset(F, T) is resolved by gen/atom_offsets.h, generated
|
||||||
* atom_cadence / atom_async) attach metadata to the atom. They can
|
* from the atom_label markers.
|
||||||
* appear in any order, with one per atom. They're independent of the
|
|
||||||
* atom_*() macro — multiple pragmatics are fine.
|
|
||||||
*
|
|
||||||
* ============================================================================
|
|
||||||
*
|
|
||||||
* WHY A SEPARATE LAYER (not just put everything in source comments)?
|
|
||||||
*
|
|
||||||
* Source comments are invisible to the compiler. Annotations live in the
|
|
||||||
* source as actual C tokens, so:
|
|
||||||
* - they can never silently get out of sync with the code (the build
|
|
||||||
* fails at preprocessing if the metaprogram disagrees)
|
|
||||||
* - they can be cross-validated against metadata (build fails if a
|
|
||||||
* WORD_COUNT entry drifts away from the .word count in source)
|
|
||||||
* - they make the C compiler a witness ("there's a marker here, and
|
|
||||||
* it's labelled, and it has arguments") without making the C compile
|
|
||||||
* itself do any work
|
|
||||||
*
|
|
||||||
* ============================================================================
|
|
||||||
*/
|
*/
|
||||||
|
|
||||||
#ifdef INTELLISENSE_DIRECTIVES
|
#ifdef INTELLISENSE_DIRECTIVES
|
||||||
#pragma once
|
#pragma once
|
||||||
// #include <stdint.h>
|
// #include <stdint.h>
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
/* ============================================================================
|
/* ============================================================================
|
||||||
* PHASE TOKENS — strings, used as the second arg to atom_annot(...)
|
* WAVE-CONTEXT REGISTERS -- canonical register set for the tape wave model.
|
||||||
*
|
|
||||||
* Why strings? They preserve the metaprogram's ability to read phase directly
|
|
||||||
* from the source-as-written, even when the macro isn't expanded. The Lua
|
|
||||||
* tool also has a MACRO_EXPANSION table for resolving phase_* source-level
|
|
||||||
* references.
|
|
||||||
*
|
|
||||||
* atom_annot(cube_tri, phase_work, ...) ← legal
|
|
||||||
* atom_annot(cube_tri, "work", ...) ← legal (and equivalent)
|
|
||||||
* atom_annot(cube_tri, phase_setup, ...) ← legal
|
|
||||||
*
|
|
||||||
* ============================================================================*/
|
|
||||||
|
|
||||||
#define phase_init "init"
|
|
||||||
#define phase_bind "bind"
|
|
||||||
#define phase_setup "setup"
|
|
||||||
#define phase_work "work"
|
|
||||||
#define phase_commit "commit"
|
|
||||||
#define phase_terminate "terminate"
|
|
||||||
|
|
||||||
/* ============================================================================
|
|
||||||
* WAVE-CONTEXT REGISTERS — canonical register set for the tape wave model.
|
|
||||||
*
|
|
||||||
* The tape-atom runtime carries four registers across a wave:
|
|
||||||
*
|
*
|
||||||
* R_PrimCursor output pointer into the prim arena (next OT entry to write)
|
* R_PrimCursor output pointer into the prim arena (next OT entry to write)
|
||||||
* R_FaceCursor input pointer into the face array (next face to consume)
|
* R_FaceCursor input pointer into the face array (next face to consume)
|
||||||
* R_VertBase base pointer into the vertex arena (this wave's vertices)
|
* R_VertBase base pointer into the vertex arena (this wave's vertices)
|
||||||
* R_OtBase base pointer into the ordering table (this wave's OT slot)
|
* R_OtBase base pointer into the ordering table (this wave's OT slot)
|
||||||
*
|
*
|
||||||
* Each atom declares its reads/writes against this canonical set. The Lua
|
* Closed set. If your atom needs to touch GTE / SP / DMA / other side state,
|
||||||
* tool rejects wave-context positions that reference any other register
|
* declare it at the source level as you normally would -- but DO NOT put
|
||||||
* (warning today — the C compiler's R_T4..R_T7 / R_RA / etc. aliases are
|
* those registers in atom_reads/atom_writes.
|
||||||
* implementation details and not part of the typed surface).
|
|
||||||
*
|
|
||||||
* If your atom needs to touch GTE / SP / DMA / other side state, declare it
|
|
||||||
* at the source level as you normally would — but DO NOT put those registers
|
|
||||||
* in tape_regs(...). Wave-context is a closed set.
|
|
||||||
*
|
*
|
||||||
* ============================================================================*/
|
* ============================================================================*/
|
||||||
|
|
||||||
/* ============================================================================
|
/* ============================================================================
|
||||||
* REGION TOKENS — memory regions atoms may allocate from or write into.
|
* atom_reads(...) / atom_writes(...) -- wave-context register list
|
||||||
*
|
*
|
||||||
* Use atom_region(name, REGION) to declare. The Lua tool validates that the
|
* atom_reads(R_PrimCursor, R_FaceCursor)
|
||||||
* region is in this set, AND that:
|
* -> (R_PrimCursor, R_FaceCursor) // comma-evaluated, discarded
|
||||||
* - rbind atoms declare the source region (usually HEAP_3D or CDROM_STREAM)
|
|
||||||
* - work atoms declare the destination region (the arena they push to)
|
|
||||||
* - commit atoms must declare a region equal to what setup wrote, so the
|
|
||||||
* C-side mirror is consistent
|
|
||||||
*
|
|
||||||
* Add new regions by extending this list and the metaprogram's KNOWN_REGIONS.
|
|
||||||
* Don't add regions ad-hoc — every new region becomes part of the contract.
|
|
||||||
*
|
|
||||||
* ============================================================================*/
|
|
||||||
#define REGION_PRIM_ARENA prim_arena /* OT/prim packet arena */
|
|
||||||
#define REGION_FACE_ARENA face_arena /* face index array */
|
|
||||||
#define REGION_VERTEX_ARENA vertex_arena /* vertex pool */
|
|
||||||
#define REGION_OT_ARENA ot_arena /* ordering-table array */
|
|
||||||
#define REGION_HEAP_3D heap_3d_models /* loaded model heap */
|
|
||||||
#define REGION_CDROM_STREAM cdrom_stream /* CDROM read buffer */
|
|
||||||
#define REGION_VRAM vram_heap /* VRAM texture/GPU buffer */
|
|
||||||
|
|
||||||
/* ============================================================================
|
|
||||||
* CADENCE TOKENS — how often the atom runs.
|
|
||||||
*
|
|
||||||
* frame runs every vsync (rendering, input poll)
|
|
||||||
* once runs exactly once per process lifetime (init, terminate)
|
|
||||||
* ondemand runs when triggered by event (CDROM load, async DMA complete)
|
|
||||||
*
|
|
||||||
* Used as a hint for the metaprogram to flag:
|
|
||||||
* - frame-cadence atoms that have side effects (they'll be hit many times,
|
|
||||||
* so avoid global state mutation unless it's idempotent)
|
|
||||||
* - once-cadence atoms inside "if (frame_count == 0)" guards (the guard
|
|
||||||
* is then provably one-shot, the metaprogram can lift initialization)
|
|
||||||
* - ondemand atoms that are missed by the wave scheduler (forces async
|
|
||||||
* and discards yield results without further processing)
|
|
||||||
*
|
|
||||||
* ============================================================================*/
|
|
||||||
#define CADENCE_FRAME frame
|
|
||||||
#define CADENCE_ONCE once
|
|
||||||
#define CADENCE_ONDEMAND ondemand
|
|
||||||
|
|
||||||
/* ============================================================================
|
|
||||||
* tape_regs(...) — wave-context register list
|
|
||||||
*
|
|
||||||
* tape_regs(R_PrimCursor, R_FaceCursor) → (R_PrimCursor, R_FaceCursor)
|
|
||||||
*
|
*
|
||||||
* The macro produces a comma-evaluated expression that the C compiler
|
* The macro produces a comma-evaluated expression that the C compiler
|
||||||
* silently discards (it's wrapped in parentheses in the call argument
|
* silently discards (it sits in an unused arg position -- the result is
|
||||||
* position — the result is never bound). The Lua tool pattern-matches the
|
* never bound). The Lua tool pattern-matches the "atom_reads(...)" /
|
||||||
* "tape_regs(...)" token to extract the list.
|
* "atom_writes(...)" token to extract the list.
|
||||||
*
|
*
|
||||||
* You can have at most one tape_regs(...) in the reads slot and one in the
|
* You can have at most one atom_reads(...) and at most one atom_writes(...)
|
||||||
* writes slot of atom_annot. To declare multiple disjoint sets (rare), just
|
* in an atom_info(...) call. To declare multiple disjoint sets (rare), just
|
||||||
* declare the union — the metaprogram doesn't track which reads need which
|
* declare the union -- the metaprogram doesn't track which reads need which
|
||||||
* writes at this granularity.
|
* writes at this granularity.
|
||||||
*
|
*
|
||||||
* ============================================================================*/
|
* ============================================================================*/
|
||||||
@@ -212,227 +128,45 @@
|
|||||||
/* ============================================================================
|
/* ============================================================================
|
||||||
* ATOM ANNOTATION MACROS
|
* ATOM ANNOTATION MACROS
|
||||||
*
|
*
|
||||||
* Each expands to `__attribute__((annotate("kind"))) MipsAtom_(name)` —
|
* atom_info -- single unified annotation. OPTIONAL. Most atoms have none.
|
||||||
* the GCC attribute is accepted under -Wno-attributes (already in your
|
*
|
||||||
* build flags) and stripped at runtime. The annotation string is just the
|
* MipsAtom_(cube_tri) atom_info(
|
||||||
* macro kind ("atom_annot", "atom_bind", etc.) — the metaprogram reads
|
* atom_reads (R_PrimCursor, R_FaceCursor, R_VertBase, R_OtBase)
|
||||||
* the macro call's full args list from the source-as-written.
|
* , atom_writes(R_PrimCursor, R_FaceCursor)
|
||||||
|
* ){ ... };
|
||||||
|
*
|
||||||
|
* Shape (sub-args order-independent; all optional):
|
||||||
|
* - atom_bind(Binds_X): at most one; pins the ABI-struct shape
|
||||||
|
* - atom_reads(...): at most one; comma-list of wave-context registers
|
||||||
|
* - atom_writes(...): at most one; comma-list of wave-context registers
|
||||||
|
*
|
||||||
|
* No phase token for now. The metaprogram doesn't check ordering across
|
||||||
|
* atoms -- phases (init / bind / setup / work / commit / terminate) will
|
||||||
|
* be reintroduced when ordering checks are added.
|
||||||
|
*
|
||||||
|
* The macro expands to a C comment (or to nothing). The C compiler does
|
||||||
|
* no work. The metaprogram reads the source-as-written directly.
|
||||||
*
|
*
|
||||||
* ============================================================================*/
|
* ============================================================================*/
|
||||||
|
#define atom_info(...) /* atom_info(__VA_ARGS__) */
|
||||||
|
|
||||||
/* ----------------------------------------------------------------------------
|
/* ----------------------------------------------------------------------------
|
||||||
* atom_init — entry into tape_runtime_main
|
* atom_bind(Binds_X) -- rbind sub-call of atom_info
|
||||||
*
|
*
|
||||||
* atom_init(tape_main)
|
* MipsAtom_(rbind_cube_tri) atom_info(
|
||||||
* internal MipsAtom_(tape_main) { ... };
|
* atom_bind(Binds_CubeTri)
|
||||||
|
* , atom_writes(R_PrimCursor, R_FaceCursor, R_VertBase, R_OtBase)
|
||||||
|
* ){ ... };
|
||||||
*
|
*
|
||||||
* Implies: no reads, no writes (wave-context not established yet).
|
* The Binds_X MUST be a typedef'd type (declared via
|
||||||
* ----------------------------------------------------------------------------*/
|
|
||||||
#define atom_init(name) __attribute__((annotate("atom_init")))
|
|
||||||
|
|
||||||
/* ----------------------------------------------------------------------------
|
|
||||||
* atom_terminate — exit from tape_runtime_main
|
|
||||||
*
|
|
||||||
* atom_terminate(tape_exit)
|
|
||||||
* internal MipsAtom_(tape_exit) { ... };
|
|
||||||
*
|
|
||||||
* Implies: no reads, no writes (wave-context destroyed at this point).
|
|
||||||
* ----------------------------------------------------------------------------*/
|
|
||||||
#define atom_terminate(name) __attribute__((annotate("atom_terminate")))
|
|
||||||
|
|
||||||
/* ----------------------------------------------------------------------------
|
|
||||||
* atom_setup — pre-work atom: prepares engine state (e.g., set_gte_world)
|
|
||||||
*
|
|
||||||
* atom_setup(set_gte_world, tape_regs(R_TapePtr))
|
|
||||||
* internal MipsAtom_(set_gte_world) { ... };
|
|
||||||
*
|
|
||||||
* Reads: anything (the engine state you're reading)
|
|
||||||
* Writes: engine state (GTE / DMA / etc. — declared in source, not part of
|
|
||||||
* wave-context, so doesn't go in tape_regs)
|
|
||||||
*
|
|
||||||
* The metaprogram checks that setup is followed (in atomic order) by a work
|
|
||||||
* atom in the same wave — there's no point in setting up state if no one
|
|
||||||
* reads it.
|
|
||||||
* ----------------------------------------------------------------------------*/
|
|
||||||
#define atom_setup(name, reads) __attribute__((annotate("atom_setup")))
|
|
||||||
|
|
||||||
/* ----------------------------------------------------------------------------
|
|
||||||
* atom_commit — post-work atom: flushes wave-context back to C-side state
|
|
||||||
*
|
|
||||||
* atom_commit(sync_prim_cursor, tape_regs(R_PrimCursor))
|
|
||||||
* internal MipsAtom_(sync_prim_cursor) { ... };
|
|
||||||
*
|
|
||||||
* Reads: wave-context registers (the ones you sync back to C)
|
|
||||||
* Writes: C-side mirror (declared in source — not part of wave-context)
|
|
||||||
*
|
|
||||||
* The metaprogram checks that commit is preceded (in atomic order) by a
|
|
||||||
* work atom that wrote the registers this commit is reading.
|
|
||||||
* ----------------------------------------------------------------------------*/
|
|
||||||
#define atom_commit(name, reads) __attribute__((annotate("atom_commit")))
|
|
||||||
|
|
||||||
/* ----------------------------------------------------------------------------
|
|
||||||
* atom_bind — rbind atom: read wave-context registers from tape pointer
|
|
||||||
*
|
|
||||||
* atom_bind(rbind_cube_tri, Binds_CubeTri,
|
|
||||||
* tape_regs(R_PrimCursor, R_FaceCursor, R_VertBase, R_OtBase))
|
|
||||||
* internal MipsAtom_(rbind_cube_tri) { ... };
|
|
||||||
*
|
|
||||||
* The binds_struct MUST be a typedef'd type (declared via
|
|
||||||
* `typedef struct Binds_X { ... } Binds_X;` somewhere in the source).
|
* `typedef struct Binds_X { ... } Binds_X;` somewhere in the source).
|
||||||
* The Lua tool cross-references this. Missing struct = error.
|
* The Lua tool cross-references this. Missing struct = error.
|
||||||
*
|
*
|
||||||
* Implicit: reads R_TapePtr, writes the four wave-context registers.
|
* atom_bind is a SUB-CALL of atom_info, not a standalone annotation macro.
|
||||||
|
*
|
||||||
|
* The macro expands to a C comment. The metaprogram reads source-as-written.
|
||||||
* ----------------------------------------------------------------------------*/
|
* ----------------------------------------------------------------------------*/
|
||||||
#define atom_bind(name, binds_struct, writes) __attribute__((annotate("atom_bind")))
|
#define atom_bind(binds_struct) /* atom_bind(binds_struct) */
|
||||||
|
|
||||||
/* ----------------------------------------------------------------------------
|
|
||||||
* atom_annot — generic work atom with explicit phase
|
|
||||||
*
|
|
||||||
* atom_annot(cube_tri, phase_work,
|
|
||||||
* tape_regs(R_PrimCursor, R_FaceCursor, R_VertBase, R_OtBase),
|
|
||||||
* tape_regs(R_PrimCursor, R_FaceCursor))
|
|
||||||
* internal MipsAtom_(cube_tri) { ... };
|
|
||||||
*
|
|
||||||
* Use this for the bulk of your atoms. For init/setup/commit/bind, prefer
|
|
||||||
* the convenience macros above — they pin the phase for you.
|
|
||||||
*
|
|
||||||
* The phase arg is one of: phase_init / phase_bind / phase_setup /
|
|
||||||
* phase_work / phase_commit / phase_terminate. Spelling mistakes are errors.
|
|
||||||
* ----------------------------------------------------------------------------*/
|
|
||||||
#define atom_annot(name, phase, reads, writes) __attribute__((annotate("atom_annot")))
|
|
||||||
|
|
||||||
/* ============================================================================
|
|
||||||
* RESOURCE / GROUP / CADENCE / REGION / ASYNC — optional atom metadata
|
|
||||||
*
|
|
||||||
* These don't annotate the atom semantically (phase/reads/writes do that).
|
|
||||||
* They attach extra context that the metaprogram uses to catch:
|
|
||||||
* - same resource loaded twice in different ways
|
|
||||||
* - atoms that span multiple regions (likely bug — pick one)
|
|
||||||
* - frame-cadence atoms that should be once-cadence (perf / correctness)
|
|
||||||
* - ondemand atoms that aren't async (CDROM races)
|
|
||||||
*
|
|
||||||
* You can use as many as apply to a given atom, in any order, immediately
|
|
||||||
* above the atom_*() macro.
|
|
||||||
*
|
|
||||||
* ============================================================================*/
|
|
||||||
|
|
||||||
/* ----------------------------------------------------------------------------
|
|
||||||
* atom_resource — name the logical resource the atom references
|
|
||||||
*
|
|
||||||
* atom_resource(cube_tri, "model_ship_cube")
|
|
||||||
* atom_resource(load_track_faces, "track_lavender_field_0x42")
|
|
||||||
* atom_resource(play_engine_sfx, "sfx_engine_loop")
|
|
||||||
*
|
|
||||||
* Use any human-readable string. The metaprogram:
|
|
||||||
* - validates resource strings are non-empty and don't contain control chars
|
|
||||||
* - flags duplicates across atoms with the same name (two atoms claiming
|
|
||||||
* ownership of a resource is usually a refactor artifact or bug)
|
|
||||||
* - flags references to resources that no atom actually defines
|
|
||||||
*
|
|
||||||
* The arg is a STRING LITERAL, so it can't accidentally alias a variable.
|
|
||||||
* ----------------------------------------------------------------------------*/
|
|
||||||
#define atom_resource(name, res_id) //_Pragma("atom " #name " resource=" res_id)
|
|
||||||
|
|
||||||
/* ----------------------------------------------------------------------------
|
|
||||||
* atom_region — name the memory region the atom touches
|
|
||||||
*
|
|
||||||
* atom_region(cube_tri, REGION_PRIM_ARENA)
|
|
||||||
* atom_region(load_faces, REGION_HEAP_3D)
|
|
||||||
* atom_region(load_tex, REGION_VRAM)
|
|
||||||
*
|
|
||||||
* Use REGION_* tokens above. The metaprogram enforces the closed set.
|
|
||||||
*
|
|
||||||
* Edge cases the metaprogram catches:
|
|
||||||
* - rbind atom that doesn't declare a SOURCE region (where is it loading from?)
|
|
||||||
* - work atom with no destination region (where is it pushing to?)
|
|
||||||
* - region that disagrees with the Binds_* struct layout (you said it's a
|
|
||||||
* prim_arena rbind but the struct has 4 faces in it — wait, that's wrong)
|
|
||||||
* ----------------------------------------------------------------------------*/
|
|
||||||
#define atom_region(name, region) //_Pragma("atom " #name " region=" #region)
|
|
||||||
|
|
||||||
/* ----------------------------------------------------------------------------
|
|
||||||
* atom_group — bundle atoms into a logical batch (track-load, sound-load, etc.)
|
|
||||||
*
|
|
||||||
* atom_group(load_track_face_42, GROUP_LOAD_FACES)
|
|
||||||
* atom_group(load_track_face_43, GROUP_LOAD_FACES)
|
|
||||||
* atom_group(swap_face_42_43, GROUP_VISIBILITY_SWAP)
|
|
||||||
*
|
|
||||||
* Use any token as the group id. The metaprogram:
|
|
||||||
* - validates all atoms in a group emit their waves in the same tb_group
|
|
||||||
* (no spawning other waves inside a group)
|
|
||||||
* - flags groups with only one member (probably a typo — meant to be a group?)
|
|
||||||
* - validates cross-group edges (no atom reads what another group writes,
|
|
||||||
* unless explicitly grouped together)
|
|
||||||
*
|
|
||||||
* Useful when:
|
|
||||||
* - subdivisible work (track-face batches, polygon subdivision) needs to
|
|
||||||
* confirm that all batches of one logical visible scene are emitted
|
|
||||||
* together
|
|
||||||
* - async loads (CDROM -> VRAM) need to be grouped so all batches complete
|
|
||||||
* before the swap
|
|
||||||
*
|
|
||||||
* Use GROUPS for sound effects to track which sound plays during which atom,
|
|
||||||
* which is needed if the sound tool ever has to validate "this atom is the
|
|
||||||
* trigger for an audio play".
|
|
||||||
* ----------------------------------------------------------------------------*/
|
|
||||||
#define atom_group(name, group_id) //_Pragma("atom " #name " group=" #group_id)
|
|
||||||
|
|
||||||
/* ----------------------------------------------------------------------------
|
|
||||||
* atom_cadence — declare execution frequency
|
|
||||||
*
|
|
||||||
* atom_cadence(render_frame, CADENCE_FRAME) // every vsync
|
|
||||||
* atom_cadence(load_track_faces, CADENCE_ONDEMAND) // on demand
|
|
||||||
* atom_cadence(init_heap, CADENCE_ONCE) // process lifetime
|
|
||||||
*
|
|
||||||
* Default (no atom_cadence call) is CADENCE_FRAME — most atoms run every
|
|
||||||
* frame. Override explicitly when not.
|
|
||||||
*
|
|
||||||
* The metaprogram's checks:
|
|
||||||
* - CADENCE_ONCE atoms inside `if (frame == 0)` or `if (!initialized)` are
|
|
||||||
* tagged, validating that guards are required (or warning if missing)
|
|
||||||
* - CADENCE_FRAME atoms that mutate state outside the wave context get
|
|
||||||
* flagged (likely a bug — state should persist through commits)
|
|
||||||
* - CADENCE_ONDEMAND atoms must have atom_async — otherwise the trigger
|
|
||||||
* mechanism is undefined
|
|
||||||
* ----------------------------------------------------------------------------*/
|
|
||||||
#define atom_cadence(name, cadence) //_Pragma("atom " #name " cadence=" #cadence)
|
|
||||||
|
|
||||||
/* ----------------------------------------------------------------------------
|
|
||||||
* atom_async — declare whether the atom yields / interacts with CDROM DMA
|
|
||||||
*
|
|
||||||
* atom_async(load_track_tex, true) // CDROM read yield
|
|
||||||
* atom_async(load_vram, true) // VRAM upload DMA
|
|
||||||
* atom_async(render_frame, false) // pure compute, no async
|
|
||||||
*
|
|
||||||
* The metaprogram requires this for CADENCE_ONDEMAND atoms. For
|
|
||||||
* CADENCE_FRAME, it's optional but documents intent.
|
|
||||||
*
|
|
||||||
* Note: CDROM ATOMS in Psy-Q are typically implemented as a chain of
|
|
||||||
* "async-init" atom followed by a "wait-for-completion" atom. Both atoms
|
|
||||||
* should be marked async=true, and both should have the same resource/group
|
|
||||||
* tag (so the metaprogram can verify they're paired).
|
|
||||||
* ----------------------------------------------------------------------------*/
|
|
||||||
#define atom_async(name, is_async) //_Pragma("atom " #name " async=" #is_async)
|
|
||||||
|
|
||||||
/* ============================================================================
|
|
||||||
* WORD-COUNT ANNOTATION FOR A #define MAC
|
|
||||||
*
|
|
||||||
* tape_words(mac_yield, 1)
|
|
||||||
* #define mac_yield() \
|
|
||||||
* load_word(R_AtomJmp, R_TapePtr, 0), \
|
|
||||||
* add_ui_self(R_TapePtr, 4), \
|
|
||||||
* jump_reg(R_AtomJmp), \
|
|
||||||
* nop
|
|
||||||
*
|
|
||||||
* The compiler accepts the unknown _Pragma. The Lua tool reads it and
|
|
||||||
* cross-checks against WORD_COUNT(mac_yield, 1) in tape_atom.metadata.h.
|
|
||||||
* If they disagree, build fails.
|
|
||||||
*
|
|
||||||
* Use sparingly — only on multi-word macros (single-word ones don't need
|
|
||||||
* drift tracking; they're checked by the .word-count pass anyway).
|
|
||||||
*
|
|
||||||
* ============================================================================*/
|
|
||||||
#define tape_words(name, n) //_Pragma(#name " tape_atom words=" #n)
|
|
||||||
|
|
||||||
/* ============================================================================
|
/* ============================================================================
|
||||||
* atom_label / atom_offset — branch target machinery
|
* atom_label / atom_offset — branch target machinery
|
||||||
|
|||||||
@@ -4,8 +4,6 @@
|
|||||||
// Auto-generated by tape_atom_annotation_pass.lua — DO NOT EDIT
|
// Auto-generated by tape_atom_annotation_pass.lua — DO NOT EDIT
|
||||||
// Source: C:\projects\Pikuma\ps1\code\duffle\lottes_tape.h
|
// Source: C:\projects\Pikuma\ps1\code\duffle\lottes_tape.h
|
||||||
// Component atoms (MipsAtomComp_(ac_*)) -> macro variants (mac_*)
|
// Component atoms (MipsAtomComp_(ac_*)) -> macro variants (mac_*)
|
||||||
// + auto word-counts (so tape_atom.metadata.h stays manual-only
|
|
||||||
// for encoding macros).
|
|
||||||
|
|
||||||
#ifndef WORD_COUNT
|
#ifndef WORD_COUNT
|
||||||
#define WORD_COUNT(name, count) enum { words_##name = (count) };
|
#define WORD_COUNT(name, count) enum { words_##name = (count) };
|
||||||
@@ -26,7 +24,7 @@ WORD_COUNT(mac_yield, 4)
|
|||||||
WORD_COUNT(mac_load_tri_indices, 3)
|
WORD_COUNT(mac_load_tri_indices, 3)
|
||||||
|
|
||||||
/* Words: 18; Translates indices to vertex addresses and pushes them to GTE */
|
/* Words: 18; Translates indices to vertex addresses and pushes them to GTE */
|
||||||
#define mac_load_tri_verts(...) \
|
#define mac_gte_load_tri_verts(...) \
|
||||||
shift_lleft(R_AT, R_T0, v3s2_byteoff) \
|
shift_lleft(R_AT, R_T0, v3s2_byteoff) \
|
||||||
, add_u_self(R_AT, R_VertBase) \
|
, add_u_self(R_AT, R_VertBase) \
|
||||||
, load_word(R_V0, R_AT, O_(V3_S2,x)) \
|
, load_word(R_V0, R_AT, O_(V3_S2,x)) \
|
||||||
@@ -45,7 +43,7 @@ WORD_COUNT(mac_load_tri_indices, 3)
|
|||||||
, load_word(R_V1, R_AT, O_(V3_S2,z)) \
|
, load_word(R_V1, R_AT, O_(V3_S2,z)) \
|
||||||
, gte_mv_to_data_r(R_V0, C2_VXY2) \
|
, gte_mv_to_data_r(R_V0, C2_VXY2) \
|
||||||
, gte_mv_to_data_r(R_V1, C2_VZ2)
|
, gte_mv_to_data_r(R_V1, C2_VZ2)
|
||||||
WORD_COUNT(mac_load_tri_verts, 18)
|
WORD_COUNT(mac_gte_load_tri_verts, 18)
|
||||||
|
|
||||||
/* Words: 11; Correctly inserts a primitive into the Ordering Table linked list.
|
/* Words: 11; Correctly inserts a primitive into the Ordering Table linked list.
|
||||||
* Hardcoded for Poly_F3 (5 words). For Poly_G4, use ac_insert_ot_tag_g4. */
|
* Hardcoded for Poly_F3 (5 words). For Poly_G4, use ac_insert_ot_tag_g4. */
|
||||||
|
|||||||
@@ -27,7 +27,7 @@
|
|||||||
* descriptive; no vendor alias is provided for them.
|
* descriptive; no vendor alias is provided for them.
|
||||||
*
|
*
|
||||||
* The vendor mnemonics are NOT registered with the duffle word-count
|
* The vendor mnemonics are NOT registered with the duffle word-count
|
||||||
* metadata (tape_atom.metadata.h). They expand to the duffle canonical
|
* metadata (word_counts.metadata.h). They expand to the duffle canonical
|
||||||
* macros which DO have word-count entries (the ones emitted by
|
* macros which DO have word-count entries (the ones emitted by
|
||||||
* mac_format_f3_color / mac_gte_store_f3 / etc.). Verification: V13
|
* mac_format_f3_color / mac_gte_store_f3 / etc.). Verification: V13
|
||||||
* (objdump byte-identical) holds.
|
* (objdump byte-identical) holds.
|
||||||
|
|||||||
+7
-5
@@ -440,6 +440,8 @@ enum { _C2_TX_SUBS_ = 0
|
|||||||
#define gte_cmdw_rtpt (gte_cmd_base | enc_gte_cmd(gte_cmd_rtpt ) | gte_cmdw_psyq_compat)
|
#define gte_cmdw_rtpt (gte_cmd_base | enc_gte_cmd(gte_cmd_rtpt ) | gte_cmdw_psyq_compat)
|
||||||
#define gte_cmdw_nclip (gte_cmd_base | enc_gte_cmd(gte_cmd_nclip))
|
#define gte_cmdw_nclip (gte_cmd_base | enc_gte_cmd(gte_cmd_nclip))
|
||||||
#define gte_cmdw_op (gte_cmd_base | enc_gte_cmd(gte_cmd_op ))
|
#define gte_cmdw_op (gte_cmd_base | enc_gte_cmd(gte_cmd_op ))
|
||||||
|
#define gte_cmdw_outer_product gte_cmdw_op /* "outer product" -- NOCASH/Sdk terminology */
|
||||||
|
#define gte_cmdw_wedge gte_cmdw_op /* "wedge product" -- geometric-algebra terminology */
|
||||||
#define gte_cmdw_mvmva (gte_cmd_base | enc_gte_cmd(gte_cmd_mvmva))
|
#define gte_cmdw_mvmva (gte_cmd_base | enc_gte_cmd(gte_cmd_mvmva))
|
||||||
|
|
||||||
#define gte_cmdw_rotate_translate_perspective_single gte_cmdw_rtps
|
#define gte_cmdw_rotate_translate_perspective_single gte_cmdw_rtps
|
||||||
@@ -713,14 +715,14 @@ enum {
|
|||||||
asm_words( \
|
asm_words( \
|
||||||
load_word(R_T5, R_T4, 0) \
|
load_word(R_T5, R_T4, 0) \
|
||||||
, load_word(R_T6, R_T4, 4) \
|
, load_word(R_T6, R_T4, 4) \
|
||||||
, gte_mt( R_T5, 0) \
|
, gte_mv_to_data_r( R_T5, 0) \
|
||||||
, gte_mt( R_T6, 1) \
|
, gte_mv_to_data_r( R_T6, 1) \
|
||||||
, load_word(R_T5, R_T4, 8) \
|
, load_word(R_T5, R_T4, 8) \
|
||||||
, load_word(R_T6, R_T4, 12) \
|
, load_word(R_T6, R_T4, 12) \
|
||||||
, load_word(R_T4, R_T4, 16) \
|
, load_word(R_T4, R_T4, 16) \
|
||||||
, gte_mt( R_T5, 2) \
|
, gte_mv_to_data_r( R_T5, 2) \
|
||||||
, gte_mt( R_T6, 3) \
|
, gte_mv_to_data_r( R_T6, 3) \
|
||||||
, gte_mt( R_T4, 4) \
|
, gte_mv_to_data_r( R_T4, 4) \
|
||||||
) \
|
) \
|
||||||
, r_use(r0) \
|
, r_use(r0) \
|
||||||
asm_clobber: clbr_volatile_gprs, rlit(R_T4), rlit(R_T5), rlit(R_T6) \
|
asm_clobber: clbr_volatile_gprs, rlit(R_T4), rlit(R_T5), rlit(R_T6) \
|
||||||
|
|||||||
@@ -21,12 +21,6 @@
|
|||||||
* gte_swc2(rt, base, off) -> gte_sw(rt, base, off)
|
* gte_swc2(rt, base, off) -> gte_sw(rt, base, off)
|
||||||
* (the lower-level vector variants gte_lw_v0_xy etc. don't have
|
* (the lower-level vector variants gte_lw_v0_xy etc. don't have
|
||||||
* vendor mnemonics; they're already gte_-prefixed and short)
|
* vendor mnemonics; they're already gte_-prefixed and short)
|
||||||
*
|
|
||||||
* The vendor mnemonics are NOT registered with the duffle word-count
|
|
||||||
* metadata (tape_atom.metadata.h). They expand to the duffle canonical
|
|
||||||
* macros which DO have word-count entries. Verification: V3 (objdump
|
|
||||||
* byte-identical) holds.
|
|
||||||
*
|
|
||||||
* ============================================================================ */
|
* ============================================================================ */
|
||||||
|
|
||||||
#ifdef INTELLISENSE_DIRECTIVES
|
#ifdef INTELLISENSE_DIRECTIVES
|
||||||
|
|||||||
@@ -31,13 +31,9 @@ typedef Slice_MipsCode MipsAtom;
|
|||||||
// FI_ MipsAtom ac_X(args) { MipsCode ac_X[] align_(4) = { body }; return slice_from_array(MipsCode, ac_X); }
|
// FI_ MipsAtom ac_X(args) { MipsCode ac_X[] align_(4) = { body }; return slice_from_array(MipsCode, ac_X); }
|
||||||
#define MipsAtomComp_Proc_(sym, ...) { MipsCode sym [] align_(4) = __VA_ARGS__; return slice_from_array(MipsCode, sym); }
|
#define MipsAtomComp_Proc_(sym, ...) { MipsCode sym [] align_(4) = __VA_ARGS__; return slice_from_array(MipsCode, sym); }
|
||||||
|
|
||||||
// Auto-generated component macros (<module>/gen/<dir>/<dir>.macs.h)
|
// Auto-generated component macros (<module>/gen/<dir>/<dir>.macs.h) are included manually by the unity build.
|
||||||
// are included manually by the unity build. The metaprogram puts them
|
|
||||||
|
|
||||||
/* Register aliases (moved up from the Tape Drive region below so that
|
/* Register aliases */
|
||||||
* mac_yield's body and the Mips Atom Builder functions can reference
|
|
||||||
* them. The C compiler processes the file top-to-bottom, so the enum
|
|
||||||
* must be visible before any use.) */
|
|
||||||
enum {
|
enum {
|
||||||
R_AtomJmp = R_T9,
|
R_AtomJmp = R_T9,
|
||||||
R_TapePtr = R_T8, /* The Instruction Stream Pointer */
|
R_TapePtr = R_T8, /* The Instruction Stream Pointer */
|
||||||
@@ -126,7 +122,7 @@ MipsAtomComp_(ac_load_tri_indices) {
|
|||||||
};
|
};
|
||||||
|
|
||||||
/* Words: 18; Translates indices to vertex addresses and pushes them to GTE */
|
/* Words: 18; Translates indices to vertex addresses and pushes them to GTE */
|
||||||
MipsAtomComp_(ac_load_tri_verts) {
|
MipsAtomComp_(ac_gte_load_tri_verts) {
|
||||||
shift_lleft(R_AT, R_T0, v3s2_byteoff), add_u_self(R_AT, R_VertBase), load_word(R_V0, R_AT, O_(V3_S2,x)), load_word(R_V1, R_AT, O_(V3_S2,z)), gte_mv_to_data_r(R_V0, C2_VXY0), gte_mv_to_data_r(R_V1, C2_VZ0),
|
shift_lleft(R_AT, R_T0, v3s2_byteoff), add_u_self(R_AT, R_VertBase), load_word(R_V0, R_AT, O_(V3_S2,x)), load_word(R_V1, R_AT, O_(V3_S2,z)), gte_mv_to_data_r(R_V0, C2_VXY0), gte_mv_to_data_r(R_V1, C2_VZ0),
|
||||||
shift_lleft(R_AT, R_T1, v3s2_byteoff), add_u_self(R_AT, R_VertBase), load_word(R_V0, R_AT, O_(V3_S2,x)), load_word(R_V1, R_AT, O_(V3_S2,z)), gte_mv_to_data_r(R_V0, C2_VXY1), gte_mv_to_data_r(R_V1, C2_VZ1),
|
shift_lleft(R_AT, R_T1, v3s2_byteoff), add_u_self(R_AT, R_VertBase), load_word(R_V0, R_AT, O_(V3_S2,x)), load_word(R_V1, R_AT, O_(V3_S2,z)), gte_mv_to_data_r(R_V0, C2_VXY1), gte_mv_to_data_r(R_V1, C2_VZ1),
|
||||||
shift_lleft(R_AT, R_T2, v3s2_byteoff), add_u_self(R_AT, R_VertBase), load_word(R_V0, R_AT, O_(V3_S2,x)), load_word(R_V1, R_AT, O_(V3_S2,z)), gte_mv_to_data_r(R_V0, C2_VXY2), gte_mv_to_data_r(R_V1, C2_VZ2),
|
shift_lleft(R_AT, R_T2, v3s2_byteoff), add_u_self(R_AT, R_VertBase), load_word(R_V0, R_AT, O_(V3_S2,x)), load_word(R_V1, R_AT, O_(V3_S2,z)), gte_mv_to_data_r(R_V0, C2_VXY2), gte_mv_to_data_r(R_V1, C2_VZ2),
|
||||||
@@ -287,7 +283,10 @@ internal MipsAtom_(mips_flush_icache) {
|
|||||||
typedef Struct_(Binds_SetGteWorld) {
|
typedef Struct_(Binds_SetGteWorld) {
|
||||||
M3_S2* transform;
|
M3_S2* transform;
|
||||||
};
|
};
|
||||||
internal MipsAtom_(set_gte_world) {
|
internal MipsAtom_(set_gte_world) atom_info(
|
||||||
|
atom_bind(Binds_SetGteWorld)
|
||||||
|
, atom_reads(R_TapePtr)
|
||||||
|
){
|
||||||
/* Pop matrix address from tape into R_T3 ($11) */
|
/* Pop matrix address from tape into R_T3 ($11) */
|
||||||
load_word(R_T3, R_TapePtr, O_(Binds_SetGteWorld,transform)),
|
load_word(R_T3, R_TapePtr, O_(Binds_SetGteWorld,transform)),
|
||||||
add_ui_self( R_TapePtr, S_(Binds_SetGteWorld)),
|
add_ui_self( R_TapePtr, S_(Binds_SetGteWorld)),
|
||||||
|
|||||||
@@ -21,12 +21,6 @@
|
|||||||
* jal -> call_addr (jump-and-link to immediate address)
|
* jal -> call_addr (jump-and-link to immediate address)
|
||||||
* jalr -> call_reg (jump-and-link to register, default $ra)
|
* jalr -> call_reg (jump-and-link to register, default $ra)
|
||||||
* (for the 2-arg `jalr rs, rd`, use `jump_link(rs, rd)` directly)
|
* (for the 2-arg `jalr rs, rd`, use `jump_link(rs, rd)` directly)
|
||||||
*
|
|
||||||
* The vendor mnemonics are NOT registered with the duffle word-count
|
|
||||||
* metadata (tape_atom.metadata.h). They expand to the duffle canonical
|
|
||||||
* macros which DO have word-count entries. Verification: V2 (objdump
|
|
||||||
* byte-identical) holds.
|
|
||||||
*
|
|
||||||
* ============================================================================ */
|
* ============================================================================ */
|
||||||
|
|
||||||
#ifdef INTELLISENSE_DIRECTIVES
|
#ifdef INTELLISENSE_DIRECTIVES
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
// tape_atom.metadata.h
|
// word_count.metadata.h
|
||||||
// Single source of truth for instruction-word counts.
|
// Single source of truth for instruction-word counts.
|
||||||
// Used by C (to define compile-time constants) AND Python (to count positions).
|
// Used by C (to define compile-time constants) AND Python (to count positions).
|
||||||
//
|
//
|
||||||
@@ -18,8 +18,8 @@
|
|||||||
# include "duffle/gen/duffle.offsets.h"
|
# include "duffle/gen/duffle.offsets.h"
|
||||||
#include "duffle/atom_dsl.h"
|
#include "duffle/atom_dsl.h"
|
||||||
#include "duffle/lottes_tape.h"
|
#include "duffle/lottes_tape.h"
|
||||||
|
#include "duffle/word_count.metadata.h"
|
||||||
|
|
||||||
# include "tape_atom.metadata.h"
|
|
||||||
# include "gen/gte_hello.offsets.h"
|
# include "gen/gte_hello.offsets.h"
|
||||||
#include "hello_gte.h"
|
#include "hello_gte.h"
|
||||||
|
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
# include "duffle/gen/duffle.offsets.h"
|
# include "duffle/gen/duffle.offsets.h"
|
||||||
# include "duffle/atom_dsl.h"
|
# include "duffle/atom_dsl.h"
|
||||||
# include "duffle/lottes_tape.h"
|
# include "duffle/lottes_tape.h"
|
||||||
# include "tape_atom.metadata.h"
|
# include "duffle/word_count.metadata.h"
|
||||||
# include "gen/gte_hello.offsets.h"
|
# include "gen/gte_hello.offsets.h"
|
||||||
# include "hello_gte.h"
|
# include "hello_gte.h"
|
||||||
#endif
|
#endif
|
||||||
@@ -22,88 +22,66 @@ typedef Struct_(Binds_CubeTri) {
|
|||||||
V3_S2* VertBase;
|
V3_S2* VertBase;
|
||||||
U4* OtBase;
|
U4* OtBase;
|
||||||
};
|
};
|
||||||
internal MipsAtom_(rbind_cube_g4_face) {
|
internal MipsAtom_(rbind_cube_g4_face) atom_info(atom_bind(Binds_CubeTri)
|
||||||
|
, atom_reads(R_TapePtr)
|
||||||
|
, atom_writes(R_PrimCursor, R_FaceCursor, R_VertBase, R_OtBase)
|
||||||
|
){
|
||||||
/* Pop 4 arguments from the tape directly into the workspace registers */
|
/* Pop 4 arguments from the tape directly into the workspace registers */
|
||||||
load_word(R_PrimCursor, R_TapePtr, O_(Binds_CubeTri,PrimCursor)),
|
load_word(R_PrimCursor, R_TapePtr, O_(Binds_CubeTri,PrimCursor)),
|
||||||
load_word(R_FaceCursor, R_TapePtr, O_(Binds_CubeTri,FaceCursor)),
|
load_word(R_FaceCursor, R_TapePtr, O_(Binds_CubeTri,FaceCursor)),
|
||||||
load_word(R_VertBase, R_TapePtr, O_(Binds_CubeTri,VertBase)),
|
load_word(R_VertBase, R_TapePtr, O_(Binds_CubeTri,VertBase)),
|
||||||
load_word(R_OtBase, R_TapePtr, O_(Binds_CubeTri,OtBase)),
|
load_word(R_OtBase, R_TapePtr, O_(Binds_CubeTri,OtBase)),
|
||||||
add_ui_self( R_TapePtr, S_(Binds_CubeTri)),
|
add_ui_self( R_TapePtr, S_(Binds_CubeTri)),
|
||||||
// Note(Ed): This entire thing is argument shuffle?
|
|
||||||
// TODO(Ed): Eliminate
|
|
||||||
mac_yield()
|
mac_yield()
|
||||||
};
|
};
|
||||||
|
|
||||||
/* ============================================================================
|
/* ============================================================================
|
||||||
* cube_g4_face — Draw one cube face (Gouraud-shaded quad) via the GTE tape pipeline
|
* cube_g4_face — Draw one cube face (Gouraud-shaded quad) via the GTE tape pipeline
|
||||||
* ============================================================================
|
* ============================================================================
|
||||||
*
|
|
||||||
* Reads 4 indices from R_FaceCur (V4_S2 = 8 bytes), loads 4 vertices into
|
* Reads 4 indices from R_FaceCur (V4_S2 = 8 bytes), loads 4 vertices into
|
||||||
* the GTE, runs the PsyQ RotAverageNclip4 sequence, and renders a Poly_G4.
|
* the GTE, runs the PsyQ RotAverageNclip4 sequence, and renders a Poly_G4.
|
||||||
*/
|
*/
|
||||||
atom_region (cube_g4_face, REGION_PRIM_ARENA)
|
|
||||||
atom_group (cube_g4_face, GROUP_RENDER_PRIMS)
|
|
||||||
atom_cadence (cube_g4_face, CADENCE_FRAME)
|
|
||||||
atom_annot(cube_g4_face, phase_work,
|
|
||||||
atom_reads( R_PrimCursor, R_FaceCursor, R_VertBase, R_OtBase),
|
|
||||||
atom_writes(R_PrimCursor, R_FaceCursor))
|
|
||||||
internal
|
internal
|
||||||
MipsAtom_(cube_g4_face) {
|
MipsAtom_(cube_g4_face) atom_info(
|
||||||
/* ── 1. Load 4 face indices from R_FaceCur (V4_S2 = 8 bytes) ───────── */
|
atom_reads( R_PrimCursor, R_FaceCursor, R_VertBase, R_OtBase),
|
||||||
|
atom_writes(R_PrimCursor, R_FaceCursor)
|
||||||
|
){
|
||||||
load_half_u(R_T0, R_FaceCursor, 0 * S_(S2)),
|
load_half_u(R_T0, R_FaceCursor, 0 * S_(S2)),
|
||||||
load_half_u(R_T1, R_FaceCursor, 1 * S_(S2)),
|
load_half_u(R_T1, R_FaceCursor, 1 * S_(S2)),
|
||||||
load_half_u(R_T2, R_FaceCursor, 2 * S_(S2)),
|
load_half_u(R_T2, R_FaceCursor, 2 * S_(S2)),
|
||||||
load_half_u(R_T3, R_FaceCursor, 3 * S_(S2)),
|
load_half_u(R_T3, R_FaceCursor, 3 * S_(S2)),
|
||||||
|
|
||||||
/* ── 2. Load V0, V1, V2 into GTE (parallel to mac_load_tri_verts) ── */
|
mac_gte_load_tri_verts(R_T0, R_T1, R_T2),
|
||||||
mac_load_tri_verts(R_T0, R_T1, R_T2),
|
|
||||||
|
|
||||||
/* ── 3. RTPT — transforms V0/V1/V2 → SXY0/SXY1/SXY2 + SZ1/SZ2/SZ3 ─── */
|
|
||||||
nop2, gte_cmdw_rotate_translate_perspective_triple,
|
nop2, gte_cmdw_rotate_translate_perspective_triple,
|
||||||
|
|
||||||
/* ── 4. NCLIP — backface culling on SXY0/SXY1/SXY2 (p0,p1,p2) ──────── */
|
|
||||||
/* MUST be done BEFORE V3-RTPS overwrites SXY0 with p3. */
|
|
||||||
nop2, gte_cmdw_nclip,
|
nop2, gte_cmdw_nclip,
|
||||||
|
|
||||||
/* ── 5. Cull check: skip format/insert if MAC0 ≤ 0 (backface) ───────── */
|
|
||||||
nop2, gte_mv_from_data_r(R_T0, C2_MAC0),
|
nop2, gte_mv_from_data_r(R_T0, C2_MAC0),
|
||||||
nop, /* COP2 stall */
|
nop,
|
||||||
branch_le_zero(R_T0, atom_offset(cull, cube_g4_face_exit)),
|
branch_le_zero(R_T0, atom_offset(cull, cube_g4_face_exit)), nop,
|
||||||
nop, /* BD slot */
|
|
||||||
|
|
||||||
/* ── 6. Format c0..c3 (color+code words) BEFORE V3-RTPS ─────────────── */
|
|
||||||
store_word(R_0, R_PrimCursor, O_(Poly_G4, tag)),
|
store_word(R_0, R_PrimCursor, O_(Poly_G4, tag)),
|
||||||
mac_format_g4_color(
|
mac_format_g4_color(
|
||||||
/* c0 magenta */ 0xFF, 0x00, 0xFF,
|
/* c0 magenta */ 0xFF, 0x00, 0xFF,
|
||||||
/* c1 yellow */ 0xFF, 0xFF, 0x00,
|
/* c1 yellow */ 0xFF, 0xFF, 0x00,
|
||||||
/* c2 cyan */ 0x00, 0xFF, 0xFF,
|
/* c2 cyan */ 0x00, 0xFF, 0xFF,
|
||||||
/* c3 green */ 0x00, 0xFF, 0x00),
|
/* c3 green */ 0x00, 0xFF, 0x00),
|
||||||
|
|
||||||
/* ── 7. Store p0..p2 BEFORE V3-RTPS overwrites SXY0 ─────────────────── */
|
|
||||||
mac_gte_store_g4_p012_post_rtpt_pre_rtps(),
|
mac_gte_store_g4_p012_post_rtpt_pre_rtps(),
|
||||||
|
|
||||||
/* ── 8. Load V3 = verts[face->w] into V0 ─────────────────────────────── */
|
|
||||||
shift_lleft(R_AT, R_T3, v3s2_byteoff), add_u(R_AT, R_AT, R_VertBase),
|
shift_lleft(R_AT, R_T3, v3s2_byteoff), add_u(R_AT, R_AT, R_VertBase),
|
||||||
load_word(R_V0, R_AT, O_(V3_S2, x)), load_word(R_V1, R_AT, O_(V3_S2, z)),
|
load_word(R_V0, R_AT, O_(V3_S2, x)), load_word(R_V1, R_AT, O_(V3_S2, z)),
|
||||||
gte_mv_to_data_r(R_V0, C2_VXY0), gte_mv_to_data_r(R_V1, C2_VZ0),
|
gte_mv_to_data_r(R_V0, C2_VXY0), gte_mv_to_data_r(R_V1, C2_VZ0),
|
||||||
|
|
||||||
/* ── 9. RTPS — transforms V0 (now V3) → SXY0 (p3) + SZ3 ─────────────── */
|
|
||||||
nop2, gte_cmdw_rotate_translate_perspective_single,
|
nop2, gte_cmdw_rotate_translate_perspective_single,
|
||||||
mac_gte_store_g4_p3_post_rtps(),
|
mac_gte_store_g4_p3_post_rtps(),
|
||||||
|
|
||||||
/* ── 10. AVSZ4 — average Z from SZ0/SZ1/SZ2/SZ3 ─────────────────────── */
|
|
||||||
nop2, gte_cmdw_avg_sort_z4,
|
nop2, gte_cmdw_avg_sort_z4,
|
||||||
nop2, gte_mv_from_data_r(R_T1, C2_OTZ),
|
nop2, gte_mv_from_data_r(R_T1, C2_OTZ),
|
||||||
|
|
||||||
/* ── 11. Bounds check OTZ < OrderingTbl_Len ─────────────────────────── */
|
|
||||||
add_ui( R_AT, R_0, OrderingTbl_Len),
|
add_ui( R_AT, R_0, OrderingTbl_Len),
|
||||||
set_lt_u( R_AT, R_T1, R_AT),
|
set_lt_u( R_AT, R_T1, R_AT),
|
||||||
branch_equal(R_AT, R_0, atom_offset(bounds_chk, cube_g4_face_exit)), nop,
|
branch_equal(R_AT, R_0, atom_offset(bounds_chk, cube_g4_face_exit)), nop,
|
||||||
|
|
||||||
/* ── 12. Insert into Ordering Table (length = 8 words for Poly_G4) ──── */
|
|
||||||
mac_insert_ot_tag_g4(),
|
mac_insert_ot_tag_g4(),
|
||||||
|
|
||||||
/* ── 13. Advance cursors & yield (both branch targets land here) ────── */
|
|
||||||
atom_label(cube_g4_face_exit)
|
atom_label(cube_g4_face_exit)
|
||||||
add_ui_self(R_PrimCursor, S_(Poly_G4)), /* 9 words = Poly_G4 */
|
add_ui_self(R_PrimCursor, S_(Poly_G4)), /* 9 words = Poly_G4 */
|
||||||
add_ui_self(R_FaceCursor, S_(S2) * 4), /* 4 × S2 = 8 bytes */
|
add_ui_self(R_FaceCursor, S_(S2) * 4), /* 4 × S2 = 8 bytes */
|
||||||
@@ -116,14 +94,11 @@ typedef Struct_(Binds_FloorTri) {
|
|||||||
V3_S2* VertBase;
|
V3_S2* VertBase;
|
||||||
U4* OtBase;
|
U4* OtBase;
|
||||||
};
|
};
|
||||||
atom_region(rbind_floor_f3_face, REGION_PRIM_ARENA)
|
|
||||||
atom_group(rbind_floor_f3_face, GROUP_RENDER_FLOOR)
|
|
||||||
atom_cadence(rbind_floor_f3_face, CADENCE_FRAME)
|
|
||||||
atom_annot(rbind_floor_f3_face, phase_bind
|
|
||||||
, atom_reads()
|
|
||||||
, atom_writes(R_PrimCursor, R_FaceCursor, R_VertBase, R_OtBase))
|
|
||||||
internal
|
internal
|
||||||
MipsAtom_(rbind_floor_f3_face) {
|
MipsAtom_(rbind_floor_f3_face) atom_info(atom_bind(Binds_FloorTri)
|
||||||
|
, atom_reads(R_TapePtr)
|
||||||
|
, atom_writes(R_PrimCursor, R_FaceCursor, R_VertBase, R_OtBase)
|
||||||
|
){
|
||||||
/* Pop 4 arguments from the tape directly into the workspace registers */
|
/* Pop 4 arguments from the tape directly into the workspace registers */
|
||||||
load_word(R_PrimCursor, R_TapePtr, O_(Binds_FloorTri,PrimCursor)),
|
load_word(R_PrimCursor, R_TapePtr, O_(Binds_FloorTri,PrimCursor)),
|
||||||
load_word(R_FaceCursor, R_TapePtr, O_(Binds_FloorTri,FaceCursor)),
|
load_word(R_FaceCursor, R_TapePtr, O_(Binds_FloorTri,FaceCursor)),
|
||||||
@@ -133,16 +108,13 @@ MipsAtom_(rbind_floor_f3_face) {
|
|||||||
mac_yield()
|
mac_yield()
|
||||||
};
|
};
|
||||||
|
|
||||||
atom_region( floor_f3_face, REGION_PRIM_ARENA)
|
|
||||||
atom_group( floor_f3_face, GROUP_RENDER_FLOOR)
|
|
||||||
atom_cadence(floor_f3_face, CADENCE_FRAME)
|
|
||||||
atom_annot( floor_f3_face, phase_work,
|
|
||||||
atom_reads( R_PrimCursor, R_FaceCursor, R_VertBase, R_OtBase),
|
|
||||||
atom_writes(R_PrimCursor, R_FaceCursor))
|
|
||||||
internal
|
internal
|
||||||
MipsAtom_(floor_f3_face) {
|
MipsAtom_(floor_f3_face) atom_info(
|
||||||
mac_load_tri_indices(R_T0, R_T1, R_T2),
|
, atom_reads( R_PrimCursor, R_FaceCursor, R_VertBase, R_OtBase)
|
||||||
mac_load_tri_verts( R_T0, R_T1, R_T2),
|
, atom_writes(R_PrimCursor, R_FaceCursor)
|
||||||
|
) {
|
||||||
|
mac_load_tri_indices( R_T0, R_T1, R_T2),
|
||||||
|
mac_gte_load_tri_verts(R_T0, R_T1, R_T2),
|
||||||
nop2, gte_cmdw_rotate_translate_perspective_triple,
|
nop2, gte_cmdw_rotate_translate_perspective_triple,
|
||||||
nop2, gte_cmdw_nclip,
|
nop2, gte_cmdw_nclip,
|
||||||
|
|
||||||
@@ -174,13 +146,10 @@ atom_label(floor_f3_face_exit)
|
|||||||
};
|
};
|
||||||
|
|
||||||
typedef Struct_(Binds_SyncPrimitiveArena) { U4 used; U4 cursor; };
|
typedef Struct_(Binds_SyncPrimitiveArena) { U4 used; U4 cursor; };
|
||||||
atom_region( sync_primitive_arena, REGION_PRIM_ARENA)
|
internal MipsAtom_(sync_primitive_arena) atom_info(atom_bind(Binds_SyncPrimitiveArena)
|
||||||
atom_group( sync_primitive_arena, GROUP_RENDER_FLOOR)
|
, atom_reads( R_TapePtr, R_PrimCursor)
|
||||||
atom_cadence(sync_primitive_arena, CADENCE_FRAME)
|
, atom_writes(R_TapePtr)
|
||||||
atom_annot( sync_primitive_arena, phase_work,
|
){
|
||||||
atom_reads( R_TapePtr, R_PrimCursor),
|
|
||||||
atom_writes(R_TapePtr))
|
|
||||||
internal MipsAtom_(sync_primitive_arena) {
|
|
||||||
load_word(R_AT, R_TapePtr, O_(Binds_SyncPrimitiveArena,used)),
|
load_word(R_AT, R_TapePtr, O_(Binds_SyncPrimitiveArena,used)),
|
||||||
load_word(R_T0, R_TapePtr, O_(Binds_SyncPrimitiveArena,cursor)),
|
load_word(R_T0, R_TapePtr, O_(Binds_SyncPrimitiveArena,cursor)),
|
||||||
add_ui_self( R_TapePtr, S_(Binds_SyncPrimitiveArena)),
|
add_ui_self( R_TapePtr, S_(Binds_SyncPrimitiveArena)),
|
||||||
|
|||||||
+7
-35
@@ -317,32 +317,14 @@ function build-graphis_hello {
|
|||||||
}
|
}
|
||||||
# build-graphis_hello
|
# build-graphis_hello
|
||||||
|
|
||||||
# ps1-meta orchestrator. Replaces generate-TapeAtomOffsets +
|
function ps1-meta { param(
|
||||||
# generate-TapeAtomAnnotations with a single invocation. Dispatches
|
|
||||||
# the 6 passes (word-counts / components / annotation / offsets /
|
|
||||||
# static-analysis / report) in dependency-topological order.
|
|
||||||
|
|
||||||
function any-stale {
|
|
||||||
param([Parameter(Mandatory=$true)][string[]]$sources,
|
|
||||||
[Parameter(Mandatory=$true)][string]$metadata,
|
|
||||||
[Parameter(Mandatory=$true)][string]$out_root)
|
|
||||||
if (-not (test-path $out_root)) { return $true }
|
|
||||||
$out_mtime = (get-item $out_root).LastWriteTimeUtc
|
|
||||||
$src_mtime = ($sources | ForEach-Object { (get-item $_).LastWriteTimeUtc } | Measure-Object -Maximum).Maximum
|
|
||||||
$meta_mtime = (get-item $metadata).LastWriteTimeUtc
|
|
||||||
return ($src_mtime -gt $out_mtime) -or ($meta_mtime -gt $out_mtime)
|
|
||||||
}
|
|
||||||
|
|
||||||
function ps1-meta {
|
|
||||||
param(
|
|
||||||
[Parameter(Mandatory=$true)][string[]]$sources,
|
[Parameter(Mandatory=$true)][string[]]$sources,
|
||||||
[Parameter(Mandatory=$true)][string]$metadata,
|
[Parameter(Mandatory=$true)][string]$metadata,
|
||||||
[string]$out_root = (join-path $path_build 'gen'),
|
[string]$out_root = (join-path $path_build 'gen'),
|
||||||
[string[]]$passes = @('--all')
|
[string[]]$passes = @('--all')
|
||||||
)
|
)
|
||||||
$script = join-path $path_scripts 'ps1_meta.lua'
|
$script = join-path $path_scripts 'ps1_meta.lua'
|
||||||
write-host "ps1-meta $($sources.Count) source(s), passes=$($passes -join ',')" `
|
write-host "ps1-meta $($sources.Count) source(s), passes=$($passes -join ',')" ` -ForegroundColor Magenta
|
||||||
-ForegroundColor Magenta
|
|
||||||
$arg_list = @($passes) + @('--metadata', $metadata) + @('--out-root', $out_root)
|
$arg_list = @($passes) + @('--metadata', $metadata) + @('--out-root', $out_root)
|
||||||
foreach ($s in $sources) { $arg_list += @('--source', $s) }
|
foreach ($s in $sources) { $arg_list += @('--source', $s) }
|
||||||
& luajit $script @arg_list
|
& luajit $script @arg_list
|
||||||
@@ -357,17 +339,11 @@ function build-gte_hello {
|
|||||||
|
|
||||||
$path_module = join-path $path_code 'gte_hello'
|
$path_module = join-path $path_code 'gte_hello'
|
||||||
$path_duffle = join-path $path_code 'duffle'
|
$path_duffle = join-path $path_code 'duffle'
|
||||||
$path_atom_metadata = join-path $path_module 'tape_atom.metadata.h'
|
$path_atom_metadata = join-path $path_duffle 'word_count.metadata.h'
|
||||||
|
|
||||||
$source_dirs = @($path_duffle, $path_module)
|
$source_dirs = @($path_duffle, $path_module)
|
||||||
$atom_sources = Get-SourceFiles -paths $source_dirs -extensions @('.h', '.c')
|
$atom_sources = Get-SourceFiles -paths $source_dirs -extensions @('.h', '.c')
|
||||||
|
ps1-meta -sources $atom_sources -metadata $path_atom_metadata -out_root (join-path $path_build 'gen')
|
||||||
if (any-stale -sources $atom_sources -metadata $path_atom_metadata -out_root (join-path $path_build 'gen')) {
|
|
||||||
ps1-meta -sources $atom_sources -metadata $path_atom_metadata -out_root (join-path $path_build 'gen')
|
|
||||||
} else {
|
|
||||||
write-host "ps1-meta all $($atom_sources.Count) source(s) up-to-date" `
|
|
||||||
-ForegroundColor DarkGray
|
|
||||||
}
|
|
||||||
|
|
||||||
$assemble_args = @()
|
$assemble_args = @()
|
||||||
$assemble_args += $f_debug
|
$assemble_args += $f_debug
|
||||||
@@ -412,18 +388,14 @@ build-gte_hello
|
|||||||
|
|
||||||
|
|
||||||
# NO idea if this works yet...
|
# NO idea if this works yet...
|
||||||
function Send-ToEmulator { param(
|
function Send-ToEmulator { param( [string]$exePath )
|
||||||
[string]$exePath
|
|
||||||
)
|
|
||||||
$uri = "http://localhost:8080/api/v1/load-exec"
|
$uri = "http://localhost:8080/api/v1/load-exec"
|
||||||
|
|
||||||
# Absolute path is safest for the emulator web server
|
# Absolute path is safest for the emulator web server
|
||||||
$absolutePath = [System.IO.Path]::GetFullPath($exePath)
|
$absolutePath = [System.IO.Path]::GetFullPath($exePath)
|
||||||
|
|
||||||
# Create JSON payload pointing to your compiled .ps-exe
|
# Create JSON payload pointing to your compiled .ps-exe
|
||||||
$body = @{
|
$body = @{ filename = $absolutePath } | ConvertTo-Json
|
||||||
filename = $absolutePath
|
|
||||||
} | ConvertTo-Json
|
|
||||||
|
|
||||||
Write-Host "Pushing hot-reload to PCSX-Redux..." -ForegroundColor Magenta
|
Write-Host "Pushing hot-reload to PCSX-Redux..." -ForegroundColor Magenta
|
||||||
try {
|
try {
|
||||||
|
|||||||
+96
-47
@@ -1,8 +1,6 @@
|
|||||||
-- duffle.lua
|
-- duffle.lua
|
||||||
--
|
--
|
||||||
-- Shared primitives + domain tables for the tape-atom metaprograms.
|
-- Shared primitives + domain tables for the tape-atom metaprograms.
|
||||||
-- Both `tape_atom_annotation_pass.lua` and `tape_atom.offset_gen.meta.lua`
|
|
||||||
-- `require("duffle")` for these.
|
|
||||||
--
|
--
|
||||||
-- 5.3-compatible Lua (no 5.4/5.5-only features):
|
-- 5.3-compatible Lua (no 5.4/5.5-only features):
|
||||||
-- - no <close> / <toclose>
|
-- - no <close> / <toclose>
|
||||||
@@ -186,12 +184,25 @@ function M.write_file(path, content)
|
|||||||
f:close()
|
f:close()
|
||||||
end
|
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).
|
||||||
|
local _ensured_dirs = {}
|
||||||
|
|
||||||
function M.ensure_dir(path)
|
function M.ensure_dir(path)
|
||||||
|
if _ensured_dirs[path] then return end
|
||||||
|
_ensured_dirs[path] = true
|
||||||
local is_win = package.config:sub(1, 1) == "\\"
|
local is_win = package.config:sub(1, 1) == "\\"
|
||||||
os.execute(is_win and ('if not exist "' .. path .. '" mkdir "' .. path .. '"')
|
os.execute(is_win and ('if not exist "' .. path .. '" mkdir "' .. path .. '"')
|
||||||
or ('mkdir -p "' .. path .. '" 2>/dev/null'))
|
or ('mkdir -p "' .. path .. '" 2>/dev/null'))
|
||||||
end
|
end
|
||||||
|
|
||||||
|
-- Test helper: clear the cache (used by tests + between process runs).
|
||||||
|
-- Not normally needed since Lua state is per-process.
|
||||||
|
function M._reset_ensured_dirs() _ensured_dirs = {} end
|
||||||
|
|
||||||
-- ════════════════════════════════════════════════════════════════════════════
|
-- ════════════════════════════════════════════════════════════════════════════
|
||||||
-- Section 4: C-language scanner primitives
|
-- Section 4: C-language scanner primitives
|
||||||
-- ════════════════════════════════════════════════════════════════════════════
|
-- ════════════════════════════════════════════════════════════════════════════
|
||||||
@@ -499,54 +510,92 @@ M.WAVE_CONTEXT_REGS = {
|
|||||||
["R_OtBase"] = { alias = "R_T6", size = 4, role = "base pointer (ordering table)" },
|
["R_OtBase"] = { alias = "R_T6", size = 4, role = "base pointer (ordering table)" },
|
||||||
}
|
}
|
||||||
|
|
||||||
M.MACRO_EXPANSION = {
|
-- The annotation DSL has been reduced to a single annotation macro:
|
||||||
["phase_init"] = "init",
|
-- atom_info(atom_bind(Binds_X), atom_reads(...), atom_writes(...))
|
||||||
["phase_bind"] = "bind",
|
-- All phase / region / cadence / async / resource / group tokens have
|
||||||
["phase_setup"] = "setup",
|
-- been dropped. They may be reintroduced later as optional sub-calls
|
||||||
["phase_work"] = "work",
|
-- of atom_info; for now, the parser only recognizes atom_info + its
|
||||||
["phase_commit"] = "commit",
|
-- three sub-calls (atom_bind, atom_reads, atom_writes).
|
||||||
["phase_terminate"] = "terminate",
|
|
||||||
["REGION_PRIM_ARENA"] = "prim_arena",
|
|
||||||
["REGION_FACE_ARENA"] = "face_arena",
|
|
||||||
["REGION_VERTEX_ARENA"] = "vertex_arena",
|
|
||||||
["REGION_OT_ARENA"] = "ot_arena",
|
|
||||||
["REGION_HEAP_3D"] = "heap_3d_models",
|
|
||||||
["REGION_CDROM_STREAM"] = "cdrom_stream",
|
|
||||||
["REGION_VRAM"] = "vram_heap",
|
|
||||||
["CADENCE_FRAME"] = "frame",
|
|
||||||
["CADENCE_ONCE"] = "once",
|
|
||||||
["CADENCE_ONDEMAND"] = "ondemand",
|
|
||||||
}
|
|
||||||
|
|
||||||
M.KNOWN_PHASES = {
|
|
||||||
["init"] = true, ["bind"] = true, ["setup"] = true,
|
|
||||||
["work"] = true, ["commit"] = true, ["terminate"] = true,
|
|
||||||
}
|
|
||||||
M.KNOWN_REGIONS = {
|
|
||||||
["prim_arena"] = true, ["face_arena"] = true,
|
|
||||||
["vertex_arena"] = true, ["ot_arena"] = true,
|
|
||||||
["heap_3d_models"] = true, ["cdrom_stream"] = true,
|
|
||||||
["vram_heap"] = true,
|
|
||||||
}
|
|
||||||
M.KNOWN_CADENCES = {
|
|
||||||
["frame"] = true, ["once"] = true, ["ondemand"] = true,
|
|
||||||
}
|
|
||||||
|
|
||||||
M.TAPE_ATOM_MACROS = {
|
M.TAPE_ATOM_MACROS = {
|
||||||
["atom_annot"] = { kind = "work", binds = false },
|
["atom_info"] = { kind = "info", binds = false },
|
||||||
["atom_bind"] = { kind = "bind", binds = true },
|
|
||||||
["atom_setup"] = { kind = "setup", binds = false },
|
|
||||||
["atom_commit"] = { kind = "commit", binds = false },
|
|
||||||
["atom_init"] = { kind = "init", binds = false },
|
|
||||||
["atom_terminate"] = { kind = "terminate", binds = false },
|
|
||||||
}
|
}
|
||||||
|
|
||||||
M.ATOM_PRAGMA_KINDS = {
|
-- GTE pipeline-fill latency table (static-analysis Phase 1).
|
||||||
["resource"] = { kind = "string" },
|
--
|
||||||
["region"] = { kind = "ident", allowed = M.KNOWN_REGIONS },
|
-- For each `gte_cmdw_*` macro in code/duffle/gte.h, the minimum number
|
||||||
["group"] = { kind = "ident" },
|
-- of consecutive COP2 "nop" words that MUST appear before any other
|
||||||
["cadence"] = { kind = "ident", allowed = M.KNOWN_CADENCES },
|
-- COP2 read or non-nop instruction (so the GTE pipeline latency is
|
||||||
["async"] = { kind = "ident", allowed = { ["true"] = true, ["false"] = true } },
|
-- 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).
|
||||||
|
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`:
|
||||||
|
--
|
||||||
|
-- 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
|
||||||
|
-- nclip 7 2 MAC0 write + 5c for sign
|
||||||
|
-- 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)
|
||||||
|
--
|
||||||
|
-- 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.
|
||||||
|
--
|
||||||
|
-- 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,
|
||||||
|
["gte_cmdw_rtpt"] = 2,
|
||||||
|
["gte_cmdw_nclip"] = 2,
|
||||||
|
["gte_cmdw_op"] = 0,
|
||||||
|
["gte_cmdw_mvmva"] = 2,
|
||||||
|
["gte_cmdw_avsz3"] = 2,
|
||||||
|
["gte_cmdw_avsz4"] = 2,
|
||||||
|
|
||||||
|
-- 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,
|
||||||
|
|
||||||
|
-- Outer product aliases (same canonical op, 0 pre-fill nops).
|
||||||
|
-- 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,
|
||||||
}
|
}
|
||||||
|
|
||||||
-- Expose the lpeg_ok flag so callers can detect the LPeg-back path.
|
-- Expose the lpeg_ok flag so callers can detect the LPeg-back path.
|
||||||
|
|||||||
+118
-295
@@ -47,12 +47,8 @@ local split_top_level_commas = duffle.split_top_level_commas
|
|||||||
|
|
||||||
-- Domain tables (single source of truth in duffle.lua).
|
-- Domain tables (single source of truth in duffle.lua).
|
||||||
local WAVE_CONTEXT_REGS = duffle.WAVE_CONTEXT_REGS
|
local WAVE_CONTEXT_REGS = duffle.WAVE_CONTEXT_REGS
|
||||||
local MACRO_EXPANSION = duffle.MACRO_EXPANSION
|
|
||||||
local KNOWN_PHASES = duffle.KNOWN_PHASES
|
|
||||||
local TAPE_ATOM_MACROS = duffle.TAPE_ATOM_MACROS
|
local TAPE_ATOM_MACROS = duffle.TAPE_ATOM_MACROS
|
||||||
local ATOM_PRAGMA_KINDS = duffle.ATOM_PRAGMA_KINDS
|
|
||||||
|
|
||||||
local function valid_phase(p) return KNOWN_PHASES[p] or false end
|
|
||||||
local function is_wave_context_reg(n) return WAVE_CONTEXT_REGS[n] ~= nil end
|
local function is_wave_context_reg(n) return WAVE_CONTEXT_REGS[n] ~= nil end
|
||||||
|
|
||||||
-- ════════════════════════════════════════════════════════════════════════════
|
-- ════════════════════════════════════════════════════════════════════════════
|
||||||
@@ -109,32 +105,44 @@ end
|
|||||||
-- Parse TAPE_ATOM_ANNOT(...) calls
|
-- Parse TAPE_ATOM_ANNOT(...) calls
|
||||||
-- ════════════════════════════════════════════════════════════════════════════
|
-- ════════════════════════════════════════════════════════════════════════════
|
||||||
|
|
||||||
-- Recognize a `atom_reads(...)` or `atom_writes(...)` register-list
|
-- Recognize a `atom_bind(...)`, `atom_reads(...)`, or `atom_writes(...)`
|
||||||
-- call embedded inside an annotation arg list. Returns the kind
|
-- sub-call embedded inside an atom_info arg list. Returns the kind
|
||||||
-- ("atom_reads" / "atom_writes") and the inner content, or nil if the
|
-- ("atom_bind" / "atom_reads" / "atom_writes") and the inner content,
|
||||||
-- token isn't a recognized register-list form. Flattened via a
|
-- or nil if the token isn't a recognized sub-call form. Flattened via
|
||||||
-- prefix lookup instead of a nested if/elseif chain.
|
-- a prefix lookup instead of a nested if/elseif chain.
|
||||||
local REGS_CALL_PREFIX = {
|
local REGS_CALL_PREFIX = {
|
||||||
["atom_reads("] = { kind = "atom_reads", inner_offset = 12 },
|
|
||||||
["atom_writes("] = { kind = "atom_writes", inner_offset = 13 },
|
["atom_writes("] = { kind = "atom_writes", inner_offset = 13 },
|
||||||
|
["atom_reads("] = { kind = "atom_reads", inner_offset = 12 },
|
||||||
|
["atom_bind("] = { kind = "atom_bind", inner_offset = 11, single_ident = true },
|
||||||
}
|
}
|
||||||
|
|
||||||
local function parse_regs_call(s)
|
local function parse_regs_call(s)
|
||||||
if s:sub(-1) ~= ")" then return nil end
|
if s:sub(-1) ~= ")" then return nil end
|
||||||
local spec = REGS_CALL_PREFIX[s:sub(1, 12)] -- longest prefix first wins
|
-- 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 return nil end
|
if not spec then return nil end
|
||||||
-- The 12-char prefix "atom_reads(" also matches "atom_writes("
|
local inner = s:sub(spec.inner_offset, -2)
|
||||||
-- would be ambiguous; the table order above handles it.
|
if spec.single_ident then
|
||||||
-- (atom_reads prefix is 11 chars, atom_writes is 12; the 12-char
|
-- atom_bind takes a single Binds_* type ident. Trim and pass through.
|
||||||
-- lookup matches atom_writes first.)
|
return spec.kind, trim(inner)
|
||||||
return spec.kind, s:sub(spec.inner_offset, -2)
|
end
|
||||||
|
return spec.kind, inner
|
||||||
end
|
end
|
||||||
|
|
||||||
-- Resolve any phase_* / R_* alias macros in a register list.
|
-- 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)
|
local function resolve_reg_aliases(regs)
|
||||||
for i, r in ipairs(regs) do
|
|
||||||
if MACRO_EXPANSION[r] then regs[i] = MACRO_EXPANSION[r] end
|
|
||||||
end
|
|
||||||
return regs
|
return regs
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -150,18 +158,19 @@ local function parse_regs_list(inner)
|
|||||||
end
|
end
|
||||||
|
|
||||||
-- Parse a single token (from split_csv_top) into an arg entry.
|
-- Parse a single token (from split_csv_top) into an arg entry.
|
||||||
-- Three forms: register-list call, bare identifier (with alias),
|
-- Three forms: register-list call, bare identifier,
|
||||||
-- "other" (preserved as text).
|
-- "other" (preserved as text).
|
||||||
local function parse_arg_token(s)
|
local function parse_arg_token(s)
|
||||||
local kind, inner = parse_regs_call(s)
|
local kind, inner = parse_regs_call(s)
|
||||||
if kind then
|
if kind then
|
||||||
|
if kind == "atom_bind" then
|
||||||
|
return { kind = kind, value = inner } -- single ident, not a list
|
||||||
|
end
|
||||||
return { kind = kind, value = parse_regs_list(inner) }
|
return { kind = kind, value = parse_regs_list(inner) }
|
||||||
end
|
end
|
||||||
local id = read_ident(s, 1)
|
local id = read_ident(s, 1)
|
||||||
if id and trim(s) == id then
|
if id and trim(s) == id then
|
||||||
local v = id
|
return { kind = "ident", value = id }
|
||||||
if MACRO_EXPANSION[v] then v = MACRO_EXPANSION[v] end
|
|
||||||
return { kind = "ident", value = v }
|
|
||||||
end
|
end
|
||||||
return { kind = "other", value = s }
|
return { kind = "other", value = s }
|
||||||
end
|
end
|
||||||
@@ -262,136 +271,6 @@ local function find_macro_word_annotations(source)
|
|||||||
return out
|
return out
|
||||||
end
|
end
|
||||||
|
|
||||||
-- ════════════════════════════════════════════════════════════════════════════
|
|
||||||
-- Parse `atom_<...>` Pragma / _Pragma annotations
|
|
||||||
-- ════════════════════════════════════════════════════════════════════════════
|
|
||||||
|
|
||||||
local ATOM_ATTR_MACROS = {
|
|
||||||
["atom_resource"] = "resource",
|
|
||||||
["atom_region"] = "region",
|
|
||||||
["atom_group"] = "group",
|
|
||||||
["atom_cadence"] = "cadence",
|
|
||||||
["atom_async"] = "async",
|
|
||||||
}
|
|
||||||
|
|
||||||
--- Parse macro form: `atom_<key>(atom_name, value, ...)`.
|
|
||||||
--- Returns (true, entry, str_end) on success, (false) on no match.
|
|
||||||
local function try_parse_atom_attr_macro(source, i, line_of)
|
|
||||||
local ident, after = read_ident(source, i)
|
|
||||||
if not ident then return false end
|
|
||||||
local key = ATOM_ATTR_MACROS[ident]
|
|
||||||
if not key then return false end
|
|
||||||
local open = skip_ws_and_cmt(source, after)
|
|
||||||
if source:sub(open, open) ~= "(" then return false end
|
|
||||||
|
|
||||||
local body, body_end = read_parens(source, open)
|
|
||||||
local first, after_name = read_ident(body, 1)
|
|
||||||
if not first then return false end
|
|
||||||
|
|
||||||
local j = after_name
|
|
||||||
while j <= #body and is_space(body:sub(j, j)) do j = j + 1 end
|
|
||||||
if body:sub(j, j) ~= "," then return false end
|
|
||||||
j = j + 1
|
|
||||||
while j <= #body and is_space(body:sub(j, j)) do j = j + 1 end
|
|
||||||
|
|
||||||
local value
|
|
||||||
if body:sub(j, j) == '"' then
|
|
||||||
local k = j + 1
|
|
||||||
while k <= #body do
|
|
||||||
local c = body:sub(k, k)
|
|
||||||
if c == "\\" then
|
|
||||||
k = k + 2
|
|
||||||
elseif c == '"' then
|
|
||||||
break
|
|
||||||
else
|
|
||||||
k = k + 1
|
|
||||||
end
|
|
||||||
end
|
|
||||||
if body:sub(k, k) ~= '"' then return false end
|
|
||||||
value = body:sub(j + 1, k - 1)
|
|
||||||
else
|
|
||||||
local id2, after_id = read_ident(body, j)
|
|
||||||
if not id2 then return false end
|
|
||||||
value = id2
|
|
||||||
if MACRO_EXPANSION[value] then value = MACRO_EXPANSION[value] end
|
|
||||||
end
|
|
||||||
|
|
||||||
return true, {
|
|
||||||
line = line_of(i),
|
|
||||||
name = first,
|
|
||||||
attrs = { [key] = value },
|
|
||||||
}, body_end
|
|
||||||
end
|
|
||||||
|
|
||||||
local function find_atom_pragmas(source)
|
|
||||||
local line_of = duffle.LineIndex(source)
|
|
||||||
local out = {}
|
|
||||||
local len = #source
|
|
||||||
local i = 1
|
|
||||||
while i <= len do
|
|
||||||
i = skip_ws_and_cmt(source, i); if i > len then break end
|
|
||||||
if source:sub(i, i) == "#" then
|
|
||||||
local j = i
|
|
||||||
while j <= len and source:sub(j, j) ~= "\n" do j = j + 1 end
|
|
||||||
i = j + 1
|
|
||||||
else
|
|
||||||
local got, entry, next_i = try_parse_atom_attr_macro(source, i, line_of)
|
|
||||||
if got then
|
|
||||||
out[#out + 1] = entry
|
|
||||||
i = next_i
|
|
||||||
else
|
|
||||||
local ident, after = read_ident(source, i)
|
|
||||||
if not ident then
|
|
||||||
i = i + 1
|
|
||||||
elseif ident == "_Pragma" then
|
|
||||||
local open = skip_ws_and_cmt(source, after)
|
|
||||||
if source:sub(open, open) == "(" then
|
|
||||||
local str, str_end = read_parens(source, open)
|
|
||||||
str = trim(str)
|
|
||||||
if str:sub(1, 1) == '"' and str:sub(-1) == '"' then
|
|
||||||
local inner = str:sub(2, -2)
|
|
||||||
local sp1 = find_byte(inner, " ", 1)
|
|
||||||
if sp1 and trim(inner:sub(1, sp1 - 1)) == "atom" then
|
|
||||||
local rest = trim(inner:sub(sp1 + 1))
|
|
||||||
local sp2 = find_byte(rest, " ", 1)
|
|
||||||
if sp2 then
|
|
||||||
local name = trim(rest:sub(1, sp2 - 1))
|
|
||||||
local attrs_str = trim(rest:sub(sp2 + 1))
|
|
||||||
local attrs = {}
|
|
||||||
local got_any = false
|
|
||||||
for _, pair in ipairs(split_ws(attrs_str)) do
|
|
||||||
local eq = find_byte(pair, "=", 1)
|
|
||||||
if eq then
|
|
||||||
local k = trim(pair:sub(1, eq - 1))
|
|
||||||
local v = trim(pair:sub(eq + 1))
|
|
||||||
if MACRO_EXPANSION[v] then v = MACRO_EXPANSION[v] end
|
|
||||||
attrs[k] = v
|
|
||||||
got_any = true
|
|
||||||
end
|
|
||||||
end
|
|
||||||
if got_any then
|
|
||||||
out[#out + 1] = {
|
|
||||||
line = line_of(i),
|
|
||||||
name = name,
|
|
||||||
attrs = attrs,
|
|
||||||
}
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
i = str_end
|
|
||||||
else
|
|
||||||
i = open + 1
|
|
||||||
end
|
|
||||||
else
|
|
||||||
i = after
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
return out
|
|
||||||
end
|
|
||||||
|
|
||||||
-- ════════════════════════════════════════════════════════════════════════════
|
-- ════════════════════════════════════════════════════════════════════════════
|
||||||
-- Parse `typedef Struct_(Binds_X) { ... };` declarations
|
-- Parse `typedef Struct_(Binds_X) { ... };` declarations
|
||||||
-- ════════════════════════════════════════════════════════════════════════════
|
-- ════════════════════════════════════════════════════════════════════════════
|
||||||
@@ -526,58 +405,35 @@ local function is_regs_arg(a)
|
|||||||
end
|
end
|
||||||
|
|
||||||
--- Per-macro arg-shape handlers. Each takes (entry, args) and mutates
|
--- Per-macro arg-shape handlers. Each takes (entry, args) and mutates
|
||||||
--- entry.{reads, writes, phase, binds, errors}. Replaces the 5-way
|
--- Per-atom_info sub-call dispatch. Each takes (entry, args) and mutates
|
||||||
--- `if/elseif/elseif/elseif/elseif` chain inside find_atom_annotations.
|
--- 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.
|
||||||
local ANNOT_ARG_HANDLERS = {}
|
local ANNOT_ARG_HANDLERS = {}
|
||||||
|
|
||||||
-- atom_bind(name, Binds_Struct, writes)
|
-- atom_info(atom_bind(Binds_X), atom_reads(...), atom_writes(...))
|
||||||
function ANNOT_ARG_HANDLERS.bind(entry, args)
|
function ANNOT_ARG_HANDLERS.info(entry, args)
|
||||||
if #args >= 2 and args[2].kind == "ident" then
|
for _, arg in ipairs(args) do
|
||||||
entry.binds = args[2].value
|
if arg.kind == "atom_bind" then
|
||||||
end
|
entry.binds = arg.value
|
||||||
if #args >= 3 and is_regs_arg(args[3]) then
|
elseif arg.kind == "atom_reads" then
|
||||||
entry.writes = args[3].value
|
entry.reads = arg.value
|
||||||
end
|
elseif arg.kind == "atom_writes" then
|
||||||
end
|
entry.writes = arg.value
|
||||||
|
elseif arg.kind == "ident" then
|
||||||
-- atom_init(name) / atom_terminate(name): name only, no extra slots.
|
-- Reserved for future phase tokens. Currently ignored.
|
||||||
ANNOT_ARG_HANDLERS.init = function() end
|
-- (Could be reintroduced as `phase_*` sub-calls of atom_info.)
|
||||||
ANNOT_ARG_HANDLERS.terminate = function() end
|
else
|
||||||
|
entry.errors[#entry.errors + 1] = string.format(
|
||||||
-- Macro name -> handler key. Replaces the `macro_def.binds` check
|
"unexpected atom_info arg kind=%s value=%s", arg.kind, tostring(arg.value))
|
||||||
-- plus the 4-way ident elseif chain.
|
|
||||||
local MACRO_HANDLER_KEY = {
|
|
||||||
["atom_bind"] = "bind",
|
|
||||||
["atom_annot"] = "annot",
|
|
||||||
["atom_setup"] = "reads_only",
|
|
||||||
["atom_commit"] = "reads_only",
|
|
||||||
["atom_init"] = "init",
|
|
||||||
["atom_terminate"] = "terminate",
|
|
||||||
}
|
|
||||||
|
|
||||||
-- atom_setup(name, reads) / atom_commit(name, reads): reads from slot 2.
|
|
||||||
function ANNOT_ARG_HANDLERS.reads_only(entry, args)
|
|
||||||
if #args >= 2 and is_regs_arg(args[2]) then
|
|
||||||
entry.reads = args[2].value
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
-- atom_annot(name, phase, reads, writes)
|
|
||||||
function ANNOT_ARG_HANDLERS.annot(entry, args)
|
|
||||||
if #args >= 2 and args[2].kind == "ident" then
|
|
||||||
entry.phase = MACRO_EXPANSION[args[2].value] or args[2].value
|
|
||||||
end
|
|
||||||
if #args >= 3 and is_regs_arg(args[3]) then
|
|
||||||
if args[3].kind == "atom_writes" then
|
|
||||||
entry.errors[#entry.errors + 1] = "reads slot has atom_writes — swap order?"
|
|
||||||
end
|
end
|
||||||
entry.reads = args[3].value
|
|
||||||
end
|
|
||||||
if #args >= 4 and is_regs_arg(args[4]) then
|
|
||||||
if args[4].kind == "atom_reads" then
|
|
||||||
entry.errors[#entry.errors + 1] = "writes slot has atom_reads — swap order?"
|
|
||||||
end
|
|
||||||
entry.writes = args[4].value
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -589,16 +445,17 @@ local function new_annot_entry(line, ident, name, kind)
|
|||||||
name = name,
|
name = name,
|
||||||
kind = kind,
|
kind = kind,
|
||||||
binds = nil,
|
binds = nil,
|
||||||
phase = nil,
|
|
||||||
reads = {},
|
reads = {},
|
||||||
writes = {},
|
writes = {},
|
||||||
errors = {},
|
errors = {},
|
||||||
}
|
}
|
||||||
end
|
end
|
||||||
|
|
||||||
--- Find every TAPE_ATOM_* macro call in source and convert it to a
|
--- Find every MipsAtom_(name) declaration in source, then look for an
|
||||||
--- normalized annotation entry. Dispatches per-macro arg-shape via
|
--- immediately-following atom_info(...) call. If present, parse its
|
||||||
--- ANNOT_ARG_HANDLERS (lookup table; no nested if/elseif chain).
|
--- sub-calls into a normalized annotation entry linked to the MipsAtom_
|
||||||
|
--- name. If no atom_info follows, emit NO annotation entry (atoms
|
||||||
|
--- without annotations are valid in the new minimal shape).
|
||||||
local function find_atom_annotations(source)
|
local function find_atom_annotations(source)
|
||||||
local line_of = duffle.LineIndex(source)
|
local line_of = duffle.LineIndex(source)
|
||||||
local annots = {}
|
local annots = {}
|
||||||
@@ -607,9 +464,6 @@ local function find_atom_annotations(source)
|
|||||||
while i <= len do
|
while i <= len do
|
||||||
i = skip_ws_and_cmt(source, i); if i > len then break end
|
i = skip_ws_and_cmt(source, i); if i > len then break end
|
||||||
-- Skip preprocessor directives (lines starting with #).
|
-- Skip preprocessor directives (lines starting with #).
|
||||||
-- Without this guard, `#define atom_init(name) ...` macro
|
|
||||||
-- definitions get misinterpreted as annotation calls with
|
|
||||||
-- the literal placeholder "name" as the atom name.
|
|
||||||
if source:sub(i, i) == "#" then
|
if source:sub(i, i) == "#" then
|
||||||
local j = i
|
local j = i
|
||||||
while j <= len and source:sub(j, j) ~= "\n" do j = j + 1 end
|
while j <= len and source:sub(j, j) ~= "\n" do j = j + 1 end
|
||||||
@@ -620,30 +474,45 @@ local function find_atom_annotations(source)
|
|||||||
local ident, after = read_ident(source, i)
|
local ident, after = read_ident(source, i)
|
||||||
if not ident then
|
if not ident then
|
||||||
i = i + 1
|
i = i + 1
|
||||||
elseif TAPE_ATOM_MACROS[ident] then
|
elseif ident == "MipsAtom_" then
|
||||||
local open = skip_ws_and_cmt(source, after)
|
local open = skip_ws_and_cmt(source, after)
|
||||||
if source:sub(open, open) ~= "(" then
|
if source:sub(open, open) ~= "(" then
|
||||||
i = open + 1
|
i = open + 1
|
||||||
else
|
goto continue
|
||||||
local inner, after_paren = read_parens(source, open)
|
end
|
||||||
local args = parse_atom_annot_args(inner)
|
local inner, after_paren = read_parens(source, open)
|
||||||
local macro_def = TAPE_ATOM_MACROS[ident]
|
local a = 1
|
||||||
|
while a <= #inner and is_space(inner:sub(a, a)) do a = a + 1 end
|
||||||
|
local b = a
|
||||||
|
while b <= #inner and is_alnum(inner:sub(b, b)) do b = b + 1 end
|
||||||
|
local name = inner:sub(a, b - 1)
|
||||||
|
|
||||||
if #args < 1 then
|
-- Look for atom_info(...) right after MipsAtom_(name).
|
||||||
annots[#annots + 1] = {
|
local lookahead = skip_ws_and_cmt(source, after_paren)
|
||||||
line = line_of(i),
|
local look_ident, look_after = read_ident(source, lookahead)
|
||||||
macro = ident,
|
if look_ident == "atom_info" then
|
||||||
kind = macro_def.kind,
|
local info_open = skip_ws_and_cmt(source, look_after)
|
||||||
error = "missing atom name (first arg)",
|
if source:sub(info_open, info_open) == "(" then
|
||||||
}
|
local info_inner, info_after = read_parens(source, info_open)
|
||||||
else
|
local args = parse_atom_annot_args(info_inner)
|
||||||
local entry = new_annot_entry(line_of(i), ident, args[1].value, macro_def.kind)
|
local entry = new_annot_entry(line_of(lookahead), "atom_info", name, "info")
|
||||||
local handler = ANNOT_ARG_HANDLERS[MACRO_HANDLER_KEY[ident]]
|
ANNOT_ARG_HANDLERS.info(entry, args)
|
||||||
if handler then handler(entry, args) end
|
|
||||||
annots[#annots + 1] = entry
|
annots[#annots + 1] = entry
|
||||||
|
i = info_after
|
||||||
|
else
|
||||||
|
i = info_open + 1
|
||||||
end
|
end
|
||||||
|
else
|
||||||
|
-- No atom_info follows this MipsAtom_. Valid in new shape.
|
||||||
i = after_paren
|
i = after_paren
|
||||||
end
|
end
|
||||||
|
|
||||||
|
-- Skip past the body { ... } if present.
|
||||||
|
local brace = scan_to_char(source, "{", i)
|
||||||
|
if brace then
|
||||||
|
local _, after_brace = read_braces(source, brace)
|
||||||
|
i = after_brace
|
||||||
|
end
|
||||||
else
|
else
|
||||||
i = after
|
i = after
|
||||||
end
|
end
|
||||||
@@ -661,7 +530,6 @@ local function validate(ctx, src)
|
|||||||
|
|
||||||
local annots = find_atom_annotations(source)
|
local annots = find_atom_annotations(source)
|
||||||
local macros = find_macro_word_annotations(source)
|
local macros = find_macro_word_annotations(source)
|
||||||
local pragmas = find_atom_pragmas(source)
|
|
||||||
local binds = find_binds_structs(source)
|
local binds = find_binds_structs(source)
|
||||||
local atoms = find_atom_names(source)
|
local atoms = find_atom_names(source)
|
||||||
|
|
||||||
@@ -692,37 +560,26 @@ local function validate(ctx, src)
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
-- 2. Every atom must have exactly one annotation (no orphans, no duplicates).
|
-- 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 = {}
|
local count_per_atom = {}
|
||||||
for _, a in ipairs(annots) do
|
for _, a in ipairs(annots) do
|
||||||
if a.name and not a.error then
|
if a.name and not a.error then
|
||||||
count_per_atom[a.name] = (count_per_atom[a.name] or 0) + 1
|
count_per_atom[a.name] = (count_per_atom[a.name] or 0) + 1
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
for _, atom in ipairs(atoms) do
|
for name, n in pairs(count_per_atom) do
|
||||||
local n = count_per_atom[atom.name] or 0
|
if n > 1 then
|
||||||
if n == 0 then
|
|
||||||
warnings[#warnings + 1] = {
|
|
||||||
line = atom.line,
|
|
||||||
msg = string.format("MipsAtom_(%s) has no TAPE_ATOM_* annotation", atom.name),
|
|
||||||
}
|
|
||||||
elseif n > 1 then
|
|
||||||
errors[#errors + 1] = {
|
errors[#errors + 1] = {
|
||||||
line = atom.line,
|
line = atom_index[name] and atom_index[name].line or 0,
|
||||||
msg = string.format("MipsAtom_(%s) has %d annotations (expected 1)", atom.name, n),
|
msg = string.format("MipsAtom_(%s) has %d annotations (expected at most 1)", name, n),
|
||||||
}
|
}
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
-- 3. Phase validity.
|
-- 3. (Phase validity check DROPPED. Phases were removed from the
|
||||||
for _, a in ipairs(annots) do
|
-- annotation DSL. They may be reintroduced later as sub-calls of
|
||||||
if a.name and not a.error and a.phase and not valid_phase(a.phase) then
|
-- atom_info, at which point ordering checks will go here.)
|
||||||
errors[#errors + 1] = {
|
|
||||||
line = a.line,
|
|
||||||
msg = string.format("'%s' has unknown phase '%s' (expected one of init/bind/setup/work/commit/terminate)", a.name, a.phase),
|
|
||||||
}
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
-- 4. BIND atoms must reference a real Binds_* struct.
|
-- 4. BIND atoms must reference a real Binds_* struct.
|
||||||
for _, a in ipairs(annots) do
|
for _, a in ipairs(annots) do
|
||||||
@@ -764,16 +621,14 @@ local function validate(ctx, src)
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
-- 6. WORK reads should be a subset of BIND writes (the wave contract).
|
-- 6. INFO reads should be wave-context registers (or R_TapePtr for rbind).
|
||||||
for _, a in ipairs(annots) do
|
for _, a in ipairs(annots) do
|
||||||
if a.kind == "work" then
|
for _, r in ipairs(a.reads) do
|
||||||
for _, r in ipairs(a.reads) do
|
if not is_wave_context_reg(r) and r ~= "R_TapePtr" then
|
||||||
if not is_wave_context_reg(r) and r ~= "R_TapePtr" then
|
warnings[#warnings + 1] = {
|
||||||
warnings[#warnings + 1] = {
|
line = a.line,
|
||||||
line = a.line,
|
msg = string.format("atom '%s' reads '%s' which is not a known wave-context register", a.name, r),
|
||||||
msg = string.format("work atom '%s' reads '%s' which is not a known wave-context register", a.name, r),
|
}
|
||||||
}
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
@@ -805,51 +660,19 @@ local function validate(ctx, src)
|
|||||||
check_macro_drift(m, ctx.shared.word_counts[m.name])
|
check_macro_drift(m, ctx.shared.word_counts[m.name])
|
||||||
end
|
end
|
||||||
|
|
||||||
-- 8. atom_<...> _Pragma validation: resource/region/group/cadence/async
|
-- 8. (atom_<...> _Pragma validation DROPPED. The pragma macros
|
||||||
for _, p in ipairs(pragmas) do
|
-- atom_resource / atom_region / atom_group / atom_cadence /
|
||||||
if not atom_index[p.name] then
|
-- atom_async were removed from atom_dsl.h. They may be
|
||||||
errors[#errors + 1] = {
|
-- reintroduced later as sub-calls of atom_info.)
|
||||||
line = p.line,
|
|
||||||
msg = string.format("pragma references unknown atom '%s'", p.name),
|
|
||||||
}
|
|
||||||
end
|
|
||||||
|
|
||||||
for k, v in pairs(p.attrs) do
|
-- 9. (CADENCE_ONDEMAND requires async check DROPPED. Same reason
|
||||||
local spec = ATOM_PRAGMA_KINDS[k]
|
-- as #8.)
|
||||||
if not spec then
|
|
||||||
errors[#errors + 1] = {
|
|
||||||
line = p.line,
|
|
||||||
msg = string.format("'%s' has unknown pragma key '%s' (allowed: resource/region/group/cadence/async)", p.name, k),
|
|
||||||
}
|
|
||||||
elseif spec.allowed and not spec.allowed[v] then
|
|
||||||
local allowed = {}
|
|
||||||
for kk in pairs(spec.allowed) do allowed[#allowed + 1] = kk end
|
|
||||||
table.sort(allowed)
|
|
||||||
local allowed_str = table.concat(allowed, ", ")
|
|
||||||
errors[#errors + 1] = {
|
|
||||||
line = p.line,
|
|
||||||
msg = string.format("'%s' pragma %s=%s but '%s' is not allowed (allowed: %s)", p.name, k, v, v, allowed_str),
|
|
||||||
}
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
-- 9. CADENCE_ONDEMAND requires async=true. Flattened as a guard
|
|
||||||
-- (single condition, no nested if).
|
|
||||||
for _, p in ipairs(pragmas) do
|
|
||||||
if p.attrs.cadence == "ondemand" and p.attrs.async ~= "true" then
|
|
||||||
errors[#errors + 1] = {
|
|
||||||
line = p.line,
|
|
||||||
msg = string.format("'%s' is CADENCE_ONDEMAND but does not declare atom_async(true)", p.name),
|
|
||||||
}
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
-- 10. Information summary.
|
-- 10. Information summary.
|
||||||
info[#info + 1] = {
|
info[#info + 1] = {
|
||||||
line = 0,
|
line = 0,
|
||||||
msg = string.format("scanned: %d atom(s), %d annotation(s), %d pragma(s), %d macro-word-decl(s), %d binds struct(s)",
|
msg = string.format("scanned: %d atom(s), %d annotation(s), %d macro-word-decl(s), %d binds struct(s)",
|
||||||
#atoms, #annots, #pragmas, #macros, #binds),
|
#atoms, #annots, #macros, #binds),
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
|||||||
@@ -459,9 +459,7 @@ local function compute_component_word_count(c, components, wc)
|
|||||||
-- It's a `mac_X(...)` call. Recurse.
|
-- It's a `mac_X(...)` call. Recurse.
|
||||||
n = n + rec(comp_name)
|
n = n + rec(comp_name)
|
||||||
elseif comp_name and wc and wc[comp_name] then
|
elseif comp_name and wc and wc[comp_name] then
|
||||||
-- Encoding macro or pseudo-instruction (e.g. mask_upper = 2,
|
-- Encoding macro or pseudo-instruction (e.g. mask_upper = 2, nop2 = 2).
|
||||||
-- nop2 = 2). Trust the metadata — tape_atom.metadata.h is the
|
|
||||||
-- single source of truth for word counts.
|
|
||||||
n = n + wc[comp_name]
|
n = n + wc[comp_name]
|
||||||
else
|
else
|
||||||
-- Unrecognized token. Fall back to 1 word.
|
-- Unrecognized token. Fall back to 1 word.
|
||||||
@@ -628,12 +626,8 @@ local function emit_component_macros_h(ctx, src, components)
|
|||||||
"// Auto-generated by tape_atom_annotation_pass.lua — DO NOT EDIT",
|
"// Auto-generated by tape_atom_annotation_pass.lua — DO NOT EDIT",
|
||||||
"// Source: " .. to_absolute_path(src.path),
|
"// Source: " .. to_absolute_path(src.path),
|
||||||
"// Component atoms (MipsAtomComp_(ac_*)) -> macro variants (mac_*)",
|
"// Component atoms (MipsAtomComp_(ac_*)) -> macro variants (mac_*)",
|
||||||
"// + auto word-counts (so tape_atom.metadata.h stays manual-only",
|
|
||||||
"// for encoding macros).",
|
|
||||||
"",
|
"",
|
||||||
-- Self-contained: define WORD_COUNT if not already defined.
|
-- Self-contained: define WORD_COUNT if not already defined.
|
||||||
-- The metadata file (tape_atom.metadata.h) defines it as
|
|
||||||
-- enum { words_##name = (count) };
|
|
||||||
-- We use the same definition here so the auto-generated
|
-- We use the same definition here so the auto-generated
|
||||||
-- entries below expand to compile-time constants whether
|
-- entries below expand to compile-time constants whether
|
||||||
-- the metadata file is included first or not.
|
-- the metadata file is included first or not.
|
||||||
|
|||||||
@@ -2,7 +2,6 @@
|
|||||||
--
|
--
|
||||||
-- Generate <module>/gen/<basename>.offsets.h with branch offset
|
-- Generate <module>/gen/<basename>.offsets.h with branch offset
|
||||||
-- immediates for every atom_offset(F, T) reference in atom bodies.
|
-- immediates for every atom_offset(F, T) reference in atom bodies.
|
||||||
-- Ported from scripts/tape_atom.offset_gen.meta.lua:148-389.
|
|
||||||
--
|
--
|
||||||
-- The branch offset regression we just fixed in commit 98e27c2 must
|
-- The branch offset regression we just fixed in commit 98e27c2 must
|
||||||
-- NOT return. The fix was in duffle.lua's split_top_level_commas +
|
-- NOT return. The fix was in duffle.lua's split_top_level_commas +
|
||||||
|
|||||||
@@ -38,9 +38,8 @@ local function render_source_report(source_path, result)
|
|||||||
add("ANNOTATION PASS — " .. source_path)
|
add("ANNOTATION PASS — " .. source_path)
|
||||||
add("========================================================")
|
add("========================================================")
|
||||||
add("")
|
add("")
|
||||||
add(string.format("Atoms: %d Annotations: %d Pragmas: %d Binds structs: %d Macro decls: %d",
|
add(string.format("Atoms: %d Annotations: %d Binds structs: %d Macro decls: %d",
|
||||||
#result.atoms, #result.annots,
|
#result.atoms, #result.annots,
|
||||||
(result.pragmas and #result.pragmas or 0),
|
|
||||||
#result.binds, #result.macros))
|
#result.binds, #result.macros))
|
||||||
add("")
|
add("")
|
||||||
|
|
||||||
@@ -55,9 +54,7 @@ local function render_source_report(source_path, result)
|
|||||||
if a.error then
|
if a.error then
|
||||||
add(string.format(" ✗ line %d %s [ERROR: %s]", a.line, a.macro or "?", a.error))
|
add(string.format(" ✗ line %d %s [ERROR: %s]", a.line, a.macro or "?", a.error))
|
||||||
else
|
else
|
||||||
local line = string.format(" %s line %d %s phase=%s",
|
local line = string.format(" ● line %d %s", a.line, a.name)
|
||||||
a.kind == "work" and "●" or (a.kind == "bind" and "◆" or "○"),
|
|
||||||
a.line, a.name, a.phase or a.kind)
|
|
||||||
if a.binds then line = line .. " binds=" .. a.binds end
|
if a.binds then line = line .. " binds=" .. a.binds end
|
||||||
if #a.reads > 0 then line = line .. " reads={" .. table.concat(a.reads, ",") .. "}" end
|
if #a.reads > 0 then line = line .. " reads={" .. table.concat(a.reads, ",") .. "}" end
|
||||||
if #a.writes > 0 then line = line .. " writes={" .. table.concat(a.writes, ",") .. "}" end
|
if #a.writes > 0 then line = line .. " writes={" .. table.concat(a.writes, ",") .. "}" end
|
||||||
@@ -81,16 +78,6 @@ local function render_source_report(source_path, result)
|
|||||||
end
|
end
|
||||||
add("")
|
add("")
|
||||||
|
|
||||||
add("── Atom pragmas (resource / region / group / cadence / async) ─")
|
|
||||||
if not result.pragmas or #result.pragmas == 0 then add(" (none)") end
|
|
||||||
for _, p in ipairs(result.pragmas or {}) do
|
|
||||||
local kvs = {}
|
|
||||||
for k, v in pairs(p.attrs) do kvs[#kvs + 1] = k .. "=" .. v end
|
|
||||||
table.sort(kvs)
|
|
||||||
add(string.format(" ◇ line %d %s {%s}", p.line, p.name, table.concat(kvs, ", ")))
|
|
||||||
end
|
|
||||||
add("")
|
|
||||||
|
|
||||||
add("── Errors ──────────────────────────────────────────────")
|
add("── Errors ──────────────────────────────────────────────")
|
||||||
if #result.errors == 0 then add(" (none)") end
|
if #result.errors == 0 then add(" (none)") end
|
||||||
for _, e in ipairs(result.errors) do
|
for _, e in ipairs(result.errors) do
|
||||||
@@ -175,12 +162,13 @@ function M.run(ctx)
|
|||||||
local annot_results = (ctx.flags and ctx.flags._annot_results) or {}
|
local annot_results = (ctx.flags and ctx.flags._annot_results) or {}
|
||||||
|
|
||||||
-- Render per-source reports.
|
-- Render per-source reports.
|
||||||
|
-- Hoist ensure_dir out of the loop (cache + hoisting = single mkdir).
|
||||||
|
if not ctx.dry_run then ensure_dir(ctx.out_root) end
|
||||||
for _, entry in ipairs(annot_results) do
|
for _, entry in ipairs(annot_results) do
|
||||||
local src = entry.source
|
local src = entry.source
|
||||||
local result = entry.result
|
local result = entry.result
|
||||||
local out_path = ctx.out_root .. "/" .. src.basename .. ".annotations.txt"
|
local out_path = ctx.out_root .. "/" .. src.basename .. ".annotations.txt"
|
||||||
if not ctx.dry_run then
|
if not ctx.dry_run then
|
||||||
ensure_dir(ctx.out_root)
|
|
||||||
write_file(out_path, render_source_report(src.path, result))
|
write_file(out_path, render_source_report(src.path, result))
|
||||||
end
|
end
|
||||||
table.insert(outputs, { annotations_txt = out_path })
|
table.insert(outputs, { annotations_txt = out_path })
|
||||||
@@ -190,12 +178,12 @@ function M.run(ctx)
|
|||||||
if not ctx.dry_run then
|
if not ctx.dry_run then
|
||||||
-- The project report references each source by its absolute path.
|
-- The project report references each source by its absolute path.
|
||||||
-- Augment the entries with a .source field for the per-source error counts.
|
-- Augment the entries with a .source field for the per-source error counts.
|
||||||
|
-- ensure_dir was already hoisted above; cache makes this a no-op.
|
||||||
local all_results = {}
|
local all_results = {}
|
||||||
for _, entry in ipairs(annot_results) do
|
for _, entry in ipairs(annot_results) do
|
||||||
entry.result.source = entry.source.path
|
entry.result.source = entry.source.path
|
||||||
table.insert(all_results, entry.result)
|
table.insert(all_results, entry.result)
|
||||||
end
|
end
|
||||||
ensure_dir(ctx.out_root)
|
|
||||||
local summary_path = ctx.out_root .. "/annotation_validation.txt"
|
local summary_path = ctx.out_root .. "/annotation_validation.txt"
|
||||||
write_file(summary_path, render_project_report(all_results))
|
write_file(summary_path, render_project_report(all_results))
|
||||||
table.insert(outputs, { summary_txt = summary_path })
|
table.insert(outputs, { summary_txt = summary_path })
|
||||||
|
|||||||
@@ -1,7 +1,635 @@
|
|||||||
-- passes/static_analysis.lua
|
-- passes/static_analysis.lua
|
||||||
--
|
--
|
||||||
-- [FUTURE] Per-atom static-analysis checks. Stub for now; the upcoming
|
-- Per-atom static-analysis checks for the tape-atom build pipeline.
|
||||||
-- static_analysis_atoms_20260708 track will deliver the 5 (+1) checks.
|
-- Currently ships Phase 1 checks (GTE pipeline-fill + mac_yield
|
||||||
|
-- uniformity). Phases 2/3 (ABI handoff discipline, GPU port-store
|
||||||
|
-- shape, per-atom cycle budget) extend this file.
|
||||||
|
--
|
||||||
|
-- Workspace boundary: same conventions as annotation.lua
|
||||||
|
-- - primitives from duffle.lua (read_parens, read_braces, scan_to_char, LineIndex)
|
||||||
|
-- - LPeg not needed: pure hand-rolled string scanning
|
||||||
|
-- - 5.3-compatible (no <close>, no continue keyword)
|
||||||
|
-- - no :match/:gmatch
|
||||||
|
-- - tab indent, EmmyLua @class/@param annotations
|
||||||
|
--
|
||||||
|
-- The orchestrator (ps1_meta.lua) wires this module in via the
|
||||||
|
-- PASSES table:
|
||||||
|
-- ["static-analysis"] = {
|
||||||
|
-- module = "passes.static_analysis",
|
||||||
|
-- kind = "validation", -- errors stop the build
|
||||||
|
-- deps = {"word-counts", "components"},
|
||||||
|
-- out = { { kind = "report",
|
||||||
|
-- path_template = "<out_root>/<basename>.static_analysis.txt" } },
|
||||||
|
-- }
|
||||||
|
|
||||||
|
-- ════════════════════════════════════════════════════════════════════════════
|
||||||
|
-- Module-scope requires + package.path setup
|
||||||
|
-- ════════════════════════════════════════════════════════════════════════════
|
||||||
|
|
||||||
|
local script_path = arg and arg[0] or "?"
|
||||||
|
local last_sep = 0
|
||||||
|
for i = 1, #script_path do
|
||||||
|
local c = script_path:sub(i, i)
|
||||||
|
if c == "/" or c == "\\" then last_sep = i end
|
||||||
|
end
|
||||||
|
local script_dir = last_sep == 0 and "./" or script_path:sub(1, last_sep)
|
||||||
|
package.path = script_dir .. "../?.lua;" .. script_dir .. "../?/init.lua;" .. script_dir .. "?.lua;" .. package.path
|
||||||
|
|
||||||
|
local duffle = require("duffle")
|
||||||
|
local read_ident = duffle.read_ident
|
||||||
|
local skip_ws_and_cmt = duffle.skip_ws_and_cmt
|
||||||
|
local read_parens = duffle.read_parens
|
||||||
|
local read_braces = duffle.read_braces
|
||||||
|
local read_brackets = duffle.read_brackets
|
||||||
|
local scan_to_char = duffle.scan_to_char
|
||||||
|
local split_top_level_commas = duffle.split_top_level_commas
|
||||||
|
local trim = duffle.trim
|
||||||
|
local ensure_dir = duffle.ensure_dir
|
||||||
|
local write_file = duffle.write_file
|
||||||
|
local basename_no_ext = duffle.basename_no_ext
|
||||||
|
|
||||||
|
-- Latency table lives in duffle.lua (shared between this pass + future
|
||||||
|
-- per-atom cycle-budget pass). Lazily read on first use.
|
||||||
|
local GTE_PIPELINE_LATENCY = duffle.GTE_PIPELINE_LATENCY
|
||||||
|
|
||||||
|
-- ════════════════════════════════════════════════════════════════════════════
|
||||||
|
-- Source walkers
|
||||||
|
-- ════════════════════════════════════════════════════════════════════════════
|
||||||
|
|
||||||
|
--- Walk source-as-written, return a list of `{line, name, body,
|
||||||
|
--- body_off, kind}` for every:
|
||||||
|
--- `MipsAtom_(name) { body };` -> kind = "atom" (baked atom)
|
||||||
|
--- `MipsAtomComp_(name) { body };` -> kind = "comp_bare" (static-array component)
|
||||||
|
--- `MipsAtomComp_Proc_(name, { body })` -> kind = "comp_proc" (procedural component)
|
||||||
|
---
|
||||||
|
--- All three forms are recognized because the user explicitly uses
|
||||||
|
--- both bare components (e.g. `ac_gte_store_f3_post_rtpt`) and
|
||||||
|
--- procedural components (e.g. `ac_format_f3_color(r, g, b)`) inside
|
||||||
|
--- atom bodies. The two component forms generate macro equivalents
|
||||||
|
--- (in gen/duffle.macs.h) that atoms call via `mac_*` -- so the parent
|
||||||
|
--- atom body is what needs the GTE pipeline-fill + mac_yield checks.
|
||||||
|
--- The component bodies themselves don't need mac_yield (control
|
||||||
|
--- transfer is the parent atom's job) but they DO need pre-fill nops
|
||||||
|
--- before any gte_cmdw_X they contain.
|
||||||
|
---
|
||||||
|
--- Comments / strings inside `name` and `body` are tolerated; `body`
|
||||||
|
--- is the raw brace inner text (with surrounding whitespace, no
|
||||||
|
--- leading/trailing `{` `}`). `body_off` is the character offset of
|
||||||
|
--- `body[1]` in `source_text`, used to compute per-token line numbers
|
||||||
|
--- later.
|
||||||
|
local function find_atom_bodies(source_text)
|
||||||
|
local line_of = duffle.LineIndex(source_text)
|
||||||
|
local out = {}
|
||||||
|
local len = #source_text
|
||||||
|
local i = 1
|
||||||
|
while i <= len do
|
||||||
|
i = skip_ws_and_cmt(source_text, i); if i > len then break end
|
||||||
|
local ident, after = read_ident(source_text, i)
|
||||||
|
if not ident then
|
||||||
|
i = i + 1
|
||||||
|
elseif ident == "MipsAtom_"
|
||||||
|
or ident == "MipsAtomComp_"
|
||||||
|
or ident == "MipsAtomComp_Proc_" then
|
||||||
|
-- Determine the kind from the exact ident (3 distinct macros,
|
||||||
|
-- each with its own kind).
|
||||||
|
local kind
|
||||||
|
if ident == "MipsAtom_" then kind = "atom"
|
||||||
|
elseif ident == "MipsAtomComp_" then kind = "comp_bare"
|
||||||
|
else kind = "comp_proc"
|
||||||
|
end
|
||||||
|
|
||||||
|
local open = skip_ws_and_cmt(source_text, after)
|
||||||
|
if source_text:sub(open, open) ~= "(" then
|
||||||
|
i = open + 1
|
||||||
|
else
|
||||||
|
local inner, after_paren = read_parens(source_text, open)
|
||||||
|
|
||||||
|
if kind == "comp_proc" then
|
||||||
|
-- MipsAtomComp_Proc_(sym, { body })
|
||||||
|
-- The body is inside the LAST `{ ... }` in the args
|
||||||
|
-- (the macro takes 2 args: sym name, then body in {}).
|
||||||
|
-- Find the last `{` in `inner`, then the matching `}`.
|
||||||
|
local last_open
|
||||||
|
for k = #inner, 1, -1 do
|
||||||
|
if inner:sub(k, k) == "{" then last_open = k; break end
|
||||||
|
end
|
||||||
|
if not last_open then
|
||||||
|
i = open + 1
|
||||||
|
else
|
||||||
|
-- Walk forward to find matching `}` honoring balanced
|
||||||
|
-- ()/[] and strings. We could call duffle.read_braces
|
||||||
|
-- from last_open+1, but read_braces expects to start at
|
||||||
|
-- the brace itself. Inline the walk for clarity.
|
||||||
|
local depth = 1
|
||||||
|
local j = last_open + 1
|
||||||
|
while j <= #inner and depth > 0 do
|
||||||
|
local c = inner:byte(j)
|
||||||
|
if c == 123 then
|
||||||
|
depth = depth + 1; j = j + 1
|
||||||
|
elseif c == 125 then
|
||||||
|
depth = depth - 1
|
||||||
|
if depth == 0 then break end
|
||||||
|
j = j + 1
|
||||||
|
elseif c == 40 then
|
||||||
|
local _, a = read_parens(inner, j); j = a
|
||||||
|
elseif c == 91 then
|
||||||
|
local _, a = read_brackets(inner, j); j = a
|
||||||
|
elseif c == 34 or c == 39 then
|
||||||
|
j = duffle.skip_str_or_cmt(inner, j) + 1
|
||||||
|
else
|
||||||
|
j = j + 1
|
||||||
|
end
|
||||||
|
end
|
||||||
|
if depth ~= 0 then
|
||||||
|
-- unmatched; bail
|
||||||
|
i = open + 1
|
||||||
|
else
|
||||||
|
-- First ident in `inner` is the comp name.
|
||||||
|
local name_match = inner:match("^%s*([%w_]+)")
|
||||||
|
local name = name_match or "?"
|
||||||
|
local body = inner:sub(last_open + 1, j - 1)
|
||||||
|
-- body_off in full source: position right after the
|
||||||
|
-- LAST `{` in `inner`, which sits at `open+1+last_open`
|
||||||
|
-- (open+1 = just inside the outer paren, +last_open
|
||||||
|
-- = at the `{`).
|
||||||
|
local body_off = open + 1 + last_open
|
||||||
|
out[#out + 1] = {
|
||||||
|
line = line_of(i),
|
||||||
|
name = name,
|
||||||
|
body = body,
|
||||||
|
body_off = body_off + 1,
|
||||||
|
kind = kind,
|
||||||
|
}
|
||||||
|
i = after_paren
|
||||||
|
end
|
||||||
|
end
|
||||||
|
else
|
||||||
|
-- MipsAtom_(sym) { body }; OR
|
||||||
|
-- MipsAtomComp_(sym) { body };
|
||||||
|
-- name is the first arg, body is the FIRST { ... } after
|
||||||
|
-- the paren.
|
||||||
|
local a = 1
|
||||||
|
while a <= #inner and inner:sub(a, a):match("[%s]") do a = a + 1 end
|
||||||
|
local b = a
|
||||||
|
while b <= #inner and inner:sub(b, b):match("[%w_]") do b = b + 1 end
|
||||||
|
local name = inner:sub(a, b - 1)
|
||||||
|
if name == "" then
|
||||||
|
i = open + 1
|
||||||
|
else
|
||||||
|
local brace = scan_to_char(source_text, "{", after_paren)
|
||||||
|
if brace then
|
||||||
|
local body, after_brace = read_braces(source_text, brace)
|
||||||
|
local body_off = brace + 1
|
||||||
|
out[#out + 1] = {
|
||||||
|
line = line_of(i),
|
||||||
|
name = name,
|
||||||
|
body = body,
|
||||||
|
body_off = body_off,
|
||||||
|
kind = kind,
|
||||||
|
}
|
||||||
|
i = after_brace
|
||||||
|
else
|
||||||
|
i = open + 1
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
else
|
||||||
|
i = after
|
||||||
|
end
|
||||||
|
end
|
||||||
|
return out
|
||||||
|
end
|
||||||
|
|
||||||
|
-- ════════════════════════════════════════════════════════════════════════════
|
||||||
|
-- Body tokenizer (top-level comma splitter + per-token classification)
|
||||||
|
-- ════════════════════════════════════════════════════════════════════════════
|
||||||
|
|
||||||
|
--- Build a map: `body_relative_char_offset` -> `body_relative_line`.
|
||||||
|
--- Used by the checks to convert per-token offsets in the body to line
|
||||||
|
--- numbers relative to the start of `body`. The atom's source-line of
|
||||||
|
--- the body-start is added by the caller.
|
||||||
|
---
|
||||||
|
--- Simple line-counting: count `\n` chars from offset 1 up to the
|
||||||
|
--- offset; that count + 1 is the line number (1-based).
|
||||||
|
local function build_body_line_index(body)
|
||||||
|
local index = {}
|
||||||
|
local len = #body
|
||||||
|
local newline_count = 0
|
||||||
|
for i = 1, len do
|
||||||
|
if i > 1 then
|
||||||
|
index[i] = newline_count + 1 -- line of `i` relative to body
|
||||||
|
end
|
||||||
|
if body:byte(i) == 10 then -- '\n'
|
||||||
|
newline_count = newline_count + 1
|
||||||
|
end
|
||||||
|
end
|
||||||
|
-- Offsets beyond the body still resolve to the final line
|
||||||
|
index[len + 1] = newline_count + 1
|
||||||
|
return index
|
||||||
|
end
|
||||||
|
|
||||||
|
--- Count of COP2-nop words contributed by a single top-level token.
|
||||||
|
-- `nop` -> 1
|
||||||
|
-- `nop2` -> 2 (i.e. `nop, nop` baked into one asm arg)
|
||||||
|
-- `nop,` / `nop2,` -> same as above; strip trailing comma defensively
|
||||||
|
-- anything else -> 0
|
||||||
|
--
|
||||||
|
-- (Branch-delay-slot nops like `branch_*(..., nop)` are tokenized
|
||||||
|
-- separately by split_top_level_commas: the branch arg ends before
|
||||||
|
-- the trailing comma, and `nop` becomes its own token. So no special
|
||||||
|
-- handling is needed here.)
|
||||||
|
local function nop_word_count(token)
|
||||||
|
local s = trim(token)
|
||||||
|
-- strip trailing comma(s) (defensive against raw text via, but our
|
||||||
|
-- tokenize_body already strips them; this is a safety net)
|
||||||
|
s = s:gsub(",$", "")
|
||||||
|
s = trim(s)
|
||||||
|
if s == "nop" then return 1 end
|
||||||
|
if s == "nop2" then return 2 end
|
||||||
|
return 0
|
||||||
|
end
|
||||||
|
|
||||||
|
--- Tokenize the body inner-text into a flat list of `(token, body_rel_offset)`
|
||||||
|
--- pairs (nested parens/braces/brackets are honored; comments and strings
|
||||||
|
--- are skipped). `body_rel_offset` is the char offset within `body` of the
|
||||||
|
--- start of the token — callers add it to the atom's `body_off` to get
|
||||||
|
--- an absolute source position for line tracking.
|
||||||
|
local function tokenize_body(body)
|
||||||
|
local out = {}
|
||||||
|
local len = #body
|
||||||
|
local rel = 1
|
||||||
|
while rel <= len do
|
||||||
|
-- Find next non-whitespace, non-comment start
|
||||||
|
local ws_end = skip_ws_and_cmt(body, rel)
|
||||||
|
if ws_end > rel then
|
||||||
|
rel = ws_end
|
||||||
|
end
|
||||||
|
if rel > len then break end
|
||||||
|
|
||||||
|
-- Find comma/newline/semicolon after this token. Read balanced
|
||||||
|
-- groups so commas inside parens/braces/brackets aren't treated
|
||||||
|
-- as separators. Comments / strings are skipped.
|
||||||
|
local i = rel
|
||||||
|
while i <= len do
|
||||||
|
local c = body:byte(i)
|
||||||
|
if c == 44 then break end -- ','
|
||||||
|
if c == 10 then break end -- '\n'
|
||||||
|
if c == 59 then break end -- ';'
|
||||||
|
if c == 40 then -- '('
|
||||||
|
local _, a = read_parens(body, i); i = a
|
||||||
|
elseif c == 123 then -- '{'
|
||||||
|
local _, a = read_braces(body, i); i = a
|
||||||
|
elseif c == 91 then -- '['
|
||||||
|
local _, a = read_brackets(body, i); i = a
|
||||||
|
elseif c == 34 or c == 39 then -- '"' or '\''
|
||||||
|
i = duffle.skip_str_or_cmt(body, i) + 1
|
||||||
|
else
|
||||||
|
i = i + 1
|
||||||
|
end
|
||||||
|
end
|
||||||
|
-- Extract token [rel .. i-1]
|
||||||
|
local tok = trim(body:sub(rel, i - 1))
|
||||||
|
if tok ~= "" then
|
||||||
|
out[#out + 1] = { tok = tok, rel = rel }
|
||||||
|
end
|
||||||
|
-- Move past the separator
|
||||||
|
if i <= len then
|
||||||
|
i = i + 1
|
||||||
|
-- Also skip whitespace before next token
|
||||||
|
local w = skip_ws_and_cmt(body, i)
|
||||||
|
if w > i then i = w end
|
||||||
|
end
|
||||||
|
rel = i
|
||||||
|
end
|
||||||
|
return out
|
||||||
|
end
|
||||||
|
|
||||||
|
-- ════════════════════════════════════════════════════════════════════════════
|
||||||
|
-- Check #1: GTE pipeline-fill
|
||||||
|
-- ════════════════════════════════════════════════════════════════════════════
|
||||||
|
|
||||||
|
--- Walk the token list. Whenever we hit a `gte_cmdw_<X>` token, count
|
||||||
|
--- consecutive nop words starting at the next token. If count < the
|
||||||
|
--- minimum declared in `GTE_PIPELINE_LATENCY[X]`, record a finding.
|
||||||
|
---
|
||||||
|
--- Aliases (`gte_cmdw_rotate_translate_perspective_single` etc.) are
|
||||||
|
--- resolved against the lookup table directly; if a macro name is not
|
||||||
|
--- in the table, emit a soft warning (the user might have added a new
|
||||||
|
--- gte_cmdw_* but not updated duffle.lua).
|
||||||
|
local function check_gte_pipeline_fill(atoms, findings, line_of)
|
||||||
|
-- Walk the token list. Whenever we hit a `gte_cmdw_<X>` token,
|
||||||
|
-- count consecutive `nop` words IMMEDIATELY PRECEDING it (the
|
||||||
|
-- source-level `nop2, gte_cmdw_X` idiom provides the pre-pipeline
|
||||||
|
-- fill that gte.h's wrapper functions provide internally). If
|
||||||
|
-- count < `GTE_PIPELINE_LATENCY[X]`, record a finding.
|
||||||
|
--
|
||||||
|
-- We count nops going backwards from the cmdw token, stopping at
|
||||||
|
-- the first non-nop token. Tokens like `mem_share` or `port_write`
|
||||||
|
-- (any non-nop) break the count. `gte_mv_to_data_r` (writes to
|
||||||
|
-- C2_DR registers) are non-nops in this sense -- they count as
|
||||||
|
-- "previous GTE state" but don't themselves count as pipeline
|
||||||
|
-- fill.
|
||||||
|
for _, a in ipairs(atoms) do
|
||||||
|
local tokens = tokenize_body(a.body)
|
||||||
|
local line_in_body = build_body_line_index(a.body)
|
||||||
|
local tn = #tokens
|
||||||
|
local ti = 1
|
||||||
|
while ti <= tn do
|
||||||
|
local tok = tokens[ti].tok
|
||||||
|
local cmdw_full = tok:match("^(gte_cmdw_[%w_]+)%s*[,%)]")
|
||||||
|
or tok:match("^(gte_cmdw_[%w_]+)%s*$")
|
||||||
|
if cmdw_full then
|
||||||
|
local variant = cmdw_full:match("^gte_cmdw_(.+)$")
|
||||||
|
local need = GTE_PIPELINE_LATENCY[cmdw_full]
|
||||||
|
if need == nil then
|
||||||
|
-- alias or new gte_cmdw_<X> not yet in latency table
|
||||||
|
local line = a.line + line_in_body[tokens[ti].rel]
|
||||||
|
findings[#findings + 1] = {
|
||||||
|
atom = a.name,
|
||||||
|
line = line,
|
||||||
|
check = "gte_pipeline_fill",
|
||||||
|
kind = "warning",
|
||||||
|
msg = string.format(
|
||||||
|
"%s at line %d uses `gte_cmdw_%s` but that macro is not in duffle.GTE_PIPELINE_LATENCY -- add a min_nops entry",
|
||||||
|
a.name, line, variant),
|
||||||
|
}
|
||||||
|
ti = ti + 1
|
||||||
|
elseif need > 0 then
|
||||||
|
-- Count consecutive nops immediately BEFORE the cmdw
|
||||||
|
-- token. We walk tokens[ti - n] backwards, accumulating
|
||||||
|
-- nop_word_count, stopping at the first non-nop.
|
||||||
|
local have = 0
|
||||||
|
local where_ti = ti - 1
|
||||||
|
while where_ti >= 1 do
|
||||||
|
local n = nop_word_count(tokens[where_ti].tok)
|
||||||
|
if n == 0 then break end
|
||||||
|
have = have + n
|
||||||
|
where_ti = where_ti - 1
|
||||||
|
end
|
||||||
|
if have < need then
|
||||||
|
local line = a.line + line_in_body[tokens[ti].rel]
|
||||||
|
findings[#findings + 1] = {
|
||||||
|
atom = a.name,
|
||||||
|
line = line,
|
||||||
|
check = "gte_pipeline_fill",
|
||||||
|
kind = "error",
|
||||||
|
msg = string.format(
|
||||||
|
"%s at line %d needs %d nop word%s immediately BEFORE `gte_cmdw_%s`; only %d found",
|
||||||
|
a.name, line, need, need == 1 and "" or "s", variant, have),
|
||||||
|
}
|
||||||
|
end
|
||||||
|
ti = ti + 1
|
||||||
|
else
|
||||||
|
ti = ti + 1
|
||||||
|
end
|
||||||
|
else
|
||||||
|
ti = ti + 1
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
-- ════════════════════════════════════════════════════════════════════════════
|
||||||
|
-- Check #2: mac_yield uniformity
|
||||||
|
-- ════════════════════════════════════════════════════════════════════════════
|
||||||
|
|
||||||
|
--- Every atom body must contain exactly one `mac_yield()` call and it
|
||||||
|
--- must be the LAST top-level token in the body (so the tape runtime
|
||||||
|
--- can pick up cleanly at the next atom's bound registers).
|
||||||
|
---
|
||||||
|
--- Empty bodies are not currently flagged — runtime infrastructure
|
||||||
|
--- atoms like `MipsAtom_(yield) { mac_yield() }` and `MipsAtom_(tape_exit)
|
||||||
|
--- { jump_reg(rret_addr), nop }` are valid as-is; mac_yield at the end
|
||||||
|
--- is the contract.
|
||||||
|
local function check_mac_yield_uniformity(atoms, findings)
|
||||||
|
-- Per-kind semantics:
|
||||||
|
-- MipsAtom_ (baked atom): exactly 1 mac_yield at the end of
|
||||||
|
-- the body. Control transfer is the atom's job.
|
||||||
|
-- MipsAtomComp_ (bare static-array component): ZERO mac_yield.
|
||||||
|
-- The component is invoked from inside an atom
|
||||||
|
-- body; the parent atom does the yield.
|
||||||
|
-- MipsAtomComp_Proc_ (procedural component): ZERO mac_yield.
|
||||||
|
-- Same reasoning -- it's a function returning
|
||||||
|
-- a MipsAtom slice, invoked from a parent atom.
|
||||||
|
--
|
||||||
|
-- The GTE pipeline-fill check applies to all 3 kinds (see
|
||||||
|
-- check_gte_pipeline_fill). Only the mac_yield rule branches on kind.
|
||||||
|
for _, a in ipairs(atoms) do
|
||||||
|
local tokens = tokenize_body(a.body)
|
||||||
|
local line_in_body = build_body_line_index(a.body)
|
||||||
|
|
||||||
|
local count = 0
|
||||||
|
local last_idx = 0
|
||||||
|
for i, t in ipairs(tokens) do
|
||||||
|
local tok = t.tok
|
||||||
|
-- Match `mac_yield(...)` or just `mac_yield`. The bareword
|
||||||
|
-- variant is rare in modern style but tolerated.
|
||||||
|
if tok:match("^mac_yield%s*%(") or tok == "mac_yield" then
|
||||||
|
count = count + 1
|
||||||
|
last_idx = i
|
||||||
|
end
|
||||||
|
end
|
||||||
|
local function line_for(idx)
|
||||||
|
return a.line + line_in_body[tokens[idx].rel]
|
||||||
|
end
|
||||||
|
|
||||||
|
if a.kind == "atom" then
|
||||||
|
-- Baked atom: exactly 1 yield at the end.
|
||||||
|
if count == 0 then
|
||||||
|
findings[#findings + 1] = {
|
||||||
|
atom = a.name,
|
||||||
|
line = a.line,
|
||||||
|
check = "mac_yield_uniformity",
|
||||||
|
kind = "warning",
|
||||||
|
msg = string.format(
|
||||||
|
"%s at line %d has no `mac_yield()`; every atom must hand control to the next via mac_yield at end",
|
||||||
|
a.name, a.line),
|
||||||
|
}
|
||||||
|
elseif count > 1 then
|
||||||
|
findings[#findings + 1] = {
|
||||||
|
atom = a.name,
|
||||||
|
line = line_for(last_idx),
|
||||||
|
check = "mac_yield_uniformity",
|
||||||
|
kind = "warning",
|
||||||
|
msg = string.format(
|
||||||
|
"%s at line %d has %d `mac_yield()` calls; exactly 1 is allowed",
|
||||||
|
a.name, line_for(last_idx), count),
|
||||||
|
}
|
||||||
|
elseif last_idx < #tokens then
|
||||||
|
-- 1 call, but not the last token. We DON'T fail if the
|
||||||
|
-- post-token is just `nop` or `nop2` or a branch with `, nop`
|
||||||
|
-- delay slot -- it's the standard "yield, then BD nop" idiom.
|
||||||
|
local post_non_nop = false
|
||||||
|
for j = last_idx + 1, #tokens do
|
||||||
|
local t = tokens[j].tok
|
||||||
|
if t ~= "" and t ~= "nop" and t ~= "nop2"
|
||||||
|
and not t:match("%,%s*nop%)%s*$") then
|
||||||
|
post_non_nop = true
|
||||||
|
break
|
||||||
|
end
|
||||||
|
end
|
||||||
|
if post_non_nop then
|
||||||
|
findings[#findings + 1] = {
|
||||||
|
atom = a.name,
|
||||||
|
line = line_for(last_idx),
|
||||||
|
check = "mac_yield_uniformity",
|
||||||
|
kind = "warning",
|
||||||
|
msg = string.format(
|
||||||
|
"%s at line %d has `mac_yield()` at token %d/%d; the yield must be the LAST non-nop token in the body",
|
||||||
|
a.name, line_for(last_idx), last_idx, #tokens),
|
||||||
|
}
|
||||||
|
end
|
||||||
|
end
|
||||||
|
else
|
||||||
|
-- Component (comp_bare or comp_proc): ZERO yields. The parent
|
||||||
|
-- atom does the yield. A yield inside a component would either
|
||||||
|
-- be dead code (bare) or prematurely terminate the function
|
||||||
|
-- (proc). Both are bugs.
|
||||||
|
if count > 0 then
|
||||||
|
findings[#findings + 1] = {
|
||||||
|
atom = a.name,
|
||||||
|
line = line_for(last_idx),
|
||||||
|
check = "mac_yield_uniformity",
|
||||||
|
kind = "warning",
|
||||||
|
msg = string.format(
|
||||||
|
"%s at line %d is a %s component but has %d `mac_yield()` call(s); components must not yield (the parent atom does)",
|
||||||
|
a.name, line_for(last_idx), a.kind, count),
|
||||||
|
}
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
-- ════════════════════════════════════════════════════════════════════════════
|
||||||
|
-- Per-source validation
|
||||||
|
-- ════════════════════════════════════════════════════════════════════════════
|
||||||
|
|
||||||
|
local function validate(ctx, src)
|
||||||
|
local source = src.text
|
||||||
|
local atoms = find_atom_bodies(source)
|
||||||
|
|
||||||
|
local findings = {}
|
||||||
|
check_gte_pipeline_fill(atoms, findings)
|
||||||
|
check_mac_yield_uniformity(atoms, findings)
|
||||||
|
|
||||||
|
-- Phase 2 (ABI handoff / GPU port-store shape) and Phase 3 (cycle
|
||||||
|
-- budget) hooks go here when those tracks are reactivated.
|
||||||
|
-- check_abi_handoff(atoms, findings)
|
||||||
|
-- check_gpu_portstore_shape(atoms, findings)
|
||||||
|
-- emit per-atom cycle counts via INSTRUCTION_LATENCY table
|
||||||
|
|
||||||
|
local errors = {}
|
||||||
|
local warnings = {}
|
||||||
|
local info = {}
|
||||||
|
for _, f in ipairs(findings) do
|
||||||
|
-- Per-finding severity is set by the check via `f.kind`
|
||||||
|
-- ("error" or "warning"). A `gte_pipeline_fill` finding can be
|
||||||
|
-- either severity (errors for missing nops; warnings for unknown
|
||||||
|
-- cmdw macros not in the latency table). Bin by `kind`, not by
|
||||||
|
-- check name.
|
||||||
|
if f.kind == "error" then
|
||||||
|
errors[#errors + 1] = { line = f.line, msg = f.msg }
|
||||||
|
else
|
||||||
|
warnings[#warnings + 1] = { line = f.line, msg = f.msg }
|
||||||
|
end
|
||||||
|
end
|
||||||
|
info[#info + 1] = {
|
||||||
|
line = 0,
|
||||||
|
msg = string.format("scanned: %d atom bodies; %d findings", #atoms, #findings),
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
atoms = atoms,
|
||||||
|
findings = findings,
|
||||||
|
errors = errors,
|
||||||
|
warnings = warnings,
|
||||||
|
info = info,
|
||||||
|
}
|
||||||
|
end
|
||||||
|
|
||||||
|
-- ════════════════════════════════════════════════════════════════════════════
|
||||||
|
-- Per-source output: build/gen/<basename>.static_analysis.txt
|
||||||
|
-- ════════════════════════════════════════════════════════════════════════════
|
||||||
|
|
||||||
|
local function emit_static_analysis_txt(ctx, src, result)
|
||||||
|
local out_path = ctx.out_root .. "/" .. src.basename .. ".static_analysis.txt"
|
||||||
|
if ctx.dry_run then return out_path end
|
||||||
|
ensure_dir(ctx.out_root)
|
||||||
|
|
||||||
|
local lines = {}
|
||||||
|
local function add(s) lines[#lines + 1] = s end
|
||||||
|
|
||||||
|
add("========================================================")
|
||||||
|
add("STATIC ANALYSIS PASS -- " .. src.path)
|
||||||
|
add("========================================================")
|
||||||
|
add("")
|
||||||
|
-- Tally atoms by kind for the header summary
|
||||||
|
local n_atoms, n_bare, n_proc = 0, 0, 0
|
||||||
|
for _, a in ipairs(result.atoms) do
|
||||||
|
n_atoms = n_atoms + 1
|
||||||
|
if a.kind == "comp_bare" then n_bare = n_bare + 1
|
||||||
|
elseif a.kind == "comp_proc" then n_proc = n_proc + 1
|
||||||
|
end
|
||||||
|
end
|
||||||
|
local header_atoms = string.format("Atoms: %d", n_atoms)
|
||||||
|
if n_bare > 0 or n_proc > 0 then
|
||||||
|
header_atoms = header_atoms .. string.format(" (atoms: %d, comp_bare: %d, comp_proc: %d)",
|
||||||
|
n_atoms - n_bare - n_proc, n_bare, n_proc)
|
||||||
|
end
|
||||||
|
add(string.format("%s Findings: %d Errors: %d Warnings: %d",
|
||||||
|
header_atoms, #result.findings, #result.errors, #result.warnings))
|
||||||
|
add("")
|
||||||
|
|
||||||
|
-- Group findings by atom for readability
|
||||||
|
local by_atom = {}
|
||||||
|
for _, f in ipairs(result.findings) do
|
||||||
|
by_atom[f.atom] = by_atom[f.atom] or {}
|
||||||
|
by_atom[f.atom][#by_atom[f.atom] + 1] = f
|
||||||
|
end
|
||||||
|
|
||||||
|
if next(by_atom) == nil then
|
||||||
|
add(" (no findings -- every atom passed all checks)")
|
||||||
|
else
|
||||||
|
add("── Findings by atom ─────────────────────────────────────")
|
||||||
|
for _, a in ipairs(result.atoms) do
|
||||||
|
local fs = by_atom[a.name]
|
||||||
|
if fs then
|
||||||
|
add(string.format(" %s line %d", a.name, a.line))
|
||||||
|
for _, f in ipairs(fs) do
|
||||||
|
add(string.format(" [%s] %s", f.check, f.msg))
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
add("")
|
||||||
|
add("── Errors ──────────────────────────────────────────────")
|
||||||
|
if #result.errors == 0 then add(" (none)") end
|
||||||
|
for _, e in ipairs(result.errors) do
|
||||||
|
add(string.format(" X line %d %s", e.line, e.msg))
|
||||||
|
end
|
||||||
|
|
||||||
|
add("")
|
||||||
|
add("── Warnings ────────────────────────────────────────────")
|
||||||
|
if #result.warnings == 0 then add(" (none)") end
|
||||||
|
for _, w in ipairs(result.warnings) do
|
||||||
|
add(string.format(" ! line %d %s", w.line, w.msg))
|
||||||
|
end
|
||||||
|
|
||||||
|
add("")
|
||||||
|
add("── Info ────────────────────────────────────────────────")
|
||||||
|
for _, i_ in ipairs(result.info) do
|
||||||
|
add(string.format(" %s", i_.msg))
|
||||||
|
end
|
||||||
|
|
||||||
|
write_file(out_path, table.concat(lines, "\n") .. "\n")
|
||||||
|
return out_path
|
||||||
|
end
|
||||||
|
|
||||||
|
-- ════════════════════════════════════════════════════════════════════════════
|
||||||
|
-- M.run — orchestrator entry
|
||||||
|
-- ════════════════════════════════════════════════════════════════════════════
|
||||||
|
|
||||||
--- @class M
|
--- @class M
|
||||||
|
|
||||||
@@ -10,7 +638,25 @@ local M = {}
|
|||||||
--- @param ctx PassCtx
|
--- @param ctx PassCtx
|
||||||
--- @return PassResult
|
--- @return PassResult
|
||||||
function M.run(ctx)
|
function M.run(ctx)
|
||||||
return { outputs = {}, errors = {}, warnings = {} }
|
local outputs = {}
|
||||||
|
local errors = {}
|
||||||
|
local warnings = {}
|
||||||
|
|
||||||
|
for _, src in ipairs(ctx.sources) do
|
||||||
|
local result = validate(ctx, src)
|
||||||
|
local out_path = emit_static_analysis_txt(ctx, src, result)
|
||||||
|
if out_path then
|
||||||
|
table.insert(outputs, { static_analysis_txt = out_path })
|
||||||
|
end
|
||||||
|
for _, e in ipairs(result.errors) do
|
||||||
|
errors[#errors + 1] = { line = e.line, msg = e.msg }
|
||||||
|
end
|
||||||
|
for _, w in ipairs(result.warnings) do
|
||||||
|
warnings[#warnings + 1] = { line = w.line, msg = w.msg }
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
return { outputs = outputs, errors = errors, warnings = warnings }
|
||||||
end
|
end
|
||||||
|
|
||||||
return M
|
return M
|
||||||
@@ -81,10 +81,6 @@ local M = {}
|
|||||||
--- (recursively if needed). For `nop2` etc., returns wc[name].
|
--- (recursively if needed). For `nop2` etc., returns wc[name].
|
||||||
--- For unknown macros, returns 1 and (optionally) warns.
|
--- For unknown macros, returns 1 and (optionally) warns.
|
||||||
---
|
---
|
||||||
--- PORT NOTE: taken verbatim from tape_atom.offset_gen.meta.lua:130-141
|
|
||||||
--- (`word_count_of_token`). Behavior is identical to preserve the
|
|
||||||
--- branch-offset fix from commit 98e27c2.
|
|
||||||
---
|
|
||||||
--- @param token string -- a single token from split_top_level_commas
|
--- @param token string -- a single token from split_top_level_commas
|
||||||
--- @param wc WordCounts -- the shared word-count table
|
--- @param wc WordCounts -- the shared word-count table
|
||||||
--- @return integer
|
--- @return integer
|
||||||
@@ -109,10 +105,6 @@ end
|
|||||||
--- No regex per the no_regex constraint — uses plain byte matching
|
--- No regex per the no_regex constraint — uses plain byte matching
|
||||||
--- via `dir /b /s` on Windows.
|
--- via `dir /b /s` on Windows.
|
||||||
---
|
---
|
||||||
--- PORT NOTE: taken from tape_atom.offset_gen.meta.lua:432-443
|
|
||||||
--- (`scan_dir`). Adapted: removed the hardcoded project_root derivation;
|
|
||||||
--- the caller passes `dir` explicitly.
|
|
||||||
---
|
|
||||||
--- @param dir string -- directory to scan (absolute or relative)
|
--- @param dir string -- directory to scan (absolute or relative)
|
||||||
--- @param suffix string -- file pattern, e.g. "*.macs.h"
|
--- @param suffix string -- file pattern, e.g. "*.macs.h"
|
||||||
--- @return string[]
|
--- @return string[]
|
||||||
@@ -138,10 +130,6 @@ end
|
|||||||
--- current pos, do NOT advance pos; if the marker call bundles an instruction
|
--- current pos, do NOT advance pos; if the marker call bundles an instruction
|
||||||
--- after it, count that instruction too).
|
--- after it, count that instruction too).
|
||||||
---
|
---
|
||||||
--- PORT NOTE: taken verbatim from tape_atom.offset_gen.meta.lua:207-239
|
|
||||||
--- (`scan_atom_body`). Behavior is identical to preserve the branch-offset
|
|
||||||
--- fix from commit 98e27c2.
|
|
||||||
---
|
|
||||||
--- @param body string -- brace-delimited atom body (without braces)
|
--- @param body string -- brace-delimited atom body (without braces)
|
||||||
--- @param wc WordCounts -- the shared word-count table
|
--- @param wc WordCounts -- the shared word-count table
|
||||||
--- @return integer -- total words
|
--- @return integer -- total words
|
||||||
@@ -177,9 +165,6 @@ end
|
|||||||
--- atom_label/atom_offset call in `tok`. Returns 0 if no such call.
|
--- atom_label/atom_offset call in `tok`. Returns 0 if no such call.
|
||||||
--- Internal helper for count_body_words.
|
--- Internal helper for count_body_words.
|
||||||
---
|
---
|
||||||
--- PORT NOTE: taken from tape_atom.offset_gen.meta.lua:181-205
|
|
||||||
--- (`find_marker_call_end`).
|
|
||||||
---
|
|
||||||
--- @param tok string
|
--- @param tok string
|
||||||
--- @return integer -- 0 if no marker call found
|
--- @return integer -- 0 if no marker call found
|
||||||
function M.find_marker_call_end(tok)
|
function M.find_marker_call_end(tok)
|
||||||
|
|||||||
Reference in New Issue
Block a user