From 2a087f735e9ae8774da9dbf9e90d2484eb6a796f Mon Sep 17 00:00:00 2001 From: Ed_ Date: Wed, 19 Aug 2026 23:35:57 -0400 Subject: [PATCH] utilizing trailing type annotations more --- scripts/duffle.lua | 58 +- scripts/duffle_isa.lua | 9 +- scripts/duffle_paths.lua | 30 +- scripts/duffle_scan.lua | 618 +++------ scripts/elf32.lua | 146 +- scripts/elf_dwarf.lua | 550 +++----- scripts/passes/annotation.lua | 177 +-- scripts/passes/atoms_source_map.lua | 225 +-- scripts/passes/auto_reg.lua | 213 +-- scripts/passes/components.lua | 405 ++---- scripts/passes/dwarf_injection.lua | 1269 ++++++----------- scripts/passes/emission_model.lua | 141 +- scripts/passes/offsets.lua | 111 +- scripts/passes/report.lua | 558 +++----- scripts/passes/scan_source.lua | 1455 +++++++------------- scripts/passes/static_analysis.lua | 1986 +++++++++------------------ scripts/passes/word_count_eval.lua | 24 +- scripts/ps1_meta.lua | 278 ++-- 18 files changed, 2785 insertions(+), 5468 deletions(-) diff --git a/scripts/duffle.lua b/scripts/duffle.lua index b05e391..fae54c4 100644 --- a/scripts/duffle.lua +++ b/scripts/duffle.lua @@ -3,36 +3,32 @@ --- @class DuffleExport --- bag: open module-export keys from duffle_scan / duffle_isa / duffle_emit ---- @type DuffleExport -local scan = require("duffle_scan") ---- @type DuffleExport -local isa = require("duffle_isa") ---- @type DuffleExport -local emit = require("duffle_emit") ---- @type DuffleExport -local M = {} +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 ---- @alias Path string ---- @alias LineNum integer ---- @alias ByteOff integer +--- @alias Path string +--- @alias LineNum integer +--- @alias ByteOff integer --- @alias MacroName string ---- @alias AtomName string ---- @alias Severity string +--- @alias AtomName string +--- @alias Severity string --- @class SourceFile ---- @field path Path ---- @field text string ---- @field dir string +--- @field path Path +--- @field text string +--- @field dir string --- @field basename string ---- @field scan SourceScan|nil +--- @field scan SourceScan|nil --- @class CorpusView ---- @field register_alias_registry table ---- @field type_name_registry table +--- @field register_alias_registry table +--- @field type_name_registry table --- @field atom_views table --- @field atom_ctxs table ---- @field atom_phases table ---- @field binds_by_name table +--- @field atom_phases table +--- @field binds_by_name table --- @field atoms_by_name table --- @field atom_infos AtomInfoEntry[] --- @field components table @@ -42,12 +38,11 @@ local M = {} --- @field source_order SourceFile[] --- @field collisions CorpusCollision[] ---- @param src DuffleExport +--- @param src DuffleExport --- @param label string --- @return nil local function merge(src, label) - --- @type string, any - for k, v in pairs(src) do + for k, v in pairs(src) do ---@type string, any if M[k] ~= nil and M[k] ~= v then error("duffle facade name collision on " .. tostring(k) .. " from " .. label, 0) end @@ -62,8 +57,7 @@ merge(emit, "duffle_emit") --- @param ctx PassCtx --- @return CorpusView function M.corpus_view(ctx) - --- @type Corpus - local corpus = ctx and ctx.shared and ctx.shared.corpus + local corpus = ctx and ctx.shared and ctx.shared.corpus ---@type Corpus if not corpus then error("requires ctx.shared.corpus", 0) end return { register_alias_registry = corpus.register_alias_registry or {}, @@ -83,17 +77,15 @@ function M.corpus_view(ctx) } end ---- @param rules CheckRule[] ---- @param phase string ---- @param item AtomEntry|SourceFile +--- @param rules CheckRule[] +--- @param phase string +--- @param item AtomEntry|SourceFile --- @param pipe_ctx PipeCtx --- @param findings CheckFinding[] --- @return nil function M.run_check_rules(rules, phase, item, pipe_ctx, findings) - --- @type integer, CheckRule - for _, rule in ipairs(rules) do - --- @type (fun(item: AtomEntry|SourceFile, pipe_ctx: PipeCtx, findings: CheckFinding[]): nil)|nil - local fn = rule[phase] + for _, rule in ipairs(rules) do ---@type integer, CheckRule + local fn = rule[phase] ---@type (fun(item: AtomEntry|SourceFile, pipe_ctx: PipeCtx, findings: CheckFinding[]): nil)|nil if fn then fn(item, pipe_ctx, findings) end end end diff --git a/scripts/duffle_isa.lua b/scripts/duffle_isa.lua index d9af32b..9fc1105 100644 --- a/scripts/duffle_isa.lua +++ b/scripts/duffle_isa.lua @@ -106,8 +106,7 @@ --- @field HARDWARE_RELATIONS HardwareRelationRow[] --- @field CU2_TRANSITION_POLICY Cu2TransitionPolicy ---- @type DuffleIsa -local M = {} +local M = {} ---@type DuffleIsa -- Section 7: domain tables -- ════════════════════════════════════════════════════════════════════════════ @@ -422,11 +421,9 @@ function M.gte (ident) return M.GTE_COMMAND[M.gte_canon(ident)] end local function build_alias_map() --- @type table -- bag: alias or canon -> canon M.ALIAS_TO_CANONICAL = {} - --- @type string, GteCommandRow - for canon, row in pairs(M.GTE_COMMAND) do + for canon, row in pairs(M.GTE_COMMAND) do ---@type string, GteCommandRow M.ALIAS_TO_CANONICAL[canon] = canon - --- @type integer, string - for _, alias in ipairs(row.aliases or {}) do + for _, alias in ipairs(row.aliases or {}) do ---@type integer, string M.ALIAS_TO_CANONICAL[alias] = canon end end diff --git a/scripts/duffle_paths.lua b/scripts/duffle_paths.lua index 43a45ee..9fd48ed 100644 --- a/scripts/duffle_paths.lua +++ b/scripts/duffle_paths.lua @@ -19,12 +19,10 @@ --- @class DufflePaths --- @field setup fun(): nil ---- @type DufflePaths -local M = {} +local M = {} ---@type DufflePaths -- Cache key for the repo root. Stored in `package.loaded` (process-global) so all 8 entry scripts + passes scripts share one resolution. ---- @type string -local CACHE_KEY = "__duffle_repo_root__" +local CACHE_KEY = "__duffle_repo_root__" ---@type string --- Resolve the repo root from this script's own path. Zero shell spawn. --- `duffle_paths.lua` always lives at `/scripts/duffle_paths.lua`, so the repo root is the parent of the directory containing this script. @@ -35,17 +33,14 @@ local CACHE_KEY = "__duffle_repo_root__" local function find_repo_root() if package.loaded[CACHE_KEY] then return package.loaded[CACHE_KEY] end - --- @type string - local source = debug.getinfo(1, "S").source + local source = debug.getinfo(1, "S").source ---@type string -- Strip the leading `@` (Lua's dofile marker) and the trailing `/duffle_paths.lua` filename. -- What remains is the directory containing this script, i.e. `/scripts/`. - --- @type string|nil - local scripts_dir = source and source:match("^@?(.*)[/\\]duffle_paths%.lua$") + local scripts_dir = source and source:match("^@?(.*)[/\\]duffle_paths%.lua$") ---@type string|nil if not scripts_dir then return nil end -- The repo root is the parent of `scripts/`. Strip the trailing `scripts/` (with or without trailing slash). - --- @type string - local root = scripts_dir:gsub("scripts[\\/]?$", "") + local root = scripts_dir:gsub("scripts[\\/]?$", "") ---@type string root = root:gsub("\\", "/") if root == "" then root = "./" end if not root:match("/$") then root = root .. "/" end @@ -60,8 +55,7 @@ end --- lpeg is built by `update_deps.ps1` to `toolchain/lpeg/`, which we wire into `package.cpath` here (so `require("lpeg")` from `duffle.lua` resolves without any global state). --- @return nil function M.setup() - --- @type string|nil - local repo_root = find_repo_root() + local repo_root = find_repo_root() ---@type string|nil if not repo_root then -- Unreachable in practice: find_repo_root() derives the repo root from this script's own source path via debug.getinfo(1, "S").source (no subprocess, no git CLI, <1ms). -- A nil return means the source path did not match the expected /scripts/duffle_paths.lua layout — a packaging bug, not a "missing git repo" condition. @@ -69,10 +63,8 @@ function M.setup() os.exit(2) end - --- @type string - local scripts_dir = repo_root .. "scripts/" - --- @type string - local passes_dir = repo_root .. "scripts/passes/" + local scripts_dir = repo_root .. "scripts/" ---@type string + local passes_dir = repo_root .. "scripts/passes/" ---@type string package.path = scripts_dir .. "?.lua;" .. scripts_dir .. "?/init.lua;" .. passes_dir .. "?.lua;" @@ -82,10 +74,8 @@ function M.setup() -- lpeg: built by `update_deps.ps1` to `toolchain/lpeg/lpeg.dll`. -- lfs: compiled from pcsx-redux's vendored luafilesystem source to `toolchain/lfs/lfs.dll`. -- Wire both directories into cpath so `require("lpeg")` and `require("lfs")` resolve. - --- @type string - local lpeg_dir = repo_root .. "toolchain/lpeg/" - --- @type string - local lfs_dir = repo_root .. "toolchain/lfs/" + local lpeg_dir = repo_root .. "toolchain/lpeg/" ---@type string + local lfs_dir = repo_root .. "toolchain/lfs/" ---@type string package.cpath = lpeg_dir .. "?.dll;" .. lfs_dir .. "?.dll;" .. package.cpath diff --git a/scripts/duffle_scan.lua b/scripts/duffle_scan.lua index a794fb8..7db3b00 100644 --- a/scripts/duffle_scan.lua +++ b/scripts/duffle_scan.lua @@ -68,70 +68,44 @@ --- @field skipped ResolverEvidence[] --- @field shadowed ResolverEvidence[] ---- @type DuffleScan -local M = {} +local M = {} ---@type DuffleScan -- Required native extension: lfs (LuaFileSystem). Built by `update_deps.ps1` to `toolchain/lfs/lfs.dll` and wired into package.cpath by `scripts/duffle_paths.lua`. -- If lfs is missing, `require` throws — fail loud per the build-tool convention. ---- @type LfsMod -local lfs = require("lfs") +local lfs = require("lfs") ---@type LfsMod -- ════════════════════════════════════════════════════════════════════════════ -- ASCII byte constants -- ════════════════════════════════════════════════════════════════════════════ ---- @type integer -local BYTE_SPACE = 0x20 -- ' ' ---- @type integer -local BYTE_TAB = 0x09 -- '\t' ---- @type integer -local BYTE_NEWLINE = 0x0A -- '\n' ---- @type integer -local BYTE_CR = 0x0D -- '\r' ---- @type integer -local BYTE_VT = 0x0B -- '\v' ---- @type integer -local BYTE_FF = 0x0C -- '\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' ---- @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 -- ';' +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 -- ';' ---- @type integer -local BYTE_OPEN_PAREN = 0x28 -- '(' ---- @type integer -local BYTE_OPEN_BRACE = 0x7B -- '{' ---- @type integer -local BYTE_OPEN_BRACK = 0x5B -- '[' +local BYTE_OPEN_PAREN = 0x28 ---@type integer -- '(' +local BYTE_OPEN_BRACE = 0x7B ---@type integer -- '{' +local BYTE_OPEN_BRACK = 0x5B ---@type integer -- '[' ---- @type integer -local BYTE_LOWER_A = 0x61 -- 'a' ---- @type integer -local BYTE_LOWER_Z = 0x7A -- 'z' ---- @type integer -local BYTE_UPPER_A = 0x41 -- 'A' ---- @type integer -local BYTE_UPPER_Z = 0x5A -- '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' ---- @type integer -local BYTE_DIGIT_0 = 0x30 -- '0' ---- @type integer -local BYTE_DIGIT_9 = 0x39 -- '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) @@ -145,47 +119,33 @@ local BYTE_DIGIT_9 = 0x39 -- '9' -- LPeg handles the high-level scanner; the byte-by-byte helpers in Section 1 handle classification primitives that LPeg's CPython-level cost would dominate. -- -- If the require fails, fail loud with an actionable message. The build script (`update_deps.ps1`) builds lpeg.dll into `toolchain/lpeg/`; run it when the dll is missing. ---- @type boolean, LpegMod|string -local lpeg_ok, lpeg = pcall(require, "lpeg") +local lpeg_ok, lpeg = pcall(require, "lpeg") ---@type boolean, LpegMod|string if not lpeg_ok then io.stderr:write("[duffle] require('lpeg') failed: ", lpeg, "\n") io.stderr:write("[duffle] lpeg.dll not found on package.cpath.\n") io.stderr:write("[duffle] Run 'scripts/update_deps.ps1' to build it into toolchain/lpeg/.\n") os.exit(1) end ---- @type LpegCtor, LpegCtor, LpegCtor -local P, S, R = lpeg.P, lpeg.S, lpeg.R +local P, S, R = lpeg.P, lpeg.S, lpeg.R ---@type LpegCtor, LpegCtor, LpegCtor -- Character class patterns ---- @type LpegPattern -local alpha_pat = R("AZ", "az") + P("_") ---- @type LpegPattern -local digit_pat = R("09") ---- @type LpegPattern -local lpeg_alnum_pat = alpha_pat + digit_pat +local alpha_pat = R("AZ", "az") + P("_") ---@type LpegPattern +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. ---- @type LpegPattern -local lpeg_alpha_pat = alpha_pat ---- @type LpegPattern -local lpeg_ident_pat = lpeg.C(alpha_pat * lpeg_alnum_pat^0) +local lpeg_alpha_pat = alpha_pat ---@type LpegPattern +local lpeg_ident_pat = lpeg.C(alpha_pat * lpeg_alnum_pat^0) ---@type LpegPattern ---- @type LpegPattern -local lpeg_str_pat = P('"') * (P(1) - S('"\\') + P('\\') * P(1))^0 * P('"') -- String literal: "..." with backslash escapes. ---- @type LpegPattern -local lpeg_chr_pat = P("'") * (P(1) - S("'\\") + P('\\') * P(1))^0 * P("'") -- Char literal: '...' with backslash escapes. ---- @type LpegPattern -local lpeg_line_cmt_pat = P("//") * (P(1) - S("\n"))^0 -- Line comment: // ... to end-of-line. ---- @type LpegPattern -local lpeg_block_cmt_pat = P("/*") * (P(1) - P("*/"))^0 * P("*/") -- Block comment: /* ... */ (no nesting per C standard). ---- @type LpegPattern -local lpeg_str_or_cmt_pat = lpeg_str_pat + lpeg_chr_pat + lpeg_line_cmt_pat + lpeg_block_cmt_pat -- String or comment (any of the four forms). +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). ---- @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 +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. -- Used by scan_to_char for non-ident / non-bracket chars. We accept any single char except the target. @@ -258,10 +218,8 @@ function M.is_alnum(c) return M.is_alpha(c) or M.is_digit(c) end --- @param s string --- @return string function M.trim(s) - --- @type integer - local a = 1; while a <= #s and M.is_space_byte(s:byte(a)) do a = a + 1 end - --- @type integer - local b = #s; while b >= a and M.is_space_byte(s:byte(b)) do b = b - 1 end + local a = 1; while a <= #s and M.is_space_byte(s:byte(a)) do a = a + 1 end ---@type integer + local b = #s; while b >= a and M.is_space_byte(s:byte(b)) do b = b - 1 end ---@type integer return s:sub(a, b) end @@ -271,8 +229,7 @@ end --- @param start integer -- optional 1-indexed start (default 1) --- @return integer|nil function M.find_byte(haystack, target, start) - --- @type integer - for pos = start or 1, #haystack do + for pos = start or 1, #haystack do ---@type integer if haystack:byte(pos) == target then return pos end end return nil @@ -282,12 +239,9 @@ end --- @param path Path --- @return Path function M.dirname(path) - --- @type integer - local last_sep = 0 - --- @type integer - for pos = 1, #path do - --- @type integer - local b = path:byte(pos) + 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 if last_sep == 0 then return "." end @@ -298,20 +252,14 @@ end --- @param path Path --- @return string function M.basename_no_ext(path) - --- @type integer - local last_sep = 0 - --- @type integer - for pos = 1, #path do - --- @type integer - local b = path:byte(pos) + 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 - --- @type integer - local a = last_sep + 1 - --- @type integer - local last_dot = #path + 1 - --- @type integer - for pos = #path, a, -1 do + local a = last_sep + 1 ---@type integer + local last_dot = #path + 1 ---@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) @@ -322,12 +270,10 @@ end --- @param input string --- @return PathRoot local function parse_path_root(input) - --- @type string|nil - local drive = input:match("^(%a:)") + local drive = input:match("^(%a:)") ---@type string|nil if drive then if input:sub(3, 3) == "/" then - --- @type string - local rest = input:sub(4) + local rest = input:sub(4) ---@type string while rest:sub(1, 1) == "/" do rest = rest:sub(2) end return { kind = "drive_absolute", prefix = drive .. "/", rest = rest, anchored = true } end @@ -335,29 +281,22 @@ local function parse_path_root(input) end if input:sub(1, 2) == "//" then - --- @type integer - local server_start = 3 - --- @type integer|nil - local server_end = M.find_byte(input, BYTE_SLASH, server_start) + 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 - --- @type string - local server = input:sub(server_start, server_end - 1) - --- @type integer - local share_start = server_end + 1 + local server = input:sub(server_start, server_end - 1) ---@type string + local share_start = server_end + 1 ---@type integer while input:sub(share_start, share_start) == "/" do share_start = share_start + 1 end - --- @type integer - local share_end = M.find_byte(input, BYTE_SLASH, share_start) or (#input + 1) + local share_end = M.find_byte(input, BYTE_SLASH, share_start) or (#input + 1) ---@type integer if share_end == share_start then error("UNC path requires //server/share: " .. input, 3) end - --- @type string - local share = input:sub(share_start, share_end - 1) - --- @type string - local rest = input:sub(share_end + 1) + local share = input:sub(share_start, 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", @@ -368,8 +307,7 @@ local function parse_path_root(input) end if input:sub(1, 1) == "/" then - --- @type string - local rest = input:sub(2) + local rest = input:sub(2) ---@type string while rest:sub(1, 1) == "/" do rest = rest:sub(2) end return { kind = "posix_absolute", prefix = "/", rest = rest, anchored = true } end @@ -384,12 +322,9 @@ function M.normalize_path(path) if type(path) ~= "string" then error("normalize_path requires a string path", 2) end if path == "" then return "" end - --- @type PathRoot - local root = parse_path_root(path:gsub("\\", "/")) - --- @type string[] - local segments = {} - --- @type string - for segment in root.rest:gmatch("[^/]+") do + local root = parse_path_root(path:gsub("\\", "/")) ---@type PathRoot + local segments = {} ---@type string[] + for segment in root.rest:gmatch("[^/]+") do ---@type string if segment == "." then -- no-op elseif segment == ".." then @@ -403,8 +338,7 @@ function M.normalize_path(path) end end - --- @type string - local tail = table.concat(segments, "/") + local tail = table.concat(segments, "/") ---@type string if root.kind == "relative" then return tail ~= "" and tail or "." end if root.kind == "drive_relative" then return root.prefix .. tail end if root.kind == "unc_absolute" then return tail ~= "" and (root.prefix .. "/" .. tail) or root.prefix end @@ -414,10 +348,8 @@ end --- @param path Path --- @return Path local function absolute_normalized_path(path) - --- @type Path - local normalized = M.normalize_path(path) - --- @type PathRoot - local root = parse_path_root(normalized) + 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) end @@ -430,15 +362,12 @@ end --- @param path Path --- @return string function M.canonical_path_key(path) - --- @type Path - local normalized = M.normalize_path(path) - --- @type PathRoot - local root = parse_path_root(normalized) + local normalized = M.normalize_path(path) ---@type Path + local root = parse_path_root(normalized) ---@type PathRoot if root.kind == "drive_relative" then error("canonical_path_key cannot compare drive-relative path: " .. normalized, 2) end - --- @type string - local key = absolute_normalized_path(normalized):lower() + local key = absolute_normalized_path(normalized):lower() ---@type string if #key > 3 and key:sub(-1) == "/" then key = key:sub(1, -2) end return key end @@ -453,38 +382,33 @@ end --- @param path Path --- @return string function M.read_file(path) - --- @type file*|nil - local f = io.open(path, "r") + local f = io.open(path, "r") ---@type file*|nil if not f then error("Cannot open " .. path) end - --- @type string - local content = f:read("*a"); f:close() + local content = f:read("*a"); f:close() ---@type string return content end ---- @param path Path +--- @param path Path --- @param content string --- @return nil function M.write_file(path, content) - --- @type file*|nil - local f = io.open(path, "w") + local f = io.open(path, "w") ---@type file*|nil if not f then error("Cannot write " .. path) end f:write(content); f:close() end --- Write content to disk in binary mode so LF line endings are preserved on Windows --- (text mode would convert LF -> CRLF, breaking byte-identical diffs against git-tracked gen/*.h files which are stored as LF). ---- @param path string +--- @param path string --- @param content string --- @return nil function M.write_file_lf(path, content) - --- @type file*|nil - local f = io.open(path, "wb") + local f = io.open(path, "wb") ---@type file*|nil if not f then error("Cannot write " .. path) end f:write(content); f:close() end ---- @type table -- bag: input path -> absolute path -local _absolute_path_cache = {} +local _absolute_path_cache = {} ---@type table -- bag: input path -> absolute path --- Convert a (possibly relative) path to an absolute path, using CWD if needed. --- Normalizes forward slashes to backslashes on Windows. @@ -496,26 +420,21 @@ function M.to_absolute_path(path) if _absolute_path_cache[path] then return _absolute_path_cache[path] end if #path >= 2 and path:sub(2, 2) == ":" then -- Already absolute; normalize slashes for consistency. - --- @type string - local result = (path:gsub("/", "\\")) + local result = (path:gsub("/", "\\")) ---@type string _absolute_path_cache[path] = result return result end - --- @type string|nil - local cwd = lfs.currentdir() + local cwd = lfs.currentdir() ---@type string|nil if not cwd then _absolute_path_cache[path] = path; return path end cwd = cwd:gsub("/", "\\") - --- @type string - local tail = (path:gsub("/", "\\")) - --- @type string - local result = cwd .. "\\" .. tail + local tail = (path:gsub("/", "\\")) ---@type string + local result = cwd .. "\\" .. tail ---@type string _absolute_path_cache[path] = result return result end -- Cache of directories already verified to exist in this process. ---- @type table -- bag: dir path -> already ensured -local _ensured_dirs = {} +local _ensured_dirs = {} ---@type table -- bag: dir path -> already ensured --- @param path Path --- @return nil @@ -533,10 +452,8 @@ end --- @param sources SourceFile[] --- @return table function M.group_sources_by_dir(sources) - --- @type table - local by_dir = {} - --- @type integer, SourceFile - for _, src in ipairs(sources) do + 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) end @@ -549,53 +466,47 @@ end -- Skip a string or C-style comment starting at position `pos`. -- Returns the position just past the construct, or `pos` unchanged if no string/comment starts there. ---- @param s string +--- @param s string --- @param pos integer --- @return integer function M.skip_str_or_cmt(s, pos) return lpeg.match(lpeg_str_or_cmt_pat, s, pos) or pos end -- Skip whitespace AND C-style comments starting at position `pos`. -- LPeg-backed; ~5-10x faster than a hand-rolled byte-by-byte walker. ---- @param s string +--- @param s string --- @param pos integer --- @return integer function M.skip_ws_and_cmt(s, pos) return lpeg.match(lpeg_ws_and_cmt_pat, s, pos) or pos end -- Read a C-style identifier (alpha followed by zero+ alnum) starting at position `pos`. -- Returns the identifier string + the position just past it, or nil + pos if no identifier starts here. ---- @param s string +--- @param s string --- @param pos integer --- @return string|nil, integer function M.read_ident(s, pos) - --- @type string|nil - local result = lpeg.match(lpeg_ident_pat, s, pos) + local result = lpeg.match(lpeg_ident_pat, s, pos) ---@type string|nil if result then return result, pos + #result end return nil, pos end -- Read a balanced-delimited group (parens, braces, or brackets) starting at position `pos`. -- Returns the inner content (between the delimiters) + the position just past the closing delimiter, or nil + pos if `s[pos]` isn't `open_char`. ---- @param s string ---- @param open_char string +--- @param s string +--- @param open_char string --- @param close_char string --- @param pos integer --- @return string|nil, integer function M.read_balanced(s, open_char, close_char, pos) - --- @type integer - local open_byte = open_char:byte() + local open_byte = open_char:byte() ---@type integer if s:byte(pos) ~= open_byte then return nil, pos end -- scan: pos = pos + 1 -- scan: - --- @type integer - local len = #s - --- @type integer - local depth = 1 - --- @type integer - local a = pos + local len = #s ---@type integer + local depth = 1 ---@type integer + local a = pos ---@type integer while pos <= len and depth > 0 do - --- @type integer - local c = s:byte(pos) + local c = s:byte(pos) ---@type integer if c == open_byte then depth = depth + 1 pos = pos + 1 @@ -606,8 +517,7 @@ function M.read_balanced(s, open_char, close_char, pos) pos = pos + 1 -- scan: (depth=depth) else - --- @type integer - local nx = M.skip_str_or_cmt(s, pos) + local nx = M.skip_str_or_cmt(s, pos) ---@type integer if nx > pos then -- scan: pos = nx @@ -621,40 +531,36 @@ function M.read_balanced(s, open_char, close_char, pos) end -- Convenience specializations of read_balanced. ---- @param s string +--- @param s string --- @param pos integer --- @return string|nil, integer M.read_parens = function(s, pos) return M.read_balanced(s, "(", ")", pos) end ---- @param s string +--- @param s string --- @param pos integer --- @return string|nil, integer M.read_braces = function(s, pos) return M.read_balanced(s, "{", "}", pos) end ---- @param s string +--- @param s string --- @param pos integer --- @return string|nil, integer M.read_brackets = function(s, pos) return M.read_balanced(s, "[", "]", pos) end -- Scan forward from position `start` until we find a specific single byte `target`, transparently stepping over balanced parens/braces/brackets. -- Returns the position of `target`, or nil if not found. ---- @param s string +--- @param s string --- @param target string ---- @param start integer +--- @param start integer --- @return integer|nil function M.scan_to_char(s, target, start) - --- @type integer - local target_byte = target:byte() - --- @type integer - local pos = start + local target_byte = target:byte() ---@type integer + local pos = start ---@type integer while pos <= #s do - --- @type integer - local c = s:byte(pos) + local c = s:byte(pos) ---@type integer if c == target_byte then return pos end -- scan: ... | if c == BYTE_OPEN_PAREN then local _, a = M.read_balanced(s, "(", ")", pos); pos = a -- scan: ... ( ) ... elseif c == BYTE_OPEN_BRACE then local _, a = M.read_balanced(s, "{", "}", pos); pos = a -- scan: ... { } ... elseif c == BYTE_OPEN_BRACK then local _, a = M.read_balanced(s, "[", "]", pos); pos = a -- scan: ... [ ] ... else - --- @type integer - local nx = M.skip_str_or_cmt(s, pos) + local nx = M.skip_str_or_cmt(s, pos) ---@type integer pos = (nx > pos) and nx or (pos + 1) -- scan: ... ... end @@ -665,15 +571,13 @@ end -- If `s[pos]` is `#`, skip to the end of the preprocessor directive line (past the newline). -- Returns the position past the newline, or nil if `s[pos]` is not `#`. -- scan: #\n -> past the newline ---- @param s string +--- @param s string --- @param pos integer --- @return integer|nil function M.skip_preprocessor_line(s, pos) if s:byte(pos) ~= 35 then return nil end -- '#' - --- @type integer - local scan = pos - --- @type integer - local len = #s + local scan = pos ---@type integer + local len = #s ---@type integer while scan <= len and s:byte(scan) ~= BYTE_NEWLINE do scan = scan + 1 end return scan + 1 end @@ -684,30 +588,27 @@ local function is_horizontal_space(byte) return byte == BYTE_SPACE or byte == BYTE_TAB or byte == BYTE_CR or byte == BYTE_VT or byte == BYTE_FF end ---- @param source string ---- @param first integer +--- @param source string +--- @param first integer --- @param after_last integer --- @return boolean local function segment_has_newline(source, first, after_last) - --- @type integer - for pos = first, after_last - 1 do + for pos = first, after_last - 1 do ---@type integer if source:byte(pos) == BYTE_NEWLINE then return true end end return false end --- @param source string ---- @param pos integer +--- @param pos integer --- @return integer|nil local function skip_directive_space(source, pos) while pos <= #source do - --- @type integer - local byte = source:byte(pos) + local byte = source:byte(pos) ---@type integer if is_horizontal_space(byte) then pos = pos + 1 elseif byte == BYTE_SLASH and source:byte(pos + 1) == BYTE_STAR then - --- @type integer - local after = M.skip_str_or_cmt(source, pos) + local after = M.skip_str_or_cmt(source, pos) ---@type integer if after == pos or segment_has_newline(source, pos, after) then return nil end pos = after elseif byte == BYTE_SLASH and source:byte(pos + 1) == BYTE_SLASH then @@ -724,21 +625,14 @@ end --- @param source string --- @return string, integer[], integer[] local function splice_c_lines(source) - --- @type string[] - local logical_bytes = {} - --- @type integer[] - local physical_pos = {} - --- @type integer[] - local physical_line = {} - --- @type integer - local pos = 1 - --- @type integer - local line = 1 + local logical_bytes = {} ---@type string[] + local physical_pos = {} ---@type integer[] + local physical_line = {} ---@type integer[] + local pos = 1 ---@type integer + local line = 1 ---@type integer while pos <= #source do - --- @type integer - local byte = source:byte(pos) - --- @type integer|nil - local splice_len = nil + local byte = source:byte(pos) ---@type integer + 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 @@ -749,8 +643,7 @@ local function splice_c_lines(source) pos = pos + splice_len line = line + 1 else - --- @type integer - local logical_pos = #logical_bytes + 1 + local logical_pos = #logical_bytes + 1 ---@type integer logical_bytes[logical_pos] = source:sub(pos, pos) physical_pos [logical_pos] = pos physical_line[logical_pos] = line @@ -774,17 +667,12 @@ 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. - --- @type string, integer[], integer[] - local logical_text, physical_pos, physical_line = splice_c_lines(source_text) - --- @type QuotedInclude[] - local includes = {} - --- @type integer - local pos = 1 - --- @type boolean - local line_leading = true + 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 while pos <= #logical_text do - --- @type integer - local byte = logical_text:byte(pos) + local byte = logical_text:byte(pos) ---@type integer if byte == BYTE_NEWLINE then -- line break; refresh leading-whitespace state for next line. line_leading = true @@ -794,22 +682,19 @@ function M.parse_direct_quoted_includes(source_text) pos = pos + 1 elseif byte == BYTE_SLASH and logical_text:byte(pos + 1) == BYTE_SLASH then -- '//' line comment: skip_str_or_cmt walks to EOL on its own, so no separate newline scan is needed here. - --- @type integer - local after = M.skip_str_or_cmt(logical_text, pos) + local after = M.skip_str_or_cmt(logical_text, pos) ---@type integer -- pos := after when the skipper agrees, else single-byte advance. pos = (after > pos) and after or (pos + 1) elseif byte == BYTE_SLASH and logical_text:byte(pos + 1) == BYTE_STAR then -- '/*' block comment. - --- @type integer - local after = M.skip_str_or_cmt(logical_text, pos) + local after = M.skip_str_or_cmt(logical_text, pos) ---@type integer if after <= pos then -- skipper refused (unterminated /*). Treat this byte as ordinary content: step one, mark non-leading. line_leading = false pos = pos + 1 else -- jump past the closing '*/'. The span may cross lines, so rescan for embedded '\n' to refresh line_leading. - --- @type integer - for scan = pos, after - 1 do + for scan = pos, after - 1 do ---@type integer if logical_text:byte(scan) == BYTE_NEWLINE then line_leading = true end end pos = after @@ -817,17 +702,14 @@ function M.parse_direct_quoted_includes(source_text) elseif byte == BYTE_DQUOTE or byte == BYTE_SQUOTE then -- enter + leave the string literal in one skip; literal bodies cannot contain a directive regardless of what they look like. line_leading = false - --- @type integer - local after = M.skip_str_or_cmt(logical_text, pos) + local after = M.skip_str_or_cmt(logical_text, pos) ---@type integer pos = (after > pos) and after or (pos + 1) elseif byte == 35 and line_leading then -- '#' at line head -- Sequential pre-checks; any one failing falls through to ::not_include:: (single-byte advance). -- 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.) - --- @type integer, integer, integer|nil, string|nil, integer, integer - local hash_pos, directive_line, scan, ident, after_ident, after_quote - --- @type string, integer, integer - local include_path, physical_first, physical_last + 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 hash_pos = pos directive_line = physical_line[hash_pos] or 1 scan = skip_directive_space(logical_text, pos + 1) @@ -871,32 +753,29 @@ function M.parse_direct_quoted_includes(source_text) return includes end ---- @param path Path +--- @param path Path --- @param wanted string --- @return boolean local function path_has_segment(path, wanted) - --- @type string - for segment in M.normalize_path(path):gmatch("[^/]+") do + for segment in M.normalize_path(path):gmatch("[^/]+") do ---@type string if segment:lower() == wanted then return true end end return false end --- @param candidate_key string ---- @param root_key string +--- @param root_key string --- @return boolean local function canonical_key_is_within(candidate_key, root_key) if candidate_key == root_key then return true end - --- @type string - local prefix = root_key .. "/" + local prefix = root_key .. "/" ---@type string return candidate_key:sub(1, #prefix) == prefix end --- @param path Path --- @return SourceFile local function load_source_record(path) - --- @type Path - local normalized = absolute_normalized_path(path) + local normalized = absolute_normalized_path(path) ---@type Path return { path = normalized, text = M.read_file(normalized), @@ -919,62 +798,47 @@ function M.resolve_source_corpus(options) error("resolve_source_corpus requires options.project_root", 2) end - --- @type Path - local project_root = absolute_normalized_path(options.project_root) - --- @type Path - local code_root = M.normalize_path(project_root .. "/code") - --- @type string - local code_root_key = M.canonical_path_key(code_root) - --- @type SourceFile - local root = load_source_record(options.unity_root) - --- @type SourceFile[] - local source_order = { root } - --- @type table - local sources_by_path = { [M.canonical_path_key(root.path)] = root, } - --- @type SourceResolver - local resolver = { + local project_root = absolute_normalized_path(options.project_root) ---@type Path + 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 resolved = { { - include_path = nil, - include_text = nil, - root_source = root.path, - root_line = 1, - candidate_a = root.path, - candidate_b = nil, + include_path = nil, + include_text = nil, + root_source = root.path, + root_line = 1, + candidate_a = root.path, + candidate_b = nil, selected_path = root.path, - disposition = "root", + disposition = "root", }, }, skipped = {}, shadowed = {}, } - --- @type integer, QuotedInclude - for _, include in ipairs(M.parse_direct_quoted_includes(root.text)) do - --- @type Path - local candidate_a = absolute_normalized_path(root.dir .. "/" .. include.path) - --- @type Path - local candidate_b = absolute_normalized_path(code_root .. "/" .. include.path) - --- @type string - local key_a = M.canonical_path_key(candidate_a) - --- @type string - local key_b = M.canonical_path_key(candidate_b) - --- @type boolean - 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 ResolverEvidence - local evidence = { - include_path = include.path, - include_text = include.include_text, - root_source = root.path, - root_line = include.line, - candidate_a = candidate_a, - candidate_b = candidate_b, + 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 + include_path = include.path, + include_text = include.include_text, + root_source = root.path, + root_line = include.line, + candidate_a = candidate_a, + candidate_b = candidate_b, candidate_a_in_code_root = inside_a, candidate_b_in_code_root = inside_b, - selected_path = nil, - disposition = nil, + selected_path = nil, + disposition = nil, } if not inside_a and not inside_b then @@ -988,16 +852,11 @@ function M.resolve_source_corpus(options) resolver.skipped[#resolver.skipped + 1] = evidence else -- Boundary checks above deliberately precede every filesystem probe. - --- @type boolean - 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 Path|nil - local selected = nil - --- @type string|nil - local selected_key = nil - --- @type string|nil - local disposition = nil + 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 if exists_a then selected = candidate_a selected_key = key_a @@ -1034,8 +893,7 @@ function M.resolve_source_corpus(options) evidence.duplicate_of = sources_by_path[selected_key].path resolver.skipped[#resolver.skipped + 1] = evidence else - --- @type SourceFile - local source = load_source_record(selected) + local source = load_source_record(selected) ---@type SourceFile evidence.disposition = disposition source_order[#source_order + 1] = source sources_by_path[selected_key] = source @@ -1062,30 +920,23 @@ end --- @param body string --- @return string[] function M.split_top_level_commas(body) - --- @type string[] - local tokens = {} - --- @type integer - local pos = 1 - --- @type integer - local body_len = #body - --- @type integer - local token_start = 1 + local tokens = {} ---@type string[] + local pos = 1 ---@type integer + local body_len = #body ---@type integer + local token_start = 1 ---@type integer -- True iff `chunk` contains any non-whitespace, non-comment, non-string content (i.e., real token material). -- Walks through ws + comments individually so a chunk like " /* trailing */ shift_lleft(...)" is correctly classified as having real content (the macro call). --- @param chunk string --- @return boolean local function has_real_content(chunk) - --- @type integer - local scan = 1 - --- @type integer - local len = #chunk + local scan = 1 ---@type integer + local len = #chunk ---@type integer while scan <= len do if M.is_space_byte(chunk:byte(scan)) then scan = scan + 1 else - --- @type integer - local nx = M.skip_str_or_cmt(chunk, scan) + local nx = M.skip_str_or_cmt(chunk, scan) ---@type integer if nx > scan then scan = nx -- skipped a comment or string else @@ -1100,8 +951,7 @@ function M.split_top_level_commas(body) --- @return nil local function emit(end_pos) if end_pos >= token_start then - --- @type string - local chunk = body:sub(token_start, end_pos) + local chunk = body:sub(token_start, end_pos) ---@type string if M.trim(chunk) ~= "" then if has_real_content(chunk) then tokens[#tokens + 1] = chunk @@ -1118,8 +968,7 @@ function M.split_top_level_commas(body) end while pos <= body_len do - --- @type integer - local c = body:byte(pos) + local c = body:byte(pos) ---@type integer if c == BYTE_OPEN_PAREN then local _, a = M.read_parens(body, pos); pos = a -- scan: ... ( ... elseif c == BYTE_OPEN_BRACE then local _, a = M.read_braces(body, pos); pos = a -- scan: ... { ... elseif c == BYTE_OPEN_BRACK then local _, a = M.read_brackets(body, pos); pos = a -- scan: ... ( ... @@ -1139,8 +988,7 @@ function M.split_top_level_commas(body) pos = pos + 1 token_start = pos else - --- @type integer - local nx = M.skip_str_or_cmt(body, pos) + local nx = M.skip_str_or_cmt(body, pos) ---@type integer if nx > pos then -- scan: ... ... -- Skipped a comment or string at top level: emit token break. @@ -1160,10 +1008,8 @@ end -- Section 4: tokenize_body + build_body_line_index (shared, memoized) -- ════════════════════════════════════════════════════════════════════════════ ---- @type table -- bag: body text -> tokens -local _tokenize_body_cache = {} ---- @type table> -- bag: body text -> offset-to-line -local _body_line_index_cache = {} +local _tokenize_body_cache = {} ---@type table -- bag: body text -> tokens +local _body_line_index_cache = {} ---@type table> -- bag: body text -> offset-to-line --- Tokenize the body inner-text into a flat list of `{tok, rel}` pairs. --- `tok` is the trimmed token string; `rel` is the byte offset within `body`. @@ -1172,23 +1018,17 @@ local _body_line_index_cache = {} --- @return BodyToken[] function M.tokenize_body(body) if _tokenize_body_cache[body] ~= nil then return _tokenize_body_cache[body] end - --- @type BodyToken[] - local out = {} - --- @type integer - local len = #body - --- @type integer - local rel = 1 + local out = {} ---@type BodyToken[] + local len = #body ---@type integer + local rel = 1 ---@type integer while rel <= len do - --- @type integer - local ws_end = M.skip_ws_and_cmt(body, rel) + local ws_end = M.skip_ws_and_cmt(body, rel) ---@type integer if ws_end > rel then rel = ws_end end if rel > len then break end - --- @type integer - local scan = rel + local scan = rel ---@type integer while scan <= len do - --- @type integer - local c = body:byte(scan) + local c = body:byte(scan) ---@type integer -- Terminator bytes (delimit a token at the top level): ',' = 0x2C, '\n' = 0x0A, ';' = 0x3B. -- These also appear as separators between argument lists inside the parens/braces/brackets, so we stop the scan when we hit any of them. if c == BYTE_COMMA then break end @@ -1196,13 +1036,11 @@ function M.tokenize_body(body) if c == BYTE_SEMI then break end -- Line-comment '// ... \n' (0x2F 0x2F): skip to (and past) the next newline, or to end-of-body. if c == BYTE_SLASH and body:byte(scan + 1) == BYTE_SLASH then - --- @type integer|nil - local nl = M.find_byte(body, BYTE_NEWLINE, scan) + local nl = M.find_byte(body, BYTE_NEWLINE, scan) ---@type integer|nil scan = nl and (nl + 1) or (len + 1) -- Block-comment '/* ... */' (0x2F 0x2A): skip to (and past) the matching '*/', or to end-of-body. elseif c == BYTE_SLASH and body:byte(scan + 1) == BYTE_STAR then - --- @type integer|nil - local close = body:find("*/", scan + 2, true) + local close = body:find("*/", scan + 2, true) ---@type integer|nil scan = close and (close + 2) or (len + 1) -- Group opener bytes (consume the balanced group via the matching reader): '(' = 0x28, '{' = 0x7B, '[' = 0x5B. elseif c == BYTE_OPEN_PAREN then local _, a = M.read_parens (body, scan); scan = a @@ -1215,13 +1053,11 @@ function M.tokenize_body(body) scan = scan + 1 end end - --- @type string - local tok = M.trim(body:sub(rel, scan - 1)) + local tok = M.trim(body:sub(rel, scan - 1)) ---@type string if tok ~= "" then out[#out + 1] = { tok = tok, rel = rel } end if scan <= len then scan = scan + 1 - --- @type integer - local w = M.skip_ws_and_cmt(body, scan) + local w = M.skip_ws_and_cmt(body, scan) ---@type integer if w > scan then scan = w end end rel = scan @@ -1236,14 +1072,10 @@ end --- @return table -- bag: byte offset -> 1-based line function M.build_body_line_index(body) if _body_line_index_cache[body] ~= nil then return _body_line_index_cache[body] end - --- @type table -- bag: byte offset -> 1-based line - local index = {} - --- @type integer - local len = #body - --- @type integer - local newline_count = 0 - --- @type integer - for pos = 1, len do + 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 if pos > 1 then index[pos] = newline_count + 1 end @@ -1265,31 +1097,20 @@ end --- @param metadata_path Path --- @return WordCounts function M.load_word_counts(metadata_path) - --- @type WordCounts - local counts = {} - --- @type string - local content = M.read_file(metadata_path) - --- @type integer - local len = #content - --- @type integer - local pos = 1 - --- @type string - local prefix = "WORD_COUNT(" + local counts = {} ---@type WordCounts + local content = M.read_file(metadata_path) ---@type string + local len = #content ---@type integer + local pos = 1 ---@type integer + local prefix = "WORD_COUNT(" ---@type string while pos <= len do - --- @type integer|nil - local nl = M.find_byte(content, BYTE_NEWLINE, pos) - --- @type integer - local line_end = nl or (len + 1) - --- @type string - local line = content:sub(pos, line_end - 1) + local nl = M.find_byte(content, BYTE_NEWLINE, pos) ---@type integer|nil + local line_end = nl or (len + 1) ---@type integer + local line = content:sub(pos, line_end - 1) ---@type string -- scan: WORD_COUNT(, ) - --- @type string - local trimmed = M.trim(line) + local trimmed = M.trim(line) ---@type string if trimmed:sub(1, #prefix) == prefix and trimmed:sub(-1) == ")" then - --- @type string - local inner = trimmed:sub(#prefix + 1, #trimmed - 1) - --- @type integer|nil - local comma = M.find_byte(inner, BYTE_COMMA, 1) + local inner = trimmed:sub(#prefix + 1, #trimmed - 1) ---@type string + local comma = M.find_byte(inner, BYTE_COMMA, 1) ---@type integer|nil if comma then counts[M.trim(inner:sub(1, comma - 1))] = tonumber(M.trim(inner:sub(comma + 1))) @@ -1307,12 +1128,9 @@ end --- @param source string --- @return LineIndexFn function M.LineIndex(source) - --- @type integer[] - local positions = {} - --- @type integer - local n = 0 - --- @type integer - for pos = 1, #source do + local positions = {} ---@type integer[] + local n = 0 ---@type integer + for pos = 1, #source do ---@type integer if source:byte(pos) == BYTE_NEWLINE then n = n + 1 positions[n] = pos @@ -1322,11 +1140,9 @@ function M.LineIndex(source) --- @param query_pos integer --- @return integer local function line_of(query_pos) - --- @type integer, integer - local lo, hi = 1, n + local lo, hi = 1, n ---@type integer, integer while lo <= hi do - --- @type integer - local mid = math.floor((lo + hi) / 2) + local mid = math.floor((lo + hi) / 2) ---@type integer if positions[mid] <= query_pos then lo = mid + 1 else hi = mid - 1 end end diff --git a/scripts/elf32.lua b/scripts/elf32.lua index e5d75f6..cf212b6 100644 --- a/scripts/elf32.lua +++ b/scripts/elf32.lua @@ -109,8 +109,7 @@ --- @field read_named_section fun(adapter: Elf32Adapter, sections: Elf32Section[], name: string): string|nil, string|nil --- @field collect_symbols fun(adapter: Elf32Adapter, sections: Elf32Section[]): table|nil, string|nil ---- @type Elf32Mod -local M = {} +local M = {} ---@type Elf32Mod -- ════════════════════════════════════════════════════════════════════════════ -- Little-endian readers (bit-weighted accumulator, math.floor only) @@ -168,8 +167,7 @@ end --- @param off integer --- @return integer function M.read_u32_le(buf, off) - --- @type integer - local byte_off = off + 1 + local byte_off = off + 1 ---@type integer return buf:byte(byte_off) + buf:byte(byte_off + 0x01) * 0x00000100 + buf:byte(byte_off + 0x02) * 0x00010000 @@ -181,8 +179,7 @@ end --- @param off integer -- zero-based wire offset --- @return integer function M.read_u16_le(buf, off) - --- @type integer - local byte_off = off + 1 + local byte_off = off + 1 ---@type integer return buf:byte(byte_off) + buf:byte(byte_off + 0x01) * 0x00000100 end @@ -282,12 +279,11 @@ end --- Extract a NUL-terminated C string from `strtab` at zero-based offset `off`. --- Returns nil if `off` is out of range or the string is not NUL-terminated. --- @param strtab string ---- @param off integer +--- @param off integer --- @return string|nil function M.get_str(strtab, off) if off < 0 or off >= #strtab then return nil end - --- @type integer|nil - local end_pos = strtab:find("\0", off + 1, true) + local end_pos = strtab:find("\0", off + 1, true) ---@type integer|nil if not end_pos then return nil end return strtab:sub(off + 1, end_pos - 1) end @@ -305,48 +301,36 @@ end --- @param adapter Elf32Adapter --- @return Elf32Header|nil, string|nil function M.parse_elf32_headers(adapter) - --- @type boolean, string|nil - local ok, err = M.validate_adapter(adapter) + local ok, err = M.validate_adapter(adapter) ---@type boolean, string|nil if not ok then return nil, err end -- 4-byte magic: 0x7F 'E' 'L' 'F'. -- The byte readers take the adapter explicitly. -- The production `Support.File` adapter is wrapped by the caller to drop its implicit `self` so the parser shape is flat pass-style. - --- @type integer|nil - local b1 = M.read_u8(adapter, 0) - --- @type integer|nil - local b2 = M.read_u8(adapter, 1) - --- @type integer|nil - local b3 = M.read_u8(adapter, 2) - --- @type integer|nil - local b4 = M.read_u8(adapter, 3) + local b1 = M.read_u8(adapter, 0) ---@type integer|nil + local b2 = M.read_u8(adapter, 1) ---@type integer|nil + local b3 = M.read_u8(adapter, 2) ---@type integer|nil + local b4 = M.read_u8(adapter, 3) ---@type integer|nil if not (b1 and b2 and b3 and b4) or not (b1 == 0x7f and b2 == 0x45 and b3 == 0x4c and b4 == 0x46) then return nil, "bad_magic" end - --- @type integer|nil - local class = M.read_u8(adapter, M.ELF32_HEADER.class_offset) + local class = M.read_u8(adapter, M.ELF32_HEADER.class_offset) ---@type integer|nil if class ~= M.ELFCLASS32 then return nil, "unsupported_elf_class" end - --- @type integer|nil - local data = M.read_u8(adapter, M.ELF32_HEADER.endian_offset) + local data = M.read_u8(adapter, M.ELF32_HEADER.endian_offset) ---@type integer|nil if data ~= M.ELFDATA2LSB then return nil, "unsupported_elf_data" end - --- @type integer|nil - local e_entry = M.read_u32(adapter, M.ELF32_HEADER.e_entry_offset) - --- @type integer|nil - local e_shoff = M.read_u32(adapter, M.ELF32_HEADER.e_shoff_offset) - --- @type integer|nil - local e_shentsize = M.read_u16(adapter, M.ELF32_HEADER.e_shentsize_offset) - --- @type integer|nil - local e_shnum = M.read_u16(adapter, M.ELF32_HEADER.e_shnum_offset) - --- @type integer|nil - local e_shstrndx = M.read_u16(adapter, M.ELF32_HEADER.e_shstrndx_offset) + local e_entry = M.read_u32(adapter, M.ELF32_HEADER.e_entry_offset) ---@type integer|nil + local e_shoff = M.read_u32(adapter, M.ELF32_HEADER.e_shoff_offset) ---@type integer|nil + local e_shentsize = M.read_u16(adapter, M.ELF32_HEADER.e_shentsize_offset) ---@type integer|nil + local e_shnum = M.read_u16(adapter, M.ELF32_HEADER.e_shnum_offset) ---@type integer|nil + local e_shstrndx = M.read_u16(adapter, M.ELF32_HEADER.e_shstrndx_offset) ---@type integer|nil if not (e_entry and e_shoff and e_shentsize and e_shnum and e_shstrndx) then return nil, "truncated_header" end @@ -367,8 +351,7 @@ end --- @param sh_off integer --- @return Elf32Section|nil, string|nil local function read_section_entry(adapter, sh_off) - --- @type Elf32Section - local entry = { + local entry = { ---@type Elf32Section sh_name = M.read_u32(adapter, sh_off + M.ELF32_SECTION.sh_name_offset), sh_type = M.read_u32(adapter, sh_off + M.ELF32_SECTION.sh_type_offset), sh_flags = M.read_u32(adapter, sh_off + M.ELF32_SECTION.sh_flags_offset), @@ -395,21 +378,16 @@ end function M.walk_sections(adapter, hdr) if not hdr or hdr.error then return nil, hdr and hdr.error or "truncated_section_headers" end - --- @type integer - local file_size = M.size(adapter) + local file_size = M.size(adapter) ---@type integer if hdr.e_shoff + hdr.e_shnum * hdr.e_shentsize > file_size then return nil, "truncated_section_headers" end -- Read every section header first; we need .shstrtab to resolve names. - --- @type Elf32Section[] - local sections = {} - --- @type integer - for i = 0, hdr.e_shnum - 1 do - --- @type integer - local sh_off = hdr.e_shoff + i * hdr.e_shentsize - --- @type Elf32Section|nil, string|nil - local entry, err = read_section_entry(adapter, sh_off) + local sections = {} ---@type Elf32Section[] + for i = 0, hdr.e_shnum - 1 do ---@type integer + local sh_off = hdr.e_shoff + i * hdr.e_shentsize ---@type integer + local entry, err = read_section_entry(adapter, sh_off) ---@type Elf32Section|nil, string|nil if not entry then return nil, err end sections[i + 1] = entry end @@ -418,20 +396,17 @@ function M.walk_sections(adapter, hdr) return nil, "missing_shstrtab" end - --- @type Elf32Section|nil - local shstrtab = sections[hdr.e_shstrndx + 1] + local shstrtab = sections[hdr.e_shstrndx + 1] ---@type Elf32Section|nil if not shstrtab or shstrtab.sh_type ~= M.SHT_STRTAB then return nil, "missing_shstrtab" end if shstrtab.sh_offset + shstrtab.sh_size > file_size then return nil, "truncated_section_headers" end - --- @type string|nil - local shstrtab_bytes = M.read_section_bytes(adapter, shstrtab) + local shstrtab_bytes = M.read_section_bytes(adapter, shstrtab) ---@type string|nil if not shstrtab_bytes then return nil, "truncated_section_headers" end - --- @type integer, Elf32Section - for _, s in ipairs(sections) do + for _, s in ipairs(sections) do ---@type integer, Elf32Section s.name = M.get_str(shstrtab_bytes, s.sh_name) or "" end @@ -444,15 +419,11 @@ end --- @param section Elf32Section --- @return string|nil function M.read_section_bytes(adapter, section) - --- @type integer - local size = section.sh_size + local size = section.sh_size ---@type integer if size == 0 then return "" end - --- @type string[] - local out = {} - --- @type integer - for i = 0, size - 1 do - --- @type integer|nil - local b = M.read_u8(adapter, section.sh_offset + i) + local out = {} ---@type string[] + for i = 0, size - 1 do ---@type integer + local b = M.read_u8(adapter, section.sh_offset + i) ---@type integer|nil if b == nil then return nil end out[#out + 1] = string.char(b) end @@ -467,11 +438,9 @@ end --- @return string|nil, string|nil function M.read_named_section(adapter, sections, name) if not sections then return nil, "missing_section" end - --- @type integer, Elf32Section - for _, s in ipairs(sections) do + for _, s in ipairs(sections) do ---@type integer, Elf32Section if s.name == name then - --- @type string|nil - local bytes = M.read_section_bytes(adapter, s) + local bytes = M.read_section_bytes(adapter, s) ---@type string|nil if not bytes then return nil, "truncated_section_data" end return bytes, nil end @@ -488,59 +457,42 @@ end --- @return table|nil, string|nil function M.collect_symbols(adapter, sections) if not sections then return nil, "missing_sections" end - --- @type table -- bag: symbol name -> Elf32Sym - local symbols = {} - --- @type integer - local file_size = M.size(adapter) - --- @type integer, Elf32Section - for _, s in ipairs(sections) do + local symbols = {} ---@type table -- bag: symbol name -> Elf32Sym + local file_size = M.size(adapter) ---@type integer + for _, s in ipairs(sections) do ---@type integer, Elf32Section if s.sh_type == M.SHT_SYMTAB then - --- @type Elf32Section|nil - local strtab = sections[s.sh_link + 1] + local strtab = sections[s.sh_link + 1] ---@type Elf32Section|nil if not strtab or strtab.sh_type ~= M.SHT_STRTAB then return nil, "missing_symtab_strtab" end if strtab.sh_offset + strtab.sh_size > file_size then return nil, "truncated_section_headers" end - --- @type string|nil - local strtab_bytes = M.read_section_bytes(adapter, strtab) + local strtab_bytes = M.read_section_bytes(adapter, strtab) ---@type string|nil if not strtab_bytes then return nil, "truncated_section_headers" end if s.sh_offset + s.sh_size > file_size then return nil, "truncated_section_headers" end - --- @type string|nil - local symtab_bytes = M.read_section_bytes(adapter, s) + local symtab_bytes = M.read_section_bytes(adapter, s) ---@type string|nil if not symtab_bytes then return nil, "truncated_section_headers" end - --- @type number - local n = #symtab_bytes / M.ELF32_SYM.sym_entry_bytes - --- @type integer - for j = 0, n - 1 do - --- @type integer - local e = s.sh_offset + j * M.ELF32_SYM.sym_entry_bytes - --- @type integer|nil - local st_name = M.read_u32(adapter, e + M.ELF32_SYM.st_name) + local n = #symtab_bytes / M.ELF32_SYM.sym_entry_bytes ---@type number + for j = 0, n - 1 do ---@type integer + local e = s.sh_offset + j * M.ELF32_SYM.sym_entry_bytes ---@type integer + local st_name = M.read_u32(adapter, e + M.ELF32_SYM.st_name) ---@type integer|nil if st_name then - --- @type integer|nil - local st_value = M.read_u32(adapter, e + M.ELF32_SYM.st_value) - --- @type integer|nil - local st_size = M.read_u32(adapter, e + M.ELF32_SYM.st_size) - --- @type integer|nil - local st_info = M.read_u8(adapter, e + M.ELF32_SYM.st_info) + local st_value = M.read_u32(adapter, e + M.ELF32_SYM.st_value) ---@type integer|nil + local st_size = M.read_u32(adapter, e + M.ELF32_SYM.st_size) ---@type integer|nil + local st_info = M.read_u8(adapter, e + M.ELF32_SYM.st_info) ---@type integer|nil -- st_shndx is at offset 14 (2 bytes) — derived from the layout -- the metaprogram reads too. Inline the read to keep the -- adapter as the only I/O surface. - --- @type integer|nil - local b1 = M.read_u8(adapter, e + 14) - --- @type integer|nil - local b2 = M.read_u8(adapter, e + 15) + local b1 = M.read_u8(adapter, e + 14) ---@type integer|nil + local b2 = M.read_u8(adapter, e + 15) ---@type integer|nil if not (b1 and b2) then return nil, "truncated_section_headers" end - --- @type integer - local st_shndx = b1 + b2 * 0x100 - --- @type string - local name = M.get_str(strtab_bytes, st_name) or "" + local st_shndx = b1 + b2 * 0x100 ---@type integer + local name = M.get_str(strtab_bytes, st_name) or "" ---@type string if name ~= "" then symbols[name] = { value = st_value, diff --git a/scripts/elf_dwarf.lua b/scripts/elf_dwarf.lua index e61c794..c48759a 100644 --- a/scripts/elf_dwarf.lua +++ b/scripts/elf_dwarf.lua @@ -84,44 +84,41 @@ --- @field attrs AbbrevAttr[] --- @class ElfDwarf ---- @field DW_TAG table -- bag: tag name -> encoding ---- @field DW_AT table -- bag: attr name -> encoding ---- @field DW_FORM table -- bag: form name -> encoding ---- @field DW_ATE table -- bag: base-type encoding name -> code ---- @field MIPS_BYTES_PER_WORD integer ---- @field dw_dwarf32_terminator integer ---- @field DWARF4_ARANGES Dwarf4Aranges ---- @field DWARF5_RNGLISTS Dwarf5Rnglists ---- @field DWARF_LINE_OPS DwarfLineOps ---- @field DWARF5_DEBUG_LINE Dwarf5DebugLine ---- @field read_u32_le fun(buf: string, off: integer): integer ---- @field read_u16_le fun(buf: string, off: integer): integer ---- @field read_uleb128_at fun(buf: string, pos: integer): integer|nil, integer ---- @field read_sleb128_at fun(buf: string, pos: integer): integer|nil, integer ---- @field find_abbrev_table_end fun(table_bytes: string, table_start: integer): integer|nil ---- @field read_ref_sig8 fun(buf: string, pos: integer): integer, integer, integer +--- @field DW_TAG table -- bag: tag name -> encoding +--- @field DW_AT table -- bag: attr name -> encoding +--- @field DW_FORM table -- bag: form name -> encoding +--- @field DW_ATE table -- bag: base-type encoding name -> code +--- @field MIPS_BYTES_PER_WORD integer +--- @field dw_dwarf32_terminator integer +--- @field DWARF4_ARANGES Dwarf4Aranges +--- @field DWARF5_RNGLISTS Dwarf5Rnglists +--- @field DWARF_LINE_OPS DwarfLineOps +--- @field DWARF5_DEBUG_LINE Dwarf5DebugLine +--- @field read_u32_le fun(buf: string, off: integer): integer +--- @field read_u16_le fun(buf: string, off: integer): integer +--- @field read_uleb128_at fun(buf: string, pos: integer): integer|nil, integer +--- @field read_sleb128_at fun(buf: string, pos: integer): integer|nil, integer +--- @field find_abbrev_table_end fun(table_bytes: string, table_start: integer): integer|nil +--- @field read_ref_sig8 fun(buf: string, pos: integer): integer, integer, integer --- @field find_type_unit_by_signature fun(info: string, target_sig_lo: integer, target_sig_hi: integer): integer|nil, integer|nil ---- @field write_u32_le fun(value: integer): string ---- @field write_u16_le fun(value: integer): string ---- @field read_elf_sections fun(elf_path: Path, section_names: string[]): table ---- @field read_nm fun(elf_path: Path): table ---- @field uleb128 fun(n: integer): string ---- @field sleb128 fun(n: integer): string ---- @field uleb128_size fun(n: integer): integer ---- @field sleb128_size fun(n: integer): integer ---- @field read_line_unit_file_table fun(elf_path: string): table|nil, table|nil, table|nil +--- @field write_u32_le fun(value: integer): string +--- @field write_u16_le fun(value: integer): string +--- @field read_elf_sections fun(elf_path: Path, section_names: string[]): table +--- @field read_nm fun(elf_path: Path): table +--- @field uleb128 fun(n: integer): string +--- @field sleb128 fun(n: integer): string +--- @field uleb128_size fun(n: integer): integer +--- @field sleb128_size fun(n: integer): integer +--- @field read_line_unit_file_table fun(elf_path: string): table|nil, table|nil, table|nil ---- @type LfsMod -local lfs = require("lfs") +local lfs = require("lfs") ---@type LfsMod -- scripts/elf32.lua contains format-constant tables + the byte-level walker. -- The this file re-exports `read_u32_le` / `read_u16_le` (and the DWARF32 terminator). -- read_u32_le is this module's reader; implementation in elf32.lua. ---- @type Elf32Mod -local E = require("elf32") +local E = require("elf32") ---@type Elf32Mod ---- @type ElfDwarf -local M = {} +local M = {} ---@type ElfDwarf -- ════════════════════════════════════════════════════════════════════════════ -- DWARF tag + form constants @@ -200,8 +197,7 @@ M.DW_ATE = { } -- DWARF5 §7.5.6 DW_FORM_implicit_const ---- @type integer -local DW_FORM_implicit_const = 0x21 +local DW_FORM_implicit_const = 0x21 ---@type integer -- ════════════════════════════════════════════════════════════════════════════ -- Format-constant tables @@ -378,13 +374,10 @@ end --- @return integer|nil --- @return integer function M.read_uleb128_at(buf, pos) - --- @type integer, integer - local value, shift = 0, 0 - --- @type integer - local len = #buf + local value, shift = 0, 0 ---@type integer, integer + local len = #buf ---@type integer while pos < len do - --- @type integer - local b = buf:byte(pos + 1) + local b = buf:byte(pos + 1) ---@type integer value = value + (b % 0x80) * (2 ^ shift) shift = shift + 7 pos = pos + 1 @@ -398,13 +391,10 @@ end --- @return integer|nil --- @return integer function M.read_sleb128_at(buf, pos) - --- @type integer, integer - local value, shift = 0, 0 - --- @type integer - local len = #buf + local value, shift = 0, 0 ---@type integer, integer + local len = #buf ---@type integer while pos < len do - --- @type integer - local b = buf:byte(pos + 1) + local b = buf:byte(pos + 1) ---@type integer value = value + (b % 0x80) * (2 ^ shift) shift = shift + 7 pos = pos + 1 @@ -423,33 +413,27 @@ end --- @param table_start integer --- @return integer|nil function M.find_abbrev_table_end(table_bytes, table_start) - --- @type integer, integer - local pos, len = table_start, #table_bytes + local pos, len = table_start, #table_bytes ---@type integer, integer if pos >= len or table_bytes:byte(pos + 1) == 0 then return pos end while pos < len do - --- @type integer|nil, integer - local _code, code_end = M.read_uleb128_at(table_bytes, pos) + local _code, code_end = M.read_uleb128_at(table_bytes, pos) ---@type integer|nil, integer if not _code then return nil end pos = code_end - --- @type integer|nil, integer - local _tag, tag_end = M.read_uleb128_at(table_bytes, pos) + local _tag, tag_end = M.read_uleb128_at(table_bytes, pos) ---@type integer|nil, integer if not _tag then return nil end pos = tag_end if pos >= len then return nil end pos = pos + 1 -- has_children byte while pos < len do - --- @type integer|nil, integer - local attr, attr_end = M.read_uleb128_at(table_bytes, pos) + local attr, attr_end = M.read_uleb128_at(table_bytes, pos) ---@type integer|nil, integer if not attr then return nil end pos = attr_end - --- @type integer|nil, integer - local form, form_end = M.read_uleb128_at(table_bytes, pos) + local form, form_end = M.read_uleb128_at(table_bytes, pos) ---@type integer|nil, integer if not form then return nil end pos = form_end if attr == 0 and form == 0 then break end if form == DW_FORM_implicit_const then - --- @type integer|nil, integer - local _c, ce = M.read_sleb128_at(table_bytes, pos) + local _c, ce = M.read_sleb128_at(table_bytes, pos) ---@type integer|nil, integer if not _c then return nil end pos = ce end @@ -466,10 +450,8 @@ end --- @param off integer --- @return string local function read_c_string_at(buf, off) - --- @type integer - local len = #buf - --- @type integer - local start = off + local len = #buf ---@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 @@ -482,41 +464,31 @@ end --- @return AbbrevDecl[]|nil --- @return string|nil local function parse_abbrev_table(table_bytes, table_start) - --- @type integer|nil - local table_end = M.find_abbrev_table_end(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 - --- @type AbbrevDecl[] - local decls = {} - --- @type integer - local pos = table_start + local decls = {} ---@type AbbrevDecl[] + local pos = table_start ---@type integer while pos < table_end do - --- @type integer|nil, integer - local code, code_end = M.read_uleb128_at(table_bytes, pos) + local code, code_end = M.read_uleb128_at(table_bytes, pos) ---@type integer|nil, integer if not code then return nil, "truncated code" end pos = code_end - --- @type integer|nil, integer - local tag, tag_end = M.read_uleb128_at(table_bytes, pos) + local tag, tag_end = M.read_uleb128_at(table_bytes, pos) ---@type integer|nil, integer if not tag then return nil, "truncated tag" end pos = tag_end - --- @type integer - local has_children = table_bytes:byte(pos + 1) + local has_children = table_bytes:byte(pos + 1) ---@type integer pos = pos + 1 - --- @type AbbrevAttr[] - local attrs = {} + local attrs = {} ---@type AbbrevAttr[] while true do - --- @type integer|nil, integer - local attr, attr_end = M.read_uleb128_at(table_bytes, pos) + local attr, attr_end = M.read_uleb128_at(table_bytes, pos) ---@type integer|nil, integer if not attr then return nil, "truncated attr" end pos = attr_end - --- @type integer|nil, integer - local form, form_end = M.read_uleb128_at(table_bytes, pos) + local form, form_end = M.read_uleb128_at(table_bytes, pos) ---@type integer|nil, integer if not form then return nil, "truncated form" end pos = form_end if attr == 0 and form == 0 then break end attrs[#attrs + 1] = { name = attr, form = form } if form == DW_FORM_implicit_const then - --- @type integer|nil, integer - local _c, ce = M.read_sleb128_at(table_bytes, pos) + local _c, ce = M.read_sleb128_at(table_bytes, pos) ---@type integer|nil, integer if not _c then return nil, "truncated const" end pos = ce end @@ -531,10 +503,9 @@ end -- For DW_FORM_strp we return the inline string resolved from `str_buf`. -- For DW_FORM_ref4 we return the absolute CU-relative offset. -- The caller decides whether to interpret that as a section offset. ---- @type table -local FORM_READERS = { +local FORM_READERS = { ---@type table --- @param buf string - --- @param _ string + --- @param _ string --- @param pos integer --- @return integer --- @return integer @@ -542,28 +513,26 @@ local FORM_READERS = { return M.read_u32_le(buf, pos), pos + 4 end, --- @param buf string - --- @param _ string + --- @param _ string --- @param pos integer --- @return string --- @return integer [M.DW_FORM.string] = function(buf, _, pos) - --- @type string - local s = read_c_string_at(buf, pos) + local s = read_c_string_at(buf, pos) ---@type string return s, pos + #s + 1 end, - --- @param buf string + --- @param buf string --- @param str_buf string - --- @param pos integer + --- @param pos integer --- @return string --- @return integer [M.DW_FORM.strp] = function(buf, str_buf, pos) -- DW_FORM_strp: 4-byte offset into .debug_str. - --- @type integer - local strp_off = M.read_u32_le(buf, pos) + local strp_off = M.read_u32_le(buf, pos) ---@type integer return read_c_string_at(str_buf, strp_off), pos + 4 end, --- @param buf string - --- @param _ string + --- @param _ string --- @param pos integer --- @return integer|nil --- @return integer @@ -579,7 +548,7 @@ local FORM_READERS = { return buf:byte(pos + 1), pos + 1 end, --- @param buf string - --- @param _ string + --- @param _ string --- @param pos integer --- @return integer --- @return integer @@ -587,7 +556,7 @@ local FORM_READERS = { return M.read_u16_le(buf, pos), pos + 2 end, --- @param buf string - --- @param _ string + --- @param _ string --- @param pos integer --- @return integer --- @return integer @@ -595,7 +564,7 @@ local FORM_READERS = { return M.read_u32_le(buf, pos), pos + 4 end, --- @param buf string - --- @param _ string + --- @param _ string --- @param pos integer --- @return integer --- @return integer @@ -603,7 +572,7 @@ local FORM_READERS = { return M.read_u32_le(buf, pos), pos + 4 end, --- @param buf string - --- @param _ string + --- @param _ string --- @param pos integer --- @return integer --- @return integer @@ -612,8 +581,8 @@ local FORM_READERS = { -- on DWARF5 32-bit it's always 4 bytes). return M.read_u32_le(buf, pos), pos + 4 end, - --- @param _ string - --- @param _ string + --- @param _ string + --- @param _ string --- @param pos integer --- @return integer --- @return integer @@ -627,8 +596,7 @@ local FORM_READERS = { --- @return integer [M.DW_FORM.exprloc] = function(buf, _, pos) -- DW_FORM_exprloc: ULEB byte count + that many bytes of DW_OP_*. - --- @type integer|nil, integer - local len, ne = M.read_uleb128_at(buf, pos) + local len, ne = M.read_uleb128_at(buf, pos) ---@type integer|nil, integer if not len then return nil, pos end return nil, ne + len end, @@ -642,7 +610,7 @@ local FORM_READERS = { return nil, pos end, --- @param buf string - --- @param _ string + --- @param _ string --- @param pos integer --- @return integer --- @return integer @@ -654,20 +622,18 @@ local FORM_READERS = { -- then the high 4 to resolve the specific type within it. -- Return the low 4 as the primary value to preserve the (value, next_pos) shape; -- the high 4 is exposed via M.read_ref_sig8 (which returns both halves). - --- @type integer, integer, integer - local _, _, next_pos = M.read_ref_sig8(buf, pos) + local _, _, next_pos = M.read_ref_sig8(buf, pos) ---@type integer, integer, integer return M.read_u32_le(buf, pos), next_pos end, } ---- @param buf string +--- @param buf string --- @param str_buf string ---- @param pos integer ---- @param form integer +--- @param pos integer +--- @param form integer --- @return string|integer|nil --- @return integer local function read_form_value(buf, str_buf, pos, form) - --- @type (fun(buf: string, str_buf: string, pos: integer): (string|integer|nil, integer))|nil - local r = FORM_READERS[form] + local r = FORM_READERS[form] ---@type (fun(buf: string, str_buf: string, pos: integer): (string|integer|nil, integer))|nil if not r then return nil, pos end @@ -701,21 +667,16 @@ 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) - --- @type integer - local pos = 0 - --- @type integer - local section_len = #info + local pos = 0 ---@type integer + local section_len = #info ---@type integer while pos + 4 < section_len do - --- @type integer - local unit_length = M.read_u32_le(info, pos) + local unit_length = M.read_u32_le(info, pos) ---@type integer if unit_length == 0xFFFFFFFF then return nil, nil -- DWARF64 not supported end -- unit_length is the body size, NOT including the 4-byte unit_length field itself. - --- @type integer - local body_start = pos + 4 - --- @type integer - local body_end = body_start + unit_length + 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 end @@ -737,14 +698,11 @@ function M.find_type_unit_by_signature(info, target_sig_lo, target_sig_hi) -- byte 4-7: debug_abbrev_offset (4) -- byte 8-15: type_signature (8) -- byte 16-19: type_offset (4) - --- @type integer - local unit_type = info:byte(body_start + 2 + 1) -- 0-based +2 = unit_type in 1-indexed + local unit_type = info:byte(body_start + 2 + 1) ---@type integer -- 0-based +2 = unit_type in 1-indexed if unit_type == 0x02 then -- DW_UT_type - --- @type integer, integer, integer - local sig_lo, sig_hi, _ = M.read_ref_sig8(info, body_start + 8) -- 0-based +8 = type_signature in 1-indexed + local sig_lo, sig_hi, _ = M.read_ref_sig8(info, body_start + 8) ---@type integer, integer, integer -- 0-based +8 = type_signature in 1-indexed if sig_lo == target_sig_lo and sig_hi == target_sig_hi then - --- @type integer - local type_offset = M.read_u32_le(info, body_start + 16) -- 0-based +16 = type_offset in 1-indexed + local type_offset = M.read_u32_le(info, body_start + 16) ---@type integer -- 0-based +16 = type_offset in 1-indexed return pos, type_offset end end @@ -796,16 +754,12 @@ 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. - --- @type table - local result = {} - --- @type integer, string - for _, name in ipairs(section_names) do result[name] = "" end + local result = {} ---@type table + for _, name in ipairs(section_names) do result[name] = "" end ---@type integer, string -- O(1) lookup set. - --- @type table -- bag: requested section name -> true - local wanted = {} - --- @type integer, string - for _, name in ipairs(section_names) do wanted[name] = true end + 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). if lfs.attributes(elf_path, "mode") ~= "file" then @@ -813,27 +767,23 @@ function M.read_elf_sections(elf_path, section_names) return result end - --- @type file*|nil - local f = io.open(elf_path, "rb") + local f = io.open(elf_path, "rb") ---@type file*|nil if not f then io.stderr:write(string.format("[elf_dwarf.read_elf_sections] io.open failed: %s\n", elf_path)) return result end - --- @type integer - local file_size + local file_size ---@type integer do f:seek("end", 0) file_size = f:seek("cur", 0) end - --- @type Elf32Adapter - local adapter = { + local adapter = { ---@type Elf32Adapter --- @param offset integer --- @return integer|nil read_u8_at = function(offset) f:seek("set", offset) - --- @type string|nil - local b = f:read(1) + local b = f:read(1) ---@type string|nil if not b then return nil end return b:byte() end, @@ -841,10 +791,8 @@ function M.read_elf_sections(elf_path, section_names) --- @return integer|nil read_u16_at = function(offset) f:seek("set", offset) - --- @type string|nil - local b1 = f:read(1) - --- @type string|nil - local b2 = f:read(1) + local b1 = f:read(1) ---@type string|nil + local b2 = f:read(1) ---@type string|nil if not b1 or not b2 then return nil end return b1:byte() + b2:byte() * 0x100 end, @@ -852,14 +800,10 @@ function M.read_elf_sections(elf_path, section_names) --- @return integer|nil read_u32_at = function(offset) f:seek("set", offset) - --- @type string|nil - local b1 = f:read(1) - --- @type string|nil - local b2 = f:read(1) - --- @type string|nil - local b3 = f:read(1) - --- @type string|nil - local b4 = f:read(1) + local b1 = f:read(1) ---@type string|nil + local b2 = f:read(1) ---@type string|nil + local b3 = f:read(1) ---@type string|nil + local b4 = f:read(1) ---@type string|nil if not b1 or not b2 or not b3 or not b4 then return nil end return b1:byte() + b2:byte() * 0x100 + b3:byte() * 0x10000 + b4:byte() * 0x1000000 @@ -869,16 +813,14 @@ function M.read_elf_sections(elf_path, section_names) } -- Delegate the header parse + section walk to E.*. - --- @type Elf32Header|nil, string|nil - local hdr, hdr_err = E.parse_elf32_headers(adapter) + local hdr, hdr_err = E.parse_elf32_headers(adapter) ---@type Elf32Header|nil, string|nil if not hdr then io.stderr:write(string.format("[elf_dwarf.read_elf_sections] header parse failed: %s\n", tostring(hdr_err))) f:close() return result end - --- @type Elf32Section[]|nil, string|nil - local sections, walk_err = E.walk_sections(adapter, hdr) + local sections, walk_err = E.walk_sections(adapter, hdr) ---@type Elf32Section[]|nil, string|nil if not sections then io.stderr:write(string.format("[elf_dwarf.read_elf_sections] section walk failed: %s\n", tostring(walk_err))) f:close() @@ -886,11 +828,9 @@ function M.read_elf_sections(elf_path, section_names) end -- Resolve the requested sections. - --- @type integer, Elf32Section - for _, s in ipairs(sections) do + for _, s in ipairs(sections) do ---@type integer, Elf32Section if wanted[s.name] then - --- @type string|nil - local bytes = E.read_section_bytes(adapter, s) + local bytes = E.read_section_bytes(adapter, s) ---@type string|nil if bytes then result[s.name] = bytes end end end @@ -912,31 +852,26 @@ end --- @param elf_path Path --- @return table function M.read_nm(elf_path) - --- @type table - local addrs = {} + local addrs = {} ---@type table -- Existence check first; an empty or missing ELF returns an empty map. if lfs.attributes(elf_path, "mode") ~= "file" then return addrs end - --- @type file*|nil - local f = io.open(elf_path, "rb") + local f = io.open(elf_path, "rb") ---@type file*|nil if not f then return addrs end -- Build the file adapter for E.*. - --- @type integer - local file_size + local file_size ---@type integer do f:seek("end", 0) file_size = f:seek("cur", 0) end - --- @type Elf32Adapter - local adapter = { + local adapter = { ---@type Elf32Adapter --- @param offset integer --- @return integer|nil read_u8_at = function(offset) f:seek("set", offset) - --- @type string|nil - local b = f:read(1) + local b = f:read(1) ---@type string|nil if not b then return nil end return b:byte() end, @@ -944,10 +879,8 @@ function M.read_nm(elf_path) --- @return integer|nil read_u16_at = function(offset) f:seek("set", offset) - --- @type string|nil - local b1 = f:read(1) - --- @type string|nil - local b2 = f:read(1) + local b1 = f:read(1) ---@type string|nil + local b2 = f:read(1) ---@type string|nil if not b1 or not b2 then return nil end return b1:byte() + b2:byte() * 0x100 end, @@ -955,14 +888,10 @@ function M.read_nm(elf_path) --- @return integer|nil read_u32_at = function(offset) f:seek("set", offset) - --- @type string|nil - local b1 = f:read(1) - --- @type string|nil - local b2 = f:read(1) - --- @type string|nil - local b3 = f:read(1) - --- @type string|nil - local b4 = f:read(1) + local b1 = f:read(1) ---@type string|nil + local b2 = f:read(1) ---@type string|nil + local b3 = f:read(1) ---@type string|nil + local b4 = f:read(1) ---@type string|nil if not b1 or not b2 or not b3 or not b4 then return nil end return b1:byte() + b2:byte() * 0x100 + b3:byte() * 0x10000 + b4:byte() * 0x1000000 @@ -972,16 +901,14 @@ function M.read_nm(elf_path) } -- Delegate the header + section walk to E.*. - --- @type Elf32Header|nil, string|nil - local hdr, hdr_err = E.parse_elf32_headers(adapter) + local hdr, hdr_err = E.parse_elf32_headers(adapter) ---@type Elf32Header|nil, string|nil if not hdr then io.stderr:write(string.format("[elf_dwarf.read_nm] header parse failed: %s\n", tostring(hdr_err))) f:close() return addrs end - --- @type Elf32Section[]|nil, string|nil - local sections, walk_err = E.walk_sections(adapter, hdr) + local sections, walk_err = E.walk_sections(adapter, hdr) ---@type Elf32Section[]|nil, string|nil if not sections then io.stderr:write(string.format("[elf_dwarf.read_nm] section walk failed: %s\n", tostring(walk_err))) f:close() @@ -990,8 +917,7 @@ function M.read_nm(elf_path) -- E.collect_symbols returns every defined symbol (no binding filter). -- The metaprogram then applies its STB_LOCAL / STB_GLOBAL + size>0 filter, matching `nm`'s default (external symbols only). - --- @type table|nil, string|nil - local symbols, sym_err = E.collect_symbols(adapter, sections) + local symbols, sym_err = E.collect_symbols(adapter, sections) ---@type table|nil, string|nil if not symbols then io.stderr:write(string.format("[elf_dwarf.read_nm] symbol collection failed: %s\n", tostring(sym_err))) f:close() @@ -1000,12 +926,10 @@ function M.read_nm(elf_path) f:close() - --- @type string, Elf32Sym - for name, entry in pairs(symbols) do + for name, entry in pairs(symbols) do ---@type string, Elf32Sym -- High nibble of st_info = binding (STB_LOCAL=0, STB_GLOBAL=1, STB_WEAK=2). -- math.floor(/16) is portable across LuaJIT 2.0/2.1 and plain Lua 5.x. - --- @type integer - local binding = math.floor(entry.info / 16) + local binding = math.floor(entry.info / 16) ---@type integer if (binding == 0 or binding == 1) and entry.size > 0 then addrs[name] = { entry.value, entry.size } end @@ -1037,17 +961,14 @@ end -- Spec: DWARF5 §7.6 "Variable-Length Data" / Appendix C. -- Top bit of each LEB128 byte. Set if more bytes follow in the encoding. ---- @type integer -local LEB_CONT_BIT = 0x80 +local LEB_CONT_BIT = 0x80 ---@type integer -- Low 7 bits of each LEB128 byte. The actual data payload. ---- @type integer -local LEB_DATA_MASK = 0x7F +local LEB_DATA_MASK = 0x7F ---@type integer -- Bit 6 of the 7-bit data (i.e. 0x40). For SLEB128: the sign-bit position used by the decoder for sign extension. -- Encoders MUST stop when the next byte would be redundant AND the sign bit in the last byte matches the value's sign. ---- @type integer -local SLEB_SIGN_BIT = 0x40 +local SLEB_SIGN_BIT = 0x40 ---@type integer --- ULEB128 (Unsigned Little-Endian Base 128) encoder. Returns the byte string for the non-negative integer `n`. --- Algorithm: @@ -1064,11 +985,9 @@ function M.uleb128(n) error("uleb128 requires non-negative number") end assert(n >= 0, "uleb128 requires non-negative input") - --- @type string[] - local bytes = {} + local bytes = {} ---@type string[] repeat - --- @type integer - local b = n % (LEB_DATA_MASK + 1) -- extract low 7 bits + local b = n % (LEB_DATA_MASK + 1) ---@type integer -- extract low 7 bits n = (n - b) / (LEB_DATA_MASK + 1) -- shift right by 7 bits if n > 0 then b = b + LEB_CONT_BIT end -- set continuation bit if more bytes follow bytes[#bytes + 1] = string.char(b) @@ -1087,13 +1006,10 @@ end --- @param n integer -- any integer (negative allowed) --- @return string function M.sleb128(n) - --- @type string[] - local bytes = {} - --- @type boolean - local more = true + local bytes = {} ---@type string[] + local more = true ---@type boolean while more do - --- @type integer - local b = n % (LEB_DATA_MASK + 1) -- extract low 7 bits + local b = n % (LEB_DATA_MASK + 1) ---@type integer -- extract low 7 bits n = (n - b) / (LEB_DATA_MASK + 1) -- arithmetic shift right by 7 -- Termination: remaining value bits fit in the sign bit of the last byte. if n == 0 and b < SLEB_SIGN_BIT then more = false end -- positive terminator @@ -1112,8 +1028,7 @@ end function M.uleb128_size(n) assert(n >= 0, "uleb128_size requires non-negative input") if n == 0 then return 1 end - --- @type integer - local bytes = 1 + local bytes = 1 ---@type integer while n >= 0x80 do n = (n - (n % (LEB_DATA_MASK + 1))) / (LEB_DATA_MASK + 1) -- arithmetic shift right by 7 bytes = bytes + 1 @@ -1129,15 +1044,11 @@ end --- @param n integer -- any integer (negative allowed) --- @return integer function M.sleb128_size(n) - --- @type boolean - local more = true - --- @type integer - local bytes = 0 - --- @type integer - local v = n + local more = true ---@type boolean + local bytes = 0 ---@type integer + local v = n ---@type integer while more do - --- @type integer - local b = v % (LEB_DATA_MASK + 1) -- extract low 7 bits + 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 if v == 0 and b < SLEB_SIGN_BIT then more = false end -- positive terminator if v == -1 and b >= SLEB_SIGN_BIT then more = false end -- negative terminator @@ -1179,49 +1090,39 @@ end --- @return table|nil --- @return table|nil function M.read_line_unit_file_table(elf_path) - --- @type table - local sections = M.read_elf_sections(elf_path, { ".debug_line", ".debug_line_str" }) - --- @type string - local line = sections[".debug_line"] - --- @type string - local lstr = sections[".debug_line_str"] or "" + 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 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 end - --- @type table -- bag: 1-based file index -> basename - local basenames = {} - --- @type table -- bag: basename -> 1-based file index - local basename_to_index = {} - --- @type table -- bag: 1-based file index -> full path - local paths = {} + local basenames = {} ---@type table -- bag: 1-based file index -> basename + local basename_to_index = {} ---@type table -- bag: basename -> 1-based file index + local paths = {} ---@type table -- bag: 1-based file index -> full path --- Read one form-code's bytes from `buf` at position `p` according to `form`. --- Returns (value, after) where `value` is: --- * the resolved string (DW_FORM_line_strp / DW_FORM_string) --- * the ULEB128 number (DW_FORM_udata) --- * nil + skip-bytes (DW_FORM_data16; we don't surface the MD5) - --- @param buf string + --- @param buf string --- @param lstr_buf string - --- @param p integer - --- @param form integer + --- @param p integer + --- @param form integer --- @return string|integer|nil --- @return integer local function read_form(buf, lstr_buf, p, form) if form == M.DWARF5_DEBUG_LINE.form_line_strp then - --- @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) + 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 - --- @type integer - local nul = buf:find("\0", p + 1, true) or (#buf + 1) + local nul = buf:find("\0", p + 1, true) or (#buf + 1) ---@type integer return buf:sub(p + 1, nul - 1), nul elseif form == M.DWARF5_DEBUG_LINE.form_udata then - --- @type integer|nil, integer - local v, after = M.read_uleb128_at(buf, p) + local v, after = M.read_uleb128_at(buf, p) ---@type integer|nil, integer return v, after elseif form == M.DWARF5_DEBUG_LINE.form_data16 then return nil, p + M.DWARF5_DEBUG_LINE.form_data16_bytes @@ -1237,52 +1138,38 @@ function M.read_line_unit_file_table(elf_path) --- Parse one DWARF-version-3-style unit (DWARF3/4 line program; gcc default in the PS1 toolchain still emits DWARF3 for line programs in `-g` mode). --- Layout: null-terminated directory list, then path(null) + dir_idx(ULEB) + time(ULEB) + size(ULEB) file entries terminated by an empty null. --- `content_start` = zero-based wire offset of the first byte of program-header content (after version + header_length fields). - --- @param buf string + --- @param buf string --- @param content_start integer - --- @param body_end integer + --- @param body_end integer --- @return table --- @return table local function parse_dwarf3_unit(buf, content_start, body_end) - --- @type integer - local up = content_start + local up = content_start ---@type integer -- 5 fixed bytes: min_insn, default_is, line_base (signed), line_range, opcode_base up = up + 5 - --- @type integer - local opcode_base = buf:byte(content_start + 5) + local opcode_base = buf:byte(content_start + 5) ---@type integer up = up + (opcode_base - 1) -- std_opcode_lengths - --- @type string[] - local dirs = {} + local dirs = {} ---@type string[] while up < body_end do - --- @type integer - local nul = buf:find("\0", up + 1, true) or (body_end + 1) + local nul = buf:find("\0", up + 1, true) or (body_end + 1) ---@type integer if nul > body_end then break end - --- @type integer - local len = nul - up - 1 + local len = nul - up - 1 ---@type integer if len == 0 then up = nul break end dirs[#dirs + 1] = buf:sub(up + 1, nul - 1) up = nul end - --- @type table -- bag: 1-based unit file index -> basename - local unit_basenames = {} - --- @type table -- bag: 1-based unit file index -> full path - local unit_paths = {} + 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 while up < body_end do - --- @type integer - local nul = buf:find("\0", up + 1, true) or (body_end + 1) + 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 - --- @type string - local path = buf:sub(up + 1, nul - 1) + local path = buf:sub(up + 1, nul - 1) ---@type string up = nul - --- @type integer|nil, integer - 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 - local idx = #unit_basenames + 1 - --- @type string - local bs = path:match("[^/\\]+$") or 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 unit_paths[idx] = path unit_basenames[idx] = bs dirs[1] = dirs[1] or "" -- safety: gcc emits "" sentinel dir at 0 @@ -1295,83 +1182,57 @@ function M.read_line_unit_file_table(elf_path) --- Parse one DWARF-version-5-style unit (DWARF5 line program; used by modern gcc with `-gdwarf-5`). --- `content_start` is the first byte of program-header content (after the 8 fixed bytes version+addr_size+seg_size+header_length). - --- @param buf string - --- @param lstr_buf string + --- @param buf string + --- @param lstr_buf string --- @param content_start integer - --- @param body_end integer + --- @param body_end integer --- @return table --- @return table local function parse_dwarf5_unit(buf, lstr_buf, content_start, body_end) - --- @type integer - local up = content_start + local up = content_start ---@type integer -- 6 fixed bytes: min_insn, max_ops_per_insn, default_is, line_base, line_range, opcode_base up = up + 6 - --- @type integer - local opcode_base = buf:byte(content_start + 6) + local opcode_base = buf:byte(content_start + 6) ---@type integer up = up + (opcode_base - 1) -- std_opcode_lengths -- directories - --- @type integer|nil, integer - local dir_format_count, after = M.read_uleb128_at(buf, up); up = after - --- @type integer[] - local dir_formats = {} - --- @type integer - for i = 1, dir_format_count do - --- @type integer|nil, integer - local f, a2 = M.read_uleb128_at(buf, up); up = a2 + 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 f, a2 = M.read_uleb128_at(buf, up); up = a2 ---@type integer|nil, integer dir_formats[i] = f end - --- @type integer|nil, integer - local dir_count, a3 = M.read_uleb128_at(buf, up); up = a3 - --- @type string[] - local dirs = {} - --- @type integer - for i = 1, dir_count do - --- @type string - local combined = "" - --- @type integer - for j = 1, dir_format_count do - --- @type string|integer|nil, integer - local v, a4 = read_form(buf, lstr_buf, up, dir_formats[j]) + 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 + 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 if j == 1 and type(v) == "string" then combined = v end end dirs[i] = combined end -- file names - --- @type integer|nil, integer - local file_format_count, after2 = M.read_uleb128_at(buf, up); up = after2 - --- @type integer[] - local file_formats = {} - --- @type integer - for i = 1, file_format_count do - --- @type integer|nil, integer - local f, a2 = M.read_uleb128_at(buf, up); up = a2 + 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 f, a2 = M.read_uleb128_at(buf, up); up = a2 ---@type integer|nil, integer file_formats[i] = f end - --- @type integer|nil, integer - local file_count, a3 = M.read_uleb128_at(buf, up); up = a3 - --- @type table -- bag: 1-based unit file index -> basename - local unit_basenames = {} - --- @type table -- bag: 1-based unit file index -> full path - local unit_paths = {} - --- @type integer - for i = 1, file_count do - --- @type string - local combined = "" - --- @type integer - local didx = 0 - --- @type integer - for j = 1, file_format_count do - --- @type string|integer|nil, integer - local v, a4 = read_form(buf, lstr_buf, up, file_formats[j]) + 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 + 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 - --- @type integer - local idx = #unit_basenames + 1 - --- @type string - local bs = combined:match("[^/\\]+$") or combined + local idx = #unit_basenames + 1 ---@type integer + local bs = combined:match("[^/\\]+$") or combined ---@type string unit_paths[idx] = combined unit_basenames[idx] = bs if didx > 0 and dirs[didx] then @@ -1382,37 +1243,27 @@ function M.read_line_unit_file_table(elf_path) end --- Walk every line-program unit in the section. - --- @type integer - local p = 0 - --- @type integer - local section_end = #line + local p = 0 ---@type integer + local section_end = #line ---@type integer while p + 4 <= section_end do - --- @type integer - local unit_length = M.read_u32_le(line, p) + local unit_length = M.read_u32_le(line, p) ---@type integer if unit_length == 0xFFFFFFFF then io.stderr:write("[elf_dwarf.read_line_unit_file_table] 64-bit DWARF (initial-length 0xFFFFFFFF); not supported\n") return nil end - --- @type integer - local body_start = p + 4 - --- @type integer - local body_end = p + 4 + unit_length + local body_start = p + 4 ---@type integer + local body_end = p + 4 + unit_length ---@type integer if body_end > section_end then break end - --- @type integer - local version = M.read_u16_le(line, body_start) - --- @type table|nil, table|nil - local unit_basenames, unit_paths + local version = M.read_u16_le(line, body_start) ---@type integer + 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 - --- @type integer - local header_length_offset = body_start + 6 -- past version(2) + addr_size(1) + seg_size(1) - wait that's wrong; past hdr len is at +6 - --- @type integer - local content_start = body_start + 8 -- 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 - --- @type integer - local content_start = body_start + 6 -- past version(2) + header_length(4) + local content_start = body_start + 6 ---@type integer -- past version(2) + header_length(4) unit_basenames, unit_paths = parse_dwarf3_unit(line, content_start, body_end) else io.stderr:write(string.format("[elf_dwarf.read_line_unit_file_table] unsupported DWARF version %d (offset 0x%x)\n", version, p)) @@ -1426,10 +1277,9 @@ function M.read_line_unit_file_table(elf_path) -- For DWARF5 (crt0.s + C unit), each carries its own per-unit file-table map; -- the atom-side DW_LNS_set_file(N) refers to the C unit's indices, NOT crt0.s's. -- Since the C unit is the one with full include_directories + 12 entries, we can use it directly. - --- @type integer, string - for idx, bs in pairs(unit_basenames) do - basenames[idx] = bs - paths[idx] = unit_paths[idx] + for idx, bs in pairs(unit_basenames) do ---@type integer, string + basenames[idx] = bs + paths [idx] = unit_paths[idx] basename_to_index[bs] = idx end p = body_end diff --git a/scripts/passes/annotation.lua b/scripts/passes/annotation.lua index 8cb0a50..4e1eccc 100644 --- a/scripts/passes/annotation.lua +++ b/scripts/passes/annotation.lua @@ -10,10 +10,8 @@ -- 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. ---- @type string -local _bootstrap_dir = debug.getinfo(1, "S").source:match("^@?(.*[/\\])") or "./" ---- @type DuffleExport -local duffle = dofile(_bootstrap_dir .. "../duffle_paths.lua") +local _bootstrap_dir = debug.getinfo(1, "S").source:match("^@?(.*[/\\])") or "./" ---@type string +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 @@ -107,8 +105,7 @@ end --- @param findings Findings --- @return nil local function check_unique_annotation(_item, pipe_ctx, findings) - --- @type string, integer - for name, n in pairs(pipe_ctx.annot_counts) do + for name, n in pairs(pipe_ctx.annot_counts) do ---@type string, integer if n > 1 then findings.errors[#findings.errors + 1] = { line = pipe_ctx.atom_index[name] and pipe_ctx.atom_index[name].line or 0, @@ -142,10 +139,8 @@ end --- @param findings Findings --- @return nil local function check_macro_word_drift(m, pipe_ctx, findings) - --- @type WordCounts - local wc = (pipe_ctx and pipe_ctx.word_counts) or {} - --- @type integer|nil - local declared = wc[m.name] + local wc = (pipe_ctx and pipe_ctx.word_counts) or {} ---@type WordCounts + local declared = wc[m.name] ---@type integer|nil if not declared then findings.errors[#findings.errors + 1] = { line = m.line, @@ -174,10 +169,8 @@ 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). - --- @type table -- bag: register ident -> first source line - local seen_first_line = {} - --- @type integer, RegTypeOccurrence - for _, occ in ipairs(pipe_ctx.type_occurrences or {}) do + 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 else @@ -189,12 +182,9 @@ local function check_semantic_reg_defaults(_src, pipe_ctx, findings) } end end - --- @type table - local reg_registry = pipe_ctx.register_alias_registry or {} - --- @type table - local type_registry = pipe_ctx.type_name_registry or {} - --- @type string, RegTypeDefault - for reg, def in pairs(pipe_ctx.types or {}) do + 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 if not reg_registry[reg] then findings.errors[#findings.errors + 1] = { line = def.source_line, @@ -229,15 +219,11 @@ end --- @param findings Findings --- @return nil local function check_atom_reg_types(_src, pipe_ctx, findings) - --- @type table - local reg_registry = pipe_ctx.register_alias_registry or {} - --- @type table - local type_registry = pipe_ctx.type_name_registry or {} - --- @type integer, AtomInfoEntry - for _, ai in ipairs(pipe_ctx.atom_infos_list or {}) do + 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 if ai.reg_type_overrides then - --- @type string, RegTypeOverride - for reg, ov in pairs(ai.reg_type_overrides) do + for reg, ov in pairs(ai.reg_type_overrides) do ---@type string, RegTypeOverride if not reg_registry[reg] then findings.errors[#findings.errors + 1] = { line = ai.info_line, @@ -265,13 +251,11 @@ end --- @param findings Findings --- @return nil local function check_atom_view_layout(_src, pipe_ctx, findings) - --- @type string, AtomViewEntry - for atom_name, view in pairs(pipe_ctx.atom_views or {}) do + for atom_name, view in pairs(pipe_ctx.atom_views or {}) do ---@type string, AtomViewEntry if not view.binds_name then -- The atom had atom_reg_types but no atom_view; no layout check needed. else - --- @type BindsEntry|nil - local bs = pipe_ctx.binds_index[view.binds_name] + local bs = pipe_ctx.binds_index[view.binds_name] ---@type BindsEntry|nil if not bs then findings.errors[#findings.errors + 1] = { line = view.info_line, @@ -297,16 +281,12 @@ end --- @param findings Findings --- @return nil local function check_binds_no_duplicate_fields(_src, pipe_ctx, findings) - --- @type integer, BindsEntry - for _, bs in ipairs(pipe_ctx.binds_list or {}) do - --- @type table -- bag: field name -> occurrence count - local seen = {} - --- @type integer, TypeField - for _, f in ipairs(bs.fields or {}) do + for _, bs in ipairs(pipe_ctx.binds_list or {}) do ---@type integer, BindsEntry + 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 - --- @type string, integer - for name, count in pairs(seen) do + for name, count in pairs(seen) do ---@type string, integer if count > 1 then findings.errors[#findings.errors + 1] = { line = bs.line, @@ -334,10 +314,8 @@ end --- @param findings Findings --- @return nil local function check_skip_marker(marker, _pipe_ctx, findings) - --- @type string - local kind = marker.marker_kind - --- @type integer - local line = marker.marker_line + local kind = marker.marker_kind ---@type string + local line = marker.marker_line ---@type integer -- Left `scan.debug_skip_markers` with production records for `atom_dbg_skip` only; other identifiers take the walker's unrelated branch. if marker.has_parens then @@ -396,13 +374,10 @@ end 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 - --- @type table - local reg_registry = pipe_ctx.register_alias_registry or {} - --- @type integer, AtomInfoEntry - for _, ai in ipairs(pipe_ctx.atom_infos_list) do + local reg_registry = pipe_ctx.register_alias_registry or {} ---@type table + for _, ai in ipairs(pipe_ctx.atom_infos_list) do ---@type integer, AtomInfoEntry if ai.reg_type_overrides then - --- @type string, RegTypeOverride - for reg, _ in pairs(ai.reg_type_overrides) do + for reg, _ in pairs(ai.reg_type_overrides) do ---@type string, RegTypeOverride if not reg_registry[reg] then findings.warnings[#findings.warnings + 1] = { line = 0, @@ -429,8 +404,7 @@ end -- -- Adding a new check = 1 row here + 1 function above. The `validate()` dispatch loop never needs editing. ---- @type CheckRule[] -local CHECK_RULES = { +local CHECK_RULES = { ---@type CheckRule[] { name = "atom_decl_exists", per_annot = check_atom_decl_exists }, { name = "binds_struct_exists", per_annot = check_binds_struct_exists }, { name = "unique_annotation", post = check_unique_annotation }, @@ -453,12 +427,9 @@ local CHECK_RULES = { --- @param ctx PassCtx --- @return PipeCtx local function build_corpus_pipe_ctx(ctx) - --- @type PipeCtx - local view = duffle.corpus_view(ctx) - --- @type table -- bag: atom name -> annotation count - local annot_counts = {} - --- @type integer, AtomInfoEntry - for _, info in ipairs(view.atom_infos) do + 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 if info and info.atom_name then annot_counts[info.atom_name] = (annot_counts[info.atom_name] or 0) + 1 end @@ -476,17 +447,13 @@ end --- @return AnnotatedResult local function validate(ctx, src, corpus_pipe_ctx) corpus_pipe_ctx = corpus_pipe_ctx or build_corpus_pipe_ctx(ctx) - --- @type SourceScan - local scan = src.scan + local scan = src.scan ---@type SourceScan -- Build a per-source pipe_ctx: shared lookups come from `corpus_pipe_ctx`, while declarations, bodies, types, views, defaults, and occurrences come from `src.scan`. - --- @type table -- bag: register ident -> occurrence count - local seen_defaults = {}; for reg, _ in pairs (scan.types or {}) do seen_defaults[reg] = (seen_defaults[reg] or 0) + 1 end - --- @type AtomInfoEntry[] - local atom_infos_list = {}; for _, ai in ipairs(scan.atom_infos or {}) do atom_infos_list[#atom_infos_list + 1] = ai end + local seen_defaults = {}; for reg, _ in pairs (scan.types or {}) do seen_defaults[reg] = (seen_defaults[reg] or 0) + 1 end ---@type table -- bag: register ident -> occurrence count + local atom_infos_list = {}; for _, ai in ipairs(scan.atom_infos or {}) do atom_infos_list[#atom_infos_list + 1] = ai end ---@type AtomInfoEntry[] - --- @type PipeCtx - local pipe_ctx = { + local pipe_ctx = { ---@type PipeCtx atom_index = {}, binds_index = {}, annot_counts = corpus_pipe_ctx.annot_counts, @@ -500,29 +467,23 @@ 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, } - --- @type AtomEntry[] - local atoms = {} - --- @type integer, AtomEntry - for _, a in ipairs(scan.atoms) do + 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 pipe_ctx.atom_index[a.raw_name or a.name] = a end end - --- @type integer, BindsEntry - for _, b in ipairs(scan.binds) do pipe_ctx.binds_index[b.name] = b end + for _, b in ipairs(scan.binds) do pipe_ctx.binds_index[b.name] = b end ---@type integer, BindsEntry -- Findings live in a single struct with three lists (errors / warnings / info). -- Each check writes to the list appropriate for its severity. - --- @type Findings - local findings = { errors = {}, warnings = {}, info = {} } + local findings = { errors = {}, warnings = {}, info = {} } ---@type Findings -- Lift parse-time errors already recorded in scan_source's atom_info payload into this pass's findings list. - --- @type integer, AtomInfoEntry - for _, info in ipairs(scan.atom_infos) do + for _, info in ipairs(scan.atom_infos) do ---@type integer, AtomInfoEntry if info.errors then - --- @type integer, string - for _, msg in ipairs(info.errors) do + for _, msg in ipairs(info.errors) do ---@type integer, string findings.errors[#findings.errors + 1] = { line = info.info_line, msg = string.format("'%s': %s", info.atom_name, msg), @@ -532,8 +493,7 @@ local function validate(ctx, src, corpus_pipe_ctx) end -- THE per-annotation pipeline. ONE loop. CHECK_RULES dispatches per_annot rules. - --- @type integer, AtomInfoEntry - for _, info in ipairs(scan.atom_infos) do + for _, info in ipairs(scan.atom_infos) do ---@type integer, AtomInfoEntry duffle.run_check_rules(CHECK_RULES, "per_annot", info, pipe_ctx, findings) end @@ -542,17 +502,14 @@ 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. - --- @type DebugSkipMarker[] - local skip_markers = scan.debug_skip_markers or {} - --- @type integer, DebugSkipMarker - for _, marker in ipairs(skip_markers) do + local skip_markers = scan.debug_skip_markers or {} ---@type 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 -- Per-macro rules (TAPE_WORDS vs WORD_COUNT drift). pipe_ctx.word_counts = corpus_pipe_ctx.word_counts - --- @type integer, MacroEntry - for _, m in ipairs(scan.macros) do + for _, m in ipairs(scan.macros) do ---@type integer, MacroEntry duffle.run_check_rules(CHECK_RULES, "per_macro", m, pipe_ctx, findings) end @@ -582,8 +539,7 @@ end -- M.run — orchestrator entry -- ════════════════════════════════════════════════════════════════════════════ ---- @type AnnotationPass -local M = {} +local M = {} ---@type AnnotationPass -- Expose `validate` for downstream passes (e.g. report.lua) that need to re-render the per-source results into a per-MODULE report. M.validate = validate @@ -591,47 +547,32 @@ M.validate = validate --- @param ctx PassCtx --- @return PassResult function M.run(ctx) - --- @type PassOutputEntry[] - local outputs = {} - --- @type PassFinding[] - local errors = {} - --- @type PassFinding[] - local warnings = {} + local outputs = {} ---@type PassOutputEntry[] + local errors = {} ---@type PassFinding[] + local warnings = {} ---@type PassFinding[] -- 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. - --- @type PipeCtx - local corpus_pipe_ctx = build_corpus_pipe_ctx(ctx) - --- @type Corpus - local corpus = ctx.shared.corpus + local corpus_pipe_ctx = build_corpus_pipe_ctx(ctx) ---@type PipeCtx + 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. - --- @type table - local by_dir = (corpus and corpus.sources_by_dir) or {} + local by_dir = (corpus and corpus.sources_by_dir) or {} ---@type table - --- @type string, SourceFile[] - for dir, dir_sources in pairs(by_dir) do - --- @type string - local dir_basename = dir:match("([^/\\]+)$") or dir - --- @type integer - local dir_atoms = 0 - --- @type PassFinding[] - local dir_errors = {} - --- @type PassFinding[] - local dir_warnings = {} - --- @type integer, SourceFile - for _, src in ipairs(dir_sources) do - --- @type AnnotatedResult - local result = validate(ctx, src, corpus_pipe_ctx) + 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 result = validate(ctx, src, corpus_pipe_ctx) ---@type AnnotatedResult result.source = src.path -- tag for downstream rendering dir_atoms = dir_atoms + #result.atoms - --- @type integer, PassFinding - for _, e in ipairs(result.errors) do + for _, e in ipairs(result.errors) do ---@type integer, PassFinding dir_errors[#dir_errors + 1] = { line = e.line, msg = e.msg, source = src.path } errors [#errors + 1] = { line = e.line, msg = e.msg } end - --- @type integer, PassFinding - for _, w in ipairs(result.warnings) do + for _, w in ipairs(result.warnings) do ---@type integer, PassFinding dir_warnings[#dir_warnings + 1] = { line = w.line, msg = w.msg } warnings [#warnings + 1] = { line = w.line, msg = w.msg } end diff --git a/scripts/passes/atoms_source_map.lua b/scripts/passes/atoms_source_map.lua index 4dc4d5a..4148ffd 100644 --- a/scripts/passes/atoms_source_map.lua +++ b/scripts/passes/atoms_source_map.lua @@ -37,12 +37,9 @@ -- 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. ---- @type string -local _bootstrap_dir = debug.getinfo(1, "S").source:match("^@?(.*[/\\])") or "./" ---- @type DuffleExport -local duffle = dofile(_bootstrap_dir .. "../duffle_paths.lua") ---- @type ElfDwarfMod -local elf_dwarf = require("elf_dwarf") +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 -- ════════════════════════════════════════════════════════════════════════════ -- Constants @@ -50,8 +47,7 @@ local elf_dwarf = require("elf_dwarf") -- Format version emitted as the first line. Bump + add a migration test if the format changes; -- the gdb runtime loader rejects mismatches (E2). ---- @type integer -local FORMAT_VERSION = 1 +local FORMAT_VERSION = 1 ---@type integer -- ════════════════════════════════════════════════════════════════════════════ -- Type declarations @@ -107,23 +103,16 @@ local FORMAT_VERSION = 1 --- @return WordMapEntry[] --- @return integer local function canonical_word_entries(atom) - --- @type AtomPaths - local paths = atom.paths or {} - --- @type WordEvent[] - local events = paths.word_events or {} - --- @type EmissionItem[] - local word_items = {} - --- @type integer, EmissionItem - for _, item in ipairs(paths.items or {}) do + 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 - --- @type WordMapEntry[] - local entries = {} - --- @type integer, WordEvent - for index, event in ipairs(events) do - --- @type EmissionItem - local item = word_items[index] or {} + 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] = { pos = event.i or (index - 1), line = event.call_line or item.line or 0, @@ -149,20 +138,14 @@ end --- @return string[] --- @return integer local function emit_provenance_stanza(src, atom, wc) - --- @type string[] - local lines = {} - --- @type string - local rel_path = src.path:gsub("\\\\", "/") - --- @type WordMapEntry[], integer - local entries, total = canonical_word_entries(atom) + 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) - --- @type integer, WordMapEntry - for _, entry in ipairs(entries) do - --- @type InvocationRecord|nil - local inv = entry.invocation - --- @type integer|nil - local macro_count = inv and wc["mac_" .. inv.component_name] + for _, entry in ipairs(entries) do ---@type integer, WordMapEntry + 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' , entry.pos, rel_path, entry.line, inv.component_name @@ -182,8 +165,7 @@ end --- @param wc WordCounts --- @return string local function render_provenance(src, wc) - --- @type string[] - local lines = {} + local lines = {} ---@type string[] lines[#lines + 1] = "# FORMAT_VERSION 1" lines[#lines + 1] = "# auto-generated by ps1_meta.lua (passes/atoms_source_map.lua) — DO NOT EDIT" lines[#lines + 1] = "# Per-.word provenance: maps each emitted .word to its call site (atom body" @@ -195,17 +177,13 @@ local function render_provenance(src, wc) --- @param atom AtomEntry --- @return nil local function append(atom) - --- @type string[] - local stanza = emit_provenance_stanza(src, atom, wc) - --- @type integer, string - for _, line in ipairs(stanza) do lines[#lines + 1] = line end + 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 - --- @type integer, AtomEntry - for _, atom in ipairs(src.scan.atoms or {}) do + for _, atom in ipairs(src.scan.atoms or {}) do ---@type integer, AtomEntry if atom.paths then append(atom) end end - --- @type integer, AtomEntry - for _, atom in ipairs(src.scan.raw_atoms or {}) do + for _, atom in ipairs(src.scan.raw_atoms or {}) do ---@type integer, AtomEntry if atom.paths then append(atom) end end @@ -219,16 +197,12 @@ end --- @return string[] --- @return integer local function emit_atom_stanza(src, atom) - --- @type string[] - local lines = {} - --- @type string - local rel_path = src.path:gsub("\\\\", "/") - --- @type WordMapEntry[], integer - local entries, total = canonical_word_entries(atom) + 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) - --- @type integer, WordMapEntry - for _, entry in ipairs(entries) do + for _, entry in ipairs(entries) do ---@type integer, WordMapEntry lines[#lines + 1] = string.format("WORD %d LINE %d TEXT %s", entry.pos, entry.line, entry.text) end @@ -242,25 +216,20 @@ end --- @param src SourceFile --- @return string local function render_source_map(src) - --- @type string[] - local lines = {} + local lines = {} ---@type string[] lines[#lines + 1] = "# FORMAT_VERSION " .. FORMAT_VERSION lines[#lines + 1] = "# auto-generated by ps1_meta.lua (passes/atoms_source_map.lua) — DO NOT EDIT" --- @param atom AtomEntry --- @return nil local function append(atom) - --- @type string[] - local stanza = emit_atom_stanza(src, atom) - --- @type integer, string - for _, line in ipairs(stanza) do lines[#lines + 1] = line end + local stanza = emit_atom_stanza(src, atom) ---@type string[] + for _, line in ipairs(stanza) do lines[#lines + 1] = line end ---@type integer, string end - --- @type integer, AtomEntry - for _, atom in ipairs(src.scan.atoms or {}) do + for _, atom in ipairs(src.scan.atoms or {}) do ---@type integer, AtomEntry if atom.paths then append(atom) end end - --- @type integer, AtomEntry - for _, atom in ipairs(src.scan.raw_atoms or {}) do + for _, atom in ipairs(src.scan.raw_atoms or {}) do ---@type integer, AtomEntry if atom.paths then append(atom) end end @@ -283,28 +252,20 @@ end --- @param ctx PassCtx --- @return GdbAtomRecord[] local function build_atom_table(ctx) - --- @type table - local addrs = elf_dwarf.read_nm(ctx.flags.elf_path) - --- @type Corpus|nil - local corpus = ctx.shared and ctx.shared.corpus - --- @type GdbAtomRecord[] - local matched = {} + 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[] - --- @type integer, SourceFile - for _, src in ipairs(corpus.source_order or {}) do - --- @type string - local file_base = src.path:match("([^/\\\\]+)$") or src.path + for _, src in ipairs(corpus.source_order or {}) do ---@type integer, SourceFile + local file_base = src.path:match("([^/\\\\]+)$") or src.path ---@type string --- @param atom AtomEntry --- @return nil local function append(atom) if not atom.paths then return end - --- @type string - local name = atom.raw_name or atom.name - --- @type NmAddr|nil - local info = addrs[name] + local name = atom.raw_name or atom.name ---@type string + local info = addrs[name] ---@type NmAddr|nil if not info then return end - --- @type WordMapEntry[], integer - local entries, total = canonical_word_entries(atom) + local entries, total = canonical_word_entries(atom) ---@type WordMapEntry[], integer matched[#matched + 1] = { name = name, src_path = src.path, @@ -315,10 +276,8 @@ local function build_atom_table(ctx) entries = entries, } 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 + 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 -- Deterministic order: sort by address (matches `nm` output ordering). @@ -326,8 +285,7 @@ local function build_atom_table(ctx) --- @param b GdbAtomRecord --- @return boolean table.sort(matched, function(a, b) return a.addr < b.addr end) - --- @type integer, GdbAtomRecord - for i, a in ipairs(matched) do a.idx = i - 1 end + for i, a in ipairs(matched) do a.idx = i - 1 end ---@type integer, GdbAtomRecord return matched end @@ -345,8 +303,7 @@ local function append_gdb_commands(lines, matched) -- ── tape_atoms ── -- Hardcoded one printf per atom. No loop. lines[#lines + 1] = "define tape_atoms" - --- @type integer, GdbAtomRecord - for _, a in ipairs(matched) do + for _, a in ipairs(matched) do ---@type integer, GdbAtomRecord -- gdb 12.1 quirk: literals in printf args require an attached target. -- Use the per-atom convenience vars set above as printf args. lines[#lines + 1] = string.format(' printf " %%-32s @ 0x%%08x %%4d words\\n", $__atom_name_%d, $__atom_addr_%d, $__atom_words_%d', @@ -361,8 +318,7 @@ local function append_gdb_commands(lines, matched) -- ── break_atom (generic) + per-atom break_atom_X ── lines[#lines + 1] = "define break_atom" lines[#lines + 1] = ' echo "Usage: break_atom_ (pick from the list below)"' - --- @type integer, GdbAtomRecord - for _, a in ipairs(matched) do + for _, a in ipairs(matched) do ---@type integer, GdbAtomRecord lines[#lines + 1] = string.format(' printf " break_atom_%%-32s\\n", $__atom_name_%d', a.idx) end lines[#lines + 1] = "end" @@ -371,8 +327,7 @@ local function append_gdb_commands(lines, matched) lines[#lines + 1] = "end" lines[#lines + 1] = "" - --- @type integer, GdbAtomRecord - for _, a in ipairs(matched) do + for _, a in ipairs(matched) do ---@type integer, GdbAtomRecord lines[#lines + 1] = string.format("define break_atom_%s", a.name) lines[#lines + 1] = string.format(" break *$__atom_addr_%d", a.idx) lines[#lines + 1] = string.format(' printf " Breakpoint set at %s (0x%%08x)\\n", $__atom_addr_%d', a.name, a.idx) @@ -386,8 +341,7 @@ local function append_gdb_commands(lines, matched) -- ── step_atom / next_atom ── -- Hardcoded one tbreak per atom. No loop. lines[#lines + 1] = "define step_atom" - --- @type integer, GdbAtomRecord - for _, a in ipairs(matched) do + for _, a in ipairs(matched) do ---@type integer, GdbAtomRecord lines[#lines + 1] = string.format(" tbreak *$__atom_addr_%d", a.idx) end lines[#lines + 1] = " continue" @@ -410,8 +364,7 @@ local function append_gdb_commands(lines, matched) lines[#lines + 1] = "define where_in_atom" lines[#lines + 1] = " set $__pc = (unsigned int)$pc" lines[#lines + 1] = " set $__matched = 0" - --- @type integer, GdbAtomRecord - for _, a in ipairs(matched) do + for _, a in ipairs(matched) do ---@type integer, GdbAtomRecord -- Precompute end_addr (gdb 12.1's expression evaluator chokes on `addr + words*4`). lines[#lines + 1] = string.format(" set $__end_%d = $__atom_addr_%d + $__atom_words_%d * 4", a.idx, a.idx, a.idx) lines[#lines + 1] = string.format(" if $__pc >= $__atom_addr_%d && $__pc < $__end_%d", a.idx, a.idx) @@ -420,18 +373,15 @@ local function append_gdb_commands(lines, matched) lines[#lines + 1] = string.format(" set $__word = ($__pc - $__atom_addr_%d) / 4", a.idx) lines[#lines + 1] = string.format(' printf "word: %%d/%%d\\n", $__word, $__atom_words_%d', a.idx) -- One inner-if per WORD entry. Each word's line + text hardcoded. - --- @type integer, WordMapEntry - for _, we in ipairs(a.entries) do + for _, we in ipairs(a.entries) do ---@type integer, WordMapEntry lines[#lines + 1] = string.format(" if $__word == %d", we.pos) -- Escape TEXT for printf format string. - --- @type string - local escaped_text = we.text:gsub("%%", "%%%%"):gsub('"', '\\"') + local escaped_text = we.text:gsub("%%", "%%%%"):gsub('"', '\\"') ---@type string lines[#lines + 1] = string.format(' printf "source: %%s:%%d %%s\\n", $__atom_file_%d, %d, "%s"', a.idx, we.line, escaped_text) lines[#lines + 1] = " end" end -- Fallback for words beyond the source map (shouldn't happen if nm matches). - --- @type integer - local max_word = 0 + local max_word = 0 ---@type integer if #a.entries > 0 then max_word = a.entries[#a.entries].pos end lines[#lines + 1] = string.format(' if $__word > %d', max_word) lines[#lines + 1] = ' printf "source: (no source-map entry for word %%d; map may be stale)\\n", $__word' @@ -456,8 +406,7 @@ local function append_gdb_commands(lines, matched) lines[#lines + 1] = " set $__in_atom = 0" lines[#lines + 1] = " set $__did_step = 0" lines[#lines + 1] = " set $__pc = (unsigned int)$pc" - --- @type integer, GdbAtomRecord - for _, a in ipairs(matched) do + for _, a in ipairs(matched) do ---@type integer, GdbAtomRecord -- Precompute end_addr in the convenience var (single expression gdb handles). lines[#lines + 1] = string.format(" set $__end_%d = $__atom_addr_%d + $__atom_words_%d * 4", a.idx, a.idx, a.idx) lines[#lines + 1] = string.format(" if $__pc >= $__atom_addr_%d && $__pc < $__end_%d", a.idx, a.idx) @@ -494,8 +443,7 @@ end --- @return nil local function emit_gdb_runtime(ctx) if not (ctx.flags and ctx.flags.gdb_runtime) then return end - --- @type string|nil - local elf_path = ctx.flags.elf_path + local elf_path = ctx.flags.elf_path ---@type string|nil if not elf_path or elf_path == "" then io.stderr:write("[atoms_source_map] --gdb-runtime requires --elf \n") return @@ -506,15 +454,13 @@ local function emit_gdb_runtime(ctx) return end - --- @type GdbAtomRecord[] - local matched = build_atom_table(ctx) + local matched = build_atom_table(ctx) ---@type GdbAtomRecord[] if #matched == 0 then io.stderr:write("[atoms_source_map] --gdb-runtime: no atoms matched against nm symbols (stale scan?).\n") return end - --- @type string[] - local lines = {} + local lines = {} ---@type string[] lines[#lines + 1] = "# Auto-generated by ps1_meta.lua (passes/atoms_source_map.lua)" lines[#lines + 1] = "# DO NOT EDIT — re-run ps1_meta.lua --atoms-source-map --gdb-runtime to regenerate" lines[#lines + 1] = "# Sourced by scripts/gdb/gdb_tape_atoms.gdb (the wrapper)." @@ -535,8 +481,7 @@ local function emit_gdb_runtime(ctx) -- Per-atom convenience vars (used as printf args; literals aren't accepted -- without an attached target on gdb 12.1). - --- @type integer, GdbAtomRecord - for _, a in ipairs(matched) do + for _, a in ipairs(matched) do ---@type integer, GdbAtomRecord lines[#lines + 1] = string.format('set $__atom_name_%d = "%s"', a.idx, gdb_escape(a.name)) lines[#lines + 1] = string.format("set $__atom_addr_%d = 0x%x", a.idx, a.addr) lines[#lines + 1] = string.format("set $__atom_words_%d = %d", a.idx, a.words) @@ -552,8 +497,7 @@ local function emit_gdb_runtime(ctx) -- Confirmation line for the source operator. lines[#lines + 1] = 'printf "[gdb_tape_atoms] runtime loaded %d atoms from %s\\n", $__atom_count, $__elf_path' - --- @type string - local out_path + local out_path ---@type string -- Move out of `/gdb_tape_atoms_runtime.gdb` to `/../gdb_tape_atoms_runtime.gdb` when the conventional `` is `/gen` -- (any equivalent spelling — relative, absolute backslash, absolute forward-slash, trailing-separator variants). -- This puts the gdb runtime alongside the ELF at `build/` rather than under the report subdir. @@ -566,8 +510,7 @@ local function emit_gdb_runtime(ctx) if ends_with_gen_dir(ctx.out_root) then -- Strip the trailing `/gen` segment, then write the runtime script under `build/`. -- e.g. "C:/projects/Pikuma/ps1/build/gen" -> "C:/projects/Pikuma/ps1/build". - --- @type string - local parent = ctx.out_root:gsub("[/\\]gen[/\\]?$", "") + local parent = ctx.out_root:gsub("[/\\]gen[/\\]?$", "") ---@type string out_path = parent .. "/gdb_tape_atoms_runtime.gdb" else out_path = ctx.out_root .. "/gdb_tape_atoms_runtime.gdb" @@ -581,8 +524,7 @@ end -- M — module exports -- ════════════════════════════════════════════════════════════════════════════ ---- @type AtomSourceMapPass -local M = {} +local M = {} ---@type AtomSourceMapPass -- Expose the pure render functions so `report.lua` and the focused tests can call them directly without triggering the file-emit path. M.render_source_map = render_source_map @@ -594,22 +536,15 @@ M.render_provenance = render_provenance 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") - --- @type WordMapEntry[], integer - local entries, total = canonical_word_entries(atom) - --- @type string[] - local lines = {} + local entries, total = canonical_word_entries(atom) ---@type WordMapEntry[], integer + local lines = {} ---@type string[] lines[#lines + 1] = string.format("ATOM %s %d", (atom.raw_name or atom.name), total) - --- @type integer, WordMapEntry - for _, entry in ipairs(entries) do - --- @type string - local word_line = string.format("WORD %d LINE %d TEXT %s", + 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) - --- @type string[] - local keys = {} - --- @type integer - for pos = 1, 16 do - --- @type string|nil - local k = entry.gpr_keys and entry.gpr_keys[pos] + 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 keys[#keys + 1] = k end @@ -635,17 +570,12 @@ function M.render_atom_provenance(atom, wc, rel_path) assert(type(atom) == "table", "render_atom_provenance: atom must be a table") 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") - --- @type WordMapEntry[], integer - local entries, total = canonical_word_entries(atom) - --- @type string[] - local lines = {} + local entries, total = canonical_word_entries(atom) ---@type WordMapEntry[], integer + local lines = {} ---@type string[] lines[#lines + 1] = string.format("ATOM %s %d", (atom.raw_name or atom.name), total) - --- @type integer, WordMapEntry - for _, entry in ipairs(entries) do - --- @type InvocationRecord|nil - local inv = entry.invocation - --- @type integer|nil - local macro_count = inv and wc and wc["mac_" .. inv.component_name] + for _, entry in ipairs(entries) do ---@type integer, WordMapEntry + 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' , entry.pos, rel_path, entry.line, inv.component_name, inv.def_path or "", inv.def_line or 0, entry.body_line) @@ -664,22 +594,17 @@ end --- @param ctx PassCtx --- @return PassResult function M.run(ctx) - --- @type PassOutputEntry[] - local outputs = {} - --- @type PassFinding[] - local errors = {} - --- @type PassFinding[] - local warnings = {} + local outputs = {} ---@type PassOutputEntry[] + local errors = {} ---@type PassFinding[] + local warnings = {} ---@type PassFinding[] - --- @type Corpus|nil - local corpus = ctx.shared and ctx.shared.corpus + local corpus = ctx.shared and ctx.shared.corpus ---@type Corpus|nil if type(corpus) ~= "table" or type(corpus.source_order) ~= "table" then error("atoms_source_map.run requires ctx.shared.corpus.source_order (canonical corpus).", 0) end -- Word counts come from `corpus.word_counts` (populated by word_count_eval + components passes). - --- @type WordCounts - local wc = corpus.word_counts or {} + local wc = corpus.word_counts or {} ---@type WordCounts if not next(wc) then warnings[#warnings + 1] = { line = 0, diff --git a/scripts/passes/auto_reg.lua b/scripts/passes/auto_reg.lua index 6bd2c0c..5a50f47 100644 --- a/scripts/passes/auto_reg.lua +++ b/scripts/passes/auto_reg.lua @@ -35,10 +35,8 @@ --- @field run fun(ctx: PassCtx): AutoRegResult --- @field POOL GprIdent[] ---- @type string -local _bootstrap_dir = debug.getinfo(1, "S").source:match("^@?(.*[/\\])") or "./" ---- @type DuffleExport -local duffle = dofile(_bootstrap_dir .. "../duffle_paths.lua") +local _bootstrap_dir = debug.getinfo(1, "S").source:match("^@?(.*[/\\])") or "./" ---@type string +local duffle = dofile(_bootstrap_dir .. "../duffle_paths.lua") ---@type DuffleExport --- ════════════════════════════════════════════════════════════════════════════ --- THE GPR ALLOCATION POOL — what's allocatable, and (more importantly) WHY @@ -56,8 +54,7 @@ local duffle = dofile(_bootstrap_dir .. "../duffle_paths.lua") --- R_K0/K1 (codes 26-27) — Kernel / interrupt handler reserves. Never touched by user code. --- R_GP/SP/FP/RA (codes 28-31) — R_SP/R_FP/R_RA are tape-runtime carriers between tape_enter and tape_exit; R_GP stays the host global pointer. --- ---- @type GprIdent[] -local POOL = { +local POOL = { ---@type GprIdent[] "R_V0", "R_V1", "R_T0", "R_T1", "R_T2", "R_T3", "R_T4", "R_T5", "R_T6", "R_T7", @@ -72,8 +69,7 @@ local POOL = { -- Only the POOL entries matter for auto_reg — non-pool aliases -- (R_AT=1, R_A0..A3=4..7, R_T8=24, R_T9=25, R_K0/K1=26..27, R_GP/SP/FP/RA=28..31) -- are deliberately omitted — see the comment block above for the WHY of each exclusion. ---- @type table -- bag: MIPS GPR code -> POOL ident -local INT_CODE_TO_POOL_GPR = { +local INT_CODE_TO_POOL_GPR = { ---@type table -- bag: MIPS GPR code -> POOL ident [2] = "R_V0", [3] = "R_V1", [4] = "R_A0", [5] = "R_A1", [6] = "R_A2", [7] = "R_A3", [8] = "R_T0", [9] = "R_T1", [10] = "R_T2", [11] = "R_T3", @@ -87,10 +83,8 @@ local INT_CODE_TO_POOL_GPR = { --- @param tbl table -- bag: key set only; values unused --- @return string[] local function stable_sort_keys(tbl) - --- @type string[] - local keys = {} - --- @type string - for k in pairs(tbl) do keys[#keys + 1] = k end + local keys = {} ---@type string[] + for k in pairs(tbl) do keys[#keys + 1] = k end ---@type string table.sort(keys) return keys end @@ -105,18 +99,12 @@ 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. - --- @type GprIdent[] - local pool = {} - --- @type integer - for i = 1, #POOL do pool[i] = POOL[i] end - --- @type GprAllocMap - local result = {} - --- @type PassFinding[] - local errors = {} - --- @type integer, string - for _, sym in ipairs(stable_sort_keys(decls)) do - --- @type GprIdent|nil - local next_gpr = table.remove(pool, 1) + 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 errors[#errors + 1] = { line = 0, @@ -144,16 +132,12 @@ end --- @return table --- @return table local function build_user_pins(corpus) - --- @type table -- bag: pinned physical GPR -> true - local user_pinned = {} - --- @type table -- bag: alias ident -> physical GPR - local alias_to_gpr = {} + local user_pinned = {} ---@type table -- bag: pinned physical GPR -> true + local alias_to_gpr = {} ---@type table -- bag: alias ident -> physical GPR if not corpus.register_alias_registry then return user_pinned, alias_to_gpr end - --- @type string, AliasEntry - for alias_name, alias_entry in pairs(corpus.register_alias_registry) do + for alias_name, alias_entry in pairs(corpus.register_alias_registry) do ---@type string, AliasEntry if alias_entry.has_atom_reg and alias_entry.code then - --- @type GprIdent|nil - local gpr = INT_CODE_TO_POOL_GPR[alias_entry.code] + local gpr = INT_CODE_TO_POOL_GPR[alias_entry.code] ---@type GprIdent|nil if gpr then user_pinned[gpr] = true alias_to_gpr[alias_name] = gpr @@ -174,29 +158,22 @@ end --- @param alias_to_gpr table -- bag: alias ident -> physical GPR --- @return table local function find_used_gprs(body_text, alias_to_gpr) - --- @type table -- bag: physical GPR -> hit count - local found = {} + local found = {} ---@type table -- bag: physical GPR -> hit count -- (a) Hardcoded physical GPRs (R_T0..R_T7, R_V0..R_V1, R_A0..R_A3, R_S0..R_S7). - --- @type GprIdent - for gpr in body_text:gmatch("(R_T%d+|R_V%d+|R_A%d+|R_S%d+)") do + for gpr in body_text:gmatch("(R_T%d+|R_V%d+|R_A%d+|R_S%d+)") do ---@type GprIdent found[gpr] = (found[gpr] or 0) + 1 end -- (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 - --- @type string[] - local aliases = {} - --- @type string - for alias_name in pairs(alias_to_gpr) do + local aliases = {} ---@type string[] + for alias_name in pairs(alias_to_gpr) do ---@type string aliases[#aliases + 1] = alias_name end table.sort(aliases) - --- @type string - local pattern = "(" .. table.concat(aliases, "|") .. ")" - --- @type string - for alias_name in body_text:gmatch(pattern) do - --- @type GprIdent|nil - local gpr = alias_to_gpr[alias_name] + local pattern = "(" .. table.concat(aliases, "|") .. ")" ---@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 end @@ -213,30 +190,24 @@ end --- @return string|nil local function emit_auto_reg_h(out_dir, dir, sources, mappings) if not mappings or next(mappings) == nil then return end - --- @type string - local out_path = out_dir .. "/" .. "auto_reg.h" + local out_path = out_dir .. "/" .. "auto_reg.h" ---@type string duffle.ensure_dir(out_dir) - --- @type string[] - local lines = { + local lines = { ---@type string[] "#ifdef INTELLISENSE_DIRECTIVES", "#pragma once", "#endif", "// Auto-generated by ps1_meta.lua (passes/auto_reg.lua) — DO NOT EDIT", "// Directory: " .. dir:gsub("/", "\\"), } - --- @type integer, SourceFile - for _, src in ipairs(sources) do + for _, src in ipairs(sources) do ---@type integer, SourceFile lines[#lines + 1] = "// source: " .. src.path end lines[#lines + 1] = "// Per-phase register allocations resolved by the lua pass." lines[#lines + 1] = "// R__Code = for every marker in this directory." lines[#lines + 1] = "" - --- @type integer, string - for _, sym in ipairs(stable_sort_keys(mappings)) do - --- @type GprIdent - local gpr = mappings[sym] - --- @type string - local gpr_code = gpr .. "_Code" + for _, sym in ipairs(stable_sort_keys(mappings)) do ---@type integer, string + local gpr = mappings[sym] ---@type GprIdent + local gpr_code = gpr .. "_Code" ---@type string lines[#lines + 1] = "#define " .. sym .. "_Code " .. gpr_code end lines[#lines + 1] = "" @@ -249,21 +220,16 @@ end -- Pass entry -- ════════════════════════════════════════════════════════════════════════════ ---- @type AutoRegPass -local M = {} +local M = {} ---@type AutoRegPass --- @param ctx PassCtx --- @return AutoRegResult function M.run(ctx) - --- @type AutoRegOutput[] - local outputs = {} - --- @type PassFinding[] - local errors = {} - --- @type PassFinding[] - local warnings = {} + local outputs = {} ---@type AutoRegOutput[] + local errors = {} ---@type PassFinding[] + local warnings = {} ---@type PassFinding[] - --- @type Corpus|nil - local corpus = ctx.shared and ctx.shared.corpus + local corpus = ctx.shared and ctx.shared.corpus ---@type Corpus|nil if type(corpus) ~= "table" then error("auto_reg.run requires ctx.shared.corpus", 0) end @@ -273,23 +239,17 @@ function M.run(ctx) -- MUST NOT be allocated to any auto-reg marker — they're preserved across atoms by the wave-context discipline. -- The corpus's register_alias_registry is the source of truth for these opt-in pins. -- Body references to those aliases (via alias_to_gpr) are also excluded on a per-atom basis in step 2 below. - --- @type table, table - local user_pinned, alias_to_gpr = build_user_pins(corpus) + 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). - --- @type table -- bag: phase_label -> alloc map - local phase_allocations = {} - --- @type string, table - for phase_label, decls in pairs(corpus.phase_auto_regs or {}) do - --- @type GprAllocMap, PassFinding[] - local mapping, errs = allocate_phase(phase_label, decls) - --- @type string, GprIdent - for sym, gpr in pairs(mapping) do + 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 phase_allocations[phase_label] = phase_allocations[phase_label] or {} phase_allocations[phase_label][sym] = gpr end - --- @type integer, PassFinding - for _, e in ipairs(errs) do + for _, e in ipairs(errs) do ---@type integer, PassFinding errors[#errors + 1] = e end end @@ -298,22 +258,16 @@ 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. - --- @type table -- bag: atom name -> phase label - local atom_name_to_phase = {} - --- @type string, AtomPhaseGroup - for phase_label, entry in pairs(corpus.atom_phases or {}) do - --- @type integer, AtomName - for _, atom_name in ipairs(entry.atoms or {}) do + 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 - --- @type table -- bag: atom scope -> alloc map - local atom_allocations = {} - --- @type AtomName, table - for atom_scope, decls in pairs(corpus.atom_auto_regs or {}) do - --- @type string|nil - local phase_label = atom_name_to_phase[atom_scope] + 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: -- (a) every GPR already committed (phase allocations + prior atom allocations) -- (b) every USER-PINNED GPR (wave-context carriers + file-scope pinned aliases) @@ -323,37 +277,26 @@ 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`. - --- @type table -- bag: committed or body-referenced GPR -> true - local used = {} - --- @type integer, GprAllocMap - 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 + 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. -- Folded into `used` so the source_pool exclusion is a single check. - --- @type AtomEntry|nil - local atom = corpus.atoms_by_name and corpus.atoms_by_name[atom_scope] + local atom = corpus.atoms_by_name and corpus.atoms_by_name[atom_scope] ---@type AtomEntry|nil if atom and atom.body then - --- @type table - local body_used = find_used_gprs(atom.body, alias_to_gpr) - --- @type GprIdent - for gpr in pairs(body_used) do used[gpr] = true end + 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 end - --- @type GprIdent[] - local source_pool = {} - --- @type integer, GprIdent - for _, gpr in ipairs(POOL) do + 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 - --- @type GprAllocMap - local result = {} - --- @type integer, string - for _, sym in ipairs(stable_sort_keys(decls)) do - --- @type GprIdent|nil - local next_gpr = table.remove(source_pool, 1) + 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 errors[#errors + 1] = { line = 0, @@ -375,15 +318,11 @@ function M.run(ctx) -- This warning is kept as a defensive safety net for cases the body scanner might miss -- (e.g. macros that expand to register references the scanner cannot resolve). -- For each resolved (scope, sym) -> R_Tn mapping, scan the atom body source for used GPRs. - --- @type AtomName, GprAllocMap - for atom_scope, decls in pairs(atom_allocations) do - --- @type AtomEntry|nil - local atom = corpus.atoms_by_name and corpus.atoms_by_name[atom_scope] + for atom_scope, decls in pairs(atom_allocations) do ---@type AtomName, GprAllocMap + local atom = corpus.atoms_by_name and corpus.atoms_by_name[atom_scope] ---@type AtomEntry|nil if atom and atom.body then - --- @type table - local used_in_body = find_used_gprs(atom.body, alias_to_gpr) - --- @type string, GprIdent - for sym, allocated_gpr in pairs(decls) do + 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 if used_in_body[allocated_gpr] and used_in_body[allocated_gpr] > 0 then warnings[#warnings + 1] = { line = atom.line or 0, @@ -398,37 +337,27 @@ 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. - --- @type table - local sources_by_dir = corpus.sources_by_dir or {} - --- @type string, SourceFile[] - for dir, sources in pairs(sources_by_dir) do - --- @type GprAllocMap - local per_dir_mappings = {} - --- @type integer, SourceFile - for _, src in ipairs(sources) do + 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 _, 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"). -- The previous `for _, scan_atom_auto` form silently assigned the VALUE (a `{sym = sym}` table) to the variable, -- which made `atom_allocations[scan_atom_auto]` a table-indexed lookup that never resolved. - --- @type string - for scope_name in pairs(src.scan and src.scan.atom_auto_regs or {}) do - --- @type string, GprIdent - for sym, gpr in pairs(atom_allocations[scope_name] or {}) do + for scope_name in pairs(src.scan and src.scan.atom_auto_regs or {}) do ---@type string + for sym, gpr in pairs(atom_allocations[scope_name] or {}) do ---@type string, GprIdent per_dir_mappings[sym] = gpr end end - --- @type string - for scope_name in pairs(src.scan and src.scan.phase_auto_regs or {}) do - --- @type string, GprIdent - for sym, gpr in pairs(phase_allocations[scope_name] or {}) do + for scope_name in pairs(src.scan and src.scan.phase_auto_regs or {}) do ---@type string + for sym, gpr in pairs(phase_allocations[scope_name] or {}) do ---@type string, GprIdent per_dir_mappings[sym] = gpr end end end - --- @type string - local out_dir = dir .. "/gen" - --- @type string|nil - local out_path = emit_auto_reg_h(out_dir, dir, sources, per_dir_mappings) + 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 return { outputs = outputs, errors = errors, warnings = warnings } diff --git a/scripts/passes/components.lua b/scripts/passes/components.lua index d98380e..2db3429 100644 --- a/scripts/passes/components.lua +++ b/scripts/passes/components.lua @@ -22,42 +22,30 @@ -- Uses `debug.getinfo` to find this file's own directory, so it works both standalone and when require'd from the orchestrator. -- Bootstrap: load `duffle_paths.lua` via `debug.getinfo(1, "S").source` (works both standalone + when require'd). -- duffle_paths.lua sets package.path then returns `require("duffle")` at the bottom, so the dofile value IS the duffle module. ---- @type string -local _bootstrap_dir = debug.getinfo(1, "S").source:match("^@?(.*[/\\])") or "./" ---- @type DuffleExport -local duffle = dofile(_bootstrap_dir .. "../duffle_paths.lua") +local _bootstrap_dir = debug.getinfo(1, "S").source:match("^@?(.*[/\\])") or "./" ---@type string +local duffle = dofile(_bootstrap_dir .. "../duffle_paths.lua") ---@type DuffleExport -- ════════════════════════════════════════════════════════════════════════════ -- Constants -- ════════════════════════════════════════════════════════════════════════════ -- Atom component declaration identifiers. ---- @type string -local ATOM_COMP_PROC = "MipsAtomComp_Proc_" ---- @type string -local MIPS_ATOM = "Slice_MipsCode" -- prefix on the function declaration that wraps an AtomComp_Proc_ +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_ -- Component-name prefixes. ---- @type string -local AC_PREFIX = "ac_" -- arg to MipsAtomComp_(ac_X); the X is the atom name ---- @type integer -local AC_PREFIX_LEN = 3 ---- @type string -local MAC_PREFIX = "mac_" -- prefix on generated macros; the rest is the atom name ---- @type integer -local MAC_PREFIX_LEN = 4 +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 MAC_PREFIX = "mac_" ---@type string -- prefix on generated macros; the rest is the atom name +local MAC_PREFIX_LEN = 4 ---@type integer -- ASCII byte values used in tokenization. ---- @type integer -local BYTE_NEWLINE = 10 ---- @type integer -local BYTE_SLASH = 47 +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). ---- @type string -local GEN_SUBDIR = "gen" ---- @type string -local MACS_FILENAME = "macs.h" +local GEN_SUBDIR = "gen" ---@type string +local MACS_FILENAME = "macs.h" ---@type string -- ════════════════════════════════════════════════════════════════════════════ -- Type declarations @@ -100,8 +88,7 @@ local MACS_FILENAME = "macs.h" -- Local helpers (file I/O + path normalization) -- ════════════════════════════════════════════════════════════════════════════ ---- @type ComponentsPass -local M = {} +local M = {} ---@type ComponentsPass -- ════════════════════════════════════════════════════════════════════════════ -- Back-walk helpers (composed into the entry point below: find_function_args_for) @@ -123,8 +110,7 @@ local M = {} --- @param before_pos integer --- @return string|nil local function find_function_args_for(source, name, before_pos) - --- @type string|nil, string|nil - local _, args_inner = duffle.find_function_decl_for(source, before_pos, #MIPS_ATOM) + local _, args_inner = duffle.find_function_decl_for(source, before_pos, #MIPS_ATOM) ---@type string|nil, string|nil return args_inner end @@ -140,31 +126,24 @@ end --- @return string[]|nil local function extract_arg_names(args_str) if not args_str or args_str == "" then return nil end - --- @type string[] - local names = {} - --- @type string[] - local tokens = duffle.split_top_level_commas(args_str) - --- @type integer, string - for _, tok in ipairs(tokens) do - --- @type string - local trimmed = duffle.trim(tok) + local names = {} ---@type string[] + local tokens = duffle.split_top_level_commas(args_str) ---@type 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. -- split_top_level_commas only skips block comments at TOP LEVEL (between commas), -- not block comments embedded WITHIN a token between a parameter and a trailing comma. -- Without this strip, the identifier-walk below stops at the `/` of `*/` and returns -- the wrong name (or nothing). See `test_extract_arg_names_handles_trailing_block_comments`. - --- @type integer - local trimmed_end = #trimmed + local trimmed_end = #trimmed ---@type integer if trimmed_end >= 2 and trimmed:sub(trimmed_end - 1, trimmed_end) == "*/" then -- Find the matching `/*` that opens the trailing comment. -- Walk back from the `*/` looking for `/*` (whitespace + `/*`). - --- @type integer - local close_pos = trimmed_end - 1 -- position of the second-to-last char + local close_pos = trimmed_end - 1 ---@type integer -- position of the second-to-last char -- Walk back: skip trailing whitespace, then look for the `/*` opener. while close_pos > 1 do - --- @type string - local ch = trimmed:sub(close_pos, close_pos) + local ch = trimmed:sub(close_pos, close_pos) ---@type string if ch == " " or ch == "\t" or ch == "\n" or ch == "\r" then close_pos = close_pos - 1 else @@ -172,10 +151,8 @@ 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). - --- @type integer|nil - local opener_pos = nil - --- @type integer - local scan = close_pos - 3 + 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 opener_pos = scan @@ -194,11 +171,9 @@ local function extract_arg_names(args_str) trimmed_end = #trimmed if trimmed_end >= 4 and trimmed:sub(trimmed_end, trimmed_end) == "]" then -- Walk back: skip digits, expect `[`. - --- @type integer - local bracket_pos = trimmed_end - 1 + local bracket_pos = trimmed_end - 1 ---@type integer while bracket_pos > 1 do - --- @type string - local ch = trimmed:sub(bracket_pos, bracket_pos) + local ch = trimmed:sub(bracket_pos, bracket_pos) ---@type string if ch >= "0" and ch <= "9" then bracket_pos = bracket_pos - 1 else @@ -212,22 +187,18 @@ local function extract_arg_names(args_str) if trimmed == "" then goto continue end -- Find the identifier at the end: walk back over trailers (whitespace + `*` + `[]`), -- then walk back over the identifier chars (alnum + `_`). - --- @type integer - local ident_end = #trimmed + local ident_end = #trimmed ---@type integer while ident_end > 0 do - --- @type string - local ch = trimmed:sub(ident_end, ident_end) + local ch = trimmed:sub(ident_end, ident_end) ---@type string if ch == " " or ch == "\t" or ch == "*" or ch == "]" or ch == "[" then ident_end = ident_end - 1 else break end end - --- @type integer - local ident_start = ident_end + local ident_start = ident_end ---@type integer while ident_start > 0 do - --- @type string - local ch = trimmed:sub(ident_start, ident_start) + local ch = trimmed:sub(ident_start, ident_start) ---@type string if duffle.is_alnum_byte(string.byte(ch)) or ch == "_" then ident_start = ident_start - 1 else @@ -235,8 +206,7 @@ local function extract_arg_names(args_str) end end ident_start = ident_start + 1 - --- @type string - local name = trimmed:sub(ident_start, ident_end) + local name = trimmed:sub(ident_start, ident_end) ---@type string if name ~= "" then names[#names + 1] = name end ::continue:: end @@ -248,8 +218,7 @@ end --- @param args_str string|nil --- @return string[]|nil local function formal_arg_names(args_str) - --- @type string[]|nil - local names = extract_arg_names(args_str) + local names = extract_arg_names(args_str) ---@type string[]|nil if not names then return nil end if names[1] == "ab" then table.remove(names, 1) end if #names == 0 then return nil end @@ -271,10 +240,8 @@ end --- @param scan SourceScan --- @return Component[] local function project_components(source, scan) - --- @type Component[] - local out = {} - --- @type integer, AtomEntry - for _, a in ipairs(scan.atoms) do + 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. -- `MipsAtom_Proc_` (kind="atom_proc") is an ATOM (ends with `mac_yield()`); it gets emitted via @@ -285,12 +252,10 @@ local function project_components(source, scan) -- Function-args lookup is meaningful for `MipsAtomComp_Proc_` components -- (the macro sits inside `FI_ Slice_MipsCode ac_X(...)`); the alias expansion -- discards the `ab` (atom-builder) arg the same way both forms do. - --- @type string|nil - local args = find_function_args_for(source, a.raw_name, a.ident_pos) + local args = find_function_args_for(source, a.raw_name, a.ident_pos) ---@type string|nil -- Comment ownership: scan_source.lua stamps `declaration_comment` on the record by walking backward past any associated bare marker. -- The pass reads `declaration_comment` directly. - --- @type string - local comment = a.declaration_comment or "" + local comment = a.declaration_comment or "" ---@type string out[#out + 1] = { line = a.line, name = a.name, @@ -321,31 +286,23 @@ end --- @param s string --- @return string local function convert_line_comments_to_block(s) - --- @type string - local result = s - --- @type integer - local pos = 1 - --- @type integer - local len = #result + local result = s ---@type string + local pos = 1 ---@type integer + local len = #result ---@type integer while pos <= len do - --- @type boolean - local is_double_slash = result:byte(pos) == BYTE_SLASH + local is_double_slash = result:byte(pos) == BYTE_SLASH ---@type boolean and pos + 1 <= len and result:byte(pos + 1) == BYTE_SLASH if not is_double_slash then pos = pos + 1 else -- Find end of line. - --- @type integer - local eol = pos + local eol = pos ---@type integer while eol <= len and result:byte(eol) ~= BYTE_NEWLINE do eol = eol + 1 end - --- @type string - local before = result:sub(1, pos - 1) - --- @type string - local comment = result:sub(pos + 2, eol - 1) -- skip the `//` - --- @type string - local after + 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 @@ -382,14 +339,11 @@ end --- @param tok string --- @return string local function strip_leading_delay_marker(tok) - --- @type string|nil - local ident = duffle.read_ident(tok, 1) + local ident = duffle.read_ident(tok, 1) ---@type string|nil if not ident or not duffle.DELAY_MARKERS[ident] then return tok end - --- @type string - local rest = tok:sub(#ident + 1):match("^%s*(.*)$") or "" + local rest = tok:sub(#ident + 1):match("^%s*(.*)$") or "" ---@type string while rest:sub(1, 2) == "/*" do - --- @type integer|nil - local close = rest:find("*/", 3, true) + local close = rest:find("*/", 3, true) ---@type integer|nil if not close then return "" end rest = rest:sub(close + 2):match("^%s*(.*)$") or "" end @@ -406,24 +360,17 @@ end 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) - --- @type Component|nil - local cc = comp_by_name[name] - --- @type integer - local n + local cc = comp_by_name[name] ---@type Component|nil + local n ---@type integer if cc then n = 0 - --- @type BodyToken[] - local tokens = cc.body_tokens - --- @type integer, BodyToken - for _, t in ipairs(tokens) do - --- @type string - local trimmed = t.tok + local tokens = cc.body_tokens ---@type BodyToken[] + for _, t in ipairs(tokens) do ---@type integer, BodyToken + local trimmed = t.tok ---@type string if trimmed ~= "" then - --- @type string - local work = trimmed + local work = trimmed ---@type string while true do - --- @type string|nil - local marker = duffle.read_ident(work, 1) + local marker = duffle.read_ident(work, 1) ---@type string|nil if marker and duffle.DELAY_MARKERS[marker] then work = strip_leading_delay_marker(work) if work == "" then break end @@ -432,8 +379,7 @@ local function word_count_rec(name, comp_by_name, wc, cache) end end if work ~= "" then - --- @type string|nil - local lookup = strip_mac_prefix(duffle.read_ident(work, 1)) + local lookup = strip_mac_prefix(duffle.read_ident(work, 1)) ---@type string|nil if lookup == "atom_label" or lookup == "atom_offset" then -- Pure metaprogram anchors; emit zero words. elseif lookup and comp_by_name[lookup] then @@ -466,16 +412,11 @@ end --- @param wc WordCounts --- @return table -- bag: bare component name -> word count local function count_all_components(components, wc) - --- @type table - local comp_by_name = {} - --- @type integer, Component - for _, cc in ipairs(components) do comp_by_name[cc.name] = cc end - --- @type table -- bag: memo; -1 in-progress sentinel - local cache = {} - --- @type table -- bag: bare name -> word count - local counts = {} - --- @type integer, Component - for _, c in ipairs(components) do + 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 counts[c.name] = word_count_rec(c.name, comp_by_name, wc, cache) end return counts @@ -504,34 +445,23 @@ end 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 } - --- @type Component|nil - local cc = comp_by_name[name] - --- @type integer - local cycle_cost - --- @type integer - local gp0_contrib + local cc = comp_by_name[name] ---@type Component|nil + local cycle_cost ---@type integer + local gp0_contrib ---@type integer if cc then - --- @type boolean - local skip_cycle = (name == "yield") - --- @type boolean - local skip_gp0 = name:match("^insert_ot_tag") ~= nil + local skip_cycle = (name == "yield") ---@type boolean + local skip_gp0 = name:match("^insert_ot_tag") ~= nil ---@type boolean cycle_cost = 0 gp0_contrib = 0 if not skip_cycle or not skip_gp0 then - --- @type BodyToken[] - local tokens = cc.body_tokens - --- @type integer, BodyToken - for _, t in ipairs(tokens) do - --- @type string - local trimmed = t.tok + local tokens = cc.body_tokens ---@type BodyToken[] + for _, t in ipairs(tokens) do ---@type integer, BodyToken + local trimmed = t.tok ---@type string if trimmed ~= "" then - --- @type string|nil - local ident = duffle.read_ident(trimmed, 1) + local ident = duffle.read_ident(trimmed, 1) ---@type string|nil if ident and ident:sub(1, MAC_PREFIX_LEN) == MAC_PREFIX then - --- @type string - local nested = ident:sub(MAC_PREFIX_LEN + 1) - --- @type ComponentMeta - local nested_meta = component_meta_rec(nested, comp_by_name, latency, cache) + 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 end @@ -540,10 +470,8 @@ local function component_meta_rec(name, comp_by_name, latency, cache) end else if not skip_cycle then - --- @type InstructionRow|nil - local isa = duffle.instr(ident) - --- @type GteCommandRow|nil - local gte = duffle.gte(ident) + local isa = duffle.instr(ident) ---@type InstructionRow|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 @@ -578,16 +506,11 @@ end --- @param latency table -- bag: ident -> cycle cost --- @return ComponentMetaMap local function compute_components_metadata(components, latency) - --- @type table - local comp_by_name = {} - --- @type integer, Component - for _, cc in ipairs(components) do comp_by_name[cc.name] = cc end - --- @type ComponentMetaMap - local cache = {} - --- @type ComponentMetaMap - local out = {} - --- @type integer, Component - for _, c in ipairs(components) do + 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 out[c.name] = component_meta_rec(c.name, comp_by_name, latency, cache) end return out @@ -602,15 +525,11 @@ end --- @param s string --- @return string[] local function split_comment_lines(s) - --- @type string[] - local out = {} - --- @type integer - local pos = 1 - --- @type integer - local s_len = #s + local out = {} ---@type string[] + local pos = 1 ---@type integer + local s_len = #s ---@type integer while pos <= s_len do - --- @type integer|nil - local nl = s:find("\n", pos, true) + local nl = s:find("\n", pos, true) ---@type integer|nil if not nl then out[#out + 1] = s:sub(pos) break @@ -629,8 +548,7 @@ end --- @param args_str string|nil --- @return string local function signature_from_args(args_str) - --- @type string[]|nil - local names = formal_arg_names(args_str) + local names = formal_arg_names(args_str) ---@type string[]|nil if names then return table.concat(names, ", ") end @@ -642,8 +560,7 @@ end --- @param lines string[] --- @return nil local function strip_trailing_continuation(lines) - --- @type string - local last = lines[#lines] + local last = lines[#lines] ---@type string if last:sub(-2) == " \\" then lines[#lines] = last:sub(1, -3) end @@ -669,37 +586,30 @@ end --- @param tok string -- a single token from split_top_level_commas (already trimmed at the start, may contain trailing whitespace + block comment) --- @return boolean local function is_pure_delay_marker_token(tok) - --- @type table -- bag: delay-marker ident -> true - local markers = duffle.DELAY_MARKERS + local markers = duffle.DELAY_MARKERS ---@type table -- bag: delay-marker ident -> true if type(markers) ~= "table" then return false end -- Identify a leading delay-marker identifier (e.g. `GteDelay_`). - --- @type integer - local ident_end = 1 + local ident_end = 1 ---@type integer while ident_end <= #tok do - --- @type string - local ch = tok:sub(ident_end, ident_end) + local ch = tok:sub(ident_end, ident_end) ---@type string if ch:match("[%w_]") then ident_end = ident_end + 1 else break end end - --- @type string - local ident = tok:sub(1, ident_end - 1) + local ident = tok:sub(1, ident_end - 1) ---@type string if not markers[ident] then return false end -- Walk the remainder: only whitespace and block comments are allowed. - --- @type integer - local scan = ident_end + local scan = ident_end ---@type integer while scan <= #tok do - --- @type string - local ch = tok:sub(scan, scan) + local ch = tok:sub(scan, scan) ---@type string if ch:match("%s") then scan = scan + 1 elseif ch == "/" and tok:sub(scan + 1, scan + 1) == "*" then - --- @type integer|nil - local close = tok:find("*/", scan + 2, true) + local close = tok:find("*/", scan + 2, true) ---@type integer|nil if not close then return false end scan = close + 2 else @@ -743,17 +653,14 @@ end --- @param tokens string[] --- @return nil local function emit_macro_body(lines, c, sig, tokens) - --- @type integer - for tok_idx = 1, #tokens do + for tok_idx = 1, #tokens do ---@type integer tokens[tok_idx] = convert_line_comments_to_block(tokens[tok_idx]) end if #tokens == 0 then return end lines[#lines + 1] = "#define mac_" .. c.name .. "(" .. sig .. ") \\" lines[#lines + 1] = "\t" .. tokens[1] .. " \\" - --- @type integer - for tok_idx = 2, #tokens do - --- @type string - local sep = token_skips_leading_comma(tokens[tok_idx]) and "\t" or ",\t" + for tok_idx = 2, #tokens do ---@type integer + local sep = token_skips_leading_comma(tokens[tok_idx]) and "\t" or ",\t" ---@type string lines[#lines + 1] = sep .. tokens[tok_idx] .. " \\" end strip_trailing_continuation(lines) @@ -768,8 +675,7 @@ end --- @param counts table -- bag: bare component name -> word count --- @return string[] -- list of lines for this component local function build_component_lines(c, counts) - --- @type string[] - local lines = {} + local lines = {} ---@type string[] -- Marker comment: emitted once for every skipped component. -- The marker is scanner-owned (declared by `atom_dbg_skip` immediately before the declaration in the source); @@ -779,21 +685,16 @@ local function build_component_lines(c, counts) end if c.comment and c.comment ~= "" then - --- @type integer, string - for _, line in ipairs(split_comment_lines(c.comment)) do + for _, line in ipairs(split_comment_lines(c.comment)) do ---@type integer, string lines[#lines + 1] = line end end - --- @type string[] - local tokens = duffle.split_top_level_commas(c.body) - --- @type integer - for i = 1, #tokens do tokens[i] = duffle.trim(tokens[i]) end - --- @type string - local sig = signature_from_args(c.args) + 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 -- Direct lookup against the per-source precomputed `counts` table (built once by count_all_components). - --- @type integer - local n = counts[c.name] + local n = counts[c.name] ---@type integer if n > 0 then emit_macro_body(lines, c, sig, tokens) @@ -816,14 +717,11 @@ end --- @param sources SourceFile[] -- Sources contributing to this directory (for the header comment) --- @return string[] local function header_boilerplate(dir, sources) - --- @type string[] - local source_lines = { "// Directory: " .. duffle.to_absolute_path(dir) .. "/" } - --- @type integer, SourceFile - for _, src in ipairs(sources) do + local source_lines = { "// Directory: " .. duffle.to_absolute_path(dir) .. "/" } ---@type string[] + for _, src in ipairs(sources) do ---@type integer, SourceFile source_lines[#source_lines + 1] = "// source: " .. duffle.to_absolute_path(src.path) end - --- @type string - local source_blob = table.concat(source_lines, "\n") + local source_blob = table.concat(source_lines, "\n") ---@type string return { -- #pragma once wrapped in #ifdef INTELLISENSE_DIRECTIVES, matching the convention in lottes_tape.h. -- The build does manual unity includes (the user controls include order), so the pragma is only active for IDE/tooling. @@ -851,10 +749,8 @@ end --- @return string -- Output directory --- @return string -- Full output path local function compute_macs_h_path(dir) - --- @type string - local out_dir = dir .. "/" .. GEN_SUBDIR - --- @type string - local out_path = out_dir .. "/" .. MACS_FILENAME + local out_dir = dir .. "/" .. GEN_SUBDIR ---@type string + local out_path = out_dir .. "/" .. MACS_FILENAME ---@type string return out_dir, out_path end @@ -868,21 +764,16 @@ 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 - --- @type string, string - local out_dir, out_path = compute_macs_h_path(dir) - --- @type string[] - local lines = header_boilerplate(dir, sources) + local out_dir, out_path = compute_macs_h_path(dir) ---@type string, string + local lines = header_boilerplate(dir, sources) ---@type string[] - --- @type integer, Component - for _, c in ipairs(components) do - --- @type integer, string - for _, l in ipairs(build_component_lines(c, counts)) do + for _, c in ipairs(components) do ---@type integer, Component + for _, l in ipairs(build_component_lines(c, counts)) do ---@type integer, string lines[#lines + 1] = l end end - --- @type string - local content = table.concat(lines, "\n") .. "\n" + local content = table.concat(lines, "\n") .. "\n" ---@type string duffle.ensure_dir(out_dir) duffle.write_file_lf(out_path, content) print(string.format(" -> %s", out_path)) @@ -900,12 +791,9 @@ end --- @param counts table -- bag: bare component name -> word count --- @return nil local function update_canonical_word_counts(corpus, components, counts) - --- @type WordCounts - local wc = corpus.word_counts - --- @type integer, Component - for _, c in ipairs(components) do - --- @type string - local key = "mac_" .. c.name + 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 wc[key] = counts[c.name] end @@ -932,15 +820,12 @@ end --- @param metadata ComponentMetaMap --- @return nil local function update_canonical_components(corpus, src, components, metadata) - --- @type string - local rel_path = src.path:gsub("\\", "/") - --- @type integer, Component - for _, c in ipairs(components) do + local rel_path = src.path:gsub("\\", "/") ---@type string + 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. - --- @type ComponentMeta|nil - local m = metadata and metadata[c.name] or nil + local m = metadata and metadata[c.name] or nil ---@type ComponentMeta|nil if corpus.components[c.name] == nil then corpus.components[c.name] = { name = c.name, @@ -954,13 +839,10 @@ local function update_canonical_components(corpus, src, components, metadata) else -- A second declaration of the same bare name: record a typed collision so static-analysis + the report can surface it. -- Identical-shape declarations (same path + line) reuse the first-wins entry without a collision record. - --- @type ComponentDef - local existing = corpus.components[c.name] + local existing = corpus.components[c.name] ---@type ComponentDef if existing.path ~= rel_path or existing.line ~= c.line then - --- @type string - local kind = c.kind or "comp_bare" - --- @type string - local first_kind = existing.kind or "comp_bare" + 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", name = c.name, @@ -983,10 +865,8 @@ end --- @param scan SourceScan --- @return nil local function update_canonical_component_body_index(corpus, src, components, scan) - --- @type (fun(pos: integer): integer)|nil - local line_of = scan and scan.line_of - --- @type integer, Component - for _, c in ipairs(components) do + local line_of = scan and scan.line_of ---@type (fun(pos: integer): integer)|nil + 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, @@ -1004,16 +884,12 @@ end --- @param ctx PassCtx --- @return PassResult function M.run(ctx) - --- @type MacsOutput[] - local outputs = {} - --- @type PassFinding[] - local errors = {} - --- @type PassFinding[] - local warnings = {} + local outputs = {} ---@type MacsOutput[] + local errors = {} ---@type PassFinding[] + local warnings = {} ---@type PassFinding[] -- Corpus ownership gate. - --- @type Corpus|nil - local corpus = ctx.shared and ctx.shared.corpus + local corpus = ctx.shared and ctx.shared.corpus ---@type Corpus|nil if type(corpus) ~= "table" then error("components.run requires ctx.shared.corpus.", 0) end @@ -1034,22 +910,15 @@ 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`). - --- @type table - local sources_by_dir = corpus.sources_by_dir or duffle.group_sources_by_dir(corpus.source_order) - --- @type string, SourceFile[] - for dir, sources in pairs(sources_by_dir) do + 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[] -- Aggregate components from every source in this directory. -- `project_components` returns nil for sources with no `MipsAtomComp_` declarations; we skip those. - --- @type Component[] - local aggregated_components = {} - --- @type table - local metadata_per_source = {} - --- @type integer, SourceFile - for _, src in ipairs(sources) do - --- @type Component[] - local per_source = project_components(src.text, src.scan) or {} - --- @type integer, Component - for _, c in ipairs(per_source) do + 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 aggregated_components[#aggregated_components + 1] = c end if #per_source > 0 then @@ -1059,18 +928,14 @@ 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. - --- @type table -- bag: bare name -> word count - local counts = count_all_components(aggregated_components, corpus.word_counts) - --- @type string|nil - local macs_path = emit_component_macros_h(ctx, dir, sources, aggregated_components, counts) + 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 } -- Populate the projections AFTER disk emission (byte-identical `.macs.h` contract). update_canonical_word_counts(corpus, aggregated_components, counts) - --- @type integer, SourceFile - for _, src in ipairs(sources) do - --- @type Component[] - local per_source = project_components(src.text, src.scan) or {} + for _, src in ipairs(sources) do ---@type integer, SourceFile + local per_source = project_components(src.text, src.scan) or {} ---@type Component[] if #per_source > 0 then update_canonical_components(corpus, src, per_source, metadata_per_source[src]) update_canonical_component_body_index(corpus, src, per_source, src.scan) diff --git a/scripts/passes/dwarf_injection.lua b/scripts/passes/dwarf_injection.lua index 4a6d761..37949bc 100644 --- a/scripts/passes/dwarf_injection.lua +++ b/scripts/passes/dwarf_injection.lua @@ -33,25 +33,19 @@ -- Load `duffle_paths.lua` via `debug.getinfo(1, "S").source` (works both standalone + when require'd). -- Sets package.path + package.cpath then returns duffle. ---- @type string -local _bootstrap_dir = debug.getinfo(1, "S").source:match("^@?(.*[/\\])") or "./" ---- @type DuffleExport -local duffle = dofile(_bootstrap_dir .. "../duffle_paths.lua") +local _bootstrap_dir = debug.getinfo(1, "S").source:match("^@?(.*[/\\])") or "./" ---@type string +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 -- (read_elf_sections, nm, source-map parser, LE byte r/w). `list_dir` is the general directory primitive in duffle.lua. ---- @type ElfDwarf -local elf_dwarf = require("elf_dwarf") +local elf_dwarf = require("elf_dwarf") ---@type ElfDwarf -- File-scope aliases to elf_dwarf helpers; the canonical implementations live in scripts/elf_dwarf.lua. ---- @type fun(table_bytes: string, table_start: integer): integer|nil -local find_abbrev_table_end = elf_dwarf.find_abbrev_table_end +local find_abbrev_table_end = elf_dwarf.find_abbrev_table_end ---@type fun(table_bytes: string, table_start: integer): integer|nil -- Local DWARF opcode constants + length-prefixed integers (uleb128 + sleb128 encoders are in elf_dwarf.lua). ---- @type fun(n: integer): string -local uleb128 = elf_dwarf.uleb128 ---- @type fun(n: integer): string -local sleb128 = elf_dwarf.sleb128 +local uleb128 = elf_dwarf.uleb128 ---@type fun(n: integer): string +local sleb128 = elf_dwarf.sleb128 ---@type fun(n: integer): string -- ════════════════════════════════════════════════════════════════════════════ -- Constants @@ -61,43 +55,27 @@ local sleb128 = elf_dwarf.sleb128 -- 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). ---- @type DwarfLineOps -local DWARF_LINE_OPS = elf_dwarf.DWARF_LINE_OPS ---- @type Dwarf5Rnglists -local DWARF5_RNGLISTS = elf_dwarf.DWARF5_RNGLISTS ---- @type integer -local MIPS_BYTES_PER_WORD = elf_dwarf.MIPS_BYTES_PER_WORD +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 ---- @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_LNE_end_sequence = DWARF_LINE_OPS.DW_LNE_end_sequence ---- @type integer -local DW_LNE_set_address = DWARF_LINE_OPS.DW_LNE_set_address +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_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 ---- @type integer -local DW_RLE_end_of_list = DWARF5_RNGLISTS.end_of_list ---- @type integer -local DW_RLE_start_length = DWARF5_RNGLISTS.start_length +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`). ---- @type table|nil -- bag -local _file_index_by_basename = nil -- [basename] = 1-based line-table file index ---- @type table|nil -- bag -local _file_path_by_index = nil -- [1-based index] = full source path (diagnostics / future consumers) ---- @type integer -local _default_atom_source_index = nil -- 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). @@ -108,39 +86,27 @@ local _default_atom_source_index = nil -- any valid index used in opaque-row fa -- 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). ---- @type integer -local ABBREV_CU = 0x64 -- 100: DW_TAG_compile_unit ---- @type integer -local ABBREV_SUBPROGRAM = 0x65 -- 101: DW_TAG_subprogram ---- @type integer -local ABBREV_VARIABLE = 0x66 -- 102: DW_TAG_variable with DW_AT_type = ref4 to U4 ---- @type integer -local ABBREV_STRUCT_TYPE = 0x67 -- 103: DW_TAG_structure_type with children (Binds_X mirror) ---- @type integer -local ABBREV_MEMBER = 0x68 -- 104: DW_TAG_member no children (DW_AT_type = ref4 to U4 base) ---- @type integer -local ABBREV_BIND_VAR = 0x69 -- 105: DW_TAG_variable no children + DW_AT_type = ref4 (the bind_args variable) ---- @type integer -local ABBREV_BASE_TYPE = 0x6A -- 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). ---- @type integer -local ABBREV_ABSTRACT_SUBPROGRAM = 0x6B -- 107: DW_TAG_subprogram (abstract — no low_pc/high_pc); for each unique mac_X component ---- @type integer -local ABBREV_INLINED_SUBROUTINE = 0x6C -- 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). ---- @type integer -local ABBREV_BIND_VAR_LOCLIST = 0x6D -- 109: DW_TAG_variable no children + DW_AT_type = ref4 + DW_AT_location = sec_offset +local ABBREV_BIND_VAR_LOCLIST = 0x6D ---@type integer -- 109: DW_TAG_variable no children + DW_AT_type = ref4 + DW_AT_location = sec_offset -- Typed-view pointer_type (for the synthetic V4_S2* / V3_S2* / U4* / void* chains). -- MUST be a fresh abbrev code in the appended table — emitting uleb128(9) collides with GCC's existing abbrev 9 -- (a pointer_type that carries DW_AT_byte_size + DW_AT_type), so gdb misparses our 4-byte ref4 as (byte_size, type[0..2]) and lands the cursor mid-attribute. ---- @type integer -local ABBREV_TYPED_VIEW_POINTER = 0x6E -- 110: DW_TAG_pointer_type no children + DW_AT_type = ref4 (typed-view / U4 / void chain) +local ABBREV_TYPED_VIEW_POINTER = 0x6E ---@type integer -- 110: DW_TAG_pointer_type no children + DW_AT_type = ref4 (typed-view / U4 / void chain) -- One row per DIE kind build_inserted_children emits. -- attrs list form + the value key filled from the atom / registry / local table. ---- @type table -local DIE_SCHEMA = { +local DIE_SCHEMA = { ---@type table base_type = { abbrev = ABBREV_BASE_TYPE, attrs = { @@ -219,88 +185,54 @@ local DIE_SCHEMA = { } -- DWARF5 §7.7.3 loclist opcodes. ---- @type integer -local DW_LLE_end_of_list = 0x00 ---- @type integer -local DW_LLE_start_length = 0x08 ---- @type integer -local DW_OP_reg0 = 0x50 -- base reg op; regN = 0x50 + N ---- @type integer -local DW_OP_breg0 = 0x70 -- base breg op; bregN = 0x70 + N (SLEB offset) ---- @type integer -local DW_OP_piece = 0x93 ---- @type integer -local MIPS_LOAD_DELAY_BYTES = 0x08 -- 1 load word + 1 BD-slot word +local DW_LLE_end_of_list = 0x00 ---@type integer +local DW_LLE_start_length = 0x08 ---@type integer +local DW_OP_reg0 = 0x50 ---@type integer -- base reg op; regN = 0x50 + N +local DW_OP_breg0 = 0x70 ---@type integer -- base breg op; bregN = 0x70 + N (SLEB offset) +local DW_OP_piece = 0x93 ---@type integer +local MIPS_LOAD_DELAY_BYTES = 0x08 ---@type integer -- 1 load word + 1 BD-slot word -- DIE children-list terminator: each DIE that has children ends with a single 0 byte (DWARF5 §7.5.3). -- This is NOT the same as DW_LLE_end_of_list despite sharing the value 0x00 — different spec sections. ---- @type integer -local DIE_CHILDREN_TERMINATOR = 0x00 +local DIE_CHILDREN_TERMINATOR = 0x00 ---@type integer -- Field/piece byte sizes for the typed-view piece chains. -- All Binds_* struct fields are U4 (sizeof(uint32_t) on MIPS32 = 4 bytes). ---- @type integer -local U4_BYTE_SIZE = 4 +local U4_BYTE_SIZE = 4 ---@type integer -- DWARF3/4/5 opcodes / attributes / forms (for the .debug_info synth). -- DW_TAG values are stable across DWARF3-5 per the standard's Table 7.1; -- gcc emits DW_TAG_structure_type=0x13 + DW_TAG_base_type=0x24 even in DWARF5-versioned CUs, so we match those exact byte values. ---- @type integer -local DW_TAG_compile_unit = 0x11 ---- @type integer -local DW_TAG_subprogram = 0x2E ---- @type integer -local DW_TAG_variable = 0x34 ---- @type integer -local DW_TAG_structure_type = 0x13 ---- @type integer -local DW_TAG_member = 0x0D ---- @type integer -local DW_TAG_base_type = 0x24 ---- @type integer -local DW_TAG_pointer_type = 0x0F +local DW_TAG_compile_unit = 0x11 ---@type integer +local DW_TAG_subprogram = 0x2E ---@type integer +local DW_TAG_variable = 0x34 ---@type integer +local DW_TAG_structure_type = 0x13 ---@type integer +local DW_TAG_member = 0x0D ---@type integer +local DW_TAG_base_type = 0x24 ---@type integer +local DW_TAG_pointer_type = 0x0F ---@type integer -- Component step-into. ---- @type integer -local DW_TAG_inlined_subroutine = 0x1D +local DW_TAG_inlined_subroutine = 0x1D ---@type integer ---- @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 -- DWARF5 §7.7.1: DW_AT_encoding for the DW_ATE_unsigned base type ---- @type integer -local DW_AT_data_member_location = 0x38 ---- @type integer -local DW_AT_type = 0x49 ---- @type integer -local DW_AT_linkage_name = 0x6E -- DWARF5 §7.7.1: DW_AT_linkage_name with DW_FORM_string ---- @type integer -local DW_AT_external = 0x3F -- marks a variable/function as externally visible +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 -- Inlined_subroutine + abstract_origin attributes. ---- @type integer -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 -- DWARF5 §7.7.1: DW_AT_inline (used by abstract subprogram for the mac_X() components) +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) -- 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. ---- @type integer -local DW_AT_decl_file = 0x3A -- DWARF5 §7.7.1: DW_AT_decl_file (1-based file index into the CU's file table) ---- @type integer -local DW_AT_decl_line = 0x3B -- 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. @@ -315,8 +247,7 @@ local DW_AT_decl_line = 0x3B -- DWARF5 §7.7.1: DW_AT_decl_line --- @return nil local function init_file_index_lookup(elf_path) if not elf_path or elf_path == "" then return end - --- @type table|nil, table|nil, table|nil - local b2i, _basenames, paths = elf_dwarf.read_line_unit_file_table(elf_path) + local b2i, _basenames, paths = elf_dwarf.read_line_unit_file_table(elf_path) ---@type table|nil, table|nil, table|nil if type(b2i) ~= "table" or type(paths) ~= "table" then io.stderr:write("[dwarf_injection] read_line_unit_file_table returned no file table for: " .. tostring(elf_path) .. "\n") return @@ -325,8 +256,7 @@ local function init_file_index_lookup(elf_path) _file_path_by_index = paths -- Pick any valid index for the opaque-row fallbacks at lines 466 + 570 -- (both sites legitimately want "any file index"; gdb resolves whatever index we emit to whatever that file's line happens to be). - --- @type integer|nil - for idx in pairs(paths) do + for idx in pairs(paths) do ---@type integer|nil _default_atom_source_index = idx break end @@ -351,17 +281,13 @@ local function resolve_provenance_file_index(path) error("[dwarf_injection] resolve_provenance_file_index: empty path") end -- Normalize backslashes → forward slashes (paths arrive with mixed separators from the provenance file). - --- @type string - local normalized = path:gsub("\\", "/") + local normalized = path:gsub("\\", "/") ---@type string -- Take the last path component (the basename). - --- @type string - local basename = normalized:match("([^/]+)$") or normalized - --- @type integer|nil - local idx = _file_index_by_basename[basename] + local basename = normalized:match("([^/]+)$") or normalized ---@type string + 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). - --- @type integer, string - for i, p in pairs(_file_path_by_index) do + for i, p in pairs(_file_path_by_index) do ---@type integer, string if p and p:gsub("\\", "/") == normalized then return i end end -- File is in the corpus but gcc omitted it from the .debug_line file table (data-only content). @@ -371,29 +297,19 @@ local function resolve_provenance_file_index(path) return 0 end ---- @type integer -local DW_FORM_addr = 0x01 ---- @type integer -local DW_FORM_data1 = 0x0B ---- @type integer -local DW_FORM_string = 0x08 -- inline null-terminated ---- @type integer -local DW_FORM_strp = 0x0E -- 4-byte offset into .debug_str ---- @type integer -local DW_FORM_exprloc = 0x18 -- length-prefixed (ULEB128) DW_OP bytes ---- @type integer -local DW_FORM_ref4 = 0x13 -- 4-byte offset within the same .debug_info CU ---- @type integer -local DW_FORM_udata = 0x0F -- ULEB128 (DW_AT_byte_size for struct_type, DW_AT_data_member_location for member) ---- @type integer -local DW_FORM_implicit_const = 0x21 -- DWARF5 §7.5.6: abbrev declaration carries a SLEB constant (used by the abbrev-table walker) ---- @type integer -local DW_FORM_sec_offset = 0x17 -- 4-byte section-relative offset (into .debug_loclists / .debug_rnglists) +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) -- DW_OP_reg0 + DW_OP_piece are declared above (lines 114-116) alongside the other DWARF5 §7.7.3 loclist opcodes. ---- @type integer -local DW_ATE_unsigned = 0x07 -- DWARF5 §7.8.1: DW_ATE_unsigned (used for U4 base type) +local DW_ATE_unsigned = 0x07 ---@type integer -- DWARF5 §7.8.1: DW_ATE_unsigned (used for U4 base type) -- (DW_LANG_Mips_Assembler = 0x8001 was used in the, but we want this CU to look like a C TU so VSCode's Variables pane treats it as code.) -- No DW_AT_language attribute is emitted (see build_debug_info_section's abbrev 100). @@ -420,49 +336,32 @@ local function build_debug_loclists_section(atom_table, registries) -- R_TapePtr comes from the merged register_alias_registry (only present if the user opted it in via `#define atom_reg` in lottes_tape.h). -- 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). - --- @type AliasEntry|nil - local tape_alias_entry = registries.register_alias_registry and registries.register_alias_registry["R_TapePtr"] - --- @type integer - local tape_reg = tape_alias_entry and tape_alias_entry.code - --- @type string[] - local parts = {} - --- @type integer, DwarfAtom - for _, atom in ipairs(atom_table) do + 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 if atom.rbind and tape_reg then - --- @type TypeField[] - local fields = atom.rbind.fields or {} - --- @type DwarfLoadPair[] - local regs = atom.rbind.regs or {} - --- @type integer - 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 string[] - local tape_pieces = {} - --- @type integer, TypeField - for _, f in ipairs(fields) do - --- @type integer - local offset = f.offset or 0 - --- @type string - local offset_sleb = elf_dwarf.sleb128(offset) + 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 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 - --- @type string - local tape_expr = table.concat(tape_pieces) - --- @type string[] - local gpr_pieces = {} - --- @type integer, DwarfLoadPair - for _, pair in ipairs(regs) do + local tape_expr = table.concat(tape_pieces) ---@type string + 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)) end - --- @type string - local gpr_expr = table.concat(gpr_pieces) + local gpr_expr = table.concat(gpr_pieces) ---@type string parts[#parts + 1] = string.char(DW_LLE_start_length) .. elf_dwarf.write_u32_le(atom.addr) .. uleb128(#tape_expr) @@ -477,14 +376,10 @@ 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). - --- @type integer - local LOCLIST_HEADER_SIZE = 12 - --- @type string - local body = table.concat(parts) - --- @type integer - local unit_length = LOCLIST_HEADER_SIZE - 4 + #body -- -4 because unit_length excludes itself - --- @type string - local header = elf_dwarf.write_u32_le(unit_length) + 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 .. string.char(0) -- segment_size @@ -506,38 +401,27 @@ 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) - --- @type integer - local LOCLIST_ENTRY_HEADER_SIZE = 1 + 4 + 1 -- DW_LLE_start_length(1) + addr(4) + uleb_length(1) - --- @type table -- bag - local offsets = {} + 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. - --- @type integer - local cursor = 4 + 2 + 1 + 1 + 4 -- = 12 - --- @type integer, DwarfAtom - for _, atom in ipairs(atom_table) do + 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 - --- @type integer - local n_fields = #atom.rbind.fields + local n_fields = #atom.rbind.fields ---@type integer -- Sum the actual size of each tape piece based on the field's offset (not an assumed constant); this expression mirrors what build_debug_loclists_section produces: -- 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) - --- @type integer - local tape_pieces_size = 0 - --- @type integer, TypeField - for _, f in ipairs(atom.rbind.fields or {}) do + 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 - --- @type integer - local gpr_pieces_size = n_fields * 3 -- each gpr piece: DW_OP_regN(1) + DW_OP_piece(1) + uleb(4)(1) = 3 bytes - --- @type integer - 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 -- +1 for DW_LLE_end_of_list + 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 cursor = cursor + body_len end end @@ -545,16 +429,13 @@ local function compute_loclists_offsets(atom_table) end -- Default name for the synthetic CU (so VSCode lists it as a known source). ---- @type string -local DEFAULT_CU_NAME = "tape_atom_locals" ---- @type string -local DEFAULT_CU_COMP_DIR = "." +local DEFAULT_CU_NAME = "tape_atom_locals" ---@type string +local DEFAULT_CU_COMP_DIR = "." ---@type string -- SECTION_WRITERS owns the .bin output path templates. -- Default basename if not provided via ctx. ---- @type string -local DEFAULT_BASENAME = "hello_gte" +local DEFAULT_BASENAME = "hello_gte" ---@type string -- ════════════════════════════════════════════════════════════════════════════ -- Type declarations @@ -704,10 +585,8 @@ 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. - --- @type AtomInfoEntry[] - local atom_infos_list = {} - --- @type integer, AtomInfoEntry - for _, ai in ipairs((corpus and corpus.atom_infos) or {}) do + 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 return { @@ -781,10 +660,8 @@ 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 - --- @type string - local addr_bytes = elf_dwarf.write_u32_le(addr) - --- @type string - local sub_size = string.char(DW_LNE_set_address) .. addr_bytes + 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 --- @return string @@ -836,20 +713,14 @@ 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. - --- @type InvocationRecord[] - local invs = atom.invocations or {} - --- @type table -- bag - local innermost_idx = {} - --- @type table -- bag - local ancestry_idx = {} - --- @type integer - for idx = 1, #atom.entries do + 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 innermost_idx[idx] = nil ancestry_idx[idx] = {} - --- @type InvocationRecord[] - local active = {} - --- @type integer, InvocationRecord - for _, inv in ipairs(invs) do + 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 end @@ -875,16 +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). - --- @type table -- bag - local body_first_line_of = {} - --- @type integer, InvocationRecord - for _, top_inv in ipairs(invs) do - --- @type integer - local earliest_nested_call_line = nil - --- @type integer - local earliest_nested_start_pos = nil - --- @type integer, InvocationRecord - for _, cand in ipairs(invs) do + 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 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 @@ -901,19 +767,15 @@ local function build_atom_sequence(atom) end end - --- @type string[] - local parts = { + local parts = { ---@type string[] set_address(atom.addr), -- 7 bytes: marker + size + sub + addr; PC := atom.addr } -- Source state tracker: emits set_file + advance_line only on transitions, keeps bytes minimal. -- Both fields stay in sync with what we emit, so we duplicate no set_file and skip no state-change emit. - --- @type integer|nil - local cur_file = nil -- line-state.file_idx (nil = uninitialized) - --- @type integer - local cur_line = 1 -- line-state.line starts at 1 (per DWARF spec) - --- @type boolean - local is_stmt = true -- main line unit default_is_stmt; every sequence ends restored + local cur_file = nil ---@type integer|nil -- line-state.file_idx (nil = uninitialized) + local cur_line = 1 ---@type integer -- line-state.line starts at 1 (per DWARF spec) + local is_stmt = true ---@type boolean -- main line unit default_is_stmt; every sequence ends restored --- @param want_stmt boolean --- @return nil @@ -943,14 +805,11 @@ local function build_atom_sequence(atom) parts[#parts + 1] = copy_op() end - --- @type integer - local call_file_idx = resolve_provenance_file_index(atom.src_path) + local call_file_idx = resolve_provenance_file_index(atom.src_path) ---@type integer -- --- Atom entry (idx 1) ------------------------------------------------- - --- @type DwarfAtomWord - local entry_1 = atom.entries[1] - --- @type InvocationRecord[] - local entry_1_ancestry = ancestry_idx[1] + 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) -- a body_lines[1] row for every active ancestor. For a non-nested invocation this is just the one pair; @@ -969,14 +828,12 @@ local function build_atom_sequence(atom) -- (statement iff unmarked; suppressed for marked outermost). -- The body_lines[1] row references body_first_line_of[anc.id] (= the body's first content line in the parent's source), -- NOT anc.body_lines[1] (= the line of the first WORD, which is wrong when the outer's body starts with a nested call). - --- @type integer, InvocationRecord - for ai, anc in ipairs(entry_1_ancestry) do + for ai, anc in ipairs(entry_1_ancestry) do ---@type integer, InvocationRecord assert(anc.body_lines, "missing body_lines: emitter did not run emission-model") assert(anc.body_lines[1] ~= nil, "dwarf_injection: body_lines[1] missing on first-word entry for inv=" .. tostring(anc.component_name)) assert(anc.call_path and anc.call_path ~= "", "dwarf_injection: inv.call_path is missing on invocation " .. tostring(anc.component_name) .. "; emitter did not run emission-model.") emit_row(resolve_provenance_file_index(anc.call_path), anc.call_line, true) - --- @type boolean - local is_outermost = (ai == 1) + local is_outermost = (ai == 1) ---@type boolean if not (is_outermost and anc.debug_skip) then emit_row(resolve_provenance_file_index(anc.def_path), body_first_line_of[anc.id] or anc.body_lines[1], not anc.debug_skip) end @@ -984,12 +841,9 @@ local function build_atom_sequence(atom) end -- --- Subsequent entries (idx 2..N) -------------------------------------- - --- @type integer|nil - for idx = 2, #atom.entries do - --- @type DwarfAtomWord - local entry = atom.entries[idx] - --- @type InvocationRecord - local inv = innermost_idx[idx] + for idx = 2, #atom.entries do ---@type integer|nil + local entry = atom.entries[idx] ---@type DwarfAtomWord + local inv = innermost_idx[idx] ---@type InvocationRecord -- Advance PC by 1 .word (4 bytes on MIPS). parts[#parts + 1] = advance_pc(MIPS_BYTES_PER_WORD) @@ -1007,16 +861,13 @@ local function build_atom_sequence(atom) -- NOT anc.body_lines[1] (= the line of the first WORD, which is wrong when the outer's body starts with a nested call: -- gdb 12.1 picks the displayed line as the LAST row at the same PC in byte-stream order, -- so the disc=1 row's value matters for what's shown when stepping into the nested case). - --- @type InvocationRecord[] - local ancestry = ancestry_idx[idx] - --- @type integer, InvocationRecord - for ai, anc in ipairs(ancestry) do + local ancestry = ancestry_idx[idx] ---@type InvocationRecord[] + for ai, anc in ipairs(ancestry) do ---@type integer, InvocationRecord assert(anc.body_lines, "missing body_lines: emitter did not run emission-model") assert(anc.body_lines[1] ~= nil, string.format("missing body_lines[1] for inv=%s start_pos=%d len=%d", anc.component_name, anc.start_pos, #(anc.body_lines or {}))) assert(anc.call_path and anc.call_path ~= "", "dwarf_injection: inv.call_path is missing on invocation " .. tostring(anc.component_name) .. "; emitter did not run emission-model.") emit_row(resolve_provenance_file_index(anc.call_path), anc.call_line, true) - --- @type boolean - local is_outermost = (ai == 1) + local is_outermost = (ai == 1) ---@type boolean if not (is_outermost and anc.debug_skip) then emit_row(resolve_provenance_file_index(anc.def_path), body_first_line_of[anc.id] or anc.body_lines[1], not anc.debug_skip) end @@ -1029,8 +880,7 @@ local function build_atom_sequence(atom) -- The previous `want = not marked_idx[idx]` (which suppressed ALL body rows when any ancestor was marked) is replaced by the per-invocation predicate. -- Marked invocations emit non-statement body rows at every body word; unmarked invocations emit statement body rows. assert(inv.body_lines, "missing body_lines: emitter did not run emission-model") - --- @type integer - local words_into = idx - inv.start_pos + local words_into = idx - inv.start_pos ---@type integer assert(inv.body_lines[words_into] ~= nil, string.format("missing body_lines[%d] for inv=%s start_pos=%d len=%d idx=%d", words_into, inv.component_name, inv.start_pos, #(inv.body_lines or {}), idx)) emit_row(resolve_provenance_file_index(inv.def_path), inv.body_lines[words_into], not inv.debug_skip) else @@ -1077,8 +927,7 @@ end --- @return DwarfAtom[] -- List of {name, addr, size_bytes, words, entries, invocations, debug_skip?} local function build_atom_table(corpus, addrs) -- Cross-ref: keep only atoms present in BOTH the nm symbol table AND `corpus.atoms_by_name`. Output is sorted by ascending addr. - --- @type table -- bag - local atoms_by_name = corpus.atoms_by_name or {} + local atoms_by_name = corpus.atoms_by_name or {} ---@type table -- bag -- Per-atom ingest. Returns nil if the atom is absent from the corpus; the caller skips it via the `if atom then ...` guard. -- `src_path` is the absolute source path that declared this atom; the build_atom_table iteration below threads `src.path` through. @@ -1088,23 +937,17 @@ local function build_atom_table(corpus, addrs) --- @param src_path string --- @return DwarfAtom|nil local function ingest_atom(name, info, src_path) - --- @type AtomEntry|nil - local atom_record = atoms_by_name[name] + local atom_record = atoms_by_name[name] ---@type AtomEntry|nil if not atom_record then return nil end - --- @type AtomPaths - local paths = atom_record.paths or {} - --- @type WordEvent[] - local word_events = paths.word_events or {} - --- @type InvocationRecord[] - local invocations_proj = paths.invocations or {} + local paths = atom_record.paths or {} ---@type AtomPaths + local word_events = paths.word_events or {} ---@type WordEvent[] + local invocations_proj = paths.invocations or {} ---@type InvocationRecord[] -- 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) - --- @type DwarfAtomWord[] - local entries = {} - --- @type integer, WordEvent - for idx, ev in ipairs(word_events) do + local entries = {} ---@type DwarfAtomWord[] + for idx, ev in ipairs(word_events) do ---@type integer, WordEvent entries[#entries + 1] = { pos = ev.i or (idx - 1), line = ev.call_line or 0, @@ -1112,8 +955,7 @@ local function build_atom_table(corpus, addrs) } end -- Whole-atom skip is read from the atom declaration record; the scanner owns it, no parallel lookup table. - --- @type DwarfAtom - local atom = { + local atom = { ---@type DwarfAtom name = name, addr = info[1], size_bytes = info[2], @@ -1131,8 +973,7 @@ local function build_atom_table(corpus, addrs) -- (which differs by the count of `invoke_begin`/`invoke_end`/marker items between this call and the previous one). -- Using `start_word` would shift every `words_into` lookup by the marker count and break the call-site + body row pairing at the first word of every invocation. atom.invocations = invocations_proj - --- @type integer, InvocationRecord - for _, inv in ipairs(atom.invocations) do + for _, inv in ipairs(atom.invocations) do ---@type integer, InvocationRecord -- `debug_skip` flag is already stamped by `duffle.emit_invoke_begin` from `corpus.components[name].debug_skip`. -- Normalize to boolean for downstream dispatch. A missing value is a corpus-plumbing bug; the fail-loud error was raised at the construction site. inv.debug_skip = inv.debug_skip == true @@ -1144,32 +985,23 @@ local function build_atom_table(corpus, addrs) return atom end - --- @type DwarfAtom[] - local out = {} + local out = {} ---@type DwarfAtom[] -- Walk every source's atom list (which preserves source order + per-source src_path). -- 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). - --- @type integer, SourceFile - for _, src in ipairs((corpus and corpus.source_order) or {}) do - --- @type string - local src_path = src.path or "" - --- @type integer, AtomEntry - for _, atom_rec in ipairs(((src.scan or {}).atoms) or {}) do - --- @type NmAddr|nil - local info = addrs[atom_rec.name or atom_rec.raw_name] + for _, src in ipairs((corpus and corpus.source_order) or {}) do ---@type integer, SourceFile + 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 - --- @type DwarfAtom|nil - local atom = ingest_atom(atom_rec.name or atom_rec.raw_name, info, src_path) + local atom = ingest_atom(atom_rec.name or atom_rec.raw_name, info, src_path) ---@type DwarfAtom|nil if atom then out[#out + 1] = atom end end end - --- @type integer, AtomEntry - for _, atom_rec in ipairs(((src.scan or {}).raw_atoms) or {}) do - --- @type NmAddr|nil - local info = addrs[atom_rec.name or atom_rec.raw_name] + for _, atom_rec in ipairs(((src.scan or {}).raw_atoms) or {}) do ---@type integer, AtomEntry + local info = addrs[atom_rec.name or atom_rec.raw_name] ---@type NmAddr|nil if info then - --- @type DwarfAtom|nil - local atom = ingest_atom(atom_rec.name or atom_rec.raw_name, info, src_path) + local atom = ingest_atom(atom_rec.name or atom_rec.raw_name, info, src_path) ---@type DwarfAtom|nil if atom then out[#out + 1] = atom end end end @@ -1186,12 +1018,9 @@ end --- @param atom_table DwarfAtom[] --- @return table local function collect_component_defs(atom_table) - --- @type table -- bag - local out = {} - --- @type integer, DwarfAtom - for _, atom in ipairs(atom_table) do - --- @type integer, InvocationRecord - for _, inv in ipairs(atom.invocations or {}) do + 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 out[inv.component_name] = { name = inv.component_name, @@ -1236,36 +1065,25 @@ 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) - --- @type DwarfLoadPair[] - local pairs = {} - --- @type table -- bag - local reg_index_by_name = (registries and registries.register_alias_registry) or {} + 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. - --- @type string - local load_pattern = "^(load_word|load_half|load_half_u|load_byte|load_byte_u|gte_lw|gte_lwc2)%s*%((.*)%)$" - --- @type integer, BodyToken - for _, t in ipairs(body_tokens or {}) do - --- @type string - local tok = duffle.trim(t.tok or "") - --- @type string|nil, string|nil - local kind, inner = tok:match(load_pattern) + 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 + local tok = duffle.trim(t.tok or "") ---@type string + local kind, inner = tok:match(load_pattern) ---@type string|nil, string|nil if kind then - --- @type string[] - local args = duffle.split_top_level_commas(inner) + local args = duffle.split_top_level_commas(inner) ---@type string[] -- Expected shape for an rbind piece-chain load: (R_, R_TapePtr, O_(Binds_, FieldName)) -- The second arg MUST be R_TapePtr — loads from other bases (e.g. `load_byte_u(R_RawStatus, R_PadRaw, 0)`) -- are field-derivative loads that read already-bound tape values; they're NOT a new piece-chain. if #args >= 3 and duffle.trim(args[2]) == "R_TapePtr" then - --- @type string - local reg_name = duffle.trim(args[1]) - --- @type string - local third_arg = duffle.trim(args[3]) + local reg_name = duffle.trim(args[1]) ---@type string + local third_arg = duffle.trim(args[3]) ---@type string -- Match O_(Binds_, FieldName) - --- @type string|nil, string|nil - local b, f = third_arg:match("^O_%((Binds_[%w_]+)%s*,%s*(.-)%s*%)$") - --- @type AliasEntry|nil - local alias_entry = reg_index_by_name[reg_name] + 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 if b and b == binds_name and alias_entry and alias_entry.code then pairs[#pairs + 1] = { reg = alias_entry.code, @@ -1294,30 +1112,23 @@ end --- @return table, table local function parse_rbind_atoms(corpus, atom_table, registries) registries = registries or {} - --- @type table -- bag - local rbind_atoms = {} - --- @type table -- bag - local rbind_structs = {} + local rbind_atoms = {} ---@type table -- bag + local rbind_structs = {} ---@type table -- bag -- 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. - --- @type table -- bag - local binds_by_name = {} - --- @type integer, SourceFile - for _, src in ipairs((corpus and corpus.source_order) or {}) do - --- @type SourceScan|nil - local scan = src.scan + 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 - --- @type integer, BindsEntry - for _, b in ipairs(scan.binds or {}) do + for _, b in ipairs(scan.binds or {}) do ---@type integer, BindsEntry binds_by_name[b.name] = b end end end - --- @type string, BindsEntry - for binds_name, b in pairs(binds_by_name) do + for binds_name, b in pairs(binds_by_name) do ---@type string, BindsEntry if b.fields and b.bytes then rbind_structs[binds_name] = { bytes = b.bytes, @@ -1328,44 +1139,32 @@ 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. - --- @type table -- bag - local body_tokens_by_atom = {} - --- @type integer, SourceFile - for _, src in ipairs((corpus and corpus.source_order) or {}) do - --- @type SourceScan|nil - local scan = src.scan + 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 - --- @type integer, AtomEntry - for _, atom in ipairs(scan.atoms or {}) do + for _, atom in ipairs(scan.atoms or {}) do ---@type integer, AtomEntry body_tokens_by_atom[atom.name] = atom.body_tokens end end end - --- @type table -- bag - local ai_by_atom = {} - --- @type integer, SourceFile - for _, src in ipairs((corpus and corpus.source_order) or {}) do - --- @type SourceScan|nil - local scan = src.scan + 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 - --- @type integer, AtomInfoEntry - for _, ai in ipairs(scan.atom_infos or {}) do + for _, ai in ipairs(scan.atom_infos or {}) do ---@type integer, AtomInfoEntry ai_by_atom[ai.atom_name] = ai end end end - --- @type string, AtomInfoEntry - for atom_name, ai in pairs(ai_by_atom) do + for atom_name, ai in pairs(ai_by_atom) do ---@type string, AtomInfoEntry if ai.binds then - --- @type DwarfRbindStruct|nil - local struct = rbind_structs[ai.binds] - --- @type BodyToken[]|nil - local body_toks = body_tokens_by_atom[atom_name] + 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 - --- @type DwarfLoadPair[] - local pairs = parse_body_load_pairs(body_toks, ai.binds, registries) + local pairs = parse_body_load_pairs(body_toks, ai.binds, registries) ---@type DwarfLoadPair[] if #pairs > 0 then rbind_atoms[atom_name] = { binds = ai.binds, @@ -1381,8 +1180,7 @@ local function parse_rbind_atoms(corpus, atom_table, registries) end -- Mark rbind atoms in the main atom_table. - --- @type integer, DwarfAtom - for _, atom in ipairs(atom_table) do + for _, atom in ipairs(atom_table) do ---@type integer, DwarfAtom if rbind_atoms[atom.name] then atom.rbind = rbind_atoms[atom.name] end @@ -1410,34 +1208,26 @@ local function build_dwarf_line_section(existing, atom_table) if #atom_table == 0 then return existing end -- Build the sequences. - --- @type string[] - local sequences = {} - --- @type integer, DwarfAtom - for _, atom in ipairs(atom_table) do sequences[#sequences + 1] = build_atom_sequence(atom) end - --- @type string - local appended = table.concat(sequences) + 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 -- 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). - --- @type integer, integer|nil, integer|nil, integer|nil - local unit_pos, last_pos, last_length, last_end = 0, nil, nil, nil + local unit_pos, last_pos, last_length, last_end = 0, nil, nil, nil ---@type integer, integer|nil, integer|nil, integer|nil while unit_pos < #existing do if unit_pos + 4 > #existing then return existing end - --- @type integer - local unit_length = elf_dwarf.read_u32_le(existing, unit_pos) + local unit_length = elf_dwarf.read_u32_le(existing, unit_pos) ---@type integer if unit_length == elf_dwarf.dw_dwarf32_terminator then return existing end - --- @type integer - local unit_end_excl = unit_pos + 4 + unit_length + local unit_end_excl = unit_pos + 4 + unit_length ---@type integer if unit_end_excl > #existing then return existing end last_pos, last_length, last_end = unit_pos, unit_length, unit_end_excl unit_pos = unit_end_excl end if unit_pos ~= #existing or not last_pos then return existing end - --- @type integer - local new_length = last_length + #appended - --- @type string - local new_length_bytes = elf_dwarf.write_u32_le(new_length) + 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) .. new_length_bytes @@ -1482,47 +1272,35 @@ 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. - --- @type string[] - local result = {} - --- @type integer - local i = 0 -- zero-based wire offset - --- @type boolean - local is_last_unit = false + local result = {} ---@type string[] + local i = 0 ---@type integer -- zero-based wire offset + local is_last_unit = false ---@type boolean while i < #existing do -- Read this unit's length. - --- @type integer - local ul = elf_dwarf.read_u32_le(existing, i) + local ul = elf_dwarf.read_u32_le(existing, i) ---@type integer if ul == elf_dwarf.dw_dwarf32_terminator then -- DWARF64 marker - not supported. io.stderr:write("[dwarf_injection] WARN: .debug_aranges contains a DWARF64 marker (0xFFFFFFFF); the 64-bit extension is not supported by this metaprogram; passing through unchanged\n") return existing end - --- @type integer - local unit_start = i - --- @type integer - local unit_end_excl = i + 4 + ul + 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. - --- @type integer - local added_bytes = #atom_table * elf_dwarf.DWARF4_ARANGES.entry_size - --- @type integer - local new_ul = ul + added_bytes - --- @type string - local new_ul_bytes = elf_dwarf.write_u32_le(new_ul) + 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 -- 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. - --- @type integer, DwarfAtom - for _, atom in ipairs(atom_table) do - --- @type integer - local a = atom.addr - --- @type integer - local size = atom.size_bytes + for _, atom in ipairs(atom_table) do ---@type integer, DwarfAtom + 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 -- Append a new terminator. @@ -1559,16 +1337,11 @@ 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 - --- @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) + 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 or version ~= elf_dwarf.DWARF5_RNGLISTS.version_expected @@ -1579,20 +1352,15 @@ local function build_dwarf_rnglists_section(existing, atom_table) return existing end - --- @type string[] - local entries = {} - --- @type integer, DwarfAtom - for _, atom in ipairs(atom_table) do + 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 - --- @type string - local appended = table.concat(entries) - --- @type integer - local new_length = unit_length + #appended - --- @type string - local new_length_bytes = elf_dwarf.write_u32_le(new_length) + 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 .. existing:sub(5, #existing - 1) @@ -1622,29 +1390,20 @@ 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) - --- @type string[] - local op_bytes = {} - --- @type table -- bag - local field_offset_by_name = {} - --- @type integer, TypeField - for _, f in ipairs(rbind.fields) do + 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 - --- @type integer - local next_offset = rbind.bytes - --- @type integer - for i = #rbind.regs, 1, -1 do -- walk backwards to know each piece's size - --- @type DwarfLoadPair - local pair = rbind.regs[i] - --- @type integer - local off = field_offset_by_name[pair.field] or 0 - --- @type integer - local size + 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 + local off = field_offset_by_name[pair.field] or 0 ---@type integer + local size ---@type integer if i == #rbind.regs then size = next_offset - off else - --- @type integer - local next_off = field_offset_by_name[rbind.regs[i + 1].field] + local next_off = field_offset_by_name[rbind.regs[i + 1].field] ---@type integer if not next_off then -- Defensive: a load_word references a field not in the Binds_X struct. -- Fall back to struct.bytes so the piece chain stays well-formed. @@ -1657,12 +1416,9 @@ local function piece_chain_exprloc(rbind) next_offset = off end -- We built it back-to-front; reverse it. - --- @type string[] - local rev = {} - --- @type integer - for i = #op_bytes, 1, -1 do rev[#rev + 1] = op_bytes[i] end - --- @type string - local op = table.concat(rev) + 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 return uleb128(#op) .. op end @@ -1684,14 +1440,10 @@ end -- "math.floor(/16) instead of bit.rshift for LuaJIT 2.1 compat" comment at line 352). -- DWARF5 compile-unit header constants. ---- @type integer -local DW_VERSION_5 = 5 ---- @type integer -local DW_UT_compile = 0x01 ---- @type integer -local DWARF32_TERMINATOR = 0xFFFFFFFF -- sentinel for DWARF64 marker ---- @type integer -local CU_HEADER_SIZE = 12 -- 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. @@ -1712,22 +1464,16 @@ local CU_HEADER_SIZE = 12 -- 4 + 2 + 1 + 1 + 4 --- @param existing string -- the .debug_info section bytes --- @return integer|nil, integer|nil, integer|nil local function find_main_cu_layout(existing) - --- @type integer - local buf_len = #existing + local buf_len = #existing ---@type integer if buf_len < CU_HEADER_SIZE then return nil end - --- @type integer - local pos = 0 - --- @type integer - local main_cu_start = nil - --- @type integer - local main_cu_end_excl = nil + 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 - --- @type integer - local unit_length = elf_dwarf.read_u32_le(existing, pos) + local unit_length = elf_dwarf.read_u32_le(existing, pos) ---@type integer if unit_length == DWARF32_TERMINATOR then return nil end - --- @type integer - local unit_end_excl = pos + 4 + unit_length + local unit_end_excl = pos + 4 + unit_length ---@type integer if unit_end_excl > buf_len then return nil end main_cu_start = pos main_cu_end_excl = unit_end_excl @@ -1743,23 +1489,17 @@ local function find_main_cu_layout(existing) -- [6] unit_type -- [7] address_size -- [8..11] debug_abbrev_offset - --- @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) + 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 end -- Final byte of the main CU must be the root children-terminator (0). - --- @type integer - local final_pos = main_cu_end_excl - 1 + local final_pos = main_cu_end_excl - 1 ---@type integer if final_pos >= buf_len or existing:byte(final_pos + 1) ~= 0 then return nil end return main_cu_start, main_cu_end_excl, abbrev_off @@ -1821,8 +1561,7 @@ local function build_new_abbrev() --- @param attrs string --- @return string local function abbrev(code, tag, has_children, attrs) - --- @type integer - local children = has_children and 0x01 or 0x00 -- DW_CHILDREN_yes / no + local children = has_children and 0x01 or 0x00 ---@type integer -- DW_CHILDREN_yes / no return uleb128(code) .. uleb128(tag) .. string.char(children) @@ -1830,21 +1569,18 @@ local function build_new_abbrev() .. string.char(0x00, 0x00) -- end of attr list (2 zeros) end - --- @type integer - local abbrev_cu = abbrev(ABBREV_CU, DW_TAG_compile_unit, true, -- DW_CHILDREN_yes + local abbrev_cu = abbrev(ABBREV_CU, DW_TAG_compile_unit, true, ---@type integer -- DW_CHILDREN_yes attr( DW_AT_name, DW_FORM_strp) .. attr(DW_AT_comp_dir, DW_FORM_strp) .. attr(DW_AT_language, DW_FORM_data1)) - --- @type integer - local abbrev_subprogram = abbrev(ABBREV_SUBPROGRAM, DW_TAG_subprogram, true, -- DW_CHILDREN_yes + local abbrev_subprogram = abbrev(ABBREV_SUBPROGRAM, DW_TAG_subprogram, true, ---@type integer -- DW_CHILDREN_yes attr( DW_AT_name, DW_FORM_string) .. attr(DW_AT_low_pc, DW_FORM_addr) .. attr(DW_AT_high_pc, DW_FORM_addr) .. attr(DW_AT_linkage_name, DW_FORM_string)) -- equals DW_AT_name; gdb resolves the subprogram, not the gcc global array - --- @type integer - local abbrev_variable = abbrev(ABBREV_VARIABLE, DW_TAG_variable, false, -- DW_CHILDREN_no + local abbrev_variable = abbrev(ABBREV_VARIABLE, DW_TAG_variable, false, ---@type integer -- DW_CHILDREN_no attr( DW_AT_name, DW_FORM_string) .. attr(DW_AT_location, DW_FORM_exprloc) .. attr(DW_AT_type, DW_FORM_ref4) @@ -1853,25 +1589,21 @@ local function build_new_abbrev() -- rbind composite. -- DW_FORM_udata (0x0F, ULEB128) is declared at module scope. For small values (struct byte_size, member offsets) 1 byte is enough; -- we emit ULEB128 anyway for spec compliance with the DWARF abbrev encoding rules. - --- @type integer - local abbrev_struct_type = abbrev(ABBREV_STRUCT_TYPE, DW_TAG_structure_type, true, -- DW_CHILDREN_yes + local abbrev_struct_type = abbrev(ABBREV_STRUCT_TYPE, DW_TAG_structure_type, true, ---@type integer -- DW_CHILDREN_yes attr( DW_AT_name, DW_FORM_string) .. attr(DW_AT_byte_size, DW_FORM_udata)) - --- @type integer - local abbrev_member = abbrev(ABBREV_MEMBER, DW_TAG_member, false, -- DW_CHILDREN_no + local abbrev_member = abbrev(ABBREV_MEMBER, DW_TAG_member, false, ---@type integer -- DW_CHILDREN_no attr( DW_AT_name, DW_FORM_string) .. attr(DW_AT_data_member_location, DW_FORM_udata) .. attr(DW_AT_type, DW_FORM_ref4)) - --- @type integer - local abbrev_bind_var = abbrev(ABBREV_BIND_VAR, DW_TAG_variable, false, -- DW_CHILDREN_no + local abbrev_bind_var = abbrev(ABBREV_BIND_VAR, DW_TAG_variable, false, ---@type integer -- DW_CHILDREN_no attr( DW_AT_name, DW_FORM_string) .. attr(DW_AT_location, DW_FORM_exprloc) .. attr(DW_AT_type, DW_FORM_ref4)) - --- @type integer - local abbrev_base_type = abbrev(ABBREV_BASE_TYPE, DW_TAG_base_type, false, -- DW_CHILDREN_no + local abbrev_base_type = abbrev(ABBREV_BASE_TYPE, DW_TAG_base_type, false, ---@type integer -- DW_CHILDREN_no attr( DW_AT_name, DW_FORM_string) .. attr(DW_AT_byte_size, DW_FORM_data1) .. attr(DW_AT_encoding, DW_FORM_data1)) @@ -1879,16 +1611,14 @@ local function build_new_abbrev() -- Abstract subprograms carry DW_AT_decl_file and DW_AT_decl_line for definition-site resolution, -- even when no inlined_subroutine instance currently maps to it. -- DW_FORM_udata is consistent with the call_file/call_line forms on abbrev 108. - --- @type integer - local abbrev_abstract_subprogram = abbrev(ABBREV_ABSTRACT_SUBPROGRAM, DW_TAG_subprogram, false, -- DW_CHILDREN_no + local abbrev_abstract_subprogram = abbrev(ABBREV_ABSTRACT_SUBPROGRAM, DW_TAG_subprogram, false, ---@type integer -- DW_CHILDREN_no attr( DW_AT_name, DW_FORM_string) .. attr(DW_AT_inline, DW_FORM_data1) .. attr(DW_AT_external, DW_FORM_data1) .. attr(DW_AT_decl_file, DW_FORM_udata) .. attr(DW_AT_decl_line, DW_FORM_udata)) - --- @type integer - local abbrev_inlined_subroutine = abbrev(ABBREV_INLINED_SUBROUTINE, DW_TAG_inlined_subroutine, false, -- DW_CHILDREN_no (emits no per-inlined-instance children; the PC range IS the inlining scope) + local abbrev_inlined_subroutine = abbrev(ABBREV_INLINED_SUBROUTINE, DW_TAG_inlined_subroutine, false, ---@type integer -- DW_CHILDREN_no (emits no per-inlined-instance children; the PC range IS the inlining scope) attr( DW_AT_abstract_origin, DW_FORM_ref4) .. attr(DW_AT_low_pc, DW_FORM_addr) .. attr(DW_AT_high_pc, DW_FORM_addr) @@ -1898,8 +1628,7 @@ local function build_new_abbrev() -- bind_args with DW_FORM_sec_offset → .debug_loclists. -- The .debug_loclists section holds a sequence of DW_LLE entries; -- the first matching entry for a PC describes each field's value (tape memory DW_OP_breg24 or register DW_OP_regN). - --- @type integer - local abbrev_bind_var_loclists = abbrev(ABBREV_BIND_VAR_LOCLIST, DW_TAG_variable, false, -- DW_CHILDREN_no + local abbrev_bind_var_loclists = abbrev(ABBREV_BIND_VAR_LOCLIST, DW_TAG_variable, false, ---@type integer -- DW_CHILDREN_no attr( DW_AT_name, DW_FORM_string) .. attr(DW_AT_location, DW_FORM_sec_offset) .. attr(DW_AT_type, DW_FORM_ref4)) @@ -1907,8 +1636,7 @@ local function build_new_abbrev() -- Typed-view pointer_type: DW_TAG_pointer_type, DW_AT_type = ref4 (no DW_AT_byte_size; the -- chain target carries the byte size via the base_type or struct_type we point at). -- MUST be in the appended table — see ABBREV_TYPED_VIEW_POINTER above. - --- @type integer - local abbrev_typed_view_pointer = abbrev(ABBREV_TYPED_VIEW_POINTER, DW_TAG_pointer_type, false, -- DW_CHILDREN_no + local abbrev_typed_view_pointer = abbrev(ABBREV_TYPED_VIEW_POINTER, DW_TAG_pointer_type, false, ---@type integer -- DW_CHILDREN_no attr(DW_AT_type, DW_FORM_ref4)) return abbrev_cu .. abbrev_subprogram .. abbrev_variable @@ -1944,28 +1672,22 @@ local function build_new_strings(atom_table, registries) registries = registries or {} -- The CU name + comp_dir are the first two strings (offsets 0 and N1). -- Then each unique atom name + each register name follows. - --- @type string[] - local strings = {} - --- @type table -- bag - local map = {} + local strings = {} ---@type string[] + local map = {} ---@type table -- bag -- CU name at offset 0 in the new blob strings[#strings + 1] = DEFAULT_CU_NAME .. "\0" map["__cu_name__"] = 0 - --- @type integer - local cu_name_len = #strings[#strings] + local cu_name_len = #strings[#strings] ---@type integer -- comp_dir at offset cu_name_len strings[#strings + 1] = DEFAULT_CU_COMP_DIR .. "\0" map["__comp_dir__"] = cu_name_len - --- @type integer - local comp_dir_len = #strings[#strings] + local comp_dir_len = #strings[#strings] ---@type integer -- Atom names (one per unique atom) - --- @type integer, DwarfAtom - for _, atom in ipairs(atom_table) do - --- @type string - local name = atom.name + for _, atom in ipairs(atom_table) do ---@type integer, DwarfAtom + local name = atom.name ---@type string if not map[name] then map[name] = #table.concat(strings) strings[#strings + 1] = name .. "\0" @@ -1976,19 +1698,15 @@ 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. - --- @type string[] - local sorted_alias_names = {} - --- @type string, AliasEntry|nil - for r_name, alias in pairs(registries.register_alias_registry or {}) do + 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 end end table.sort(sorted_alias_names) - --- @type integer, string - for _, r_name in ipairs(sorted_alias_names) do - --- @type string - local rr_name = "RR_" .. strip_r_prefix(r_name) + for _, r_name in ipairs(sorted_alias_names) do ---@type integer, string + local rr_name = "RR_" .. strip_r_prefix(r_name) ---@type string if not map[rr_name] then map[rr_name] = #table.concat(strings) strings[#strings + 1] = rr_name .. "\0" @@ -2057,18 +1775,14 @@ 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. - --- @type table -- bag - local by_alias = {} - --- @type string, AliasEntry|nil - for r_name, alias in pairs(registries.register_alias_registry or {}) do + 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 - --- @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 + 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) -- Build a name->atom lookup for fast rbind_atom resolution during the per-atom phase/ctx propagation. @@ -2076,26 +1790,21 @@ 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) - --- @type table -- bag - local m = {} - --- @type integer, AliasEntry|nil - for _, a in ipairs(atoms or {}) do + 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 return m end - --- @type table -- bag - local atom_by_name_global = build_atom_name_index(atom_table) + local atom_by_name_global = build_atom_name_index(atom_table) ---@type table -- bag -- We insert IMMEDIATELY BEFORE the main CU's root children-terminator (the last byte of the main CU). -- The first emitted byte lives at section offset (main_cu_end_excl - 1). - --- @type integer - local insertion_start = main_cu_end_excl - 1 + local insertion_start = main_cu_end_excl - 1 ---@type integer -- Closure state: bytes + next_offset + the typed-view/structure/abstract offset caches. -- All step-emitters mutate this state in place. - --- @type DwarfEmitState - local S = { + local S = { ---@type DwarfEmitState bytes = {}, next_offset = insertion_start, -- 0-based section offset of the NEXT byte to emit type_chain_offsets = {}, -- {[type_name.."|"..depth] = section_offset for the outermost pointer_type} @@ -2111,8 +1820,7 @@ local function build_inserted_children(main_cu_offset, main_cu_end_excl, atom_ta S.bytes[#S.bytes + 1] = s S.next_offset = S.next_offset + #s end - --- @type table - local FORM_WRITERS = { + local FORM_WRITERS = { ---@type table --- @param emit fun(s: string) --- @param v string|integer --- @return nil @@ -2150,15 +1858,11 @@ local function build_inserted_children(main_cu_offset, main_cu_end_excl, atom_ta --- @param values DieValues --- @return nil local function emit_die(schema_name, values) - --- @type DieSchema - local row = DIE_SCHEMA[schema_name] + local row = DIE_SCHEMA[schema_name] ---@type DieSchema emit(uleb128(row.abbrev)) - --- @type integer, DieSchemaAttr - for _, attr in ipairs(row.attrs) do - --- @type string|integer - local v = values[attr.key] - --- @type DieFormWriter - local w = FORM_WRITERS[attr.form] + for _, attr in ipairs(row.attrs) do ---@type integer, DieSchemaAttr + 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) end @@ -2172,8 +1876,7 @@ local function build_inserted_children(main_cu_offset, main_cu_end_excl, atom_ta -- 1) Emit the base_type DIE first (member ref4s reference it). - --- @type integer - local base_type_section_offset = S.next_offset + local base_type_section_offset = S.next_offset ---@type integer emit_die("base_type", { name = "unsigned int", byte_size = 4, @@ -2191,21 +1894,16 @@ local function build_inserted_children(main_cu_offset, main_cu_end_excl, atom_ta -- For a field declared "V4_S2*" (depth=1), the chain is: -- V4_S2 (typedef, references base_type 4-byte unsigned) + ptr_to_V4_S2_1 (pointer_type, byte_size 4, refs the typedef) -- gdb walks: variable type = ptr_to_V4_S2_1 → V4_S2 → base_type, and displays "V4_S2 *" (the typedef's name + the pointer depth). - --- @type table -- bag - local type_offsets = {} -- {[type_name] = section_offset for the typedef DIE} + local type_offsets = {} ---@type table -- bag -- {[type_name] = section_offset for the typedef DIE} -- 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. - --- @type table -- bag - local used_typed_views = {} -- { [type_name] = max_depth } - --- @type integer, DwarfAtom - for _, atom in ipairs(atom_table) do + 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 - --- @type integer, TypeField - for _, f in ipairs(atom.rbind.fields) do + for _, f in ipairs(atom.rbind.fields) do ---@type integer, TypeField if f.type_name and f.pointer_depth and f.pointer_depth > 0 then - --- @type integer - local depth = used_typed_views[f.type_name] or 0 + local depth = used_typed_views[f.type_name] or 0 ---@type integer if f.pointer_depth > depth then used_typed_views[f.type_name] = f.pointer_depth end @@ -2214,10 +1912,8 @@ local function build_inserted_children(main_cu_offset, main_cu_end_excl, atom_ta end end -- Sort for deterministic emission. - --- @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 + 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). -- The typedef gives gdb a named anchor; the pointer_type chain wraps it. @@ -2233,21 +1929,17 @@ local function build_inserted_children(main_cu_offset, main_cu_end_excl, atom_ta -- Layout for V* / Rect_* / Reg_* / Slice / … comes from corpus.type_name_registry. -- scan_source parses Struct_() in math.h, math.atom.h, memory.h and fills field offset + byte_size. -- U1–U4 / S1–S4 / B1–B4 stay authored fundamentals (BUILTIN_BYTE_SIZES + base_type DIEs below). - --- @type table -- bag - local type_reg = registries.type_name_registry or {} + local type_reg = registries.type_name_registry or {} ---@type table -- bag --- @param tn string --- @return DwarfTypeLayout|nil local function layout_from_registry(tn) - --- @type TypeNameEntry|nil - local entry = type_reg[tn] + local entry = type_reg[tn] ---@type TypeNameEntry|nil if not entry or entry.kind ~= "struct" or not entry.fields or #entry.fields == 0 then return nil end if entry.byte_size == nil then return nil end - --- @type DwarfTypeLayoutMember[] - local members = {} - --- @type integer, TypeField - for _, f in ipairs(entry.fields) do + 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] = { name = f.name, @@ -2265,25 +1957,20 @@ local function build_inserted_children(main_cu_offset, main_cu_end_excl, atom_ta if type(tn) == "string" and tn:match("^S[124]$") then return 5 end return 7 end - --- @type integer - local S2_TYPE_BYTE_SIZE = 2 - --- @type integer - local S4_TYPE_BYTE_SIZE = 4 + local S2_TYPE_BYTE_SIZE = 2 ---@type integer + local S4_TYPE_BYTE_SIZE = 4 ---@type integer -- Pre-emit the signed base types (S2, S4) once if any typed view's member needs them. -- We emit per-typed-view lazily below; build a member-base-type offset cache (idempotent). - --- @type table -- bag - local member_base_type_offsets = {} + local member_base_type_offsets = {} ---@type table -- bag --- @param tn string --- @param byte_size integer --- @param encoding integer --- @return integer local function ensure_member_base_type(tn, byte_size, encoding) - --- @type string - local key = tn .. "|" .. byte_size .. "|" .. encoding + local key = tn .. "|" .. byte_size .. "|" .. encoding ---@type string if member_base_type_offsets[key] then return member_base_type_offsets[key] end - --- @type integer - local off = next_offset() + local off = next_offset() ---@type integer emit_die("base_type", { name = tn, byte_size = byte_size, @@ -2297,34 +1984,27 @@ local function build_inserted_children(main_cu_offset, main_cu_end_excl, atom_ta ensure_member_base_type("S2", S2_TYPE_BYTE_SIZE, 5) ensure_member_base_type("S4", S4_TYPE_BYTE_SIZE, 5) - --- @type table -- bag - local type_chain_offsets = {} - --- @type table -- bag - local struct_die_offsets = {} + local type_chain_offsets = {} ---@type table -- bag + local struct_die_offsets = {} ---@type table -- bag --- @param tn string --- @return integer|nil local function emit_struct_layout(tn) if struct_die_offsets[tn] then return struct_die_offsets[tn] end - --- @type DwarfTypeLayout|nil - local type_info = layout_from_registry(tn) + local type_info = layout_from_registry(tn) ---@type DwarfTypeLayout|nil if not type_info then return nil end - --- @type integer, DwarfTypeLayoutMember - for _, m in ipairs(type_info.members) do + for _, m in ipairs(type_info.members) do ---@type integer, DwarfTypeLayoutMember if (m.pointer_depth or 0) == 0 and layout_from_registry(m.type_name) then emit_struct_layout(m.type_name) end end - --- @type integer - local struct_offset = next_offset() + local struct_offset = next_offset() ---@type integer struct_die_offsets[tn] = struct_offset emit_die("structure_type", { name = tn, byte_size = type_info.byte_size, }) - --- @type integer, DwarfTypeLayoutMember - for _, m in ipairs(type_info.members) do - --- @type integer - local member_type_off + for _, m in ipairs(type_info.members) do ---@type integer, DwarfTypeLayoutMember + local member_type_off ---@type integer if (m.pointer_depth or 0) > 0 then member_type_off = type_chain_offsets[m.type_name .. "|" .. m.pointer_depth] elseif layout_from_registry(m.type_name) then @@ -2346,28 +2026,22 @@ local function build_inserted_children(main_cu_offset, main_cu_end_excl, atom_ta emit(string.char(DIE_CHILDREN_TERMINATOR)) return struct_offset end - --- @type integer, string - for _, tn in ipairs(sorted_typed_types) do + for _, tn in ipairs(sorted_typed_types) do ---@type integer, string if tn ~= "U4" then - --- @type integer - local depth = used_typed_views[tn] - --- @type integer - local struct_offset = emit_struct_layout(tn) + local depth = used_typed_views[tn] ---@type integer + local struct_offset = emit_struct_layout(tn) ---@type integer if not struct_offset then - --- @type integer - local innermost_offset = next_offset() + local innermost_offset = next_offset() ---@type integer emit_die("base_type", { name = tn, byte_size = U4_BYTE_SIZE, encoding = DW_ATE_unsigned, }) - --- @type integer - local outermost_offset = next_offset() + local outermost_offset = next_offset() ---@type integer emit_die("pointer_type", { type = ref4_of(innermost_offset) }) type_chain_offsets[tn .. "|" .. depth] = outermost_offset elseif depth == 1 then - --- @type integer - local outermost_offset = next_offset() + local outermost_offset = next_offset() ---@type integer emit_die("pointer_type", { type = ref4_of(struct_offset) }) type_chain_offsets[tn .. "|" .. depth] = outermost_offset else @@ -2383,8 +2057,7 @@ local function build_inserted_children(main_cu_offset, main_cu_end_excl, atom_ta -- The void base_type is emitted BEFORE any other typed chain so its ref4 pointer remains stable. -- Follow the SAME pattern as the typed-views chain above: capture the offset BEFORE the uleb tag -- (this is the ref4 target), emit the DIE bytes, then emit the pointer_type pointing at the offset. - --- @type integer - local void_chain_offset = next_offset() + local void_chain_offset = next_offset() ---@type integer emit_die("base_type", { name = "void", byte_size = 1, @@ -2396,8 +2069,7 @@ local function build_inserted_children(main_cu_offset, main_cu_end_excl, atom_ta -- The variable's DW_AT_type must reference the pointer_type, not the base_type. Patch below. -- Capture the pointer_type's offset (the last-thing-emitted DIE start) and overwrite the lookup. -- The pointer_type was emitted as: uleb(9) (1 byte) + 4-byte ref4 = 5 bytes. Its tag byte is at void_chain_offset + 8 (the base_type's 8 bytes: 1 tag + 5 name + 1 byte_size + 1 encoding). - --- @type integer - local ptr_void_offset = void_chain_offset + 8 + local ptr_void_offset = void_chain_offset + 8 ---@type integer type_chain_offsets["void|1"] = ptr_void_offset -- 1c) Emit the U4 * pointer chain (step (e) fallback for enum-site `atom_type(U4 *)`). @@ -2408,22 +2080,16 @@ local function build_inserted_children(main_cu_offset, main_cu_end_excl, atom_ta -- Once this chain is registered as `type_chain_offsets["U4|1"]`, step (e) of the per-RR_ precedence chain will resolve `atom_type(U4 *)` -- declarations on aliases like `R_PrimCursor` and `R_OtBase` to `U4 *` (gdb renders as `(unsigned int *)` with the value displayed in hex). emit_die("pointer_type", { type = ref4_of(base_type_section_offset) }) - --- @type integer - local u4_chain_offset = next_offset() - 5 -- 1 (uleb tag) + 4 (ref4) = 5 bytes; capture the pointer_type's start offset + local u4_chain_offset = next_offset() - 5 ---@type integer -- 1 (uleb tag) + 4 (ref4) = 5 bytes; capture the pointer_type's start offset type_chain_offsets["U4|1"] = u4_chain_offset -- 2) Emit one DW_TAG_structure_type per unique Binds_X. - --- @type table -- bag - local struct_section_offsets = {} - --- @type string[] - local sorted_struct_names = {} - --- @type string - for k in pairs(rbind_structs) do sorted_struct_names[#sorted_struct_names + 1] = k end + 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) - --- @type integer, string - for _, binds_name in ipairs(sorted_struct_names) do - --- @type DwarfRbindStruct - local struct = rbind_structs[binds_name] + for _, binds_name in ipairs(sorted_struct_names) do ---@type integer, string + local struct = rbind_structs[binds_name] ---@type DwarfRbindStruct struct_section_offsets[binds_name] = next_offset() emit_die("structure_type", { @@ -2431,10 +2097,8 @@ local function build_inserted_children(main_cu_offset, main_cu_end_excl, atom_ta byte_size = struct.bytes, }) - --- @type integer, TypeField - for _, field in ipairs(struct.fields) do - --- @type integer - local field_type_offset + for _, field in ipairs(struct.fields) do ---@type integer, TypeField + local field_type_offset ---@type integer if field.pointer_depth and field.pointer_depth > 0 then field_type_offset = type_chain_offsets[field.type_name .. "|" .. field.pointer_depth] end @@ -2455,22 +2119,15 @@ 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). - --- @type table -- bag - local component_defs = collect_component_defs(atom_table) - --- @type table -- bag - local abstract_offsets = {} -- name -> section offset - --- @type string[] - local sorted_comp_names = {} - --- @type string - for name in pairs(component_defs) do sorted_comp_names[#sorted_comp_names + 1] = name end + 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. - --- @type integer - local DW_INL_inlined = 0x01 - --- @type integer, string - for _, comp_name in ipairs(sorted_comp_names) do - --- @type DwarfComponentSite - local def = component_defs[comp_name] + 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() emit_die("abstract_subprogram", { name = "mac_" .. comp_name, @@ -2485,8 +2142,7 @@ local function build_inserted_children(main_cu_offset, main_cu_end_excl, atom_ta -- Subprogram names match the written C ident (the ELF symbol). -- The gcc global `[]` is a DW_TAG_variable without children; our subprogram has the wave-context var children. -- gdb's symbol resolution picks our subprogram (it has low_pc/high_pc + children) over the gcc global for function-context lookups. - --- @type integer, DwarfAtom - for _, atom in ipairs(atom_table) do + for _, atom in ipairs(atom_table) do ---@type integer, DwarfAtom emit_die("subprogram", { name = atom.name, low_pc = atom.addr, @@ -2503,80 +2159,60 @@ 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. - --- @type table|nil -- bag - local atom_view_ctx_fields = nil -- populated by step (b); map field_name -> field entry - --- @type table|nil -- bag - local reg_to_field_ctx = nil -- populated by step (b); map GPR index -> field name - --- @type table|nil -- bag - local atom_view_phase_fields = nil -- populated by step (d); map field_name -> field entry - --- @type table|nil -- bag - local reg_to_field_phase = nil -- 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. - --- @type table -- bag - local field_type_by_name = {} + local field_type_by_name = {} ---@type table -- bag if atom.rbind and atom.rbind.fields then - --- @type integer, TypeField - for _, f in ipairs(atom.rbind.fields) do + for _, f in ipairs(atom.rbind.fields) do ---@type integer, TypeField if f.type_name then field_type_by_name[f.name] = f end end end - --- @type table -- bag - local reg_to_field = {} + local reg_to_field = {} ---@type table -- bag if atom.rbind and atom.rbind.regs then - --- @type integer, DwarfLoadPair - for _, pair in ipairs(atom.rbind.regs) do + for _, pair in ipairs(atom.rbind.regs) do ---@type integer, DwarfLoadPair reg_to_field[pair.reg] = pair.field end end - --- @type AtomViewEntry|nil - local atom_view = (registries.atom_views or {})[atom.name] + local atom_view = (registries.atom_views or {})[atom.name] ---@type AtomViewEntry|nil -- step (b) inputs: this atom's `atom_ctx()` (resolved from the registries' atom_ctxs) - --- @type AtomCtxEntry|nil - local this_ctx = registries.atom_ctxs and registries.atom_ctxs[atom.name] + local this_ctx = registries.atom_ctxs and registries.atom_ctxs[atom.name] ---@type AtomCtxEntry|nil if this_ctx and this_ctx.rbind_atom then - --- @type DwarfAtom|nil - local rbind = atom_by_name_global[this_ctx.rbind_atom] + local rbind = atom_by_name_global[this_ctx.rbind_atom] ---@type DwarfAtom|nil if rbind and rbind.rbind and rbind.rbind.fields then atom_view_ctx_fields = {} - --- @type integer, TypeField - for _, f in ipairs(rbind.rbind.fields) do atom_view_ctx_fields[f.name] = f end + for _, f in ipairs(rbind.rbind.fields) do atom_view_ctx_fields[f.name] = f end ---@type integer, TypeField if rbind.rbind.regs then reg_to_field_ctx = {} - --- @type integer, DwarfLoadPair - for _, pair in ipairs(rbind.rbind.regs) do reg_to_field_ctx[pair.reg] = pair.field end + for _, pair in ipairs(rbind.rbind.regs) do reg_to_field_ctx[pair.reg] = pair.field end ---@type integer, DwarfLoadPair end end end -- step (d) inputs: this atom's `atom_phase(