Fix off by one ahead issue with stepping into atoms. Support for local register symbols used in atoms + atom bindings locals in gdb.

This commit is contained in:
2026-07-13 12:41:48 -04:00
parent b43d22008e
commit 2d901003f9
6 changed files with 1217 additions and 129 deletions
+6 -7
View File
@@ -14,7 +14,7 @@ typedef U4 const MipsCode;
typedef Slice_(MipsCode);
typedef Slice_MipsCode MipsAtom;
#define MipsAtom_(sym) MipsCode tmpl(code,sym) [] align_(4) =
#define MipsAtom_(sym) MipsCode sym [] align_(4) =
// Bare form: file-scope declaration with hardcoded body.
// Used for components with no args (e.g., ac_load_tri_indices) or identifier-args (hardcoded register names).
@@ -89,13 +89,13 @@ FI_ void tb_init(TapeBuilder* tb, FArena* arena) { tb->ptr = arena->start
FI_ TapeBuilder tb_make_old( FArena* arena) { return (TapeBuilder){ arena->start, 0 }; }
FI_ TapeBuilder tb_make(Slice mem) { return (TapeBuilder){ mem.ptr, mem.len, 0 }; }
#define tb_emit_(tb, atom) tb_emit(tb, tmpl(code,atom))
#define tb_emit_(tb, atom) tb_emit(tb, atom)
FI_ void tb_emit(TapeBuilder* tb, MipsCode* atom) { u4_r(tb->ptr)[tb->used] = u4_(atom); ++ tb->used; }
FI_ void tb_data(TapeBuilder* tb, U4 data) { u4_r(tb->ptr)[tb->used] = u4_(data); ++ tb->used; }
FI_ Slice_U4 tb_end (TapeBuilder* tb) { tb_emit(tb,code_tape_exit); return (Slice_U4){ C_(U4*,tb->ptr), tb->used }; }
FI_ Slice_U4 tb_end (TapeBuilder* tb) { tb_emit(tb,tape_exit); return (Slice_U4){ C_(U4*,tb->ptr), tb->used }; }
FI_ Slice_U4 tb_slice(TapeBuilder tb) { return (Slice_U4){ C_(U4*,tb.ptr), tb.used }; }
#define tb_scope(tb) for(U4 tbs_once=0;tbs_once==0;++tbs_once,tb_emit(tb,code_tape_exit))
#define tb_scope(tb) for(U4 tbs_once=0;tbs_once==0;++tbs_once,tb_emit(tb,tape_exit))
#pragma endregion Tape Drive
@@ -237,9 +237,8 @@ FI_ void atombuilder_unroll(MipsAtomBuilder_R ab, Slice_MipsCode_R code) {
// When done authoring, utilize this to cap-off the atom
FI_ void atombuilder_end(MipsAtomBuilder_R ab) {
LP_ MipsAtom_(yield) { mac_yield() };
mem_copy(ab->start, u4_(code_yield), S_(code_yield));
mem_bump(ab->start, ab->capacity, & ab->used, S_(code_yield));
mem_copy(ab->start, u4_(ac_yield), S_(ac_yield));
mem_bump(ab->start, ab->capacity, & ab->used, S_(ac_yield));
}
#define mipsatom_from_builder(ab) (MipsAtom){ab.start, ab.used}
+8 -8
View File
@@ -261,7 +261,7 @@ void update(PrimitiveArena* pa, U4* ordering_buf)
LP_ U4 mem_temp_tape[512]; FArena tape_arena; farena_init(& tape_arena, slice_ut_arr(mem_temp_tape));
TapeBuilder tb = tb_make_old(&tape_arena); tb_scope(& tb) {
tb_emit(& tb, code_rbind_cube_g4_face);
tb_emit(& tb, rbind_cube_g4_face);
tb_data(& tb, prim_cursor);
tb_data(& tb, u4_(smem.cube.faces));
tb_data(& tb, u4_(smem.cube.verts));
@@ -269,10 +269,10 @@ void update(PrimitiveArena* pa, U4* ordering_buf)
for (U4 i = 0; i < Cube_num_faces; i++) {
// Two triangles per quad face: (x,y,z) and (x,z,w)
tb_emit(& tb, code_cube_g4_face);
tb_emit(& tb, cube_g4_face);
}
tb_emit(& tb, code_sync_primitive_arena);
tb_emit(& tb, sync_primitive_arena);
tb_data(& tb, u4_(& pa->used));
tb_data(& tb, prim_base);
}
@@ -350,20 +350,20 @@ void update(PrimitiveArena* pa, U4* ordering_buf)
// Prepare the tape. (Push protocol to tape)
LP_ U4 mem_temp_tape[512];
TapeBuilder tb = tb_make(slice_ut_arr(mem_temp_tape)); tb_scope(& tb) {
tb_emit(& tb, code_set_gte_world);
tb_emit(& tb, set_gte_world);
tb_data(& tb, u4_(& smem.tform_world));
tb_emit(& tb, code_rbind_floor_f3_face);
tb_emit(& tb, rbind_floor_f3_face);
// TODO(Ed): Just use a single context struct ref
tb_data(& tb, prim_cursor);
tb_data(& tb, u4_(smem.floor.faces));
tb_data(& tb, u4_(smem.floor.verts));
tb_data(& tb, u4_(ordering_buf));
for (U4 i = 0; i < Floor_num_faces; i++) {
tb_emit(& tb, code_floor_f3_face);
tb_emit(& tb, floor_f3_face);
}
// After code_floor_f3_face iterations complete, the primitive arena's used counter needs updating.
tb_emit(& tb, code_sync_primitive_arena);
// After floor_f3_face iterations complete, the primitive arena's used counter needs updating.
tb_emit(& tb, sync_primitive_arena);
tb_data(& tb, u4_(& pa->used));
tb_data(& tb, prim_base);
}
+79 -33
View File
@@ -385,61 +385,107 @@ function build-gte_hello {
link-modules $link_modules $elf $link_args
make-binary $elf $exe
# Post-link: emit ONLY build/gen/gdb_tape_atoms_runtime.gdb. The per-source
# *.atoms.sourcemap.txt was already generated by the pre-link --all call,
# so we skip --atoms-source-map here to avoid re-doing the work. The
# gdb-runtime emission requires --elf (for nm-based address lookup) so it
# MUST happen post-link. ON by default per user pref 2026-07-11.
# TODO(Ed): Do both -gdb-runtime and dwarf-injection passes in a single ps1-meta call.
# Post-link: emit ONLY build/gen/gdb_tape_atoms_runtime.gdb.
# The per-source *.atoms.sourcemap.txt was already generated by the pre-link --all call,
# so we skip --atoms-source-map here to avoid re-doing the work.
# The gdb-runtime emission requires --elf (for nm-based address lookup) so it MUST happen post-link.
ps1-meta -sources $atom_sources -metadata $path_atom_metadata `
-out_root (join-path $path_build 'gen') `
-passes @('--gdb-runtime') `
-extra_args @('--elf', $elf)
# F' + G' consolidated: --dwarf-injection now emits 7 .bin blobs
# (.debug_line, .debug_aranges, .debug_rnglists, .debug_info, .debug_abbrev, .debug_str, .debug_loc) all in one pass.
ps1-meta -sources $atom_sources -metadata $path_atom_metadata `
-out_root (join-path $path_build 'gen') `
-passes @('--dwarf-injection') `
-extra_args @('--elf', $elf)
# F' track: post-link DWARF injection. The new Lua pass writes
# build/gen/<basename>.dwarf_*.bin blobs; we splice them into a COPY
# of the ELF via objcopy --update-section (works fine from PowerShell).
#TODO(Ed): Move the below into ps-1 meta pass to reduce syscall latency?
# F' track: post-link DWARF injection. The new Lua pass writes build/gen/<basename>.dwarf_*.bin blobs;
# we splice them into a COPY of the ELF via objcopy --update-section (works fine from PowerShell).
# The un-injected $elf + $exe are unchanged (shipping binary).
$dwarfLineBin = Join-Path (Join-Path $path_build 'gen') 'hello_gte.dwarf_line.bin'
$dwarfArangesBin = Join-Path (Join-Path $path_build 'gen') 'hello_gte.dwarf_aranges.bin'
$dwarfRnglistsBin = Join-Path (Join-Path $path_build 'gen') 'hello_gte.dwarf_rnglists.bin'
$injectElf = Join-Path $path_build 'hello_gte.dwarf-injected.elf'
if ((Test-Path $dwarfLineBin) -and (Test-Path $dwarfArangesBin) -and (Test-Path $dwarfRnglistsBin)) {
if ((Test-Path $dwarfLineBin) -and (Test-Path $dwarfArangesBin) -and (Test-Path $dwarfRnglistsBin))
{
Write-Host "[build] DWARF-injecting $elf -> $injectElf"
Copy-Item -LiteralPath $elf -Destination $injectElf
& $Objcopy --update-section ".debug_line=$dwarfLineBin" $injectElf
if ($LASTEXITCODE -ne 0) {
$last_exit_code_error = $LASTEXITCODE -ne 0
if ($last_exit_code_error) {
Write-Warning "[build] objcopy .debug_line update failed (exit $LASTEXITCODE); removing $injectElf"
Remove-Item -LiteralPath $injectElf -ErrorAction SilentlyContinue
} else {
& $Objcopy --update-section ".debug_aranges=$dwarfArangesBin" $injectElf
return;
}
& $Objcopy --update-section ".debug_aranges=$dwarfArangesBin" $injectElf
$last_exit_code_error = $LASTEXITCODE -ne 0
if ($LASTEXITCODE -ne 0) {
Write-Warning "[build] objcopy .debug_aranges update failed (exit $LASTEXITCODE); removing $injectElf"
Remove-Item -LiteralPath $injectElf -ErrorAction SilentlyContinue
return;
}
& $Objcopy --update-section ".debug_rnglists=$dwarfRnglistsBin" $injectElf
if ($LASTEXITCODE -ne 0) {
Write-Warning "[build] objcopy .debug_rnglists update failed (exit $LASTEXITCODE); removing $injectElf"
Remove-Item -LiteralPath $injectElf -ErrorAction SilentlyContinue
}
else
{
# Baked atoms execute from RAM but are emitted as C data arrays, so their ELF sections lack SHF_EXECINSTR.
# GDB discards line rows for non-code sections.
# Mark only the debug-copy sections executable; the shipping ELF and PS-EXE remain byte/flag unchanged.
& $Objcopy `
--set-section-flags ".rodata=alloc,load,readonly,code,contents" `
--set-section-flags ".data=alloc,load,data,code,contents" `
$injectElf
if ($LASTEXITCODE -ne 0) {
Write-Warning "[build] objcopy .debug_aranges update failed (exit $LASTEXITCODE); removing $injectElf"
Write-Warning "[build] objcopy atom-section flag update failed (exit $LASTEXITCODE); removing $injectElf"
Remove-Item -LiteralPath $injectElf -ErrorAction SilentlyContinue
} else {
& $Objcopy --update-section ".debug_rnglists=$dwarfRnglistsBin" $injectElf
if ($LASTEXITCODE -ne 0) {
Write-Warning "[build] objcopy .debug_rnglists update failed (exit $LASTEXITCODE); removing $injectElf"
Remove-Item -LiteralPath $injectElf -ErrorAction SilentlyContinue
} else {
# Baked atoms execute from RAM but are emitted as C data arrays, so
# their ELF sections lack SHF_EXECINSTR. GDB discards line rows for
# non-code sections. Mark only the debug-copy sections executable;
# the shipping ELF and PS-EXE remain byte/flag unchanged.
& $Objcopy `
--set-section-flags ".rodata=alloc,load,readonly,code,contents" `
--set-section-flags ".data=alloc,load,data,code,contents" `
$injectElf
if ($LASTEXITCODE -ne 0) {
Write-Warning "[build] objcopy atom-section flag update failed (exit $LASTEXITCODE); removing $injectElf"
Remove-Item -LiteralPath $injectElf -ErrorAction SilentlyContinue
} else {
Write-Host "[build] DWARF-injected ELF: $injectElf"
}
}
Write-Host "[build] DWARF-injected ELF: $injectElf"
}
}
}
# G' (atom locals) is now part of --dwarf-injection.
# The F' splice block above already covered .debug_line / .debug_aranges / .debug_rnglists;
# we extend the same Copy-Item + objcopy chain to splice the G' 4 sections
# (.debug_info, .debug_abbrev, .debug_str via --update-section; .debug_loc via --add-section since it doesn't exist in the source ELF).
$dwarfInfoBin = Join-Path (Join-Path $path_build 'gen') 'hello_gte.dwarf_info.bin'
$dwarfAbbrevBin = Join-Path (Join-Path $path_build 'gen') 'hello_gte.dwarf_abbrev.bin'
$dwarfStrBin = Join-Path (Join-Path $path_build 'gen') 'hello_gte.dwarf_str.bin'
$dwarfLocBin = Join-Path (Join-Path $path_build 'gen') 'hello_gte.dwarf_loc.bin'
if ((Test-Path $dwarfInfoBin) -and (Test-Path $dwarfAbbrevBin) -and (Test-Path $dwarfStrBin) -and (Test-Path $dwarfLocBin))
{
Write-Host "[build] G' atom-locals: splicing .debug_info/.debug_abbrev/.debug_str/.debug_loc into $injectElf"
& $Objcopy --update-section ".debug_info=$dwarfInfoBin" $injectElf
$last_exit_code_error = ($LASTEXITCODE -ne 0)
if ($last_exit_code_error) {
Write-Warning "[build] objcopy .debug_info update failed (exit $LASTEXITCODE)"
return;
}
& $Objcopy --update-section ".debug_abbrev=$dwarfAbbrevBin" $injectElf
if ($LASTEXITCODE -ne 0) {
Write-Warning "[build] objcopy .debug_abbrev update failed (exit $LASTEXITCODE)"
return;
}
& $Objcopy --update-section ".debug_str=$dwarfStrBin" $injectElf
if ($LASTEXITCODE -ne 0) {
Write-Warning "[build] objcopy .debug_str update failed (exit $LASTEXITCODE)"
}
else
{
# .debug_loc doesn't exist in the source ELF; --add-section creates it.
& $Objcopy --add-section ".debug_loc=$dwarfLocBin" $injectElf
if ($LASTEXITCODE -ne 0) {
Write-Warning "[build] objcopy .debug_loc add-section failed (exit $LASTEXITCODE)"
} else {
Write-Host "[build] G' atom-locals-injected: $injectElf"
}
}
}
+15 -28
View File
@@ -3,11 +3,9 @@
--- All ELF32 + DWARF-specific code lives here.
---
--- **What this module contains:**
--- - **Format-constant tables** (
--- the byte-offset / opcode / size encyclopedias for ELF32, DWARF4 aranges, DWARF5 rnglists, DWARF line-program, MIPS).
--- - **Format-constant tables** (the byte-offset / opcode / size encyclopedias for ELF32, DWARF4 aranges, DWARF5 rnglists, DWARF line-program, MIPS).
--- Every constant carries a spec:` comment naming the spec section that defines it (convention established by F'').
--- - **I/O helpers**: little-endian byte read/write, ELF32 section walker, nm symbol reader,
--- source-map parser, native directory glob.
--- - **I/O helpers**: little-endian byte read/write, ELF32 section walker, nm symbol reader, source-map parser, native directory glob.
---
--- **Conventions:** tabs (1/level), EmmyLua annotations, no regex,
--- Lua 5.3 compatible.
@@ -208,7 +206,6 @@ end
--- **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).
--- 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.
--- @param elf_path Path
--- @param section_names string[] -- list of section names to read
@@ -305,9 +302,8 @@ function M.read_elf_sections(elf_path, section_names)
return result
end
--- Read ELF symbol addresses by walking the `.symtab` + `.strtab` sections
--- directly (no `nm` subprocess). Returns a map `{name -> {addr, size_bytes}}`
--- for every `code_<name>` symbol.
--- Read ELF symbol addresses by walking the `.symtab` + `.strtab` sections directly (no `nm` subprocess).
--- Returns a map `{name -> {addr, size_bytes}}` for every `code_<name>` symbol.
---
--- **Why direct parsing instead of `mipsel-none-elf-nm -S`?**
--- The `nm` subprocess costs ~50ms per spawn on Windows (cmd.exe + mipsel-none-elf-nm.exe). Parsing `.symtab` ourselves is ~0ms.
@@ -319,9 +315,6 @@ end
--- - We strip the `code_` prefix to match the previous `read_nm` output.
--- - `st_size > 0` filter excludes undefined/imported symbols.
---
--- **Cost:** ~1ms (file open + 2 section reads + symtab iteration).
--- Previously: ~50ms (nm subprocess spawn).
---
--- @param elf_path Path
--- @return table<string, {integer, integer}>
function M.read_nm(elf_path)
@@ -359,11 +352,11 @@ function M.read_nm(elf_path)
-- Extract the name from .strtab (null-terminated C string).
local name_end = strtab:find("\0", st_name_off + 1, true) or (st_name_off + 1)
local name = strtab:sub(st_name_off + 1, name_end - 1)
-- Filter: only `code_<name>` symbols (matches nm output).
local short = name:match("^code_(.+)$")
if short then
-- Filter: keep all symbol-table symbols (atoms emit their name as the bare `<name>` since the `code_` prefix was removed from the MipsAtom_ macro).
-- The atoms_source_map pass already filters out non-atom symbols via the source-map.txt cross-ref.
if name and #name > 0 then
local st_value = M.read_u32_le(symtab, entry_off + SYM_ST_VALUE - 1)
addrs[short] = { st_value, st_size }
addrs[name] = { st_value, st_size }
end
end
end
@@ -376,9 +369,8 @@ end
-- LEB128 encoders (Unsigned + Signed Little-Endian Base 128)
-- ════════════════════════════════════════════════════════════════════════════
--
-- DWARF uses LEB128 to encode variable-length integers in its wire format
-- (line-program opcodes, DW_AT values, etc.). Both encoders pack 7 bits
-- of data per byte + 1 bit of "more bytes follow" signaling.
-- DWARF uses LEB128 to encode variable-length integers in its wire format (line-program opcodes, DW_AT values, etc.).
-- Both encoders pack 7 bits of data per byte + 1 bit of "more bytes follow" signaling.
--
-- Per-byte layout:
--
@@ -391,8 +383,7 @@ end
-- bit 6 = 0 → value is positive (or zero); zero-extend on decode
-- bit 6 = 1 → value is negative; one-extend on decode
--
-- The signed encoder must emit the MINIMUM number of bytes whose final 7-bit
-- payload already has the correct sign bit set
-- The signed encoder must emit the MINIMUM number of bytes whose final 7-bit payload already has the correct sign bit set
-- (otherwise the decoder would round-trip to a different value).
--
-- Spec: DWARF5 §7.6 "Variable-Length Data" / Appendix C.
@@ -403,14 +394,11 @@ local LEB_CONT_BIT = 0x80
-- Low 7 bits of each LEB128 byte. The actual data payload.
local LEB_DATA_MASK = 0x7F
-- Bit 6 of the 7-bit data (i.e. 0x40). For SLEB128: the sign-bit position
-- used by the decoder for sign extension. Encoders MUST stop when the next
-- byte would be redundant AND the sign bit in the last byte matches the
-- value's sign.
-- Bit 6 of the 7-bit data (i.e. 0x40). For SLEB128: the sign-bit position used by the decoder for sign extension.
-- Encoders MUST stop when the next byte would be redundant AND the sign bit in the last byte matches the value's sign.
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:
--- - Extract the low 7 bits of `n` (LEB_DATA_MASK = 0x7F).
@@ -442,8 +430,7 @@ end
--- - If `n == -1` (sign-extended all-1s) AND bit 6 of the data = 1 → negative terminator (sign bit says "one-extend").
---
--- 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)
--- @return string
File diff suppressed because it is too large Load Diff
+15 -10
View File
@@ -158,12 +158,16 @@ local PASSES = {
["dwarf-injection"] = {
module = "passes.dwarf_injection",
kind = "shared",
deps = {"atoms-source-map"},
desc = "Inject per-atom .debug_line + .debug_aranges into the ELF (post-link; writes section .bin blobs for objcopy splice)",
deps = {"scan-source", "atoms-source-map"},
desc = "Inject per-atom .debug_line + .debug_aranges (F') + per-atom .debug_info subprogram + per-wave-context-reg .debug_info variables (G') into the ELF (post-link; writes 7 section .bin blobs for objcopy splice). (rbind composite) reads ctx.sources[i].scan to find atom_bind(Binds_X) atoms + their Binds_X struct fields; emits per-Binds_X DW_TAG_structure_type DIEs + per-rbind-atom DW_TAG_variable 'bind_args' DIEs with piece-chain DW_OP_bregN/DW_OP_piece location expressions.",
out = {
{ kind = "report", path_template = "<out_root>/<basename>.dwarf_line.bin" },
{ kind = "report", path_template = "<out_root>/<basename>.dwarf_aranges.bin" },
{ kind = "report", path_template = "<out_root>/<basename>.dwarf_line.bin" },
{ kind = "report", path_template = "<out_root>/<basename>.dwarf_aranges.bin" },
{ kind = "report", path_template = "<out_root>/<basename>.dwarf_rnglists.bin" },
{ kind = "report", path_template = "<out_root>/<basename>.dwarf_abbrev.bin" },
{ kind = "report", path_template = "<out_root>/<basename>.dwarf_info.bin" },
{ kind = "report", path_template = "<out_root>/<basename>.dwarf_str.bin" },
{ kind = "report", path_template = "<out_root>/<basename>.dwarf_loc.bin" },
},
},
report = {
@@ -290,6 +294,8 @@ FLAG_HANDLERS["--dwarf-injection"] = function(args)
args.requested_set[#args.requested_set + 1] = "dwarf-injection"
end
-- 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.
FLAG_HANDLERS[PASS_FLAG_DISPATCH_KEY] = function(args, a)
local name = PASS_FLAG_TO_NAME[a]
@@ -381,9 +387,8 @@ local function build_ctx(args)
dir = dir:sub(1, -2)
end
-- src.scan is populated by the "scan-source" pass (the first pass in the
-- dep graph). build_ctx just opens + reads the files; the scan itself
-- happens in the pass module, not inline in the orchestrator.
-- src.scan is populated by the "scan-source" pass (the first pass in the dep graph).
-- build_ctx just opens + reads the files; the scan itself happens in the pass module, not inline in the orchestrator.
sources[#sources + 1] = {
path = path,
text = text,
@@ -523,9 +528,9 @@ local function topo_sort(passes, requested_set)
process_next_ready(passes, needed, in_degree, ready, order)
end
-- Cycle detection: if order doesn't include all needed passes, some are stuck with in_degree > 0 (the cycle closed on itself
-- before Kahn could process them). Without this check, a fully-closed cycle (e.g. A -> B -> A) would silently return an emspty order list,
-- leaving the orchestrator to dispatch nothing.
-- Cycle detection: if order doesn't include all needed passes, some are stuck with in_degree > 0
-- (the cycle closed on itself before Kahn could process them).
-- Without this check, a fully-closed cycle (e.g. A -> B -> A) would silently return an emspty order list, leaving the orchestrator to dispatch nothing.
if #order ~= count_entries(needed) then
for name, deg in pairs(in_degree) do
if deg > 0 then