Review Pass: Type annotations.

This commit is contained in:
ed
2026-08-19 23:18:32 -04:00
parent c226e8a7d3
commit 449216967b
19 changed files with 5602 additions and 429 deletions
+250 -14
View File
@@ -1,8 +1,13 @@
--- duffle_emit.lua — project_emission + decl finders.
--- @type DuffleScan
local scan = require("duffle_scan")
--- @type DuffleIsa
local isa = require("duffle_isa")
--- @type DuffleEmit
local M = {}
--- @type string, any
for k, v in pairs(scan) do M[k] = v end
--- @type string, any
for k, v in pairs(isa) do M[k] = v end
-- Section 8: Cross-source component-body index + word-event expansion
@@ -12,25 +17,55 @@ for k, v in pairs(isa) do M[k] = v end
-- built once from the pre-tokenized bodies.
--- @class ComponentBodyEntry
--- @field body_tokens table -- pre-tokenized {{tok=string, rel=integer}, ...}
--- @field body_tokens BodyToken[]
--- @field body_off integer -- byte offset of body[1] in `source`
--- @field line_of fun(pos:integer):integer -- byte-offset → 1-based line number in `source`
--- @field source string -- absolute path of the source containing the declaration
--- @field declaration integer -- 1-based line number of the MipsAtomComp_(ac_X) declaration
--- @field kind string -- "comp_bare" | "comp_proc"
--- @field arg_names string[]|nil
--- @field sub_map table<string, string>|nil -- bag: formal -> substituted operand
--- @class EmissionWalkCtx
--- @field component_index table<string, ComponentBodyEntry>
--- @field word_counts WordCounts
--- @field components table<string, ComponentDef>
--- @field reg_use_schema RegUseSchema|nil
--- @field reg_use_param string|nil
--- @field atom_name AtomName|nil
--- @field schema_name string|nil
--- @field visiting table<string, boolean>|nil -- bag: component name on DFS stack
--- @field root_call_path string|nil
--- @field root_call_line integer|nil
--- @class RegUseCtx
--- @field reg_use_schema RegUseSchema|nil
--- @field reg_use_param string|nil
--- @field atom_name AtomName|nil
--- @field schema_name string|nil
--- @class DuffleEmit
-- The cross-source component-body index is owned by the corpus (`corpus.component_body_index`, populated by `passes/components.lua`).
-- Consumers (`passes/static_analysis.lua`, `passes/emission_model.lua`) read it directly; per-pass memoization helpers stay out of scope.
-- ASCII byte constants used by split_call_args (kept local to keep Section 8 self-contained).
--- @type integer
local E_BYTE_OPEN_PAREN = 0x28
--- @type integer
local E_BYTE_OPEN_BRACE = 0x7B
--- @type integer
local E_BYTE_OPEN_BRACK = 0x5B
--- @type integer
local E_BYTE_DQUOTE = 0x22
--- @type integer
local E_BYTE_SQUOTE = 0x27
--- @type integer
local E_BYTE_COMMA = 0x2C
-- Map an open-delimiter byte to its matching close string for read_balanced.
--- @type table<integer, string> -- bag: open-delimiter byte -> close string
local E_OPEN_CLOSE = {
[E_BYTE_OPEN_PAREN] = ")",
[E_BYTE_OPEN_BRACE] = "}",
@@ -44,15 +79,22 @@ local E_OPEN_CLOSE = {
--- @param inner string
--- @return string[]
local function split_call_args(inner)
--- @type string[]
local args = {}
if not inner or inner == "" then return args end
--- @type integer
local pos = 1
--- @type integer
local len = #inner
--- @type integer
local start = 1
while pos <= len do
--- @type integer
local c = inner:byte(pos)
--- @type string|nil
local close = E_OPEN_CLOSE[c]
if close then
--- @type integer, integer
local _, after = M.read_balanced(inner, string.char(c), close, pos)
pos = after
elseif c == E_BYTE_DQUOTE or c == E_BYTE_SQUOTE then
@@ -74,17 +116,22 @@ end
--- @param tok string
--- @return string, string[]
local function token_ident_and_args(tok)
--- @type string|nil, integer
local ident, after = M.read_ident(tok, 1)
if not ident then return "?", {} end
--- @type integer
local paren_pos = M.skip_ws_and_cmt(tok, after)
if tok:sub(paren_pos, paren_pos) ~= "(" then return ident, {} end
--- @type string|nil
local inner = M.read_parens(tok, paren_pos)
if not inner then return ident, {} end
return ident, split_call_args(inner)
end
-- The macro-name prefix that marks a `mac_X(...)` component invocation.
--- @type string
local E_MAC_PREFIX = "mac_"
--- @type integer
local E_MAC_PREFIX_LEN = 4
--- Expand a body entry into the flat sequence of emitted machine-word events.
@@ -104,10 +151,10 @@ local E_MAC_PREFIX_LEN = 4
---
--- Pure: reads `body_entry` / `component_index` / `word_counts`. Memoization is the caller's responsibility.
--- Callers wanting `word_events` / `word_event_errors` precomputed for many atoms should memoize them per atom.
--- @param body_entry table -- `{body_tokens, body_off, line_of, source, declaration}` (declaration = root atom's atom.line)
--- @param component_index table -- the bare-name → ComponentBodyEntry map from M.get_component_body_index
--- @param word_counts table -- macro name → emitted-word count (from `ctx.shared.word_counts`)
--- @return WordEvent[], WordEventError[]
--- @param body_entry ComponentBodyEntry
--- @param component_index table<string, ComponentBodyEntry>
--- @param word_counts WordCounts
--- @return WordEvent[], EmitError[]
-- ════════════════════════════════════════════════════════════════════════════
-- Section 11: project_emission (per-atom emission projection)
@@ -122,12 +169,12 @@ local E_MAC_PREFIX_LEN = 4
-- word_counts table is authored-metadata + current-component count table.
--- @class EmissionProjection
--- @field items table[] -- Ordered stream of word|label|offset|invoke_begin|invoke_end
--- @field word_events table[] -- Dense view of items where kind == "word"
--- @field markers table[] -- Dense view of items where kind == "label"|"offset"
--- @field items EmissionItem[]
--- @field word_events WordEvent[]
--- @field markers EmissionMarker[]
--- @field invocations InvocationRecord[] -- dense view of items where kind == "invoke_begin"|"invoke_end"
--- @field errors table[] -- Token-resolution failures surfaced without fail-loud
--- @field warnings table[] -- Opaque warnings (e.g. unknown uncounted macro)
--- @field errors EmitError[]
--- @field warnings EmitWarning[]
--- @class InvocationRecord
--- Lives at `atom.paths.invocations[*]`. Constructed once at the single invocation-construction site
@@ -148,7 +195,7 @@ local E_MAC_PREFIX_LEN = 4
--- @field end_word integer -- 1-based items index of the `invoke_end` item (set by `emit_invoke_end`)
--- @field word_count integer -- Number of `word` items emitted between `start_word` and `end_word` (inclusive)
--- @field debug_skip boolean -- `debug_skip` stamp; true iff `corpus.components[name].debug_skip` is true at construction. Always boolean (never `nil`).
--- @field errors table[] -- Per-invocation construction errors (cycle / count_mismatch); does not include pass-level errors
--- @field errors EmitError[]
-- Internal recursive walker. The items stream holds every emitted event in order; `word_events`, `markers`,
-- `invocations`, `errors`, `warnings` are dense views / side outputs appended alongside.
@@ -166,35 +213,58 @@ local E_MAC_PREFIX_LEN = 4
-- then breaks out without recursing (the cycle entry still receives an invocation ID + paired `invoke_begin` / `invoke_end` items, so the boundary invariant is preserved).
-- * Component declared-count mismatch (declared vs. measured) is a construction error (kind = "count_mismatch"); recorded on the invocation record and pass-level errors list.
-- * Final boundary check: if any invocation is still open at end of walk, surface a "unbalanced" construction error.
--- @param root_body_entry ComponentBodyEntry
--- @param ctx_table EmissionWalkCtx
--- @return EmissionProjection
local function _project_emission_inner(root_body_entry, ctx_table)
--- @type EmissionItem[]
local items = {}
--- @type WordEvent[]
local word_events = {}
--- @type EmissionMarker[]
local markers = {}
--- @type InvocationRecord[]
local invocations = {}
--- @type EmitError[]
local errors = {}
--- @type EmitWarning[]
local warnings = {}
--- @type integer
local word_idx = 0
--- @type InvocationRecord[]
local invocation_stack = {} -- stack of currently-open invocation records
--- @type integer
local next_inv_id = 0
--- @type RegUseSchema|nil
local reg_use_schema = ctx_table.reg_use_schema
--- @type string|nil
local reg_use_param = ctx_table.reg_use_param
--- @type AtomName|nil
local atom_name = ctx_table.atom_name
--- @type table<string, boolean> -- bag: slot name -> readonly
local slot_readonly = {}
if reg_use_schema then
--- @type integer, RegUseSlot
for _, slot in ipairs(reg_use_schema.slots or {}) do
slot_readonly[slot.name] = slot.readonly == true
end
end
--- @param sub_map table<string, string>|nil
--- @param operand string|any
--- @return any
local function apply_sub(sub_map, operand)
if not (sub_map and type(operand) == "string") then return operand end
if sub_map[operand] then return sub_map[operand] end
--- @type integer|nil
local dot = operand:find(".", 1, true)
if dot then
--- @type string
local head = operand:sub(1, dot - 1)
--- @type string|nil
local mapped = sub_map[head]
if type(mapped) == "string" then
return mapped .. operand:sub(dot)
@@ -203,39 +273,65 @@ local function _project_emission_inner(root_body_entry, ctx_table)
return operand
end
--- @param operand string|any
--- @return string|nil, string|nil, string|nil
local function resolve_gpr_key(operand)
if type(operand) ~= "string" then return nil end
if operand:sub(1, 2) == "R_" then return operand end
if not (reg_use_schema and reg_use_param) then return nil end
--- @type string
local prefix = reg_use_param .. "."
if operand:sub(1, #prefix) ~= prefix then return nil end
--- @type string
local member_path = operand:sub(#prefix + 1)
--- @type string|nil
local slot = reg_use_schema.alias_to_slot[member_path]
if not slot then return nil, member_path end
return "reguse:" .. atom_name .. ":" .. slot, nil, slot
end
--- @return integer[]
local function open_invocation_ids_snapshot()
--- @type integer[]
local ids = {}
--- @type integer, InvocationRecord
for _, inv in ipairs(invocation_stack) do
ids[#ids + 1] = inv.id
end
return ids
end
--- @param encoder string
--- @param args string[]|nil
--- @param line integer
--- @param word_call_text string|nil
--- @param def_source_now string|nil
--- @param def_line_now integer|nil
--- @param immediate_call_text string|nil
--- @param root_call_text_w string|nil
--- @param sub_map table<string, string>|nil
--- @return nil
local function emit_word(encoder, args, line, word_call_text, def_source_now, def_line_now, immediate_call_text, root_call_text_w, sub_map)
--- @type integer[]
local inv_ids = open_invocation_ids_snapshot()
--- @type integer
local outermost = inv_ids[1] or 0
-- For words emitted at the root atom body, `immediate_call_text` is nil and the walker's `word_call_text` (the word's own token, e.g. "nop") becomes the effective call_text.
-- For words emitted inside a component expansion, `immediate_call_text` is the immediate outer `mac_X(...)` token text;
-- The call that triggered the body expansion we're currently walking.
--- @type string|nil
local eff_call_text = immediate_call_text or word_call_text
--- @type string|nil
local eff_root_call_text = root_call_text_w
--- @type string[]|nil
local gpr_keys = nil
if reg_use_schema or sub_map then
gpr_keys = {}
--- @type integer, string
for pos, arg in ipairs(args or {}) do
--- @type any
local effective = apply_sub(sub_map, arg)
--- @type string|nil, string|nil, string|nil
local key, unresolved, slot = resolve_gpr_key(effective)
gpr_keys[pos] = key
if unresolved then
@@ -247,8 +343,10 @@ local function _project_emission_inner(root_body_entry, ctx_table)
}
end
if key and slot and slot_readonly[slot] then
--- @type InstructionRow|nil
local row = M.instr(encoder)
if row and row.writes then
--- @type integer, integer
for _, wpos in ipairs(row.writes) do
if wpos == pos then
errors[#errors + 1] = {
@@ -266,16 +364,25 @@ local function _project_emission_inner(root_body_entry, ctx_table)
if not reg_use_schema then
gpr_keys = nil
end
--- @type InstructionRow|nil
local isa = M.instr(encoder)
--- @type string
local isa_kind = isa and isa.kind or "unknown"
--- @type integer
local nop_words = (encoder == "nop" and 1) or (encoder == "nop2" and 2) or 0
--- @type boolean
local is_yield = (encoder == "mac_yield" or encoder == "mac_yield_tail")
--- @type string|nil
local gp0_shape = type(encoder) == "string"
and encoder:match("^mac_format_([%w_]+)_color$")
or nil
--- @type boolean
local is_load = (isa_kind == "load")
--- @type boolean
local is_branch = (isa_kind == "branch")
--- @type boolean
local is_unconditional_jump = (encoder == "jump" or encoder == "call_addr")
--- @type boolean
local is_terminal_jump = (encoder == "jump_reg" or encoder == "call_reg" or encoder == "jump_link")
items[#items + 1] = {
kind = "word",
@@ -324,10 +431,21 @@ local function _project_emission_inner(root_body_entry, ctx_table)
word_idx = word_idx + 1
end
--- @param kind string
--- @param name string
--- @param target string|nil
--- @param line integer
--- @param immediate_call_text string|nil
--- @param root_call_text_w string|nil
--- @param consuming_encoder string|nil
--- @param consuming_arg_pos integer|nil
--- @return nil
local function emit_marker(kind, name, target, line,
immediate_call_text, root_call_text_w,
consuming_encoder, consuming_arg_pos)
--- @type integer[]
local inv_ids = open_invocation_ids_snapshot()
--- @type integer
local outermost = inv_ids[1] or 0
-- Markers carry the open invocation stack snapshot. `call_text` / `root_call_text` belong to words, not markers — markers are zero-width and skip per-word call-site attribution.
-- `consuming_encoder` + `consuming_arg_pos` carry the surrounding control-transfer instruction context
@@ -338,6 +456,7 @@ local function _project_emission_inner(root_body_entry, ctx_table)
if kind == "offset" and (consuming_encoder == nil or consuming_encoder == "") then
return
end
--- @type EmissionItem
local it = {
kind = kind,
name = name,
@@ -364,21 +483,32 @@ local function _project_emission_inner(root_body_entry, ctx_table)
-- Count top-level commas in `tok` between position `from_pos` (inclusive) and `to_pos` (exclusive).
-- Tracks paren depth so commas inside nested () don't count. Skips string literals + comments.
-- Used by `emit_embedded_markers` to compute `consuming_arg_pos` for each embedded marker.
--- @param tok string
--- @param from_pos integer
--- @param to_pos integer
--- @return integer
local function count_top_level_commas(tok, from_pos, to_pos)
--- @type integer
local depth = 0
--- @type integer
local count = 0
--- @type integer
local i = from_pos
while i < to_pos do
--- @type string
local c = tok:sub(i, i)
if c == "'" or c == '"' then
--- @type integer
local next_pos = M.skip_str_or_cmt(tok, i)
i = (next_pos > i) and next_pos or (i + 1)
elseif c == "/" and tok:sub(i + 1, i + 1) == "/" then
-- line comment: skip to end of line
--- @type integer|nil
local nl = tok:find("\n", i, true)
i = (nl and nl + 1) or (#tok + 1)
elseif c == "/" and tok:sub(i + 1, i + 1) == "*" then
-- block comment: skip to matching */
--- @type integer|nil
local close = tok:find("*/", i + 2, true)
i = (close and close + 2) or (#tok + 1)
elseif c == "(" then
@@ -399,9 +529,13 @@ local function _project_emission_inner(root_body_entry, ctx_table)
-- Find the position of the consuming instruction's open paren (the `(` that starts the consuming instruction's argument list).
-- Returns nil if the token's leading text isn't an ident followed by `(` (e.g. the ident is at the start of a non-instruction token).
--- @param tok string
--- @return integer|nil
local function find_consuming_paren(tok)
--- @type integer
local i = 1
while i <= #tok do
--- @type string
local c = tok:sub(i, i)
if c == "(" then return i end
if not c:match("[%w_]") and c ~= " " then return nil end
@@ -410,24 +544,33 @@ local function _project_emission_inner(root_body_entry, ctx_table)
return nil
end
--- @param tok string
--- @param tok_line integer
--- @param consuming_encoder string|nil
--- @return nil
local function emit_embedded_markers(tok, tok_line, consuming_encoder)
-- When called with a non-nil `consuming_encoder`, the marker is nested inside that instruction's argument list.
-- We compute each marker's arg position by counting top-level commas between the consuming instruction's `(` and the marker's start.
--- @type integer|nil
local consuming_paren = nil
if consuming_encoder then consuming_paren = find_consuming_paren(tok) end
--- @type integer
local pos = 1
while pos <= #tok do
-- Trim leading whitespace and comments before each scan.
pos = M.skip_ws_and_cmt(tok, pos)
if pos > #tok then break end
--- @type string|nil, integer
local ident, after = M.read_ident(tok, pos)
if not ident then
-- Not an ident: token is a string or comment; skip or one-step.
--- @type integer
local next_pos = M.skip_str_or_cmt(tok, pos)
pos = (next_pos > pos) and next_pos or (pos + 1)
goto continue_loop
end
if M.DELAY_MARKERS[ident] then
--- @type integer|nil
local arg_pos = nil
if consuming_encoder and consuming_paren then
arg_pos = count_top_level_commas(tok, consuming_paren + 1, pos) + 1
@@ -442,7 +585,9 @@ local function _project_emission_inner(root_body_entry, ctx_table)
goto continue_loop
end
-- Marker ident: parse the (...) arguments.
--- @type integer
local open = M.skip_ws_and_cmt(tok, after)
--- @type string|nil, integer
local inner, after_paren = M.read_parens(tok, open)
if not inner then
-- (...) Unreadable: fall back to non-marker behavior.
@@ -453,10 +598,12 @@ local function _project_emission_inner(root_body_entry, ctx_table)
-- For embedded markers, propagate the consuming_encoder + the marker's arg position
-- (1-based) so `passes/offsets.lua` can dispatch per-consuming-instruction offset encoding.
-- Offset markers are emitted only when a consuming encoder is present.
--- @type integer|nil
local arg_pos = nil
if consuming_encoder and consuming_paren then
arg_pos = count_top_level_commas(tok, consuming_paren + 1, pos) + 1
end
--- @type string[]
local args = split_call_args(inner)
if ident == "atom_label" then emit_marker("label", args[1] or "", nil, tok_line, nil, nil, consuming_encoder, arg_pos)
elseif consuming_encoder then emit_marker("offset", args[1] or "", args[2] or "", tok_line, nil, nil, consuming_encoder, arg_pos)
@@ -466,6 +613,13 @@ local function _project_emission_inner(root_body_entry, ctx_table)
end
end
--- @param inv_kind string
--- @param component_name string
--- @param call_text string
--- @param root_call_text string|nil
--- @param call_path string
--- @param call_line integer
--- @return InvocationRecord
local function emit_invoke_begin(inv_kind, component_name, call_text,
root_call_text, call_path, call_line)
next_inv_id = next_inv_id + 1
@@ -476,7 +630,9 @@ local function _project_emission_inner(root_body_entry, ctx_table)
-- The walker has already found the component body in `ctx_table.component_index[component_name]`, so the matching entry MUST exist in `ctx_table.components[component_name]`
-- (both registries are populated from the same source by the components pass).
-- A missing entry is a corpus-plumbing bug; we fail loudly here rather than silently stamp `false` and mask the regression.
--- @type table<string, ComponentDef>|nil
local components = ctx_table.components
--- @type ComponentDef|nil
local component_def = components and components[component_name] or nil
if not component_def then
error("duffle.emit_invoke_begin: component " .. string.format("%q", component_name)
@@ -486,7 +642,9 @@ local function _project_emission_inner(root_body_entry, ctx_table)
, 0
)
end
--- @type boolean
local debug_skip_stamp = component_def.debug_skip == true
--- @type InvocationRecord
local inv = {
id = next_inv_id,
parent_id = 0, -- patched below by caller
@@ -521,6 +679,8 @@ local function _project_emission_inner(root_body_entry, ctx_table)
return inv
end
--- @param inv InvocationRecord
--- @return nil
local function emit_invoke_end(inv)
-- 0-based emitted-word position of the LAST word inside this invocation.
-- After the last body word was emitted, `word_idx` was incremented past it, so `word_idx - 1` is the 0-based position of the last word.
@@ -532,6 +692,7 @@ local function _project_emission_inner(root_body_entry, ctx_table)
word_index = word_idx,
invocation_ids = open_invocation_ids_snapshot(),
}
--- @type integer
for i = #invocation_stack, 1, -1 do
if invocation_stack[i] == inv then
table.remove(invocation_stack, i)
@@ -542,9 +703,14 @@ local function _project_emission_inner(root_body_entry, ctx_table)
-- Resolve the per-token word count. If unresolved, surface ONE warning
-- and fall back to 1 opaque word so the cycle budget still accounts for the slot.
--- @param ident string
--- @param tok_line integer
--- @return integer
local function resolve_count(ident, tok_line)
--- @type WordCounts|nil
local wc = ctx_table.word_counts
if wc and wc[ident] then return wc[ident] end
--- @type string
local canon = M.gte_canon(ident)
if canon ~= ident and wc and wc[canon] then return wc[canon] end
warnings[#warnings + 1] = {
@@ -561,24 +727,40 @@ local function _project_emission_inner(root_body_entry, ctx_table)
-- walk_root_call_text: Outermost `mac_X(...)` token text (preserved across recursion).
-- walk_immediate_call_text: IMMEDIATE outer `mac_X(...)` token text for words emitted in this body — nil for the root atom body.
-- Two trackers are propagated as separate parameters so words deep inside nested expansions correctly identify both their immediate call site and the outermost call site.
--- @param body_entry ComponentBodyEntry
--- @param walk_parent_inv_id integer
--- @param walk_root_call_text string|nil
--- @param walk_immediate_call_text string|nil
--- @return nil
local function walk_body_entry(body_entry, walk_parent_inv_id,
walk_root_call_text, walk_immediate_call_text)
--- @type BodyToken[]
local tokens = body_entry.body_tokens or {}
--- @type integer
local body_off = body_entry.body_off or 0
--- @type LineIndexFn
local line_of = body_entry.line_of or M.LineIndex("")
--- @type string
local def_source = body_entry.source or ""
--- @type integer
local def_line = body_entry.declaration or 0
--- @type table<string, string>|nil
local sub_map = body_entry.sub_map
-- Per-token dispatch: each matched branch returns; only the fall-through
-- "opaque word" emit handles direct encoders + mac_X-without-component.
--- @param bt BodyToken
--- @return nil
local function process_token(bt)
--- @type string
local tok = M.trim(bt.tok or "")
-- Substituted MipsCode args can carry // comments from the call site.
while tok ~= "" do
if tok:sub(1, 2) == "//" then
--- @type integer|nil
local nl = tok:find("\n")
tok = M.trim(nl and tok:sub(nl + 1) or "")
elseif tok:sub(1, 2) == "/*" then
--- @type integer|nil
local close = tok:find("*/", 3, true)
tok = M.trim(close and tok:sub(close + 2) or "")
else
@@ -586,19 +768,25 @@ local function _project_emission_inner(root_body_entry, ctx_table)
end
end
if tok == "" then return end
--- @type string|nil, integer
local ident, after = M.read_ident(tok, 1)
if not ident then ident = "?" end
--- @type string, string[]
local _, args = token_ident_and_args(tok)
--- @type integer
local tok_line = line_of(body_off + bt.rel) or 0
if M.DELAY_MARKERS[ident] then
emit_marker("delay", ident, nil, tok_line)
--- @type string
local rest = tok:sub(after or (#tok + 1))
while true do
rest = M.trim(rest)
if rest:sub(1, 2) == "//" then
--- @type integer|nil
local nl = rest:find("\n")
rest = nl and rest:sub(nl + 1) or ""
elseif rest:sub(1, 2) == "/*" then
--- @type integer|nil
local close = rest:find("*/", 3, true)
if not close then rest = ""; break end
rest = rest:sub(close + 2)
@@ -615,6 +803,7 @@ local function _project_emission_inner(root_body_entry, ctx_table)
-- Pass `ident` as the consuming instruction so `emit_embedded_markers` can compute each marker's arg position + record the consuming_encoder for the offsets pass.
-- Canonicalize `jump_rel` to `branch_equal` (its preprocessor-expanded form) so the `consuming_encoder` metadata in marker records is canonical.
-- `jump_rel`: unconditional jump alias from `code/duffle/mips.h`.
--- @type string
local consuming_encoder_for_markers = (ident == "jump_rel") and "branch_equal" or ident
if ident ~= "atom_label" and ident ~= "atom_offset" then
emit_embedded_markers(tok, tok_line, consuming_encoder_for_markers)
@@ -628,6 +817,7 @@ local function _project_emission_inner(root_body_entry, ctx_table)
-- MipsCode formals (nop_slot1, …): the ident is a sub_map key.
-- Re-process the replacement token so load_word(...) becomes a real encoder.
if sub_map and type(sub_map[ident]) == "string" and sub_map[ident] ~= ident then
--- @type string
local repl = M.trim(sub_map[ident])
if repl ~= "" then
process_token({ tok = repl, rel = bt.rel })
@@ -635,15 +825,20 @@ local function _project_emission_inner(root_body_entry, ctx_table)
end
end
if ident:sub(1, 4) == "mac_" then
--- @type string
local bare = ident:sub(5)
--- @type ComponentBodyEntry|nil
local comp = ctx_table.component_index[bare]
if comp then
--- @type string
local invocation_root_call_text = walk_root_call_text or tok
if ctx_table.visiting[bare] then
-- Cycle: still allocate inv_id, emit zero-width begin/end, record the cycle error; do NOT recurse.
--- @type InvocationRecord
local inv = emit_invoke_begin(comp.kind or "comp_bare", bare, tok, invocation_root_call_text, def_source, tok_line)
inv.parent_id = walk_parent_inv_id
inv.call_text = tok
--- @type EmitError
local err = {
kind = "cycle",
msg = string.format("project_emission: component cycle detected: %q", bare),
@@ -657,6 +852,7 @@ local function _project_emission_inner(root_body_entry, ctx_table)
end
-- First visit: descend + count + count_mismatch-check below.
ctx_table.visiting[bare] = true
--- @type InvocationRecord
local inv = emit_invoke_begin(comp.kind or "comp_bare", bare, tok, invocation_root_call_text, def_source, tok_line)
inv.parent_id = walk_parent_inv_id
inv.call_text = tok
@@ -665,11 +861,14 @@ local function _project_emission_inner(root_body_entry, ctx_table)
-- Propagate trackers into the recursive walk:
-- immediate_call_text = this call's tok (the IMMEDIATE outer call for words emitted in this body)
-- root_call_text = the OUTERMOST call (immutable across the recursion)
--- @type string[]|nil
local formal_names = ctx_table.component_index[bare]
and ctx_table.component_index[bare].arg_names
--- @type table<string, string>|nil
local child_map = nil
if formal_names then
child_map = {}
--- @type integer, string
for i, fname in ipairs(formal_names) do
child_map[fname] = apply_sub(sub_map, args[i])
end
@@ -688,8 +887,11 @@ local function _project_emission_inner(root_body_entry, ctx_table)
ctx_table.visiting[bare] = nil
emit_invoke_end(inv)
-- Count `word` items inside [start_word, end_word].
--- @type integer
local wc_inside = 0
--- @type integer
for i = inv.start_word, inv.end_word do
--- @type EmissionItem|nil
local it = items[i]
if it and it.kind == "word" then
wc_inside = wc_inside + 1
@@ -698,8 +900,10 @@ local function _project_emission_inner(root_body_entry, ctx_table)
inv.word_count = wc_inside
-- count_mismatch is a construction error: word_counts["mac_X"] is the declared count populated by the components pass;
-- We compare against the measured word count.
--- @type integer|nil
local declared = ctx_table.word_counts["mac_" .. bare]
if declared and wc_inside ~= declared then
--- @type EmitError
local err = {
kind = "count_mismatch",
msg = string.format("project_emission: mac_%s declared=%d measured=%d", bare, declared, wc_inside),
@@ -715,13 +919,17 @@ local function _project_emission_inner(root_body_entry, ctx_table)
end
-- Direct encoder, or mac_X-without-component: resolve count + emit n words.
-- Resolve_count may emit a warning if the count is unresolved.
--- @type integer
local n = resolve_count(ident, tok_line)
--- @type string
local out_ident = (ident == "nop2") and "nop" or ident
--- @type integer
for _ = 1, n do
emit_word(out_ident, args, tok_line, tok, def_source, def_line, walk_immediate_call_text, walk_root_call_text, sub_map)
end
end
--- @type integer, BodyToken
for _, bt in ipairs(tokens) do
process_token(bt)
end
@@ -780,11 +988,12 @@ end
--- for the open invocation stack at that word.
---
--- @param body_text string -- the raw atom body string
--- @param component_index table -- bare-name → component record (corpus.component_body_index)
--- @param word_counts table -- macro name → emitted word count
--- @param components table -- bare-name → component definition (corpus.components); REQUIRED — consumed at the invocation-construction site to stamp
--- @param component_index table<string, ComponentBodyEntry>
--- @param word_counts WordCounts
--- @param components table<string, ComponentDef>
--- `invocation.debug_skip`. A missing or non-table `components` raises a fail-loud error rather than silently falling back.
--- @return EmissionProjection
--- @param reg_use_ctx RegUseCtx|nil
function M.project_emission(body_text, component_index, word_counts, components, reg_use_ctx)
-- The recursive walk delegates to `_project_emission_inner` so component bodies (which arrive as
-- `{body_tokens, body_off, line_of, source, declaration}` records from `corpus.component_body_index`)
@@ -816,6 +1025,7 @@ function M.project_emission(body_text, component_index, word_counts, components,
}
end
--- @type BodyToken[]
local tokens = M.tokenize_body(body_text)
return _project_emission_inner({
body_tokens = tokens,
@@ -849,10 +1059,17 @@ end
-- (FI_, atom_dbg_skip, comments) until it finds an ident followed by "(".
-- That ident is the function name; the parens contents are the args.
-------------------------------------------------------------------------------
--- @param source string
--- @param before_pos integer
--- @param slice_mips_code_len integer
--- @return string|nil, string|nil
function M.find_function_decl_for(source, before_pos, slice_mips_code_len)
--- @type integer
local search_pos = 1
--- @type integer|nil
local last_match = nil
while true do
--- @type integer|nil
local found = source:find("Slice_MipsCode", search_pos, true)
if not found or found >= before_pos then break end
last_match = found
@@ -860,10 +1077,12 @@ function M.find_function_decl_for(source, before_pos, slice_mips_code_len)
end
if not last_match then return nil, nil end
--- @type integer
local pos = last_match + slice_mips_code_len
while pos < before_pos do
-- skip whitespace
while pos <= #source do
--- @type string
local c = source:sub(pos, pos)
if c == " " or c == "\t" or c == "\n" or c == "\r" then
pos = pos + 1
@@ -880,17 +1099,21 @@ function M.find_function_decl_for(source, before_pos, slice_mips_code_len)
end
-- skip block comments
if source:sub(pos, pos + 1) == "/*" then
--- @type integer|nil
local close = source:find("*/", pos + 2, true)
if not close then break end
pos = close + 2
goto continue
end
-- try to read an ident
--- @type string|nil, integer
local ident, ident_end = M.read_ident(source, pos)
if not ident then break end
-- check if the next non-ws char after ident is "("
--- @type integer
local next_pos = M.skip_ws_and_cmt(source, ident_end)
if source:sub(next_pos, next_pos) == "(" then
--- @type string|nil
local inner = M.read_parens(source, next_pos)
if inner then
return ident, inner
@@ -917,11 +1140,18 @@ end
-- The walk finds the LAST "MipsAtom*" before before_pos, then skips whitespace + qualifiers (internal, I_, FI_, comments)
-- until it finds an ident followed by "(".
-------------------------------------------------------------------------------
--- @param source string
--- @param before_pos integer
--- @param mips_atom_ptr_len integer
--- @return string|nil, string|nil, string|nil, integer|nil
function M.find_atom_proc_decl_for(source, before_pos, mips_atom_ptr_len)
--- @type integer
local search_pos = 1
--- @type integer|nil
local last_match = nil
while true do
-- plain=true: "*" is literal, no escaping needed
--- @type integer|nil
local found = source:find("MipsAtom*", search_pos, true)
if not found or found >= before_pos then break end
last_match = found
@@ -929,10 +1159,12 @@ function M.find_atom_proc_decl_for(source, before_pos, mips_atom_ptr_len)
end
if not last_match then return nil, nil end
--- @type integer
local pos = last_match + mips_atom_ptr_len
while pos < before_pos do
-- skip whitespace
while pos <= #source do
--- @type string
local c = source:sub(pos, pos)
if c == " " or c == "\t" or c == "\n" or c == "\r" then
pos = pos + 1
@@ -949,17 +1181,21 @@ function M.find_atom_proc_decl_for(source, before_pos, mips_atom_ptr_len)
end
-- skip block comments
if source:sub(pos, pos + 1) == "/*" then
--- @type integer|nil
local close = source:find("*/", pos + 2, true)
if not close then break end
pos = close + 2
goto continue
end
-- try to read an ident
--- @type string|nil, integer
local ident, ident_end = M.read_ident(source, pos)
if not ident then break end
-- check if the next non-ws char after ident is "("
--- @type integer
local next_pos = M.skip_ws_and_cmt(source, ident_end)
if source:sub(next_pos, next_pos) == "(" then
--- @type string|nil, integer
local inner, after_paren = M.read_parens(source, next_pos)
if inner then
return ident, inner, ident, after_paren