diff --git a/code/duffle/lottes_tape.h b/code/duffle/lottes_tape.h index 9ffe7fe..826b506 100644 --- a/code/duffle/lottes_tape.h +++ b/code/duffle/lottes_tape.h @@ -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} diff --git a/code/gte_hello/hello_gte.c b/code/gte_hello/hello_gte.c index c51726f..f7fc232 100644 --- a/code/gte_hello/hello_gte.c +++ b/code/gte_hello/hello_gte.c @@ -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); } diff --git a/scripts/build_psyq.ps1 b/scripts/build_psyq.ps1 index 90941b6..0ffc83d 100644 --- a/scripts/build_psyq.ps1 +++ b/scripts/build_psyq.ps1 @@ -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/.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/.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" } } } diff --git a/scripts/elf_dwarf.lua b/scripts/elf_dwarf.lua index f1f2d9e..1c3f5a5 100644 --- a/scripts/elf_dwarf.lua +++ b/scripts/elf_dwarf.lua @@ -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_` 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_` 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 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_` 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 `` 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 diff --git a/scripts/passes/dwarf_injection.lua b/scripts/passes/dwarf_injection.lua index 4d00474..1e1ac8e 100644 --- a/scripts/passes/dwarf_injection.lua +++ b/scripts/passes/dwarf_injection.lua @@ -1,20 +1,33 @@ ---- passes/dwarf_injection.lua — Per-atom DWARF injection for tape-atom step-debug (F'). +--- passes/dwarf_injection.lua — Per-atom DWARF injection for tape-atom step-debug (F' + G'). --- ---- Reads the post-link ELF directly (lfs + io.open; walks the ELF32 section ---- header table to find `.debug_line` + `.debug_aranges` + `.debug_rnglists`), ---- APPENDS synthetic DWARF line-program sequences for every `code_` atom, ---- EXTENDS the `.debug_aranges` and main-CU range tables with the atom ranges. ---- Writes the new section data to `/.dwarf_*.bin`. +--- Reads the post-link ELF directly +--- (lfs + io.open; walks the ELF32 section header table to find +--- `.debug_info` +--- + `.debug_abbrev` +--- + `.debug_str` +--- + `.debug_line` +--- + `.debug_aranges` +--- + `.debug_rnglists`), +--- APPENDS synthetic DWARF line-program sequences for every `code_` atom, EXTENDS the `.debug_aranges` and main-CU range tables with the atom ranges, +--- APPENDS a new compilation unit to `.debug_info` with per-atom `DW_TAG_subprogram` + per-wave-context-reg `DW_TAG_variable` entries +--- (so `R_PrimCursor` etc. appear as atom-scoped locals in VSCode's Variables pane). +--- Writes the new section data to `/.dwarf_*.bin` (7 blobs total). --- --- The `build_psyq.ps1` post-link hook then splices those `.bin` files into a copy of the ELF via: +--- mipsel-none-elf-objcopy --update-section .debug_info= +--- mipsel-none-elf-objcopy --update-section .debug_abbrev= +--- mipsel-none-elf-objcopy --update-section .debug_str= --- mipsel-none-elf-objcopy --update-section .debug_line= --- mipsel-none-elf-objcopy --update-section .debug_aranges= --- mipsel-none-elf-objcopy --update-section .debug_rnglists= ---- (Splice step runs from PowerShell — no Lua subprocess; no cmd /c parsing issues. +--- mipsel-none-elf-objcopy --add-section .debug_loc= +--- (.debug_loc doesn't exist in the source ELF; --add-section creates it. +--- Splice step runs from PowerShell — no Lua subprocess; no cmd /c parsing issues. --- objcopy's --update-section works fine in PowerShell even though Lua's `os.execute`/`io.popen` would mangle the `=` on Windows.) --- ---- Result: VSCode's source gutter follows per-stepi inside atom bodies. ---- Native VSCode UX (gutter arrow + highlighted line + Run to Cursor + conditional BPs by source line). +--- Result: VSCode's source gutter follows per-stepi inside atom bodies (F'), +--- AND the Variables pane shows the wave-context regs as atom-scoped locals (G'). +--- Native VSCode UX (gutter arrow + highlighted line + Run to Cursor + conditional BPs by source line + per-atom locals). --- No VSCode plugin, no Python, no pyelftools — pure Lua + objcopy. --- --- **Conventions:** tabs (1/level), EmmyLua annotations, no regex, @@ -48,13 +61,13 @@ local DWARF_LINE_OPS = elf_dwarf.DWARF_LINE_OPS local DWARF5_RNGLISTS = elf_dwarf.DWARF5_RNGLISTS local MIPS_BYTES_PER_WORD = elf_dwarf.MIPS_BYTES_PER_WORD -local DW_LNS_copy = DWARF_LINE_OPS.DW_LNS_copy -local DW_LNS_advance_pc = DWARF_LINE_OPS.DW_LNS_advance_pc -local DW_LNS_advance_line = DWARF_LINE_OPS.DW_LNS_advance_line -local DW_LNS_set_file = DWARF_LINE_OPS.DW_LNS_set_file -local DW_LNS_extended = DWARF_LINE_OPS.DW_LNS_extended -local DW_LNE_end_sequence = DWARF_LINE_OPS.DW_LNE_end_sequence -local DW_LNE_set_address = DWARF_LINE_OPS.DW_LNE_set_address +local DW_LNS_copy = DWARF_LINE_OPS.DW_LNS_copy +local DW_LNS_advance_pc = DWARF_LINE_OPS.DW_LNS_advance_pc +local DW_LNS_advance_line = DWARF_LINE_OPS.DW_LNS_advance_line +local DW_LNS_set_file = DWARF_LINE_OPS.DW_LNS_set_file +local DW_LNS_extended = DWARF_LINE_OPS.DW_LNS_extended +local DW_LNE_end_sequence = DWARF_LINE_OPS.DW_LNE_end_sequence +local DW_LNE_set_address = DWARF_LINE_OPS.DW_LNE_set_address local DW_RLE_end_of_list = DWARF5_RNGLISTS.end_of_list local DW_RLE_start_length = DWARF5_RNGLISTS.start_length @@ -63,6 +76,108 @@ local DW_RLE_start_length = DWARF5_RNGLISTS.start_length -- The injector extends that unit rather than appending an unreferenced unit. local ATOM_SOURCE_FILE_INDEX = 11 +-- Wave-context registers (per duffle.lua :: WAVE_CONTEXT_REGS). The MIPS +-- register number is what DW_OP_regN uses as N. +-- +-- R_PrimCursor = R_T7 = $15 → DW_OP_reg15 +-- R_FaceCursor = R_T4 = $12 → DW_OP_reg12 +-- R_VertBase = R_T5 = $13 → DW_OP_reg13 +-- R_OtBase = R_T6 = $14 → DW_OP_reg14 +-- R_TapePtr = R_T8 = $24 → DW_OP_reg24 +-- R_AtomJmp = R_T9 = $25 → DW_OP_reg25 +-- +-- A register-resident variable uses DW_OP_regN (opcode 0x50 + N). +-- DW_OP_bregN instead describes a memory location addressed from a register; +-- it makes GDB dereference the atom register value rather than display it. +local WAVE_CONTEXT_LOCATIONS = { + -- Names prefixed with `RR` to avoid collision with the C-level enum `{R_AtomJmp = 25, R_TapePtr = 24, R_InCursor = 12, R_PrimCursor = 15, ...}` + -- declared in the duffle headers (code/duffle/dsl.h or atom_dsl.h). + -- Without the prefix, gdb's `print R_PrimCursor` resolves to the enum constant (= 15), not to this DWARF variable. + -- With `set print enum on` (gdb default for enum values), gdb displays the enum identifier, so the Watch panel shows + -- `R_PrimCursor = R_PrimCursor` instead of the register value. + { name = "RR_PrimCursor", reg = 0x0F }, -- $15 + { name = "RR_FaceCursor", reg = 0x0C }, -- $12 + { name = "RR_VertBase", reg = 0x0D }, -- $13 + { name = "RR_OtBase", reg = 0x0E }, -- $14 + { name = "RR_TapePtr", reg = 0x18 }, -- $24 + { name = "RR_AtomJmp", reg = 0x19 }, -- $25 +} + +-- New abbreviation codes (100+ to avoid collision with gcc's existing +-- 1-60+ codes). +local ABBREV_CU = 0x64 -- 100: DW_TAG_compile_unit +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) +-- 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_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_BASE_TYPE = 0x6A -- 106: DW_TAG_base_type no children (U4) + +-- 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; +-- gcc emits DW_TAG_structure_type=0x13 + DW_TAG_base_type=0x24 even in +-- DWARF5-versioned CUs, so we match those exact byte values. +local DW_TAG_compile_unit = 0x11 +local DW_TAG_subprogram = 0x2E +local DW_TAG_variable = 0x34 +local DW_TAG_structure_type = 0x13 +local DW_TAG_member = 0x0D +local DW_TAG_base_type = 0x24 + +local DW_AT_name = 0x03 +local DW_AT_low_pc = 0x11 +local DW_AT_high_pc = 0x12 +local DW_AT_language = 0x13 +local DW_AT_location = 0x02 +local DW_AT_comp_dir = 0x1B +local DW_AT_byte_size = 0x0B +local DW_AT_encoding = 0x3E -- DWARF5 §7.7.1: DW_AT_encoding (for DW_ATE_unsigned base type; was 0x13 = DW_AT_language in prior slice — semantically wrong) +local DW_AT_data_member_location = 0x38 +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_external = 0x3F -- marks a variable/function as externally visible + +local DW_FORM_addr = 0x01 +local DW_FORM_data1 = 0x0B +local DW_FORM_string = 0x08 -- inline null-terminated +local DW_FORM_strp = 0x0E -- 4-byte offset into .debug_str +local DW_FORM_exprloc = 0x18 -- length-prefixed (ULEB128) DW_OP bytes +local DW_FORM_ref4 = 0x13 -- 4-byte offset within the same .debug_info CU +local DW_FORM_udata = 0x0F -- ULEB128 (DW_AT_byte_size for struct_type, DW_AT_data_member_location for member) +local DW_FORM_implicit_const = 0x21 -- DWARF5 §7.5.6: abbrev declaration carries a SLEB constant (used by the abbrev-table walker) + +local DW_OP_reg0 = 0x50 +-- DW_OP_regN = 0x50 + N; the variable's value resides in that register. +local DW_OP_piece = 0x93 -- followed by ULEB128 byte count (per DWARF5 §7.7.5) + +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 +-- (DW_LANG_Mips_Assembler = 0x8001 was used in the F' Task 5 test, but we want +-- this CU to look like a C TU so VSCode's Variables pane treats it as code.) + +-- R_ alias → MIPS register number. +-- 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). +-- The wave-context entries match WAVE_CONTEXT_LOCATIONS above; the rest are the standard MIPS C-ABI register names. +local R_NAME_TO_INDEX = { + ["R_0"] = 0, ["R_AT"] = 1, ["R_V0"] = 2, ["R_V1"] = 3, + ["R_A0"] = 4, ["R_A1"] = 5, ["R_A2"] = 6, ["R_A3"] = 7, + ["R_T0"] = 8, ["R_T1"] = 9, ["R_T2"] = 10, ["R_T3"] = 11, + ["R_T4"] = 12, ["R_T5"] = 13, ["R_T6"] = 14, ["R_T7"] = 15, + ["R_S0"] = 16, ["R_S1"] = 17, + ["R_T8"] = 24, ["R_T9"] = 25, + -- Wave-context aliases (same encoding as R_T4..R_T7, R_T8..R_T9). + ["R_PrimCursor"] = 15, ["R_FaceCursor"] = 12, + ["R_VertBase"] = 13, ["R_OtBase"] = 14, + ["R_TapePtr"] = 24, ["R_AtomJmp"] = 25, +} + +-- Default name for the synthetic CU (so VSCode lists it as a known source). +local DEFAULT_CU_NAME = "tape_atom_locals" +local DEFAULT_CU_COMP_DIR = "." + -- Path templates for the .bin outputs are now in SECTION_WRITERS (see below). -- Default basename if not provided via ctx. @@ -95,7 +210,7 @@ local sleb128 = elf_dwarf.sleb128 --- Build the byte sequence for ONE atom's line program: --- DW_LNE_set_address(addr) ---- DW_LNS_copy -- entry 1: addr, file, line=first.line +--- DW_LNS_copy -- entry 1: addr, file, line=first.line --- for each subsequent word: --- DW_LNS_advance_pc(1 .word = 4 bytes) --- DW_LNS_advance_line(line - prev_line) @@ -111,11 +226,10 @@ local sleb128 = elf_dwarf.sleb128 --- @return string local function build_atom_sequence(atom) local function set_address(addr) - -- Per DWARF5 §6.2.5.3: - -- marker(0) + size(ULEB128, includes sub_opcode byte) + sub_opcode + payload + -- Per DWARF5 §6.2.5.3: marker(0) + size(ULEB128, includes sub_opcode byte) + sub_opcode + payload -- For set_address: size = 1 (sub_opcode) + 4 (addr) = 5 local addr_bytes = elf_dwarf.write_u32_le(addr) - local sub_size = string.char(DW_LNE_set_address) .. addr_bytes + local sub_size = string.char(DW_LNE_set_address) .. addr_bytes return string.char(DW_LNS_extended) .. uleb128(#sub_size) .. sub_size end local function copy_op() return string.char(DW_LNS_copy) end @@ -140,6 +254,7 @@ local function build_atom_sequence(atom) set_address(atom.addr), -- 7 bytes: marker + size + sub + addr advance_line(atom.entries[1].line - 1), -- (entries[1].line - 1) bytes; line_state.line -> entries[1].line copy_op(), -- emit entry 1: addr=atom.addr, line=entries[1].line + copy_op(), -- same PC/line: GDB 12 zero-instruction-prologue marker } local prev_line = atom.entries[1].line for idx = 2, #atom.entries do @@ -203,6 +318,236 @@ local function build_atom_table(ctx) return out end +-- ════════════════════════════════════════════════════════════════════════════ +-- G' Phase 2 Task 8: rbind atom detection + (reg, field) pairing +-- ════════════════════════════════════════════════════════════════════════════ +-- +-- An "rbind atom" has `atom_bind(Binds_X)` in its `atom_info(...)` sub-call. +-- The atom body pops one field per `load_word(R_, R_TapePtr, O_(Binds_X,Field))` +-- and the order of these load_word calls defines the (reg, field) pairing for the piece-chain DWARF location expression. +-- +-- We use the SourceScan payload populated by `passes/scan_source.lua` (the dep-closed upstream pass). +-- That pass walks each source once and populates `src.scan.atom_infos` (the atom_info sub-call parse) and `src.scan.binds` (the Binds_X struct field parse). +-- We re-walk the body for the (reg, field) pairing because the SourceScan pass doesn't preserve which R_ receives which Binds_X.. +-- +-- **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`). +-- 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: +-- 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}. +--- Walks one ident at a time. A field declaration looks like: +--- U4 PrimCursor; | M3_S2* transform; | V4_S2* FaceCursor; | U4* OtBase; +--- which tokenizes as: TYPE [WS+ *] WS+ FIELD SEMI +--- Where TYPE is `U4` (recognized), or TYPE + optional `*` (pointer, recognized). +--- Other type tokens (e.g. user-defined `Struct_(Foo)`) are skipped + we move on. +--- @param body string -- the typedef body (between `{` and `}`) +--- @return table, integer -- (fields, byte_count) +local function reparse_binds_body(body) + local fields = {} + local byte_off = 0 + local pos = 1 + local body_len = #body + while pos <= body_len do + pos = duffle.skip_ws_and_cmt(body, pos) + if pos > body_len then break end + local type_ident, type_end = duffle.read_ident(body, pos) + if not type_ident then + pos = pos + 1 + else + -- Scan past optional whitespace + `*` + whitespace. + -- Recognized field types: U4 (no *) OR any TYPE* (pointer, 4 bytes on MIPS32). + local p = duffle.skip_ws_and_cmt(body, type_end) + local is_pointer = false + if p <= body_len and body:sub(p, p) == "*" then + is_pointer = true + p = p + 1 + end + local field_pos = duffle.skip_ws_and_cmt(body, p) + local field_ident, field_end = duffle.read_ident(body, field_pos) + -- A field is "recognized" if type is `U4` (no *) OR if it's a pointer. + if field_ident and (type_ident == "U4" or is_pointer) then + fields[#fields + 1] = { name = field_ident, offset = byte_off } + byte_off = byte_off + 0x04 -- 4 bytes per field on MIPS32 + end + -- Advance past the field ident (or the type ident if no field was found). + pos = field_end or (type_end + 1) + end + end + return fields, byte_off +end + +--- Find every `load_word(R_, R_TapePtr, O_(Binds_X, FieldName))` call in the +--- atom body and return ordered (reg_index, field_name) pairs. +--- +--- Source pattern (single-line, comma-separated at top level): +--- load_word(R_PrimCursor, R_TapePtr, O_(Binds_CubeTri,PrimCursor)), +--- load_word(R_FaceCursor, R_TapePtr, O_(Binds_CubeTri,FaceCursor)), +--- ... +--- +--- @param body string -- the atom body text (between { ... }) +--- @param binds_name string -- expected Binds_X name (skip pairs with mismatching binds) +--- @return table[] -- list of {reg = , field = } +local function parse_body_load_pairs(body, binds_name) + local pairs = {} + local pos = 1 + local body_len = #body + while pos <= body_len do + -- Find the next "load_word" identifier. + pos = duffle.skip_ws_and_cmt(body, pos) + if pos > body_len then break end + local ident, ident_end = duffle.read_ident(body, pos) + if not ident or ident ~= "load_word" then + -- Either no ident here OR a non-load_word ident (e.g. add_ui_self). + -- Either way, advance by at least 1 byte to avoid infinite loops. + -- ident_end is `pos` when no ident matches (read_ident returns nil, pos). + pos = (ident_end and ident_end > pos) and ident_end or (pos + 1) + else + -- Read the argument list. + local p_open = duffle.skip_ws_and_cmt(body, ident_end) + if body:sub(p_open, p_open) ~= "(" then + pos = p_open + 1 + else + local inner, p_after = duffle.read_parens(body, p_open) + -- Split top-level commas. + local args = duffle.split_top_level_commas(inner) + -- Expected shape: (R_, R_TapePtr, O_(Binds_, FieldName)) + if #args >= 3 then + local reg_name = duffle.trim(args[1]) + local third_arg = duffle.trim(args[3]) + -- Match O_(Binds_, FieldName) + local b, f = third_arg:match("^O_%((Binds_[%w_]+)%s*,%s*(.-)%s*%)$") + if b and b == binds_name and R_NAME_TO_INDEX[reg_name] then + pairs[#pairs + 1] = { + reg = R_NAME_TO_INDEX[reg_name], + field = duffle.trim(f), + } + end + end + pos = p_after + end + end + end + return pairs +end + +--- Collect every rbind atom + the matching Binds_X struct + (reg, field) pairs. +--- +--- Inputs come from the dep-closed `scan-source` pass: +--- ctx.sources[i].scan.atom_infos -- list of {atom_name, binds, reads, writes, info_line} +--- ctx.sources[i].scan.binds -- list of {line, name, fields, bytes} +--- +--- Returns: +--- rbind_atoms = {[atom_name] = {binds, fields, regs, byte_size, info_line}} +--- rbind_structs = {[binds_name] = {byte_size, fields, atom_names}} +--- +--- 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. +--- The piece chain uses (DW_OP_regN, DW_OP_piece, ULEB128(field_size)). +--- +--- @param ctx DwarfInjectionCtx +--- @param atom_table table[] -- the cross-ref'd atom table from build_atom_table +--- @return table, table -- (rbind_atoms, rbind_structs) +local function parse_rbind_atoms(ctx, atom_table) + local rbind_atoms = {} + local rbind_structs = {} + + -- Index binds by struct name; RE-PARSE the body locally (Task 8's local parser + -- handles pointer types too; the scan-source pass's parser only handles U4). + local binds_body_by_name = {} + for _, src in ipairs(ctx.sources or {}) do + local scan = src.scan + if scan then + for _, b in ipairs(scan.binds or {}) do + binds_body_by_name[b.name] = b -- need `b.body`; falls through to local reparse below + end + end + end + + -- Re-parse each Binds_X body via the local reparser (U4 + pointer support). + -- The scan-source pass preserves the original body text under each entry's `line` field but doesn't re-emit the body; + -- so we re-walk the source file directly for each Binds_X typedef. (Future: ask scan_source to preserve body.) + for binds_name in pairs(binds_body_by_name) do + local body_text = nil + for _, src in ipairs(ctx.sources or {}) do + local text = src.text + if text then + -- Look for `typedef Struct_()` in the source. + -- Plain search (4th arg = true) avoids Lua pattern metachar interpretation. + local needle = "typedef Struct_(" .. binds_name .. ")" + local s, e = text:find(needle, 1, true) + if s then + -- Find the body between `{` and matching `}` after `e`. + local brace = text:find("{", e, true) + if brace then + local inner, _after = duffle.read_braces(text, brace) + body_text = inner + break + end + end + end + end + if body_text then + local fields, byte_count = reparse_binds_body(body_text) + rbind_structs[binds_name] = { + bytes = byte_count, + fields = fields, + atom_names = {}, + } + end + end + + -- Walk every atom_info; if `binds` is set, find the atom body + parse load_word pairs. + local body_by_atom = {} + for _, src in ipairs(ctx.sources or {}) do + local scan = src.scan + if scan then + for _, atom in ipairs(scan.atoms or {}) do + body_by_atom[atom.name] = atom.body + end + end + end + + local ai_by_atom = {} + for _, src in ipairs(ctx.sources or {}) do + local scan = src.scan + if scan then + for _, ai in ipairs(scan.atom_infos or {}) do + ai_by_atom[ai.atom_name] = ai + end + end + end + + for atom_name, ai in pairs(ai_by_atom) do + if ai.binds then + local struct = rbind_structs[ai.binds] + local body = body_by_atom[atom_name] + if struct and body then + local pairs = parse_body_load_pairs(body, ai.binds) + if #pairs > 0 then + rbind_atoms[atom_name] = { + binds = ai.binds, + fields = struct.fields, -- {name, offset} from local reparse + bytes = struct.bytes, + regs = pairs, -- ordered list of {reg, field} + info_line = ai.info_line, + } + table.insert(struct.atom_names, atom_name) + end + end + end + end + + -- Mark rbind atoms in the main atom_table. + for _, atom in ipairs(atom_table) do + if rbind_atoms[atom.name] then + atom.rbind = rbind_atoms[atom.name] + end + end + + return rbind_atoms, rbind_structs +end + -- ════════════════════════════════════════════════════════════════════════════ -- Section builders (single dispatch table — see guide_metaprogram_ssdl.md §11) -- ════════════════════════════════════════════════════════════════════════════ @@ -234,9 +579,9 @@ local function build_dwarf_line_section(existing, atom_table) local unit_pos, last_pos, last_length, last_end = 1, nil, nil, nil while unit_pos <= #existing do if unit_pos + 3 > #existing then return existing end - local unit_length = elf_dwarf.read_u32_le(existing, unit_pos) - if unit_length == elf_dwarf.ELF32.dw_dwarf32_terminator then return existing end - local unit_end = unit_pos + 3 + unit_length + local unit_length = elf_dwarf.read_u32_le(existing, unit_pos) + if unit_length == elf_dwarf.ELF32.dw_dwarf32_terminator then return existing end + local unit_end = unit_pos + 3 + unit_length if unit_end > #existing then return existing end last_pos, last_length, last_end = unit_pos, unit_length, unit_end unit_pos = unit_end + 1 @@ -294,7 +639,7 @@ local function build_dwarf_aranges_section(existing, atom_table) while i <= #existing do -- Read this unit's length. local ul = elf_dwarf.read_u32_le(existing, i) - if ul == elf_dwarf.ELF32.dw_dwarf32_terminator then + if ul == elf_dwarf.ELF32.dw_dwarf32_terminator then -- DWARF64 marker - not supported. return existing end @@ -304,8 +649,7 @@ local function build_dwarf_aranges_section(existing, atom_table) is_last_unit = (unit_end == #existing) if is_last_unit then - -- The old terminator is replaced by entries + a new terminator, so - -- net section growth (and unit_length growth) is entries only. + -- The old terminator is replaced by entries + a new terminator, so net section growth (and unit_length growth) is entries only. local added_bytes = #atom_table * elf_dwarf.DWARF4_ARANGES.entry_size local new_ul = ul + added_bytes local new_ul_bytes = elf_dwarf.write_u32_le(new_ul) @@ -382,10 +726,632 @@ local function build_dwarf_rnglists_section(existing, atom_table) .. string.char(DW_RLE_end_of_list) end +-- ════════════════════════════════════════════════════════════════════════════ +-- G' helpers: per-atom .debug_info synthesis (DW_TAG_subprogram + DW_TAG_variable) +-- ════════════════════════════════════════════════════════════════════════════ + +--- Build the location bytes (DW_FORM_exprloc) for a register-resident value. +--- DW_OP_reg0..reg31 occupy opcodes 0x50..0x6f. DW_OP_reg15 is 0x5f; +--- 0x90 is DW_OP_regx, not the base of the compact register opcode range. +--- @param reg_n integer -- 0..31 (MIPS register number) +--- @return string -- the exprloc byte sequence (length-prefixed) +local function reg_exprloc(reg_n) + local op = string.char(DW_OP_reg0 + reg_n) + return uleb128(#op) .. op +end + +--- Build the piece-chain location bytes (DW_FORM_exprloc) for an rbind atom's bind_args. +--- +--- The location expression is a sequence of (DW_OP_regN, DW_OP_piece, ULEB128(size)) tuples, one per field. +--- GDB composites the pieces into a struct-shaped value matching the Binds_X layout. +--- +--- Order matters: the i-th piece corresponds to the i-th byte range in the composite. +--- For Binds_CubeTri (4 fields × 4 bytes), the chain is: +--- reg15 piece(4) -- R_PrimCursor → PrimCursor @ offset 0 +--- reg12 piece(4) -- R_FaceCursor → FaceCursor @ offset 4 +--- reg13 piece(4) -- R_VertBase → VertBase @ offset 8 +--- reg14 piece(4) -- R_OtBase → OtBase @ offset 12 +--- +--- 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. +--- +--- @param rbind table -- {regs = {{reg, field}, ...}, fields = {{name, offset}, ...}, bytes = N} +--- @return string -- the exprloc byte sequence (length-prefixed) +local function piece_chain_exprloc(rbind) + local op_bytes = {} + local field_offset_by_name = {} + for _, f in ipairs(rbind.fields) do + field_offset_by_name[f.name] = f.offset + end + local next_offset = rbind.bytes + for i = #rbind.regs, 1, -1 do -- walk backwards to know each piece's size + local pair = rbind.regs[i] + local off = field_offset_by_name[pair.field] or 0 + local size + if i == #rbind.regs then + size = next_offset - off + else + local next_off = field_offset_by_name[rbind.regs[i + 1].field] + if not next_off then + -- Defensive: a load_word references a field not in the Binds_X struct. + -- Use struct.bytes as a fallback so the piece chain is still well-formed. + next_off = rbind.bytes + end + size = next_off - off + end + -- DW_OP_regN, DW_OP_piece, ULEB128(size) + op_bytes[#op_bytes + 1] = string.char(DW_OP_reg0 + pair.reg, DW_OP_piece) .. uleb128(size) + next_offset = off + end + -- We built it back-to-front; reverse it. + local rev = {} + for i = #op_bytes, 1, -1 do rev[#rev + 1] = op_bytes[i] end + local op = table.concat(rev) + return uleb128(#op) .. op +end + +-- ════════════════════════════════════════════════════════════════════════════ +-- ULEB/SLEB decoders + .debug_abbrev walker + main-CU locator +-- ════════════════════════════════════════════════════════════════════════════ +-- +-- Per-atom DWARF DIE synthesis from a DETACHED synthetic CU (which GDB ignored at PC lookup) into the MAIN CU as inserted children. +-- That requires us to: +-- 1. Locate the main CU in .debug_info (walk DWARF32 unit lengths). +-- 2. Locate the main CU's abbrev table in .debug_abbrev (from the CU header). +-- 3. Duplicate that table to a new offset + append codes 100..106. +-- 4. Patch the main CU's debug_abbrev_offset to the duplicate. +-- 5. Insert our DIEs as children of the main CU (just before its root terminator). +-- +-- All offsets in this section are 0-based (matching the user's "section offset 0x24" convention). +-- When reading via elf_dwarf.read_u32_le (1-indexed), call with `pos + 1`. +-- +-- Pure-Lua 5.3 LEB decoders (no `bit` library; `2^shift` arithmetic matches elf_dwarf.lua's +-- "math.floor(/16) instead of bit.rshift for LuaJIT 2.1 compat" comment at line 352). + +--- Read a ULEB128 value starting at 0-based byte offset `pos` in `buf`. +--- Returns (value, next_pos) where `next_pos` is the 0-based offset of the byte AFTER the last byte consumed. +--- Returns (nil, pos) on truncation. +--- @param buf string +--- @param pos integer -- 0-based +--- @return integer|nil, integer +local function read_uleb128_at(buf, pos) + local value = 0 + local shift = 0 + local buf_len = #buf + while pos < buf_len do + local byte = buf:byte(pos + 1) + value = value + (byte % 0x80) * (2 ^ shift) -- extract low 7 bits, shift left + shift = shift + 7 + pos = pos + 1 + if byte < 0x80 then return value, pos end -- continuation bit clear → final byte + end + return nil, pos +end + +--- Read a SLEB128 value starting at 0-based byte offset `pos` in `buf`. +--- Returns (value, next_pos). Returns (nil, pos) on truncation. +--- @param buf string +--- @param pos integer -- 0-based +--- @return integer|nil, integer +local function read_sleb128_at(buf, pos) + local value = 0 + local shift = 0 + local buf_len = #buf + while pos < buf_len do + local byte = buf:byte(pos + 1) + value = value + (byte % 0x80) * (2 ^ shift) + shift = shift + 7 + pos = pos + 1 + if byte < 0x80 then + -- Sign-extend if the sign bit (bit 6) of the last byte is set. + if byte >= 0x40 then + value = value - (2 ^ shift) + end + return value, pos + end + end + return nil, pos +end + +--- Walk a .debug_abbrev table starting at 0-based offset `table_start` and return the 0-based offset of the table-terminator byte +--- (a single 0 byte marking the end of declarations in this table per DWARF5 §7.5.3). +--- +--- The walker consumes each declaration's abbrev code (ULEB) + tag (ULEB) + has_children byte + (attr, form) +--- attr-spec pairs (each pair = ULEB + ULEB, with an extra SLEB constant for DW_FORM_implicit_const per DWARF5 §7.5.6). +--- The declaration's attr-spec list ends at the (0, 0) pair. +--- +--- Returns nil on truncated or malformed input. +--- @param existing string -- the .debug_abbrev section bytes +--- @param table_start integer -- 0-based offset of the table's first byte +--- @return integer|nil -- 0-based offset of the table-terminator byte +local function find_abbrev_table_end(existing, table_start) + local pos = table_start + local buf_len = #existing + -- Empty table = terminator byte at table_start. + if pos >= buf_len or existing:byte(pos + 1) == 0 then return pos end + while pos < buf_len do + -- Each declaration: ULEB code, ULEB tag, 1 byte has_children. + local _code, code_end = read_uleb128_at(existing, pos) + if not _code then return nil end + pos = code_end + local _tag, tag_end = read_uleb128_at(existing, pos) + if not _tag then return nil end + pos = tag_end + -- has_children: 1 byte (DW_CHILDREN_yes=1 / DW_CHILDREN_no=0). + if pos >= buf_len then return nil end + pos = pos + 1 + -- attr/form loop until (0, 0) terminator pair. + while pos < buf_len do + local attr, attr_end = read_uleb128_at(existing, pos) + if not attr then return nil end + pos = attr_end + local form, form_end = read_uleb128_at(existing, pos) + if not form then return nil end + pos = form_end + if attr == 0 and form == 0 then break end + -- DW_FORM_implicit_const: declaration carries a SLEB constant. + if form == DW_FORM_implicit_const then + local _const, const_end = read_sleb128_at(existing, pos) + if not _const then return nil end + pos = const_end + end + end + -- Next declaration starts here; if next byte is 0, the table is done. + if pos >= buf_len then return nil end + if existing:byte(pos + 1) == 0 then return pos end + end + return nil +end + +-- DWARF5 compile-unit header constants. +local DW_VERSION_5 = 5 +local DW_UT_compile = 0x01 +local DWARF32_TERMINATOR = 0xFFFFFFFF -- sentinel for DWARF64 marker +local CU_HEADER_SIZE = 12 -- 4 + 2 + 1 + 1 + 4 + +--- Walk .debug_info to find the FINAL compilation unit, validate it as a DWARF5 32-bit compile-unit, and extract its bounds + abbrev-table offset. +--- Returns nil on any layout mismatch. Callers fall back to existing sections. +--- +--- Returns (cu_start, cu_end_excl, abbrev_offset), all 0-based: +--- cu_start -- byte offset of the unit_length field +--- cu_end_excl -- byte offset of the first byte AFTER the main CU +--- abbrev_offset -- the main CU's debug_abbrev_offset field value (0-based) +--- +--- Validation: +--- - Section is non-empty. +--- - All units are DWARF32 (unit_length != 0xFFFFFFFF). +--- - No truncated units (each unit's end <= section length). +--- - The section ends exactly on a unit boundary. +--- - 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 byte of the main CU is 0x00 (the CU's root children-terminator). +--- +--- @param existing string -- the .debug_info section bytes +--- @return integer|nil, integer|nil, integer|nil +local function find_main_cu_layout(existing) + local buf_len = #existing + if buf_len < CU_HEADER_SIZE then return nil end + + local pos = 0 + local main_cu_start = nil + local main_cu_end_excl = nil + while pos + 4 <= buf_len do + local unit_length = elf_dwarf.read_u32_le(existing, pos + 1) + if unit_length == DWARF32_TERMINATOR then return nil end + local unit_end_excl = pos + 4 + unit_length + if unit_end_excl > buf_len then return nil end + main_cu_start = pos + main_cu_end_excl = unit_end_excl + pos = unit_end_excl + end + if pos ~= buf_len or main_cu_start == nil then return nil end + if main_cu_start == 0 then return nil end -- need at least 2 units (crt + main) + + -- Validate the main CU header. + -- Bytes relative to main_cu_start (0-based): + -- [0..3] unit_length + -- [4..5] version + -- [6] unit_type + -- [7] address_size + -- [8..11] debug_abbrev_offset + local hdr = main_cu_start + 4 + local version = elf_dwarf.read_u16_le(existing, hdr + 1) + local unit_type = existing:byte(hdr + 3) + local address_size = existing:byte(hdr + 4) + local abbrev_off = elf_dwarf.read_u32_le(existing, hdr + 5) + if version ~= DW_VERSION_5 or unit_type ~= DW_UT_compile or address_size ~= 4 then + return nil + end + + -- Final byte of the main CU must be the root children-terminator (0). + local final_pos = main_cu_end_excl - 1 + if final_pos >= buf_len or existing:byte(final_pos + 1) ~= 0 then return nil end + + return main_cu_start, main_cu_end_excl, abbrev_off +end + +--- 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): +--- Abbrev 100 (DW_TAG_compile_unit, with children): +--- DW_AT_name (DW_FORM_strp) -- CU name +--- DW_AT_comp_dir (DW_FORM_strp) -- compilation dir +--- DW_AT_language (DW_FORM_data1) -- DW_LANG_C99 +--- Abbrev 101 (DW_TAG_subprogram, with children): +--- DW_AT_name (DW_FORM_string) -- atom function name +--- DW_AT_low_pc (DW_FORM_addr) -- atom.addr +--- DW_AT_high_pc (DW_FORM_addr) -- atom.addr + size +--- Abbrev 102 (DW_TAG_variable, no children): +--- DW_AT_name (DW_FORM_string) -- "R_PrimCursor" etc. +--- DW_AT_location (DW_FORM_exprloc) -- DW_OP_regN +--- Abbrev 103 (DW_TAG_structure_type, with children): -- NEW Task 8 +--- DW_AT_name (DW_FORM_string) -- "Binds_CubeTri" etc. +--- DW_AT_byte_size (DW_FORM_udata) -- struct byte size +--- Abbrev 104 (DW_TAG_member, no children): -- NEW Task 8 +--- DW_AT_name (DW_FORM_string) -- "PrimCursor" etc. +--- DW_AT_data_member_location (DW_FORM_udata) -- byte offset in struct +--- DW_AT_type (DW_FORM_ref4) -- → U4 base type +--- Abbrev 105 (DW_TAG_variable, no children): -- NEW Task 8 +--- DW_AT_name (DW_FORM_string) -- "bind_args" +--- DW_AT_location (DW_FORM_exprloc) -- DW_OP_regN, piece, regN, piece, ... +--- DW_AT_type (DW_FORM_ref4) -- → DW_TAG_structure_type DIE +--- Abbrev 106 (DW_TAG_base_type, no children): -- NEW Task 8 +--- DW_AT_name (DW_FORM_string) -- "unsigned int" +--- DW_AT_byte_size (DW_FORM_data1) -- 4 +--- DW_AT_encoding (DW_FORM_data1) -- DW_ATE_unsigned +--- +--- Returns the 7 abbrev declarations + a trailing `\0` byte (the table terminator per DWARF5 §7.5.3). +--- When APPENDED to the existing .debug_abbrev (which has its own terminator), the result is: +--- [existing declarations] [existing \0] [new declarations] [\0] +--- which is a valid (extended) abbrev table. +--- @return string +local function build_new_abbrev() + local function attr(name, form) return uleb128(name) .. uleb128(form) end + local function abbrev(code, tag, has_children, attrs) + local children = has_children and 0x01 or 0x00 -- DW_CHILDREN_yes / no + return uleb128(code) + .. uleb128(tag) + .. string.char(children) + .. attrs + .. string.char(0x00, 0x00) -- end of attr list (2 zeros) + end + + local abbrev_cu = abbrev(ABBREV_CU, DW_TAG_compile_unit, true, -- DW_CHILDREN_yes + attr( DW_AT_name, DW_FORM_strp) + .. attr(DW_AT_comp_dir, DW_FORM_strp) + .. attr(DW_AT_language, DW_FORM_data1)) + + local abbrev_subprogram = abbrev(ABBREV_SUBPROGRAM, DW_TAG_subprogram, true, -- DW_CHILDREN_yes + attr( DW_AT_name, DW_FORM_string) + .. attr(DW_AT_low_pc, DW_FORM_addr) + .. attr(DW_AT_high_pc, DW_FORM_addr) + .. attr(DW_AT_linkage_name, DW_FORM_string)) -- equals DW_AT_name; lets gdb's symbol-table lookup resolve to our subprogram (not the gcc global `code_` const U4 array) + + local abbrev_variable = abbrev(ABBREV_VARIABLE, DW_TAG_variable, false, -- DW_CHILDREN_no + attr( DW_AT_name, DW_FORM_string) + .. attr(DW_AT_location, DW_FORM_exprloc) + .. 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) + + -- Task 8: rbind composite. + -- 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. + local abbrev_struct_type = abbrev(ABBREV_STRUCT_TYPE, DW_TAG_structure_type, true, -- DW_CHILDREN_yes + attr( DW_AT_name, DW_FORM_string) + .. attr(DW_AT_byte_size, DW_FORM_udata)) + + local abbrev_member = abbrev(ABBREV_MEMBER, DW_TAG_member, false, -- DW_CHILDREN_no + attr( DW_AT_name, DW_FORM_string) + .. attr(DW_AT_data_member_location, DW_FORM_udata) + .. attr(DW_AT_type, DW_FORM_ref4)) + + local abbrev_bind_var = abbrev(ABBREV_BIND_VAR, DW_TAG_variable, false, -- DW_CHILDREN_no + attr( DW_AT_name, DW_FORM_string) + .. attr(DW_AT_location, DW_FORM_exprloc) + .. attr(DW_AT_type, DW_FORM_ref4)) + + local abbrev_base_type = abbrev(ABBREV_BASE_TYPE, DW_TAG_base_type, false, -- DW_CHILDREN_no + attr( DW_AT_name, DW_FORM_string) + .. attr(DW_AT_byte_size, DW_FORM_data1) + .. attr(DW_AT_encoding, DW_FORM_data1)) + + return abbrev_cu .. abbrev_subprogram .. abbrev_variable + .. abbrev_struct_type .. abbrev_member .. abbrev_bind_var .. abbrev_base_type + .. string.char(0x00) +end + +--- Build the new strings to append to .debug_str. Each string is null-terminated. +--- The CU name + comp_dir + 6 per-atom register names all go into one new blob. +--- @param atom_table table[] -- list of {name, addr, size_bytes, ...} +--- @return string, table -- (new_strings_blob, map_of_name_to_offset_in_new_blob) +local function build_new_strings(atom_table) + -- The CU name + comp_dir are the first two strings (offsets 0 and N1). + -- Then each unique atom name + each register name follows. + local strings = {} + local map = {} + + -- CU name at offset 0 in the new blob + strings[#strings + 1] = DEFAULT_CU_NAME .. "\0" + map["__cu_name__"] = 0 + local cu_name_len = #strings[#strings] + + -- comp_dir at offset cu_name_len + strings[#strings + 1] = DEFAULT_CU_COMP_DIR .. "\0" + map["__comp_dir__"] = cu_name_len + local comp_dir_len = #strings[#strings] + + -- Atom names (one per unique atom) + for _, atom in ipairs(atom_table) do + local name = atom.name + if not map[name] then + map[name] = #table.concat(strings) + strings[#strings + 1] = name .. "\0" + end + end + + -- Register names (one per unique wave-context reg) + for _, loc in ipairs(WAVE_CONTEXT_LOCATIONS) do + if not map[loc.name] then + map[loc.name] = #table.concat(strings) + strings[#strings + 1] = loc.name .. "\0" + end + end + + return table.concat(strings), map +end + +--- Build the DWARF DIE bytes to insert into the MAIN CU as children, immediately +--- before the main CU's root children-terminator (the final 0 byte of the CU). +--- +--- The same content was emitted as a DETACHED synthetic CU appended after the main CU. +--- GDB's PC lookup selects the main CU, so the synthetic CU was out of scope and `RR_PrimCursor` + `bind_args` never appeared in the current frame. +--- Inserting the DIEs as children of the main CU puts them in scope for every PC the main CU owns; +--- including every atom PC (since `.debug_aranges` + `.debug_rnglists` already assign atom PCs to it). +--- +--- Layout (matches the pre-build_new_cu exactly; only the insertion point and ref4 basis change): +--- 1) DW_TAG_base_type "unsigned int" (abbrev 106) +--- DW_AT_name = "unsigned int" (DW_FORM_string) +--- DW_AT_byte_size = 4 (DW_FORM_data1) +--- DW_AT_encoding = DW_ATE_unsigned (DW_FORM_data1) +--- 2) Per Binds_X: DW_TAG_structure_type (abbrev 103, children=yes) +--- DW_AT_name = "Binds_CubeTri" etc. (DW_FORM_string) +--- DW_AT_byte_size = struct bytes (DW_FORM_udata) +--- (children: one DW_TAG_member per field) +--- 3) Per field: DW_TAG_member (abbrev 104) +--- DW_AT_name = "PrimCursor" etc. +--- DW_AT_data_member_location = byte offset (DW_FORM_udata) +--- DW_AT_type = ref4 → base_type DIE +--- 4) Per atom: DW_TAG_subprogram (abbrev 101, children=yes) +--- DW_AT_name = atom.name (DW_FORM_string) +--- DW_AT_low_pc = atom.addr +--- DW_AT_high_pc = atom.addr + size_bytes +--- DW_AT_linkage_name = atom.name (DW_FORM_string; same as DW_AT_name) +--- (children: 6 wave-context vars + 1 bind_args var if rbind) +--- 5) Per wave-context reg: DW_TAG_variable (abbrev 102) +--- DW_AT_name = "RR_" (DW_FORM_string) +--- DW_AT_location = DW_OP_regN (DW_FORM_exprloc) +--- DW_AT_type = ref4 → base_type DIE +--- DW_AT_external = 1 +--- 6) For rbind atoms: DW_TAG_variable "bind_args" (abbrev 105) +--- DW_AT_name = "bind_args" +--- DW_AT_location = piece-chain (DW_FORM_exprloc) +--- DW_AT_type = ref4 → structure_type DIE +--- +--- **DOES NOT** emit the final 0 byte (root terminator). +--- build_debug_info_section splices our bytes between the existing DIE bytes and that terminator, which is preserved verbatim. +--- +--- **ref4 basis**: DW_FORM_ref4 is CU-relative (offset from the first byte of the CU header). +--- Our inserted DIEs live in the main CU, so every ref4 = (target section offset) - main_cu_offset. +--- Per-die section offsets are tracked via the running `next_offset` cursor (= section offset of the NEXT byte to emit). +--- +--- @param main_cu_offset integer -- 0-based section offset of the main CU's unit_length field +--- @param main_cu_end_excl integer -- 0-based section offset of the first byte AFTER the main CU +--- @param atom_table table[] -- atoms (with atom.rbind set if rbind) +--- @param rbind_structs table -- {[binds_name] = {bytes, fields, atom_names}} +--- @return string -- bytes to splice into the main CU just before its root terminator +local function build_inserted_children(main_cu_offset, main_cu_end_excl, atom_table, rbind_structs) + rbind_structs = rbind_structs or {} + + -- We insert IMMEDIATELY BEFORE the main CU's root children-terminator (the last byte of the main CU). + -- The first emitted byte lives at section offset (main_cu_end_excl - 1). + local insertion_start = main_cu_end_excl - 1 + + local bytes = {} + local next_offset = insertion_start -- 0-based section offset of the NEXT byte to emit + + local function emit(s) + bytes[#bytes + 1] = s + next_offset = next_offset + #s + end + local function ref4_of(section_offset) + return section_offset - main_cu_offset + end + + -- 1) Emit the base_type DIE first (member ref4s reference it). + local base_type_section_offset = next_offset + emit(uleb128(ABBREV_BASE_TYPE)) + emit("unsigned int\0") -- DW_FORM_string (DW_AT_name) + emit(string.char(4)) -- DW_FORM_data1 (DW_AT_byte_size) + emit(string.char(DW_ATE_unsigned)) -- DW_FORM_data1 (DW_AT_encoding) + + -- 2) Emit one DW_TAG_structure_type per unique Binds_X. + local struct_section_offsets = {} + local sorted_struct_names = {} + for k in pairs(rbind_structs) do sorted_struct_names[#sorted_struct_names + 1] = k end + table.sort(sorted_struct_names) + for _, binds_name in ipairs(sorted_struct_names) do + local struct = rbind_structs[binds_name] + struct_section_offsets[binds_name] = next_offset + + emit(uleb128(ABBREV_STRUCT_TYPE)) + emit(binds_name .. "\0") -- DW_FORM_string (DW_AT_name) + emit(uleb128(struct.bytes)) -- DW_FORM_udata (DW_AT_byte_size) + + -- Emit DW_TAG_member children (one per field). + for _, field in ipairs(struct.fields) do + emit(uleb128(ABBREV_MEMBER)) + emit(field.name .. "\0") -- DW_FORM_string (DW_AT_name) + emit(uleb128(field.offset)) -- DW_FORM_udata (DW_AT_data_member_location) + emit(elf_dwarf.write_u32_le(ref4_of(base_type_section_offset))) -- DW_FORM_ref4 → base_type + end + + emit(string.char(0x00)) -- end of structure_type's children + end + + -- 3) Emit per-atom DW_TAG_subprograms (children of main CU). + -- Subprograms are named `` (matching the nm symbol; the `code_` prefix was removed from the MipsAtom_ macro in code/duffle/lottes_tape.h). + -- The gcc global `[]` is a DW_TAG_variable without children; our subprogram has the wave-context var children. + -- gdb's symbol resolution picks our subprogram (it has low_pc/high_pc + children) over the gcc global for function-context lookups. + for _, atom in ipairs(atom_table) do + emit(uleb128(ABBREV_SUBPROGRAM)) + emit(atom.name .. "\0") -- DW_FORM_string (DW_AT_name) + emit(elf_dwarf.write_u32_le(atom.addr)) + 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) + + -- Per wave-context reg: DW_TAG_variable (type = U4 via ref4 to base_type DIE) + for _, loc in ipairs(WAVE_CONTEXT_LOCATIONS) do + emit(uleb128(ABBREV_VARIABLE)) + emit(loc.name .. "\0") -- DW_FORM_string + emit(reg_exprloc(loc.reg)) -- DW_FORM_exprloc (DW_OP_regN) + emit(elf_dwarf.write_u32_le(ref4_of(base_type_section_offset))) -- DW_FORM_ref4 → base_type + emit(string.char(0x01)) -- DW_AT_external=1 (visible at CU scope) + end + + -- If rbind, emit bind_args variable with piece-chain location. + if atom.rbind then + local binds_name = atom.rbind.binds + emit(uleb128(ABBREV_BIND_VAR)) + emit("bind_args\0") -- DW_FORM_string (DW_AT_name) + emit(piece_chain_exprloc(atom.rbind)) -- DW_FORM_exprloc + emit(elf_dwarf.write_u32_le(ref4_of(struct_section_offsets[binds_name]))) -- DW_FORM_ref4 → struct_type + end + + emit(string.char(0x00)) -- end of subprogram's children + end + + -- DO NOT emit a final 0 here — that's the main CU's root terminator, which + -- build_debug_info_section preserves verbatim. + return table.concat(bytes) +end + +--- Build the new .debug_abbrev: existing + duplicate of the main CU's table + the new declarations 100..106 (with their terminating 0). +--- +--- **Why duplicate the main table**: a CU can name only one abbrev table via its `debug_abbrev_offset` field. +--- The main CU currently points at the gcc-generated table (codes 1..60+); +--- codes 100..106 are NEW, so they live in a different table. +--- To keep all main-CU abbreviation codes in one table we DUPLICATE +--- the gcc-generated codes at a new offset and append our new declarations 100..106 immediately after (with a single shared terminator 0). +--- +--- Result layout (0-based section offsets): +--- [0 .. #existing-1] existing .debug_abbrev (unchanged) +--- [#existing .. +#main_dup-1] duplicate of the main table (codes 1..60+, NO terminator) +--- [+#main_dup .. +#new_abbrevs-1] codes 100..106 + final 0 +--- +--- The MAIN CU's debug_abbrev_offset is patched to `#existing` (start of the duplicate table). +--- Codes 100..106 live inside the same table immediately after the duplicated gcc codes, so a single abbrev-table pointer is enough. +--- +--- Fails safely by returning existing sections unchanged if the table walker can't find the table terminator (malformed input). +--- +--- @param existing string -- existing .debug_abbrev bytes (verbatim) +--- @param main_abbrev_offset integer -- 0-based offset into `existing` of the main CU's abbrev table +--- @return string, integer -- (new_abbrev_bytes, offset_where_duplicate_table_starts = #existing) +local function build_debug_abbrev_section(existing, main_abbrev_offset) + local table_end = find_abbrev_table_end(existing, main_abbrev_offset) + if not table_end then return existing, nil end + + -- Duplicate the main table declarations EXCLUDING its terminating 0 byte. + -- (1-indexed sub: existing:sub(main_abbrev_offset + 1, table_end) reads bytes + -- from 0-based [main_abbrev_offset .. table_end - 1].) + local main_table_dup = existing:sub(main_abbrev_offset + 1, table_end) + local new_abbrevs = build_new_abbrev() -- includes its own terminating 0 + + -- The MAIN CU's debug_abbrev_offset points to the duplicate's start (= #existing). + -- Codes 100..106 follow the duplicate's declarations inside that same table. + return existing .. main_table_dup .. new_abbrevs, #existing +end + +--- Build the new .debug_str: existing strings + new strings appended. +--- @param existing string -- existing .debug_str bytes (verbatim) +--- @param atom_table table[] +--- @return string, integer, table -- (new_str_bytes, new_strings_offset, string_map) +local function build_debug_str_section(existing, atom_table) + local new_strings, string_map = build_new_strings(atom_table) + return existing .. new_strings, #existing, string_map +end + +--- Build the new .debug_info: SPLICE inserted DIEs into the MAIN CU as children. +--- +--- This function appended a DETACHED synthetic CU to the end of .debug_info. +--- That put every atom DIE in a separate CU from the one GDB selected for PC lookup, so `RR_PrimCursor` + `bind_args` never appeared in scope. +--- This implementation instead: +--- 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. +--- 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. +--- 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. +--- +--- **No detached synthetic CU is appended.** +--- +--- @param existing string -- existing .debug_info bytes (verbatim) +--- @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 new_abbrev_offset integer -- 0-based offset into the new .debug_abbrev of the duplicate main table +--- @param atom_table table[] +--- @param rbind_structs table -- {[binds_name] = {bytes, fields, atom_names}} (Task 8) +--- @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) + -- 1) Build the inserted children bytes (just before the main CU's root terminator). + local inserted = build_inserted_children(main_cu_start, main_cu_end_excl, atom_table, rbind_structs) + local inserted_len = #inserted + + -- 2) Patch main CU's unit_length += inserted_len. + local old_unit_length = elf_dwarf.read_u32_le(existing, main_cu_start + 1) + local new_unit_length = old_unit_length + inserted_len + local new_unit_length_bytes = elf_dwarf.write_u32_le(new_unit_length) + + -- 3) Patch main CU's debug_abbrev_offset (bytes [main_cu_start + 8 .. + 11]). + local new_abbrev_offset_bytes = elf_dwarf.write_u32_le(new_abbrev_offset) + + -- 4) Splice. All offsets below are 0-based; existing:sub is 1-indexed inclusive. + -- Byte ranges (0-based, inclusive): + -- [0 .. main_cu_start - 1] crt CU (verbatim) + -- [main_cu_start + 0 .. + 3] unit_length (PATCHED) + -- [main_cu_start + 4 .. + 7] version + unit_type + address_size (verbatim) + -- [main_cu_start + 8 .. + 11] debug_abbrev_offset (PATCHED) + -- [main_cu_start + 12 .. main_cu_end_excl - 2] existing DIE bytes (verbatim) + -- [main_cu_end_excl - 1] root children-terminator (verbatim 0) + local pre_end = main_cu_end_excl - 2 -- 0-based end of existing DIE bytes (inclusive) + local root_terminator = main_cu_end_excl - 1 -- 0-based position of the final 0 byte + + return existing:sub(1, main_cu_start) -- crt CU + .. new_unit_length_bytes -- patched unit_length (4 bytes) + .. existing:sub(main_cu_start + 5, main_cu_start + 8) -- version(2) + unit_type(1) + address_size(1) verbatim + .. new_abbrev_offset_bytes -- patched debug_abbrev_offset (4 bytes) + .. existing:sub(main_cu_start + 13, pre_end + 1) -- existing DIE bytes verbatim + .. inserted -- our inserted children + .. existing:sub(root_terminator + 1, main_cu_end_excl) -- root children-terminator (verbatim 0) +end + +--- 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; +--- 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. +--- @return string +local function build_debug_loc_section() + return string.char(0x00) +end + local SECTION_BUILDERS = { debug_line = build_dwarf_line_section, debug_aranges = build_dwarf_aranges_section, debug_rnglists = build_dwarf_rnglists_section, + debug_abbrev = build_debug_abbrev_section, + debug_info = build_debug_info_section, + debug_str = build_debug_str_section, + debug_loc = function() return build_debug_loc_section() end, } -- Per-section output path resolver (mirrors SECTION_BUILDERS). @@ -394,6 +1360,10 @@ local SECTION_WRITERS = { debug_line = function(out_root, basename) return out_root .. "\\" .. basename .. ".dwarf_line.bin" end, debug_aranges = function(out_root, basename) return out_root .. "\\" .. basename .. ".dwarf_aranges.bin" end, debug_rnglists = function(out_root, basename) return out_root .. "\\" .. basename .. ".dwarf_rnglists.bin" end, + debug_abbrev = function(out_root, basename) return out_root .. "\\" .. basename .. ".dwarf_abbrev.bin" end, + debug_info = function(out_root, basename) return out_root .. "\\" .. basename .. ".dwarf_info.bin" end, + debug_str = function(out_root, basename) return out_root .. "\\" .. basename .. ".dwarf_str.bin" end, + debug_loc = function(out_root, basename) return out_root .. "\\" .. basename .. ".dwarf_loc.bin" end, } -- ════════════════════════════════════════════════════════════════════════════ @@ -402,8 +1372,8 @@ local SECTION_WRITERS = { local M = {} ---- M.run — orchestrator entry. Phase 1 Tasks 3-4 read + cross-ref. Phase 2 ---- Tasks 5-7 fill in the section builders + .bin emission. +--- M.run — orchestrator entry. Phase 1 Tasks 3-4 read + cross-ref. +--- Phase 2 Tasks 5-7 fill in the section builders + .bin emission. --- @param ctx DwarfInjectionCtx --- @return table function M.run(ctx) @@ -423,36 +1393,117 @@ function M.run(ctx) elf_path = duffle.to_absolute_path(elf_path) -- Read the existing DWARF sections directly (no subprocess; lfs + io.open + manual ELF32 section-header walk). - local existing_sections = elf_dwarf.read_elf_sections(elf_path, {".debug_line", ".debug_aranges", ".debug_rnglists"}) - local existing_line = existing_sections[".debug_line"] or "" - local existing_aranges = existing_sections[".debug_aranges"] or "" - local existing_rnglists = existing_sections[".debug_rnglists"] or "" - io.stderr:write(string.format("[dwarf_injection] read .debug_line (%d) + .debug_aranges (%d) + .debug_rnglists (%d) bytes\n" - , #existing_line, #existing_aranges, #existing_rnglists)) + -- We need ALL 7 sections since the F' group (.debug_line, .debug_aranges, .debug_rnglists) + -- extends the existing sections + the G' group (.debug_info, .debug_abbrev, .debug_str, .debug_loc) appends a new compile unit. + -- The dispatch (SECTION_BUILDERS) handles each. + local existing_sections = elf_dwarf.read_elf_sections(elf_path, { + ".debug_line", ".debug_aranges", ".debug_rnglists", + ".debug_info", ".debug_abbrev", ".debug_str", + -- .debug_loc doesn't exist in the source ELF (we --add-section it on splice); + -- reading it just returns "" which is the "missing" case the builder handles. + ".debug_loc", + }) + io.stderr:write(string.format( + "[dwarf_injection] read %d DWARF sections: .debug_line=%d .debug_aranges=%d .debug_rnglists=%d .debug_info=%d .debug_abbrev=%d .debug_str=%d .debug_loc=%d\n", + 7, + #(existing_sections[".debug_line"] or ""), + #(existing_sections[".debug_aranges"] or ""), + #(existing_sections[".debug_rnglists"] or ""), + #(existing_sections[".debug_info"] or ""), + #(existing_sections[".debug_abbrev"] or ""), + #(existing_sections[".debug_str"] or ""), + #(existing_sections[".debug_loc"] or ""))) -- Build the atom table (cross-ref nm symbols with source-map.txt entries). local atom_table = build_atom_table(ctx) 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). + local _rbind_atoms, rbind_structs = parse_rbind_atoms(ctx, atom_table) + local rbind_count = 0 + for _ in pairs(_rbind_atoms) do rbind_count = rbind_count + 1 end + io.stderr:write(string.format("[dwarf_injection] matched %d rbind atoms across %d Binds_* structs\n", + rbind_count, (function() local n = 0; for _ in pairs(rbind_structs) do n = n + 1 end; return n end)())) + -- 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): + -- 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). + -- 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; + -- preserves all original DIE bytes; does NOT append a synthetic CU). + -- 3. Build .debug_line + .debug_aranges + .debug_rnglists (independent, F' tasks). + -- 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, + -- so no new strings are required. The existing strings table is preserved verbatim. + -- (The pre-Task 2 synthetic-CU build used .debug_str for CU name + comp_dir; that path is gone.) local basename = ctx.basename or DEFAULT_BASENAME if ctx.out_root and ctx.out_root ~= "" then duffle.ensure_dir(ctx.out_root) - -- Build + write each section via the SECTION_BUILDERS / SECTION_WRITERS dispatch. + -- Step 0: layout validation. Bail out safely if the .debug_info layout doesn't match what we expect (crt CU + DWARF5 main CU + final 0 byte). + local existing_info = existing_sections[".debug_info"] or "" + local existing_abbrev = existing_sections[".debug_abbrev"] or "" + local main_cu_start, main_cu_end_excl, main_abbrev_offset = find_main_cu_layout(existing_info) + if not main_cu_start then + io.stderr:write("[dwarf_injection] layout validation failed; writing existing sections unchanged\n") + local results_safe = { + { name = "debug_info", data = existing_info, was = #existing_info }, + { name = "debug_abbrev", data = existing_abbrev, was = #existing_abbrev }, + { name = "debug_str", data = existing_sections[".debug_str"] or "", was = #(existing_sections[".debug_str"] or "") }, + { name = "debug_line", data = build_dwarf_line_section (existing_sections[".debug_line"] or "", atom_table), was = #(existing_sections[".debug_line"] or "") }, + { name = "debug_aranges", data = build_dwarf_aranges_section (existing_sections[".debug_aranges"] or "", atom_table), was = #(existing_sections[".debug_aranges"] or "") }, + { name = "debug_rnglists", data = build_dwarf_rnglists_section(existing_sections[".debug_rnglists"] or "", atom_table), was = #(existing_sections[".debug_rnglists"] or "") }, + { name = "debug_loc", data = build_debug_loc_section(), was = #(existing_sections[".debug_loc"] or "") }, + } + local outputs_safe = {} + for _, r in ipairs(results_safe) do + local path = SECTION_WRITERS[r.name](ctx.out_root, basename) + local f = io.open(path, "wb") + if not f then + io.stderr:write(string.format("[dwarf_injection] failed to open %s for write\n", path)) + else + f:write(r.data); f:close() + outputs_safe[#outputs_safe + 1] = { [r.name .. "_bin"] = path } + end + end + return { outputs = outputs_safe, errors = {}, warnings = {} } + end + + -- Step 1: duplicate main abbrev table + append new codes 100..106. + local new_abbrev, new_abbrev_offset = build_debug_abbrev_section(existing_abbrev, main_abbrev_offset) + if not new_abbrev_offset then + io.stderr:write("[dwarf_injection] main abbrev-table validation failed; refusing to emit malformed DWARF\n") + return { outputs = {}, errors = {}, warnings = {"main abbrev-table validation failed"} } + end + + -- Step 2: splice inserted children into the main CU (patches unit_length + abbrev_offset; no synthetic CU). + local new_info = build_debug_info_section(existing_info, main_cu_start, main_cu_end_excl, new_abbrev_offset, atom_table, rbind_structs) + + -- Step 3-4: independent sections (.debug_str left untouched — see comment above). + local results = { + { name = "debug_abbrev", data = new_abbrev, was = #existing_abbrev }, + { name = "debug_info", data = new_info, was = #existing_info }, + { name = "debug_str", data = existing_sections[".debug_str"] or "", was = #(existing_sections[".debug_str"] or "") }, + { name = "debug_line", data = build_dwarf_line_section (existing_sections[".debug_line"] or "", atom_table), was = #(existing_sections[".debug_line"] or "") }, + { name = "debug_aranges", data = build_dwarf_aranges_section (existing_sections[".debug_aranges"] or "", atom_table), was = #(existing_sections[".debug_aranges"] or "") }, + { name = "debug_rnglists", data = build_dwarf_rnglists_section(existing_sections[".debug_rnglists"] or "", atom_table), was = #(existing_sections[".debug_rnglists"] or "") }, + { name = "debug_loc", data = build_debug_loc_section(), was = #(existing_sections[".debug_loc"] or "") }, + } + local outputs = {} - for name, builder in pairs(SECTION_BUILDERS) do - local existing = existing_sections["." .. name] or "" - local bytes = builder(existing, atom_table) - local path = SECTION_WRITERS[name](ctx.out_root, basename) - io.stderr:write(string.format("[dwarf_injection] new .%s = %d bytes (was %d, +%d atoms)\n" - , name, #bytes, #existing, #atom_table)) + for _, r in ipairs(results) do + local path = SECTION_WRITERS[r.name](ctx.out_root, basename) local f = io.open(path, "wb") if not f then io.stderr:write(string.format("[dwarf_injection] failed to open %s for write\n", path)) else - f:write(bytes); f:close() - outputs[#outputs + 1] = { [name .. "_bin"] = path } + f:write(r.data); f:close() + io.stderr:write(string.format("[dwarf_injection] new .%s = %d bytes (was %d, +%d atoms)\n" + , r.name, #r.data, r.was, #atom_table)) + outputs[#outputs + 1] = { [r.name .. "_bin"] = path } end end return { diff --git a/scripts/ps1_meta.lua b/scripts/ps1_meta.lua index ca74a17..6ebdb16 100644 --- a/scripts/ps1_meta.lua +++ b/scripts/ps1_meta.lua @@ -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 = "/.dwarf_line.bin" }, - { kind = "report", path_template = "/.dwarf_aranges.bin" }, + { kind = "report", path_template = "/.dwarf_line.bin" }, + { kind = "report", path_template = "/.dwarf_aranges.bin" }, { kind = "report", path_template = "/.dwarf_rnglists.bin" }, + { kind = "report", path_template = "/.dwarf_abbrev.bin" }, + { kind = "report", path_template = "/.dwarf_info.bin" }, + { kind = "report", path_template = "/.dwarf_str.bin" }, + { kind = "report", path_template = "/.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