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 --- @class DuffleExport
--- bag: open module-export keys from duffle_scan / duffle_isa / duffle_emit --- bag: open module-export keys from duffle_scan / duffle_isa / duffle_emit
--- @type DuffleExport local scan = require("duffle_scan") ---@type DuffleExport
local scan = require("duffle_scan") local isa = require("duffle_isa") ---@type DuffleExport
--- @type DuffleExport local emit = require("duffle_emit") ---@type DuffleExport
local isa = require("duffle_isa") local M = {} ---@type DuffleExport
--- @type DuffleExport
local emit = require("duffle_emit")
--- @type DuffleExport
local M = {}
--- @alias Path string --- @alias Path string
--- @alias LineNum integer --- @alias LineNum integer
@@ -46,8 +42,7 @@ local M = {}
--- @param label string --- @param label string
--- @return nil --- @return nil
local function merge(src, label) local function merge(src, label)
--- @type string, any for k, v in pairs(src) do ---@type string, any
for k, v in pairs(src) do
if M[k] ~= nil and M[k] ~= v then if M[k] ~= nil and M[k] ~= v then
error("duffle facade name collision on " .. tostring(k) .. " from " .. label, 0) error("duffle facade name collision on " .. tostring(k) .. " from " .. label, 0)
end end
@@ -62,8 +57,7 @@ merge(emit, "duffle_emit")
--- @param ctx PassCtx --- @param ctx PassCtx
--- @return CorpusView --- @return CorpusView
function M.corpus_view(ctx) function M.corpus_view(ctx)
--- @type Corpus local corpus = ctx and ctx.shared and ctx.shared.corpus ---@type Corpus
local corpus = ctx and ctx.shared and ctx.shared.corpus
if not corpus then error("requires ctx.shared.corpus", 0) end if not corpus then error("requires ctx.shared.corpus", 0) end
return { return {
register_alias_registry = corpus.register_alias_registry or {}, register_alias_registry = corpus.register_alias_registry or {},
@@ -90,10 +84,8 @@ end
--- @param findings CheckFinding[] --- @param findings CheckFinding[]
--- @return nil --- @return nil
function M.run_check_rules(rules, phase, item, pipe_ctx, findings) function M.run_check_rules(rules, phase, item, pipe_ctx, findings)
--- @type integer, CheckRule for _, rule in ipairs(rules) do ---@type integer, CheckRule
for _, rule in ipairs(rules) do local fn = rule[phase] ---@type (fun(item: AtomEntry|SourceFile, pipe_ctx: PipeCtx, findings: CheckFinding[]): nil)|nil
--- @type (fun(item: AtomEntry|SourceFile, pipe_ctx: PipeCtx, findings: CheckFinding[]): nil)|nil
local fn = rule[phase]
if fn then fn(item, pipe_ctx, findings) end if fn then fn(item, pipe_ctx, findings) end
end end
end end
+3 -6
View File
@@ -106,8 +106,7 @@
--- @field HARDWARE_RELATIONS HardwareRelationRow[] --- @field HARDWARE_RELATIONS HardwareRelationRow[]
--- @field CU2_TRANSITION_POLICY Cu2TransitionPolicy --- @field CU2_TRANSITION_POLICY Cu2TransitionPolicy
--- @type DuffleIsa local M = {} ---@type DuffleIsa
local M = {}
-- Section 7: domain tables -- 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() local function build_alias_map()
--- @type table<string, string> -- bag: alias or canon -> canon --- @type table<string, string> -- bag: alias or canon -> canon
M.ALIAS_TO_CANONICAL = {} M.ALIAS_TO_CANONICAL = {}
--- @type string, GteCommandRow for canon, row in pairs(M.GTE_COMMAND) do ---@type string, GteCommandRow
for canon, row in pairs(M.GTE_COMMAND) do
M.ALIAS_TO_CANONICAL[canon] = canon M.ALIAS_TO_CANONICAL[canon] = canon
--- @type integer, string for _, alias in ipairs(row.aliases or {}) do ---@type integer, string
for _, alias in ipairs(row.aliases or {}) do
M.ALIAS_TO_CANONICAL[alias] = canon M.ALIAS_TO_CANONICAL[alias] = canon
end end
end end
+10 -20
View File
@@ -19,12 +19,10 @@
--- @class DufflePaths --- @class DufflePaths
--- @field setup fun(): nil --- @field setup fun(): nil
--- @type DufflePaths local M = {} ---@type DufflePaths
local M = {}
-- Cache key for the repo root. Stored in `package.loaded` (process-global) so all 8 entry scripts + passes scripts share one resolution. -- 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__" ---@type string
local CACHE_KEY = "__duffle_repo_root__"
--- Resolve the repo root from this script's own path. Zero shell spawn. --- 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. --- `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() local function find_repo_root()
if package.loaded[CACHE_KEY] then return package.loaded[CACHE_KEY] end if package.loaded[CACHE_KEY] then return package.loaded[CACHE_KEY] end
--- @type string local source = debug.getinfo(1, "S").source ---@type string
local source = debug.getinfo(1, "S").source
-- Strip the leading `@` (Lua's dofile marker) and the trailing `/duffle_paths.lua` filename. -- 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/`. -- 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$") ---@type string|nil
local scripts_dir = source and source:match("^@?(.*)[/\\]duffle_paths%.lua$")
if not scripts_dir then return nil end 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). -- 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[\\/]?$", "") ---@type string
local root = scripts_dir:gsub("scripts[\\/]?$", "")
root = root:gsub("\\", "/") root = root:gsub("\\", "/")
if root == "" then root = "./" end if root == "" then root = "./" end
if not root:match("/$") then root = 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). --- 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 --- @return nil
function M.setup() function M.setup()
--- @type string|nil local repo_root = find_repo_root() ---@type string|nil
local repo_root = find_repo_root()
if not repo_root then 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). -- 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. -- 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) os.exit(2)
end end
--- @type string local scripts_dir = repo_root .. "scripts/" ---@type string
local scripts_dir = repo_root .. "scripts/" local passes_dir = repo_root .. "scripts/passes/" ---@type string
--- @type string
local passes_dir = repo_root .. "scripts/passes/"
package.path = scripts_dir .. "?.lua;" package.path = scripts_dir .. "?.lua;"
.. scripts_dir .. "?/init.lua;" .. scripts_dir .. "?/init.lua;"
.. passes_dir .. "?.lua;" .. passes_dir .. "?.lua;"
@@ -82,10 +74,8 @@ function M.setup()
-- lpeg: built by `update_deps.ps1` to `toolchain/lpeg/lpeg.dll`. -- 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`. -- 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. -- Wire both directories into cpath so `require("lpeg")` and `require("lfs")` resolve.
--- @type string local lpeg_dir = repo_root .. "toolchain/lpeg/" ---@type string
local lpeg_dir = repo_root .. "toolchain/lpeg/" local lfs_dir = repo_root .. "toolchain/lfs/" ---@type string
--- @type string
local lfs_dir = repo_root .. "toolchain/lfs/"
package.cpath = lpeg_dir .. "?.dll;" package.cpath = lpeg_dir .. "?.dll;"
.. lfs_dir .. "?.dll;" .. lfs_dir .. "?.dll;"
.. package.cpath .. package.cpath
+184 -368
View File
@@ -68,70 +68,44 @@
--- @field skipped ResolverEvidence[] --- @field skipped ResolverEvidence[]
--- @field shadowed ResolverEvidence[] --- @field shadowed ResolverEvidence[]
--- @type DuffleScan local M = {} ---@type DuffleScan
local M = {}
-- 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`. -- 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. -- If lfs is missing, `require` throws — fail loud per the build-tool convention.
--- @type LfsMod local lfs = require("lfs") ---@type LfsMod
local lfs = require("lfs")
-- ════════════════════════════════════════════════════════════════════════════ -- ════════════════════════════════════════════════════════════════════════════
-- ASCII byte constants -- ASCII byte constants
-- ════════════════════════════════════════════════════════════════════════════ -- ════════════════════════════════════════════════════════════════════════════
--- @type integer local BYTE_SPACE = 0x20 ---@type integer -- ' '
local BYTE_SPACE = 0x20 -- ' ' local BYTE_TAB = 0x09 ---@type integer -- '\t'
--- @type integer local BYTE_NEWLINE = 0x0A ---@type integer -- '\n'
local BYTE_TAB = 0x09 -- '\t' local BYTE_CR = 0x0D ---@type integer -- '\r'
--- @type integer local BYTE_VT = 0x0B ---@type integer -- '\v'
local BYTE_NEWLINE = 0x0A -- '\n' local BYTE_FF = 0x0C ---@type integer -- '\f'
--- @type integer
local BYTE_CR = 0x0D -- '\r'
--- @type integer
local BYTE_VT = 0x0B -- '\v'
--- @type integer
local BYTE_FF = 0x0C -- '\f'
--- @type integer local BYTE_UNDERSCORE = 0x5F ---@type integer -- '_'
local BYTE_UNDERSCORE = 0x5F -- '_' local BYTE_DOT = 0x2E ---@type integer -- '.'
--- @type integer local BYTE_SLASH = 0x2F ---@type integer -- '/'
local BYTE_DOT = 0x2E -- '.' local BYTE_BACKSLASH = 0x5C ---@type integer -- '\\'
--- @type integer local BYTE_STAR = 0x2A ---@type integer -- '*'
local BYTE_SLASH = 0x2F -- '/' local BYTE_DQUOTE = 0x22 ---@type integer -- '"'
--- @type integer local BYTE_SQUOTE = 0x27 ---@type integer -- '\''
local BYTE_BACKSLASH = 0x5C -- '\\' local BYTE_COMMA = 0x2C ---@type integer -- ','
--- @type integer local BYTE_SEMI = 0x3B ---@type integer -- ';'
local BYTE_STAR = 0x2A -- '*'
--- @type integer
local BYTE_DQUOTE = 0x22 -- '"'
--- @type integer
local BYTE_SQUOTE = 0x27 -- '\''
--- @type integer
local BYTE_COMMA = 0x2C -- ','
--- @type integer
local BYTE_SEMI = 0x3B -- ';'
--- @type integer local BYTE_OPEN_PAREN = 0x28 ---@type integer -- '('
local BYTE_OPEN_PAREN = 0x28 -- '(' local BYTE_OPEN_BRACE = 0x7B ---@type integer -- '{'
--- @type integer local BYTE_OPEN_BRACK = 0x5B ---@type integer -- '['
local BYTE_OPEN_BRACE = 0x7B -- '{'
--- @type integer
local BYTE_OPEN_BRACK = 0x5B -- '['
--- @type integer local BYTE_LOWER_A = 0x61 ---@type integer -- 'a'
local BYTE_LOWER_A = 0x61 -- 'a' local BYTE_LOWER_Z = 0x7A ---@type integer -- 'z'
--- @type integer local BYTE_UPPER_A = 0x41 ---@type integer -- 'A'
local BYTE_LOWER_Z = 0x7A -- 'z' local BYTE_UPPER_Z = 0x5A ---@type integer -- 'Z'
--- @type integer
local BYTE_UPPER_A = 0x41 -- 'A'
--- @type integer
local BYTE_UPPER_Z = 0x5A -- 'Z'
--- @type integer local BYTE_DIGIT_0 = 0x30 ---@type integer -- '0'
local BYTE_DIGIT_0 = 0x30 -- '0' local BYTE_DIGIT_9 = 0x39 ---@type integer -- '9'
--- @type integer
local BYTE_DIGIT_9 = 0x39 -- '9'
-- ════════════════════════════════════════════════════════════════════════════ -- ════════════════════════════════════════════════════════════════════════════
-- Section -1: Bootstrap (path-setup at module load) -- 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. -- 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. -- 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") ---@type boolean, LpegMod|string
local lpeg_ok, lpeg = pcall(require, "lpeg")
if not lpeg_ok then if not lpeg_ok then
io.stderr:write("[duffle] require('lpeg') failed: ", lpeg, "\n") 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] lpeg.dll not found on package.cpath.\n")
io.stderr:write("[duffle] Run 'scripts/update_deps.ps1' to build it into toolchain/lpeg/.\n") io.stderr:write("[duffle] Run 'scripts/update_deps.ps1' to build it into toolchain/lpeg/.\n")
os.exit(1) os.exit(1)
end end
--- @type LpegCtor, LpegCtor, LpegCtor local P, S, R = lpeg.P, lpeg.S, lpeg.R ---@type LpegCtor, LpegCtor, LpegCtor
local P, S, R = lpeg.P, lpeg.S, lpeg.R
-- Character class patterns -- Character class patterns
--- @type LpegPattern local alpha_pat = R("AZ", "az") + P("_") ---@type LpegPattern
local alpha_pat = R("AZ", "az") + P("_") local digit_pat = R("09") ---@type LpegPattern
--- @type LpegPattern local lpeg_alnum_pat = alpha_pat + digit_pat ---@type LpegPattern
local digit_pat = R("09")
--- @type LpegPattern
local lpeg_alnum_pat = alpha_pat + digit_pat
-- Identifier: alpha followed by zero+ alnum. Capture as a string. -- Identifier: alpha followed by zero+ alnum. Capture as a string.
--- @type LpegPattern local lpeg_alpha_pat = alpha_pat ---@type LpegPattern
local lpeg_alpha_pat = alpha_pat local lpeg_ident_pat = lpeg.C(alpha_pat * lpeg_alnum_pat^0) ---@type LpegPattern
--- @type LpegPattern
local lpeg_ident_pat = lpeg.C(alpha_pat * lpeg_alnum_pat^0)
--- @type LpegPattern local lpeg_str_pat = P('"') * (P(1) - S('"\\') + P('\\') * P(1))^0 * P('"') ---@type LpegPattern -- String literal: "..." with backslash escapes.
local lpeg_str_pat = P('"') * (P(1) - S('"\\') + P('\\') * P(1))^0 * P('"') -- 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.
--- @type LpegPattern local lpeg_line_cmt_pat = P("//") * (P(1) - S("\n"))^0 ---@type LpegPattern -- Line comment: // ... to end-of-line.
local lpeg_chr_pat = P("'") * (P(1) - S("'\\") + P('\\') * P(1))^0 * P("'") -- Char literal: '...' with backslash escapes. local lpeg_block_cmt_pat = P("/*") * (P(1) - P("*/"))^0 * P("*/") ---@type LpegPattern -- 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 ---@type LpegPattern -- String or comment (any of the four forms).
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).
-- Whitespace + comment skipper: zero+ (whitespace run | string | comment). -- Whitespace + comment skipper: zero+ (whitespace run | string | comment).
--- @type LpegPattern local ws_pat = S(" \t\n\r\v\f") ---@type LpegPattern
local ws_pat = S(" \t\n\r\v\f") local lpeg_ws_and_cmt_pat = (ws_pat + lpeg_str_or_cmt_pat)^0 ---@type LpegPattern
--- @type LpegPattern
local lpeg_ws_and_cmt_pat = (ws_pat + lpeg_str_or_cmt_pat)^0
-- Generic "skip until target, but step over balanced groups" matcher. -- 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. -- 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 --- @param s string
--- @return string --- @return string
function M.trim(s) 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 a = 1; while a <= #s and M.is_space_byte(s:byte(a)) do a = a + 1 end local b = #s; while b >= a and M.is_space_byte(s:byte(b)) do b = b - 1 end ---@type integer
--- @type integer
local b = #s; while b >= a and M.is_space_byte(s:byte(b)) do b = b - 1 end
return s:sub(a, b) return s:sub(a, b)
end end
@@ -271,8 +229,7 @@ end
--- @param start integer -- optional 1-indexed start (default 1) --- @param start integer -- optional 1-indexed start (default 1)
--- @return integer|nil --- @return integer|nil
function M.find_byte(haystack, target, start) function M.find_byte(haystack, target, start)
--- @type integer for pos = start or 1, #haystack do ---@type integer
for pos = start or 1, #haystack do
if haystack:byte(pos) == target then return pos end if haystack:byte(pos) == target then return pos end
end end
return nil return nil
@@ -282,12 +239,9 @@ end
--- @param path Path --- @param path Path
--- @return Path --- @return Path
function M.dirname(path) function M.dirname(path)
--- @type integer local last_sep = 0 ---@type integer
local last_sep = 0 for pos = 1, #path do ---@type integer
--- @type integer local b = path:byte(pos) ---@type integer
for pos = 1, #path do
--- @type integer
local b = path:byte(pos)
if b == BYTE_SLASH or b == BYTE_BACKSLASH then last_sep = pos end if b == BYTE_SLASH or b == BYTE_BACKSLASH then last_sep = pos end
end end
if last_sep == 0 then return "." end if last_sep == 0 then return "." end
@@ -298,20 +252,14 @@ end
--- @param path Path --- @param path Path
--- @return string --- @return string
function M.basename_no_ext(path) function M.basename_no_ext(path)
--- @type integer local last_sep = 0 ---@type integer
local last_sep = 0 for pos = 1, #path do ---@type integer
--- @type integer local b = path:byte(pos) ---@type integer
for pos = 1, #path do
--- @type integer
local b = path:byte(pos)
if b == BYTE_SLASH or b == BYTE_BACKSLASH then last_sep = pos end if b == BYTE_SLASH or b == BYTE_BACKSLASH then last_sep = pos end
end end
--- @type integer local a = last_sep + 1 ---@type integer
local a = last_sep + 1 local last_dot = #path + 1 ---@type integer
--- @type integer for pos = #path, a, -1 do ---@type integer
local last_dot = #path + 1
--- @type integer
for pos = #path, a, -1 do
if path:byte(pos) == BYTE_DOT then last_dot = pos; break end if path:byte(pos) == BYTE_DOT then last_dot = pos; break end
end end
return path:sub(a, last_dot - 1) return path:sub(a, last_dot - 1)
@@ -322,12 +270,10 @@ end
--- @param input string --- @param input string
--- @return PathRoot --- @return PathRoot
local function parse_path_root(input) local function parse_path_root(input)
--- @type string|nil local drive = input:match("^(%a:)") ---@type string|nil
local drive = input:match("^(%a:)")
if drive then if drive then
if input:sub(3, 3) == "/" then if input:sub(3, 3) == "/" then
--- @type string local rest = input:sub(4) ---@type string
local rest = input:sub(4)
while rest:sub(1, 1) == "/" do rest = rest:sub(2) end while rest:sub(1, 1) == "/" do rest = rest:sub(2) end
return { kind = "drive_absolute", prefix = drive .. "/", rest = rest, anchored = true } return { kind = "drive_absolute", prefix = drive .. "/", rest = rest, anchored = true }
end end
@@ -335,29 +281,22 @@ local function parse_path_root(input)
end end
if input:sub(1, 2) == "//" then if input:sub(1, 2) == "//" then
--- @type integer local server_start = 3 ---@type integer
local server_start = 3 local server_end = M.find_byte(input, BYTE_SLASH, server_start) ---@type integer|nil
--- @type integer|nil
local server_end = M.find_byte(input, BYTE_SLASH, server_start)
if not server_end or server_end == server_start then if not server_end or server_end == server_start then
error("UNC path requires //server/share: " .. input, 3) error("UNC path requires //server/share: " .. input, 3)
end end
--- @type string local server = input:sub(server_start, server_end - 1) ---@type string
local server = input:sub(server_start, server_end - 1) local share_start = server_end + 1 ---@type integer
--- @type integer
local share_start = server_end + 1
while input:sub(share_start, share_start) == "/" do while input:sub(share_start, share_start) == "/" do
share_start = share_start + 1 share_start = share_start + 1
end end
--- @type integer local share_end = M.find_byte(input, BYTE_SLASH, share_start) or (#input + 1) ---@type integer
local share_end = M.find_byte(input, BYTE_SLASH, share_start) or (#input + 1)
if share_end == share_start then if share_end == share_start then
error("UNC path requires //server/share: " .. input, 3) error("UNC path requires //server/share: " .. input, 3)
end end
--- @type string local share = input:sub(share_start, share_end - 1) ---@type string
local share = input:sub(share_start, share_end - 1) local rest = input:sub(share_end + 1) ---@type string
--- @type string
local rest = input:sub(share_end + 1)
while rest:sub(1, 1) == "/" do rest = rest:sub(2) end while rest:sub(1, 1) == "/" do rest = rest:sub(2) end
return { return {
kind = "unc_absolute", kind = "unc_absolute",
@@ -368,8 +307,7 @@ local function parse_path_root(input)
end end
if input:sub(1, 1) == "/" then if input:sub(1, 1) == "/" then
--- @type string local rest = input:sub(2) ---@type string
local rest = input:sub(2)
while rest:sub(1, 1) == "/" do rest = rest:sub(2) end while rest:sub(1, 1) == "/" do rest = rest:sub(2) end
return { kind = "posix_absolute", prefix = "/", rest = rest, anchored = true } return { kind = "posix_absolute", prefix = "/", rest = rest, anchored = true }
end 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 type(path) ~= "string" then error("normalize_path requires a string path", 2) end
if path == "" then return "" end if path == "" then return "" end
--- @type PathRoot local root = parse_path_root(path:gsub("\\", "/")) ---@type PathRoot
local root = parse_path_root(path:gsub("\\", "/")) local segments = {} ---@type string[]
--- @type string[] for segment in root.rest:gmatch("[^/]+") do ---@type string
local segments = {}
--- @type string
for segment in root.rest:gmatch("[^/]+") do
if segment == "." then if segment == "." then
-- no-op -- no-op
elseif segment == ".." then elseif segment == ".." then
@@ -403,8 +338,7 @@ function M.normalize_path(path)
end end
end end
--- @type string local tail = table.concat(segments, "/") ---@type string
local tail = table.concat(segments, "/")
if root.kind == "relative" then return tail ~= "" and tail or "." end 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 == "drive_relative" then return root.prefix .. tail end
if root.kind == "unc_absolute" then return tail ~= "" and (root.prefix .. "/" .. tail) or root.prefix 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 --- @param path Path
--- @return Path --- @return Path
local function absolute_normalized_path(path) local function absolute_normalized_path(path)
--- @type Path local normalized = M.normalize_path(path) ---@type Path
local normalized = M.normalize_path(path) local root = parse_path_root(normalized) ---@type PathRoot
--- @type PathRoot
local root = parse_path_root(normalized)
if root.kind == "drive_relative" then if root.kind == "drive_relative" then
error("drive-relative path cannot be resolved without a per-drive cwd: " .. normalized, 3) error("drive-relative path cannot be resolved without a per-drive cwd: " .. normalized, 3)
end end
@@ -430,15 +362,12 @@ end
--- @param path Path --- @param path Path
--- @return string --- @return string
function M.canonical_path_key(path) function M.canonical_path_key(path)
--- @type Path local normalized = M.normalize_path(path) ---@type Path
local normalized = M.normalize_path(path) local root = parse_path_root(normalized) ---@type PathRoot
--- @type PathRoot
local root = parse_path_root(normalized)
if root.kind == "drive_relative" then if root.kind == "drive_relative" then
error("canonical_path_key cannot compare drive-relative path: " .. normalized, 2) error("canonical_path_key cannot compare drive-relative path: " .. normalized, 2)
end end
--- @type string local key = absolute_normalized_path(normalized):lower() ---@type string
local key = absolute_normalized_path(normalized):lower()
if #key > 3 and key:sub(-1) == "/" then key = key:sub(1, -2) end if #key > 3 and key:sub(-1) == "/" then key = key:sub(1, -2) end
return key return key
end end
@@ -453,11 +382,9 @@ end
--- @param path Path --- @param path Path
--- @return string --- @return string
function M.read_file(path) function M.read_file(path)
--- @type file*|nil local f = io.open(path, "r") ---@type file*|nil
local f = io.open(path, "r")
if not f then error("Cannot open " .. path) end if not f then error("Cannot open " .. path) end
--- @type string local content = f:read("*a"); f:close() ---@type string
local content = f:read("*a"); f:close()
return content return content
end end
@@ -465,8 +392,7 @@ end
--- @param content string --- @param content string
--- @return nil --- @return nil
function M.write_file(path, content) function M.write_file(path, content)
--- @type file*|nil local f = io.open(path, "w") ---@type file*|nil
local f = io.open(path, "w")
if not f then error("Cannot write " .. path) end if not f then error("Cannot write " .. path) end
f:write(content); f:close() f:write(content); f:close()
end end
@@ -477,14 +403,12 @@ end
--- @param content string --- @param content string
--- @return nil --- @return nil
function M.write_file_lf(path, content) function M.write_file_lf(path, content)
--- @type file*|nil local f = io.open(path, "wb") ---@type file*|nil
local f = io.open(path, "wb")
if not f then error("Cannot write " .. path) end if not f then error("Cannot write " .. path) end
f:write(content); f:close() f:write(content); f:close()
end end
--- @type table<string, string> -- bag: input path -> absolute path local _absolute_path_cache = {} ---@type table<string, string> -- bag: input path -> absolute path
local _absolute_path_cache = {}
--- Convert a (possibly relative) path to an absolute path, using CWD if needed. --- Convert a (possibly relative) path to an absolute path, using CWD if needed.
--- Normalizes forward slashes to backslashes on Windows. --- 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 _absolute_path_cache[path] then return _absolute_path_cache[path] end
if #path >= 2 and path:sub(2, 2) == ":" then if #path >= 2 and path:sub(2, 2) == ":" then
-- Already absolute; normalize slashes for consistency. -- Already absolute; normalize slashes for consistency.
--- @type string local result = (path:gsub("/", "\\")) ---@type string
local result = (path:gsub("/", "\\"))
_absolute_path_cache[path] = result _absolute_path_cache[path] = result
return result return result
end end
--- @type string|nil local cwd = lfs.currentdir() ---@type string|nil
local cwd = lfs.currentdir()
if not cwd then _absolute_path_cache[path] = path; return path end if not cwd then _absolute_path_cache[path] = path; return path end
cwd = cwd:gsub("/", "\\") cwd = cwd:gsub("/", "\\")
--- @type string local tail = (path:gsub("/", "\\")) ---@type string
local tail = (path:gsub("/", "\\")) local result = cwd .. "\\" .. tail ---@type string
--- @type string
local result = cwd .. "\\" .. tail
_absolute_path_cache[path] = result _absolute_path_cache[path] = result
return result return result
end end
-- Cache of directories already verified to exist in this process. -- Cache of directories already verified to exist in this process.
--- @type table<string, boolean> -- bag: dir path -> already ensured local _ensured_dirs = {} ---@type table<string, boolean> -- bag: dir path -> already ensured
local _ensured_dirs = {}
--- @param path Path --- @param path Path
--- @return nil --- @return nil
@@ -533,10 +452,8 @@ end
--- @param sources SourceFile[] --- @param sources SourceFile[]
--- @return table<string, SourceFile[]> --- @return table<string, SourceFile[]>
function M.group_sources_by_dir(sources) function M.group_sources_by_dir(sources)
--- @type table<string, SourceFile[]> local by_dir = {} ---@type table<string, SourceFile[]>
local by_dir = {} for _, src in ipairs(sources) do ---@type integer, SourceFile
--- @type integer, SourceFile
for _, src in ipairs(sources) do
by_dir[src.dir] = by_dir[src.dir] or {} by_dir[src.dir] = by_dir[src.dir] or {}
table.insert(by_dir[src.dir], src) table.insert(by_dir[src.dir], src)
end 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 --- @param pos integer
--- @return string|nil, integer --- @return string|nil, integer
function M.read_ident(s, pos) function M.read_ident(s, pos)
--- @type string|nil local result = lpeg.match(lpeg_ident_pat, s, pos) ---@type string|nil
local result = lpeg.match(lpeg_ident_pat, s, pos)
if result then return result, pos + #result end if result then return result, pos + #result end
return nil, pos return nil, pos
end end
@@ -581,21 +497,16 @@ end
--- @param pos integer --- @param pos integer
--- @return string|nil, integer --- @return string|nil, integer
function M.read_balanced(s, open_char, close_char, pos) function M.read_balanced(s, open_char, close_char, pos)
--- @type integer local open_byte = open_char:byte() ---@type integer
local open_byte = open_char:byte()
if s:byte(pos) ~= open_byte then return nil, pos end if s:byte(pos) ~= open_byte then return nil, pos end
-- scan: <open_char> -- scan: <open_char>
pos = pos + 1 pos = pos + 1
-- scan: <open_char> <inner...> -- scan: <open_char> <inner...>
--- @type integer local len = #s ---@type integer
local len = #s local depth = 1 ---@type integer
--- @type integer local a = pos ---@type integer
local depth = 1
--- @type integer
local a = pos
while pos <= len and depth > 0 do while pos <= len and depth > 0 do
--- @type integer local c = s:byte(pos) ---@type integer
local c = s:byte(pos)
if c == open_byte then if c == open_byte then
depth = depth + 1 depth = depth + 1
pos = pos + 1 pos = pos + 1
@@ -606,8 +517,7 @@ function M.read_balanced(s, open_char, close_char, pos)
pos = pos + 1 pos = pos + 1
-- scan: <open_char> <inner...> <close_char> (depth=depth) -- scan: <open_char> <inner...> <close_char> (depth=depth)
else else
--- @type integer local nx = M.skip_str_or_cmt(s, pos) ---@type integer
local nx = M.skip_str_or_cmt(s, pos)
if nx > pos then if nx > pos then
-- scan: <open_char> <inner...> <str|cmt> -- scan: <open_char> <inner...> <str|cmt>
pos = nx pos = nx
@@ -641,20 +551,16 @@ M.read_brackets = function(s, pos) return M.read_balanced(s, "[", "]", pos) end
--- @param start integer --- @param start integer
--- @return integer|nil --- @return integer|nil
function M.scan_to_char(s, target, start) function M.scan_to_char(s, target, start)
--- @type integer local target_byte = target:byte() ---@type integer
local target_byte = target:byte() local pos = start ---@type integer
--- @type integer
local pos = start
while pos <= #s do while pos <= #s do
--- @type integer local c = s:byte(pos) ---@type integer
local c = s:byte(pos)
if c == target_byte then return pos end -- scan: ... <target found> | <skipping to target> 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> ) ... 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_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> ] ... elseif c == BYTE_OPEN_BRACK then local _, a = M.read_balanced(s, "[", "]", pos); pos = a -- scan: ... [ <balanced> ] ...
else else
--- @type integer local nx = M.skip_str_or_cmt(s, pos) ---@type integer
local nx = M.skip_str_or_cmt(s, pos)
pos = (nx > pos) and nx or (pos + 1) pos = (nx > pos) and nx or (pos + 1)
-- scan: ... <str|cmt skipped> ... -- scan: ... <str|cmt skipped> ...
end end
@@ -670,10 +576,8 @@ end
--- @return integer|nil --- @return integer|nil
function M.skip_preprocessor_line(s, pos) function M.skip_preprocessor_line(s, pos)
if s:byte(pos) ~= 35 then return nil end -- '#' if s:byte(pos) ~= 35 then return nil end -- '#'
--- @type integer local scan = pos ---@type integer
local scan = pos local len = #s ---@type integer
--- @type integer
local len = #s
while scan <= len and s:byte(scan) ~= BYTE_NEWLINE do scan = scan + 1 end while scan <= len and s:byte(scan) ~= BYTE_NEWLINE do scan = scan + 1 end
return scan + 1 return scan + 1
end end
@@ -689,8 +593,7 @@ end
--- @param after_last integer --- @param after_last integer
--- @return boolean --- @return boolean
local function segment_has_newline(source, first, after_last) local function segment_has_newline(source, first, after_last)
--- @type integer for pos = first, after_last - 1 do ---@type integer
for pos = first, after_last - 1 do
if source:byte(pos) == BYTE_NEWLINE then return true end if source:byte(pos) == BYTE_NEWLINE then return true end
end end
return false return false
@@ -701,13 +604,11 @@ end
--- @return integer|nil --- @return integer|nil
local function skip_directive_space(source, pos) local function skip_directive_space(source, pos)
while pos <= #source do while pos <= #source do
--- @type integer local byte = source:byte(pos) ---@type integer
local byte = source:byte(pos)
if is_horizontal_space(byte) then if is_horizontal_space(byte) then
pos = pos + 1 pos = pos + 1
elseif byte == BYTE_SLASH and source:byte(pos + 1) == BYTE_STAR then elseif byte == BYTE_SLASH and source:byte(pos + 1) == BYTE_STAR then
--- @type integer local after = M.skip_str_or_cmt(source, pos) ---@type integer
local after = M.skip_str_or_cmt(source, pos)
if after == pos or segment_has_newline(source, pos, after) then return nil end if after == pos or segment_has_newline(source, pos, after) then return nil end
pos = after pos = after
elseif byte == BYTE_SLASH and source:byte(pos + 1) == BYTE_SLASH then elseif byte == BYTE_SLASH and source:byte(pos + 1) == BYTE_SLASH then
@@ -724,21 +625,14 @@ end
--- @param source string --- @param source string
--- @return string, integer[], integer[] --- @return string, integer[], integer[]
local function splice_c_lines(source) local function splice_c_lines(source)
--- @type string[] local logical_bytes = {} ---@type string[]
local logical_bytes = {} local physical_pos = {} ---@type integer[]
--- @type integer[] local physical_line = {} ---@type integer[]
local physical_pos = {} local pos = 1 ---@type integer
--- @type integer[] local line = 1 ---@type integer
local physical_line = {}
--- @type integer
local pos = 1
--- @type integer
local line = 1
while pos <= #source do while pos <= #source do
--- @type integer local byte = source:byte(pos) ---@type integer
local byte = source:byte(pos) local splice_len = nil ---@type integer|nil
--- @type integer|nil
local splice_len = nil
if byte == BYTE_BACKSLASH and source:byte(pos + 1) == BYTE_NEWLINE then if byte == BYTE_BACKSLASH and source:byte(pos + 1) == BYTE_NEWLINE then
splice_len = 2 splice_len = 2
elseif byte == BYTE_BACKSLASH and source:byte(pos + 1) == BYTE_CR and source:byte(pos + 2) == BYTE_NEWLINE then 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 pos = pos + splice_len
line = line + 1 line = line + 1
else else
--- @type integer local logical_pos = #logical_bytes + 1 ---@type integer
local logical_pos = #logical_bytes + 1
logical_bytes[logical_pos] = source:sub(pos, pos) logical_bytes[logical_pos] = source:sub(pos, pos)
physical_pos [logical_pos] = pos physical_pos [logical_pos] = pos
physical_line[logical_pos] = line 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. -- Each arm's effect on (pos, line_leading) is annotated at the branch site.
-- Arm order: newline / horiz-space / '//' / '/*' / '"' / '\'' / '#' / default. -- Arm order: newline / horiz-space / '//' / '/*' / '"' / '\'' / '#' / default.
--- @type string, integer[], integer[] local logical_text, physical_pos, physical_line = splice_c_lines(source_text) ---@type string, integer[], integer[]
local logical_text, physical_pos, physical_line = splice_c_lines(source_text) local includes = {} ---@type QuotedInclude[]
--- @type QuotedInclude[] local pos = 1 ---@type integer
local includes = {} local line_leading = true ---@type boolean
--- @type integer
local pos = 1
--- @type boolean
local line_leading = true
while pos <= #logical_text do while pos <= #logical_text do
--- @type integer local byte = logical_text:byte(pos) ---@type integer
local byte = logical_text:byte(pos)
if byte == BYTE_NEWLINE then if byte == BYTE_NEWLINE then
-- line break; refresh leading-whitespace state for next line. -- line break; refresh leading-whitespace state for next line.
line_leading = true line_leading = true
@@ -794,22 +682,19 @@ function M.parse_direct_quoted_includes(source_text)
pos = pos + 1 pos = pos + 1
elseif byte == BYTE_SLASH and logical_text:byte(pos + 1) == BYTE_SLASH then 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. -- '//' 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) ---@type integer
local after = M.skip_str_or_cmt(logical_text, pos)
-- pos := after when the skipper agrees, else single-byte advance. -- pos := after when the skipper agrees, else single-byte advance.
pos = (after > pos) and after or (pos + 1) pos = (after > pos) and after or (pos + 1)
elseif byte == BYTE_SLASH and logical_text:byte(pos + 1) == BYTE_STAR then elseif byte == BYTE_SLASH and logical_text:byte(pos + 1) == BYTE_STAR then
-- '/*' block comment. -- '/*' block comment.
--- @type integer local after = M.skip_str_or_cmt(logical_text, pos) ---@type integer
local after = M.skip_str_or_cmt(logical_text, pos)
if after <= pos then if after <= pos then
-- skipper refused (unterminated /*). Treat this byte as ordinary content: step one, mark non-leading. -- skipper refused (unterminated /*). Treat this byte as ordinary content: step one, mark non-leading.
line_leading = false line_leading = false
pos = pos + 1 pos = pos + 1
else else
-- jump past the closing '*/'. The span may cross lines, so rescan for embedded '\n' to refresh line_leading. -- 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 ---@type integer
for scan = pos, after - 1 do
if logical_text:byte(scan) == BYTE_NEWLINE then line_leading = true end if logical_text:byte(scan) == BYTE_NEWLINE then line_leading = true end
end end
pos = after pos = after
@@ -817,17 +702,14 @@ function M.parse_direct_quoted_includes(source_text)
elseif byte == BYTE_DQUOTE or byte == BYTE_SQUOTE then 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. -- enter + leave the string literal in one skip; literal bodies cannot contain a directive regardless of what they look like.
line_leading = false line_leading = false
--- @type integer local after = M.skip_str_or_cmt(logical_text, pos) ---@type integer
local after = M.skip_str_or_cmt(logical_text, pos)
pos = (after > pos) and after or (pos + 1) pos = (after > pos) and after or (pos + 1)
elseif byte == 35 and line_leading then -- '#' at line head elseif byte == 35 and line_leading then -- '#' at line head
-- Sequential pre-checks; any one failing falls through to ::not_include:: (single-byte advance). -- 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. -- 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.) -- (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 integer, integer, integer|nil, string|nil, integer, integer
local hash_pos, directive_line, scan, ident, after_ident, after_quote local include_path, physical_first, physical_last ---@type string, integer, integer
--- @type string, integer, integer
local include_path, physical_first, physical_last
hash_pos = pos hash_pos = pos
directive_line = physical_line[hash_pos] or 1 directive_line = physical_line[hash_pos] or 1
scan = skip_directive_space(logical_text, pos + 1) scan = skip_directive_space(logical_text, pos + 1)
@@ -875,8 +757,7 @@ end
--- @param wanted string --- @param wanted string
--- @return boolean --- @return boolean
local function path_has_segment(path, wanted) local function path_has_segment(path, wanted)
--- @type string for segment in M.normalize_path(path):gmatch("[^/]+") do ---@type string
for segment in M.normalize_path(path):gmatch("[^/]+") do
if segment:lower() == wanted then return true end if segment:lower() == wanted then return true end
end end
return false return false
@@ -887,16 +768,14 @@ end
--- @return boolean --- @return boolean
local function canonical_key_is_within(candidate_key, root_key) local function canonical_key_is_within(candidate_key, root_key)
if candidate_key == root_key then return true end if candidate_key == root_key then return true end
--- @type string local prefix = root_key .. "/" ---@type string
local prefix = root_key .. "/"
return candidate_key:sub(1, #prefix) == prefix return candidate_key:sub(1, #prefix) == prefix
end end
--- @param path Path --- @param path Path
--- @return SourceFile --- @return SourceFile
local function load_source_record(path) local function load_source_record(path)
--- @type Path local normalized = absolute_normalized_path(path) ---@type Path
local normalized = absolute_normalized_path(path)
return { return {
path = normalized, path = normalized,
text = M.read_file(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) error("resolve_source_corpus requires options.project_root", 2)
end end
--- @type Path local project_root = absolute_normalized_path(options.project_root) ---@type Path
local project_root = absolute_normalized_path(options.project_root) local code_root = M.normalize_path(project_root .. "/code") ---@type Path
--- @type Path local code_root_key = M.canonical_path_key(code_root) ---@type string
local code_root = M.normalize_path(project_root .. "/code") local root = load_source_record(options.unity_root) ---@type SourceFile
--- @type string local source_order = { root } ---@type SourceFile[]
local code_root_key = M.canonical_path_key(code_root) local sources_by_path = { [M.canonical_path_key(root.path)] = root, } ---@type table<Path, SourceFile>
--- @type SourceFile local resolver = { ---@type SourceResolver
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 = {
resolved = { resolved = {
{ {
include_path = nil, include_path = nil,
@@ -949,22 +821,14 @@ function M.resolve_source_corpus(options)
shadowed = {}, shadowed = {},
} }
--- @type integer, QuotedInclude for _, include in ipairs(M.parse_direct_quoted_includes(root.text)) do ---@type integer, QuotedInclude
for _, include in ipairs(M.parse_direct_quoted_includes(root.text)) do local candidate_a = absolute_normalized_path(root.dir .. "/" .. include.path) ---@type Path
--- @type Path local candidate_b = absolute_normalized_path(code_root .. "/" .. include.path) ---@type Path
local candidate_a = absolute_normalized_path(root.dir .. "/" .. include.path) local key_a = M.canonical_path_key(candidate_a) ---@type string
--- @type Path local key_b = M.canonical_path_key(candidate_b) ---@type string
local candidate_b = absolute_normalized_path(code_root .. "/" .. include.path) local inside_a = canonical_key_is_within(key_a, code_root_key) ---@type boolean
--- @type string local inside_b = canonical_key_is_within(key_b, code_root_key) ---@type boolean
local key_a = M.canonical_path_key(candidate_a) local evidence = { ---@type ResolverEvidence
--- @type string
local key_b = M.canonical_path_key(candidate_b)
--- @type boolean
local inside_a = canonical_key_is_within(key_a, code_root_key)
--- @type boolean
local inside_b = canonical_key_is_within(key_b, code_root_key)
--- @type ResolverEvidence
local evidence = {
include_path = include.path, include_path = include.path,
include_text = include.include_text, include_text = include.include_text,
root_source = root.path, root_source = root.path,
@@ -988,16 +852,11 @@ function M.resolve_source_corpus(options)
resolver.skipped[#resolver.skipped + 1] = evidence resolver.skipped[#resolver.skipped + 1] = evidence
else else
-- Boundary checks above deliberately precede every filesystem probe. -- 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_a = inside_a and lfs.attributes(candidate_a, "mode") == "file" local exists_b = inside_b and ((key_b == key_a and exists_a) or lfs.attributes(candidate_b, "mode") == "file") ---@type boolean
--- @type boolean local selected = nil ---@type Path|nil
local exists_b = inside_b and ((key_b == key_a and exists_a) or lfs.attributes(candidate_b, "mode") == "file") local selected_key = nil ---@type string|nil
--- @type Path|nil local disposition = nil ---@type string|nil
local selected = nil
--- @type string|nil
local selected_key = nil
--- @type string|nil
local disposition = nil
if exists_a then if exists_a then
selected = candidate_a selected = candidate_a
selected_key = key_a selected_key = key_a
@@ -1034,8 +893,7 @@ function M.resolve_source_corpus(options)
evidence.duplicate_of = sources_by_path[selected_key].path evidence.duplicate_of = sources_by_path[selected_key].path
resolver.skipped[#resolver.skipped + 1] = evidence resolver.skipped[#resolver.skipped + 1] = evidence
else else
--- @type SourceFile local source = load_source_record(selected) ---@type SourceFile
local source = load_source_record(selected)
evidence.disposition = disposition evidence.disposition = disposition
source_order[#source_order + 1] = source source_order[#source_order + 1] = source
sources_by_path[selected_key] = source sources_by_path[selected_key] = source
@@ -1062,30 +920,23 @@ end
--- @param body string --- @param body string
--- @return string[] --- @return string[]
function M.split_top_level_commas(body) function M.split_top_level_commas(body)
--- @type string[] local tokens = {} ---@type string[]
local tokens = {} local pos = 1 ---@type integer
--- @type integer local body_len = #body ---@type integer
local pos = 1 local token_start = 1 ---@type integer
--- @type integer
local body_len = #body
--- @type integer
local token_start = 1
-- True iff `chunk` contains any non-whitespace, non-comment, non-string content (i.e., real token material). -- 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). -- 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 --- @param chunk string
--- @return boolean --- @return boolean
local function has_real_content(chunk) local function has_real_content(chunk)
--- @type integer local scan = 1 ---@type integer
local scan = 1 local len = #chunk ---@type integer
--- @type integer
local len = #chunk
while scan <= len do while scan <= len do
if M.is_space_byte(chunk:byte(scan)) then if M.is_space_byte(chunk:byte(scan)) then
scan = scan + 1 scan = scan + 1
else else
--- @type integer local nx = M.skip_str_or_cmt(chunk, scan) ---@type integer
local nx = M.skip_str_or_cmt(chunk, scan)
if nx > scan then if nx > scan then
scan = nx -- skipped a comment or string scan = nx -- skipped a comment or string
else else
@@ -1100,8 +951,7 @@ function M.split_top_level_commas(body)
--- @return nil --- @return nil
local function emit(end_pos) local function emit(end_pos)
if end_pos >= token_start then if end_pos >= token_start then
--- @type string local chunk = body:sub(token_start, end_pos) ---@type string
local chunk = body:sub(token_start, end_pos)
if M.trim(chunk) ~= "" then if M.trim(chunk) ~= "" then
if has_real_content(chunk) then if has_real_content(chunk) then
tokens[#tokens + 1] = chunk tokens[#tokens + 1] = chunk
@@ -1118,8 +968,7 @@ function M.split_top_level_commas(body)
end end
while pos <= body_len do while pos <= body_len do
--- @type integer local c = body:byte(pos) ---@type integer
local c = body:byte(pos)
if c == BYTE_OPEN_PAREN then local _, a = M.read_parens(body, pos); pos = a -- scan: ... ( <balanced> ... 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_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> ... 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 pos = pos + 1
token_start = pos token_start = pos
else else
--- @type integer local nx = M.skip_str_or_cmt(body, pos) ---@type integer
local nx = M.skip_str_or_cmt(body, pos)
if nx > pos then if nx > pos then
-- scan: ... <str|cmt> ... -- scan: ... <str|cmt> ...
-- Skipped a comment or string at top level: emit token break. -- 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) -- 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, BodyToken[]> -- bag: body text -> tokens
local _tokenize_body_cache = {} local _body_line_index_cache = {} ---@type table<string, table<integer, integer>> -- bag: body text -> offset-to-line
--- @type table<string, table<integer, integer>> -- bag: body text -> offset-to-line
local _body_line_index_cache = {}
--- Tokenize the body inner-text into a flat list of `{tok, rel}` pairs. --- 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`. --- `tok` is the trimmed token string; `rel` is the byte offset within `body`.
@@ -1172,23 +1018,17 @@ local _body_line_index_cache = {}
--- @return BodyToken[] --- @return BodyToken[]
function M.tokenize_body(body) function M.tokenize_body(body)
if _tokenize_body_cache[body] ~= nil then return _tokenize_body_cache[body] end if _tokenize_body_cache[body] ~= nil then return _tokenize_body_cache[body] end
--- @type BodyToken[] local out = {} ---@type BodyToken[]
local out = {} local len = #body ---@type integer
--- @type integer local rel = 1 ---@type integer
local len = #body
--- @type integer
local rel = 1
while rel <= len do while rel <= len do
--- @type integer local ws_end = M.skip_ws_and_cmt(body, rel) ---@type integer
local ws_end = M.skip_ws_and_cmt(body, rel)
if ws_end > rel then rel = ws_end end if ws_end > rel then rel = ws_end end
if rel > len then break end if rel > len then break end
--- @type integer local scan = rel ---@type integer
local scan = rel
while scan <= len do while scan <= len do
--- @type integer local c = body:byte(scan) ---@type integer
local c = body:byte(scan)
-- Terminator bytes (delimit a token at the top level): ',' = 0x2C, '\n' = 0x0A, ';' = 0x3B. -- 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. -- 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 if c == BYTE_COMMA then break end
@@ -1196,13 +1036,11 @@ function M.tokenize_body(body)
if c == BYTE_SEMI then break end if c == BYTE_SEMI then break end
-- Line-comment '// ... \n' (0x2F 0x2F): skip to (and past) the next newline, or to end-of-body. -- 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 if c == BYTE_SLASH and body:byte(scan + 1) == BYTE_SLASH then
--- @type integer|nil local nl = M.find_byte(body, BYTE_NEWLINE, scan) ---@type integer|nil
local nl = M.find_byte(body, BYTE_NEWLINE, scan)
scan = nl and (nl + 1) or (len + 1) scan = nl and (nl + 1) or (len + 1)
-- Block-comment '/* ... */' (0x2F 0x2A): skip to (and past) the matching '*/', or to end-of-body. -- 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 elseif c == BYTE_SLASH and body:byte(scan + 1) == BYTE_STAR then
--- @type integer|nil local close = body:find("*/", scan + 2, true) ---@type integer|nil
local close = body:find("*/", scan + 2, true)
scan = close and (close + 2) or (len + 1) scan = close and (close + 2) or (len + 1)
-- Group opener bytes (consume the balanced group via the matching reader): '(' = 0x28, '{' = 0x7B, '[' = 0x5B. -- 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 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 scan = scan + 1
end end
end end
--- @type string local tok = M.trim(body:sub(rel, scan - 1)) ---@type string
local tok = M.trim(body:sub(rel, scan - 1))
if tok ~= "" then out[#out + 1] = { tok = tok, rel = rel } end if tok ~= "" then out[#out + 1] = { tok = tok, rel = rel } end
if scan <= len then if scan <= len then
scan = scan + 1 scan = scan + 1
--- @type integer local w = M.skip_ws_and_cmt(body, scan) ---@type integer
local w = M.skip_ws_and_cmt(body, scan)
if w > scan then scan = w end if w > scan then scan = w end
end end
rel = scan rel = scan
@@ -1236,14 +1072,10 @@ end
--- @return table<integer, integer> -- bag: byte offset -> 1-based line --- @return table<integer, integer> -- bag: byte offset -> 1-based line
function M.build_body_line_index(body) function M.build_body_line_index(body)
if _body_line_index_cache[body] ~= nil then return _body_line_index_cache[body] end 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 table<integer, integer> -- bag: byte offset -> 1-based line
local index = {} local len = #body ---@type integer
--- @type integer local newline_count = 0 ---@type integer
local len = #body for pos = 1, len do ---@type integer
--- @type integer
local newline_count = 0
--- @type integer
for pos = 1, len do
if pos > 1 then if pos > 1 then
index[pos] = newline_count + 1 index[pos] = newline_count + 1
end end
@@ -1265,31 +1097,20 @@ end
--- @param metadata_path Path --- @param metadata_path Path
--- @return WordCounts --- @return WordCounts
function M.load_word_counts(metadata_path) function M.load_word_counts(metadata_path)
--- @type WordCounts local counts = {} ---@type WordCounts
local counts = {} local content = M.read_file(metadata_path) ---@type string
--- @type string local len = #content ---@type integer
local content = M.read_file(metadata_path) local pos = 1 ---@type integer
--- @type integer local prefix = "WORD_COUNT(" ---@type string
local len = #content
--- @type integer
local pos = 1
--- @type string
local prefix = "WORD_COUNT("
while pos <= len do while pos <= len do
--- @type integer|nil local nl = M.find_byte(content, BYTE_NEWLINE, pos) ---@type integer|nil
local nl = M.find_byte(content, BYTE_NEWLINE, pos) local line_end = nl or (len + 1) ---@type integer
--- @type integer local line = content:sub(pos, line_end - 1) ---@type string
local line_end = nl or (len + 1)
--- @type string
local line = content:sub(pos, line_end - 1)
-- scan: WORD_COUNT(<name>, <N>) -- scan: WORD_COUNT(<name>, <N>)
--- @type string local trimmed = M.trim(line) ---@type string
local trimmed = M.trim(line)
if trimmed:sub(1, #prefix) == prefix and trimmed:sub(-1) == ")" then if trimmed:sub(1, #prefix) == prefix and trimmed:sub(-1) == ")" then
--- @type string local inner = trimmed:sub(#prefix + 1, #trimmed - 1) ---@type string
local inner = trimmed:sub(#prefix + 1, #trimmed - 1) local comma = M.find_byte(inner, BYTE_COMMA, 1) ---@type integer|nil
--- @type integer|nil
local comma = M.find_byte(inner, BYTE_COMMA, 1)
if comma then if comma then
counts[M.trim(inner:sub(1, comma - 1))] = counts[M.trim(inner:sub(1, comma - 1))] =
tonumber(M.trim(inner:sub(comma + 1))) tonumber(M.trim(inner:sub(comma + 1)))
@@ -1307,12 +1128,9 @@ end
--- @param source string --- @param source string
--- @return LineIndexFn --- @return LineIndexFn
function M.LineIndex(source) function M.LineIndex(source)
--- @type integer[] local positions = {} ---@type integer[]
local positions = {} local n = 0 ---@type integer
--- @type integer for pos = 1, #source do ---@type integer
local n = 0
--- @type integer
for pos = 1, #source do
if source:byte(pos) == BYTE_NEWLINE then if source:byte(pos) == BYTE_NEWLINE then
n = n + 1 n = n + 1
positions[n] = pos positions[n] = pos
@@ -1322,11 +1140,9 @@ function M.LineIndex(source)
--- @param query_pos integer --- @param query_pos integer
--- @return integer --- @return integer
local function line_of(query_pos) local function line_of(query_pos)
--- @type integer, integer local lo, hi = 1, n ---@type integer, integer
local lo, hi = 1, n
while lo <= hi do while lo <= hi do
--- @type integer local mid = math.floor((lo + hi) / 2) ---@type integer
local mid = math.floor((lo + hi) / 2)
if positions[mid] <= query_pos then lo = mid + 1 if positions[mid] <= query_pos then lo = mid + 1
else hi = mid - 1 end else hi = mid - 1 end
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 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 --- @field collect_symbols fun(adapter: Elf32Adapter, sections: Elf32Section[]): table<string, Elf32Sym>|nil, string|nil
--- @type Elf32Mod local M = {} ---@type Elf32Mod
local M = {}
-- ════════════════════════════════════════════════════════════════════════════ -- ════════════════════════════════════════════════════════════════════════════
-- Little-endian readers (bit-weighted accumulator, math.floor only) -- Little-endian readers (bit-weighted accumulator, math.floor only)
@@ -168,8 +167,7 @@ end
--- @param off integer --- @param off integer
--- @return integer --- @return integer
function M.read_u32_le(buf, off) function M.read_u32_le(buf, off)
--- @type integer local byte_off = off + 1 ---@type integer
local byte_off = off + 1
return buf:byte(byte_off) return buf:byte(byte_off)
+ buf:byte(byte_off + 0x01) * 0x00000100 + buf:byte(byte_off + 0x01) * 0x00000100
+ buf:byte(byte_off + 0x02) * 0x00010000 + buf:byte(byte_off + 0x02) * 0x00010000
@@ -181,8 +179,7 @@ end
--- @param off integer -- zero-based wire offset --- @param off integer -- zero-based wire offset
--- @return integer --- @return integer
function M.read_u16_le(buf, off) function M.read_u16_le(buf, off)
--- @type integer local byte_off = off + 1 ---@type integer
local byte_off = off + 1
return buf:byte(byte_off) + buf:byte(byte_off + 0x01) * 0x00000100 return buf:byte(byte_off) + buf:byte(byte_off + 0x01) * 0x00000100
end end
@@ -286,8 +283,7 @@ end
--- @return string|nil --- @return string|nil
function M.get_str(strtab, off) function M.get_str(strtab, off)
if off < 0 or off >= #strtab then return nil end if off < 0 or off >= #strtab then return nil end
--- @type integer|nil local end_pos = strtab:find("\0", off + 1, true) ---@type integer|nil
local end_pos = strtab:find("\0", off + 1, true)
if not end_pos then return nil end if not end_pos then return nil end
return strtab:sub(off + 1, end_pos - 1) return strtab:sub(off + 1, end_pos - 1)
end end
@@ -305,48 +301,36 @@ end
--- @param adapter Elf32Adapter --- @param adapter Elf32Adapter
--- @return Elf32Header|nil, string|nil --- @return Elf32Header|nil, string|nil
function M.parse_elf32_headers(adapter) function M.parse_elf32_headers(adapter)
--- @type boolean, string|nil local ok, err = M.validate_adapter(adapter) ---@type boolean, string|nil
local ok, err = M.validate_adapter(adapter)
if not ok then return nil, err end if not ok then return nil, err end
-- 4-byte magic: 0x7F 'E' 'L' 'F'. -- 4-byte magic: 0x7F 'E' 'L' 'F'.
-- The byte readers take the adapter explicitly. -- 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. -- 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 b1 = M.read_u8(adapter, 0) local b2 = M.read_u8(adapter, 1) ---@type integer|nil
--- @type integer|nil local b3 = M.read_u8(adapter, 2) ---@type integer|nil
local b2 = M.read_u8(adapter, 1) local b4 = M.read_u8(adapter, 3) ---@type integer|nil
--- @type integer|nil
local b3 = M.read_u8(adapter, 2)
--- @type integer|nil
local b4 = M.read_u8(adapter, 3)
if not (b1 and b2 and b3 and b4) if not (b1 and b2 and b3 and b4)
or not (b1 == 0x7f and b2 == 0x45 and b3 == 0x4c and b4 == 0x46) then or not (b1 == 0x7f and b2 == 0x45 and b3 == 0x4c and b4 == 0x46) then
return nil, "bad_magic" return nil, "bad_magic"
end end
--- @type integer|nil local class = M.read_u8(adapter, M.ELF32_HEADER.class_offset) ---@type integer|nil
local class = M.read_u8(adapter, M.ELF32_HEADER.class_offset)
if class ~= M.ELFCLASS32 then if class ~= M.ELFCLASS32 then
return nil, "unsupported_elf_class" return nil, "unsupported_elf_class"
end end
--- @type integer|nil local data = M.read_u8(adapter, M.ELF32_HEADER.endian_offset) ---@type integer|nil
local data = M.read_u8(adapter, M.ELF32_HEADER.endian_offset)
if data ~= M.ELFDATA2LSB then if data ~= M.ELFDATA2LSB then
return nil, "unsupported_elf_data" return nil, "unsupported_elf_data"
end end
--- @type integer|nil local e_entry = M.read_u32(adapter, M.ELF32_HEADER.e_entry_offset) ---@type integer|nil
local e_entry = M.read_u32(adapter, M.ELF32_HEADER.e_entry_offset) local e_shoff = M.read_u32(adapter, M.ELF32_HEADER.e_shoff_offset) ---@type integer|nil
--- @type integer|nil local e_shentsize = M.read_u16(adapter, M.ELF32_HEADER.e_shentsize_offset) ---@type integer|nil
local e_shoff = M.read_u32(adapter, M.ELF32_HEADER.e_shoff_offset) local e_shnum = M.read_u16(adapter, M.ELF32_HEADER.e_shnum_offset) ---@type integer|nil
--- @type integer|nil local e_shstrndx = M.read_u16(adapter, M.ELF32_HEADER.e_shstrndx_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)
if not (e_entry and e_shoff and e_shentsize and e_shnum and e_shstrndx) then if not (e_entry and e_shoff and e_shentsize and e_shnum and e_shstrndx) then
return nil, "truncated_header" return nil, "truncated_header"
end end
@@ -367,8 +351,7 @@ end
--- @param sh_off integer --- @param sh_off integer
--- @return Elf32Section|nil, string|nil --- @return Elf32Section|nil, string|nil
local function read_section_entry(adapter, sh_off) local function read_section_entry(adapter, sh_off)
--- @type Elf32Section local entry = { ---@type Elf32Section
local entry = {
sh_name = M.read_u32(adapter, sh_off + M.ELF32_SECTION.sh_name_offset), 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_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), 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) function M.walk_sections(adapter, hdr)
if not hdr or hdr.error then return nil, hdr and hdr.error or "truncated_section_headers" end 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) ---@type integer
local file_size = M.size(adapter)
if hdr.e_shoff + hdr.e_shnum * hdr.e_shentsize > file_size then if hdr.e_shoff + hdr.e_shnum * hdr.e_shentsize > file_size then
return nil, "truncated_section_headers" return nil, "truncated_section_headers"
end end
-- Read every section header first; we need .shstrtab to resolve names. -- Read every section header first; we need .shstrtab to resolve names.
--- @type Elf32Section[] local sections = {} ---@type Elf32Section[]
local sections = {} for i = 0, hdr.e_shnum - 1 do ---@type integer
--- @type integer local sh_off = hdr.e_shoff + i * hdr.e_shentsize ---@type integer
for i = 0, hdr.e_shnum - 1 do local entry, err = read_section_entry(adapter, sh_off) ---@type Elf32Section|nil, string|nil
--- @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)
if not entry then return nil, err end if not entry then return nil, err end
sections[i + 1] = entry sections[i + 1] = entry
end end
@@ -418,20 +396,17 @@ function M.walk_sections(adapter, hdr)
return nil, "missing_shstrtab" return nil, "missing_shstrtab"
end end
--- @type Elf32Section|nil local shstrtab = sections[hdr.e_shstrndx + 1] ---@type Elf32Section|nil
local shstrtab = sections[hdr.e_shstrndx + 1]
if not shstrtab or shstrtab.sh_type ~= M.SHT_STRTAB then if not shstrtab or shstrtab.sh_type ~= M.SHT_STRTAB then
return nil, "missing_shstrtab" return nil, "missing_shstrtab"
end end
if shstrtab.sh_offset + shstrtab.sh_size > file_size then if shstrtab.sh_offset + shstrtab.sh_size > file_size then
return nil, "truncated_section_headers" return nil, "truncated_section_headers"
end end
--- @type string|nil local shstrtab_bytes = M.read_section_bytes(adapter, shstrtab) ---@type string|nil
local shstrtab_bytes = M.read_section_bytes(adapter, shstrtab)
if not shstrtab_bytes then return nil, "truncated_section_headers" end if not shstrtab_bytes then return nil, "truncated_section_headers" end
--- @type integer, Elf32Section for _, s in ipairs(sections) do ---@type integer, Elf32Section
for _, s in ipairs(sections) do
s.name = M.get_str(shstrtab_bytes, s.sh_name) or "" s.name = M.get_str(shstrtab_bytes, s.sh_name) or ""
end end
@@ -444,15 +419,11 @@ end
--- @param section Elf32Section --- @param section Elf32Section
--- @return string|nil --- @return string|nil
function M.read_section_bytes(adapter, section) function M.read_section_bytes(adapter, section)
--- @type integer local size = section.sh_size ---@type integer
local size = section.sh_size
if size == 0 then return "" end if size == 0 then return "" end
--- @type string[] local out = {} ---@type string[]
local out = {} for i = 0, size - 1 do ---@type integer
--- @type integer local b = M.read_u8(adapter, section.sh_offset + i) ---@type integer|nil
for i = 0, size - 1 do
--- @type integer|nil
local b = M.read_u8(adapter, section.sh_offset + i)
if b == nil then return nil end if b == nil then return nil end
out[#out + 1] = string.char(b) out[#out + 1] = string.char(b)
end end
@@ -467,11 +438,9 @@ end
--- @return string|nil, string|nil --- @return string|nil, string|nil
function M.read_named_section(adapter, sections, name) function M.read_named_section(adapter, sections, name)
if not sections then return nil, "missing_section" end if not sections then return nil, "missing_section" end
--- @type integer, Elf32Section for _, s in ipairs(sections) do ---@type integer, Elf32Section
for _, s in ipairs(sections) do
if s.name == name then if s.name == name then
--- @type string|nil local bytes = M.read_section_bytes(adapter, s) ---@type string|nil
local bytes = M.read_section_bytes(adapter, s)
if not bytes then return nil, "truncated_section_data" end if not bytes then return nil, "truncated_section_data" end
return bytes, nil return bytes, nil
end end
@@ -488,59 +457,42 @@ end
--- @return table<string, Elf32Sym>|nil, string|nil --- @return table<string, Elf32Sym>|nil, string|nil
function M.collect_symbols(adapter, sections) function M.collect_symbols(adapter, sections)
if not sections then return nil, "missing_sections" end if not sections then return nil, "missing_sections" end
--- @type table<string, Elf32Sym> -- bag: symbol name -> Elf32Sym local symbols = {} ---@type table<string, Elf32Sym> -- bag: symbol name -> Elf32Sym
local symbols = {} local file_size = M.size(adapter) ---@type integer
--- @type integer for _, s in ipairs(sections) do ---@type integer, Elf32Section
local file_size = M.size(adapter)
--- @type integer, Elf32Section
for _, s in ipairs(sections) do
if s.sh_type == M.SHT_SYMTAB then if s.sh_type == M.SHT_SYMTAB then
--- @type Elf32Section|nil local strtab = sections[s.sh_link + 1] ---@type Elf32Section|nil
local strtab = sections[s.sh_link + 1]
if not strtab or strtab.sh_type ~= M.SHT_STRTAB then if not strtab or strtab.sh_type ~= M.SHT_STRTAB then
return nil, "missing_symtab_strtab" return nil, "missing_symtab_strtab"
end end
if strtab.sh_offset + strtab.sh_size > file_size then if strtab.sh_offset + strtab.sh_size > file_size then
return nil, "truncated_section_headers" return nil, "truncated_section_headers"
end end
--- @type string|nil local strtab_bytes = M.read_section_bytes(adapter, strtab) ---@type string|nil
local strtab_bytes = M.read_section_bytes(adapter, strtab)
if not strtab_bytes then return nil, "truncated_section_headers" end if not strtab_bytes then return nil, "truncated_section_headers" end
if s.sh_offset + s.sh_size > file_size then if s.sh_offset + s.sh_size > file_size then
return nil, "truncated_section_headers" return nil, "truncated_section_headers"
end end
--- @type string|nil local symtab_bytes = M.read_section_bytes(adapter, s) ---@type string|nil
local symtab_bytes = M.read_section_bytes(adapter, s)
if not symtab_bytes then return nil, "truncated_section_headers" end if not symtab_bytes then return nil, "truncated_section_headers" end
--- @type number local n = #symtab_bytes / M.ELF32_SYM.sym_entry_bytes ---@type number
local n = #symtab_bytes / M.ELF32_SYM.sym_entry_bytes for j = 0, n - 1 do ---@type integer
--- @type integer local e = s.sh_offset + j * M.ELF32_SYM.sym_entry_bytes ---@type integer
for j = 0, n - 1 do local st_name = M.read_u32(adapter, e + M.ELF32_SYM.st_name) ---@type integer|nil
--- @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)
if st_name then 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_value = M.read_u32(adapter, e + M.ELF32_SYM.st_value) local st_size = M.read_u32(adapter, e + M.ELF32_SYM.st_size) ---@type integer|nil
--- @type integer|nil local st_info = M.read_u8(adapter, e + M.ELF32_SYM.st_info) ---@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)
-- st_shndx is at offset 14 (2 bytes) — derived from the layout -- st_shndx is at offset 14 (2 bytes) — derived from the layout
-- the metaprogram reads too. Inline the read to keep the -- the metaprogram reads too. Inline the read to keep the
-- adapter as the only I/O surface. -- adapter as the only I/O surface.
--- @type integer|nil local b1 = M.read_u8(adapter, e + 14) ---@type integer|nil
local b1 = M.read_u8(adapter, e + 14) local b2 = M.read_u8(adapter, e + 15) ---@type integer|nil
--- @type integer|nil
local b2 = M.read_u8(adapter, e + 15)
if not (b1 and b2) then if not (b1 and b2) then
return nil, "truncated_section_headers" return nil, "truncated_section_headers"
end end
--- @type integer local st_shndx = b1 + b2 * 0x100 ---@type integer
local st_shndx = b1 + b2 * 0x100 local name = M.get_str(strtab_bytes, st_name) or "" ---@type string
--- @type string
local name = M.get_str(strtab_bytes, st_name) or ""
if name ~= "" then if name ~= "" then
symbols[name] = { symbols[name] = {
value = st_value, value = st_value,
+150 -300
View File
@@ -111,17 +111,14 @@
--- @field sleb128_size fun(n: integer): integer --- @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 --- @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") ---@type LfsMod
local lfs = require("lfs")
-- scripts/elf32.lua contains format-constant tables + the byte-level walker. -- 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). -- 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. -- read_u32_le is this module's reader; implementation in elf32.lua.
--- @type Elf32Mod local E = require("elf32") ---@type Elf32Mod
local E = require("elf32")
--- @type ElfDwarf local M = {} ---@type ElfDwarf
local M = {}
-- ════════════════════════════════════════════════════════════════════════════ -- ════════════════════════════════════════════════════════════════════════════
-- DWARF tag + form constants -- DWARF tag + form constants
@@ -200,8 +197,7 @@ M.DW_ATE = {
} }
-- DWARF5 §7.5.6 DW_FORM_implicit_const -- DWARF5 §7.5.6 DW_FORM_implicit_const
--- @type integer local DW_FORM_implicit_const = 0x21 ---@type integer
local DW_FORM_implicit_const = 0x21
-- ════════════════════════════════════════════════════════════════════════════ -- ════════════════════════════════════════════════════════════════════════════
-- Format-constant tables -- Format-constant tables
@@ -378,13 +374,10 @@ end
--- @return integer|nil --- @return integer|nil
--- @return integer --- @return integer
function M.read_uleb128_at(buf, pos) function M.read_uleb128_at(buf, pos)
--- @type integer, integer local value, shift = 0, 0 ---@type integer, integer
local value, shift = 0, 0 local len = #buf ---@type integer
--- @type integer
local len = #buf
while pos < len do while pos < len do
--- @type integer local b = buf:byte(pos + 1) ---@type integer
local b = buf:byte(pos + 1)
value = value + (b % 0x80) * (2 ^ shift) value = value + (b % 0x80) * (2 ^ shift)
shift = shift + 7 shift = shift + 7
pos = pos + 1 pos = pos + 1
@@ -398,13 +391,10 @@ end
--- @return integer|nil --- @return integer|nil
--- @return integer --- @return integer
function M.read_sleb128_at(buf, pos) function M.read_sleb128_at(buf, pos)
--- @type integer, integer local value, shift = 0, 0 ---@type integer, integer
local value, shift = 0, 0 local len = #buf ---@type integer
--- @type integer
local len = #buf
while pos < len do while pos < len do
--- @type integer local b = buf:byte(pos + 1) ---@type integer
local b = buf:byte(pos + 1)
value = value + (b % 0x80) * (2 ^ shift) value = value + (b % 0x80) * (2 ^ shift)
shift = shift + 7 shift = shift + 7
pos = pos + 1 pos = pos + 1
@@ -423,33 +413,27 @@ end
--- @param table_start integer --- @param table_start integer
--- @return integer|nil --- @return integer|nil
function M.find_abbrev_table_end(table_bytes, table_start) function M.find_abbrev_table_end(table_bytes, table_start)
--- @type integer, integer local pos, len = table_start, #table_bytes ---@type integer, integer
local pos, len = table_start, #table_bytes
if pos >= len or table_bytes:byte(pos + 1) == 0 then return pos end if pos >= len or table_bytes:byte(pos + 1) == 0 then return pos end
while pos < len do while pos < len do
--- @type integer|nil, integer local _code, code_end = M.read_uleb128_at(table_bytes, pos) ---@type integer|nil, integer
local _code, code_end = M.read_uleb128_at(table_bytes, pos)
if not _code then return nil end if not _code then return nil end
pos = code_end pos = code_end
--- @type integer|nil, integer local _tag, tag_end = M.read_uleb128_at(table_bytes, pos) ---@type integer|nil, integer
local _tag, tag_end = M.read_uleb128_at(table_bytes, pos)
if not _tag then return nil end if not _tag then return nil end
pos = tag_end pos = tag_end
if pos >= len then return nil end if pos >= len then return nil end
pos = pos + 1 -- has_children byte pos = pos + 1 -- has_children byte
while pos < len do while pos < len do
--- @type integer|nil, integer local attr, attr_end = M.read_uleb128_at(table_bytes, pos) ---@type integer|nil, integer
local attr, attr_end = M.read_uleb128_at(table_bytes, pos)
if not attr then return nil end if not attr then return nil end
pos = attr_end pos = attr_end
--- @type integer|nil, integer local form, form_end = M.read_uleb128_at(table_bytes, pos) ---@type integer|nil, integer
local form, form_end = M.read_uleb128_at(table_bytes, pos)
if not form then return nil end if not form then return nil end
pos = form_end pos = form_end
if attr == 0 and form == 0 then break end if attr == 0 and form == 0 then break end
if form == DW_FORM_implicit_const then if form == DW_FORM_implicit_const then
--- @type integer|nil, integer local _c, ce = M.read_sleb128_at(table_bytes, pos) ---@type integer|nil, integer
local _c, ce = M.read_sleb128_at(table_bytes, pos)
if not _c then return nil end if not _c then return nil end
pos = ce pos = ce
end end
@@ -466,10 +450,8 @@ end
--- @param off integer --- @param off integer
--- @return string --- @return string
local function read_c_string_at(buf, off) local function read_c_string_at(buf, off)
--- @type integer local len = #buf ---@type integer
local len = #buf local start = off ---@type integer
--- @type integer
local start = off
while off < len and buf:byte(off + 1) ~= 0 do off = off + 1 end while off < len and buf:byte(off + 1) ~= 0 do off = off + 1 end
return buf:sub(start + 1, off) return buf:sub(start + 1, off)
end end
@@ -482,41 +464,31 @@ end
--- @return AbbrevDecl[]|nil --- @return AbbrevDecl[]|nil
--- @return string|nil --- @return string|nil
local function parse_abbrev_table(table_bytes, table_start) local function parse_abbrev_table(table_bytes, table_start)
--- @type integer|nil local table_end = M.find_abbrev_table_end(table_bytes, table_start) ---@type integer|nil
local table_end = M.find_abbrev_table_end(table_bytes, table_start)
if not table_end then return nil, "no terminator" end if not table_end then return nil, "no terminator" end
--- @type AbbrevDecl[] local decls = {} ---@type AbbrevDecl[]
local decls = {} local pos = table_start ---@type integer
--- @type integer
local pos = table_start
while pos < table_end do while pos < table_end do
--- @type integer|nil, integer local code, code_end = M.read_uleb128_at(table_bytes, pos) ---@type integer|nil, integer
local code, code_end = M.read_uleb128_at(table_bytes, pos)
if not code then return nil, "truncated code" end if not code then return nil, "truncated code" end
pos = code_end pos = code_end
--- @type integer|nil, integer local tag, tag_end = M.read_uleb128_at(table_bytes, pos) ---@type integer|nil, integer
local tag, tag_end = M.read_uleb128_at(table_bytes, pos)
if not tag then return nil, "truncated tag" end if not tag then return nil, "truncated tag" end
pos = tag_end pos = tag_end
--- @type integer local has_children = table_bytes:byte(pos + 1) ---@type integer
local has_children = table_bytes:byte(pos + 1)
pos = pos + 1 pos = pos + 1
--- @type AbbrevAttr[] local attrs = {} ---@type AbbrevAttr[]
local attrs = {}
while true do while true do
--- @type integer|nil, integer local attr, attr_end = M.read_uleb128_at(table_bytes, pos) ---@type integer|nil, integer
local attr, attr_end = M.read_uleb128_at(table_bytes, pos)
if not attr then return nil, "truncated attr" end if not attr then return nil, "truncated attr" end
pos = attr_end pos = attr_end
--- @type integer|nil, integer local form, form_end = M.read_uleb128_at(table_bytes, pos) ---@type integer|nil, integer
local form, form_end = M.read_uleb128_at(table_bytes, pos)
if not form then return nil, "truncated form" end if not form then return nil, "truncated form" end
pos = form_end pos = form_end
if attr == 0 and form == 0 then break end if attr == 0 and form == 0 then break end
attrs[#attrs + 1] = { name = attr, form = form } attrs[#attrs + 1] = { name = attr, form = form }
if form == DW_FORM_implicit_const then if form == DW_FORM_implicit_const then
--- @type integer|nil, integer local _c, ce = M.read_sleb128_at(table_bytes, pos) ---@type integer|nil, integer
local _c, ce = M.read_sleb128_at(table_bytes, pos)
if not _c then return nil, "truncated const" end if not _c then return nil, "truncated const" end
pos = ce pos = ce
end end
@@ -531,8 +503,7 @@ end
-- For DW_FORM_strp we return the inline string resolved from `str_buf`. -- For DW_FORM_strp we return the inline string resolved from `str_buf`.
-- For DW_FORM_ref4 we return the absolute CU-relative offset. -- For DW_FORM_ref4 we return the absolute CU-relative offset.
-- The caller decides whether to interpret that as a section 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 = { ---@type table<integer, fun(buf: string, str_buf: string, pos: integer): (string|integer|nil, integer)>
local FORM_READERS = {
--- @param buf string --- @param buf string
--- @param _ string --- @param _ string
--- @param pos integer --- @param pos integer
@@ -547,8 +518,7 @@ local FORM_READERS = {
--- @return string --- @return string
--- @return integer --- @return integer
[M.DW_FORM.string] = function(buf, _, pos) [M.DW_FORM.string] = function(buf, _, pos)
--- @type string local s = read_c_string_at(buf, pos) ---@type string
local s = read_c_string_at(buf, pos)
return s, pos + #s + 1 return s, pos + #s + 1
end, end,
--- @param buf string --- @param buf string
@@ -558,8 +528,7 @@ local FORM_READERS = {
--- @return integer --- @return integer
[M.DW_FORM.strp] = function(buf, str_buf, pos) [M.DW_FORM.strp] = function(buf, str_buf, pos)
-- DW_FORM_strp: 4-byte offset into .debug_str. -- DW_FORM_strp: 4-byte offset into .debug_str.
--- @type integer local strp_off = M.read_u32_le(buf, pos) ---@type integer
local strp_off = M.read_u32_le(buf, pos)
return read_c_string_at(str_buf, strp_off), pos + 4 return read_c_string_at(str_buf, strp_off), pos + 4
end, end,
--- @param buf string --- @param buf string
@@ -627,8 +596,7 @@ local FORM_READERS = {
--- @return integer --- @return integer
[M.DW_FORM.exprloc] = function(buf, _, pos) [M.DW_FORM.exprloc] = function(buf, _, pos)
-- DW_FORM_exprloc: ULEB byte count + that many bytes of DW_OP_*. -- 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) ---@type integer|nil, integer
local len, ne = M.read_uleb128_at(buf, pos)
if not len then return nil, pos end if not len then return nil, pos end
return nil, ne + len return nil, ne + len
end, end,
@@ -654,8 +622,7 @@ local FORM_READERS = {
-- then the high 4 to resolve the specific type within it. -- 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; -- 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). -- 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) ---@type integer, integer, integer
local _, _, next_pos = M.read_ref_sig8(buf, pos)
return M.read_u32_le(buf, pos), next_pos return M.read_u32_le(buf, pos), next_pos
end, end,
} }
@@ -666,8 +633,7 @@ local FORM_READERS = {
--- @return string|integer|nil --- @return string|integer|nil
--- @return integer --- @return integer
local function read_form_value(buf, str_buf, pos, form) 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] ---@type (fun(buf: string, str_buf: string, pos: integer): (string|integer|nil, integer))|nil
local r = FORM_READERS[form]
if not r then if not r then
return nil, pos return nil, pos
end 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 --- @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 --- @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) function M.find_type_unit_by_signature(info, target_sig_lo, target_sig_hi)
--- @type integer local pos = 0 ---@type integer
local pos = 0 local section_len = #info ---@type integer
--- @type integer
local section_len = #info
while pos + 4 < section_len do while pos + 4 < section_len do
--- @type integer local unit_length = M.read_u32_le(info, pos) ---@type integer
local unit_length = M.read_u32_le(info, pos)
if unit_length == 0xFFFFFFFF then if unit_length == 0xFFFFFFFF then
return nil, nil -- DWARF64 not supported return nil, nil -- DWARF64 not supported
end end
-- unit_length is the body size, NOT including the 4-byte unit_length field itself. -- 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_start = pos + 4 local body_end = body_start + unit_length ---@type integer
--- @type integer
local body_end = body_start + unit_length
if body_end > section_len then if body_end > section_len then
return nil, nil -- malformed return nil, nil -- malformed
end 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 4-7: debug_abbrev_offset (4)
-- byte 8-15: type_signature (8) -- byte 8-15: type_signature (8)
-- byte 16-19: type_offset (4) -- byte 16-19: type_offset (4)
--- @type integer local unit_type = info:byte(body_start + 2 + 1) ---@type integer -- 0-based +2 = unit_type in 1-indexed
local unit_type = info:byte(body_start + 2 + 1) -- 0-based +2 = unit_type in 1-indexed
if unit_type == 0x02 then -- DW_UT_type 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) ---@type integer, integer, integer -- 0-based +8 = type_signature in 1-indexed
local sig_lo, sig_hi, _ = M.read_ref_sig8(info, body_start + 8) -- 0-based +8 = type_signature in 1-indexed
if sig_lo == target_sig_lo and sig_hi == target_sig_hi then 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) ---@type integer -- 0-based +16 = type_offset in 1-indexed
local type_offset = M.read_u32_le(info, body_start + 16) -- 0-based +16 = type_offset in 1-indexed
return pos, type_offset return pos, type_offset
end end
end end
@@ -796,16 +754,12 @@ end
function M.read_elf_sections(elf_path, section_names) function M.read_elf_sections(elf_path, section_names)
-- Initialize result with all requested names set to "" so callers can do `sections[X] -- Initialize result with all requested names set to "" so callers can do `sections[X]
-- or ""` for missing sections without nil-checks. -- or ""` for missing sections without nil-checks.
--- @type table<string, string> local result = {} ---@type table<string, string>
local result = {} for _, name in ipairs(section_names) do result[name] = "" end ---@type integer, string
--- @type integer, string
for _, name in ipairs(section_names) do result[name] = "" end
-- O(1) lookup set. -- O(1) lookup set.
--- @type table<string, boolean> -- bag: requested section name -> true local wanted = {} ---@type table<string, boolean> -- bag: requested section name -> true
local wanted = {} for _, name in ipairs(section_names) do wanted[name] = true end ---@type integer, string
--- @type integer, string
for _, name in ipairs(section_names) do wanted[name] = true end
-- Existence check (lfs.attributes avoids an io.open-vs-fail race). -- Existence check (lfs.attributes avoids an io.open-vs-fail race).
if lfs.attributes(elf_path, "mode") ~= "file" then if lfs.attributes(elf_path, "mode") ~= "file" then
@@ -813,27 +767,23 @@ function M.read_elf_sections(elf_path, section_names)
return result return result
end end
--- @type file*|nil local f = io.open(elf_path, "rb") ---@type file*|nil
local f = io.open(elf_path, "rb")
if not f then if not f then
io.stderr:write(string.format("[elf_dwarf.read_elf_sections] io.open failed: %s\n", elf_path)) io.stderr:write(string.format("[elf_dwarf.read_elf_sections] io.open failed: %s\n", elf_path))
return result return result
end end
--- @type integer local file_size ---@type integer
local file_size
do do
f:seek("end", 0) f:seek("end", 0)
file_size = f:seek("cur", 0) file_size = f:seek("cur", 0)
end end
--- @type Elf32Adapter local adapter = { ---@type Elf32Adapter
local adapter = {
--- @param offset integer --- @param offset integer
--- @return integer|nil --- @return integer|nil
read_u8_at = function(offset) read_u8_at = function(offset)
f:seek("set", offset) f:seek("set", offset)
--- @type string|nil local b = f:read(1) ---@type string|nil
local b = f:read(1)
if not b then return nil end if not b then return nil end
return b:byte() return b:byte()
end, end,
@@ -841,10 +791,8 @@ function M.read_elf_sections(elf_path, section_names)
--- @return integer|nil --- @return integer|nil
read_u16_at = function(offset) read_u16_at = function(offset)
f:seek("set", offset) f:seek("set", offset)
--- @type string|nil local b1 = f:read(1) ---@type string|nil
local b1 = f:read(1) local b2 = f:read(1) ---@type string|nil
--- @type string|nil
local b2 = f:read(1)
if not b1 or not b2 then return nil end if not b1 or not b2 then return nil end
return b1:byte() + b2:byte() * 0x100 return b1:byte() + b2:byte() * 0x100
end, end,
@@ -852,14 +800,10 @@ function M.read_elf_sections(elf_path, section_names)
--- @return integer|nil --- @return integer|nil
read_u32_at = function(offset) read_u32_at = function(offset)
f:seek("set", offset) f:seek("set", offset)
--- @type string|nil local b1 = f:read(1) ---@type string|nil
local b1 = f:read(1) local b2 = f:read(1) ---@type string|nil
--- @type string|nil local b3 = f:read(1) ---@type string|nil
local b2 = f:read(1) local b4 = f:read(1) ---@type string|nil
--- @type string|nil
local b3 = f:read(1)
--- @type string|nil
local b4 = f:read(1)
if not b1 or not b2 or not b3 or not b4 then return nil end if not b1 or not b2 or not b3 or not b4 then return nil end
return b1:byte() + b2:byte() * 0x100 return b1:byte() + b2:byte() * 0x100
+ b3:byte() * 0x10000 + b4:byte() * 0x1000000 + 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.*. -- Delegate the header parse + section walk to E.*.
--- @type Elf32Header|nil, string|nil local hdr, hdr_err = E.parse_elf32_headers(adapter) ---@type Elf32Header|nil, string|nil
local hdr, hdr_err = E.parse_elf32_headers(adapter)
if not hdr then if not hdr then
io.stderr:write(string.format("[elf_dwarf.read_elf_sections] header parse failed: %s\n", tostring(hdr_err))) io.stderr:write(string.format("[elf_dwarf.read_elf_sections] header parse failed: %s\n", tostring(hdr_err)))
f:close() f:close()
return result return result
end end
--- @type Elf32Section[]|nil, string|nil local sections, walk_err = E.walk_sections(adapter, hdr) ---@type Elf32Section[]|nil, string|nil
local sections, walk_err = E.walk_sections(adapter, hdr)
if not sections then if not sections then
io.stderr:write(string.format("[elf_dwarf.read_elf_sections] section walk failed: %s\n", tostring(walk_err))) io.stderr:write(string.format("[elf_dwarf.read_elf_sections] section walk failed: %s\n", tostring(walk_err)))
f:close() f:close()
@@ -886,11 +828,9 @@ function M.read_elf_sections(elf_path, section_names)
end end
-- Resolve the requested sections. -- Resolve the requested sections.
--- @type integer, Elf32Section for _, s in ipairs(sections) do ---@type integer, Elf32Section
for _, s in ipairs(sections) do
if wanted[s.name] then if wanted[s.name] then
--- @type string|nil local bytes = E.read_section_bytes(adapter, s) ---@type string|nil
local bytes = E.read_section_bytes(adapter, s)
if bytes then result[s.name] = bytes end if bytes then result[s.name] = bytes end
end end
end end
@@ -912,31 +852,26 @@ end
--- @param elf_path Path --- @param elf_path Path
--- @return table<string, NmAddr> --- @return table<string, NmAddr>
function M.read_nm(elf_path) function M.read_nm(elf_path)
--- @type table<string, NmAddr> local addrs = {} ---@type table<string, NmAddr>
local addrs = {}
-- Existence check first; an empty or missing ELF returns an empty map. -- Existence check first; an empty or missing ELF returns an empty map.
if lfs.attributes(elf_path, "mode") ~= "file" then return addrs end if lfs.attributes(elf_path, "mode") ~= "file" then return addrs end
--- @type file*|nil local f = io.open(elf_path, "rb") ---@type file*|nil
local f = io.open(elf_path, "rb")
if not f then return addrs end if not f then return addrs end
-- Build the file adapter for E.*. -- Build the file adapter for E.*.
--- @type integer local file_size ---@type integer
local file_size
do do
f:seek("end", 0) f:seek("end", 0)
file_size = f:seek("cur", 0) file_size = f:seek("cur", 0)
end end
--- @type Elf32Adapter local adapter = { ---@type Elf32Adapter
local adapter = {
--- @param offset integer --- @param offset integer
--- @return integer|nil --- @return integer|nil
read_u8_at = function(offset) read_u8_at = function(offset)
f:seek("set", offset) f:seek("set", offset)
--- @type string|nil local b = f:read(1) ---@type string|nil
local b = f:read(1)
if not b then return nil end if not b then return nil end
return b:byte() return b:byte()
end, end,
@@ -944,10 +879,8 @@ function M.read_nm(elf_path)
--- @return integer|nil --- @return integer|nil
read_u16_at = function(offset) read_u16_at = function(offset)
f:seek("set", offset) f:seek("set", offset)
--- @type string|nil local b1 = f:read(1) ---@type string|nil
local b1 = f:read(1) local b2 = f:read(1) ---@type string|nil
--- @type string|nil
local b2 = f:read(1)
if not b1 or not b2 then return nil end if not b1 or not b2 then return nil end
return b1:byte() + b2:byte() * 0x100 return b1:byte() + b2:byte() * 0x100
end, end,
@@ -955,14 +888,10 @@ function M.read_nm(elf_path)
--- @return integer|nil --- @return integer|nil
read_u32_at = function(offset) read_u32_at = function(offset)
f:seek("set", offset) f:seek("set", offset)
--- @type string|nil local b1 = f:read(1) ---@type string|nil
local b1 = f:read(1) local b2 = f:read(1) ---@type string|nil
--- @type string|nil local b3 = f:read(1) ---@type string|nil
local b2 = f:read(1) local b4 = f:read(1) ---@type string|nil
--- @type string|nil
local b3 = f:read(1)
--- @type string|nil
local b4 = f:read(1)
if not b1 or not b2 or not b3 or not b4 then return nil end if not b1 or not b2 or not b3 or not b4 then return nil end
return b1:byte() + b2:byte() * 0x100 return b1:byte() + b2:byte() * 0x100
+ b3:byte() * 0x10000 + b4:byte() * 0x1000000 + b3:byte() * 0x10000 + b4:byte() * 0x1000000
@@ -972,16 +901,14 @@ function M.read_nm(elf_path)
} }
-- Delegate the header + section walk to E.*. -- Delegate the header + section walk to E.*.
--- @type Elf32Header|nil, string|nil local hdr, hdr_err = E.parse_elf32_headers(adapter) ---@type Elf32Header|nil, string|nil
local hdr, hdr_err = E.parse_elf32_headers(adapter)
if not hdr then if not hdr then
io.stderr:write(string.format("[elf_dwarf.read_nm] header parse failed: %s\n", tostring(hdr_err))) io.stderr:write(string.format("[elf_dwarf.read_nm] header parse failed: %s\n", tostring(hdr_err)))
f:close() f:close()
return addrs return addrs
end end
--- @type Elf32Section[]|nil, string|nil local sections, walk_err = E.walk_sections(adapter, hdr) ---@type Elf32Section[]|nil, string|nil
local sections, walk_err = E.walk_sections(adapter, hdr)
if not sections then if not sections then
io.stderr:write(string.format("[elf_dwarf.read_nm] section walk failed: %s\n", tostring(walk_err))) io.stderr:write(string.format("[elf_dwarf.read_nm] section walk failed: %s\n", tostring(walk_err)))
f:close() f:close()
@@ -990,8 +917,7 @@ function M.read_nm(elf_path)
-- E.collect_symbols returns every defined symbol (no binding filter). -- 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). -- 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) ---@type table<string, Elf32Sym>|nil, string|nil
local symbols, sym_err = E.collect_symbols(adapter, sections)
if not symbols then if not symbols then
io.stderr:write(string.format("[elf_dwarf.read_nm] symbol collection failed: %s\n", tostring(sym_err))) io.stderr:write(string.format("[elf_dwarf.read_nm] symbol collection failed: %s\n", tostring(sym_err)))
f:close() f:close()
@@ -1000,12 +926,10 @@ function M.read_nm(elf_path)
f:close() f:close()
--- @type string, Elf32Sym for name, entry in pairs(symbols) do ---@type string, Elf32Sym
for name, entry in pairs(symbols) do
-- High nibble of st_info = binding (STB_LOCAL=0, STB_GLOBAL=1, STB_WEAK=2). -- 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. -- 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) ---@type integer
local binding = math.floor(entry.info / 16)
if (binding == 0 or binding == 1) and entry.size > 0 then if (binding == 0 or binding == 1) and entry.size > 0 then
addrs[name] = { entry.value, entry.size } addrs[name] = { entry.value, entry.size }
end end
@@ -1037,17 +961,14 @@ end
-- Spec: DWARF5 §7.6 "Variable-Length Data" / Appendix C. -- Spec: DWARF5 §7.6 "Variable-Length Data" / Appendix C.
-- Top bit of each LEB128 byte. Set if more bytes follow in the encoding. -- Top bit of each LEB128 byte. Set if more bytes follow in the encoding.
--- @type integer local LEB_CONT_BIT = 0x80 ---@type integer
local LEB_CONT_BIT = 0x80
-- Low 7 bits of each LEB128 byte. The actual data payload. -- Low 7 bits of each LEB128 byte. The actual data payload.
--- @type integer local LEB_DATA_MASK = 0x7F ---@type integer
local LEB_DATA_MASK = 0x7F
-- Bit 6 of the 7-bit data (i.e. 0x40). For SLEB128: the sign-bit position used by the decoder for sign extension. -- 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. -- 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 ---@type integer
local SLEB_SIGN_BIT = 0x40
--- ULEB128 (Unsigned Little-Endian Base 128) encoder. Returns the byte string for the non-negative integer `n`. --- ULEB128 (Unsigned Little-Endian Base 128) encoder. Returns the byte string for the non-negative integer `n`.
--- Algorithm: --- Algorithm:
@@ -1064,11 +985,9 @@ function M.uleb128(n)
error("uleb128 requires non-negative number") error("uleb128 requires non-negative number")
end end
assert(n >= 0, "uleb128 requires non-negative input") assert(n >= 0, "uleb128 requires non-negative input")
--- @type string[] local bytes = {} ---@type string[]
local bytes = {}
repeat repeat
--- @type integer local b = n % (LEB_DATA_MASK + 1) ---@type integer -- extract low 7 bits
local b = n % (LEB_DATA_MASK + 1) -- extract low 7 bits
n = (n - b) / (LEB_DATA_MASK + 1) -- shift right by 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 if n > 0 then b = b + LEB_CONT_BIT end -- set continuation bit if more bytes follow
bytes[#bytes + 1] = string.char(b) bytes[#bytes + 1] = string.char(b)
@@ -1087,13 +1006,10 @@ end
--- @param n integer -- any integer (negative allowed) --- @param n integer -- any integer (negative allowed)
--- @return string --- @return string
function M.sleb128(n) function M.sleb128(n)
--- @type string[] local bytes = {} ---@type string[]
local bytes = {} local more = true ---@type boolean
--- @type boolean
local more = true
while more do while more do
--- @type integer local b = n % (LEB_DATA_MASK + 1) ---@type integer -- extract low 7 bits
local b = n % (LEB_DATA_MASK + 1) -- extract low 7 bits
n = (n - b) / (LEB_DATA_MASK + 1) -- arithmetic shift right by 7 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. -- 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 if n == 0 and b < SLEB_SIGN_BIT then more = false end -- positive terminator
@@ -1112,8 +1028,7 @@ end
function M.uleb128_size(n) function M.uleb128_size(n)
assert(n >= 0, "uleb128_size requires non-negative input") assert(n >= 0, "uleb128_size requires non-negative input")
if n == 0 then return 1 end if n == 0 then return 1 end
--- @type integer local bytes = 1 ---@type integer
local bytes = 1
while n >= 0x80 do while n >= 0x80 do
n = (n - (n % (LEB_DATA_MASK + 1))) / (LEB_DATA_MASK + 1) -- arithmetic shift right by 7 n = (n - (n % (LEB_DATA_MASK + 1))) / (LEB_DATA_MASK + 1) -- arithmetic shift right by 7
bytes = bytes + 1 bytes = bytes + 1
@@ -1129,15 +1044,11 @@ end
--- @param n integer -- any integer (negative allowed) --- @param n integer -- any integer (negative allowed)
--- @return integer --- @return integer
function M.sleb128_size(n) function M.sleb128_size(n)
--- @type boolean local more = true ---@type boolean
local more = true local bytes = 0 ---@type integer
--- @type integer local v = n ---@type integer
local bytes = 0
--- @type integer
local v = n
while more do while more do
--- @type integer local b = v % (LEB_DATA_MASK + 1) ---@type integer -- extract low 7 bits
local b = v % (LEB_DATA_MASK + 1) -- extract low 7 bits
v = (v - b) / (LEB_DATA_MASK + 1) -- arithmetic shift right by 7 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 == 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 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
--- @return table<integer, string>|nil --- @return table<integer, string>|nil
function M.read_line_unit_file_table(elf_path) 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 table<string, string>
local sections = M.read_elf_sections(elf_path, { ".debug_line", ".debug_line_str" }) local line = sections[".debug_line"] ---@type string
--- @type string local lstr = sections[".debug_line_str"] or "" ---@type string
local line = sections[".debug_line"]
--- @type string
local lstr = sections[".debug_line_str"] or ""
if not line or line == "" then 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") io.stderr:write("[elf_dwarf.read_line_unit_file_table] no .debug_line section in: " .. tostring(elf_path) .. "\n")
return nil return nil
end end
--- @type table<integer, string> -- bag: 1-based file index -> basename local basenames = {} ---@type table<integer, string> -- bag: 1-based file index -> basename
local basenames = {} local basename_to_index = {} ---@type table<string, integer> -- bag: basename -> 1-based file index
--- @type table<string, integer> -- bag: basename -> 1-based file index local paths = {} ---@type table<integer, string> -- bag: 1-based file index -> full path
local basename_to_index = {}
--- @type table<integer, string> -- bag: 1-based file index -> full path
local paths = {}
--- Read one form-code's bytes from `buf` at position `p` according to `form`. --- Read one form-code's bytes from `buf` at position `p` according to `form`.
--- Returns (value, after) where `value` is: --- Returns (value, after) where `value` is:
@@ -1210,18 +1115,14 @@ function M.read_line_unit_file_table(elf_path)
--- @return integer --- @return integer
local function read_form(buf, lstr_buf, p, form) local function read_form(buf, lstr_buf, p, form)
if form == M.DWARF5_DEBUG_LINE.form_line_strp then if form == M.DWARF5_DEBUG_LINE.form_line_strp then
--- @type integer local strp = M.read_u32_le(buf, p) ---@type integer
local strp = M.read_u32_le(buf, p) local end_pos = lstr_buf:find("\0", strp + 1, true) or (#lstr_buf + 1) ---@type integer
--- @type integer
local end_pos = lstr_buf:find("\0", strp + 1, true) or (#lstr_buf + 1)
return lstr_buf:sub(strp + 1, end_pos - 1), p + M.DWARF5_DEBUG_LINE.form_strp_bytes 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 elseif form == M.DWARF5_DEBUG_LINE.form_string then
--- @type integer local nul = buf:find("\0", p + 1, true) or (#buf + 1) ---@type integer
local nul = buf:find("\0", p + 1, true) or (#buf + 1)
return buf:sub(p + 1, nul - 1), nul return buf:sub(p + 1, nul - 1), nul
elseif form == M.DWARF5_DEBUG_LINE.form_udata then elseif form == M.DWARF5_DEBUG_LINE.form_udata then
--- @type integer|nil, integer local v, after = M.read_uleb128_at(buf, p) ---@type integer|nil, integer
local v, after = M.read_uleb128_at(buf, p)
return v, after return v, after
elseif form == M.DWARF5_DEBUG_LINE.form_data16 then elseif form == M.DWARF5_DEBUG_LINE.form_data16 then
return nil, p + M.DWARF5_DEBUG_LINE.form_data16_bytes 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>
--- @return table<integer, string> --- @return table<integer, string>
local function parse_dwarf3_unit(buf, content_start, body_end) local function parse_dwarf3_unit(buf, content_start, body_end)
--- @type integer local up = content_start ---@type integer
local up = content_start
-- 5 fixed bytes: min_insn, default_is, line_base (signed), line_range, opcode_base -- 5 fixed bytes: min_insn, default_is, line_base (signed), line_range, opcode_base
up = up + 5 up = up + 5
--- @type integer local opcode_base = buf:byte(content_start + 5) ---@type integer
local opcode_base = buf:byte(content_start + 5)
up = up + (opcode_base - 1) -- std_opcode_lengths up = up + (opcode_base - 1) -- std_opcode_lengths
--- @type string[] local dirs = {} ---@type string[]
local dirs = {}
while up < body_end do while up < body_end do
--- @type integer local nul = buf:find("\0", up + 1, true) or (body_end + 1) ---@type integer
local nul = buf:find("\0", up + 1, true) or (body_end + 1)
if nul > body_end then break end if nul > body_end then break end
--- @type integer local len = nul - up - 1 ---@type integer
local len = nul - up - 1
if len == 0 then up = nul break end if len == 0 then up = nul break end
dirs[#dirs + 1] = buf:sub(up + 1, nul - 1) dirs[#dirs + 1] = buf:sub(up + 1, nul - 1)
up = nul up = nul
end 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 -> basename
local unit_basenames = {} local unit_paths = {} ---@type table<integer, string> -- bag: 1-based unit file index -> full path
--- @type table<integer, string> -- bag: 1-based unit file index -> full path
local unit_paths = {}
while up < body_end do while up < body_end do
--- @type integer local nul = buf:find("\0", up + 1, true) or (body_end + 1) ---@type integer
local nul = buf:find("\0", up + 1, true) or (body_end + 1)
if nul > body_end or nul == up + 1 then up = nul break end if nul > body_end or nul == up + 1 then up = nul break end
--- @type string local path = buf:sub(up + 1, nul - 1) ---@type string
local path = buf:sub(up + 1, nul - 1)
up = nul up = nul
--- @type integer|nil, integer local didx, up_next = M.read_uleb128_at(buf, up); up = up_next ---@type integer|nil, integer
local didx, up_next = M.read_uleb128_at(buf, up); up = up_next local _time, up_next2 = M.read_uleb128_at(buf, up); up = up_next2 ---@type integer|nil, integer
--- @type integer|nil, integer local _size, up_next3 = M.read_uleb128_at(buf, up); up = up_next3 ---@type integer|nil, integer
local _time, up_next2 = M.read_uleb128_at(buf, up); up = up_next2 local idx = #unit_basenames + 1 ---@type integer
--- @type integer|nil, integer local bs = path:match("[^/\\]+$") or path ---@type string
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
unit_paths[idx] = path unit_paths[idx] = path
unit_basenames[idx] = bs unit_basenames[idx] = bs
dirs[1] = dirs[1] or "" -- safety: gcc emits "" sentinel dir at 0 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>
--- @return table<integer, string> --- @return table<integer, string>
local function parse_dwarf5_unit(buf, lstr_buf, content_start, body_end) local function parse_dwarf5_unit(buf, lstr_buf, content_start, body_end)
--- @type integer local up = content_start ---@type integer
local up = content_start
-- 6 fixed bytes: min_insn, max_ops_per_insn, default_is, line_base, line_range, opcode_base -- 6 fixed bytes: min_insn, max_ops_per_insn, default_is, line_base, line_range, opcode_base
up = up + 6 up = up + 6
--- @type integer local opcode_base = buf:byte(content_start + 6) ---@type integer
local opcode_base = buf:byte(content_start + 6)
up = up + (opcode_base - 1) -- std_opcode_lengths up = up + (opcode_base - 1) -- std_opcode_lengths
-- directories -- directories
--- @type integer|nil, integer local dir_format_count, after = M.read_uleb128_at(buf, up); up = after ---@type integer|nil, integer
local dir_format_count, after = M.read_uleb128_at(buf, up); up = after local dir_formats = {} ---@type integer[]
--- @type integer[] for i = 1, dir_format_count do ---@type integer
local dir_formats = {} local f, a2 = M.read_uleb128_at(buf, up); up = a2 ---@type integer|nil, integer
--- @type integer
for i = 1, dir_format_count do
--- @type integer|nil, integer
local f, a2 = M.read_uleb128_at(buf, up); up = a2
dir_formats[i] = f dir_formats[i] = f
end end
--- @type integer|nil, integer local dir_count, a3 = M.read_uleb128_at(buf, up); up = a3 ---@type integer|nil, integer
local dir_count, a3 = M.read_uleb128_at(buf, up); up = a3 local dirs = {} ---@type string[]
--- @type string[] for i = 1, dir_count do ---@type integer
local dirs = {} local combined = "" ---@type string
--- @type integer for j = 1, dir_format_count do ---@type integer
for i = 1, dir_count do local v, a4 = read_form(buf, lstr_buf, up, dir_formats[j]) ---@type string|integer|nil, integer
--- @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])
up = a4 up = a4
if j == 1 and type(v) == "string" then combined = v end if j == 1 and type(v) == "string" then combined = v end
end end
dirs[i] = combined dirs[i] = combined
end end
-- file names -- file names
--- @type integer|nil, integer local file_format_count, after2 = M.read_uleb128_at(buf, up); up = after2 ---@type integer|nil, integer
local file_format_count, after2 = M.read_uleb128_at(buf, up); up = after2 local file_formats = {} ---@type integer[]
--- @type integer[] for i = 1, file_format_count do ---@type integer
local file_formats = {} local f, a2 = M.read_uleb128_at(buf, up); up = a2 ---@type integer|nil, integer
--- @type integer
for i = 1, file_format_count do
--- @type integer|nil, integer
local f, a2 = M.read_uleb128_at(buf, up); up = a2
file_formats[i] = f file_formats[i] = f
end end
--- @type integer|nil, integer local file_count, a3 = M.read_uleb128_at(buf, up); up = a3 ---@type integer|nil, integer
local file_count, a3 = M.read_uleb128_at(buf, up); up = a3 local unit_basenames = {} ---@type table<integer, string> -- bag: 1-based unit file index -> basename
--- @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
local unit_basenames = {} for i = 1, file_count do ---@type integer
--- @type table<integer, string> -- bag: 1-based unit file index -> full path local combined = "" ---@type string
local unit_paths = {} local didx = 0 ---@type integer
--- @type integer for j = 1, file_format_count do ---@type integer
for i = 1, file_count do local v, a4 = read_form(buf, lstr_buf, up, file_formats[j]) ---@type string|integer|nil, integer
--- @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])
up = a4 up = a4
if j == 1 and type(v) == "string" then combined = v end if j == 1 and type(v) == "string" then combined = v end
if j == 2 and type(v) == "number" then didx = v end if j == 2 and type(v) == "number" then didx = v end
end end
--- @type integer local idx = #unit_basenames + 1 ---@type integer
local idx = #unit_basenames + 1 local bs = combined:match("[^/\\]+$") or combined ---@type string
--- @type string
local bs = combined:match("[^/\\]+$") or combined
unit_paths[idx] = combined unit_paths[idx] = combined
unit_basenames[idx] = bs unit_basenames[idx] = bs
if didx > 0 and dirs[didx] then if didx > 0 and dirs[didx] then
@@ -1382,37 +1243,27 @@ function M.read_line_unit_file_table(elf_path)
end end
--- Walk every line-program unit in the section. --- Walk every line-program unit in the section.
--- @type integer local p = 0 ---@type integer
local p = 0 local section_end = #line ---@type integer
--- @type integer
local section_end = #line
while p + 4 <= section_end do while p + 4 <= section_end do
--- @type integer local unit_length = M.read_u32_le(line, p) ---@type integer
local unit_length = M.read_u32_le(line, p)
if unit_length == 0xFFFFFFFF then if unit_length == 0xFFFFFFFF then
io.stderr:write("[elf_dwarf.read_line_unit_file_table] 64-bit DWARF (initial-length 0xFFFFFFFF); not supported\n") io.stderr:write("[elf_dwarf.read_line_unit_file_table] 64-bit DWARF (initial-length 0xFFFFFFFF); not supported\n")
return nil return nil
end end
--- @type integer local body_start = p + 4 ---@type integer
local body_start = p + 4 local body_end = p + 4 + unit_length ---@type integer
--- @type integer
local body_end = p + 4 + unit_length
if body_end > section_end then break end if body_end > section_end then break end
--- @type integer local version = M.read_u16_le(line, body_start) ---@type integer
local version = M.read_u16_le(line, body_start) local unit_basenames, unit_paths ---@type table<integer, string>|nil, table<integer, string>|nil
--- @type table<integer, string>|nil, table<integer, string>|nil
local unit_basenames, unit_paths
if version >= 5 then if version >= 5 then
-- DWARF5 header: version(2) + addr_size(1) + seg_size(1) + header_length(4) + content -- DWARF5 header: version(2) + addr_size(1) + seg_size(1) + header_length(4) + content
--- @type integer 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 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 local content_start = body_start + 8 ---@type integer -- past version(2) + addr_size(1) + seg_size(1) + header_length(4)
--- @type integer
local content_start = body_start + 8 -- 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) unit_basenames, unit_paths = parse_dwarf5_unit(line, lstr, content_start, body_end)
elseif version >= 2 then elseif version >= 2 then
-- DWARF2/3/4 header: version(2) + header_length(4) + content -- DWARF2/3/4 header: version(2) + header_length(4) + content
--- @type integer local content_start = body_start + 6 ---@type integer -- past version(2) + header_length(4)
local content_start = body_start + 6 -- past version(2) + header_length(4)
unit_basenames, unit_paths = parse_dwarf3_unit(line, content_start, body_end) unit_basenames, unit_paths = parse_dwarf3_unit(line, content_start, body_end)
else else
io.stderr:write(string.format("[elf_dwarf.read_line_unit_file_table] unsupported DWARF version %d (offset 0x%x)\n", version, p)) io.stderr:write(string.format("[elf_dwarf.read_line_unit_file_table] unsupported DWARF version %d (offset 0x%x)\n", version, p))
@@ -1426,8 +1277,7 @@ function M.read_line_unit_file_table(elf_path)
-- For DWARF5 (crt0.s + C unit), each carries its own per-unit file-table map; -- 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. -- 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. -- 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 ---@type integer, string
for idx, bs in pairs(unit_basenames) do
basenames[idx] = bs basenames[idx] = bs
paths [idx] = unit_paths[idx] paths [idx] = unit_paths[idx]
basename_to_index[bs] = idx basename_to_index[bs] = idx
+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. -- 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. -- `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 string
local _bootstrap_dir = debug.getinfo(1, "S").source:match("^@?(.*[/\\])") or "./" local duffle = dofile(_bootstrap_dir .. "../duffle_paths.lua") ---@type DuffleExport
--- @type DuffleExport
local duffle = dofile(_bootstrap_dir .. "../duffle_paths.lua")
-- The annotation pass reads the source-derived registries from scan_source: -- 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 -- * 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 --- @param findings Findings
--- @return nil --- @return nil
local function check_unique_annotation(_item, pipe_ctx, findings) local function check_unique_annotation(_item, pipe_ctx, findings)
--- @type string, integer for name, n in pairs(pipe_ctx.annot_counts) do ---@type string, integer
for name, n in pairs(pipe_ctx.annot_counts) do
if n > 1 then if n > 1 then
findings.errors[#findings.errors + 1] = { findings.errors[#findings.errors + 1] = {
line = pipe_ctx.atom_index[name] and pipe_ctx.atom_index[name].line or 0, line = pipe_ctx.atom_index[name] and pipe_ctx.atom_index[name].line or 0,
@@ -142,10 +139,8 @@ end
--- @param findings Findings --- @param findings Findings
--- @return nil --- @return nil
local function check_macro_word_drift(m, pipe_ctx, findings) local function check_macro_word_drift(m, pipe_ctx, findings)
--- @type WordCounts local wc = (pipe_ctx and pipe_ctx.word_counts) or {} ---@type WordCounts
local wc = (pipe_ctx and pipe_ctx.word_counts) or {} local declared = wc[m.name] ---@type integer|nil
--- @type integer|nil
local declared = wc[m.name]
if not declared then if not declared then
findings.errors[#findings.errors + 1] = { findings.errors[#findings.errors + 1] = {
line = m.line, line = m.line,
@@ -174,10 +169,8 @@ end
--- @return nil --- @return nil
local function check_semantic_reg_defaults(_src, pipe_ctx, findings) 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). -- 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 table<string, integer> -- bag: register ident -> first source line
local seen_first_line = {} for _, occ in ipairs(pipe_ctx.type_occurrences or {}) do ---@type integer, RegTypeOccurrence
--- @type integer, RegTypeOccurrence
for _, occ in ipairs(pipe_ctx.type_occurrences or {}) do
if seen_first_line[occ.reg] == nil then if seen_first_line[occ.reg] == nil then
seen_first_line[occ.reg] = occ.source_line seen_first_line[occ.reg] = occ.source_line
else else
@@ -189,12 +182,9 @@ local function check_semantic_reg_defaults(_src, pipe_ctx, findings)
} }
end end
end end
--- @type table<string, AliasEntry> local reg_registry = pipe_ctx.register_alias_registry or {} ---@type table<string, AliasEntry>
local reg_registry = pipe_ctx.register_alias_registry or {} local type_registry = pipe_ctx.type_name_registry or {} ---@type table<string, TypeNameEntry>
--- @type table<string, TypeNameEntry> for reg, def in pairs(pipe_ctx.types or {}) do ---@type string, RegTypeDefault
local type_registry = pipe_ctx.type_name_registry or {}
--- @type string, RegTypeDefault
for reg, def in pairs(pipe_ctx.types or {}) do
if not reg_registry[reg] then if not reg_registry[reg] then
findings.errors[#findings.errors + 1] = { findings.errors[#findings.errors + 1] = {
line = def.source_line, line = def.source_line,
@@ -229,15 +219,11 @@ end
--- @param findings Findings --- @param findings Findings
--- @return nil --- @return nil
local function check_atom_reg_types(_src, pipe_ctx, findings) 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, AliasEntry>
local reg_registry = pipe_ctx.register_alias_registry or {} local type_registry = pipe_ctx.type_name_registry or {} ---@type table<string, TypeNameEntry>
--- @type table<string, TypeNameEntry> for _, ai in ipairs(pipe_ctx.atom_infos_list or {}) do ---@type integer, AtomInfoEntry
local type_registry = pipe_ctx.type_name_registry or {}
--- @type integer, AtomInfoEntry
for _, ai in ipairs(pipe_ctx.atom_infos_list or {}) do
if ai.reg_type_overrides then if ai.reg_type_overrides then
--- @type string, RegTypeOverride for reg, ov in pairs(ai.reg_type_overrides) do ---@type string, RegTypeOverride
for reg, ov in pairs(ai.reg_type_overrides) do
if not reg_registry[reg] then if not reg_registry[reg] then
findings.errors[#findings.errors + 1] = { findings.errors[#findings.errors + 1] = {
line = ai.info_line, line = ai.info_line,
@@ -265,13 +251,11 @@ end
--- @param findings Findings --- @param findings Findings
--- @return nil --- @return nil
local function check_atom_view_layout(_src, pipe_ctx, findings) 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 ---@type string, AtomViewEntry
for atom_name, view in pairs(pipe_ctx.atom_views or {}) do
if not view.binds_name then if not view.binds_name then
-- The atom had atom_reg_types but no atom_view; no layout check needed. -- The atom had atom_reg_types but no atom_view; no layout check needed.
else else
--- @type BindsEntry|nil local bs = pipe_ctx.binds_index[view.binds_name] ---@type BindsEntry|nil
local bs = pipe_ctx.binds_index[view.binds_name]
if not bs then if not bs then
findings.errors[#findings.errors + 1] = { findings.errors[#findings.errors + 1] = {
line = view.info_line, line = view.info_line,
@@ -297,16 +281,12 @@ end
--- @param findings Findings --- @param findings Findings
--- @return nil --- @return nil
local function check_binds_no_duplicate_fields(_src, pipe_ctx, findings) 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 integer, BindsEntry
for _, bs in ipairs(pipe_ctx.binds_list or {}) do local seen = {} ---@type table<string, integer> -- bag: field name -> occurrence count
--- @type table<string, integer> -- bag: field name -> occurrence count for _, f in ipairs(bs.fields or {}) do ---@type integer, TypeField
local seen = {}
--- @type integer, TypeField
for _, f in ipairs(bs.fields or {}) do
seen[f.name] = (seen[f.name] or 0) + 1 seen[f.name] = (seen[f.name] or 0) + 1
end end
--- @type string, integer for name, count in pairs(seen) do ---@type string, integer
for name, count in pairs(seen) do
if count > 1 then if count > 1 then
findings.errors[#findings.errors + 1] = { findings.errors[#findings.errors + 1] = {
line = bs.line, line = bs.line,
@@ -334,10 +314,8 @@ end
--- @param findings Findings --- @param findings Findings
--- @return nil --- @return nil
local function check_skip_marker(marker, _pipe_ctx, findings) local function check_skip_marker(marker, _pipe_ctx, findings)
--- @type string local kind = marker.marker_kind ---@type string
local kind = marker.marker_kind local line = marker.marker_line ---@type integer
--- @type integer
local line = marker.marker_line
-- Left `scan.debug_skip_markers` with production records for `atom_dbg_skip` only; other identifiers take the walker's unrelated branch. -- 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 if marker.has_parens then
@@ -396,13 +374,10 @@ end
local function check_wave_context_migration(_src, pipe_ctx, findings) 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.types and next(pipe_ctx.types)) then return end
if not (pipe_ctx.atom_infos_list) 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 table<string, AliasEntry>
local reg_registry = pipe_ctx.register_alias_registry or {} for _, ai in ipairs(pipe_ctx.atom_infos_list) do ---@type integer, AtomInfoEntry
--- @type integer, AtomInfoEntry
for _, ai in ipairs(pipe_ctx.atom_infos_list) do
if ai.reg_type_overrides then if ai.reg_type_overrides then
--- @type string, RegTypeOverride for reg, _ in pairs(ai.reg_type_overrides) do ---@type string, RegTypeOverride
for reg, _ in pairs(ai.reg_type_overrides) do
if not reg_registry[reg] then if not reg_registry[reg] then
findings.warnings[#findings.warnings + 1] = { findings.warnings[#findings.warnings + 1] = {
line = 0, line = 0,
@@ -429,8 +404,7 @@ end
-- --
-- Adding a new check = 1 row here + 1 function above. The `validate()` dispatch loop never needs editing. -- Adding a new check = 1 row here + 1 function above. The `validate()` dispatch loop never needs editing.
--- @type CheckRule[] local CHECK_RULES = { ---@type CheckRule[]
local CHECK_RULES = {
{ name = "atom_decl_exists", per_annot = check_atom_decl_exists }, { name = "atom_decl_exists", per_annot = check_atom_decl_exists },
{ name = "binds_struct_exists", per_annot = check_binds_struct_exists }, { name = "binds_struct_exists", per_annot = check_binds_struct_exists },
{ name = "unique_annotation", post = check_unique_annotation }, { name = "unique_annotation", post = check_unique_annotation },
@@ -453,12 +427,9 @@ local CHECK_RULES = {
--- @param ctx PassCtx --- @param ctx PassCtx
--- @return PipeCtx --- @return PipeCtx
local function build_corpus_pipe_ctx(ctx) local function build_corpus_pipe_ctx(ctx)
--- @type PipeCtx local view = duffle.corpus_view(ctx) ---@type PipeCtx
local view = duffle.corpus_view(ctx) local annot_counts = {} ---@type table<string, integer> -- bag: atom name -> annotation count
--- @type table<string, integer> -- bag: atom name -> annotation count for _, info in ipairs(view.atom_infos) do ---@type integer, AtomInfoEntry
local annot_counts = {}
--- @type integer, AtomInfoEntry
for _, info in ipairs(view.atom_infos) do
if info and info.atom_name then if info and info.atom_name then
annot_counts[info.atom_name] = (annot_counts[info.atom_name] or 0) + 1 annot_counts[info.atom_name] = (annot_counts[info.atom_name] or 0) + 1
end end
@@ -476,17 +447,13 @@ end
--- @return AnnotatedResult --- @return AnnotatedResult
local function validate(ctx, src, corpus_pipe_ctx) local function validate(ctx, src, corpus_pipe_ctx)
corpus_pipe_ctx = corpus_pipe_ctx or build_corpus_pipe_ctx(ctx) corpus_pipe_ctx = corpus_pipe_ctx or build_corpus_pipe_ctx(ctx)
--- @type SourceScan local scan = src.scan ---@type SourceScan
local scan = src.scan
-- 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`. -- 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 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 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 AtomInfoEntry[]
local atom_infos_list = {}; for _, ai in ipairs(scan.atom_infos or {}) do atom_infos_list[#atom_infos_list + 1] = ai end
--- @type PipeCtx local pipe_ctx = { ---@type PipeCtx
local pipe_ctx = {
atom_index = {}, atom_index = {},
binds_index = {}, binds_index = {},
annot_counts = corpus_pipe_ctx.annot_counts, 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, register_alias_registry = corpus_pipe_ctx.register_alias_registry,
type_name_registry = corpus_pipe_ctx.type_name_registry, type_name_registry = corpus_pipe_ctx.type_name_registry,
} }
--- @type AtomEntry[] local atoms = {} ---@type AtomEntry[]
local atoms = {} for _, a in ipairs(scan.atoms) do ---@type integer, AtomEntry
--- @type integer, AtomEntry
for _, a in ipairs(scan.atoms) do
if a.kind == "atom" or a.kind == "atom_proc" then if a.kind == "atom" or a.kind == "atom_proc" then
atoms[#atoms + 1] = a atoms[#atoms + 1] = a
pipe_ctx.atom_index[a.raw_name or a.name] = a pipe_ctx.atom_index[a.raw_name or a.name] = a
end end
end end
--- @type integer, BindsEntry for _, b in ipairs(scan.binds) do pipe_ctx.binds_index[b.name] = b end ---@type integer, BindsEntry
for _, b in ipairs(scan.binds) do pipe_ctx.binds_index[b.name] = b end
-- Findings live in a single struct with three lists (errors / warnings / info). -- Findings live in a single struct with three lists (errors / warnings / info).
-- Each check writes to the list appropriate for its severity. -- Each check writes to the list appropriate for its severity.
--- @type Findings local findings = { errors = {}, warnings = {}, info = {} } ---@type Findings
local findings = { errors = {}, warnings = {}, info = {} }
-- Lift parse-time errors already recorded in scan_source's atom_info payload into this pass's findings list. -- 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 ---@type integer, AtomInfoEntry
for _, info in ipairs(scan.atom_infos) do
if info.errors then if info.errors then
--- @type integer, string for _, msg in ipairs(info.errors) do ---@type integer, string
for _, msg in ipairs(info.errors) do
findings.errors[#findings.errors + 1] = { findings.errors[#findings.errors + 1] = {
line = info.info_line, line = info.info_line,
msg = string.format("'%s': %s", info.atom_name, msg), msg = string.format("'%s': %s", info.atom_name, msg),
@@ -532,8 +493,7 @@ local function validate(ctx, src, corpus_pipe_ctx)
end end
-- THE per-annotation pipeline. ONE loop. CHECK_RULES dispatches per_annot rules. -- THE per-annotation pipeline. ONE loop. CHECK_RULES dispatches per_annot rules.
--- @type integer, AtomInfoEntry for _, info in ipairs(scan.atom_infos) do ---@type integer, AtomInfoEntry
for _, info in ipairs(scan.atom_infos) do
duffle.run_check_rules(CHECK_RULES, "per_annot", info, pipe_ctx, findings) duffle.run_check_rules(CHECK_RULES, "per_annot", info, pipe_ctx, findings)
end 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. -- 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. -- 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 DebugSkipMarker[]
local skip_markers = scan.debug_skip_markers or {} for _, marker in ipairs(skip_markers) do ---@type integer, DebugSkipMarker
--- @type integer, DebugSkipMarker
for _, marker in ipairs(skip_markers) do
duffle.run_check_rules(CHECK_RULES, "per_skip_marker", marker, pipe_ctx, findings) duffle.run_check_rules(CHECK_RULES, "per_skip_marker", marker, pipe_ctx, findings)
end end
-- Per-macro rules (TAPE_WORDS vs WORD_COUNT drift). -- Per-macro rules (TAPE_WORDS vs WORD_COUNT drift).
pipe_ctx.word_counts = corpus_pipe_ctx.word_counts pipe_ctx.word_counts = corpus_pipe_ctx.word_counts
--- @type integer, MacroEntry for _, m in ipairs(scan.macros) do ---@type integer, MacroEntry
for _, m in ipairs(scan.macros) do
duffle.run_check_rules(CHECK_RULES, "per_macro", m, pipe_ctx, findings) duffle.run_check_rules(CHECK_RULES, "per_macro", m, pipe_ctx, findings)
end end
@@ -582,8 +539,7 @@ end
-- M.run — orchestrator entry -- M.run — orchestrator entry
-- ════════════════════════════════════════════════════════════════════════════ -- ════════════════════════════════════════════════════════════════════════════
--- @type AnnotationPass local M = {} ---@type AnnotationPass
local M = {}
-- Expose `validate` for downstream passes (e.g. report.lua) that need to re-render the per-source results into a per-MODULE report. -- 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 M.validate = validate
@@ -591,47 +547,32 @@ M.validate = validate
--- @param ctx PassCtx --- @param ctx PassCtx
--- @return PassResult --- @return PassResult
function M.run(ctx) function M.run(ctx)
--- @type PassOutputEntry[] local outputs = {} ---@type PassOutputEntry[]
local outputs = {} local errors = {} ---@type PassFinding[]
--- @type PassFinding[] local warnings = {} ---@type PassFinding[]
local errors = {}
--- @type PassFinding[]
local warnings = {}
-- Build the shared pipe_ctx once for this run; every validate() call sees the same cross-source registries. -- 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. -- 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 PipeCtx
local corpus_pipe_ctx = build_corpus_pipe_ctx(ctx) local corpus = ctx.shared.corpus ---@type Corpus
--- @type Corpus
local corpus = ctx.shared.corpus
-- Group `corpus.sources_by_dir` by module, validate every source in each bucket, and emit one errors.h per directory. -- 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 {} ---@type table<string, SourceFile[]>
local by_dir = (corpus and corpus.sources_by_dir) or {}
--- @type string, SourceFile[] for dir, dir_sources in pairs(by_dir) do ---@type string, SourceFile[]
for dir, dir_sources in pairs(by_dir) do local dir_basename = dir:match("([^/\\]+)$") or dir ---@type string
--- @type string local dir_atoms = 0 ---@type integer
local dir_basename = dir:match("([^/\\]+)$") or dir local dir_errors = {} ---@type PassFinding[]
--- @type integer local dir_warnings = {} ---@type PassFinding[]
local dir_atoms = 0 for _, src in ipairs(dir_sources) do ---@type integer, SourceFile
--- @type PassFinding[] local result = validate(ctx, src, corpus_pipe_ctx) ---@type AnnotatedResult
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)
result.source = src.path -- tag for downstream rendering result.source = src.path -- tag for downstream rendering
dir_atoms = dir_atoms + #result.atoms dir_atoms = dir_atoms + #result.atoms
--- @type integer, PassFinding for _, e in ipairs(result.errors) do ---@type integer, PassFinding
for _, e in ipairs(result.errors) do
dir_errors[#dir_errors + 1] = { line = e.line, msg = e.msg, source = src.path } dir_errors[#dir_errors + 1] = { line = e.line, msg = e.msg, source = src.path }
errors [#errors + 1] = { line = e.line, msg = e.msg } errors [#errors + 1] = { line = e.line, msg = e.msg }
end end
--- @type integer, PassFinding for _, w in ipairs(result.warnings) do ---@type integer, PassFinding
for _, w in ipairs(result.warnings) do
dir_warnings[#dir_warnings + 1] = { line = w.line, msg = w.msg } dir_warnings[#dir_warnings + 1] = { line = w.line, msg = w.msg }
warnings [#warnings + 1] = { line = w.line, msg = w.msg } warnings [#warnings + 1] = { line = w.line, msg = w.msg }
end end
+75 -150
View File
@@ -37,12 +37,9 @@
-- Bootstrap: load `duffle_paths.lua` via `debug.getinfo(1, "S").source` -- 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")` -- (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. -- at the bottom, so the dofile value IS the duffle module.
--- @type string local _bootstrap_dir = debug.getinfo(1, "S").source:match("^@?(.*[/\\])") or "./" ---@type string
local _bootstrap_dir = debug.getinfo(1, "S").source:match("^@?(.*[/\\])") or "./" local duffle = dofile(_bootstrap_dir .. "../duffle_paths.lua") ---@type DuffleExport
--- @type DuffleExport local elf_dwarf = require("elf_dwarf") ---@type ElfDwarfMod
local duffle = dofile(_bootstrap_dir .. "../duffle_paths.lua")
--- @type ElfDwarfMod
local elf_dwarf = require("elf_dwarf")
-- ════════════════════════════════════════════════════════════════════════════ -- ════════════════════════════════════════════════════════════════════════════
-- Constants -- 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; -- Format version emitted as the first line. Bump + add a migration test if the format changes;
-- the gdb runtime loader rejects mismatches (E2). -- the gdb runtime loader rejects mismatches (E2).
--- @type integer local FORMAT_VERSION = 1 ---@type integer
local FORMAT_VERSION = 1
-- ════════════════════════════════════════════════════════════════════════════ -- ════════════════════════════════════════════════════════════════════════════
-- Type declarations -- Type declarations
@@ -107,23 +103,16 @@ local FORMAT_VERSION = 1
--- @return WordMapEntry[] --- @return WordMapEntry[]
--- @return integer --- @return integer
local function canonical_word_entries(atom) local function canonical_word_entries(atom)
--- @type AtomPaths local paths = atom.paths or {} ---@type AtomPaths
local paths = atom.paths or {} local events = paths.word_events or {} ---@type WordEvent[]
--- @type WordEvent[] local word_items = {} ---@type EmissionItem[]
local events = paths.word_events or {} for _, item in ipairs(paths.items or {}) do ---@type integer, EmissionItem
--- @type EmissionItem[]
local word_items = {}
--- @type integer, EmissionItem
for _, item in ipairs(paths.items or {}) do
if item.kind == "word" then word_items[#word_items + 1] = item end if item.kind == "word" then word_items[#word_items + 1] = item end
end end
--- @type WordMapEntry[] local entries = {} ---@type WordMapEntry[]
local entries = {} for index, event in ipairs(events) do ---@type integer, WordEvent
--- @type integer, WordEvent local item = word_items[index] or {} ---@type EmissionItem
for index, event in ipairs(events) do
--- @type EmissionItem
local item = word_items[index] or {}
entries[#entries + 1] = { entries[#entries + 1] = {
pos = event.i or (index - 1), pos = event.i or (index - 1),
line = event.call_line or item.line or 0, line = event.call_line or item.line or 0,
@@ -149,20 +138,14 @@ end
--- @return string[] --- @return string[]
--- @return integer --- @return integer
local function emit_provenance_stanza(src, atom, wc) local function emit_provenance_stanza(src, atom, wc)
--- @type string[] local lines = {} ---@type string[]
local lines = {} local rel_path = src.path:gsub("\\\\", "/") ---@type string
--- @type string local entries, total = canonical_word_entries(atom) ---@type WordMapEntry[], integer
local rel_path = src.path:gsub("\\\\", "/")
--- @type WordMapEntry[], integer
local entries, total = canonical_word_entries(atom)
lines[#lines + 1] = string.format('ATOM %s "%s" 0', atom.raw_name or atom.name, rel_path) 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 integer, WordMapEntry
for _, entry in ipairs(entries) do local inv = entry.invocation ---@type InvocationRecord|nil
--- @type InvocationRecord|nil local macro_count = inv and wc["mac_" .. inv.component_name] ---@type integer|nil
local inv = entry.invocation
--- @type integer|nil
local macro_count = inv and wc["mac_" .. inv.component_name]
if inv and macro_count ~= nil then if inv and macro_count ~= nil then
lines[#lines + 1] = string.format('WORD %d CALL %s:%d MACRO %s "%s:%d" BODY %d' 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 , entry.pos, rel_path, entry.line, inv.component_name
@@ -182,8 +165,7 @@ end
--- @param wc WordCounts --- @param wc WordCounts
--- @return string --- @return string
local function render_provenance(src, wc) local function render_provenance(src, wc)
--- @type string[] local lines = {} ---@type string[]
local lines = {}
lines[#lines + 1] = "# FORMAT_VERSION 1" 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] = "# 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" 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 --- @param atom AtomEntry
--- @return nil --- @return nil
local function append(atom) local function append(atom)
--- @type string[] local stanza = emit_provenance_stanza(src, atom, wc) ---@type string[]
local stanza = emit_provenance_stanza(src, atom, wc) for _, line in ipairs(stanza) do lines[#lines + 1] = line end ---@type integer, string
--- @type integer, string
for _, line in ipairs(stanza) do lines[#lines + 1] = line end
end end
--- @type integer, AtomEntry for _, atom in ipairs(src.scan.atoms or {}) do ---@type integer, AtomEntry
for _, atom in ipairs(src.scan.atoms or {}) do
if atom.paths then append(atom) end if atom.paths then append(atom) end
end end
--- @type integer, AtomEntry for _, atom in ipairs(src.scan.raw_atoms or {}) do ---@type integer, AtomEntry
for _, atom in ipairs(src.scan.raw_atoms or {}) do
if atom.paths then append(atom) end if atom.paths then append(atom) end
end end
@@ -219,16 +197,12 @@ end
--- @return string[] --- @return string[]
--- @return integer --- @return integer
local function emit_atom_stanza(src, atom) local function emit_atom_stanza(src, atom)
--- @type string[] local lines = {} ---@type string[]
local lines = {} local rel_path = src.path:gsub("\\\\", "/") ---@type string
--- @type string local entries, total = canonical_word_entries(atom) ---@type WordMapEntry[], integer
local rel_path = src.path:gsub("\\\\", "/")
--- @type WordMapEntry[], integer
local entries, total = canonical_word_entries(atom)
lines[#lines + 1] = string.format('ATOM %s "%s" 0', atom.raw_name or atom.name, rel_path) 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 integer, WordMapEntry
for _, entry in ipairs(entries) do
lines[#lines + 1] = string.format("WORD %d LINE %d TEXT %s", lines[#lines + 1] = string.format("WORD %d LINE %d TEXT %s",
entry.pos, entry.line, entry.text) entry.pos, entry.line, entry.text)
end end
@@ -242,25 +216,20 @@ end
--- @param src SourceFile --- @param src SourceFile
--- @return string --- @return string
local function render_source_map(src) local function render_source_map(src)
--- @type string[] local lines = {} ---@type string[]
local lines = {}
lines[#lines + 1] = "# FORMAT_VERSION " .. FORMAT_VERSION lines[#lines + 1] = "# FORMAT_VERSION " .. FORMAT_VERSION
lines[#lines + 1] = "# auto-generated by ps1_meta.lua (passes/atoms_source_map.lua) — DO NOT EDIT" lines[#lines + 1] = "# auto-generated by ps1_meta.lua (passes/atoms_source_map.lua) — DO NOT EDIT"
--- @param atom AtomEntry --- @param atom AtomEntry
--- @return nil --- @return nil
local function append(atom) local function append(atom)
--- @type string[] local stanza = emit_atom_stanza(src, atom) ---@type string[]
local stanza = emit_atom_stanza(src, atom) for _, line in ipairs(stanza) do lines[#lines + 1] = line end ---@type integer, string
--- @type integer, string
for _, line in ipairs(stanza) do lines[#lines + 1] = line end
end end
--- @type integer, AtomEntry for _, atom in ipairs(src.scan.atoms or {}) do ---@type integer, AtomEntry
for _, atom in ipairs(src.scan.atoms or {}) do
if atom.paths then append(atom) end if atom.paths then append(atom) end
end end
--- @type integer, AtomEntry for _, atom in ipairs(src.scan.raw_atoms or {}) do ---@type integer, AtomEntry
for _, atom in ipairs(src.scan.raw_atoms or {}) do
if atom.paths then append(atom) end if atom.paths then append(atom) end
end end
@@ -283,28 +252,20 @@ end
--- @param ctx PassCtx --- @param ctx PassCtx
--- @return GdbAtomRecord[] --- @return GdbAtomRecord[]
local function build_atom_table(ctx) local function build_atom_table(ctx)
--- @type table<string, NmAddr> local addrs = elf_dwarf.read_nm(ctx.flags.elf_path) ---@type table<string, NmAddr>
local addrs = elf_dwarf.read_nm(ctx.flags.elf_path) local corpus = ctx.shared and ctx.shared.corpus ---@type Corpus|nil
--- @type Corpus|nil local matched = {} ---@type GdbAtomRecord[]
local corpus = ctx.shared and ctx.shared.corpus
--- @type GdbAtomRecord[]
local matched = {}
--- @type integer, SourceFile for _, src in ipairs(corpus.source_order or {}) do ---@type integer, SourceFile
for _, src in ipairs(corpus.source_order or {}) do local file_base = src.path:match("([^/\\\\]+)$") or src.path ---@type string
--- @type string
local file_base = src.path:match("([^/\\\\]+)$") or src.path
--- @param atom AtomEntry --- @param atom AtomEntry
--- @return nil --- @return nil
local function append(atom) local function append(atom)
if not atom.paths then return end if not atom.paths then return end
--- @type string local name = atom.raw_name or atom.name ---@type string
local name = atom.raw_name or atom.name local info = addrs[name] ---@type NmAddr|nil
--- @type NmAddr|nil
local info = addrs[name]
if not info then return end if not info then return end
--- @type WordMapEntry[], integer local entries, total = canonical_word_entries(atom) ---@type WordMapEntry[], integer
local entries, total = canonical_word_entries(atom)
matched[#matched + 1] = { matched[#matched + 1] = {
name = name, name = name,
src_path = src.path, src_path = src.path,
@@ -315,10 +276,8 @@ local function build_atom_table(ctx)
entries = entries, entries = entries,
} }
end 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 {}).atoms or {}) do append(atom) end for _, atom in ipairs((src.scan or {}).raw_atoms or {}) do append(atom) end ---@type integer, AtomEntry
--- @type integer, AtomEntry
for _, atom in ipairs((src.scan or {}).raw_atoms or {}) do append(atom) end
end end
-- Deterministic order: sort by address (matches `nm` output ordering). -- Deterministic order: sort by address (matches `nm` output ordering).
@@ -326,8 +285,7 @@ local function build_atom_table(ctx)
--- @param b GdbAtomRecord --- @param b GdbAtomRecord
--- @return boolean --- @return boolean
table.sort(matched, function(a, b) return a.addr < b.addr end) 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 ---@type integer, GdbAtomRecord
for i, a in ipairs(matched) do a.idx = i - 1 end
return matched return matched
end end
@@ -345,8 +303,7 @@ local function append_gdb_commands(lines, matched)
-- ── tape_atoms ── -- ── tape_atoms ──
-- Hardcoded one printf per atom. No loop. -- Hardcoded one printf per atom. No loop.
lines[#lines + 1] = "define tape_atoms" lines[#lines + 1] = "define tape_atoms"
--- @type integer, GdbAtomRecord for _, a in ipairs(matched) do ---@type integer, GdbAtomRecord
for _, a in ipairs(matched) do
-- gdb 12.1 quirk: literals in printf args require an attached target. -- gdb 12.1 quirk: literals in printf args require an attached target.
-- Use the per-atom convenience vars set above as printf args. -- 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', 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 ── -- ── break_atom (generic) + per-atom break_atom_X ──
lines[#lines + 1] = "define break_atom" lines[#lines + 1] = "define break_atom"
lines[#lines + 1] = ' echo "Usage: break_atom_<exact_name> (pick from the list below)"' lines[#lines + 1] = ' echo "Usage: break_atom_<exact_name> (pick from the list below)"'
--- @type integer, GdbAtomRecord for _, a in ipairs(matched) do ---@type integer, GdbAtomRecord
for _, a in ipairs(matched) do
lines[#lines + 1] = string.format(' printf " break_atom_%%-32s\\n", $__atom_name_%d', a.idx) lines[#lines + 1] = string.format(' printf " break_atom_%%-32s\\n", $__atom_name_%d', a.idx)
end end
lines[#lines + 1] = "end" lines[#lines + 1] = "end"
@@ -371,8 +327,7 @@ local function append_gdb_commands(lines, matched)
lines[#lines + 1] = "end" lines[#lines + 1] = "end"
lines[#lines + 1] = "" lines[#lines + 1] = ""
--- @type integer, GdbAtomRecord for _, a in ipairs(matched) do ---@type integer, GdbAtomRecord
for _, a in ipairs(matched) do
lines[#lines + 1] = string.format("define break_atom_%s", a.name) 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(" 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) 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 ── -- ── step_atom / next_atom ──
-- Hardcoded one tbreak per atom. No loop. -- Hardcoded one tbreak per atom. No loop.
lines[#lines + 1] = "define step_atom" lines[#lines + 1] = "define step_atom"
--- @type integer, GdbAtomRecord for _, a in ipairs(matched) do ---@type integer, GdbAtomRecord
for _, a in ipairs(matched) do
lines[#lines + 1] = string.format(" tbreak *$__atom_addr_%d", a.idx) lines[#lines + 1] = string.format(" tbreak *$__atom_addr_%d", a.idx)
end end
lines[#lines + 1] = " continue" 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] = "define where_in_atom"
lines[#lines + 1] = " set $__pc = (unsigned int)$pc" lines[#lines + 1] = " set $__pc = (unsigned int)$pc"
lines[#lines + 1] = " set $__matched = 0" lines[#lines + 1] = " set $__matched = 0"
--- @type integer, GdbAtomRecord for _, a in ipairs(matched) do ---@type integer, GdbAtomRecord
for _, a in ipairs(matched) do
-- Precompute end_addr (gdb 12.1's expression evaluator chokes on `addr + words*4`). -- 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(" 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) 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(" 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) 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. -- One inner-if per WORD entry. Each word's line + text hardcoded.
--- @type integer, WordMapEntry for _, we in ipairs(a.entries) do ---@type integer, WordMapEntry
for _, we in ipairs(a.entries) do
lines[#lines + 1] = string.format(" if $__word == %d", we.pos) lines[#lines + 1] = string.format(" if $__word == %d", we.pos)
-- Escape TEXT for printf format string. -- Escape TEXT for printf format string.
--- @type string local escaped_text = we.text:gsub("%%", "%%%%"):gsub('"', '\\"') ---@type string
local escaped_text = we.text:gsub("%%", "%%%%"):gsub('"', '\\"')
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] = string.format(' printf "source: %%s:%%d %%s\\n", $__atom_file_%d, %d, "%s"', a.idx, we.line, escaped_text)
lines[#lines + 1] = " end" lines[#lines + 1] = " end"
end end
-- Fallback for words beyond the source map (shouldn't happen if nm matches). -- Fallback for words beyond the source map (shouldn't happen if nm matches).
--- @type integer local max_word = 0 ---@type integer
local max_word = 0
if #a.entries > 0 then max_word = a.entries[#a.entries].pos end 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] = 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' 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 $__in_atom = 0"
lines[#lines + 1] = " set $__did_step = 0" lines[#lines + 1] = " set $__did_step = 0"
lines[#lines + 1] = " set $__pc = (unsigned int)$pc" lines[#lines + 1] = " set $__pc = (unsigned int)$pc"
--- @type integer, GdbAtomRecord for _, a in ipairs(matched) do ---@type integer, GdbAtomRecord
for _, a in ipairs(matched) do
-- Precompute end_addr in the convenience var (single expression gdb handles). -- 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(" 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) lines[#lines + 1] = string.format(" if $__pc >= $__atom_addr_%d && $__pc < $__end_%d", a.idx, a.idx)
@@ -494,8 +443,7 @@ end
--- @return nil --- @return nil
local function emit_gdb_runtime(ctx) local function emit_gdb_runtime(ctx)
if not (ctx.flags and ctx.flags.gdb_runtime) then return end if not (ctx.flags and ctx.flags.gdb_runtime) then return end
--- @type string|nil local elf_path = ctx.flags.elf_path ---@type string|nil
local elf_path = ctx.flags.elf_path
if not elf_path or elf_path == "" then if not elf_path or elf_path == "" then
io.stderr:write("[atoms_source_map] --gdb-runtime requires --elf <elf>\n") io.stderr:write("[atoms_source_map] --gdb-runtime requires --elf <elf>\n")
return return
@@ -506,15 +454,13 @@ local function emit_gdb_runtime(ctx)
return return
end end
--- @type GdbAtomRecord[] local matched = build_atom_table(ctx) ---@type GdbAtomRecord[]
local matched = build_atom_table(ctx)
if #matched == 0 then if #matched == 0 then
io.stderr:write("[atoms_source_map] --gdb-runtime: no atoms matched against nm symbols (stale scan?).\n") io.stderr:write("[atoms_source_map] --gdb-runtime: no atoms matched against nm symbols (stale scan?).\n")
return return
end end
--- @type string[] local lines = {} ---@type string[]
local lines = {}
lines[#lines + 1] = "# Auto-generated by ps1_meta.lua (passes/atoms_source_map.lua)" 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] = "# 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)." 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 -- Per-atom convenience vars (used as printf args; literals aren't accepted
-- without an attached target on gdb 12.1). -- without an attached target on gdb 12.1).
--- @type integer, GdbAtomRecord for _, a in ipairs(matched) do ---@type integer, GdbAtomRecord
for _, a in ipairs(matched) do
lines[#lines + 1] = string.format('set $__atom_name_%d = "%s"', a.idx, gdb_escape(a.name)) 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_addr_%d = 0x%x", a.idx, a.addr)
lines[#lines + 1] = string.format("set $__atom_words_%d = %d", a.idx, a.words) 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. -- Confirmation line for the source operator.
lines[#lines + 1] = 'printf "[gdb_tape_atoms] runtime loaded %d atoms from %s\\n", $__atom_count, $__elf_path' lines[#lines + 1] = 'printf "[gdb_tape_atoms] runtime loaded %d atoms from %s\\n", $__atom_count, $__elf_path'
--- @type string local out_path ---@type string
local out_path
-- 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` -- 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). -- (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. -- 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 if ends_with_gen_dir(ctx.out_root) then
-- Strip the trailing `/gen` segment, then write the runtime script under `build/`. -- 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". -- e.g. "C:/projects/Pikuma/ps1/build/gen" -> "C:/projects/Pikuma/ps1/build".
--- @type string local parent = ctx.out_root:gsub("[/\\]gen[/\\]?$", "") ---@type string
local parent = ctx.out_root:gsub("[/\\]gen[/\\]?$", "")
out_path = parent .. "/gdb_tape_atoms_runtime.gdb" out_path = parent .. "/gdb_tape_atoms_runtime.gdb"
else else
out_path = ctx.out_root .. "/gdb_tape_atoms_runtime.gdb" out_path = ctx.out_root .. "/gdb_tape_atoms_runtime.gdb"
@@ -581,8 +524,7 @@ end
-- M — module exports -- M — module exports
-- ════════════════════════════════════════════════════════════════════════════ -- ════════════════════════════════════════════════════════════════════════════
--- @type AtomSourceMapPass local M = {} ---@type AtomSourceMapPass
local M = {}
-- Expose the pure render functions so `report.lua` and the focused tests can call them directly without triggering the file-emit path. -- 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 M.render_source_map = render_source_map
@@ -594,22 +536,15 @@ M.render_provenance = render_provenance
function M.render_atom_source_map(atom) function M.render_atom_source_map(atom)
assert(type(atom) == "table", "render_atom_source_map: atom must be a table") 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") 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 WordMapEntry[], integer
local entries, total = canonical_word_entries(atom) local lines = {} ---@type string[]
--- @type string[]
local lines = {}
lines[#lines + 1] = string.format("ATOM %s %d", (atom.raw_name or atom.name), total) 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 integer, WordMapEntry
for _, entry in ipairs(entries) do local word_line = string.format("WORD %d LINE %d TEXT %s", ---@type string
--- @type string
local word_line = string.format("WORD %d LINE %d TEXT %s",
entry.pos, entry.line, entry.text) entry.pos, entry.line, entry.text)
--- @type string[] local keys = {} ---@type string[]
local keys = {} for pos = 1, 16 do ---@type integer
--- @type integer local k = entry.gpr_keys and entry.gpr_keys[pos] ---@type string|nil
for pos = 1, 16 do
--- @type string|nil
local k = entry.gpr_keys and entry.gpr_keys[pos]
if type(k) == "string" and k:sub(1, 7) == "reguse:" then if type(k) == "string" and k:sub(1, 7) == "reguse:" then
keys[#keys + 1] = k keys[#keys + 1] = k
end 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) == "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(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") 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 WordMapEntry[], integer
local entries, total = canonical_word_entries(atom) local lines = {} ---@type string[]
--- @type string[]
local lines = {}
lines[#lines + 1] = string.format("ATOM %s %d", (atom.raw_name or atom.name), total) 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 integer, WordMapEntry
for _, entry in ipairs(entries) do local inv = entry.invocation ---@type InvocationRecord|nil
--- @type InvocationRecord|nil local macro_count = inv and wc and wc["mac_" .. inv.component_name] ---@type integer|nil
local inv = entry.invocation
--- @type integer|nil
local macro_count = inv and wc and wc["mac_" .. inv.component_name]
if inv and macro_count ~= nil then if inv and macro_count ~= nil then
lines[#lines + 1] = string.format('WORD %d CALL %s:%d MACRO %s "%s:%d" BODY %d' 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) , 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 --- @param ctx PassCtx
--- @return PassResult --- @return PassResult
function M.run(ctx) function M.run(ctx)
--- @type PassOutputEntry[] local outputs = {} ---@type PassOutputEntry[]
local outputs = {} local errors = {} ---@type PassFinding[]
--- @type PassFinding[] local warnings = {} ---@type PassFinding[]
local errors = {}
--- @type PassFinding[]
local warnings = {}
--- @type Corpus|nil local corpus = ctx.shared and ctx.shared.corpus ---@type Corpus|nil
local corpus = ctx.shared and ctx.shared.corpus
if type(corpus) ~= "table" or type(corpus.source_order) ~= "table" then 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) error("atoms_source_map.run requires ctx.shared.corpus.source_order (canonical corpus).", 0)
end end
-- Word counts come from `corpus.word_counts` (populated by word_count_eval + components passes). -- Word counts come from `corpus.word_counts` (populated by word_count_eval + components passes).
--- @type WordCounts local wc = corpus.word_counts or {} ---@type WordCounts
local wc = corpus.word_counts or {}
if not next(wc) then if not next(wc) then
warnings[#warnings + 1] = { warnings[#warnings + 1] = {
line = 0, line = 0,
+71 -142
View File
@@ -35,10 +35,8 @@
--- @field run fun(ctx: PassCtx): AutoRegResult --- @field run fun(ctx: PassCtx): AutoRegResult
--- @field POOL GprIdent[] --- @field POOL GprIdent[]
--- @type string local _bootstrap_dir = debug.getinfo(1, "S").source:match("^@?(.*[/\\])") or "./" ---@type string
local _bootstrap_dir = debug.getinfo(1, "S").source:match("^@?(.*[/\\])") or "./" local duffle = dofile(_bootstrap_dir .. "../duffle_paths.lua") ---@type DuffleExport
--- @type DuffleExport
local duffle = dofile(_bootstrap_dir .. "../duffle_paths.lua")
--- ════════════════════════════════════════════════════════════════════════════ --- ════════════════════════════════════════════════════════════════════════════
--- THE GPR ALLOCATION POOL — what's allocatable, and (more importantly) WHY --- 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_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. --- 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 = { ---@type GprIdent[]
local POOL = {
"R_V0", "R_V1", "R_V0", "R_V1",
"R_T0", "R_T1", "R_T2", "R_T3", "R_T0", "R_T1", "R_T2", "R_T3",
"R_T4", "R_T5", "R_T6", "R_T7", "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 -- 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) -- (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. -- 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 = { ---@type table<integer, GprIdent> -- bag: MIPS GPR code -> POOL ident
local INT_CODE_TO_POOL_GPR = {
[2] = "R_V0", [3] = "R_V1", [2] = "R_V0", [3] = "R_V1",
[4] = "R_A0", [5] = "R_A1", [6] = "R_A2", [7] = "R_A3", [4] = "R_A0", [5] = "R_A1", [6] = "R_A2", [7] = "R_A3",
[8] = "R_T0", [9] = "R_T1", [10] = "R_T2", [11] = "R_T3", [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 --- @param tbl table<string, string> -- bag: key set only; values unused
--- @return string[] --- @return string[]
local function stable_sort_keys(tbl) local function stable_sort_keys(tbl)
--- @type string[] local keys = {} ---@type string[]
local keys = {} for k in pairs(tbl) do keys[#keys + 1] = k end ---@type string
--- @type string
for k in pairs(tbl) do keys[#keys + 1] = k end
table.sort(keys) table.sort(keys)
return keys return keys
end 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) }` -- 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), -- 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. -- 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 GprIdent[]
local pool = {} for i = 1, #POOL do pool[i] = POOL[i] end ---@type integer
--- @type integer local result = {} ---@type GprAllocMap
for i = 1, #POOL do pool[i] = POOL[i] end local errors = {} ---@type PassFinding[]
--- @type GprAllocMap for _, sym in ipairs(stable_sort_keys(decls)) do ---@type integer, string
local result = {} local next_gpr = table.remove(pool, 1) ---@type GprIdent|nil
--- @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)
if not next_gpr then if not next_gpr then
errors[#errors + 1] = { errors[#errors + 1] = {
line = 0, line = 0,
@@ -144,16 +132,12 @@ end
--- @return table<GprIdent, boolean> --- @return table<GprIdent, boolean>
--- @return table<string, GprIdent> --- @return table<string, GprIdent>
local function build_user_pins(corpus) local function build_user_pins(corpus)
--- @type table<GprIdent, boolean> -- bag: pinned physical GPR -> true local user_pinned = {} ---@type table<GprIdent, boolean> -- bag: pinned physical GPR -> true
local user_pinned = {} local alias_to_gpr = {} ---@type table<string, GprIdent> -- bag: alias ident -> physical GPR
--- @type table<string, GprIdent> -- bag: alias ident -> physical GPR
local alias_to_gpr = {}
if not corpus.register_alias_registry then return user_pinned, alias_to_gpr end 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 ---@type string, AliasEntry
for alias_name, alias_entry in pairs(corpus.register_alias_registry) do
if alias_entry.has_atom_reg and alias_entry.code then if alias_entry.has_atom_reg and alias_entry.code then
--- @type GprIdent|nil local gpr = INT_CODE_TO_POOL_GPR[alias_entry.code] ---@type GprIdent|nil
local gpr = INT_CODE_TO_POOL_GPR[alias_entry.code]
if gpr then if gpr then
user_pinned[gpr] = true user_pinned[gpr] = true
alias_to_gpr[alias_name] = gpr alias_to_gpr[alias_name] = gpr
@@ -174,29 +158,22 @@ end
--- @param alias_to_gpr table<string, GprIdent> -- bag: alias ident -> physical GPR --- @param alias_to_gpr table<string, GprIdent> -- bag: alias ident -> physical GPR
--- @return table<GprIdent, integer> --- @return table<GprIdent, integer>
local function find_used_gprs(body_text, alias_to_gpr) local function find_used_gprs(body_text, alias_to_gpr)
--- @type table<GprIdent, integer> -- bag: physical GPR -> hit count local found = {} ---@type table<GprIdent, integer> -- bag: physical GPR -> hit count
local found = {}
-- (a) Hardcoded physical GPRs (R_T0..R_T7, R_V0..R_V1, R_A0..R_A3, R_S0..R_S7). -- (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 ---@type GprIdent
for gpr in body_text:gmatch("(R_T%d+|R_V%d+|R_A%d+|R_S%d+)") do
found[gpr] = (found[gpr] or 0) + 1 found[gpr] = (found[gpr] or 0) + 1
end end
-- (b) Alias references (R_<Alias>) resolved to physical GPRs via the registry. -- (b) Alias references (R_<Alias>) resolved to physical GPRs via the registry.
-- Sorted by name so the regex is byte-stable across runs. -- Sorted by name so the regex is byte-stable across runs.
if alias_to_gpr and next(alias_to_gpr) then if alias_to_gpr and next(alias_to_gpr) then
--- @type string[] local aliases = {} ---@type string[]
local aliases = {} for alias_name in pairs(alias_to_gpr) do ---@type string
--- @type string
for alias_name in pairs(alias_to_gpr) do
aliases[#aliases + 1] = alias_name aliases[#aliases + 1] = alias_name
end end
table.sort(aliases) table.sort(aliases)
--- @type string local pattern = "(" .. table.concat(aliases, "|") .. ")" ---@type string
local pattern = "(" .. table.concat(aliases, "|") .. ")" for alias_name in body_text:gmatch(pattern) do ---@type string
--- @type string local gpr = alias_to_gpr[alias_name] ---@type GprIdent|nil
for alias_name in body_text:gmatch(pattern) do
--- @type GprIdent|nil
local gpr = alias_to_gpr[alias_name]
if gpr and not found[gpr] then if gpr and not found[gpr] then
found[gpr] = 1 found[gpr] = 1
end end
@@ -213,30 +190,24 @@ end
--- @return string|nil --- @return string|nil
local function emit_auto_reg_h(out_dir, dir, sources, mappings) local function emit_auto_reg_h(out_dir, dir, sources, mappings)
if not mappings or next(mappings) == nil then return end if not mappings or next(mappings) == nil then return end
--- @type string local out_path = out_dir .. "/" .. "auto_reg.h" ---@type string
local out_path = out_dir .. "/" .. "auto_reg.h"
duffle.ensure_dir(out_dir) duffle.ensure_dir(out_dir)
--- @type string[] local lines = { ---@type string[]
local lines = {
"#ifdef INTELLISENSE_DIRECTIVES", "#ifdef INTELLISENSE_DIRECTIVES",
"#pragma once", "#pragma once",
"#endif", "#endif",
"// Auto-generated by ps1_meta.lua (passes/auto_reg.lua) — DO NOT EDIT", "// Auto-generated by ps1_meta.lua (passes/auto_reg.lua) — DO NOT EDIT",
"// Directory: " .. dir:gsub("/", "\\"), "// Directory: " .. dir:gsub("/", "\\"),
} }
--- @type integer, SourceFile for _, src in ipairs(sources) do ---@type integer, SourceFile
for _, src in ipairs(sources) do
lines[#lines + 1] = "// source: " .. src.path lines[#lines + 1] = "// source: " .. src.path
end end
lines[#lines + 1] = "// Per-phase register allocations resolved by the lua pass." 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] = "// R_<Sym>_Code = <chosen GPR's _Code constant> for every marker in this directory."
lines[#lines + 1] = "" lines[#lines + 1] = ""
--- @type integer, string for _, sym in ipairs(stable_sort_keys(mappings)) do ---@type integer, string
for _, sym in ipairs(stable_sort_keys(mappings)) do local gpr = mappings[sym] ---@type GprIdent
--- @type GprIdent local gpr_code = gpr .. "_Code" ---@type string
local gpr = mappings[sym]
--- @type string
local gpr_code = gpr .. "_Code"
lines[#lines + 1] = "#define " .. sym .. "_Code " .. gpr_code lines[#lines + 1] = "#define " .. sym .. "_Code " .. gpr_code
end end
lines[#lines + 1] = "" lines[#lines + 1] = ""
@@ -249,21 +220,16 @@ end
-- Pass entry -- Pass entry
-- ════════════════════════════════════════════════════════════════════════════ -- ════════════════════════════════════════════════════════════════════════════
--- @type AutoRegPass local M = {} ---@type AutoRegPass
local M = {}
--- @param ctx PassCtx --- @param ctx PassCtx
--- @return AutoRegResult --- @return AutoRegResult
function M.run(ctx) function M.run(ctx)
--- @type AutoRegOutput[] local outputs = {} ---@type AutoRegOutput[]
local outputs = {} local errors = {} ---@type PassFinding[]
--- @type PassFinding[] local warnings = {} ---@type PassFinding[]
local errors = {}
--- @type PassFinding[]
local warnings = {}
--- @type Corpus|nil local corpus = ctx.shared and ctx.shared.corpus ---@type Corpus|nil
local corpus = ctx.shared and ctx.shared.corpus
if type(corpus) ~= "table" then if type(corpus) ~= "table" then
error("auto_reg.run requires ctx.shared.corpus", 0) error("auto_reg.run requires ctx.shared.corpus", 0)
end 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. -- 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. -- 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. -- 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) ---@type table<GprIdent, boolean>, table<string, GprIdent>
local user_pinned, alias_to_gpr = build_user_pins(corpus)
-- 1. Allocate phase pools first (phase declarations take precedence over per-atom declarations). -- 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 table<string, GprAllocMap> -- bag: phase_label -> alloc map
local phase_allocations = {} for phase_label, decls in pairs(corpus.phase_auto_regs or {}) do ---@type string, table<string, string>
--- @type string, table<string, string> local mapping, errs = allocate_phase(phase_label, decls) ---@type GprAllocMap, PassFinding[]
for phase_label, decls in pairs(corpus.phase_auto_regs or {}) do for sym, gpr in pairs(mapping) do ---@type string, GprIdent
--- @type GprAllocMap, PassFinding[]
local mapping, errs = allocate_phase(phase_label, decls)
--- @type string, GprIdent
for sym, gpr in pairs(mapping) do
phase_allocations[phase_label] = phase_allocations[phase_label] or {} phase_allocations[phase_label] = phase_allocations[phase_label] or {}
phase_allocations[phase_label][sym] = gpr phase_allocations[phase_label][sym] = gpr
end end
--- @type integer, PassFinding for _, e in ipairs(errs) do ---@type integer, PassFinding
for _, e in ipairs(errs) do
errors[#errors + 1] = e errors[#errors + 1] = e
end end
end end
@@ -298,22 +258,16 @@ function M.run(ctx)
-- Otherwise, allocate a private pool for the atom. -- 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>)` -- 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. -- 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 table<AtomName, string> -- bag: atom name -> phase label
local atom_name_to_phase = {} for phase_label, entry in pairs(corpus.atom_phases or {}) do ---@type string, AtomPhaseGroup
--- @type string, AtomPhaseGroup for _, atom_name in ipairs(entry.atoms or {}) do ---@type integer, AtomName
for phase_label, entry in pairs(corpus.atom_phases or {}) do
--- @type integer, AtomName
for _, atom_name in ipairs(entry.atoms or {}) do
atom_name_to_phase[atom_name] = phase_label atom_name_to_phase[atom_name] = phase_label
end end
end end
--- @type table<AtomName, GprAllocMap> -- bag: atom scope -> alloc map local atom_allocations = {} ---@type table<AtomName, GprAllocMap> -- bag: atom scope -> alloc map
local atom_allocations = {} for atom_scope, decls in pairs(corpus.atom_auto_regs or {}) do ---@type AtomName, table<string, string>
--- @type AtomName, table<string, string> local phase_label = atom_name_to_phase[atom_scope] ---@type string|nil
for atom_scope, decls in pairs(corpus.atom_auto_regs or {}) do
--- @type string|nil
local phase_label = atom_name_to_phase[atom_scope]
-- Build the atom's source pool: start with the full POOL, subtract: -- Build the atom's source pool: start with the full POOL, subtract:
-- (a) every GPR already committed (phase allocations + prior atom allocations) -- (a) every GPR already committed (phase allocations + prior atom allocations)
-- (b) every USER-PINNED GPR (wave-context carriers + file-scope pinned aliases) -- (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 -- 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)` -- 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`. -- 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 table<GprIdent, boolean> -- bag: committed or body-referenced GPR -> true
local used = {} for _, m in pairs(phase_allocations) do for _, gpr in pairs(m) do used[gpr] = true end end ---@type integer, GprAllocMap
--- @type integer, GprAllocMap for _, m in pairs(atom_allocations) do for _, gpr in pairs(m) do used[gpr] = true end end ---@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
-- (c) Body references — scan the atom body for hardcoded + alias-resolved GPRs. -- (c) Body references — scan the atom body for hardcoded + alias-resolved GPRs.
-- Folded into `used` so the source_pool exclusion is a single check. -- 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] ---@type AtomEntry|nil
local atom = corpus.atoms_by_name and corpus.atoms_by_name[atom_scope]
if atom and atom.body then if atom and atom.body then
--- @type table<GprIdent, integer> local body_used = find_used_gprs(atom.body, alias_to_gpr) ---@type table<GprIdent, integer>
local body_used = find_used_gprs(atom.body, alias_to_gpr) for gpr in pairs(body_used) do used[gpr] = true end ---@type GprIdent
--- @type GprIdent
for gpr in pairs(body_used) do used[gpr] = true end
end end
--- @type GprIdent[] local source_pool = {} ---@type GprIdent[]
local source_pool = {} for _, gpr in ipairs(POOL) do ---@type integer, GprIdent
--- @type integer, GprIdent
for _, gpr in ipairs(POOL) do
-- Exclude (a) prior commitments, (b) USER-PINNED GPRs (wave-context carriers declared via atom_reg + _Code defs, preserved across atoms globally). -- 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 if not used[gpr] and not user_pinned[gpr] then
source_pool[#source_pool + 1] = gpr source_pool[#source_pool + 1] = gpr
end end
end end
--- @type GprAllocMap local result = {} ---@type GprAllocMap
local result = {} for _, sym in ipairs(stable_sort_keys(decls)) do ---@type integer, string
--- @type integer, string local next_gpr = table.remove(source_pool, 1) ---@type GprIdent|nil
for _, sym in ipairs(stable_sort_keys(decls)) do
--- @type GprIdent|nil
local next_gpr = table.remove(source_pool, 1)
if not next_gpr then if not next_gpr then
errors[#errors + 1] = { errors[#errors + 1] = {
line = 0, 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 -- 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). -- (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. -- 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 AtomName, GprAllocMap
for atom_scope, decls in pairs(atom_allocations) do local atom = corpus.atoms_by_name and corpus.atoms_by_name[atom_scope] ---@type AtomEntry|nil
--- @type AtomEntry|nil
local atom = corpus.atoms_by_name and corpus.atoms_by_name[atom_scope]
if atom and atom.body then if atom and atom.body then
--- @type table<GprIdent, integer> local used_in_body = find_used_gprs(atom.body, alias_to_gpr) ---@type table<GprIdent, integer>
local used_in_body = find_used_gprs(atom.body, alias_to_gpr) for sym, allocated_gpr in pairs(decls) do ---@type string, GprIdent
--- @type string, GprIdent
for sym, allocated_gpr in pairs(decls) do
if used_in_body[allocated_gpr] and used_in_body[allocated_gpr] > 0 then if used_in_body[allocated_gpr] and used_in_body[allocated_gpr] > 0 then
warnings[#warnings + 1] = { warnings[#warnings + 1] = {
line = atom.line or 0, line = atom.line or 0,
@@ -398,37 +337,27 @@ function M.run(ctx)
-- 4. Emit per-directory gen/auto_reg.h. -- 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. -- 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 table<string, SourceFile[]>
local sources_by_dir = corpus.sources_by_dir or {} for dir, sources in pairs(sources_by_dir) do ---@type string, SourceFile[]
--- @type string, SourceFile[] local per_dir_mappings = {} ---@type GprAllocMap
for dir, sources in pairs(sources_by_dir) do for _, src in ipairs(sources) do ---@type integer, SourceFile
--- @type GprAllocMap
local per_dir_mappings = {}
--- @type integer, SourceFile
for _, src in ipairs(sources) do
-- Collect every (sym -> gpr) entry that originated from a source in this directory. -- 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"). -- `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, -- 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. -- 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
for scope_name in pairs(src.scan and src.scan.atom_auto_regs or {}) do for sym, gpr in pairs(atom_allocations[scope_name] or {}) do ---@type string, GprIdent
--- @type string, GprIdent
for sym, gpr in pairs(atom_allocations[scope_name] or {}) do
per_dir_mappings[sym] = gpr per_dir_mappings[sym] = gpr
end end
end end
--- @type string for scope_name in pairs(src.scan and src.scan.phase_auto_regs or {}) do ---@type string
for scope_name in pairs(src.scan and src.scan.phase_auto_regs or {}) do for sym, gpr in pairs(phase_allocations[scope_name] or {}) do ---@type string, GprIdent
--- @type string, GprIdent
for sym, gpr in pairs(phase_allocations[scope_name] or {}) do
per_dir_mappings[sym] = gpr per_dir_mappings[sym] = gpr
end end
end end
end end
--- @type string local out_dir = dir .. "/gen" ---@type string
local out_dir = dir .. "/gen" local out_path = emit_auto_reg_h(out_dir, dir, sources, per_dir_mappings) ---@type string|nil
--- @type string|nil
local out_path = emit_auto_reg_h(out_dir, dir, sources, per_dir_mappings)
if out_path then outputs[#outputs + 1] = { auto_reg_h = out_path } end if out_path then outputs[#outputs + 1] = { auto_reg_h = out_path } end
end end
return { outputs = outputs, errors = errors, warnings = warnings } 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. -- 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). -- 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. -- 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 string
local _bootstrap_dir = debug.getinfo(1, "S").source:match("^@?(.*[/\\])") or "./" local duffle = dofile(_bootstrap_dir .. "../duffle_paths.lua") ---@type DuffleExport
--- @type DuffleExport
local duffle = dofile(_bootstrap_dir .. "../duffle_paths.lua")
-- ════════════════════════════════════════════════════════════════════════════ -- ════════════════════════════════════════════════════════════════════════════
-- Constants -- Constants
-- ════════════════════════════════════════════════════════════════════════════ -- ════════════════════════════════════════════════════════════════════════════
-- Atom component declaration identifiers. -- Atom component declaration identifiers.
--- @type string local ATOM_COMP_PROC = "MipsAtomComp_Proc_" ---@type string
local ATOM_COMP_PROC = "MipsAtomComp_Proc_" local MIPS_ATOM = "Slice_MipsCode" ---@type string -- prefix on the function declaration that wraps an AtomComp_Proc_
--- @type string
local MIPS_ATOM = "Slice_MipsCode" -- prefix on the function declaration that wraps an AtomComp_Proc_
-- Component-name prefixes. -- Component-name prefixes.
--- @type string local AC_PREFIX = "ac_" ---@type string -- arg to MipsAtomComp_(ac_X); the X is the atom name
local AC_PREFIX = "ac_" -- arg to MipsAtomComp_(ac_X); the X is the atom name local AC_PREFIX_LEN = 3 ---@type integer
--- @type integer local MAC_PREFIX = "mac_" ---@type string -- prefix on generated macros; the rest is the atom name
local AC_PREFIX_LEN = 3 local MAC_PREFIX_LEN = 4 ---@type integer
--- @type string
local MAC_PREFIX = "mac_" -- prefix on generated macros; the rest is the atom name
--- @type integer
local MAC_PREFIX_LEN = 4
-- ASCII byte values used in tokenization. -- ASCII byte values used in tokenization.
--- @type integer local BYTE_NEWLINE = 10 ---@type integer
local BYTE_NEWLINE = 10 local BYTE_SLASH = 47 ---@type integer
--- @type integer
local BYTE_SLASH = 47
-- Output gen subdirectory + filename (per-directory aggregation; the directory name is the namespace). -- Output gen subdirectory + filename (per-directory aggregation; the directory name is the namespace).
--- @type string local GEN_SUBDIR = "gen" ---@type string
local GEN_SUBDIR = "gen" local MACS_FILENAME = "macs.h" ---@type string
--- @type string
local MACS_FILENAME = "macs.h"
-- ════════════════════════════════════════════════════════════════════════════ -- ════════════════════════════════════════════════════════════════════════════
-- Type declarations -- Type declarations
@@ -100,8 +88,7 @@ local MACS_FILENAME = "macs.h"
-- Local helpers (file I/O + path normalization) -- Local helpers (file I/O + path normalization)
-- ════════════════════════════════════════════════════════════════════════════ -- ════════════════════════════════════════════════════════════════════════════
--- @type ComponentsPass local M = {} ---@type ComponentsPass
local M = {}
-- ════════════════════════════════════════════════════════════════════════════ -- ════════════════════════════════════════════════════════════════════════════
-- Back-walk helpers (composed into the entry point below: find_function_args_for) -- Back-walk helpers (composed into the entry point below: find_function_args_for)
@@ -123,8 +110,7 @@ local M = {}
--- @param before_pos integer --- @param before_pos integer
--- @return string|nil --- @return string|nil
local function find_function_args_for(source, name, before_pos) 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) ---@type string|nil, string|nil
local _, args_inner = duffle.find_function_decl_for(source, before_pos, #MIPS_ATOM)
return args_inner return args_inner
end end
@@ -140,31 +126,24 @@ end
--- @return string[]|nil --- @return string[]|nil
local function extract_arg_names(args_str) local function extract_arg_names(args_str)
if not args_str or args_str == "" then return nil end if not args_str or args_str == "" then return nil end
--- @type string[] local names = {} ---@type string[]
local names = {} local tokens = duffle.split_top_level_commas(args_str) ---@type string[]
--- @type string[] for _, tok in ipairs(tokens) do ---@type integer, string
local tokens = duffle.split_top_level_commas(args_str) local trimmed = duffle.trim(tok) ---@type string
--- @type integer, string
for _, tok in ipairs(tokens) do
--- @type string
local trimmed = duffle.trim(tok)
if trimmed ~= "" then if trimmed ~= "" then
-- Strip trailing block comment (/* ... */) from the token, if present. -- Strip trailing block comment (/* ... */) from the token, if present.
-- split_top_level_commas only skips block comments at TOP LEVEL (between commas), -- 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. -- 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 -- 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`. -- the wrong name (or nothing). See `test_extract_arg_names_handles_trailing_block_comments`.
--- @type integer local trimmed_end = #trimmed ---@type integer
local trimmed_end = #trimmed
if trimmed_end >= 2 and trimmed:sub(trimmed_end - 1, trimmed_end) == "*/" then if trimmed_end >= 2 and trimmed:sub(trimmed_end - 1, trimmed_end) == "*/" then
-- Find the matching `/*` that opens the trailing comment. -- Find the matching `/*` that opens the trailing comment.
-- Walk back from the `*/` looking for `/*` (whitespace + `/*`). -- Walk back from the `*/` looking for `/*` (whitespace + `/*`).
--- @type integer local close_pos = trimmed_end - 1 ---@type integer -- position of the second-to-last char
local close_pos = trimmed_end - 1 -- position of the second-to-last char
-- Walk back: skip trailing whitespace, then look for the `/*` opener. -- Walk back: skip trailing whitespace, then look for the `/*` opener.
while close_pos > 1 do while close_pos > 1 do
--- @type string local ch = trimmed:sub(close_pos, close_pos) ---@type string
local ch = trimmed:sub(close_pos, close_pos)
if ch == " " or ch == "\t" or ch == "\n" or ch == "\r" then if ch == " " or ch == "\t" or ch == "\n" or ch == "\r" then
close_pos = close_pos - 1 close_pos = close_pos - 1
else else
@@ -172,10 +151,8 @@ local function extract_arg_names(args_str)
end end
end end
-- Now scan back from close_pos for the `/*` opener (slashes are at close_pos-1 and close_pos-2). -- 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|nil
local opener_pos = nil local scan = close_pos - 3 ---@type integer
--- @type integer
local scan = close_pos - 3
while scan >= 1 do while scan >= 1 do
if trimmed:sub(scan, scan + 1) == "/*" then if trimmed:sub(scan, scan + 1) == "/*" then
opener_pos = scan opener_pos = scan
@@ -194,11 +171,9 @@ local function extract_arg_names(args_str)
trimmed_end = #trimmed trimmed_end = #trimmed
if trimmed_end >= 4 and trimmed:sub(trimmed_end, trimmed_end) == "]" then if trimmed_end >= 4 and trimmed:sub(trimmed_end, trimmed_end) == "]" then
-- Walk back: skip digits, expect `[`. -- Walk back: skip digits, expect `[`.
--- @type integer local bracket_pos = trimmed_end - 1 ---@type integer
local bracket_pos = trimmed_end - 1
while bracket_pos > 1 do while bracket_pos > 1 do
--- @type string local ch = trimmed:sub(bracket_pos, bracket_pos) ---@type string
local ch = trimmed:sub(bracket_pos, bracket_pos)
if ch >= "0" and ch <= "9" then if ch >= "0" and ch <= "9" then
bracket_pos = bracket_pos - 1 bracket_pos = bracket_pos - 1
else else
@@ -212,22 +187,18 @@ local function extract_arg_names(args_str)
if trimmed == "" then goto continue end if trimmed == "" then goto continue end
-- Find the identifier at the end: walk back over trailers (whitespace + `*` + `[]`), -- Find the identifier at the end: walk back over trailers (whitespace + `*` + `[]`),
-- then walk back over the identifier chars (alnum + `_`). -- then walk back over the identifier chars (alnum + `_`).
--- @type integer local ident_end = #trimmed ---@type integer
local ident_end = #trimmed
while ident_end > 0 do while ident_end > 0 do
--- @type string local ch = trimmed:sub(ident_end, ident_end) ---@type string
local ch = trimmed:sub(ident_end, ident_end)
if ch == " " or ch == "\t" or ch == "*" or ch == "]" or ch == "[" then if ch == " " or ch == "\t" or ch == "*" or ch == "]" or ch == "[" then
ident_end = ident_end - 1 ident_end = ident_end - 1
else else
break break
end end
end end
--- @type integer local ident_start = ident_end ---@type integer
local ident_start = ident_end
while ident_start > 0 do while ident_start > 0 do
--- @type string local ch = trimmed:sub(ident_start, ident_start) ---@type string
local ch = trimmed:sub(ident_start, ident_start)
if duffle.is_alnum_byte(string.byte(ch)) or ch == "_" then if duffle.is_alnum_byte(string.byte(ch)) or ch == "_" then
ident_start = ident_start - 1 ident_start = ident_start - 1
else else
@@ -235,8 +206,7 @@ local function extract_arg_names(args_str)
end end
end end
ident_start = ident_start + 1 ident_start = ident_start + 1
--- @type string local name = trimmed:sub(ident_start, ident_end) ---@type string
local name = trimmed:sub(ident_start, ident_end)
if name ~= "" then names[#names + 1] = name end if name ~= "" then names[#names + 1] = name end
::continue:: ::continue::
end end
@@ -248,8 +218,7 @@ end
--- @param args_str string|nil --- @param args_str string|nil
--- @return string[]|nil --- @return string[]|nil
local function formal_arg_names(args_str) local function formal_arg_names(args_str)
--- @type string[]|nil local names = extract_arg_names(args_str) ---@type string[]|nil
local names = extract_arg_names(args_str)
if not names then return nil end if not names then return nil end
if names[1] == "ab" then table.remove(names, 1) end if names[1] == "ab" then table.remove(names, 1) end
if #names == 0 then return nil end if #names == 0 then return nil end
@@ -271,10 +240,8 @@ end
--- @param scan SourceScan --- @param scan SourceScan
--- @return Component[] --- @return Component[]
local function project_components(source, scan) local function project_components(source, scan)
--- @type Component[] local out = {} ---@type Component[]
local out = {} for _, a in ipairs(scan.atoms) do ---@type integer, AtomEntry
--- @type integer, AtomEntry
for _, a in ipairs(scan.atoms) do
-- Only `MipsAtomComp_(ac_X)` (kind="comp_bare") and `MipsAtomComp_Proc_(ac_X, ...)` (kind="comp_proc") -- 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. -- 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 -- `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 -- Function-args lookup is meaningful for `MipsAtomComp_Proc_` components
-- (the macro sits inside `FI_ Slice_MipsCode ac_X(...)`); the alias expansion -- (the macro sits inside `FI_ Slice_MipsCode ac_X(...)`); the alias expansion
-- discards the `ab` (atom-builder) arg the same way both forms do. -- 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) ---@type string|nil
local args = find_function_args_for(source, a.raw_name, a.ident_pos)
-- Comment ownership: scan_source.lua stamps `declaration_comment` on the record by walking backward past any associated bare marker. -- 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. -- The pass reads `declaration_comment` directly.
--- @type string local comment = a.declaration_comment or "" ---@type string
local comment = a.declaration_comment or ""
out[#out + 1] = { out[#out + 1] = {
line = a.line, line = a.line,
name = a.name, name = a.name,
@@ -321,31 +286,23 @@ end
--- @param s string --- @param s string
--- @return string --- @return string
local function convert_line_comments_to_block(s) local function convert_line_comments_to_block(s)
--- @type string local result = s ---@type string
local result = s local pos = 1 ---@type integer
--- @type integer local len = #result ---@type integer
local pos = 1
--- @type integer
local len = #result
while pos <= len do while pos <= len do
--- @type boolean local is_double_slash = result:byte(pos) == BYTE_SLASH ---@type boolean
local is_double_slash = result:byte(pos) == BYTE_SLASH
and pos + 1 <= len and result:byte(pos + 1) == BYTE_SLASH and pos + 1 <= len and result:byte(pos + 1) == BYTE_SLASH
if not is_double_slash then if not is_double_slash then
pos = pos + 1 pos = pos + 1
else else
-- Find end of line. -- Find end of line.
--- @type integer local eol = pos ---@type integer
local eol = pos
while eol <= len and result:byte(eol) ~= BYTE_NEWLINE do while eol <= len and result:byte(eol) ~= BYTE_NEWLINE do
eol = eol + 1 eol = eol + 1
end end
--- @type string local before = result:sub(1, pos - 1) ---@type string
local before = result:sub(1, pos - 1) local comment = result:sub(pos + 2, eol - 1) ---@type string -- skip the `//`
--- @type string local after ---@type string
local comment = result:sub(pos + 2, eol - 1) -- skip the `//`
--- @type string
local after
if eol <= len and result:byte(eol) == BYTE_NEWLINE then if eol <= len and result:byte(eol) == BYTE_NEWLINE then
after = " */" .. result:sub(eol) -- keep the newline after = " */" .. result:sub(eol) -- keep the newline
else else
@@ -382,14 +339,11 @@ end
--- @param tok string --- @param tok string
--- @return string --- @return string
local function strip_leading_delay_marker(tok) local function strip_leading_delay_marker(tok)
--- @type string|nil local ident = duffle.read_ident(tok, 1) ---@type string|nil
local ident = duffle.read_ident(tok, 1)
if not ident or not duffle.DELAY_MARKERS[ident] then return tok end if not ident or not duffle.DELAY_MARKERS[ident] then return tok end
--- @type string local rest = tok:sub(#ident + 1):match("^%s*(.*)$") or "" ---@type string
local rest = tok:sub(#ident + 1):match("^%s*(.*)$") or ""
while rest:sub(1, 2) == "/*" do while rest:sub(1, 2) == "/*" do
--- @type integer|nil local close = rest:find("*/", 3, true) ---@type integer|nil
local close = rest:find("*/", 3, true)
if not close then return "" end if not close then return "" end
rest = rest:sub(close + 2):match("^%s*(.*)$") or "" rest = rest:sub(close + 2):match("^%s*(.*)$") or ""
end end
@@ -406,24 +360,17 @@ end
local function word_count_rec(name, comp_by_name, wc, cache) local function word_count_rec(name, comp_by_name, wc, cache)
if cache[name] ~= nil then return cache[name] end if cache[name] ~= nil then return cache[name] end
cache[name] = -1 -- mark in-progress (cycle detection) cache[name] = -1 -- mark in-progress (cycle detection)
--- @type Component|nil local cc = comp_by_name[name] ---@type Component|nil
local cc = comp_by_name[name] local n ---@type integer
--- @type integer
local n
if cc then if cc then
n = 0 n = 0
--- @type BodyToken[] local tokens = cc.body_tokens ---@type BodyToken[]
local tokens = cc.body_tokens for _, t in ipairs(tokens) do ---@type integer, BodyToken
--- @type integer, BodyToken local trimmed = t.tok ---@type string
for _, t in ipairs(tokens) do
--- @type string
local trimmed = t.tok
if trimmed ~= "" then if trimmed ~= "" then
--- @type string local work = trimmed ---@type string
local work = trimmed
while true do while true do
--- @type string|nil local marker = duffle.read_ident(work, 1) ---@type string|nil
local marker = duffle.read_ident(work, 1)
if marker and duffle.DELAY_MARKERS[marker] then if marker and duffle.DELAY_MARKERS[marker] then
work = strip_leading_delay_marker(work) work = strip_leading_delay_marker(work)
if work == "" then break end if work == "" then break end
@@ -432,8 +379,7 @@ local function word_count_rec(name, comp_by_name, wc, cache)
end end
end end
if work ~= "" then if work ~= "" then
--- @type string|nil local lookup = strip_mac_prefix(duffle.read_ident(work, 1)) ---@type string|nil
local lookup = strip_mac_prefix(duffle.read_ident(work, 1))
if lookup == "atom_label" or lookup == "atom_offset" then if lookup == "atom_label" or lookup == "atom_offset" then
-- Pure metaprogram anchors; emit zero words. -- Pure metaprogram anchors; emit zero words.
elseif lookup and comp_by_name[lookup] then elseif lookup and comp_by_name[lookup] then
@@ -466,16 +412,11 @@ end
--- @param wc WordCounts --- @param wc WordCounts
--- @return table<string, integer> -- bag: bare component name -> word count --- @return table<string, integer> -- bag: bare component name -> word count
local function count_all_components(components, wc) local function count_all_components(components, wc)
--- @type table<string, Component> local comp_by_name = {} ---@type table<string, Component>
local comp_by_name = {} for _, cc in ipairs(components) do comp_by_name[cc.name] = cc end ---@type integer, Component
--- @type integer, Component local cache = {} ---@type table<string, integer> -- bag: memo; -1 in-progress sentinel
for _, cc in ipairs(components) do comp_by_name[cc.name] = cc end local counts = {} ---@type table<string, integer> -- bag: bare name -> word count
--- @type table<string, integer> -- bag: memo; -1 in-progress sentinel for _, c in ipairs(components) do ---@type integer, Component
local cache = {}
--- @type table<string, integer> -- bag: bare name -> word count
local counts = {}
--- @type integer, Component
for _, c in ipairs(components) do
counts[c.name] = word_count_rec(c.name, comp_by_name, wc, cache) counts[c.name] = word_count_rec(c.name, comp_by_name, wc, cache)
end end
return counts return counts
@@ -504,34 +445,23 @@ end
local function component_meta_rec(name, comp_by_name, latency, cache) local function component_meta_rec(name, comp_by_name, latency, cache)
if cache[name] ~= nil then return cache[name] end if cache[name] ~= nil then return cache[name] end
cache[name] = { cycle_cost = -1, gp0_contrib = -1 } cache[name] = { cycle_cost = -1, gp0_contrib = -1 }
--- @type Component|nil local cc = comp_by_name[name] ---@type Component|nil
local cc = comp_by_name[name] local cycle_cost ---@type integer
--- @type integer local gp0_contrib ---@type integer
local cycle_cost
--- @type integer
local gp0_contrib
if cc then if cc then
--- @type boolean local skip_cycle = (name == "yield") ---@type boolean
local skip_cycle = (name == "yield") local skip_gp0 = name:match("^insert_ot_tag") ~= nil ---@type boolean
--- @type boolean
local skip_gp0 = name:match("^insert_ot_tag") ~= nil
cycle_cost = 0 cycle_cost = 0
gp0_contrib = 0 gp0_contrib = 0
if not skip_cycle or not skip_gp0 then if not skip_cycle or not skip_gp0 then
--- @type BodyToken[] local tokens = cc.body_tokens ---@type BodyToken[]
local tokens = cc.body_tokens for _, t in ipairs(tokens) do ---@type integer, BodyToken
--- @type integer, BodyToken local trimmed = t.tok ---@type string
for _, t in ipairs(tokens) do
--- @type string
local trimmed = t.tok
if trimmed ~= "" then if trimmed ~= "" then
--- @type string|nil local ident = duffle.read_ident(trimmed, 1) ---@type string|nil
local ident = duffle.read_ident(trimmed, 1)
if ident and ident:sub(1, MAC_PREFIX_LEN) == MAC_PREFIX then if ident and ident:sub(1, MAC_PREFIX_LEN) == MAC_PREFIX then
--- @type string local nested = ident:sub(MAC_PREFIX_LEN + 1) ---@type string
local nested = ident:sub(MAC_PREFIX_LEN + 1) local nested_meta = component_meta_rec(nested, comp_by_name, latency, cache) ---@type ComponentMeta
--- @type ComponentMeta
local nested_meta = component_meta_rec(nested, comp_by_name, latency, cache)
if not skip_cycle then if not skip_cycle then
cycle_cost = cycle_cost + nested_meta.cycle_cost cycle_cost = cycle_cost + nested_meta.cycle_cost
end end
@@ -540,10 +470,8 @@ local function component_meta_rec(name, comp_by_name, latency, cache)
end end
else else
if not skip_cycle then if not skip_cycle then
--- @type InstructionRow|nil local isa = duffle.instr(ident) ---@type InstructionRow|nil
local isa = duffle.instr(ident) local gte = duffle.gte(ident) ---@type GteCommandRow|nil
--- @type GteCommandRow|nil
local gte = duffle.gte(ident)
cycle_cost = cycle_cost + ((isa and isa.cycles) or (gte and gte.cycles) or latency[ident] or 1) cycle_cost = cycle_cost + ((isa and isa.cycles) or (gte and gte.cycles) or latency[ident] or 1)
end end
if not skip_gp0 then if not skip_gp0 then
@@ -578,16 +506,11 @@ end
--- @param latency table<string, integer> -- bag: ident -> cycle cost --- @param latency table<string, integer> -- bag: ident -> cycle cost
--- @return ComponentMetaMap --- @return ComponentMetaMap
local function compute_components_metadata(components, latency) local function compute_components_metadata(components, latency)
--- @type table<string, Component> local comp_by_name = {} ---@type table<string, Component>
local comp_by_name = {} for _, cc in ipairs(components) do comp_by_name[cc.name] = cc end ---@type integer, Component
--- @type integer, Component local cache = {} ---@type ComponentMetaMap
for _, cc in ipairs(components) do comp_by_name[cc.name] = cc end local out = {} ---@type ComponentMetaMap
--- @type ComponentMetaMap for _, c in ipairs(components) do ---@type integer, Component
local cache = {}
--- @type ComponentMetaMap
local out = {}
--- @type integer, Component
for _, c in ipairs(components) do
out[c.name] = component_meta_rec(c.name, comp_by_name, latency, cache) out[c.name] = component_meta_rec(c.name, comp_by_name, latency, cache)
end end
return out return out
@@ -602,15 +525,11 @@ end
--- @param s string --- @param s string
--- @return string[] --- @return string[]
local function split_comment_lines(s) local function split_comment_lines(s)
--- @type string[] local out = {} ---@type string[]
local out = {} local pos = 1 ---@type integer
--- @type integer local s_len = #s ---@type integer
local pos = 1
--- @type integer
local s_len = #s
while pos <= s_len do while pos <= s_len do
--- @type integer|nil local nl = s:find("\n", pos, true) ---@type integer|nil
local nl = s:find("\n", pos, true)
if not nl then if not nl then
out[#out + 1] = s:sub(pos) out[#out + 1] = s:sub(pos)
break break
@@ -629,8 +548,7 @@ end
--- @param args_str string|nil --- @param args_str string|nil
--- @return string --- @return string
local function signature_from_args(args_str) local function signature_from_args(args_str)
--- @type string[]|nil local names = formal_arg_names(args_str) ---@type string[]|nil
local names = formal_arg_names(args_str)
if names then if names then
return table.concat(names, ", ") return table.concat(names, ", ")
end end
@@ -642,8 +560,7 @@ end
--- @param lines string[] --- @param lines string[]
--- @return nil --- @return nil
local function strip_trailing_continuation(lines) local function strip_trailing_continuation(lines)
--- @type string local last = lines[#lines] ---@type string
local last = lines[#lines]
if last:sub(-2) == " \\" then if last:sub(-2) == " \\" then
lines[#lines] = last:sub(1, -3) lines[#lines] = last:sub(1, -3)
end 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) --- @param tok string -- a single token from split_top_level_commas (already trimmed at the start, may contain trailing whitespace + block comment)
--- @return boolean --- @return boolean
local function is_pure_delay_marker_token(tok) local function is_pure_delay_marker_token(tok)
--- @type table<string, boolean> -- bag: delay-marker ident -> true local markers = duffle.DELAY_MARKERS ---@type table<string, boolean> -- bag: delay-marker ident -> true
local markers = duffle.DELAY_MARKERS
if type(markers) ~= "table" then return false end if type(markers) ~= "table" then return false end
-- Identify a leading delay-marker identifier (e.g. `GteDelay_`). -- Identify a leading delay-marker identifier (e.g. `GteDelay_`).
--- @type integer local ident_end = 1 ---@type integer
local ident_end = 1
while ident_end <= #tok do while ident_end <= #tok do
--- @type string local ch = tok:sub(ident_end, ident_end) ---@type string
local ch = tok:sub(ident_end, ident_end)
if ch:match("[%w_]") then if ch:match("[%w_]") then
ident_end = ident_end + 1 ident_end = ident_end + 1
else else
break break
end end
end end
--- @type string local ident = tok:sub(1, ident_end - 1) ---@type string
local ident = tok:sub(1, ident_end - 1)
if not markers[ident] then return false end if not markers[ident] then return false end
-- Walk the remainder: only whitespace and block comments are allowed. -- Walk the remainder: only whitespace and block comments are allowed.
--- @type integer local scan = ident_end ---@type integer
local scan = ident_end
while scan <= #tok do while scan <= #tok do
--- @type string local ch = tok:sub(scan, scan) ---@type string
local ch = tok:sub(scan, scan)
if ch:match("%s") then if ch:match("%s") then
scan = scan + 1 scan = scan + 1
elseif ch == "/" and tok:sub(scan + 1, scan + 1) == "*" then elseif ch == "/" and tok:sub(scan + 1, scan + 1) == "*" then
--- @type integer|nil local close = tok:find("*/", scan + 2, true) ---@type integer|nil
local close = tok:find("*/", scan + 2, true)
if not close then return false end if not close then return false end
scan = close + 2 scan = close + 2
else else
@@ -743,17 +653,14 @@ end
--- @param tokens string[] --- @param tokens string[]
--- @return nil --- @return nil
local function emit_macro_body(lines, c, sig, tokens) local function emit_macro_body(lines, c, sig, tokens)
--- @type integer for tok_idx = 1, #tokens do ---@type integer
for tok_idx = 1, #tokens do
tokens[tok_idx] = convert_line_comments_to_block(tokens[tok_idx]) tokens[tok_idx] = convert_line_comments_to_block(tokens[tok_idx])
end end
if #tokens == 0 then return end if #tokens == 0 then return end
lines[#lines + 1] = "#define mac_" .. c.name .. "(" .. sig .. ") \\" lines[#lines + 1] = "#define mac_" .. c.name .. "(" .. sig .. ") \\"
lines[#lines + 1] = "\t" .. tokens[1] .. " \\" lines[#lines + 1] = "\t" .. tokens[1] .. " \\"
--- @type integer for tok_idx = 2, #tokens do ---@type integer
for tok_idx = 2, #tokens do local sep = token_skips_leading_comma(tokens[tok_idx]) and "\t" or ",\t" ---@type string
--- @type string
local sep = token_skips_leading_comma(tokens[tok_idx]) and "\t" or ",\t"
lines[#lines + 1] = sep .. tokens[tok_idx] .. " \\" lines[#lines + 1] = sep .. tokens[tok_idx] .. " \\"
end end
strip_trailing_continuation(lines) strip_trailing_continuation(lines)
@@ -768,8 +675,7 @@ end
--- @param counts table<string, integer> -- bag: bare component name -> word count --- @param counts table<string, integer> -- bag: bare component name -> word count
--- @return string[] -- list of lines for this component --- @return string[] -- list of lines for this component
local function build_component_lines(c, counts) local function build_component_lines(c, counts)
--- @type string[] local lines = {} ---@type string[]
local lines = {}
-- Marker comment: emitted once for every skipped component. -- 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); -- 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 end
if c.comment and c.comment ~= "" then if c.comment and c.comment ~= "" then
--- @type integer, string for _, line in ipairs(split_comment_lines(c.comment)) do ---@type integer, string
for _, line in ipairs(split_comment_lines(c.comment)) do
lines[#lines + 1] = line lines[#lines + 1] = line
end end
end end
--- @type string[] local tokens = duffle.split_top_level_commas(c.body) ---@type string[]
local tokens = duffle.split_top_level_commas(c.body) for i = 1, #tokens do tokens[i] = duffle.trim(tokens[i]) end ---@type integer
--- @type integer local sig = signature_from_args(c.args) ---@type string
for i = 1, #tokens do tokens[i] = duffle.trim(tokens[i]) end
--- @type string
local sig = signature_from_args(c.args)
-- Direct lookup against the per-source precomputed `counts` table (built once by count_all_components). -- Direct lookup against the per-source precomputed `counts` table (built once by count_all_components).
--- @type integer local n = counts[c.name] ---@type integer
local n = counts[c.name]
if n > 0 then if n > 0 then
emit_macro_body(lines, c, sig, tokens) emit_macro_body(lines, c, sig, tokens)
@@ -816,14 +717,11 @@ end
--- @param sources SourceFile[] -- Sources contributing to this directory (for the header comment) --- @param sources SourceFile[] -- Sources contributing to this directory (for the header comment)
--- @return string[] --- @return string[]
local function header_boilerplate(dir, sources) local function header_boilerplate(dir, sources)
--- @type string[] local source_lines = { "// Directory: " .. duffle.to_absolute_path(dir) .. "/" } ---@type string[]
local source_lines = { "// Directory: " .. duffle.to_absolute_path(dir) .. "/" } for _, src in ipairs(sources) do ---@type integer, SourceFile
--- @type integer, SourceFile
for _, src in ipairs(sources) do
source_lines[#source_lines + 1] = "// source: " .. duffle.to_absolute_path(src.path) source_lines[#source_lines + 1] = "// source: " .. duffle.to_absolute_path(src.path)
end end
--- @type string local source_blob = table.concat(source_lines, "\n") ---@type string
local source_blob = table.concat(source_lines, "\n")
return { return {
-- #pragma once wrapped in #ifdef INTELLISENSE_DIRECTIVES, matching the convention in lottes_tape.h. -- #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. -- 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 -- Output directory
--- @return string -- Full output path --- @return string -- Full output path
local function compute_macs_h_path(dir) local function compute_macs_h_path(dir)
--- @type string local out_dir = dir .. "/" .. GEN_SUBDIR ---@type string
local out_dir = dir .. "/" .. GEN_SUBDIR local out_path = out_dir .. "/" .. MACS_FILENAME ---@type string
--- @type string
local out_path = out_dir .. "/" .. MACS_FILENAME
return out_dir, out_path return out_dir, out_path
end end
@@ -868,21 +764,16 @@ end
--- @return string|nil -- Path to the written file (nil if no components) --- @return string|nil -- Path to the written file (nil if no components)
local function emit_component_macros_h(ctx, dir, sources, components, counts) local function emit_component_macros_h(ctx, dir, sources, components, counts)
if #components == 0 then return nil end if #components == 0 then return nil end
--- @type string, string local out_dir, out_path = compute_macs_h_path(dir) ---@type string, string
local out_dir, out_path = compute_macs_h_path(dir) local lines = header_boilerplate(dir, sources) ---@type string[]
--- @type string[]
local lines = header_boilerplate(dir, sources)
--- @type integer, Component for _, c in ipairs(components) do ---@type integer, Component
for _, c in ipairs(components) do for _, l in ipairs(build_component_lines(c, counts)) do ---@type integer, string
--- @type integer, string
for _, l in ipairs(build_component_lines(c, counts)) do
lines[#lines + 1] = l lines[#lines + 1] = l
end end
end end
--- @type string local content = table.concat(lines, "\n") .. "\n" ---@type string
local content = table.concat(lines, "\n") .. "\n"
duffle.ensure_dir(out_dir) duffle.ensure_dir(out_dir)
duffle.write_file_lf(out_path, content) duffle.write_file_lf(out_path, content)
print(string.format(" -> %s", out_path)) print(string.format(" -> %s", out_path))
@@ -900,12 +791,9 @@ end
--- @param counts table<string, integer> -- bag: bare component name -> word count --- @param counts table<string, integer> -- bag: bare component name -> word count
--- @return nil --- @return nil
local function update_canonical_word_counts(corpus, components, counts) local function update_canonical_word_counts(corpus, components, counts)
--- @type WordCounts local wc = corpus.word_counts ---@type WordCounts
local wc = corpus.word_counts for _, c in ipairs(components) do ---@type integer, Component
--- @type integer, Component local key = "mac_" .. c.name ---@type string
for _, c in ipairs(components) do
--- @type string
local key = "mac_" .. c.name
if wc[key] == nil then if wc[key] == nil then
wc[key] = counts[c.name] wc[key] = counts[c.name]
end end
@@ -932,15 +820,12 @@ end
--- @param metadata ComponentMetaMap --- @param metadata ComponentMetaMap
--- @return nil --- @return nil
local function update_canonical_components(corpus, src, components, metadata) local function update_canonical_components(corpus, src, components, metadata)
--- @type string local rel_path = src.path:gsub("\\", "/") ---@type string
local rel_path = src.path:gsub("\\", "/") for _, c in ipairs(components) do ---@type integer, Component
--- @type integer, Component
for _, c in ipairs(components) do
-- Keyed by bare name (e.g. `yield`, `load_tri_indices`). -- Keyed by bare name (e.g. `yield`, `load_tri_indices`).
-- The atoms_source_map pass looks up components by bare name from the corpus; -- 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. -- `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 ---@type ComponentMeta|nil
local m = metadata and metadata[c.name] or nil
if corpus.components[c.name] == nil then if corpus.components[c.name] == nil then
corpus.components[c.name] = { corpus.components[c.name] = {
name = c.name, name = c.name,
@@ -954,13 +839,10 @@ local function update_canonical_components(corpus, src, components, metadata)
else else
-- A second declaration of the same bare name: record a typed collision so static-analysis + the report can surface it. -- 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. -- Identical-shape declarations (same path + line) reuse the first-wins entry without a collision record.
--- @type ComponentDef local existing = corpus.components[c.name] ---@type ComponentDef
local existing = corpus.components[c.name]
if existing.path ~= rel_path or existing.line ~= c.line then if existing.path ~= rel_path or existing.line ~= c.line then
--- @type string local kind = c.kind or "comp_bare" ---@type string
local kind = c.kind or "comp_bare" local first_kind = existing.kind or "comp_bare" ---@type string
--- @type string
local first_kind = existing.kind or "comp_bare"
corpus.collisions[#corpus.collisions + 1] = { corpus.collisions[#corpus.collisions + 1] = {
kind = "component", kind = "component",
name = c.name, name = c.name,
@@ -983,10 +865,8 @@ end
--- @param scan SourceScan --- @param scan SourceScan
--- @return nil --- @return nil
local function update_canonical_component_body_index(corpus, src, components, scan) 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 (fun(pos: integer): integer)|nil
local line_of = scan and scan.line_of for _, c in ipairs(components) do ---@type integer, Component
--- @type integer, Component
for _, c in ipairs(components) do
if corpus.component_body_index[c.name] == nil then if corpus.component_body_index[c.name] == nil then
corpus.component_body_index[c.name] = { corpus.component_body_index[c.name] = {
body_tokens = c.body_tokens, body_tokens = c.body_tokens,
@@ -1004,16 +884,12 @@ end
--- @param ctx PassCtx --- @param ctx PassCtx
--- @return PassResult --- @return PassResult
function M.run(ctx) function M.run(ctx)
--- @type MacsOutput[] local outputs = {} ---@type MacsOutput[]
local outputs = {} local errors = {} ---@type PassFinding[]
--- @type PassFinding[] local warnings = {} ---@type PassFinding[]
local errors = {}
--- @type PassFinding[]
local warnings = {}
-- Corpus ownership gate. -- Corpus ownership gate.
--- @type Corpus|nil local corpus = ctx.shared and ctx.shared.corpus ---@type Corpus|nil
local corpus = ctx.shared and ctx.shared.corpus
if type(corpus) ~= "table" then if type(corpus) ~= "table" then
error("components.run requires ctx.shared.corpus.", 0) error("components.run requires ctx.shared.corpus.", 0)
end end
@@ -1034,22 +910,15 @@ function M.run(ctx)
-- Per-directory aggregation: every source in the same directory contributes to one `gen/macs.h`. -- 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`). -- 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 table<string, SourceFile[]>
local sources_by_dir = corpus.sources_by_dir or duffle.group_sources_by_dir(corpus.source_order) for dir, sources in pairs(sources_by_dir) do ---@type string, SourceFile[]
--- @type string, SourceFile[]
for dir, sources in pairs(sources_by_dir) do
-- Aggregate components from every source in this directory. -- Aggregate components from every source in this directory.
-- `project_components` returns nil for sources with no `MipsAtomComp_` declarations; we skip those. -- `project_components` returns nil for sources with no `MipsAtomComp_` declarations; we skip those.
--- @type Component[] local aggregated_components = {} ---@type Component[]
local aggregated_components = {} local metadata_per_source = {} ---@type table<SourceFile, ComponentMetaMap>
--- @type table<SourceFile, ComponentMetaMap> for _, src in ipairs(sources) do ---@type integer, SourceFile
local metadata_per_source = {} local per_source = project_components(src.text, src.scan) or {} ---@type Component[]
--- @type integer, SourceFile for _, c in ipairs(per_source) do ---@type integer, Component
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
aggregated_components[#aggregated_components + 1] = c aggregated_components[#aggregated_components + 1] = c
end end
if #per_source > 0 then if #per_source > 0 then
@@ -1059,18 +928,14 @@ function M.run(ctx)
if #aggregated_components > 0 then if #aggregated_components > 0 then
-- Compute word counts across the aggregated set. `corpus.word_counts` carries the -- Compute word counts across the aggregated set. `corpus.word_counts` carries the
-- same-source + prior-directory entries so the recursive lookup sees both. -- 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 table<string, integer> -- bag: bare name -> word count
local counts = count_all_components(aggregated_components, corpus.word_counts) local macs_path = emit_component_macros_h(ctx, dir, sources, aggregated_components, counts) ---@type string|nil
--- @type string|nil
local macs_path = emit_component_macros_h(ctx, dir, sources, aggregated_components, counts)
if macs_path then if macs_path then
outputs[#outputs + 1] = { macs_h = macs_path } outputs[#outputs + 1] = { macs_h = macs_path }
-- Populate the projections AFTER disk emission (byte-identical `.macs.h` contract). -- Populate the projections AFTER disk emission (byte-identical `.macs.h` contract).
update_canonical_word_counts(corpus, aggregated_components, counts) update_canonical_word_counts(corpus, aggregated_components, counts)
--- @type integer, SourceFile for _, src in ipairs(sources) do ---@type integer, SourceFile
for _, src in ipairs(sources) do local per_source = project_components(src.text, src.scan) or {} ---@type Component[]
--- @type Component[]
local per_source = project_components(src.text, src.scan) or {}
if #per_source > 0 then if #per_source > 0 then
update_canonical_components(corpus, src, per_source, metadata_per_source[src]) update_canonical_components(corpus, src, per_source, metadata_per_source[src])
update_canonical_component_body_index(corpus, src, per_source, src.scan) 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 --- @class EmissionModelPass
--- @field run fun(ctx: PassCtx): PassResult --- @field run fun(ctx: PassCtx): PassResult
--- @type EmissionModelPass local M = {} ---@type EmissionModelPass
local M = {}
-- ───────────────────────────────────────────────────────────────────────── -- ─────────────────────────────────────────────────────────────────────────
-- 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. -- 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 string
local _bootstrap_dir = debug.getinfo(1, "S").source:match("^@?(.*[/\\])") or "./" local duffle = dofile(_bootstrap_dir .. "../duffle_paths.lua") ---@type DuffleExport
--- @type DuffleExport
local duffle = dofile(_bootstrap_dir .. "../duffle_paths.lua")
-- ───────────────────────────────────────────────────────────────────────── -- ─────────────────────────────────────────────────────────────────────────
-- Helpers -- Helpers
@@ -149,8 +146,7 @@ local duffle = dofile(_bootstrap_dir .. "../duffle_paths.lua")
--- @param corpus Corpus --- @param corpus Corpus
--- @return nil --- @return nil
local function stamp_root_provenance(projection, atom_record, src, corpus) 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 ---@type (fun(pos: integer): integer)|nil
local root_line_of = src.scan and src.scan.line_of
assert(type(root_line_of) == "function" 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") , "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" 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`. -- `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)`. -- 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. -- `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 integer
local root_body_line = root_line_of(atom_record.body_off - 1) or atom_record.line or 0 local component_index = corpus.component_body_index or {} ---@type table<string, ComponentBodyEntry>
--- @type table<string, ComponentBodyEntry> local word_items = {} ---@type EmissionItem[]
local component_index = corpus.component_body_index or {}
--- @type EmissionItem[]
local word_items = {}
--- @type integer, EmissionItem for _, item in ipairs(projection.items) do ---@type integer, EmissionItem
for _, item in ipairs(projection.items) do
if item.kind == "word" then word_items[#word_items + 1] = item end if item.kind == "word" then word_items[#word_items + 1] = item end
end end
@@ -177,18 +169,14 @@ local function stamp_root_provenance(projection, atom_record, src, corpus)
--- @param item EmissionItem --- @param item EmissionItem
--- @return integer --- @return integer
local function body_line_for(event, item) local function body_line_for(event, item)
--- @type integer[] local ids = event.invocation_ids or {} ---@type integer[]
local ids = event.invocation_ids or {}
-- The innermost open invocation identifies which line index the walker used. -- 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. -- 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 if ids and #ids > 0 then
--- @type integer local inner_id = ids[#ids] ---@type integer
local inner_id = ids[#ids] local inner_inv = inner_id and projection.invocations[inner_id] ---@type InvocationRecord|nil
--- @type InvocationRecord|nil
local inner_inv = inner_id and projection.invocations[inner_id]
if inner_inv then if inner_inv then
--- @type ComponentBodyEntry|nil local component = component_index[inner_inv.component_name] ---@type ComponentBodyEntry|nil
local component = component_index[inner_inv.component_name]
if component and component.line_of then if component and component.line_of then
-- Walker used `comp.line_of`, which is the source's physical LineIndex. item.line is already physical. -- Walker used `comp.line_of`, which is the source's physical LineIndex. item.line is already physical.
return item.line or 0 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. -- 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. -- 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. -- 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 string
local root_path = src.path or "" for _, inv in ipairs(projection.invocations) do ---@type integer, InvocationRecord
--- @type integer, InvocationRecord
for _, inv in ipairs(projection.invocations) do
if inv.call_path == nil or inv.call_path == "" then if inv.call_path == nil or inv.call_path == "" then
inv.call_path = root_path inv.call_path = root_path
end end
@@ -215,8 +201,7 @@ local function stamp_root_provenance(projection, atom_record, src, corpus)
-- Normalize `inv.call_line` to a physical source line. -- 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`. -- * 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. -- * 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 ---@type integer, InvocationRecord
for _, inv in ipairs(projection.invocations) do
if inv.parent_id == 0 then if inv.parent_id == 0 then
inv.call_line = (root_body_line or 0) + (inv.call_line or 1) - 1 inv.call_line = (root_body_line or 0) + (inv.call_line or 1) - 1
end end
@@ -225,21 +210,14 @@ local function stamp_root_provenance(projection, atom_record, src, corpus)
-- Build `body_lines` for each invocation. -- Build `body_lines` for each invocation.
-- `atoms_source_map` and `dwarf_injection` read `inv.body_lines[k]` directly from the invocation record created here. -- `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. -- 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, InvocationRecord
for _, inv in ipairs(projection.invocations) do local sw = inv.start_word ---@type integer
--- @type integer local ew = inv.end_word ---@type integer
local sw = inv.start_word local bls = {} ---@type integer[]
--- @type integer for i = sw, ew do ---@type integer
local ew = inv.end_word local it = projection.items and projection.items[i] ---@type EmissionItem|nil
--- @type integer[]
local bls = {}
--- @type integer
for i = sw, ew do
--- @type EmissionItem|nil
local it = projection.items and projection.items[i]
if it and it.kind == "word" then if it and it.kind == "word" then
--- @type WordEvent local fake_event = { invocation_ids = { inv.id } } ---@type WordEvent
local fake_event = { invocation_ids = { inv.id } }
bls[#bls + 1] = body_line_for(fake_event, it) or 0 bls[#bls + 1] = body_line_for(fake_event, it) or 0
end end
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`. -- 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. -- 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. -- 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 integer, WordEvent
for index, we in ipairs(projection.word_events) do local item = word_items[index] or {} ---@type EmissionItem
--- @type EmissionItem local body_line = body_line_for(we, item) ---@type integer
local item = word_items[index] or {}
--- @type integer
local body_line = body_line_for(we, item)
item.line = body_line item.line = body_line
we.body_line = body_line we.body_line = body_line
--- @type integer local call_line = body_line ---@type integer
local call_line = body_line local outer_id = we.outermost_invocation_id or 0 ---@type integer
--- @type integer local outer_inv = projection.invocations[outer_id] ---@type InvocationRecord|nil
local outer_id = we.outermost_invocation_id or 0
--- @type InvocationRecord|nil
local outer_inv = projection.invocations[outer_id]
if outer_inv then if outer_inv then
-- `outer_inv.call_line` is physical after the conversion loop above, so use it directly. -- `outer_inv.call_line` is physical after the conversion loop above, so use it directly.
call_line = outer_inv.call_line call_line = outer_inv.call_line
@@ -283,20 +255,15 @@ end
--- @param corpus Corpus --- @param corpus Corpus
--- @return EmissionProjection --- @return EmissionProjection
local function project_atom(atom_record, src, corpus) local function project_atom(atom_record, src, corpus)
--- @type string local body = atom_record.body or "" ---@type string
local body = atom_record.body or "" local wc = corpus.word_counts or {} ---@type WordCounts
--- @type WordCounts local cbi = corpus.component_body_index or {} ---@type table<string, ComponentBodyEntry>
local wc = corpus.word_counts or {} local schema = nil ---@type RegUseSchema|nil
--- @type table<string, ComponentBodyEntry>
local cbi = corpus.component_body_index or {}
--- @type RegUseSchema|nil
local schema = nil
if atom_record.reg_use_schema_name then if atom_record.reg_use_schema_name then
schema = corpus.reg_use_schemas and corpus.reg_use_schemas[atom_record.reg_use_schema_name] schema = corpus.reg_use_schemas and corpus.reg_use_schemas[atom_record.reg_use_schema_name]
end end
-- That construction site stamps `invocation.debug_skip` while appending each record to `proj.invocations`. -- 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, { ---@type EmissionProjection
local proj = duffle.project_emission(body, cbi, wc, corpus.components, {
reg_use_schema = schema, reg_use_schema = schema,
reg_use_param = atom_record.reg_use_param_name, reg_use_param = atom_record.reg_use_param_name,
atom_name = atom_record.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), msg = string.format("RegUse schema %q is missing", atom_record.reg_use_schema_name),
} }
end end
--- @type integer, EmitError for _, err in ipairs(corpus.reg_use_errors or {}) do ---@type integer, EmitError
for _, err in ipairs(corpus.reg_use_errors or {}) do
if err.schema_name == atom_record.reg_use_schema_name then if err.schema_name == atom_record.reg_use_schema_name then
proj.errors[#proj.errors + 1] = err proj.errors[#proj.errors + 1] = err
end end
end end
--- @type AtomPaths local paths = { ---@type AtomPaths
local paths = {
tokens = atom_record.body_tokens or {}, tokens = atom_record.body_tokens or {},
line_in_body = duffle.build_body_line_index(body), line_in_body = duffle.build_body_line_index(body),
items = proj.items, items = proj.items,
@@ -337,15 +302,11 @@ end
--- @param ctx PassCtx -- { shared = { corpus = ... }, out_root, ... } --- @param ctx PassCtx -- { shared = { corpus = ... }, out_root, ... }
--- @return PassResult --- @return PassResult
function M.run(ctx) function M.run(ctx)
--- @type PassOutputEntry[] local outputs = {} ---@type PassOutputEntry[]
local outputs = {} local errors = {} ---@type EmitError[]
--- @type EmitError[] local warnings = {} ---@type EmitWarning[]
local errors = {}
--- @type EmitWarning[]
local warnings = {}
--- @type Corpus|nil local corpus = ctx and ctx.shared and ctx.shared.corpus ---@type Corpus|nil
local corpus = ctx and ctx.shared and ctx.shared.corpus
if type(corpus) ~= "table" then error("emission_model: ctx.shared.corpus is required (canonical projection)", 0) end 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 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 --- @return nil
local function process_atom(atom, src) local function process_atom(atom, src)
if not (atom and atom.body) then return end if not (atom and atom.body) then return end
--- @type string local kind = atom.kind ---@type string
local kind = atom.kind
if kind ~= "atom" and kind ~= "atom_proc" and kind ~= "raw_atom" and kind ~= "comp_bare" and kind ~= "comp_proc" then if kind ~= "atom" and kind ~= "atom_proc" and kind ~= "raw_atom" and kind ~= "comp_bare" and kind ~= "comp_proc" then
return return
end end
--- @type EmissionProjection local proj = project_atom(atom, src, corpus) ---@type EmissionProjection
local proj = project_atom(atom, src, corpus) for _, e in ipairs(proj.errors) do ---@type integer, EmitError
--- @type integer, EmitError
for _, e in ipairs(proj.errors) do
-- Preserve `kind` (cycle / count_mismatch / unbalanced) so readers dispatch on the diagnostic class and leave the message string as display text. -- Preserve `kind` (cycle / count_mismatch / unbalanced) so readers dispatch on the diagnostic class and leave the message string as display text.
errors[#errors + 1] = { errors[#errors + 1] = {
kind = e.kind, kind = e.kind,
@@ -373,8 +331,7 @@ function M.run(ctx)
source = e.source or src.path, source = e.source or src.path,
} }
end end
--- @type integer, EmitWarning for _, w in ipairs(proj.warnings) do ---@type integer, EmitWarning
for _, w in ipairs(proj.warnings) do
warnings[#warnings + 1] = { warnings[#warnings + 1] = {
kind = w.kind, kind = w.kind,
line = w.line, line = w.line,
@@ -386,16 +343,12 @@ function M.run(ctx)
-- Walk `corpus.source_order`; within each source, visit atoms followed by raw_atoms. -- 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. -- 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. -- 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 integer, SourceFile
for _, src in ipairs(corpus.source_order) do local scan = src.scan or {} ---@type SourceScan
--- @type SourceScan for _, atom in ipairs(scan.atoms or {}) do ---@type integer, AtomEntry
local scan = src.scan or {}
--- @type integer, AtomEntry
for _, atom in ipairs(scan.atoms or {}) do
process_atom(atom, src) process_atom(atom, src)
end end
--- @type integer, AtomEntry for _, atom in ipairs(scan.raw_atoms or {}) do ---@type integer, AtomEntry
for _, atom in ipairs(scan.raw_atoms or {}) do
process_atom(atom, src) process_atom(atom, src)
end end
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. -- 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). -- 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. -- 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 string
local _bootstrap_dir = debug.getinfo(1, "S").source:match("^@?(.*[/\\])") or "./" local duffle = dofile(_bootstrap_dir .. "../duffle_paths.lua") ---@type DuffleExport
--- @type DuffleExport
local duffle = dofile(_bootstrap_dir .. "../duffle_paths.lua")
-- ════════════════════════════════════════════════════════════════════════════ -- ════════════════════════════════════════════════════════════════════════════
-- Constants -- Constants
-- ════════════════════════════════════════════════════════════════════════════ -- ════════════════════════════════════════════════════════════════════════════
-- Offset macro/enum naming prefixes (the emitted header uses these). -- Offset macro/enum naming prefixes (the emitted header uses these).
--- @type string local OFFSET_MACRO_PREFIX = "_atom_offset_" ---@type string
local OFFSET_MACRO_PREFIX = "_atom_offset_" local OFFSET_ENUM_PREFIX = "atom_offset_" ---@type string
--- @type string
local OFFSET_ENUM_PREFIX = "atom_offset_"
-- Column width for the `#define _atom_offset_F_T = N` alignment. -- Column width for the `#define _atom_offset_F_T = N` alignment.
--- @type integer local OFFSET_MACRO_COL = 44 ---@type integer
local OFFSET_MACRO_COL = 44
-- ════════════════════════════════════════════════════════════════════════════ -- ════════════════════════════════════════════════════════════════════════════
-- Type declarations -- Type declarations
@@ -91,8 +86,7 @@ local OFFSET_MACRO_COL = 44
-- MARKER_PROJECTORS is the marker-kind data table. -- MARKER_PROJECTORS is the marker-kind data table.
-- The emission-model pass already records marker word positions + consuming-instruction context; -- 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. -- 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 = { ---@type table<string, fun(state: MarkerProjectState, marker: EmissionMarker): nil>
local MARKER_PROJECTORS = {
--- @param state MarkerProjectState --- @param state MarkerProjectState
--- @param marker EmissionMarker --- @param marker EmissionMarker
--- @return nil --- @return nil
@@ -119,12 +113,9 @@ local MARKER_PROJECTORS = {
--- @return table<string, integer> --- @return table<string, integer>
--- @return OffsetBranch[] --- @return OffsetBranch[]
local function project_markers(markers) local function project_markers(markers)
--- @type MarkerProjectState local state = { labels = {}, branches = {} } ---@type MarkerProjectState
local state = { labels = {}, branches = {} } for _, marker in ipairs(markers or {}) do ---@type integer, EmissionMarker
--- @type integer, EmissionMarker local project = MARKER_PROJECTORS[marker.kind] ---@type (fun(state: MarkerProjectState, marker: EmissionMarker): nil)|nil
for _, marker in ipairs(markers or {}) do
--- @type (fun(state: MarkerProjectState, marker: EmissionMarker): nil)|nil
local project = MARKER_PROJECTORS[marker.kind]
if project then project(state, marker) end if project then project(state, marker) end
end end
return state.labels, state.branches return state.labels, state.branches
@@ -149,20 +140,16 @@ end
--- @param errors PassFinding[] --- @param errors PassFinding[]
--- @return BranchOffset[] --- @return BranchOffset[]
local function compute_offsets(labels, branches, errors) local function compute_offsets(labels, branches, errors)
--- @type BranchOffset[] local results = {} ---@type BranchOffset[]
local results = {} for _, br in ipairs(branches) do ---@type integer, OffsetBranch
--- @type integer, OffsetBranch local target = labels[br.target] ---@type integer|nil
for _, br in ipairs(branches) do
--- @type integer|nil
local target = labels[br.target]
if not target then if not target then
errors[#errors + 1] = { errors[#errors + 1] = {
line = br.line or 0, line = br.line or 0,
msg = "Branch target '" .. br.target .. "' has no atom_label (at word " .. br.branch_word .. ")", msg = "Branch target '" .. br.target .. "' has no atom_label (at word " .. br.branch_word .. ")",
} }
else else
--- @type string|nil local consuming = br.consuming_encoder ---@type string|nil
local consuming = br.consuming_encoder
if consuming == nil or consuming == "" then if consuming == nil or consuming == "" then
errors[#errors + 1] = { errors[#errors + 1] = {
line = br.line or 0, line = br.line or 0,
@@ -218,20 +205,16 @@ local function emit_atom_offsets(add, atom)
if #atom.offsets == 0 then return end if #atom.offsets == 0 then return end
add("// --- atom: " .. atom.name .. " (" .. atom.total_words .. " words) ---") add("// --- atom: " .. atom.name .. " (" .. atom.total_words .. " words) ---")
add("") add("")
--- @type OffsetConst[] local consts = {} ---@type OffsetConst[]
local consts = {} for _, r in ipairs(atom.offsets) do ---@type integer, BranchOffset
--- @type integer, BranchOffset
for _, r in ipairs(atom.offsets) do
consts[#consts + 1] = make_offset_const(r) consts[#consts + 1] = make_offset_const(r)
end end
--- @type integer, OffsetConst for _, c in ipairs(consts) do ---@type integer, OffsetConst
for _, c in ipairs(consts) do
add("#define " .. pad_right(c.macro_name, OFFSET_MACRO_COL) .. " " .. c.value) add("#define " .. pad_right(c.macro_name, OFFSET_MACRO_COL) .. " " .. c.value)
end end
add("") add("")
add("enum {") add("enum {")
--- @type integer, OffsetConst for _, c in ipairs(consts) do ---@type integer, OffsetConst
for _, c in ipairs(consts) do
add(" " .. c.enum_name .. " = " .. c.macro_name .. ",") add(" " .. c.enum_name .. " = " .. c.macro_name .. ",")
end end
add("};") add("};")
@@ -244,19 +227,16 @@ end
--- @param atoms_data AtomData[] --- @param atoms_data AtomData[]
--- @return string --- @return string
local function generate_header(dir, sources, atoms_data) local function generate_header(dir, sources, atoms_data)
--- @type string local dir_basename = duffle.basename_no_ext(dir) ---@type string
local dir_basename = duffle.basename_no_ext(dir)
--- @type string[] local lines = {} ---@type string[]
local lines = {}
--- @param s string --- @param s string
--- @return nil --- @return nil
local function add(s) lines[#lines + 1] = s end local function add(s) lines[#lines + 1] = s end
add("// Auto-generated by ps1_meta.lua (passes/offsets.lua) — DO NOT EDIT") add("// Auto-generated by ps1_meta.lua (passes/offsets.lua) — DO NOT EDIT")
add("// Directory: " .. dir:gsub("/", "\\") .. "\\") add("// Directory: " .. dir:gsub("/", "\\") .. "\\")
--- @type integer, SourceFile for _, src in ipairs(sources) do ---@type integer, SourceFile
for _, src in ipairs(sources) do
add("// source: " .. src.path:gsub("/", "\\")) add("// source: " .. src.path:gsub("/", "\\"))
end end
add("#pragma once") add("#pragma once")
@@ -264,8 +244,7 @@ local function generate_header(dir, sources, atoms_data)
add("#pragma region " .. dir_basename) add("#pragma region " .. dir_basename)
add("") add("")
add("") add("")
--- @type integer, AtomData for _, atom in ipairs(atoms_data) do ---@type integer, AtomData
for _, atom in ipairs(atoms_data) do
emit_atom_offsets(add, atom) emit_atom_offsets(add, atom)
end end
add("#pragma endregion " .. dir_basename) add("#pragma endregion " .. dir_basename)
@@ -273,8 +252,7 @@ local function generate_header(dir, sources, atoms_data)
return table.concat(lines, "\n") .. "\n" return table.concat(lines, "\n") .. "\n"
end end
--- @type OffsetsPass local M = {} ---@type OffsetsPass
local M = {}
--- (internal) Aggregate atoms from every source in one directory, render the per-directory `offsets.h`. --- (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. --- Returns the offsets_h path if a header was written, or nil.
@@ -284,17 +262,14 @@ local M = {}
--- @param errors PassFinding[] --- @param errors PassFinding[]
--- @return string|nil --- @return string|nil
local function process_directory(ctx, dir, sources, errors) local function process_directory(ctx, dir, sources, errors)
--- @type AtomData[] local atoms_data = {} ---@type AtomData[]
local atoms_data = {}
--- @param atom AtomEntry --- @param atom AtomEntry
--- @return nil --- @return nil
local function append_atom(atom) local function append_atom(atom)
--- @type AtomPaths|nil local paths = atom and atom.paths ---@type AtomPaths|nil
local paths = atom and atom.paths
if not paths then return end if not paths then return end
--- @type table<string, integer>, OffsetBranch[] local labels, branches = project_markers(paths.markers) ---@type table<string, integer>, OffsetBranch[]
local labels, branches = project_markers(paths.markers)
atoms_data[#atoms_data + 1] = { atoms_data[#atoms_data + 1] = {
name = atom.raw_name or atom.name, name = atom.raw_name or atom.name,
total_words = #(paths.word_events or {}), total_words = #(paths.word_events or {}),
@@ -302,19 +277,14 @@ local function process_directory(ctx, dir, sources, errors)
} }
end end
--- @type integer, SourceFile for _, src in ipairs(sources) do ---@type integer, SourceFile
for _, src in ipairs(sources) do local scan = src.scan or {} ---@type SourceScan
--- @type SourceScan for _, atom in ipairs(scan.atoms or {}) do append_atom(atom) end ---@type integer, AtomEntry
local scan = src.scan or {} for _, atom in ipairs(scan.raw_atoms or {}) do append_atom(atom) end ---@type integer, AtomEntry
--- @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
end end
if #atoms_data == 0 then return nil end if #atoms_data == 0 then return nil end
--- @type string local out_path = dir .. "/gen/offsets.h" ---@type string
local out_path = dir .. "/gen/offsets.h"
duffle.ensure_dir(duffle.dirname(out_path)) duffle.ensure_dir(duffle.dirname(out_path))
duffle.write_file(out_path, generate_header(dir, sources, atoms_data)) duffle.write_file(out_path, generate_header(dir, sources, atoms_data))
return out_path return out_path
@@ -326,15 +296,11 @@ end
--- @param ctx PassCtx --- @param ctx PassCtx
--- @return PassResult --- @return PassResult
function M.run(ctx) function M.run(ctx)
--- @type OffsetOutput[] local outputs = {} ---@type OffsetOutput[]
local outputs = {} local errors = {} ---@type PassFinding[]
--- @type PassFinding[] local warnings = {} ---@type PassFinding[]
local errors = {}
--- @type PassFinding[]
local warnings = {}
--- @type Corpus|nil local corpus = ctx.shared and ctx.shared.corpus ---@type Corpus|nil
local corpus = ctx.shared and ctx.shared.corpus
if type(corpus) ~= "table" then if type(corpus) ~= "table" then
error("offsets.run requires ctx.shared.corpus", 0) error("offsets.run requires ctx.shared.corpus", 0)
end end
@@ -343,12 +309,9 @@ function M.run(ctx)
end end
-- Per-directory aggregation: every source in the same directory contributes to one `gen/offsets.h`. -- 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 table<string, SourceFile[]>
local sources_by_dir = corpus.sources_by_dir or duffle.group_sources_by_dir(corpus.source_order) for dir, sources in pairs(sources_by_dir) do ---@type string, SourceFile[]
--- @type string, SourceFile[] local out_path = process_directory(ctx, dir, sources, errors) ---@type string|nil
for dir, sources in pairs(sources_by_dir) do
--- @type string|nil
local out_path = process_directory(ctx, dir, sources, errors)
if out_path then if out_path then
outputs[#outputs + 1] = { offsets_h = out_path } outputs[#outputs + 1] = { offsets_h = out_path }
end 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. -- 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). -- 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. -- 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 string
local _bootstrap_dir = debug.getinfo(1, "S").source:match("^@?(.*[/\\])") or "./" local duffle = dofile(_bootstrap_dir .. "../duffle_paths.lua") ---@type DuffleExport
--- @type DuffleExport
local duffle = dofile(_bootstrap_dir .. "../duffle_paths.lua")
-- 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). -- 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. -- 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). -- 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") ---@type AtomSourceMapPass
local atoms_source_map = dofile(_bootstrap_dir .. "atoms_source_map.lua")
-- ════════════════════════════════════════════════════════════════════════════ -- ════════════════════════════════════════════════════════════════════════════
-- Constants -- Constants
@@ -35,32 +32,22 @@ local atoms_source_map = dofile(_bootstrap_dir .. "atoms_source_map.lua")
-- Section separators used in the rendered text reports. -- 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. -- 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 RULE_THICK = "========================================================" local SECTION_HEADER_ATOMS = "── Atoms ────────────────────────────────────────────────" ---@type string
--- @type string local SECTION_HEADER_ANNOTS = "── Annotations ──────────────────────────────────────────" ---@type string
local SECTION_HEADER_ATOMS = "── Atoms ────────────────────────────────────────────────" local SECTION_HEADER_BINDS = "── Binds_* structs ──────────────────────────────────────" ---@type string
--- @type string local SECTION_HEADER_MACROS = "── Macro word-count declarations ─────────────────────────" ---@type string
local SECTION_HEADER_ANNOTS = "── Annotations ──────────────────────────────────────────" local SECTION_HEADER_ERRORS = "── Errors ──────────────────────────────────────────────" ---@type string
--- @type string local SECTION_HEADER_WARNINGS = "── Warnings ────────────────────────────────────────────" ---@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 ────────────────────────────────────────────"
-- Lua pattern that captures the basename (last path segment) of a forward- or back-slash separated path. -- Lua pattern that captures the basename (last path segment) of a forward- or back-slash separated path.
--- @type string local BASENAME_PATTERN = "([^/\\]+)$" ---@type string
local BASENAME_PATTERN = "([^/\\]+)$"
-- Debug flag name — set to truthy in `_G` to enable verbose logging. -- Debug flag name — set to truthy in `_G` to enable verbose logging.
--- @type string local DEBUG_FLAG = "_DEBUG_REPORT" ---@type string
local DEBUG_FLAG = "_DEBUG_REPORT"
-- Pass identifier for log messages. -- Pass identifier for log messages.
--- @type string local PASS_NAME = "report" ---@type string
local PASS_NAME = "report"
-- ════════════════════════════════════════════════════════════════════════════ -- ════════════════════════════════════════════════════════════════════════════
-- Type declarations -- Type declarations
@@ -245,18 +232,15 @@ end
--- @param all_results ProjectSummaryRow[] --- @param all_results ProjectSummaryRow[]
--- @return string --- @return string
local function render_project_summary(all_results) local function render_project_summary(all_results)
--- @type string[] local lines = { ---@type string[]
local lines = {
"# Project summary", "# Project summary",
"> Auto-generated by ps1_meta.lua (passes/report.lua).", "> Auto-generated by ps1_meta.lua (passes/report.lua).",
"", "",
"| module | atoms | annots | binds | macros | findings | errors | warnings | info |", "| 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 ProjectSummaryTotals
local totals = { atoms = 0, annots = 0, binds = 0, macros = 0, findings = 0, errors = 0, warnings = 0, info = 0 } for _, e in ipairs(all_results) do ---@type integer, ProjectSummaryRow
--- @type integer, ProjectSummaryRow
for _, e in ipairs(all_results) do
lines[#lines + 1] = string.format("| %s | %d | %d | %d | %d | %d | %d | %d | %d |" 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) , e.module, e.atoms, e.annots, e.binds, e.macros, e.findings, e.errors, e.warnings, e.info)
totals.atoms = totals.atoms + e.atoms totals.atoms = totals.atoms + e.atoms
@@ -281,29 +265,22 @@ end
--- @param wc WordCounts --- @param wc WordCounts
--- @return string --- @return string
local function render_module_atoms_md(dir, dir_sources, wc) local function render_module_atoms_md(dir, dir_sources, wc)
--- @type string local dir_basename = source_basename(dir) ---@type string
local dir_basename = source_basename(dir) local lines = { ---@type string[]
--- @type string[]
local lines = {
"# " .. dir_basename .. " — atoms (verbose source map)", "# " .. dir_basename .. " — atoms (verbose source map)",
"> Per-word call-site + provenance. Auto-generated.", "> Per-word call-site + provenance. Auto-generated.",
"", "",
} }
--- @type integer, SourceFile for _, src in ipairs(dir_sources) do ---@type integer, SourceFile
for _, src in ipairs(dir_sources) do local src_name = source_basename(src.path) ---@type string
--- @type string
local src_name = source_basename(src.path)
lines[#lines + 1] = "## " .. src_name lines[#lines + 1] = "## " .. src_name
lines[#lines + 1] = "" lines[#lines + 1] = ""
-- For each atom with a projection, render its sourcemap + provenance. -- For each atom with a projection, render its sourcemap + provenance.
--- @type AtomEntry[] local atoms_list = {} ---@type AtomEntry[]
local atoms_list = {} for _, atom in ipairs((src.scan or {}).atoms or {}) do ---@type integer, AtomEntry
--- @type integer, AtomEntry
for _, atom in ipairs((src.scan or {}).atoms or {}) do
if atom.paths then atoms_list[#atoms_list + 1] = atom end if atom.paths then atoms_list[#atoms_list + 1] = atom end
end end
--- @type integer, AtomEntry for _, atom in ipairs((src.scan or {}).raw_atoms or {}) do ---@type integer, AtomEntry
for _, atom in ipairs((src.scan or {}).raw_atoms or {}) do
if atom.paths then atoms_list[#atoms_list + 1] = atom end if atom.paths then atoms_list[#atoms_list + 1] = atom end
end end
if #atoms_list == 0 then if #atoms_list == 0 then
@@ -312,10 +289,8 @@ local function render_module_atoms_md(dir, dir_sources, wc)
else else
-- Per-source forward-slash path (same one `emit_atom_stanza` / `emit_provenance_stanza` would derive; -- 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). -- computed once per `## <source>` heading and reused by each atom's `WORD N CALL ...` field).
--- @type string local rel_path = src.path:gsub("\\\\", "/") ---@type string
local rel_path = src.path:gsub("\\\\", "/") for _, atom in ipairs(atoms_list) do ---@type integer, AtomEntry
--- @type integer, AtomEntry
for _, atom in ipairs(atoms_list) do
lines[#lines + 1] = string.format( lines[#lines + 1] = string.format(
"### atom: %s (line %d, %d words)", "### atom: %s (line %d, %d words)",
atom.name, atom.line or 0, #((atom.paths or {}).word_events or {})) atom.name, atom.line or 0, #((atom.paths or {}).word_events or {}))
@@ -342,18 +317,15 @@ end
--- @param atom AtomEntry --- @param atom AtomEntry
--- @return integer --- @return integer
local function decl_words(atom) local function decl_words(atom)
--- @type AtomPaths local p = atom.paths or {} ---@type AtomPaths
local p = atom.paths or {}
return #(p.word_events or {}) return #(p.word_events or {})
end end
--- @param decls AtomEntry[]|nil --- @param decls AtomEntry[]|nil
--- @return KindCounts --- @return KindCounts
local function count_kinds(decls) local function count_kinds(decls)
--- @type KindCounts local n = { atom = 0, atom_proc = 0, comp_bare = 0, comp_proc = 0 } ---@type KindCounts
local n = { atom = 0, atom_proc = 0, comp_bare = 0, comp_proc = 0 } for _, a in ipairs(decls or {}) do ---@type integer, AtomEntry
--- @type integer, AtomEntry
for _, a in ipairs(decls or {}) do
if n[a.kind] ~= nil then n[a.kind] = n[a.kind] + 1 end if n[a.kind] ~= nil then n[a.kind] = n[a.kind] + 1 end
end end
return n return n
@@ -369,10 +341,8 @@ end
--- @param view ModuleView --- @param view ModuleView
--- @return table<string, boolean> --- @return table<string, boolean>
local function decl_names(view) local function decl_names(view)
--- @type table<string, boolean> -- bag: atom name -> true local names = {} ---@type table<string, boolean> -- bag: atom name -> true
local names = {} for _, a in ipairs(view.decls or {}) do ---@type integer, AtomEntry
--- @type integer, AtomEntry
for _, a in ipairs(view.decls or {}) do
if a.name then names[a.name] = true end if a.name then names[a.name] = true end
end end
return names return names
@@ -383,15 +353,12 @@ end
--- @return boolean --- @return boolean
local function path_in_module(path, view) local function path_in_module(path, view)
if type(path) ~= "string" or path == "" then return false end if type(path) ~= "string" or path == "" then return false end
--- @type string local norm = path:gsub("\\", "/") ---@type string
local norm = path:gsub("\\", "/") local dir = (view.dir or ""):gsub("\\", "/") ---@type string
--- @type string
local dir = (view.dir or ""):gsub("\\", "/")
if dir ~= "" and (norm == dir or norm:sub(1, #dir + 1) == dir .. "/") then if dir ~= "" and (norm == dir or norm:sub(1, #dir + 1) == dir .. "/") then
return true return true
end end
--- @type integer, SourceFile for _, src in ipairs(view.sources or {}) do ---@type integer, SourceFile
for _, src in ipairs(view.sources or {}) do
if (src.path or ""):gsub("\\", "/") == norm then return true end if (src.path or ""):gsub("\\", "/") == norm then return true end
end end
return false return false
@@ -402,26 +369,18 @@ end
--- @param corpus Corpus --- @param corpus Corpus
--- @return ModuleView --- @return ModuleView
local function build_module_view(dir, dir_sources, corpus) local function build_module_view(dir, dir_sources, corpus)
--- @type AtomEntry[] local decls = {} ---@type AtomEntry[]
local decls = {} for _, src in ipairs(dir_sources or {}) do ---@type integer, SourceFile
--- @type integer, SourceFile for _, a in ipairs((src.scan and src.scan.atoms) or {}) do ---@type integer, AtomEntry
for _, src in ipairs(dir_sources or {}) do
--- @type integer, AtomEntry
for _, a in ipairs((src.scan and src.scan.atoms) or {}) do
if not a.source_path then a.source_path = src.path end if not a.source_path then a.source_path = src.path end
decls[#decls + 1] = a decls[#decls + 1] = a
end end
end end
--- @type string local dir_basename = source_basename(dir) ---@type string
local dir_basename = source_basename(dir) local sa = (corpus.static_analysis_results or {})[dir_basename] or {} ---@type AtomAnalysis
--- @type AtomAnalysis local schemas = {} ---@type RegUseSchema[]
local sa = (corpus.static_analysis_results or {})[dir_basename] or {} for name, schema in pairs(corpus.reg_use_schemas or {}) do ---@type string, RegUseSchema
--- @type RegUseSchema[] for _, a in ipairs(decls) do ---@type integer, AtomEntry
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
if a.reg_use_schema_name == name then if a.reg_use_schema_name == name then
schemas[#schemas + 1] = schema schemas[#schemas + 1] = schema
break break
@@ -445,10 +404,8 @@ local function render_section_declarations(add, view)
if #view.decls == 0 then add("_(none)_"); add(""); return end if #view.decls == 0 then add("_(none)_"); add(""); return end
add("| kind | name | source | line | words | min | max | branches | paths |") add("| kind | name | source | line | words | min | max | branches | paths |")
add("|------|------|--------|------|-------|-----|-----|----------|-------|") add("|------|------|--------|------|-------|-----|-----|----------|-------|")
--- @type integer, AtomEntry for _, a in ipairs(view.decls) do ---@type integer, AtomEntry
for _, a in ipairs(view.decls) do local p = a.paths or {} ---@type AtomPaths
--- @type AtomPaths
local p = a.paths or {}
add(string.format("| %s | %s | %s | %d | %d | %s | %s | %s | %s |", add(string.format("| %s | %s | %s | %d | %d | %s | %s | %s | %s |",
a.kind or "?", a.kind or "?",
a.name or "?", a.name or "?",
@@ -467,17 +424,12 @@ end
--- @param view ModuleView --- @param view ModuleView
--- @return nil --- @return nil
local function render_section_components(add, view) local function render_section_components(add, view)
--- @type ComponentReportRow[] local rows = {} ---@type ComponentReportRow[]
local rows = {} local index = (view.corpus and view.corpus.component_body_index) or {} ---@type table<string, ComponentBodyEntry>
--- @type table<string, ComponentBodyEntry> for _, a in ipairs(view.decls) do ---@type integer, AtomEntry
local index = (view.corpus and view.corpus.component_body_index) or {}
--- @type integer, AtomEntry
for _, a in ipairs(view.decls) do
if a.kind == "comp_bare" or a.kind == "comp_proc" then if a.kind == "comp_bare" or a.kind == "comp_proc" then
--- @type ComponentBodyEntry local idx = index[a.name] or {} ---@type ComponentBodyEntry
local idx = index[a.name] or {} local args = idx.arg_names or {} ---@type string[]
--- @type string[]
local args = idx.arg_names or {}
rows[#rows + 1] = { rows[#rows + 1] = {
name = a.name, name = a.name,
kind = a.kind, kind = a.kind,
@@ -490,8 +442,7 @@ local function render_section_components(add, view)
if #rows == 0 then add("_(none)_"); add(""); return end if #rows == 0 then add("_(none)_"); add(""); return end
add("| name | kind | arg_names | words | map |") add("| name | kind | arg_names | words | map |")
add("|------|------|-----------|-------|-----|") add("|------|------|-----------|-------|-----|")
--- @type integer, ComponentReportRow for _, r in ipairs(rows) do ---@type integer, ComponentReportRow
for _, r in ipairs(rows) do
add(string.format("| %s | %s | %s | %d | %s |", add(string.format("| %s | %s | %s | %d | %s |",
r.name, r.kind, r.args ~= "" and r.args or "", r.words, r.map)) r.name, r.kind, r.args ~= "" and r.args or "", r.words, r.map))
end end
@@ -502,38 +453,28 @@ end
--- @param view ModuleView --- @param view ModuleView
--- @return nil --- @return nil
local function render_section_reguse(add, view) local function render_section_reguse(add, view)
--- @type boolean local wrote = false ---@type boolean
local wrote = false for _, schema in ipairs(view.schemas or {}) do ---@type integer, RegUseSchema
--- @type integer, RegUseSchema
for _, schema in ipairs(view.schemas or {}) do
wrote = true wrote = true
add(string.format("### %s", schema.name or "?")) add(string.format("### %s", schema.name or "?"))
--- @type integer, RegUseSlot for _, slot in ipairs(schema.slots or {}) do ---@type integer, RegUseSlot
for _, slot in ipairs(schema.slots or {}) do local aliases = table.concat(slot.aliases or { slot.name }, ", ") ---@type string
--- @type string local ro = slot.readonly and " readonly" or "" ---@type string
local aliases = table.concat(slot.aliases or { slot.name }, ", ")
--- @type string
local ro = slot.readonly and " readonly" or ""
add(string.format("- slot `%s` aliases %s%s", slot.name, aliases, ro)) add(string.format("- slot `%s` aliases %s%s", slot.name, aliases, ro))
end end
--- @type integer, AtomEntry for _, a in ipairs(view.decls) do ---@type integer, AtomEntry
for _, a in ipairs(view.decls) do
if a.reg_use_schema_name == schema.name then if a.reg_use_schema_name == schema.name then
add(string.format("- bound `%s` param `%s`", a.name, a.reg_use_param_name or "?")) add(string.format("- bound `%s` param `%s`", a.name, a.reg_use_param_name or "?"))
end end
end end
add("") add("")
end end
--- @type table<string, boolean> -- bag: schema name -> true local bound = {} ---@type table<string, boolean> -- bag: schema name -> true
local bound = {} for _, schema in ipairs(view.schemas or {}) do ---@type integer, RegUseSchema
--- @type integer, RegUseSchema
for _, schema in ipairs(view.schemas or {}) do
if schema.name then bound[schema.name] = true end if schema.name then bound[schema.name] = true end
end end
--- @type RegUseError[] local errors = {} ---@type RegUseError[]
local errors = {} for _, err in ipairs((view.corpus and view.corpus.reg_use_errors) or {}) do ---@type integer, RegUseError
--- @type integer, RegUseError
for _, err in ipairs((view.corpus and view.corpus.reg_use_errors) or {}) do
if bound[err.schema_name] or path_in_module(err.source_file, view) then if bound[err.schema_name] or path_in_module(err.source_file, view) then
errors[#errors + 1] = err errors[#errors + 1] = err
end end
@@ -541,8 +482,7 @@ local function render_section_reguse(add, view)
if #errors > 0 then if #errors > 0 then
wrote = true wrote = true
add("### parse errors") add("### parse errors")
--- @type integer, RegUseError for _, err in ipairs(errors) do ---@type integer, RegUseError
for _, err in ipairs(errors) do
add(string.format("- `%s` %s", err.kind or "?", err.schema_name or "")) add(string.format("- `%s` %s", err.kind or "?", err.schema_name or ""))
end end
add("") add("")
@@ -554,12 +494,9 @@ end
--- @param view ModuleView --- @param view ModuleView
--- @return nil --- @return nil
local function render_section_annotations(add, view) local function render_section_annotations(add, view)
--- @type AnnotReportRow[] local rows = {} ---@type AnnotReportRow[]
local rows = {} for _, src in ipairs(view.sources) do ---@type integer, SourceFile
--- @type integer, SourceFile for _, info in ipairs((src.scan and src.scan.atom_infos) or {}) do ---@type integer, AtomInfoEntry
for _, src in ipairs(view.sources) do
--- @type integer, AtomInfoEntry
for _, info in ipairs((src.scan and src.scan.atom_infos) or {}) do
rows[#rows + 1] = { rows[#rows + 1] = {
source = source_basename(src.path), source = source_basename(src.path),
line = info.info_line or 0, 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 if #rows == 0 then add("_(none)_"); add(""); return end
add("| source | line | name | binds | reads | writes | phase |") add("| source | line | name | binds | reads | writes | phase |")
add("|--------|------|------|-------|-------|--------|-------|") add("|--------|------|------|-------|-------|--------|-------|")
--- @type integer, AnnotReportRow for _, r in ipairs(rows) do ---@type integer, AnnotReportRow
for _, r in ipairs(rows) do
add(string.format("| %s | %d | %s | %s | %s | %s | %s |", add(string.format("| %s | %d | %s | %s | %s | %s | %s |",
r.source, r.line, r.name, r.binds, r.reads, r.writes, r.phase)) r.source, r.line, r.name, r.binds, r.reads, r.writes, r.phase))
end end
@@ -586,12 +522,9 @@ end
--- @param view ModuleView --- @param view ModuleView
--- @return nil --- @return nil
local function render_section_component_annotations(add, view) local function render_section_component_annotations(add, view)
--- @type CompAnnotReportRow[] local rows = {} ---@type CompAnnotReportRow[]
local rows = {} for _, src in ipairs(view.sources) do ---@type integer, SourceFile
--- @type integer, SourceFile for _, info in ipairs((src.scan and src.scan.component_atom_infos) or {}) do ---@type integer, AtomInfoEntry
for _, src in ipairs(view.sources) do
--- @type integer, AtomInfoEntry
for _, info in ipairs((src.scan and src.scan.component_atom_infos) or {}) do
rows[#rows + 1] = { rows[#rows + 1] = {
source = source_basename(src.path), source = source_basename(src.path),
line = info.info_line or 0, 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 if #rows == 0 then add("_(none)_"); add(""); return end
add("| source | line | name | reads | writes |") add("| source | line | name | reads | writes |")
add("|--------|------|------|-------|--------|") add("|--------|------|------|-------|--------|")
--- @type integer, CompAnnotReportRow for _, r in ipairs(rows) do ---@type integer, CompAnnotReportRow
for _, r in ipairs(rows) do
add(string.format("| %s | %d | %s | %s | %s |", add(string.format("| %s | %d | %s | %s | %s |",
r.source, r.line, r.name, r.reads, r.writes)) r.source, r.line, r.name, r.reads, r.writes))
end end
@@ -616,17 +548,13 @@ end
--- @param view ModuleView --- @param view ModuleView
--- @return nil --- @return nil
local function render_section_binds(add, view) local function render_section_binds(add, view)
--- @type boolean local wrote = false ---@type boolean
local wrote = false for _, src in ipairs(view.sources) do ---@type integer, SourceFile
--- @type integer, SourceFile for _, b in ipairs((src.scan and src.scan.binds) or {}) do ---@type integer, BindsEntry
for _, src in ipairs(view.sources) do
--- @type integer, BindsEntry
for _, b in ipairs((src.scan and src.scan.binds) or {}) do
wrote = true wrote = true
add(string.format("### %s (%s:%s, %s bytes)", add(string.format("### %s (%s:%s, %s bytes)",
b.name, source_basename(src.path), tostring(b.line or 0), tostring(b.bytes or ""))) 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 ---@type integer, TypeField
for _, f in ipairs(b.fields or {}) do
add(string.format("- `+%s %s`", tostring(f.offset or "?"), f.name or "?")) add(string.format("- `+%s %s`", tostring(f.offset or "?"), f.name or "?"))
end end
add("") add("")
@@ -639,18 +567,12 @@ end
--- @param view ModuleView --- @param view ModuleView
--- @return nil --- @return nil
local function render_section_phases(add, view) local function render_section_phases(add, view)
--- @type Corpus local corpus = view.corpus or {} ---@type Corpus
local corpus = view.corpus or {} local names = decl_names(view) ---@type table<string, boolean>
--- @type table<string, boolean> local wrote = false ---@type boolean
local names = decl_names(view) for phase, entry in pairs(corpus.atom_phases or {}) do ---@type string, AtomPhaseGroup
--- @type boolean local here = {} ---@type string[]
local wrote = false for _, atom_name in ipairs(entry.atoms or {}) do ---@type integer, string
--- @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
if names[atom_name] then here[#here + 1] = atom_name end if names[atom_name] then here[#here + 1] = atom_name end
end end
if #here > 0 then 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, ", "))) add(string.format("- phase `%s`: %s", phase, table.concat(here, ", ")))
end end
end end
--- @type string, AtomViewEntry for name, entry in pairs(corpus.atom_views or {}) do ---@type string, AtomViewEntry
for name, entry in pairs(corpus.atom_views or {}) do
if names[name] then if names[name] then
wrote = true wrote = true
add(string.format("- view `%s` binds `%s`", name, entry.binds_name or "")) add(string.format("- view `%s` binds `%s`", name, entry.binds_name or ""))
end end
end end
--- @type string, AtomCtxEntry for name, entry in pairs(corpus.atom_ctxs or {}) do ---@type string, AtomCtxEntry
for name, entry in pairs(corpus.atom_ctxs or {}) do
if names[name] then if names[name] then
wrote = true wrote = true
add(string.format("- ctx `%s` rbind `%s`", name, entry.rbind_atom or "")) add(string.format("- ctx `%s` rbind `%s`", name, entry.rbind_atom or ""))
@@ -680,14 +600,10 @@ end
--- @param view ModuleView --- @param view ModuleView
--- @return nil --- @return nil
local function render_section_aliases(add, view) local function render_section_aliases(add, view)
--- @type string[] local names = {} ---@type string[]
local names = {} local seen = {} ---@type table<string, AliasEntry>
--- @type table<string, AliasEntry> for _, src in ipairs(view.sources or {}) do ---@type integer, SourceFile
local seen = {} for name, entry in pairs((src.scan and src.scan.register_alias_registry) or {}) do ---@type string, AliasEntry
--- @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
if not seen[name] then if not seen[name] then
seen[name] = entry seen[name] = entry
names[#names + 1] = name names[#names + 1] = name
@@ -698,10 +614,8 @@ local function render_section_aliases(add, view)
if #names == 0 then add("_(none)_"); add(""); return end if #names == 0 then add("_(none)_"); add(""); return end
add("| alias | type |") add("| alias | type |")
add("|-------|------|") add("|-------|------|")
--- @type integer, string for _, name in ipairs(names) do ---@type integer, string
for _, name in ipairs(names) do local e = seen[name] ---@type AliasEntry|nil
--- @type AliasEntry|nil
local e = seen[name]
add(string.format("| %s | %s |", name, (e and e.default_type) or "")) add(string.format("| %s | %s |", name, (e and e.default_type) or ""))
end end
add("") add("")
@@ -711,40 +625,30 @@ end
--- @param view ModuleView --- @param view ModuleView
--- @return nil --- @return nil
local function render_section_autoreg(add, view) local function render_section_autoreg(add, view)
--- @type table<string, boolean> local allowed = decl_names(view) ---@type table<string, boolean>
local allowed = decl_names(view) for phase, entry in pairs((view.corpus and view.corpus.atom_phases) or {}) do ---@type string, AtomPhaseGroup
--- @type string, AtomPhaseGroup for _, atom_name in ipairs(entry.atoms or {}) do ---@type integer, string
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
if allowed[atom_name] then allowed[phase] = true end if allowed[atom_name] then allowed[phase] = true end
end end
end end
--- @type boolean local wrote = false ---@type boolean
local wrote = false local seen = {} ---@type table<string, boolean> -- bag: label\\0scope -> already dumped
--- @type table<string, boolean> -- bag: label\\0scope -> already dumped
local seen = {}
--- @param label string --- @param label string
--- @param table_map table<string, GprAllocMap>|nil --- @param table_map table<string, GprAllocMap>|nil
--- @return nil --- @return nil
local function dump(label, table_map) local function dump(label, table_map)
--- @type string[] local scopes = {} ---@type string[]
local scopes = {} for scope in pairs(table_map or {}) do ---@type string
--- @type string
for scope in pairs(table_map or {}) do
if allowed[scope] and not seen[label .. "\0" .. scope] then if allowed[scope] and not seen[label .. "\0" .. scope] then
scopes[#scopes + 1] = scope scopes[#scopes + 1] = scope
end end
end end
table.sort(scopes) table.sort(scopes)
--- @type integer, string for _, scope in ipairs(scopes) do ---@type integer, string
for _, scope in ipairs(scopes) do
seen[label .. "\0" .. scope] = true seen[label .. "\0" .. scope] = true
wrote = true wrote = true
--- @type string[] local syms = {} ---@type string[]
local syms = {} for sym, gpr in pairs(table_map[scope] or {}) do ---@type string, string
--- @type string, string
for sym, gpr in pairs(table_map[scope] or {}) do
if type(gpr) == "string" and gpr ~= sym then if type(gpr) == "string" and gpr ~= sym then
syms[#syms + 1] = string.format("%s → %s", sym, gpr) syms[#syms + 1] = string.format("%s → %s", sym, gpr)
else else
@@ -755,12 +659,10 @@ local function render_section_autoreg(add, view)
add(string.format("- %s `%s`: %s", label, scope, table.concat(syms, ", "))) add(string.format("- %s `%s`: %s", label, scope, table.concat(syms, ", ")))
end end
end end
--- @type Corpus local corpus = view.corpus or {} ---@type Corpus
local corpus = view.corpus or {}
dump("atom", corpus.atom_auto_regs) dump("atom", corpus.atom_auto_regs)
dump("phase", corpus.phase_auto_regs) dump("phase", corpus.phase_auto_regs)
--- @type integer, SourceFile for _, src in ipairs(view.sources or {}) do ---@type integer, SourceFile
for _, src in ipairs(view.sources or {}) do
dump("atom", src.scan and src.scan.atom_auto_regs) dump("atom", src.scan and src.scan.atom_auto_regs)
dump("phase", src.scan and src.scan.phase_auto_regs) dump("phase", src.scan and src.scan.phase_auto_regs)
end end
@@ -772,25 +674,18 @@ end
--- @param view ModuleView --- @param view ModuleView
--- @return nil --- @return nil
local function render_section_collisions(add, view) local function render_section_collisions(add, view)
--- @type CorpusCollision[] local rows = {} ---@type CorpusCollision[]
local rows = {} for _, c in ipairs((view.corpus and view.corpus.collisions) or {}) do ---@type integer, CorpusCollision
--- @type integer, CorpusCollision local first = c.first_site or {} ---@type CollisionSite
for _, c in ipairs((view.corpus and view.corpus.collisions) or {}) do local other = c.conflicting_site or {} ---@type CollisionSite
--- @type CollisionSite
local first = c.first_site or {}
--- @type CollisionSite
local other = c.conflicting_site or {}
if path_in_module(first.path, view) or path_in_module(other.path, view) then if path_in_module(first.path, view) or path_in_module(other.path, view) then
rows[#rows + 1] = c rows[#rows + 1] = c
end end
end end
if #rows == 0 then add("_(none)_"); add(""); return end if #rows == 0 then add("_(none)_"); add(""); return end
--- @type integer, CorpusCollision for _, c in ipairs(rows) do ---@type integer, CorpusCollision
for _, c in ipairs(rows) do local first = c.first_site or {} ---@type CollisionSite
--- @type CollisionSite local other = c.conflicting_site or {} ---@type CollisionSite
local first = c.first_site or {}
--- @type CollisionSite
local other = c.conflicting_site or {}
add(string.format("- `%s` `%s` first %s:%s conflict %s:%s", add(string.format("- `%s` `%s` first %s:%s conflict %s:%s",
c.kind or "?", c.name or "?", c.kind or "?", c.name or "?",
tostring(first.path or "?"), tostring(first.line or "?"), tostring(first.path or "?"), tostring(first.line or "?"),
@@ -803,29 +698,22 @@ end
--- @param view ModuleView --- @param view ModuleView
--- @return nil --- @return nil
local function render_section_findings(add, view) local function render_section_findings(add, view)
--- @type table<string, CheckFinding[]> local by_atom = {} ---@type table<string, CheckFinding[]>
local by_atom = {} for _, f in ipairs(view.findings or {}) do ---@type integer, CheckFinding
--- @type integer, CheckFinding local key = f.atom or "?" ---@type string
for _, f in ipairs(view.findings or {}) do
--- @type string
local key = f.atom or "?"
by_atom[key] = by_atom[key] or {} by_atom[key] = by_atom[key] or {}
by_atom[key][#by_atom[key] + 1] = f by_atom[key][#by_atom[key] + 1] = f
end end
if next(by_atom) == nil then add("_(none)_"); add(""); return end if next(by_atom) == nil then add("_(none)_"); add(""); return end
--- @type table<string, boolean> -- bag: atom name already emitted local seen = {} ---@type table<string, boolean> -- bag: atom name already emitted
local seen = {}
--- @param name string --- @param name string
--- @param fs CheckFinding[] --- @param fs CheckFinding[]
--- @return nil --- @return nil
local function emit(name, fs) local function emit(name, fs)
add("### " .. name) add("### " .. name)
--- @type integer, CheckFinding for _, f in ipairs(fs) do ---@type integer, CheckFinding
for _, f in ipairs(fs) do local msg = f.msg or "" ---@type string
--- @type string local slot = slot_suffix(f.gpr_key or f.producer_destination) ---@type string|nil
local msg = f.msg or ""
--- @type string|nil
local slot = slot_suffix(f.gpr_key or f.producer_destination)
if slot and not msg:find("(slot ", 1, true) then if slot and not msg:find("(slot ", 1, true) then
msg = msg .. " (slot " .. slot .. ")" msg = msg .. " (slot " .. slot .. ")"
end end
@@ -833,45 +721,34 @@ local function render_section_findings(add, view)
end end
add("") add("")
end end
--- @type integer, AtomEntry for _, a in ipairs(view.decls) do ---@type integer, AtomEntry
for _, a in ipairs(view.decls) do
if by_atom[a.name] then if by_atom[a.name] then
seen[a.name] = true seen[a.name] = true
emit(a.name, by_atom[a.name]) emit(a.name, by_atom[a.name])
end end
end end
--- @type string[] local leftovers = {} ---@type string[]
local leftovers = {} for name in pairs(by_atom) do ---@type string
--- @type string
for name in pairs(by_atom) do
if not seen[name] then leftovers[#leftovers + 1] = name end if not seen[name] then leftovers[#leftovers + 1] = name end
end end
table.sort(leftovers) table.sort(leftovers)
--- @type integer, string for _, name in ipairs(leftovers) do emit(name, by_atom[name]) end ---@type integer, string
for _, name in ipairs(leftovers) do emit(name, by_atom[name]) end
end end
--- @param add fun(s: string): nil --- @param add fun(s: string): nil
--- @param view ModuleView --- @param view ModuleView
--- @return nil --- @return nil
local function render_section_relations(add, view) local function render_section_relations(add, view)
--- @type boolean local wrote = false ---@type boolean
local wrote = false for _, a in ipairs(view.decls) do ---@type integer, AtomEntry
--- @type integer, AtomEntry local rels = (a.paths and a.paths.relations) or {} ---@type AtomRelation[]
for _, a in ipairs(view.decls) do
--- @type AtomRelation[]
local rels = (a.paths and a.paths.relations) or {}
if #rels > 0 then if #rels > 0 then
wrote = true wrote = true
add("### " .. a.name) add("### " .. a.name)
--- @type integer, AtomRelation for _, rel in ipairs(rels) do ---@type integer, AtomRelation
for _, rel in ipairs(rels) do local dest = rel.destination or rel.producer_destination or "" ---@type string
--- @type string local slot = slot_suffix(dest) ---@type string|nil
local dest = rel.destination or rel.producer_destination or "" local dest_s = tostring(dest) ---@type string
--- @type string|nil
local slot = slot_suffix(dest)
--- @type string
local dest_s = tostring(dest)
if slot then dest_s = dest_s .. " (slot " .. slot .. ")" end if slot then dest_s = dest_s .. " (slot " .. slot .. ")" end
add(string.format("- `%s` words %s → %s dest %s", add(string.format("- `%s` words %s → %s dest %s",
rel.semantic or "?", rel.semantic or "?",
@@ -885,13 +762,11 @@ local function render_section_relations(add, view)
if not wrote then add("_(none)_"); add("") end if not wrote then add("_(none)_"); add("") end
end end
--- @type table<string, boolean> -- bag: GPR key hidden unless an encoder wrote it local HIDDEN_UNLESS_WRITTEN = { ---@type table<string, boolean> -- bag: GPR key hidden unless an encoder wrote it
local HIDDEN_UNLESS_WRITTEN = {
R_AT = true, R_TapePtr = true, R_AtomJmp = true, R_AT = true, R_TapePtr = true, R_AtomJmp = true,
} }
--- @type table<string, boolean> -- bag: physical GPR alias -> true local PHYSICAL_GPR = { ---@type table<string, boolean> -- bag: physical GPR alias -> true
local PHYSICAL_GPR = {
R_T0 = true, R_T1 = true, R_T2 = true, R_T3 = 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_T4 = true, R_T5 = true, R_T6 = true, R_T7 = true,
R_V0 = true, R_V1 = true, R_V0 = true, R_V1 = true,
@@ -901,10 +776,8 @@ local PHYSICAL_GPR = {
--- @param key string --- @param key string
--- @return boolean --- @return boolean
local function encoder_wrote_key(atom, key) 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, WordEvent
for _, ev in ipairs((atom.paths and atom.paths.word_events) or {}) do for _, dest in pairs(ev.gpr_keys or {}) do ---@type integer|string, string
--- @type integer|string, string
for _, dest in pairs(ev.gpr_keys or {}) do
if dest == key then return true end if dest == key then return true end
end end
end end
@@ -915,11 +788,9 @@ end
--- @param atom AtomEntry --- @param atom AtomEntry
--- @return string --- @return string
local function written_name_for(key, atom) local function written_name_for(key, atom)
--- @type string|nil local slot = key:match("^reguse:.+:(.+)$") ---@type string|nil
local slot = key:match("^reguse:.+:(.+)$")
if slot then if slot then
--- @type string|nil local param = atom.reg_use_param_name ---@type string|nil
local param = atom.reg_use_param_name
if param and param ~= "" then return param .. "." .. slot end if param and param ~= "" then return param .. "." .. slot end
return slot return slot
end end
@@ -931,21 +802,15 @@ end
--- @param view ModuleView --- @param view ModuleView
--- @return string --- @return string
local function aliases_for_key(key, atom, view) local function aliases_for_key(key, atom, view)
--- @type string|nil local slot = key:match("^reguse:.+:(.+)$") ---@type string|nil
local slot = key:match("^reguse:.+:(.+)$")
if not slot then return "" end if not slot then return "" end
--- @type string|nil local schema_name = atom.reg_use_schema_name ---@type string|nil
local schema_name = atom.reg_use_schema_name local schema = view.corpus and view.corpus.reg_use_schemas and view.corpus.reg_use_schemas[schema_name] ---@type RegUseSchema|nil
--- @type RegUseSchema|nil
local schema = view.corpus and view.corpus.reg_use_schemas and view.corpus.reg_use_schemas[schema_name]
if not schema then return "" end if not schema then return "" end
--- @type integer, RegUseSlot for _, s in ipairs(schema.slots or {}) do ---@type integer, RegUseSlot
for _, s in ipairs(schema.slots or {}) do
if s.name == slot then if s.name == slot then
--- @type string[] local names = {} ---@type string[]
local names = {} for _, alias in ipairs(s.aliases or {}) do ---@type integer, string
--- @type integer, string
for _, alias in ipairs(s.aliases or {}) do
if alias ~= slot then names[#names + 1] = alias end if alias ~= slot then names[#names + 1] = alias end
end end
if #names == 0 then if #names == 0 then
@@ -964,25 +829,19 @@ end
--- @return string --- @return string
local function physical_for_key(key, atom, view) local function physical_for_key(key, atom, view)
if PHYSICAL_GPR[key] then return key end if PHYSICAL_GPR[key] then return key end
--- @type Corpus local corpus = view.corpus or {} ---@type Corpus
local corpus = view.corpus or {} local alias = (corpus.register_alias_registry or {})[key] ---@type AliasEntry|string|nil
--- @type AliasEntry|string|nil
local alias = (corpus.register_alias_registry or {})[key]
if type(alias) == "table" then if type(alias) == "table" then
--- @type string|nil local phys = alias.physical or alias.gpr or alias.code_name ---@type string|nil
local phys = alias.physical or alias.gpr or alias.code_name
if type(phys) == "string" and PHYSICAL_GPR[phys] then return phys end 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 if type(alias.name) == "string" and PHYSICAL_GPR[alias.name] then return alias.name end
elseif type(alias) == "string" and PHYSICAL_GPR[alias] then elseif type(alias) == "string" and PHYSICAL_GPR[alias] then
return alias return alias
end end
--- @type GprAllocMap|nil local atom_map = (corpus.atom_auto_regs or {})[atom.name] ---@type GprAllocMap|nil
local atom_map = (corpus.atom_auto_regs or {})[atom.name]
if type(atom_map) == "table" then if type(atom_map) == "table" then
--- @type string local slot = key:match("^reguse:.+:(.+)$") or key ---@type string
local slot = key:match("^reguse:.+:(.+)$") or key local bound = atom_map[slot] or atom_map["R_" .. slot] ---@type string|nil
--- @type string|nil
local bound = atom_map[slot] or atom_map["R_" .. slot]
if type(bound) == "string" and PHYSICAL_GPR[bound] then return bound end if type(bound) == "string" and PHYSICAL_GPR[bound] then return bound end
end end
return "" return ""
@@ -992,21 +851,15 @@ end
--- @param atom AtomEntry --- @param atom AtomEntry
--- @return string --- @return string
local function last_relation_for(key, atom) local function last_relation_for(key, atom)
--- @type AtomRelation|nil local last = nil ---@type AtomRelation|nil
local last = nil for _, rel in ipairs((atom.paths and atom.paths.relations) or {}) do ---@type integer, AtomRelation
--- @type integer, AtomRelation local dest = rel.destination or rel.producer_destination ---@type string|nil
for _, rel in ipairs((atom.paths and atom.paths.relations) or {}) do
--- @type string|nil
local dest = rel.destination or rel.producer_destination
if dest == key then last = rel end if dest == key then last = rel end
end end
if not last then return "" end if not last then return "" end
--- @type string local sem = last.semantic or "?" ---@type string
local sem = last.semantic or "?" local a = last.producer_word ---@type integer|nil
--- @type integer|nil local b = last.consumer_word ---@type integer|nil
local a = last.producer_word
--- @type integer|nil
local b = last.consumer_word
if a and b then return string.format("%s w%s→%s", sem, tostring(a), tostring(b)) end if a and b then return string.format("%s w%s→%s", sem, tostring(a), tostring(b)) end
return sem return sem
end end
@@ -1015,16 +868,11 @@ end
--- @param view ModuleView --- @param view ModuleView
--- @return nil --- @return nil
local function render_section_forward(add, view) local function render_section_forward(add, view)
--- @type boolean local wrote = false ---@type boolean
local wrote = false for _, a in ipairs(view.decls) do ---@type integer, AtomEntry
--- @type integer, AtomEntry local gpr = a.paths and a.paths.forward_state and a.paths.forward_state.gpr_values ---@type table<string, GprLatticeSlot>|nil
for _, a in ipairs(view.decls) do local keys = {} ---@type string[]
--- @type table<string, GprLatticeSlot>|nil for k in pairs(gpr or {}) do ---@type string
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
if k == "R_0" then if k == "R_0" then
-- hidden -- hidden
elseif HIDDEN_UNLESS_WRITTEN[k] and not encoder_wrote_key(a, k) then 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("| written | aliases | physical | lattice | last relation |")
add("|---|---|---|---|---|") add("|---|---|---|---|---|")
table.sort(keys) table.sort(keys)
--- @type integer, string for _, k in ipairs(keys) do ---@type integer, string
for _, k in ipairs(keys) do local slot = gpr[k] ---@type GprLatticeSlot|nil
--- @type GprLatticeSlot|nil local lattice = "" ---@type string
local slot = gpr[k]
--- @type string
local lattice = ""
if slot and slot.kind == "constant" then if slot and slot.kind == "constant" then
lattice = tostring(slot.value) lattice = tostring(slot.value)
end end
@@ -1061,8 +906,7 @@ local function render_section_forward(add, view)
if not wrote then add("_(none)_"); add("") end if not wrote then add("_(none)_"); add("") end
end end
--- @type SectionRenderer[] local SECTION_RENDERERS = { ---@type SectionRenderer[]
local SECTION_RENDERERS = {
{ header = "## Declarations", render = render_section_declarations }, { header = "## Declarations", render = render_section_declarations },
{ header = "## Components", render = render_section_components }, { header = "## Components", render = render_section_components },
{ header = "## RegUse schemas", render = render_section_reguse }, { header = "## RegUse schemas", render = render_section_reguse },
@@ -1083,10 +927,8 @@ local SECTION_RENDERERS = {
--- @param view ModuleView --- @param view ModuleView
--- @return string --- @return string
local function render_module_meta_report(view) local function render_module_meta_report(view)
--- @type string local dir_basename = source_basename(view.dir) ---@type string
local dir_basename = source_basename(view.dir) local lines = { ---@type string[]
--- @type string[]
local lines = {
"# " .. dir_basename .. " — atom meta report", "# " .. dir_basename .. " — atom meta report",
"> Auto-generated by ps1_meta.lua (passes/report.lua). Do not edit.", "> 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 --- @return nil
local function add(s) lines[#lines + 1] = s end local function add(s) lines[#lines + 1] = s end
--- @type KindCounts local kinds = count_kinds(view.decls) ---@type KindCounts
local kinds = count_kinds(view.decls) local n_annot, n_binds, n_macros = 0, 0, 0 ---@type integer, integer, integer
--- @type integer, integer, integer for _, src in ipairs(view.sources) do ---@type integer, SourceFile
local n_annot, n_binds, n_macros = 0, 0, 0
--- @type integer, SourceFile
for _, src in ipairs(view.sources) do
n_annot = n_annot + #((src.scan and src.scan.atom_infos) or {}) n_annot = n_annot + #((src.scan and src.scan.atom_infos) or {})
n_binds = n_binds + #((src.scan and src.scan.binds) or {}) n_binds = n_binds + #((src.scan and src.scan.binds) or {})
n_macros = n_macros + #((src.scan and src.scan.macros) or {}) n_macros = n_macros + #((src.scan and src.scan.macros) or {})
end end
--- @type integer, integer, integer local n_err, n_warn, n_info = 0, 0, 0 ---@type integer, integer, integer
local n_err, n_warn, n_info = 0, 0, 0 for _, f in ipairs(view.findings or {}) do ---@type integer, CheckFinding
--- @type integer, CheckFinding
for _, f in ipairs(view.findings or {}) do
if f.kind == "error" then n_err = n_err + 1 if f.kind == "error" then n_err = n_err + 1
elseif f.kind == "warning" then n_warn = n_warn + 1 elseif f.kind == "warning" then n_warn = n_warn + 1
else n_info = n_info + 1 else n_info = n_info + 1
@@ -1128,12 +965,10 @@ local function render_module_meta_report(view)
add("") add("")
add("## Sources"); add("") add("## Sources"); add("")
--- @type integer, SourceFile for _, s in ipairs(view.sources) do add("- `" .. s.path .. "`") end ---@type integer, SourceFile
for _, s in ipairs(view.sources) do add("- `" .. s.path .. "`") end
add("") add("")
--- @type integer, SectionRenderer for _, row in ipairs(SECTION_RENDERERS) do ---@type integer, SectionRenderer
for _, row in ipairs(SECTION_RENDERERS) do
add(row.header); add("") add(row.header); add("")
row.render(add, view) row.render(add, view)
end end
@@ -1147,8 +982,7 @@ end
-- `once = true` means render once at the project level (not per-module). -- `once = true` means render once at the project level (not per-module).
-- `basename(dir_basename)` yields the file's basename for that kind. -- `basename(dir_basename)` yields the file's basename for that kind.
-- `gather(ctx, dir, dir_sources [, all_modules])` returns the rendered string. -- `gather(ctx, dir, dir_sources [, all_modules])` returns the rendered string.
--- @type ReportRenderer[] local REPORT_RENDERERS = { ---@type ReportRenderer[]
local REPORT_RENDERERS = {
{ {
name = "atom_meta_report", name = "atom_meta_report",
ext = "md", ext = "md",
@@ -1161,8 +995,7 @@ local REPORT_RENDERERS = {
--- @param dir_sources SourceFile[] --- @param dir_sources SourceFile[]
--- @return string --- @return string
gather = function(ctx, dir, dir_sources) gather = function(ctx, dir, dir_sources)
--- @type Corpus local corpus = ctx.shared.corpus ---@type Corpus
local corpus = ctx.shared.corpus
return render_module_meta_report(build_module_view(dir, dir_sources, corpus)) return render_module_meta_report(build_module_view(dir, dir_sources, corpus))
end, end,
}, },
@@ -1204,20 +1037,16 @@ local REPORT_RENDERERS = {
-- M — public pass surface -- M — public pass surface
-- ════════════════════════════════════════════════════════════════════════════ -- ════════════════════════════════════════════════════════════════════════════
--- @type ReportPass local M = {} ---@type ReportPass
local M = {}
--- 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). --- 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(). --- Reads `corpus.static_analysis_results` (added in Phase 1) to populate per-module findings without re-running validate().
--- @param ctx PassCtx --- @param ctx PassCtx
--- @return PassResult --- @return PassResult
function M.run(ctx) function M.run(ctx)
--- @type PassOutputEntry[] local outputs = {} ---@type PassOutputEntry[]
local outputs = {} local corpus = ctx.shared and ctx.shared.corpus ---@type Corpus|nil
--- @type Corpus|nil local by_dir = (corpus and corpus.sources_by_dir) or {} ---@type table<string, SourceFile[]>
local corpus = ctx.shared and ctx.shared.corpus
--- @type table<string, SourceFile[]>
local by_dir = (corpus and corpus.sources_by_dir) or {}
-- `out_path_root`: when the conventional `out_root` is `build/gen` (any spelling — relative, absolute, separator variants). -- `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/`. -- 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 return type(p) == "string" and (p:match("[/\\]gen[/\\]?$") ~= nil
or p == "build/gen" or p == "build\\gen") or p == "build/gen" or p == "build\\gen")
end end
--- @type string local out_root_effective = ends_with_gen(ctx.out_root) ---@type string
local out_root_effective = ends_with_gen(ctx.out_root)
and ctx.out_root:gsub("[/\\]gen[/\\]?$", "") and ctx.out_root:gsub("[/\\]gen[/\\]?$", "")
or ctx.out_root or ctx.out_root
duffle.ensure_dir(out_root_effective) duffle.ensure_dir(out_root_effective)
-- Aggregator for the project-wide `once = true` summary renderer. -- Aggregator for the project-wide `once = true` summary renderer.
--- @type ProjectSummaryRow[] local all_modules = {} ---@type ProjectSummaryRow[]
local all_modules = {}
--- @type string, SourceFile[] for dir, dir_sources in pairs(by_dir) do ---@type string, SourceFile[]
for dir, dir_sources in pairs(by_dir) do local dir_basename = dir:match("([^/\\]+)$") or dir ---@type string
--- @type string
local dir_basename = dir:match("([^/\\]+)$") or dir
-- Per-renderer dispatch for the per-module renderers (once = false). -- Per-renderer dispatch for the per-module renderers (once = false).
--- @type integer, ReportRenderer for _, renderer in ipairs(REPORT_RENDERERS) do ---@type integer, ReportRenderer
for _, renderer in ipairs(REPORT_RENDERERS) do
if not renderer.once then if not renderer.once then
--- @type string local body = renderer.gather(ctx, dir, dir_sources) ---@type string
local body = renderer.gather(ctx, dir, dir_sources) local out_path = out_root_effective .. "/" .. renderer.basename(dir_basename) .. "." .. renderer.ext ---@type string
--- @type string
local out_path = out_root_effective .. "/" .. renderer.basename(dir_basename) .. "." .. renderer.ext
duffle.write_file(out_path, body) duffle.write_file(out_path, body)
outputs[#outputs + 1] = { kind = renderer.name, path = out_path } outputs[#outputs + 1] = { kind = renderer.name, path = out_path }
end end
end end
--- @type ModuleView local view = build_module_view(dir, dir_sources, corpus) ---@type ModuleView
local view = build_module_view(dir, dir_sources, corpus) local n_annot, n_binds, n_macros = 0, 0, 0 ---@type integer, integer, integer
--- @type integer, integer, integer for _, src in ipairs(dir_sources) do ---@type integer, SourceFile
local n_annot, n_binds, n_macros = 0, 0, 0
--- @type integer, SourceFile
for _, src in ipairs(dir_sources) do
n_annot = n_annot + #((src.scan and src.scan.atom_infos) or {}) n_annot = n_annot + #((src.scan and src.scan.atom_infos) or {})
n_binds = n_binds + #((src.scan and src.scan.binds) or {}) n_binds = n_binds + #((src.scan and src.scan.binds) or {})
n_macros = n_macros + #((src.scan and src.scan.macros) or {}) n_macros = n_macros + #((src.scan and src.scan.macros) or {})
end end
--- @type integer, integer, integer local n_err, n_warn, n_info = 0, 0, 0 ---@type integer, integer, integer
local n_err, n_warn, n_info = 0, 0, 0 for _, f in ipairs(view.findings or {}) do ---@type integer, CheckFinding
--- @type integer, CheckFinding
for _, f in ipairs(view.findings or {}) do
if f.kind == "error" then n_err = n_err + 1 if f.kind == "error" then n_err = n_err + 1
elseif f.kind == "warning" then n_warn = n_warn + 1 elseif f.kind == "warning" then n_warn = n_warn + 1
else n_info = n_info + 1 else n_info = n_info + 1
@@ -1290,13 +1107,10 @@ function M.run(ctx)
end end
-- Project-wide renderer (once = true): write the summary file. -- Project-wide renderer (once = true): write the summary file.
--- @type integer, ReportRenderer for _, renderer in ipairs(REPORT_RENDERERS) do ---@type integer, ReportRenderer
for _, renderer in ipairs(REPORT_RENDERERS) do
if renderer.once then if renderer.once then
--- @type string local body = renderer.gather(ctx, nil, nil, all_modules) ---@type string
local body = renderer.gather(ctx, nil, nil, all_modules) local out_path = out_root_effective .. "/" .. renderer.basename("") .. "." .. renderer.ext ---@type string
--- @type string
local out_path = out_root_effective .. "/" .. renderer.basename("") .. "." .. renderer.ext
duffle.write_file(out_path, body) duffle.write_file(out_path, body)
outputs[#outputs + 1] = { kind = renderer.name, path = out_path } outputs[#outputs + 1] = { kind = renderer.name, path = out_path }
end 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). -- 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. -- 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. -- 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 string
local _bootstrap_dir = debug.getinfo(1, "S").source:match("^@?(.*[/\\])") or "./" local duffle = dofile(_bootstrap_dir .. "../duffle_paths.lua") ---@type DuffleExport
--- @type DuffleExport
local duffle = dofile(_bootstrap_dir .. "../duffle_paths.lua")
-- ════════════════════════════════════════════════════════════════════════════ -- ════════════════════════════════════════════════════════════════════════════
-- Type declarations -- Type declarations
@@ -46,8 +44,7 @@ local duffle = dofile(_bootstrap_dir .. "../duffle_paths.lua")
-- Module exports -- Module exports
-- ════════════════════════════════════════════════════════════════════════════ -- ════════════════════════════════════════════════════════════════════════════
--- @type WordCountEval local M = {} ---@type WordCountEval
local M = {}
-- ┌────────────────────────────────────────────────────────────────────┐ -- ┌────────────────────────────────────────────────────────────────────┐
-- │ Shared utility: count_token_words │ -- │ Shared utility: count_token_words │
@@ -61,15 +58,12 @@ local M = {}
--- @param wc WordCounts -- the shared word-count table --- @param wc WordCounts -- the shared word-count table
--- @return integer --- @return integer
function M.count_token_words(token, wc) function M.count_token_words(token, wc)
--- @type string local s = duffle.trim(token) ---@type string
local s = duffle.trim(token)
if s == "" then return 0 end if s == "" then return 0 end
--- @type string|nil, integer local name, after = duffle.read_ident(s, 1) ---@type string|nil, integer
local name, after = duffle.read_ident(s, 1)
if not name then return 1 end if not name then return 1 end
if wc[name] then return wc[name] end if wc[name] then return wc[name] end
--- @type integer local paren_pos = duffle.skip_ws_and_cmt(s, after) ---@type integer
local paren_pos = duffle.skip_ws_and_cmt(s, after)
if s:sub(paren_pos, paren_pos) == "(" then if s:sub(paren_pos, paren_pos) == "(" then
io.stderr:write(" warning: unknown macro '" .. name .. "', assuming 1 word\n") io.stderr:write(" warning: unknown macro '" .. name .. "', assuming 1 word\n")
end end
@@ -95,8 +89,7 @@ end
--- @return PassResult --- @return PassResult
function M.run(ctx) function M.run(ctx)
-- 1. Canonical-corpus ownership gate. -- 1. Canonical-corpus ownership gate.
--- @type Corpus|nil local corpus = ctx.shared and ctx.shared.corpus ---@type Corpus|nil
local corpus = ctx.shared and ctx.shared.corpus
if type(corpus) ~= "table" then 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) error("word_count_eval.run requires ctx.shared.corpus (canonical corpus). The fixture must install the corpus before running this pass.", 0)
end end
@@ -108,8 +101,7 @@ function M.run(ctx)
-- 3. Load authored metadata. Generated .macs.h files are NOT scanned -- 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). -- (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) ---@type WordCounts
local wc = duffle.load_word_counts(ctx.metadata_path)
-- 4. Assign the count table. ONE assignment, no copy. The assignment creates no secondary alias. -- 4. Assign the count table. ONE assignment, no copy. The assignment creates no secondary alias.
corpus.word_counts = wc 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). -- 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")`. -- 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. -- 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 boolean
local _is_entry_script = arg and arg[0] and arg[0]:match("ps1_meta%.lua$") ~= nil local _bootstrap_src ---@type string
--- @type string
local _bootstrap_src
if _is_entry_script then if _is_entry_script then
_bootstrap_src = arg[0] _bootstrap_src = arg[0]
else else
@@ -30,33 +28,26 @@ else
-- strip the leading "@" so the directory match works in both cases. -- strip the leading "@" so the directory match works in both cases.
_bootstrap_src = debug.getinfo(1, "S").source:sub(2) _bootstrap_src = debug.getinfo(1, "S").source:sub(2)
end end
--- @type DuffleExport local duffle = dofile((_bootstrap_src:match("(.*[/\\])") or "./") .. "duffle_paths.lua") ---@type DuffleExport
local duffle = dofile((_bootstrap_src:match("(.*[/\\])") or "./") .. "duffle_paths.lua")
-- ════════════════════════════════════════════════════════════════════════════ -- ════════════════════════════════════════════════════════════════════════════
-- Constants -- Constants
-- ════════════════════════════════════════════════════════════════════════════ -- ════════════════════════════════════════════════════════════════════════════
-- Exit codes (per the --help text and the post-build summary convention). -- Exit codes (per the --help text and the post-build summary convention).
--- @type integer local EXIT_OK = 0 ---@type integer
local EXIT_OK = 0 local EXIT_VALIDATION_ERRORS = 1 ---@type integer
--- @type integer local EXIT_INTERNAL_ERROR = 2 ---@type integer
local EXIT_VALIDATION_ERRORS = 1
--- @type integer
local EXIT_INTERNAL_ERROR = 2
-- Default --out-root value if not provided. -- Default --out-root value if not provided.
--- @type string local DEFAULT_OUT_ROOT = "build/gen" ---@type string
local DEFAULT_OUT_ROOT = "build/gen"
-- Sentinel for "all passes" in `PASS_FLAG_TO_NAME`. Distinguishes `--all` from the per-pass flags (which map to individual pass names). -- 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__" ---@type string
local ALL_PASSES_SENTINEL = "__all__"
-- Sentinel key for the pass-flag dispatcher in `FLAG_HANDLERS`. -- 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. -- 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__" ---@type string
local PASS_FLAG_DISPATCH_KEY = "__pass__"
-- ════════════════════════════════════════════════════════════════════════════ -- ════════════════════════════════════════════════════════════════════════════
-- Type declarations -- 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, -- 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). -- but it remains directly requestable through its explicit CLI flag (e.g. --atoms-source-map, --scan-source).
--- @type table<string, PassDescriptor> local PASSES = { ---@type table<string, PassDescriptor>
local PASSES = {
["scan-source"] = { ["scan-source"] = {
module = "passes.scan_source", module = "passes.scan_source",
kind = "shared", deps = {}, kind = "shared", deps = {},
@@ -230,13 +220,10 @@ local PASSES = {
--- @param group_name string -- Build-phase group ("pre-link" | "post-link") --- @param group_name string -- Build-phase group ("pre-link" | "post-link")
--- @return string[] -- Sorted root pass names belonging to that group --- @return string[] -- Sorted root pass names belonging to that group
local function roots_for_group(group_name) local function roots_for_group(group_name)
--- @type string[] local names = {} ---@type string[]
local names = {} for name, pass in pairs(PASSES) do ---@type string, PassDescriptor
--- @type string, PassDescriptor
for name, pass in pairs(PASSES) do
if pass.groups then if pass.groups then
--- @type integer, string for _, g in ipairs(pass.groups) do ---@type integer, string
for _, g in ipairs(pass.groups) do
if g == group_name then if g == group_name then
names[#names + 1] = name names[#names + 1] = name
break break
@@ -255,14 +242,12 @@ end
--- @param group_name string --- @param group_name string
--- @return nil --- @return nil
local function request_roots_for_group(args, group_name) local function request_roots_for_group(args, group_name)
--- @type string[] local roots = roots_for_group(group_name) ---@type string[]
local roots = roots_for_group(group_name)
if #roots == 0 then 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" 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)) , group_name, group_name))
end end
--- @type integer, string for _, name in ipairs(roots) do ---@type integer, string
for _, name in ipairs(roots) do
args.requested_set[#args.requested_set + 1] = name args.requested_set[#args.requested_set + 1] = name
end end
end end
@@ -270,8 +255,7 @@ end
-- Pass-kind taxonomy: findings always print. No pass kind stops the build. -- Pass-kind taxonomy: findings always print. No pass kind stops the build.
-- Report severity is independent from process exit policy. -- 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". -- 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 = { ---@type table<string, boolean> -- bag: pass kind -> stop-on-error
local PASS_KIND_STOP_ON_ERROR = {
["shared"] = false, ["shared"] = false,
["header-output"] = false, ["header-output"] = false,
["validation"] = 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. -- 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 -- 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. -- (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 = { ---@type table<string, string> -- bag: CLI flag -> pass name or ALL_PASSES_SENTINEL
local PASS_FLAG_TO_NAME = {
["--word-counts"] = "word-counts", ["--word-counts"] = "word-counts",
["--components"] = "components", ["--components"] = "components",
["--validate"] = "annotation", ["--validate"] = "annotation",
@@ -301,21 +284,17 @@ local PASS_FLAG_TO_NAME = {
--- @param args ParsedArgs --- @param args ParsedArgs
--- @return nil --- @return nil
local function request_all_passes(args) local function request_all_passes(args)
--- @type string[] local names = {} ---@type string[]
local names = {} for name in pairs(PASSES) do names[#names + 1] = name end ---@type string
--- @type string
for name in pairs(PASSES) do names[#names + 1] = name end
table.sort(names) table.sort(names)
--- @type integer, string for _, n in ipairs(names) do ---@type integer, string
for _, n in ipairs(names) do
args.requested_set[#args.requested_set + 1] = n args.requested_set[#args.requested_set + 1] = n
end end
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). -- 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). -- Returning nil + os.exit() handles termination flags (--help).
--- @type table<string, FlagHandler> local FLAG_HANDLERS = {} ---@type table<string, FlagHandler>
local FLAG_HANDLERS = {}
-- ════════════════════════════════════════════════════════════════════════════ -- ════════════════════════════════════════════════════════════════════════════
-- CLI parsing -- CLI parsing
@@ -372,8 +351,7 @@ EXAMPLES:
]]) ]])
end end
--- @type table<string, string> -- bag: flag -> value metavar local FLAG_VALUE_NAMES = { ---@type table<string, string> -- bag: flag -> value metavar
local FLAG_VALUE_NAMES = {
["--source"] = "FILE", ["--source"] = "FILE",
["--unity-root"] = "FILE", ["--unity-root"] = "FILE",
["--metadata"] = "PATH", ["--metadata"] = "PATH",
@@ -388,10 +366,8 @@ local FLAG_VALUE_NAMES = {
--- @return string --- @return string
--- @return integer --- @return integer
local function require_flag_value(argv, arg_idx, flag) local function require_flag_value(argv, arg_idx, flag)
--- @type string|nil local value = argv[arg_idx + 1] ---@type string|nil
local value = argv[arg_idx + 1] local next_known = type(value) == "string" ---@type boolean
--- @type boolean
local next_known = type(value) == "string"
and (FLAG_HANDLERS[value] ~= nil or PASS_FLAG_TO_NAME[value] ~= nil) and (FLAG_HANDLERS[value] ~= nil or PASS_FLAG_TO_NAME[value] ~= nil)
if value == nil or next_known then if value == nil or next_known then
io.stderr:write("ps1_meta: " .. flag .. " requires " .. FLAG_VALUE_NAMES[flag] .. "\n") 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 --- @param arg_idx integer
--- @return integer --- @return integer
FLAG_HANDLERS["--source"] = function(args, argv, arg_idx) FLAG_HANDLERS["--source"] = function(args, argv, arg_idx)
--- @type string, integer local value, value_idx = require_flag_value(argv, arg_idx, "--source") ---@type string, integer
local value, value_idx = require_flag_value(argv, arg_idx, "--source")
args.sources[#args.sources + 1] = value args.sources[#args.sources + 1] = value
return value_idx return value_idx
end end
@@ -427,8 +402,7 @@ end
--- @param arg_idx integer --- @param arg_idx integer
--- @return integer --- @return integer
FLAG_HANDLERS["--unity-root"] = function(args, argv, arg_idx) FLAG_HANDLERS["--unity-root"] = function(args, argv, arg_idx)
--- @type string, integer local value, value_idx = require_flag_value(argv, arg_idx, "--unity-root") ---@type string, integer
local value, value_idx = require_flag_value(argv, arg_idx, "--unity-root")
args.unity_root = value args.unity_root = value
return value_idx return value_idx
end end
@@ -437,8 +411,7 @@ end
--- @param arg_idx integer --- @param arg_idx integer
--- @return integer --- @return integer
FLAG_HANDLERS["--metadata"] = function(args, argv, arg_idx) FLAG_HANDLERS["--metadata"] = function(args, argv, arg_idx)
--- @type string, integer local value, value_idx = require_flag_value(argv, arg_idx, "--metadata") ---@type string, integer
local value, value_idx = require_flag_value(argv, arg_idx, "--metadata")
args.metadata = value args.metadata = value
return value_idx return value_idx
end end
@@ -447,8 +420,7 @@ end
--- @param arg_idx integer --- @param arg_idx integer
--- @return integer --- @return integer
FLAG_HANDLERS["--out-root"] = function(args, argv, arg_idx) FLAG_HANDLERS["--out-root"] = function(args, argv, arg_idx)
--- @type string, integer local value, value_idx = require_flag_value(argv, arg_idx, "--out-root") ---@type string, integer
local value, value_idx = require_flag_value(argv, arg_idx, "--out-root")
args.out_root = value args.out_root = value
return value_idx return value_idx
end end
@@ -457,8 +429,7 @@ end
--- @param arg_idx integer --- @param arg_idx integer
--- @return integer --- @return integer
FLAG_HANDLERS["--project-root"] = function(args, argv, arg_idx) FLAG_HANDLERS["--project-root"] = function(args, argv, arg_idx)
--- @type string, integer local value, value_idx = require_flag_value(argv, arg_idx, "--project-root") ---@type string, integer
local value, value_idx = require_flag_value(argv, arg_idx, "--project-root")
args.project_root = value args.project_root = value
return value_idx return value_idx
end end
@@ -476,8 +447,7 @@ end
--- @param arg_idx integer --- @param arg_idx integer
--- @return integer --- @return integer
FLAG_HANDLERS["--elf"] = function(args, argv, arg_idx) FLAG_HANDLERS["--elf"] = function(args, argv, arg_idx)
--- @type string, integer local value, value_idx = require_flag_value(argv, arg_idx, "--elf") ---@type string, integer
local value, value_idx = require_flag_value(argv, arg_idx, "--elf")
args.flags = args.flags or {} args.flags = args.flags or {}
args.flags.elf_path = value args.flags.elf_path = value
return value_idx return value_idx
@@ -517,8 +487,7 @@ end
--- @param a string --- @param a string
--- @return nil --- @return nil
FLAG_HANDLERS[PASS_FLAG_DISPATCH_KEY] = function(args, a) FLAG_HANDLERS[PASS_FLAG_DISPATCH_KEY] = function(args, a)
--- @type string|nil local name = PASS_FLAG_TO_NAME[a] ---@type string|nil
local name = PASS_FLAG_TO_NAME[a]
if name == ALL_PASSES_SENTINEL then if name == ALL_PASSES_SENTINEL then
request_all_passes(args) request_all_passes(args)
return return
@@ -530,8 +499,7 @@ end
--- @param argv string[] --- @param argv string[]
--- @return ParsedArgs --- @return ParsedArgs
local function parse_args(argv) local function parse_args(argv)
--- @type ParsedArgs local args = { ---@type ParsedArgs
local args = {
requested_set = {}, requested_set = {},
sources = {}, sources = {},
unity_root = nil, unity_root = nil,
@@ -541,13 +509,10 @@ local function parse_args(argv)
verbose = false, verbose = false,
} }
--- @type integer local pos = 1 ---@type integer
local pos = 1
while pos <= #argv do while pos <= #argv do
--- @type string local a = argv[pos] ---@type string
local a = argv[pos] local handler = FLAG_HANDLERS[a] ---@type FlagHandler|nil
--- @type FlagHandler|nil
local handler = FLAG_HANDLERS[a]
if handler then if handler then
pos = handler(args, argv, pos) or pos pos = handler(args, argv, pos) or pos
elseif PASS_FLAG_TO_NAME[a] then 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. -- `<repo>/code/duffle/word_count.metadata.h` is the canonical metadata location.
-- `project_root` names `<repo>`; the resolver derives `<project_root>/code` separately. -- `project_root` names `<repo>`; the resolver derives `<project_root>/code` separately.
if not args.project_root then if not args.project_root then
--- @type string local metadata_dir = duffle.dirname(duffle.normalize_path(args.metadata)) ---@type string
local metadata_dir = duffle.dirname(duffle.normalize_path(args.metadata)) local code_root = duffle.dirname(metadata_dir) ---@type string
--- @type string
local code_root = duffle.dirname(metadata_dir)
args.project_root = duffle.dirname(code_root) args.project_root = duffle.dirname(code_root)
else else
args.project_root = duffle.normalize_path(args.project_root) args.project_root = duffle.normalize_path(args.project_root)
end end
--- @type boolean local has_unity = type(args.unity_root) == "string" and args.unity_root ~= "" ---@type boolean
local has_unity = type(args.unity_root) == "string" and args.unity_root ~= ""
if has_unity and #args.sources > 0 then if has_unity and #args.sources > 0 then
io.stderr:write("ps1_meta: --unity-root FILE and --source FILE are mutually exclusive\n") io.stderr:write("ps1_meta: --unity-root FILE and --source FILE are mutually exclusive\n")
os.exit(EXIT_INTERNAL_ERROR) 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. -- 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. -- 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. -- This covers the explicit --post-link batch, --dwarf-injection by itself, and --gdb-runtime by itself.
--- @type PassFlags local flags = args.flags or {} ---@type PassFlags
local flags = args.flags or {} local elf_path = flags.elf_path ---@type string|nil
--- @type string|nil local has_elf = type(elf_path) == "string" and #elf_path > 0 ---@type boolean
local elf_path = flags.elf_path local post_links = flags.gdb_runtime or flags.dwarf_injection ---@type boolean
--- @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
if post_links and not has_elf then if post_links and not has_elf then
io.stderr:write("ps1_meta: --elf PATH is required for post-link output\n") io.stderr:write("ps1_meta: --elf PATH is required for post-link output\n")
os.exit(EXIT_INTERNAL_ERROR) os.exit(EXIT_INTERNAL_ERROR)
@@ -621,12 +579,9 @@ end
--- @param args ParsedArgs --- @param args ParsedArgs
--- @return PassCtx --- @return PassCtx
local function build_ctx(args) local function build_ctx(args)
--- @type string local normalized_project_root = duffle.normalize_path(args.project_root) ---@type string
local normalized_project_root = duffle.normalize_path(args.project_root) local project_root = normalized_project_root ---@type string
--- @type string local project_root_is_absolute = normalized_project_root:match("^%a:/") ---@type boolean
local project_root = normalized_project_root
--- @type boolean
local project_root_is_absolute = normalized_project_root:match("^%a:/")
or normalized_project_root:sub(1, 2) == "//" or normalized_project_root:sub(1, 2) == "//"
or normalized_project_root:sub(1, 1) == "/" or normalized_project_root:sub(1, 1) == "/"
if not project_root_is_absolute then 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. -- Do not route POSIX/UNC/drive-absolute paths through to_absolute_path.
duffle.canonical_path_key(project_root) duffle.canonical_path_key(project_root)
end end
--- @type Corpus local resolution ---@type Corpus
local resolution
if args.unity_root then if args.unity_root then
--- @type boolean, Corpus|string local ok_resolve, resolved = pcall(duffle.resolve_source_corpus, { ---@type boolean, Corpus|string
local ok_resolve, resolved = pcall(duffle.resolve_source_corpus, {
unity_root = args.unity_root, unity_root = args.unity_root,
project_root = project_root, project_root = project_root,
}) })
@@ -651,45 +604,35 @@ local function build_ctx(args)
end end
resolution = resolved resolution = resolved
else else
--- @type SourceFile[] local source_order = {} ---@type SourceFile[]
local source_order = {} local sources_by_path = {} ---@type table<Path, SourceFile>
--- @type table<Path, SourceFile> local resolver = { ---@type SourceResolver
local sources_by_path = {}
--- @type SourceResolver
local resolver = {
resolved = {}, resolved = {},
skipped = {}, skipped = {},
shadowed = {}, shadowed = {},
} }
--- @type integer, string for _, input_path in ipairs(args.sources) do ---@type integer, string
for _, input_path in ipairs(args.sources) do local path = duffle.normalize_path(input_path) ---@type string
--- @type string local key_ok, key_or_error = pcall(duffle.canonical_path_key, path) ---@type boolean, string
local path = duffle.normalize_path(input_path)
--- @type boolean, string
local key_ok, key_or_error = pcall(duffle.canonical_path_key, path)
if not key_ok then if not key_ok then
error("ps1_meta: invalid --source " .. input_path .. ": " .. tostring(key_or_error), 0) error("ps1_meta: invalid --source " .. input_path .. ": " .. tostring(key_or_error), 0)
end end
--- @type file*|nil local file = io.open(path, "r") ---@type file*|nil
local file = io.open(path, "r")
if not file then if not file then
io.stderr:write("ps1_meta: cannot open --source " .. input_path .. "\n") io.stderr:write("ps1_meta: cannot open --source " .. input_path .. "\n")
os.exit(EXIT_INTERNAL_ERROR) os.exit(EXIT_INTERNAL_ERROR)
end end
--- @type string local text = file:read("*a") ---@type string
local text = file:read("*a")
file:close() file:close()
--- @type SourceFile local source = { ---@type SourceFile
local source = {
path = path, path = path,
text = text, text = text,
dir = duffle.dirname(path), dir = duffle.dirname(path),
basename = duffle.basename_no_ext(path), basename = duffle.basename_no_ext(path),
} }
source_order[#source_order + 1] = source source_order[#source_order + 1] = source
--- @type string local key = key_or_error ---@type string
local key = key_or_error
if not sources_by_path[key] then sources_by_path[key] = source end if not sources_by_path[key] then sources_by_path[key] = source end
resolver.resolved[#resolver.resolved + 1] = { resolver.resolved[#resolver.resolved + 1] = {
include_path = path, include_path = path,
@@ -713,8 +656,7 @@ local function build_ctx(args)
} }
end end
--- @type Corpus local corpus = { ---@type Corpus
local corpus = {
unity_root = resolution.unity_root, unity_root = resolution.unity_root,
project_root = resolution.project_root, project_root = resolution.project_root,
code_root = resolution.code_root, code_root = resolution.code_root,
@@ -735,8 +677,7 @@ local function build_ctx(args)
collisions = {}, collisions = {},
resolver = resolution.resolver, resolver = resolution.resolver,
} }
--- @type PassCtx local ctx = { ---@type PassCtx
local ctx = {
metadata_path = args.metadata, metadata_path = args.metadata,
shared = { corpus = corpus }, shared = { corpus = corpus },
out_root = args.out_root, out_root = args.out_root,
@@ -765,21 +706,15 @@ end
--- Keeping these blocks local makes the topological sort self-contained. --- Keeping these blocks local makes the topological sort self-contained.
local function topo_sort(passes, requested_set) local function topo_sort(passes, requested_set)
-- Dependency closure: include every pass transitively required by `requested_set`. -- Dependency closure: include every pass transitively required by `requested_set`.
--- @type table<string, boolean> -- bag: pass name -> needed local needed = {} ---@type table<string, boolean> -- bag: pass name -> needed
local needed = {} for _, name in ipairs(requested_set) do needed[name] = true end ---@type integer, string
--- @type integer, string local changed = true ---@type boolean
for _, name in ipairs(requested_set) do needed[name] = true end
--- @type boolean
local changed = true
while changed do while changed do
changed = false changed = false
--- @type string, boolean for name, _ in pairs(needed) do ---@type string, boolean
for name, _ in pairs(needed) do local pass = passes[name] ---@type PassDescriptor
--- @type PassDescriptor
local pass = passes[name]
if not pass then error("unknown pass '" .. name .. "' requested") end if not pass then error("unknown pass '" .. name .. "' requested") end
--- @type integer, string for _, dep in ipairs(pass.deps) do ---@type integer, string
for _, dep in ipairs(pass.deps) do
if not needed[dep] then if not needed[dep] then
needed[dep] = true needed[dep] = true
changed = true changed = true
@@ -789,14 +724,10 @@ local function topo_sort(passes, requested_set)
end end
-- In-degree calculation: count each needed pass's needed dependencies. -- In-degree calculation: count each needed pass's needed dependencies.
--- @type table<string, integer> -- bag: pass name -> in-degree local in_degree = {} ---@type table<string, integer> -- bag: pass name -> in-degree
local in_degree = {} for name, _ in pairs(needed) do in_degree[name] = 0 end ---@type string, boolean
--- @type string, boolean for name, _ in pairs(needed) do ---@type string, boolean
for name, _ in pairs(needed) do in_degree[name] = 0 end for _, dep in ipairs(passes[name].deps) do ---@type integer, string
--- @type string, boolean
for name, _ in pairs(needed) do
--- @type integer, string
for _, dep in ipairs(passes[name].deps) do
if needed[dep] then if needed[dep] then
in_degree[name] = in_degree[name] + 1 in_degree[name] = in_degree[name] + 1
end end
@@ -804,27 +735,21 @@ local function topo_sort(passes, requested_set)
end end
-- Ready-queue seeding: add zero-in-degree passes in deterministic order. -- Ready-queue seeding: add zero-in-degree passes in deterministic order.
--- @type string[] local ready = {} ---@type string[]
local ready = {} for name, deg in pairs(in_degree) do ---@type string, integer
--- @type string, integer
for name, deg in pairs(in_degree) do
if deg == 0 then ready[#ready + 1] = name end if deg == 0 then ready[#ready + 1] = name end
end end
table.sort(ready) table.sort(ready)
-- Ready-queue drain: decrement dependents when each pass is emitted. -- Ready-queue drain: decrement dependents when each pass is emitted.
-- Newly-zero-degree passes are inserted back into the ready queue (kept sorted). -- Newly-zero-degree passes are inserted back into the ready queue (kept sorted).
--- @type string[] local order = {} ---@type string[]
local order = {}
while #ready > 0 do while #ready > 0 do
--- @type string local just_finished = table.remove(ready, 1) ---@type string
local just_finished = table.remove(ready, 1)
order[#order + 1] = just_finished order[#order + 1] = just_finished
--- @type string, boolean for name, _ in pairs(needed) do ---@type string, boolean
for name, _ in pairs(needed) do
if name ~= just_finished then if name ~= just_finished then
--- @type integer, string for _, dep in ipairs(passes[name].deps) do ---@type integer, string
for _, dep in ipairs(passes[name].deps) do
if dep == just_finished then if dep == just_finished then
in_degree[name] = in_degree[name] - 1 in_degree[name] = in_degree[name] - 1
if in_degree[name] == 0 then 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 -- 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). -- (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. -- 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 integer
local needed_count = 0 for _ in pairs(needed) do needed_count = needed_count + 1 end ---@type string -- count hash entries; Lua's #t doesn't work
--- @type string
for _ in pairs(needed) do needed_count = needed_count + 1 end -- count hash entries; Lua's #t doesn't work
if #order ~= needed_count then if #order ~= needed_count then
--- @type string, integer for name, deg in pairs(in_degree) do ---@type string, integer
for name, deg in pairs(in_degree) do
if deg > 0 then if deg > 0 then
error("dependency cycle detected involving pass '" .. name .. "'") error("dependency cycle detected involving pass '" .. name .. "'")
end end
@@ -867,11 +789,9 @@ end
--- @param result PassResult --- @param result PassResult
--- @return boolean --- @return boolean
local function report_validation_errors(pass_name, pass, result) local function report_validation_errors(pass_name, pass, result)
--- @type boolean local has_errors = result.errors and #result.errors > 0 ---@type boolean
local has_errors = result.errors and #result.errors > 0
if not has_errors then return false end if not has_errors then return false end
--- @type integer, PassFinding for _, e in ipairs(result.errors) do ---@type integer, PassFinding
for _, e in ipairs(result.errors) do
io.stderr:write(string.format("[%s] line %d: %s\n", pass_name, e.line or 0, e.msg or "")) io.stderr:write(string.format("[%s] line %d: %s\n", pass_name, e.line or 0, e.msg or ""))
end end
return PASS_KIND_STOP_ON_ERROR[pass.kind] == true return PASS_KIND_STOP_ON_ERROR[pass.kind] == true
@@ -882,16 +802,11 @@ end
--- @param order string[] --- @param order string[]
--- @return boolean -- true if any validation errors were reported --- @return boolean -- true if any validation errors were reported
local function dispatch_passes(ctx, order) local function dispatch_passes(ctx, order)
--- @type boolean local had_errors = false ---@type boolean
local had_errors = false for _, pass_name in ipairs(order) do ---@type integer, string
--- @type integer, string local pass = PASSES[pass_name] ---@type PassDescriptor
for _, pass_name in ipairs(order) do local mod = require(pass.module) ---@type PassModule
--- @type PassDescriptor local result = mod.run(ctx) ---@type PassResult
local pass = PASSES[pass_name]
--- @type PassModule
local mod = require(pass.module)
--- @type PassResult
local result = mod.run(ctx)
if report_validation_errors(pass_name, pass, result) then if report_validation_errors(pass_name, pass, result) then
had_errors = true had_errors = true
end end
@@ -903,20 +818,14 @@ end
--- @param argv string[] --- @param argv string[]
--- @return nil --- @return nil
local function main(argv) local function main(argv)
--- @type boolean, string|nil local ok, err = pcall(function() ---@type boolean, string|nil
local ok, err = pcall(function() local args = parse_args(argv) ---@type ParsedArgs
--- @type ParsedArgs local ctx = build_ctx(args) ---@type PassCtx
local args = parse_args(argv)
--- @type PassCtx
local ctx = build_ctx(args)
--- @type string[] local requested = args.requested_set ---@type string[]
local requested = args.requested_set local closed = topo_sort(PASSES, requested) ---@type string[]
--- @type string[]
local closed = topo_sort(PASSES, requested)
--- @type boolean local had_errors = dispatch_passes(ctx, closed) ---@type boolean
local had_errors = dispatch_passes(ctx, closed)
if had_errors then os.exit(EXIT_VALIDATION_ERRORS) end if had_errors then os.exit(EXIT_VALIDATION_ERRORS) end
end) end)
@@ -931,8 +840,7 @@ end
-- Module export for in-process consumers (tests that dofile this script). -- 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"); -- 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. -- 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 = { ---@type Ps1MetaMod
local _M = {
PASSES = PASSES, PASSES = PASSES,
PASS_KIND_STOP_ON_ERROR = PASS_KIND_STOP_ON_ERROR, PASS_KIND_STOP_ON_ERROR = PASS_KIND_STOP_ON_ERROR,
parse_args = parse_args, parse_args = parse_args,