WIP: Better step debug on atom components, better db_skip annotation, lots of curation passes on lua.

Still don't have this thing in its final state for  the curse but its close.
This commit is contained in:
ed
2026-07-26 13:55:47 -04:00
parent f247d56c32
commit 80a35aa23a
21 changed files with 1214 additions and 1244 deletions
+264 -181
View File
@@ -1,12 +1,12 @@
--- passes/scan_source.lua — Source pre-scan pass (the "mega entity" pass).
---
--- Single source-walk pass that produces the fat `SourceScan` payload consumed by all downstream passes. Walks each `ctx.sources` entry once,
--- Single source-walk pass that produces the fat `SourceScan` payload consumed by all downstream passes. Walks each corpus source record once,
--- extracting every construct type the metaprograms need:
---
--- 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)
--- atom_dbg_skip — bare 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("...")
@@ -37,38 +37,29 @@ local parse_enum_int_literal
-- ════════════════════════════════════════════════════════════════════════════
--- @class SourceScan
--- @field atoms AtomEntry[] -- MipsAtom_ + MipsAtomComp_ + MipsAtomComp_Proc_
--- @field raw_atoms AtomEntry[] -- MipsCode code_<name> { body } (offsets pass only)
--- @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 atom_ctxs table<string, AtomCtxEntry> -- MipsAtom_(name) -> {rbind_atom, info_line, source} (atom_ctx(...) call sites)
--- @field atom_phases table<string, AtomPhaseGroup> -- phase_label -> {atoms = {atom_name1, atom_name2, ...}} (atom_phase(...) tags)
--- @field line_of fun(pos: integer): integer -- shared LineIndex closure
--- @field atoms AtomEntry[] -- MipsAtom_ + MipsAtomComp_ + MipsAtomComp_Proc_
--- @field raw_atoms AtomEntry[] -- MipsCode code_<name> { body } (offsets pass only)
--- @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 debug_skip_markers DebugSkipMarker[] -- raw marker evidence for annotation validation; `debug_skip` lives on the declaration record itself
--- @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 atom_ctxs table<string, AtomCtxEntry> -- MipsAtom_(name) -> {rbind_atom, info_line, source} (atom_ctx(...) call sites)
--- @field atom_phases table<string, AtomPhaseGroup> -- phase_label -> {atoms = {atom_name1, atom_name2, ...}} (atom_phase(...) tags)
--- @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""
--- @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 DebugSkipMarker
--- @field marker_kind string -- exact marker ident read from source. Only "atom_dbg_skip" (bare) is positive; any other ident reaches the unrelated fallback and is never associated with a declaration.
--- @field marker_line integer -- line of the marker ident start
--- @field marker_pos integer -- byte position of the marker ident start (the comment walker anchors here)
--- @field is_bare boolean -- true iff marker_kind == "atom_dbg_skip" AND has_parens == false (the only positive form)
--- @field has_parens boolean -- true iff a `(...)` follows the marker ident (diagnostic-only)
--- @field args string|nil -- trimmed args inside the `(...)` (nil when has_parens is false)
--- @field pending boolean -- true while awaiting the following declaration
--- @field superseded_by_marker_line integer|nil -- set when a newer marker bumped this one out of the pending slot
--- @field target_kind string|nil -- "atom" | "comp_bare" | "comp_proc" | "unrelated" once observed (nil if no declaration ever followed)
--- @field proc_prelude boolean|nil -- true after the marker crossed an `FI_` prelude and awaits `MipsAtomComp_Proc_`
--- @class RegTypeDefault
--- @field name string -- "R_TapePtr" (the register ident; without the value part)
@@ -96,12 +87,6 @@ local parse_enum_int_literal
--- @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
@@ -125,16 +110,16 @@ local parse_enum_int_literal
--- @field warnings table[]
--- @class AtomEntry
--- @field line integer
--- @field name string -- atom name (for components: without ac_ prefix)
--- @field body string -- brace-delimited body (without the braces)
--- @field body_off integer -- char offset of body[1] in source
--- @field kind string -- "atom" | "comp_bare" | "comp_proc" | "raw_atom"
--- @field raw_name string -- un-stripped name (for components: with ac_ prefix)
--- @field ident_pos integer -- position of the MipsAtom_/MipsAtomComp_ ident start
--- @field after_paren integer -- position past the closing paren
--- @field args string|nil -- populated by components pass (backward lookup)
--- @field comment string|nil -- populated by components pass (backward lookup)
--- @field line integer
--- @field name string -- atom name (for components: without ac_ prefix)
--- @field body string -- brace-delimited body (without the braces)
--- @field body_off integer -- char offset of body[1] in source
--- @field kind string -- "atom" | "comp_bare" | "comp_proc" | "raw_atom"
--- @field raw_name string -- un-stripped name (for components: with ac_ prefix)
--- @field ident_pos integer -- position of the MipsAtom_/MipsAtomComp_ ident start
--- @field after_paren integer -- position past the closing paren
--- @field debug_skip boolean -- true when an `atom_dbg_skip` bare marker immediately precedes this declaration (sole-owner stamp; see push_debug_skip_marker)
--- @field declaration_comment string|nil -- populated by the scanner (backward walk past the marker, captures contiguous `/* */` or `//` block)
-- ════════════════════════════════════════════════════════════════════════════
-- Local helpers (shared by per-form parsers)
@@ -166,8 +151,10 @@ local function strip_ac_prefix(raw_name)
end
-- Preserve a source marker until the following declaration parser observes it.
local function push_skip_over_marker(out, marker)
local markers = out.skip_over.markers
-- The scanner is the sole owner of marker recognition, placement association, declaration comment attachment, and canonical `debug_skip` fields.
-- Raw marker evidence lives in `out.debug_skip_markers` for annotation validation; the declaration record carries the resolved `debug_skip` boolean directly.
local function push_debug_skip_marker(out, marker)
local markers = out.debug_skip_markers
local prior = markers[#markers]
if prior and prior.pending then
prior.pending = false
@@ -197,55 +184,150 @@ local function find_body_braces(source, after_paren, fallback)
return body, after_brace, brace + 1
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
-- Walk backward from `start_pos` capturing contiguous `/* */` block(s) and
-- `//` line(s) that immediately precede it. The caller (preceding_declaration_comment)
-- supplies `start_pos` so the walker does not need to detect marker shape or prelude layout.
-- The scanner already knows the marker_pos + decl ident_pos and threads that knowledge forward.
--
-- The walker captures:
-- - Block comment close `*/` followed by walking back to `/*`.
-- - `//` line comments (the line containing the current non-ws position starts with `//`).
-- It stops at the first non-ws char that does not begin a comment block or line.
-- Empty string if no comment is adjacent.
-- @param source string
-- @param start_pos integer -- exclusive upper bound for the captured block
-- @return string
local function preceding_comment_walk_backward(source, start_pos)
local pieces = {}
local scan_pos = start_pos
while scan_pos > 0 do
local non_ws = scan_pos - 1
while non_ws > 0 do
local ch = source:sub(non_ws, non_ws)
if ch == " " or ch == "\t" or ch == "\n" or ch == "\r" then
non_ws = non_ws - 1
else
break
end
end
if non_ws == 0 then break 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
if non_ws >= 2 and source:sub(non_ws - 1, non_ws) == "*/" then
-- Block comment close: walk back over `/*` candidates.
local prefix = source:sub(1, non_ws - 1)
local open_at = nil
for scan = #prefix - 1, 1, -1 do
if prefix:sub(scan, scan + 1) == "/*" then
open_at = scan
break
end
end
if not open_at then break end
local block_start = open_at
while block_start > 1 do
local ch = source:sub(block_start - 1, block_start - 1)
if ch ~= " " and ch ~= "\t" then break end
block_start = block_start - 1
end
table.insert(pieces, 1, source:sub(block_start, non_ws))
scan_pos = block_start
else
-- Line comment check: walk back from non_ws to the most recent `\n`
-- (or position 1) and inspect the resulting line. This handles both
-- `// foo\n<marker>` (non_ws ends on `o`) and `// foo\r\n<marker>`.
local line_start = non_ws
while line_start > 1 and source:sub(line_start - 1, line_start - 1) ~= "\n" do
line_start = line_start - 1
end
local line = source:sub(line_start, non_ws)
if line:sub(1, 2) ~= "//" then break end
table.insert(pieces, 1, line)
scan_pos = line_start - 1
end
end
if #pieces == 0 then return "" end
return table.concat(pieces, "\n")
end
-- Register a parsed atom entry in `out.atoms` and link its skip-over marker.
-- Captures the shared 8-field shape used by MipsAtom_, MipsAtomComp_, MipsAtomComp_Proc_.
local function register_atom(out, kind, declaration_line, name, body, body_off, raw_name, pos, after_paren)
-- Resolve the start position for the declaration-comment walk.
-- When a debug-skip marker is pending, the walker must start from the position immediately before the marker ident
-- (so it walks backward past the marker text and any `FI_ MipsAtom ac_X(args)` proc-prelude layout — neither of which is visible if we start from the declaration ident_pos).
-- When no marker is pending, the walker starts from the declaration ident_pos directly.
-- @param pending_marker DebugSkipMarker|nil
-- @param ident_pos integer -- declaration ident position
-- @return integer
local function comment_walk_start(pending_marker, ident_pos)
if pending_marker then
return pending_marker.marker_pos - 1
end
return ident_pos - 1
end
-- Attach the pending marker to the next declaration.
-- The declaration form disambiguates whole atoms from components; the resolved `debug_skip` is stamped directly on the declaration record
-- (sole-owner discipline; see push_debug_skip_marker).
--
-- A marker is POSITIVE (stamps `debug_skip = true` on the declaration) iff:
-- marker_kind == "atom_dbg_skip" AND is_bare == true
-- Any other spelling or shape (parenthesized form, legacy name) is recorded as a raw marker for annotation validation but never stamps `debug_skip`.
-- @param out SourceScan
-- @param target_kind string|nil -- "atom" | "comp_bare" | "comp_proc" | "unrelated" once observed
-- @return boolean|nil -- true iff the marker is the positive bare form
local function attach_debug_skip_marker(out, target_kind)
local markers = out.debug_skip_markers
local marker = markers[#markers]
if not (marker and marker.pending) then return nil end
marker.pending = false
marker.target_kind = target_kind
if marker.marker_kind == "atom_dbg_skip" and marker.is_bare then
return true
end
return nil
end
-- Register a parsed atom entry in `out.atoms`. Stamps the resolved `debug_skip` boolean
-- on the record when a positive bare `atom_dbg_skip` marker is pending.
-- Captures the shared shape used by MipsAtom_, MipsAtomComp_, MipsAtomComp_Proc_.
local function register_atom(out, kind, declaration_line, name, body, body_off, raw_name, pos, after_paren, source)
-- Capture the pending marker BEFORE attaching so the walker can anchor the backward comment walk on the marker's marker_pos
-- (which is the correct anchor even when an `FI_ MipsAtom ac_X(args)` proc-prelude separates the marker from the declaration).
local pending_marker = nil
local markers = out.debug_skip_markers
local m = markers[#markers]
if m and m.pending then pending_marker = m end
local positive = attach_debug_skip_marker(out, kind)
local comment = ""
if kind == "comp_bare" or kind == "comp_proc" then
-- Scanner-owned declaration-comment attachment.
-- The walker does not need to detect marker shape.
-- A pending_marker record (or the declaration ident_pos fallback) supplies the anchor position.
local start_pos = comment_walk_start(pending_marker, pos)
comment = preceding_comment_walk_backward(source, start_pos)
end
out.atoms[#out.atoms + 1] = {
line = declaration_line, name = name, body = body, body_off = body_off,
kind = kind, raw_name = raw_name,
ident_pos = pos, after_paren = after_paren,
line = declaration_line,
name = name,
body = body,
body_off = body_off,
kind = kind,
raw_name = raw_name,
ident_pos = pos,
after_paren = after_paren,
debug_skip = positive == true,
declaration_comment = comment,
}
associate_skip_over_marker(out, name, raw_name, kind, declaration_line, pos)
end
-- Register a parsed raw-atom entry in `out.raw_atoms` and link its skip-over marker.
-- Register a parsed raw-atom entry in `out.raw_atoms`.
-- Captures the 5-field shape used by MipsCode (the raw-atom form; offsets pass only).
local function register_raw_atom(out, declaration_line, name, body, body_off, raw_name, pos, marker_kind)
local function register_raw_atom(out, declaration_line, name, body, body_off, raw_name, pos)
out.raw_atoms[#out.raw_atoms + 1] = {
line = declaration_line, name = name, body = body, body_off = body_off,
kind = "raw_atom", raw_name = raw_name,
}
associate_skip_over_marker(out, name, raw_name, marker_kind, declaration_line, pos)
end
-- Parse a `Type*` chain (zero or more `*` separated by optional whitespace) followed by the type ident.
@@ -354,7 +436,7 @@ end
-- Parse the `Enum_(<underlying>, <name>) { <body> }` body for entries.
-- Captures one field per named enumerator with the shape { name, value }.
-- The value is the integer literal parsed from the source via the canonical `parse_enum_int_literal`.
-- The value is the integer literal parsed from the source via `parse_enum_int_literal`.
local function parse_enum_body_fields(body)
return walk_body_fields(body, function(entry_name, name_end, after_name)
local value
@@ -652,20 +734,13 @@ local function scan_atom_info_subcalls(info_inner, info_line)
}
end
local SUBCALL_HANDLERS = {
-- scan: atom_bind(<Binds_X>)
atom_bind = function(sub_inner) binds = duffle.trim(sub_inner) end,
-- scan: atom_reads(<R_X [atom_type(<T>)], ...>)
atom_reads = function(sub_inner, info_line) rw_handler(sub_inner, info_line, "atom_reads") end,
-- scan: atom_writes(<R_X [atom_type(<T>)], ...>)
atom_writes = function(sub_inner, info_line) rw_handler(sub_inner, info_line, "atom_writes") end,
-- scan: atom_view(<Binds_X>)
atom_view = function(sub_inner) view_binds = duffle.trim(sub_inner) end,
-- scan: atom_reg_types(<R_X>, <T>)
atom_reg_types = reg_types_handler,
-- scan: atom_ctx(<atom_name>)
atom_ctx = function(sub_inner, info_line) ident_handler(sub_inner, info_line, "ctx_atom_name") end,
-- scan: atom_phase(<label>)
atom_phase = function(sub_inner, info_line) ident_handler(sub_inner, info_line, "phase_label") end,
atom_bind = function(sub_inner) binds = duffle.trim(sub_inner) end, -- scan: atom_bind(<Binds_X>)
atom_reads = function(sub_inner, info_line) rw_handler(sub_inner, info_line, "atom_reads") end, -- scan: atom_reads(<R_X [atom_type(<T>)], ...>)
atom_writes = function(sub_inner, info_line) rw_handler(sub_inner, info_line, "atom_writes") end, -- scan: atom_writes(<R_X [atom_type(<T>)], ...>)
atom_view = function(sub_inner) view_binds = duffle.trim(sub_inner) end, -- scan: atom_view(<Binds_X>)
atom_reg_types = reg_types_handler, -- scan: atom_reg_types(<R_X>, <T>)
atom_ctx = function(sub_inner, info_line) ident_handler(sub_inner, info_line, "ctx_atom_name") end, -- scan: atom_ctx(<atom_name>)
atom_phase = function(sub_inner, info_line) ident_handler(sub_inner, info_line, "phase_label") end, -- scan: atom_phase(<label>)
}
local sub_pos = 1
@@ -1006,7 +1081,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.skip_over)
-- out -- the SourceScan out table (mutated in place: out.atoms / out.raw_atoms / out.binds / out.atom_infos / out.macros / out.debug_skip_markers)
-- 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).
@@ -1015,34 +1090,45 @@ 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.
--- The marker_kind is the source ident itself (e.g. `atom_dbg_skip_over`).
--- The dispatch table maps each ident to this same function;
--- the marker_kind is derived from the source so future idents route through the same row.
--- Parse a `atom_dbg_skip` marker and record its raw placement evidence.
---
--- Positive path: the BARE form (`atom_dbg_skip` followed by whitespace + a supported declaration)
--- stamps the `debug_skip` field on the immediately-following declaration record via `attach_debug_skip_marker`. `is_bare`
--- is set true only when `marker_kind == "atom_dbg_skip"` and there are no parens.
---
--- Diagnostic-only path: a following `(...)` is recorded as an invalid parenthesized-form marker so the annotation rule can emit a precise "parenthesized form" diagnostic.
--- The parenthesized form stays diagnostic; the bare form alone carries the runtime stamp.
--- @param source string
--- @param pos integer
--- @param ident_end integer
--- @param line_of fun(pos: integer): integer
--- @param out SourceScan
--- @return integer
local function parse_skip_over_marker(source, pos, ident_end, line_of, out)
local open_paren = duffle.skip_ws_and_cmt(source, ident_end)
local marker = {
marker_kind = source:sub(pos, ident_end - 1),
marker_line = line_of(pos),
marker_pos = pos,
after_paren = ident_end,
args = nil,
has_parens = false,
}
--- @return integer -- source cursor position to resume from
local function parse_dbg_skip_marker(source, pos, ident_end, line_of, out)
local marker_kind = source:sub(pos, ident_end - 1)
-- Diagnostic-only detection of an invalid following `(...)`.
-- The cursor is advanced past the `()` either way to keep token order coherent for the next scan iteration.
local marker_end = ident_end
local open_paren = duffle.skip_ws_and_cmt(source, ident_end)
local has_parens = false
local args = nil
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
marker_end = after_paren
has_parens = true
args = duffle.trim(inner)
end
push_skip_over_marker(out, marker)
return marker.after_paren
push_debug_skip_marker(out, {
marker_kind = marker_kind,
marker_line = line_of(pos),
marker_pos = pos,
is_bare = (marker_kind == "atom_dbg_skip") and (not has_parens),
has_parens = has_parens,
args = args,
})
return marker_end
end
-- Parse `atom_dbg_reg_default(R_X, <type>...)`;
@@ -1138,7 +1224,7 @@ local function parse_mips_atom(source, pos, ident_end, line_of, out)
local body, after_brace, body_off = find_body_braces(source, brace_search_pos, open_paren + 1)
if not body then return after_brace end
if raw_name and raw_name ~= "" then
register_atom(out, "atom", line_of(pos), raw_name, body, body_off, raw_name, pos, after_paren)
register_atom(out, "atom", line_of(pos), raw_name, body, body_off, raw_name, pos, after_paren, source)
end
return after_brace
@@ -1161,7 +1247,7 @@ local function parse_mips_atom_comp(source, pos, ident_end, line_of, out)
local body, after_brace, body_off = find_body_braces(source, after_paren, open_paren + 1)
if not body then return after_brace end
local name = strip_ac_prefix(raw_name)
register_atom(out, "comp_bare", line_of(pos), name, body, body_off, raw_name, pos, after_paren)
register_atom(out, "comp_bare", line_of(pos), name, body, body_off, raw_name, pos, after_paren, source)
return after_brace
end
@@ -1195,7 +1281,7 @@ local function parse_mips_atom_comp_proc(source, pos, ident_end, line_of, out)
-- 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
register_atom(out, "comp_proc", line_of(pos), name, body, body_off, raw_name, pos, after_paren)
register_atom(out, "comp_proc", line_of(pos), name, body, body_off, raw_name, pos, after_paren, source)
return after_paren
end
@@ -1217,7 +1303,7 @@ local function parse_mips_code(source, pos, ident_end, line_of, out)
local atom_name = next_ident:sub(6)
local body, after_brace, body_off = find_body_braces(source, next_after, ident_end)
if not body then return after_brace end
register_raw_atom(out, line_of(pos), atom_name, body, body_off, atom_name, pos, "unrelated")
register_raw_atom(out, line_of(pos), atom_name, body, body_off, atom_name, pos)
return after_brace
end
@@ -1316,7 +1402,7 @@ end
--- 4. `typedef <type> TSet_(<name>);` duffle TSet_ convention.
--- Strips TSet_ wrapper; adds to type_name_registry (kind="typedef") with underlying_type=<type>.
---
--- All four shapes also associate an "unrelated" skip-over marker (the existing behavior — typedef declarations don't carry atom_dbg_skip_over).
--- All four shapes also attach an "unrelated" debug-skip marker (the existing behavior — typedef declarations don't carry atom_dbg_skip).
--- @param source string
--- @param pos integer
--- @param ident_end integer
@@ -1337,7 +1423,7 @@ local function parse_typedef_binds(source, pos, ident_end, line_of, out)
local body, after_brace = find_body_braces(source, after_paren, open_paren + 1)
if not body then return after_brace end
register_struct_type(body, name, pos, line_of, out)
associate_skip_over_marker(out, name, name, "unrelated", line_of(pos), pos)
attach_debug_skip_marker(out, "unrelated")
return after_brace
-- ── Shape 2: `typedef Enum_(<underlying>, <name>) { <body> } <alias>;`
@@ -1353,7 +1439,7 @@ local function parse_typedef_binds(source, pos, ident_end, line_of, out)
local body, after_brace = find_body_braces(source, after_paren, open_paren + 1)
if not body then return after_brace end
register_enum_type(underlying, name, body, pos, line_of, out)
associate_skip_over_marker(out, name, name, "unrelated", line_of(pos), pos)
attach_debug_skip_marker(out, "unrelated")
return after_brace
end
@@ -1389,7 +1475,7 @@ local function parse_typedef_binds(source, pos, ident_end, line_of, out)
-- Empty underlying span is acceptable; the TSet_ wrapper itself
-- encodes the alias identity (per the duffle TSet_ convention).
register_typedef_alias("", tset_name, pos, line_of, out)
associate_skip_over_marker(out, tset_name, tset_name, "unrelated", line_of(pos), pos)
attach_debug_skip_marker(out, "unrelated")
return after_paren
end
@@ -1432,7 +1518,7 @@ local function parse_typedef_binds(source, pos, ident_end, line_of, out)
local underlying_span = source:sub(after_typedef, tset_pos - 1)
local underlying = duffle.trim(underlying_span)
register_typedef_alias(underlying, tset_arg, pos, line_of, out)
associate_skip_over_marker(out, tset_arg, tset_arg, "unrelated", line_of(pos), pos)
attach_debug_skip_marker(out, "unrelated")
return tset_arg_end or (semi_pos + 1)
end
@@ -1441,7 +1527,7 @@ local function parse_typedef_binds(source, pos, ident_end, line_of, out)
local underlying_span = source:sub(after_typedef, last_ident_pos - 1)
local underlying = duffle.trim(underlying_span)
register_typedef_alias(underlying, last_ident, pos, line_of, out)
associate_skip_over_marker(out, last_ident, last_ident, "unrelated", line_of(pos), pos)
attach_debug_skip_marker(out, "unrelated")
return last_ident_end
end
@@ -1629,7 +1715,9 @@ local DECL_PARSERS = {
MipsAtom_ = parse_mips_atom,
MipsAtomComp_ = parse_mips_atom_comp,
MipsAtomComp_Proc_ = parse_mips_atom_comp_proc,
atom_dbg_skip_over = parse_skip_over_marker,
-- `atom_dbg_skip` is the only debug-skip parser entry. Every other
-- identifier follows the ordinary unrelated-token path; there is no alias.
atom_dbg_skip = parse_dbg_skip_marker,
atom_dbg_reg_default = parse_atom_dbg_reg_default,
MipsCode = parse_mips_code,
typedef = parse_typedef_binds,
@@ -1638,6 +1726,9 @@ local DECL_PARSERS = {
enum = parse_enum,
}
-- Only the bare `atom_dbg_skip` marker reaches `parse_dbg_skip_marker`.
-- Unknown identifiers follow the same unrelated-token path as every other unsupported source token.
-- ════════════════════════════════════════════════════════════════════════════
-- The single source walker
-- ════════════════════════════════════════════════════════════════════════════
@@ -1648,33 +1739,31 @@ local DECL_PARSERS = {
--- @param source_file string|nil -- absolute source path (forwarded into AliasEntry.source_file)
--- @param code_macros table|nil -- cross-source `R_*_Code` registry; nil = local-only
--- @param code_macro_bodies table|nil -- cross-source raw RHS body table; nil = local-only
--- @return table -- SourceScan { atoms, raw_atoms, binds, atom_infos, macros, skip_over, line_of, register_alias_registry, _code_macros, _code_macro_bodies }
--- @return table -- SourceScan { atoms, raw_atoms, binds, atom_infos, macros, debug_skip_markers, line_of, register_alias_registry, _code_macros, _code_macro_bodies }
local function scan_source(source, source_file, code_macros, code_macro_bodies)
local line_of = duffle.LineIndex(source)
local out = {
atoms = {},
raw_atoms = {},
binds = {},
atom_infos = {},
macros = {},
skip_over = {
atoms = {},
components = {},
markers = {},
},
types = {},
atom_views = {},
line_of = line_of,
atoms = {},
raw_atoms = {},
binds = {},
atom_infos = {},
macros = {},
-- Raw marker evidence for annotation validation. The `debug_skip` boolean
-- is stamped on the declaration record itself; the projection lives on AtomEntry.debug_skip.
debug_skip_markers = {},
types = {},
atom_views = {},
line_of = line_of,
-- Source-derived register-alias registry (atom_reg opt-in entries).
-- Keys are full R_* idents (never stripped); see parse_enum / parse_enum_body.
register_alias_registry = {},
-- Source-derived type-name registry.
-- Populated from `typedef Struct_(...)`, `typedef Enum_(...)`, `typedef <type> <alias>`, and `typedef <type> TSet_(<name>)` declarations.
-- The propagation pass at the end of `scan_source()` resolves byte_size via the builtin map,
-- Source-derived type-name registry.
-- Populated from `typedef Struct_(...)`, `typedef Enum_(...)`, `typedef <type> <alias>`, and `typedef <type> TSet_(<name>)` declarations.
-- The propagation pass at the end of `scan_source()` resolves byte_size via the builtin map,
-- typedef chain walking (cycle-guarded, depth <= 8), and struct field sums.
-- See `propagate_type_sizes()` below.
type_name_registry = {},
-- Shared `R_*_Code -> integer code` registry
-- Shared `R_*_Code -> integer code` registry
-- (passed in from M.run pass 1; same reference so preprocessor intercept writes are visible to the enum-value resolver).
-- Stripped from `src.scan` before return.
_code_macros = code_macros or {},
@@ -1708,26 +1797,27 @@ local function scan_source(source, source_file, code_macros, code_macro_bodies)
if parser then
pos = parser(source, pos, ident_end, line_of, out)
else
-- 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
-- Unsupported identifiers follow the unrelated-token path. If a
-- pending marker is still open, consume it so it cannot drift to a
-- later declaration. Unsupported identifiers never create marker records.
local markers = out.debug_skip_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)
attach_debug_skip_marker(out, "unrelated")
end
end
pos = ident_end
end
else
local markers = out.skip_over.markers
local markers = out.debug_skip_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)
attach_debug_skip_marker(out, "unrelated")
end
end
pos = pos + 1
@@ -1748,8 +1838,8 @@ end
-- Corpus merge — first-wins lookup identity + typed collisions
-- ════════════════════════════════════════════════════════════════════════════
-- These helpers run ONCE per `M.run` invocation, after every per-source scan has attached `src.scan`.
-- They merge per-source scans into the canonical `ctx.shared.corpus.*` registries.
-- The corpus is the source of truth; `src.scan` keeps the source-local projection for the duration of the run but the cross-source visibility lives on `corpus`.
-- They merge per-source scans into the `ctx.shared.corpus.*` registries.
-- `src.scan` keeps the source-local projection for the duration of the run; the cross-source visibility lives on `corpus`.
-- Build a deterministic site record (path + line) from a per-source entry.
-- Falls back to the placeholder when an entry lacks a recorded source file or line.
@@ -1858,8 +1948,8 @@ local function phase_shape(entry)
end
-- Merge a new declaration site into a registry following the first-wins discipline.
-- * first declaration: Entry becomes the canonical corpus entry (entry.sites initialized).
-- * identical subsequent: Append the new site to entry.sites (no collision).
-- * first declaration: Entry becomes the corpus entry (entry.sites initialized).
-- * identical subsequent: Append the new site to entry.sites.
-- * conflicting shape: Keep first entry, append ONE typed collision record with shape diff.
local function merge_named_with_sites(registry, name, new_entry, site, collisions, kind, shape_fn)
if registry[name] == nil then
@@ -1888,8 +1978,8 @@ local function merge_named_with_sites(registry, name, new_entry, site, collision
}
end
-- Merge per-source scans into the canonical corpus registries.
-- Iterates `corpus.source_order` (not `ctx.sources`) — the corpus is the source of truth.
-- Merge per-source scans into the corpus registries.
-- Iterates `corpus.source_order` (not `ctx.sources`).
-- Each source owns only its `src.scan`; the corpus owns the cross-source lookup tables.
local function merge_corpus_registries(corpus)
-- Ensure every expected corpus table exists (the fixture_ctx seeds most of these,
@@ -2002,8 +2092,7 @@ local M = {}
--- No output files; this is a pure in-memory pre-processing pass.
---
--- Runs in 5 phases.
--- Resolve: Resolve the canonical source order from `ctx.shared.corpus.source_order`.
--- The canonical corpus is the SOLE source of truth; no `ctx.sources` alias is consulted and no per-source fallback synthesis is performed.
--- Resolve: Source order from `ctx.shared.corpus.source_order` (the corpus owns it; the check below enforces the invariant).
--- Pass 1a: `scan_source_pre_pass` over every source, populating LOCAL `code_macros` AND LOCAL `code_macro_bodies` tables.
--- The bodies table holds the raw post-`=` text of every `#define R_*_Code` line (cross-source)
--- so the chain walker can fall back when the defining `#define` lives in a different source than the chain call site.
@@ -2012,8 +2101,8 @@ local M = {}
--- Pass 2: The full `scan_source(source, source_file, code_macros, code_macro_bodies)` walk per source. The per-source `src.scan` payload includes the source-local registries
--- (register_alias_registry, type_name_registry, atom_views, atom_ctxs, atom_phases, binds, atoms, atom_infos, ...).
--- Strip: Strip `src.scan._code_macros`, `src.scan._code_macro_bodies`, and the `_source_file` pointer.
--- The LOCAL tables `code_macros` and `code_macro_bodies` go out of scope here; they MUST NOT appear on `ctx.shared`, `ctx.shared.corpus`, or any `src.scan` after this point.
--- Merge: Iterate `ctx.shared.corpus.source_order` in declared order. For every source's local registry, first-wins lookup identity (entry from the first declaration site becomes the canonical corpus entry);
--- The LOCAL tables `code_macros` and `code_macro_bodies` stay confined to this function; they go out of scope on return.
--- Merge: Iterate `ctx.shared.corpus.source_order` in declared order. For every source's local registry, first-wins lookup identity (entry from the first declaration site becomes the corpus entry);
--- identical shapes coalesce by appending the declaration site; conflicting shapes keep the first lookup entry and append ONE typed collision record with shape diff.
--- Populate `register_alias_registry`, `type_name_registry`, `binds_by_name`, `atoms_by_name`, `atom_views`, `atom_ctxs`, `atom_phases`.
--- `atom_infos` ALWAYS appends every record (preserving source order + duplicates for annotation evidence).
@@ -2022,14 +2111,12 @@ local M = {}
--- @return PassResult
function M.run(ctx)
-- The cross-source _code_macros / _code_macro_bodies tables are LOCAL to this run.
-- They are shared across source scans ONLY long enough to resolve cross-source R_*_Code chains, then DISCARDED.
-- They MUST NOT appear on ctx.shared, ctx.shared.corpus, or any src.scan after
-- this function returns.
-- They live across source scans only long enough to resolve cross-source R_*_Code chains, then go out of scope on M.run return.
-- The Lua GC reclaims them; nothing here survives onto ctx.shared, ctx.shared.corpus, or any src.scan.
local code_macros = {}
local code_macro_bodies = {}
-- Resolve the canonical source list. The corpus owns the authoritative source_order.
-- A context without `ctx.shared.corpus` is rejected with an explicit canonical-corpus error.
-- Canonical-corpus check (see the docstring Resolve phase). The corpus is the only source of source_order.
ctx.shared = ctx.shared or {}
local corpus = ctx.shared.corpus
if not corpus or type(corpus.source_order) ~= "table" then
@@ -2069,20 +2156,16 @@ function M.run(ctx)
src.scan._code_macro_bodies = nil
src.scan._source_file = nil
end
-- Pre-tokenize each atom body once (plex: single source of truth).
-- Downstream passes (offsets, word-counts, components, static-analysis) read from `atom.body_tokens` instead of calling `split_top_level_commas` / `tokenize_body` independently.
-- The tokens are memoized in duffle.lua's cache, so re-access is O(1).
-- Pre-tokenize each atom body once (plex: cache lives in duffle.lua; downstream passes read from `atom.body_tokens` instead of calling `split_top_level_commas` / `tokenize_body` independently).
-- Re-access is O(1) thanks to the memoization.
for _, atom in ipairs(src.scan.atoms) do atom.body_tokens = duffle.tokenize_body(atom.body) end
for _, atom in ipairs(src.scan.raw_atoms or {}) do atom.body_tokens = duffle.tokenize_body(atom.body) end
end
-- Merge per-source scans into the canonical corpus registries.
-- First-wins lookup identity + collision discipline (see merge_corpus_registries).
-- The corpus is always present; no conditional / fallback path.
-- Merge per-source scans into the corpus registries (see merge_corpus_registries for first-wins + collision discipline).
merge_corpus_registries(corpus)
-- code_macros and code_macro_bodies go out of scope here; their references are not captured on corpus, ctx.shared, or any src.scan.
-- The Lua GC reclaims them on M.run return.
-- code_macros and code_macro_bodies are function-local; the GC reclaims them on M.run return.
return { outputs = {}, errors = {}, warnings = {} }
end