From c824c998ebbe37db63ac980e07ab852b4a65003e Mon Sep 17 00:00:00 2001 From: Ed_ Date: Fri, 10 Jul 2026 09:08:29 -0400 Subject: [PATCH] broken. --- code/duffle/atom_dsl.h | 474 ++++-------------- code/duffle/gen/duffle.macs.h | 2 - code/duffle/gp_vendor_sym.h | 2 +- code/duffle/gte_vendor_sym.h | 6 - code/duffle/lottes_tape.h | 13 +- code/duffle/mips_vendor_sym.h | 6 - .../word_count.metadata.h} | 2 +- code/gte_hello/hello_gte.c | 2 +- code/gte_hello/hello_gte_tape.c | 53 +- scripts/build_psyq.ps1 | 16 +- scripts/duffle.lua | 56 +-- scripts/passes/annotation.lua | 413 +++++---------- scripts/passes/components.lua | 8 +- scripts/passes/offsets.lua | 3 +- scripts/passes/report.lua | 17 +- scripts/word_count_eval.lua | 15 - 16 files changed, 273 insertions(+), 815 deletions(-) rename code/{gte_hello/tape_atom.metadata.h => duffle/word_count.metadata.h} (99%) diff --git a/code/duffle/atom_dsl.h b/code/duffle/atom_dsl.h index 4fc87ef..c4f089a 100644 --- a/code/duffle/atom_dsl.h +++ b/code/duffle/atom_dsl.h @@ -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 #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 diff --git a/code/duffle/gen/duffle.macs.h b/code/duffle/gen/duffle.macs.h index d9a2286..97c93b2 100644 --- a/code/duffle/gen/duffle.macs.h +++ b/code/duffle/gen/duffle.macs.h @@ -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) }; diff --git a/code/duffle/gp_vendor_sym.h b/code/duffle/gp_vendor_sym.h index f421a80..07a545d 100644 --- a/code/duffle/gp_vendor_sym.h +++ b/code/duffle/gp_vendor_sym.h @@ -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. diff --git a/code/duffle/gte_vendor_sym.h b/code/duffle/gte_vendor_sym.h index 8355f3f..5874d5d 100644 --- a/code/duffle/gte_vendor_sym.h +++ b/code/duffle/gte_vendor_sym.h @@ -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 diff --git a/code/duffle/lottes_tape.h b/code/duffle/lottes_tape.h index 23e87c8..b724cb5 100644 --- a/code/duffle/lottes_tape.h +++ b/code/duffle/lottes_tape.h @@ -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 (/gen//.macs.h) -// are included manually by the unity build. The metaprogram puts them +// Auto-generated component macros (/gen//.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)), diff --git a/code/duffle/mips_vendor_sym.h b/code/duffle/mips_vendor_sym.h index e481486..1c24bf6 100644 --- a/code/duffle/mips_vendor_sym.h +++ b/code/duffle/mips_vendor_sym.h @@ -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 diff --git a/code/gte_hello/tape_atom.metadata.h b/code/duffle/word_count.metadata.h similarity index 99% rename from code/gte_hello/tape_atom.metadata.h rename to code/duffle/word_count.metadata.h index 19c3d8b..8e0e17b 100644 --- a/code/gte_hello/tape_atom.metadata.h +++ b/code/duffle/word_count.metadata.h @@ -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). // diff --git a/code/gte_hello/hello_gte.c b/code/gte_hello/hello_gte.c index 75aa462..c51726f 100644 --- a/code/gte_hello/hello_gte.c +++ b/code/gte_hello/hello_gte.c @@ -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" diff --git a/code/gte_hello/hello_gte_tape.c b/code/gte_hello/hello_gte_tape.c index d517d61..b3e42a6 100644 --- a/code/gte_hello/hello_gte_tape.c +++ b/code/gte_hello/hello_gte_tape.c @@ -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)), diff --git a/scripts/build_psyq.ps1 b/scripts/build_psyq.ps1 index d483673..602cb0a 100644 --- a/scripts/build_psyq.ps1 +++ b/scripts/build_psyq.ps1 @@ -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. (annotation + offsets passes — build/gen/) + # 2. /gen/ (component pass — code/duffle/gen/, etc.) + # Tracking only 's mtime is fragile — if the in-source + # gen dir was wiped but 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') diff --git a/scripts/duffle.lua b/scripts/duffle.lua index d11bfbd..5bedc43 100644 --- a/scripts/duffle.lua +++ b/scripts/duffle.lua @@ -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 / @@ -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. diff --git a/scripts/passes/annotation.lua b/scripts/passes/annotation.lua index a714cef..7771687 100644 --- a/scripts/passes/annotation.lua +++ b/scripts/passes/annotation.lua @@ -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_(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 { diff --git a/scripts/passes/components.lua b/scripts/passes/components.lua index 0de6204..b30809c 100644 --- a/scripts/passes/components.lua +++ b/scripts/passes/components.lua @@ -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. diff --git a/scripts/passes/offsets.lua b/scripts/passes/offsets.lua index 639a758..1ba75dc 100644 --- a/scripts/passes/offsets.lua +++ b/scripts/passes/offsets.lua @@ -2,7 +2,6 @@ -- -- Generate /gen/.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 \ No newline at end of file +return M diff --git a/scripts/passes/report.lua b/scripts/passes/report.lua index 7b3cefa..dff3314 100644 --- a/scripts/passes/report.lua +++ b/scripts/passes/report.lua @@ -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 diff --git a/scripts/word_count_eval.lua b/scripts/word_count_eval.lua index d872e7d..593e87d 100644 --- a/scripts/word_count_eval.lua +++ b/scripts/word_count_eval.lua @@ -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)