First pass review

This commit is contained in:
ed
2026-07-14 22:55:16 -04:00
parent 7d5b13aadb
commit 137549b1c8
17 changed files with 666 additions and 993 deletions
+34 -127
View File
@@ -3,36 +3,20 @@
* ============================================================================ * ============================================================================
* *
* ATOM DSL: Annotation layer for tape atoms (lottes_tape.h). * ATOM DSL: Annotation layer for tape atoms (lottes_tape.h).
* The metaprogram (scripts/passes/annotation.lua) reads source-as-written and validates:
* - atom_info(...) shape: up to three sub-calls (atom_bind(Binds_X), atom_reads(...), atom_writes(...)) in any order and are optional.
* - rbind atoms (atom_info(..., atom_bind(Binds_X), ...)) reference a real Binds_* struct declaration.
* - wave-context positions only reference the 4-register set: R_PrimCursor / R_FaceCursor / R_VertBase / R_OtBase.
* Note(Ed): Not sure if I'll generlaize this later.
* - atom word-counts in word_counts.metadata.h match the body's actual .word count.
* *
* WHAT THIS HEADER IS * Pure macro anntation.
* -------------------
* The metaprogram (scripts/passes/annotation.lua) reads source-as-written
* and validates:
* - 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 PURE MACROS
* --------------- * ---------------
* atom_info, atom_bind, atom_reads, atom_writes, atom_label, * Don't want to constraint the macro usage to some attribute placment constraint, etc, don't want ot dela with the compiler.
* atom_dbg_skip_over each expand to a C comment or to nothing. The C * atom_info, atom_bind, atom_reads, atom_writes, atom_label, atom_dbg_skip_over each expand to a C comment or to nothing
* comment or to nothing. The C preprocessor strips them to whitespace. * (C preprocessor strips them to whitespace).
* The metaprogram reads the literal tokens 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
* *
* ============================================================================ * ============================================================================
*
* Usage: * Usage:
* MipsAtom_(cube_tri) atom_info( * MipsAtom_(cube_tri) atom_info(
* atom_reads (R_PrimCursor, R_FaceCursor, R_VertBase, R_OtBase) * atom_reads (R_PrimCursor, R_FaceCursor, R_VertBase, R_OtBase)
@@ -62,30 +46,15 @@
* *
* Annotation rules * Annotation rules
* ---------------- * ----------------
* 1. atom_info(...) is OPTIONAL. Most atoms have no annotation. * 1. atom_info(...) is OPTIONAL. Atoms without atom_info are silently skipped by the metaprogram.
* Atoms without atom_info are silently skipped by the metaprogram. * 2. If present, atom_info takes up to three sub-calls, all order-independent within the arg list:
* * - atom_bind(Binds_X) (binds a struct context to an atom).
* 2. If present, atom_info takes up to three sub-calls, all * - atom_reads(...) (context registers)
* order-independent within the arg list: * - atom_writes(...) (context registers)
* - atom_bind(Binds_X) (optional; only for rbind atoms) * 3. atom_bind(Binds_X): metaprogram cross-references Binds_X against the `typedef struct Binds_X { ... } Binds_X;` declaration.
* - atom_reads(...) (optional; wave-context registers) * 4. atom_reads(...) and atom_writes(...) used to to check if registers are used correctly in macros: R_PrimCursor / R_FaceCursor / R_VertBase / R_OtBase.
* - atom_writes(...) (optional; wave-context registers) * 5. atom_label(name) utilize with atom_offset as a target location.
* * 6. atom_offset(F, T) is resolved by gen/atom_offsets.h, generated from the atom_label markers. Calculated during the offset pass of the lua metaprogram.
* 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_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_label(name) is an anchor -- the macro is empty in C; the
* metaprogram records the marker at the current pos for offset
* calculation.
*
* 6. atom_offset(F, T) is resolved by gen/atom_offsets.h, generated
* from the atom_label markers.
*/ */
#ifdef INTELLISENSE_DIRECTIVES #ifdef INTELLISENSE_DIRECTIVES
@@ -94,75 +63,32 @@
#endif #endif
/* ============================================================================ /* ============================================================================
* WAVE-CONTEXT REGISTERS -- canonical register set for the tape wave model. * atom_reads(...) / atom_writes(...)
*
* 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)
*
* 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.
*
* ============================================================================*/
/* ============================================================================
* atom_reads(...) / atom_writes(...) -- wave-context register list
*
* 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 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 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.
* *
* Used during the static analysis pass of the metaprogram to do
* ============================================================================*/ * ============================================================================*/
#define atom_reads(...) (__VA_ARGS__) #define atom_reads(...) (__VA_ARGS__)
#define atom_writes(...) (__VA_ARGS__) #define atom_writes(...) (__VA_ARGS__)
/* ============================================================================ /* ============================================================================
* ATOM ANNOTATION MACROS * atom_info :
*
* atom_info -- single unified annotation. OPTIONAL. Most atoms have none.
*
* MipsAtom_(cube_tri) atom_info( * MipsAtom_(cube_tri) atom_info(
* atom_reads (R_PrimCursor, R_FaceCursor, R_VertBase, R_OtBase) * atom_reads (R_PrimCursor, R_FaceCursor, R_VertBase, R_OtBase)
* , atom_writes(R_PrimCursor, R_FaceCursor) * , atom_writes(R_PrimCursor, R_FaceCursor)
* ){ ... }; * ){ ... };
* *
* Shape (sub-args order-independent; all optional): * - atom_bind(Binds_X): metaprogram cross-references Binds_X against the `typedef struct Binds_X { ... } Binds_X;` declaration.
* - atom_bind(Binds_X): at most one; pins the ABI-struct shape * - atom_reads(...): comma-list of registers
* - atom_reads(...): at most one; comma-list of wave-context registers * - atom_writes(...): comma-list of 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__) */ #define atom_info(...) /* atom_info(__VA_ARGS__) */
/* ---------------------------------------------------------------------------- /* ----------------------------------------------------------------------------
* DEBUG SOURCE-STEP MARKERS * DEBUG SOURCE-STEP MARKERS
* *
* Place atom_dbg_skip_over() before a MipsAtom_, MipsAtomComp_, or * Place atom_dbg_skip_over() before a MipsAtom_, MipsAtomComp_, or MipsAtomComp_Proc_.
* MipsAtomComp_Proc_ declaration. The following declaration kind determines * The following declaration kind determines whether the marker selects a whole atom or a component inline view.
* whether the marker selects a whole atom or a component inline view. The * The source scanner associates the marker with that declaration; placement diagnostics are handled by the annotation pass.
* source scanner associates the marker with that declaration; placement
* diagnostics are handled by the annotation pass.
*
* The macro expands to a comment only. It emits no C data or MIPS words and
* therefore cannot affect runtime output.
* ----------------------------------------------------------------------------*/ * ----------------------------------------------------------------------------*/
#define atom_dbg_skip_over() /* atom_dbg_skip_over: skip the following atom or component source view */ #define atom_dbg_skip_over() /* atom_dbg_skip_over: skip the following atom or component source view */
@@ -174,13 +100,7 @@
* , atom_writes(R_PrimCursor, R_FaceCursor, R_VertBase, R_OtBase) * , atom_writes(R_PrimCursor, R_FaceCursor, R_VertBase, R_OtBase)
* ){ ... }; * ){ ... };
* *
* The Binds_X 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).
* `typedef struct Binds_X { ... } Binds_X;` somewhere in the source).
* The Lua tool cross-references this. Missing struct = error.
*
* 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(binds_struct) /* atom_bind(binds_struct) */ #define atom_bind(binds_struct) /* atom_bind(binds_struct) */
@@ -193,25 +113,12 @@
* *
* atom_offset(culling, bounds_chk) ← resolved by gen/.offsets.h * atom_offset(culling, bounds_chk) ← resolved by gen/.offsets.h
* *
* The metaprogram generates gen/atom_offsets.h with one * The metaprogram generates gen/atom_offsets.h with one #define with the offset value per atom_offset(F, T) call.
* #define atom_offset__culling__bounds_chk ((target - branch_pos - 1)) * The preprocessor then expands the call to the right immediate value.
* per atom_offset(F, T) call. The preprocessor then expands your call to
* the right immediate value.
*
* If gen/atom_offsets.h is stale (or atom_label(name) is undefined),
* `atom_offset__F__T` becomes an undefined macro and the C build fails.
* This catches:
* - typo in atom_label (no anchor → metaprogram doesn't emit the macro)
* - .offsets.h not regenerated after body edits
* - body edit that broke the offset math (recompile + retest picks it up
* in CPU emulator)
* *
* If gen/atom_offsets.h is stale (or atom_label(name) is undefined), `atom_offset_F_T`
* becomes an undefined macro and the C build fails.
* ============================================================================*/ * ============================================================================*/
#define atom_offset(F, T) atom_offset_ ## F ## _ ## T #define atom_offset(F, T) atom_offset_ ## F ## _ ## T
/* atom_label is a pure annotation for the metaprogram's offset calculations. // atom_label is a pure annotation for the metaprogram's offset calculations.
* The macro expands to a C comment, so the C preprocessor strips it to
* whitespace — NO instruction word is emitted in the asm. The metaprogram
* still recognises the literal `atom_label(name)` token in source and
* records the marker at the current pos. */
#define atom_label(name) /* atom_label anchor: name */ #define atom_label(name) /* atom_label anchor: name */
+1 -2
View File
@@ -168,8 +168,7 @@ MipsAtomComp_Proc_(ac_pack_color_word, {
}) })
/* Words: 3; Emits the F3 command+color word (cmd byte | BLUE | GREEN | RED) /* Words: 3; Emits the F3 command+color word (cmd byte | BLUE | GREEN | RED)
* Args: _r, _g, _b are 8-bit RGB byte values (not raw 16-bit fields). * Args: _r, _g, _b are 8-bit RGB byte values (not raw 16-bit fields). */
* Migrated from hello_gte_tape.c; takes RGB form per the Phase 3 convention. */
FI_ MipsAtom ac_format_f3_color(U1 r, U1 g, U1 b) FI_ MipsAtom ac_format_f3_color(U1 r, U1 g, U1 b)
MipsAtomComp_Proc_(ac_format_f3_color, { mac_pack_color_word(O_(Poly_F3,color), gp0_cmd_poly_f3, r, g, b) }) MipsAtomComp_Proc_(ac_format_f3_color, { mac_pack_color_word(O_(Poly_F3,color), gp0_cmd_poly_f3, r, g, b) })
+3 -1
View File
@@ -485,7 +485,9 @@ function build-gte_hello {
& $Objcopy --add-section ".debug_loc=$dwarfLocBin" $injectElf & $Objcopy --add-section ".debug_loc=$dwarfLocBin" $injectElf
if ($LASTEXITCODE -ne 0) { if ($LASTEXITCODE -ne 0) {
Write-Warning "[build] objcopy .debug_loc add-section failed (exit $LASTEXITCODE)" Write-Warning "[build] objcopy .debug_loc add-section failed (exit $LASTEXITCODE)"
} else { }
else
{
# .debug_loclists doesn't exist in the source ELF; --add-section creates it. # .debug_loclists doesn't exist in the source ELF; --add-section creates it.
& $Objcopy --add-section ".debug_loclists=$dwarfLoclistsBin" $injectElf & $Objcopy --add-section ".debug_loclists=$dwarfLoclistsBin" $injectElf
if ($LASTEXITCODE -ne 0) { if ($LASTEXITCODE -ne 0) {
+15 -26
View File
@@ -261,11 +261,8 @@ function M.write_file_lf(path, content)
f:write(content); f:close() f:write(content); f:close()
end end
-- Return `{path, ...}` for files in `out_root` whose basename matches -- Return `{path, ...}` for files in `out_root` whose basename matches `pattern` (Lua pattern, NOT regex — `%.` not `\.`).
-- `pattern` (Lua pattern, NOT regex — `%.` not `\.`). Empty list if -- Empty list if `out_root` doesn't exist or matches nothing.
-- `out_root` doesn't exist or matches nothing.
--
-- **Cost:** ~2ms native (lfs.dir) vs ~56ms subprocess (`dir /b`).
-- @param out_root Path -- @param out_root Path
-- @param pattern string -- Lua pattern matched against basename only -- @param pattern string -- Lua pattern matched against basename only
-- @return string[] -- @return string[]
@@ -309,9 +306,6 @@ function M.to_absolute_path(path)
end end
-- Cache of directories already verified to exist in this process. -- Cache of directories already verified to exist in this process.
-- Each ensure_dir() call may otherwise spawn a `cmd.exe mkdir` (50-100ms per call on Windows) — calling it inside per-source loops added 1.5+
-- seconds to the report pass. Cache makes ensure_dir idempotent within the process lifetime.
-- (safe across passes; the dir state doesn't change).
local _ensured_dirs = {} local _ensured_dirs = {}
function M.ensure_dir(path) function M.ensure_dir(path)
@@ -445,7 +439,7 @@ end
-- --
-- FIX (2026-07-09): split at top-level NEWLINES and SEMICOLONS too, AND emit a token break after a top-level comment/string. -- FIX (2026-07-09): split at top-level NEWLINES and SEMICOLONS too, AND emit a token break after a top-level comment/string.
-- Previous behavior glued the macro call after a comment into the same token, so `word_count_of_token` only saw the -- Previous behavior glued the macro call after a comment into the same token, so `word_count_of_token` only saw the
-- leading ident (often nil after stripping the comment), undercounting the body. See Phase 1 of the branch-offset regression investigation. -- leading ident (often nil after stripping the comment), undercounting the body.
-- Pure-comment / pure-string chunks (which now appear between real statements) are filtered out so they contribute 0 words instead of 1. -- Pure-comment / pure-string chunks (which now appear between real statements) are filtered out so they contribute 0 words instead of 1.
function M.split_top_level_commas(body) function M.split_top_level_commas(body)
local tokens = {} local tokens = {}
@@ -537,9 +531,6 @@ end
-- Section 4b: tokenize_body + build_body_line_index (shared, memoized) -- Section 4b: tokenize_body + build_body_line_index (shared, memoized)
-- ════════════════════════════════════════════════════════════════════════════ -- ════════════════════════════════════════════════════════════════════════════
-- Moved here from passes/static_analysis.lua so all passes can share the memoized
-- per-body tokenization. The memoization key is the body string (immutable per pass).
local _tokenize_body_cache = {} local _tokenize_body_cache = {}
local _tokenize_body_simple_cache = {} local _tokenize_body_simple_cache = {}
local _body_line_index_cache = {} local _body_line_index_cache = {}
@@ -562,20 +553,17 @@ function M.tokenize_body(body)
local scan = rel local scan = rel
while scan <= len do while scan <= len do
local c = body:byte(scan) local c = body:byte(scan)
-- Terminator bytes (delimit a token at the top level): ',' = 0x2C, -- Terminator bytes (delimit a token at the top level): ',' = 0x2C, '\n' = 0x0A, ';' = 0x3B.
-- '\n' = 0x0A, ';' = 0x3B. These also appear as separators between -- These also appear as separators between argument lists inside the parens/braces/brackets,
-- argument lists inside the parens/braces/brackets, so we stop the -- so we stop the scan when we hit any of them.
-- scan when we hit any of them.
if c == BYTE_COMMA then break end if c == BYTE_COMMA then break end
if c == BYTE_NEWLINE then break end if c == BYTE_NEWLINE then break end
if c == BYTE_SEMI then break end if c == BYTE_SEMI then break end
-- Group opener bytes (consume the balanced group via the matching reader): -- Group opener bytes (consume the balanced group via the matching reader): '(' = 0x28, '{' = 0x7B, '[' = 0x5B.
-- '(' = 0x28, '{' = 0x7B, '[' = 0x5B.
if c == BYTE_OPEN_PAREN then local _, a = M.read_parens (body, scan); scan = a if c == BYTE_OPEN_PAREN then local _, a = M.read_parens (body, scan); scan = a
elseif c == BYTE_OPEN_BRACE then local _, a = M.read_braces (body, scan); scan = a elseif c == BYTE_OPEN_BRACE then local _, a = M.read_braces (body, scan); scan = a
elseif c == BYTE_OPEN_BRACK then local _, a = M.read_brackets (body, scan); scan = a elseif c == BYTE_OPEN_BRACK then local _, a = M.read_brackets (body, scan); scan = a
-- String-literal byte ('"' = 0x22 or '\'' = 0x27): skip past the -- String-literal byte ('"' = 0x22 or '\'' = 0x27): skip past the quoted region in one shot.
-- quoted region in one shot.
elseif c == BYTE_DQUOTE or c == BYTE_SQUOTE then elseif c == BYTE_DQUOTE or c == BYTE_SQUOTE then
scan = M.skip_str_or_cmt(body, scan) + 1 scan = M.skip_str_or_cmt(body, scan) + 1
else else
@@ -731,12 +719,13 @@ M.TAPE_ATOM_MACROS = {
-- The check (`scripts/passes/static_analysis.lua :: check_gte_pipeline_fill`) walks each atom body, -- The check (`scripts/passes/static_analysis.lua :: check_gte_pipeline_fill`) walks each atom body,
-- counts the consecutive nop words before every `gte_cmdw_*` invocation, and reports a finding if the count is below this minimum. -- counts the consecutive nop words before every `gte_cmdw_*` invocation, and reports a finding if the count is below this minimum.
-- --
-- PRE-FILL vs POST-FILL: this table models PRE-cmdw nops (retiring preceding C2 writes), NOT the post-cmdw input-latch -- PRE-FILL vs POST-FILL: this table models PRE-cmdw nops (retiring preceding C2 writes),
-- window. The PSX-SPX pipeline timings doc (`docs/psx-spx/docs/gtepipelinetimings.md`) measures a DIFFERENT number: -- NOT the post-cmdw input-latch window.
-- the smallest N nops between `cop2` and `mtc2` to a specific input register at which the write no longer affects -- The PSX-SPX pipeline timings doc (`docs/psx-spx/docs/gtepipelinetimings.md`) measures a DIFFERENT number:
-- the output. For nearly all instructions, inputs latch in the first 0-4 cycles — the GTE snapshots its input -- the smallest N nops between `cop2` and `mtc2` to a specific input register at which the write no longer affects the output.
-- register file early and works from internal pipeline storage afterward. The documented total cycle count is -- For nearly all instructions, inputs latch in the first 0-4 cycles — the GTE snapshots its input register file early and works
-- NOT the "do not touch inputs" window; the actual read window is much shorter. -- from internal pipeline storage afterward. The documented total cycle count is NOT the "do not touch inputs" window;
-- the actual read window is much shorter.
-- --
-- The `gte_rtpt()` / `gte_nclip()` wrapper macros in gte.h emit the pre-cmd nops internally (asm_words(nop, nop, ...)), -- The `gte_rtpt()` / `gte_nclip()` wrapper macros in gte.h emit the pre-cmd nops internally (asm_words(nop, nop, ...)),
-- but THOSE WRAPPERS ARE NOT USED INSIDE ATOM BODIES in this codebase. -- but THOSE WRAPPERS ARE NOT USED INSIDE ATOM BODIES in this codebase.
+46 -78
View File
@@ -14,9 +14,8 @@
-- Native dependencies -- Native dependencies
-- ════════════════════════════════════════════════════════════════════════════ -- ════════════════════════════════════════════════════════════════════════════
-- lfs is wired into package.cpath by `duffle_paths.lua` (vendored under -- lfs is wired into package.cpath by `duffle_paths.lua` (vendored under `toolchain/lfs/lfs.dll`).
-- `toolchain/lfs/lfs.dll`). Required here for native directory ops -- Required here for native directory ops (replaces the ~56ms `dir /b` subprocess with ~2ms native).
-- (replaces the ~56ms `dir /b` subprocess with ~2ms native).
local lfs = require("lfs") local lfs = require("lfs")
local M = {} local M = {}
@@ -24,8 +23,7 @@ local M = {}
-- ════════════════════════════════════════════════════════════════════════════ -- ════════════════════════════════════════════════════════════════════════════
-- DWARF tag + form constants -- DWARF tag + form constants
-- ════════════════════════════════════════════ -- ════════════════════════════════════════════
-- (DWARF5 §7.5.5 "Tag Encodings" + Table 7.1; gcc emits these exact values for -- (DWARF5 §7.5.5 "Tag Encodings" + Table 7.1; gcc emits these exact values for the DWARF3-extension and DWARF5 line units.)
-- the DWARF3-extension and DWARF5 line units.)
M.DW_TAG = { M.DW_TAG = {
compile_unit = 0x11, compile_unit = 0x11,
@@ -107,13 +105,12 @@ M.MIPS_BYTES_PER_WORD = 0x04
-- ---------------------------------------------------------------------------- -- ----------------------------------------------------------------------------
-- ELF32 (System V ABI gABI v1.2) -- ELF32 (System V ABI gABI v1.2)
-- ---------------------------------------------------------------------------- -- ----------------------------------------------------------------------------
--
-- All offsets are 1-INDEXED (matching Lua string.sub convention), -- All offsets are 1-INDEXED (matching Lua string.sub convention),
-- expressed in hex so they map directly to the wire-format byte positions in the binary file. -- expressed in hex so they map directly to the wire-format byte positions in the binary file.
-- To compute the 0-indexed file offset, subtract 1. -- To compute the 0-indexed file offset, subtract 1.
-- --
-- Example: e_shoff_offset = 0x21 means the 4-byte e_shoff field -- Example: e_shoff_offset = 0x21 means the 4-byte e_shoff field starts at
-- starts at string.sub byte 0x21 (= 33 in 1-indexed), i.e. file offset 0x20 (= 32). -- string.sub byte 0x21 (= 33 in 1-indexed), i.e. file offset 0x20 (= 32).
--- spec: System V ABI gABI v1.2 §"ELF Header" (Table 1) + §"Section Header Table" --- spec: System V ABI gABI v1.2 §"ELF Header" (Table 1) + §"Section Header Table"
M.ELF32 = { M.ELF32 = {
@@ -139,7 +136,6 @@ M.ELF32 = {
-- ---------------------------------------------------------------------------- -- ----------------------------------------------------------------------------
-- DWARF4 .debug_aranges (per DWARF5 spec §7.4 — Address Range Table) -- DWARF4 .debug_aranges (per DWARF5 spec §7.4 — Address Range Table)
-- ---------------------------------------------------------------------------- -- ----------------------------------------------------------------------------
--
-- All offsets are 1-INDEXED (matching Lua string.sub convention), in hex. -- All offsets are 1-INDEXED (matching Lua string.sub convention), in hex.
--- spec: DWARF5 spec §7.4 (Address Range Table) — 32-bit DWARF form --- spec: DWARF5 spec §7.4 (Address Range Table) — 32-bit DWARF form
@@ -159,7 +155,6 @@ M.DWARF4_ARANGES = {
-- ---------------------------------------------------------------------------- -- ----------------------------------------------------------------------------
-- DWARF5 .debug_rnglists (per DWARF5 spec §2.17 + §7.21) -- DWARF5 .debug_rnglists (per DWARF5 spec §2.17 + §7.21)
-- ---------------------------------------------------------------------------- -- ----------------------------------------------------------------------------
--
-- All offsets are 1-INDEXED (matching Lua string.sub convention), in hex. -- All offsets are 1-INDEXED (matching Lua string.sub convention), in hex.
--- spec: DWARF5 spec §2.17 + §7.21 (Range List Table) — 32-bit DWARF form --- spec: DWARF5 spec §2.17 + §7.21 (Range List Table) — 32-bit DWARF form
@@ -181,7 +176,6 @@ M.DWARF5_RNGLISTS = {
-- ---------------------------------------------------------------------------- -- ----------------------------------------------------------------------------
-- DWARF line-program opcodes (per DWARF5 spec §6.2.5) -- DWARF line-program opcodes (per DWARF5 spec §6.2.5)
-- ---------------------------------------------------------------------------- -- ----------------------------------------------------------------------------
--
-- Opcode VALUES stay in decimal — they're identifiers (DW_LNS_copy = 1), not binary positions. -- Opcode VALUES stay in decimal — they're identifiers (DW_LNS_copy = 1), not binary positions.
-- Compare to the *_offset fields above which are hex. -- Compare to the *_offset fields above which are hex.
@@ -275,11 +269,9 @@ local function read_sleb128_at(buf, pos)
return nil, pos return nil, pos
end end
-- Find the 0-based offset of the table-terminator byte (a single 0) for the -- Find the 0-based offset of the table-terminator byte (a single 0) for the abbrev table starting at `table_start`.
-- abbrev table starting at `table_start`. Returns nil on truncated input. -- Returns nil on truncated input. Walks declaration headers
-- Walks declaration headers (code, tag, has_children, attr/form pairs, -- (code, tag, has_children, attr/form pairs, DW_FORM_implicit_const constant) until it finds a 0 byte that follows a complete declaration. Mirrors dwarf_injection.lua :: find_abbrev_table_end.
-- DW_FORM_implicit_const constant) until it finds a 0 byte that follows a
-- complete declaration. Mirrors dwarf_injection.lua :: find_abbrev_table_end.
local function find_abbrev_table_end(table_bytes, table_start) local function find_abbrev_table_end(table_bytes, table_start)
local pos, len = table_start, #table_bytes local pos, len = table_start, #table_bytes
if pos >= len or table_bytes:byte(pos + 1) == 0 then return pos end if pos >= len or table_bytes:byte(pos + 1) == 0 then return pos end
@@ -321,8 +313,8 @@ local function read_c_string_at(buf, off)
return buf:sub(start + 1, off) return buf:sub(start + 1, off)
end end
-- Walk the .debug_abbrev table starting at 0-based offset `table_start` and -- Walk the .debug_abbrev table starting at 0-based offset `table_start` and return a list of declarations:
-- return a list of declarations: {code, tag, has_children, attrs={ {name, form}, ... }}. -- {code, tag, has_children, attrs={ {name, form}, ... }}.
-- Stops at the table terminator. -- Stops at the table terminator.
local function parse_abbrev_table(table_bytes, table_start) local function parse_abbrev_table(table_bytes, table_start)
local table_end = find_abbrev_table_end(table_bytes, table_start) local table_end = find_abbrev_table_end(table_bytes, table_start)
@@ -362,8 +354,8 @@ end
-- Read a ULEB attribute value at 0-based offset `pos` for the given `form`. -- Read a ULEB attribute value at 0-based offset `pos` for the given `form`.
-- Returns (value, next_pos). For DW_FORM_string we return the inline string. -- Returns (value, next_pos). For DW_FORM_string we return the inline string.
-- For DW_FORM_strp we return the inline string resolved from `str_buf`. -- For DW_FORM_strp we return the inline string resolved from `str_buf`.
-- For DW_FORM_ref4 we return the absolute CU-relative offset. The caller -- For DW_FORM_ref4 we return the absolute CU-relative offset.
-- decides whether to interpret that as a section offset. -- The caller decides whether to interpret that as a section offset.
local function read_form_value(buf, str_buf, pos, form) local function read_form_value(buf, str_buf, pos, form)
if form == M.DW_FORM.addr then if form == M.DW_FORM.addr then
return M.read_u32_le(buf, pos + 1), pos + 4 return M.read_u32_le(buf, pos + 1), pos + 4
@@ -374,16 +366,11 @@ local function read_form_value(buf, str_buf, pos, form)
-- DW_FORM_strp: 4-byte offset into .debug_str. -- DW_FORM_strp: 4-byte offset into .debug_str.
local strp_off = M.read_u32_le(buf, pos + 1) local strp_off = M.read_u32_le(buf, pos + 1)
return read_c_string_at(str_buf, strp_off), pos + 4 return read_c_string_at(str_buf, strp_off), pos + 4
elseif form == M.DW_FORM.udata then elseif form == M.DW_FORM.udata then return read_uleb128_at(buf, pos)
return read_uleb128_at(buf, pos) elseif form == M.DW_FORM.data1 then return buf:byte(pos + 1), pos + 1
elseif form == M.DW_FORM.data1 then elseif form == M.DW_FORM.data2 then return M.read_u16_le(buf, pos + 1), pos + 2
return buf:byte(pos + 1), pos + 1 elseif form == M.DW_FORM.data4 then return M.read_u32_le(buf, pos + 1), pos + 4
elseif form == M.DW_FORM.data2 then elseif form == M.DW_FORM.ref4 then return M.read_u32_le(buf, pos + 1), pos + 4
return M.read_u16_le(buf, pos + 1), pos + 2
elseif form == M.DW_FORM.data4 then
return M.read_u32_le(buf, pos + 1), pos + 4
elseif form == M.DW_FORM.ref4 then
return M.read_u32_le(buf, pos + 1), pos + 4
elseif form == M.DW_FORM.sec_offset then elseif form == M.DW_FORM.sec_offset then
-- DW_FORM_sec_offset: 4-byte offset (size depends on DWARF version; -- DW_FORM_sec_offset: 4-byte offset (size depends on DWARF version;
-- on DWARF5 32-bit it's always 4 bytes). -- on DWARF5 32-bit it's always 4 bytes).
@@ -405,10 +392,9 @@ local function read_form_value(buf, str_buf, pos, form)
end end
end end
-- Index the .debug_info + .debug_abbrev sections of an existing ELF and -- Index the .debug_info + .debug_abbrev sections of an existing ELF and collect one entry
-- collect one entry per "interesting" type DIE in the FIRST compilation -- per "interesting" type DIE in the FIRST compilation unit.
-- unit. The index supports typed-view resolution for Phase 5: -- The index supports typed-views:
--
-- index = { -- index = {
-- by_name = { ["V4_S2"] = {kind="structure_type", die_offset, byte_size, fields={...}}, -- by_name = { ["V4_S2"] = {kind="structure_type", die_offset, byte_size, fields={...}},
-- ["U4"] = {kind="base_type", die_offset, byte_size, encoding="unsigned"}, -- ["U4"] = {kind="base_type", die_offset, byte_size, encoding="unsigned"},
@@ -416,8 +402,6 @@ end
-- by_offset = { [die_offset] = {kind, name, ...} }, -- reverse lookup -- by_offset = { [die_offset] = {kind, name, ...} }, -- reverse lookup
-- } -- }
-- --
-- Only the main CU is indexed. We do not walk nested CUs (this matches the
-- Phase 2-4 scope: the main CU is the only one in this build).
-- @param info string -- .debug_info section bytes -- @param info string -- .debug_info section bytes
-- @param abbrev string -- .debug_abbrev section bytes -- @param abbrev string -- .debug_abbrev section bytes
-- @param str_buf string -- .debug_str section bytes (for DW_FORM_strp name resolution) -- @param str_buf string -- .debug_str section bytes (for DW_FORM_strp name resolution)
@@ -427,12 +411,10 @@ end
function M.index_main_cu_types(info, abbrev, str_buf, abbrev_offset, cu_start) function M.index_main_cu_types(info, abbrev, str_buf, abbrev_offset, cu_start)
if not info or #info < 12 or not abbrev or not abbrev_offset then return nil, "missing input" end if not info or #info < 12 or not abbrev or not abbrev_offset then return nil, "missing input" end
str_buf = str_buf or "" str_buf = str_buf or ""
-- Index the main table at abbrev_offset. The F' pass writes a trailing -- Index the main table at abbrev_offset.
-- 0 byte after the main table; the G' pass may append additional codes -- The F' pass writes a trailing 0 byte after the main table; the G' pass may append additional codes (100-108) + a 0 terminator.
-- (100-108) + a 0 terminator. The walker may encounter a code that -- The walker may encounter a code that wasn't in the main table but exists later in the same .debug_abbrev;
-- wasn't in the main table but exists later in the same .debug_abbrev; -- on the first miss, walk the rest of the section to add any new abbrevs we encounter.
-- on the first miss, walk the rest of the section to add any new
-- abbrevs we encounter.
local abbrev_decls, err = parse_abbrev_table(abbrev, abbrev_offset) local abbrev_decls, err = parse_abbrev_table(abbrev, abbrev_offset)
if not abbrev_decls then return nil, err end if not abbrev_decls then return nil, err end
local abbrev_by_code = {} local abbrev_by_code = {}
@@ -488,11 +470,10 @@ function M.index_main_cu_types(info, abbrev, str_buf, abbrev_offset, cu_start)
return { by_name = by_name, by_offset = by_offset } return { by_name = by_name, by_offset = by_offset }
end end
-- Helper: skip a subtree rooted at the current DIE (pos_cursor is -- Helper: skip a subtree rooted at the current DIE (pos_cursor is positioned at the first child).
-- positioned at the first child). Walks down and right until the -- Walks down and right until the matching null terminator is consumed.
-- matching null terminator is consumed. Returns the new pos_cursor. -- Returns the new pos_cursor.
-- For DIE trees that contain only the closed type + member shapes, -- For DIE trees that contain only the closed type + member shapes, the depth never exceeds 2 (type DIE -> member DIE -> null).
-- the depth never exceeds 2 (type DIE -> member DIE -> null).
local function skip_subtree(pos) local function skip_subtree(pos)
local depth = 1 local depth = 1
while pos < cu_end_excl and depth > 0 do while pos < cu_end_excl and depth > 0 do
@@ -513,8 +494,8 @@ function M.index_main_cu_types(info, abbrev, str_buf, abbrev_offset, cu_start)
-- Pre-declare n_visited so the closure helpers can read it. -- Pre-declare n_visited so the closure helpers can read it.
local n_visited = 0 local n_visited = 0
-- Helper: read one DIE's attributes. Returns (name, byte_size, encoding, -- Helper: read one DIE's attributes.
-- type_ref, new_pos_cursor) on success; (nil, error_string) on truncation. -- Returns (name, byte_size, encoding, type_ref, new_pos_cursor) on success; (nil, error_string) on truncation.
local function read_die_attributes(decl, pos) local function read_die_attributes(decl, pos)
local die_name = nil local die_name = nil
local die_byte_size = nil local die_byte_size = nil
@@ -567,17 +548,16 @@ function M.index_main_cu_types(info, abbrev, str_buf, abbrev_offset, cu_start)
return fields, pos return fields, pos
end end
-- Top-level walker: iterate siblings. For each DIE, decide whether to -- Top-level walker: iterate siblings.
-- record (if type), descend (if structure_type with members), or skip -- For each DIE, decide whether to record (if type), descend (if structure_type with members), or skip its subtree.
-- its subtree.
while pos_cursor < cu_end_excl do while pos_cursor < cu_end_excl do
local code = info:byte(pos_cursor + 1) local code = info:byte(pos_cursor + 1)
if code == 0 then pos_cursor = pos_cursor + 1; break end if code == 0 then pos_cursor = pos_cursor + 1; break end
local decl = abbrev_by_code[code] local decl = abbrev_by_code[code]
if not decl then if not decl then
-- Lazily scan the rest of the section for the missing code. -- Lazily scan the rest of the section for the missing code.
-- The G' pass appends new abbrevs (100-108) after the main table's -- The G' pass appends new abbrevs (100-108) after the main table's terminator.
-- terminator. On the first miss, walk the rest of the section. -- On the first miss, walk the rest of the section.
local scan_pos = 0 local scan_pos = 0
while scan_pos < #abbrev do while scan_pos < #abbrev do
if abbrev:byte(scan_pos + 1) == 0 then if abbrev:byte(scan_pos + 1) == 0 then
@@ -655,9 +635,8 @@ function M.index_main_cu_types(info, abbrev, str_buf, abbrev_offset, cu_start)
return { by_name = by_name, by_offset = by_offset } return { by_name = by_name, by_offset = by_offset }
end end
-- Resolve a chain of pointer + const + typedef + structure_type down to a -- Resolve a chain of pointer + const + typedef + structure_type down to a canonical struct or base type.
-- canonical struct or base type. The returned entry has kind, name, byte_size, -- The returned entry has kind, name, byte_size, and (for structure_type) fields with {name, offset, type_name, pointer_depth}.
-- and (for structure_type) fields with {name, offset, type_name, pointer_depth}.
-- Returns nil if the chain cannot be resolved (e.g., missing DIE). -- Returns nil if the chain cannot be resolved (e.g., missing DIE).
-- @param index table -- M.index_main_cu_types result -- @param index table -- M.index_main_cu_types result
-- @param start_offset integer -- CU-relative DW_FORM_ref4 offset of the start -- @param start_offset integer -- CU-relative DW_FORM_ref4 offset of the start
@@ -673,12 +652,9 @@ function M.resolve_type_chain(index, start_offset, max_depth)
local entry = index.by_offset[cur_offset] local entry = index.by_offset[cur_offset]
if not entry then return nil end if not entry then return nil end
chain[#chain + 1] = entry chain[#chain + 1] = entry
if entry.kind == "pointer_type" then if entry.kind == "pointer_type" then cur_offset = entry.type_ref
cur_offset = entry.type_ref elseif entry.kind == "const_type" then cur_offset = entry.type_ref
elseif entry.kind == "const_type" then elseif entry.kind == "typedef" then cur_offset = entry.type_ref
cur_offset = entry.type_ref
elseif entry.kind == "typedef" then
cur_offset = entry.type_ref
else else
break break
end end
@@ -767,13 +743,13 @@ end
--- (no subprocess; lfs only for the existence check). Returns `{[name] = bytes_or_empty_string, ...}`. --- (no subprocess; lfs only for the existence check). Returns `{[name] = bytes_or_empty_string, ...}`.
--- ---
--- **Convention:** offsets from `M.ELF32` (1-indexed for string.sub). --- **Convention:** offsets from `M.ELF32` (1-indexed for string.sub).
--- Every requested name has an entry in the returned dict; missing sections have an empty string (NOT nil) --- Every requested name has an entry in the returned dict;
--- so callers can do `sections[".debug_x"] or ""` for the missing case. --- missing sections have an empty string (NOT nil) so callers can do `sections[".debug_x"] or ""` for the missing case.
--- ---
--- **Cost:** one file open + one `f:seek` + one `f:read` per section header --- **Cost:** one file open + one `f:seek` + one `f:read` per section header
--- (we walk all `e_shnum` headers regardless of how many names are requested, to find the .shstrtab first). --- (we walk all `e_shnum` headers regardless of how many names are requested, to find the .shstrtab first).
--- For frequent callers, pass the union of all needed sections in one call. --- For frequent callers, pass the union of all needed sections in one call.
-- can add `.debug_info` + `.debug_loc` + `.debug_str_offsets` to the list without writing a 2nd ELF walker. -- Can add `.debug_info` + `.debug_loc` + `.debug_str_offsets` to the list without writing a 2nd ELF walker.
--- @param elf_path Path --- @param elf_path Path
--- @param section_names string[] -- list of section names to read --- @param section_names string[] -- list of section names to read
--- @return table<string, string> --- @return table<string, string>
@@ -897,8 +873,7 @@ function M.read_nm(elf_path)
end end
-- Iterate the 16-byte ELF32 symtab entries. -- Iterate the 16-byte ELF32 symtab entries.
-- Each entry (1-indexed): st_name at 1, st_value at 5, st_size at 9, -- Each entry (1-indexed): st_name at 1, st_value at 5, st_size at 9, st_info at 13, st_other at 14, st_shndx at 15.
-- st_info at 13, st_other at 14, st_shndx at 15.
local SYM_ENTRY_BYTES = 0x10 local SYM_ENTRY_BYTES = 0x10
local SYM_ST_NAME = 0x01 local SYM_ST_NAME = 0x01
local SYM_ST_VALUE = 0x05 local SYM_ST_VALUE = 0x05
@@ -940,7 +915,6 @@ end
-- Both encoders pack 7 bits of data per byte + 1 bit of "more bytes follow" signaling. -- Both encoders pack 7 bits of data per byte + 1 bit of "more bytes follow" signaling.
-- --
-- Per-byte layout: -- Per-byte layout:
--
-- bit: 7 6 5 4 3 2 1 0 -- bit: 7 6 5 4 3 2 1 0
-- │ └───── 7-bit data ─────┘ -- │ └───── 7-bit data ─────┘
-- └─ continuation flag (LEB_CONT_BIT = 0x80) -- └─ continuation flag (LEB_CONT_BIT = 0x80)
@@ -966,13 +940,11 @@ local LEB_DATA_MASK = 0x7F
local SLEB_SIGN_BIT = 0x40 local SLEB_SIGN_BIT = 0x40
--- ULEB128 (Unsigned Little-Endian Base 128) encoder. Returns the byte string for the non-negative integer `n`. --- ULEB128 (Unsigned Little-Endian Base 128) encoder. Returns the byte string for the non-negative integer `n`.
---
--- Algorithm: --- Algorithm:
--- - Extract the low 7 bits of `n` (LEB_DATA_MASK = 0x7F). --- - Extract the low 7 bits of `n` (LEB_DATA_MASK = 0x7F).
--- - Shift `n` right by 7 bits. --- - Shift `n` right by 7 bits.
--- - If more bytes remain, OR in the continuation flag (LEB_CONT_BIT). --- - If more bytes remain, OR in the continuation flag (LEB_CONT_BIT).
--- - Repeat until `n` is fully consumed. --- - Repeat until `n` is fully consumed.
---
--- @param n integer -- non-negative --- @param n integer -- non-negative
--- @return string --- @return string
function M.uleb128(n) function M.uleb128(n)
@@ -992,9 +964,7 @@ function M.uleb128(n)
return table.concat(bytes) return table.concat(bytes)
end end
--- SLEB128 (Signed Little-Endian Base 128) encoder. Returns the byte --- SLEB128 (Signed Little-Endian Base 128) encoder. Returns the byte string for the integer `n` (may be negative).
--- string for the integer `n` (may be negative).
---
--- Algorithm differs from ULEB128 by the termination condition: stop when --- Algorithm differs from ULEB128 by the termination condition: stop when
--- the remaining bits can be inferred from the sign bit in the last byte's --- the remaining bits can be inferred from the sign bit in the last byte's
--- 7-bit data payload. --- 7-bit data payload.
@@ -1003,7 +973,6 @@ end
--- ---
--- Without these checks, the decoder would round-trip to a different value --- Without these checks, the decoder would round-trip to a different value
--- (e.g. encoding `0` as `0x80 0x00` decodes to `0` correctly but is 2 bytes long; the termination check picks the 1-byte `0x00` form). --- (e.g. encoding `0` as `0x80 0x00` decodes to `0` correctly but is 2 bytes long; the termination check picks the 1-byte `0x00` form).
---
--- @param n integer -- any integer (negative allowed) --- @param n integer -- any integer (negative allowed)
--- @return string --- @return string
function M.sleb128(n) function M.sleb128(n)
@@ -1087,7 +1056,7 @@ end
--- Returns `{name -> {total = N, words = {{pos, call_file, call_line, comp_name, comp_file, comp_line}, ...}}}`. --- Returns `{name -> {total = N, words = {{pos, call_file, call_line, comp_name, comp_file, comp_line}, ...}}}`.
--- Returns `{}` on format-version mismatch (and logs to stderr). --- Returns `{}` on format-version mismatch (and logs to stderr).
--- ---
--- **Wire format** (emitted by `passes/atoms_source_map.lua` since the Phase 3 debug_ux work): --- **Wire format** (emitted by `passes/atoms_source_map.lua`):
--- ``` --- ```
--- # FORMAT_VERSION <n> --- # FORMAT_VERSION <n>
--- ATOM <name> "<abs-source-path>" <total> --- ATOM <name> "<abs-source-path>" <total>
@@ -1097,11 +1066,10 @@ end
--- ENDATOM --- ENDATOM
--- ``` --- ```
--- ---
--- **Used by** `passes/dwarf_injection.lua` (Phase 3 — debug_ux) to: --- **Used by** `passes/dwarf_injection.lua` to:
--- - group consecutive MACRO rows into component invocations (one `DW_TAG_inlined_subroutine` each) --- - group consecutive MACRO rows into component invocations (one `DW_TAG_inlined_subroutine` each)
--- - emit abstract `DW_TAG_subprogram` per unique component name --- - emit abstract `DW_TAG_subprogram` per unique component name
--- - extend `.debug_line` so stepping into a `mac_X(...)` lands on the component's source line. --- - extend `.debug_line` so stepping into a `mac_X(...)` lands on the component's source line.
---
--- @param prov_path string -- path to *.atoms.provenance.txt --- @param prov_path string -- path to *.atoms.provenance.txt
--- @param expected_version integer -- expected FORMAT_VERSION line --- @param expected_version integer -- expected FORMAT_VERSION line
--- @return table<string, table> --- @return table<string, table>
+13 -27
View File
@@ -1,29 +1,15 @@
# scripts/gdb/gdb_tape_atoms.gdb # scripts/gdb/gdb_tape_atoms.gdb
# #
# Wrapper for the tape-atom step-debug helpers. The 9 user commands are defined # Wrapper for the tape-atom step-debug helpers.
# here as STUBS (degraded-state messages). The real implementations + the # The 9 user commands are defined here as STUBS (degraded-state messages).
# per-atom data tables are emitted by `passes/atoms_source_map.lua` (post-link # The real implementations + the per-atom data tables are emitted by `passes/atoms_source_map.lua`
# invocation: `ps1_meta.lua --atoms-source-map --gdb-runtime --elf <elf>`) into # (post-link invocation: `ps1_meta.lua --atoms-source-map --gdb-runtime --elf <elf>`) into `build/gen/gdb_tape_atoms_runtime.gdb`.
# `build/gen/gdb_tape_atoms_runtime.gdb`. Sourcing that file RE-DEFINES the # Sourcing that file RE-DEFINES the commands with real implementations.
# commands with real implementations.
# #
# If `build/gen/gdb_tape_atoms_runtime.gdb` is missing or stale, the stubs # If `build/gen/gdb_tape_atoms_runtime.gdb` is missing or stale, the stubs remain (E1: no source map).
# remain (E1: no source map). The user just needs to re-run `build_psyq.ps1` # The user just needs to re-run `build_psyq.ps1` to regenerate.
# to regenerate. No exceptions; no crashes.
#
# Why a wrapper + separate runtime file?
# - The runtime file is auto-generated per-build; not in git.
# - The wrapper is checked into git; always works.
# - This split keeps the script trivial and the data plumbing out of git.
#
# Compatible with every gdb build (no Python, no Tcl, no Guile required) —
# pure gdb command scripting + `set $var = val` + `define ... end`.
#
# Generated by track gdb_tape_atom_debugging_20260711 — see
# C:\projects\Pikuma\ps1-ai\docs\gdb_tape_atom_debugging.md for the manual.
# ── Stub commands (defined here so they're always present, even if the # ── Stub commands (defined here so they're always present, even if the runtime file is missing). The runtime file overrides these if sourced. ──
# runtime file is missing). The runtime file overrides these if sourced. ──
define tape_atoms define tape_atoms
echo "[gdb_tape_atoms] STUB: runtime file build/gen/gdb_tape_atoms_runtime.gdb not found." echo "[gdb_tape_atoms] STUB: runtime file build/gen/gdb_tape_atoms_runtime.gdb not found."
@@ -107,13 +93,13 @@ end
# Try to source from project-root-relative path first (the typical case). # Try to source from project-root-relative path first (the typical case).
# If the user is in a different CWD, the source will fail and stubs remain. # If the user is in a different CWD, the source will fail and stubs remain.
# The runtime file path is computed relative to the ELF's source map convention # The runtime file path is computed relative to the ELF's source map convention (build/gen/gdb_tape_atoms_runtime.gdb).
# (build/gen/gdb_tape_atoms_runtime.gdb).
echo [gdb_tape_atoms] Wrapper loaded. Sourcing runtime file... echo [gdb_tape_atoms] Wrapper loaded. Sourcing runtime file...
# Suppress the "Redefine command" prompts that would otherwise appear when the # Suppress the "Redefine command" prompts that would otherwise appear when the runtime file overrides the 9 stub commands defined above.
# runtime file overrides the 9 stub commands defined above. The runtime's # The runtime's `define` blocks are intended to overwrite — there's no ambiguity to confirm.
# `define` blocks are intended to overwrite — there's no ambiguity to confirm.
set confirm off set confirm off
# Source the runtime file (re-defines commands with real impls + data).
source build/gen/gdb_tape_atoms_runtime.gdb source build/gen/gdb_tape_atoms_runtime.gdb
set confirm on set confirm on
echo [gdb_tape_atoms] Runtime sourced successfully (9 commands now have real implementations). echo [gdb_tape_atoms] Runtime sourced successfully (9 commands now have real implementations).
+30 -40
View File
@@ -13,8 +13,7 @@
-- Bootstrap: same as entry scripts. See `ps1_meta.lua` for the rationale. -- Bootstrap: same as entry scripts. See `ps1_meta.lua` for the rationale.
-- Bootstrap: load `scripts/duffle_paths.lua` (sets package.path + package.cpath). -- Bootstrap: load `scripts/duffle_paths.lua` (sets package.path + package.cpath).
-- Uses `debug.getinfo` to find this file's own directory, so it works -- Uses `debug.getinfo` to find this file's own directory, so it works both standalone and when require'd from the orchestrator.
-- both standalone and when require'd from the orchestrator.
-- Bootstrap: load `duffle_paths.lua` via `debug.getinfo(1, "S").source` (works both standalone + when require'd). -- Bootstrap: load `duffle_paths.lua` via `debug.getinfo(1, "S").source` (works both standalone + when require'd).
-- duffle_paths.lua sets package.path then returns `require("duffle")` at the bottom, so the dofile value IS the duffle module. -- duffle_paths.lua sets package.path then returns `require("duffle")` at the bottom, so the dofile value IS the duffle module.
local _bootstrap_dir = debug.getinfo(1, "S").source:match("^@?(.*[/\\])") or "./" local _bootstrap_dir = debug.getinfo(1, "S").source:match("^@?(.*[/\\])") or "./"
@@ -27,8 +26,8 @@ local WAVE_CONTEXT_REGS = duffle.WAVE_CONTEXT_REGS
local function is_wave_context_reg(n) return WAVE_CONTEXT_REGS[n] ~= nil end local function is_wave_context_reg(n) return WAVE_CONTEXT_REGS[n] ~= nil end
-- Phase 5 closed sets. atom_dbg_reg_default and atom_reg_types MUST target -- Closed sets. atom_dbg_reg_default and atom_reg_types MUST target one of these registers;
-- one of these registers; atom_view MUST reference a real Binds_* struct. -- atom_view MUST reference a real Binds_* struct.
local SEMANTIC_DEFAULT_REGS = { local SEMANTIC_DEFAULT_REGS = {
["R_TapePtr"] = true, ["R_AtomJmp"] = true, ["R_TapePtr"] = true, ["R_AtomJmp"] = true,
["R_PrimCursor"] = true, ["R_FaceCursor"] = true, ["R_PrimCursor"] = true, ["R_FaceCursor"] = true,
@@ -39,9 +38,8 @@ local COMPUTE_REG_PREFIX = "R_"
local COMPUTE_REG_RE = -- permits R_T0..R_T3 + caller-trash (not wave-context) local COMPUTE_REG_RE = -- permits R_T0..R_T3 + caller-trash (not wave-context)
"^R_T[0-3]$" "^R_T[0-3]$"
-- Compute-register types are restricted to byte-width primitives (no struct -- Compute-register types are restricted to byte-width primitives.
-- identity in this phase). Pointer depth is also bounded typed pointer -- Pointer depth is also bounded typed pointer chains live in the Binds_* struct, not in a per-atom override.
-- chains live in the Binds_* struct, not in a per-atom override.
local ALLOWED_COMPUTE_TYPES = { local ALLOWED_COMPUTE_TYPES = {
["U4"] = true, ["S4"] = true, ["U4"] = true, ["S4"] = true,
["U2"] = true, ["S2"] = true, ["U2"] = true, ["S2"] = true,
@@ -132,8 +130,8 @@ local ALLOWED_COMPUTE_TYPES = {
-- Each check has a uniform `append_to_findings` shape (errors[] / warnings[] / info[]). -- Each check has a uniform `append_to_findings` shape (errors[] / warnings[] / info[]).
-- The dispatcher in `validate()` decides which findings list each check writes to — by convention, -- The dispatcher in `validate()` decides which findings list each check writes to — by convention,
-- "existence" checks (declaration must exist, struct must exist) write errors[]; "shape" checks -- "existence" checks (declaration must exist, struct must exist) write errors[]; "shape" checks
-- (writes/reads must be wave-context) write warnings[]. The `macro_word_drift` check writes -- (writes/reads must be wave-context) write warnings[].
-- both errors[] (missing/mismatch) and info[] (match). -- The `macro_word_drift` check writes both errors[] (missing/mismatch) and info[] (match).
--- Check: every annotated atom must have a matching MipsAtom_(name) declaration. --- Check: every annotated atom must have a matching MipsAtom_(name) declaration.
--- @param a AtomAnnotation --- @param a AtomAnnotation
@@ -164,9 +162,8 @@ local function check_unique_annotation(pipe_ctx, findings)
end end
--- Check: BIND atoms must reference a real Binds_* struct. --- Check: BIND atoms must reference a real Binds_* struct.
--- Demoted from error to warning (2026-07-10): the same condition is now caught by passes/static_analysis.lua's --- Emitting a warning here keeps the annotation pass from being stop-on-error for the common test-fixture case,
--- check_abi_handoff() as an error. Emitting a warning here keeps the annotation pass from being stop-on-error --- while still surfacing the issue in the report.
--- for the common test-fixture case, while still surfacing the issue in the report.
--- The static-analysis report remains the source of truth for build-stopping errors. --- The static-analysis report remains the source of truth for build-stopping errors.
--- @param a AtomAnnotation --- @param a AtomAnnotation
--- @param pipe_ctx PipeCtx --- @param pipe_ctx PipeCtx
@@ -253,16 +250,13 @@ local function check_macro_word_drift(m, wc, findings)
} }
end end
---- Check: atom_dbg_reg_default(R_X, <type>) must target a wave-context register ---- Check: atom_dbg_reg_default(R_X, <type>) must target a wave-context register and the type name must be a recognized C type
--- and the type name must be a recognized C type (we currently allow the --- (we currently allow the types actually used in the codebase; pointer depth must be 0 or 1).
--- types actually used in the codebase; pointer depth must be 0 or 1).
--- Also detects duplicate defaults for the same register. --- Also detects duplicate defaults for the same register.
--- @param _src SourceFile -- unused (kept for the per_source shape) --- @param _src SourceFile -- unused (kept for the per_source shape)
--- @param pipe_ctx PipeCtx --- @param pipe_ctx PipeCtx
--- @param findings Findings --- @param findings Findings
-- Known type names accepted in atom_dbg_reg_default and the Binds_*-via-atom_view -- Known type names accepted in atom_dbg_reg_default and the Binds_*-via-atom_view resolution path.
-- resolution path. The Phase 5 list mirrors the canonical duffle math.h
-- typedefs plus the byte-width primitives used as compute-register overrides.
local KNOWN_REG_DEFAULT_TYPES = { local KNOWN_REG_DEFAULT_TYPES = {
["U4"] = true, ["S4"] = true, ["U4"] = true, ["S4"] = true,
["U2"] = true, ["S2"] = true, ["U2"] = true, ["S2"] = true,
@@ -349,8 +343,7 @@ local function check_atom_reg_types(_src, pipe_ctx, findings)
end end
end end
--- Check: atom_view(Binds_X) entries must reference a real Binds_* struct --- Check: atom_view(Binds_X) entries must reference a real Binds_* struct and that struct must declare at least one field.
--- and that struct must declare at least one field.
--- @param _src SourceFile --- @param _src SourceFile
--- @param pipe_ctx PipeCtx --- @param pipe_ctx PipeCtx
--- @param findings Findings --- @param findings Findings
@@ -379,8 +372,8 @@ local function check_atom_view_layout(_src, pipe_ctx, findings)
end end
end end
--- Check: Binds_* structs may not have duplicate field names (they would --- Check: Binds_* structs may not have duplicate field names
--- defeat the typed-field name lookup that atom_view exposes in gdb). --- (they would defeat the typed-field name lookup that atom_view exposes in gdb).
--- @param _src SourceFile --- @param _src SourceFile
--- @param pipe_ctx PipeCtx --- @param pipe_ctx PipeCtx
--- @param findings Findings --- @param findings Findings
@@ -413,7 +406,7 @@ end
--- 4. pending + no target_kind -> dangling (no following declaration) --- 4. pending + no target_kind -> dangling (no following declaration)
--- 5. unsupported target_kind -> marker precedes an unrelated declaration --- 5. unsupported target_kind -> marker precedes an unrelated declaration
--- Valid markers before whole-atom / bare-component / proc-component declarations --- Valid markers before whole-atom / bare-component / proc-component declarations
--- emit no error and remain in src.scan.skip_over.atoms / .components for Task 15. --- emit no error and remain in src.scan.skip_over.atoms / .components.
--- @param marker SkipOverMarker --- @param marker SkipOverMarker
--- @param _pipe_ctx PipeCtx -- unused today; kept for plex-shape consistency with per_annot --- @param _pipe_ctx PipeCtx -- unused today; kept for plex-shape consistency with per_annot
--- @param findings Findings --- @param findings Findings
@@ -440,8 +433,8 @@ local function check_skip_marker(marker, _pipe_ctx, findings)
if marker.superseded_by_marker_line then if marker.superseded_by_marker_line then
findings.errors[#findings.errors + 1] = { findings.errors[#findings.errors + 1] = {
line = line, line = line,
msg = string.format("duplicate %s marker at line %d; superseded by another %s marker at line %d", msg = string.format("duplicate %s marker at line %d; superseded by another %s marker at line %d"
kind, line, kind, marker.superseded_by_marker_line), , kind, line, kind, marker.superseded_by_marker_line),
} }
return return
end end
@@ -449,8 +442,8 @@ local function check_skip_marker(marker, _pipe_ctx, findings)
if marker.pending and not marker.target_kind then if marker.pending and not marker.target_kind then
findings.errors[#findings.errors + 1] = { findings.errors[#findings.errors + 1] = {
line = line, line = line,
msg = string.format("dangling %s marker at line %d: no following MipsAtom_/MipsAtomComp_/MipsAtomComp_Proc_ declaration", msg = string.format("dangling %s marker at line %d: no following MipsAtom_/MipsAtomComp_/MipsAtomComp_Proc_ declaration"
kind, line), , kind, line),
} }
return return
end end
@@ -461,8 +454,8 @@ local function check_skip_marker(marker, _pipe_ctx, findings)
and marker.target_kind ~= "comp_proc" then and marker.target_kind ~= "comp_proc" then
findings.errors[#findings.errors + 1] = { findings.errors[#findings.errors + 1] = {
line = line, line = line,
msg = string.format("%s marker at line %d must precede MipsAtom_, MipsAtomComp_, or MipsAtomComp_Proc_; found an unrelated declaration", msg = string.format("%s marker at line %d must precede MipsAtom_, MipsAtomComp_, or MipsAtomComp_Proc_; found an unrelated declaration"
kind, line), , kind, line),
} }
end end
end end
@@ -475,7 +468,7 @@ end
-- per_annot(annot, pipe_ctx, findings) — runs once per AtomAnnotation -- per_annot(annot, pipe_ctx, findings) — runs once per AtomAnnotation
-- post(pipe_ctx, findings) — runs once after all per_annot calls complete (full-corpus aggregation) -- post(pipe_ctx, findings) — runs once after all per_annot calls complete (full-corpus aggregation)
-- per_macro(macro, wc, findings) — runs once per TAPE_WORDS / _Pragma macro declaration -- per_macro(macro, wc, findings) — runs once per TAPE_WORDS / _Pragma macro declaration
-- per_skip_marker(marker, pipe_ctx, findings) — runs once per src.scan.skip_over.markers entry (Task 14) -- per_skip_marker(marker, pipe_ctx, findings) — runs once per src.scan.skip_over.markers entry
-- --
-- Adding a new check = 1 row here + 1 function above. The `validate()` dispatch loop never needs editing. -- Adding a new check = 1 row here + 1 function above. The `validate()` dispatch loop never needs editing.
@@ -532,8 +525,7 @@ local function validate(ctx, src)
-- Build pipe_ctx (Fleury: expose structure). Pre-compute everything the per-check functions need. -- Build pipe_ctx (Fleury: expose structure). Pre-compute everything the per-check functions need.
-- Single source of truth for atom / binds / annotation-count lookups. -- Single source of truth for atom / binds / annotation-count lookups.
-- Phase 5 typed views: pipe_ctx.types / pipe_ctx.atom_views / pipe_ctx.seen_defaults -- pipe_ctx.types / pipe_ctx.atom_views / pipe_ctx.seen_defaults are projected from the scan payload so per_source check rules can iterate.
-- are projected from the scan payload so per_source check rules can iterate.
local seen_defaults = {} local seen_defaults = {}
for reg, _ in pairs(scan.types or {}) do for reg, _ in pairs(scan.types or {}) do
seen_defaults[reg] = (seen_defaults[reg] or 0) + 1 seen_defaults[reg] = (seen_defaults[reg] or 0) + 1
@@ -592,10 +584,10 @@ local function validate(ctx, src)
if rule.post then rule.post(pipe_ctx, findings) end if rule.post then rule.post(pipe_ctx, findings) end
end end
-- Per-skip-marker rules (Task 14). Each raw marker recorded by scan_source -- Per-skip-marker rules.
-- (in scan.skip_over.markers) is validated independently; the check emits at -- Each raw marker recorded by scan_source (in scan.skip_over.markers) is validated independently;
-- most one error per marker. Valid markers stay attached to scan.skip_over.atoms / -- the check emits at most one error per marker.
-- .components for Task 15's dwarf_injection.lua consumer. -- Valid markers stay attached to scan.skip_over.atoms /.components for dwarf_injection.lua consumer.
local skip_markers = scan.skip_over and scan.skip_over.markers or {} local skip_markers = scan.skip_over and scan.skip_over.markers or {}
for _, marker in ipairs(skip_markers) do for _, marker in ipairs(skip_markers) do
for _, rule in ipairs(CHECK_RULES) do for _, rule in ipairs(CHECK_RULES) do
@@ -611,9 +603,8 @@ local function validate(ctx, src)
end end
end end
-- Per-source rules (Phase 5 typed views: reg defaults, atom_view layout, -- Per-source rules (reg defaults, atom_view layout, compute-register type overrides, Binds_* field uniqueness).
-- compute-register type overrides, Binds_* field uniqueness). Each -- Each per_source rule sees the full scan payload via pipe_ctx.
-- per_source rule sees the full scan payload via pipe_ctx.
for _, rule in ipairs(CHECK_RULES) do for _, rule in ipairs(CHECK_RULES) do
if rule.per_source then rule.per_source(src, pipe_ctx, findings) end if rule.per_source then rule.per_source(src, pipe_ctx, findings) end
end end
@@ -707,7 +698,6 @@ function M.run(ctx)
for dir, dir_sources in pairs(by_dir) do for dir, dir_sources in pairs(by_dir) do
local dir_basename = dir:match("([^/\\]+)$") or dir local dir_basename = dir:match("([^/\\]+)$") or dir
local dir_atoms = 0 local dir_atoms = 0
local dir_errors = {} local dir_errors = {}
local dir_warnings = {} local dir_warnings = {}
+52 -89
View File
@@ -10,8 +10,8 @@
--- **Two output forms** (per the workspace's per-emission-form pattern from --- **Two output forms** (per the workspace's per-emission-form pattern from
--- `guide_metaprogram_ssdl.md`): --- `guide_metaprogram_ssdl.md`):
--- 1. **Canonical text form** — `<out_root>/<basename>.atoms.sourcemap.txt`. --- 1. **Canonical text form** — `<out_root>/<basename>.atoms.sourcemap.txt`.
--- Always emitted. Format-version-tagged for forward-compat. --- Format-version-tagged for forward-compat.
--- Lives in `<out_root>/` (build/gen) NOT `<source_dir>/gen/`. This file is a **build report**, not a compile artifact. --- Lives in `<out_root>/` (build/gen).
--- Matches the convention used by `annotation.lua` (`<out_root>/<basename>.errors.h`) + `static_analysis.lua` (`<out_root>/<basename>.static_analysis.txt`). --- Matches the convention used by `annotation.lua` (`<out_root>/<basename>.errors.h`) + `static_analysis.lua` (`<out_root>/<basename>.static_analysis.txt`).
--- Compile artifacts (`*.macs.h`, `*.offsets.h`) stay in `<source_dir>/gen/`. --- Compile artifacts (`*.macs.h`, `*.offsets.h`) stay in `<source_dir>/gen/`.
--- 2. **gdb-runtime form** — `<ctx.out_root>/gdb_tape_atoms_runtime.gdb` --- 2. **gdb-runtime form** — `<ctx.out_root>/gdb_tape_atoms_runtime.gdb`
@@ -47,9 +47,9 @@
-- Module-scope requires + package.path setup -- Module-scope requires + package.path setup
-- ════════════════════════════════════════════════════════════════════════════ -- ════════════════════════════════════════════════════════════════════════════
-- Bootstrap: load `duffle_paths.lua` via `debug.getinfo(1, "S").source` (works -- Bootstrap: load `duffle_paths.lua` via `debug.getinfo(1, "S").source`
-- both standalone + when require'd). `duffle_paths.lua` sets package.path then -- (works both standalone + when require'd). `duffle_paths.lua` sets package.path then returns `require("duffle")`
-- returns `require("duffle")` at the bottom, so the dofile value IS the duffle module. -- at the bottom, so the dofile value IS the duffle module.
local _bootstrap_dir = debug.getinfo(1, "S").source:match("^@?(.*[/\\])") or "./" local _bootstrap_dir = debug.getinfo(1, "S").source:match("^@?(.*[/\\])") or "./"
local duffle = dofile(_bootstrap_dir .. "../duffle_paths.lua") local duffle = dofile(_bootstrap_dir .. "../duffle_paths.lua")
local elf_dwarf = require("elf_dwarf") local elf_dwarf = require("elf_dwarf")
@@ -60,8 +60,8 @@ local count_token_words = word_count_eval.count_token_words
-- Constants -- Constants
-- ════════════════════════════════════════════════════════════════════════════ -- ════════════════════════════════════════════════════════════════════════════
-- Format version emitted as the first line. Bump + add a migration test if the -- Format version emitted as the first line. Bump + add a migration test if the format changes;
-- format changes; the gdb runtime loader rejects mismatches (E2). -- the gdb runtime loader rejects mismatches (E2).
local FORMAT_VERSION = 1 local FORMAT_VERSION = 1
-- Marker-call identifiers (mirrors offsets.lua:33-34). -- Marker-call identifiers (mirrors offsets.lua:33-34).
@@ -108,8 +108,7 @@ local function count_marker_rest(tok, wc)
end end
--- Compute per-word entries for an atom. --- Compute per-word entries for an atom.
--- Shared between the canonical text form (per-source `.atoms.sourcemap.txt`) --- Shared between the canonical text form and the gdb-runtime form.
--- and the gdb-runtime form (`gdb_tape_atoms_runtime.gdb`).
--- ---
--- Returns a list of `{pos, line, text}` entries + the total word count. --- Returns a list of `{pos, line, text}` entries + the total word count.
--- Markers contribute 0 entries (the marker call emits 0 `.word`s). --- Markers contribute 0 entries (the marker call emits 0 `.word`s).
@@ -135,9 +134,8 @@ local function compute_word_entries(atom, src, wc)
-- Source line for THIS token = line containing byte offset `atom.body_off + rel`. -- Source line for THIS token = line containing byte offset `atom.body_off + rel`.
-- `src.scan.line_of(...)` is O(log N) via LineIndex. -- `src.scan.line_of(...)` is O(log N) via LineIndex.
local line = src.scan.line_of(atom.body_off + rel) local line = src.scan.line_of(atom.body_off + rel)
-- Flatten newlines + tabs in TEXT to spaces so each WORD entry fits on -- Flatten newlines + tabs in TEXT to spaces so each WORD entry fits on one physical line.
-- one physical line. The gdb Python parser (or our pure-gdb parser) -- The gdb Python parser (or our pure-gdb parser) does line-based splits; multi-line TEXT would break it.
-- does line-based splits; multi-line TEXT would break it.
local text = duffle.trim(tok):gsub("[\t\r\n]+", " ") local text = duffle.trim(tok):gsub("[\t\r\n]+", " ")
for _ = 1, words do for _ = 1, words do
entries[#entries + 1] = { pos = pos, line = line, text = text } entries[#entries + 1] = { pos = pos, line = line, text = text }
@@ -149,16 +147,16 @@ local function compute_word_entries(atom, src, wc)
end end
-- ════════════════════════════════════════════════════════════════════════════ -- ════════════════════════════════════════════════════════════════════════════
-- Provenance emission (Phase 3 — debug_ux) -- Provenance emission
-- ════════════════════════════════════════════════════════════════════════════ -- ════════════════════════════════════════════════════════════════════════════
-- Component-macro invocation prefix (mirrors components.lua's MAC_PREFIX). -- Component-macro invocation prefix (mirrors components.lua's MAC_PREFIX).
local MAC_PREFIX = "mac_" local MAC_PREFIX = "mac_"
local MAC_PREFIX_LEN = 4 local MAC_PREFIX_LEN = 4
--- Strip the `mac_` prefix from a token's leading identifier. Returns nil --- Strip the `mac_` prefix from a token's leading identifier.
--- if the identifier doesn't start with `mac_` (so non-component tokens --- Returns nil if the identifier doesn't start with `mac_`
--- like `load_half_u`, `nop2`, `gte_cmdw_*` fall through cleanly). --- (so non-component tokens like `load_half_u`, `nop2`, `gte_cmdw_*` fall through cleanly).
--- @param tok string --- @param tok string
--- @return string|nil --- @return string|nil
local function strip_mac_prefix_from_token(tok) local function strip_mac_prefix_from_token(tok)
@@ -170,14 +168,13 @@ local function strip_mac_prefix_from_token(tok)
return nil return nil
end end
--- Compute per-word provenance entries for an atom. Mirrors `compute_word_entries` --- Compute per-word provenance entries for an atom. Mirrors `compute_word_entries` but additionally classifies each emitted `.word` as either:
--- but additionally classifies each emitted `.word` as either:
--- - `RAW` — emitted by a direct instruction token (no component provenance) --- - `RAW` — emitted by a direct instruction token (no component provenance)
--- - `MACRO X` — emitted by a `mac_X(...)` component invocation, with the --- - `MACRO X` — emitted by a `mac_X(...)` component invocation,
--- component's definition file:line resolved from `ctx.shared.components`. --- with the component's definition file:line resolved from `ctx.shared.components`.
--- ---
--- Returns a list of `{pos, line, text, comp_name, comp_line, comp_path}` entries + the --- Returns a list of `{pos, line, text, comp_name, comp_line, comp_path}` entries + the total word count.
--- total word count. `comp_name` is nil for RAW rows. --- `comp_name` is nil for RAW rows.
--- @param atom table -- one entry of scan.atoms / scan.raw_atoms --- @param atom table -- one entry of scan.atoms / scan.raw_atoms
--- @param src table -- SourceFile (has .scan with .line_of(), .path) --- @param src table -- SourceFile (has .scan with .line_of(), .path)
--- @param wc table -- shared.word_counts --- @param wc table -- shared.word_counts
@@ -249,13 +246,10 @@ local function emit_provenance_stanza(src, atom, wc, comp)
for _, pe in ipairs(entries) do for _, pe in ipairs(entries) do
if pe.comp_name then if pe.comp_name then
lines[#lines + 1] = string.format( lines[#lines + 1] = string.format('WORD %d CALL %s:%d MACRO %s "%s:%d"',
'WORD %d CALL %s:%d MACRO %s "%s:%d"',
pe.pos, rel_path, pe.line, pe.comp_name, pe.comp_path, pe.comp_line) pe.pos, rel_path, pe.line, pe.comp_name, pe.comp_path, pe.comp_line)
else else
lines[#lines + 1] = string.format( lines[#lines + 1] = string.format("WORD %d CALL %s:%d RAW", pe.pos, rel_path, pe.line)
"WORD %d CALL %s:%d RAW",
pe.pos, rel_path, pe.line)
end end
end end
@@ -276,27 +270,23 @@ local function render_provenance(src, wc, comp)
lines[#lines + 1] = "# auto-generated by ps1_meta.lua (passes/atoms_source_map.lua) — DO NOT EDIT" lines[#lines + 1] = "# auto-generated by ps1_meta.lua (passes/atoms_source_map.lua) — DO NOT EDIT"
lines[#lines + 1] = "# Per-.word provenance: maps each emitted .word to its call site (atom body" lines[#lines + 1] = "# Per-.word provenance: maps each emitted .word to its call site (atom body"
lines[#lines + 1] = "# file:line) and, when the word was emitted by a `mac_X(...)` component invocation," lines[#lines + 1] = "# file:line) and, when the word was emitted by a `mac_X(...)` component invocation,"
lines[#lines + 1] = "# the component's definition file:line. Used by dwarf_injection (Phase 3) to synthesize" lines[#lines + 1] = "# the component's definition file:line. Used by dwarf_injection to synthesize"
lines[#lines + 1] = "# DW_TAG_inlined_subroutine instances for component step-into." lines[#lines + 1] = "# DW_TAG_inlined_subroutine instances for component step-into."
for _, atom in ipairs(src.scan.atoms or {}) do for _, atom in ipairs(src.scan.atoms or {}) do
local stanza = emit_provenance_stanza(src, atom, wc, comp) local stanza = emit_provenance_stanza(src, atom, wc, comp)
for _, line in ipairs(stanza) do for _, line in ipairs(stanza) do lines[#lines + 1] = line end
lines[#lines + 1] = line
end
end end
for _, atom in ipairs(src.scan.raw_atoms or {}) do for _, atom in ipairs(src.scan.raw_atoms or {}) do
local stanza = emit_provenance_stanza(src, atom, wc, comp) local stanza = emit_provenance_stanza(src, atom, wc, comp)
for _, line in ipairs(stanza) do for _, line in ipairs(stanza) do lines[#lines + 1] = line end
lines[#lines + 1] = line
end
end end
return table.concat(lines, "\n") .. "\n" return table.concat(lines, "\n") .. "\n"
end end
--- Render one atom's stanza for the canonical text form --- Render one atom's stanza for the canonical text form (ATOM header line, N WORD lines, ENDATOM marker).
--- (ATOM header line, N WORD lines, ENDATOM marker). Returns (lines, total_words). --- Returns (lines, total_words).
--- @param src table --- @param src table
--- @param atom table --- @param atom table
--- @param wc table --- @param wc table
@@ -308,7 +298,6 @@ local function emit_atom_stanza(src, atom, wc)
-- ATOM header line with placeholder total (patched after we know it). -- ATOM header line with placeholder total (patched after we know it).
lines[#lines + 1] = string.format('ATOM %s "%s" 0', atom.raw_name or atom.name, rel_path) lines[#lines + 1] = string.format('ATOM %s "%s" 0', atom.raw_name or atom.name, rel_path)
for _, we in ipairs(entries) do for _, we in ipairs(entries) do
lines[#lines + 1] = string.format("WORD %d LINE %d TEXT %s", lines[#lines + 1] = string.format("WORD %d LINE %d TEXT %s",
we.pos, we.line, we.text) we.pos, we.line, we.text)
@@ -332,15 +321,11 @@ local function render_source_map(src, wc)
for _, atom in ipairs(src.scan.atoms or {}) do for _, atom in ipairs(src.scan.atoms or {}) do
local stanza = emit_atom_stanza(src, atom, wc) local stanza = emit_atom_stanza(src, atom, wc)
for _, line in ipairs(stanza) do for _, line in ipairs(stanza) do lines[#lines + 1] = line end
lines[#lines + 1] = line
end
end end
for _, atom in ipairs(src.scan.raw_atoms or {}) do for _, atom in ipairs(src.scan.raw_atoms or {}) do
local stanza = emit_atom_stanza(src, atom, wc) local stanza = emit_atom_stanza(src, atom, wc)
for _, line in ipairs(stanza) do for _, line in ipairs(stanza) do lines[#lines + 1] = line end
lines[#lines + 1] = line
end
end end
return table.concat(lines, "\n") .. "\n" return table.concat(lines, "\n") .. "\n"
@@ -431,8 +416,7 @@ local function append_gdb_commands(lines, matched)
for _, a in ipairs(matched) do for _, a in ipairs(matched) do
-- gdb 12.1 quirk: literals in printf args require an attached target. -- gdb 12.1 quirk: literals in printf args require an attached target.
-- Use the per-atom convenience vars set above as printf args. -- Use the per-atom convenience vars set above as printf args.
lines[#lines + 1] = string.format( lines[#lines + 1] = string.format(' printf " code_%%-32s @ 0x%%08x %%4d words\\n", $__atom_name_%d, $__atom_addr_%d, $__atom_words_%d',
' printf " code_%%-32s @ 0x%%08x %%4d words\\n", $__atom_name_%d, $__atom_addr_%d, $__atom_words_%d',
a.idx, a.idx, a.idx) a.idx, a.idx, a.idx)
end end
lines[#lines + 1] = "end" lines[#lines + 1] = "end"
@@ -445,8 +429,7 @@ local function append_gdb_commands(lines, matched)
lines[#lines + 1] = "define break_atom" lines[#lines + 1] = "define break_atom"
lines[#lines + 1] = ' echo "Usage: break_atom_<exact_name> (pick from the list below)"' lines[#lines + 1] = ' echo "Usage: break_atom_<exact_name> (pick from the list below)"'
for _, a in ipairs(matched) do for _, a in ipairs(matched) do
lines[#lines + 1] = string.format( lines[#lines + 1] = string.format(' printf " break_atom_%%-32s\\n", $__atom_name_%d', a.idx)
' printf " break_atom_%%-32s\\n", $__atom_name_%d', a.idx)
end end
lines[#lines + 1] = "end" lines[#lines + 1] = "end"
lines[#lines + 1] = "document break_atom" lines[#lines + 1] = "document break_atom"
@@ -457,8 +440,7 @@ local function append_gdb_commands(lines, matched)
for _, a in ipairs(matched) do for _, a in ipairs(matched) do
lines[#lines + 1] = string.format("define break_atom_%s", a.name) lines[#lines + 1] = string.format("define break_atom_%s", a.name)
lines[#lines + 1] = string.format(" break *$__atom_addr_%d", a.idx) lines[#lines + 1] = string.format(" break *$__atom_addr_%d", a.idx)
lines[#lines + 1] = string.format( lines[#lines + 1] = string.format(' printf " Breakpoint set at code_%s (0x%%08x)\\n", $__atom_addr_%d', a.name, a.idx)
' printf " Breakpoint set at code_%s (0x%%08x)\\n", $__atom_addr_%d', a.name, a.idx)
lines[#lines + 1] = "end" lines[#lines + 1] = "end"
lines[#lines + 1] = string.format("document break_atom_%s", a.name) lines[#lines + 1] = string.format("document break_atom_%s", a.name)
lines[#lines + 1] = string.format(" Set a breakpoint at code_%s.", a.name) lines[#lines + 1] = string.format(" Set a breakpoint at code_%s.", a.name)
@@ -494,33 +476,24 @@ local function append_gdb_commands(lines, matched)
lines[#lines + 1] = " set $__matched = 0" lines[#lines + 1] = " set $__matched = 0"
for _, a in ipairs(matched) do for _, a in ipairs(matched) do
-- Precompute end_addr (gdb 12.1's expression evaluator chokes on `addr + words*4`). -- Precompute end_addr (gdb 12.1's expression evaluator chokes on `addr + words*4`).
lines[#lines + 1] = string.format( lines[#lines + 1] = string.format(" set $__end_%d = $__atom_addr_%d + $__atom_words_%d * 4", a.idx, a.idx, a.idx)
" set $__end_%d = $__atom_addr_%d + $__atom_words_%d * 4", a.idx, a.idx, a.idx) lines[#lines + 1] = string.format(" if $__pc >= $__atom_addr_%d && $__pc < $__end_%d", a.idx, a.idx)
lines[#lines + 1] = string.format( lines[#lines + 1] = string.format(' printf "atom: code_%%s\\n", $__atom_name_%d', a.idx)
" if $__pc >= $__atom_addr_%d && $__pc < $__end_%d", a.idx, a.idx)
lines[#lines + 1] = string.format(
' printf "atom: code_%%s\\n", $__atom_name_%d', a.idx)
lines[#lines + 1] = ' printf "addr: 0x%08x\\n", $__pc' lines[#lines + 1] = ' printf "addr: 0x%08x\\n", $__pc'
lines[#lines + 1] = string.format( lines[#lines + 1] = string.format(" set $__word = ($__pc - $__atom_addr_%d) / 4", a.idx)
" set $__word = ($__pc - $__atom_addr_%d) / 4", a.idx) lines[#lines + 1] = string.format(' printf "word: %%d/%%d\\n", $__word, $__atom_words_%d', a.idx)
lines[#lines + 1] = string.format(
' printf "word: %%d/%%d\\n", $__word, $__atom_words_%d', a.idx)
-- One inner-if per WORD entry. Each word's line + text hardcoded. -- One inner-if per WORD entry. Each word's line + text hardcoded.
for _, we in ipairs(a.entries) do for _, we in ipairs(a.entries) do
lines[#lines + 1] = string.format( lines[#lines + 1] = string.format(" if $__word == %d", we.pos)
" if $__word == %d", we.pos)
-- Escape TEXT for printf format string. -- Escape TEXT for printf format string.
local escaped_text = we.text:gsub("%%", "%%%%"):gsub('"', '\\"') local escaped_text = we.text:gsub("%%", "%%%%"):gsub('"', '\\"')
lines[#lines + 1] = string.format( lines[#lines + 1] = string.format(' printf "source: %%s:%%d %%s\\n", $__atom_file_%d, %d, "%s"', a.idx, we.line, escaped_text)
' printf "source: %%s:%%d %%s\\n", $__atom_file_%d, %d, "%s"',
a.idx, we.line, escaped_text)
lines[#lines + 1] = " end" lines[#lines + 1] = " end"
end end
-- Fallback for words beyond the source map (shouldn't happen if nm matches). -- Fallback for words beyond the source map (shouldn't happen if nm matches).
local max_word = 0 local max_word = 0
if #a.entries > 0 then max_word = a.entries[#a.entries].pos end if #a.entries > 0 then max_word = a.entries[#a.entries].pos end
lines[#lines + 1] = string.format( lines[#lines + 1] = string.format(' if $__word > %d', max_word)
' if $__word > %d', max_word)
lines[#lines + 1] = ' printf "source: (no source-map entry for word %%d; map may be stale)\\n", $__word' lines[#lines + 1] = ' printf "source: (no source-map entry for word %%d; map may be stale)\\n", $__word'
lines[#lines + 1] = " end" lines[#lines + 1] = " end"
lines[#lines + 1] = " set $__matched = 1" lines[#lines + 1] = " set $__matched = 1"
@@ -537,19 +510,16 @@ local function append_gdb_commands(lines, matched)
-- ── stepi_inside_atom ── -- ── stepi_inside_atom ──
-- Hardcoded one if-containment-check per atom (no loop). -- Hardcoded one if-containment-check per atom (no loop).
-- Precompute end_addr in Lua so we don't ask gdb to evaluate `addr + words*4` -- Precompute end_addr in Lua so we don't ask gdb to evaluate `addr + words*4` inside the if condition
-- inside the if condition (gdb 12.1's expression evaluator chokes on the -- (gdb 12.1's expression evaluator chokes on the `*` and emits a misleading 'function malloc' error in some gdb builds).
-- `*` and emits a misleading 'function malloc' error in some gdb builds).
lines[#lines + 1] = "define stepi_inside_atom" lines[#lines + 1] = "define stepi_inside_atom"
lines[#lines + 1] = " set $__in_atom = 0" lines[#lines + 1] = " set $__in_atom = 0"
lines[#lines + 1] = " set $__did_step = 0" lines[#lines + 1] = " set $__did_step = 0"
lines[#lines + 1] = " set $__pc = (unsigned int)$pc" lines[#lines + 1] = " set $__pc = (unsigned int)$pc"
for _, a in ipairs(matched) do for _, a in ipairs(matched) do
-- Precompute end_addr in the convenience var (single expression gdb handles). -- Precompute end_addr in the convenience var (single expression gdb handles).
lines[#lines + 1] = string.format( lines[#lines + 1] = string.format(" set $__end_%d = $__atom_addr_%d + $__atom_words_%d * 4", a.idx, a.idx, a.idx)
" set $__end_%d = $__atom_addr_%d + $__atom_words_%d * 4", a.idx, a.idx, a.idx) lines[#lines + 1] = string.format(" if $__pc >= $__atom_addr_%d && $__pc < $__end_%d", a.idx, a.idx)
lines[#lines + 1] = string.format(
" if $__pc >= $__atom_addr_%d && $__pc < $__end_%d", a.idx, a.idx)
lines[#lines + 1] = " set $__in_atom = 1" lines[#lines + 1] = " set $__in_atom = 1"
lines[#lines + 1] = " stepi" lines[#lines + 1] = " stepi"
lines[#lines + 1] = " set $__did_step = 1" lines[#lines + 1] = " set $__did_step = 1"
@@ -678,13 +648,10 @@ end
local M = {} local M = {}
--- Pass entry: emit one `<out_root>/<basename>.atoms.sourcemap.txt` per source file --- Pass entry: emit one `<out_root>/<basename>.atoms.sourcemap.txt` per source file that contains at least one `MipsAtom_(name)` / `MipsCode code_<name>` declaration.
--- that contains at least one `MipsAtom_(name)` / `MipsCode code_<name>` declaration. --- Also emits `<out_root>/<basename>.atoms.provenance.txt`:
--- Also emits `<out_root>/<basename>.atoms.provenance.txt` (Phase 3 — debug_ux): --- per-.word provenance with `mac_X(...)` component resolution back to the component's definition file:line.
--- per-.word provenance with `mac_X(...)` component resolution back to the --- Optionally also emit `<ctx.out_root>/gdb_tape_atoms_runtime.gdb` when `ctx.flags.gdb_runtime` is true.
--- component's definition file:line.
--- Optionally also emit `<ctx.out_root>/gdb_tape_atoms_runtime.gdb` when
--- `ctx.flags.gdb_runtime` is true.
--- @param ctx PassCtx --- @param ctx PassCtx
--- @return PassResult --- @return PassResult
function M.run(ctx) function M.run(ctx)
@@ -702,10 +669,9 @@ function M.run(ctx)
} }
end end
-- Phase 3 (debug_ux): shared.components map is populated by `passes/components.lua`. -- shared.components map is populated by `passes/components.lua`.
-- Used to attribute each emitted `.word` to either a component macro or the -- Used to attribute each emitted `.word` to either a component macro or the enclosing atom body.
-- enclosing atom body. If absent, all words fall through as RAW (correct -- If absent, all words fall through as RAW (correct behavior — provenance is additive).
-- behavior — provenance is additive).
local comp = (ctx.shared and ctx.shared.components) or {} local comp = (ctx.shared and ctx.shared.components) or {}
-- Always emit the canonical text form (per-source). -- Always emit the canonical text form (per-source).
@@ -720,11 +686,8 @@ function M.run(ctx)
local sourcemap_path = ctx.out_root .. "/" .. basename .. ".atoms.sourcemap.txt" local sourcemap_path = ctx.out_root .. "/" .. basename .. ".atoms.sourcemap.txt"
local sourcemap_body = render_source_map(src, wc) local sourcemap_body = render_source_map(src, wc)
-- (2) atoms.provenance.txt — Phase 3 (debug_ux): per-.word provenance -- (2) atoms.provenance.txt — per-.word provenance with `mac_X(...)` component resolution back to the component's definition file:line.
-- with `mac_X(...)` component resolution back to the component's -- Consumed by `passes/dwarf_injection.lua` to synthesize `DW_TAG_inlined_subroutine` instances for source-level Step Into on component invocations.
-- definition file:line. Consumed by `passes/dwarf_injection.lua`
-- to synthesize `DW_TAG_inlined_subroutine` instances for
-- source-level Step Into on component invocations.
local prov_path = ctx.out_root .. "/" .. basename .. ".atoms.provenance.txt" local prov_path = ctx.out_root .. "/" .. basename .. ".atoms.provenance.txt"
local prov_body = render_provenance(src, wc, comp) local prov_body = render_provenance(src, wc, comp)
+22 -29
View File
@@ -1,12 +1,12 @@
--- passes/components.lua — Component-macro header generator. --- passes/components.lua — Component-macro header generator.
--- ---
--- Reads the pre-scanned SourceScan payload (produced once upstream by `duffle.scan_source`) --- Reads the pre-scanned SourceScan payload (produced once upstream by `duffle.scan_source`)
--- for `MipsAtomComp_(ac_X)` and `MipsAtomComp_Proc_(ac_X, { body })` declarations, then does --- for `MipsAtomComp_(ac_X)` and `MipsAtomComp_Proc_(ac_X, { body })` declarations, then does per-source backward lookups
--- per-source backward lookups for the function-args string (from the preceding `FI_ MipsAtom ac_X(...)` --- for the function-args string (from the preceding `FI_ MipsAtom ac_X(...)` function declaration)
--- function declaration) and the preceding comment block (for LSP/IntelliSense signature docs). --- and the preceding comment block (for LSP/IntelliSense signature docs).
--- ---
--- Emits a per-directory `<dir_basename>.macs.h` containing one `#define mac_X(sig) \` macro per component --- Emits a per-directory `<dir_basename>.macs.h` containing one `#define mac_X(sig) \` macro per component + `WORD_COUNT(mac_X, N)`
--- + `WORD_COUNT(mac_X, N)` entries for downstream offset computation. --- entries for downstream offset computation.
--- ---
--- **Conventions**: tabs (1/level), EmmyLua annotations, no regex, --- **Conventions**: tabs (1/level), EmmyLua annotations, no regex,
--- Lua 5.3 compatible. --- Lua 5.3 compatible.
@@ -26,8 +26,7 @@
-- Bootstrap: same as entry scripts. See `ps1_meta.lua` for the rationale. -- Bootstrap: same as entry scripts. See `ps1_meta.lua` for the rationale.
-- Bootstrap: load `scripts/duffle_paths.lua` (sets package.path + package.cpath). -- Bootstrap: load `scripts/duffle_paths.lua` (sets package.path + package.cpath).
-- Uses `debug.getinfo` to find this file's own directory, so it works -- Uses `debug.getinfo` to find this file's own directory, so it works both standalone and when require'd from the orchestrator.
-- both standalone and when require'd from the orchestrator.
-- Bootstrap: load `duffle_paths.lua` via `debug.getinfo(1, "S").source` (works both standalone + when require'd). -- Bootstrap: load `duffle_paths.lua` via `debug.getinfo(1, "S").source` (works both standalone + when require'd).
-- duffle_paths.lua sets package.path then returns `require("duffle")` at the bottom, so the dofile value IS the duffle module. -- duffle_paths.lua sets package.path then returns `require("duffle")` at the bottom, so the dofile value IS the duffle module.
local _bootstrap_dir = debug.getinfo(1, "S").source:match("^@?(.*[/\\])") or "./" local _bootstrap_dir = debug.getinfo(1, "S").source:match("^@?(.*[/\\])") or "./"
@@ -348,8 +347,7 @@ end
-- Project pre-scanned MipsAtomComp_ / MipsAtomComp_Proc_ entries into Component shape. -- Project pre-scanned MipsAtomComp_ / MipsAtomComp_Proc_ entries into Component shape.
-- Does per-source backward lookups for args (preceding function decl) and comment (preceding comment block). -- Does per-source backward lookups for args (preceding function decl) and comment (preceding comment block).
-- Carries `body_tokens` forward from scan-source so word_count_rec reads from the precomputed table -- Carries `body_tokens` forward from scan-source so word_count_rec reads from the precomputed table instead of calling duffle.tokenize_body again.
-- instead of calling duffle.tokenize_body again.
-- @param source string -- the full source text (needed for backward lookups) -- @param source string -- the full source text (needed for backward lookups)
-- @param scan table -- SourceScan from duffle.scan_source -- @param scan table -- SourceScan from duffle.scan_source
-- @return Component[] -- @return Component[]
@@ -366,7 +364,7 @@ local function project_components(source, scan)
body_tokens = a.body_tokens, body_tokens = a.body_tokens,
args = args, args = args,
comment = comment, comment = comment,
kind = a.kind, -- "comp_bare" | "comp_proc"; Phase 3 provenance emitter reads this. kind = a.kind, -- "comp_bare" | "comp_proc"; provenance emitter reads this.
} }
end end
end end
@@ -474,10 +472,9 @@ end
--- Compute word counts for every component in `components` in a single pass. --- Compute word counts for every component in `components` in a single pass.
--- The name-lookup table + memoization cache are built ONCE (per source) instead of per-component, --- The name-lookup table + memoization cache are built ONCE (per source) instead of per-component,
--- so the cache survives across siblings and a component's recursive `mac_Y(...)` references hit memoized values --- so the cache survives across siblings and a component's recursive `mac_Y(...)`
--- instead of re-walking the body. --- references hit memoized values instead of re-walking the body.
--- Cycle detection (A -> B -> A) is preserved via the in-progress `-1` sentinel in `cache`. --- Cycle detection (A -> B -> A) is preserved via the in-progress `-1` sentinel in `cache`.
---
--- @param components Component[] --- @param components Component[]
--- @param wc table<string, integer> --- @param wc table<string, integer>
--- @return table<string, integer> -- map of component name (without `mac_`) -> word count --- @return table<string, integer> -- map of component name (without `mac_`) -> word count
@@ -608,8 +605,8 @@ local function header_boilerplate(src)
"// Component atoms (MipsAtomComp_(ac_*)) -> macro variants (mac_*)", "// Component atoms (MipsAtomComp_(ac_*)) -> macro variants (mac_*)",
"", "",
-- Self-contained: define WORD_COUNT if not already defined. -- Self-contained: define WORD_COUNT if not already defined.
-- We use the same definition here so the auto-generated entries below expand to compile-time constants whether -- We use the same definition here so the auto-generated entries below expand
-- the metadata file is included first or not. -- to compile-time constants whether the metadata file is included first or not.
"#ifndef WORD_COUNT", "#ifndef WORD_COUNT",
"#define WORD_COUNT(name, count) enum { words_##name = (count) };", "#define WORD_COUNT(name, count) enum { words_##name = (count) };",
"#endif", "#endif",
@@ -633,7 +630,6 @@ end
--- Emit a per-source `.macs.h` header with the `mac_X` macros + `WORD_COUNT` entries. --- Emit a per-source `.macs.h` header with the `mac_X` macros + `WORD_COUNT` entries.
--- Writes in BINARY mode so LF line endings are preserved (the git blob is LF; Windows text-mode would emit CRLF and break the byte-identical diff). --- Writes in BINARY mode so LF line endings are preserved (the git blob is LF; Windows text-mode would emit CRLF and break the byte-identical diff).
--- Honors `ctx.dry_run`: prints the intended path but does not write the file. --- Honors `ctx.dry_run`: prints the intended path but does not write the file.
---
--- @param ctx PassCtx --- @param ctx PassCtx
--- @param src SourceFile --- @param src SourceFile
--- @param components Component[] --- @param components Component[]
@@ -666,8 +662,7 @@ end
-- Pass entry -- Pass entry
-- ════════════════════════════════════════════════════════════════════════════ -- ════════════════════════════════════════════════════════════════════════════
-- (internal) Extend `ctx.shared.word_counts` with this source's component macros -- (internal) Extend `ctx.shared.word_counts` with this source's component macros so offsets sees them without re-reading the file.
-- so offsets sees them without re-reading the file.
-- @param ctx PassCtx -- @param ctx PassCtx
-- @param components Component[] -- @param components Component[]
-- @param counts table<string, integer> -- precomputed word counts (from count_all_components) -- @param counts table<string, integer> -- precomputed word counts (from count_all_components)
@@ -684,11 +679,9 @@ end
--- @field path string -- absolute source path of the definition --- @field path string -- absolute source path of the definition
--- @field kind string -- "comp_bare" | "comp_proc" --- @field kind string -- "comp_bare" | "comp_proc"
--- (internal) Extend `ctx.shared.components` with this source's components-by-name --- (internal) Extend `ctx.shared.components` with this source's components-by-name map so downstream passes
-- map so downstream passes (atoms_source_map, dwarf_injection) can resolve --- (atoms_source_map, dwarf_injection) can resolve `mac_X(...)` invocations back to their component definition file:line.
-- `mac_X(...)` invocations back to their component definition file:line. --- provenance emission uses this to attribute each emitted `.word` to either a component macro or the enclosing atom body.
--- Phase 3 (debug_ux) provenance emission uses this to attribute each emitted
-- `.word` to either a component macro or the enclosing atom body.
-- @param ctx PassCtx -- @param ctx PassCtx
-- @param src SourceFile -- @param src SourceFile
-- @param components Component[] -- @param components Component[]
@@ -696,8 +689,8 @@ local function update_shared_components(ctx, src, components)
ctx.shared.components = ctx.shared.components or {} ctx.shared.components = ctx.shared.components or {}
local rel_path = src.path:gsub("\\", "/") local rel_path = src.path:gsub("\\", "/")
for _, c in ipairs(components) do for _, c in ipairs(components) do
-- Keyed by bare name (e.g. `yield`, `load_tri_indices`). The atoms_source_map -- Keyed by bare name (e.g. `yield`, `load_tri_indices`).
-- pass strips the `mac_` prefix from the call site identifier before lookup. -- The atoms_source_map pass strips the `mac_` prefix from the call site identifier before lookup.
ctx.shared.components[c.name] = { ctx.shared.components[c.name] = {
name = c.name, name = c.name,
line = c.line, line = c.line,
@@ -714,9 +707,9 @@ function M.run(ctx)
local errors = {} local errors = {}
local warnings = {} local warnings = {}
-- Initialize shared component map (Phase 3 — debug_ux). The atoms_source_map -- Initialize shared component map.
-- and dwarf_injection passes consume `ctx.shared.components` to resolve -- The atoms_source_map and dwarf_injection passes consume `ctx.shared.components` to resolve `mac_X(...)`
-- `mac_X(...)` invocations back to the component's definition file:line. -- invocations back to the component's definition file:line.
ctx.shared.components = ctx.shared.components or {} ctx.shared.components = ctx.shared.components or {}
for _, src in ipairs(ctx.sources) do for _, src in ipairs(ctx.sources) do
@@ -729,7 +722,7 @@ function M.run(ctx)
if macs_path then if macs_path then
outputs[#outputs + 1] = { macs_h = macs_path } outputs[#outputs + 1] = { macs_h = macs_path }
update_shared_word_counts(ctx, components, counts) update_shared_word_counts(ctx, components, counts)
-- Phase 3 (debug_ux): share component definitions with downstream passes. -- share component definitions with downstream passes.
-- `mac_X(...)` invocations in atom bodies resolve back to (path, line) via this map. -- `mac_X(...)` invocations in atom bodies resolve back to (path, line) via this map.
update_shared_components(ctx, src, components) update_shared_components(ctx, src, components)
end end
+184 -271
View File
@@ -78,8 +78,7 @@ local DW_RLE_start_length = DWARF5_RNGLISTS.start_length
-- The injector extends that unit rather than appending an unreferenced unit. -- The injector extends that unit rather than appending an unreferenced unit.
local ATOM_SOURCE_FILE_INDEX = 11 local ATOM_SOURCE_FILE_INDEX = 11
-- Wave-context registers (per duffle.lua :: WAVE_CONTEXT_REGS). The MIPS -- Wave-context registers (per duffle.lua :: WAVE_CONTEXT_REGS). The MIPS register number is what DW_OP_regN uses as N.
-- register number is what DW_OP_regN uses as N.
-- --
-- R_PrimCursor = R_T7 = $15 → DW_OP_reg15 -- R_PrimCursor = R_T7 = $15 → DW_OP_reg15
-- R_FaceCursor = R_T4 = $12 → DW_OP_reg12 -- R_FaceCursor = R_T4 = $12 → DW_OP_reg12
@@ -110,17 +109,15 @@ local WAVE_CONTEXT_LOCATIONS = {
local ABBREV_CU = 0x64 -- 100: DW_TAG_compile_unit local ABBREV_CU = 0x64 -- 100: DW_TAG_compile_unit
local ABBREV_SUBPROGRAM = 0x65 -- 101: DW_TAG_subprogram local ABBREV_SUBPROGRAM = 0x65 -- 101: DW_TAG_subprogram
local ABBREV_VARIABLE = 0x66 -- 102: DW_TAG_variable (DW_AT_type = ref4 to U4; was missing pre-2026-07-13 → gdb resolved R_PrimCursor against the C-level enum, not the register) local ABBREV_VARIABLE = 0x66 -- 102: DW_TAG_variable (DW_AT_type = ref4 to U4; was missing pre-2026-07-13 → gdb resolved R_PrimCursor against the C-level enum, not the register)
-- Task 8 (rbind composite via DW_OP_piece): 4 new abbreviations.
local ABBREV_STRUCT_TYPE = 0x67 -- 103: DW_TAG_structure_type with children (Binds_X mirror) local ABBREV_STRUCT_TYPE = 0x67 -- 103: DW_TAG_structure_type with children (Binds_X mirror)
local ABBREV_MEMBER = 0x68 -- 104: DW_TAG_member no children (DW_AT_type = ref4 to U4 base) local ABBREV_MEMBER = 0x68 -- 104: DW_TAG_member no children (DW_AT_type = ref4 to U4 base)
local ABBREV_BIND_VAR = 0x69 -- 105: DW_TAG_variable no children + DW_AT_type = ref4 (the bind_args variable) local ABBREV_BIND_VAR = 0x69 -- 105: DW_TAG_variable no children + DW_AT_type = ref4 (the bind_args variable)
local ABBREV_BASE_TYPE = 0x6A -- 106: DW_TAG_base_type no children (U4) local ABBREV_BASE_TYPE = 0x6A -- 106: DW_TAG_base_type no children (U4)
-- Phase 3 (debug_ux): component step-into (DW_TAG_inlined_subroutine + abstract DW_TAG_subprogram). -- Component step-into (DW_TAG_inlined_subroutine + abstract DW_TAG_subprogram).
local ABBREV_ABSTRACT_SUBPROGRAM = 0x6B -- 107: DW_TAG_subprogram (abstract — no low_pc/high_pc); for each unique mac_X component local ABBREV_ABSTRACT_SUBPROGRAM = 0x6B -- 107: DW_TAG_subprogram (abstract — no low_pc/high_pc); for each unique mac_X component
local ABBREV_INLINED_SUBROUTINE = 0x6C -- 108: DW_TAG_inlined_subroutine with children (per-component invocation range) local ABBREV_INLINED_SUBROUTINE = 0x6C -- 108: DW_TAG_inlined_subroutine with children (per-component invocation range)
-- Phase 5 (debug_ux): bind_args uses DW_FORM_sec_offset → .debug_loclists for -- Bind_args uses DW_FORM_sec_offset → .debug_loclists for PC-ranged liveness
-- PC-ranged liveness (each field transitions from tape memory to GPR at -- (each field transitions from tape memory to GPR at load_pc + 8 = MIPS I load-delay slot boundary).
-- load_pc + 8 = MIPS I load-delay slot boundary).
local ABBREV_BIND_VAR_LOCLIST = 0x6D -- 109: DW_TAG_variable no children + DW_AT_type = ref4 + DW_AT_location = sec_offset local ABBREV_BIND_VAR_LOCLIST = 0x6D -- 109: DW_TAG_variable no children + DW_AT_type = ref4 + DW_AT_location = sec_offset
-- DWARF5 §7.7.3 loclist opcodes. -- DWARF5 §7.7.3 loclist opcodes.
@@ -129,30 +126,26 @@ local DW_LLE_start_length = 0x06
local DW_OP_reg0 = 0x50 -- base reg op; regN = 0x50 + N local DW_OP_reg0 = 0x50 -- base reg op; regN = 0x50 + N
local DW_OP_breg0 = 0x70 -- base breg op; bregN = 0x70 + N (SLEB offset) local DW_OP_breg0 = 0x70 -- base breg op; bregN = 0x70 + N (SLEB offset)
local MIPS_LOAD_DELAY_BYTES = 0x08 -- 1 load word + 1 BD-slot word local MIPS_LOAD_DELAY_BYTES = 0x08 -- 1 load word + 1 BD-slot word
-- Late-binding table for forward references. The loclist functions -- Late-binding table for forward references.
-- (defined below) use uleb128 + elf_dwarf which are not yet defined -- The loclist functions (defined below) use uleb128 + elf_dwarf which are not yet defined at this point.
-- at this point. They look them up via this table at call time. -- They look them up via this table at call time.
local _late = { uleb128 = nil, elf_dwarf = nil } local _late = { uleb128 = nil, elf_dwarf = nil }
-- Build the .debug_loclists section for every rbind atom. Returns the -- Build the .debug_loclists section for every rbind atom.
-- section bytes (DWARF5 layout: unit_length(4) + version=5(2) + address_size=4(1) -- Returns the section bytes (DWARF5 layout: unit_length(4) + version=5(2) + address_size=4(1) + segment_size=0(1) + offset_entry_count=0(4) + DW_LLE entries;
-- + segment_size=0(1) + offset_entry_count=0(4) + DW_LLE entries; each atom -- each atom contributes one loclist terminated by DW_LLE_end_of_list).
-- contributes one loclist terminated by DW_LLE_end_of_list).
-- --
-- Per rbind atom we emit 2 loclist entries (tape → GPR split) using the -- Per rbind atom we emit 2 loclist entries (tape → GPR split) using the last field's transition as the boundary.
-- last field's transition as the boundary. This is a conservative approximation -- This is a conservative approximation that always reads the correct GPR value once the first load has retired (load_pc + 8).
-- that always reads the correct GPR value once the first load has retired -- A future task can refine to per-field transitions.
-- (load_pc + 8). A future task can refine to per-field transitions.
-- @param atom_table table[] -- list of atoms with .rbind set -- @param atom_table table[] -- list of atoms with .rbind set
-- @return string -- section bytes -- @return string -- section bytes
local function build_debug_loclists_section(atom_table) local function build_debug_loclists_section(atom_table)
-- Loclist header (per DWARF5 §7.7.3): -- Loclist header (per DWARF5 §7.7.3):
-- unit_length(4) + version(2) + address_size(1) + segment_size(1) + offset_entry_count(4) -- unit_length(4) + version(2) + address_size(1) + segment_size(1) + offset_entry_count(4)
-- All our entries are DW_LLE_start_length + addr(4) + length(ULEB) + expression -- All our entries are DW_LLE_start_length + addr(4) + length(ULEB) + expression terminated by DW_LLE_end_of_list (1 byte).
-- terminated by DW_LLE_end_of_list (1 byte).
-- R_TapePtr is register 24 ($t8). R_<reg> maps via R_NAME_TO_INDEX above. -- R_TapePtr is register 24 ($t8). R_<reg> maps via R_NAME_TO_INDEX above.
-- We compute the field-by-field transition: at each load_pc + 8, one more -- We compute the field-by-field transition: at each load_pc + 8, one more field has committed from tape memory to its destination GPR.
-- field has committed from tape memory to its destination GPR.
local tape_reg = R_NAME_TO_INDEX["R_TapePtr"] -- = 24 local tape_reg = R_NAME_TO_INDEX["R_TapePtr"] -- = 24
if not tape_reg then tape_reg = 24 end -- fallback if R_TapePtr isn't in the table if not tape_reg then tape_reg = 24 end -- fallback if R_TapePtr isn't in the table
local parts = {} local parts = {}
@@ -206,12 +199,11 @@ local function build_debug_loclists_section(atom_table)
return header .. body return header .. body
end end
-- Compute the per-atom loclist offset within a .debug_loclists section that -- Compute the per-atom loclist offset within a .debug_loclists section that the caller will later pass to .debug_info
-- the caller will later pass to .debug_info (DW_FORM_sec_offset is a 4-byte -- (DW_FORM_sec_offset is a 4-byte section-relative offset).
-- section-relative offset). Returns a table {[atom_name] = offset_in_loclists}. -- Returns a table {[atom_name] = offset_in_loclists}.
-- The header is 12 bytes (unit_length(4) + version(2) + address_size(1) + -- The header is 12 bytes (unit_length(4) + version(2) + address_size(1) + segment_size(1) + offset_entry_count(4));
-- segment_size(1) + offset_entry_count(4)); each loclist body is the sum of -- each loclist body is the sum of its DW_LLE entries (computed by replaying the same emission).
-- its DW_LLE entries (computed by replaying the same emission).
-- @param atom_table table[] -- list of atoms with .rbind set -- @param atom_table table[] -- list of atoms with .rbind set
-- @return table -- {[atom_name] = offset_in_section} -- @return table -- {[atom_name] = offset_in_section}
local function compute_loclists_offsets(atom_table) local function compute_loclists_offsets(atom_table)
@@ -223,9 +215,9 @@ local function compute_loclists_offsets(atom_table)
offsets[atom.name] = cursor offsets[atom.name] = cursor
-- Each loclist body: 2 DW_LLE_start_length + 1 DW_LLE_end_of_list. -- Each loclist body: 2 DW_LLE_start_length + 1 DW_LLE_end_of_list.
-- Per DW_LLE_start_length: 1 (opcode) + 4 (addr) + ULEB(length) + length bytes. -- Per DW_LLE_start_length: 1 (opcode) + 4 (addr) + ULEB(length) + length bytes.
-- The length bytes are N pieces × (1 reg op + 1 piece op + 1 ULEB4) -- The length bytes are N pieces × (1 reg op + 1 piece op + 1 ULEB4) = 3N bytes for one-piece blocks
-- = 3N bytes for one-piece blocks (tape: reg + SLEB + piece+4; -- (tape: reg + SLEB + piece+4; GPR: reg + piece+4).
-- GPR: reg + piece+4). For the all-tape form: N × (1 + 1 + 1) = 3N. -- For the all-tape form: N × (1 + 1 + 1) = 3N.
-- We compute the exact length for the current build. -- We compute the exact length for the current build.
local n_fields = #atom.rbind.fields local n_fields = #atom.rbind.fields
-- tape_expr: for each field, 1 (breg) + 1..4 (SLEB offset) + 1 (piece) + 1 (ULEB 4) -- tape_expr: for each field, 1 (breg) + 1..4 (SLEB offset) + 1 (piece) + 1 (ULEB 4)
@@ -245,15 +237,14 @@ end
-- DWARF3/4/5 opcodes / attributes / forms (for the .debug_info synth). -- DWARF3/4/5 opcodes / attributes / forms (for the .debug_info synth).
-- DW_TAG values are stable across DWARF3-5 per the standard's Table 7.1; -- DW_TAG values are stable across DWARF3-5 per the standard's Table 7.1;
-- gcc emits DW_TAG_structure_type=0x13 + DW_TAG_base_type=0x24 even in -- gcc emits DW_TAG_structure_type=0x13 + DW_TAG_base_type=0x24 even in DWARF5-versioned CUs, so we match those exact byte values.
-- DWARF5-versioned CUs, so we match those exact byte values.
local DW_TAG_compile_unit = 0x11 local DW_TAG_compile_unit = 0x11
local DW_TAG_subprogram = 0x2E local DW_TAG_subprogram = 0x2E
local DW_TAG_variable = 0x34 local DW_TAG_variable = 0x34
local DW_TAG_structure_type = 0x13 local DW_TAG_structure_type = 0x13
local DW_TAG_member = 0x0D local DW_TAG_member = 0x0D
local DW_TAG_base_type = 0x24 local DW_TAG_base_type = 0x24
-- Phase 3 (debug_ux): component step-into. -- Component step-into.
local DW_TAG_inlined_subroutine = 0x1D local DW_TAG_inlined_subroutine = 0x1D
local DW_AT_name = 0x03 local DW_AT_name = 0x03
@@ -268,41 +259,32 @@ local DW_AT_data_member_location = 0x38
local DW_AT_type = 0x49 local DW_AT_type = 0x49
local DW_AT_linkage_name = 0x6E -- DWARF5 §7.7.1: DW_AT_linkage_name (standard form; 0x200027 was the GNU extension form - wrong vs DW_FORM_string abbrev) local DW_AT_linkage_name = 0x6E -- DWARF5 §7.7.1: DW_AT_linkage_name (standard form; 0x200027 was the GNU extension form - wrong vs DW_FORM_string abbrev)
local DW_AT_external = 0x3F -- marks a variable/function as externally visible local DW_AT_external = 0x3F -- marks a variable/function as externally visible
-- Phase 3 (debug_ux): inlined_subroutine + abstract_origin attributes. -- Inlined_subroutine + abstract_origin attributes.
local DW_AT_abstract_origin = 0x31 local DW_AT_abstract_origin = 0x31
local DW_AT_call_file = 0x58 local DW_AT_call_file = 0x58
local DW_AT_call_line = 0x59 local DW_AT_call_line = 0x59
local DW_AT_inline = 0x20 -- DWARF5 §7.7.1: DW_AT_inline (used by abstract subprogram for the mac_X() components) local DW_AT_inline = 0x20 -- DWARF5 §7.7.1: DW_AT_inline (used by abstract subprogram for the mac_X() components)
-- Phase 3 Task 3 (debug_ux): decl_file + decl_line on the abstract subprogram so consumers -- decl_file + decl_line on the abstract subprogram so consumers can resolve an abstract origin back to its definition site even when no inlined_subroutine instance currently maps to it.
-- can resolve an abstract origin back to its definition site even when no
-- inlined_subroutine instance currently maps to it.
local DW_AT_decl_file = 0x3A -- DWARF5 §7.7.1: DW_AT_decl_file (1-based file index into the CU's file table) local DW_AT_decl_file = 0x3A -- DWARF5 §7.7.1: DW_AT_decl_file (1-based file index into the CU's file table)
local DW_AT_decl_line = 0x3B -- DWARF5 §7.7.1: DW_AT_decl_line local DW_AT_decl_line = 0x3B -- DWARF5 §7.7.1: DW_AT_decl_line
-- File index lookup table for the existing main line unit (Unit 2). -- File index lookup table for the existing main line unit (Unit 2).
-- Provenance paths come back with mixed slashes; we normalize to basename -- Provenance paths come back with mixed slashes; we normalize to basename and look up against the line unit's actual file table.
-- and look up against the line unit's actual file table. The current Phase 3 -- Current scope has two provenance basenames: hello_gte_tape.c (the atom's call site) and lottes_tape.h (the component definition).
-- scope has two provenance basenames: hello_gte_tape.c (the atom's call site) -- Both live in the existing gcc-generated line unit;
-- and lottes_tape.h (the component definition). Both live in the existing -- their 1-based indices are stable across rebuilds because the include order in code/gte_hello/hello_gte.c determines the unit's file table.
-- gcc-generated line unit; their 1-based indices are stable across rebuilds
-- because the include order in code/gte_hello/hello_gte.c determines the
-- unit's file table.
local PROVENANCE_BASENAME_TO_FILE_INDEX = { local PROVENANCE_BASENAME_TO_FILE_INDEX = {
["hello_gte_tape.c"] = ATOM_SOURCE_FILE_INDEX, -- = 11 ["hello_gte_tape.c"] = ATOM_SOURCE_FILE_INDEX, -- = 11
["lottes_tape.h"] = 4, ["lottes_tape.h"] = 4,
} }
--- Resolve an absolute provenance path to the line-unit file index used by the --- Resolve an absolute provenance path to the line-unit file index used by the emitting line program.
--- emitting line program. Normalizes mixed `/` and `\` separators to a basename --- Normalizes mixed `/` and `\` separators to a basename and looks it up against the known file table.
--- and looks it up against the known Phase 3 file table.
--- ---
--- Fails loudly on an unknown provenance basename: adding a new component --- Fails loudly on an unknown provenance basename: adding a new component source file requires extending
--- source file requires extending `PROVENANCE_BASENAME_TO_FILE_INDEX` so the --- `PROVENANCE_BASENAME_TO_FILE_INDEX` so the line-program emission contract stays explicit.
--- line-program emission contract stays explicit. Silent fallback to --- Silent fallback to ATOM_SOURCE_FILE_INDEX would mask the new-file case by misattributing component rows to the atom's source file.
--- ATOM_SOURCE_FILE_INDEX would mask the new-file case by misattributing --- @param path string -- absolute provenance path (e.g. "C:/.../lottes_thttps://www.youtube.com/watch?v=ORM4yLkdKx8ape.h" or "C:\\...\\lottes_tape.h")
--- component rows to the atom's source file.
---
--- @param path string -- absolute provenance path (e.g. "C:/.../lottes_tape.h" or "C:\\...\\lottes_tape.h")
--- @return integer -- 1-based line-unit file index --- @return integer -- 1-based line-unit file index
local function resolve_provenance_file_index(path) local function resolve_provenance_file_index(path)
if path == nil or path == "" then if path == nil or path == "" then
@@ -341,12 +323,11 @@ local DW_OP_piece = 0x93 -- followed by ULEB128 byte count (per DWA
local DW_ATE_unsigned = 0x07 -- DWARF5 §7.8.1: DW_ATE_unsigned (used for U4 base type) local DW_ATE_unsigned = 0x07 -- DWARF5 §7.8.1: DW_ATE_unsigned (used for U4 base type)
local DW_LANG_C99 = 0x0C -- = 12 (C99); use as a safe "C-like" placeholder local DW_LANG_C99 = 0x0C -- = 12 (C99); use as a safe "C-like" placeholder
-- (DW_LANG_Mips_Assembler = 0x8001 was used in the F' Task 5 test, but we want -- (DW_LANG_Mips_Assembler = 0x8001 was used in the F', but we want this CU to look like a C TU so VSCode's Variables pane treats it as code.)
-- this CU to look like a C TU so VSCode's Variables pane treats it as code.)
-- R_ alias → MIPS register number. -- R_ alias → MIPS register number.
-- Subset covering every R_ name used in the source atoms. -- Subset covering every R_ name used in the source atoms.
-- Used by Task 8 to build the piece-chain location for bind_args (which field comes from which reg). -- Used to build the piece-chain location for bind_args (which field comes from which reg).
-- The wave-context entries match WAVE_CONTEXT_LOCATIONS above; the rest are the standard MIPS C-ABI register names. -- The wave-context entries match WAVE_CONTEXT_LOCATIONS above; the rest are the standard MIPS C-ABI register names.
local R_NAME_TO_INDEX = { local R_NAME_TO_INDEX = {
["R_0"] = 0, ["R_AT"] = 1, ["R_V0"] = 2, ["R_V1"] = 3, ["R_0"] = 0, ["R_AT"] = 1, ["R_V0"] = 2, ["R_V1"] = 3,
@@ -361,15 +342,13 @@ local R_NAME_TO_INDEX = {
["R_TapePtr"] = 24, ["R_AtomJmp"] = 25, ["R_TapePtr"] = 24, ["R_AtomJmp"] = 25,
} }
-- Build the .debug_loclists section for every rbind atom. Returns the -- Build the .debug_loclists section for every rbind atom.
-- section bytes (DWARF5 layout: unit_length(4) + version=5(2) + address_size=4(1) -- Returns the section bytes (DWARF5 layout: unit_length(4) + version=5(2) + address_size=4(1)
-- + segment_size=0(1) + offset_entry_count=0(4) + DW_LLE entries; each atom -- + segment_size=0(1) + offset_entry_count=0(4) + DW_LLE entries; each atom contributes one loclist terminated by DW_LLE_end_of_list).
-- contributes one loclist terminated by DW_LLE_end_of_list).
-- --
-- Per rbind atom we emit 2 loclist entries (tape → GPR split) using the -- Per rbind atom we emit 2 loclist entries (tape → GPR split) using the last field's transition as the boundary.
-- last field's transition as the boundary. This is a conservative approximation -- This is a conservative approximation that always reads the correct GPR value once the first load has retired (load_pc + 8).
-- that always reads the correct GPR value once the first load has retired -- A future task can refine to per-field transitions.
-- (load_pc + 8). A future task can refine to per-field transitions.
-- @param atom_table table[] -- list of atoms with .rbind set -- @param atom_table table[] -- list of atoms with .rbind set
-- @return string -- section bytes -- @return string -- section bytes
local function build_debug_loclists_section(atom_table) local function build_debug_loclists_section(atom_table)
@@ -459,8 +438,7 @@ local DEFAULT_BASENAME = "hello_gte"
--- @field basename string -- input ELF basename (default "hello_gte") --- @field basename string -- input ELF basename (default "hello_gte")
--- Normalize a source path for GDB linespecs and component-association keys. --- Normalize a source path for GDB linespecs and component-association keys.
--- GDB accepts forward slashes on Windows; absolute paths avoid cwd-dependent --- GDB accepts forward slashes on Windows; absolute paths avoid cwd-dependent sidecars and match provenance contract.
--- sidecars and match the Phase-3 provenance contract.
--- @param path string --- @param path string
--- @return string --- @return string
local function normalize_debug_path(path) local function normalize_debug_path(path)
@@ -475,11 +453,9 @@ local function component_skip_key(path, component_name)
return normalize_debug_path(path):lower() .. "\0" .. component_name return normalize_debug_path(path):lower() .. "\0" .. component_name
end end
--- Consume the per-source scanner associations without naming any atom or --- Consume the per-source scanner associations without naming any atom or component in production.
--- component in production. Whole atoms remain symbol-keyed; components are --- Whole atoms remain symbol-keyed; components are file-qualified internally so a source marker associates
--- file-qualified internally so a source marker associates with its exact --- with its exact component definition even though GDB 12 requires function-only skip entries for the resulting synthetic inline frame.
--- component definition even though GDB 12 requires function-only skip entries
--- for the resulting synthetic inline frame.
--- @param ctx DwarfInjectionCtx --- @param ctx DwarfInjectionCtx
--- @return table -- {atoms = {[symbol] = association}, components = {[file|name] = association}} --- @return table -- {atoms = {[symbol] = association}, components = {[file|name] = association}}
local function collect_skip_over(ctx) local function collect_skip_over(ctx)
@@ -508,10 +484,9 @@ local function collect_skip_over(ctx)
end end
--- Render deterministic debugger skip commands. Ordering is stable by category: --- Render deterministic debugger skip commands. Ordering is stable by category:
--- exact atom symbols first (lexicographic), then exact component function --- exact atom symbols first (lexicographic), then exact component function names (lexicographic full command).
--- names (lexicographic full command). Atom commands come from the matched --- Atom commands come from the matched nm/source-map table so the emitted name is the actual ELF symbol.
--- nm/source-map table so the emitted name is the actual ELF symbol. The --- The scanner tables and command set both deduplicate repeated source observations.
--- scanner tables and command set both deduplicate repeated source observations.
--- @param skip_over table --- @param skip_over table
--- @param atom_table table[] -- nm/source-map cross-reference; names are actual ELF symbols --- @param atom_table table[] -- nm/source-map cross-reference; names are actual ELF symbols
--- @return string --- @return string
@@ -567,33 +542,27 @@ local sleb128 = elf_dwarf.sleb128
--- [entry emission at new PC] --- [entry emission at new PC]
--- DW_LNE_end_sequence --- DW_LNE_end_sequence
--- ---
--- Each "entry emission" emits one or more rows at the same PC, tracking --- Each "entry emission" emits one or more rows at the same PC, tracking the source state {file_idx, line}.
--- the source state {file_idx, line}. State transitions emit DW_LNS_set_file --- State transitions emit DW_LNS_set_file + DW_LNS_advance_line as needed;
--- + DW_LNS_advance_line as needed; same-state transitions emit only the --- same-state transitions emit only the row (copy).
--- row (copy).
--- ---
--- Phase 3 (debug_ux) entry rules: --- Entry rules:
--- * RAW entry (no invocation): emit (call_file, entry.line) at this PC. --- * RAW entry (no invocation): emit (call_file, entry.line) at this PC.
--- * Invocation entry, NOT first word: emit (comp_file, comp_line). --- * Invocation entry, NOT first word: emit (comp_file, comp_line).
--- * Invocation entry, IS first word: emit TWO rows at the same PC in --- * Invocation entry, IS first word: emit TWO rows at the same PC in order: (call_file, inv.call_line), then (comp_file, inv.comp_line).
--- order: (call_file, inv.call_line), then (comp_file, inv.comp_line).
--- ---
--- Phase 3 Task 3 (debug_ux) atom-entry rules: --- atom-entry rules:
--- * Atom entry 1 also gets a duplicate copy_op for the GDB 12 --- * Atom entry 1 also gets a duplicate copy_op for the GDB 12 zero-instruction-prologue marker (no advance_pc — same PC).
--- zero-instruction-prologue marker (no advance_pc — same PC).
--- ---
--- Wire format reminder (DWARF5 §6.2.5): --- Wire format reminder (DWARF5 §6.2.5):
--- - Standard opcodes: 1 byte opcode + payload (per opcode length). --- - Standard opcodes: 1 byte opcode + payload (per opcode length).
--- - Extended opcode marker byte = 0. --- - Extended opcode marker byte = 0.
--- - Extended opcodes: marker byte + ULEB128 size + sub_opcode + payload. --- - Extended opcodes: marker byte + ULEB128 size + sub_opcode + payload.
--- ---
--- Phase 4 (debug_ux) statement-state rules: --- Statement-state rules:
--- * A marked whole atom emits one opaque is_stmt=false range row and no --- * A marked whole atom emits one opaque is_stmt=false range row and no nested component rows; its subprogram symbol/range remains available.
--- nested component rows; its subprogram symbol/range remains available. --- * A marked component keeps its first-word call-site row true, toggles only the definition row/range false, and restores before the next unmarked row.
--- * A marked component keeps its first-word call-site row true, toggles only
--- the definition row/range false, and restores before the next unmarked row.
--- * Whole-atom suppression wins over component markers; no nested inversion. --- * Whole-atom suppression wins over component markers; no nested inversion.
---
--- @param atom table -- {name, addr, size_bytes, words, entries, invocations?, skip_over?} --- @param atom table -- {name, addr, size_bytes, words, entries, invocations?, skip_over?}
--- @return string --- @return string
local function build_atom_sequence(atom) local function build_atom_sequence(atom)
@@ -618,12 +587,11 @@ local function build_atom_sequence(atom)
if not atom.entries or #atom.entries == 0 then return set_address(atom.addr) .. end_sequence() end if not atom.entries or #atom.entries == 0 then return set_address(atom.addr) .. end_sequence() end
-- A jump-threaded atom is reached by `jr`, not a C call. GDB therefore -- A jump-threaded atom is reached by `jr`, not a C call.
-- cannot apply the parent `skip function` entry before descending into a -- GDB therefore cannot apply the parent `skip function` entry before descending into a visible child inline frame.
-- visible child inline frame. Make an explicitly marked atom DWARF-opaque: -- Make an explicitly marked atom DWARF-opaque:
-- one non-statement row covers its complete address range, with no per-word -- one non-statement row covers its complete address range, with no per-word or component source transitions.
-- or component source transitions. The atom's subprogram DIE remains, so -- The atom's subprogram DIE remains, so symbolic lookup, explicit address breakpoints, and stepi are unaffected.
-- symbolic lookup, explicit address breakpoints, and stepi are unaffected.
if atom.skip_over then if atom.skip_over then
return table.concat({ return table.concat({
set_address(atom.addr), set_address(atom.addr),
@@ -637,9 +605,8 @@ local function build_atom_sequence(atom)
}) })
end end
-- Find which invocation each entry belongs to (if any). Returns the -- Find which invocation each entry belongs to (if any).
-- invocation record or nil. Pre-computed once so we don't rescan per -- Returns the invocation record or nil. Pre-computed once so we don't rescan per emitted row.
-- emitted row.
local function inv_for_idx(idx) local function inv_for_idx(idx)
if not atom.invocations then return nil end if not atom.invocations then return nil end
for _, inv in ipairs(atom.invocations) do for _, inv in ipairs(atom.invocations) do
@@ -655,9 +622,8 @@ local function build_atom_sequence(atom)
set_address(atom.addr), -- 7 bytes: marker + size + sub + addr; PC := atom.addr set_address(atom.addr), -- 7 bytes: marker + size + sub + addr; PC := atom.addr
} }
-- Source state tracker: emits set_file + advance_line only on transitions, -- Source state tracker: emits set_file + advance_line only on transitions, keeps bytes minimal.
-- keeps bytes minimal. Both fields stay in sync with what we emit so we -- Both fields stay in sync with what we emit so we never duplicate a set_file or skip a state-change emit.
-- never duplicate a set_file or skip a state-change emit.
local cur_file = nil -- line-state.file_idx (nil = uninitialized) local cur_file = nil -- line-state.file_idx (nil = uninitialized)
local cur_line = 1 -- line-state.line starts at 1 (per DWARF spec) local cur_line = 1 -- line-state.line starts at 1 (per DWARF spec)
local is_stmt = true -- main line unit default_is_stmt; every sequence ends restored local is_stmt = true -- main line unit default_is_stmt; every sequence ends restored
@@ -669,8 +635,8 @@ local function build_atom_sequence(atom)
end end
end end
-- Emit a state transition (set_file + advance_line + is_stmt as needed) -- Emit a state transition (set_file + advance_line + is_stmt as needed) before a row, then the row itself.
-- before a row, then the row itself. Used for the row(s) at one entry PC. -- Used for the row(s) at one entry PC.
local function emit_row(file_idx, line, want_stmt) local function emit_row(file_idx, line, want_stmt)
if cur_file ~= file_idx then if cur_file ~= file_idx then
parts[#parts + 1] = set_file(file_idx) parts[#parts + 1] = set_file(file_idx)
@@ -684,22 +650,20 @@ local function build_atom_sequence(atom)
parts[#parts + 1] = copy_op() parts[#parts + 1] = copy_op()
end end
-- Whole-atom suppression wraps the independent sequence itself. File/line -- Whole-atom suppression wraps the independent sequence itself.
-- state setup follows while is_stmt is already false; the matching restore -- File/line state setup follows while is_stmt is already false; the matching restore is emitted after the final row below.
-- is emitted after the final row below.
if atom.skip_over then set_statement_state(false) end if atom.skip_over then set_statement_state(false) end
-- --- Atom entry (idx 1) ------------------------------------------------- -- --- Atom entry (idx 1) -------------------------------------------------
-- Determine entry-1's primary row (always the call-site file/line; we may -- Determine entry-1's primary row (always the call-site file/line;
-- emit a second row immediately after if entry 1 is also a component -- we may emit a second row immediately after if entry 1 is also a component invocation's first word).
-- invocation's first word).
local inv1 = inv_for_idx(1) local inv1 = inv_for_idx(1)
local entry_1 = atom.entries[1] local entry_1 = atom.entries[1]
-- The atom body's own source file (the call site for any mac_X(...) inside it). -- The atom body's own source file (the call site for any mac_X(...) inside it).
local call_file_idx = ATOM_SOURCE_FILE_INDEX local call_file_idx = ATOM_SOURCE_FILE_INDEX
-- Primary row: call site at atom start. Capture its (file, line) so we can -- Primary row: call site at atom start.
-- re-emit it as the GDB 12 zero-instruction-prologue marker below. -- Capture its (file, line) so we can re-emit it as the GDB 12 zero-instruction-prologue marker below.
local entry_1_call_file = call_file_idx local entry_1_call_file = call_file_idx
local entry_1_call_line = (inv1 and inv1.call_line) or entry_1.line local entry_1_call_line = (inv1 and inv1.call_line) or entry_1.line
local atom_is_stmt = not atom.skip_over local atom_is_stmt = not atom.skip_over
@@ -707,22 +671,20 @@ local function build_atom_sequence(atom)
-- Primary row: call site at atom start. -- Primary row: call site at atom start.
emit_row(entry_1_call_file, entry_1_call_line, atom_is_stmt) emit_row(entry_1_call_file, entry_1_call_line, atom_is_stmt)
-- If entry 1 is the first word of a component invocation, emit the -- If entry 1 is the first word of a component invocation, emit the component-definition row at the same PC immediately after.
-- component-definition row at the same PC immediately after. A selected -- A selected component suppresses only this definition view;
-- component suppresses only this definition view; a selected atom suppresses -- a selected atom suppresses both rows and therefore never inverts state for nested component markers.
-- both rows and therefore never inverts state for nested component markers.
if inv1 and 1 == inv1.start_pos + 1 then if inv1 and 1 == inv1.start_pos + 1 then
local comp_file_idx_1 = resolve_provenance_file_index(inv1.comp_file) local comp_file_idx_1 = resolve_provenance_file_index(inv1.comp_file)
emit_row(comp_file_idx_1, inv1.comp_line, atom_is_stmt and not inv1.skip_over) emit_row(comp_file_idx_1, inv1.comp_line, atom_is_stmt and not inv1.skip_over)
end end
-- GDB 12 zero-instruction-prologue marker: duplicate of the PRIMARY -- GDB 12 zero-instruction-prologue marker:
-- entry-1 emission (the call-site row). We must explicitly restore the -- duplicate of the PRIMARY entry-1 emission (the call-site row).
-- source state to (call_file, call_line) before emitting the duplicate, -- We must explicitly restore the source state to (call_file, call_line) before emitting the duplicate,
-- otherwise the duplicate lands at whatever state we ended entry-1 in -- otherwise the duplicate lands at whatever state we ended entry-1 in (the component row, if entry 1 is an invocation start),
-- (the component row, if entry 1 is an invocation start) — which would ---which would attach the marker to the wrong source statement.
-- attach the marker to the wrong source statement. ---
--
-- Only emitted at atom entry, NOT at every component-invocation start. -- Only emitted at atom entry, NOT at every component-invocation start.
emit_row(entry_1_call_file, entry_1_call_line, atom_is_stmt) emit_row(entry_1_call_file, entry_1_call_line, atom_is_stmt)
@@ -746,15 +708,14 @@ local function build_atom_sequence(atom)
emit_row(resolve_provenance_file_index(inv.comp_file), inv.comp_line, emit_row(resolve_provenance_file_index(inv.comp_file), inv.comp_line,
atom_is_stmt and not inv.skip_over) atom_is_stmt and not inv.skip_over)
else else
-- RAW word: single call-site row. emit_row restores is_stmt after a -- RAW word: single call-site row. emit_row restores is_stmt after a selected component range before exposing this adjacent row.
-- selected component range before exposing this adjacent row.
emit_row(call_file_idx, entry.line, atom_is_stmt) emit_row(call_file_idx, entry.line, atom_is_stmt)
end end
end end
-- Every sequence starts from default_is_stmt=true. Restore that state before -- Every sequence starts from default_is_stmt=true.
-- DW_LNE_end_sequence so a marked whole atom has explicit bounded toggles and -- Restore that state before DW_LNE_end_sequence so a marked whole atom has explicit bounded toggles
-- no state can leak to a following independent atom sequence. -- and no state can leak to a following independent atom sequence.
set_statement_state(true) set_statement_state(true)
parts[#parts + 1] = end_sequence() parts[#parts + 1] = end_sequence()
return table.concat(parts) return table.concat(parts)
@@ -766,9 +727,8 @@ end
--- Build the atom table the section builders consume. --- Build the atom table the section builders consume.
--- Cross-references nm symbols with source-map.txt entries; sorted by addr. --- Cross-references nm symbols with source-map.txt entries; sorted by addr.
--- Also consumes the provenance file to record per-component invocations --- Also consumes the provenance file to record per-component invocations.
--- (Phase 3 — debug_ux). Each atom gains an `invocations` field with one --- Each atom gains an `invocations` field with one entry per `mac_X(...)` call site:
--- entry per `mac_X(...)` call site:
--- `{comp_name, call_file, call_line, comp_file, comp_line, start_pos, end_pos}`. --- `{comp_name, call_file, call_line, comp_file, comp_line, start_pos, end_pos}`.
--- @param ctx DwarfInjectionCtx --- @param ctx DwarfInjectionCtx
--- @param skip_over table -- collect_skip_over(ctx) result --- @param skip_over table -- collect_skip_over(ctx) result
@@ -797,9 +757,8 @@ local function build_atom_table(ctx, skip_over)
end end
end end
-- Phase 3 (debug_ux): also read *.atoms.provenance.txt to extract per-component invocations. -- Also read *.atoms.provenance.txt to extract per-component invocations.
-- Files are merged by atom name; entries carry the original {pos, call_file, call_line, -- Files are merged by atom name; entries carry the original {pos, call_file, call_line, comp_name, comp_file, comp_line} shape.
-- comp_name, comp_file, comp_line} shape.
local prov_files = duffle.list_dir(ctx.out_root, "%.atoms.provenance%.txt$") local prov_files = duffle.list_dir(ctx.out_root, "%.atoms.provenance%.txt$")
local prov_merged = {} local prov_merged = {}
for _, prov_path in ipairs(prov_files) do for _, prov_path in ipairs(prov_files) do
@@ -824,8 +783,7 @@ local function build_atom_table(ctx, skip_over)
} }
-- Group consecutive MACRO rows in this atom's provenance into invocations. -- Group consecutive MACRO rows in this atom's provenance into invocations.
-- An invocation = one `mac_X(...)` call site spanning N consecutive .word rows. -- An invocation = one `mac_X(...)` call site spanning N consecutive .word rows.
-- Two consecutive rows with the same (comp_name, call_file, call_line, comp_file, comp_line) -- Two consecutive rows with the same (comp_name, call_file, call_line, comp_file, comp_line) are part of the same invocation.
-- are part of the same invocation.
local prov_data = prov_merged[name] local prov_data = prov_merged[name]
if prov_data and prov_data.words then if prov_data and prov_data.words then
local invocations = {} local invocations = {}
@@ -887,7 +845,7 @@ local function collect_component_defs(atom_table)
end end
-- ════════════════════════════════════════════════════════════════════════════ -- ════════════════════════════════════════════════════════════════════════════
-- G' Phase 2 Task 8: rbind atom detection + (reg, field) pairing -- rbind atom detection + (reg, field) pairing
-- ════════════════════════════════════════════════════════════════════════════ -- ════════════════════════════════════════════════════════════════════════════
-- --
-- An "rbind atom" has `atom_bind(Binds_X)` in its `atom_info(...)` sub-call. -- An "rbind atom" has `atom_bind(Binds_X)` in its `atom_info(...)` sub-call.
@@ -900,7 +858,7 @@ end
-- --
-- **Local Binds_X field re-parse**: the source-scan pass's `scan_binds_fields` only recognizes U4 fields; -- **Local Binds_X field re-parse**: the source-scan pass's `scan_binds_fields` only recognizes U4 fields;
-- The current source uses pointer types too (`V4_S2* FaceCursor`, `U4* OtBase`). -- The current source uses pointer types too (`V4_S2* FaceCursor`, `U4* OtBase`).
-- To keep Task 8 contained (don't change the shared scan-source pass), we re-parse the Binds_X typedef body locally with a more permissive rule: -- (don't change the shared scan-source pass), we re-parse the Binds_X typedef body locally with a more permissive rule:
-- U4 field = 4 bytes; any pointer type (`*`-suffixed) = 4 bytes (MIPS32 pointer). Everything else = skip + advance. -- U4 field = 4 bytes; any pointer type (`*`-suffixed) = 4 bytes (MIPS32 pointer). Everything else = skip + advance.
--- Re-parse a Binds_X typedef body, returning {fields = {{name, offset}, ...}, bytes = N}. --- Re-parse a Binds_X typedef body, returning {fields = {{name, offset}, ...}, bytes = N}.
@@ -953,14 +911,12 @@ local function reparse_binds_body(body)
return fields, byte_off return fields, byte_off
end end
--- Find every `load_word(R_<reg>, R_TapePtr, O_(Binds_X, FieldName))` call in the --- Find every `load_word(R_<reg>, R_TapePtr, O_(Binds_X, FieldName))` call in the atom body and return ordered (reg_index, field_name) pairs.
--- atom body and return ordered (reg_index, field_name) pairs.
--- ---
--- Source pattern (single-line, comma-separated at top level): --- Source pattern (single-line, comma-separated at top level):
--- load_word(R_PrimCursor, R_TapePtr, O_(Binds_CubeTri,PrimCursor)), --- load_word(R_PrimCursor, R_TapePtr, O_(Binds_CubeTri,PrimCursor)),
--- load_word(R_FaceCursor, R_TapePtr, O_(Binds_CubeTri,FaceCursor)), --- load_word(R_FaceCursor, R_TapePtr, O_(Binds_CubeTri,FaceCursor)),
--- ... --- ...
---
--- @param body string -- the atom body text (between { ... }) --- @param body string -- the atom body text (between { ... })
--- @param binds_name string -- expected Binds_X name (skip pairs with mismatching binds) --- @param binds_name string -- expected Binds_X name (skip pairs with mismatching binds)
--- @return table[] -- list of {reg = <MIPS index>, field = <field name>} --- @return table[] -- list of {reg = <MIPS index>, field = <field name>}
@@ -1017,10 +973,8 @@ end
--- rbind_atoms = {[atom_name] = {binds, fields, regs, byte_size, info_line}} --- rbind_atoms = {[atom_name] = {binds, fields, regs, byte_size, info_line}}
--- rbind_structs = {[binds_name] = {byte_size, fields, atom_names}} --- rbind_structs = {[binds_name] = {byte_size, fields, atom_names}}
--- ---
--- The `regs` list per atom is ordered: each entry is the MIPS reg index --- The `regs` list per atom is ordered: each entry is the MIPS reg index that holds the matching field in the source-order pop sequence.
--- that holds the matching field in the source-order pop sequence.
--- The piece chain uses (DW_OP_regN, DW_OP_piece, ULEB128(field_size)). --- The piece chain uses (DW_OP_regN, DW_OP_piece, ULEB128(field_size)).
---
--- @param ctx DwarfInjectionCtx --- @param ctx DwarfInjectionCtx
--- @param atom_table table[] -- the cross-ref'd atom table from build_atom_table --- @param atom_table table[] -- the cross-ref'd atom table from build_atom_table
--- @return table, table -- (rbind_atoms, rbind_structs) --- @return table, table -- (rbind_atoms, rbind_structs)
@@ -1028,8 +982,7 @@ local function parse_rbind_atoms(ctx, atom_table)
local rbind_atoms = {} local rbind_atoms = {}
local rbind_structs = {} local rbind_structs = {}
-- Index binds by struct name; RE-PARSE the body locally (Task 8's local parser -- Index binds by struct name; RE-PARSE the body locally (local parser handles pointer types too; the scan-source pass's parser only handles U4).
-- handles pointer types too; the scan-source pass's parser only handles U4).
local binds_body_by_name = {} local binds_body_by_name = {}
for _, src in ipairs(ctx.sources or {}) do for _, src in ipairs(ctx.sources or {}) do
local scan = src.scan local scan = src.scan
@@ -1136,7 +1089,6 @@ end
--- It also encoded byte 13 as the extended-opcode marker; byte 13 is actually the first special opcode. --- It also encoded byte 13 as the extended-opcode marker; byte 13 is actually the first special opcode.
--- The existing final unit already contains hello_gte_tape.c as file index 11 and ends with a valid end_sequence. --- The existing final unit already contains hello_gte_tape.c as file index 11 and ends with a valid end_sequence.
--- We preserve its bytes, append independent atom sequences, and increase only that unit's DWARF32 unit_length. --- We preserve its bytes, append independent atom sequences, and increase only that unit's DWARF32 unit_length.
---
--- @param existing string -- existing section bytes (verbatim) --- @param existing string -- existing section bytes (verbatim)
--- @param atom_table table -- list of {name, addr, size_bytes, words, entries} --- @param atom_table table -- list of {name, addr, size_bytes, words, entries}
--- @return string --- @return string
@@ -1185,7 +1137,6 @@ end
--- segment_size (1 byte) -- = 0 --- segment_size (1 byte) -- = 0
--- [entries...] -- address(4) + length(4) per entry --- [entries...] -- address(4) + length(4) per entry
--- terminator -- address=0 + length=0 (8 zero bytes) --- terminator -- address=0 + length=0 (8 zero bytes)
---
--- @param existing string --- @param existing string
--- @param atom_table table --- @param atom_table table
--- @return string --- @return string
@@ -1330,7 +1281,6 @@ end
--- ---
--- Each piece's size = byte offset of next field - byte offset of this field (or struct.byte_size for the last piece). --- Each piece's size = byte offset of next field - byte offset of this field (or struct.byte_size for the last piece).
--- Since all fields are U4/pointer (4 bytes each) in the current source, each piece is 4 bytes. --- Since all fields are U4/pointer (4 bytes each) in the current source, each piece is 4 bytes.
---
--- @param rbind table -- {regs = {{reg, field}, ...}, fields = {{name, offset}, ...}, bytes = N} --- @param rbind table -- {regs = {{reg, field}, ...}, fields = {{name, offset}, ...}, bytes = N}
--- @return string -- the exprloc byte sequence (length-prefixed) --- @return string -- the exprloc byte sequence (length-prefixed)
local function piece_chain_exprloc(rbind) local function piece_chain_exprloc(rbind)
@@ -1501,7 +1451,6 @@ local CU_HEADER_SIZE = 12 -- 4 + 2 + 1 + 1 + 4
--- - At least 2 units present (crt CU + main CU). --- - At least 2 units present (crt CU + main CU).
--- - The FINAL unit is DWARF5 32-bit compile-unit (version=5, unit_type=0x01, address_size=4). --- - The FINAL unit is DWARF5 32-bit compile-unit (version=5, unit_type=0x01, address_size=4).
--- - The final byte of the main CU is 0x00 (the CU's root children-terminator). --- - The final byte of the main CU is 0x00 (the CU's root children-terminator).
---
--- @param existing string -- the .debug_info section bytes --- @param existing string -- the .debug_info section bytes
--- @return integer|nil, integer|nil, integer|nil --- @return integer|nil, integer|nil, integer|nil
local function find_main_cu_layout(existing) local function find_main_cu_layout(existing)
@@ -1548,7 +1497,7 @@ end
--- Build the new abbreviation declarations that go AFTER the existing gcc-generated ones. We use codes 100+ to avoid collision. --- Build the new abbreviation declarations that go AFTER the existing gcc-generated ones. We use codes 100+ to avoid collision.
--- ---
--- The new abbreviations define a 4-deep tree (Task 8 addition): --- The new abbreviations define a 4-deep tree:
--- Abbrev 100 (DW_TAG_compile_unit, with children): --- Abbrev 100 (DW_TAG_compile_unit, with children):
--- DW_AT_name (DW_FORM_strp) -- CU name --- DW_AT_name (DW_FORM_strp) -- CU name
--- DW_AT_comp_dir (DW_FORM_strp) -- compilation dir --- DW_AT_comp_dir (DW_FORM_strp) -- compilation dir
@@ -1560,26 +1509,26 @@ end
--- Abbrev 102 (DW_TAG_variable, no children): --- Abbrev 102 (DW_TAG_variable, no children):
--- DW_AT_name (DW_FORM_string) -- "R_PrimCursor" etc. --- DW_AT_name (DW_FORM_string) -- "R_PrimCursor" etc.
--- DW_AT_location (DW_FORM_exprloc) -- DW_OP_regN --- DW_AT_location (DW_FORM_exprloc) -- DW_OP_regN
--- Abbrev 103 (DW_TAG_structure_type, with children): -- NEW Task 8 --- Abbrev 103 (DW_TAG_structure_type, with children):
--- DW_AT_name (DW_FORM_string) -- "Binds_CubeTri" etc. --- DW_AT_name (DW_FORM_string) -- "Binds_CubeTri" etc.
--- DW_AT_byte_size (DW_FORM_udata) -- struct byte size --- DW_AT_byte_size (DW_FORM_udata) -- struct byte size
--- Abbrev 104 (DW_TAG_member, no children): -- NEW Task 8 --- Abbrev 104 (DW_TAG_member, no children):
--- DW_AT_name (DW_FORM_string) -- "PrimCursor" etc. --- DW_AT_name (DW_FORM_string) -- "PrimCursor" etc.
--- DW_AT_data_member_location (DW_FORM_udata) -- byte offset in struct --- DW_AT_data_member_location (DW_FORM_udata) -- byte offset in struct
--- DW_AT_type (DW_FORM_ref4) -- → U4 base type --- DW_AT_type (DW_FORM_ref4) -- → U4 base type
--- Abbrev 105 (DW_TAG_variable, no children): -- NEW Task 8 --- Abbrev 105 (DW_TAG_variable, no children):
--- DW_AT_name (DW_FORM_string) -- "bind_args" --- DW_AT_name (DW_FORM_string) -- "bind_args"
--- DW_AT_location (DW_FORM_exprloc) -- DW_OP_regN, piece, regN, piece, ... --- DW_AT_location (DW_FORM_exprloc) -- DW_OP_regN, piece, regN, piece, ...
--- DW_AT_type (DW_FORM_ref4) -- → DW_TAG_structure_type DIE --- DW_AT_type (DW_FORM_ref4) -- → DW_TAG_structure_type DIE
--- Abbrev 106 (DW_TAG_base_type, no children): -- NEW Task 8 --- Abbrev 106 (DW_TAG_base_type, no children):
--- DW_AT_name (DW_FORM_string) -- "unsigned int" --- DW_AT_name (DW_FORM_string) -- "unsigned int"
--- DW_AT_byte_size (DW_FORM_data1) -- 4 --- DW_AT_byte_size (DW_FORM_data1) -- 4
--- DW_AT_encoding (DW_FORM_data1) -- DW_ATE_unsigned --- DW_AT_encoding (DW_FORM_data1) -- DW_ATE_unsigned
--- Abbrev 107 (DW_TAG_subprogram, NO children) — Phase 3 abstract origin. -- NEW Phase 3 (debug_ux) --- Abbrev 107 (DW_TAG_subprogram, NO children) — abstract origin.
--- DW_AT_name (DW_FORM_string) -- "mac_yield" etc. (component ident) --- DW_AT_name (DW_FORM_string) -- "mac_yield" etc. (component ident)
--- DW_AT_inline (DW_FORM_data1) -- DW_INL_declared_inlined (= 3) --- DW_AT_inline (DW_FORM_data1) -- DW_INL_declared_inlined (= 3)
--- DW_AT_external (DW_FORM_data1) -- 1 (visible across the CU) --- DW_AT_external (DW_FORM_data1) -- 1 (visible across the CU)
--- Abbrev 108 (DW_TAG_inlined_subroutine, with children): -- NEW Phase 3 (debug_ux) --- Abbrev 108 (DW_TAG_inlined_subroutine, with children):
--- DW_AT_abstract_origin (DW_FORM_ref4) -- → ABBREV_ABSTRACT_SUBPROGRAM --- DW_AT_abstract_origin (DW_FORM_ref4) -- → ABBREV_ABSTRACT_SUBPROGRAM
--- DW_AT_low_pc (DW_FORM_addr) -- invocation start PC --- DW_AT_low_pc (DW_FORM_addr) -- invocation start PC
--- DW_AT_high_pc (DW_FORM_addr) -- invocation end PC --- DW_AT_high_pc (DW_FORM_addr) -- invocation end PC
@@ -1619,7 +1568,7 @@ local function build_new_abbrev()
.. attr(DW_AT_type, DW_FORM_ref4) .. attr(DW_AT_type, DW_FORM_ref4)
.. attr(DW_AT_external, DW_FORM_data1)) -- DW_AT_external=1: visible at CU scope (gdb's `info locals` + Variables pane shows these for the current frame even if the scope lookup misses) .. attr(DW_AT_external, DW_FORM_data1)) -- DW_AT_external=1: visible at CU scope (gdb's `info locals` + Variables pane shows these for the current frame even if the scope lookup misses)
-- Task 8: rbind composite. -- rbind composite.
-- DW_FORM_udata (0x0F, ULEB128) is declared at module scope. For small values (struct byte_size, member offsets) 1 byte is enough; -- DW_FORM_udata (0x0F, ULEB128) is declared at module scope. For small values (struct byte_size, member offsets) 1 byte is enough;
-- we emit ULEB128 anyway for spec compliance. -- we emit ULEB128 anyway for spec compliance.
local abbrev_struct_type = abbrev(ABBREV_STRUCT_TYPE, DW_TAG_structure_type, true, -- DW_CHILDREN_yes local abbrev_struct_type = abbrev(ABBREV_STRUCT_TYPE, DW_TAG_structure_type, true, -- DW_CHILDREN_yes
@@ -1641,10 +1590,9 @@ local function build_new_abbrev()
.. attr(DW_AT_byte_size, DW_FORM_data1) .. attr(DW_AT_byte_size, DW_FORM_data1)
.. attr(DW_AT_encoding, DW_FORM_data1)) .. attr(DW_AT_encoding, DW_FORM_data1))
-- Phase 3 (debug_ux): component step-into abstract + inline DIE abbreviations. -- Component step-into abstract + inline DIE abbreviations.
local DW_INL_declared_inlined = 0x03 -- DWARF5 §3.33.3: "this subroutine was declared inline" local DW_INL_declared_inlined = 0x03 -- DWARF5 §3.33.3: "this subroutine was declared inline"
-- Phase 3 Task 3: abstract subprograms now carry DW_AT_decl_file + DW_AT_decl_line -- Abstract subprograms now carry DW_AT_decl_file + DW_AT_decl_line so consumers can resolve the abstract origin back to its definition site
-- so consumers can resolve the abstract origin back to its definition site
-- even when no inlined_subroutine instance currently maps to it. -- even when no inlined_subroutine instance currently maps to it.
-- DW_FORM_udata is consistent with the call_file/call_line forms on abbrev 108. -- DW_FORM_udata is consistent with the call_file/call_line forms on abbrev 108.
local abbrev_abstract_subprogram = abbrev(ABBREV_ABSTRACT_SUBPROGRAM, DW_TAG_subprogram, false, -- DW_CHILDREN_no local abbrev_abstract_subprogram = abbrev(ABBREV_ABSTRACT_SUBPROGRAM, DW_TAG_subprogram, false, -- DW_CHILDREN_no
@@ -1654,17 +1602,16 @@ local function build_new_abbrev()
.. attr(DW_AT_decl_file, DW_FORM_udata) .. attr(DW_AT_decl_file, DW_FORM_udata)
.. attr(DW_AT_decl_line, DW_FORM_udata)) .. attr(DW_AT_decl_line, DW_FORM_udata))
local abbrev_inlined_subroutine = abbrev(ABBREV_INLINED_SUBROUTINE, DW_TAG_inlined_subroutine, false, -- DW_CHILDREN_no (Phase 3 emits no per-inlined-instance children; the PC range IS the inlining scope) local abbrev_inlined_subroutine = abbrev(ABBREV_INLINED_SUBROUTINE, DW_TAG_inlined_subroutine, false, -- DW_CHILDREN_no (emits no per-inlined-instance children; the PC range IS the inlining scope)
attr( DW_AT_abstract_origin, DW_FORM_ref4) attr( DW_AT_abstract_origin, DW_FORM_ref4)
.. attr(DW_AT_low_pc, DW_FORM_addr) .. attr(DW_AT_low_pc, DW_FORM_addr)
.. attr(DW_AT_high_pc, DW_FORM_addr) .. attr(DW_AT_high_pc, DW_FORM_addr)
.. attr(DW_AT_call_file, DW_FORM_udata) .. attr(DW_AT_call_file, DW_FORM_udata)
.. attr(DW_AT_call_line, DW_FORM_udata)) .. attr(DW_AT_call_line, DW_FORM_udata))
-- Phase 5 (debug_ux): bind_args with DW_FORM_sec_offset → .debug_loclists. -- bind_args with DW_FORM_sec_offset → .debug_loclists.
-- The .debug_loclists section holds a sequence of DW_LLE entries; the -- The .debug_loclists section holds a sequence of DW_LLE entries;
-- first matching entry for a PC describes each field's value (tape -- the first matching entry for a PC describes each field's value (tape memory DW_OP_breg24 or register DW_OP_regN).
-- memory DW_OP_breg24 or register DW_OP_regN).
local abbrev_bind_var_loclists = abbrev(ABBREV_BIND_VAR_LOCLIST, DW_TAG_variable, false, -- DW_CHILDREN_no local abbrev_bind_var_loclists = abbrev(ABBREV_BIND_VAR_LOCLIST, DW_TAG_variable, false, -- DW_CHILDREN_no
attr( DW_AT_name, DW_FORM_string) attr( DW_AT_name, DW_FORM_string)
.. attr(DW_AT_location, DW_FORM_sec_offset) .. attr(DW_AT_location, DW_FORM_sec_offset)
@@ -1793,15 +1740,13 @@ local function build_inserted_children(main_cu_offset, main_cu_end_excl, atom_ta
emit(string.char(4)) -- DW_FORM_data1 (DW_AT_byte_size) emit(string.char(4)) -- DW_FORM_data1 (DW_AT_byte_size)
emit(string.char(DW_ATE_unsigned)) -- DW_FORM_data1 (DW_AT_encoding) emit(string.char(DW_ATE_unsigned)) -- DW_FORM_data1 (DW_AT_encoding)
-- Phase 5 (debug_ux): Typed local views. For each unique -- Typed local views.
-- (type_name, pointer_depth) pair across all rbind atom fields, emit -- For each unique (type_name, pointer_depth) pair across all rbind atom fields, emit a synthetic type chain.
-- a synthetic type chain. The chain is: ... → pointer_type → typedef → base_type. -- The chain is: ... → pointer_type → typedef → base_type.
-- The outermost pointer_type (depth = N) is the variable's DW_AT_type. -- The outermost pointer_type (depth = N) is the variable's DW_AT_type.
-- For a field declared "V4_S2*" (depth=1), the chain is: -- For a field declared "V4_S2*" (depth=1), the chain is:
-- V4_S2 (typedef, references base_type 4-byte unsigned) + -- V4_S2 (typedef, references base_type 4-byte unsigned) + ptr_to_V4_S2_1 (pointer_type, byte_size 4, refs the typedef)
-- ptr_to_V4_S2_1 (pointer_type, byte_size 4, refs the typedef) -- gdb walks: variable type = ptr_to_V4_S2_1 → V4_S2 → base_type, and displays "V4_S2 *" (the typedef's name + the pointer depth).
-- gdb walks: variable type = ptr_to_V4_S2_1 → V4_S2 → base_type, and
-- displays "V4_S2 *" (the typedef's name + the pointer depth).
local type_offsets = {} -- {[type_name] = section_offset for the typedef DIE} local type_offsets = {} -- {[type_name] = section_offset for the typedef DIE}
-- Always include the base_type "unsigned int" as the U4 target. -- Always include the base_type "unsigned int" as the U4 target.
type_offsets["U4"] = base_type_section_offset type_offsets["U4"] = base_type_section_offset
@@ -1823,46 +1768,35 @@ local function build_inserted_children(main_cu_offset, main_cu_end_excl, atom_ta
local sorted_typed_types = {} local sorted_typed_types = {}
for tn in pairs(used_typed_views) do sorted_typed_types[#sorted_typed_types + 1] = tn end for tn in pairs(used_typed_views) do sorted_typed_types[#sorted_typed_types + 1] = tn end
table.sort(sorted_typed_types) table.sort(sorted_typed_types)
-- For each non-U4 type, emit a typedef (DW_TAG_typedef) named after the -- For each non-U4 type, emit a typedef (DW_TAG_typedef) named after the type and referencing the base_type "unsigned int" (4 bytes).
-- type and referencing the base_type "unsigned int" (4 bytes). The -- The typedef gives gdb a named anchor; the pointer_type chain wraps it.
-- typedef gives gdb a named anchor; the pointer_type chain wraps it. -- We use a fresh abbrev for the typedef + the pointer_type.
-- We use a fresh abbrev for the typedef + the pointer_type. To stay -- To stay within the existing abbrev budget, we reuse the duplicated main table's abbrev 4 (DW_TAG_typedef) and abbrev 9 (DW_TAG_pointer_type)
-- within the existing abbrev budget, we reuse the duplicated main -- via a synthetic-emit pattern: emit the abbrev code (a ULEB) + attribute bytes using DW_FORM values that match those abbrevs.
-- table's abbrev 4 (DW_TAG_typedef) and abbrev 9 (DW_TAG_pointer_type)
-- via a synthetic-emit pattern: emit the abbrev code (a ULEB) +
-- attribute bytes using DW_FORM values that match those abbrevs.
-- The cleanest path is to define new abbrevs in build_new_abbrev(). -- The cleanest path is to define new abbrevs in build_new_abbrev().
-- For the scope of this task, we emit fresh DW_TAG_base_type DIEs -- We emit fresh DW_TAG_base_type DIEs (the simplest correct shape: byte_size 4, encoding unsigned)
-- (the simplest correct shape: byte_size 4, encoding unsigned) and -- and the variable's DW_AT_type references the pointer chain's outermost base_type.
-- the variable's DW_AT_type references the pointer chain's outermost -- The displayed type name is the type_name (e.g. "V4_S2") because we use DW_FORM_string on the field type.
-- base_type. The displayed type name is the type_name (e.g. "V4_S2") -- gdb walks the chain and displays the name.
-- because we use DW_FORM_string on the field type. gdb walks the
-- chain and displays the name.
-- --
-- For each (type_name, depth), emit (depth) base_type DIEs forming a -- For each (type_name, depth), emit (depth) base_type DIEs forming a chain.
-- chain. The outermost base_type's name is the type_name; the inner -- The outermost base_type's name is the type_name; the inner ones are named "ptr_<type>_<d>".
-- ones are named "ptr_<type>_<d>". gdb shows the outermost name as -- gdb shows the outermost name as the type.
-- the type.
-- --
-- (Full typedef + pointer_type chain with proper DW_TAG_pointer_type -- (Full typedef + pointer_type chain with proper DW_TAG_pointer_type + byte_size=4 is a follow-up;
-- + byte_size=4 is a follow-up; for now we use base_type entries -- for now we use base_type entries which gdb can resolve.)
-- which gdb can resolve.)
local type_chain_offsets = {} -- { [type_name .. "|" .. depth] = section_offset (the OUTERMOST entry, the one referenced as the variable's type) } local type_chain_offsets = {} -- { [type_name .. "|" .. depth] = section_offset (the OUTERMOST entry, the one referenced as the variable's type) }
-- For pointer_depth = 1, the chain is: [base_type "V4_S2" 4 bytes] [pointer_type → V4_S2]. -- For pointer_depth = 1, the chain is: [base_type "V4_S2" 4 bytes] [pointer_type → V4_S2].
-- For pointer_depth = 2, the chain is: [base_type "ptr_V4_S2_1"] [base_type "V4_S2"] [pointer_type → ptr_V4_S2_1]. -- For pointer_depth = 2, the chain is: [base_type "ptr_V4_S2_1"] [base_type "V4_S2"] [pointer_type → ptr_V4_S2_1].
-- The OUTERMOST pointer_type is what gdb shows as "<type> *" and is -- The OUTERMOST pointer_type is what gdb shows as "<type> *" and is what the Locals panel uses to render the dereference affordance.
-- what the Locals panel uses to render the dereference affordance. -- Build the chain bottom-up: emit the innermost base_type first, then each next-outer pointer_type, recording the offset of each.
-- Build the chain bottom-up: emit the innermost base_type first, then -- The outermost is the last one emitted; type_chain_offsets stores its offset as the variable's DW_AT_type.
-- each next-outer pointer_type, recording the offset of each. The
-- outermost is the last one emitted; type_chain_offsets stores its
-- offset as the variable's DW_AT_type.
for _, tn in ipairs(sorted_typed_types) do for _, tn in ipairs(sorted_typed_types) do
if tn ~= "U4" then if tn ~= "U4" then
local depth = used_typed_views[tn] local depth = used_typed_views[tn]
-- First, emit the chain of base_types from innermost (d=1) to d=depth-1. -- First, emit the chain of base_types from innermost (d=1) to d=depth-1.
-- The innermost (d=1) carries the user's type_name (V4_S2); outer -- The innermost (d=1) carries the user's type_name (V4_S2); outer levels are placeholders named "ptr_<type>_<d>".
-- levels are placeholders named "ptr_<type>_<d>". Each base_type -- Each base_type is 4 bytes unsigned (a stand-in for the actual type).
-- is 4 bytes unsigned (a stand-in for the actual type).
local innermost_offset local innermost_offset
local base_offsets = {} -- base_offsets[d] = offset of the d-th level base_type (1-indexed) local base_offsets = {} -- base_offsets[d] = offset of the d-th level base_type (1-indexed)
for d = 1, depth do for d = 1, depth do
@@ -1878,36 +1812,27 @@ local function build_inserted_children(main_cu_offset, main_cu_end_excl, atom_ta
emit(string.char(DW_ATE_unsigned)) -- encoding = unsigned emit(string.char(DW_ATE_unsigned)) -- encoding = unsigned
innermost_offset = off innermost_offset = off
end end
-- Now emit a DW_TAG_pointer_type (abbrev 9 in the duplicate) -- Now emit a DW_TAG_pointer_type (abbrev 9 in the duplicate) ON TOP of the chain.
-- ON TOP of the chain. The pointer_type's DW_AT_type ref4 points -- The pointer_type's DW_AT_type ref4 points at the base_type it dereferences.
-- at the base_type it dereferences. The outermost pointer_type -- The outermost pointer_type is what the variable's DW_AT_type uses.
-- is what the variable's DW_AT_type uses.
local outermost_offset = next_offset local outermost_offset = next_offset
emit(uleb128(9)) -- DW_TAG_pointer_type abbrev code (abbrev 9 in duplicate) emit(uleb128(9)) -- DW_TAG_pointer_type abbrev code (abbrev 9 in duplicate)
-- Abbrev 9's attributes: DW_AT_byte_size (implicit_const 4 no DIE -- Abbrev 9's attributes: DW_AT_byte_size (implicit_const 4; no DIE bytes), DW_AT_type (ref4 → target).
-- bytes), DW_AT_type (ref4 → target). So the DIE bytes are just -- So the DIE bytes are just the 4-byte ref4 pointing at the base_type it dereferences.
-- the 4-byte ref4 pointing at the base_type it dereferences. -- The variable uses the OUTERMOST pointer_type.
-- The variable uses the OUTERMOST pointer_type. Inner pointer -- Inner pointer levels (for depth > 1) would chain pointer_type → pointer_type → ... → base_type. For the user's case (depth ≤ 1), one pointer_type suffices.
-- levels (for depth > 1) would chain pointer_type → pointer_type -- For depth > 1, we'd need intermediate pointer_type DIEs — emit depth-1 of them, each pointing at the next.
-- → ... → base_type. For the user's case (depth ≤ 1), one
-- pointer_type suffices. For depth > 1, we'd need intermediate
-- pointer_type DIEs — emit depth-1 of them, each pointing at
-- the next.
if depth == 1 then if depth == 1 then
-- Single pointer: target = innermost base_type -- Single pointer: target = innermost base_type
emit(elf_dwarf.write_u32_le(ref4_of(innermost_offset))) emit(elf_dwarf.write_u32_le(ref4_of(innermost_offset)))
else else
-- depth > 1: emit (depth - 1) pointer_types, each pointing -- depth > 1: emit (depth - 1) pointer_types, each pointing at the next.
-- at the next. The last pointer_type (outermost) becomes the -- The last pointer_type (outermost) becomes the variable's DW_AT_type.
-- variable's DW_AT_type. The innermost pointer_type points -- The innermost pointer_type points at the innermost base_type.
-- at the innermost base_type. -- For simplicity, emit depth pointer_types total, each pointing at the previous base_type or pointer_type.
-- For simplicity, emit depth pointer_types total, each -- The outermost is what we return.
-- pointing at the previous base_type or pointer_type. The -- Actually, for the current build the only typed fields are at depth 1 (V4_S2* / V3_S2*). depth > 1 is not exercised.
-- outermost is what we return. -- We can handle depth == 1 inline and defer depth > 1 to a future task.
-- Actually, for the current build the only typed fields
-- are at depth 1 (V4_S2* / V3_S2*). depth > 1 is not
-- exercised. We can handle depth == 1 inline and defer
-- depth > 1 to a future task.
error("typed-view: pointer_depth > 1 is not yet supported in this emission path") error("typed-view: pointer_depth > 1 is not yet supported in this emission path")
end end
type_chain_offsets[tn .. "|" .. depth] = outermost_offset type_chain_offsets[tn .. "|" .. depth] = outermost_offset
@@ -1932,8 +1857,8 @@ local function build_inserted_children(main_cu_offset, main_cu_end_excl, atom_ta
emit(uleb128(ABBREV_MEMBER)) emit(uleb128(ABBREV_MEMBER))
emit(field.name .. "\0") -- DW_FORM_string (DW_AT_name) emit(field.name .. "\0") -- DW_FORM_string (DW_AT_name)
emit(uleb128(field.offset)) -- DW_FORM_udata (DW_AT_data_member_location) emit(uleb128(field.offset)) -- DW_FORM_udata (DW_AT_data_member_location)
-- Phase 5 (debug_ux): typed field. For a pointer-typed field, the -- typed field.
-- member's DW_AT_type points at the deepest pointer_type in its chain. -- For a pointer-typed field, the member's DW_AT_type points at the deepest pointer_type in its chain.
-- For U4 (no pointer), it points at the base_type. -- For U4 (no pointer), it points at the base_type.
local field_type_offset local field_type_offset
if field.pointer_depth and field.pointer_depth > 0 then if field.pointer_depth and field.pointer_depth > 0 then
@@ -1948,12 +1873,10 @@ local function build_inserted_children(main_cu_offset, main_cu_end_excl, atom_ta
emit(string.char(0x00)) -- end of structure_type's children emit(string.char(0x00)) -- end of structure_type's children
end end
-- 3) Phase 3 (debug_ux): emit one abstract DW_TAG_subprogram per unique mac_X component. -- 3) Emit one abstract DW_TAG_subprogram per unique mac_X component.
-- Each abstract DIE is a CU-level child (sibling of the per-atom subprograms below). -- Each abstract DIE is a CU-level child (sibling of the per-atom subprograms below).
-- The abstract DIE's section offset is later used by inlined_subroutine DIEs -- The abstract DIE's section offset is later used by inlined_subroutine DIEs (which embed `DW_AT_abstract_origin = ref4 → abstract DIE`).
-- (which embed `DW_AT_abstract_origin = ref4 → abstract DIE`). -- Each abstract DIE also carries DW_AT_decl_file + DW_AT_decl_line pointing at the component's definition site (file path + body line).
-- Phase 3 Task 3: each abstract DIE also carries DW_AT_decl_file + DW_AT_decl_line
-- pointing at the component's definition site (file path + body line).
local component_defs = collect_component_defs(atom_table) local component_defs = collect_component_defs(atom_table)
local abstract_offsets = {} -- name -> section offset local abstract_offsets = {} -- name -> section offset
local sorted_comp_names = {} local sorted_comp_names = {}
@@ -1968,8 +1891,7 @@ local function build_inserted_children(main_cu_offset, main_cu_end_excl, atom_ta
emit("mac_" .. comp_name .. "\0") -- DW_FORM_string (DW_AT_name) emit("mac_" .. comp_name .. "\0") -- DW_FORM_string (DW_AT_name)
emit(string.char(DW_INL_inlined)) -- DW_FORM_data1 (DW_AT_inline) emit(string.char(DW_INL_inlined)) -- DW_FORM_data1 (DW_AT_inline)
emit(string.char(0x01)) -- DW_FORM_data1 (DW_AT_external=1) emit(string.char(0x01)) -- DW_FORM_data1 (DW_AT_external=1)
-- Phase 3 Task 3: decl_file + decl_line resolve the abstract origin back to its -- decl_file + decl_line resolve the abstract origin back to its definition site even when no inlined_subroutine instance maps to it.
-- definition site even when no inlined_subroutine instance maps to it.
emit(uleb128(resolve_provenance_file_index(def.def_file))) -- DW_FORM_udata (DW_AT_decl_file) emit(uleb128(resolve_provenance_file_index(def.def_file))) -- DW_FORM_udata (DW_AT_decl_file)
emit(uleb128(def.def_line)) -- DW_FORM_udata (DW_AT_decl_line) emit(uleb128(def.def_line)) -- DW_FORM_udata (DW_AT_decl_line)
end end
@@ -1985,15 +1907,14 @@ local function build_inserted_children(main_cu_offset, main_cu_end_excl, atom_ta
emit(elf_dwarf.write_u32_le(atom.addr + atom.size_bytes)) emit(elf_dwarf.write_u32_le(atom.addr + atom.size_bytes))
emit(atom.name .. "\0") -- DW_FORM_string (DW_AT_linkage_name; same as DW_AT_name for non-mangled C) emit(atom.name .. "\0") -- DW_FORM_string (DW_AT_linkage_name; same as DW_AT_name for non-mangled C)
-- Per wave-context reg: DW_TAG_variable. Type precedence (Task 20): -- Per wave-context reg: DW_TAG_variable.
-- 1. explicit per-atom register type annotation (atom_reg_types) -- 1. explicit per-atom register type annotation (atom_reg_types)
-- 2. atom_view(Binds_X) field type (from the rbind regs → fields map) -- 2. atom_view(Binds_X) field type (from the rbind regs → fields map)
-- 3. semantic default (R_TapePtr → U4*, R_AtomJmp → MipsCode*, -- 3. semantic default (R_TapePtr → U4*, R_AtomJmp → MipsCode*,
-- cursor/base aliases → void*) -- cursor/base aliases → void*)
-- 4. void* (unannotated) -- 4. void* (unannotated)
-- For rbind atoms the precedence comes from the rbind regs/fields -- For rbind atoms the precedence comes from the rbind regs/fields
-- (the load_word calls already mapped each R_<reg> to a Binds_X.<field> -- (the load_word calls already mapped each R_<reg> to a Binds_X.<field> with a known type_name + pointer_depth).
-- with a known type_name + pointer_depth).
local field_type_by_name = {} local field_type_by_name = {}
if atom.rbind and atom.rbind.fields then if atom.rbind and atom.rbind.fields then
for _, f in ipairs(atom.rbind.fields) do for _, f in ipairs(atom.rbind.fields) do
@@ -2040,12 +1961,9 @@ local function build_inserted_children(main_cu_offset, main_cu_end_excl, atom_ta
end end
-- If rbind, emit bind_args variable with PC-ranged location list. -- If rbind, emit bind_args variable with PC-ranged location list.
-- Phase 5 (debug_ux): the loclist is in .debug_loclists, indexed by -- The loclist is in .debug_loclists, indexed by `DW_FORM_sec_offset` (4-byte section-relative offset).
-- `DW_FORM_sec_offset` (4-byte section-relative offset). The piece -- The piece chain is replaced by two PC ranges: [atom.addr, last_load+8) where every field is described as a tape-memory
-- chain is replaced by two PC ranges: [atom.addr, last_load+8) where -- (DW_OP_bregN + offset) piece, and [last_load+8, atom.end) where every field is described as a GPR (DW_OP_regN) piece.
-- every field is described as a tape-memory (DW_OP_bregN + offset)
-- piece, and [last_load+8, atom.end) where every field is described
-- as a GPR (DW_OP_regN) piece.
if atom.rbind then if atom.rbind then
local binds_name = atom.rbind.binds local binds_name = atom.rbind.binds
local loclists_offset = loclists_offsets[atom.name] or 0 local loclists_offset = loclists_offsets[atom.name] or 0
@@ -2055,12 +1973,11 @@ local function build_inserted_children(main_cu_offset, main_cu_end_excl, atom_ta
emit(elf_dwarf.write_u32_le(ref4_of(struct_section_offsets[binds_name]))) -- DW_FORM_ref4 → struct_type emit(elf_dwarf.write_u32_le(ref4_of(struct_section_offsets[binds_name]))) -- DW_FORM_ref4 → struct_type
end end
-- Phase 3 (debug_ux): per-component invocation inlined_subroutine instances. -- Per-component invocation inlined_subroutine instances.
-- Each invocation covers a contiguous .word range [start_pos, end_pos] within the atom. -- Each invocation covers a contiguous .word range [start_pos, end_pos] within the atom.
-- We compute the corresponding PC range from the atom's start + .word offsets × MIPS_BYTES_PER_WORD. -- We compute the corresponding PC range from the atom's start + .word offsets × MIPS_BYTES_PER_WORD.
-- Phase 3 Task 3: call_file now resolves inv.call_file to the line-unit file index -- call_file now resolves inv.call_file to the line-unit file index (previously hardcoded to ATOM_SOURCE_FILE_INDEX;
-- (previously hardcoded to ATOM_SOURCE_FILE_INDEX; that lost the call-site attribution -- that lost the call-site attribution for any invocation whose call site was NOT the atom's source file).
-- for any invocation whose call site was NOT the atom's source file).
if atom.invocations and not atom.skip_over then if atom.invocations and not atom.skip_over then
for _, inv in ipairs(atom.invocations) do for _, inv in ipairs(atom.invocations) do
local inv_low = atom.addr + inv.start_pos * MIPS_BYTES_PER_WORD local inv_low = atom.addr + inv.start_pos * MIPS_BYTES_PER_WORD
@@ -2135,20 +2052,18 @@ end
--- 1. Builds the inserted-children bytes (base_type, struct_types, subprograms with their RR_* + bind_args children) via build_inserted_children. --- 1. Builds the inserted-children bytes (base_type, struct_types, subprograms with their RR_* + bind_args children) via build_inserted_children.
--- 2. Patches the main CU's `unit_length` field to account for the inserted bytes. --- 2. Patches the main CU's `unit_length` field to account for the inserted bytes.
--- 3. Patches the main CU's `debug_abbrev_offset` field to point at the duplicate abbrev table (offset = #existing_abbrev_before_append). --- 3. Patches the main CU's `debug_abbrev_offset` field to point at the duplicate abbrev table (offset = #existing_abbrev_before_append).
--- 4. Preserves all original main CU DIE bytes verbatim. --- 4. Preserves all original main CU DIE bytes.
--- 5. Replaces the main CU's final byte (the root children-terminator, 0) with: inserted_children_bytes + a single 0 byte (root terminator preserved). --- 5. Replaces the main CU's final byte (the root children-terminator, 0) with: inserted_children_bytes + a single 0 byte (root terminator preserved).
--- ---
--- The crt CU (everything before main_cu_start) is preserved verbatim. --- The crt CU (everything before main_cu_start) is preserved.
---
--- **No detached synthetic CU is appended.** --- **No detached synthetic CU is appended.**
---
--- @param existing string -- existing .debug_info section bytes --- @param existing string -- existing .debug_info section bytes
--- @param main_cu_start integer -- 0-based offset of the main CU's unit_length field --- @param main_cu_start integer -- 0-based offset of the main CU's unit_length field
--- @param main_cu_end_excl integer -- 0-based offset of the first byte AFTER the main CU --- @param main_cu_end_excl integer -- 0-based offset of the first byte AFTER the main CU
--- @param new_abbrev_offset integer -- 0-based offset into the new .debug_abbrev of the duplicate main table --- @param new_abbrev_offset integer -- 0-based offset into the new .debug_abbrev of the duplicate main table
--- @param atom_table table[] --- @param atom_table table[]
--- @param rbind_structs table -- {[binds_name] = {bytes, fields, atom_names}} (Task 8) --- @param rbind_structs table -- {[binds_name] = {bytes, fields, atom_names}}
--- @param loclists_offsets table -- {[atom_name] = section-relative offset} (Task 19) --- @param loclists_offsets table -- {[atom_name] = section-relative offset}
--- @return string -- the rebuilt .debug_info bytes --- @return string -- the rebuilt .debug_info bytes
local function build_debug_info_section(existing, main_cu_start, main_cu_end_excl, new_abbrev_offset, atom_table, rbind_structs, loclists_offsets) local function build_debug_info_section(existing, main_cu_start, main_cu_end_excl, new_abbrev_offset, atom_table, rbind_structs, loclists_offsets)
-- 1) Build the inserted children bytes (just before the main CU's root terminator). -- 1) Build the inserted children bytes (just before the main CU's root terminator).
@@ -2184,8 +2099,8 @@ local function build_debug_info_section(existing, main_cu_start, main_cu_end_exc
end end
--- Build the .debug_loc: just a terminator. --- Build the .debug_loc: just a terminator.
--- Atoms don't have stack frames (the .debug_loc section describes per-instruction location adjustments for call-frame-based variables; --- Atoms don't have stack frames. The .debug_loc section describes per-instruction location adjustments for call-frame-based variables;
--- we use DW_OP_regN which is register-based and doesn't need .debug_loc entries). --- We use DW_OP_regN which is register-based and doesn't need .debug_loc entries).
--- The section itself must not be empty OR gdb may complain; the DW_LLE_end_of_list marker (per DWARF5 §7.7) is a single byte 0x00. --- The section itself must not be empty OR gdb may complain; the DW_LLE_end_of_list marker (per DWARF5 §7.7) is a single byte 0x00.
--- @return string --- @return string
local function build_debug_loc_section() local function build_debug_loc_section()
@@ -2228,8 +2143,7 @@ end
local M = {} local M = {}
--- M.run — orchestrator entry. Phase 1 Tasks 3-4 read + cross-ref. --- M.run — orchestrator entry.
--- Phase 2 Tasks 5-7 fill in the section builders + .bin emission.
--- @param ctx DwarfInjectionCtx --- @param ctx DwarfInjectionCtx
--- @return table --- @return table
function M.run(ctx) function M.run(ctx)
@@ -2255,9 +2169,8 @@ function M.run(ctx)
local existing_sections = elf_dwarf.read_elf_sections(elf_path, { local existing_sections = elf_dwarf.read_elf_sections(elf_path, {
".debug_line", ".debug_aranges", ".debug_rnglists", ".debug_line", ".debug_aranges", ".debug_rnglists",
".debug_info", ".debug_abbrev", ".debug_str", ".debug_info", ".debug_abbrev", ".debug_str",
-- .debug_loc and .debug_loclists don't exist in the source ELF (we -- .debug_loc and .debug_loclists don't exist in the source ELF (we add-section them on splice);
-- --add-section them on splice); reading them just returns "" which is -- reading them just returns "" which is the "missing" case the builder handles.
-- the "missing" case the builder handles.
".debug_loc", ".debug_loclists", ".debug_loc", ".debug_loclists",
}) })
io.stderr:write(string.format( io.stderr:write(string.format(
@@ -2278,7 +2191,7 @@ function M.run(ctx)
local atom_table = build_atom_table(ctx, skip_over) local atom_table = build_atom_table(ctx, skip_over)
io.stderr:write(string.format("[dwarf_injection] matched %d atoms between nm + source-map\n", #atom_table)) io.stderr:write(string.format("[dwarf_injection] matched %d atoms between nm + source-map\n", #atom_table))
-- Task 8: detect rbind atoms + index Binds_* struct fields (from ctx.sources[i].scan, populated by scan-source pass). -- Detect rbind atoms + index Binds_* struct fields (from ctx.sources[i].scan, populated by scan-source pass).
local _rbind_atoms, rbind_structs = parse_rbind_atoms(ctx, atom_table) local _rbind_atoms, rbind_structs = parse_rbind_atoms(ctx, atom_table)
local rbind_count = 0 local rbind_count = 0
for _ in pairs(_rbind_atoms) do rbind_count = rbind_count + 1 end for _ in pairs(_rbind_atoms) do rbind_count = rbind_count + 1 end
@@ -2287,18 +2200,18 @@ function M.run(ctx)
-- Write the .bin files. The build_psyq.ps1 post-link hook splices these into a copy of the ELF via objcopy --update-section. -- Write the .bin files. The build_psyq.ps1 post-link hook splices these into a copy of the ELF via objcopy --update-section.
-- --
-- Build order (Task 2 GREEN: scope ownership): -- Build order:
-- 0. Validate .debug_info layout (crT CU + DWARF5 main CU + final 0 root terminator). -- 0. Validate .debug_info layout (crT CU + DWARF5 main CU + final 0 root terminator).
-- If validation fails, FAIL SAFELY by writing the existing sections verbatim (no malformed output, no synthetic CU append, no header patch). -- If validation fails, FAIL SAFELY by writing the existing sections verbatim (no malformed output, no synthetic CU append, no header patch).
-- 1. Build new .debug_abbrev using the main CU's abbrev offset → returns the offset of the duplicate main table (= #existing_abbrev). -- 1. Build new .debug_abbrev using the main CU's abbrev offset → returns the offset of the duplicate main table (= #existing_abbrev).
-- 2. Build new .debug_info by splicing inserted children into the main CU (patches main CU's unit_length + debug_abbrev_offset; -- 2. Build new .debug_info by splicing inserted children into the main CU (patches main CU's unit_length + debug_abbrev_offset;
-- preserves all original DIE bytes; does NOT append a synthetic CU). -- preserves all original DIE bytes; does NOT append a synthetic CU).
-- 3. Build .debug_line + .debug_aranges + .debug_rnglists (independent, F' tasks). -- 3. Build .debug_line + .debug_aranges + .debug_rnglists.
-- 4. Build .debug_loc (just a terminator). -- 4. Build .debug_loc (just a terminator).
-- --
-- .debug_str is left untouched in this slice: the inserted children use DW_FORM_string (inline) for all DW_AT_name values, -- .debug_str is left untouched in this slice:
-- so no new strings are required. The existing strings table is preserved verbatim. -- the inserted children use DW_FORM_string (inline) for all DW_AT_name values, so no new strings are required.
-- (The pre-Task 2 synthetic-CU build used .debug_str for CU name + comp_dir; that path is gone.) -- The existing strings table is preserved verbatim.
local basename = ctx.basename or duffle.basename_no_ext(elf_path) or DEFAULT_BASENAME local basename = ctx.basename or duffle.basename_no_ext(elf_path) or DEFAULT_BASENAME
if ctx.out_root and ctx.out_root ~= "" then if ctx.out_root and ctx.out_root ~= "" then
duffle.ensure_dir(ctx.out_root) duffle.ensure_dir(ctx.out_root)
+1 -2
View File
@@ -231,8 +231,7 @@ end
-- Offset computation + header generation -- Offset computation + header generation
-- ════════════════════════════════════════════════════════════════════════════ -- ════════════════════════════════════════════════════════════════════════════
-- Compute branch offsets as `target_word - branch_word - 1` -- Compute branch offsets as `target_word - branch_word - 1` (the standard MIPS branch-immediate encoding).
-- (the standard MIPS branch-immediate encoding).
-- @param labels table<string, integer> -- @param labels table<string, integer>
-- @param branches table[] -- @param branches table[]
-- @return BranchOffset[] -- @return BranchOffset[]
+3 -3
View File
@@ -16,7 +16,7 @@
-- ════════════════════════════════════════════════════════════════════════════ -- ════════════════════════════════════════════════════════════════════════════
-- Resolve `arg[0]` to an absolute-ish script directory so that `require("duffle")` resolves against `scripts/` regardless of CWD. -- Resolve `arg[0]` to an absolute-ish script directory so that `require("duffle")` resolves against `scripts/` regardless of CWD.
-- Note: this boilerplate is duplicated in 6 other entry scripts; a Phase-6 extraction target (`duffle.setup_package_path()`). -- Note: this boilerplate is duplicated in 6 other entry scripts; extraction target (`duffle.setup_package_path()`).
-- Bootstrap: see `ps1_meta.lua` for the rationale. -- Bootstrap: see `ps1_meta.lua` for the rationale.
-- Bootstrap: load `scripts/duffle_paths.lua` (sets package.path + package.cpath). -- Bootstrap: load `scripts/duffle_paths.lua` (sets package.path + package.cpath).
-- Uses `debug.getinfo` to find this file's own directory, so it works -- Uses `debug.getinfo` to find this file's own directory, so it works
@@ -312,8 +312,8 @@ local function render_module_report(dir, sources, results)
macros = total_macros, errors = total_errors, warnings = total_warnings, macros = total_macros, errors = total_errors, warnings = total_warnings,
} }
-- THE per-section dispatch. ONE loop over SECTION_RENDERERS. Each renderer writes its -- THE per-section dispatch. ONE loop over SECTION_RENDERERS.
-- header + content via the `add` closure (pre-bound above). -- Each renderer writes its header + content via the `add` closure (pre-bound above).
-- Adding a new section = 1 row here + 1 render_<thing>_section function. -- Adding a new section = 1 row here + 1 render_<thing>_section function.
for _, section in ipairs(SECTION_RENDERERS) do for _, section in ipairs(SECTION_RENDERERS) do
add(section.header) add(section.header)
+15 -18
View File
@@ -51,7 +51,7 @@ local duffle = dofile(_bootstrap_dir .. "../duffle_paths.lua")
--- @field marker_line integer --- @field marker_line integer
--- @field marker_pos integer --- @field marker_pos integer
--- @field after_paren integer --- @field after_paren integer
--- @field args string|nil -- trimmed marker args; valid Task 13 markers use "" --- @field args string|nil -- trimmed marker args""
--- @field has_parens boolean --- @field has_parens boolean
--- @field pending boolean --- @field pending boolean
--- @field superseded_by_marker_line integer|nil --- @field superseded_by_marker_line integer|nil
@@ -133,13 +133,13 @@ local QUALIFIER_KEYWORDS = {
["internal"] = true, ["LP_"] = true, ["global"] = true, ["gkknown"] = true, ["internal"] = true, ["LP_"] = true, ["global"] = true, ["gkknown"] = true,
} }
-- "ac_" prefix length on component names (e.g., `MipsAtomComp_(ac_X, ...)`). The components pass strips -- "ac_" prefix length on component names (e.g., `MipsAtomComp_(ac_X, ...)`).
-- this prefix to derive the macro name (e.g., `mac_X`). Single source of truth — was duplicated in two -- The components pass strips this prefix to derive the macro name (e.g., `mac_X`).
-- branches of the pre-refactor scan_source.
local AC_PREFIX = "ac_" local AC_PREFIX = "ac_"
local AC_PREFIX_LEN = 3 local AC_PREFIX_LEN = 3
-- Strip the "ac_" prefix from a component name. Returns the input unchanged if it doesn't start with the prefix. -- Strip the "ac_" prefix from a component name.
-- Returns the input unchanged if it doesn't start with the prefix.
-- @param raw_name string -- @param raw_name string
-- @return string -- @return string
local function strip_ac_prefix(raw_name) local function strip_ac_prefix(raw_name)
@@ -150,8 +150,6 @@ local function strip_ac_prefix(raw_name)
end end
-- Preserve a source marker until the following declaration parser observes it. -- Preserve a source marker until the following declaration parser observes it.
-- Task 13 records evidence only; Task 14 diagnoses non-empty args,
-- duplicates, dangling markers, and unrelated declaration placement.
local function push_skip_over_marker(out, marker) local function push_skip_over_marker(out, marker)
local markers = out.skip_over.markers local markers = out.skip_over.markers
local prior = markers[#markers] local prior = markers[#markers]
@@ -163,9 +161,9 @@ local function push_skip_over_marker(out, marker)
markers[#markers + 1] = marker markers[#markers + 1] = marker
end end
-- Attach the pending marker to the next declaration. The declaration form -- Attach the pending marker to the next declaration.
-- disambiguates whole atoms from components; unsupported declarations retain -- The declaration form disambiguates whole atoms from components;
-- placement evidence for annotation.lua and populate neither lookup table. -- unsupported declarations retain placement evidence for annotation.lua and populate neither lookup table.
local function associate_skip_over_marker(out, target_name, target_raw_name, target_kind, declaration_line, declaration_pos) local function associate_skip_over_marker(out, target_name, target_raw_name, target_kind, declaration_line, declaration_pos)
local markers = out.skip_over.markers local markers = out.skip_over.markers
local marker = markers[#markers] local marker = markers[#markers]
@@ -193,8 +191,8 @@ local function associate_skip_over_marker(out, target_name, target_raw_name, tar
end end
end end
-- Read a top-level comma-delimited argument list. Mirrors split_top_level_commas -- Read a top-level comma-delimited argument list. Mirrors split_top_level_commas in shape;
-- in shape; kept inline so scan_source can remain dependency-free. -- kept inline so scan_source can remain dependency-free.
local function read_top_level_args(text, pos) local function read_top_level_args(text, pos)
local args = {} local args = {}
pos = duffle.skip_ws_and_cmt(text, pos) pos = duffle.skip_ws_and_cmt(text, pos)
@@ -219,8 +217,8 @@ local function read_top_level_args(text, pos)
return args return args
end end
-- Parse a `Type*` chain (zero or more `*` separated by optional whitespace) -- Parse a `Type*` chain (zero or more `*` separated by optional whitespace) followed by the type ident.
-- followed by the type ident. Returns (type_name, pointer_depth) or nil. -- Returns (type_name, pointer_depth) or nil.
local function parse_type_chain(text, pos) local function parse_type_chain(text, pos)
if pos > #text then return nil end if pos > #text then return nil end
-- Skip leading whitespace before the type ident. -- Skip leading whitespace before the type ident.
@@ -419,8 +417,8 @@ local function parse_atom_dbg_skip_over(source, pos, ident_end, line_of, out)
return parse_skip_over_marker(source, pos, ident_end, line_of, out, "atom_dbg_skip_over") return parse_skip_over_marker(source, pos, ident_end, line_of, out, "atom_dbg_skip_over")
end end
-- Parse `atom_dbg_reg_default(R_X, <type>...)`; the second argument may be -- Parse `atom_dbg_reg_default(R_X, <type>...)`;
-- a `Type` or `Type*`/`Type**` chain. Records in `out.types[R_X]`. -- the second argument may be a `Type` or `Type*`/`Type**` chain. Records in `out.types[R_X]`.
local function parse_atom_dbg_reg_default(source, pos, ident_end, line_of, out) local function parse_atom_dbg_reg_default(source, pos, ident_end, line_of, out)
local open_paren = duffle.skip_ws_and_cmt(source, ident_end) local open_paren = duffle.skip_ws_and_cmt(source, ident_end)
if source:sub(open_paren, open_paren) ~= "(" then return open_paren + 1 end if source:sub(open_paren, open_paren) ~= "(" then return open_paren + 1 end
@@ -488,8 +486,7 @@ local function parse_mips_atom(source, pos, ident_end, line_of, out)
} }
elseif raw_name and ai_overrides then elseif raw_name and ai_overrides then
-- Record per-atom overrides even without atom_view. -- Record per-atom overrides even without atom_view.
out.atom_views[raw_name] = out.atom_views[raw_name] out.atom_views[raw_name] = out.atom_views[raw_name] or { atom_name = raw_name, binds_name = nil, reg_type_overrides = nil, info_line = line_of(lookahead) }
or { atom_name = raw_name, binds_name = nil, reg_type_overrides = nil, info_line = line_of(lookahead) }
out.atom_views[raw_name].reg_type_overrides = ai_overrides out.atom_views[raw_name].reg_type_overrides = ai_overrides
end end
brace_search_pos = info_after brace_search_pos = info_after
+19 -33
View File
@@ -9,8 +9,7 @@
--- `mac_insert_ot_tag_X` words must equal the GP0 cmd's expected packet size. --- `mac_insert_ot_tag_X` words must equal the GP0 cmd's expected packet size.
--- 5. **per-atom cycle budget** — sum each atom body's instruction latencies (per `duffle.INSTRUCTION_LATENCY`); report total. --- 5. **per-atom cycle budget** — sum each atom body's instruction latencies (per `duffle.INSTRUCTION_LATENCY`); report total.
--- ---
--- The orchestrator (`ps1_meta.lua`) wires this module in via the --- The orchestrator (`ps1_meta.lua`) wires this module in via the PASSES table:
--- PASSES table:
--- `["static-analysis"] = { module = "passes.static_analysis", kind = "validation", deps = {"word-counts", "components"}, --- `["static-analysis"] = { module = "passes.static_analysis", kind = "validation", deps = {"word-counts", "components"},
--- out = { { kind = "report", path_template = "<out_root>/<basename>.static_analysis.txt" } } }` --- out = { { kind = "report", path_template = "<out_root>/<basename>.static_analysis.txt" } } }`
--- ---
@@ -156,8 +155,8 @@ local OUTPUT_EXTENSION = ".static_analysis.txt"
--- @field o_arg2 string|nil -- second arg of O_(<a>, <b>) captures --- @field o_arg2 string|nil -- second arg of O_(<a>, <b>) captures
--- @field s_arg1 string|nil -- arg of S_(<a>) captures; nil for non-S_ tokens --- @field s_arg1 string|nil -- arg of S_(<a>) captures; nil for non-S_ tokens
-- Patterns for O_(<arg1>, <arg2>) and S_(<arg>) captures. UNANCHORED — the substring can appear -- Patterns for O_(<arg1>, <arg2>) and S_(<arg>) captures.
-- anywhere in the token (e.g., `load_word(R_T0, R_TapePtr, O_(Binds_X, field))` matches at position ~24). -- UNANCHORED, the substring can appea anywhere in the token (e.g., `load_word(R_T0, R_TapePtr, O_(Binds_X, field))` matches at position ~24).
-- The binds_name match is deferred to check_abi_handoff (which compares tc.o_arg1 == atom.info.binds). -- The binds_name match is deferred to check_abi_handoff (which compares tc.o_arg1 == atom.info.binds).
local O_PATTERN = "O_%(([%w_]+),%s*([%w_]+)%s*%)" local O_PATTERN = "O_%(([%w_]+),%s*([%w_]+)%s*%)"
local S_PATTERN = "S_%(([%w_]+)%s*%)" local S_PATTERN = "S_%(([%w_]+)%s*%)"
@@ -181,8 +180,8 @@ local function classify_tokens(tokens)
local is_load_word = ident == "load_word" local is_load_word = ident == "load_word"
local is_store_word = ident == "store_word" local is_store_word = ident == "store_word"
-- Per-check pre-computes (R3 lift). Each pre-compute eliminates one per-token regex/string-find -- Per-check pre-computes (R3 lift).
-- call from check_abi_handoff / check_gpu_portstore_shape. -- Each pre-compute eliminates one per-token regex/string-find call from check_abi_handoff / check_gpu_portstore_shape.
local mac_format_shape = nil local mac_format_shape = nil
local is_gte_store = false local is_gte_store = false
local is_ot_tag = false local is_ot_tag = false
@@ -235,10 +234,8 @@ local function classify_tokens(tokens)
s_arg1 = s_arg1, s_arg1 = s_arg1,
} }
-- Advance the nop run for the NEXT token. -- Advance the nop run for the NEXT token.
if nop_words > 0 then if nop_words > 0 then nop_run = nop_run + nop_words
nop_run = nop_run + nop_words else nop_run = 0
else
nop_run = 0
end end
end end
return tc return tc
@@ -435,8 +432,8 @@ local function check_abi_handoff(atom, pipe_ctx, findings)
local found_field_set = {} local found_field_set = {}
local found_advance = false local found_advance = false
-- Reads from tc_entry fields pre-computed by classify_tokens (R3 lift). Eliminates 3 per-token -- Reads from tc_entry fields pre-computed by classify_tokens (R3 lift).
-- string-find/match calls (R_TapePtr + O_(binds_name,...) + bind_re) → 3 O(1) field reads. -- Eliminates 3 per-token string-find/match calls (R_TapePtr + O_(binds_name,...) + bind_re) → 3 O(1) field reads.
for tok_idx = 1, #tokens do for tok_idx = 1, #tokens do
local tc_entry = tc[tok_idx] local tc_entry = tc[tok_idx]
-- scan: load_word(R_*, R_TapePtr, O_(<Binds_X>, <field>)) -- scan: load_word(R_*, R_TapePtr, O_(<Binds_X>, <field>))
@@ -607,14 +604,10 @@ local function analyze_atom_paths(atom)
end end
-- A token is a terminator if it's `mac_yield`. -- A token is a terminator if it's `mac_yield`.
local function is_terminator(tok_idx) local function is_terminator(tok_idx) return tc[tok_idx].is_yield end
return tc[tok_idx].is_yield
end
-- A token is a "branch" if the classification says so. -- A token is a "branch" if the classification says so.
local function is_branch(tok_idx) local function is_branch(tok_idx) return tc[tok_idx].is_branch end
return tc[tok_idx].is_branch
end
local function successors(tok_idx) local function successors(tok_idx)
local tok = tokens[tok_idx].tok local tok = tokens[tok_idx].tok
if is_terminator(tok_idx) then if is_terminator(tok_idx) then
@@ -639,9 +632,7 @@ local function analyze_atom_paths(atom)
return succ, nil return succ, nil
end end
-- Normal token: just the next one -- Normal token: just the next one
if tok_idx + 1 <= n then if tok_idx + 1 <= n then return { tok_idx + 1 }, nil end
return { tok_idx + 1 }, nil
end
return {}, nil return {}, nil
end end
@@ -722,14 +713,9 @@ end
--- Per-source check that emits one finding per unknown macro seen --- Per-source check that emits one finding per unknown macro seen
--- (deduplicated across atoms so the warning section doesn't get spammed with N copies of "macro X not in duffle.INSTRUCTION_LATENCY"). --- (deduplicated across atoms so the warning section doesn't get spammed with N copies of "macro X not in duffle.INSTRUCTION_LATENCY").
--- Reuses `analyze_atom_paths`'s per-atom unknown_macros discovery (it's the canonical place that walks tokens --- Per-atom: emit one finding per unknown macro seen, deduplicated across atoms
--- and computes per-token cycle costs). We just sort + emit. --- (so the warning section doesn't get spammed with N copies of "macro X not in duffle.INSTRUCTION_LATENCY").
--- Per-atom: emit one finding per unknown macro seen, deduplicated across atoms (so the warning --- Reuses `analyze_atom_paths`'s per-atom unknown_macros discovery (it's the canonical place that walks tokens and computes per-token cycle costs).
--- section doesn't get spammed with N copies of "macro X not in duffle.INSTRUCTION_LATENCY").
--- Reuses `analyze_atom_paths`'s per-atom unknown_macros discovery (it's the canonical place that walks tokens
--- and computes per-token cycle costs). We just sort + emit.
--- Signature changed in Stage 1B: `(atom, pipe_ctx, findings)` — the `unknown_seen` dedup table lives on
--- `pipe_ctx` so it persists across the per-atom loop in validate().
local function check_per_atom_cycle_budget(atom, pipe_ctx, findings) local function check_per_atom_cycle_budget(atom, pipe_ctx, findings)
local p = atom.paths or {} local p = atom.paths or {}
for _, name in ipairs(p.unknown_macros or {}) do for _, name in ipairs(p.unknown_macros or {}) do
@@ -782,8 +768,8 @@ local function validate(ctx, src)
end end
-- pipe_ctx: the cross-atom shared state for the per-atom pipeline (Fleury "expose structure"). -- pipe_ctx: the cross-atom shared state for the per-atom pipeline (Fleury "expose structure").
-- Pre-allocated here, mutated by each per-atom check call below. Replaces the per-check -- Pre-allocated here, mutated by each per-atom check call below.
-- local tables that used to live inside each check_* function body. -- Replaces the per-check local tables that used to live inside each check_* function body.
-- info_by_atom — atom_name -> atom_info (built once; check_abi_handoff reads it) -- info_by_atom — atom_name -> atom_info (built once; check_abi_handoff reads it)
-- binds_index — Binds_X -> binds struct (built once; check_abi_handoff reads it) -- binds_index — Binds_X -> binds struct (built once; check_abi_handoff reads it)
-- unknown_seen — macro_name -> first atom line (accumulated across atoms; check_per_atom_cycle_budget dedups) -- unknown_seen — macro_name -> first atom line (accumulated across atoms; check_per_atom_cycle_budget dedups)
@@ -1066,9 +1052,9 @@ function M.run(ctx)
local errors = {} local errors = {}
local warnings = {} local warnings = {}
-- Aggregate per-DIRECTORY (per-module). One static_analysis.txt per source-directory, emitted only if the directory contains at least one atom. -- Aggregate per-DIRECTORY (per-module).
-- One static_analysis.txt per source-directory, emitted only if the directory contains at least one atom.
-- Empty-source directories (e.g. duffle headers with no atoms) produce no report. -- Empty-source directories (e.g. duffle headers with no atoms) produce no report.
--
-- Group sources by `src.dir`. The first component of `dir` is the module name (e.g. "code/duffle" -> "duffle", "code/gte_hello" -> "gte_hello"). -- Group sources by `src.dir`. The first component of `dir` is the module name (e.g. "code/duffle" -> "duffle", "code/gte_hello" -> "gte_hello").
-- Output path is `<out_root>/<module_basename>.static_analysis.txt`. -- Output path is `<out_root>/<module_basename>.static_analysis.txt`.
local by_dir = ctx.by_dir or duffle.group_sources_by_dir(ctx.sources) local by_dir = ctx.by_dir or duffle.group_sources_by_dir(ctx.sources)
+1 -12
View File
@@ -15,7 +15,7 @@
-- ════════════════════════════════════════════════════════════════════════════ -- ════════════════════════════════════════════════════════════════════════════
-- Resolve `arg[0]` to an absolute-ish script directory so that `require("duffle")` resolves against `scripts/` regardless of CWD. -- Resolve `arg[0]` to an absolute-ish script directory so that `require("duffle")` resolves against `scripts/` regardless of CWD.
-- Note: this boilerplate is duplicated in 6 other entry scripts; a Phase-6 extraction target (`duffle.setup_package_path()`). -- Note: this boilerplate is duplicated in 6 other entry scripts; extraction target (`duffle.setup_package_path()`).
-- Bootstrap: see `ps1_meta.lua` for the rationale. -- Bootstrap: see `ps1_meta.lua` for the rationale.
-- Bootstrap: load `scripts/duffle_paths.lua` (sets package.path + package.cpath). -- Bootstrap: load `scripts/duffle_paths.lua` (sets package.path + package.cpath).
-- Uses `debug.getinfo` to find this file's own directory, so it works both standalone and when require'd from the orchestrator. -- Uses `debug.getinfo` to find this file's own directory, so it works both standalone and when require'd from the orchestrator.
@@ -80,7 +80,6 @@ local M = {}
--- For most tokens (regular MIPS instructions) this returns 1. --- For most tokens (regular MIPS instructions) this returns 1.
--- For `mac_X(...)` calls, this returns the resolved word count from `wc` (recursively if needed). For `nop2` etc., returns wc[name]. --- For `mac_X(...)` calls, this returns the resolved word count from `wc` (recursively if needed). For `nop2` etc., returns wc[name].
--- For unknown macros, returns 1 and (optionally) warns. --- For unknown macros, returns 1 and (optionally) warns.
---
--- @param token string -- a single token from split_top_level_commas --- @param token string -- a single token from split_top_level_commas
--- @param wc WordCounts -- the shared word-count table --- @param wc WordCounts -- the shared word-count table
--- @return integer --- @return integer
@@ -101,14 +100,6 @@ end
-- │ Shared utility: scan_dir │ -- │ Shared utility: scan_dir │
-- └────────────────────────────────────────────────────────────────────┘ -- └────────────────────────────────────────────────────────────────────┘
--- Recursively scan a directory for files matching a glob suffix.
---
--- The `.macs.h` files produced by the components pass always live at `<project_root>/<module>/gen/`.
--- Native walk via lfs.attributes + lfs.dir: ~2ms vs ~56ms for the prior `dir /b /s` subprocess.
---
--- @param dir string -- directory to scan (absolute or relative)
--- @param suffix string -- file pattern, e.g. "*.macs.h"
--- @return string[]
-- Cache the scan_dir result per (dir, suffix) in package.loaded. -- Cache the scan_dir result per (dir, suffix) in package.loaded.
-- The cache persists for the lifetime of the Lua process (cleared when ps1_meta.lua exits). -- The cache persists for the lifetime of the Lua process (cleared when ps1_meta.lua exits).
-- If a build removes/creates .macs.h files mid-process, the caller can invalidate by calling `M._invalidate_scan_cache()`. -- If a build removes/creates .macs.h files mid-process, the caller can invalidate by calling `M._invalidate_scan_cache()`.
@@ -116,7 +107,6 @@ local SCAN_CACHE_KEY = "__word_count_eval_scan_cache__"
--- Scan `code/` for files matching `suffix` (e.g. `*.macs.h`). --- Scan `code/` for files matching `suffix` (e.g. `*.macs.h`).
--- Native directory enumeration via lfs (~2ms). Zero subprocess spawns. --- Native directory enumeration via lfs (~2ms). Zero subprocess spawns.
---
--- @param dir string -- project root directory --- @param dir string -- project root directory
--- @param suffix string -- file pattern, e.g. "*.macs.h" --- @param suffix string -- file pattern, e.g. "*.macs.h"
--- @return string[] --- @return string[]
@@ -160,7 +150,6 @@ function M._invalidate_scan_cache() package.loaded[SCAN_CACHE_KEY] = nil end
--- Load metadata.h + scan for existing *.macs.h files into ctx.shared.word_counts. --- Load metadata.h + scan for existing *.macs.h files into ctx.shared.word_counts.
--- Loading the .macs.h files is idempotent: entries from later (current-build) .macs.h files override metadata.h entries of the same name. --- Loading the .macs.h files is idempotent: entries from later (current-build) .macs.h files override metadata.h entries of the same name.
---
--- @param ctx PassCtx --- @param ctx PassCtx
--- @return PassResult --- @return PassResult
function M.run(ctx) function M.run(ctx)
+7 -11
View File
@@ -1,17 +1,15 @@
-- autoexec.lua - pcsx_debug_helper plugin entry point. -- autoexec.lua - pcsx_debug_helper plugin entry point.
-- Packaged in scripts/pcsx_debug_helper.zip. Loaded by pcsx-redux via -- Packaged in scripts/pcsx_debug_helper.zip. Loaded by pcsx-redux via the -archive CLI flag (see scripts/launch_pcsx_debug.ps1).
-- the -archive CLI flag (see scripts/launch_pcsx_debug.ps1).
-- --
-- Registers two web handlers for external CLI tools: -- Registers two web handlers for external CLI tools:
-- /api/v1/lua/gte - full GTE state (32 data + 32 control regs + PC) -- /api/v1/lua/gte - full GTE state (32 data + 32 control regs + PC)
-- /api/v1/lua/gp - GP state summary (screenshot endpoint + VRAM endpoint refs) -- /api/v1/lua/gp - GP state summary (screenshot endpoint + VRAM endpoint refs)
-- --
-- The GTE handler reads COP2 regs via PCSX.getRegisters().CP2D/CP2C. The -- The GTE handler reads COP2 regs via PCSX.getRegisters().CP2D/CP2C.
-- pcsx-redux gdb stub doesn't expose COP2, so this is the only way for -- The pcsx-redux gdb stub doesn't expose COP2, so this is the only way for external tools to see GTE state.
-- external tools to see GTE state.
-- --
-- The GP handler is a thin pointer: pcsx-redux's Lua API exposes only PCSX.GPU.takeScreenShot() -- The GP handler is a thin pointer:
-- (no GPUSTAT, no GP0/GP1 command log, no display state). For richer GP state, the existing web endpoints are the practical path: -- pcsx-redux's Lua API exposes only PCSX.GPU.takeScreenShot() (no GPUSTAT, no GP0/GP1 command log, no display state). For richer GP state, the existing web endpoints are the practical path:
-- /api/v1/state/still - PNG screenshot -- /api/v1/state/still - PNG screenshot
-- /api/v1/gpu/vram/raw - VRAM raw bytes (1MB) -- /api/v1/gpu/vram/raw - VRAM raw bytes (1MB)
-- --
@@ -48,8 +46,6 @@ local function register_handlers()
end end
local ok, err = pcall(register_handlers) local ok, err = pcall(register_handlers)
if ok then if ok then print("[pcsx_debug_helper] handlers registered: gte, gp")
print("[pcsx_debug_helper] handlers registered: gte, gp") else print("[pcsx_debug_helper] registration failed: " .. tostring(err))
else
print("[pcsx_debug_helper] registration failed: " .. tostring(err))
end end
+18 -22
View File
@@ -1,8 +1,6 @@
--- ps1_meta.lua — Orchestrator entry point for the tape-atom metaprogram pipeline. --- ps1_meta.lua — Orchestrator entry point for the tape-atom metaprogram.
--- ---
--- Dispatches to pass modules under `scripts/passes/`, resolving --- Dispatches to pass modules under `scripts/passes/`, resolving dependencies topologically (Kahn's algorithm + cycle detection).
--- dependencies topologically (Kahn's algorithm + cycle detection).
--- Single CLI surface (`--<pass>` flags + auto-dep expansion + --dry-run).
--- ---
--- **Architecture**: --- **Architecture**:
--- - **PASSES table** — declarative dep graph (data, not code). --- - **PASSES table** — declarative dep graph (data, not code).
@@ -10,7 +8,7 @@
--- - **parse_args** → **build_ctx** (just opens + reads source files; no inline scanning) → **topo_sort** → **dispatch_passes**. --- - **parse_args** → **build_ctx** (just opens + reads source files; no inline scanning) → **topo_sort** → **dispatch_passes**.
--- - The first pass in the dep graph is `scan-source` (see `passes/scan_source.lua`). --- - The first pass in the dep graph is `scan-source` (see `passes/scan_source.lua`).
--- It calls `duffle.scan_source` once per source to produce the fat `SourceScan` payload, which is attached to each `src.scan`. --- It calls `duffle.scan_source` once per source to produce the fat `SourceScan` payload, which is attached to each `src.scan`.
--- Every other pass that reads source structure depends on `scan-source` and consumes `src.scan` as a read-only payload. --- Every other pass that reads source structure depends on `scan-source` and consumes `src.scan` as a read-only.
--- ---
--- **Conventions**: tabs (1/level), EmmyLua annotations, no regex, --- **Conventions**: tabs (1/level), EmmyLua annotations, no regex,
--- Lua 5.3 compatible. --- Lua 5.3 compatible.
@@ -152,7 +150,7 @@ local PASSES = {
module = "passes.atoms_source_map", module = "passes.atoms_source_map",
kind = "header-output", kind = "header-output",
deps = {"word-counts", "components"}, deps = {"word-counts", "components"},
desc = "Emit gen/<basename>.atoms.sourcemap.txt (per-.word C source line map for gdb debugging) AND gen/<basename>.atoms.provenance.txt (per-.word provenance; each word tagged with its call-site file:line and, when emitted by a mac_X(...) component invocation, the component's definition file:line). Consumed by passes/dwarf_injection.lua to synthesize DW_TAG_inlined_subroutine instances for source-level Step Into on component invocations (Phase 3 — debug_ux).", desc = "Emit gen/<basename>.atoms.sourcemap.txt (per-.word C source line map for gdb debugging) AND gen/<basename>.atoms.provenance.txt (per-.word provenance; each word tagged with its call-site file:line and, when emitted by a mac_X(...) component invocation, the component's definition file:line). Consumed by passes/dwarf_injection.lua to synthesize DW_TAG_inlined_subroutine instances for source-level Step Into on component invocations.",
out = { out = {
{ kind = "report", path_template = "<out_root>/<basename>.atoms.sourcemap.txt" }, { kind = "report", path_template = "<out_root>/<basename>.atoms.sourcemap.txt" },
{ kind = "report", path_template = "<out_root>/<basename>.atoms.provenance.txt" }, { kind = "report", path_template = "<out_root>/<basename>.atoms.provenance.txt" },
@@ -206,8 +204,15 @@ local PASS_FLAG_TO_NAME = {
} }
local ALL_PASS_NAMES = { local ALL_PASS_NAMES = {
"scan-source", "word-counts", "components", "annotation", "scan-source",
"offsets", "static-analysis", "atoms-source-map", "dwarf-injection", "report", "word-counts",
"components",
"annotation",
"offsets",
"static-analysis",
"atoms-source-map",
"dwarf-injection",
"report",
} }
--- Append every pass name to args.requested_set. Used by --all and by the "default to --all if no pass flags were given" fallback. --- Append every pass name to args.requested_set. Used by --all and by the "default to --all if no pass flags were given" fallback.
@@ -286,8 +291,8 @@ FLAG_HANDLERS["--metadata"] = function(args, argv, arg_idx) args.metadata
FLAG_HANDLERS["--out-root"] = function(args, argv, arg_idx) args.out_root = argv[arg_idx + 1]; return arg_idx + 1 end FLAG_HANDLERS["--out-root"] = function(args, argv, arg_idx) args.out_root = argv[arg_idx + 1]; return arg_idx + 1 end
FLAG_HANDLERS["--project-root"] = function(args, argv, arg_idx) args.project_root = argv[arg_idx + 1]; return arg_idx + 1 end FLAG_HANDLERS["--project-root"] = function(args, argv, arg_idx) args.project_root = argv[arg_idx + 1]; return arg_idx + 1 end
-- Per-pass stash flags. Read by `passes/atoms_source_map.lua` to opt into the -- Per-pass stash flags. Read by `passes/atoms_source_map.lua` to opt into the post-link gdb-runtime emission.
-- post-link gdb-runtime emission. Same shape as the existing per-flag handlers: -- Same shape as the existing per-flag handlers:
-- mutates `args.flags` (which propagates into `ctx.flags`). -- mutates `args.flags` (which propagates into `ctx.flags`).
FLAG_HANDLERS["--gdb-runtime"] = function(args) args.flags = args.flags or {}; args.flags.gdb_runtime = true end FLAG_HANDLERS["--gdb-runtime"] = function(args) args.flags = args.flags or {}; args.flags.gdb_runtime = true end
FLAG_HANDLERS["--elf"] = function(args, argv, arg_idx) args.flags = args.flags or {}; args.flags.elf_path = argv[arg_idx + 1]; return arg_idx + 1 end FLAG_HANDLERS["--elf"] = function(args, argv, arg_idx) args.flags = args.flags or {}; args.flags.elf_path = argv[arg_idx + 1]; return arg_idx + 1 end
@@ -300,7 +305,7 @@ end
-- G' (atom locals) is now consolidated into --dwarf-injection; no separate flag. -- G' (atom locals) is now consolidated into --dwarf-injection; no separate flag.
-- Pass-flag handler. Reads the closed-set table, expands --all, appends to requested_set. Single-statement, no nesting. -- Pass-flag handler. Reads the closed-set table, expands --all, appends to requested_set.
FLAG_HANDLERS[PASS_FLAG_DISPATCH_KEY] = function(args, a) FLAG_HANDLERS[PASS_FLAG_DISPATCH_KEY] = function(args, a)
local name = PASS_FLAG_TO_NAME[a] local name = PASS_FLAG_TO_NAME[a]
if name == ALL_PASSES_SENTINEL then if name == ALL_PASSES_SENTINEL then
@@ -311,7 +316,6 @@ FLAG_HANDLERS[PASS_FLAG_DISPATCH_KEY] = function(args, a)
end end
--- Parse argv into a structured table. Validates against a closed enum. --- Parse argv into a structured table. Validates against a closed enum.
---
--- @param argv string[] --- @param argv string[]
--- @return ParsedArgs --- @return ParsedArgs
local function parse_args(argv) local function parse_args(argv)
@@ -371,7 +375,6 @@ end
--- Build the PassCtx from parsed args. Reads each source file once at startup; --- Build the PassCtx from parsed args. Reads each source file once at startup;
--- passes consume `src.text`, not the path (path is preserved for error reporting). --- passes consume `src.text`, not the path (path is preserved for error reporting).
---
--- @param args ParsedArgs --- @param args ParsedArgs
--- @return PassCtx --- @return PassCtx
local function build_ctx(args) local function build_ctx(args)
@@ -425,7 +428,6 @@ end
-- ════════════════════════════════════════════════════════════════════════════ -- ════════════════════════════════════════════════════════════════════════════
--- Compute the dep-closure of `requested_set`: include every pass name transitively required by the requested set. --- Compute the dep-closure of `requested_set`: include every pass name transitively required by the requested set.
---
--- @param passes table<string, PassDescriptor> --- @param passes table<string, PassDescriptor>
--- @param requested_set string[] --- @param requested_set string[]
--- @return table<string, boolean> -- set of pass names needed (including transitive deps) --- @return table<string, boolean> -- set of pass names needed (including transitive deps)
@@ -461,7 +463,6 @@ local function count_entries(t)
end end
--- Compute in-degrees for the Kahn sort: for each pass in `needed`, the number of its deps that are also in `needed`. --- Compute in-degrees for the Kahn sort: for each pass in `needed`, the number of its deps that are also in `needed`.
---
--- @param passes table<string, PassDescriptor> --- @param passes table<string, PassDescriptor>
--- @param needed table<string, boolean> --- @param needed table<string, boolean>
--- @return table<string, integer> --- @return table<string, integer>
@@ -479,8 +480,7 @@ local function compute_in_degrees(passes, needed)
end end
--- Seed the Kahn ready queue with passes whose in-degree is 0, sorted alphabetically for deterministic execution order. --- Seed the Kahn ready queue with passes whose in-degree is 0, sorted alphabetically for deterministic execution order.
--- -- @param in_degree table<string, integer>
--- @param in_degree table<string, integer>
--- @return string[] --- @return string[]
local function seed_ready_queue(in_degree) local function seed_ready_queue(in_degree)
local ready = {} local ready = {}
@@ -518,7 +518,6 @@ end
--- Topologically sort the requested pass set, augmented with all transitive deps. --- Topologically sort the requested pass set, augmented with all transitive deps.
--- Detects cycles and errors out with details. --- Detects cycles and errors out with details.
---
--- @param passes table<string, PassDescriptor> --- @param passes table<string, PassDescriptor>
--- @param requested_set string[] --- @param requested_set string[]
--- @return string[] -- execution order --- @return string[] -- execution order
@@ -552,7 +551,6 @@ end
--- Render the dep graph as ASCII art. Output width capped at 78 columns. --- Render the dep graph as ASCII art. Output width capped at 78 columns.
--- Falls back to the simpler "Resolved dependency order" list only if graph width exceeds terminal width. --- Falls back to the simpler "Resolved dependency order" list only if graph width exceeds terminal width.
---
--- @param passes table<string, PassDescriptor> --- @param passes table<string, PassDescriptor>
--- @param requested string[] -- originally-requested passes (subset of closed) --- @param requested string[] -- originally-requested passes (subset of closed)
--- @param closed string[] -- dep-closed execution order --- @param closed string[] -- dep-closed execution order
@@ -572,8 +570,7 @@ local function render_dep_graph(passes, requested, closed)
add("") add("")
-- Data-driven ASCII graph built from the actual PASSES table. -- Data-driven ASCII graph built from the actual PASSES table.
-- Shows the source -> scan_source -> pass chain. Each pass is -- Shows the source -> scan_source -> pass chain. Each pass is shown once; edges are "feeds into" arrows based on deps.
-- shown once; edges are "feeds into" arrows based on deps.
add("[ps1_meta] Pass graph (read top-to-bottom; edges = 'feeds into'):") add("[ps1_meta] Pass graph (read top-to-bottom; edges = 'feeds into'):")
add("") add("")
@@ -658,7 +655,6 @@ local function report_validation_errors(pass_name, pass, result)
end end
-- (internal) Run each pass in `order` in topological sequence. -- (internal) Run each pass in `order` in topological sequence.
--
-- @param ctx PassCtx -- @param ctx PassCtx
-- @param order string[] -- @param order string[]
-- @return boolean -- true if any validation errors were reported -- @return boolean -- true if any validation errors were reported