utilizing trailing type annotations more

This commit is contained in:
ed
2026-08-19 23:35:57 -04:00
parent 449216967b
commit 2a087f735e
18 changed files with 2785 additions and 5468 deletions
+8 -16
View File
@@ -3,14 +3,10 @@
--- @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
@@ -46,8 +42,7 @@ local M = {}
--- @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 {},
@@ -90,10 +84,8 @@ end
--- @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
+3 -6
View File
@@ -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<string, string> -- 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
+10 -20
View File
@@ -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 `<repo>/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. `<repo>/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 <repo>/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
+184 -368
View File
@@ -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,11 +382,9 @@ 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
@@ -465,8 +392,7 @@ end
--- @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
@@ -477,14 +403,12 @@ end
--- @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<string, string> -- bag: input path -> absolute path
local _absolute_path_cache = {}
local _absolute_path_cache = {} ---@type table<string, string> -- 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<string, boolean> -- bag: dir path -> already ensured
local _ensured_dirs = {}
local _ensured_dirs = {} ---@type table<string, boolean> -- bag: dir path -> already ensured
--- @param path Path
--- @return nil
@@ -533,10 +452,8 @@ end
--- @param sources SourceFile[]
--- @return table<string, SourceFile[]>
function M.group_sources_by_dir(sources)
--- @type table<string, SourceFile[]>
local by_dir = {}
--- @type integer, SourceFile
for _, src in ipairs(sources) do
local by_dir = {} ---@type table<string, SourceFile[]>
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
@@ -567,8 +484,7 @@ function M.skip_ws_and_cmt(s, pos) return lpeg.match(lpeg_ws_and_cmt_pat, s, pos
--- @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
@@ -581,21 +497,16 @@ end
--- @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: <open_char>
pos = pos + 1
-- scan: <open_char> <inner...>
--- @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: <open_char> <inner...> <close_char> (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: <open_char> <inner...> <str|cmt>
pos = nx
@@ -641,20 +551,16 @@ M.read_brackets = function(s, pos) return M.read_balanced(s, "[", "]", pos) end
--- @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: ... <target found> | <skipping to target>
if c == BYTE_OPEN_PAREN then local _, a = M.read_balanced(s, "(", ")", pos); pos = a -- scan: ... ( <balanced> ) ...
elseif c == BYTE_OPEN_BRACE then local _, a = M.read_balanced(s, "{", "}", pos); pos = a -- scan: ... { <balanced> } ...
elseif c == BYTE_OPEN_BRACK then local _, a = M.read_balanced(s, "[", "]", pos); pos = a -- scan: ... [ <balanced> ] ...
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: ... <str|cmt skipped> ...
end
@@ -670,10 +576,8 @@ end
--- @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
@@ -689,8 +593,7 @@ end
--- @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
@@ -701,13 +604,11 @@ end
--- @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)
@@ -875,8 +757,7 @@ end
--- @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
@@ -887,16 +768,14 @@ end
--- @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,20 +798,13 @@ 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<Path, SourceFile>
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<Path, SourceFile>
local resolver = { ---@type SourceResolver
resolved = {
{
include_path = nil,
@@ -949,22 +821,14 @@ function M.resolve_source_corpus(options)
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 = {
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,
@@ -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: ... ( <balanced> ...
elseif c == BYTE_OPEN_BRACE then local _, a = M.read_braces(body, pos); pos = a -- scan: ... { <balanced> ...
elseif c == BYTE_OPEN_BRACK then local _, a = M.read_brackets(body, pos); pos = a -- scan: ... ( <balanced> ...
@@ -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: ... <str|cmt> ...
-- 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<string, BodyToken[]> -- bag: body text -> tokens
local _tokenize_body_cache = {}
--- @type table<string, table<integer, integer>> -- bag: body text -> offset-to-line
local _body_line_index_cache = {}
local _tokenize_body_cache = {} ---@type table<string, BodyToken[]> -- bag: body text -> tokens
local _body_line_index_cache = {} ---@type table<string, table<integer, integer>> -- 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<integer, integer> -- 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<integer, integer> -- 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<integer, integer> -- 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(<name>, <N>)
--- @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
+48 -96
View File
@@ -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<string, Elf32Sym>|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
@@ -286,8 +283,7 @@ end
--- @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<string, Elf32Sym>|nil, string|nil
function M.collect_symbols(adapter, sections)
if not sections then return nil, "missing_sections" end
--- @type table<string, Elf32Sym> -- 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<string, Elf32Sym> -- 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,
+151 -301
View File
@@ -111,17 +111,14 @@
--- @field sleb128_size fun(n: integer): integer
--- @field read_line_unit_file_table fun(elf_path: string): table<string, integer>|nil, table<integer, string>|nil, table<integer, string>|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,8 +503,7 @@ 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<integer, fun(buf: string, str_buf: string, pos: integer): (string|integer|nil, integer)>
local FORM_READERS = {
local FORM_READERS = { ---@type table<integer, fun(buf: string, str_buf: string, pos: integer): (string|integer|nil, integer)>
--- @param buf string
--- @param _ string
--- @param pos integer
@@ -547,8 +518,7 @@ local FORM_READERS = {
--- @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
@@ -558,8 +528,7 @@ local FORM_READERS = {
--- @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
@@ -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,
@@ -654,8 +622,7 @@ 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,
}
@@ -666,8 +633,7 @@ local FORM_READERS = {
--- @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<string, string>
local result = {}
--- @type integer, string
for _, name in ipairs(section_names) do result[name] = "" end
local result = {} ---@type table<string, string>
for _, name in ipairs(section_names) do result[name] = "" end ---@type integer, string
-- O(1) lookup set.
--- @type table<string, boolean> -- 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<string, boolean> -- 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<string, NmAddr>
function M.read_nm(elf_path)
--- @type table<string, NmAddr>
local addrs = {}
local addrs = {} ---@type table<string, NmAddr>
-- 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<string, Elf32Sym>|nil, string|nil
local symbols, sym_err = E.collect_symbols(adapter, sections)
local symbols, sym_err = E.collect_symbols(adapter, sections) ---@type table<string, Elf32Sym>|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,23 +1090,17 @@ end
--- @return table<integer, string>|nil
--- @return table<integer, string>|nil
function M.read_line_unit_file_table(elf_path)
--- @type table<string, string>
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<string, string>
local line = sections[".debug_line"] ---@type string
local lstr = sections[".debug_line_str"] or "" ---@type string
if not line or line == "" then
io.stderr:write("[elf_dwarf.read_line_unit_file_table] no .debug_line section in: " .. tostring(elf_path) .. "\n")
return nil
end
--- @type table<integer, string> -- bag: 1-based file index -> basename
local basenames = {}
--- @type table<string, integer> -- bag: basename -> 1-based file index
local basename_to_index = {}
--- @type table<integer, string> -- bag: 1-based file index -> full path
local paths = {}
local basenames = {} ---@type table<integer, string> -- bag: 1-based file index -> basename
local basename_to_index = {} ---@type table<string, integer> -- bag: basename -> 1-based file index
local paths = {} ---@type table<integer, string> -- 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:
@@ -1210,18 +1115,14 @@ function M.read_line_unit_file_table(elf_path)
--- @return integer
local function read_form(buf, lstr_buf, p, form)
if form == M.DWARF5_DEBUG_LINE.form_line_strp then
--- @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
@@ -1243,46 +1144,32 @@ function M.read_line_unit_file_table(elf_path)
--- @return table<integer, string>
--- @return table<integer, string>
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<integer, string> -- bag: 1-based unit file index -> basename
local unit_basenames = {}
--- @type table<integer, string> -- bag: 1-based unit file index -> full path
local unit_paths = {}
local unit_basenames = {} ---@type table<integer, string> -- bag: 1-based unit file index -> basename
local unit_paths = {} ---@type table<integer, string> -- 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
@@ -1302,76 +1189,50 @@ function M.read_line_unit_file_table(elf_path)
--- @return table<integer, string>
--- @return table<integer, string>
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<integer, string> -- bag: 1-based unit file index -> basename
local unit_basenames = {}
--- @type table<integer, string> -- 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<integer, string> -- bag: 1-based unit file index -> basename
local unit_paths = {} ---@type table<integer, string> -- 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<integer, string>|nil, table<integer, string>|nil
local unit_basenames, unit_paths
local version = M.read_u16_le(line, body_start) ---@type integer
local unit_basenames, unit_paths ---@type table<integer, string>|nil, table<integer, string>|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
for idx, bs in pairs(unit_basenames) do ---@type integer, string
basenames[idx] = bs
paths[idx] = unit_paths[idx]
paths [idx] = unit_paths[idx]
basename_to_index[bs] = idx
end
p = body_end
+59 -118
View File
@@ -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<string, integer> -- 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<string, integer> -- 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<string, AliasEntry>
local reg_registry = pipe_ctx.register_alias_registry or {}
--- @type table<string, TypeNameEntry>
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<string, AliasEntry>
local type_registry = pipe_ctx.type_name_registry or {} ---@type table<string, TypeNameEntry>
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<string, AliasEntry>
local reg_registry = pipe_ctx.register_alias_registry or {}
--- @type table<string, TypeNameEntry>
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<string, AliasEntry>
local type_registry = pipe_ctx.type_name_registry or {} ---@type table<string, TypeNameEntry>
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<string, integer> -- 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<string, integer> -- 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<string, AliasEntry>
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<string, AliasEntry>
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<string, integer> -- 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<string, integer> -- 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<string, integer> -- 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<string, integer> -- 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<string, SourceFile[]>
local by_dir = (corpus and corpus.sources_by_dir) or {}
local by_dir = (corpus and corpus.sources_by_dir) or {} ---@type table<string, SourceFile[]>
--- @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
+75 -150
View File
@@ -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<string, NmAddr>
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<string, NmAddr>
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_<exact_name> (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 <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 `<out_root>/gdb_tape_atoms_runtime.gdb` to `<out_root>/../gdb_tape_atoms_runtime.gdb` when the conventional `<out_root>` is `<build>/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,
+71 -142
View File
@@ -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<integer, GprIdent> -- bag: MIPS GPR code -> POOL ident
local INT_CODE_TO_POOL_GPR = {
local INT_CODE_TO_POOL_GPR = { ---@type table<integer, GprIdent> -- 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<string, string> -- 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<GprIdent, boolean>
--- @return table<string, GprIdent>
local function build_user_pins(corpus)
--- @type table<GprIdent, boolean> -- bag: pinned physical GPR -> true
local user_pinned = {}
--- @type table<string, GprIdent> -- bag: alias ident -> physical GPR
local alias_to_gpr = {}
local user_pinned = {} ---@type table<GprIdent, boolean> -- bag: pinned physical GPR -> true
local alias_to_gpr = {} ---@type table<string, GprIdent> -- 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<string, GprIdent> -- bag: alias ident -> physical GPR
--- @return table<GprIdent, integer>
local function find_used_gprs(body_text, alias_to_gpr)
--- @type table<GprIdent, integer> -- bag: physical GPR -> hit count
local found = {}
local found = {} ---@type table<GprIdent, integer> -- 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_<Alias>) 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_<Sym>_Code = <chosen GPR's _Code constant> 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<GprIdent, boolean>, table<string, GprIdent>
local user_pinned, alias_to_gpr = build_user_pins(corpus)
local user_pinned, alias_to_gpr = build_user_pins(corpus) ---@type table<GprIdent, boolean>, table<string, GprIdent>
-- 1. Allocate phase pools first (phase declarations take precedence over per-atom declarations).
--- @type table<string, GprAllocMap> -- bag: phase_label -> alloc map
local phase_allocations = {}
--- @type string, table<string, string>
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<string, GprAllocMap> -- bag: phase_label -> alloc map
for phase_label, decls in pairs(corpus.phase_auto_regs or {}) do ---@type string, table<string, string>
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(<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<AtomName, string> -- 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<AtomName, string> -- 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<AtomName, GprAllocMap> -- bag: atom scope -> alloc map
local atom_allocations = {}
--- @type AtomName, table<string, string>
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<AtomName, GprAllocMap> -- bag: atom scope -> alloc map
for atom_scope, decls in pairs(corpus.atom_auto_regs or {}) do ---@type AtomName, table<string, string>
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<GprIdent, boolean> -- 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<GprIdent, boolean> -- 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<GprIdent, integer>
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<GprIdent, integer>
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<GprIdent, integer>
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<GprIdent, integer>
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<string, SourceFile[]>
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<string, SourceFile[]>
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 }
+135 -270
View File
@@ -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_<name>` 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<string, integer> -- bag: bare component name -> word count
local function count_all_components(components, wc)
--- @type table<string, Component>
local comp_by_name = {}
--- @type integer, Component
for _, cc in ipairs(components) do comp_by_name[cc.name] = cc end
--- @type table<string, integer> -- bag: memo; -1 in-progress sentinel
local cache = {}
--- @type table<string, integer> -- bag: bare name -> word count
local counts = {}
--- @type integer, Component
for _, c in ipairs(components) do
local comp_by_name = {} ---@type table<string, Component>
for _, cc in ipairs(components) do comp_by_name[cc.name] = cc end ---@type integer, Component
local cache = {} ---@type table<string, integer> -- bag: memo; -1 in-progress sentinel
local counts = {} ---@type table<string, integer> -- 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<string, integer> -- bag: ident -> cycle cost
--- @return ComponentMetaMap
local function compute_components_metadata(components, latency)
--- @type table<string, Component>
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<string, Component>
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<string, boolean> -- bag: delay-marker ident -> true
local markers = duffle.DELAY_MARKERS
local markers = duffle.DELAY_MARKERS ---@type table<string, boolean> -- 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<string, integer> -- 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<string, integer> -- 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<string, SourceFile[]>
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<string, SourceFile[]>
for dir, sources in pairs(sources_by_dir) do ---@type string, SourceFile[]
-- Aggregate components from every source in this directory.
-- `project_components` returns nil for sources with no `MipsAtomComp_` declarations; we skip those.
--- @type Component[]
local aggregated_components = {}
--- @type table<SourceFile, ComponentMetaMap>
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<SourceFile, ComponentMetaMap>
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<string, integer> -- 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<string, integer> -- 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)
File diff suppressed because it is too large Load Diff
+47 -94
View File
@@ -117,16 +117,13 @@
--- @class EmissionModelPass
--- @field run fun(ctx: PassCtx): PassResult
--- @type EmissionModelPass
local M = {}
local M = {} ---@type EmissionModelPass
-- ─────────────────────────────────────────────────────────────────────────
-- Bootstrap: load `duffle_paths.lua` via debug.getinfo so the module works standalone (run as `luajit passes/emission_model.lua`) and when require'd from the orchestrator.
-- ─────────────────────────────────────────────────────────────────────────
--- @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
-- ─────────────────────────────────────────────────────────────────────────
-- Helpers
@@ -149,8 +146,7 @@ local duffle = dofile(_bootstrap_dir .. "../duffle_paths.lua")
--- @param corpus Corpus
--- @return nil
local function stamp_root_provenance(projection, atom_record, src, corpus)
--- @type (fun(pos: integer): integer)|nil
local root_line_of = src.scan and src.scan.line_of
local root_line_of = src.scan and src.scan.line_of ---@type (fun(pos: integer): integer)|nil
assert(type(root_line_of) == "function"
, "emission_model: src.scan.line_of is required (canonical LineIndex closure over the source text) to stamp physical provenance")
assert(type(atom_record.body_off) == "number"
@@ -158,15 +154,11 @@ local function stamp_root_provenance(projection, atom_record, src, corpus)
-- `root_body_line` is the physical source line of the ATOM HEADER byte containing the opening `{`; that byte is one byte BEFORE `atom_record.body_off`.
-- The walker assigns line 2 to the body's first content line because line 1 is the trailing `\n` after `{`. Body-text line k therefore maps to `root_body_line + (k - 1)`.
-- `body_off - 1` points at the opening `{`, whose line index identifies the header line. `body_off` points after `{` and would shift every word row forward by one line.
--- @type integer
local root_body_line = root_line_of(atom_record.body_off - 1) or atom_record.line or 0
--- @type table<string, ComponentBodyEntry>
local component_index = corpus.component_body_index or {}
--- @type EmissionItem[]
local word_items = {}
local root_body_line = root_line_of(atom_record.body_off - 1) or atom_record.line or 0 ---@type integer
local component_index = corpus.component_body_index or {} ---@type table<string, ComponentBodyEntry>
local word_items = {} ---@type EmissionItem[]
--- @type integer, EmissionItem
for _, item in ipairs(projection.items) do
for _, item in ipairs(projection.items) do ---@type integer, EmissionItem
if item.kind == "word" then word_items[#word_items + 1] = item end
end
@@ -177,18 +169,14 @@ local function stamp_root_provenance(projection, atom_record, src, corpus)
--- @param item EmissionItem
--- @return integer
local function body_line_for(event, item)
--- @type integer[]
local ids = event.invocation_ids or {}
local ids = event.invocation_ids or {} ---@type integer[]
-- The innermost open invocation identifies which line index the walker used.
-- A component `line_of` makes `item.line` physical; the atom's `body_text` line index makes it body-relative.
if ids and #ids > 0 then
--- @type integer
local inner_id = ids[#ids]
--- @type InvocationRecord|nil
local inner_inv = inner_id and projection.invocations[inner_id]
local inner_id = ids[#ids] ---@type integer
local inner_inv = inner_id and projection.invocations[inner_id] ---@type InvocationRecord|nil
if inner_inv then
--- @type ComponentBodyEntry|nil
local component = component_index[inner_inv.component_name]
local component = component_index[inner_inv.component_name] ---@type ComponentBodyEntry|nil
if component and component.line_of then
-- Walker used `comp.line_of`, which is the source's physical LineIndex. item.line is already physical.
return item.line or 0
@@ -203,10 +191,8 @@ local function stamp_root_provenance(projection, atom_record, src, corpus)
-- Stamp the root source path onto invocation records whose `call_path` the walker left empty.
-- The walker passes `body_entry.source` to `emit_invoke_begin`; `M.project_emission` creates the root `body_entry` with source `""`, leaving its `call_path` empty.
-- This stamp gives every invocation a physical `call_path` matching `passes/atoms_source_map.lua`'s in-memory provenance projection.
--- @type string
local root_path = src.path or ""
--- @type integer, InvocationRecord
for _, inv in ipairs(projection.invocations) do
local root_path = src.path or "" ---@type string
for _, inv in ipairs(projection.invocations) do ---@type integer, InvocationRecord
if inv.call_path == nil or inv.call_path == "" then
inv.call_path = root_path
end
@@ -215,8 +201,7 @@ local function stamp_root_provenance(projection, atom_record, src, corpus)
-- Normalize `inv.call_line` to a physical source line.
-- * ROOT invocations (`parent_id == 0`) carry body-relative `call_line` values from `M.LineIndex(body_text)`; convert them once with `root_body_line`.
-- * INNER invocations (`parent_id ~= 0`) carry physical `call_line` values from the component's `line_of`; retain them unchanged.
--- @type integer, InvocationRecord
for _, inv in ipairs(projection.invocations) do
for _, inv in ipairs(projection.invocations) do ---@type integer, InvocationRecord
if inv.parent_id == 0 then
inv.call_line = (root_body_line or 0) + (inv.call_line or 1) - 1
end
@@ -225,21 +210,14 @@ local function stamp_root_provenance(projection, atom_record, src, corpus)
-- Build `body_lines` for each invocation.
-- `atoms_source_map` and `dwarf_injection` read `inv.body_lines[k]` directly from the invocation record created here.
-- Component words already carry physical `item.line` values from the walker's COMPONENT line index, so `body_line_for` returns them unchanged.
--- @type integer, InvocationRecord
for _, inv in ipairs(projection.invocations) do
--- @type integer
local sw = inv.start_word
--- @type integer
local ew = inv.end_word
--- @type integer[]
local bls = {}
--- @type integer
for i = sw, ew do
--- @type EmissionItem|nil
local it = projection.items and projection.items[i]
for _, inv in ipairs(projection.invocations) do ---@type integer, InvocationRecord
local sw = inv.start_word ---@type integer
local ew = inv.end_word ---@type integer
local bls = {} ---@type integer[]
for i = sw, ew do ---@type integer
local it = projection.items and projection.items[i] ---@type EmissionItem|nil
if it and it.kind == "word" then
--- @type WordEvent
local fake_event = { invocation_ids = { inv.id } }
local fake_event = { invocation_ids = { inv.id } } ---@type WordEvent
bls[#bls + 1] = body_line_for(fake_event, it) or 0
end
end
@@ -249,21 +227,15 @@ local function stamp_root_provenance(projection, atom_record, src, corpus)
-- Resolve each `word_event`'s physical `body_line` and `call_line`.
-- For words inside an invocation, `we.call_line` identifies the OUTER atom source line containing the `mac_X(...)` token that triggered expansion.
-- The root-invocation conversion above makes every `inv.call_line` physical; forward it directly and use each raw word's `body_line` as the fallback.
--- @type integer, WordEvent
for index, we in ipairs(projection.word_events) do
--- @type EmissionItem
local item = word_items[index] or {}
--- @type integer
local body_line = body_line_for(we, item)
for index, we in ipairs(projection.word_events) do ---@type integer, WordEvent
local item = word_items[index] or {} ---@type EmissionItem
local body_line = body_line_for(we, item) ---@type integer
item.line = body_line
we.body_line = body_line
--- @type integer
local call_line = body_line
--- @type integer
local outer_id = we.outermost_invocation_id or 0
--- @type InvocationRecord|nil
local outer_inv = projection.invocations[outer_id]
local call_line = body_line ---@type integer
local outer_id = we.outermost_invocation_id or 0 ---@type integer
local outer_inv = projection.invocations[outer_id] ---@type InvocationRecord|nil
if outer_inv then
-- `outer_inv.call_line` is physical after the conversion loop above, so use it directly.
call_line = outer_inv.call_line
@@ -283,20 +255,15 @@ end
--- @param corpus Corpus
--- @return EmissionProjection
local function project_atom(atom_record, src, corpus)
--- @type string
local body = atom_record.body or ""
--- @type WordCounts
local wc = corpus.word_counts or {}
--- @type table<string, ComponentBodyEntry>
local cbi = corpus.component_body_index or {}
--- @type RegUseSchema|nil
local schema = nil
local body = atom_record.body or "" ---@type string
local wc = corpus.word_counts or {} ---@type WordCounts
local cbi = corpus.component_body_index or {} ---@type table<string, ComponentBodyEntry>
local schema = nil ---@type RegUseSchema|nil
if atom_record.reg_use_schema_name then
schema = corpus.reg_use_schemas and corpus.reg_use_schemas[atom_record.reg_use_schema_name]
end
-- That construction site stamps `invocation.debug_skip` while appending each record to `proj.invocations`.
--- @type EmissionProjection
local proj = duffle.project_emission(body, cbi, wc, corpus.components, {
local proj = duffle.project_emission(body, cbi, wc, corpus.components, { ---@type EmissionProjection
reg_use_schema = schema,
reg_use_param = atom_record.reg_use_param_name,
atom_name = atom_record.name,
@@ -308,14 +275,12 @@ local function project_atom(atom_record, src, corpus)
msg = string.format("RegUse schema %q is missing", atom_record.reg_use_schema_name),
}
end
--- @type integer, EmitError
for _, err in ipairs(corpus.reg_use_errors or {}) do
for _, err in ipairs(corpus.reg_use_errors or {}) do ---@type integer, EmitError
if err.schema_name == atom_record.reg_use_schema_name then
proj.errors[#proj.errors + 1] = err
end
end
--- @type AtomPaths
local paths = {
local paths = { ---@type AtomPaths
tokens = atom_record.body_tokens or {},
line_in_body = duffle.build_body_line_index(body),
items = proj.items,
@@ -337,15 +302,11 @@ end
--- @param ctx PassCtx -- { shared = { corpus = ... }, out_root, ... }
--- @return PassResult
function M.run(ctx)
--- @type PassOutputEntry[]
local outputs = {}
--- @type EmitError[]
local errors = {}
--- @type EmitWarning[]
local warnings = {}
local outputs = {} ---@type PassOutputEntry[]
local errors = {} ---@type EmitError[]
local warnings = {} ---@type EmitWarning[]
--- @type Corpus|nil
local corpus = ctx and ctx.shared and ctx.shared.corpus
local corpus = ctx and ctx.shared and ctx.shared.corpus ---@type Corpus|nil
if type(corpus) ~= "table" then error("emission_model: ctx.shared.corpus is required (canonical projection)", 0) end
if type(corpus.source_order) ~= "table" then error("emission_model: ctx.shared.corpus.source_order is required", 0) end
@@ -356,15 +317,12 @@ function M.run(ctx)
--- @return nil
local function process_atom(atom, src)
if not (atom and atom.body) then return end
--- @type string
local kind = atom.kind
local kind = atom.kind ---@type string
if kind ~= "atom" and kind ~= "atom_proc" and kind ~= "raw_atom" and kind ~= "comp_bare" and kind ~= "comp_proc" then
return
end
--- @type EmissionProjection
local proj = project_atom(atom, src, corpus)
--- @type integer, EmitError
for _, e in ipairs(proj.errors) do
local proj = project_atom(atom, src, corpus) ---@type EmissionProjection
for _, e in ipairs(proj.errors) do ---@type integer, EmitError
-- Preserve `kind` (cycle / count_mismatch / unbalanced) so readers dispatch on the diagnostic class and leave the message string as display text.
errors[#errors + 1] = {
kind = e.kind,
@@ -373,8 +331,7 @@ function M.run(ctx)
source = e.source or src.path,
}
end
--- @type integer, EmitWarning
for _, w in ipairs(proj.warnings) do
for _, w in ipairs(proj.warnings) do ---@type integer, EmitWarning
warnings[#warnings + 1] = {
kind = w.kind,
line = w.line,
@@ -386,16 +343,12 @@ function M.run(ctx)
-- Walk `corpus.source_order`; within each source, visit atoms followed by raw_atoms.
-- Recognized kinds (atom | atom_proc | raw_atom | comp_bare | comp_proc) each receive the atom.paths projection via duffle.project_emission.
-- Components are macros inlined into atom bodies; focused tests and isolated component analyses consume atom.paths directly.
--- @type integer, SourceFile
for _, src in ipairs(corpus.source_order) do
--- @type SourceScan
local scan = src.scan or {}
--- @type integer, AtomEntry
for _, atom in ipairs(scan.atoms or {}) do
for _, src in ipairs(corpus.source_order) do ---@type integer, SourceFile
local scan = src.scan or {} ---@type SourceScan
for _, atom in ipairs(scan.atoms or {}) do ---@type integer, AtomEntry
process_atom(atom, src)
end
--- @type integer, AtomEntry
for _, atom in ipairs(scan.raw_atoms or {}) do
for _, atom in ipairs(scan.raw_atoms or {}) do ---@type integer, AtomEntry
process_atom(atom, src)
end
end
+37 -74
View File
@@ -20,24 +20,19 @@
-- 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
-- ════════════════════════════════════════════════════════════════════════════
-- Offset macro/enum naming prefixes (the emitted header uses these).
--- @type string
local OFFSET_MACRO_PREFIX = "_atom_offset_"
--- @type string
local OFFSET_ENUM_PREFIX = "atom_offset_"
local OFFSET_MACRO_PREFIX = "_atom_offset_" ---@type string
local OFFSET_ENUM_PREFIX = "atom_offset_" ---@type string
-- Column width for the `#define _atom_offset_F_T = N` alignment.
--- @type integer
local OFFSET_MACRO_COL = 44
local OFFSET_MACRO_COL = 44 ---@type integer
-- ════════════════════════════════════════════════════════════════════════════
-- Type declarations
@@ -91,8 +86,7 @@ local OFFSET_MACRO_COL = 44
-- MARKER_PROJECTORS is the marker-kind data table.
-- The emission-model pass already records marker word positions + consuming-instruction context;
-- this pass only projects those records into the label/branch lookup shape needed by offset computation.
--- @type table<string, fun(state: MarkerProjectState, marker: EmissionMarker): nil>
local MARKER_PROJECTORS = {
local MARKER_PROJECTORS = { ---@type table<string, fun(state: MarkerProjectState, marker: EmissionMarker): nil>
--- @param state MarkerProjectState
--- @param marker EmissionMarker
--- @return nil
@@ -119,12 +113,9 @@ local MARKER_PROJECTORS = {
--- @return table<string, integer>
--- @return OffsetBranch[]
local function project_markers(markers)
--- @type MarkerProjectState
local state = { labels = {}, branches = {} }
--- @type integer, EmissionMarker
for _, marker in ipairs(markers or {}) do
--- @type (fun(state: MarkerProjectState, marker: EmissionMarker): nil)|nil
local project = MARKER_PROJECTORS[marker.kind]
local state = { labels = {}, branches = {} } ---@type MarkerProjectState
for _, marker in ipairs(markers or {}) do ---@type integer, EmissionMarker
local project = MARKER_PROJECTORS[marker.kind] ---@type (fun(state: MarkerProjectState, marker: EmissionMarker): nil)|nil
if project then project(state, marker) end
end
return state.labels, state.branches
@@ -149,20 +140,16 @@ end
--- @param errors PassFinding[]
--- @return BranchOffset[]
local function compute_offsets(labels, branches, errors)
--- @type BranchOffset[]
local results = {}
--- @type integer, OffsetBranch
for _, br in ipairs(branches) do
--- @type integer|nil
local target = labels[br.target]
local results = {} ---@type BranchOffset[]
for _, br in ipairs(branches) do ---@type integer, OffsetBranch
local target = labels[br.target] ---@type integer|nil
if not target then
errors[#errors + 1] = {
line = br.line or 0,
msg = "Branch target '" .. br.target .. "' has no atom_label (at word " .. br.branch_word .. ")",
}
else
--- @type string|nil
local consuming = br.consuming_encoder
local consuming = br.consuming_encoder ---@type string|nil
if consuming == nil or consuming == "" then
errors[#errors + 1] = {
line = br.line or 0,
@@ -218,20 +205,16 @@ local function emit_atom_offsets(add, atom)
if #atom.offsets == 0 then return end
add("// --- atom: " .. atom.name .. " (" .. atom.total_words .. " words) ---")
add("")
--- @type OffsetConst[]
local consts = {}
--- @type integer, BranchOffset
for _, r in ipairs(atom.offsets) do
local consts = {} ---@type OffsetConst[]
for _, r in ipairs(atom.offsets) do ---@type integer, BranchOffset
consts[#consts + 1] = make_offset_const(r)
end
--- @type integer, OffsetConst
for _, c in ipairs(consts) do
for _, c in ipairs(consts) do ---@type integer, OffsetConst
add("#define " .. pad_right(c.macro_name, OFFSET_MACRO_COL) .. " " .. c.value)
end
add("")
add("enum {")
--- @type integer, OffsetConst
for _, c in ipairs(consts) do
for _, c in ipairs(consts) do ---@type integer, OffsetConst
add(" " .. c.enum_name .. " = " .. c.macro_name .. ",")
end
add("};")
@@ -244,19 +227,16 @@ end
--- @param atoms_data AtomData[]
--- @return string
local function generate_header(dir, sources, atoms_data)
--- @type string
local dir_basename = duffle.basename_no_ext(dir)
local dir_basename = duffle.basename_no_ext(dir) ---@type string
--- @type string[]
local lines = {}
local lines = {} ---@type string[]
--- @param s string
--- @return nil
local function add(s) lines[#lines + 1] = s end
add("// Auto-generated by ps1_meta.lua (passes/offsets.lua) — DO NOT EDIT")
add("// Directory: " .. dir:gsub("/", "\\") .. "\\")
--- @type integer, SourceFile
for _, src in ipairs(sources) do
for _, src in ipairs(sources) do ---@type integer, SourceFile
add("// source: " .. src.path:gsub("/", "\\"))
end
add("#pragma once")
@@ -264,8 +244,7 @@ local function generate_header(dir, sources, atoms_data)
add("#pragma region " .. dir_basename)
add("")
add("")
--- @type integer, AtomData
for _, atom in ipairs(atoms_data) do
for _, atom in ipairs(atoms_data) do ---@type integer, AtomData
emit_atom_offsets(add, atom)
end
add("#pragma endregion " .. dir_basename)
@@ -273,8 +252,7 @@ local function generate_header(dir, sources, atoms_data)
return table.concat(lines, "\n") .. "\n"
end
--- @type OffsetsPass
local M = {}
local M = {} ---@type OffsetsPass
--- (internal) Aggregate atoms from every source in one directory, render the per-directory `offsets.h`.
--- Returns the offsets_h path if a header was written, or nil.
@@ -284,17 +262,14 @@ local M = {}
--- @param errors PassFinding[]
--- @return string|nil
local function process_directory(ctx, dir, sources, errors)
--- @type AtomData[]
local atoms_data = {}
local atoms_data = {} ---@type AtomData[]
--- @param atom AtomEntry
--- @return nil
local function append_atom(atom)
--- @type AtomPaths|nil
local paths = atom and atom.paths
local paths = atom and atom.paths ---@type AtomPaths|nil
if not paths then return end
--- @type table<string, integer>, OffsetBranch[]
local labels, branches = project_markers(paths.markers)
local labels, branches = project_markers(paths.markers) ---@type table<string, integer>, OffsetBranch[]
atoms_data[#atoms_data + 1] = {
name = atom.raw_name or atom.name,
total_words = #(paths.word_events or {}),
@@ -302,19 +277,14 @@ local function process_directory(ctx, dir, sources, errors)
}
end
--- @type integer, SourceFile
for _, src in ipairs(sources) do
--- @type SourceScan
local scan = src.scan or {}
--- @type integer, AtomEntry
for _, atom in ipairs(scan.atoms or {}) do append_atom(atom) end
--- @type integer, AtomEntry
for _, atom in ipairs(scan.raw_atoms or {}) do append_atom(atom) end
for _, src in ipairs(sources) do ---@type integer, SourceFile
local scan = src.scan or {} ---@type SourceScan
for _, atom in ipairs(scan.atoms or {}) do append_atom(atom) end ---@type integer, AtomEntry
for _, atom in ipairs(scan.raw_atoms or {}) do append_atom(atom) end ---@type integer, AtomEntry
end
if #atoms_data == 0 then return nil end
--- @type string
local out_path = dir .. "/gen/offsets.h"
local out_path = dir .. "/gen/offsets.h" ---@type string
duffle.ensure_dir(duffle.dirname(out_path))
duffle.write_file(out_path, generate_header(dir, sources, atoms_data))
return out_path
@@ -326,15 +296,11 @@ end
--- @param ctx PassCtx
--- @return PassResult
function M.run(ctx)
--- @type OffsetOutput[]
local outputs = {}
--- @type PassFinding[]
local errors = {}
--- @type PassFinding[]
local warnings = {}
local outputs = {} ---@type OffsetOutput[]
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("offsets.run requires ctx.shared.corpus", 0)
end
@@ -343,12 +309,9 @@ function M.run(ctx)
end
-- Per-directory aggregation: every source in the same directory contributes to one `gen/offsets.h`.
--- @type table<string, SourceFile[]>
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
--- @type string|nil
local out_path = process_directory(ctx, dir, sources, errors)
local sources_by_dir = corpus.sources_by_dir or duffle.group_sources_by_dir(corpus.source_order) ---@type table<string, SourceFile[]>
for dir, sources in pairs(sources_by_dir) do ---@type string, SourceFile[]
local out_path = process_directory(ctx, dir, sources, errors) ---@type string|nil
if out_path then
outputs[#outputs + 1] = { offsets_h = out_path }
end
+186 -372
View File
@@ -18,16 +18,13 @@
-- 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
-- Load atoms_source_map for the `render_source_map` / `render_provenance` module functions (used by `render_module_atoms_md` to produce `<module>.atoms.md` without re-walking source tokens).
-- The pass itself emits no per-source files anymore; we only consume the two pure renderers here.
-- Defined BEFORE the renderer functions below so their upvalues resolve to this local (not the global `atoms_source_map`, which is nil).
--- @type AtomSourceMapPass
local atoms_source_map = dofile(_bootstrap_dir .. "atoms_source_map.lua")
local atoms_source_map = dofile(_bootstrap_dir .. "atoms_source_map.lua") ---@type AtomSourceMapPass
-- ════════════════════════════════════════════════════════════════════════════
-- Constants
@@ -35,32 +32,22 @@ local atoms_source_map = dofile(_bootstrap_dir .. "atoms_source_map.lua")
-- Section separators used in the rendered text reports.
-- The thin rules are hand-tuned to align with the per-section content width; do not change without also checking the section renderers below.
--- @type string
local RULE_THICK = "========================================================"
--- @type string
local SECTION_HEADER_ATOMS = "── Atoms ────────────────────────────────────────────────"
--- @type string
local SECTION_HEADER_ANNOTS = "── Annotations ──────────────────────────────────────────"
--- @type string
local SECTION_HEADER_BINDS = "── Binds_* structs ──────────────────────────────────────"
--- @type string
local SECTION_HEADER_MACROS = "── Macro word-count declarations ─────────────────────────"
--- @type string
local SECTION_HEADER_ERRORS = "── Errors ──────────────────────────────────────────────"
--- @type string
local SECTION_HEADER_WARNINGS = "── Warnings ────────────────────────────────────────────"
local RULE_THICK = "========================================================" ---@type string
local SECTION_HEADER_ATOMS = "── Atoms ────────────────────────────────────────────────" ---@type string
local SECTION_HEADER_ANNOTS = "── Annotations ──────────────────────────────────────────" ---@type string
local SECTION_HEADER_BINDS = "── Binds_* structs ──────────────────────────────────────" ---@type string
local SECTION_HEADER_MACROS = "── Macro word-count declarations ─────────────────────────" ---@type string
local SECTION_HEADER_ERRORS = "── Errors ──────────────────────────────────────────────" ---@type string
local SECTION_HEADER_WARNINGS = "── Warnings ────────────────────────────────────────────" ---@type string
-- Lua pattern that captures the basename (last path segment) of a forward- or back-slash separated path.
--- @type string
local BASENAME_PATTERN = "([^/\\]+)$"
local BASENAME_PATTERN = "([^/\\]+)$" ---@type string
-- Debug flag name — set to truthy in `_G` to enable verbose logging.
--- @type string
local DEBUG_FLAG = "_DEBUG_REPORT"
local DEBUG_FLAG = "_DEBUG_REPORT" ---@type string
-- Pass identifier for log messages.
--- @type string
local PASS_NAME = "report"
local PASS_NAME = "report" ---@type string
-- ════════════════════════════════════════════════════════════════════════════
-- Type declarations
@@ -245,18 +232,15 @@ end
--- @param all_results ProjectSummaryRow[]
--- @return string
local function render_project_summary(all_results)
--- @type string[]
local lines = {
local lines = { ---@type string[]
"# Project summary",
"> Auto-generated by ps1_meta.lua (passes/report.lua).",
"",
"| module | atoms | annots | binds | macros | findings | errors | warnings | info |",
"|--------|-------|--------|-------|--------|----------|--------|----------|------|",
}
--- @type ProjectSummaryTotals
local totals = { atoms = 0, annots = 0, binds = 0, macros = 0, findings = 0, errors = 0, warnings = 0, info = 0 }
--- @type integer, ProjectSummaryRow
for _, e in ipairs(all_results) do
local totals = { atoms = 0, annots = 0, binds = 0, macros = 0, findings = 0, errors = 0, warnings = 0, info = 0 } ---@type ProjectSummaryTotals
for _, e in ipairs(all_results) do ---@type integer, ProjectSummaryRow
lines[#lines + 1] = string.format("| %s | %d | %d | %d | %d | %d | %d | %d | %d |"
, e.module, e.atoms, e.annots, e.binds, e.macros, e.findings, e.errors, e.warnings, e.info)
totals.atoms = totals.atoms + e.atoms
@@ -281,29 +265,22 @@ end
--- @param wc WordCounts
--- @return string
local function render_module_atoms_md(dir, dir_sources, wc)
--- @type string
local dir_basename = source_basename(dir)
--- @type string[]
local lines = {
local dir_basename = source_basename(dir) ---@type string
local lines = { ---@type string[]
"# " .. dir_basename .. " — atoms (verbose source map)",
"> Per-word call-site + provenance. Auto-generated.",
"",
}
--- @type integer, SourceFile
for _, src in ipairs(dir_sources) do
--- @type string
local src_name = source_basename(src.path)
for _, src in ipairs(dir_sources) do ---@type integer, SourceFile
local src_name = source_basename(src.path) ---@type string
lines[#lines + 1] = "## " .. src_name
lines[#lines + 1] = ""
-- For each atom with a projection, render its sourcemap + provenance.
--- @type AtomEntry[]
local atoms_list = {}
--- @type integer, AtomEntry
for _, atom in ipairs((src.scan or {}).atoms or {}) do
local atoms_list = {} ---@type AtomEntry[]
for _, atom in ipairs((src.scan or {}).atoms or {}) do ---@type integer, AtomEntry
if atom.paths then atoms_list[#atoms_list + 1] = atom end
end
--- @type integer, AtomEntry
for _, atom in ipairs((src.scan or {}).raw_atoms or {}) do
for _, atom in ipairs((src.scan or {}).raw_atoms or {}) do ---@type integer, AtomEntry
if atom.paths then atoms_list[#atoms_list + 1] = atom end
end
if #atoms_list == 0 then
@@ -312,10 +289,8 @@ local function render_module_atoms_md(dir, dir_sources, wc)
else
-- Per-source forward-slash path (same one `emit_atom_stanza` / `emit_provenance_stanza` would derive;
-- computed once per `## <source>` heading and reused by each atom's `WORD N CALL ...` field).
--- @type string
local rel_path = src.path:gsub("\\\\", "/")
--- @type integer, AtomEntry
for _, atom in ipairs(atoms_list) do
local rel_path = src.path:gsub("\\\\", "/") ---@type string
for _, atom in ipairs(atoms_list) do ---@type integer, AtomEntry
lines[#lines + 1] = string.format(
"### atom: %s (line %d, %d words)",
atom.name, atom.line or 0, #((atom.paths or {}).word_events or {}))
@@ -342,18 +317,15 @@ end
--- @param atom AtomEntry
--- @return integer
local function decl_words(atom)
--- @type AtomPaths
local p = atom.paths or {}
local p = atom.paths or {} ---@type AtomPaths
return #(p.word_events or {})
end
--- @param decls AtomEntry[]|nil
--- @return KindCounts
local function count_kinds(decls)
--- @type KindCounts
local n = { atom = 0, atom_proc = 0, comp_bare = 0, comp_proc = 0 }
--- @type integer, AtomEntry
for _, a in ipairs(decls or {}) do
local n = { atom = 0, atom_proc = 0, comp_bare = 0, comp_proc = 0 } ---@type KindCounts
for _, a in ipairs(decls or {}) do ---@type integer, AtomEntry
if n[a.kind] ~= nil then n[a.kind] = n[a.kind] + 1 end
end
return n
@@ -369,10 +341,8 @@ end
--- @param view ModuleView
--- @return table<string, boolean>
local function decl_names(view)
--- @type table<string, boolean> -- bag: atom name -> true
local names = {}
--- @type integer, AtomEntry
for _, a in ipairs(view.decls or {}) do
local names = {} ---@type table<string, boolean> -- bag: atom name -> true
for _, a in ipairs(view.decls or {}) do ---@type integer, AtomEntry
if a.name then names[a.name] = true end
end
return names
@@ -383,15 +353,12 @@ end
--- @return boolean
local function path_in_module(path, view)
if type(path) ~= "string" or path == "" then return false end
--- @type string
local norm = path:gsub("\\", "/")
--- @type string
local dir = (view.dir or ""):gsub("\\", "/")
local norm = path:gsub("\\", "/") ---@type string
local dir = (view.dir or ""):gsub("\\", "/") ---@type string
if dir ~= "" and (norm == dir or norm:sub(1, #dir + 1) == dir .. "/") then
return true
end
--- @type integer, SourceFile
for _, src in ipairs(view.sources or {}) do
for _, src in ipairs(view.sources or {}) do ---@type integer, SourceFile
if (src.path or ""):gsub("\\", "/") == norm then return true end
end
return false
@@ -402,26 +369,18 @@ end
--- @param corpus Corpus
--- @return ModuleView
local function build_module_view(dir, dir_sources, corpus)
--- @type AtomEntry[]
local decls = {}
--- @type integer, SourceFile
for _, src in ipairs(dir_sources or {}) do
--- @type integer, AtomEntry
for _, a in ipairs((src.scan and src.scan.atoms) or {}) do
local decls = {} ---@type AtomEntry[]
for _, src in ipairs(dir_sources or {}) do ---@type integer, SourceFile
for _, a in ipairs((src.scan and src.scan.atoms) or {}) do ---@type integer, AtomEntry
if not a.source_path then a.source_path = src.path end
decls[#decls + 1] = a
end
end
--- @type string
local dir_basename = source_basename(dir)
--- @type AtomAnalysis
local sa = (corpus.static_analysis_results or {})[dir_basename] or {}
--- @type RegUseSchema[]
local schemas = {}
--- @type string, RegUseSchema
for name, schema in pairs(corpus.reg_use_schemas or {}) do
--- @type integer, AtomEntry
for _, a in ipairs(decls) do
local dir_basename = source_basename(dir) ---@type string
local sa = (corpus.static_analysis_results or {})[dir_basename] or {} ---@type AtomAnalysis
local schemas = {} ---@type RegUseSchema[]
for name, schema in pairs(corpus.reg_use_schemas or {}) do ---@type string, RegUseSchema
for _, a in ipairs(decls) do ---@type integer, AtomEntry
if a.reg_use_schema_name == name then
schemas[#schemas + 1] = schema
break
@@ -445,10 +404,8 @@ local function render_section_declarations(add, view)
if #view.decls == 0 then add("_(none)_"); add(""); return end
add("| kind | name | source | line | words | min | max | branches | paths |")
add("|------|------|--------|------|-------|-----|-----|----------|-------|")
--- @type integer, AtomEntry
for _, a in ipairs(view.decls) do
--- @type AtomPaths
local p = a.paths or {}
for _, a in ipairs(view.decls) do ---@type integer, AtomEntry
local p = a.paths or {} ---@type AtomPaths
add(string.format("| %s | %s | %s | %d | %d | %s | %s | %s | %s |",
a.kind or "?",
a.name or "?",
@@ -467,17 +424,12 @@ end
--- @param view ModuleView
--- @return nil
local function render_section_components(add, view)
--- @type ComponentReportRow[]
local rows = {}
--- @type table<string, ComponentBodyEntry>
local index = (view.corpus and view.corpus.component_body_index) or {}
--- @type integer, AtomEntry
for _, a in ipairs(view.decls) do
local rows = {} ---@type ComponentReportRow[]
local index = (view.corpus and view.corpus.component_body_index) or {} ---@type table<string, ComponentBodyEntry>
for _, a in ipairs(view.decls) do ---@type integer, AtomEntry
if a.kind == "comp_bare" or a.kind == "comp_proc" then
--- @type ComponentBodyEntry
local idx = index[a.name] or {}
--- @type string[]
local args = idx.arg_names or {}
local idx = index[a.name] or {} ---@type ComponentBodyEntry
local args = idx.arg_names or {} ---@type string[]
rows[#rows + 1] = {
name = a.name,
kind = a.kind,
@@ -490,8 +442,7 @@ local function render_section_components(add, view)
if #rows == 0 then add("_(none)_"); add(""); return end
add("| name | kind | arg_names | words | map |")
add("|------|------|-----------|-------|-----|")
--- @type integer, ComponentReportRow
for _, r in ipairs(rows) do
for _, r in ipairs(rows) do ---@type integer, ComponentReportRow
add(string.format("| %s | %s | %s | %d | %s |",
r.name, r.kind, r.args ~= "" and r.args or "", r.words, r.map))
end
@@ -502,38 +453,28 @@ end
--- @param view ModuleView
--- @return nil
local function render_section_reguse(add, view)
--- @type boolean
local wrote = false
--- @type integer, RegUseSchema
for _, schema in ipairs(view.schemas or {}) do
local wrote = false ---@type boolean
for _, schema in ipairs(view.schemas or {}) do ---@type integer, RegUseSchema
wrote = true
add(string.format("### %s", schema.name or "?"))
--- @type integer, RegUseSlot
for _, slot in ipairs(schema.slots or {}) do
--- @type string
local aliases = table.concat(slot.aliases or { slot.name }, ", ")
--- @type string
local ro = slot.readonly and " readonly" or ""
for _, slot in ipairs(schema.slots or {}) do ---@type integer, RegUseSlot
local aliases = table.concat(slot.aliases or { slot.name }, ", ") ---@type string
local ro = slot.readonly and " readonly" or "" ---@type string
add(string.format("- slot `%s` aliases %s%s", slot.name, aliases, ro))
end
--- @type integer, AtomEntry
for _, a in ipairs(view.decls) do
for _, a in ipairs(view.decls) do ---@type integer, AtomEntry
if a.reg_use_schema_name == schema.name then
add(string.format("- bound `%s` param `%s`", a.name, a.reg_use_param_name or "?"))
end
end
add("")
end
--- @type table<string, boolean> -- bag: schema name -> true
local bound = {}
--- @type integer, RegUseSchema
for _, schema in ipairs(view.schemas or {}) do
local bound = {} ---@type table<string, boolean> -- bag: schema name -> true
for _, schema in ipairs(view.schemas or {}) do ---@type integer, RegUseSchema
if schema.name then bound[schema.name] = true end
end
--- @type RegUseError[]
local errors = {}
--- @type integer, RegUseError
for _, err in ipairs((view.corpus and view.corpus.reg_use_errors) or {}) do
local errors = {} ---@type RegUseError[]
for _, err in ipairs((view.corpus and view.corpus.reg_use_errors) or {}) do ---@type integer, RegUseError
if bound[err.schema_name] or path_in_module(err.source_file, view) then
errors[#errors + 1] = err
end
@@ -541,8 +482,7 @@ local function render_section_reguse(add, view)
if #errors > 0 then
wrote = true
add("### parse errors")
--- @type integer, RegUseError
for _, err in ipairs(errors) do
for _, err in ipairs(errors) do ---@type integer, RegUseError
add(string.format("- `%s` %s", err.kind or "?", err.schema_name or ""))
end
add("")
@@ -554,12 +494,9 @@ end
--- @param view ModuleView
--- @return nil
local function render_section_annotations(add, view)
--- @type AnnotReportRow[]
local rows = {}
--- @type integer, SourceFile
for _, src in ipairs(view.sources) do
--- @type integer, AtomInfoEntry
for _, info in ipairs((src.scan and src.scan.atom_infos) or {}) do
local rows = {} ---@type AnnotReportRow[]
for _, src in ipairs(view.sources) do ---@type integer, SourceFile
for _, info in ipairs((src.scan and src.scan.atom_infos) or {}) do ---@type integer, AtomInfoEntry
rows[#rows + 1] = {
source = source_basename(src.path),
line = info.info_line or 0,
@@ -574,8 +511,7 @@ local function render_section_annotations(add, view)
if #rows == 0 then add("_(none)_"); add(""); return end
add("| source | line | name | binds | reads | writes | phase |")
add("|--------|------|------|-------|-------|--------|-------|")
--- @type integer, AnnotReportRow
for _, r in ipairs(rows) do
for _, r in ipairs(rows) do ---@type integer, AnnotReportRow
add(string.format("| %s | %d | %s | %s | %s | %s | %s |",
r.source, r.line, r.name, r.binds, r.reads, r.writes, r.phase))
end
@@ -586,12 +522,9 @@ end
--- @param view ModuleView
--- @return nil
local function render_section_component_annotations(add, view)
--- @type CompAnnotReportRow[]
local rows = {}
--- @type integer, SourceFile
for _, src in ipairs(view.sources) do
--- @type integer, AtomInfoEntry
for _, info in ipairs((src.scan and src.scan.component_atom_infos) or {}) do
local rows = {} ---@type CompAnnotReportRow[]
for _, src in ipairs(view.sources) do ---@type integer, SourceFile
for _, info in ipairs((src.scan and src.scan.component_atom_infos) or {}) do ---@type integer, AtomInfoEntry
rows[#rows + 1] = {
source = source_basename(src.path),
line = info.info_line or 0,
@@ -604,8 +537,7 @@ local function render_section_component_annotations(add, view)
if #rows == 0 then add("_(none)_"); add(""); return end
add("| source | line | name | reads | writes |")
add("|--------|------|------|-------|--------|")
--- @type integer, CompAnnotReportRow
for _, r in ipairs(rows) do
for _, r in ipairs(rows) do ---@type integer, CompAnnotReportRow
add(string.format("| %s | %d | %s | %s | %s |",
r.source, r.line, r.name, r.reads, r.writes))
end
@@ -616,17 +548,13 @@ end
--- @param view ModuleView
--- @return nil
local function render_section_binds(add, view)
--- @type boolean
local wrote = false
--- @type integer, SourceFile
for _, src in ipairs(view.sources) do
--- @type integer, BindsEntry
for _, b in ipairs((src.scan and src.scan.binds) or {}) do
local wrote = false ---@type boolean
for _, src in ipairs(view.sources) do ---@type integer, SourceFile
for _, b in ipairs((src.scan and src.scan.binds) or {}) do ---@type integer, BindsEntry
wrote = true
add(string.format("### %s (%s:%s, %s bytes)",
b.name, source_basename(src.path), tostring(b.line or 0), tostring(b.bytes or "")))
--- @type integer, TypeField
for _, f in ipairs(b.fields or {}) do
for _, f in ipairs(b.fields or {}) do ---@type integer, TypeField
add(string.format("- `+%s %s`", tostring(f.offset or "?"), f.name or "?"))
end
add("")
@@ -639,18 +567,12 @@ end
--- @param view ModuleView
--- @return nil
local function render_section_phases(add, view)
--- @type Corpus
local corpus = view.corpus or {}
--- @type table<string, boolean>
local names = decl_names(view)
--- @type boolean
local wrote = false
--- @type string, AtomPhaseGroup
for phase, entry in pairs(corpus.atom_phases or {}) do
--- @type string[]
local here = {}
--- @type integer, string
for _, atom_name in ipairs(entry.atoms or {}) do
local corpus = view.corpus or {} ---@type Corpus
local names = decl_names(view) ---@type table<string, boolean>
local wrote = false ---@type boolean
for phase, entry in pairs(corpus.atom_phases or {}) do ---@type string, AtomPhaseGroup
local here = {} ---@type string[]
for _, atom_name in ipairs(entry.atoms or {}) do ---@type integer, string
if names[atom_name] then here[#here + 1] = atom_name end
end
if #here > 0 then
@@ -658,15 +580,13 @@ local function render_section_phases(add, view)
add(string.format("- phase `%s`: %s", phase, table.concat(here, ", ")))
end
end
--- @type string, AtomViewEntry
for name, entry in pairs(corpus.atom_views or {}) do
for name, entry in pairs(corpus.atom_views or {}) do ---@type string, AtomViewEntry
if names[name] then
wrote = true
add(string.format("- view `%s` binds `%s`", name, entry.binds_name or ""))
end
end
--- @type string, AtomCtxEntry
for name, entry in pairs(corpus.atom_ctxs or {}) do
for name, entry in pairs(corpus.atom_ctxs or {}) do ---@type string, AtomCtxEntry
if names[name] then
wrote = true
add(string.format("- ctx `%s` rbind `%s`", name, entry.rbind_atom or ""))
@@ -680,14 +600,10 @@ end
--- @param view ModuleView
--- @return nil
local function render_section_aliases(add, view)
--- @type string[]
local names = {}
--- @type table<string, AliasEntry>
local seen = {}
--- @type integer, SourceFile
for _, src in ipairs(view.sources or {}) do
--- @type string, AliasEntry
for name, entry in pairs((src.scan and src.scan.register_alias_registry) or {}) do
local names = {} ---@type string[]
local seen = {} ---@type table<string, AliasEntry>
for _, src in ipairs(view.sources or {}) do ---@type integer, SourceFile
for name, entry in pairs((src.scan and src.scan.register_alias_registry) or {}) do ---@type string, AliasEntry
if not seen[name] then
seen[name] = entry
names[#names + 1] = name
@@ -698,10 +614,8 @@ local function render_section_aliases(add, view)
if #names == 0 then add("_(none)_"); add(""); return end
add("| alias | type |")
add("|-------|------|")
--- @type integer, string
for _, name in ipairs(names) do
--- @type AliasEntry|nil
local e = seen[name]
for _, name in ipairs(names) do ---@type integer, string
local e = seen[name] ---@type AliasEntry|nil
add(string.format("| %s | %s |", name, (e and e.default_type) or ""))
end
add("")
@@ -711,40 +625,30 @@ end
--- @param view ModuleView
--- @return nil
local function render_section_autoreg(add, view)
--- @type table<string, boolean>
local allowed = decl_names(view)
--- @type string, AtomPhaseGroup
for phase, entry in pairs((view.corpus and view.corpus.atom_phases) or {}) do
--- @type integer, string
for _, atom_name in ipairs(entry.atoms or {}) do
local allowed = decl_names(view) ---@type table<string, boolean>
for phase, entry in pairs((view.corpus and view.corpus.atom_phases) or {}) do ---@type string, AtomPhaseGroup
for _, atom_name in ipairs(entry.atoms or {}) do ---@type integer, string
if allowed[atom_name] then allowed[phase] = true end
end
end
--- @type boolean
local wrote = false
--- @type table<string, boolean> -- bag: label\\0scope -> already dumped
local seen = {}
local wrote = false ---@type boolean
local seen = {} ---@type table<string, boolean> -- bag: label\\0scope -> already dumped
--- @param label string
--- @param table_map table<string, GprAllocMap>|nil
--- @return nil
local function dump(label, table_map)
--- @type string[]
local scopes = {}
--- @type string
for scope in pairs(table_map or {}) do
local scopes = {} ---@type string[]
for scope in pairs(table_map or {}) do ---@type string
if allowed[scope] and not seen[label .. "\0" .. scope] then
scopes[#scopes + 1] = scope
end
end
table.sort(scopes)
--- @type integer, string
for _, scope in ipairs(scopes) do
for _, scope in ipairs(scopes) do ---@type integer, string
seen[label .. "\0" .. scope] = true
wrote = true
--- @type string[]
local syms = {}
--- @type string, string
for sym, gpr in pairs(table_map[scope] or {}) do
local syms = {} ---@type string[]
for sym, gpr in pairs(table_map[scope] or {}) do ---@type string, string
if type(gpr) == "string" and gpr ~= sym then
syms[#syms + 1] = string.format("%s → %s", sym, gpr)
else
@@ -755,12 +659,10 @@ local function render_section_autoreg(add, view)
add(string.format("- %s `%s`: %s", label, scope, table.concat(syms, ", ")))
end
end
--- @type Corpus
local corpus = view.corpus or {}
local corpus = view.corpus or {} ---@type Corpus
dump("atom", corpus.atom_auto_regs)
dump("phase", corpus.phase_auto_regs)
--- @type integer, SourceFile
for _, src in ipairs(view.sources or {}) do
for _, src in ipairs(view.sources or {}) do ---@type integer, SourceFile
dump("atom", src.scan and src.scan.atom_auto_regs)
dump("phase", src.scan and src.scan.phase_auto_regs)
end
@@ -772,25 +674,18 @@ end
--- @param view ModuleView
--- @return nil
local function render_section_collisions(add, view)
--- @type CorpusCollision[]
local rows = {}
--- @type integer, CorpusCollision
for _, c in ipairs((view.corpus and view.corpus.collisions) or {}) do
--- @type CollisionSite
local first = c.first_site or {}
--- @type CollisionSite
local other = c.conflicting_site or {}
local rows = {} ---@type CorpusCollision[]
for _, c in ipairs((view.corpus and view.corpus.collisions) or {}) do ---@type integer, CorpusCollision
local first = c.first_site or {} ---@type CollisionSite
local other = c.conflicting_site or {} ---@type CollisionSite
if path_in_module(first.path, view) or path_in_module(other.path, view) then
rows[#rows + 1] = c
end
end
if #rows == 0 then add("_(none)_"); add(""); return end
--- @type integer, CorpusCollision
for _, c in ipairs(rows) do
--- @type CollisionSite
local first = c.first_site or {}
--- @type CollisionSite
local other = c.conflicting_site or {}
for _, c in ipairs(rows) do ---@type integer, CorpusCollision
local first = c.first_site or {} ---@type CollisionSite
local other = c.conflicting_site or {} ---@type CollisionSite
add(string.format("- `%s` `%s` first %s:%s conflict %s:%s",
c.kind or "?", c.name or "?",
tostring(first.path or "?"), tostring(first.line or "?"),
@@ -803,29 +698,22 @@ end
--- @param view ModuleView
--- @return nil
local function render_section_findings(add, view)
--- @type table<string, CheckFinding[]>
local by_atom = {}
--- @type integer, CheckFinding
for _, f in ipairs(view.findings or {}) do
--- @type string
local key = f.atom or "?"
local by_atom = {} ---@type table<string, CheckFinding[]>
for _, f in ipairs(view.findings or {}) do ---@type integer, CheckFinding
local key = f.atom or "?" ---@type string
by_atom[key] = by_atom[key] or {}
by_atom[key][#by_atom[key] + 1] = f
end
if next(by_atom) == nil then add("_(none)_"); add(""); return end
--- @type table<string, boolean> -- bag: atom name already emitted
local seen = {}
local seen = {} ---@type table<string, boolean> -- bag: atom name already emitted
--- @param name string
--- @param fs CheckFinding[]
--- @return nil
local function emit(name, fs)
add("### " .. name)
--- @type integer, CheckFinding
for _, f in ipairs(fs) do
--- @type string
local msg = f.msg or ""
--- @type string|nil
local slot = slot_suffix(f.gpr_key or f.producer_destination)
for _, f in ipairs(fs) do ---@type integer, CheckFinding
local msg = f.msg or "" ---@type string
local slot = slot_suffix(f.gpr_key or f.producer_destination) ---@type string|nil
if slot and not msg:find("(slot ", 1, true) then
msg = msg .. " (slot " .. slot .. ")"
end
@@ -833,45 +721,34 @@ local function render_section_findings(add, view)
end
add("")
end
--- @type integer, AtomEntry
for _, a in ipairs(view.decls) do
for _, a in ipairs(view.decls) do ---@type integer, AtomEntry
if by_atom[a.name] then
seen[a.name] = true
emit(a.name, by_atom[a.name])
end
end
--- @type string[]
local leftovers = {}
--- @type string
for name in pairs(by_atom) do
local leftovers = {} ---@type string[]
for name in pairs(by_atom) do ---@type string
if not seen[name] then leftovers[#leftovers + 1] = name end
end
table.sort(leftovers)
--- @type integer, string
for _, name in ipairs(leftovers) do emit(name, by_atom[name]) end
for _, name in ipairs(leftovers) do emit(name, by_atom[name]) end ---@type integer, string
end
--- @param add fun(s: string): nil
--- @param view ModuleView
--- @return nil
local function render_section_relations(add, view)
--- @type boolean
local wrote = false
--- @type integer, AtomEntry
for _, a in ipairs(view.decls) do
--- @type AtomRelation[]
local rels = (a.paths and a.paths.relations) or {}
local wrote = false ---@type boolean
for _, a in ipairs(view.decls) do ---@type integer, AtomEntry
local rels = (a.paths and a.paths.relations) or {} ---@type AtomRelation[]
if #rels > 0 then
wrote = true
add("### " .. a.name)
--- @type integer, AtomRelation
for _, rel in ipairs(rels) do
--- @type string
local dest = rel.destination or rel.producer_destination or ""
--- @type string|nil
local slot = slot_suffix(dest)
--- @type string
local dest_s = tostring(dest)
for _, rel in ipairs(rels) do ---@type integer, AtomRelation
local dest = rel.destination or rel.producer_destination or "" ---@type string
local slot = slot_suffix(dest) ---@type string|nil
local dest_s = tostring(dest) ---@type string
if slot then dest_s = dest_s .. " (slot " .. slot .. ")" end
add(string.format("- `%s` words %s → %s dest %s",
rel.semantic or "?",
@@ -885,13 +762,11 @@ local function render_section_relations(add, view)
if not wrote then add("_(none)_"); add("") end
end
--- @type table<string, boolean> -- bag: GPR key hidden unless an encoder wrote it
local HIDDEN_UNLESS_WRITTEN = {
local HIDDEN_UNLESS_WRITTEN = { ---@type table<string, boolean> -- bag: GPR key hidden unless an encoder wrote it
R_AT = true, R_TapePtr = true, R_AtomJmp = true,
}
--- @type table<string, boolean> -- bag: physical GPR alias -> true
local PHYSICAL_GPR = {
local PHYSICAL_GPR = { ---@type table<string, boolean> -- bag: physical GPR alias -> true
R_T0 = true, R_T1 = true, R_T2 = true, R_T3 = true,
R_T4 = true, R_T5 = true, R_T6 = true, R_T7 = true,
R_V0 = true, R_V1 = true,
@@ -901,10 +776,8 @@ local PHYSICAL_GPR = {
--- @param key string
--- @return boolean
local function encoder_wrote_key(atom, key)
--- @type integer, WordEvent
for _, ev in ipairs((atom.paths and atom.paths.word_events) or {}) do
--- @type integer|string, string
for _, dest in pairs(ev.gpr_keys or {}) do
for _, ev in ipairs((atom.paths and atom.paths.word_events) or {}) do ---@type integer, WordEvent
for _, dest in pairs(ev.gpr_keys or {}) do ---@type integer|string, string
if dest == key then return true end
end
end
@@ -915,11 +788,9 @@ end
--- @param atom AtomEntry
--- @return string
local function written_name_for(key, atom)
--- @type string|nil
local slot = key:match("^reguse:.+:(.+)$")
local slot = key:match("^reguse:.+:(.+)$") ---@type string|nil
if slot then
--- @type string|nil
local param = atom.reg_use_param_name
local param = atom.reg_use_param_name ---@type string|nil
if param and param ~= "" then return param .. "." .. slot end
return slot
end
@@ -931,21 +802,15 @@ end
--- @param view ModuleView
--- @return string
local function aliases_for_key(key, atom, view)
--- @type string|nil
local slot = key:match("^reguse:.+:(.+)$")
local slot = key:match("^reguse:.+:(.+)$") ---@type string|nil
if not slot then return "" end
--- @type string|nil
local schema_name = atom.reg_use_schema_name
--- @type RegUseSchema|nil
local schema = view.corpus and view.corpus.reg_use_schemas and view.corpus.reg_use_schemas[schema_name]
local schema_name = atom.reg_use_schema_name ---@type string|nil
local schema = view.corpus and view.corpus.reg_use_schemas and view.corpus.reg_use_schemas[schema_name] ---@type RegUseSchema|nil
if not schema then return "" end
--- @type integer, RegUseSlot
for _, s in ipairs(schema.slots or {}) do
for _, s in ipairs(schema.slots or {}) do ---@type integer, RegUseSlot
if s.name == slot then
--- @type string[]
local names = {}
--- @type integer, string
for _, alias in ipairs(s.aliases or {}) do
local names = {} ---@type string[]
for _, alias in ipairs(s.aliases or {}) do ---@type integer, string
if alias ~= slot then names[#names + 1] = alias end
end
if #names == 0 then
@@ -964,25 +829,19 @@ end
--- @return string
local function physical_for_key(key, atom, view)
if PHYSICAL_GPR[key] then return key end
--- @type Corpus
local corpus = view.corpus or {}
--- @type AliasEntry|string|nil
local alias = (corpus.register_alias_registry or {})[key]
local corpus = view.corpus or {} ---@type Corpus
local alias = (corpus.register_alias_registry or {})[key] ---@type AliasEntry|string|nil
if type(alias) == "table" then
--- @type string|nil
local phys = alias.physical or alias.gpr or alias.code_name
local phys = alias.physical or alias.gpr or alias.code_name ---@type string|nil
if type(phys) == "string" and PHYSICAL_GPR[phys] then return phys end
if type(alias.name) == "string" and PHYSICAL_GPR[alias.name] then return alias.name end
elseif type(alias) == "string" and PHYSICAL_GPR[alias] then
return alias
end
--- @type GprAllocMap|nil
local atom_map = (corpus.atom_auto_regs or {})[atom.name]
local atom_map = (corpus.atom_auto_regs or {})[atom.name] ---@type GprAllocMap|nil
if type(atom_map) == "table" then
--- @type string
local slot = key:match("^reguse:.+:(.+)$") or key
--- @type string|nil
local bound = atom_map[slot] or atom_map["R_" .. slot]
local slot = key:match("^reguse:.+:(.+)$") or key ---@type string
local bound = atom_map[slot] or atom_map["R_" .. slot] ---@type string|nil
if type(bound) == "string" and PHYSICAL_GPR[bound] then return bound end
end
return ""
@@ -992,21 +851,15 @@ end
--- @param atom AtomEntry
--- @return string
local function last_relation_for(key, atom)
--- @type AtomRelation|nil
local last = nil
--- @type integer, AtomRelation
for _, rel in ipairs((atom.paths and atom.paths.relations) or {}) do
--- @type string|nil
local dest = rel.destination or rel.producer_destination
local last = nil ---@type AtomRelation|nil
for _, rel in ipairs((atom.paths and atom.paths.relations) or {}) do ---@type integer, AtomRelation
local dest = rel.destination or rel.producer_destination ---@type string|nil
if dest == key then last = rel end
end
if not last then return "" end
--- @type string
local sem = last.semantic or "?"
--- @type integer|nil
local a = last.producer_word
--- @type integer|nil
local b = last.consumer_word
local sem = last.semantic or "?" ---@type string
local a = last.producer_word ---@type integer|nil
local b = last.consumer_word ---@type integer|nil
if a and b then return string.format("%s w%s→%s", sem, tostring(a), tostring(b)) end
return sem
end
@@ -1015,16 +868,11 @@ end
--- @param view ModuleView
--- @return nil
local function render_section_forward(add, view)
--- @type boolean
local wrote = false
--- @type integer, AtomEntry
for _, a in ipairs(view.decls) do
--- @type table<string, GprLatticeSlot>|nil
local gpr = a.paths and a.paths.forward_state and a.paths.forward_state.gpr_values
--- @type string[]
local keys = {}
--- @type string
for k in pairs(gpr or {}) do
local wrote = false ---@type boolean
for _, a in ipairs(view.decls) do ---@type integer, AtomEntry
local gpr = a.paths and a.paths.forward_state and a.paths.forward_state.gpr_values ---@type table<string, GprLatticeSlot>|nil
local keys = {} ---@type string[]
for k in pairs(gpr or {}) do ---@type string
if k == "R_0" then
-- hidden
elseif HIDDEN_UNLESS_WRITTEN[k] and not encoder_wrote_key(a, k) then
@@ -1039,12 +887,9 @@ local function render_section_forward(add, view)
add("| written | aliases | physical | lattice | last relation |")
add("|---|---|---|---|---|")
table.sort(keys)
--- @type integer, string
for _, k in ipairs(keys) do
--- @type GprLatticeSlot|nil
local slot = gpr[k]
--- @type string
local lattice = ""
for _, k in ipairs(keys) do ---@type integer, string
local slot = gpr[k] ---@type GprLatticeSlot|nil
local lattice = "" ---@type string
if slot and slot.kind == "constant" then
lattice = tostring(slot.value)
end
@@ -1061,8 +906,7 @@ local function render_section_forward(add, view)
if not wrote then add("_(none)_"); add("") end
end
--- @type SectionRenderer[]
local SECTION_RENDERERS = {
local SECTION_RENDERERS = { ---@type SectionRenderer[]
{ header = "## Declarations", render = render_section_declarations },
{ header = "## Components", render = render_section_components },
{ header = "## RegUse schemas", render = render_section_reguse },
@@ -1083,10 +927,8 @@ local SECTION_RENDERERS = {
--- @param view ModuleView
--- @return string
local function render_module_meta_report(view)
--- @type string
local dir_basename = source_basename(view.dir)
--- @type string[]
local lines = {
local dir_basename = source_basename(view.dir) ---@type string
local lines = { ---@type string[]
"# " .. dir_basename .. " — atom meta report",
"> Auto-generated by ps1_meta.lua (passes/report.lua). Do not edit.",
"",
@@ -1095,20 +937,15 @@ local function render_module_meta_report(view)
--- @return nil
local function add(s) lines[#lines + 1] = s end
--- @type KindCounts
local kinds = count_kinds(view.decls)
--- @type integer, integer, integer
local n_annot, n_binds, n_macros = 0, 0, 0
--- @type integer, SourceFile
for _, src in ipairs(view.sources) do
local kinds = count_kinds(view.decls) ---@type KindCounts
local n_annot, n_binds, n_macros = 0, 0, 0 ---@type integer, integer, integer
for _, src in ipairs(view.sources) do ---@type integer, SourceFile
n_annot = n_annot + #((src.scan and src.scan.atom_infos) or {})
n_binds = n_binds + #((src.scan and src.scan.binds) or {})
n_macros = n_macros + #((src.scan and src.scan.macros) or {})
end
--- @type integer, integer, integer
local n_err, n_warn, n_info = 0, 0, 0
--- @type integer, CheckFinding
for _, f in ipairs(view.findings or {}) do
local n_err, n_warn, n_info = 0, 0, 0 ---@type integer, integer, integer
for _, f in ipairs(view.findings or {}) do ---@type integer, CheckFinding
if f.kind == "error" then n_err = n_err + 1
elseif f.kind == "warning" then n_warn = n_warn + 1
else n_info = n_info + 1
@@ -1128,12 +965,10 @@ local function render_module_meta_report(view)
add("")
add("## Sources"); add("")
--- @type integer, SourceFile
for _, s in ipairs(view.sources) do add("- `" .. s.path .. "`") end
for _, s in ipairs(view.sources) do add("- `" .. s.path .. "`") end ---@type integer, SourceFile
add("")
--- @type integer, SectionRenderer
for _, row in ipairs(SECTION_RENDERERS) do
for _, row in ipairs(SECTION_RENDERERS) do ---@type integer, SectionRenderer
add(row.header); add("")
row.render(add, view)
end
@@ -1147,8 +982,7 @@ end
-- `once = true` means render once at the project level (not per-module).
-- `basename(dir_basename)` yields the file's basename for that kind.
-- `gather(ctx, dir, dir_sources [, all_modules])` returns the rendered string.
--- @type ReportRenderer[]
local REPORT_RENDERERS = {
local REPORT_RENDERERS = { ---@type ReportRenderer[]
{
name = "atom_meta_report",
ext = "md",
@@ -1161,8 +995,7 @@ local REPORT_RENDERERS = {
--- @param dir_sources SourceFile[]
--- @return string
gather = function(ctx, dir, dir_sources)
--- @type Corpus
local corpus = ctx.shared.corpus
local corpus = ctx.shared.corpus ---@type Corpus
return render_module_meta_report(build_module_view(dir, dir_sources, corpus))
end,
},
@@ -1204,20 +1037,16 @@ local REPORT_RENDERERS = {
-- M — public pass surface
-- ════════════════════════════════════════════════════════════════════════════
--- @type ReportPass
local M = {}
local M = {} ---@type ReportPass
--- Run the report pass. Emits 1 `atom_meta_report.summary.md` per build + 2 `atom_meta_report.md` + 2 `atoms.md` files per module (duffle + gte_hello).
--- Reads `corpus.static_analysis_results` (added in Phase 1) to populate per-module findings without re-running validate().
--- @param ctx PassCtx
--- @return PassResult
function M.run(ctx)
--- @type PassOutputEntry[]
local outputs = {}
--- @type Corpus|nil
local corpus = ctx.shared and ctx.shared.corpus
--- @type table<string, SourceFile[]>
local by_dir = (corpus and corpus.sources_by_dir) or {}
local outputs = {} ---@type PassOutputEntry[]
local corpus = ctx.shared and ctx.shared.corpus ---@type Corpus|nil
local by_dir = (corpus and corpus.sources_by_dir) or {} ---@type table<string, SourceFile[]>
-- `out_path_root`: when the conventional `out_root` is `build/gen` (any spelling — relative, absolute, separator variants).
-- Write the md files to `build/` (parent of `gen/`) instead of nested under `gen/`.
@@ -1228,49 +1057,37 @@ function M.run(ctx)
return type(p) == "string" and (p:match("[/\\]gen[/\\]?$") ~= nil
or p == "build/gen" or p == "build\\gen")
end
--- @type string
local out_root_effective = ends_with_gen(ctx.out_root)
local out_root_effective = ends_with_gen(ctx.out_root) ---@type string
and ctx.out_root:gsub("[/\\]gen[/\\]?$", "")
or ctx.out_root
duffle.ensure_dir(out_root_effective)
-- Aggregator for the project-wide `once = true` summary renderer.
--- @type ProjectSummaryRow[]
local all_modules = {}
local all_modules = {} ---@type ProjectSummaryRow[]
--- @type string, SourceFile[]
for dir, dir_sources in pairs(by_dir) do
--- @type string
local dir_basename = dir:match("([^/\\]+)$") or dir
for dir, dir_sources in pairs(by_dir) do ---@type string, SourceFile[]
local dir_basename = dir:match("([^/\\]+)$") or dir ---@type string
-- Per-renderer dispatch for the per-module renderers (once = false).
--- @type integer, ReportRenderer
for _, renderer in ipairs(REPORT_RENDERERS) do
for _, renderer in ipairs(REPORT_RENDERERS) do ---@type integer, ReportRenderer
if not renderer.once then
--- @type string
local body = renderer.gather(ctx, dir, dir_sources)
--- @type string
local out_path = out_root_effective .. "/" .. renderer.basename(dir_basename) .. "." .. renderer.ext
local body = renderer.gather(ctx, dir, dir_sources) ---@type string
local out_path = out_root_effective .. "/" .. renderer.basename(dir_basename) .. "." .. renderer.ext ---@type string
duffle.write_file(out_path, body)
outputs[#outputs + 1] = { kind = renderer.name, path = out_path }
end
end
--- @type ModuleView
local view = build_module_view(dir, dir_sources, corpus)
--- @type integer, integer, integer
local n_annot, n_binds, n_macros = 0, 0, 0
--- @type integer, SourceFile
for _, src in ipairs(dir_sources) do
local view = build_module_view(dir, dir_sources, corpus) ---@type ModuleView
local n_annot, n_binds, n_macros = 0, 0, 0 ---@type integer, integer, integer
for _, src in ipairs(dir_sources) do ---@type integer, SourceFile
n_annot = n_annot + #((src.scan and src.scan.atom_infos) or {})
n_binds = n_binds + #((src.scan and src.scan.binds) or {})
n_macros = n_macros + #((src.scan and src.scan.macros) or {})
end
--- @type integer, integer, integer
local n_err, n_warn, n_info = 0, 0, 0
--- @type integer, CheckFinding
for _, f in ipairs(view.findings or {}) do
local n_err, n_warn, n_info = 0, 0, 0 ---@type integer, integer, integer
for _, f in ipairs(view.findings or {}) do ---@type integer, CheckFinding
if f.kind == "error" then n_err = n_err + 1
elseif f.kind == "warning" then n_warn = n_warn + 1
else n_info = n_info + 1
@@ -1290,13 +1107,10 @@ function M.run(ctx)
end
-- Project-wide renderer (once = true): write the summary file.
--- @type integer, ReportRenderer
for _, renderer in ipairs(REPORT_RENDERERS) do
for _, renderer in ipairs(REPORT_RENDERERS) do ---@type integer, ReportRenderer
if renderer.once then
--- @type string
local body = renderer.gather(ctx, nil, nil, all_modules)
--- @type string
local out_path = out_root_effective .. "/" .. renderer.basename("") .. "." .. renderer.ext
local body = renderer.gather(ctx, nil, nil, all_modules) ---@type string
local out_path = out_root_effective .. "/" .. renderer.basename("") .. "." .. renderer.ext ---@type string
duffle.write_file(out_path, body)
outputs[#outputs + 1] = { kind = renderer.name, path = out_path }
end
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
+8 -16
View File
@@ -23,10 +23,8 @@
-- Bootstrap: load `scripts/duffle_paths.lua` (sets package.path + package.cpath).
-- Uses `debug.getinfo` to find this file's own directory, so it works both standalone and when require'd from the orchestrator.
-- 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
-- ════════════════════════════════════════════════════════════════════════════
-- Type declarations
@@ -46,8 +44,7 @@ local duffle = dofile(_bootstrap_dir .. "../duffle_paths.lua")
-- Module exports
-- ════════════════════════════════════════════════════════════════════════════
--- @type WordCountEval
local M = {}
local M = {} ---@type WordCountEval
-- ┌────────────────────────────────────────────────────────────────────┐
-- │ Shared utility: count_token_words │
@@ -61,15 +58,12 @@ local M = {}
--- @param wc WordCounts -- the shared word-count table
--- @return integer
function M.count_token_words(token, wc)
--- @type string
local s = duffle.trim(token)
local s = duffle.trim(token) ---@type string
if s == "" then return 0 end
--- @type string|nil, integer
local name, after = duffle.read_ident(s, 1)
local name, after = duffle.read_ident(s, 1) ---@type string|nil, integer
if not name then return 1 end
if wc[name] then return wc[name] end
--- @type integer
local paren_pos = duffle.skip_ws_and_cmt(s, after)
local paren_pos = duffle.skip_ws_and_cmt(s, after) ---@type integer
if s:sub(paren_pos, paren_pos) == "(" then
io.stderr:write(" warning: unknown macro '" .. name .. "', assuming 1 word\n")
end
@@ -95,8 +89,7 @@ end
--- @return PassResult
function M.run(ctx)
-- 1. Canonical-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("word_count_eval.run requires ctx.shared.corpus (canonical corpus). The fixture must install the corpus before running this pass.", 0)
end
@@ -108,8 +101,7 @@ function M.run(ctx)
-- 3. Load authored metadata. Generated .macs.h files are NOT scanned
-- (the pass computes their counts from the just-built bodies after disk emission; see passes/components.lua).
--- @type WordCounts
local wc = duffle.load_word_counts(ctx.metadata_path)
local wc = duffle.load_word_counts(ctx.metadata_path) ---@type WordCounts
-- 4. Assign the count table. ONE assignment, no copy. The assignment creates no secondary alias.
corpus.word_counts = wc
+92 -184
View File
@@ -19,10 +19,8 @@
-- fall back to `debug.getinfo(1, "S").source` when this file is being dofile()'d or require()'d (in which case `arg[0]` is the *caller's* path).
-- That single statement: (a) sets `package.path` + `package.cpath`, (b) at the bottom returns `require("duffle")`.
-- So the dofile's return value is the duffle module.
--- @type boolean
local _is_entry_script = arg and arg[0] and arg[0]:match("ps1_meta%.lua$") ~= nil
--- @type string
local _bootstrap_src
local _is_entry_script = arg and arg[0] and arg[0]:match("ps1_meta%.lua$") ~= nil ---@type boolean
local _bootstrap_src ---@type string
if _is_entry_script then
_bootstrap_src = arg[0]
else
@@ -30,33 +28,26 @@ else
-- strip the leading "@" so the directory match works in both cases.
_bootstrap_src = debug.getinfo(1, "S").source:sub(2)
end
--- @type DuffleExport
local duffle = dofile((_bootstrap_src:match("(.*[/\\])") or "./") .. "duffle_paths.lua")
local duffle = dofile((_bootstrap_src:match("(.*[/\\])") or "./") .. "duffle_paths.lua") ---@type DuffleExport
-- ════════════════════════════════════════════════════════════════════════════
-- Constants
-- ════════════════════════════════════════════════════════════════════════════
-- Exit codes (per the --help text and the post-build summary convention).
--- @type integer
local EXIT_OK = 0
--- @type integer
local EXIT_VALIDATION_ERRORS = 1
--- @type integer
local EXIT_INTERNAL_ERROR = 2
local EXIT_OK = 0 ---@type integer
local EXIT_VALIDATION_ERRORS = 1 ---@type integer
local EXIT_INTERNAL_ERROR = 2 ---@type integer
-- Default --out-root value if not provided.
--- @type string
local DEFAULT_OUT_ROOT = "build/gen"
local DEFAULT_OUT_ROOT = "build/gen" ---@type string
-- Sentinel for "all passes" in `PASS_FLAG_TO_NAME`. Distinguishes `--all` from the per-pass flags (which map to individual pass names).
--- @type string
local ALL_PASSES_SENTINEL = "__all__"
local ALL_PASSES_SENTINEL = "__all__" ---@type string
-- Sentinel key for the pass-flag dispatcher in `FLAG_HANDLERS`.
-- The actual pass names are looked up via `PASS_FLAG_TO_NAME`, not direct dispatch, so this key never matches a real flag.
--- @type string
local PASS_FLAG_DISPATCH_KEY = "__pass__"
local PASS_FLAG_DISPATCH_KEY = "__pass__" ---@type string
-- ════════════════════════════════════════════════════════════════════════════
-- Type declarations
@@ -159,8 +150,7 @@ local PASS_FLAG_DISPATCH_KEY = "__pass__"
-- A row without a `groups` entry is dependency-only: it runs only when a transitive dep requests it,
-- but it remains directly requestable through its explicit CLI flag (e.g. --atoms-source-map, --scan-source).
--- @type table<string, PassDescriptor>
local PASSES = {
local PASSES = { ---@type table<string, PassDescriptor>
["scan-source"] = {
module = "passes.scan_source",
kind = "shared", deps = {},
@@ -230,13 +220,10 @@ local PASSES = {
--- @param group_name string -- Build-phase group ("pre-link" | "post-link")
--- @return string[] -- Sorted root pass names belonging to that group
local function roots_for_group(group_name)
--- @type string[]
local names = {}
--- @type string, PassDescriptor
for name, pass in pairs(PASSES) do
local names = {} ---@type string[]
for name, pass in pairs(PASSES) do ---@type string, PassDescriptor
if pass.groups then
--- @type integer, string
for _, g in ipairs(pass.groups) do
for _, g in ipairs(pass.groups) do ---@type integer, string
if g == group_name then
names[#names + 1] = name
break
@@ -255,14 +242,12 @@ end
--- @param group_name string
--- @return nil
local function request_roots_for_group(args, group_name)
--- @type string[]
local roots = roots_for_group(group_name)
local roots = roots_for_group(group_name) ---@type string[]
if #roots == 0 then
error(string.format("ps1_meta: build-phase group %q has zero roots in PASSES; check PASSES rows for a `groups = { %q }` field"
, group_name, group_name))
end
--- @type integer, string
for _, name in ipairs(roots) do
for _, name in ipairs(roots) do ---@type integer, string
args.requested_set[#args.requested_set + 1] = name
end
end
@@ -270,8 +255,7 @@ end
-- Pass-kind taxonomy: findings always print. No pass kind stops the build.
-- Report severity is independent from process exit policy.
-- Adding a new pass kind requires listing it here explicitly; an unknown kind must not silently fall back to "true".
--- @type table<string, boolean> -- bag: pass kind -> stop-on-error
local PASS_KIND_STOP_ON_ERROR = {
local PASS_KIND_STOP_ON_ERROR = { ---@type table<string, boolean> -- bag: pass kind -> stop-on-error
["shared"] = false,
["header-output"] = false,
["validation"] = false,
@@ -283,8 +267,7 @@ local PASS_KIND_STOP_ON_ERROR = {
-- Per-pass flags (e.g. --word-counts); phase flags (--pre-link, --post-link, --all) are within FLAG_HANDLERS because they own side effects or invoke group-derivation logic.
-- dwarf-injection is *also* a per-pass opt-in flag, but its selection + opt-in state are both owned by the explicit FLAG_HANDLERS entry below
-- (it sets args.flags.dwarf_injection and appends "dwarf-injection" to requested_set), so it is intentionally absent from this table.
--- @type table<string, string> -- bag: CLI flag -> pass name or ALL_PASSES_SENTINEL
local PASS_FLAG_TO_NAME = {
local PASS_FLAG_TO_NAME = { ---@type table<string, string> -- bag: CLI flag -> pass name or ALL_PASSES_SENTINEL
["--word-counts"] = "word-counts",
["--components"] = "components",
["--validate"] = "annotation",
@@ -301,21 +284,17 @@ local PASS_FLAG_TO_NAME = {
--- @param args ParsedArgs
--- @return nil
local function request_all_passes(args)
--- @type string[]
local names = {}
--- @type string
for name in pairs(PASSES) do names[#names + 1] = name end
local names = {} ---@type string[]
for name in pairs(PASSES) do names[#names + 1] = name end ---@type string
table.sort(names)
--- @type integer, string
for _, n in ipairs(names) do
for _, n in ipairs(names) do ---@type integer, string
args.requested_set[#args.requested_set + 1] = n
end
end
-- Per-flag handlers. Each handler takes (args, argv, arg_idx) and returns the new arg_idx (so multi-arg flags like --source FILE advance it).
-- Returning nil + os.exit() handles termination flags (--help).
--- @type table<string, FlagHandler>
local FLAG_HANDLERS = {}
local FLAG_HANDLERS = {} ---@type table<string, FlagHandler>
-- ════════════════════════════════════════════════════════════════════════════
-- CLI parsing
@@ -372,8 +351,7 @@ EXAMPLES:
]])
end
--- @type table<string, string> -- bag: flag -> value metavar
local FLAG_VALUE_NAMES = {
local FLAG_VALUE_NAMES = { ---@type table<string, string> -- bag: flag -> value metavar
["--source"] = "FILE",
["--unity-root"] = "FILE",
["--metadata"] = "PATH",
@@ -388,10 +366,8 @@ local FLAG_VALUE_NAMES = {
--- @return string
--- @return integer
local function require_flag_value(argv, arg_idx, flag)
--- @type string|nil
local value = argv[arg_idx + 1]
--- @type boolean
local next_known = type(value) == "string"
local value = argv[arg_idx + 1] ---@type string|nil
local next_known = type(value) == "string" ---@type boolean
and (FLAG_HANDLERS[value] ~= nil or PASS_FLAG_TO_NAME[value] ~= nil)
if value == nil or next_known then
io.stderr:write("ps1_meta: " .. flag .. " requires " .. FLAG_VALUE_NAMES[flag] .. "\n")
@@ -417,8 +393,7 @@ FLAG_HANDLERS["--verbose"] = function(args) args.verbose = true end
--- @param arg_idx integer
--- @return integer
FLAG_HANDLERS["--source"] = function(args, argv, arg_idx)
--- @type string, integer
local value, value_idx = require_flag_value(argv, arg_idx, "--source")
local value, value_idx = require_flag_value(argv, arg_idx, "--source") ---@type string, integer
args.sources[#args.sources + 1] = value
return value_idx
end
@@ -427,8 +402,7 @@ end
--- @param arg_idx integer
--- @return integer
FLAG_HANDLERS["--unity-root"] = function(args, argv, arg_idx)
--- @type string, integer
local value, value_idx = require_flag_value(argv, arg_idx, "--unity-root")
local value, value_idx = require_flag_value(argv, arg_idx, "--unity-root") ---@type string, integer
args.unity_root = value
return value_idx
end
@@ -437,8 +411,7 @@ end
--- @param arg_idx integer
--- @return integer
FLAG_HANDLERS["--metadata"] = function(args, argv, arg_idx)
--- @type string, integer
local value, value_idx = require_flag_value(argv, arg_idx, "--metadata")
local value, value_idx = require_flag_value(argv, arg_idx, "--metadata") ---@type string, integer
args.metadata = value
return value_idx
end
@@ -447,8 +420,7 @@ end
--- @param arg_idx integer
--- @return integer
FLAG_HANDLERS["--out-root"] = function(args, argv, arg_idx)
--- @type string, integer
local value, value_idx = require_flag_value(argv, arg_idx, "--out-root")
local value, value_idx = require_flag_value(argv, arg_idx, "--out-root") ---@type string, integer
args.out_root = value
return value_idx
end
@@ -457,8 +429,7 @@ end
--- @param arg_idx integer
--- @return integer
FLAG_HANDLERS["--project-root"] = function(args, argv, arg_idx)
--- @type string, integer
local value, value_idx = require_flag_value(argv, arg_idx, "--project-root")
local value, value_idx = require_flag_value(argv, arg_idx, "--project-root") ---@type string, integer
args.project_root = value
return value_idx
end
@@ -476,8 +447,7 @@ end
--- @param arg_idx integer
--- @return integer
FLAG_HANDLERS["--elf"] = function(args, argv, arg_idx)
--- @type string, integer
local value, value_idx = require_flag_value(argv, arg_idx, "--elf")
local value, value_idx = require_flag_value(argv, arg_idx, "--elf") ---@type string, integer
args.flags = args.flags or {}
args.flags.elf_path = value
return value_idx
@@ -517,8 +487,7 @@ end
--- @param a string
--- @return nil
FLAG_HANDLERS[PASS_FLAG_DISPATCH_KEY] = function(args, a)
--- @type string|nil
local name = PASS_FLAG_TO_NAME[a]
local name = PASS_FLAG_TO_NAME[a] ---@type string|nil
if name == ALL_PASSES_SENTINEL then
request_all_passes(args)
return
@@ -530,8 +499,7 @@ end
--- @param argv string[]
--- @return ParsedArgs
local function parse_args(argv)
--- @type ParsedArgs
local args = {
local args = { ---@type ParsedArgs
requested_set = {},
sources = {},
unity_root = nil,
@@ -541,13 +509,10 @@ local function parse_args(argv)
verbose = false,
}
--- @type integer
local pos = 1
local pos = 1 ---@type integer
while pos <= #argv do
--- @type string
local a = argv[pos]
--- @type FlagHandler|nil
local handler = FLAG_HANDLERS[a]
local a = argv[pos] ---@type string
local handler = FLAG_HANDLERS[a] ---@type FlagHandler|nil
if handler then
pos = handler(args, argv, pos) or pos
elseif PASS_FLAG_TO_NAME[a] then
@@ -572,17 +537,14 @@ local function parse_args(argv)
-- `<repo>/code/duffle/word_count.metadata.h` is the canonical metadata location.
-- `project_root` names `<repo>`; the resolver derives `<project_root>/code` separately.
if not args.project_root then
--- @type string
local metadata_dir = duffle.dirname(duffle.normalize_path(args.metadata))
--- @type string
local code_root = duffle.dirname(metadata_dir)
local metadata_dir = duffle.dirname(duffle.normalize_path(args.metadata)) ---@type string
local code_root = duffle.dirname(metadata_dir) ---@type string
args.project_root = duffle.dirname(code_root)
else
args.project_root = duffle.normalize_path(args.project_root)
end
--- @type boolean
local has_unity = type(args.unity_root) == "string" and args.unity_root ~= ""
local has_unity = type(args.unity_root) == "string" and args.unity_root ~= "" ---@type boolean
if has_unity and #args.sources > 0 then
io.stderr:write("ps1_meta: --unity-root FILE and --source FILE are mutually exclusive\n")
os.exit(EXIT_INTERNAL_ERROR)
@@ -595,14 +557,10 @@ local function parse_args(argv)
-- Post-link opt-ins (--gdb-runtime, --dwarf-injection) write output that depends on the linked ELF.
-- Without --elf the metaprogram can't satisfy those requests, so refuse loud and early.
-- This covers the explicit --post-link batch, --dwarf-injection by itself, and --gdb-runtime by itself.
--- @type PassFlags
local flags = args.flags or {}
--- @type string|nil
local elf_path = flags.elf_path
--- @type boolean
local has_elf = type(elf_path) == "string" and #elf_path > 0
--- @type boolean
local post_links = flags.gdb_runtime or flags.dwarf_injection
local flags = args.flags or {} ---@type PassFlags
local elf_path = flags.elf_path ---@type string|nil
local has_elf = type(elf_path) == "string" and #elf_path > 0 ---@type boolean
local post_links = flags.gdb_runtime or flags.dwarf_injection ---@type boolean
if post_links and not has_elf then
io.stderr:write("ps1_meta: --elf PATH is required for post-link output\n")
os.exit(EXIT_INTERNAL_ERROR)
@@ -621,12 +579,9 @@ end
--- @param args ParsedArgs
--- @return PassCtx
local function build_ctx(args)
--- @type string
local normalized_project_root = duffle.normalize_path(args.project_root)
--- @type string
local project_root = normalized_project_root
--- @type boolean
local project_root_is_absolute = normalized_project_root:match("^%a:/")
local normalized_project_root = duffle.normalize_path(args.project_root) ---@type string
local project_root = normalized_project_root ---@type string
local project_root_is_absolute = normalized_project_root:match("^%a:/") ---@type boolean
or normalized_project_root:sub(1, 2) == "//"
or normalized_project_root:sub(1, 1) == "/"
if not project_root_is_absolute then
@@ -637,11 +592,9 @@ local function build_ctx(args)
-- Do not route POSIX/UNC/drive-absolute paths through to_absolute_path.
duffle.canonical_path_key(project_root)
end
--- @type Corpus
local resolution
local resolution ---@type Corpus
if args.unity_root then
--- @type boolean, Corpus|string
local ok_resolve, resolved = pcall(duffle.resolve_source_corpus, {
local ok_resolve, resolved = pcall(duffle.resolve_source_corpus, { ---@type boolean, Corpus|string
unity_root = args.unity_root,
project_root = project_root,
})
@@ -651,45 +604,35 @@ local function build_ctx(args)
end
resolution = resolved
else
--- @type SourceFile[]
local source_order = {}
--- @type table<Path, SourceFile>
local sources_by_path = {}
--- @type SourceResolver
local resolver = {
local source_order = {} ---@type SourceFile[]
local sources_by_path = {} ---@type table<Path, SourceFile>
local resolver = { ---@type SourceResolver
resolved = {},
skipped = {},
shadowed = {},
}
--- @type integer, string
for _, input_path in ipairs(args.sources) do
--- @type string
local path = duffle.normalize_path(input_path)
--- @type boolean, string
local key_ok, key_or_error = pcall(duffle.canonical_path_key, path)
for _, input_path in ipairs(args.sources) do ---@type integer, string
local path = duffle.normalize_path(input_path) ---@type string
local key_ok, key_or_error = pcall(duffle.canonical_path_key, path) ---@type boolean, string
if not key_ok then
error("ps1_meta: invalid --source " .. input_path .. ": " .. tostring(key_or_error), 0)
end
--- @type file*|nil
local file = io.open(path, "r")
local file = io.open(path, "r") ---@type file*|nil
if not file then
io.stderr:write("ps1_meta: cannot open --source " .. input_path .. "\n")
os.exit(EXIT_INTERNAL_ERROR)
end
--- @type string
local text = file:read("*a")
local text = file:read("*a") ---@type string
file:close()
--- @type SourceFile
local source = {
local source = { ---@type SourceFile
path = path,
text = text,
dir = duffle.dirname(path),
basename = duffle.basename_no_ext(path),
}
source_order[#source_order + 1] = source
--- @type string
local key = key_or_error
local key = key_or_error ---@type string
if not sources_by_path[key] then sources_by_path[key] = source end
resolver.resolved[#resolver.resolved + 1] = {
include_path = path,
@@ -713,8 +656,7 @@ local function build_ctx(args)
}
end
--- @type Corpus
local corpus = {
local corpus = { ---@type Corpus
unity_root = resolution.unity_root,
project_root = resolution.project_root,
code_root = resolution.code_root,
@@ -735,8 +677,7 @@ local function build_ctx(args)
collisions = {},
resolver = resolution.resolver,
}
--- @type PassCtx
local ctx = {
local ctx = { ---@type PassCtx
metadata_path = args.metadata,
shared = { corpus = corpus },
out_root = args.out_root,
@@ -765,21 +706,15 @@ end
--- Keeping these blocks local makes the topological sort self-contained.
local function topo_sort(passes, requested_set)
-- Dependency closure: include every pass transitively required by `requested_set`.
--- @type table<string, boolean> -- bag: pass name -> needed
local needed = {}
--- @type integer, string
for _, name in ipairs(requested_set) do needed[name] = true end
--- @type boolean
local changed = true
local needed = {} ---@type table<string, boolean> -- bag: pass name -> needed
for _, name in ipairs(requested_set) do needed[name] = true end ---@type integer, string
local changed = true ---@type boolean
while changed do
changed = false
--- @type string, boolean
for name, _ in pairs(needed) do
--- @type PassDescriptor
local pass = passes[name]
for name, _ in pairs(needed) do ---@type string, boolean
local pass = passes[name] ---@type PassDescriptor
if not pass then error("unknown pass '" .. name .. "' requested") end
--- @type integer, string
for _, dep in ipairs(pass.deps) do
for _, dep in ipairs(pass.deps) do ---@type integer, string
if not needed[dep] then
needed[dep] = true
changed = true
@@ -789,14 +724,10 @@ local function topo_sort(passes, requested_set)
end
-- In-degree calculation: count each needed pass's needed dependencies.
--- @type table<string, integer> -- bag: pass name -> in-degree
local in_degree = {}
--- @type string, boolean
for name, _ in pairs(needed) do in_degree[name] = 0 end
--- @type string, boolean
for name, _ in pairs(needed) do
--- @type integer, string
for _, dep in ipairs(passes[name].deps) do
local in_degree = {} ---@type table<string, integer> -- bag: pass name -> in-degree
for name, _ in pairs(needed) do in_degree[name] = 0 end ---@type string, boolean
for name, _ in pairs(needed) do ---@type string, boolean
for _, dep in ipairs(passes[name].deps) do ---@type integer, string
if needed[dep] then
in_degree[name] = in_degree[name] + 1
end
@@ -804,27 +735,21 @@ local function topo_sort(passes, requested_set)
end
-- Ready-queue seeding: add zero-in-degree passes in deterministic order.
--- @type string[]
local ready = {}
--- @type string, integer
for name, deg in pairs(in_degree) do
local ready = {} ---@type string[]
for name, deg in pairs(in_degree) do ---@type string, integer
if deg == 0 then ready[#ready + 1] = name end
end
table.sort(ready)
-- Ready-queue drain: decrement dependents when each pass is emitted.
-- Newly-zero-degree passes are inserted back into the ready queue (kept sorted).
--- @type string[]
local order = {}
local order = {} ---@type string[]
while #ready > 0 do
--- @type string
local just_finished = table.remove(ready, 1)
local just_finished = table.remove(ready, 1) ---@type string
order[#order + 1] = just_finished
--- @type string, boolean
for name, _ in pairs(needed) do
for name, _ in pairs(needed) do ---@type string, boolean
if name ~= just_finished then
--- @type integer, string
for _, dep in ipairs(passes[name].deps) do
for _, dep in ipairs(passes[name].deps) do ---@type integer, string
if dep == just_finished then
in_degree[name] = in_degree[name] - 1
if in_degree[name] == 0 then
@@ -840,13 +765,10 @@ local function topo_sort(passes, requested_set)
-- Cycle detection: if `order` doesn't include all needed passes, some are stuck with in_degree > 0
-- (the cycle closed on itself before Kahn could process them).
-- Without this check, a fully-closed cycle (e.g. A -> B -> A) would silently return an empty order list, leaving the orchestrator to dispatch nothing.
--- @type integer
local needed_count = 0
--- @type string
for _ in pairs(needed) do needed_count = needed_count + 1 end -- count hash entries; Lua's #t doesn't work
local needed_count = 0 ---@type integer
for _ in pairs(needed) do needed_count = needed_count + 1 end ---@type string -- count hash entries; Lua's #t doesn't work
if #order ~= needed_count then
--- @type string, integer
for name, deg in pairs(in_degree) do
for name, deg in pairs(in_degree) do ---@type string, integer
if deg > 0 then
error("dependency cycle detected involving pass '" .. name .. "'")
end
@@ -867,11 +789,9 @@ end
--- @param result PassResult
--- @return boolean
local function report_validation_errors(pass_name, pass, result)
--- @type boolean
local has_errors = result.errors and #result.errors > 0
local has_errors = result.errors and #result.errors > 0 ---@type boolean
if not has_errors then return false end
--- @type integer, PassFinding
for _, e in ipairs(result.errors) do
for _, e in ipairs(result.errors) do ---@type integer, PassFinding
io.stderr:write(string.format("[%s] line %d: %s\n", pass_name, e.line or 0, e.msg or ""))
end
return PASS_KIND_STOP_ON_ERROR[pass.kind] == true
@@ -882,16 +802,11 @@ end
--- @param order string[]
--- @return boolean -- true if any validation errors were reported
local function dispatch_passes(ctx, order)
--- @type boolean
local had_errors = false
--- @type integer, string
for _, pass_name in ipairs(order) do
--- @type PassDescriptor
local pass = PASSES[pass_name]
--- @type PassModule
local mod = require(pass.module)
--- @type PassResult
local result = mod.run(ctx)
local had_errors = false ---@type boolean
for _, pass_name in ipairs(order) do ---@type integer, string
local pass = PASSES[pass_name] ---@type PassDescriptor
local mod = require(pass.module) ---@type PassModule
local result = mod.run(ctx) ---@type PassResult
if report_validation_errors(pass_name, pass, result) then
had_errors = true
end
@@ -903,20 +818,14 @@ end
--- @param argv string[]
--- @return nil
local function main(argv)
--- @type boolean, string|nil
local ok, err = pcall(function()
--- @type ParsedArgs
local args = parse_args(argv)
--- @type PassCtx
local ctx = build_ctx(args)
local ok, err = pcall(function() ---@type boolean, string|nil
local args = parse_args(argv) ---@type ParsedArgs
local ctx = build_ctx(args) ---@type PassCtx
--- @type string[]
local requested = args.requested_set
--- @type string[]
local closed = topo_sort(PASSES, requested)
local requested = args.requested_set ---@type string[]
local closed = topo_sort(PASSES, requested) ---@type string[]
--- @type boolean
local had_errors = dispatch_passes(ctx, closed)
local had_errors = dispatch_passes(ctx, closed) ---@type boolean
if had_errors then os.exit(EXIT_VALIDATION_ERRORS) end
end)
@@ -931,8 +840,7 @@ end
-- Module export for in-process consumers (tests that dofile this script).
-- The conditional `main(...)` call below only fires when this file is invoked as the entry script (arg[0] ends in "ps1_meta.lua");
-- in dofile() mode (test's arg[0] does not match), main() is skipped and the chunk returns `_M` to the caller.
--- @type Ps1MetaMod
local _M = {
local _M = { ---@type Ps1MetaMod
PASSES = PASSES,
PASS_KIND_STOP_ON_ERROR = PASS_KIND_STOP_ON_ERROR,
parse_args = parse_args,