auto-column alignment formatting pass on trailing type annotations.

This commit is contained in:
ed
2026-08-19 23:48:46 -04:00
parent 2a087f735e
commit de13bc3ce9
16 changed files with 1035 additions and 1181 deletions
+148 -294
View File
@@ -1,14 +1,9 @@
--- duffle_emit.lua — project_emission + decl finders. --- duffle_emit.lua — project_emission + decl finders.
--- @type DuffleScan local scan = require("duffle_scan") ---@type DuffleScan
local scan = require("duffle_scan") local isa = require("duffle_isa") ---@type DuffleIsa
--- @type DuffleIsa local M = {} ---@type DuffleEmit
local isa = require("duffle_isa") for k, v in pairs(scan) do M[k] = v end ---@type string, any
--- @type DuffleEmit for k, v in pairs(isa) do M[k] = v end ---@type string, any
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 -- Section 8: Cross-source component-body index + word-event expansion
-- ════════════════════════════════════════════════════════════════════════════ -- ════════════════════════════════════════════════════════════════════════════
@@ -51,22 +46,15 @@ for k, v in pairs(isa) do M[k] = v end
-- Consumers (`passes/static_analysis.lua`, `passes/emission_model.lua`) read it directly; per-pass memoization helpers stay out of scope. -- 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). -- 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_PAREN = 0x28 local E_BYTE_OPEN_BRACE = 0x7B ---@type integer
--- @type integer local E_BYTE_OPEN_BRACK = 0x5B ---@type integer
local E_BYTE_OPEN_BRACE = 0x7B local E_BYTE_DQUOTE = 0x22 ---@type integer
--- @type integer local E_BYTE_SQUOTE = 0x27 ---@type integer
local E_BYTE_OPEN_BRACK = 0x5B local E_BYTE_COMMA = 0x2C ---@type integer
--- @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. -- 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 = { ---@type table<integer, string> -- bag: open-delimiter byte -> close string
local E_OPEN_CLOSE = {
[E_BYTE_OPEN_PAREN] = ")", [E_BYTE_OPEN_PAREN] = ")",
[E_BYTE_OPEN_BRACE] = "}", [E_BYTE_OPEN_BRACE] = "}",
[E_BYTE_OPEN_BRACK] = "]", [E_BYTE_OPEN_BRACK] = "]",
@@ -79,23 +67,16 @@ local E_OPEN_CLOSE = {
--- @param inner string --- @param inner string
--- @return string[] --- @return string[]
local function split_call_args(inner) local function split_call_args(inner)
--- @type string[] local args = {} ---@type string[]
local args = {}
if not inner or inner == "" then return args end if not inner or inner == "" then return args end
--- @type integer local pos = 1 ---@type integer
local pos = 1 local len = #inner ---@type integer
--- @type integer local start = 1 ---@type integer
local len = #inner
--- @type integer
local start = 1
while pos <= len do while pos <= len do
--- @type integer local c = inner:byte(pos) ---@type integer
local c = inner:byte(pos) local close = E_OPEN_CLOSE[c] ---@type string|nil
--- @type string|nil
local close = E_OPEN_CLOSE[c]
if close then if close then
--- @type integer, integer local _, after = M.read_balanced(inner, string.char(c), close, pos) ---@type integer, integer
local _, after = M.read_balanced(inner, string.char(c), close, pos)
pos = after pos = after
elseif c == E_BYTE_DQUOTE or c == E_BYTE_SQUOTE then elseif c == E_BYTE_DQUOTE or c == E_BYTE_SQUOTE then
pos = M.skip_str_or_cmt(inner, pos) pos = M.skip_str_or_cmt(inner, pos)
@@ -116,23 +97,18 @@ end
--- @param tok string --- @param tok string
--- @return string, string[] --- @return string, string[]
local function token_ident_and_args(tok) local function token_ident_and_args(tok)
--- @type string|nil, integer local ident, after = M.read_ident(tok, 1) ---@type string|nil, integer
local ident, after = M.read_ident(tok, 1)
if not ident then return "?", {} end if not ident then return "?", {} end
--- @type integer local paren_pos = M.skip_ws_and_cmt(tok, after) ---@type integer
local paren_pos = M.skip_ws_and_cmt(tok, after)
if tok:sub(paren_pos, paren_pos) ~= "(" then return ident, {} end if tok:sub(paren_pos, paren_pos) ~= "(" then return ident, {} end
--- @type string|nil local inner = M.read_parens(tok, paren_pos) ---@type string|nil
local inner = M.read_parens(tok, paren_pos)
if not inner then return ident, {} end if not inner then return ident, {} end
return ident, split_call_args(inner) return ident, split_call_args(inner)
end end
-- The macro-name prefix that marks a `mac_X(...)` component invocation. -- The macro-name prefix that marks a `mac_X(...)` component invocation.
--- @type string local E_MAC_PREFIX = "mac_" ---@type string
local E_MAC_PREFIX = "mac_" local E_MAC_PREFIX_LEN = 4 ---@type integer
--- @type integer
local E_MAC_PREFIX_LEN = 4
--- Expand a body entry into the flat sequence of emitted machine-word events. --- Expand a body entry into the flat sequence of emitted machine-word events.
--- ---
@@ -217,38 +193,24 @@ local E_MAC_PREFIX_LEN = 4
--- @param ctx_table EmissionWalkCtx --- @param ctx_table EmissionWalkCtx
--- @return EmissionProjection --- @return EmissionProjection
local function _project_emission_inner(root_body_entry, ctx_table) local function _project_emission_inner(root_body_entry, ctx_table)
--- @type EmissionItem[] local items = {} ---@type EmissionItem[]
local items = {} local word_events = {} ---@type WordEvent[]
--- @type WordEvent[] local markers = {} ---@type EmissionMarker[]
local word_events = {} local invocations = {} ---@type InvocationRecord[]
--- @type EmissionMarker[] local errors = {} ---@type EmitError[]
local markers = {} local warnings = {} ---@type EmitWarning[]
--- @type InvocationRecord[]
local invocations = {}
--- @type EmitError[]
local errors = {}
--- @type EmitWarning[]
local warnings = {}
--- @type integer local word_idx = 0 ---@type integer
local word_idx = 0 local invocation_stack = {} ---@type InvocationRecord[] -- stack of currently-open invocation records
--- @type InvocationRecord[] local next_inv_id = 0 ---@type integer
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 RegUseSchema|nil
local reg_use_schema = ctx_table.reg_use_schema local reg_use_param = ctx_table.reg_use_param ---@type string|nil
--- @type string|nil local atom_name = ctx_table.atom_name ---@type AtomName|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 = {} ---@type table<string, boolean> -- bag: slot name -> readonly
local slot_readonly = {}
if reg_use_schema then if reg_use_schema then
--- @type integer, RegUseSlot for _, slot in ipairs(reg_use_schema.slots or {}) do ---@type integer, RegUseSlot
for _, slot in ipairs(reg_use_schema.slots or {}) do
slot_readonly[slot.name] = slot.readonly == true slot_readonly[slot.name] = slot.readonly == true
end end
end end
@@ -259,13 +221,10 @@ local function _project_emission_inner(root_body_entry, ctx_table)
local function apply_sub(sub_map, operand) local function apply_sub(sub_map, operand)
if not (sub_map and type(operand) == "string") then return operand end if not (sub_map and type(operand) == "string") then return operand end
if sub_map[operand] then return sub_map[operand] end if sub_map[operand] then return sub_map[operand] end
--- @type integer|nil local dot = operand:find(".", 1, true) ---@type integer|nil
local dot = operand:find(".", 1, true)
if dot then if dot then
--- @type string local head = operand:sub(1, dot - 1) ---@type string
local head = operand:sub(1, dot - 1) local mapped = sub_map[head] ---@type string|nil
--- @type string|nil
local mapped = sub_map[head]
if type(mapped) == "string" then if type(mapped) == "string" then
return mapped .. operand:sub(dot) return mapped .. operand:sub(dot)
end end
@@ -279,23 +238,18 @@ local function _project_emission_inner(root_body_entry, ctx_table)
if type(operand) ~= "string" then return nil end if type(operand) ~= "string" then return nil end
if operand:sub(1, 2) == "R_" then return operand end if operand:sub(1, 2) == "R_" then return operand end
if not (reg_use_schema and reg_use_param) then return nil end if not (reg_use_schema and reg_use_param) then return nil end
--- @type string local prefix = reg_use_param .. "." ---@type string
local prefix = reg_use_param .. "."
if operand:sub(1, #prefix) ~= prefix then return nil end if operand:sub(1, #prefix) ~= prefix then return nil end
--- @type string local member_path = operand:sub(#prefix + 1) ---@type string
local member_path = operand:sub(#prefix + 1) local slot = reg_use_schema.alias_to_slot[member_path] ---@type string|nil
--- @type string|nil
local slot = reg_use_schema.alias_to_slot[member_path]
if not slot then return nil, member_path end if not slot then return nil, member_path end
return "reguse:" .. atom_name .. ":" .. slot, nil, slot return "reguse:" .. atom_name .. ":" .. slot, nil, slot
end end
--- @return integer[] --- @return integer[]
local function open_invocation_ids_snapshot() local function open_invocation_ids_snapshot()
--- @type integer[] local ids = {} ---@type integer[]
local ids = {} for _, inv in ipairs(invocation_stack) do ---@type integer, InvocationRecord
--- @type integer, InvocationRecord
for _, inv in ipairs(invocation_stack) do
ids[#ids + 1] = inv.id ids[#ids + 1] = inv.id
end end
return ids return ids
@@ -312,27 +266,19 @@ local function _project_emission_inner(root_body_entry, ctx_table)
--- @param sub_map table<string, string>|nil --- @param sub_map table<string, string>|nil
--- @return 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) 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 inv_ids = open_invocation_ids_snapshot() local outermost = inv_ids[1] or 0 ---@type integer
--- @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 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; -- 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. -- 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_call_text = immediate_call_text or word_call_text local eff_root_call_text = root_call_text_w ---@type string|nil
--- @type string|nil local gpr_keys = nil ---@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 if reg_use_schema or sub_map then
gpr_keys = {} gpr_keys = {}
--- @type integer, string for pos, arg in ipairs(args or {}) do ---@type integer, string
for pos, arg in ipairs(args or {}) do local effective = apply_sub(sub_map, arg) ---@type any
--- @type any local key, unresolved, slot = resolve_gpr_key(effective) ---@type string|nil, string|nil, string|nil
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 gpr_keys[pos] = key
if unresolved then if unresolved then
errors[#errors + 1] = { errors[#errors + 1] = {
@@ -343,11 +289,9 @@ local function _project_emission_inner(root_body_entry, ctx_table)
} }
end end
if key and slot and slot_readonly[slot] then if key and slot and slot_readonly[slot] then
--- @type InstructionRow|nil local row = M.instr(encoder) ---@type InstructionRow|nil
local row = M.instr(encoder)
if row and row.writes then if row and row.writes then
--- @type integer, integer for _, wpos in ipairs(row.writes) do ---@type integer, integer
for _, wpos in ipairs(row.writes) do
if wpos == pos then if wpos == pos then
errors[#errors + 1] = { errors[#errors + 1] = {
kind = "reguse_const_write", kind = "reguse_const_write",
@@ -364,26 +308,17 @@ local function _project_emission_inner(root_body_entry, ctx_table)
if not reg_use_schema then if not reg_use_schema then
gpr_keys = nil gpr_keys = nil
end end
--- @type InstructionRow|nil local isa = M.instr(encoder) ---@type InstructionRow|nil
local isa = M.instr(encoder) local isa_kind = isa and isa.kind or "unknown" ---@type string
--- @type string local nop_words = (encoder == "nop" and 1) or (encoder == "nop2" and 2) or 0 ---@type integer
local isa_kind = isa and isa.kind or "unknown" local is_yield = (encoder == "mac_yield" or encoder == "mac_yield_tail") ---@type boolean
--- @type integer local gp0_shape = type(encoder) == "string" ---@type string|nil
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$") and encoder:match("^mac_format_([%w_]+)_color$")
or nil or nil
--- @type boolean local is_load = (isa_kind == "load") ---@type boolean
local is_load = (isa_kind == "load") local is_branch = (isa_kind == "branch") ---@type boolean
--- @type boolean local is_unconditional_jump = (encoder == "jump" or encoder == "call_addr") ---@type boolean
local is_branch = (isa_kind == "branch") local is_terminal_jump = (encoder == "jump_reg" or encoder == "call_reg" or encoder == "jump_link") ---@type boolean
--- @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] = { items[#items + 1] = {
kind = "word", kind = "word",
encoder = encoder, encoder = encoder,
@@ -443,10 +378,8 @@ local function _project_emission_inner(root_body_entry, ctx_table)
local function emit_marker(kind, name, target, line, local function emit_marker(kind, name, target, line,
immediate_call_text, root_call_text_w, immediate_call_text, root_call_text_w,
consuming_encoder, consuming_arg_pos) consuming_encoder, consuming_arg_pos)
--- @type integer[] local inv_ids = open_invocation_ids_snapshot() ---@type integer[]
local inv_ids = open_invocation_ids_snapshot() local outermost = inv_ids[1] or 0 ---@type integer
--- @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. -- 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 -- `consuming_encoder` + `consuming_arg_pos` carry the surrounding control-transfer instruction context
-- (e.g. `branch_le_zero` consuming its 3rd argument, or `jump` / `call_addr` consuming their only argument). -- (e.g. `branch_le_zero` consuming its 3rd argument, or `jump` / `call_addr` consuming their only argument).
@@ -456,8 +389,7 @@ local function _project_emission_inner(root_body_entry, ctx_table)
if kind == "offset" and (consuming_encoder == nil or consuming_encoder == "") then if kind == "offset" and (consuming_encoder == nil or consuming_encoder == "") then
return return
end end
--- @type EmissionItem local it = { ---@type EmissionItem
local it = {
kind = kind, kind = kind,
name = name, name = name,
line = line, line = line,
@@ -488,28 +420,21 @@ local function _project_emission_inner(root_body_entry, ctx_table)
--- @param to_pos integer --- @param to_pos integer
--- @return integer --- @return integer
local function count_top_level_commas(tok, from_pos, to_pos) local function count_top_level_commas(tok, from_pos, to_pos)
--- @type integer local depth = 0 ---@type integer
local depth = 0 local count = 0 ---@type integer
--- @type integer local i = from_pos ---@type integer
local count = 0
--- @type integer
local i = from_pos
while i < to_pos do while i < to_pos do
--- @type string local c = tok:sub(i, i) ---@type string
local c = tok:sub(i, i)
if c == "'" or c == '"' then if c == "'" or c == '"' then
--- @type integer local next_pos = M.skip_str_or_cmt(tok, i) ---@type integer
local next_pos = M.skip_str_or_cmt(tok, i)
i = (next_pos > i) and next_pos or (i + 1) i = (next_pos > i) and next_pos or (i + 1)
elseif c == "/" and tok:sub(i + 1, i + 1) == "/" then elseif c == "/" and tok:sub(i + 1, i + 1) == "/" then
-- line comment: skip to end of line -- line comment: skip to end of line
--- @type integer|nil local nl = tok:find("\n", i, true) ---@type integer|nil
local nl = tok:find("\n", i, true)
i = (nl and nl + 1) or (#tok + 1) i = (nl and nl + 1) or (#tok + 1)
elseif c == "/" and tok:sub(i + 1, i + 1) == "*" then elseif c == "/" and tok:sub(i + 1, i + 1) == "*" then
-- block comment: skip to matching */ -- block comment: skip to matching */
--- @type integer|nil local close = tok:find("*/", i + 2, true) ---@type integer|nil
local close = tok:find("*/", i + 2, true)
i = (close and close + 2) or (#tok + 1) i = (close and close + 2) or (#tok + 1)
elseif c == "(" then elseif c == "(" then
depth = depth + 1 depth = depth + 1
@@ -532,11 +457,9 @@ local function _project_emission_inner(root_body_entry, ctx_table)
--- @param tok string --- @param tok string
--- @return integer|nil --- @return integer|nil
local function find_consuming_paren(tok) local function find_consuming_paren(tok)
--- @type integer local i = 1 ---@type integer
local i = 1
while i <= #tok do while i <= #tok do
--- @type string local c = tok:sub(i, i) ---@type string
local c = tok:sub(i, i)
if c == "(" then return i end if c == "(" then return i end
if not c:match("[%w_]") and c ~= " " then return nil end if not c:match("[%w_]") and c ~= " " then return nil end
i = i + 1 i = i + 1
@@ -551,27 +474,22 @@ local function _project_emission_inner(root_body_entry, ctx_table)
local function emit_embedded_markers(tok, tok_line, consuming_encoder) 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. -- 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. -- 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 ---@type integer|nil
local consuming_paren = nil
if consuming_encoder then consuming_paren = find_consuming_paren(tok) end if consuming_encoder then consuming_paren = find_consuming_paren(tok) end
--- @type integer local pos = 1 ---@type integer
local pos = 1
while pos <= #tok do while pos <= #tok do
-- Trim leading whitespace and comments before each scan. -- Trim leading whitespace and comments before each scan.
pos = M.skip_ws_and_cmt(tok, pos) pos = M.skip_ws_and_cmt(tok, pos)
if pos > #tok then break end if pos > #tok then break end
--- @type string|nil, integer local ident, after = M.read_ident(tok, pos) ---@type string|nil, integer
local ident, after = M.read_ident(tok, pos)
if not ident then if not ident then
-- Not an ident: token is a string or comment; skip or one-step. -- 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) ---@type integer
local next_pos = M.skip_str_or_cmt(tok, pos)
pos = (next_pos > pos) and next_pos or (pos + 1) pos = (next_pos > pos) and next_pos or (pos + 1)
goto continue_loop goto continue_loop
end end
if M.DELAY_MARKERS[ident] then if M.DELAY_MARKERS[ident] then
--- @type integer|nil local arg_pos = nil ---@type integer|nil
local arg_pos = nil
if consuming_encoder and consuming_paren then if consuming_encoder and consuming_paren then
arg_pos = count_top_level_commas(tok, consuming_paren + 1, pos) + 1 arg_pos = count_top_level_commas(tok, consuming_paren + 1, pos) + 1
end end
@@ -585,10 +503,8 @@ local function _project_emission_inner(root_body_entry, ctx_table)
goto continue_loop goto continue_loop
end end
-- Marker ident: parse the (...) arguments. -- Marker ident: parse the (...) arguments.
--- @type integer local open = M.skip_ws_and_cmt(tok, after) ---@type integer
local open = M.skip_ws_and_cmt(tok, after) local inner, after_paren = M.read_parens(tok, open) ---@type string|nil, integer
--- @type string|nil, integer
local inner, after_paren = M.read_parens(tok, open)
if not inner then if not inner then
-- (...) Unreadable: fall back to non-marker behavior. -- (...) Unreadable: fall back to non-marker behavior.
pos = after pos = after
@@ -598,13 +514,11 @@ local function _project_emission_inner(root_body_entry, ctx_table)
-- For embedded markers, propagate the consuming_encoder + the marker's arg position -- 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. -- (1-based) so `passes/offsets.lua` can dispatch per-consuming-instruction offset encoding.
-- Offset markers are emitted only when a consuming encoder is present. -- Offset markers are emitted only when a consuming encoder is present.
--- @type integer|nil local arg_pos = nil ---@type integer|nil
local arg_pos = nil
if consuming_encoder and consuming_paren then if consuming_encoder and consuming_paren then
arg_pos = count_top_level_commas(tok, consuming_paren + 1, pos) + 1 arg_pos = count_top_level_commas(tok, consuming_paren + 1, pos) + 1
end end
--- @type string[] local args = split_call_args(inner) ---@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) 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) elseif consuming_encoder then emit_marker("offset", args[1] or "", args[2] or "", tok_line, nil, nil, consuming_encoder, arg_pos)
end end
@@ -630,10 +544,8 @@ 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]` -- 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). -- (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. -- 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 table<string, ComponentDef>|nil
local components = ctx_table.components local component_def = components and components[component_name] or nil ---@type ComponentDef|nil
--- @type ComponentDef|nil
local component_def = components and components[component_name] or nil
if not component_def then if not component_def then
error("duffle.emit_invoke_begin: component " .. string.format("%q", component_name) error("duffle.emit_invoke_begin: component " .. string.format("%q", component_name)
.. " is present in `component_index` (the walker matched a `mac_" .. component_name .. "()` call) but absent from `components` (the canonical corpus.components registry). " .. " is present in `component_index` (the walker matched a `mac_" .. component_name .. "()` call) but absent from `components` (the canonical corpus.components registry). "
@@ -642,10 +554,8 @@ local function _project_emission_inner(root_body_entry, ctx_table)
, 0 , 0
) )
end end
--- @type boolean local debug_skip_stamp = component_def.debug_skip == true ---@type boolean
local debug_skip_stamp = component_def.debug_skip == true local inv = { ---@type InvocationRecord
--- @type InvocationRecord
local inv = {
id = next_inv_id, id = next_inv_id,
parent_id = 0, -- patched below by caller parent_id = 0, -- patched below by caller
kind = inv_kind, kind = inv_kind,
@@ -692,8 +602,7 @@ local function _project_emission_inner(root_body_entry, ctx_table)
word_index = word_idx, word_index = word_idx,
invocation_ids = open_invocation_ids_snapshot(), invocation_ids = open_invocation_ids_snapshot(),
} }
--- @type integer for i = #invocation_stack, 1, -1 do ---@type integer
for i = #invocation_stack, 1, -1 do
if invocation_stack[i] == inv then if invocation_stack[i] == inv then
table.remove(invocation_stack, i) table.remove(invocation_stack, i)
break break
@@ -707,11 +616,9 @@ local function _project_emission_inner(root_body_entry, ctx_table)
--- @param tok_line integer --- @param tok_line integer
--- @return integer --- @return integer
local function resolve_count(ident, tok_line) local function resolve_count(ident, tok_line)
--- @type WordCounts|nil local wc = ctx_table.word_counts ---@type WordCounts|nil
local wc = ctx_table.word_counts
if wc and wc[ident] then return wc[ident] end if wc and wc[ident] then return wc[ident] end
--- @type string local canon = M.gte_canon(ident) ---@type string
local canon = M.gte_canon(ident)
if canon ~= ident and wc and wc[canon] then return wc[canon] end if canon ~= ident and wc and wc[canon] then return wc[canon] end
warnings[#warnings + 1] = { warnings[#warnings + 1] = {
kind = "uncounted", kind = "uncounted",
@@ -734,60 +641,45 @@ local function _project_emission_inner(root_body_entry, ctx_table)
--- @return nil --- @return nil
local function walk_body_entry(body_entry, walk_parent_inv_id, local function walk_body_entry(body_entry, walk_parent_inv_id,
walk_root_call_text, walk_immediate_call_text) walk_root_call_text, walk_immediate_call_text)
--- @type BodyToken[] local tokens = body_entry.body_tokens or {} ---@type BodyToken[]
local tokens = body_entry.body_tokens or {} local body_off = body_entry.body_off or 0 ---@type integer
--- @type integer local line_of = body_entry.line_of or M.LineIndex("") ---@type LineIndexFn
local body_off = body_entry.body_off or 0 local def_source = body_entry.source or "" ---@type string
--- @type LineIndexFn local def_line = body_entry.declaration or 0 ---@type integer
local line_of = body_entry.line_of or M.LineIndex("") local sub_map = body_entry.sub_map ---@type table<string, string>|nil
--- @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 -- Per-token dispatch: each matched branch returns; only the fall-through
-- "opaque word" emit handles direct encoders + mac_X-without-component. -- "opaque word" emit handles direct encoders + mac_X-without-component.
--- @param bt BodyToken --- @param bt BodyToken
--- @return nil --- @return nil
local function process_token(bt) local function process_token(bt)
--- @type string local tok = M.trim(bt.tok or "") ---@type string
local tok = M.trim(bt.tok or "")
-- Substituted MipsCode args can carry // comments from the call site. -- Substituted MipsCode args can carry // comments from the call site.
while tok ~= "" do while tok ~= "" do
if tok:sub(1, 2) == "//" then if tok:sub(1, 2) == "//" then
--- @type integer|nil local nl = tok:find("\n") ---@type integer|nil
local nl = tok:find("\n")
tok = M.trim(nl and tok:sub(nl + 1) or "") tok = M.trim(nl and tok:sub(nl + 1) or "")
elseif tok:sub(1, 2) == "/*" then elseif tok:sub(1, 2) == "/*" then
--- @type integer|nil local close = tok:find("*/", 3, true) ---@type integer|nil
local close = tok:find("*/", 3, true)
tok = M.trim(close and tok:sub(close + 2) or "") tok = M.trim(close and tok:sub(close + 2) or "")
else else
break break
end end
end end
if tok == "" then return end if tok == "" then return end
--- @type string|nil, integer local ident, after = M.read_ident(tok, 1) ---@type string|nil, integer
local ident, after = M.read_ident(tok, 1)
if not ident then ident = "?" end if not ident then ident = "?" end
--- @type string, string[] local _, args = token_ident_and_args(tok) ---@type string, string[]
local _, args = token_ident_and_args(tok) local tok_line = line_of(body_off + bt.rel) or 0 ---@type integer
--- @type integer
local tok_line = line_of(body_off + bt.rel) or 0
if M.DELAY_MARKERS[ident] then if M.DELAY_MARKERS[ident] then
emit_marker("delay", ident, nil, tok_line) emit_marker("delay", ident, nil, tok_line)
--- @type string local rest = tok:sub(after or (#tok + 1)) ---@type string
local rest = tok:sub(after or (#tok + 1))
while true do while true do
rest = M.trim(rest) rest = M.trim(rest)
if rest:sub(1, 2) == "//" then if rest:sub(1, 2) == "//" then
--- @type integer|nil local nl = rest:find("\n") ---@type integer|nil
local nl = rest:find("\n")
rest = nl and rest:sub(nl + 1) or "" rest = nl and rest:sub(nl + 1) or ""
elseif rest:sub(1, 2) == "/*" then elseif rest:sub(1, 2) == "/*" then
--- @type integer|nil local close = rest:find("*/", 3, true) ---@type integer|nil
local close = rest:find("*/", 3, true)
if not close then rest = ""; break end if not close then rest = ""; break end
rest = rest:sub(close + 2) rest = rest:sub(close + 2)
else else
@@ -803,8 +695,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. -- 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. -- 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`. -- `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 ---@type string
local consuming_encoder_for_markers = (ident == "jump_rel") and "branch_equal" or ident
if ident ~= "atom_label" and ident ~= "atom_offset" then if ident ~= "atom_label" and ident ~= "atom_offset" then
emit_embedded_markers(tok, tok_line, consuming_encoder_for_markers) emit_embedded_markers(tok, tok_line, consuming_encoder_for_markers)
end end
@@ -817,29 +708,23 @@ local function _project_emission_inner(root_body_entry, ctx_table)
-- MipsCode formals (nop_slot1, …): the ident is a sub_map key. -- MipsCode formals (nop_slot1, …): the ident is a sub_map key.
-- Re-process the replacement token so load_word(...) becomes a real encoder. -- 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 if sub_map and type(sub_map[ident]) == "string" and sub_map[ident] ~= ident then
--- @type string local repl = M.trim(sub_map[ident]) ---@type string
local repl = M.trim(sub_map[ident])
if repl ~= "" then if repl ~= "" then
process_token({ tok = repl, rel = bt.rel }) process_token({ tok = repl, rel = bt.rel })
return return
end end
end end
if ident:sub(1, 4) == "mac_" then if ident:sub(1, 4) == "mac_" then
--- @type string local bare = ident:sub(5) ---@type string
local bare = ident:sub(5) local comp = ctx_table.component_index[bare] ---@type ComponentBodyEntry|nil
--- @type ComponentBodyEntry|nil
local comp = ctx_table.component_index[bare]
if comp then if comp then
--- @type string local invocation_root_call_text = walk_root_call_text or tok ---@type string
local invocation_root_call_text = walk_root_call_text or tok
if ctx_table.visiting[bare] then if ctx_table.visiting[bare] then
-- Cycle: still allocate inv_id, emit zero-width begin/end, record the cycle error; do NOT recurse. -- 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) ---@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.parent_id = walk_parent_inv_id
inv.call_text = tok inv.call_text = tok
--- @type EmitError local err = { ---@type EmitError
local err = {
kind = "cycle", kind = "cycle",
msg = string.format("project_emission: component cycle detected: %q", bare), msg = string.format("project_emission: component cycle detected: %q", bare),
source = def_source, source = def_source,
@@ -852,8 +737,7 @@ local function _project_emission_inner(root_body_entry, ctx_table)
end end
-- First visit: descend + count + count_mismatch-check below. -- First visit: descend + count + count_mismatch-check below.
ctx_table.visiting[bare] = true 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) ---@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.parent_id = walk_parent_inv_id
inv.call_text = tok inv.call_text = tok
inv.def_path = comp.source inv.def_path = comp.source
@@ -861,15 +745,12 @@ local function _project_emission_inner(root_body_entry, ctx_table)
-- Propagate trackers into the recursive walk: -- Propagate trackers into the recursive walk:
-- immediate_call_text = this call's tok (the IMMEDIATE outer call for words emitted in this body) -- 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) -- root_call_text = the OUTERMOST call (immutable across the recursion)
--- @type string[]|nil local formal_names = ctx_table.component_index[bare] ---@type string[]|nil
local formal_names = ctx_table.component_index[bare]
and ctx_table.component_index[bare].arg_names and ctx_table.component_index[bare].arg_names
--- @type table<string, string>|nil local child_map = nil ---@type table<string, string>|nil
local child_map = nil
if formal_names then if formal_names then
child_map = {} child_map = {}
--- @type integer, string for i, fname in ipairs(formal_names) do ---@type integer, string
for i, fname in ipairs(formal_names) do
child_map[fname] = apply_sub(sub_map, args[i]) child_map[fname] = apply_sub(sub_map, args[i])
end end
end end
@@ -887,12 +768,9 @@ local function _project_emission_inner(root_body_entry, ctx_table)
ctx_table.visiting[bare] = nil ctx_table.visiting[bare] = nil
emit_invoke_end(inv) emit_invoke_end(inv)
-- Count `word` items inside [start_word, end_word]. -- Count `word` items inside [start_word, end_word].
--- @type integer local wc_inside = 0 ---@type integer
local wc_inside = 0 for i = inv.start_word, inv.end_word do ---@type integer
--- @type integer local it = items[i] ---@type EmissionItem|nil
for i = inv.start_word, inv.end_word do
--- @type EmissionItem|nil
local it = items[i]
if it and it.kind == "word" then if it and it.kind == "word" then
wc_inside = wc_inside + 1 wc_inside = wc_inside + 1
end end
@@ -900,11 +778,9 @@ local function _project_emission_inner(root_body_entry, ctx_table)
inv.word_count = wc_inside inv.word_count = wc_inside
-- count_mismatch is a construction error: word_counts["mac_X"] is the declared count populated by the components pass; -- 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. -- We compare against the measured word count.
--- @type integer|nil local declared = ctx_table.word_counts["mac_" .. bare] ---@type integer|nil
local declared = ctx_table.word_counts["mac_" .. bare]
if declared and wc_inside ~= declared then if declared and wc_inside ~= declared then
--- @type EmitError local err = { ---@type EmitError
local err = {
kind = "count_mismatch", kind = "count_mismatch",
msg = string.format("project_emission: mac_%s declared=%d measured=%d", bare, declared, wc_inside), msg = string.format("project_emission: mac_%s declared=%d measured=%d", bare, declared, wc_inside),
source = def_source, source = def_source,
@@ -919,18 +795,14 @@ local function _project_emission_inner(root_body_entry, ctx_table)
end end
-- Direct encoder, or mac_X-without-component: resolve count + emit n words. -- Direct encoder, or mac_X-without-component: resolve count + emit n words.
-- Resolve_count may emit a warning if the count is unresolved. -- Resolve_count may emit a warning if the count is unresolved.
--- @type integer local n = resolve_count(ident, tok_line) ---@type integer
local n = resolve_count(ident, tok_line) local out_ident = (ident == "nop2") and "nop" or ident ---@type string
--- @type string for _ = 1, n do ---@type integer
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) emit_word(out_ident, args, tok_line, tok, def_source, def_line, walk_immediate_call_text, walk_root_call_text, sub_map)
end end
end end
--- @type integer, BodyToken for _, bt in ipairs(tokens) do ---@type integer, BodyToken
for _, bt in ipairs(tokens) do
process_token(bt) process_token(bt)
end end
end end
@@ -1025,8 +897,7 @@ function M.project_emission(body_text, component_index, word_counts, components,
} }
end end
--- @type BodyToken[] local tokens = M.tokenize_body(body_text) ---@type BodyToken[]
local tokens = M.tokenize_body(body_text)
return _project_emission_inner({ return _project_emission_inner({
body_tokens = tokens, body_tokens = tokens,
body_off = 0, body_off = 0,
@@ -1064,26 +935,21 @@ end
--- @param slice_mips_code_len integer --- @param slice_mips_code_len integer
--- @return string|nil, string|nil --- @return string|nil, string|nil
function M.find_function_decl_for(source, before_pos, slice_mips_code_len) function M.find_function_decl_for(source, before_pos, slice_mips_code_len)
--- @type integer local search_pos = 1 ---@type integer
local search_pos = 1 local last_match = nil ---@type integer|nil
--- @type integer|nil
local last_match = nil
while true do while true do
--- @type integer|nil local found = source:find("Slice_MipsCode", search_pos, true) ---@type integer|nil
local found = source:find("Slice_MipsCode", search_pos, true)
if not found or found >= before_pos then break end if not found or found >= before_pos then break end
last_match = found last_match = found
search_pos = found + slice_mips_code_len search_pos = found + slice_mips_code_len
end end
if not last_match then return nil, nil end if not last_match then return nil, nil end
--- @type integer local pos = last_match + slice_mips_code_len ---@type integer
local pos = last_match + slice_mips_code_len
while pos < before_pos do while pos < before_pos do
-- skip whitespace -- skip whitespace
while pos <= #source do while pos <= #source do
--- @type string local c = source:sub(pos, pos) ---@type string
local c = source:sub(pos, pos)
if c == " " or c == "\t" or c == "\n" or c == "\r" then if c == " " or c == "\t" or c == "\n" or c == "\r" then
pos = pos + 1 pos = pos + 1
else else
@@ -1099,22 +965,18 @@ function M.find_function_decl_for(source, before_pos, slice_mips_code_len)
end end
-- skip block comments -- skip block comments
if source:sub(pos, pos + 1) == "/*" then if source:sub(pos, pos + 1) == "/*" then
--- @type integer|nil local close = source:find("*/", pos + 2, true) ---@type integer|nil
local close = source:find("*/", pos + 2, true)
if not close then break end if not close then break end
pos = close + 2 pos = close + 2
goto continue goto continue
end end
-- try to read an ident -- try to read an ident
--- @type string|nil, integer local ident, ident_end = M.read_ident(source, pos) ---@type string|nil, integer
local ident, ident_end = M.read_ident(source, pos)
if not ident then break end if not ident then break end
-- check if the next non-ws char after ident is "(" -- check if the next non-ws char after ident is "("
--- @type integer local next_pos = M.skip_ws_and_cmt(source, ident_end) ---@type integer
local next_pos = M.skip_ws_and_cmt(source, ident_end)
if source:sub(next_pos, next_pos) == "(" then if source:sub(next_pos, next_pos) == "(" then
--- @type string|nil local inner = M.read_parens(source, next_pos) ---@type string|nil
local inner = M.read_parens(source, next_pos)
if inner then if inner then
return ident, inner return ident, inner
end end
@@ -1145,27 +1007,22 @@ end
--- @param mips_atom_ptr_len integer --- @param mips_atom_ptr_len integer
--- @return string|nil, string|nil, string|nil, integer|nil --- @return string|nil, string|nil, string|nil, integer|nil
function M.find_atom_proc_decl_for(source, before_pos, mips_atom_ptr_len) function M.find_atom_proc_decl_for(source, before_pos, mips_atom_ptr_len)
--- @type integer local search_pos = 1 ---@type integer
local search_pos = 1 local last_match = nil ---@type integer|nil
--- @type integer|nil
local last_match = nil
while true do while true do
-- plain=true: "*" is literal, no escaping needed -- plain=true: "*" is literal, no escaping needed
--- @type integer|nil local found = source:find("MipsAtom*", search_pos, true) ---@type integer|nil
local found = source:find("MipsAtom*", search_pos, true)
if not found or found >= before_pos then break end if not found or found >= before_pos then break end
last_match = found last_match = found
search_pos = found + mips_atom_ptr_len search_pos = found + mips_atom_ptr_len
end end
if not last_match then return nil, nil end if not last_match then return nil, nil end
--- @type integer local pos = last_match + mips_atom_ptr_len ---@type integer
local pos = last_match + mips_atom_ptr_len
while pos < before_pos do while pos < before_pos do
-- skip whitespace -- skip whitespace
while pos <= #source do while pos <= #source do
--- @type string local c = source:sub(pos, pos) ---@type string
local c = source:sub(pos, pos)
if c == " " or c == "\t" or c == "\n" or c == "\r" then if c == " " or c == "\t" or c == "\n" or c == "\r" then
pos = pos + 1 pos = pos + 1
else else
@@ -1181,22 +1038,18 @@ function M.find_atom_proc_decl_for(source, before_pos, mips_atom_ptr_len)
end end
-- skip block comments -- skip block comments
if source:sub(pos, pos + 1) == "/*" then if source:sub(pos, pos + 1) == "/*" then
--- @type integer|nil local close = source:find("*/", pos + 2, true) ---@type integer|nil
local close = source:find("*/", pos + 2, true)
if not close then break end if not close then break end
pos = close + 2 pos = close + 2
goto continue goto continue
end end
-- try to read an ident -- try to read an ident
--- @type string|nil, integer local ident, ident_end = M.read_ident(source, pos) ---@type string|nil, integer
local ident, ident_end = M.read_ident(source, pos)
if not ident then break end if not ident then break end
-- check if the next non-ws char after ident is "(" -- check if the next non-ws char after ident is "("
--- @type integer local next_pos = M.skip_ws_and_cmt(source, ident_end) ---@type integer
local next_pos = M.skip_ws_and_cmt(source, ident_end)
if source:sub(next_pos, next_pos) == "(" then if source:sub(next_pos, next_pos) == "(" then
--- @type string|nil, integer local inner, after_paren = M.read_parens(source, next_pos) ---@type string|nil, integer
local inner, after_paren = M.read_parens(source, next_pos)
if inner then if inner then
return ident, inner, ident, after_paren return ident, inner, ident, after_paren
end end
@@ -1209,3 +1062,4 @@ function M.find_atom_proc_decl_for(source, before_pos, mips_atom_ptr_len)
end end
return M return M