diff --git a/scripts/duffle.lua b/scripts/duffle.lua index fae54c4..583f2a3 100644 --- a/scripts/duffle.lua +++ b/scripts/duffle.lua @@ -6,7 +6,7 @@ local scan = require("duffle_scan") ---@type DuffleExport local isa = require("duffle_isa") ---@type DuffleExport local emit = require("duffle_emit") ---@type DuffleExport -local M = {} ---@type DuffleExport +local M = {} ---@type DuffleExport --- @alias Path string --- @alias LineNum integer diff --git a/scripts/duffle_emit.lua b/scripts/duffle_emit.lua index b7c137d..0292b36 100644 --- a/scripts/duffle_emit.lua +++ b/scripts/duffle_emit.lua @@ -1,14 +1,9 @@ --- 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 +local scan = require("duffle_scan") ---@type DuffleScan +local isa = require("duffle_isa") ---@type DuffleIsa +local M = {} ---@type DuffleEmit +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 ---@type string, any -- 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. -- 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 +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 ---@type integer -- Map an open-delimiter byte to its matching close string for read_balanced. ---- @type table -- bag: open-delimiter byte -> close string -local E_OPEN_CLOSE = { +local E_OPEN_CLOSE = { ---@type table -- bag: open-delimiter byte -> close string [E_BYTE_OPEN_PAREN] = ")", [E_BYTE_OPEN_BRACE] = "}", [E_BYTE_OPEN_BRACK] = "]", @@ -79,23 +67,16 @@ local E_OPEN_CLOSE = { --- @param inner string --- @return string[] local function split_call_args(inner) ---- @type string[] - local args = {} + local args = {} ---@type string[] if not inner or inner == "" then return args end ---- @type integer - local pos = 1 ---- @type integer - local len = #inner ---- @type integer - local start = 1 + local pos = 1 ---@type integer + local len = #inner ---@type integer + local start = 1 ---@type integer while pos <= len do ---- @type integer - local c = inner:byte(pos) ---- @type string|nil - local close = E_OPEN_CLOSE[c] + local c = inner:byte(pos) ---@type integer + local close = E_OPEN_CLOSE[c] ---@type string|nil if close then ---- @type integer, integer - local _, after = M.read_balanced(inner, string.char(c), close, pos) + local _, after = M.read_balanced(inner, string.char(c), close, pos) ---@type integer, integer pos = after elseif c == E_BYTE_DQUOTE or c == E_BYTE_SQUOTE then pos = M.skip_str_or_cmt(inner, pos) @@ -116,23 +97,18 @@ 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) + local ident, after = M.read_ident(tok, 1) ---@type string|nil, integer if not ident then return "?", {} end ---- @type integer - local paren_pos = M.skip_ws_and_cmt(tok, after) + local paren_pos = M.skip_ws_and_cmt(tok, after) ---@type integer if tok:sub(paren_pos, paren_pos) ~= "(" then return ident, {} end ---- @type string|nil - local inner = M.read_parens(tok, paren_pos) + local inner = M.read_parens(tok, paren_pos) ---@type string|nil 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 +local E_MAC_PREFIX = "mac_" ---@type string +local E_MAC_PREFIX_LEN = 4 ---@type integer --- 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 --- @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 = {} + local items = {} ---@type EmissionItem[] + local word_events = {} ---@type WordEvent[] + local markers = {} ---@type EmissionMarker[] + local invocations = {} ---@type InvocationRecord[] + local errors = {} ---@type EmitError[] + local warnings = {} ---@type EmitWarning[] ---- @type integer - local word_idx = 0 ---- @type InvocationRecord[] - local invocation_stack = {} -- stack of currently-open invocation records ---- @type integer - local next_inv_id = 0 + local word_idx = 0 ---@type integer + local invocation_stack = {} ---@type InvocationRecord[] -- stack of currently-open invocation records + local next_inv_id = 0 ---@type integer ---- @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 + local reg_use_schema = ctx_table.reg_use_schema ---@type RegUseSchema|nil + local reg_use_param = ctx_table.reg_use_param ---@type string|nil + local atom_name = ctx_table.atom_name ---@type AtomName|nil ---- @type table -- bag: slot name -> readonly - local slot_readonly = {} + local slot_readonly = {} ---@type table -- bag: slot name -> readonly if reg_use_schema then ---- @type integer, RegUseSlot - for _, slot in ipairs(reg_use_schema.slots or {}) do + for _, slot in ipairs(reg_use_schema.slots or {}) do ---@type integer, RegUseSlot slot_readonly[slot.name] = slot.readonly == true end end @@ -259,13 +221,10 @@ local function _project_emission_inner(root_body_entry, ctx_table) 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) + local dot = operand:find(".", 1, true) ---@type integer|nil if dot then ---- @type string - local head = operand:sub(1, dot - 1) ---- @type string|nil - local mapped = sub_map[head] + local head = operand:sub(1, dot - 1) ---@type string + local mapped = sub_map[head] ---@type string|nil if type(mapped) == "string" then return mapped .. operand:sub(dot) end @@ -279,23 +238,18 @@ local function _project_emission_inner(root_body_entry, ctx_table) 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 .. "." + local prefix = reg_use_param .. "." ---@type string 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] + local member_path = operand:sub(#prefix + 1) ---@type string + local slot = reg_use_schema.alias_to_slot[member_path] ---@type string|nil 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 + local ids = {} ---@type integer[] + for _, inv in ipairs(invocation_stack) do ---@type integer, InvocationRecord ids[#ids + 1] = inv.id end return ids @@ -312,27 +266,19 @@ local function _project_emission_inner(root_body_entry, ctx_table) --- @param sub_map table|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 + local inv_ids = open_invocation_ids_snapshot() ---@type integer[] + local outermost = inv_ids[1] or 0 ---@type integer -- 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 + 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 ---@type string[]|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) + for pos, arg in ipairs(args or {}) do ---@type integer, string + local effective = apply_sub(sub_map, arg) ---@type any + local key, unresolved, slot = resolve_gpr_key(effective) ---@type string|nil, string|nil, string|nil gpr_keys[pos] = key if unresolved then errors[#errors + 1] = { @@ -343,11 +289,9 @@ 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) + local row = M.instr(encoder) ---@type InstructionRow|nil if row and row.writes then ---- @type integer, integer - for _, wpos in ipairs(row.writes) do + for _, wpos in ipairs(row.writes) do ---@type integer, integer if wpos == pos then errors[#errors + 1] = { kind = "reguse_const_write", @@ -364,26 +308,17 @@ 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" + local isa = M.instr(encoder) ---@type InstructionRow|nil + local isa_kind = isa and isa.kind or "unknown" ---@type string + local nop_words = (encoder == "nop" and 1) or (encoder == "nop2" and 2) or 0 ---@type integer + local is_yield = (encoder == "mac_yield" or encoder == "mac_yield_tail") ---@type boolean + local gp0_shape = type(encoder) == "string" ---@type string|nil 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") + 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") ---@type boolean items[#items + 1] = { kind = "word", encoder = encoder, @@ -443,10 +378,8 @@ local function _project_emission_inner(root_body_entry, ctx_table) 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 + local inv_ids = open_invocation_ids_snapshot() ---@type integer[] + local outermost = inv_ids[1] or 0 ---@type integer -- 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 -- (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 return end ---- @type EmissionItem - local it = { + local it = { ---@type EmissionItem kind = kind, name = name, line = line, @@ -488,28 +420,21 @@ local function _project_emission_inner(root_body_entry, ctx_table) --- @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 + local depth = 0 ---@type integer + local count = 0 ---@type integer + local i = from_pos ---@type integer while i < to_pos do ---- @type string - local c = tok:sub(i, i) + local c = tok:sub(i, i) ---@type string if c == "'" or c == '"' then ---- @type integer - local next_pos = M.skip_str_or_cmt(tok, i) + local next_pos = M.skip_str_or_cmt(tok, i) ---@type integer 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) + local nl = tok:find("\n", i, true) ---@type integer|nil 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) + local close = tok:find("*/", i + 2, true) ---@type integer|nil i = (close and close + 2) or (#tok + 1) elseif c == "(" then depth = depth + 1 @@ -532,11 +457,9 @@ local function _project_emission_inner(root_body_entry, ctx_table) --- @param tok string --- @return integer|nil local function find_consuming_paren(tok) ---- @type integer - local i = 1 + local i = 1 ---@type integer while i <= #tok do ---- @type string - local c = tok:sub(i, i) + local c = tok:sub(i, i) ---@type string if c == "(" then return i end if not c:match("[%w_]") and c ~= " " then return nil end 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) -- 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 + local consuming_paren = nil ---@type integer|nil if consuming_encoder then consuming_paren = find_consuming_paren(tok) end ---- @type integer - local pos = 1 + local pos = 1 ---@type integer 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) + local ident, after = M.read_ident(tok, pos) ---@type string|nil, integer 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) + local next_pos = M.skip_str_or_cmt(tok, pos) ---@type integer 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 + local arg_pos = nil ---@type integer|nil if consuming_encoder and consuming_paren then arg_pos = count_top_level_commas(tok, consuming_paren + 1, pos) + 1 end @@ -585,10 +503,8 @@ 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) + local open = M.skip_ws_and_cmt(tok, after) ---@type integer + local inner, after_paren = M.read_parens(tok, open) ---@type string|nil, integer if not inner then -- (...) Unreadable: fall back to non-marker behavior. 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 -- (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 + local arg_pos = nil ---@type integer|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) + local args = split_call_args(inner) ---@type string[] 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) 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]` -- (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|nil - local components = ctx_table.components ---- @type ComponentDef|nil - local component_def = components and components[component_name] or nil + local components = ctx_table.components ---@type table|nil + local component_def = components and components[component_name] or nil ---@type ComponentDef|nil if not component_def then 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). " @@ -642,10 +554,8 @@ 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 = { + local debug_skip_stamp = component_def.debug_skip == true ---@type boolean + local inv = { ---@type InvocationRecord id = next_inv_id, parent_id = 0, -- patched below by caller kind = inv_kind, @@ -692,8 +602,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 + for i = #invocation_stack, 1, -1 do ---@type integer if invocation_stack[i] == inv then table.remove(invocation_stack, i) break @@ -707,11 +616,9 @@ local function _project_emission_inner(root_body_entry, ctx_table) --- @param tok_line integer --- @return integer local function resolve_count(ident, tok_line) ---- @type WordCounts|nil - local wc = ctx_table.word_counts + local wc = ctx_table.word_counts ---@type WordCounts|nil if wc and wc[ident] then return wc[ident] end ---- @type string - local canon = M.gte_canon(ident) + local canon = M.gte_canon(ident) ---@type string if canon ~= ident and wc and wc[canon] then return wc[canon] end warnings[#warnings + 1] = { kind = "uncounted", @@ -734,60 +641,45 @@ local function _project_emission_inner(root_body_entry, ctx_table) --- @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|nil - local sub_map = body_entry.sub_map + local tokens = body_entry.body_tokens or {} ---@type BodyToken[] + local body_off = body_entry.body_off or 0 ---@type integer + local line_of = body_entry.line_of or M.LineIndex("") ---@type LineIndexFn + local def_source = body_entry.source or "" ---@type string + local def_line = body_entry.declaration or 0 ---@type integer + local sub_map = body_entry.sub_map ---@type table|nil -- 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 "") + local tok = M.trim(bt.tok or "") ---@type string -- 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") + local nl = tok:find("\n") ---@type integer|nil 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) + local close = tok:find("*/", 3, true) ---@type integer|nil tok = M.trim(close and tok:sub(close + 2) or "") else break end end if tok == "" then return end ---- @type string|nil, integer - local ident, after = M.read_ident(tok, 1) + local ident, after = M.read_ident(tok, 1) ---@type string|nil, integer 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 + local _, args = token_ident_and_args(tok) ---@type string, string[] + local tok_line = line_of(body_off + bt.rel) or 0 ---@type integer if M.DELAY_MARKERS[ident] then emit_marker("delay", ident, nil, tok_line) ---- @type string - local rest = tok:sub(after or (#tok + 1)) + local rest = tok:sub(after or (#tok + 1)) ---@type string while true do rest = M.trim(rest) if rest:sub(1, 2) == "//" then ---- @type integer|nil - local nl = rest:find("\n") + local nl = rest:find("\n") ---@type integer|nil rest = nl and rest:sub(nl + 1) or "" elseif rest:sub(1, 2) == "/*" then ---- @type integer|nil - local close = rest:find("*/", 3, true) + local close = rest:find("*/", 3, true) ---@type integer|nil if not close then rest = ""; break end rest = rest:sub(close + 2) 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. -- 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 + local consuming_encoder_for_markers = (ident == "jump_rel") and "branch_equal" or ident ---@type string if ident ~= "atom_label" and ident ~= "atom_offset" then emit_embedded_markers(tok, tok_line, consuming_encoder_for_markers) 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. -- 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]) + local repl = M.trim(sub_map[ident]) ---@type string if repl ~= "" then process_token({ tok = repl, rel = bt.rel }) return 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] + local bare = ident:sub(5) ---@type string + local comp = ctx_table.component_index[bare] ---@type ComponentBodyEntry|nil if comp then ---- @type string - local invocation_root_call_text = walk_root_call_text or tok + local invocation_root_call_text = walk_root_call_text or tok ---@type string 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) + local inv = emit_invoke_begin(comp.kind or "comp_bare", bare, tok, invocation_root_call_text, def_source, tok_line) ---@type InvocationRecord inv.parent_id = walk_parent_inv_id inv.call_text = tok ---- @type EmitError - local err = { + local err = { ---@type EmitError kind = "cycle", msg = string.format("project_emission: component cycle detected: %q", bare), source = def_source, @@ -852,8 +737,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) + local inv = emit_invoke_begin(comp.kind or "comp_bare", bare, tok, invocation_root_call_text, def_source, tok_line) ---@type InvocationRecord inv.parent_id = walk_parent_inv_id inv.call_text = tok 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: -- 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] + local formal_names = ctx_table.component_index[bare] ---@type string[]|nil and ctx_table.component_index[bare].arg_names ---- @type table|nil - local child_map = nil + local child_map = nil ---@type table|nil if formal_names then child_map = {} ---- @type integer, string - for i, fname in ipairs(formal_names) do + for i, fname in ipairs(formal_names) do ---@type integer, string child_map[fname] = apply_sub(sub_map, args[i]) end end @@ -887,12 +768,9 @@ 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] + local wc_inside = 0 ---@type integer + for i = inv.start_word, inv.end_word do ---@type integer + local it = items[i] ---@type EmissionItem|nil if it and it.kind == "word" then wc_inside = wc_inside + 1 end @@ -900,11 +778,9 @@ 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] + local declared = ctx_table.word_counts["mac_" .. bare] ---@type integer|nil if declared and wc_inside ~= declared then ---- @type EmitError - local err = { + local err = { ---@type EmitError kind = "count_mismatch", msg = string.format("project_emission: mac_%s declared=%d measured=%d", bare, declared, wc_inside), source = def_source, @@ -919,18 +795,14 @@ 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 + local n = resolve_count(ident, tok_line) ---@type integer + local out_ident = (ident == "nop2") and "nop" or ident ---@type string + for _ = 1, n do ---@type integer 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 + for _, bt in ipairs(tokens) do ---@type integer, BodyToken process_token(bt) end end @@ -1025,8 +897,7 @@ function M.project_emission(body_text, component_index, word_counts, components, } end ---- @type BodyToken[] - local tokens = M.tokenize_body(body_text) + local tokens = M.tokenize_body(body_text) ---@type BodyToken[] return _project_emission_inner({ body_tokens = tokens, body_off = 0, @@ -1064,26 +935,21 @@ end --- @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 + local search_pos = 1 ---@type integer + local last_match = nil ---@type integer|nil while true do ---- @type integer|nil - local found = source:find("Slice_MipsCode", search_pos, true) + local found = source:find("Slice_MipsCode", search_pos, true) ---@type integer|nil if not found or found >= before_pos then break end last_match = found search_pos = found + slice_mips_code_len end if not last_match then return nil, nil end ---- @type integer - local pos = last_match + slice_mips_code_len + local pos = last_match + slice_mips_code_len ---@type integer while pos < before_pos do -- skip whitespace while pos <= #source do ---- @type string - local c = source:sub(pos, pos) + local c = source:sub(pos, pos) ---@type string if c == " " or c == "\t" or c == "\n" or c == "\r" then pos = pos + 1 else @@ -1099,22 +965,18 @@ 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) + local close = source:find("*/", pos + 2, true) ---@type integer|nil 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) + local ident, ident_end = M.read_ident(source, pos) ---@type string|nil, integer 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) + local next_pos = M.skip_ws_and_cmt(source, ident_end) ---@type integer if source:sub(next_pos, next_pos) == "(" then ---- @type string|nil - local inner = M.read_parens(source, next_pos) + local inner = M.read_parens(source, next_pos) ---@type string|nil if inner then return ident, inner end @@ -1145,27 +1007,22 @@ end --- @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 + local search_pos = 1 ---@type integer + local last_match = nil ---@type integer|nil while true do -- plain=true: "*" is literal, no escaping needed ---- @type integer|nil - local found = source:find("MipsAtom*", search_pos, true) + local found = source:find("MipsAtom*", search_pos, true) ---@type integer|nil if not found or found >= before_pos then break end last_match = found search_pos = found + mips_atom_ptr_len end if not last_match then return nil, nil end ---- @type integer - local pos = last_match + mips_atom_ptr_len + local pos = last_match + mips_atom_ptr_len ---@type integer while pos < before_pos do -- skip whitespace while pos <= #source do ---- @type string - local c = source:sub(pos, pos) + local c = source:sub(pos, pos) ---@type string if c == " " or c == "\t" or c == "\n" or c == "\r" then pos = pos + 1 else @@ -1181,22 +1038,18 @@ 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) + local close = source:find("*/", pos + 2, true) ---@type integer|nil 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) + local ident, ident_end = M.read_ident(source, pos) ---@type string|nil, integer 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) + local next_pos = M.skip_ws_and_cmt(source, ident_end) ---@type integer if source:sub(next_pos, next_pos) == "(" then ---- @type string|nil, integer - local inner, after_paren = M.read_parens(source, next_pos) + local inner, after_paren = M.read_parens(source, next_pos) ---@type string|nil, integer if inner then return ident, inner, ident, after_paren end @@ -1209,3 +1062,4 @@ function M.find_atom_proc_decl_for(source, before_pos, mips_atom_ptr_len) end return M + diff --git a/scripts/duffle_scan.lua b/scripts/duffle_scan.lua index 7db3b00..218f5aa 100644 --- a/scripts/duffle_scan.lua +++ b/scripts/duffle_scan.lua @@ -78,34 +78,34 @@ local lfs = require("lfs") ---@type LfsMod -- ASCII byte constants -- ════════════════════════════════════════════════════════════════════════════ -local BYTE_SPACE = 0x20 ---@type integer -- ' ' -local BYTE_TAB = 0x09 ---@type integer -- '\t' -local BYTE_NEWLINE = 0x0A ---@type integer -- '\n' -local BYTE_CR = 0x0D ---@type integer -- '\r' -local BYTE_VT = 0x0B ---@type integer -- '\v' -local BYTE_FF = 0x0C ---@type integer -- '\f' +local BYTE_SPACE = 0x20 ---@type integer -- ' ' +local BYTE_TAB = 0x09 ---@type integer -- '\t' +local BYTE_NEWLINE = 0x0A ---@type integer -- '\n' +local BYTE_CR = 0x0D ---@type integer -- '\r' +local BYTE_VT = 0x0B ---@type integer -- '\v' +local BYTE_FF = 0x0C ---@type integer -- '\f' -local BYTE_UNDERSCORE = 0x5F ---@type integer -- '_' -local BYTE_DOT = 0x2E ---@type integer -- '.' -local BYTE_SLASH = 0x2F ---@type integer -- '/' -local BYTE_BACKSLASH = 0x5C ---@type integer -- '\\' -local BYTE_STAR = 0x2A ---@type integer -- '*' -local BYTE_DQUOTE = 0x22 ---@type integer -- '"' -local BYTE_SQUOTE = 0x27 ---@type integer -- '\'' -local BYTE_COMMA = 0x2C ---@type integer -- ',' -local BYTE_SEMI = 0x3B ---@type integer -- ';' +local BYTE_UNDERSCORE = 0x5F ---@type integer -- '_' +local BYTE_DOT = 0x2E ---@type integer -- '.' +local BYTE_SLASH = 0x2F ---@type integer -- '/' +local BYTE_BACKSLASH = 0x5C ---@type integer -- '\\' +local BYTE_STAR = 0x2A ---@type integer -- '*' +local BYTE_DQUOTE = 0x22 ---@type integer -- '"' +local BYTE_SQUOTE = 0x27 ---@type integer -- '\'' +local BYTE_COMMA = 0x2C ---@type integer -- ',' +local BYTE_SEMI = 0x3B ---@type integer -- ';' -local BYTE_OPEN_PAREN = 0x28 ---@type integer -- '(' -local BYTE_OPEN_BRACE = 0x7B ---@type integer -- '{' -local BYTE_OPEN_BRACK = 0x5B ---@type integer -- '[' +local BYTE_OPEN_PAREN = 0x28 ---@type integer -- '(' +local BYTE_OPEN_BRACE = 0x7B ---@type integer -- '{' +local BYTE_OPEN_BRACK = 0x5B ---@type integer -- '[' -local BYTE_LOWER_A = 0x61 ---@type integer -- 'a' -local BYTE_LOWER_Z = 0x7A ---@type integer -- 'z' -local BYTE_UPPER_A = 0x41 ---@type integer -- 'A' -local BYTE_UPPER_Z = 0x5A ---@type integer -- 'Z' +local BYTE_LOWER_A = 0x61 ---@type integer -- 'a' +local BYTE_LOWER_Z = 0x7A ---@type integer -- 'z' +local BYTE_UPPER_A = 0x41 ---@type integer -- 'A' +local BYTE_UPPER_Z = 0x5A ---@type integer -- 'Z' -local BYTE_DIGIT_0 = 0x30 ---@type integer -- '0' -local BYTE_DIGIT_9 = 0x39 ---@type integer -- '9' +local BYTE_DIGIT_0 = 0x30 ---@type integer -- '0' +local BYTE_DIGIT_9 = 0x39 ---@type integer -- '9' -- ════════════════════════════════════════════════════════════════════════════ -- Section -1: Bootstrap (path-setup at module load) @@ -134,17 +134,17 @@ local digit_pat = R("09") ---@type LpegPattern local lpeg_alnum_pat = alpha_pat + digit_pat ---@type LpegPattern -- Identifier: alpha followed by zero+ alnum. Capture as a string. -local lpeg_alpha_pat = alpha_pat ---@type LpegPattern +local lpeg_alpha_pat = alpha_pat ---@type LpegPattern local lpeg_ident_pat = lpeg.C(alpha_pat * lpeg_alnum_pat^0) ---@type LpegPattern -local lpeg_str_pat = P('"') * (P(1) - S('"\\') + P('\\') * P(1))^0 * P('"') ---@type LpegPattern -- String literal: "..." with backslash escapes. -local lpeg_chr_pat = P("'") * (P(1) - S("'\\") + P('\\') * P(1))^0 * P("'") ---@type LpegPattern -- Char literal: '...' with backslash escapes. -local lpeg_line_cmt_pat = P("//") * (P(1) - S("\n"))^0 ---@type LpegPattern -- Line comment: // ... to end-of-line. -local lpeg_block_cmt_pat = P("/*") * (P(1) - P("*/"))^0 * P("*/") ---@type LpegPattern -- Block comment: /* ... */ (no nesting per C standard). +local lpeg_str_pat = P('"') * (P(1) - S('"\\') + P('\\') * P(1))^0 * P('"') ---@type LpegPattern -- String literal: "..." with backslash escapes. +local lpeg_chr_pat = P("'") * (P(1) - S("'\\") + P('\\') * P(1))^0 * P("'") ---@type LpegPattern -- Char literal: '...' with backslash escapes. +local lpeg_line_cmt_pat = P("//") * (P(1) - S("\n"))^0 ---@type LpegPattern -- Line comment: // ... to end-of-line. +local lpeg_block_cmt_pat = P("/*") * (P(1) - P("*/"))^0 * P("*/") ---@type LpegPattern -- Block comment: /* ... */ (no nesting per C standard). local lpeg_str_or_cmt_pat = lpeg_str_pat + lpeg_chr_pat + lpeg_line_cmt_pat + lpeg_block_cmt_pat ---@type LpegPattern -- String or comment (any of the four forms). -- Whitespace + comment skipper: zero+ (whitespace run | string | comment). -local ws_pat = S(" \t\n\r\v\f") ---@type LpegPattern +local ws_pat = S(" \t\n\r\v\f") ---@type LpegPattern local lpeg_ws_and_cmt_pat = (ws_pat + lpeg_str_or_cmt_pat)^0 ---@type LpegPattern -- Generic "skip until target, but step over balanced groups" matcher. @@ -239,7 +239,7 @@ end --- @param path Path --- @return Path function M.dirname(path) - local last_sep = 0 ---@type integer + local last_sep = 0 ---@type integer for pos = 1, #path do ---@type integer local b = path:byte(pos) ---@type integer if b == BYTE_SLASH or b == BYTE_BACKSLASH then last_sep = pos end @@ -252,14 +252,14 @@ end --- @param path Path --- @return string function M.basename_no_ext(path) - local last_sep = 0 ---@type integer + local last_sep = 0 ---@type integer for pos = 1, #path do ---@type integer local b = path:byte(pos) ---@type integer if b == BYTE_SLASH or b == BYTE_BACKSLASH then last_sep = pos end end local a = last_sep + 1 ---@type integer local last_dot = #path + 1 ---@type integer - for pos = #path, a, -1 do ---@type integer + for pos = #path, a, -1 do ---@type integer if path:byte(pos) == BYTE_DOT then last_dot = pos; break end end return path:sub(a, last_dot - 1) @@ -281,13 +281,13 @@ local function parse_path_root(input) end if input:sub(1, 2) == "//" then - local server_start = 3 ---@type integer + local server_start = 3 ---@type integer local server_end = M.find_byte(input, BYTE_SLASH, server_start) ---@type integer|nil if not server_end or server_end == server_start then error("UNC path requires //server/share: " .. input, 3) end local server = input:sub(server_start, server_end - 1) ---@type string - local share_start = server_end + 1 ---@type integer + local share_start = server_end + 1 ---@type integer while input:sub(share_start, share_start) == "/" do share_start = share_start + 1 end @@ -296,7 +296,7 @@ local function parse_path_root(input) error("UNC path requires //server/share: " .. input, 3) end local share = input:sub(share_start, share_end - 1) ---@type string - local rest = input:sub(share_end + 1) ---@type string + local rest = input:sub(share_end + 1) ---@type string while rest:sub(1, 1) == "/" do rest = rest:sub(2) end return { kind = "unc_absolute", @@ -323,8 +323,8 @@ function M.normalize_path(path) if path == "" then return "" end local root = parse_path_root(path:gsub("\\", "/")) ---@type PathRoot - local segments = {} ---@type string[] - for segment in root.rest:gmatch("[^/]+") do ---@type string + local segments = {} ---@type string[] + for segment in root.rest:gmatch("[^/]+") do ---@type string if segment == "." then -- no-op elseif segment == ".." then @@ -348,7 +348,7 @@ end --- @param path Path --- @return Path local function absolute_normalized_path(path) - local normalized = M.normalize_path(path) ---@type Path + local normalized = M.normalize_path(path) ---@type Path local root = parse_path_root(normalized) ---@type PathRoot if root.kind == "drive_relative" then error("drive-relative path cannot be resolved without a per-drive cwd: " .. normalized, 3) @@ -428,7 +428,7 @@ function M.to_absolute_path(path) if not cwd then _absolute_path_cache[path] = path; return path end cwd = cwd:gsub("/", "\\") local tail = (path:gsub("/", "\\")) ---@type string - local result = cwd .. "\\" .. tail ---@type string + local result = cwd .. "\\" .. tail ---@type string _absolute_path_cache[path] = result return result end @@ -452,7 +452,7 @@ end --- @param sources SourceFile[] --- @return table function M.group_sources_by_dir(sources) - local by_dir = {} ---@type table + local by_dir = {} ---@type table for _, src in ipairs(sources) do ---@type integer, SourceFile by_dir[src.dir] = by_dir[src.dir] or {} table.insert(by_dir[src.dir], src) @@ -552,7 +552,7 @@ M.read_brackets = function(s, pos) return M.read_balanced(s, "[", "]", pos) end --- @return integer|nil function M.scan_to_char(s, target, start) local target_byte = target:byte() ---@type integer - local pos = start ---@type integer + local pos = start ---@type integer while pos <= #s do local c = s:byte(pos) ---@type integer if c == target_byte then return pos end -- scan: ... | @@ -632,7 +632,7 @@ local function splice_c_lines(source) local line = 1 ---@type integer while pos <= #source do local byte = source:byte(pos) ---@type integer - local splice_len = nil ---@type integer|nil + local splice_len = nil ---@type integer|nil if byte == BYTE_BACKSLASH and source:byte(pos + 1) == BYTE_NEWLINE then splice_len = 2 elseif byte == BYTE_BACKSLASH and source:byte(pos + 1) == BYTE_CR and source:byte(pos + 2) == BYTE_NEWLINE then @@ -668,9 +668,9 @@ function M.parse_direct_quoted_includes(source_text) -- Each arm's effect on (pos, line_leading) is annotated at the branch site. -- Arm order: newline / horiz-space / '//' / '/*' / '"' / '\'' / '#' / default. local logical_text, physical_pos, physical_line = splice_c_lines(source_text) ---@type string, integer[], integer[] - local includes = {} ---@type QuotedInclude[] - local pos = 1 ---@type integer - local line_leading = true ---@type boolean + local includes = {} ---@type QuotedInclude[] + local pos = 1 ---@type integer + local line_leading = true ---@type boolean while pos <= #logical_text do local byte = logical_text:byte(pos) ---@type integer if byte == BYTE_NEWLINE then @@ -709,7 +709,7 @@ function M.parse_direct_quoted_includes(source_text) -- Full success pushes the record and jumps to ::directive_done:: without ever entering the not-include path. -- (All locals are pre-declared at the top of this arm because Lua forbids a goto from crossing a local declaration into its scope.) local hash_pos, directive_line, scan, ident, after_ident, after_quote ---@type integer, integer, integer|nil, string|nil, integer, integer - local include_path, physical_first, physical_last ---@type string, integer, integer + local include_path, physical_first, physical_last ---@type string, integer, integer hash_pos = pos directive_line = physical_line[hash_pos] or 1 scan = skip_directive_space(logical_text, pos + 1) @@ -802,9 +802,9 @@ function M.resolve_source_corpus(options) local code_root = M.normalize_path(project_root .. "/code") ---@type Path local code_root_key = M.canonical_path_key(code_root) ---@type string local root = load_source_record(options.unity_root) ---@type SourceFile - local source_order = { root } ---@type SourceFile[] - local sources_by_path = { [M.canonical_path_key(root.path)] = root, } ---@type table - local resolver = { ---@type SourceResolver + local source_order = { root } ---@type SourceFile[] + local sources_by_path = { [M.canonical_path_key(root.path)] = root, } ---@type table + local resolver = { ---@type SourceResolver resolved = { { include_path = nil, @@ -824,11 +824,11 @@ function M.resolve_source_corpus(options) for _, include in ipairs(M.parse_direct_quoted_includes(root.text)) do ---@type integer, QuotedInclude local candidate_a = absolute_normalized_path(root.dir .. "/" .. include.path) ---@type Path local candidate_b = absolute_normalized_path(code_root .. "/" .. include.path) ---@type Path - local key_a = M.canonical_path_key(candidate_a) ---@type string - local key_b = M.canonical_path_key(candidate_b) ---@type string - local inside_a = canonical_key_is_within(key_a, code_root_key) ---@type boolean - local inside_b = canonical_key_is_within(key_b, code_root_key) ---@type boolean - local evidence = { ---@type ResolverEvidence + local key_a = M.canonical_path_key(candidate_a) ---@type string + local key_b = M.canonical_path_key(candidate_b) ---@type string + local inside_a = canonical_key_is_within(key_a, code_root_key) ---@type boolean + local inside_b = canonical_key_is_within(key_b, code_root_key) ---@type boolean + local evidence = { ---@type ResolverEvidence include_path = include.path, include_text = include.include_text, root_source = root.path, @@ -854,9 +854,9 @@ function M.resolve_source_corpus(options) -- Boundary checks above deliberately precede every filesystem probe. local exists_a = inside_a and lfs.attributes(candidate_a, "mode") == "file" ---@type boolean local exists_b = inside_b and ((key_b == key_a and exists_a) or lfs.attributes(candidate_b, "mode") == "file") ---@type boolean - local selected = nil ---@type Path|nil - local selected_key = nil ---@type string|nil - local disposition = nil ---@type string|nil + local selected = nil ---@type Path|nil + local selected_key = nil ---@type string|nil + local disposition = nil ---@type string|nil if exists_a then selected = candidate_a selected_key = key_a @@ -1075,7 +1075,7 @@ function M.build_body_line_index(body) local index = {} ---@type table -- bag: byte offset -> 1-based line local len = #body ---@type integer local newline_count = 0 ---@type integer - for pos = 1, len do ---@type integer + for pos = 1, len do ---@type integer if pos > 1 then index[pos] = newline_count + 1 end diff --git a/scripts/elf_dwarf.lua b/scripts/elf_dwarf.lua index c48759a..495c3e5 100644 --- a/scripts/elf_dwarf.lua +++ b/scripts/elf_dwarf.lua @@ -392,7 +392,7 @@ end --- @return integer function M.read_sleb128_at(buf, pos) local value, shift = 0, 0 ---@type integer, integer - local len = #buf ---@type integer + local len = #buf ---@type integer while pos < len do local b = buf:byte(pos + 1) ---@type integer value = value + (b % 0x80) * (2 ^ shift) @@ -451,7 +451,7 @@ end --- @return string local function read_c_string_at(buf, off) local len = #buf ---@type integer - local start = off ---@type integer + local start = off ---@type integer while off < len and buf:byte(off + 1) ~= 0 do off = off + 1 end return buf:sub(start + 1, off) end @@ -466,7 +466,7 @@ end local function parse_abbrev_table(table_bytes, table_start) local table_end = M.find_abbrev_table_end(table_bytes, table_start) ---@type integer|nil if not table_end then return nil, "no terminator" end - local decls = {} ---@type AbbrevDecl[] + local decls = {} ---@type AbbrevDecl[] local pos = table_start ---@type integer while pos < table_end do local code, code_end = M.read_uleb128_at(table_bytes, pos) ---@type integer|nil, integer @@ -667,7 +667,7 @@ function M.read_ref_sig8(buf, pos) return M.read_u32_le(buf, pos), M.read_u32_le --- @param target_sig_hi integer -- high 4 bytes (LE) of the desired signature --- @return integer|nil, integer|nil -- unit offset, type_offset within the unit function M.find_type_unit_by_signature(info, target_sig_lo, target_sig_hi) - local pos = 0 ---@type integer + local pos = 0 ---@type integer local section_len = #info ---@type integer while pos + 4 < section_len do local unit_length = M.read_u32_le(info, pos) ---@type integer @@ -675,7 +675,7 @@ function M.find_type_unit_by_signature(info, target_sig_lo, target_sig_hi) return nil, nil -- DWARF64 not supported end -- unit_length is the body size, NOT including the 4-byte unit_length field itself. - local body_start = pos + 4 ---@type integer + local body_start = pos + 4 ---@type integer local body_end = body_start + unit_length ---@type integer if body_end > section_len then return nil, nil -- malformed @@ -754,11 +754,11 @@ end function M.read_elf_sections(elf_path, section_names) -- Initialize result with all requested names set to "" so callers can do `sections[X] -- or ""` for missing sections without nil-checks. - local result = {} ---@type table + local result = {} ---@type table for _, name in ipairs(section_names) do result[name] = "" end ---@type integer, string -- O(1) lookup set. - local wanted = {} ---@type table -- bag: requested section name -> true + local wanted = {} ---@type table -- bag: requested section name -> true for _, name in ipairs(section_names) do wanted[name] = true end ---@type integer, string -- Existence check (lfs.attributes avoids an io.open-vs-fail race). @@ -1006,7 +1006,7 @@ end --- @param n integer -- any integer (negative allowed) --- @return string function M.sleb128(n) - local bytes = {} ---@type string[] + local bytes = {} ---@type string[] local more = true ---@type boolean while more do local b = n % (LEB_DATA_MASK + 1) ---@type integer -- extract low 7 bits @@ -1045,8 +1045,8 @@ end --- @return integer function M.sleb128_size(n) local more = true ---@type boolean - local bytes = 0 ---@type integer - local v = n ---@type integer + local bytes = 0 ---@type integer + local v = n ---@type integer while more do local b = v % (LEB_DATA_MASK + 1) ---@type integer -- extract low 7 bits v = (v - b) / (LEB_DATA_MASK + 1) -- arithmetic shift right by 7 @@ -1091,8 +1091,8 @@ end --- @return table|nil function M.read_line_unit_file_table(elf_path) local sections = M.read_elf_sections(elf_path, { ".debug_line", ".debug_line_str" }) ---@type table - local line = sections[".debug_line"] ---@type string - local lstr = sections[".debug_line_str"] or "" ---@type string + local line = sections[".debug_line"] ---@type string + local lstr = sections[".debug_line_str"] or "" ---@type string if not line or line == "" then io.stderr:write("[elf_dwarf.read_line_unit_file_table] no .debug_line section in: " .. tostring(elf_path) .. "\n") return nil @@ -1115,7 +1115,7 @@ function M.read_line_unit_file_table(elf_path) --- @return integer local function read_form(buf, lstr_buf, p, form) if form == M.DWARF5_DEBUG_LINE.form_line_strp then - local strp = M.read_u32_le(buf, p) ---@type integer + local strp = M.read_u32_le(buf, p) ---@type integer local end_pos = lstr_buf:find("\0", strp + 1, true) or (#lstr_buf + 1) ---@type integer return lstr_buf:sub(strp + 1, end_pos - 1), p + M.DWARF5_DEBUG_LINE.form_strp_bytes elseif form == M.DWARF5_DEBUG_LINE.form_string then @@ -1159,7 +1159,7 @@ function M.read_line_unit_file_table(elf_path) up = nul end local unit_basenames = {} ---@type table -- bag: 1-based unit file index -> basename - local unit_paths = {} ---@type table -- bag: 1-based unit file index -> full path + local unit_paths = {} ---@type table -- bag: 1-based unit file index -> full path while up < body_end do local nul = buf:find("\0", up + 1, true) or (body_end + 1) ---@type integer if nul > body_end or nul == up + 1 then up = nul break end @@ -1168,8 +1168,8 @@ function M.read_line_unit_file_table(elf_path) local didx, up_next = M.read_uleb128_at(buf, up); up = up_next ---@type integer|nil, integer local _time, up_next2 = M.read_uleb128_at(buf, up); up = up_next2 ---@type integer|nil, integer local _size, up_next3 = M.read_uleb128_at(buf, up); up = up_next3 ---@type integer|nil, integer - local idx = #unit_basenames + 1 ---@type integer - local bs = path:match("[^/\\]+$") or path ---@type string + local idx = #unit_basenames + 1 ---@type integer + local bs = path:match("[^/\\]+$") or path ---@type string unit_paths[idx] = path unit_basenames[idx] = bs dirs[1] = dirs[1] or "" -- safety: gcc emits "" sentinel dir at 0 @@ -1196,15 +1196,15 @@ function M.read_line_unit_file_table(elf_path) up = up + (opcode_base - 1) -- std_opcode_lengths -- directories local dir_format_count, after = M.read_uleb128_at(buf, up); up = after ---@type integer|nil, integer - local dir_formats = {} ---@type integer[] - for i = 1, dir_format_count do ---@type integer + local dir_formats = {} ---@type integer[] + for i = 1, dir_format_count do ---@type integer local f, a2 = M.read_uleb128_at(buf, up); up = a2 ---@type integer|nil, integer dir_formats[i] = f end local dir_count, a3 = M.read_uleb128_at(buf, up); up = a3 ---@type integer|nil, integer - local dirs = {} ---@type string[] - for i = 1, dir_count do ---@type integer - local combined = "" ---@type string + local dirs = {} ---@type string[] + for i = 1, dir_count do ---@type integer + local combined = "" ---@type string for j = 1, dir_format_count do ---@type integer local v, a4 = read_form(buf, lstr_buf, up, dir_formats[j]) ---@type string|integer|nil, integer up = a4 @@ -1214,24 +1214,24 @@ function M.read_line_unit_file_table(elf_path) end -- file names local file_format_count, after2 = M.read_uleb128_at(buf, up); up = after2 ---@type integer|nil, integer - local file_formats = {} ---@type integer[] - for i = 1, file_format_count do ---@type integer + local file_formats = {} ---@type integer[] + for i = 1, file_format_count do ---@type integer local f, a2 = M.read_uleb128_at(buf, up); up = a2 ---@type integer|nil, integer file_formats[i] = f end local file_count, a3 = M.read_uleb128_at(buf, up); up = a3 ---@type integer|nil, integer - local unit_basenames = {} ---@type table -- bag: 1-based unit file index -> basename - local unit_paths = {} ---@type table -- bag: 1-based unit file index -> full path - for i = 1, file_count do ---@type integer - local combined = "" ---@type string - local didx = 0 ---@type integer + local unit_basenames = {} ---@type table -- bag: 1-based unit file index -> basename + local unit_paths = {} ---@type table -- bag: 1-based unit file index -> full path + for i = 1, file_count do ---@type integer + local combined = "" ---@type string + local didx = 0 ---@type integer for j = 1, file_format_count do ---@type integer local v, a4 = read_form(buf, lstr_buf, up, file_formats[j]) ---@type string|integer|nil, integer up = a4 if j == 1 and type(v) == "string" then combined = v end if j == 2 and type(v) == "number" then didx = v end end - local idx = #unit_basenames + 1 ---@type integer + local idx = #unit_basenames + 1 ---@type integer local bs = combined:match("[^/\\]+$") or combined ---@type string unit_paths[idx] = combined unit_basenames[idx] = bs @@ -1243,7 +1243,7 @@ function M.read_line_unit_file_table(elf_path) end --- Walk every line-program unit in the section. - local p = 0 ---@type integer + local p = 0 ---@type integer local section_end = #line ---@type integer while p + 4 <= section_end do local unit_length = M.read_u32_le(line, p) ---@type integer @@ -1255,11 +1255,11 @@ function M.read_line_unit_file_table(elf_path) local body_end = p + 4 + unit_length ---@type integer if body_end > section_end then break end local version = M.read_u16_le(line, body_start) ---@type integer - local unit_basenames, unit_paths ---@type table|nil, table|nil + local unit_basenames, unit_paths ---@type table|nil, table|nil if version >= 5 then -- DWARF5 header: version(2) + addr_size(1) + seg_size(1) + header_length(4) + content - local header_length_offset = body_start + 6 ---@type integer -- past version(2) + addr_size(1) + seg_size(1) - wait that's wrong; past hdr len is at +6 - local content_start = body_start + 8 ---@type integer -- past version(2) + addr_size(1) + seg_size(1) + header_length(4) + local header_length_offset = body_start + 6 ---@type integer -- past version(2) + addr_size(1) + seg_size(1) - wait that's wrong; past hdr len is at +6 + local content_start = body_start + 8 ---@type integer -- past version(2) + addr_size(1) + seg_size(1) + header_length(4) unit_basenames, unit_paths = parse_dwarf5_unit(line, lstr, content_start, body_end) elseif version >= 2 then -- DWARF2/3/4 header: version(2) + header_length(4) + content diff --git a/scripts/passes/annotation.lua b/scripts/passes/annotation.lua index 4e1eccc..a6fbf9a 100644 --- a/scripts/passes/annotation.lua +++ b/scripts/passes/annotation.lua @@ -11,7 +11,7 @@ -- Bootstrap follows the entry scripts; `scripts/duffle_paths.lua` sets package.path and package.cpath. See `ps1_meta.lua` for the rationale. -- `debug.getinfo(1, "S").source` locates this file for standalone and orchestrated runs, then `duffle_paths.lua` returns the loaded `duffle` module. local _bootstrap_dir = debug.getinfo(1, "S").source:match("^@?(.*[/\\])") or "./" ---@type string -local duffle = dofile(_bootstrap_dir .. "../duffle_paths.lua") ---@type DuffleExport +local duffle = dofile(_bootstrap_dir .. "../duffle_paths.lua") ---@type DuffleExport -- The annotation pass reads the source-derived registries from scan_source: -- * pipe_ctx.register_alias_registry — for atom_dbg_reg_default(R_X, ...) and atom_reg_types(R_X, ...) member-identity checks @@ -140,7 +140,7 @@ end --- @return nil local function check_macro_word_drift(m, pipe_ctx, findings) local wc = (pipe_ctx and pipe_ctx.word_counts) or {} ---@type WordCounts - local declared = wc[m.name] ---@type integer|nil + local declared = wc[m.name] ---@type integer|nil if not declared then findings.errors[#findings.errors + 1] = { line = m.line, @@ -169,7 +169,7 @@ end --- @return nil local function check_semantic_reg_defaults(_src, pipe_ctx, findings) -- Detect duplicate defaults using the ordered occurrence list (the out.types hash only retains the last declaration). - local seen_first_line = {} ---@type table -- bag: register ident -> first source line + local seen_first_line = {} ---@type table -- bag: register ident -> first source line for _, occ in ipairs(pipe_ctx.type_occurrences or {}) do ---@type integer, RegTypeOccurrence if seen_first_line[occ.reg] == nil then seen_first_line[occ.reg] = occ.source_line @@ -184,7 +184,7 @@ local function check_semantic_reg_defaults(_src, pipe_ctx, findings) end local reg_registry = pipe_ctx.register_alias_registry or {} ---@type table local type_registry = pipe_ctx.type_name_registry or {} ---@type table - for reg, def in pairs(pipe_ctx.types or {}) do ---@type string, RegTypeDefault + for reg, def in pairs(pipe_ctx.types or {}) do ---@type string, RegTypeDefault if not reg_registry[reg] then findings.errors[#findings.errors + 1] = { line = def.source_line, @@ -221,7 +221,7 @@ end local function check_atom_reg_types(_src, pipe_ctx, findings) local reg_registry = pipe_ctx.register_alias_registry or {} ---@type table local type_registry = pipe_ctx.type_name_registry or {} ---@type table - for _, ai in ipairs(pipe_ctx.atom_infos_list or {}) do ---@type integer, AtomInfoEntry + for _, ai in ipairs(pipe_ctx.atom_infos_list or {}) do ---@type integer, AtomInfoEntry if ai.reg_type_overrides then for reg, ov in pairs(ai.reg_type_overrides) do ---@type string, RegTypeOverride if not reg_registry[reg] then @@ -282,7 +282,7 @@ end --- @return nil local function check_binds_no_duplicate_fields(_src, pipe_ctx, findings) for _, bs in ipairs(pipe_ctx.binds_list or {}) do ---@type integer, BindsEntry - local seen = {} ---@type table -- bag: field name -> occurrence count + local seen = {} ---@type table -- bag: field name -> occurrence count for _, f in ipairs(bs.fields or {}) do ---@type integer, TypeField seen[f.name] = (seen[f.name] or 0) + 1 end @@ -375,7 +375,7 @@ local function check_wave_context_migration(_src, pipe_ctx, findings) if not (pipe_ctx.types and next(pipe_ctx.types)) then return end if not (pipe_ctx.atom_infos_list) then return end local reg_registry = pipe_ctx.register_alias_registry or {} ---@type table - for _, ai in ipairs(pipe_ctx.atom_infos_list) do ---@type integer, AtomInfoEntry + for _, ai in ipairs(pipe_ctx.atom_infos_list) do ---@type integer, AtomInfoEntry if ai.reg_type_overrides then for reg, _ in pairs(ai.reg_type_overrides) do ---@type string, RegTypeOverride if not reg_registry[reg] then @@ -428,8 +428,8 @@ local CHECK_RULES = { ---@type CheckRule[] --- @return PipeCtx local function build_corpus_pipe_ctx(ctx) local view = duffle.corpus_view(ctx) ---@type PipeCtx - local annot_counts = {} ---@type table -- bag: atom name -> annotation count - for _, info in ipairs(view.atom_infos) do ---@type integer, AtomInfoEntry + local annot_counts = {} ---@type table -- bag: atom name -> annotation count + for _, info in ipairs(view.atom_infos) do ---@type integer, AtomInfoEntry if info and info.atom_name then annot_counts[info.atom_name] = (annot_counts[info.atom_name] or 0) + 1 end @@ -467,7 +467,7 @@ local function validate(ctx, src, corpus_pipe_ctx) register_alias_registry = corpus_pipe_ctx.register_alias_registry, type_name_registry = corpus_pipe_ctx.type_name_registry, } - local atoms = {} ---@type AtomEntry[] + local atoms = {} ---@type AtomEntry[] for _, a in ipairs(scan.atoms) do ---@type integer, AtomEntry if a.kind == "atom" or a.kind == "atom_proc" then atoms[#atoms + 1] = a @@ -503,7 +503,7 @@ local function validate(ctx, src, corpus_pipe_ctx) -- scan_source records each marker in scan.debug_skip_markers; this loop validates each record independently and emits at most one error per marker. -- Valid markers stamp `debug_skip = true` on the following atom or component declaration, which downstream consumers read directly. local skip_markers = scan.debug_skip_markers or {} ---@type DebugSkipMarker[] - for _, marker in ipairs(skip_markers) do ---@type integer, DebugSkipMarker + for _, marker in ipairs(skip_markers) do ---@type integer, DebugSkipMarker duffle.run_check_rules(CHECK_RULES, "per_skip_marker", marker, pipe_ctx, findings) end @@ -554,17 +554,17 @@ function M.run(ctx) -- Build the shared pipe_ctx once for this run; every validate() call sees the same cross-source registries. -- The corpus owns the canonical cross-source registries; per-source scans retain body / declaration ownership. local corpus_pipe_ctx = build_corpus_pipe_ctx(ctx) ---@type PipeCtx - local corpus = ctx.shared.corpus ---@type Corpus + local corpus = ctx.shared.corpus ---@type Corpus -- Group `corpus.sources_by_dir` by module, validate every source in each bucket, and emit one errors.h per directory. local by_dir = (corpus and corpus.sources_by_dir) or {} ---@type table for dir, dir_sources in pairs(by_dir) do ---@type string, SourceFile[] local dir_basename = dir:match("([^/\\]+)$") or dir ---@type string - local dir_atoms = 0 ---@type integer - local dir_errors = {} ---@type PassFinding[] - local dir_warnings = {} ---@type PassFinding[] - for _, src in ipairs(dir_sources) do ---@type integer, SourceFile + local dir_atoms = 0 ---@type integer + local dir_errors = {} ---@type PassFinding[] + local dir_warnings = {} ---@type PassFinding[] + for _, src in ipairs(dir_sources) do ---@type integer, SourceFile local result = validate(ctx, src, corpus_pipe_ctx) ---@type AnnotatedResult result.source = src.path -- tag for downstream rendering dir_atoms = dir_atoms + #result.atoms diff --git a/scripts/passes/atoms_source_map.lua b/scripts/passes/atoms_source_map.lua index 4148ffd..553dd63 100644 --- a/scripts/passes/atoms_source_map.lua +++ b/scripts/passes/atoms_source_map.lua @@ -38,8 +38,8 @@ -- (works both standalone + when require'd). `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 "./" ---@type string -local duffle = dofile(_bootstrap_dir .. "../duffle_paths.lua") ---@type DuffleExport -local elf_dwarf = require("elf_dwarf") ---@type ElfDwarfMod +local duffle = dofile(_bootstrap_dir .. "../duffle_paths.lua") ---@type DuffleExport +local elf_dwarf = require("elf_dwarf") ---@type ElfDwarfMod -- ════════════════════════════════════════════════════════════════════════════ -- Constants @@ -103,14 +103,14 @@ local FORMAT_VERSION = 1 ---@type integer --- @return WordMapEntry[] --- @return integer local function canonical_word_entries(atom) - local paths = atom.paths or {} ---@type AtomPaths - local events = paths.word_events or {} ---@type WordEvent[] - local word_items = {} ---@type EmissionItem[] + local paths = atom.paths or {} ---@type AtomPaths + local events = paths.word_events or {} ---@type WordEvent[] + local word_items = {} ---@type EmissionItem[] for _, item in ipairs(paths.items or {}) do ---@type integer, EmissionItem if item.kind == "word" then word_items[#word_items + 1] = item end end - local entries = {} ---@type WordMapEntry[] + local entries = {} ---@type WordMapEntry[] for index, event in ipairs(events) do ---@type integer, WordEvent local item = word_items[index] or {} ---@type EmissionItem entries[#entries + 1] = { @@ -138,13 +138,13 @@ end --- @return string[] --- @return integer local function emit_provenance_stanza(src, atom, wc) - local lines = {} ---@type string[] - local rel_path = src.path:gsub("\\\\", "/") ---@type string + local lines = {} ---@type string[] + local rel_path = src.path:gsub("\\\\", "/") ---@type string local entries, total = canonical_word_entries(atom) ---@type WordMapEntry[], integer lines[#lines + 1] = string.format('ATOM %s "%s" 0', atom.raw_name or atom.name, rel_path) for _, entry in ipairs(entries) do ---@type integer, WordMapEntry - local inv = entry.invocation ---@type InvocationRecord|nil + local inv = entry.invocation ---@type InvocationRecord|nil local macro_count = inv and wc["mac_" .. inv.component_name] ---@type integer|nil if inv and macro_count ~= nil then lines[#lines + 1] = string.format('WORD %d CALL %s:%d MACRO %s "%s:%d" BODY %d' @@ -177,7 +177,7 @@ local function render_provenance(src, wc) --- @param atom AtomEntry --- @return nil local function append(atom) - local stanza = emit_provenance_stanza(src, atom, wc) ---@type string[] + local stanza = emit_provenance_stanza(src, atom, wc) ---@type string[] for _, line in ipairs(stanza) do lines[#lines + 1] = line end ---@type integer, string end for _, atom in ipairs(src.scan.atoms or {}) do ---@type integer, AtomEntry @@ -197,8 +197,8 @@ end --- @return string[] --- @return integer local function emit_atom_stanza(src, atom) - local lines = {} ---@type string[] - local rel_path = src.path:gsub("\\\\", "/") ---@type string + local lines = {} ---@type string[] + local rel_path = src.path:gsub("\\\\", "/") ---@type string local entries, total = canonical_word_entries(atom) ---@type WordMapEntry[], integer lines[#lines + 1] = string.format('ATOM %s "%s" 0', atom.raw_name or atom.name, rel_path) @@ -223,7 +223,7 @@ local function render_source_map(src) --- @param atom AtomEntry --- @return nil local function append(atom) - local stanza = emit_atom_stanza(src, atom) ---@type string[] + local stanza = emit_atom_stanza(src, atom) ---@type string[] for _, line in ipairs(stanza) do lines[#lines + 1] = line end ---@type integer, string end for _, atom in ipairs(src.scan.atoms or {}) do ---@type integer, AtomEntry @@ -253,8 +253,8 @@ end --- @return GdbAtomRecord[] local function build_atom_table(ctx) local addrs = elf_dwarf.read_nm(ctx.flags.elf_path) ---@type table - local corpus = ctx.shared and ctx.shared.corpus ---@type Corpus|nil - local matched = {} ---@type GdbAtomRecord[] + local corpus = ctx.shared and ctx.shared.corpus ---@type Corpus|nil + local matched = {} ---@type GdbAtomRecord[] for _, src in ipairs(corpus.source_order or {}) do ---@type integer, SourceFile local file_base = src.path:match("([^/\\\\]+)$") or src.path ---@type string @@ -263,7 +263,7 @@ local function build_atom_table(ctx) local function append(atom) if not atom.paths then return end local name = atom.raw_name or atom.name ---@type string - local info = addrs[name] ---@type NmAddr|nil + local info = addrs[name] ---@type NmAddr|nil if not info then return end local entries, total = canonical_word_entries(atom) ---@type WordMapEntry[], integer matched[#matched + 1] = { @@ -276,7 +276,7 @@ local function build_atom_table(ctx) entries = entries, } end - for _, atom in ipairs((src.scan or {}).atoms or {}) do append(atom) end ---@type integer, AtomEntry + for _, atom in ipairs((src.scan or {}).atoms or {}) do append(atom) end ---@type integer, AtomEntry for _, atom in ipairs((src.scan or {}).raw_atoms or {}) do append(atom) end ---@type integer, AtomEntry end @@ -537,12 +537,12 @@ function M.render_atom_source_map(atom) assert(type(atom) == "table", "render_atom_source_map: atom must be a table") assert(type(atom.paths) == "table", "render_atom_source_map: atom.paths must be a table") local entries, total = canonical_word_entries(atom) ---@type WordMapEntry[], integer - local lines = {} ---@type string[] + local lines = {} ---@type string[] lines[#lines + 1] = string.format("ATOM %s %d", (atom.raw_name or atom.name), total) for _, entry in ipairs(entries) do ---@type integer, WordMapEntry local word_line = string.format("WORD %d LINE %d TEXT %s", ---@type string entry.pos, entry.line, entry.text) - local keys = {} ---@type string[] + local keys = {} ---@type string[] for pos = 1, 16 do ---@type integer local k = entry.gpr_keys and entry.gpr_keys[pos] ---@type string|nil if type(k) == "string" and k:sub(1, 7) == "reguse:" then @@ -571,10 +571,10 @@ function M.render_atom_provenance(atom, wc, rel_path) assert(type(atom.paths) == "table", "render_atom_provenance: atom.paths must be a table") assert(type(rel_path) == "string", "render_atom_provenance: rel_path must be a string") local entries, total = canonical_word_entries(atom) ---@type WordMapEntry[], integer - local lines = {} ---@type string[] + local lines = {} ---@type string[] lines[#lines + 1] = string.format("ATOM %s %d", (atom.raw_name or atom.name), total) for _, entry in ipairs(entries) do ---@type integer, WordMapEntry - local inv = entry.invocation ---@type InvocationRecord|nil + local inv = entry.invocation ---@type InvocationRecord|nil local macro_count = inv and wc and wc["mac_" .. inv.component_name] ---@type integer|nil if inv and macro_count ~= nil then lines[#lines + 1] = string.format('WORD %d CALL %s:%d MACRO %s "%s:%d" BODY %d' diff --git a/scripts/passes/auto_reg.lua b/scripts/passes/auto_reg.lua index 5a50f47..36e7863 100644 --- a/scripts/passes/auto_reg.lua +++ b/scripts/passes/auto_reg.lua @@ -36,7 +36,7 @@ --- @field POOL GprIdent[] local _bootstrap_dir = debug.getinfo(1, "S").source:match("^@?(.*[/\\])") or "./" ---@type string -local duffle = dofile(_bootstrap_dir .. "../duffle_paths.lua") ---@type DuffleExport +local duffle = dofile(_bootstrap_dir .. "../duffle_paths.lua") ---@type DuffleExport --- ════════════════════════════════════════════════════════════════════════════ --- THE GPR ALLOCATION POOL — what's allocatable, and (more importantly) WHY @@ -83,7 +83,7 @@ local INT_CODE_TO_POOL_GPR = { ---@type table -- bag: MIPS G --- @param tbl table -- bag: key set only; values unused --- @return string[] local function stable_sort_keys(tbl) - local keys = {} ---@type string[] + local keys = {} ---@type string[] for k in pairs(tbl) do keys[#keys + 1] = k end ---@type string table.sort(keys) return keys @@ -99,10 +99,10 @@ local function allocate_phase(phase_label, decls) -- Deep-copy POOL into a fresh sequence table. The original `table.unpack and table.unpack(POOL) or { unpack(POOL) }` -- idiom wraps the unpacked values in a single inner table under LuaJIT 5.1 (`table.unpack` is nil; the `or` returns one value), -- which corrupts the pool into `{ {R_T0, R_T1, ...} }` — making `table.remove(pool, 1)` return the inner table on iteration. - local pool = {} ---@type GprIdent[] - for i = 1, #POOL do pool[i] = POOL[i] end ---@type integer - local result = {} ---@type GprAllocMap - local errors = {} ---@type PassFinding[] + local pool = {} ---@type GprIdent[] + for i = 1, #POOL do pool[i] = POOL[i] end ---@type integer + local result = {} ---@type GprAllocMap + local errors = {} ---@type PassFinding[] for _, sym in ipairs(stable_sort_keys(decls)) do ---@type integer, string local next_gpr = table.remove(pool, 1) ---@type GprIdent|nil if not next_gpr then @@ -166,13 +166,13 @@ local function find_used_gprs(body_text, alias_to_gpr) -- (b) Alias references (R_) resolved to physical GPRs via the registry. -- Sorted by name so the regex is byte-stable across runs. if alias_to_gpr and next(alias_to_gpr) then - local aliases = {} ---@type string[] + local aliases = {} ---@type string[] for alias_name in pairs(alias_to_gpr) do ---@type string aliases[#aliases + 1] = alias_name end table.sort(aliases) local pattern = "(" .. table.concat(aliases, "|") .. ")" ---@type string - for alias_name in body_text:gmatch(pattern) do ---@type string + for alias_name in body_text:gmatch(pattern) do ---@type string local gpr = alias_to_gpr[alias_name] ---@type GprIdent|nil if gpr and not found[gpr] then found[gpr] = 1 @@ -206,7 +206,7 @@ local function emit_auto_reg_h(out_dir, dir, sources, mappings) lines[#lines + 1] = "// R__Code = for every marker in this directory." lines[#lines + 1] = "" for _, sym in ipairs(stable_sort_keys(mappings)) do ---@type integer, string - local gpr = mappings[sym] ---@type GprIdent + local gpr = mappings[sym] ---@type GprIdent local gpr_code = gpr .. "_Code" ---@type string lines[#lines + 1] = "#define " .. sym .. "_Code " .. gpr_code end @@ -242,10 +242,10 @@ function M.run(ctx) local user_pinned, alias_to_gpr = build_user_pins(corpus) ---@type table, table -- 1. Allocate phase pools first (phase declarations take precedence over per-atom declarations). - local phase_allocations = {} ---@type table -- bag: phase_label -> alloc map + local phase_allocations = {} ---@type table -- bag: phase_label -> alloc map for phase_label, decls in pairs(corpus.phase_auto_regs or {}) do ---@type string, table local mapping, errs = allocate_phase(phase_label, decls) ---@type GprAllocMap, PassFinding[] - for sym, gpr in pairs(mapping) do ---@type string, GprIdent + for sym, gpr in pairs(mapping) do ---@type string, GprIdent phase_allocations[phase_label] = phase_allocations[phase_label] or {} phase_allocations[phase_label][sym] = gpr end @@ -258,14 +258,14 @@ function M.run(ctx) -- Otherwise, allocate a private pool for the atom. -- The phase membership is in `corpus.atom_phases[phase_label].atoms` (an array of atom names declared via `atom_phase()` -- in the atom's `atom_info` line). Build a reverse map `atom_name -> phase_label` so the lookup is O(1) per atom scope. - local atom_name_to_phase = {} ---@type table -- bag: atom name -> phase label + local atom_name_to_phase = {} ---@type table -- bag: atom name -> phase label for phase_label, entry in pairs(corpus.atom_phases or {}) do ---@type string, AtomPhaseGroup for _, atom_name in ipairs(entry.atoms or {}) do ---@type integer, AtomName atom_name_to_phase[atom_name] = phase_label end end - local atom_allocations = {} ---@type table -- bag: atom scope -> alloc map + local atom_allocations = {} ---@type table -- bag: atom scope -> alloc map for atom_scope, decls in pairs(corpus.atom_auto_regs or {}) do ---@type AtomName, table local phase_label = atom_name_to_phase[atom_scope] ---@type string|nil -- Build the atom's source pool: start with the full POOL, subtract: @@ -277,7 +277,7 @@ function M.run(ctx) -- the original `source_pool = phase_allocations[phase_label]` form used the phase -- allocation MAP as a pool, but that map has no array part, so `table.remove(source_pool, 1)` -- returned nil and every atom-with-phase marker errored with `phase_register_pool_exhausted`. - local used = {} ---@type table -- bag: committed or body-referenced GPR -> true + local used = {} ---@type table -- bag: committed or body-referenced GPR -> true for _, m in pairs(phase_allocations) do for _, gpr in pairs(m) do used[gpr] = true end end ---@type integer, GprAllocMap for _, m in pairs(atom_allocations) do for _, gpr in pairs(m) do used[gpr] = true end end ---@type integer, GprAllocMap -- (c) Body references — scan the atom body for hardcoded + alias-resolved GPRs. @@ -285,16 +285,16 @@ function M.run(ctx) local atom = corpus.atoms_by_name and corpus.atoms_by_name[atom_scope] ---@type AtomEntry|nil if atom and atom.body then local body_used = find_used_gprs(atom.body, alias_to_gpr) ---@type table - for gpr in pairs(body_used) do used[gpr] = true end ---@type GprIdent + for gpr in pairs(body_used) do used[gpr] = true end ---@type GprIdent end - local source_pool = {} ---@type GprIdent[] + local source_pool = {} ---@type GprIdent[] for _, gpr in ipairs(POOL) do ---@type integer, GprIdent -- Exclude (a) prior commitments, (b) USER-PINNED GPRs (wave-context carriers declared via atom_reg + _Code defs, preserved across atoms globally). if not used[gpr] and not user_pinned[gpr] then source_pool[#source_pool + 1] = gpr end end - local result = {} ---@type GprAllocMap + local result = {} ---@type GprAllocMap for _, sym in ipairs(stable_sort_keys(decls)) do ---@type integer, string local next_gpr = table.remove(source_pool, 1) ---@type GprIdent|nil if not next_gpr then @@ -322,7 +322,7 @@ function M.run(ctx) local atom = corpus.atoms_by_name and corpus.atoms_by_name[atom_scope] ---@type AtomEntry|nil if atom and atom.body then local used_in_body = find_used_gprs(atom.body, alias_to_gpr) ---@type table - for sym, allocated_gpr in pairs(decls) do ---@type string, GprIdent + for sym, allocated_gpr in pairs(decls) do ---@type string, GprIdent if used_in_body[allocated_gpr] and used_in_body[allocated_gpr] > 0 then warnings[#warnings + 1] = { line = atom.line or 0, @@ -338,8 +338,8 @@ function M.run(ctx) -- 4. Emit per-directory gen/auto_reg.h. -- For each source directory that has atom_auto_regs or phase_auto_regs entries, emit one header. local sources_by_dir = corpus.sources_by_dir or {} ---@type table - for dir, sources in pairs(sources_by_dir) do ---@type string, SourceFile[] - local per_dir_mappings = {} ---@type GprAllocMap + for dir, sources in pairs(sources_by_dir) do ---@type string, SourceFile[] + local per_dir_mappings = {} ---@type GprAllocMap for _, src in ipairs(sources) do ---@type integer, SourceFile -- Collect every (sym -> gpr) entry that originated from a source in this directory. -- `src.scan.atom_auto_regs` is keyed by ATOM SCOPE NAME; `pairs(t)` iterates KEYS so `scope_name` here is the scope ident (e.g. "cube_g4_face"). @@ -356,7 +356,7 @@ function M.run(ctx) end end end - local out_dir = dir .. "/gen" ---@type string + local out_dir = dir .. "/gen" ---@type string local out_path = emit_auto_reg_h(out_dir, dir, sources, per_dir_mappings) ---@type string|nil if out_path then outputs[#outputs + 1] = { auto_reg_h = out_path } end end diff --git a/scripts/passes/components.lua b/scripts/passes/components.lua index 2db3429..1c1fdd8 100644 --- a/scripts/passes/components.lua +++ b/scripts/passes/components.lua @@ -23,7 +23,7 @@ -- Bootstrap: load `duffle_paths.lua` via `debug.getinfo(1, "S").source` (works both standalone + when require'd). -- 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 "./" ---@type string -local duffle = dofile(_bootstrap_dir .. "../duffle_paths.lua") ---@type DuffleExport +local duffle = dofile(_bootstrap_dir .. "../duffle_paths.lua") ---@type DuffleExport -- ════════════════════════════════════════════════════════════════════════════ -- Constants @@ -31,20 +31,20 @@ local duffle = dofile(_bootstrap_dir .. "../duffle_paths.lua") ---@type -- Atom component declaration identifiers. local ATOM_COMP_PROC = "MipsAtomComp_Proc_" ---@type string -local MIPS_ATOM = "Slice_MipsCode" ---@type string -- prefix on the function declaration that wraps an AtomComp_Proc_ +local MIPS_ATOM = "Slice_MipsCode" ---@type string -- prefix on the function declaration that wraps an AtomComp_Proc_ -- Component-name prefixes. local AC_PREFIX = "ac_" ---@type string -- arg to MipsAtomComp_(ac_X); the X is the atom name -local AC_PREFIX_LEN = 3 ---@type integer +local AC_PREFIX_LEN = 3 ---@type integer local MAC_PREFIX = "mac_" ---@type string -- prefix on generated macros; the rest is the atom name -local MAC_PREFIX_LEN = 4 ---@type integer +local MAC_PREFIX_LEN = 4 ---@type integer -- ASCII byte values used in tokenization. local BYTE_NEWLINE = 10 ---@type integer local BYTE_SLASH = 47 ---@type integer -- Output gen subdirectory + filename (per-directory aggregation; the directory name is the namespace). -local GEN_SUBDIR = "gen" ---@type string +local GEN_SUBDIR = "gen" ---@type string local MACS_FILENAME = "macs.h" ---@type string -- ════════════════════════════════════════════════════════════════════════════ @@ -126,9 +126,9 @@ end --- @return string[]|nil local function extract_arg_names(args_str) if not args_str or args_str == "" then return nil end - local names = {} ---@type string[] + local names = {} ---@type string[] local tokens = duffle.split_top_level_commas(args_str) ---@type string[] - for _, tok in ipairs(tokens) do ---@type integer, string + for _, tok in ipairs(tokens) do ---@type integer, string local trimmed = duffle.trim(tok) ---@type string if trimmed ~= "" then -- Strip trailing block comment (/* ... */) from the token, if present. @@ -151,7 +151,7 @@ local function extract_arg_names(args_str) end end -- Now scan back from close_pos for the `/*` opener (slashes are at close_pos-1 and close_pos-2). - local opener_pos = nil ---@type integer|nil + local opener_pos = nil ---@type integer|nil local scan = close_pos - 3 ---@type integer while scan >= 1 do if trimmed:sub(scan, scan + 1) == "/*" then @@ -240,7 +240,7 @@ end --- @param scan SourceScan --- @return Component[] local function project_components(source, scan) - local out = {} ---@type Component[] + local out = {} ---@type Component[] for _, a in ipairs(scan.atoms) do ---@type integer, AtomEntry -- Only `MipsAtomComp_(ac_X)` (kind="comp_bare") and `MipsAtomComp_Proc_(ac_X, ...)` (kind="comp_proc") -- are COMPONENTS — they get inlined via `mac_` aliases inside atom bodies. @@ -286,8 +286,8 @@ end --- @param s string --- @return string local function convert_line_comments_to_block(s) - local result = s ---@type string - local pos = 1 ---@type integer + local result = s ---@type string + local pos = 1 ---@type integer local len = #result ---@type integer while pos <= len do local is_double_slash = result:byte(pos) == BYTE_SLASH ---@type boolean @@ -300,9 +300,9 @@ local function convert_line_comments_to_block(s) while eol <= len and result:byte(eol) ~= BYTE_NEWLINE do eol = eol + 1 end - local before = result:sub(1, pos - 1) ---@type string - local comment = result:sub(pos + 2, eol - 1) ---@type string -- skip the `//` - local after ---@type string + local before = result:sub(1, pos - 1) ---@type string + local comment = result:sub(pos + 2, eol - 1) ---@type string -- skip the `//` + local after ---@type string if eol <= len and result:byte(eol) == BYTE_NEWLINE then after = " */" .. result:sub(eol) -- keep the newline else @@ -361,7 +361,7 @@ local function word_count_rec(name, comp_by_name, wc, cache) if cache[name] ~= nil then return cache[name] end cache[name] = -1 -- mark in-progress (cycle detection) local cc = comp_by_name[name] ---@type Component|nil - local n ---@type integer + local n ---@type integer if cc then n = 0 local tokens = cc.body_tokens ---@type BodyToken[] @@ -412,11 +412,11 @@ end --- @param wc WordCounts --- @return table -- bag: bare component name -> word count local function count_all_components(components, wc) - local comp_by_name = {} ---@type table + local comp_by_name = {} ---@type table for _, cc in ipairs(components) do comp_by_name[cc.name] = cc end ---@type integer, Component - local cache = {} ---@type table -- bag: memo; -1 in-progress sentinel - local counts = {} ---@type table -- bag: bare name -> word count - for _, c in ipairs(components) do ---@type integer, Component + local cache = {} ---@type table -- bag: memo; -1 in-progress sentinel + local counts = {} ---@type table -- bag: bare name -> word count + for _, c in ipairs(components) do ---@type integer, Component counts[c.name] = word_count_rec(c.name, comp_by_name, wc, cache) end return counts @@ -446,10 +446,10 @@ local function component_meta_rec(name, comp_by_name, latency, cache) if cache[name] ~= nil then return cache[name] end cache[name] = { cycle_cost = -1, gp0_contrib = -1 } local cc = comp_by_name[name] ---@type Component|nil - local cycle_cost ---@type integer - local gp0_contrib ---@type integer + local cycle_cost ---@type integer + local gp0_contrib ---@type integer if cc then - local skip_cycle = (name == "yield") ---@type boolean + local skip_cycle = (name == "yield") ---@type boolean local skip_gp0 = name:match("^insert_ot_tag") ~= nil ---@type boolean cycle_cost = 0 gp0_contrib = 0 @@ -460,7 +460,7 @@ local function component_meta_rec(name, comp_by_name, latency, cache) if trimmed ~= "" then local ident = duffle.read_ident(trimmed, 1) ---@type string|nil if ident and ident:sub(1, MAC_PREFIX_LEN) == MAC_PREFIX then - local nested = ident:sub(MAC_PREFIX_LEN + 1) ---@type string + local nested = ident:sub(MAC_PREFIX_LEN + 1) ---@type string local nested_meta = component_meta_rec(nested, comp_by_name, latency, cache) ---@type ComponentMeta if not skip_cycle then cycle_cost = cycle_cost + nested_meta.cycle_cost @@ -471,7 +471,7 @@ local function component_meta_rec(name, comp_by_name, latency, cache) else if not skip_cycle then local isa = duffle.instr(ident) ---@type InstructionRow|nil - local gte = duffle.gte(ident) ---@type GteCommandRow|nil + local gte = duffle.gte(ident) ---@type GteCommandRow|nil cycle_cost = cycle_cost + ((isa and isa.cycles) or (gte and gte.cycles) or latency[ident] or 1) end if not skip_gp0 then @@ -506,11 +506,11 @@ end --- @param latency table -- bag: ident -> cycle cost --- @return ComponentMetaMap local function compute_components_metadata(components, latency) - local comp_by_name = {} ---@type table + local comp_by_name = {} ---@type table for _, cc in ipairs(components) do comp_by_name[cc.name] = cc end ---@type integer, Component - local cache = {} ---@type ComponentMetaMap - local out = {} ---@type ComponentMetaMap - for _, c in ipairs(components) do ---@type integer, Component + local cache = {} ---@type ComponentMetaMap + local out = {} ---@type ComponentMetaMap + for _, c in ipairs(components) do ---@type integer, Component out[c.name] = component_meta_rec(c.name, comp_by_name, latency, cache) end return out @@ -526,7 +526,7 @@ end --- @return string[] local function split_comment_lines(s) local out = {} ---@type string[] - local pos = 1 ---@type integer + local pos = 1 ---@type integer local s_len = #s ---@type integer while pos <= s_len do local nl = s:find("\n", pos, true) ---@type integer|nil @@ -690,9 +690,9 @@ local function build_component_lines(c, counts) end end - local tokens = duffle.split_top_level_commas(c.body) ---@type string[] + local tokens = duffle.split_top_level_commas(c.body) ---@type string[] for i = 1, #tokens do tokens[i] = duffle.trim(tokens[i]) end ---@type integer - local sig = signature_from_args(c.args) ---@type string + local sig = signature_from_args(c.args) ---@type string -- Direct lookup against the per-source precomputed `counts` table (built once by count_all_components). local n = counts[c.name] ---@type integer @@ -718,7 +718,7 @@ end --- @return string[] local function header_boilerplate(dir, sources) local source_lines = { "// Directory: " .. duffle.to_absolute_path(dir) .. "/" } ---@type string[] - for _, src in ipairs(sources) do ---@type integer, SourceFile + for _, src in ipairs(sources) do ---@type integer, SourceFile source_lines[#source_lines + 1] = "// source: " .. duffle.to_absolute_path(src.path) end local source_blob = table.concat(source_lines, "\n") ---@type string @@ -749,7 +749,7 @@ end --- @return string -- Output directory --- @return string -- Full output path local function compute_macs_h_path(dir) - local out_dir = dir .. "/" .. GEN_SUBDIR ---@type string + local out_dir = dir .. "/" .. GEN_SUBDIR ---@type string local out_path = out_dir .. "/" .. MACS_FILENAME ---@type string return out_dir, out_path end @@ -764,7 +764,7 @@ end --- @return string|nil -- Path to the written file (nil if no components) local function emit_component_macros_h(ctx, dir, sources, components, counts) if #components == 0 then return nil end - local out_dir, out_path = compute_macs_h_path(dir) ---@type string, string + local out_dir, out_path = compute_macs_h_path(dir) ---@type string, string local lines = header_boilerplate(dir, sources) ---@type string[] for _, c in ipairs(components) do ---@type integer, Component @@ -791,7 +791,7 @@ end --- @param counts table -- bag: bare component name -> word count --- @return nil local function update_canonical_word_counts(corpus, components, counts) - local wc = corpus.word_counts ---@type WordCounts + local wc = corpus.word_counts ---@type WordCounts for _, c in ipairs(components) do ---@type integer, Component local key = "mac_" .. c.name ---@type string if wc[key] == nil then @@ -821,7 +821,7 @@ end --- @return nil local function update_canonical_components(corpus, src, components, metadata) local rel_path = src.path:gsub("\\", "/") ---@type string - for _, c in ipairs(components) do ---@type integer, Component + for _, c in ipairs(components) do ---@type integer, Component -- Keyed by bare name (e.g. `yield`, `load_tri_indices`). -- The atoms_source_map pass looks up components by bare name from the corpus; -- `mac_` prefix lives at the call-site identifier and is stripped before lookup. @@ -841,7 +841,7 @@ local function update_canonical_components(corpus, src, components, metadata) -- Identical-shape declarations (same path + line) reuse the first-wins entry without a collision record. local existing = corpus.components[c.name] ---@type ComponentDef if existing.path ~= rel_path or existing.line ~= c.line then - local kind = c.kind or "comp_bare" ---@type string + local kind = c.kind or "comp_bare" ---@type string local first_kind = existing.kind or "comp_bare" ---@type string corpus.collisions[#corpus.collisions + 1] = { kind = "component", @@ -866,7 +866,7 @@ end --- @return nil local function update_canonical_component_body_index(corpus, src, components, scan) local line_of = scan and scan.line_of ---@type (fun(pos: integer): integer)|nil - for _, c in ipairs(components) do ---@type integer, Component + for _, c in ipairs(components) do ---@type integer, Component if corpus.component_body_index[c.name] == nil then corpus.component_body_index[c.name] = { body_tokens = c.body_tokens, @@ -911,14 +911,14 @@ function M.run(ctx) -- Per-directory aggregation: every source in the same directory contributes to one `gen/macs.h`. -- The directory itself is the namespace. `corpus.sources_by_dir` preserves source-order within each bucket (matches `corpus.source_order`). local sources_by_dir = corpus.sources_by_dir or duffle.group_sources_by_dir(corpus.source_order) ---@type table - for dir, sources in pairs(sources_by_dir) do ---@type string, SourceFile[] + for dir, sources in pairs(sources_by_dir) do ---@type string, SourceFile[] -- Aggregate components from every source in this directory. -- `project_components` returns nil for sources with no `MipsAtomComp_` declarations; we skip those. local aggregated_components = {} ---@type Component[] local metadata_per_source = {} ---@type table for _, src in ipairs(sources) do ---@type integer, SourceFile local per_source = project_components(src.text, src.scan) or {} ---@type Component[] - for _, c in ipairs(per_source) do ---@type integer, Component + for _, c in ipairs(per_source) do ---@type integer, Component aggregated_components[#aggregated_components + 1] = c end if #per_source > 0 then @@ -928,7 +928,7 @@ function M.run(ctx) if #aggregated_components > 0 then -- Compute word counts across the aggregated set. `corpus.word_counts` carries the -- same-source + prior-directory entries so the recursive lookup sees both. - local counts = count_all_components(aggregated_components, corpus.word_counts) ---@type table -- bag: bare name -> word count + local counts = count_all_components(aggregated_components, corpus.word_counts) ---@type table -- bag: bare name -> word count local macs_path = emit_component_macros_h(ctx, dir, sources, aggregated_components, counts) ---@type string|nil if macs_path then outputs[#outputs + 1] = { macs_h = macs_path } diff --git a/scripts/passes/dwarf_injection.lua b/scripts/passes/dwarf_injection.lua index 37949bc..5a16abb 100644 --- a/scripts/passes/dwarf_injection.lua +++ b/scripts/passes/dwarf_injection.lua @@ -34,7 +34,7 @@ -- Load `duffle_paths.lua` via `debug.getinfo(1, "S").source` (works both standalone + when require'd). -- Sets package.path + package.cpath then returns duffle. local _bootstrap_dir = debug.getinfo(1, "S").source:match("^@?(.*[/\\])") or "./" ---@type string -local duffle = dofile(_bootstrap_dir .. "../duffle_paths.lua") ---@type DuffleExport +local duffle = dofile(_bootstrap_dir .. "../duffle_paths.lua") ---@type DuffleExport -- ELF32 / DWARF / atoms-source-map utilities (post-link debug-info injection). -- Sister module to duffle.lua — contains the format-constant tables (ELF32 byte offsets, DWARF opcodes, etc.) and the I/O helpers @@ -55,27 +55,27 @@ local sleb128 = elf_dwarf.sleb128 ---@type fun(n: integer): string -- All values lifted from `elf_dwarf.DWARF_LINE_OPS` + `elf_dwarf.DWARF5_RNGLISTS`. -- Local aliases preserve the code's readability -- (e.g. `DW_LNS_copy` reads better than `elf_dwarf.DWARF_LINE_OPS.DW_LNS_copy` in an emitter body). -local DWARF_LINE_OPS = elf_dwarf.DWARF_LINE_OPS ---@type DwarfLineOps -local DWARF5_RNGLISTS = elf_dwarf.DWARF5_RNGLISTS ---@type Dwarf5Rnglists +local DWARF_LINE_OPS = elf_dwarf.DWARF_LINE_OPS ---@type DwarfLineOps +local DWARF5_RNGLISTS = elf_dwarf.DWARF5_RNGLISTS ---@type Dwarf5Rnglists local MIPS_BYTES_PER_WORD = elf_dwarf.MIPS_BYTES_PER_WORD ---@type integer -local DW_LNS_copy = DWARF_LINE_OPS.DW_LNS_copy ---@type integer -local DW_LNS_advance_pc = DWARF_LINE_OPS.DW_LNS_advance_pc ---@type integer +local DW_LNS_copy = DWARF_LINE_OPS.DW_LNS_copy ---@type integer +local DW_LNS_advance_pc = DWARF_LINE_OPS.DW_LNS_advance_pc ---@type integer local DW_LNS_advance_line = DWARF_LINE_OPS.DW_LNS_advance_line ---@type integer -local DW_LNS_set_file = DWARF_LINE_OPS.DW_LNS_set_file ---@type integer -local DW_LNS_negate_stmt = DWARF_LINE_OPS.DW_LNS_negate_stmt ---@type integer -local DW_LNS_extended = DWARF_LINE_OPS.DW_LNS_extended ---@type integer +local DW_LNS_set_file = DWARF_LINE_OPS.DW_LNS_set_file ---@type integer +local DW_LNS_negate_stmt = DWARF_LINE_OPS.DW_LNS_negate_stmt ---@type integer +local DW_LNS_extended = DWARF_LINE_OPS.DW_LNS_extended ---@type integer local DW_LNE_end_sequence = DWARF_LINE_OPS.DW_LNE_end_sequence ---@type integer -local DW_LNE_set_address = DWARF_LINE_OPS.DW_LNE_set_address ---@type integer +local DW_LNE_set_address = DWARF_LINE_OPS.DW_LNE_set_address ---@type integer -local DW_RLE_end_of_list = DWARF5_RNGLISTS.end_of_list ---@type integer +local DW_RLE_end_of_list = DWARF5_RNGLISTS.end_of_list ---@type integer local DW_RLE_start_length = DWARF5_RNGLISTS.start_length ---@type integer -- File-index lookup for the existing main line unit (Unit 2). -- Populated at pass start by `init_file_index_lookup(elf_path)` from the runtime ELF (see `elf_dwarf.read_line_unit_file_table`). -local _file_index_by_basename = nil ---@type table|nil -- bag -- [basename] = 1-based line-table file index -local _file_path_by_index = nil ---@type table|nil -- bag -- [1-based index] = full source path (diagnostics / future consumers) -local _default_atom_source_index = nil ---@type integer -- any valid index used in opaque-row fallbacks +local _file_index_by_basename = nil ---@type table|nil -- bag -- [basename] = 1-based line-table file index +local _file_path_by_index = nil ---@type table|nil -- bag -- [1-based index] = full source path (diagnostics / future consumers) +local _default_atom_source_index = nil ---@type integer -- any valid index used in opaque-row fallbacks -- RR_ debug-visible variables come from the merged register_alias_registry filtered to aliases whose code is a valid MIPS GPR 0..31 -- (see collect_per_source_registries + by_alias in build_inserted_children). @@ -86,16 +86,16 @@ local _default_atom_source_index = nil ---@type integer -- any valid index used -- DW_OP_bregN would describe a memory location addressed from a register; the breg form would make gdb dereference the atom register value rather than display it. -- New abbreviation codes (100+ to avoid collision with gcc's existing 1-60+ codes). -local ABBREV_CU = 0x64 ---@type integer -- 100: DW_TAG_compile_unit -local ABBREV_SUBPROGRAM = 0x65 ---@type integer -- 101: DW_TAG_subprogram -local ABBREV_VARIABLE = 0x66 ---@type integer -- 102: DW_TAG_variable with DW_AT_type = ref4 to U4 -local ABBREV_STRUCT_TYPE = 0x67 ---@type integer -- 103: DW_TAG_structure_type with children (Binds_X mirror) -local ABBREV_MEMBER = 0x68 ---@type integer -- 104: DW_TAG_member no children (DW_AT_type = ref4 to U4 base) -local ABBREV_BIND_VAR = 0x69 ---@type integer -- 105: DW_TAG_variable no children + DW_AT_type = ref4 (the bind_args variable) -local ABBREV_BASE_TYPE = 0x6A ---@type integer -- 106: DW_TAG_base_type no children (U4) +local ABBREV_CU = 0x64 ---@type integer -- 100: DW_TAG_compile_unit +local ABBREV_SUBPROGRAM = 0x65 ---@type integer -- 101: DW_TAG_subprogram +local ABBREV_VARIABLE = 0x66 ---@type integer -- 102: DW_TAG_variable with DW_AT_type = ref4 to U4 +local ABBREV_STRUCT_TYPE = 0x67 ---@type integer -- 103: DW_TAG_structure_type with children (Binds_X mirror) +local ABBREV_MEMBER = 0x68 ---@type integer -- 104: DW_TAG_member no children (DW_AT_type = ref4 to U4 base) +local ABBREV_BIND_VAR = 0x69 ---@type integer -- 105: DW_TAG_variable no children + DW_AT_type = ref4 (the bind_args variable) +local ABBREV_BASE_TYPE = 0x6A ---@type integer -- 106: DW_TAG_base_type no children (U4) -- Component step-into (DW_TAG_inlined_subroutine + abstract DW_TAG_subprogram). -local ABBREV_ABSTRACT_SUBPROGRAM = 0x6B ---@type integer -- 107: DW_TAG_subprogram (abstract — no low_pc/high_pc); for each unique mac_X component -local ABBREV_INLINED_SUBROUTINE = 0x6C ---@type integer -- 108: DW_TAG_inlined_subroutine with children (per-component invocation range) +local ABBREV_ABSTRACT_SUBPROGRAM = 0x6B ---@type integer -- 107: DW_TAG_subprogram (abstract — no low_pc/high_pc); for each unique mac_X component +local ABBREV_INLINED_SUBROUTINE = 0x6C ---@type integer -- 108: DW_TAG_inlined_subroutine with children (per-component invocation range) -- Bind_args uses DW_FORM_sec_offset → .debug_loclists for PC-ranged liveness -- (each field transitions from tape memory to GPR at load_pc + 8 = MIPS I load-delay slot boundary). local ABBREV_BIND_VAR_LOCLIST = 0x6D ---@type integer -- 109: DW_TAG_variable no children + DW_AT_type = ref4 + DW_AT_location = sec_offset @@ -213,26 +213,26 @@ local DW_TAG_pointer_type = 0x0F ---@type integer -- Component step-into. local DW_TAG_inlined_subroutine = 0x1D ---@type integer -local DW_AT_name = 0x03 ---@type integer -local DW_AT_low_pc = 0x11 ---@type integer -local DW_AT_high_pc = 0x12 ---@type integer -local DW_AT_language = 0x13 ---@type integer -local DW_AT_location = 0x02 ---@type integer -local DW_AT_comp_dir = 0x1B ---@type integer -local DW_AT_byte_size = 0x0B ---@type integer -local DW_AT_encoding = 0x3E ---@type integer -- DWARF5 §7.7.1: DW_AT_encoding for the DW_ATE_unsigned base type +local DW_AT_name = 0x03 ---@type integer +local DW_AT_low_pc = 0x11 ---@type integer +local DW_AT_high_pc = 0x12 ---@type integer +local DW_AT_language = 0x13 ---@type integer +local DW_AT_location = 0x02 ---@type integer +local DW_AT_comp_dir = 0x1B ---@type integer +local DW_AT_byte_size = 0x0B ---@type integer +local DW_AT_encoding = 0x3E ---@type integer -- DWARF5 §7.7.1: DW_AT_encoding for the DW_ATE_unsigned base type local DW_AT_data_member_location = 0x38 ---@type integer -local DW_AT_type = 0x49 ---@type integer -local DW_AT_linkage_name = 0x6E ---@type integer -- DWARF5 §7.7.1: DW_AT_linkage_name with DW_FORM_string -local DW_AT_external = 0x3F ---@type integer -- marks a variable/function as externally visible +local DW_AT_type = 0x49 ---@type integer +local DW_AT_linkage_name = 0x6E ---@type integer -- DWARF5 §7.7.1: DW_AT_linkage_name with DW_FORM_string +local DW_AT_external = 0x3F ---@type integer -- marks a variable/function as externally visible -- Inlined_subroutine + abstract_origin attributes. local DW_AT_abstract_origin = 0x31 ---@type integer local DW_AT_call_file = 0x58 ---@type integer local DW_AT_call_line = 0x59 ---@type integer -local DW_AT_inline = 0x20 ---@type integer -- DWARF5 §7.7.1: DW_AT_inline (used by abstract subprogram for the mac_X() components) +local DW_AT_inline = 0x20 ---@type integer -- DWARF5 §7.7.1: DW_AT_inline (used by abstract subprogram for the mac_X() components) -- decl_file + decl_line on the abstract subprogram so consumers can resolve an abstract origin back to its definition site even when no inlined_subroutine instance currently maps to it. -local DW_AT_decl_file = 0x3A ---@type integer -- DWARF5 §7.7.1: DW_AT_decl_file (1-based file index into the CU's file table) -local DW_AT_decl_line = 0x3B ---@type integer -- DWARF5 §7.7.1: DW_AT_decl_line +local DW_AT_decl_file = 0x3A ---@type integer -- DWARF5 §7.7.1: DW_AT_decl_file (1-based file index into the CU's file table) +local DW_AT_decl_line = 0x3B ---@type integer -- DWARF5 §7.7.1: DW_AT_decl_line -- Replaced the hardcoded `ATOM_SOURCE_FILE_INDEX = 11` and the `PROVENANCE_BASENAME_TO_FILE_INDEX` table below with a runtime lookup -- (`init_file_index_lookup` + `resolve_provenance_file_index`) that reads the actual `.debug_line` file table from the post-link ELF. @@ -284,7 +284,7 @@ local function resolve_provenance_file_index(path) local normalized = path:gsub("\\", "/") ---@type string -- Take the last path component (the basename). local basename = normalized:match("([^/]+)$") or normalized ---@type string - local idx = _file_index_by_basename[basename] ---@type integer|nil + local idx = _file_index_by_basename[basename] ---@type integer|nil if idx ~= nil then return idx end -- Last-resort exact-path match (handles paths that don't reduce to a known basename). for i, p in pairs(_file_path_by_index) do ---@type integer, string @@ -299,13 +299,13 @@ end local DW_FORM_addr = 0x01 ---@type integer local DW_FORM_data1 = 0x0B ---@type integer -local DW_FORM_string = 0x08 ---@type integer -- inline null-terminated -local DW_FORM_strp = 0x0E ---@type integer -- 4-byte offset into .debug_str -local DW_FORM_exprloc = 0x18 ---@type integer -- length-prefixed (ULEB128) DW_OP bytes -local DW_FORM_ref4 = 0x13 ---@type integer -- 4-byte offset within the same .debug_info CU -local DW_FORM_udata = 0x0F ---@type integer -- ULEB128 (DW_AT_byte_size for struct_type, DW_AT_data_member_location for member) -local DW_FORM_implicit_const = 0x21 ---@type integer -- DWARF5 §7.5.6: abbrev declaration carries a SLEB constant (used by the abbrev-table walker) -local DW_FORM_sec_offset = 0x17 ---@type integer -- 4-byte section-relative offset (into .debug_loclists / .debug_rnglists) +local DW_FORM_string = 0x08 ---@type integer -- inline null-terminated +local DW_FORM_strp = 0x0E ---@type integer -- 4-byte offset into .debug_str +local DW_FORM_exprloc = 0x18 ---@type integer -- length-prefixed (ULEB128) DW_OP bytes +local DW_FORM_ref4 = 0x13 ---@type integer -- 4-byte offset within the same .debug_info CU +local DW_FORM_udata = 0x0F ---@type integer -- ULEB128 (DW_AT_byte_size for struct_type, DW_AT_data_member_location for member) +local DW_FORM_implicit_const = 0x21 ---@type integer -- DWARF5 §7.5.6: abbrev declaration carries a SLEB constant (used by the abbrev-table walker) +local DW_FORM_sec_offset = 0x17 ---@type integer -- 4-byte section-relative offset (into .debug_loclists / .debug_rnglists) -- DW_OP_reg0 + DW_OP_piece are declared above (lines 114-116) alongside the other DWARF5 §7.7.3 loclist opcodes. @@ -337,26 +337,26 @@ local function build_debug_loclists_section(atom_table, registries) -- When absent we emit just the section terminator (a single DW_LLE_end_of_list byte); the .debug_loclists section stays non-empty so the linker accepts it, -- and `bind_args` will be emitted with no loclist PC range (readelf will display it as having no .debug_loclists entries). local tape_alias_entry = registries.register_alias_registry and registries.register_alias_registry["R_TapePtr"] ---@type AliasEntry|nil - local tape_reg = tape_alias_entry and tape_alias_entry.code ---@type integer - local parts = {} ---@type string[] - for _, atom in ipairs(atom_table) do ---@type integer, DwarfAtom + local tape_reg = tape_alias_entry and tape_alias_entry.code ---@type integer + local parts = {} ---@type string[] + for _, atom in ipairs(atom_table) do ---@type integer, DwarfAtom if atom.rbind and tape_reg then - local fields = atom.rbind.fields or {} ---@type TypeField[] - local regs = atom.rbind.regs or {} ---@type DwarfLoadPair[] - local n_fields = #fields ---@type integer + local fields = atom.rbind.fields or {} ---@type TypeField[] + local regs = atom.rbind.regs or {} ---@type DwarfLoadPair[] + local n_fields = #fields ---@type integer local last_load_pc = atom.addr + (n_fields - 1) * MIPS_BYTES_PER_WORD ---@type integer - local transition_pc = last_load_pc + MIPS_LOAD_DELAY_BYTES ---@type integer - local tape_pieces = {} ---@type string[] - for _, f in ipairs(fields) do ---@type integer, TypeField - local offset = f.offset or 0 ---@type integer + local transition_pc = last_load_pc + MIPS_LOAD_DELAY_BYTES ---@type integer + local tape_pieces = {} ---@type string[] + for _, f in ipairs(fields) do ---@type integer, TypeField + local offset = f.offset or 0 ---@type integer local offset_sleb = elf_dwarf.sleb128(offset) ---@type string -- (DW_OP_bregN, SLEB128(offset), DW_OP_piece, ULEB128(U4_BYTE_SIZE)) -- 4 = U4_BYTE_SIZE: each piece is sizeof(uint32_t) on MIPS32. table.insert(tape_pieces, string.char(DW_OP_breg0 + tape_reg) .. offset_sleb .. string.char(DW_OP_piece) .. uleb128(U4_BYTE_SIZE)) end local tape_expr = table.concat(tape_pieces) ---@type string - local gpr_pieces = {} ---@type string[] - for _, pair in ipairs(regs) do ---@type integer, DwarfLoadPair + local gpr_pieces = {} ---@type string[] + for _, pair in ipairs(regs) do ---@type integer, DwarfLoadPair -- (DW_OP_regN, DW_OP_piece, ULEB128(4)) — one piece per GPR-resident field. -- The 4 = U4_BYTE_SIZE: each piece is sizeof(uint32_t) on MIPS32. table.insert(gpr_pieces, string.char(DW_OP_reg0 + pair.reg) .. string.char(DW_OP_piece) .. uleb128(U4_BYTE_SIZE)) @@ -376,9 +376,9 @@ local function build_debug_loclists_section(atom_table, registries) -- Loclist unit header (DWARF5 §7.7.2): -- unit_length(4) + version(2) + address_size(1) + segment_size(1) + offset_entry_count(4) = 12 bytes header. -- version = 5 (DWARF5); address_size = 4 (MIPS32); segment_size = 0; offset_entry_count = 0 (we use DW_LLE_start_length, not offsets). - local LOCLIST_HEADER_SIZE = 12 ---@type integer - local body = table.concat(parts) ---@type string - local unit_length = LOCLIST_HEADER_SIZE - 4 + #body ---@type integer -- -4 because unit_length excludes itself + local LOCLIST_HEADER_SIZE = 12 ---@type integer + local body = table.concat(parts) ---@type string + local unit_length = LOCLIST_HEADER_SIZE - 4 + #body ---@type integer -- -4 because unit_length excludes itself local header = elf_dwarf.write_u32_le(unit_length) ---@type string .. elf_dwarf.write_u16_le(5) -- DWARF5 .. string.char(U4_BYTE_SIZE) -- address_size @@ -401,11 +401,11 @@ end -- @param atom_table DwarfAtom[] -- list of atoms with .rbind set -- @return table -- bag: atom name -> offset_in_section local function compute_loclists_offsets(atom_table) - local LOCLIST_ENTRY_HEADER_SIZE = 1 + 4 + 1 ---@type integer -- DW_LLE_start_length(1) + addr(4) + uleb_length(1) - local offsets = {} ---@type table -- bag + local LOCLIST_ENTRY_HEADER_SIZE = 1 + 4 + 1 ---@type integer -- DW_LLE_start_length(1) + addr(4) + uleb_length(1) + local offsets = {} ---@type table -- bag -- Loclist unit header (DWARF5 §7.7.2): unit_length(4) + version(2) + address_size(1) + segment_size(1) + offset_entry_count(4) = 12 bytes. -- The unit_length itself is not counted in the unit_length value, so the body starts at byte 12. - local cursor = 4 + 2 + 1 + 1 + 4 ---@type integer -- = 12 + local cursor = 4 + 2 + 1 + 1 + 4 ---@type integer -- = 12 for _, atom in ipairs(atom_table) do ---@type integer, DwarfAtom if atom.rbind then offsets[atom.name] = cursor @@ -414,14 +414,14 @@ local function compute_loclists_offsets(atom_table) -- 1 (DW_LLE_start_length) + 4 (PC) + 1 (uleb length prefix) + sum(tape_piece_size(field.offset)) -- + 1 (DW_LLE_start_length) + 4 (transition_pc) + 1 (uleb length prefix) + n_fields * 3 (gpr pieces) -- + 1 (DW_LLE_end_of_list) - local tape_pieces_size = 0 ---@type integer + local tape_pieces_size = 0 ---@type integer for _, f in ipairs(atom.rbind.fields or {}) do ---@type integer, TypeField tape_pieces_size = tape_pieces_size + tape_piece_size(f.offset or 0) end - local gpr_pieces_size = n_fields * 3 ---@type integer -- each gpr piece: DW_OP_regN(1) + DW_OP_piece(1) + uleb(4)(1) = 3 bytes + local gpr_pieces_size = n_fields * 3 ---@type integer -- each gpr piece: DW_OP_regN(1) + DW_OP_piece(1) + uleb(4)(1) = 3 bytes local tape_entry = LOCLIST_ENTRY_HEADER_SIZE + tape_pieces_size ---@type integer - local gpr_entry = LOCLIST_ENTRY_HEADER_SIZE + gpr_pieces_size ---@type integer - local body_len = tape_entry + gpr_entry + 1 ---@type integer -- +1 for DW_LLE_end_of_list + local gpr_entry = LOCLIST_ENTRY_HEADER_SIZE + gpr_pieces_size ---@type integer + local body_len = tape_entry + gpr_entry + 1 ---@type integer -- +1 for DW_LLE_end_of_list cursor = cursor + body_len end end @@ -430,7 +430,7 @@ end -- Default name for the synthetic CU (so VSCode lists it as a known source). local DEFAULT_CU_NAME = "tape_atom_locals" ---@type string -local DEFAULT_CU_COMP_DIR = "." ---@type string +local DEFAULT_CU_COMP_DIR = "." ---@type string -- SECTION_WRITERS owns the .bin output path templates. @@ -558,11 +558,11 @@ local DEFAULT_BASENAME = "hello_gte" ---@type string --- @field data string --- @class DwarfInjectionPass ---- @field run fun(ctx: PassCtx): PassResult ---- @field compute_loclists_offsets_for_test fun(atom_table: DwarfAtom[]): table +--- @field run fun(ctx: PassCtx): PassResult +--- @field compute_loclists_offsets_for_test fun(atom_table: DwarfAtom[]): table --- @field build_debug_loclists_section_for_test fun(atom_table: DwarfAtom[], registries: DwarfRegistries): string ---- @field tape_piece_size_for_test fun(offset: integer): integer ---- @field build_atom_table_for_test fun(corpus: Corpus, addrs: table): DwarfAtom[] +--- @field tape_piece_size_for_test fun(offset: integer): integer +--- @field build_atom_table_for_test fun(corpus: Corpus, addrs: table): DwarfAtom[] --- Project the corpus registries into the shape the section builders expect. @@ -585,7 +585,7 @@ local function collect_per_source_registries(corpus) -- `passes.scan_source.lua` has already folded every per-source scan into the corpus tables, so no per-source iteration is needed here. -- `atom_infos` is preserved byte-for-byte with no filtering; consumers consult `corpus.atoms_by_name` -- themselves when they need to know whether a particular atom_info corresponds to an actual atom record. - local atom_infos_list = {} ---@type AtomInfoEntry[] + local atom_infos_list = {} ---@type AtomInfoEntry[] for _, ai in ipairs((corpus and corpus.atom_infos) or {}) do ---@type integer, AtomInfoEntry atom_infos_list[#atom_infos_list + 1] = ai end @@ -660,7 +660,7 @@ local function build_atom_sequence(atom) local function set_address(addr) -- Per DWARF5 §6.2.5.3: marker(0) + size(ULEB128, includes sub_opcode byte) + sub_opcode + payload -- For set_address: size = 1 (sub_opcode) + 4 (addr) = 5 - local addr_bytes = elf_dwarf.write_u32_le(addr) ---@type string + local addr_bytes = elf_dwarf.write_u32_le(addr) ---@type string local sub_size = string.char(DW_LNE_set_address) .. addr_bytes ---@type string return string.char(DW_LNS_extended) .. uleb128(#sub_size) .. sub_size end @@ -714,12 +714,12 @@ local function build_atom_sequence(atom) -- `start_pos` / `end_pos` are 0-based emitted-word positions stamped at construction/close time by `duffle.emit_invoke_begin` / `duffle.emit_invoke_end`; -- Missing values are a corpus-plumbing bug, so we let the index expression fail loud with arithmetic-on-nil rather than silently producing `0+1=1` for a missing start_pos. local invs = atom.invocations or {} ---@type InvocationRecord[] - local innermost_idx = {} ---@type table -- bag - local ancestry_idx = {} ---@type table -- bag - for idx = 1, #atom.entries do ---@type integer + local innermost_idx = {} ---@type table -- bag + local ancestry_idx = {} ---@type table -- bag + for idx = 1, #atom.entries do ---@type integer innermost_idx[idx] = nil ancestry_idx[idx] = {} - local active = {} ---@type InvocationRecord[] + local active = {} ---@type InvocationRecord[] for _, inv in ipairs(invs) do ---@type integer, InvocationRecord if idx >= inv.start_pos + 1 and idx <= inv.end_pos + 1 then active[#active + 1] = inv @@ -746,11 +746,11 @@ local function build_atom_sequence(atom) -- This is the value the multi-row PC's body_lines[1] row must reference for source-order display: `anc.body_lines[1]` is the line of the FIRST WORD -- (which for an outer whose body starts with a nested expansion is inside the inner's expansion = wrong for display purposes); -- `anc.body_first_line` is the body's first content line in the parent's source (= correct for display). - local body_first_line_of = {} ---@type table -- bag + local body_first_line_of = {} ---@type table -- bag for _, top_inv in ipairs(invs) do ---@type integer, InvocationRecord local earliest_nested_call_line = nil ---@type integer local earliest_nested_start_pos = nil ---@type integer - for _, cand in ipairs(invs) do ---@type integer, InvocationRecord + for _, cand in ipairs(invs) do ---@type integer, InvocationRecord if cand.parent_id == top_inv.id and cand.call_line ~= nil then if earliest_nested_start_pos == nil or cand.start_pos < earliest_nested_start_pos then earliest_nested_start_pos = cand.start_pos @@ -808,7 +808,7 @@ local function build_atom_sequence(atom) local call_file_idx = resolve_provenance_file_index(atom.src_path) ---@type integer -- --- Atom entry (idx 1) ------------------------------------------------- - local entry_1 = atom.entries[1] ---@type DwarfAtomWord + local entry_1 = atom.entries[1] ---@type DwarfAtomWord local entry_1_ancestry = ancestry_idx[1] ---@type InvocationRecord[] -- If atom entry 1 starts inside an invocation, walk the ancestry and emit a call-site row + (when applicable) @@ -842,7 +842,7 @@ local function build_atom_sequence(atom) -- --- Subsequent entries (idx 2..N) -------------------------------------- for idx = 2, #atom.entries do ---@type integer|nil - local entry = atom.entries[idx] ---@type DwarfAtomWord + local entry = atom.entries[idx] ---@type DwarfAtomWord local inv = innermost_idx[idx] ---@type InvocationRecord -- Advance PC by 1 .word (4 bytes on MIPS). @@ -946,7 +946,7 @@ local function build_atom_table(corpus, addrs) -- Build the dense entries list from `word_events`. -- `word_events[i].i` = the 0-based `.word` position -- `call_line` = the root atom's physical source line for that word (stamped by emission_model) - local entries = {} ---@type DwarfAtomWord[] + local entries = {} ---@type DwarfAtomWord[] for idx, ev in ipairs(word_events) do ---@type integer, WordEvent entries[#entries + 1] = { pos = ev.i or (idx - 1), @@ -990,7 +990,7 @@ local function build_atom_table(corpus, addrs) -- Cross-ref with the nm symbol table; atoms absent from `addrs` are skipped -- (an atom declared in source but not emitted as a symbol is a metaprogram or atom-info bug, not a source-correlation bug — emit_no_emit would catch it upstream). for _, src in ipairs((corpus and corpus.source_order) or {}) do ---@type integer, SourceFile - local src_path = src.path or "" ---@type string + local src_path = src.path or "" ---@type string for _, atom_rec in ipairs(((src.scan or {}).atoms) or {}) do ---@type integer, AtomEntry local info = addrs[atom_rec.name or atom_rec.raw_name] ---@type NmAddr|nil if info then @@ -1018,7 +1018,7 @@ end --- @param atom_table DwarfAtom[] --- @return table local function collect_component_defs(atom_table) - local out = {} ---@type table -- bag + local out = {} ---@type table -- bag for _, atom in ipairs(atom_table) do ---@type integer, DwarfAtom for _, inv in ipairs(atom.invocations or {}) do ---@type integer, InvocationRecord if not out[inv.component_name] then @@ -1065,14 +1065,14 @@ end --- @param registries DwarfRegistries -- Merged registries from collect_per_source_registries --- @return DwarfLoadPair[] -- List of {reg = , field = } local function parse_body_load_pairs(body_tokens, binds_name, registries) - local pairs = {} ---@type DwarfLoadPair[] + local pairs = {} ---@type DwarfLoadPair[] local reg_index_by_name = (registries and registries.register_alias_registry) or {} ---@type table -- bag -- One regex that matches any of: load_word, load_half, load_half_u, load_byte, load_byte_u, gte_lw, gte_lwc2. -- The captured ident is `kind`; `inner` holds the parens body for arg parsing. local load_pattern = "^(load_word|load_half|load_half_u|load_byte|load_byte_u|gte_lw|gte_lwc2)%s*%((.*)%)$" ---@type string - for _, t in ipairs(body_tokens or {}) do ---@type integer, BodyToken + for _, t in ipairs(body_tokens or {}) do ---@type integer, BodyToken local tok = duffle.trim(t.tok or "") ---@type string - local kind, inner = tok:match(load_pattern) ---@type string|nil, string|nil + local kind, inner = tok:match(load_pattern) ---@type string|nil, string|nil if kind then local args = duffle.split_top_level_commas(inner) ---@type string[] -- Expected shape for an rbind piece-chain load: (R_, R_TapePtr, O_(Binds_, FieldName)) @@ -1083,7 +1083,7 @@ local function parse_body_load_pairs(body_tokens, binds_name, registries) local third_arg = duffle.trim(args[3]) ---@type string -- Match O_(Binds_, FieldName) local b, f = third_arg:match("^O_%((Binds_[%w_]+)%s*,%s*(.-)%s*%)$") ---@type string|nil, string|nil - local alias_entry = reg_index_by_name[reg_name] ---@type AliasEntry|nil + local alias_entry = reg_index_by_name[reg_name] ---@type AliasEntry|nil if b and b == binds_name and alias_entry and alias_entry.code then pairs[#pairs + 1] = { reg = alias_entry.code, @@ -1118,7 +1118,7 @@ local function parse_rbind_atoms(corpus, atom_table, registries) -- Index binds by struct name; consume `scan.binds[i].fields` directly. -- The scan-source pass emits each Binds_X's fields as {[type_name, pointer_depth, offset, byte_size, ...]}, -- so this pass builds the rbind_structs entry without re-parsing. - local binds_by_name = {} ---@type table -- bag + local binds_by_name = {} ---@type table -- bag for _, src in ipairs((corpus and corpus.source_order) or {}) do ---@type integer, SourceFile local scan = src.scan ---@type SourceScan|nil if scan then @@ -1139,7 +1139,7 @@ local function parse_rbind_atoms(corpus, atom_table, registries) end -- Walk every atom_info; if `binds` is set, find the atom body_tokens + parse load_word pairs. - local body_tokens_by_atom = {} ---@type table -- bag + local body_tokens_by_atom = {} ---@type table -- bag for _, src in ipairs((corpus and corpus.source_order) or {}) do ---@type integer, SourceFile local scan = src.scan ---@type SourceScan|nil if scan then @@ -1149,7 +1149,7 @@ local function parse_rbind_atoms(corpus, atom_table, registries) end end - local ai_by_atom = {} ---@type table -- bag + local ai_by_atom = {} ---@type table -- bag for _, src in ipairs((corpus and corpus.source_order) or {}) do ---@type integer, SourceFile local scan = src.scan ---@type SourceScan|nil if scan then @@ -1161,7 +1161,7 @@ local function parse_rbind_atoms(corpus, atom_table, registries) for atom_name, ai in pairs(ai_by_atom) do ---@type string, AtomInfoEntry if ai.binds then - local struct = rbind_structs[ai.binds] ---@type DwarfRbindStruct|nil + local struct = rbind_structs[ai.binds] ---@type DwarfRbindStruct|nil local body_toks = body_tokens_by_atom[atom_name] ---@type BodyToken[]|nil if struct and body_toks then local pairs = parse_body_load_pairs(body_toks, ai.binds, registries) ---@type DwarfLoadPair[] @@ -1208,9 +1208,9 @@ local function build_dwarf_line_section(existing, atom_table) if #atom_table == 0 then return existing end -- Build the sequences. - local sequences = {} ---@type string[] + local sequences = {} ---@type string[] for _, atom in ipairs(atom_table) do sequences[#sequences + 1] = build_atom_sequence(atom) end ---@type integer, DwarfAtom - local appended = table.concat(sequences) ---@type string + local appended = table.concat(sequences) ---@type string -- Walk DWARF32 line units and retain the final unit's bounds. -- The main C CU points at this final unit (DW_AT_stmt_list = 0x5b in today's ELF). @@ -1226,7 +1226,7 @@ local function build_dwarf_line_section(existing, atom_table) end if unit_pos ~= #existing or not last_pos then return existing end - local new_length = last_length + #appended ---@type integer + local new_length = last_length + #appended ---@type integer local new_length_bytes = elf_dwarf.write_u32_le(new_length) ---@type string return existing:sub(1, last_pos) @@ -1272,8 +1272,8 @@ local function build_dwarf_aranges_section(existing, atom_table) -- Walk all units and emit each one (preserving existing structure). -- For the LAST unit, replace the terminator with my entries + new term. - local result = {} ---@type string[] - local i = 0 ---@type integer -- zero-based wire offset + local result = {} ---@type string[] + local i = 0 ---@type integer -- zero-based wire offset local is_last_unit = false ---@type boolean while i < #existing do @@ -1285,21 +1285,21 @@ local function build_dwarf_aranges_section(existing, atom_table) return existing end - local unit_start = i ---@type integer + local unit_start = i ---@type integer local unit_end_excl = i + 4 + ul ---@type integer is_last_unit = (unit_end_excl == #existing) if is_last_unit then -- The old terminator is replaced by entries + a new terminator, so net section growth (and unit_length growth) is entries only. local added_bytes = #atom_table * elf_dwarf.DWARF4_ARANGES.entry_size ---@type integer - local new_ul = ul + added_bytes ---@type integer - local new_ul_bytes = elf_dwarf.write_u32_le(new_ul) ---@type string + local new_ul = ul + added_bytes ---@type integer + local new_ul_bytes = elf_dwarf.write_u32_le(new_ul) ---@type string -- Emit everything EXCEPT the last 8 bytes (terminator). result[#result + 1] = new_ul_bytes .. existing:sub(i + 5, unit_end_excl - elf_dwarf.DWARF4_ARANGES.terminator_size) -- Append my atom entries. for _, atom in ipairs(atom_table) do ---@type integer, DwarfAtom - local a = atom.addr ---@type integer + local a = atom.addr ---@type integer local size = atom.size_bytes ---@type integer result[#result + 1] = elf_dwarf.write_u32_le(a) .. elf_dwarf.write_u32_le(size) end @@ -1337,10 +1337,10 @@ end local function build_dwarf_rnglists_section(existing, atom_table) if #existing <= elf_dwarf.DWARF5_RNGLISTS.first_entry_offset or #atom_table == 0 then return existing end - local unit_length = elf_dwarf.read_u32_le(existing, elf_dwarf.DWARF5_RNGLISTS.unit_length_offset) ---@type integer - local version = elf_dwarf.read_u16_le(existing, elf_dwarf.DWARF5_RNGLISTS.version_offset) ---@type integer - local address_size = existing:byte(elf_dwarf.DWARF5_RNGLISTS.addr_size_offset + 1) ---@type integer - local segment_size = existing:byte(elf_dwarf.DWARF5_RNGLISTS.seg_size_offset + 1) ---@type integer + local unit_length = elf_dwarf.read_u32_le(existing, elf_dwarf.DWARF5_RNGLISTS.unit_length_offset) ---@type integer + local version = elf_dwarf.read_u16_le(existing, elf_dwarf.DWARF5_RNGLISTS.version_offset) ---@type integer + local address_size = existing:byte(elf_dwarf.DWARF5_RNGLISTS.addr_size_offset + 1) ---@type integer + local segment_size = existing:byte(elf_dwarf.DWARF5_RNGLISTS.seg_size_offset + 1) ---@type integer local offset_entry_count = elf_dwarf.read_u32_le(existing, elf_dwarf.DWARF5_RNGLISTS.offset_count_offset) ---@type integer if unit_length + 4 ~= #existing @@ -1352,14 +1352,14 @@ local function build_dwarf_rnglists_section(existing, atom_table) return existing end - local entries = {} ---@type string[] + local entries = {} ---@type string[] for _, atom in ipairs(atom_table) do ---@type integer, DwarfAtom entries[#entries + 1] = string.char(DW_RLE_start_length) .. elf_dwarf.write_u32_le(atom.addr) .. uleb128(atom.size_bytes) end - local appended = table.concat(entries) ---@type string - local new_length = unit_length + #appended ---@type integer + local appended = table.concat(entries) ---@type string + local new_length = unit_length + #appended ---@type integer local new_length_bytes = elf_dwarf.write_u32_le(new_length) ---@type string return new_length_bytes @@ -1390,16 +1390,16 @@ end --- @param rbind DwarfRbind -- {regs = {{reg, field}, ...}, fields = {{name, offset}, ...}, bytes = N} --- @return string -- the exprloc byte sequence (length-prefixed) local function piece_chain_exprloc(rbind) - local op_bytes = {} ---@type string[] - local field_offset_by_name = {} ---@type table -- bag + local op_bytes = {} ---@type string[] + local field_offset_by_name = {} ---@type table -- bag for _, f in ipairs(rbind.fields) do ---@type integer, TypeField field_offset_by_name[f.name] = f.offset end local next_offset = rbind.bytes ---@type integer - for i = #rbind.regs, 1, -1 do ---@type integer -- walk backwards to know each piece's size - local pair = rbind.regs[i] ---@type DwarfLoadPair + for i = #rbind.regs, 1, -1 do ---@type integer -- walk backwards to know each piece's size + local pair = rbind.regs[i] ---@type DwarfLoadPair local off = field_offset_by_name[pair.field] or 0 ---@type integer - local size ---@type integer + local size ---@type integer if i == #rbind.regs then size = next_offset - off else @@ -1416,9 +1416,9 @@ local function piece_chain_exprloc(rbind) next_offset = off end -- We built it back-to-front; reverse it. - local rev = {} ---@type string[] + local rev = {} ---@type string[] for i = #op_bytes, 1, -1 do rev[#rev + 1] = op_bytes[i] end ---@type integer - local op = table.concat(rev) ---@type string + local op = table.concat(rev) ---@type string return uleb128(#op) .. op end @@ -1440,10 +1440,10 @@ end -- "math.floor(/16) instead of bit.rshift for LuaJIT 2.1 compat" comment at line 352). -- DWARF5 compile-unit header constants. -local DW_VERSION_5 = 5 ---@type integer -local DW_UT_compile = 0x01 ---@type integer -local DWARF32_TERMINATOR = 0xFFFFFFFF ---@type integer -- sentinel for DWARF64 marker -local CU_HEADER_SIZE = 12 ---@type integer -- 4 + 2 + 1 + 1 + 4 +local DW_VERSION_5 = 5 ---@type integer +local DW_UT_compile = 0x01 ---@type integer +local DWARF32_TERMINATOR = 0xFFFFFFFF ---@type integer -- sentinel for DWARF64 marker +local CU_HEADER_SIZE = 12 ---@type integer -- 4 + 2 + 1 + 1 + 4 --- Walk .debug_info to find the FINAL compilation unit, validate it as a DWARF5 32-bit compile-unit, and extract its bounds + abbrev-table offset. --- Returns nil on any layout mismatch. Callers fall back to existing sections. @@ -1467,7 +1467,7 @@ local function find_main_cu_layout(existing) local buf_len = #existing ---@type integer if buf_len < CU_HEADER_SIZE then return nil end - local pos = 0 ---@type integer + local pos = 0 ---@type integer local main_cu_start = nil ---@type integer local main_cu_end_excl = nil ---@type integer while pos + 4 <= buf_len do @@ -1489,10 +1489,10 @@ local function find_main_cu_layout(existing) -- [6] unit_type -- [7] address_size -- [8..11] debug_abbrev_offset - local hdr = main_cu_start + 4 ---@type integer - local version = elf_dwarf.read_u16_le(existing, hdr) ---@type integer - local unit_type = existing:byte(hdr + 2 + 1) ---@type integer - local address_size = existing:byte(hdr + 3 + 1) ---@type integer + local hdr = main_cu_start + 4 ---@type integer + local version = elf_dwarf.read_u16_le(existing, hdr) ---@type integer + local unit_type = existing:byte(hdr + 2 + 1) ---@type integer + local address_size = existing:byte(hdr + 3 + 1) ---@type integer local abbrev_off = elf_dwarf.read_u32_le(existing, hdr + 4) ---@type integer if version ~= DW_VERSION_5 or unit_type ~= DW_UT_compile or address_size ~= 4 then return nil @@ -1673,7 +1673,7 @@ local function build_new_strings(atom_table, registries) -- The CU name + comp_dir are the first two strings (offsets 0 and N1). -- Then each unique atom name + each register name follows. local strings = {} ---@type string[] - local map = {} ---@type table -- bag + local map = {} ---@type table -- bag -- CU name at offset 0 in the new blob strings[#strings + 1] = DEFAULT_CU_NAME .. "\0" @@ -1698,7 +1698,7 @@ local function build_new_strings(atom_table, registries) -- filtered to MIPS GPR 0..31 — the same filter that build_inserted_children applies -- for the RR_ locals, so .debug_str entries stay in sync with .debug_info). -- Lua's pairs() is non-deterministic; sort the alias names first so the emitted .debug_str bytes are byte-identical across runs. - local sorted_alias_names = {} ---@type string[] + local sorted_alias_names = {} ---@type string[] for r_name, alias in pairs(registries.register_alias_registry or {}) do ---@type string, AliasEntry|nil if alias.code and alias.code >= 0 and alias.code <= 31 then sorted_alias_names[#sorted_alias_names + 1] = r_name @@ -1775,13 +1775,13 @@ local function build_inserted_children(main_cu_offset, main_cu_end_excl, atom_ta -- by_alias_order: sorted list of by_alias keys, for deterministic iteration order. -- Lua's pairs() order is implementation-defined and varies between runs; without sorting, the per-atom variable emission order -- would be non-deterministic and the .debug_info bytes would differ across builds. - local by_alias = {} ---@type table -- bag + local by_alias = {} ---@type table -- bag for r_name, alias in pairs(registries.register_alias_registry or {}) do ---@type string, AliasEntry|nil if alias.code and alias.code >= 0 and alias.code <= 31 then by_alias[r_name] = alias end end - local by_alias_order = {} ---@type string[] + local by_alias_order = {} ---@type string[] for r_name in pairs(by_alias) do by_alias_order[#by_alias_order + 1] = r_name end ---@type string table.sort(by_alias_order) @@ -1790,7 +1790,7 @@ local function build_inserted_children(main_cu_offset, main_cu_end_excl, atom_ta --- @param atoms DwarfAtom[] --- @return table local function build_atom_name_index(atoms) - local m = {} ---@type table -- bag + local m = {} ---@type table -- bag for _, a in ipairs(atoms or {}) do ---@type integer, AliasEntry|nil if a and a.name then m[a.name] = a end end @@ -1861,7 +1861,7 @@ local function build_inserted_children(main_cu_offset, main_cu_end_excl, atom_ta local row = DIE_SCHEMA[schema_name] ---@type DieSchema emit(uleb128(row.abbrev)) for _, attr in ipairs(row.attrs) do ---@type integer, DieSchemaAttr - local v = values[attr.key] ---@type string|integer + local v = values[attr.key] ---@type string|integer local w = FORM_WRITERS[attr.form] ---@type DieFormWriter if not w then error("emit_die: unknown form " .. tostring(attr.form)) end w(emit, v) @@ -1898,7 +1898,7 @@ local function build_inserted_children(main_cu_offset, main_cu_end_excl, atom_ta -- Always include the base_type "unsigned int" as the U4 target. type_offsets["U4"] = base_type_section_offset -- Collect every unique (type_name, max_pointer_depth) used by any rbind field. - local used_typed_views = {} ---@type table -- bag -- { [type_name] = max_depth } + local used_typed_views = {} ---@type table -- bag -- { [type_name] = max_depth } for _, atom in ipairs(atom_table) do ---@type integer, DwarfAtom if atom.rbind and atom.rbind.fields then for _, f in ipairs(atom.rbind.fields) do ---@type integer, TypeField @@ -1912,7 +1912,7 @@ local function build_inserted_children(main_cu_offset, main_cu_end_excl, atom_ta end end -- Sort for deterministic emission. - local sorted_typed_types = {} ---@type string[] + local sorted_typed_types = {} ---@type string[] for tn in pairs(used_typed_views) do sorted_typed_types[#sorted_typed_types + 1] = tn end ---@type string table.sort(sorted_typed_types) -- For each non-U4 type, emit a typedef (DW_TAG_typedef) named after the type and referencing the base_type "unsigned int" (4 bytes). @@ -1938,7 +1938,7 @@ local function build_inserted_children(main_cu_offset, main_cu_end_excl, atom_ta return nil end if entry.byte_size == nil then return nil end - local members = {} ---@type DwarfTypeLayoutMember[] + local members = {} ---@type DwarfTypeLayoutMember[] for _, f in ipairs(entry.fields) do ---@type integer, TypeField if f.offset == nil or f.byte_size == nil then return nil end members[#members + 1] = { @@ -2028,7 +2028,7 @@ local function build_inserted_children(main_cu_offset, main_cu_end_excl, atom_ta end for _, tn in ipairs(sorted_typed_types) do ---@type integer, string if tn ~= "U4" then - local depth = used_typed_views[tn] ---@type integer + local depth = used_typed_views[tn] ---@type integer local struct_offset = emit_struct_layout(tn) ---@type integer if not struct_offset then local innermost_offset = next_offset() ---@type integer @@ -2084,8 +2084,8 @@ local function build_inserted_children(main_cu_offset, main_cu_end_excl, atom_ta type_chain_offsets["U4|1"] = u4_chain_offset -- 2) Emit one DW_TAG_structure_type per unique Binds_X. - local struct_section_offsets = {} ---@type table -- bag - local sorted_struct_names = {} ---@type string[] + local struct_section_offsets = {} ---@type table -- bag + local sorted_struct_names = {} ---@type string[] for k in pairs(rbind_structs) do sorted_struct_names[#sorted_struct_names + 1] = k end ---@type string table.sort(sorted_struct_names) for _, binds_name in ipairs(sorted_struct_names) do ---@type integer, string @@ -2119,13 +2119,13 @@ local function build_inserted_children(main_cu_offset, main_cu_end_excl, atom_ta -- Each abstract DIE is a CU-level child (sibling of the per-atom subprograms below). -- The abstract DIE's section offset is later used by inlined_subroutine DIEs (which embed `DW_AT_abstract_origin = ref4 → abstract DIE`). -- Each abstract DIE also carries DW_AT_decl_file + DW_AT_decl_line pointing at the component's definition site (file path + body line). - local component_defs = collect_component_defs(atom_table) ---@type table -- bag - local abstract_offsets = {} ---@type table -- bag -- name -> section offset - local sorted_comp_names = {} ---@type string[] + local component_defs = collect_component_defs(atom_table) ---@type table -- bag + local abstract_offsets = {} ---@type table -- bag -- name -> section offset + local sorted_comp_names = {} ---@type string[] for name in pairs(component_defs) do sorted_comp_names[#sorted_comp_names + 1] = name end ---@type string table.sort(sorted_comp_names) -- DW_INL_inlined (1) = "this subroutine was inlined" — accurate for the mac_* components. - local DW_INL_inlined = 0x01 ---@type integer + local DW_INL_inlined = 0x01 ---@type integer for _, comp_name in ipairs(sorted_comp_names) do ---@type integer, string local def = component_defs[comp_name] ---@type DwarfComponentSite abstract_offsets[comp_name] = next_offset() @@ -2159,10 +2159,10 @@ local function build_inserted_children(main_cu_offset, main_cu_end_excl, atom_ta -- (e) enum-site atom_type() default: register_alias_registry[R_Name].default_type (per-alias fallback declared in lottes_tape.h) -- (f) void* fallback: the void_chain_offset built in section 1b; gdb renders `(void *) 0x...` (hex) -- An R_Name absent from the registry AND missed by all of (a..e) skips emission for that alias entirely. - local atom_view_ctx_fields = nil ---@type table|nil -- bag -- populated by step (b); map field_name -> field entry - local reg_to_field_ctx = nil ---@type table|nil -- bag -- populated by step (b); map GPR index -> field name - local atom_view_phase_fields = nil ---@type table|nil -- bag -- populated by step (d); map field_name -> field entry - local reg_to_field_phase = nil ---@type table|nil -- bag -- populated by step (d); map GPR index -> field name + local atom_view_ctx_fields = nil ---@type table|nil -- bag -- populated by step (b); map field_name -> field entry + local reg_to_field_ctx = nil ---@type table|nil -- bag -- populated by step (b); map GPR index -> field name + local atom_view_phase_fields = nil ---@type table|nil -- bag -- populated by step (d); map field_name -> field entry + local reg_to_field_phase = nil ---@type table|nil -- bag -- populated by step (d); map GPR index -> field name -- atom-name -> atom lookup is precomputed once as atom_by_name_global. local field_type_by_name = {} ---@type table -- bag if atom.rbind and atom.rbind.fields then @@ -2194,7 +2194,7 @@ local function build_inserted_children(main_cu_offset, main_cu_end_excl, atom_ta end -- step (d) inputs: this atom's `atom_phase(