mirror of
https://github.com/Ed94/pikuma_ps1.git
synced 2026-07-16 14:21:26 -07:00
TODO: need to review snapshot
This commit is contained in:
Vendored
+1
@@ -138,6 +138,7 @@
|
||||
"monitor reset shellhalt",
|
||||
"load build/hello_gte.dwarf-injected.elf",
|
||||
"source scripts/gdb/gdb_tape_atoms.gdb",
|
||||
"source build/gen/hello_gte.gdbinit",
|
||||
"tbreak main",
|
||||
"continue"
|
||||
]
|
||||
|
||||
+21
-5
@@ -18,11 +18,13 @@
|
||||
* - atom word-counts in word_counts.metadata.h agree with the body's
|
||||
* actual .word count.
|
||||
*
|
||||
* WHY A PURE MACRO (atom_info, atom_bind, atom_reads, atom_writes, atom_label)
|
||||
* -----------------------------------------------------------------
|
||||
* Each of these expands to a C comment or to nothing. The C preprocessor
|
||||
* strips them to whitespace. The metaprogram reads the literal token from
|
||||
* source-as-written, NOT from the preprocessed output. This means:
|
||||
* WHY PURE MACROS
|
||||
* ---------------
|
||||
* atom_info, atom_bind, atom_reads, atom_writes, atom_label,
|
||||
* atom_dbg_skip_over each expand to a C comment or to nothing. The C
|
||||
* comment or to nothing. The C preprocessor strips them to whitespace.
|
||||
* The metaprogram reads the literal tokens from source-as-written, NOT from
|
||||
* the preprocessed output. This means:
|
||||
* - the C compiler does no work for them (no __attribute__, no
|
||||
* _Pragma, no asm side-effects)
|
||||
* - they can never silently drift from the metaprogram's view
|
||||
@@ -150,6 +152,20 @@
|
||||
* ============================================================================*/
|
||||
#define atom_info(...) /* atom_info(__VA_ARGS__) */
|
||||
|
||||
/* ----------------------------------------------------------------------------
|
||||
* DEBUG SOURCE-STEP MARKERS
|
||||
*
|
||||
* Place atom_dbg_skip_over() before a MipsAtom_, MipsAtomComp_, or
|
||||
* MipsAtomComp_Proc_ declaration. The following declaration kind determines
|
||||
* whether the marker selects a whole atom or a component inline view. The
|
||||
* source scanner associates the marker with that declaration; placement
|
||||
* diagnostics are handled by the annotation pass.
|
||||
*
|
||||
* The macro expands to a comment only. It emits no C data or MIPS words and
|
||||
* therefore cannot affect runtime output.
|
||||
* ----------------------------------------------------------------------------*/
|
||||
#define atom_dbg_skip_over() /* atom_dbg_skip_over: skip the following atom or component source view */
|
||||
|
||||
/* ----------------------------------------------------------------------------
|
||||
* atom_bind(Binds_X) -- rbind sub-call of atom_info
|
||||
*
|
||||
|
||||
@@ -23,7 +23,6 @@ WORD_COUNT(mac_yield, 4)
|
||||
, load_half_u(R_T2, R_FaceCursor, 2 * S_(S2))
|
||||
WORD_COUNT(mac_load_tri_indices, 3)
|
||||
|
||||
/* Words: 18; Translates indices to vertex addresses and pushes them to GTE */
|
||||
#define mac_gte_load_tri_verts(...) \
|
||||
shift_lleft(R_AT, R_T0, v3s2_byteoff) \
|
||||
, add_u_self(R_AT, R_VertBase) \
|
||||
@@ -77,7 +76,7 @@ WORD_COUNT(mac_insert_ot_tag_g4, 11)
|
||||
|
||||
#define mac_pack_color_word(off, cmd, r, g, b) \
|
||||
load_upper_i(R_AT, (cmd) << 8 | (b)) \
|
||||
, or_i_self( R_AT, ((g) << 8) | (r)) \
|
||||
, or_i_self( R_AT, ((g) << 8) | (r)) \
|
||||
, store_word( R_AT, R_PrimCursor, (off))
|
||||
WORD_COUNT(mac_pack_color_word, 3)
|
||||
|
||||
|
||||
@@ -121,6 +121,7 @@ MipsAtomComp_(ac_load_tri_indices) {
|
||||
};
|
||||
|
||||
/* Words: 18; Translates indices to vertex addresses and pushes them to GTE */
|
||||
atom_dbg_skip_over()
|
||||
MipsAtomComp_(ac_gte_load_tri_verts) {
|
||||
shift_lleft(R_AT, R_T0, v3s2_byteoff), add_u_self(R_AT, R_VertBase), load_word(R_V0, R_AT, O_(V3_S2,x)), load_word(R_V1, R_AT, O_(V3_S2,z)), gte_mv_to_data_r(R_V0, C2_VXY0), gte_mv_to_data_r(R_V1, C2_VZ0),
|
||||
shift_lleft(R_AT, R_T1, v3s2_byteoff), add_u_self(R_AT, R_VertBase), load_word(R_V0, R_AT, O_(V3_S2,x)), load_word(R_V1, R_AT, O_(V3_S2,z)), gte_mv_to_data_r(R_V0, C2_VXY1), gte_mv_to_data_r(R_V1, C2_VZ1),
|
||||
@@ -162,7 +163,7 @@ MipsAtomComp_(ac_insert_ot_tag_g4) {
|
||||
FI_ MipsAtom ac_pack_color_word(U4 off, U4 cmd, U1 r, U1 g, U1 b)
|
||||
MipsAtomComp_Proc_(ac_pack_color_word, {
|
||||
load_upper_i(R_AT, (cmd) << 8 | (b)),
|
||||
or_i_self( R_AT, ((g) << 8) | (r)),
|
||||
or_i_self( R_AT, ((g) << 8) | (r)),
|
||||
store_word( R_AT, R_PrimCursor, (off)),
|
||||
})
|
||||
|
||||
|
||||
@@ -109,6 +109,7 @@ MipsAtom_(rbind_floor_f3_face) atom_info(atom_bind(Binds_FloorTri)
|
||||
};
|
||||
|
||||
internal
|
||||
atom_dbg_skip_over()
|
||||
MipsAtom_(floor_f3_face) atom_info(
|
||||
, atom_reads( R_PrimCursor, R_FaceCursor, R_VertBase, R_OtBase)
|
||||
, atom_writes(R_PrimCursor, R_FaceCursor)
|
||||
|
||||
+19
-12
@@ -456,36 +456,43 @@ function build-gte_hello {
|
||||
# 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))
|
||||
$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'
|
||||
$dwarfLoclistsBin = Join-Path (Join-Path $path_build 'gen') 'hello_gte.dwarf_loclists.bin'
|
||||
if ((Test-Path $dwarfInfoBin) -and (Test-Path $dwarfAbbrevBin) -and (Test-Path $dwarfStrBin) -and (Test-Path $dwarfLocBin) -and (Test-Path $dwarfLoclistsBin))
|
||||
{
|
||||
Write-Host "[build] G' atom-locals: splicing .debug_info/.debug_abbrev/.debug_str/.debug_loc into $injectElf"
|
||||
Write-Host "[build] G' atom-locals: splicing .debug_info/.debug_abbrev/.debug_str/.debug_loc/.debug_loclists into $injectElf"
|
||||
& $Objcopy --update-section ".debug_info=$dwarfInfoBin" $injectElf
|
||||
$last_exit_code_error = ($LASTEXITCODE -ne 0)
|
||||
$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
|
||||
}
|
||||
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"
|
||||
# .debug_loclists doesn't exist in the source ELF; --add-section creates it.
|
||||
& $Objcopy --add-section ".debug_loclists=$dwarfLoclistsBin" $injectElf
|
||||
if ($LASTEXITCODE -ne 0) {
|
||||
Write-Warning "[build] objcopy .debug_loclists add-section failed (exit $LASTEXITCODE)"
|
||||
} else {
|
||||
Write-Host "[build] G' atom-locals-injected: $injectElf"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -21,6 +21,78 @@ local lfs = require("lfs")
|
||||
|
||||
local M = {}
|
||||
|
||||
-- ════════════════════════════════════════════════════════════════════════════
|
||||
-- DWARF tag + form constants
|
||||
-- ════════════════════════════════════════════
|
||||
-- (DWARF5 §7.5.5 "Tag Encodings" + Table 7.1; gcc emits these exact values for
|
||||
-- the DWARF3-extension and DWARF5 line units.)
|
||||
|
||||
M.DW_TAG = {
|
||||
compile_unit = 0x11,
|
||||
subprogram = 0x2E,
|
||||
variable = 0x34,
|
||||
structure_type = 0x13,
|
||||
member = 0x0D,
|
||||
base_type = 0x24,
|
||||
typedef = 0x2A,
|
||||
pointer_type = 0x0F,
|
||||
const_type = 0x26,
|
||||
volatile_type = 0x27,
|
||||
inlined_subroutine = 0x1D,
|
||||
-- We index the canonical gcc-emitted tags. Anything else falls through.
|
||||
}
|
||||
|
||||
M.DW_AT = {
|
||||
name = 0x03,
|
||||
low_pc = 0x11,
|
||||
high_pc = 0x12,
|
||||
language = 0x13,
|
||||
location = 0x02,
|
||||
comp_dir = 0x1B,
|
||||
byte_size = 0x0B,
|
||||
encoding = 0x3E,
|
||||
data_member_location = 0x38,
|
||||
type = 0x49,
|
||||
linkage_name = 0x6E,
|
||||
external = 0x3F,
|
||||
abstract_origin = 0x31,
|
||||
call_file = 0x58,
|
||||
call_line = 0x59,
|
||||
inline = 0x20,
|
||||
decl_file = 0x3A,
|
||||
decl_line = 0x3B,
|
||||
}
|
||||
|
||||
M.DW_FORM = {
|
||||
addr = 0x01,
|
||||
data1 = 0x0B,
|
||||
data2 = 0x05,
|
||||
data4 = 0x06,
|
||||
string = 0x08,
|
||||
strp = 0x0E,
|
||||
exprloc = 0x18,
|
||||
ref4 = 0x13,
|
||||
udata = 0x0F,
|
||||
ref_sig8 = 0x20,
|
||||
implicit_const = 0x21,
|
||||
flag_present = 0x19,
|
||||
sec_offset = 0x17,
|
||||
}
|
||||
|
||||
M.DW_ATE = {
|
||||
address = 0x01,
|
||||
boolean = 0x02,
|
||||
complex_float = 0x03,
|
||||
float = 0x04,
|
||||
signed = 0x05,
|
||||
signed_char = 0x06,
|
||||
unsigned = 0x07,
|
||||
unsigned_char = 0x08,
|
||||
}
|
||||
|
||||
-- DWARF5 §7.5.6 DW_FORM_implicit_const
|
||||
local DW_FORM_implicit_const = 0x21
|
||||
|
||||
-- ════════════════════════════════════════════════════════════════════════════
|
||||
-- Format-constant tables
|
||||
-- ════════════════════════════════════════════════════════════════════════════
|
||||
@@ -121,6 +193,7 @@ M.DWARF_LINE_OPS = {
|
||||
DW_LNS_advance_pc = 2,
|
||||
DW_LNS_advance_line = 3,
|
||||
DW_LNS_set_file = 4,
|
||||
DW_LNS_negate_stmt = 6, -- spec: §6.2.5.2 — toggle the line-state is_stmt register
|
||||
-- Extended sub-opcodes (§6.2.5.3)
|
||||
DW_LNE_end_sequence = 1, -- spec: §6.2.5.3
|
||||
DW_LNE_set_address = 2, -- spec: §6.2.5.3
|
||||
@@ -171,6 +244,500 @@ function M.read_u16_le(buf, off)
|
||||
return buf:byte(off) + buf:byte(off + 0x01) * 0x00000100
|
||||
end
|
||||
|
||||
-- Pure-Lua 5.3 LEB128 readers (no `bit` library). `2^shift` arithmetic matches
|
||||
-- the existing F' parser. Offsets are 0-based; returns (value, next_pos).
|
||||
local function read_uleb128_at(buf, pos)
|
||||
local value, shift = 0, 0
|
||||
local len = #buf
|
||||
while pos < len do
|
||||
local b = buf:byte(pos + 1)
|
||||
value = value + (b % 0x80) * (2 ^ shift)
|
||||
shift = shift + 7
|
||||
pos = pos + 1
|
||||
if b < 0x80 then return value, pos end
|
||||
end
|
||||
return nil, pos
|
||||
end
|
||||
|
||||
local function read_sleb128_at(buf, pos)
|
||||
local value, shift = 0, 0
|
||||
local len = #buf
|
||||
while pos < len do
|
||||
local b = buf:byte(pos + 1)
|
||||
value = value + (b % 0x80) * (2 ^ shift)
|
||||
shift = shift + 7
|
||||
pos = pos + 1
|
||||
if b < 0x80 then
|
||||
if b >= 0x40 then value = value - (2 ^ shift) end
|
||||
return value, pos
|
||||
end
|
||||
end
|
||||
return nil, pos
|
||||
end
|
||||
|
||||
-- Find the 0-based offset of the table-terminator byte (a single 0) for the
|
||||
-- abbrev table starting at `table_start`. Returns nil on truncated input.
|
||||
-- Walks declaration headers (code, tag, has_children, attr/form pairs,
|
||||
-- DW_FORM_implicit_const constant) until it finds a 0 byte that follows a
|
||||
-- complete declaration. Mirrors dwarf_injection.lua :: find_abbrev_table_end.
|
||||
local function find_abbrev_table_end(table_bytes, table_start)
|
||||
local pos, len = table_start, #table_bytes
|
||||
if pos >= len or table_bytes:byte(pos + 1) == 0 then return pos end
|
||||
while pos < len do
|
||||
local _code, code_end = read_uleb128_at(table_bytes, pos)
|
||||
if not _code then return nil end
|
||||
pos = code_end
|
||||
local _tag, tag_end = read_uleb128_at(table_bytes, pos)
|
||||
if not _tag then return nil end
|
||||
pos = tag_end
|
||||
if pos >= len then return nil end
|
||||
pos = pos + 1 -- has_children byte
|
||||
while pos < len do
|
||||
local attr, attr_end = read_uleb128_at(table_bytes, pos)
|
||||
if not attr then return nil end
|
||||
pos = attr_end
|
||||
local form, form_end = read_uleb128_at(table_bytes, pos)
|
||||
if not form then return nil end
|
||||
pos = form_end
|
||||
if attr == 0 and form == 0 then break end
|
||||
if form == DW_FORM_implicit_const then
|
||||
local _c, ce = read_sleb128_at(table_bytes, pos)
|
||||
if not _c then return nil end
|
||||
pos = ce
|
||||
end
|
||||
end
|
||||
if pos >= len then return nil end
|
||||
if table_bytes:byte(pos + 1) == 0 then return pos end
|
||||
end
|
||||
return nil
|
||||
end
|
||||
|
||||
-- Read the null-terminated C string at 0-based offset `off` in `buf`.
|
||||
-- Stops at the first 0 byte or end of buffer.
|
||||
local function read_c_string_at(buf, off)
|
||||
local len = #buf
|
||||
local start = off
|
||||
while off < len and buf:byte(off + 1) ~= 0 do off = off + 1 end
|
||||
return buf:sub(start + 1, off)
|
||||
end
|
||||
|
||||
-- Walk the .debug_abbrev table starting at 0-based offset `table_start` and
|
||||
-- return a list of declarations: {code, tag, has_children, attrs={ {name, form}, ... }}.
|
||||
-- Stops at the table terminator.
|
||||
local function parse_abbrev_table(table_bytes, table_start)
|
||||
local table_end = find_abbrev_table_end(table_bytes, table_start)
|
||||
if not table_end then return nil, "no terminator" end
|
||||
local decls = {}
|
||||
local pos = table_start
|
||||
while pos < table_end do
|
||||
local code, code_end = read_uleb128_at(table_bytes, pos)
|
||||
if not code then return nil, "truncated code" end
|
||||
pos = code_end
|
||||
local tag, tag_end = read_uleb128_at(table_bytes, pos)
|
||||
if not tag then return nil, "truncated tag" end
|
||||
pos = tag_end
|
||||
local has_children = table_bytes:byte(pos + 1)
|
||||
pos = pos + 1
|
||||
local attrs = {}
|
||||
while true do
|
||||
local attr, attr_end = read_uleb128_at(table_bytes, pos)
|
||||
if not attr then return nil, "truncated attr" end
|
||||
pos = attr_end
|
||||
local form, form_end = read_uleb128_at(table_bytes, pos)
|
||||
if not form then return nil, "truncated form" end
|
||||
pos = form_end
|
||||
if attr == 0 and form == 0 then break end
|
||||
attrs[#attrs + 1] = { name = attr, form = form }
|
||||
if form == DW_FORM_implicit_const then
|
||||
local _c, ce = read_sleb128_at(table_bytes, pos)
|
||||
if not _c then return nil, "truncated const" end
|
||||
pos = ce
|
||||
end
|
||||
end
|
||||
decls[#decls + 1] = { code = code, tag = tag, has_children = has_children, attrs = attrs }
|
||||
end
|
||||
return decls
|
||||
end
|
||||
|
||||
-- Read a ULEB attribute value at 0-based offset `pos` for the given `form`.
|
||||
-- Returns (value, next_pos). For DW_FORM_string we return the inline string.
|
||||
-- For DW_FORM_strp we return the inline string resolved from `str_buf`.
|
||||
-- For DW_FORM_ref4 we return the absolute CU-relative offset. The caller
|
||||
-- decides whether to interpret that as a section offset.
|
||||
local function read_form_value(buf, str_buf, pos, form)
|
||||
if form == M.DW_FORM.addr then
|
||||
return M.read_u32_le(buf, pos + 1), pos + 4
|
||||
elseif form == M.DW_FORM.string then
|
||||
local s = read_c_string_at(buf, pos)
|
||||
return s, pos + #s + 1
|
||||
elseif form == M.DW_FORM.strp then
|
||||
-- DW_FORM_strp: 4-byte offset into .debug_str.
|
||||
local strp_off = M.read_u32_le(buf, pos + 1)
|
||||
return read_c_string_at(str_buf, strp_off), pos + 4
|
||||
elseif form == M.DW_FORM.udata then
|
||||
return read_uleb128_at(buf, pos)
|
||||
elseif form == M.DW_FORM.data1 then
|
||||
return buf:byte(pos + 1), pos + 1
|
||||
elseif form == M.DW_FORM.data2 then
|
||||
return M.read_u16_le(buf, pos + 1), pos + 2
|
||||
elseif form == M.DW_FORM.data4 then
|
||||
return M.read_u32_le(buf, pos + 1), pos + 4
|
||||
elseif form == M.DW_FORM.ref4 then
|
||||
return M.read_u32_le(buf, pos + 1), pos + 4
|
||||
elseif form == M.DW_FORM.sec_offset then
|
||||
-- DW_FORM_sec_offset: 4-byte offset (size depends on DWARF version;
|
||||
-- on DWARF5 32-bit it's always 4 bytes).
|
||||
return M.read_u32_le(buf, pos + 1), pos + 4
|
||||
elseif form == M.DW_FORM.flag_present then
|
||||
return 1, pos
|
||||
elseif form == M.DW_FORM.exprloc then
|
||||
-- DW_FORM_exprloc: ULEB byte count + that many bytes of DW_OP_*.
|
||||
local len, ne = read_uleb128_at(buf, pos)
|
||||
if not len then return nil, pos end
|
||||
return nil, ne + len
|
||||
elseif form == DW_FORM_implicit_const then
|
||||
-- The constant is declared in the abbrev; no value bytes in the DIE.
|
||||
return nil, pos
|
||||
elseif form == M.DW_FORM.ref_sig8 then
|
||||
return M.read_u32_le(buf, pos + 1), pos + 8
|
||||
else
|
||||
return nil, pos
|
||||
end
|
||||
end
|
||||
|
||||
-- Index the .debug_info + .debug_abbrev sections of an existing ELF and
|
||||
-- collect one entry per "interesting" type DIE in the FIRST compilation
|
||||
-- unit. The index supports typed-view resolution for Phase 5:
|
||||
--
|
||||
-- index = {
|
||||
-- by_name = { ["V4_S2"] = {kind="structure_type", die_offset, byte_size, fields={...}},
|
||||
-- ["U4"] = {kind="base_type", die_offset, byte_size, encoding="unsigned"},
|
||||
-- ["MipsCode"]= {kind="typedef", die_offset, target_kind=..., target_die_offset=...} },
|
||||
-- by_offset = { [die_offset] = {kind, name, ...} }, -- reverse lookup
|
||||
-- }
|
||||
--
|
||||
-- Only the main CU is indexed. We do not walk nested CUs (this matches the
|
||||
-- Phase 2-4 scope: the main CU is the only one in this build).
|
||||
-- @param info string -- .debug_info section bytes
|
||||
-- @param abbrev string -- .debug_abbrev section bytes
|
||||
-- @param str_buf string -- .debug_str section bytes (for DW_FORM_strp name resolution)
|
||||
-- @param abbrev_offset integer -- 0-based offset of the main CU's abbrev table
|
||||
-- @param cu_start integer|nil -- 0-based offset of the main CU (caller-known)
|
||||
-- @return table|nil, string|nil -- (index, error)
|
||||
function M.index_main_cu_types(info, abbrev, str_buf, abbrev_offset, cu_start)
|
||||
if not info or #info < 12 or not abbrev or not abbrev_offset then return nil, "missing input" end
|
||||
str_buf = str_buf or ""
|
||||
-- Index the main table at abbrev_offset. The F' pass writes a trailing
|
||||
-- 0 byte after the main table; the G' pass may append additional codes
|
||||
-- (100-108) + a 0 terminator. The walker may encounter a code that
|
||||
-- wasn't in the main table but exists later in the same .debug_abbrev;
|
||||
-- on the first miss, walk the rest of the section to add any new
|
||||
-- abbrevs we encounter.
|
||||
local abbrev_decls, err = parse_abbrev_table(abbrev, abbrev_offset)
|
||||
if not abbrev_decls then return nil, err end
|
||||
local abbrev_by_code = {}
|
||||
for _, d in ipairs(abbrev_decls) do abbrev_by_code[d.code] = d end
|
||||
|
||||
-- Resolve cu_start + cu_end_excl.
|
||||
local cu_end_excl
|
||||
if cu_start then
|
||||
local ul = M.read_u32_le(info, cu_start + 1)
|
||||
if ul == 0xFFFFFFFF then return nil, "DWARF64 not supported" end
|
||||
cu_end_excl = cu_start + 4 + ul
|
||||
else
|
||||
local pos = 0
|
||||
cu_start = nil
|
||||
while pos < #info do
|
||||
local ul = M.read_u32_le(info, pos + 1)
|
||||
if ul == 0xFFFFFFFF then break end
|
||||
local unit_end = pos + 4 + ul
|
||||
if unit_end > #info then break end
|
||||
local unit_abbrev = M.read_u32_le(info, pos + 9)
|
||||
if unit_abbrev == abbrev_offset then
|
||||
cu_start = pos
|
||||
cu_end_excl = unit_end
|
||||
break
|
||||
end
|
||||
pos = unit_end
|
||||
end
|
||||
if not cu_start then return nil, "no CU matches abbrev_offset" end
|
||||
end
|
||||
|
||||
-- Walk the CU's DIE tree at 0-based offset cu_start + 12.
|
||||
-- Emit a flat list of type-bearing DEIs; recursion handles nested children
|
||||
-- (members inside structure_type, types inside subprograms, etc.).
|
||||
-- A non-type DIE's subtree is SKIPPED by walking until the matching null.
|
||||
local by_name = {}
|
||||
local by_offset = {}
|
||||
local pos_cursor = cu_start + 12
|
||||
-- Root DIE is the compile_unit; skip past it.
|
||||
if pos_cursor >= cu_end_excl then return nil, "no DIE bytes" end
|
||||
local root_decl_code
|
||||
root_decl_code, pos_cursor = read_uleb128_at(info, pos_cursor)
|
||||
if not root_decl_code then return nil, "truncated root DIE code" end
|
||||
local root_decl = abbrev_by_code[root_decl_code]
|
||||
if not root_decl then return nil, "unknown root DIE abbrev" end
|
||||
-- Skip root DIE attributes.
|
||||
for _, attr in ipairs(root_decl.attrs) do
|
||||
local _, ne = read_form_value(info, str_buf, pos_cursor, attr.form)
|
||||
if not ne then return nil, "truncated root attr" end
|
||||
pos_cursor = ne
|
||||
end
|
||||
-- If root has no children, return an empty index.
|
||||
if not root_decl.has_children or root_decl.has_children == 0 then
|
||||
return { by_name = by_name, by_offset = by_offset }
|
||||
end
|
||||
|
||||
-- Helper: skip a subtree rooted at the current DIE (pos_cursor is
|
||||
-- positioned at the first child). Walks down and right until the
|
||||
-- matching null terminator is consumed. Returns the new pos_cursor.
|
||||
-- For DIE trees that contain only the closed type + member shapes,
|
||||
-- the depth never exceeds 2 (type DIE -> member DIE -> null).
|
||||
local function skip_subtree(pos)
|
||||
local depth = 1
|
||||
while pos < cu_end_excl and depth > 0 do
|
||||
local code = info:byte(pos + 1)
|
||||
pos = pos + 1
|
||||
if code == 0 then
|
||||
depth = depth - 1
|
||||
else
|
||||
local d = abbrev_by_code[code]
|
||||
if d and d.has_children ~= 0 then
|
||||
depth = depth + 1
|
||||
end
|
||||
end
|
||||
end
|
||||
return pos
|
||||
end
|
||||
|
||||
-- Pre-declare n_visited so the closure helpers can read it.
|
||||
local n_visited = 0
|
||||
|
||||
-- Helper: read one DIE's attributes. Returns (name, byte_size, encoding,
|
||||
-- type_ref, new_pos_cursor) on success; (nil, error_string) on truncation.
|
||||
local function read_die_attributes(decl, pos)
|
||||
local die_name = nil
|
||||
local die_byte_size = nil
|
||||
local die_encoding = nil
|
||||
local die_type_ref = nil
|
||||
for ai, attr in ipairs(decl.attrs) do
|
||||
local before = pos
|
||||
local val, ne = read_form_value(info, str_buf, pos, attr.form)
|
||||
if not ne then
|
||||
return nil, "truncated DIE attr"
|
||||
end
|
||||
pos = ne
|
||||
if attr.name == M.DW_AT.name then
|
||||
die_name = val
|
||||
elseif attr.name == M.DW_AT.byte_size and
|
||||
(decl.tag == M.DW_TAG.base_type or decl.tag == M.DW_TAG.structure_type) then
|
||||
die_byte_size = val
|
||||
elseif attr.name == M.DW_AT.encoding and decl.tag == M.DW_TAG.base_type then
|
||||
die_encoding = val
|
||||
elseif attr.name == M.DW_AT.type then
|
||||
die_type_ref = val
|
||||
end
|
||||
end
|
||||
return die_name, die_byte_size, die_encoding, die_type_ref, pos
|
||||
end
|
||||
|
||||
-- Helper: read structure_type members. Returns (member_fields, new_pos).
|
||||
local function read_member_fields(decl, pos)
|
||||
local fields = {}
|
||||
while pos < cu_end_excl do
|
||||
local mcode = info:byte(pos + 1)
|
||||
if mcode == 0 then
|
||||
pos = pos + 1
|
||||
break
|
||||
end
|
||||
local mdecl = abbrev_by_code[mcode]
|
||||
if not mdecl then return nil, "unknown member abbrev" end
|
||||
pos = pos + 1
|
||||
local mname, mtype_ref, moffset
|
||||
for _, a in ipairs(mdecl.attrs) do
|
||||
local v, ne = read_form_value(info, str_buf, pos, a.form)
|
||||
if not ne then return nil, "truncated member attr" end
|
||||
pos = ne
|
||||
if a.name == M.DW_AT.name then mname = v
|
||||
elseif a.name == M.DW_AT.type then mtype_ref = v
|
||||
elseif a.name == M.DW_AT.data_member_location then moffset = v end
|
||||
end
|
||||
fields[#fields + 1] = { name = mname, type_offset = mtype_ref, offset = moffset }
|
||||
end
|
||||
return fields, pos
|
||||
end
|
||||
|
||||
-- Top-level walker: iterate siblings. For each DIE, decide whether to
|
||||
-- record (if type), descend (if structure_type with members), or skip
|
||||
-- its subtree.
|
||||
while pos_cursor < cu_end_excl do
|
||||
local code = info:byte(pos_cursor + 1)
|
||||
if code == 0 then pos_cursor = pos_cursor + 1; break end
|
||||
local decl = abbrev_by_code[code]
|
||||
if not decl then
|
||||
-- Lazily scan the rest of the section for the missing code.
|
||||
-- The G' pass appends new abbrevs (100-108) after the main table's
|
||||
-- terminator. On the first miss, walk the rest of the section.
|
||||
local scan_pos = 0
|
||||
while scan_pos < #abbrev do
|
||||
if abbrev:byte(scan_pos + 1) == 0 then
|
||||
scan_pos = scan_pos + 1
|
||||
goto continue
|
||||
end
|
||||
local new_table, e3 = parse_abbrev_table(abbrev, scan_pos)
|
||||
if not new_table then
|
||||
scan_pos = scan_pos + 1
|
||||
goto continue
|
||||
end
|
||||
for _, d in ipairs(new_table) do
|
||||
if not abbrev_by_code[d.code] then
|
||||
abbrev_by_code[d.code] = d
|
||||
end
|
||||
end
|
||||
-- Find the terminator (0 byte) of this table.
|
||||
local term = find_abbrev_table_end(abbrev, scan_pos)
|
||||
if not term then break end
|
||||
scan_pos = term + 1
|
||||
::continue::
|
||||
end
|
||||
decl = abbrev_by_code[code]
|
||||
if not decl then
|
||||
return nil, string.format("unknown abbrev %d at offset 0x%x", code, pos_cursor)
|
||||
end
|
||||
end
|
||||
local die_offset = pos_cursor
|
||||
pos_cursor = pos_cursor + 1
|
||||
local read_result = { read_die_attributes(decl, pos_cursor) }
|
||||
if #read_result == 2 then
|
||||
return nil, read_result[2]
|
||||
end
|
||||
local die_name, die_byte_size, die_encoding, die_type_ref, pos_after_attrs
|
||||
= read_result[1], read_result[2], read_result[3], read_result[4], read_result[5]
|
||||
n_visited = n_visited + 1
|
||||
local is_type = (decl.tag == M.DW_TAG.base_type
|
||||
or decl.tag == M.DW_TAG.structure_type
|
||||
or decl.tag == M.DW_TAG.typedef
|
||||
or decl.tag == M.DW_TAG.pointer_type
|
||||
or decl.tag == M.DW_TAG.const_type)
|
||||
|
||||
local member_fields = nil
|
||||
pos_cursor = pos_after_attrs
|
||||
if decl.tag == M.DW_TAG.structure_type and decl.has_children ~= 0 then
|
||||
local f, np = read_member_fields(decl, pos_cursor)
|
||||
if not f then return nil, np end
|
||||
member_fields = f
|
||||
pos_cursor = np
|
||||
elseif decl.has_children ~= 0 then
|
||||
-- Skip the subtree (e.g., DW_TAG_subprogram, DW_TAG_variable, etc.).
|
||||
pos_cursor = skip_subtree(pos_cursor)
|
||||
end
|
||||
|
||||
if is_type and die_name then
|
||||
local kind
|
||||
if decl.tag == M.DW_TAG.base_type then kind = "base_type"
|
||||
elseif decl.tag == M.DW_TAG.structure_type then kind = "structure_type"
|
||||
elseif decl.tag == M.DW_TAG.typedef then kind = "typedef"
|
||||
elseif decl.tag == M.DW_TAG.pointer_type then kind = "pointer_type"
|
||||
elseif decl.tag == M.DW_TAG.const_type then kind = "const_type" end
|
||||
local entry = {
|
||||
kind = kind,
|
||||
name = die_name,
|
||||
die_offset = die_offset,
|
||||
byte_size = die_byte_size,
|
||||
encoding = die_encoding,
|
||||
type_ref = die_type_ref,
|
||||
fields = member_fields,
|
||||
}
|
||||
by_name[die_name] = entry
|
||||
by_offset[die_offset] = entry
|
||||
end
|
||||
end
|
||||
return { by_name = by_name, by_offset = by_offset }
|
||||
end
|
||||
|
||||
-- Resolve a chain of pointer + const + typedef + structure_type down to a
|
||||
-- canonical struct or base type. The returned entry has kind, name, byte_size,
|
||||
-- and (for structure_type) fields with {name, offset, type_name, pointer_depth}.
|
||||
-- Returns nil if the chain cannot be resolved (e.g., missing DIE).
|
||||
-- @param index table -- M.index_main_cu_types result
|
||||
-- @param start_offset integer -- CU-relative DW_FORM_ref4 offset of the start
|
||||
-- @param max_depth integer -- cycle protection
|
||||
-- @return table|nil -- {kind, name, byte_size, fields?, pointer_depth}
|
||||
function M.resolve_type_chain(index, start_offset, max_depth)
|
||||
max_depth = max_depth or 16
|
||||
if not index or not start_offset then return nil end
|
||||
local chain = {}
|
||||
local cur_offset = start_offset
|
||||
local depth = 0
|
||||
while cur_offset and depth < max_depth do
|
||||
local entry = index.by_offset[cur_offset]
|
||||
if not entry then return nil end
|
||||
chain[#chain + 1] = entry
|
||||
if entry.kind == "pointer_type" then
|
||||
cur_offset = entry.type_ref
|
||||
elseif entry.kind == "const_type" then
|
||||
cur_offset = entry.type_ref
|
||||
elseif entry.kind == "typedef" then
|
||||
cur_offset = entry.type_ref
|
||||
else
|
||||
break
|
||||
end
|
||||
depth = depth + 1
|
||||
end
|
||||
-- Compute pointer_depth = number of pointer/const wrappers.
|
||||
local pointer_depth = 0
|
||||
for _, e in ipairs(chain) do
|
||||
if e.kind == "pointer_type" then pointer_depth = pointer_depth + 1 end
|
||||
end
|
||||
-- The last entry is the "naked" type.
|
||||
local naked = chain[#chain]
|
||||
if not naked then return nil end
|
||||
-- If the last entry is a structure_type, resolve each field's type name
|
||||
-- + pointer depth too (for `bind_args` shape expansion).
|
||||
if naked.kind == "structure_type" and naked.fields then
|
||||
local fields = {}
|
||||
for _, f in ipairs(naked.fields) do
|
||||
local field_entry = f.type_offset and index.by_offset[f.type_offset] or nil
|
||||
local field_chain = {}
|
||||
local d = 0
|
||||
local co = f.type_offset
|
||||
while co and d < 16 do
|
||||
local e2 = index.by_offset[co]
|
||||
if not e2 then break end
|
||||
field_chain[#field_chain + 1] = e2
|
||||
if e2.kind == "pointer_type" or e2.kind == "const_type" or e2.kind == "typedef" then
|
||||
co = e2.type_ref
|
||||
else
|
||||
break
|
||||
end
|
||||
d = d + 1
|
||||
end
|
||||
local fpd = 0
|
||||
for _, x in ipairs(field_chain) do if x.kind == "pointer_type" then fpd = fpd + 1 end end
|
||||
fields[#fields + 1] = {
|
||||
name = f.name,
|
||||
offset = f.offset,
|
||||
type_name = (field_chain[#field_chain] and field_chain[#field_chain].name) or "?",
|
||||
pointer_depth = fpd,
|
||||
}
|
||||
end
|
||||
return {
|
||||
kind = "structure_type",
|
||||
name = naked.name,
|
||||
byte_size = naked.byte_size,
|
||||
fields = fields,
|
||||
pointer_depth = pointer_depth,
|
||||
}
|
||||
end
|
||||
return {
|
||||
kind = naked.kind,
|
||||
name = naked.name,
|
||||
byte_size = naked.byte_size,
|
||||
encoding = naked.encoding,
|
||||
pointer_depth = pointer_depth,
|
||||
}
|
||||
end
|
||||
|
||||
--- Return a 4-byte little-endian byte string for `value`.
|
||||
--- Caller concatenates with `..` if composing multi-word blobs.
|
||||
---
|
||||
@@ -409,6 +976,11 @@ local SLEB_SIGN_BIT = 0x40
|
||||
--- @param n integer -- non-negative
|
||||
--- @return string
|
||||
function M.uleb128(n)
|
||||
if n == nil or type(n) ~= "number" then
|
||||
io.stderr:write("[elf_dwarf.uleb128] got " .. type(n) .. ": " .. tostring(n) .. "\n")
|
||||
io.stderr:write(debug.traceback() .. "\n")
|
||||
error("uleb128 requires non-negative number")
|
||||
end
|
||||
assert(n >= 0, "uleb128 requires non-negative input")
|
||||
local bytes = {}
|
||||
repeat
|
||||
@@ -511,4 +1083,87 @@ function M.parse_source_map_file(sm_path, expected_version)
|
||||
return out
|
||||
end
|
||||
|
||||
--- Parse a FORMAT_VERSION <expected_version> `*.atoms.provenance.txt` file.
|
||||
--- Returns `{name -> {total = N, words = {{pos, call_file, call_line, comp_name, comp_file, comp_line}, ...}}}`.
|
||||
--- Returns `{}` on format-version mismatch (and logs to stderr).
|
||||
---
|
||||
--- **Wire format** (emitted by `passes/atoms_source_map.lua` since the Phase 3 debug_ux work):
|
||||
--- ```
|
||||
--- # FORMAT_VERSION <n>
|
||||
--- ATOM <name> "<abs-source-path>" <total>
|
||||
--- WORD <n> CALL <src-file>:<src-line> RAW
|
||||
--- WORD <n> CALL <src-file>:<src-line> MACRO <comp_name> "<comp-file>:<comp-line>"
|
||||
--- ...
|
||||
--- ENDATOM
|
||||
--- ```
|
||||
---
|
||||
--- **Used by** `passes/dwarf_injection.lua` (Phase 3 — debug_ux) to:
|
||||
--- - group consecutive MACRO rows into component invocations (one `DW_TAG_inlined_subroutine` each)
|
||||
--- - emit abstract `DW_TAG_subprogram` per unique component name
|
||||
--- - extend `.debug_line` so stepping into a `mac_X(...)` lands on the component's source line.
|
||||
---
|
||||
--- @param prov_path string -- path to *.atoms.provenance.txt
|
||||
--- @param expected_version integer -- expected FORMAT_VERSION line
|
||||
--- @return table<string, table>
|
||||
function M.parse_provenance_file(prov_path, expected_version)
|
||||
local out = {}
|
||||
local cur_name, cur_words = nil, {}
|
||||
for raw in io.lines(prov_path) do
|
||||
local line = raw
|
||||
if line:match("^#") then
|
||||
local ver = line:match("^# FORMAT_VERSION%s+(%d+)")
|
||||
if ver and tonumber(ver) ~= expected_version then
|
||||
io.stderr:write(string.format(
|
||||
"[elf_dwarf.parse_provenance_file] provenance version mismatch (got %s, expected %d) in %s\n",
|
||||
ver, expected_version, prov_path))
|
||||
return {}
|
||||
end
|
||||
-- skip other comments
|
||||
elseif line:sub(1, 4) == "ATOM" then
|
||||
-- ATOM <name> "<abs-source-path>" <total>
|
||||
local _, _, name = line:find("ATOM%s+(%S+)%s+\"[^\"]*\"%s+(%d+)")
|
||||
if name then
|
||||
cur_name = name
|
||||
cur_words = {}
|
||||
out[name] = { total = 0, words = cur_words }
|
||||
end
|
||||
elseif line == "ENDATOM" then
|
||||
if cur_name and out[cur_name] then
|
||||
out[cur_name].total = #cur_words
|
||||
end
|
||||
cur_name, cur_words = nil, {}
|
||||
elseif line:sub(1, 4) == "WORD" and cur_name then
|
||||
-- Two accepted shapes:
|
||||
-- WORD <n> CALL <call-file>:<call-line> RAW
|
||||
-- WORD <n> CALL <call-file>:<call-line> MACRO <comp_name> "<comp-file>:<comp-line>"
|
||||
local pos, call_file, call_line, comp_name, comp_file, comp_line =
|
||||
line:match('WORD%s+(%d+)%s+CALL%s+(.-):(%d+)%s+MACRO%s+(%S+)%s+"([^"]*):(%d+)"')
|
||||
if pos then
|
||||
cur_words[#cur_words + 1] = {
|
||||
pos = tonumber(pos),
|
||||
call_file = call_file,
|
||||
call_line = tonumber(call_line),
|
||||
comp_name = comp_name,
|
||||
comp_file = comp_file,
|
||||
comp_line = tonumber(comp_line),
|
||||
}
|
||||
else
|
||||
-- RAW row.
|
||||
local raw_pos, raw_file, raw_line = line:match('WORD%s+(%d+)%s+CALL%s+(.-):(%d+)%s+RAW')
|
||||
if raw_pos then
|
||||
cur_words[#cur_words + 1] = {
|
||||
pos = tonumber(raw_pos),
|
||||
call_file = raw_file,
|
||||
call_line = tonumber(raw_line),
|
||||
comp_name = nil,
|
||||
comp_file = nil,
|
||||
comp_line = nil,
|
||||
}
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
return out
|
||||
end
|
||||
|
||||
return M
|
||||
|
||||
@@ -28,6 +28,11 @@ param(
|
||||
|
||||
$ErrorActionPreference = 'Stop'
|
||||
|
||||
$gdbInitPath = [System.IO.Path]::GetFullPath((Join-Path $PSScriptRoot '..\build\gen\hello_gte.gdbinit'))
|
||||
if (-not (Test-Path -LiteralPath $gdbInitPath -PathType Leaf)) {
|
||||
Write-Warning "Generated GDB skip sidecar missing (non-fatal): $gdbInitPath. Run the GTE build to regenerate it; debugger launch will continue without generated skip-over commands."
|
||||
}
|
||||
|
||||
# ── Pre-checks ──
|
||||
foreach ($p in @($PcsxPath, $ExePath, $HelperZip)) {
|
||||
if (-not (Test-Path $p)) {
|
||||
|
||||
+307
-16
@@ -27,6 +27,27 @@ local WAVE_CONTEXT_REGS = duffle.WAVE_CONTEXT_REGS
|
||||
|
||||
local function is_wave_context_reg(n) return WAVE_CONTEXT_REGS[n] ~= nil end
|
||||
|
||||
-- Phase 5 closed sets. atom_dbg_reg_default and atom_reg_types MUST target
|
||||
-- one of these registers; atom_view MUST reference a real Binds_* struct.
|
||||
local SEMANTIC_DEFAULT_REGS = {
|
||||
["R_TapePtr"] = true, ["R_AtomJmp"] = true,
|
||||
["R_PrimCursor"] = true, ["R_FaceCursor"] = true,
|
||||
["R_VertBase"] = true, ["R_OtBase"] = true,
|
||||
}
|
||||
|
||||
local COMPUTE_REG_PREFIX = "R_"
|
||||
local COMPUTE_REG_RE = -- permits R_T0..R_T3 + caller-trash (not wave-context)
|
||||
"^R_T[0-3]$"
|
||||
|
||||
-- Compute-register types are restricted to byte-width primitives (no struct
|
||||
-- identity in this phase). Pointer depth is also bounded — typed pointer
|
||||
-- chains live in the Binds_* struct, not in a per-atom override.
|
||||
local ALLOWED_COMPUTE_TYPES = {
|
||||
["U4"] = true, ["S4"] = true,
|
||||
["U2"] = true, ["S2"] = true,
|
||||
["U1"] = true, ["S1"] = true,
|
||||
}
|
||||
|
||||
-- ════════════════════════════════════════════════════════════════════════════
|
||||
-- Type declarations
|
||||
-- ════════════════════════════════════════════════════════════════════════════
|
||||
@@ -66,6 +87,16 @@ local function is_wave_context_reg(n) return WAVE_CONTEXT_REGS[n] ~= nil end
|
||||
--- @field writes string[] -- R_* names (write targets)
|
||||
--- @field errors string[]|nil -- parse-time errors from scan_source (atom_info body malformed)
|
||||
|
||||
--- @class SkipOverMarker -- sub-shape of scan_source.lua's @class SkipOverMarker
|
||||
--- @field marker_kind string -- exact marker ident (always "atom_dbg_skip_over")
|
||||
--- @field marker_line integer
|
||||
--- @field args string|nil -- trimmed text inside the parens (nil when has_parens is false)
|
||||
--- @field has_parens boolean
|
||||
--- @field pending boolean -- true while awaiting the following declaration
|
||||
--- @field superseded_by_marker_line integer|nil -- set on a marker that was bumped out of the pending slot
|
||||
--- @field target_kind string|nil -- "atom" | "comp_bare" | "comp_proc" | "unrelated" once observed
|
||||
--- @field declaration_line integer|nil
|
||||
|
||||
--- @class Finding
|
||||
--- @field line integer -- source line (or 0 for pass-level)
|
||||
--- @field msg string -- finding message
|
||||
@@ -76,9 +107,14 @@ local function is_wave_context_reg(n) return WAVE_CONTEXT_REGS[n] ~= nil end
|
||||
--- @field info Finding[]
|
||||
|
||||
--- @class PipeCtx
|
||||
--- @field atom_index table<string, AtomAnnotation> -- name -> AtomAnnotation (only kind=="atom")
|
||||
--- @field binds_index table<string, BindsStruct> -- name -> BindsStruct
|
||||
--- @field annot_counts table<string, integer> -- name -> annotation count (for unique_annotation check)
|
||||
--- @field atom_index table<string, AtomAnnotation> -- name -> AtomAnnotation (only kind=="atom")
|
||||
--- @field binds_index table<string, BindsStruct> -- name -> BindsStruct
|
||||
--- @field annot_counts table<string, integer> -- name -> annotation count (for unique_annotation check)
|
||||
--- @field types table<string, RegTypeDefault> -- from scan_source
|
||||
--- @field atom_views table<string, AtomViewEntry> -- from scan_source
|
||||
--- @field seen_defaults table<string, integer> -- duplicate atom_dbg_reg_default detection
|
||||
--- @field seen_field table<string, integer> -- Binds_* -> count of fields (set/checked by check_binds_no_duplicate_fields)
|
||||
--- @field _scan SourceScan -- full scan payload (typed-view sub-calls live here)
|
||||
|
||||
--- @class AnnotatedResult
|
||||
--- @field atoms AtomEntry[]
|
||||
@@ -217,24 +253,244 @@ local function check_macro_word_drift(m, wc, findings)
|
||||
}
|
||||
end
|
||||
|
||||
---- Check: atom_dbg_reg_default(R_X, <type>) must target a wave-context register
|
||||
--- and the type name must be a recognized C type (we currently allow the
|
||||
--- types actually used in the codebase; pointer depth must be 0 or 1).
|
||||
--- Also detects duplicate defaults for the same register.
|
||||
--- @param _src SourceFile -- unused (kept for the per_source shape)
|
||||
--- @param pipe_ctx PipeCtx
|
||||
--- @param findings Findings
|
||||
-- Known type names accepted in atom_dbg_reg_default and the Binds_*-via-atom_view
|
||||
-- resolution path. The Phase 5 list mirrors the canonical duffle math.h
|
||||
-- typedefs plus the byte-width primitives used as compute-register overrides.
|
||||
local KNOWN_REG_DEFAULT_TYPES = {
|
||||
["U4"] = true, ["S4"] = true,
|
||||
["U2"] = true, ["S2"] = true,
|
||||
["U1"] = true, ["S1"] = true,
|
||||
["MipsCode"] = true, ["MipsAtom"] = true,
|
||||
["V2_S2"] = true, ["V2_S4"] = true,
|
||||
["V3_S2"] = true, ["V3_S4"] = true,
|
||||
["V4_S2"] = true, ["V4_S4"] = true,
|
||||
["M3_S2"] = true,
|
||||
["void"] = true,
|
||||
}
|
||||
|
||||
local function check_semantic_reg_defaults(_src, pipe_ctx, findings)
|
||||
-- Detect duplicate defaults using the ordered occurrence list (the
|
||||
-- out.types hash only retains the last declaration).
|
||||
local seen_first_line = {}
|
||||
for _, occ in ipairs(pipe_ctx.type_occurrences or {}) do
|
||||
if seen_first_line[occ.reg] == nil then
|
||||
seen_first_line[occ.reg] = occ.source_line
|
||||
else
|
||||
findings.errors[#findings.errors + 1] = {
|
||||
line = occ.source_line,
|
||||
msg = string.format(
|
||||
"duplicate atom_dbg_reg_default for %q at line %d (first declared at line %d); one default per register",
|
||||
occ.reg, occ.source_line, seen_first_line[occ.reg]),
|
||||
}
|
||||
end
|
||||
end
|
||||
for reg, def in pairs(pipe_ctx.types or {}) do
|
||||
if not SEMANTIC_DEFAULT_REGS[reg] then
|
||||
findings.errors[#findings.errors + 1] = {
|
||||
line = def.source_line,
|
||||
msg = string.format(
|
||||
"atom_dbg_reg_default at line %d references unknown register %q; expected one of R_TapePtr, R_AtomJmp, R_PrimCursor, R_FaceCursor, R_VertBase, R_OtBase",
|
||||
def.source_line, reg),
|
||||
}
|
||||
end
|
||||
if def.pointer_depth == nil or def.pointer_depth < 0 or def.pointer_depth > 1 then
|
||||
findings.errors[#findings.errors + 1] = {
|
||||
line = def.source_line,
|
||||
msg = string.format(
|
||||
"atom_dbg_reg_default at line %d for %q has unsupported pointer depth %d (expected 0 or 1)",
|
||||
def.source_line, reg, def.pointer_depth or -1),
|
||||
}
|
||||
end
|
||||
if not def.type_name or not KNOWN_REG_DEFAULT_TYPES[def.type_name] then
|
||||
findings.errors[#findings.errors + 1] = {
|
||||
line = def.source_line,
|
||||
msg = string.format(
|
||||
"atom_dbg_reg_default at line %d for %q uses unknown type %q (allowed: byte-width primitives, V*_S*, M3_S2, MipsCode, void)",
|
||||
def.source_line, reg, tostring(def.type_name)),
|
||||
}
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
--- Check: atom_reg_types(R_X, <type>) entries must point to a compute register
|
||||
--- (R_T0..R_T3) with a recognized fundamental type.
|
||||
--- @param _src SourceFile
|
||||
--- @param pipe_ctx PipeCtx
|
||||
--- @param findings Findings
|
||||
local function check_atom_reg_types(_src, pipe_ctx, findings)
|
||||
for _, ai in ipairs(pipe_ctx.atom_infos_list or {}) do
|
||||
if ai.reg_type_overrides then
|
||||
for reg, ov in pairs(ai.reg_type_overrides) do
|
||||
if not reg:match(COMPUTE_REG_RE) then
|
||||
findings.errors[#findings.errors + 1] = {
|
||||
line = ai.info_line,
|
||||
msg = string.format(
|
||||
"atom '%s' has atom_reg_types for %q; compute-register types are restricted to R_T0..R_T3",
|
||||
ai.atom_name, reg),
|
||||
}
|
||||
end
|
||||
if not ov.type_name or not ALLOWED_COMPUTE_TYPES[ov.type_name] then
|
||||
findings.errors[#findings.errors + 1] = {
|
||||
line = ai.info_line,
|
||||
msg = string.format(
|
||||
"atom '%s' atom_reg_types for %q uses unknown compute type %q (allowed: U1/U2/U4/S1/S2/S4)",
|
||||
ai.atom_name, reg, tostring(ov.type_name)),
|
||||
}
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
--- Check: atom_view(Binds_X) entries must reference a real Binds_* struct
|
||||
--- and that struct must declare at least one field.
|
||||
--- @param _src SourceFile
|
||||
--- @param pipe_ctx PipeCtx
|
||||
--- @param findings Findings
|
||||
local function check_atom_view_layout(_src, pipe_ctx, findings)
|
||||
for atom_name, view in pairs(pipe_ctx.atom_views or {}) do
|
||||
if not view.binds_name then
|
||||
-- The atom had atom_reg_types but no atom_view; no layout check needed.
|
||||
else
|
||||
local bs = pipe_ctx.binds_index[view.binds_name]
|
||||
if not bs then
|
||||
findings.errors[#findings.errors + 1] = {
|
||||
line = view.info_line,
|
||||
msg = string.format(
|
||||
"atom '%s' has atom_view(%s) but no Struct_(%s) { ... } declaration was found",
|
||||
atom_name, view.binds_name, view.binds_name),
|
||||
}
|
||||
elseif not bs.fields or #bs.fields == 0 then
|
||||
findings.errors[#findings.errors + 1] = {
|
||||
line = bs.line,
|
||||
msg = string.format(
|
||||
"atom '%s' has atom_view(%s) but that struct declares zero typed fields",
|
||||
atom_name, view.binds_name),
|
||||
}
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
--- Check: Binds_* structs may not have duplicate field names (they would
|
||||
--- defeat the typed-field name lookup that atom_view exposes in gdb).
|
||||
--- @param _src SourceFile
|
||||
--- @param pipe_ctx PipeCtx
|
||||
--- @param findings Findings
|
||||
local function check_binds_no_duplicate_fields(_src, pipe_ctx, findings)
|
||||
for _, bs in ipairs(pipe_ctx.binds_list or {}) do
|
||||
local seen = {}
|
||||
for _, f in ipairs(bs.fields or {}) do
|
||||
seen[f.name] = (seen[f.name] or 0) + 1
|
||||
end
|
||||
for name, count in pairs(seen) do
|
||||
if count > 1 then
|
||||
findings.errors[#findings.errors + 1] = {
|
||||
line = bs.line,
|
||||
msg = string.format(
|
||||
"%s has duplicate field name %q (count %d); the typed-view contract requires unique field names",
|
||||
bs.name, name, count),
|
||||
}
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
-- Check: skip-over markers must satisfy shape + placement constraints.
|
||||
--- Walks the priority list once; at most one error is appended per marker so that
|
||||
--- a single source-level defect does not cascade into multiple findings.
|
||||
--- Priority order (first defect wins):
|
||||
--- 1. has_parens == false -> requires parentheses: marker()
|
||||
--- 2. args ~= "" -> takes no arguments
|
||||
--- 3. superseded_by_marker_line -> duplicate marker (cite superseding line)
|
||||
--- 4. pending + no target_kind -> dangling (no following declaration)
|
||||
--- 5. unsupported target_kind -> marker precedes an unrelated declaration
|
||||
--- Valid markers before whole-atom / bare-component / proc-component declarations
|
||||
--- emit no error and remain in src.scan.skip_over.atoms / .components for Task 15.
|
||||
--- @param marker SkipOverMarker
|
||||
--- @param _pipe_ctx PipeCtx -- unused today; kept for plex-shape consistency with per_annot
|
||||
--- @param findings Findings
|
||||
local function check_skip_marker(marker, _pipe_ctx, findings)
|
||||
local kind = marker.marker_kind
|
||||
local line = marker.marker_line
|
||||
|
||||
if not marker.has_parens then
|
||||
findings.errors[#findings.errors + 1] = {
|
||||
line = line,
|
||||
msg = string.format("%s marker at line %d requires parentheses: marker()", kind, line),
|
||||
}
|
||||
return
|
||||
end
|
||||
|
||||
if marker.args ~= nil and marker.args ~= "" then
|
||||
findings.errors[#findings.errors + 1] = {
|
||||
line = line,
|
||||
msg = string.format("%s marker at line %d takes no arguments; found %q", kind, line, marker.args),
|
||||
}
|
||||
return
|
||||
end
|
||||
|
||||
if marker.superseded_by_marker_line then
|
||||
findings.errors[#findings.errors + 1] = {
|
||||
line = line,
|
||||
msg = string.format("duplicate %s marker at line %d; superseded by another %s marker at line %d",
|
||||
kind, line, kind, marker.superseded_by_marker_line),
|
||||
}
|
||||
return
|
||||
end
|
||||
|
||||
if marker.pending and not marker.target_kind then
|
||||
findings.errors[#findings.errors + 1] = {
|
||||
line = line,
|
||||
msg = string.format("dangling %s marker at line %d: no following MipsAtom_/MipsAtomComp_/MipsAtomComp_Proc_ declaration",
|
||||
kind, line),
|
||||
}
|
||||
return
|
||||
end
|
||||
|
||||
if marker.target_kind
|
||||
and marker.target_kind ~= "atom"
|
||||
and marker.target_kind ~= "comp_bare"
|
||||
and marker.target_kind ~= "comp_proc" then
|
||||
findings.errors[#findings.errors + 1] = {
|
||||
line = line,
|
||||
msg = string.format("%s marker at line %d must precede MipsAtom_, MipsAtomComp_, or MipsAtomComp_Proc_; found an unrelated declaration",
|
||||
kind, line),
|
||||
}
|
||||
end
|
||||
end
|
||||
|
||||
-- ════════════════════════════════════════════════════════════════════════════
|
||||
-- CHECK_RULES — data-driven check dispatch (the plex pattern)
|
||||
-- ════════════════════════════════════════════════════════════════════════════
|
||||
--
|
||||
-- Each rule entry picks one of three "shapes" of dispatch:
|
||||
-- per_annot(annot, pipe_ctx, findings) — runs once per AtomAnnotation
|
||||
-- post(pipe_ctx, findings) — runs once after all per_annot calls complete (full-corpus aggregation)
|
||||
-- per_macro(macro, wc, findings) — runs once per TAPE_WORDS / _Pragma macro declaration
|
||||
-- Each rule entry picks one of four "shapes" of dispatch:
|
||||
-- per_annot(annot, pipe_ctx, findings) — runs once per AtomAnnotation
|
||||
-- post(pipe_ctx, findings) — runs once after all per_annot calls complete (full-corpus aggregation)
|
||||
-- per_macro(macro, wc, findings) — runs once per TAPE_WORDS / _Pragma macro declaration
|
||||
-- per_skip_marker(marker, pipe_ctx, findings) — runs once per src.scan.skip_over.markers entry (Task 14)
|
||||
--
|
||||
-- Adding a new check = 1 row here + 1 function above. The `validate()` dispatch loop never needs editing.
|
||||
|
||||
local CHECK_RULES = {
|
||||
{ name = "atom_decl_exists", per_annot = check_atom_decl_exists },
|
||||
{ name = "binds_struct_exists", per_annot = check_binds_struct_exists },
|
||||
{ name = "binds_field_wave_context", per_annot = check_binds_field_wave_context },
|
||||
{ name = "reads_wave_context", per_annot = check_reads_wave_context },
|
||||
{ name = "unique_annotation", post = check_unique_annotation },
|
||||
{ name = "macro_word_drift", per_macro = check_macro_word_drift },
|
||||
{ name = "atom_decl_exists", per_annot = check_atom_decl_exists },
|
||||
{ name = "binds_struct_exists", per_annot = check_binds_struct_exists },
|
||||
{ name = "binds_field_wave_context", per_annot = check_binds_field_wave_context },
|
||||
{ name = "reads_wave_context", per_annot = check_reads_wave_context },
|
||||
{ name = "unique_annotation", post = check_unique_annotation },
|
||||
{ name = "macro_word_drift", per_macro = check_macro_word_drift },
|
||||
{ name = "skip_marker_validation", per_skip_marker = check_skip_marker },
|
||||
{ name = "semantic_reg_defaults", per_source = check_semantic_reg_defaults },
|
||||
{ name = "atom_reg_types", per_source = check_atom_reg_types },
|
||||
{ name = "atom_view_layout", per_source = check_atom_view_layout },
|
||||
{ name = "binds_no_duplicate_fields", per_source = check_binds_no_duplicate_fields },
|
||||
}
|
||||
|
||||
-- ════════════════════════════════════════════════════════════════════════════
|
||||
@@ -276,10 +532,27 @@ local function validate(ctx, src)
|
||||
|
||||
-- Build pipe_ctx (Fleury: expose structure). Pre-compute everything the per-check functions need.
|
||||
-- Single source of truth for atom / binds / annotation-count lookups.
|
||||
-- Phase 5 typed views: pipe_ctx.types / pipe_ctx.atom_views / pipe_ctx.seen_defaults
|
||||
-- are projected from the scan payload so per_source check rules can iterate.
|
||||
local seen_defaults = {}
|
||||
for reg, _ in pairs(scan.types or {}) do
|
||||
seen_defaults[reg] = (seen_defaults[reg] or 0) + 1
|
||||
end
|
||||
local atom_infos_list = {}
|
||||
for _, ai in ipairs(scan.atom_infos or {}) do
|
||||
atom_infos_list[#atom_infos_list + 1] = ai
|
||||
end
|
||||
|
||||
local pipe_ctx = {
|
||||
atom_index = {},
|
||||
binds_index = {},
|
||||
annot_counts = {},
|
||||
atom_index = {},
|
||||
binds_index = {},
|
||||
annot_counts = {},
|
||||
types = scan.types or {},
|
||||
type_occurrences = scan.type_occurrences or {},
|
||||
atom_views = scan.atom_views or {},
|
||||
seen_defaults = seen_defaults,
|
||||
atom_infos_list = atom_infos_list,
|
||||
binds_list = scan.binds or {},
|
||||
}
|
||||
for _, a in ipairs(atoms) do pipe_ctx.atom_index [a.name] = a end
|
||||
for _, b in ipairs(scan.binds) do pipe_ctx.binds_index[b.name] = b end
|
||||
@@ -319,6 +592,17 @@ local function validate(ctx, src)
|
||||
if rule.post then rule.post(pipe_ctx, findings) end
|
||||
end
|
||||
|
||||
-- Per-skip-marker rules (Task 14). Each raw marker recorded by scan_source
|
||||
-- (in scan.skip_over.markers) is validated independently; the check emits at
|
||||
-- most one error per marker. Valid markers stay attached to scan.skip_over.atoms /
|
||||
-- .components for Task 15's dwarf_injection.lua consumer.
|
||||
local skip_markers = scan.skip_over and scan.skip_over.markers or {}
|
||||
for _, marker in ipairs(skip_markers) do
|
||||
for _, rule in ipairs(CHECK_RULES) do
|
||||
if rule.per_skip_marker then rule.per_skip_marker(marker, pipe_ctx, findings) end
|
||||
end
|
||||
end
|
||||
|
||||
-- Per-macro rules (TAPE_WORDS vs WORD_COUNT drift).
|
||||
local wc = ctx.shared.word_counts
|
||||
for _, m in ipairs(scan.macros) do
|
||||
@@ -327,6 +611,13 @@ local function validate(ctx, src)
|
||||
end
|
||||
end
|
||||
|
||||
-- Per-source rules (Phase 5 typed views: reg defaults, atom_view layout,
|
||||
-- compute-register type overrides, Binds_* field uniqueness). Each
|
||||
-- per_source rule sees the full scan payload via pipe_ctx.
|
||||
for _, rule in ipairs(CHECK_RULES) do
|
||||
if rule.per_source then rule.per_source(src, pipe_ctx, findings) end
|
||||
end
|
||||
|
||||
-- Information summary (always emitted).
|
||||
findings.info[#findings.info + 1] = {
|
||||
line = 0,
|
||||
|
||||
@@ -148,6 +148,153 @@ local function compute_word_entries(atom, src, wc)
|
||||
return entries, pos
|
||||
end
|
||||
|
||||
-- ════════════════════════════════════════════════════════════════════════════
|
||||
-- Provenance emission (Phase 3 — debug_ux)
|
||||
-- ════════════════════════════════════════════════════════════════════════════
|
||||
|
||||
-- Component-macro invocation prefix (mirrors components.lua's MAC_PREFIX).
|
||||
local MAC_PREFIX = "mac_"
|
||||
local MAC_PREFIX_LEN = 4
|
||||
|
||||
--- Strip the `mac_` prefix from a token's leading identifier. Returns nil
|
||||
--- if the identifier doesn't start with `mac_` (so non-component tokens
|
||||
--- like `load_half_u`, `nop2`, `gte_cmdw_*` fall through cleanly).
|
||||
--- @param tok string
|
||||
--- @return string|nil
|
||||
local function strip_mac_prefix_from_token(tok)
|
||||
local leading = duffle.read_ident(tok, 1)
|
||||
if not leading then return nil end
|
||||
if leading:sub(1, MAC_PREFIX_LEN) == MAC_PREFIX then
|
||||
return leading:sub(MAC_PREFIX_LEN + 1)
|
||||
end
|
||||
return nil
|
||||
end
|
||||
|
||||
--- Compute per-word provenance entries for an atom. Mirrors `compute_word_entries`
|
||||
--- but additionally classifies each emitted `.word` as either:
|
||||
--- - `RAW` — emitted by a direct instruction token (no component provenance)
|
||||
--- - `MACRO X` — emitted by a `mac_X(...)` component invocation, with the
|
||||
--- component's definition file:line resolved from `ctx.shared.components`.
|
||||
---
|
||||
--- Returns a list of `{pos, line, text, comp_name, comp_line, comp_path}` entries + the
|
||||
--- total word count. `comp_name` is nil for RAW rows.
|
||||
--- @param atom table -- one entry of scan.atoms / scan.raw_atoms
|
||||
--- @param src table -- SourceFile (has .scan with .line_of(), .path)
|
||||
--- @param wc table -- shared.word_counts
|
||||
--- @param comp table -- shared.components map: bare_name -> {name=, line=, path=, kind=}
|
||||
--- @return table[], integer
|
||||
local function compute_provenance_entries(atom, src, wc, comp)
|
||||
local entries = {}
|
||||
local pos = 0
|
||||
for _, t in ipairs(atom.body_tokens) do
|
||||
local tok = t.tok
|
||||
local rel = t.rel
|
||||
|
||||
local words
|
||||
if is_marker_token(tok) then
|
||||
words = count_marker_rest(tok, wc)
|
||||
else
|
||||
words = count_token_words(tok, wc)
|
||||
end
|
||||
|
||||
-- Resolve component provenance for this token (if any).
|
||||
local comp_name = nil
|
||||
local comp_line = nil
|
||||
local comp_path = nil
|
||||
local comp_kind = nil
|
||||
local bare = strip_mac_prefix_from_token(tok)
|
||||
if bare and comp and comp[bare] then
|
||||
comp_name = bare
|
||||
comp_line = comp[bare].line
|
||||
comp_path = comp[bare].path
|
||||
comp_kind = comp[bare].kind
|
||||
end
|
||||
|
||||
if words > 0 then
|
||||
local line = src.scan.line_of(atom.body_off + rel)
|
||||
local text = duffle.trim(tok):gsub("[\t\r\n]+", " ")
|
||||
for _ = 1, words do
|
||||
entries[#entries + 1] = {
|
||||
pos = pos,
|
||||
line = line,
|
||||
text = text,
|
||||
comp_name = comp_name,
|
||||
comp_line = comp_line,
|
||||
comp_path = comp_path,
|
||||
comp_kind = comp_kind,
|
||||
}
|
||||
pos = pos + 1
|
||||
end
|
||||
end
|
||||
end
|
||||
return entries, pos
|
||||
end
|
||||
|
||||
--- Render one atom's provenance stanza. Format:
|
||||
--- `WORD N CALL <src-path>:<src-line> MACRO <name> "<def-path>:<def-line>"` (for component words)
|
||||
--- `WORD N CALL <src-path>:<src-line> RAW` (for direct instructions)
|
||||
--- Returns (lines, total_words).
|
||||
--- @param src table
|
||||
--- @param atom table
|
||||
--- @param wc table
|
||||
--- @param comp table -- shared.components map
|
||||
--- @return string[], integer
|
||||
local function emit_provenance_stanza(src, atom, wc, comp)
|
||||
local lines = {}
|
||||
local rel_path = src.path:gsub("\\", "/")
|
||||
local entries, total = compute_provenance_entries(atom, src, wc, comp)
|
||||
|
||||
-- ATOM header line with placeholder total (patched after we know it).
|
||||
lines[#lines + 1] = string.format('ATOM %s "%s" 0', atom.raw_name or atom.name, rel_path)
|
||||
|
||||
for _, pe in ipairs(entries) do
|
||||
if pe.comp_name then
|
||||
lines[#lines + 1] = string.format(
|
||||
'WORD %d CALL %s:%d MACRO %s "%s:%d"',
|
||||
pe.pos, rel_path, pe.line, pe.comp_name, pe.comp_path, pe.comp_line)
|
||||
else
|
||||
lines[#lines + 1] = string.format(
|
||||
"WORD %d CALL %s:%d RAW",
|
||||
pe.pos, rel_path, pe.line)
|
||||
end
|
||||
end
|
||||
|
||||
-- Patch the placeholder total in the ATOM header line.
|
||||
lines[1] = lines[1]:gsub(" 0$", " " .. tostring(total))
|
||||
lines[#lines + 1] = "ENDATOM"
|
||||
return lines, total
|
||||
end
|
||||
|
||||
--- Render the full provenance file content for one source (one `.atoms.provenance.txt` per source).
|
||||
--- @param src table
|
||||
--- @param wc table
|
||||
--- @param comp table -- shared.components map
|
||||
--- @return string
|
||||
local function render_provenance(src, wc, comp)
|
||||
local lines = {}
|
||||
lines[#lines + 1] = "# FORMAT_VERSION 1"
|
||||
lines[#lines + 1] = "# auto-generated by ps1_meta.lua (passes/atoms_source_map.lua) — DO NOT EDIT"
|
||||
lines[#lines + 1] = "# Per-.word provenance: maps each emitted .word to its call site (atom body"
|
||||
lines[#lines + 1] = "# file:line) and, when the word was emitted by a `mac_X(...)` component invocation,"
|
||||
lines[#lines + 1] = "# the component's definition file:line. Used by dwarf_injection (Phase 3) to synthesize"
|
||||
lines[#lines + 1] = "# DW_TAG_inlined_subroutine instances for component step-into."
|
||||
|
||||
for _, atom in ipairs(src.scan.atoms or {}) do
|
||||
local stanza = emit_provenance_stanza(src, atom, wc, comp)
|
||||
for _, line in ipairs(stanza) do
|
||||
lines[#lines + 1] = line
|
||||
end
|
||||
end
|
||||
for _, atom in ipairs(src.scan.raw_atoms or {}) do
|
||||
local stanza = emit_provenance_stanza(src, atom, wc, comp)
|
||||
for _, line in ipairs(stanza) do
|
||||
lines[#lines + 1] = line
|
||||
end
|
||||
end
|
||||
|
||||
return table.concat(lines, "\n") .. "\n"
|
||||
end
|
||||
|
||||
--- Render one atom's stanza for the canonical text form
|
||||
--- (ATOM header line, N WORD lines, ENDATOM marker). Returns (lines, total_words).
|
||||
--- @param src table
|
||||
@@ -533,6 +680,9 @@ local M = {}
|
||||
|
||||
--- Pass entry: emit one `<out_root>/<basename>.atoms.sourcemap.txt` per source file
|
||||
--- that contains at least one `MipsAtom_(name)` / `MipsCode code_<name>` declaration.
|
||||
--- Also emits `<out_root>/<basename>.atoms.provenance.txt` (Phase 3 — debug_ux):
|
||||
--- per-.word provenance with `mac_X(...)` component resolution back to the
|
||||
--- component's definition file:line.
|
||||
--- Optionally also emit `<ctx.out_root>/gdb_tape_atoms_runtime.gdb` when
|
||||
--- `ctx.flags.gdb_runtime` is true.
|
||||
--- @param ctx PassCtx
|
||||
@@ -552,6 +702,12 @@ function M.run(ctx)
|
||||
}
|
||||
end
|
||||
|
||||
-- Phase 3 (debug_ux): shared.components map is populated by `passes/components.lua`.
|
||||
-- Used to attribute each emitted `.word` to either a component macro or the
|
||||
-- enclosing atom body. If absent, all words fall through as RAW (correct
|
||||
-- behavior — provenance is additive).
|
||||
local comp = (ctx.shared and ctx.shared.components) or {}
|
||||
|
||||
-- Always emit the canonical text form (per-source).
|
||||
for _, src in ipairs(ctx.sources) do
|
||||
if src.scan then
|
||||
@@ -559,18 +715,27 @@ function M.run(ctx)
|
||||
local n_raw_atoms = src.scan.raw_atoms and #src.scan.raw_atoms or 0
|
||||
if n_atoms + n_raw_atoms > 0 then
|
||||
local basename = duffle.basename_no_ext(src.path)
|
||||
-- Build report, NOT compile artifact: live in <out_root> alongside the other reports
|
||||
-- (annotation.lua's *.errors.h, static_analysis.lua's *.static_analysis.txt, gdb_tape_atoms_runtime.gdb).
|
||||
-- The per-source <source_dir>/gen/ is reserved for headers actually #included by C.
|
||||
local out_path = ctx.out_root .. "/" .. basename .. ".atoms.sourcemap.txt"
|
||||
local content = render_source_map(src, wc)
|
||||
|
||||
-- (1) atoms.sourcemap.txt — per-.word line map (unchanged contract).
|
||||
local sourcemap_path = ctx.out_root .. "/" .. basename .. ".atoms.sourcemap.txt"
|
||||
local sourcemap_body = render_source_map(src, wc)
|
||||
|
||||
-- (2) atoms.provenance.txt — Phase 3 (debug_ux): per-.word provenance
|
||||
-- with `mac_X(...)` component resolution back to the component's
|
||||
-- definition file:line. Consumed by `passes/dwarf_injection.lua`
|
||||
-- to synthesize `DW_TAG_inlined_subroutine` instances for
|
||||
-- source-level Step Into on component invocations.
|
||||
local prov_path = ctx.out_root .. "/" .. basename .. ".atoms.provenance.txt"
|
||||
local prov_body = render_provenance(src, wc, comp)
|
||||
|
||||
if not ctx.dry_run then
|
||||
duffle.ensure_dir(duffle.dirname(out_path))
|
||||
duffle.write_file_lf(out_path, content)
|
||||
duffle.ensure_dir(duffle.dirname(sourcemap_path))
|
||||
duffle.write_file_lf(sourcemap_path, sourcemap_body)
|
||||
duffle.write_file_lf(prov_path, prov_body)
|
||||
end
|
||||
|
||||
outputs[#outputs + 1] = { kind = "report", path = out_path }
|
||||
outputs[#outputs + 1] = { kind = "report", path = sourcemap_path }
|
||||
outputs[#outputs + 1] = { kind = "report", path = prov_path }
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@@ -366,6 +366,7 @@ local function project_components(source, scan)
|
||||
body_tokens = a.body_tokens,
|
||||
args = args,
|
||||
comment = comment,
|
||||
kind = a.kind, -- "comp_bare" | "comp_proc"; Phase 3 provenance emitter reads this.
|
||||
}
|
||||
end
|
||||
end
|
||||
@@ -677,6 +678,35 @@ local function update_shared_word_counts(ctx, components, counts)
|
||||
end
|
||||
end
|
||||
|
||||
--- @class ComponentDef
|
||||
--- @field name string -- bare name (without ac_/mac_ prefix)
|
||||
--- @field line integer -- definition source line (line of `MipsAtomComp_(ac_X)` / `MipsAtomComp_Proc_(ac_X, ...)`)
|
||||
--- @field path string -- absolute source path of the definition
|
||||
--- @field kind string -- "comp_bare" | "comp_proc"
|
||||
|
||||
--- (internal) Extend `ctx.shared.components` with this source's components-by-name
|
||||
-- map so downstream passes (atoms_source_map, dwarf_injection) can resolve
|
||||
-- `mac_X(...)` invocations back to their component definition file:line.
|
||||
--- Phase 3 (debug_ux) provenance emission uses this to attribute each emitted
|
||||
-- `.word` to either a component macro or the enclosing atom body.
|
||||
-- @param ctx PassCtx
|
||||
-- @param src SourceFile
|
||||
-- @param components Component[]
|
||||
local function update_shared_components(ctx, src, components)
|
||||
ctx.shared.components = ctx.shared.components or {}
|
||||
local rel_path = src.path:gsub("\\", "/")
|
||||
for _, c in ipairs(components) do
|
||||
-- Keyed by bare name (e.g. `yield`, `load_tri_indices`). The atoms_source_map
|
||||
-- pass strips the `mac_` prefix from the call site identifier before lookup.
|
||||
ctx.shared.components[c.name] = {
|
||||
name = c.name,
|
||||
line = c.line,
|
||||
path = rel_path,
|
||||
kind = c.kind or "comp_bare",
|
||||
}
|
||||
end
|
||||
end
|
||||
|
||||
--- @param ctx PassCtx
|
||||
--- @return PassResult
|
||||
function M.run(ctx)
|
||||
@@ -684,6 +714,11 @@ function M.run(ctx)
|
||||
local errors = {}
|
||||
local warnings = {}
|
||||
|
||||
-- Initialize shared component map (Phase 3 — debug_ux). The atoms_source_map
|
||||
-- and dwarf_injection passes consume `ctx.shared.components` to resolve
|
||||
-- `mac_X(...)` invocations back to the component's definition file:line.
|
||||
ctx.shared.components = ctx.shared.components or {}
|
||||
|
||||
for _, src in ipairs(ctx.sources) do
|
||||
-- project_components reads from src.scan + does backward lookups on src.text
|
||||
local components = project_components(src.text, src.scan)
|
||||
@@ -694,6 +729,9 @@ function M.run(ctx)
|
||||
if macs_path then
|
||||
outputs[#outputs + 1] = { macs_h = macs_path }
|
||||
update_shared_word_counts(ctx, components, counts)
|
||||
-- Phase 3 (debug_ux): share component definitions with downstream passes.
|
||||
-- `mac_X(...)` invocations in atom bodies resolve back to (path, line) via this map.
|
||||
update_shared_components(ctx, src, components)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
+306
-18
@@ -6,6 +6,7 @@
|
||||
--- MipsAtom_ (kind = "atom", with optional atom_info inner)
|
||||
--- MipsAtomComp_ (kind = "comp_bare")
|
||||
--- MipsAtomComp_Proc_ (kind = "comp_proc", body inside last {})
|
||||
--- atom_dbg_skip_over (whole-atom/component debug-step marker; following declaration disambiguates)
|
||||
--- MipsCode code_<name> (kind = "raw_atom", offsets pass only)
|
||||
--- typedef Struct_(Binds_X) { fields }
|
||||
--- #pragma mac_X tape_atom words=N + _Pragma("...")
|
||||
@@ -35,8 +36,55 @@ local duffle = dofile(_bootstrap_dir .. "../duffle_paths.lua")
|
||||
--- @field binds BindsEntry[] -- typedef Struct_(Binds_X) { fields } (fields pre-parsed)
|
||||
--- @field atom_infos AtomInfoEntry[] -- MipsAtom_(name) atom_info(...) (sub-calls pre-parsed)
|
||||
--- @field macros MacroEntry[] -- #pragma mac_X tape_atom words=N + _Pragma("...")
|
||||
--- @field skip_over SkipOverScan -- atom/component debug-step markers + resolved declaration associations
|
||||
--- @field types table<string, RegTypeDefault> -- atom_dbg_reg_default(R_X, <type>) declarations
|
||||
--- @field atom_views table<string, AtomViewEntry> -- MipsAtom_(name) -> {binds_name, reg_type_overrides, info_line}
|
||||
--- @field line_of fun(pos: integer): integer -- shared LineIndex closure
|
||||
|
||||
--- @class SkipOverScan
|
||||
--- @field atoms table<string, SkipOverAssociation>
|
||||
--- @field components table<string, SkipOverAssociation>
|
||||
--- @field markers SkipOverMarker[]
|
||||
|
||||
--- @class SkipOverMarker
|
||||
--- @field marker_kind string -- exact marker ident (always "atom_dbg_skip_over")
|
||||
--- @field marker_line integer
|
||||
--- @field marker_pos integer
|
||||
--- @field after_paren integer
|
||||
--- @field args string|nil -- trimmed marker args; valid Task 13 markers use ""
|
||||
--- @field has_parens boolean
|
||||
--- @field pending boolean
|
||||
--- @field superseded_by_marker_line integer|nil
|
||||
--- @field target_name string|nil -- stripped declaration name once observed
|
||||
--- @field target_raw_name string|nil -- source-written declaration name once observed
|
||||
--- @field target_kind string|nil -- "atom" | "comp_bare" | "comp_proc" | "unrelated" once observed
|
||||
--- @field declaration_line integer|nil
|
||||
--- @field declaration_pos integer|nil
|
||||
--- @field proc_prelude boolean|nil -- marker has crossed FI_ and awaits MipsAtomComp_Proc_
|
||||
|
||||
--- @class RegTypeDefault
|
||||
--- @field name string -- "R_TapePtr" (the register ident; without the value part)
|
||||
--- @field type_name string -- "U4" / "V3_S2" / "void" (the pointer/struct base name)
|
||||
--- @field pointer_depth integer -- 0 for `U4`, 1 for `U4*`, 2 for `U4**`
|
||||
--- @field source_line integer -- 1-based source line of the declaration
|
||||
|
||||
--- @class RegTypeOverride
|
||||
--- @field reg string -- "R_T0"
|
||||
--- @field type_name string
|
||||
--- @field pointer_depth integer
|
||||
|
||||
--- @class AtomViewEntry
|
||||
--- @field atom_name string -- e.g. "red_cube_g4_face"
|
||||
--- @field binds_name string|nil -- "Binds_CubeTri" if attached
|
||||
--- @field reg_type_overrides table<string, RegTypeOverride> -- "R_T0" -> override
|
||||
--- @field info_line integer -- line of the atom_info call
|
||||
|
||||
--- @class SkipOverAssociation
|
||||
--- @field marker_line integer
|
||||
--- @field declaration_line integer
|
||||
--- @field kind string
|
||||
--- @field marker SkipOverMarker
|
||||
|
||||
--- @class SourceFile
|
||||
--- @field path string -- absolute path to the source file
|
||||
--- @field text string -- the full source text
|
||||
@@ -101,6 +149,94 @@ local function strip_ac_prefix(raw_name)
|
||||
return raw_name
|
||||
end
|
||||
|
||||
-- Preserve a source marker until the following declaration parser observes it.
|
||||
-- Task 13 records evidence only; Task 14 diagnoses non-empty args,
|
||||
-- duplicates, dangling markers, and unrelated declaration placement.
|
||||
local function push_skip_over_marker(out, marker)
|
||||
local markers = out.skip_over.markers
|
||||
local prior = markers[#markers]
|
||||
if prior and prior.pending then
|
||||
prior.pending = false
|
||||
prior.superseded_by_marker_line = marker.marker_line
|
||||
end
|
||||
marker.pending = true
|
||||
markers[#markers + 1] = marker
|
||||
end
|
||||
|
||||
-- Attach the pending marker to the next declaration. The declaration form
|
||||
-- disambiguates whole atoms from components; unsupported declarations retain
|
||||
-- placement evidence for annotation.lua and populate neither lookup table.
|
||||
local function associate_skip_over_marker(out, target_name, target_raw_name, target_kind, declaration_line, declaration_pos)
|
||||
local markers = out.skip_over.markers
|
||||
local marker = markers[#markers]
|
||||
if not (marker and marker.pending) then return end
|
||||
|
||||
marker.pending = false
|
||||
marker.target_name = target_name
|
||||
marker.target_raw_name = target_raw_name
|
||||
marker.target_kind = target_kind
|
||||
marker.declaration_line = declaration_line
|
||||
marker.declaration_pos = declaration_pos
|
||||
|
||||
if not (marker.has_parens and marker.args == "") then return end
|
||||
|
||||
local association = {
|
||||
marker_line = marker.marker_line,
|
||||
declaration_line = declaration_line,
|
||||
kind = target_kind,
|
||||
marker = marker,
|
||||
}
|
||||
if target_kind == "atom" then
|
||||
out.skip_over.atoms[target_name] = association
|
||||
elseif target_kind == "comp_bare" or target_kind == "comp_proc" then
|
||||
out.skip_over.components[target_name] = association
|
||||
end
|
||||
end
|
||||
|
||||
-- Read a top-level comma-delimited argument list. Mirrors split_top_level_commas
|
||||
-- in shape; kept inline so scan_source can remain dependency-free.
|
||||
local function read_top_level_args(text, pos)
|
||||
local args = {}
|
||||
pos = duffle.skip_ws_and_cmt(text, pos)
|
||||
while pos <= #text do
|
||||
local buf, level = {}, 0
|
||||
while pos <= #text do
|
||||
local c = text:sub(pos, pos)
|
||||
if c == "(" or c == "[" or c == "{" then level = level + 1
|
||||
elseif c == ")" or c == "]" or c == "}" then
|
||||
if level == 0 then break end
|
||||
level = level - 1
|
||||
elseif c == "," and level == 0 then break end
|
||||
buf[#buf + 1] = c
|
||||
pos = pos + 1
|
||||
end
|
||||
local arg = duffle.trim(table.concat(buf))
|
||||
if arg ~= "" then args[#args + 1] = arg end
|
||||
if pos > #text then break end
|
||||
if text:sub(pos, pos) == "," then pos = pos + 1; pos = duffle.skip_ws_and_cmt(text, pos) end
|
||||
if not text:sub(pos, pos) or text:sub(pos, pos) == ")" then break end
|
||||
end
|
||||
return args
|
||||
end
|
||||
|
||||
-- Parse a `Type*` chain (zero or more `*` separated by optional whitespace)
|
||||
-- followed by the type ident. Returns (type_name, pointer_depth) or nil.
|
||||
local function parse_type_chain(text, pos)
|
||||
if pos > #text then return nil end
|
||||
-- Skip leading whitespace before the type ident.
|
||||
local start = duffle.skip_ws_and_cmt(text, pos)
|
||||
local ident, after = duffle.read_ident(text, start)
|
||||
if not ident then return nil end
|
||||
local depth = 0
|
||||
local cursor = duffle.skip_ws_and_cmt(text, after)
|
||||
while cursor <= #text and text:sub(cursor, cursor) == "*" do
|
||||
depth = depth + 1
|
||||
cursor = cursor + 1
|
||||
cursor = duffle.skip_ws_and_cmt(text, cursor)
|
||||
end
|
||||
return ident, depth, cursor
|
||||
end
|
||||
|
||||
-- Parse the U4 fields from a Binds_X body. Returns (fields, byte_count).
|
||||
local function scan_binds_fields(body)
|
||||
local fields = {}
|
||||
@@ -146,10 +282,13 @@ local function scan_reg_list(sub_inner)
|
||||
return regs
|
||||
end
|
||||
|
||||
-- Parse the sub-calls inside `atom_info(atom_bind(...), atom_reads(...), atom_writes(...))`.
|
||||
-- Returns (binds, reads, writes).
|
||||
-- Parse the sub-calls inside `atom_info(atom_bind(...), atom_reads(...), atom_writes(...), atom_view(...), atom_reg_types(...))`.
|
||||
-- Returns (binds, reads, writes, view_binds, reg_overrides).
|
||||
-- `view_binds` is the Binds_X ident from `atom_view(Binds_X)`.
|
||||
-- `reg_overrides` is a {reg_name = {type_name, pointer_depth}} table for `atom_reg_types(R_X, <type>)`.
|
||||
local function scan_atom_info_subcalls(info_inner)
|
||||
local binds, reads, writes = nil, nil, nil
|
||||
local view_binds, reg_overrides = nil, nil
|
||||
local sub_pos = 1
|
||||
while sub_pos <= #info_inner do
|
||||
sub_pos = duffle.skip_ws_and_cmt(info_inner, sub_pos)
|
||||
@@ -179,11 +318,43 @@ local function scan_atom_info_subcalls(info_inner)
|
||||
else
|
||||
sub_pos = sub_open + 1
|
||||
end
|
||||
elseif sub_ident == "atom_view" then
|
||||
local sub_open = duffle.skip_ws_and_cmt(info_inner, sub_end)
|
||||
if info_inner:sub(sub_open, sub_open) == "(" then
|
||||
local sub_inner, sub_after2 = duffle.read_parens(info_inner, sub_open)
|
||||
view_binds = duffle.trim(sub_inner)
|
||||
sub_pos = sub_after2
|
||||
else
|
||||
sub_pos = sub_open + 1
|
||||
end
|
||||
elseif sub_ident == "atom_reg_types" then
|
||||
local sub_open = duffle.skip_ws_and_cmt(info_inner, sub_end)
|
||||
if info_inner:sub(sub_open, sub_open) == "(" then
|
||||
local sub_inner, sub_after2 = duffle.read_parens(info_inner, sub_open)
|
||||
local args = read_top_level_args(sub_inner, 1)
|
||||
if args[1] then
|
||||
reg_overrides = reg_overrides or {}
|
||||
local reg_name = duffle.trim(args[1])
|
||||
local type_name, depth = nil, 0
|
||||
if args[2] then
|
||||
local parsed_name, parsed_depth = parse_type_chain(args[2], 1)
|
||||
if parsed_name then
|
||||
type_name, depth = parsed_name, parsed_depth
|
||||
else
|
||||
type_name, depth = duffle.trim(args[2]), 0
|
||||
end
|
||||
end
|
||||
reg_overrides[reg_name] = { type_name = type_name, pointer_depth = depth }
|
||||
end
|
||||
sub_pos = sub_after2
|
||||
else
|
||||
sub_pos = sub_open + 1
|
||||
end
|
||||
else
|
||||
sub_pos = sub_end
|
||||
end
|
||||
end
|
||||
return binds, reads, writes
|
||||
return binds, reads, writes, view_binds, reg_overrides
|
||||
end
|
||||
|
||||
-- Skip C qualifier keywords and return the position past the last one.
|
||||
@@ -207,7 +378,7 @@ end
|
||||
-- pos -- position of the construct's leading ident (e.g., `M` of `MipsAtom_`)
|
||||
-- ident_end -- position past the leading ident (where the `(` should be)
|
||||
-- line_of -- closure over LineIndex(source) for 1-based line lookups
|
||||
-- out -- the SourceScan out table (mutated in place: out.atoms / out.raw_atoms / out.binds / out.atom_infos / out.macros)
|
||||
-- out -- the SourceScan out table (mutated in place: out.atoms / out.raw_atoms / out.binds / out.atom_infos / out.macros / out.skip_over)
|
||||
-- returns -- new position after the construct
|
||||
--
|
||||
-- All parsers read source-as-written via the duffle primitives (skip_ws_and_cmt / read_parens / read_braces / read_balanced).
|
||||
@@ -216,6 +387,68 @@ end
|
||||
--
|
||||
-- Adding a new construct = 1 row in DECL_PARSERS + 1 parser function. The scan_source() loop never needs editing.
|
||||
|
||||
--- Parse an empty debug-skip marker and retain its raw placement evidence.
|
||||
--- @param source string
|
||||
--- @param pos integer
|
||||
--- @param ident_end integer
|
||||
--- @param line_of fun(pos: integer): integer
|
||||
--- @param out SourceScan
|
||||
--- @param marker_kind string
|
||||
--- @return integer
|
||||
local function parse_skip_over_marker(source, pos, ident_end, line_of, out, marker_kind)
|
||||
local open_paren = duffle.skip_ws_and_cmt(source, ident_end)
|
||||
local marker = {
|
||||
marker_kind = marker_kind,
|
||||
marker_line = line_of(pos),
|
||||
marker_pos = pos,
|
||||
after_paren = ident_end,
|
||||
args = nil,
|
||||
has_parens = false,
|
||||
}
|
||||
if source:sub(open_paren, open_paren) == "(" then
|
||||
local inner, after_paren = duffle.read_parens(source, open_paren)
|
||||
marker.after_paren = after_paren
|
||||
marker.args = duffle.trim(inner)
|
||||
marker.has_parens = true
|
||||
end
|
||||
push_skip_over_marker(out, marker)
|
||||
return marker.after_paren
|
||||
end
|
||||
|
||||
local function parse_atom_dbg_skip_over(source, pos, ident_end, line_of, out)
|
||||
return parse_skip_over_marker(source, pos, ident_end, line_of, out, "atom_dbg_skip_over")
|
||||
end
|
||||
|
||||
-- Parse `atom_dbg_reg_default(R_X, <type>...)`; the second argument may be
|
||||
-- a `Type` or `Type*`/`Type**` chain. Records in `out.types[R_X]`.
|
||||
local function parse_atom_dbg_reg_default(source, pos, ident_end, line_of, out)
|
||||
local open_paren = duffle.skip_ws_and_cmt(source, ident_end)
|
||||
if source:sub(open_paren, open_paren) ~= "(" then return open_paren + 1 end
|
||||
local inner, after_paren = duffle.read_parens(source, open_paren)
|
||||
local args = read_top_level_args(inner, 1)
|
||||
if #args < 1 then
|
||||
-- Annotation pass surfaces this; we still consume the marker.
|
||||
return after_paren
|
||||
end
|
||||
local reg_name = duffle.trim(args[1])
|
||||
local type_part = args[2] or "void"
|
||||
local type_name, depth = parse_type_chain(type_part, 1)
|
||||
if not type_name then type_name, depth = duffle.trim(type_part), 0 end
|
||||
out.types[reg_name] = {
|
||||
name = reg_name,
|
||||
type_name = type_name,
|
||||
pointer_depth = depth,
|
||||
source_line = line_of(pos),
|
||||
}
|
||||
out.type_occurrences = out.type_occurrences or {}
|
||||
out.type_occurrences[#out.type_occurrences + 1] = {
|
||||
reg = reg_name,
|
||||
type_name = type_name,
|
||||
source_line = line_of(pos),
|
||||
}
|
||||
return after_paren
|
||||
end
|
||||
|
||||
--- Parse: `MipsAtom_(<name>) [atom_info(<binds>, <reads>, <writes>)] { <body> }`
|
||||
--- @param source string
|
||||
--- @param pos integer
|
||||
@@ -238,12 +471,27 @@ local function parse_mips_atom(source, pos, ident_end, line_of, out)
|
||||
local info_open = duffle.skip_ws_and_cmt(source, look_end)
|
||||
if source:sub(info_open, info_open) == "(" then
|
||||
local info_inner, info_after = duffle.read_parens(source, info_open)
|
||||
local ai_binds, ai_reads, ai_writes = scan_atom_info_subcalls(info_inner)
|
||||
local ai_binds, ai_reads, ai_writes, ai_view, ai_overrides = scan_atom_info_subcalls(info_inner)
|
||||
out.atom_infos[#out.atom_infos + 1] = {
|
||||
atom_name = raw_name or "?", binds = ai_binds,
|
||||
reads = ai_reads or {}, writes = ai_writes or {},
|
||||
view = ai_view,
|
||||
reg_type_overrides = ai_overrides,
|
||||
info_line = line_of(lookahead),
|
||||
}
|
||||
if ai_view and raw_name then
|
||||
out.atom_views[raw_name] = {
|
||||
atom_name = raw_name,
|
||||
binds_name = ai_view,
|
||||
reg_type_overrides = ai_overrides,
|
||||
info_line = line_of(lookahead),
|
||||
}
|
||||
elseif raw_name and ai_overrides then
|
||||
-- Record per-atom overrides even without atom_view.
|
||||
out.atom_views[raw_name] = out.atom_views[raw_name]
|
||||
or { atom_name = raw_name, binds_name = nil, reg_type_overrides = nil, info_line = line_of(lookahead) }
|
||||
out.atom_views[raw_name].reg_type_overrides = ai_overrides
|
||||
end
|
||||
brace_search_pos = info_after
|
||||
end
|
||||
end
|
||||
@@ -253,11 +501,13 @@ local function parse_mips_atom(source, pos, ident_end, line_of, out)
|
||||
|
||||
local body, after_brace = duffle.read_braces(source, brace)
|
||||
if raw_name and raw_name ~= "" then
|
||||
local declaration_line = line_of(pos)
|
||||
out.atoms[#out.atoms + 1] = {
|
||||
line = line_of(pos), name = raw_name, body = body, body_off = brace + 1,
|
||||
line = declaration_line, name = raw_name, body = body, body_off = brace + 1,
|
||||
kind = "atom", raw_name = raw_name,
|
||||
ident_pos = pos, after_paren = after_paren,
|
||||
}
|
||||
associate_skip_over_marker(out, raw_name, raw_name, "atom", declaration_line, pos)
|
||||
end
|
||||
|
||||
return after_brace
|
||||
@@ -282,12 +532,15 @@ local function parse_mips_atom_comp(source, pos, ident_end, line_of, out)
|
||||
if not brace then return open_paren + 1 end
|
||||
|
||||
local body, after_brace = duffle.read_braces(source, brace)
|
||||
local name = strip_ac_prefix(raw_name)
|
||||
local declaration_line = line_of(pos)
|
||||
|
||||
out.atoms[#out.atoms + 1] = {
|
||||
line = line_of(pos), name = strip_ac_prefix(raw_name), body = body, body_off = brace + 1,
|
||||
line = declaration_line, name = name, body = body, body_off = brace + 1,
|
||||
kind = "comp_bare", raw_name = raw_name,
|
||||
ident_pos = pos, after_paren = after_paren,
|
||||
}
|
||||
associate_skip_over_marker(out, name, raw_name, "comp_bare", declaration_line, pos)
|
||||
|
||||
return after_brace
|
||||
end
|
||||
@@ -318,14 +571,17 @@ local function parse_mips_atom_comp_proc(source, pos, ident_end, line_of, out)
|
||||
if close_pos > #inner + 1 then return after_paren end
|
||||
|
||||
local raw_name = inner:match("^%s*([%w_]+)") or "?"
|
||||
local name = strip_ac_prefix(raw_name)
|
||||
local declaration_line = line_of(pos)
|
||||
-- Position of body[1] in source = open_paren + 1 (start of inner) + last_brace_pos + 1 (past '{').
|
||||
local body_off = open_paren + 2 + last_brace_pos
|
||||
|
||||
out.atoms[#out.atoms + 1] = {
|
||||
line = line_of(pos), name = strip_ac_prefix(raw_name), body = body, body_off = body_off,
|
||||
line = declaration_line, name = name, body = body, body_off = body_off,
|
||||
kind = "comp_proc", raw_name = raw_name,
|
||||
ident_pos = pos, after_paren = after_paren,
|
||||
}
|
||||
associate_skip_over_marker(out, name, raw_name, "comp_proc", declaration_line, pos)
|
||||
|
||||
return after_paren
|
||||
end
|
||||
@@ -349,10 +605,12 @@ local function parse_mips_code(source, pos, ident_end, line_of, out)
|
||||
if not brace_pos then return ident_end end
|
||||
|
||||
local body, after_brace = duffle.read_braces(source, brace_pos)
|
||||
local declaration_line = line_of(pos)
|
||||
out.raw_atoms[#out.raw_atoms + 1] = {
|
||||
line = line_of(pos), name = atom_name, body = body, body_off = brace_pos + 1,
|
||||
line = declaration_line, name = atom_name, body = body, body_off = brace_pos + 1,
|
||||
kind = "raw_atom", raw_name = atom_name,
|
||||
}
|
||||
associate_skip_over_marker(out, atom_name, next_ident, "unrelated", declaration_line, pos)
|
||||
|
||||
return after_brace
|
||||
end
|
||||
@@ -383,6 +641,7 @@ local function parse_typedef_binds(source, pos, ident_end, line_of, out)
|
||||
local fields, byte_off = scan_binds_fields(body)
|
||||
out.binds[#out.binds + 1] = { line = line_of(pos), name = name, fields = fields, bytes = byte_off }
|
||||
end
|
||||
associate_skip_over_marker(out, name, name, "unrelated", line_of(pos), pos)
|
||||
|
||||
return after_brace
|
||||
end
|
||||
@@ -443,13 +702,15 @@ end
|
||||
-- Adding a new construct = 1 row here + 1 parser function above.
|
||||
|
||||
local DECL_PARSERS = {
|
||||
MipsAtom_ = parse_mips_atom,
|
||||
MipsAtomComp_ = parse_mips_atom_comp,
|
||||
MipsAtomComp_Proc_ = parse_mips_atom_comp_proc,
|
||||
MipsCode = parse_mips_code,
|
||||
typedef = parse_typedef_binds,
|
||||
_Pragma = parse_pragma_macro,
|
||||
pragma = parse_pragma_dummy,
|
||||
MipsAtom_ = parse_mips_atom,
|
||||
MipsAtomComp_ = parse_mips_atom_comp,
|
||||
MipsAtomComp_Proc_ = parse_mips_atom_comp_proc,
|
||||
atom_dbg_skip_over = parse_atom_dbg_skip_over,
|
||||
atom_dbg_reg_default = parse_atom_dbg_reg_default,
|
||||
MipsCode = parse_mips_code,
|
||||
typedef = parse_typedef_binds,
|
||||
_Pragma = parse_pragma_macro,
|
||||
pragma = parse_pragma_dummy,
|
||||
}
|
||||
|
||||
-- ════════════════════════════════════════════════════════════════════════════
|
||||
@@ -459,7 +720,7 @@ local DECL_PARSERS = {
|
||||
--- Single-pass source scan. Walks the source ONCE and extracts every construct type the metaprogram passes need.
|
||||
--- Returns a fat SourceScan table. Each pass filters from this payload instead of re-walking the source.
|
||||
--- @param source string
|
||||
--- @return table -- SourceScan { atoms, raw_atoms, binds, atom_infos, macros, line_of }
|
||||
--- @return table -- SourceScan { atoms, raw_atoms, binds, atom_infos, macros, skip_over, line_of }
|
||||
local function scan_source(source)
|
||||
local line_of = duffle.LineIndex(source)
|
||||
local out = {
|
||||
@@ -468,6 +729,13 @@ local function scan_source(source)
|
||||
binds = {},
|
||||
atom_infos = {},
|
||||
macros = {},
|
||||
skip_over = {
|
||||
atoms = {},
|
||||
components = {},
|
||||
markers = {},
|
||||
},
|
||||
types = {},
|
||||
atom_views = {},
|
||||
line_of = line_of,
|
||||
}
|
||||
local pos = 1
|
||||
@@ -492,10 +760,30 @@ local function scan_source(source)
|
||||
if parser then
|
||||
pos = parser(source, pos, ident_end, line_of, out)
|
||||
else
|
||||
-- Unrecognized ident — advance past it.
|
||||
-- A component-procedure declaration has an FI_ signature before
|
||||
-- MipsAtomComp_Proc_; keep the marker pending across that prelude.
|
||||
-- Any other identifier begins an unrelated declaration/construct
|
||||
-- and consumes the marker so it cannot drift to a later atom.
|
||||
local markers = out.skip_over.markers
|
||||
local marker = markers[#markers]
|
||||
if marker and marker.pending then
|
||||
if ident == "FI_" then
|
||||
marker.proc_prelude = true
|
||||
elseif not marker.proc_prelude then
|
||||
associate_skip_over_marker(out, ident, ident, "unrelated", line_of(pos), pos)
|
||||
end
|
||||
end
|
||||
pos = ident_end
|
||||
end
|
||||
else
|
||||
local markers = out.skip_over.markers
|
||||
local marker = markers[#markers]
|
||||
if marker and marker.pending and marker.proc_prelude then
|
||||
local c = source:sub(pos, pos)
|
||||
if c == "{" or c == ";" then
|
||||
associate_skip_over_marker(out, c, c, "unrelated", line_of(pos), pos)
|
||||
end
|
||||
end
|
||||
pos = pos + 1
|
||||
end
|
||||
end
|
||||
|
||||
@@ -152,14 +152,17 @@ local PASSES = {
|
||||
module = "passes.atoms_source_map",
|
||||
kind = "header-output",
|
||||
deps = {"word-counts", "components"},
|
||||
desc = "Emit gen/<basename>.atoms.sourcemap.txt (per-.word C source line map for gdb debugging)",
|
||||
out = { { kind = "header", path_template = "<source_dir>/gen/<basename>.atoms.sourcemap.txt" } },
|
||||
desc = "Emit gen/<basename>.atoms.sourcemap.txt (per-.word C source line map for gdb debugging) AND gen/<basename>.atoms.provenance.txt (per-.word provenance; each word tagged with its call-site file:line and, when emitted by a mac_X(...) component invocation, the component's definition file:line). Consumed by passes/dwarf_injection.lua to synthesize DW_TAG_inlined_subroutine instances for source-level Step Into on component invocations (Phase 3 — debug_ux).",
|
||||
out = {
|
||||
{ kind = "report", path_template = "<out_root>/<basename>.atoms.sourcemap.txt" },
|
||||
{ kind = "report", path_template = "<out_root>/<basename>.atoms.provenance.txt" },
|
||||
},
|
||||
},
|
||||
["dwarf-injection"] = {
|
||||
module = "passes.dwarf_injection",
|
||||
kind = "shared",
|
||||
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.",
|
||||
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 plus one deterministic .gdbinit sidecar). (rbind composite) reads ctx.sources[i].scan to find atom_bind(Binds_X) atoms + their Binds_X struct fields; emits per-Binds_X DW_TAG_structure_type DIEs + per-rbind-atom DW_TAG_variable 'bind_args' DIEs with piece-chain DW_OP_bregN/DW_OP_piece location expressions.",
|
||||
out = {
|
||||
{ kind = "report", path_template = "<out_root>/<basename>.dwarf_line.bin" },
|
||||
{ kind = "report", path_template = "<out_root>/<basename>.dwarf_aranges.bin" },
|
||||
@@ -168,6 +171,7 @@ local PASSES = {
|
||||
{ kind = "report", path_template = "<out_root>/<basename>.dwarf_info.bin" },
|
||||
{ kind = "report", path_template = "<out_root>/<basename>.dwarf_str.bin" },
|
||||
{ kind = "report", path_template = "<out_root>/<basename>.dwarf_loc.bin" },
|
||||
{ kind = "report", path_template = "<out_root>/<basename>.gdbinit" },
|
||||
},
|
||||
},
|
||||
report = {
|
||||
|
||||
Reference in New Issue
Block a user