--- 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 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 — bare whole-atom/component debug-step marker; following declaration disambiguates --- MipsCode code_ (kind = "raw_atom", offsets pass only) --- typedef Struct_(Binds_X) { fields } --- #pragma mac_X tape_atom words=N + _Pragma("...") --- --- The result is attached to each `src.scan` so downstream passes can read from `src.scan.atoms` / `src.scan.binds` / etc. without re-walking the source. --- This is the first pass in the dep graph (no deps). --- Every other pass that reads source structure depends on this one — see `ps1_meta.lua :: PASSES`. --- --- **Conventions**: tabs (1/level), EmmyLua annotations, no regex, Lua 5.3 compatible -- Bootstrap: same as entry scripts. See `ps1_meta.lua` for the rationale. -- Bootstrap: load `scripts/duffle_paths.lua` (sets package.path + package.cpath). -- Uses `debug.getinfo` to find this file's own directory, so it works both standalone and when require'd from the orchestrator. -- Bootstrap: load `duffle_paths.lua` via `debug.getinfo(1, "S").source` (works both standalone + when required). -- duffle_paths.lua sets package.path then returns `require("duffle")` at the bottom, so the dofile value IS the duffle module. local _bootstrap_dir = debug.getinfo(1, "S").source:match("^@?(.*[/\\])") or "./" local duffle = dofile(_bootstrap_dir .. "../duffle_paths.lua") -- Forward declarations for helpers used by earlier parsers (parse_enum_body_fields needs parse_enum_int_literal; -- parse_typedef_binds needs duffle.find_byte). -- Lua local scoping rules require explicit forward declarations because locals are visible only AFTER their declaration site. -- The actual assignments happen later in this file; -- the closures captured by the early parsers resolve the upvalue at call time (Lua 5.3 / LuaJIT upvalue semantics). local parse_enum_int_literal -- ════════════════════════════════════════════════════════════════════════════ -- Type declarations -- ════════════════════════════════════════════════════════════════════════════ --- @class SourceScan --- @field atoms AtomEntry[] -- MipsAtom_ + MipsAtomComp_ + MipsAtomComp_Proc_ --- @field raw_atoms AtomEntry[] -- MipsCode code_ { 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 -- atom_dbg_reg_default(R_X, ) declarations --- @field atom_views table -- MipsAtom_(name) -> {binds_name, reg_type_overrides, info_line} --- @field atom_ctxs table -- MipsAtom_(name) -> {rbind_atom, info_line, source} (atom_ctx(...) call sites) --- @field atom_phases table -- phase_label -> {atoms = {atom_name1, atom_name2, ...}} (atom_phase(...) tags) --- @field line_of fun(pos: integer): integer -- shared LineIndex closure --- @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) --- @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 --- @field source_line integer -- line of the call site (callsite or enum-site) --- @class AtomCtxEntry --- @field rbind_atom string -- the rbind atom ident that this consumer should propagate types from --- @field info_line integer --- @field source string -- absolute path of the source file --- @class AtomPhaseGroup --- @field atoms string[] -- atom names tagged with this phase label (source-order) --- @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 -- "R_T0" -> override --- @field info_line integer -- line of the atom_info call --- @class SourceFile --- @field path string -- absolute path to the source file --- @field text string -- the full source text --- @field dir string -- the directory containing the source --- @field basename string -- filename without extension --- @field scan table -- pre-scanned SourceScan payload (set by this pass) --- @class PassCtx --- @field sources SourceFile[] --- @field metadata_path string --- @field shared table --- @field out_root string --- @field project_root string --- @field upstream table --- @field flags table --- @field verbose boolean --- @class PassResult --- @field outputs table[] --- @field errors table[] --- @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 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) -- ════════════════════════════════════════════════════════════════════════════ -- C qualifier keywords that may precede a MipsAtom_ / MipsCode declaration. -- (typedef is NOT a qualifier here — it's a separate construct (`typedef Struct_(Binds_X) { ... };`) -- and must be read as an ident so the typedef check below can match it.) local QUALIFIER_KEYWORDS = { ["static"] = true, ["const"] = true, ["volatile"] = true, ["extern"] = true, ["register"] = true, ["auto"] = true, ["inline"] = true, ["internal"] = true, ["LP_"] = true, ["global"] = true, ["gkknown"] = true, } -- "ac_" prefix length on component names (e.g., `MipsAtomComp_(ac_X, ...)`). -- The components pass strips this prefix to derive the macro name (e.g., `mac_X`). local AC_PREFIX = "ac_" local AC_PREFIX_LEN = 3 -- Strip the "ac_" prefix from a component name. -- Returns the input unchanged if it doesn't start with the prefix. -- @param raw_name string -- @return string local function strip_ac_prefix(raw_name) if #raw_name > AC_PREFIX_LEN and raw_name:sub(1, AC_PREFIX_LEN) == AC_PREFIX then return raw_name:sub(AC_PREFIX_LEN + 1) end return raw_name end -- Preserve a source marker until the following declaration parser observes it. -- 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 prior.superseded_by_marker_line = marker.marker_line end marker.pending = true markers[#markers + 1] = marker end -- Try to read `(...)` parens after `ident_end`. -- Returns (inner, after_paren, open_paren) on success, or (nil, fallback_pos) if no parens. -- `fallback_pos` defaults to `open_paren + 1` (the common "advance by 1" no-parens case). local function read_parens_after(source, ident_end, fallback) local open_paren = duffle.skip_ws_and_cmt(source, ident_end) if source:sub(open_paren, open_paren) ~= "(" then return nil, fallback or open_paren + 1 end local inner, after_paren = duffle.read_parens(source, open_paren) return inner, after_paren, open_paren end -- Find the opening `{` of a body block and read its contents. -- Returns (body, after_brace, body_off) on success, or (nil, fallback_pos) on no brace. -- `fallback_pos` defaults to `after_paren + 1` (the common "advance by 1" case). local function find_body_braces(source, after_paren, fallback) local brace = duffle.scan_to_char(source, "{", after_paren) if not brace then return nil, fallback or (after_paren + 1) end local body, after_brace = duffle.read_braces(source, brace) return body, after_brace, brace + 1 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 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` (non_ws ends on `o`) and `// foo\r\n`. 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 -- 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, debug_skip = positive == true, declaration_comment = comment, } end -- 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) 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, } 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 -- Byte-size lookup for builtin C primitives + the GCC __UINT*_TYPE__ family. -- Returns a confident byte_size (positive integer) or nil if `type_name` is not a known builtin primitive. -- Builtin primitive map; used by the byte-size propagation pass to seed confident byte_size values for typedef chains that bottom out at a builtin. local BUILTIN_BYTE_SIZES = { ["U1"] = 1, ["U2"] = 2, ["U4"] = 4, ["S1"] = 1, ["S2"] = 2, ["S4"] = 4, -- GCC __UINT*/__INT*_TYPE__ family (used by the duffle TSet_ convention in dsl.h). -- MIPS32 has no 64-bit types; __UINT64_TYPE__/__INT64_TYPE__ are excluded. ["__UINT8_TYPE__"] = 1, ["__UINT16_TYPE__"] = 2, ["__UINT32_TYPE__"] = 4, ["__INT8_TYPE__"] = 1, ["__INT16_TYPE__"] = 2, ["__INT32_TYPE__"] = 4, } -- Pointer fields collapse to 4 bytes on MIPS32 (PS1). local POINTER_BYTE_SIZE = 4 -- Maximum chain depth when resolving typedef / TSet_ chains (cycle guard). local TYPE_CHAIN_MAX_DEPTH = 8 --- Walk a `Struct_` / `Enum_` body, calling `build_field(first, first_end, after_first)` for each entry. --- The builder returns either: --- - (record, new_pos) -- append record to fields; advance body_pos to new_pos --- - (nil, new_pos) -- skip this entry; advance body_pos to new_pos --- After each entry, the walker skips a single trailing `,` or `;`. --- The 2 body-field parsers in this file (struct + enum) share this body-walk loop. --- @param body string --- @param build_field fun(first: string, first_end: integer, after_first: integer): (table|nil, integer) --- @return table[] local function walk_body_fields(body, build_field) local fields = {} local body_pos = 1 local body_len = #body while body_pos <= body_len do body_pos = duffle.skip_ws_and_cmt(body, body_pos) if body_pos > body_len then break end local first, first_end = duffle.read_ident(body, body_pos) if not first then body_pos = body_pos + 1 else local after_first = duffle.skip_ws_and_cmt(body, first_end) local result, new_pos = build_field(first, first_end, after_first) if result then fields[#fields + 1] = result end body_pos = new_pos or first_end -- Skip a single trailing `,` or `;`. if body_pos <= body_len and (body:sub(body_pos, body_pos) == "," or body:sub(body_pos, body_pos) == ";") then body_pos = body_pos + 1 end end end return fields end -- Parse the ` ;` declarations from a Struct_ body. -- Returns the raw fields array with `{name, type_name, pointer_depth}` only (NO offset / byte_size). -- The propagation pass `resolve_struct_field_sizes` walks each struct's fields AFTER type resolution and populates offset + byte_size in place. -- Returns (fields). The aggregate byte_count is computed in the propagation pass (it depends on whether every field's type resolved). local function parse_struct_body_fields(body) return walk_body_fields(body, function(type_name, type_end, after_type) -- Parse the trailing `*` chain to derive pointer_depth. local depth, cursor = 0, after_type while cursor <= #body and body:sub(cursor, cursor) == "*" do depth = depth + 1 cursor = cursor + 1 cursor = duffle.skip_ws_and_cmt(body, cursor) end -- Read the field ident immediately after the type chain. local field_ident, field_end = duffle.read_ident(body, cursor) if not field_ident then return nil, type_end + 1 end return { name = field_ident, type_name = type_name, pointer_depth = depth, -- offset + byte_size filled by resolve_struct_field_sizes offset = nil, byte_size = nil, }, field_end end) end -- Parse the `Enum_(, ) { }` 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 `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 local new_pos if body:sub(after_name, after_name) == "=" then local val_pos = duffle.skip_ws_and_cmt(body, after_name + 1) local v, end_pos = parse_enum_int_literal(body, val_pos) if v ~= nil then value = v new_pos = end_pos else new_pos = after_name + 1 end else new_pos = name_end end return { name = entry_name, value = value }, new_pos end) end -- Resolve a typedef chain's `byte_size` via repeated underlying_type walks. -- Returns a positive integer byte_size when the chain bottoms out at a builtin, or nil if the chain is broken, exceeds TYPE_CHAIN_MAX_DEPTH, or contains a cycle. local function resolve_typedef_byte_size(type_name, type_name_registry, visited, depth) if depth > TYPE_CHAIN_MAX_DEPTH then return nil end if visited[type_name] then return nil end visited[type_name] = true -- Check the builtin primitive map FIRST. -- This handles undeclared builtin idents (e.g. `__UINT32_TYPE__` appears as underlying_type in `typedef __UINT32_TYPE__ TSet_(V4_S2);` -- even though the fixture never declares `__UINT32_TYPE__` itself). local builtin = BUILTIN_BYTE_SIZES[type_name] if builtin ~= nil then return builtin end local entry = type_name_registry[type_name] if not entry then return nil end -- Confident: this entry was already resolved by the propagation pass (e.g., a builtin or a struct whose fields are all resolved). if entry.byte_size ~= nil then return entry.byte_size end -- Chain-following: typedef / TSet_ aliases follow underlying_type. if entry.underlying_type then return resolve_typedef_byte_size(entry.underlying_type, type_name_registry, visited, depth + 1) end -- Struct_ entries with unresolved byte_size can still resolve when their fields are all resolved. if entry.kind == "struct" and entry.fields then local sum = 0 local all_have = true for _, f in ipairs(entry.fields) do if f.byte_size == nil then all_have = false; break end sum = sum + f.byte_size end if all_have and #entry.fields > 0 then return sum end end return nil end -- Propagation pass: resolve every entry's `byte_size` field by walking typedef chains, struct field sums, and the builtin primitive map. -- Builtins seed confident values; chains follow underlying_type recursively with a per-chain cycle guard (depth <= 8); -- struct byte_size derives from confident fields; void is invalid and skipped; pointers collapse to 4 bytes at parse time. -- Mutates `out.type_name_registry[name].byte_size` AND each struct's fields' `offset` + `byte_size` in place. local function propagate_type_sizes(out) local reg = out.type_name_registry if not reg then return end -- Seed builtin primitives (U1/U2/U4/S1/S2/S4 + __UINT*_TYPE__ family). for name, size in pairs(BUILTIN_BYTE_SIZES) do if reg[name] and reg[name].byte_size == nil then reg[name].byte_size = size end end -- Resolve typedef / TSet_ chains. Iterate to a fixed point (max TYPE_CHAIN_MAX_DEPTH iterations) -- since chains may span multiple hops and the resolution order isn't guaranteed by declaration order. -- The visited map is empty when handed to the resolver. -- The resolver marks visited as it enters each node, so the cycle guard fires only on RECURSIVE re-entry (not on the initial call). for _ = 1, TYPE_CHAIN_MAX_DEPTH do local any_change = false for name, entry in pairs(reg) do if entry.byte_size == nil then local resolved = resolve_typedef_byte_size(name, reg, {}, 1) if resolved ~= nil then entry.byte_size = resolved any_change = true end end end if not any_change then break end end -- Resolve struct field byte_sizes + offsets, then aggregate the struct's own byte_size. -- Iterate to a fixed point: struct A may reference struct B which hasn't been resolved yet on the first pass. -- Each pass updates as many fields + aggregates as possible; the loop terminates when no struct's byte_size changes between passes. for _ = 1, TYPE_CHAIN_MAX_DEPTH do local any_change = false for _, entry in pairs(reg) do if entry.kind == "struct" and entry.fields then local byte_off = 0 local gap_seen = false local sum = 0 local all_have = true for _, f in ipairs(entry.fields) do -- Resolve field byte_size (pointer / builtin / typedef chain). if f.byte_size == nil then if f.pointer_depth > 0 then f.byte_size = POINTER_BYTE_SIZE elseif BUILTIN_BYTE_SIZES[f.type_name] then f.byte_size = BUILTIN_BYTE_SIZES[f.type_name] elseif reg[f.type_name] and reg[f.type_name].byte_size ~= nil then f.byte_size = reg[f.type_name].byte_size end end -- Set offset (clean prefix rule). if not gap_seen then if f.byte_size ~= nil then f.offset = byte_off byte_off = byte_off + f.byte_size else f.offset = nil gap_seen = true end else f.offset = nil end if f.byte_size ~= nil then sum = sum + f.byte_size else all_have = false end end if all_have and #entry.fields > 0 then if entry.byte_size ~= sum then entry.byte_size = sum any_change = true end end end end if not any_change then break end end -- Mirror aggregate byte_size onto the Binds_* entries in out.binds (Binds_* structs are shared via type_name_registry fields; -- Only the aggregate bytes needs a mirror write since binds.bytes is a separate field from type_name_registry[name].byte_size). for _, bind_entry in ipairs(out.binds or {}) do local reg_entry = reg[bind_entry.name] if reg_entry then bind_entry.bytes = reg_entry.byte_size end end end -- Parse the register list from inside `atom_reads(...)` or `atom_writes(...)`. local function scan_reg_list(sub_inner) local regs = {} local sub_inner_pos = 1 while sub_inner_pos <= #sub_inner do sub_inner_pos = duffle.skip_ws_and_cmt(sub_inner, sub_inner_pos) if sub_inner_pos > #sub_inner then break end local reg_ident, reg_end = duffle.read_ident(sub_inner, sub_inner_pos) if reg_ident then regs[#regs + 1] = duffle.trim(reg_ident) sub_inner_pos = reg_end else sub_inner_pos = sub_inner_pos + 1 end if sub_inner_pos > #sub_inner then break end if sub_inner:sub(sub_inner_pos, sub_inner_pos) == "," then sub_inner_pos = sub_inner_pos + 1 end end return regs end -- Parse one entry inside `atom_reads(...)` or `atom_writes(...)`. -- Each top-level comma-separated entry is either: -- - a plain register ident: `R_FaceCursor` → reg_name only -- - register + atom_type sub-call: `R_FaceCursor atom_type(V4_S2*)` → reg_name + override entry -- Returns (reg_name, override_entry_or_nil, malformed_flag). -- On malformed `atom_type(...)` (missing close paren, trailing tokens after the close paren, empty type chain, trailing junk inside the parens like `V4_S2*()`), -- the function still returns the leading reg_name but sets `malformed_flag = true` and `override_entry = nil` -- so the caller can silently drop the override while keeping the register in the reads/writes list. local function parse_atom_info_reg_entry(entry) local pos = 1 pos = duffle.skip_ws_and_cmt(entry, pos) if pos > #entry then return nil, nil, false end local reg_name, reg_end = duffle.read_ident(entry, pos) if not reg_name then return nil, nil, false end pos = duffle.skip_ws_and_cmt(entry, reg_end) -- Plain register, no adjacent atom_type — done. if pos > #entry then return reg_name, nil, false end -- Adjacent ident must be a bare `atom_type` (word-bounded both sides). local next_ident, next_end = duffle.read_ident(entry, pos) if not next_ident or next_ident ~= "atom_type" then return reg_name, nil, false end -- Left word-boundary: `_atom_type` should NOT match `atom_type`. if pos > 1 then local prev = entry:byte(pos - 1) if duffle.is_alnum_byte(prev) then return reg_name, nil, false end end -- Right word-boundary: `atom_type_foo` should NOT match `atom_type`. if next_end <= #entry then local nxt = entry:byte(next_end) if duffle.is_alnum_byte(nxt) then return reg_name, nil, false end end -- Expect `(` immediately after `atom_type` (whitespace tolerated). pos = duffle.skip_ws_and_cmt(entry, next_end) if pos > #entry or entry:sub(pos, pos) ~= "(" then return reg_name, nil, true end local sub_inner, sub_after = duffle.read_parens(entry, pos) -- Reject any trailing tokens (including `;` / `,`) after the close paren. -- The outer caller already split on top-level commas, so the only legal terminator here is end-of-entry. local after_close = duffle.skip_ws_and_cmt(entry, sub_after) if after_close <= #entry then return reg_name, nil, true end -- Parse the type chain inside the parens; require full consumption. -- parse_type_chain returns (ident, depth, end_pos); -- We reject any non-whitespace residue past end_pos (catches `V4_S2*()` etc.). local type_name, depth, after_chain = parse_type_chain(sub_inner, 1) if not type_name then return reg_name, nil, true end local end_check = duffle.skip_ws_and_cmt(sub_inner, after_chain) if end_check <= #sub_inner then return reg_name, nil, true end return reg_name, { type_name = type_name, pointer_depth = depth }, false end -- Parse the sub-calls inside `atom_info(atom_bind(...), atom_reads(...), atom_writes(...), atom_view(...), atom_reg_types(...), atom_ctx(...), atom_phase(...))`. -- Returns (binds, reads, writes, view_binds, reg_overrides, ctx_atom_name, phase_label). -- `view_binds` is the Binds_X ident from `atom_view(Binds_X)`. -- `reg_overrides` is a {reg_name = {reg, type_name, pointer_depth, source_line}} -- table populated from `atom_reg_types(R_X, )` and `atom_type(...)` sub-entries inside `atom_reads(...)` / `atom_writes(...)`. -- `ctx_atom_name` is the rbind atom ident from `atom_ctx()` (singular; last-write-wins). -- `phase_label` is the user-authored C-ident label from `atom_phase(