This commit is contained in:
2026-07-10 09:08:29 -04:00
parent 9d066ae292
commit c824c998eb
16 changed files with 273 additions and 815 deletions
+104 -370
View File
@@ -2,207 +2,123 @@
* 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
* a small named DSL that the metaprogram can validate against.
*
* 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
* WHAT THIS HEADER IS
* -------------------
* The metaprogram (scripts/passes/annotation.lua) reads source-as-written
* and validates:
* - every MipsAtom_ has one atom_*() annotation (no orphans)
* - phase is recognized (init/bind/setup/work/commit/terminate)
* - reads/writes reference canonical wave-context registers
* - rbind atoms reference a real Binds_* struct declaration
* - word-counts in tapre metadata agree with the body's actual .word count
* - resource/region/group/cadence/async pragmas are spelled correctly and
* reference known enum values
* - atom_info(...) shape: up to three sub-calls (atom_bind(Binds_X),
* atom_reads(...), atom_writes(...)) in any order. All optional.
* (No phase token for now; phases may be reintroduced later.)
* - rbind atoms (atom_info(..., atom_bind(Binds_X), ...)) reference a
* real Binds_* struct declaration.
* - wave-context positions only reference the canonical 4-register
* 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
*
* _tape_resources_
* atom_resource(cube_tri, "model_ship_cube")
* 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) {
* Usage:
* MipsAtom_(cube_tri) atom_info(
* atom_reads (R_PrimCursor, R_FaceCursor, R_VertBase, R_OtBase)
* , atom_writes(R_PrimCursor, R_FaceCursor)
* ){
* atom_label(culling),
* // ... atom body ...
* atom_offset(culling, bounds_chk) // branch target, validated
* // ... atom body ...
* 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):
* typedef struct Binds_TrackFaceBatch {
* U4 R_PrimCursor, R_FaceCursor,
* R_VertBase, R_OtBase;
* } Binds_TrackFaceBatch;
*
* atom_resource(rbind_track_face_batch, "track_face_batch_42")
* atom_region (rbind_track_face_batch, HEAP_3D)
* atom_group (rbind_track_face_batch, GROUP_LOAD_FACES)
* atom_cadence (rbind_track_face_batch, CADENCE_ONDEMAND)
* 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) { ... };
* typedef Struct_(Binds_TrackFaceBatch) {
* U4 PrimCursor;
* U4 FaceCursor;
* U4 VertBase;
* U4 OtBase;
* };
* MipsAtom_(rbind_track_face_batch) atom_info(
* atom_bind(Binds_TrackFaceBatch)
* , atom_writes(R_PrimCursor, R_FaceCursor, R_VertBase, R_OtBase)
* ){ ... };
*
* Annotation rules
* ----------------
* 1. Each MipsAtom_(name) needs EXACTLY ONE atom_*() macro on the line
* immediately above. No annotation = orphan (warning). Two annotations
* on the same name = duplicate (error).
* 1. atom_info(...) is OPTIONAL. Most atoms have no annotation.
* Atoms without atom_info are silently skipped by the metaprogram.
*
* 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.
* Phase tokens: phase_init / phase_bind / phase_setup / phase_work /
* phase_commit / phase_terminate.
* 5. atom_label(name) is an anchor -- the macro is empty in C; the
* metaprogram records the marker at the current pos for offset
* calculation.
*
* 6. Optional pragmas (atom_resource / atom_region / atom_group /
* atom_cadence / atom_async) attach metadata to the atom. They can
* 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
*
* ============================================================================
* 6. atom_offset(F, T) is resolved by gen/atom_offsets.h, generated
* from the atom_label markers.
*/
#ifdef INTELLISENSE_DIRECTIVES
#ifdef INTELLISENSE_DIRECTIVES
#pragma once
// #include <stdint.h>
#endif
/* ============================================================================
* PHASE TOKENS — strings, used as the second arg to atom_annot(...)
*
* 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:
* WAVE-CONTEXT REGISTERS -- canonical register set for the tape wave model.
*
* 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_VertBase base pointer into the vertex arena (this wave's vertices)
* 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
* tool rejects wave-context positions that reference any other register
* (warning today — the C compiler's R_T4..R_T7 / R_RA / etc. aliases are
* 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.
* Closed set. 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 atom_reads/atom_writes.
*
* ============================================================================*/
/* ============================================================================
* 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
* region is in this set, AND that:
* - 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)
* atom_reads(R_PrimCursor, R_FaceCursor)
* -> (R_PrimCursor, R_FaceCursor) // comma-evaluated, discarded
*
* The macro produces a comma-evaluated expression that the C compiler
* silently discards (it's wrapped in parentheses in the call argument
* position — the result is never bound). The Lua tool pattern-matches the
* "tape_regs(...)" token to extract the list.
* silently discards (it sits in an unused arg position -- the result is
* never bound). The Lua tool pattern-matches the "atom_reads(...)" /
* "atom_writes(...)" token to extract the list.
*
* You can have at most one tape_regs(...) in the reads slot and one in the
* writes slot of atom_annot. To declare multiple disjoint sets (rare), just
* declare the union the metaprogram doesn't track which reads need which
* You can have at most one atom_reads(...) and at most one atom_writes(...)
* in an atom_info(...) call. To declare multiple disjoint sets (rare), just
* declare the union -- the metaprogram doesn't track which reads need which
* writes at this granularity.
*
* ============================================================================*/
@@ -212,227 +128,45 @@
/* ============================================================================
* ATOM ANNOTATION MACROS
*
* Each expands to `__attribute__((annotate("kind"))) MipsAtom_(name)` —
* the GCC attribute is accepted under -Wno-attributes (already in your
* build flags) and stripped at runtime. The annotation string is just the
* macro kind ("atom_annot", "atom_bind", etc.) — the metaprogram reads
* the macro call's full args list from the source-as-written.
* atom_info -- single unified annotation. OPTIONAL. Most atoms have none.
*
* MipsAtom_(cube_tri) atom_info(
* atom_reads (R_PrimCursor, R_FaceCursor, R_VertBase, R_OtBase)
* , 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)
* internal MipsAtom_(tape_main) { ... };
* MipsAtom_(rbind_cube_tri) atom_info(
* atom_bind(Binds_CubeTri)
* , atom_writes(R_PrimCursor, R_FaceCursor, R_VertBase, R_OtBase)
* ){ ... };
*
* Implies: no reads, no writes (wave-context not established yet).
* ----------------------------------------------------------------------------*/
#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
* The Binds_X MUST be a typedef'd type (declared via
* `typedef struct Binds_X { ... } Binds_X;` somewhere in the source).
* 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")))
/* ----------------------------------------------------------------------------
* 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)
#define atom_bind(binds_struct) /* atom_bind(binds_struct) */
/* ============================================================================
* atom_label / atom_offset — branch target machinery
-2
View File
@@ -4,8 +4,6 @@
// Auto-generated by tape_atom_annotation_pass.lua — DO NOT EDIT
// Source: C:\projects\Pikuma\ps1\code\duffle\lottes_tape.h
// Component atoms (MipsAtomComp_(ac_*)) -> macro variants (mac_*)
// + auto word-counts (so tape_atom.metadata.h stays manual-only
// for encoding macros).
#ifndef WORD_COUNT
#define WORD_COUNT(name, count) enum { words_##name = (count) };
+1 -1
View File
@@ -27,7 +27,7 @@
* descriptive; no vendor alias is provided for them.
*
* 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
* mac_format_f3_color / mac_gte_store_f3 / etc.). Verification: V13
* (objdump byte-identical) holds.
-6
View File
@@ -21,12 +21,6 @@
* gte_swc2(rt, base, off) -> gte_sw(rt, base, off)
* (the lower-level vector variants gte_lw_v0_xy etc. don't have
* 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
+6 -7
View File
@@ -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); }
#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)
// are included manually by the unity build. The metaprogram puts them
// Auto-generated component macros (<module>/gen/<dir>/<dir>.macs.h) are included manually by the unity build.
/* Register aliases (moved up from the Tape Drive region below so that
* 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.) */
/* Register aliases */
enum {
R_AtomJmp = R_T9,
R_TapePtr = R_T8, /* The Instruction Stream Pointer */
@@ -287,7 +283,10 @@ internal MipsAtom_(mips_flush_icache) {
typedef Struct_(Binds_SetGteWorld) {
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) */
load_word(R_T3, R_TapePtr, O_(Binds_SetGteWorld,transform)),
add_ui_self( R_TapePtr, S_(Binds_SetGteWorld)),
-6
View File
@@ -21,12 +21,6 @@
* jal -> call_addr (jump-and-link to immediate address)
* jalr -> call_reg (jump-and-link to register, default $ra)
* (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
@@ -1,4 +1,4 @@
// tape_atom.metadata.h
// word_count.metadata.h
// Single source of truth for instruction-word counts.
// Used by C (to define compile-time constants) AND Python (to count positions).
//
+1 -1
View File
@@ -18,8 +18,8 @@
# include "duffle/gen/duffle.offsets.h"
#include "duffle/atom_dsl.h"
#include "duffle/lottes_tape.h"
#include "duffle/word_count.metadata.h"
# include "tape_atom.metadata.h"
# include "gen/gte_hello.offsets.h"
#include "hello_gte.h"
+21 -32
View File
@@ -3,7 +3,7 @@
# include "duffle/gen/duffle.offsets.h"
# include "duffle/atom_dsl.h"
# include "duffle/lottes_tape.h"
# include "tape_atom.metadata.h"
# include "duffle/word_count.metadata.h"
# include "gen/gte_hello.offsets.h"
# include "hello_gte.h"
#endif
@@ -22,15 +22,16 @@ typedef Struct_(Binds_CubeTri) {
V3_S2* VertBase;
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 */
load_word(R_PrimCursor, R_TapePtr, O_(Binds_CubeTri,PrimCursor)),
load_word(R_FaceCursor, R_TapePtr, O_(Binds_CubeTri,FaceCursor)),
load_word(R_VertBase, R_TapePtr, O_(Binds_CubeTri,VertBase)),
load_word(R_OtBase, R_TapePtr, O_(Binds_CubeTri,OtBase)),
add_ui_self( R_TapePtr, S_(Binds_CubeTri)),
// Note(Ed): This entire thing is argument shuffle?
// TODO(Ed): Eliminate
mac_yield()
};
@@ -41,14 +42,11 @@ internal MipsAtom_(rbind_cube_g4_face) {
* 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.
*/
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
MipsAtom_(cube_g4_face) {
MipsAtom_(cube_g4_face) atom_info(
atom_reads( R_PrimCursor, R_FaceCursor, R_VertBase, R_OtBase),
atom_writes(R_PrimCursor, R_FaceCursor)
){
/* ── 1. Load 4 face indices from R_FaceCur (V4_S2 = 8 bytes) ───────── */
load_half_u(R_T0, R_FaceCursor, 0 * S_(S2)),
load_half_u(R_T1, R_FaceCursor, 1 * S_(S2)),
@@ -116,14 +114,11 @@ typedef Struct_(Binds_FloorTri) {
V3_S2* VertBase;
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
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 */
load_word(R_PrimCursor, R_TapePtr, O_(Binds_FloorTri,PrimCursor)),
load_word(R_FaceCursor, R_TapePtr, O_(Binds_FloorTri,FaceCursor)),
@@ -133,14 +128,11 @@ MipsAtom_(rbind_floor_f3_face) {
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
MipsAtom_(floor_f3_face) {
MipsAtom_(floor_f3_face) atom_info(
, atom_reads(R_PrimCursor, R_FaceCursor, R_VertBase, R_OtBase)
, atom_writes(R_PrimCursor, R_FaceCursor)
) {
mac_load_tri_indices(R_T0, R_T1, R_T2),
mac_load_tri_verts( R_T0, R_T1, R_T2),
nop2, gte_cmdw_rotate_translate_perspective_triple,
@@ -174,13 +166,10 @@ atom_label(floor_f3_face_exit)
};
typedef Struct_(Binds_SyncPrimitiveArena) { U4 used; U4 cursor; };
atom_region( sync_primitive_arena, REGION_PRIM_ARENA)
atom_group( sync_primitive_arena, GROUP_RENDER_FLOOR)
atom_cadence(sync_primitive_arena, CADENCE_FRAME)
atom_annot( sync_primitive_arena, phase_work,
atom_reads( R_TapePtr, R_PrimCursor),
atom_writes(R_TapePtr))
internal MipsAtom_(sync_primitive_arena) {
internal MipsAtom_(sync_primitive_arena) atom_info(atom_bind(Binds_SyncPrimitiveArena)
, atom_reads( R_TapePtr, R_PrimCursor)
, atom_writes(R_TapePtr)
){
load_word(R_AT, R_TapePtr, O_(Binds_SyncPrimitiveArena,used)),
load_word(R_T0, R_TapePtr, O_(Binds_SyncPrimitiveArena,cursor)),
add_ui_self( R_TapePtr, S_(Binds_SyncPrimitiveArena)),
+10 -6
View File
@@ -326,11 +326,15 @@ 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)
# Always return true. ps1-meta writes to TWO locations:
# 1. <out_root> (annotation + offsets passes — build/gen/)
# 2. <src.dir>/gen/ (component pass — code/duffle/gen/, etc.)
# Tracking only <out_root>'s mtime is fragile — if the in-source
# gen dir was wiped but <out_root> still exists with a newer mtime,
# we'd skip the regen and the compile would fail with a missing
# duffle.macs.h. ps1-meta is fast enough (~100ms) that always
# running it is the safe default.
return $true
}
function ps1-meta {
@@ -357,7 +361,7 @@ function build-gte_hello {
$path_module = join-path $path_code 'gte_hello'
$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)
$atom_sources = Get-SourceFiles -paths $source_dirs -extensions @('.h', '.c')
+7 -49
View File
@@ -1,8 +1,6 @@
-- duffle.lua
--
-- 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):
-- - no <close> / <toclose>
@@ -499,54 +497,14 @@ M.WAVE_CONTEXT_REGS = {
["R_OtBase"] = { alias = "R_T6", size = 4, role = "base pointer (ordering table)" },
}
M.MACRO_EXPANSION = {
["phase_init"] = "init",
["phase_bind"] = "bind",
["phase_setup"] = "setup",
["phase_work"] = "work",
["phase_commit"] = "commit",
["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,
}
-- The annotation DSL has been reduced to a single annotation macro:
-- atom_info(atom_bind(Binds_X), atom_reads(...), atom_writes(...))
-- All phase / region / cadence / async / resource / group tokens have
-- been dropped. They may be reintroduced later as optional sub-calls
-- of atom_info; for now, the parser only recognizes atom_info + its
-- three sub-calls (atom_bind, atom_reads, atom_writes).
M.TAPE_ATOM_MACROS = {
["atom_annot"] = { kind = "work", 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 = {
["resource"] = { kind = "string" },
["region"] = { kind = "ident", allowed = M.KNOWN_REGIONS },
["group"] = { kind = "ident" },
["cadence"] = { kind = "ident", allowed = M.KNOWN_CADENCES },
["async"] = { kind = "ident", allowed = { ["true"] = true, ["false"] = true } },
["atom_info"] = { kind = "info", binds = false },
}
-- Expose the lpeg_ok flag so callers can detect the LPeg-back path.
+118 -295
View File
@@ -47,12 +47,8 @@ local split_top_level_commas = duffle.split_top_level_commas
-- Domain tables (single source of truth in duffle.lua).
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 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
-- ════════════════════════════════════════════════════════════════════════════
@@ -109,32 +105,44 @@ end
-- Parse TAPE_ATOM_ANNOT(...) calls
-- ════════════════════════════════════════════════════════════════════════════
-- Recognize a `atom_reads(...)` or `atom_writes(...)` register-list
-- call embedded inside an annotation arg list. Returns the kind
-- ("atom_reads" / "atom_writes") and the inner content, or nil if the
-- token isn't a recognized register-list form. Flattened via a
-- prefix lookup instead of a nested if/elseif chain.
-- Recognize a `atom_bind(...)`, `atom_reads(...)`, or `atom_writes(...)`
-- sub-call embedded inside an atom_info arg list. Returns the kind
-- ("atom_bind" / "atom_reads" / "atom_writes") and the inner content,
-- or nil if the token isn't a recognized sub-call form. Flattened via
-- a prefix lookup instead of a nested if/elseif chain.
local REGS_CALL_PREFIX = {
["atom_reads("] = { kind = "atom_reads", inner_offset = 12 },
["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)
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
-- The 12-char prefix "atom_reads(" also matches "atom_writes("
-- would be ambiguous; the table order above handles it.
-- (atom_reads prefix is 11 chars, atom_writes is 12; the 12-char
-- lookup matches atom_writes first.)
return spec.kind, s:sub(spec.inner_offset, -2)
local inner = s:sub(spec.inner_offset, -2)
if spec.single_ident then
-- atom_bind takes a single Binds_* type ident. Trim and pass through.
return spec.kind, trim(inner)
end
return spec.kind, inner
end
-- Resolve any phase_* / R_* alias macros in a register list.
-- (Phase / region / cadence aliases have been dropped. Kept as an
-- identity function so callers can stay uniform.)
local function resolve_reg_aliases(regs)
for i, r in ipairs(regs) do
if MACRO_EXPANSION[r] then regs[i] = MACRO_EXPANSION[r] end
end
return regs
end
@@ -150,18 +158,19 @@ local function parse_regs_list(inner)
end
-- 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).
local function parse_arg_token(s)
local kind, inner = parse_regs_call(s)
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) }
end
local id = read_ident(s, 1)
if id and trim(s) == id then
local v = id
if MACRO_EXPANSION[v] then v = MACRO_EXPANSION[v] end
return { kind = "ident", value = v }
return { kind = "ident", value = id }
end
return { kind = "other", value = s }
end
@@ -262,136 +271,6 @@ local function find_macro_word_annotations(source)
return out
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
-- ════════════════════════════════════════════════════════════════════════════
@@ -526,58 +405,35 @@ local function is_regs_arg(a)
end
--- Per-macro arg-shape handlers. Each takes (entry, args) and mutates
--- entry.{reads, writes, phase, binds, errors}. Replaces the 5-way
--- `if/elseif/elseif/elseif/elseif` chain inside find_atom_annotations.
--- Per-atom_info sub-call dispatch. Each takes (entry, args) and mutates
--- entry.{reads, writes, binds, errors}. The new annotation shape is:
---
--- MipsAtom_(name) atom_info(
--- atom_bind(Binds_X)
--- , atom_reads(...)
--- , atom_writes(...)
--- ) { ... };
---
--- All sub-calls are order-independent; each is dispatched on its
--- `kind` (atom_bind / atom_reads / atom_writes) when parsed.
local ANNOT_ARG_HANDLERS = {}
-- atom_bind(name, Binds_Struct, writes)
function ANNOT_ARG_HANDLERS.bind(entry, args)
if #args >= 2 and args[2].kind == "ident" then
entry.binds = args[2].value
end
if #args >= 3 and is_regs_arg(args[3]) then
entry.writes = args[3].value
end
end
-- atom_init(name) / atom_terminate(name): name only, no extra slots.
ANNOT_ARG_HANDLERS.init = function() end
ANNOT_ARG_HANDLERS.terminate = function() end
-- Macro name -> handler key. Replaces the `macro_def.binds` check
-- 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?"
-- atom_info(atom_bind(Binds_X), atom_reads(...), atom_writes(...))
function ANNOT_ARG_HANDLERS.info(entry, args)
for _, arg in ipairs(args) do
if arg.kind == "atom_bind" then
entry.binds = arg.value
elseif arg.kind == "atom_reads" then
entry.reads = arg.value
elseif arg.kind == "atom_writes" then
entry.writes = arg.value
elseif arg.kind == "ident" then
-- Reserved for future phase tokens. Currently ignored.
-- (Could be reintroduced as `phase_*` sub-calls of atom_info.)
else
entry.errors[#entry.errors + 1] = string.format(
"unexpected atom_info arg kind=%s value=%s", arg.kind, tostring(arg.value))
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
@@ -589,16 +445,17 @@ local function new_annot_entry(line, ident, name, kind)
name = name,
kind = kind,
binds = nil,
phase = nil,
reads = {},
writes = {},
errors = {},
}
end
--- Find every TAPE_ATOM_* macro call in source and convert it to a
--- normalized annotation entry. Dispatches per-macro arg-shape via
--- ANNOT_ARG_HANDLERS (lookup table; no nested if/elseif chain).
--- Find every MipsAtom_(name) declaration in source, then look for an
--- immediately-following atom_info(...) call. If present, parse its
--- 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 line_of = duffle.LineIndex(source)
local annots = {}
@@ -607,9 +464,6 @@ local function find_atom_annotations(source)
while i <= len do
i = skip_ws_and_cmt(source, i); if i > len then break end
-- 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
local j = i
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)
if not ident then
i = i + 1
elseif TAPE_ATOM_MACROS[ident] then
elseif ident == "MipsAtom_" then
local open = skip_ws_and_cmt(source, after)
if source:sub(open, open) ~= "(" then
i = open + 1
else
local inner, after_paren = read_parens(source, open)
local args = parse_atom_annot_args(inner)
local macro_def = TAPE_ATOM_MACROS[ident]
goto continue
end
local inner, after_paren = read_parens(source, open)
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
annots[#annots + 1] = {
line = line_of(i),
macro = ident,
kind = macro_def.kind,
error = "missing atom name (first arg)",
}
else
local entry = new_annot_entry(line_of(i), ident, args[1].value, macro_def.kind)
local handler = ANNOT_ARG_HANDLERS[MACRO_HANDLER_KEY[ident]]
if handler then handler(entry, args) end
-- Look for atom_info(...) right after MipsAtom_(name).
local lookahead = skip_ws_and_cmt(source, after_paren)
local look_ident, look_after = read_ident(source, lookahead)
if look_ident == "atom_info" then
local info_open = skip_ws_and_cmt(source, look_after)
if source:sub(info_open, info_open) == "(" then
local info_inner, info_after = read_parens(source, info_open)
local args = parse_atom_annot_args(info_inner)
local entry = new_annot_entry(line_of(lookahead), "atom_info", name, "info")
ANNOT_ARG_HANDLERS.info(entry, args)
annots[#annots + 1] = entry
i = info_after
else
i = info_open + 1
end
else
-- No atom_info follows this MipsAtom_. Valid in new shape.
i = after_paren
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
i = after
end
@@ -661,7 +530,6 @@ local function validate(ctx, src)
local annots = find_atom_annotations(source)
local macros = find_macro_word_annotations(source)
local pragmas = find_atom_pragmas(source)
local binds = find_binds_structs(source)
local atoms = find_atom_names(source)
@@ -692,37 +560,26 @@ local function validate(ctx, src)
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 = {}
for _, a in ipairs(annots) do
if a.name and not a.error then
count_per_atom[a.name] = (count_per_atom[a.name] or 0) + 1
end
end
for _, atom in ipairs(atoms) do
local n = count_per_atom[atom.name] or 0
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
for name, n in pairs(count_per_atom) do
if n > 1 then
errors[#errors + 1] = {
line = atom.line,
msg = string.format("MipsAtom_(%s) has %d annotations (expected 1)", atom.name, n),
line = atom_index[name] and atom_index[name].line or 0,
msg = string.format("MipsAtom_(%s) has %d annotations (expected at most 1)", name, n),
}
end
end
-- 3. Phase validity.
for _, a in ipairs(annots) do
if a.name and not a.error and a.phase and not valid_phase(a.phase) then
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
-- 3. (Phase validity check DROPPED. Phases were removed from the
-- annotation DSL. They may be reintroduced later as sub-calls of
-- atom_info, at which point ordering checks will go here.)
-- 4. BIND atoms must reference a real Binds_* struct.
for _, a in ipairs(annots) do
@@ -764,16 +621,14 @@ local function validate(ctx, src)
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
if a.kind == "work" then
for _, r in ipairs(a.reads) do
if not is_wave_context_reg(r) and r ~= "R_TapePtr" then
warnings[#warnings + 1] = {
line = a.line,
msg = string.format("work atom '%s' reads '%s' which is not a known wave-context register", a.name, r),
}
end
for _, r in ipairs(a.reads) do
if not is_wave_context_reg(r) and r ~= "R_TapePtr" then
warnings[#warnings + 1] = {
line = a.line,
msg = string.format("atom '%s' reads '%s' which is not a known wave-context register", a.name, r),
}
end
end
end
@@ -805,51 +660,19 @@ local function validate(ctx, src)
check_macro_drift(m, ctx.shared.word_counts[m.name])
end
-- 8. atom_<...> _Pragma validation: resource/region/group/cadence/async
for _, p in ipairs(pragmas) do
if not atom_index[p.name] then
errors[#errors + 1] = {
line = p.line,
msg = string.format("pragma references unknown atom '%s'", p.name),
}
end
-- 8. (atom_<...> _Pragma validation DROPPED. The pragma macros
-- atom_resource / atom_region / atom_group / atom_cadence /
-- atom_async were removed from atom_dsl.h. They may be
-- reintroduced later as sub-calls of atom_info.)
for k, v in pairs(p.attrs) do
local spec = ATOM_PRAGMA_KINDS[k]
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
-- 9. (CADENCE_ONDEMAND requires async check DROPPED. Same reason
-- as #8.)
-- 10. Information summary.
info[#info + 1] = {
line = 0,
msg = string.format("scanned: %d atom(s), %d annotation(s), %d pragma(s), %d macro-word-decl(s), %d binds struct(s)",
#atoms, #annots, #pragmas, #macros, #binds),
msg = string.format("scanned: %d atom(s), %d annotation(s), %d macro-word-decl(s), %d binds struct(s)",
#atoms, #annots, #macros, #binds),
}
return {
+1 -7
View File
@@ -459,9 +459,7 @@ local function compute_component_word_count(c, components, wc)
-- It's a `mac_X(...)` call. Recurse.
n = n + rec(comp_name)
elseif comp_name and wc and wc[comp_name] then
-- Encoding macro or pseudo-instruction (e.g. mask_upper = 2,
-- nop2 = 2). Trust the metadata — tape_atom.metadata.h is the
-- single source of truth for word counts.
-- Encoding macro or pseudo-instruction (e.g. mask_upper = 2, nop2 = 2).
n = n + wc[comp_name]
else
-- 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",
"// Source: " .. to_absolute_path(src.path),
"// 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.
-- The metadata file (tape_atom.metadata.h) defines it as
-- enum { words_##name = (count) };
-- We use the same definition here so the auto-generated
-- entries below expand to compile-time constants whether
-- the metadata file is included first or not.
+1 -2
View File
@@ -2,7 +2,6 @@
--
-- Generate <module>/gen/<basename>.offsets.h with branch offset
-- 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
-- NOT return. The fix was in duffle.lua's split_top_level_commas +
@@ -392,4 +391,4 @@ function M.run(ctx)
return { outputs = outputs, errors = errors, warnings = warnings }
end
return M
return M
+2 -15
View File
@@ -38,9 +38,8 @@ local function render_source_report(source_path, result)
add("ANNOTATION PASS — " .. source_path)
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.pragmas and #result.pragmas or 0),
#result.binds, #result.macros))
add("")
@@ -55,9 +54,7 @@ local function render_source_report(source_path, result)
if a.error then
add(string.format(" ✗ line %d %s [ERROR: %s]", a.line, a.macro or "?", a.error))
else
local line = string.format(" %s line %d %s phase=%s",
a.kind == "work" and "" or (a.kind == "bind" and "" or ""),
a.line, a.name, a.phase or a.kind)
local line = string.format(" line %d %s", a.line, a.name)
if a.binds then line = line .. " binds=" .. a.binds end
if #a.reads > 0 then line = line .. " reads={" .. table.concat(a.reads, ",") .. "}" end
if #a.writes > 0 then line = line .. " writes={" .. table.concat(a.writes, ",") .. "}" end
@@ -81,16 +78,6 @@ local function render_source_report(source_path, result)
end
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 ──────────────────────────────────────────────")
if #result.errors == 0 then add(" (none)") end
for _, e in ipairs(result.errors) do
-15
View File
@@ -81,10 +81,6 @@ local M = {}
--- (recursively if needed). For `nop2` etc., returns wc[name].
--- 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 wc WordCounts -- the shared word-count table
--- @return integer
@@ -109,10 +105,6 @@ end
--- No regex per the no_regex constraint — uses plain byte matching
--- 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 suffix string -- file pattern, e.g. "*.macs.h"
--- @return string[]
@@ -138,10 +130,6 @@ end
--- current pos, do NOT advance pos; if the marker call bundles an instruction
--- 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 wc WordCounts -- the shared word-count table
--- @return integer -- total words
@@ -177,9 +165,6 @@ end
--- atom_label/atom_offset call in `tok`. Returns 0 if no such call.
--- 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
--- @return integer -- 0 if no marker call found
function M.find_marker_call_end(tok)