--- passes/dwarf_injection.lua — Per-atom DWARF injection for tape-atom step-debug. --- --- 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, and 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` plus deterministic `/.gdbinit` skip commands. --- --- 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= --- 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, AND the Variables pane shows the wave-context regs as atom-scoped locals. --- 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, Lua 5.3 compatible. -- ════════════════════════════════════════════════════════════════════════════ -- Bootstrap -- ════════════════════════════════════════════════════════════════════════════ -- Load `duffle_paths.lua` via `debug.getinfo(1, "S").source` (works both standalone + when require'd). -- Sets package.path + package.cpath then returns duffle. local _bootstrap_dir = debug.getinfo(1, "S").source:match("^@?(.*[/\\])") or "./" local duffle = dofile(_bootstrap_dir .. "../duffle_paths.lua") -- ELF32 / DWARF / atoms-source-map utilities (post-link debug-info injection). -- Sister module to duffle.lua — contains the format-constant tables (ELF32 byte offsets, DWARF opcodes, etc.) and the I/O helpers -- (read_elf_sections, nm, source-map parser, LE byte r/w). `list_dir` lives in duffle.lua as a general I/O primitive (lifted out during F''). local elf_dwarf = require("elf_dwarf") -- word-counting helper shared with passes/atoms_source_map.lua. -- Used here to walk a component's body_tokens in lockstep with their word-count allocation -- when we propagate per-word body lines into each invocation's `body_lines` array. local word_count_eval = require("word_count_eval") local count_token_words = word_count_eval.count_token_words local lfs = require("lfs") -- File-scope aliases to elf_dwarf helpers; the canonical implementations live in scripts/elf_dwarf.lua. -- (2-caller lift: these were duplicated file-locals; the canonical is in elf_dwarf.lua, used by parse_abbrev_table + read_form_value.) local read_uleb128_at = elf_dwarf.read_uleb128_at local read_sleb128_at = elf_dwarf.read_sleb128_at local find_abbrev_table_end = elf_dwarf.find_abbrev_table_end -- Note: uleb128 + sleb128 are encoders; read_uleb128_at + read_sleb128_at are decoders. Different functions. local uleb128 = elf_dwarf.uleb128 local sleb128 = elf_dwarf.sleb128 -- ════════════════════════════════════════════════════════════════════════════ -- Constants -- ════════════════════════════════════════════════════════════════════════════ -- DWARF line-program opcodes + range-list entry encodings (per DWARF5 spec). -- All values lifted from `elf_dwarf.DWARF_LINE_OPS` + `elf_dwarf.DWARF5_RNGLISTS`. -- Local aliases preserve the code's readability -- (e.g. `DW_LNS_copy` reads better than `elf_dwarf.DWARF_LINE_OPS.DW_LNS_copy` in an emitter body). 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_negate_stmt = DWARF_LINE_OPS.DW_LNS_negate_stmt 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 -- File index 11 in the existing main line unit is hello_gte_tape.c. -- The injector extends that unit rather than appending an unreferenced unit. local ATOM_SOURCE_FILE_INDEX = 11 -- RR_ debug-visible variables come from the merged register_alias_registry filtered to aliases whose code is a valid MIPS GPR 0..31 -- (see collect_per_source_registries + by_alias in build_inserted_children). -- Names are prefixed with `RR` to avoid collision with the C-level enum (`{R_AtomJmp = 25, R_TapePtr = 24, R_PrimCursor = 15, ...}` declared in duffle headers); -- without the prefix, gdb's `print R_PrimCursor` resolves to the enum constant, not to this DWARF variable. -- With `set print enum on`, gdb displays the enum identifier, so the Watch panel would show `R_PrimCursor = R_PrimCursor` instead of the register value. -- Each RR_'s DW_OP_regN uses the alias's `code` as N. -- DW_OP_bregN would describe a memory location addressed from a register; -- the breg form would make gdb dereference the atom register value rather than display it. -- 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) 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) -- Component step-into (DW_TAG_inlined_subroutine + abstract DW_TAG_subprogram). local ABBREV_ABSTRACT_SUBPROGRAM = 0x6B -- 107: DW_TAG_subprogram (abstract — no low_pc/high_pc); for each unique mac_X component local ABBREV_INLINED_SUBROUTINE = 0x6C -- 108: DW_TAG_inlined_subroutine with children (per-component invocation range) -- Bind_args uses DW_FORM_sec_offset → .debug_loclists for PC-ranged liveness -- (each field transitions from tape memory to GPR at load_pc + 8 = MIPS I load-delay slot boundary). local ABBREV_BIND_VAR_LOCLIST = 0x6D -- 109: DW_TAG_variable no children + DW_AT_type = ref4 + DW_AT_location = sec_offset -- Typed-view pointer_type (for the synthetic V4_S2* / V3_S2* / U4* / void* chains). -- MUST be a fresh abbrev code in the appended table — emitting uleb128(9) collides with GCC's -- existing abbrev 9 (a pointer_type that carries DW_AT_byte_size + DW_AT_type), so gdb misparses -- our 4-byte ref4 as (byte_size, type[0..2]) and lands the cursor mid-attribute. local ABBREV_TYPED_VIEW_POINTER = 0x6E -- 110: DW_TAG_pointer_type no children + DW_AT_type = ref4 (typed-view / U4 / void chain) -- DWARF5 §7.7.3 loclist opcodes. local DW_LLE_end_of_list = 0x00 local DW_LLE_start_length = 0x08 local DW_OP_reg0 = 0x50 -- base reg op; regN = 0x50 + N local DW_OP_breg0 = 0x70 -- base breg op; bregN = 0x70 + N (SLEB offset) local DW_OP_piece = 0x93 local MIPS_LOAD_DELAY_BYTES = 0x08 -- 1 load word + 1 BD-slot word -- DIE children-list terminator: each DIE that has children ends with a single 0 byte (DWARF5 §7.5.3). -- This is NOT the same as DW_LLE_end_of_list despite sharing the value 0x00 — different spec sections. local DIE_CHILDREN_TERMINATOR = 0x00 -- Field/piece byte sizes for the typed-view piece chains. -- All Binds_* struct fields are U4 (sizeof(uint32_t) on MIPS32 = 4 bytes). local U4_BYTE_SIZE = 4 -- 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_TAG_pointer_type = 0x0F -- Component step-into. local DW_TAG_inlined_subroutine = 0x1D 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 -- Inlined_subroutine + abstract_origin attributes. local DW_AT_abstract_origin = 0x31 local DW_AT_call_file = 0x58 local DW_AT_call_line = 0x59 local DW_AT_inline = 0x20 -- DWARF5 §7.7.1: DW_AT_inline (used by abstract subprogram for the mac_X() components) -- decl_file + decl_line on the abstract subprogram so consumers can resolve an abstract origin back to its definition site even when no inlined_subroutine instance currently maps to it. local DW_AT_decl_file = 0x3A -- DWARF5 §7.7.1: DW_AT_decl_file (1-based file index into the CU's file table) local DW_AT_decl_line = 0x3B -- DWARF5 §7.7.1: DW_AT_decl_line -- File index lookup table for the existing main line unit (Unit 2). -- Provenance paths come back with mixed slashes; we normalize to basename and look up against the line unit's actual file table. -- Current scope has two provenance basenames: hello_gte_tape.c (the atom's call site) and lottes_tape.h (the component definition). -- Both live in the existing gcc-generated line unit; -- their 1-based indices are stable across rebuilds because the include order in code/gte_hello/hello_gte.c determines the unit's file table. local PROVENANCE_BASENAME_TO_FILE_INDEX = { ["hello_gte_tape.c"] = ATOM_SOURCE_FILE_INDEX, -- = 11 ["lottes_tape.h"] = 4, } --- Resolve an absolute provenance path to the line-unit file index used by the emitting line program. --- Normalizes mixed `/` and `\` separators to a basename and looks it up against the known file table. --- --- Fails loudly on an unknown provenance basename: adding a new component source file requires extending --- `PROVENANCE_BASENAME_TO_FILE_INDEX` so the line-program emission contract stays explicit. --- Silent fallback to ATOM_SOURCE_FILE_INDEX would mask the new-file case by misattributing component rows to the atom's source file. --- @param path string -- absolute provenance path (e.g. "C:/.../lottes_thttps://www.youtube.com/watch?v=ORM4yLkdKx8ape.h" or "C:\\...\\lottes_tape.h") --- @return integer -- 1-based line-unit file index local function resolve_provenance_file_index(path) if path == nil or path == "" then error("[dwarf_injection] resolve_provenance_file_index: empty path") end -- Normalize backslashes → forward slashes (paths arrive with mixed separators from the provenance file: forward slashes from Lua's io.lines; -- backslashes if the input ever round-trips through Windows shell expansion). local normalized = path:gsub("\\", "/") -- Take the last path component (the basename). local basename = normalized:match("([^/]+)$") or normalized local idx = PROVENANCE_BASENAME_TO_FILE_INDEX[basename] if idx == nil then error(string.format( "[dwarf_injection] resolve_provenance_file_index: unknown provenance basename '%s' (from '%s'). " .. "Extend PROVENANCE_BASENAME_TO_FILE_INDEX in passes/dwarf_injection.lua.", basename, path)) end return idx end 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_FORM_sec_offset = 0x17 -- 4-byte section-relative offset (into .debug_loclists / .debug_rnglists) 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, but we want this CU to look like a C TU so VSCode's Variables pane treats it as code.) -- R_ → MIPS GPR lookups go through the merged register_alias_registry -- (collected by collect_per_source_registries from ctx.sources[*].scan.register_alias_registry). -- Aliases without an `atom_reg` opt-in are absent from the registry, which means they are NOT debug-visible (no fallback GPR). -- See the precedence chain in build_inserted_children for the per-alias type resolution. -- Build the .debug_loclists section for every rbind atom. -- Returns the section bytes (DWARF5 layout: unit_length(4) + version=5(2) + address_size=4(1) + segment_size=0(1) + offset_entry_count=0(4) + DW_LLE entries; -- each atom contributes one loclist terminated by DW_LLE_end_of_list). -- -- Per rbind atom we emit 2 loclist entries (tape → GPR split) using the last field's transition as the boundary. -- This is a conservative approximation that always reads the correct GPR value once the first load has retired (load_pc + 8). -- -- `tape_alias` (default: "R_TapePtr") names the wave-runtime pointer register whose value is the tape address. -- The GPR integer comes from the merged registry; if the alias is absent, this function fails loud -- (the build was misconfigured; rbind atoms depend on R_TapePtr being in the registry for their piece-chain DW_OP_breg location). -- @param atom_table table[] -- list of atoms with .rbind set -- @param registries table -- merged registries from collect_per_source_registries -- @return string -- section bytes local function build_debug_loclists_section(atom_table, registries) registries = registries or {} -- R_TapePtr comes from the merged register_alias_registry (only present if the user opted it in via `#define atom_reg` in lottes_tape.h). -- When absent we emit just the section terminator (a single DW_LLE_end_of_list byte); -- the .debug_loclists section MUST not be empty for the linker, and `bind_args` will be emitted with no loclist PC range -- (readelf will display it as having no .debug_loclists entries). local tape_alias_entry = registries.register_alias_registry and registries.register_alias_registry["R_TapePtr"] local tape_reg = tape_alias_entry and tape_alias_entry.code local parts = {} for _, atom in ipairs(atom_table) do if atom.rbind and tape_reg then local fields = atom.rbind.fields or {} local regs = atom.rbind.regs or {} local n_fields = #fields local last_load_pc = atom.addr + (n_fields - 1) * MIPS_BYTES_PER_WORD local transition_pc = last_load_pc + MIPS_LOAD_DELAY_BYTES local tape_pieces = {} for _, f in ipairs(fields) do local offset = f.offset or 0 local offset_sleb = elf_dwarf.sleb128(offset) -- (DW_OP_bregN, SLEB128(offset), DW_OP_piece, ULEB128(U4_BYTE_SIZE)) -- 4 = U4_BYTE_SIZE: each piece is sizeof(uint32_t) on MIPS32. table.insert(tape_pieces, string.char(DW_OP_breg0 + tape_reg) .. offset_sleb .. string.char(DW_OP_piece) .. uleb128(U4_BYTE_SIZE)) end local tape_expr = table.concat(tape_pieces) local gpr_pieces = {} for _, pair in ipairs(regs) do -- (DW_OP_regN, DW_OP_piece, ULEB128(4)) — one piece per GPR-resident field. -- The 4 = U4_BYTE_SIZE: each piece is sizeof(uint32_t) on MIPS32. table.insert(gpr_pieces, string.char(DW_OP_reg0 + pair.reg) .. string.char(DW_OP_piece) .. uleb128(U4_BYTE_SIZE)) end local gpr_expr = table.concat(gpr_pieces) parts[#parts + 1] = string.char(DW_LLE_start_length) .. elf_dwarf.write_u32_le(atom.addr) .. uleb128(#tape_expr) .. tape_expr parts[#parts + 1] = string.char(DW_LLE_start_length) .. elf_dwarf.write_u32_le(transition_pc) .. uleb128(#gpr_expr) .. gpr_expr parts[#parts + 1] = string.char(DW_LLE_end_of_list) end end -- Loclist unit header (DWARF5 §7.7.2): -- unit_length(4) + version(2) + address_size(1) + segment_size(1) + offset_entry_count(4) = 12 bytes header. -- version = 5 (DWARF5); address_size = 4 (MIPS32); segment_size = 0; offset_entry_count = 0 (we use DW_LLE_start_length, not offsets). local LOCLIST_HEADER_SIZE = 12 local body = table.concat(parts) local unit_length = LOCLIST_HEADER_SIZE - 4 + #body -- -4 because unit_length excludes itself local header = elf_dwarf.write_u32_le(unit_length) .. elf_dwarf.write_u16_le(5) -- DWARF5 .. string.char(U4_BYTE_SIZE) -- address_size .. string.char(0) -- segment_size .. elf_dwarf.write_u32_le(0) -- offset_entry_count return header .. body end -- Compute the size of one tape piece for an atom's struct field offset. -- The piece is: DW_OP_bregN(1) + sleb128(offset)(1..5) + DW_OP_piece(1) + uleb128(4)(1). -- Sleb128(0) is 1 byte; sleb128(63) is 1 byte; sleb128(64) is 2 bytes; ...; sleb128(2^28) is 5 bytes. -- 4 = U4_BYTE_SIZE: each field is sizeof(uint32_t) on MIPS32. -- @param offset integer -- the field's offset within the struct (0..2^32-1) -- @return integer -- encoded size in bytes (4 for offsets < 64, 5 for offsets 64..8191, ..., 8 for > 2^28) local function tape_piece_size(offset) return 1 + elf_dwarf.sleb128_size(offset) + 1 + elf_dwarf.uleb128_size(U4_BYTE_SIZE) end -- Compute the per-atom loclist offset within a .debug_loclists section. -- @param atom_table table[] -- list of atoms with .rbind set -- @return table -- {[atom_name] = offset_in_section} local function compute_loclists_offsets(atom_table) local LOCLIST_ENTRY_HEADER_SIZE = 1 + 4 + 1 -- DW_LLE_start_length(1) + addr(4) + uleb_length(1) local offsets = {} -- Loclist unit header (DWARF5 §7.7.2): unit_length(4) + version(2) + address_size(1) + segment_size(1) + offset_entry_count(4) = 12 bytes. -- The unit_length itself is not counted in the unit_length value, so the body starts at byte 12. local cursor = 4 + 2 + 1 + 1 + 4 -- = 12 for _, atom in ipairs(atom_table) do if atom.rbind then offsets[atom.name] = cursor local n_fields = #atom.rbind.fields -- Sum the actual size of each tape piece based on the field's offset, not an assumed constant. -- The expression below mirrors what build_debug_loclists_section produces: -- 1 (DW_LLE_start_length) + 4 (PC) + 1 (uleb length prefix) + sum(tape_piece_size(field.offset)) -- + 1 (DW_LLE_start_length) + 4 (transition_pc) + 1 (uleb length prefix) + n_fields * 3 (gpr pieces) -- + 1 (DW_LLE_end_of_list) local tape_pieces_size = 0 for _, f in ipairs(atom.rbind.fields or {}) do tape_pieces_size = tape_pieces_size + tape_piece_size(f.offset or 0) end local gpr_pieces_size = n_fields * 3 -- each gpr piece: DW_OP_regN(1) + DW_OP_piece(1) + uleb(4)(1) = 3 bytes local tape_entry = LOCLIST_ENTRY_HEADER_SIZE + tape_pieces_size local gpr_entry = LOCLIST_ENTRY_HEADER_SIZE + gpr_pieces_size local body_len = tape_entry + gpr_entry + 1 -- +1 for DW_LLE_end_of_list cursor = cursor + body_len end end return offsets end -- 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. local DEFAULT_BASENAME = "hello_gte" -- ════════════════════════════════════════════════════════════════════════════ -- Type declarations -- ════════════════════════════════════════════════════════════════════════════ --- @class DwarfInjectionCtx --- @field flags table -- ctx.flags; reads flags.elf_path + flags.dwarf_injection --- @field sources table[] -- source files with scan.skip_over associations --- @field out_root string -- output root (e.g. "build/gen") --- @field basename string -- input ELF basename (default "hello_gte") --- Normalize a source path for GDB linespecs and component-association keys. --- GDB accepts forward slashes on Windows; absolute paths avoid cwd-dependent sidecars and match provenance contract. --- @param path string --- @return string local function normalize_debug_path(path) return duffle.to_absolute_path(path):gsub("\\", "/") end --- Consume the per-source scanner associations without naming any atom or component in production. --- Whole atoms remain symbol-keyed; components are file-qualified internally so a source marker associates --- with its exact component definition even though GDB 12 requires function-only skip entries for the resulting synthetic inline frame. --- @param ctx DwarfInjectionCtx --- @return table -- {atoms = {[symbol] = association}, components = {[file|name] = association}} local function collect_skip_over(ctx) local skip_over = { atoms = {}, components = {} } for _, src in ipairs(ctx.sources or {}) do local scan_skip = src.scan and src.scan.skip_over if scan_skip then for atom_name, association in pairs(scan_skip.atoms or {}) do skip_over.atoms[atom_name] = { name = atom_name, source_path = normalize_debug_path(src.path), association = association, } end for component_name, association in pairs(scan_skip.components or {}) do local source_path = normalize_debug_path(src.path) -- component_skip_key (the lower() .. "\0" .. component_name part) inlined at this single call site -- (no other callers remain after pass 17). skip_over.components[source_path:lower() .. "\0" .. component_name] = { name = component_name, source_path = source_path, association = association, } end end end return skip_over end --- Merge the per-source scanner registries (register_alias_registry, type_name_registry, atom_views) --- into a single set of tables that downstream consumers can read from without re-iterating ctx.sources. --- --- Every `R_*` lookup and per-atom type override resolution in this file goes through this merged table. --- Aliases without `atom_reg` adjacent are absent; the absence is treated as "not debug-visible" (see build_inserted_children for the precedence chain). --- --- When two sources register the same key, the last-writer wins (later sources override earlier). --- Today only one source declares wave-context enums, so collisions are absent. --- @param ctx DwarfInjectionCtx --- @return table -- { --- register_alias_registry = {[R_Name] = AliasEntry}, --- type_name_registry = {[T] = TypeEntry}, --- atom_views = {[atom_name] = AtomViewEntry}, --- } local function collect_per_source_registries(ctx) local merged = { register_alias_registry = {}, type_name_registry = {}, atom_views = {}, -- Per-atom atom_ctx declarations: atom_name -> {rbind_atom, ...} -- (populated by scan_source from `atom_ctx()` sub-calls inside `atom_info`) atom_ctxs = {}, -- Per-phase atom groups: phase_label -> {atoms = {atom_name1, ...}} -- (populated by scan_source from `atom_phase(