mirror of
https://github.com/Ed94/pikuma_ps1.git
synced 2026-08-25 02:20:33 +00:00
utilizing trailing type annotations more
This commit is contained in:
+8
-16
@@ -3,14 +3,10 @@
|
||||
--- @class DuffleExport
|
||||
--- bag: open module-export keys from duffle_scan / duffle_isa / duffle_emit
|
||||
|
||||
--- @type DuffleExport
|
||||
local scan = require("duffle_scan")
|
||||
--- @type DuffleExport
|
||||
local isa = require("duffle_isa")
|
||||
--- @type DuffleExport
|
||||
local emit = require("duffle_emit")
|
||||
--- @type DuffleExport
|
||||
local M = {}
|
||||
local scan = require("duffle_scan") ---@type DuffleExport
|
||||
local isa = require("duffle_isa") ---@type DuffleExport
|
||||
local emit = require("duffle_emit") ---@type DuffleExport
|
||||
local M = {} ---@type DuffleExport
|
||||
|
||||
--- @alias Path string
|
||||
--- @alias LineNum integer
|
||||
@@ -46,8 +42,7 @@ local M = {}
|
||||
--- @param label string
|
||||
--- @return nil
|
||||
local function merge(src, label)
|
||||
--- @type string, any
|
||||
for k, v in pairs(src) do
|
||||
for k, v in pairs(src) do ---@type string, any
|
||||
if M[k] ~= nil and M[k] ~= v then
|
||||
error("duffle facade name collision on " .. tostring(k) .. " from " .. label, 0)
|
||||
end
|
||||
@@ -62,8 +57,7 @@ merge(emit, "duffle_emit")
|
||||
--- @param ctx PassCtx
|
||||
--- @return CorpusView
|
||||
function M.corpus_view(ctx)
|
||||
--- @type Corpus
|
||||
local corpus = ctx and ctx.shared and ctx.shared.corpus
|
||||
local corpus = ctx and ctx.shared and ctx.shared.corpus ---@type Corpus
|
||||
if not corpus then error("requires ctx.shared.corpus", 0) end
|
||||
return {
|
||||
register_alias_registry = corpus.register_alias_registry or {},
|
||||
@@ -90,10 +84,8 @@ end
|
||||
--- @param findings CheckFinding[]
|
||||
--- @return nil
|
||||
function M.run_check_rules(rules, phase, item, pipe_ctx, findings)
|
||||
--- @type integer, CheckRule
|
||||
for _, rule in ipairs(rules) do
|
||||
--- @type (fun(item: AtomEntry|SourceFile, pipe_ctx: PipeCtx, findings: CheckFinding[]): nil)|nil
|
||||
local fn = rule[phase]
|
||||
for _, rule in ipairs(rules) do ---@type integer, CheckRule
|
||||
local fn = rule[phase] ---@type (fun(item: AtomEntry|SourceFile, pipe_ctx: PipeCtx, findings: CheckFinding[]): nil)|nil
|
||||
if fn then fn(item, pipe_ctx, findings) end
|
||||
end
|
||||
end
|
||||
|
||||
@@ -106,8 +106,7 @@
|
||||
--- @field HARDWARE_RELATIONS HardwareRelationRow[]
|
||||
--- @field CU2_TRANSITION_POLICY Cu2TransitionPolicy
|
||||
|
||||
--- @type DuffleIsa
|
||||
local M = {}
|
||||
local M = {} ---@type DuffleIsa
|
||||
|
||||
-- Section 7: domain tables
|
||||
-- ════════════════════════════════════════════════════════════════════════════
|
||||
@@ -422,11 +421,9 @@ function M.gte (ident) return M.GTE_COMMAND[M.gte_canon(ident)] end
|
||||
local function build_alias_map()
|
||||
--- @type table<string, string> -- bag: alias or canon -> canon
|
||||
M.ALIAS_TO_CANONICAL = {}
|
||||
--- @type string, GteCommandRow
|
||||
for canon, row in pairs(M.GTE_COMMAND) do
|
||||
for canon, row in pairs(M.GTE_COMMAND) do ---@type string, GteCommandRow
|
||||
M.ALIAS_TO_CANONICAL[canon] = canon
|
||||
--- @type integer, string
|
||||
for _, alias in ipairs(row.aliases or {}) do
|
||||
for _, alias in ipairs(row.aliases or {}) do ---@type integer, string
|
||||
M.ALIAS_TO_CANONICAL[alias] = canon
|
||||
end
|
||||
end
|
||||
|
||||
+10
-20
@@ -19,12 +19,10 @@
|
||||
--- @class DufflePaths
|
||||
--- @field setup fun(): nil
|
||||
|
||||
--- @type DufflePaths
|
||||
local M = {}
|
||||
local M = {} ---@type DufflePaths
|
||||
|
||||
-- Cache key for the repo root. Stored in `package.loaded` (process-global) so all 8 entry scripts + passes scripts share one resolution.
|
||||
--- @type string
|
||||
local CACHE_KEY = "__duffle_repo_root__"
|
||||
local CACHE_KEY = "__duffle_repo_root__" ---@type string
|
||||
|
||||
--- Resolve the repo root from this script's own path. Zero shell spawn.
|
||||
--- `duffle_paths.lua` always lives at `<repo>/scripts/duffle_paths.lua`, so the repo root is the parent of the directory containing this script.
|
||||
@@ -35,17 +33,14 @@ local CACHE_KEY = "__duffle_repo_root__"
|
||||
local function find_repo_root()
|
||||
if package.loaded[CACHE_KEY] then return package.loaded[CACHE_KEY] end
|
||||
|
||||
--- @type string
|
||||
local source = debug.getinfo(1, "S").source
|
||||
local source = debug.getinfo(1, "S").source ---@type string
|
||||
-- Strip the leading `@` (Lua's dofile marker) and the trailing `/duffle_paths.lua` filename.
|
||||
-- What remains is the directory containing this script, i.e. `<repo>/scripts/`.
|
||||
--- @type string|nil
|
||||
local scripts_dir = source and source:match("^@?(.*)[/\\]duffle_paths%.lua$")
|
||||
local scripts_dir = source and source:match("^@?(.*)[/\\]duffle_paths%.lua$") ---@type string|nil
|
||||
if not scripts_dir then return nil end
|
||||
|
||||
-- The repo root is the parent of `scripts/`. Strip the trailing `scripts/` (with or without trailing slash).
|
||||
--- @type string
|
||||
local root = scripts_dir:gsub("scripts[\\/]?$", "")
|
||||
local root = scripts_dir:gsub("scripts[\\/]?$", "") ---@type string
|
||||
root = root:gsub("\\", "/")
|
||||
if root == "" then root = "./" end
|
||||
if not root:match("/$") then root = root .. "/" end
|
||||
@@ -60,8 +55,7 @@ end
|
||||
--- lpeg is built by `update_deps.ps1` to `toolchain/lpeg/`, which we wire into `package.cpath` here (so `require("lpeg")` from `duffle.lua` resolves without any global state).
|
||||
--- @return nil
|
||||
function M.setup()
|
||||
--- @type string|nil
|
||||
local repo_root = find_repo_root()
|
||||
local repo_root = find_repo_root() ---@type string|nil
|
||||
if not repo_root then
|
||||
-- Unreachable in practice: find_repo_root() derives the repo root from this script's own source path via debug.getinfo(1, "S").source (no subprocess, no git CLI, <1ms).
|
||||
-- A nil return means the source path did not match the expected <repo>/scripts/duffle_paths.lua layout — a packaging bug, not a "missing git repo" condition.
|
||||
@@ -69,10 +63,8 @@ function M.setup()
|
||||
os.exit(2)
|
||||
end
|
||||
|
||||
--- @type string
|
||||
local scripts_dir = repo_root .. "scripts/"
|
||||
--- @type string
|
||||
local passes_dir = repo_root .. "scripts/passes/"
|
||||
local scripts_dir = repo_root .. "scripts/" ---@type string
|
||||
local passes_dir = repo_root .. "scripts/passes/" ---@type string
|
||||
package.path = scripts_dir .. "?.lua;"
|
||||
.. scripts_dir .. "?/init.lua;"
|
||||
.. passes_dir .. "?.lua;"
|
||||
@@ -82,10 +74,8 @@ function M.setup()
|
||||
-- lpeg: built by `update_deps.ps1` to `toolchain/lpeg/lpeg.dll`.
|
||||
-- lfs: compiled from pcsx-redux's vendored luafilesystem source to `toolchain/lfs/lfs.dll`.
|
||||
-- Wire both directories into cpath so `require("lpeg")` and `require("lfs")` resolve.
|
||||
--- @type string
|
||||
local lpeg_dir = repo_root .. "toolchain/lpeg/"
|
||||
--- @type string
|
||||
local lfs_dir = repo_root .. "toolchain/lfs/"
|
||||
local lpeg_dir = repo_root .. "toolchain/lpeg/" ---@type string
|
||||
local lfs_dir = repo_root .. "toolchain/lfs/" ---@type string
|
||||
package.cpath = lpeg_dir .. "?.dll;"
|
||||
.. lfs_dir .. "?.dll;"
|
||||
.. package.cpath
|
||||
|
||||
+184
-368
@@ -68,70 +68,44 @@
|
||||
--- @field skipped ResolverEvidence[]
|
||||
--- @field shadowed ResolverEvidence[]
|
||||
|
||||
--- @type DuffleScan
|
||||
local M = {}
|
||||
local M = {} ---@type DuffleScan
|
||||
|
||||
-- Required native extension: lfs (LuaFileSystem). Built by `update_deps.ps1` to `toolchain/lfs/lfs.dll` and wired into package.cpath by `scripts/duffle_paths.lua`.
|
||||
-- If lfs is missing, `require` throws — fail loud per the build-tool convention.
|
||||
--- @type LfsMod
|
||||
local lfs = require("lfs")
|
||||
local lfs = require("lfs") ---@type LfsMod
|
||||
|
||||
-- ════════════════════════════════════════════════════════════════════════════
|
||||
-- ASCII byte constants
|
||||
-- ════════════════════════════════════════════════════════════════════════════
|
||||
|
||||
--- @type integer
|
||||
local BYTE_SPACE = 0x20 -- ' '
|
||||
--- @type integer
|
||||
local BYTE_TAB = 0x09 -- '\t'
|
||||
--- @type integer
|
||||
local BYTE_NEWLINE = 0x0A -- '\n'
|
||||
--- @type integer
|
||||
local BYTE_CR = 0x0D -- '\r'
|
||||
--- @type integer
|
||||
local BYTE_VT = 0x0B -- '\v'
|
||||
--- @type integer
|
||||
local BYTE_FF = 0x0C -- '\f'
|
||||
local BYTE_SPACE = 0x20 ---@type integer -- ' '
|
||||
local BYTE_TAB = 0x09 ---@type integer -- '\t'
|
||||
local BYTE_NEWLINE = 0x0A ---@type integer -- '\n'
|
||||
local BYTE_CR = 0x0D ---@type integer -- '\r'
|
||||
local BYTE_VT = 0x0B ---@type integer -- '\v'
|
||||
local BYTE_FF = 0x0C ---@type integer -- '\f'
|
||||
|
||||
--- @type integer
|
||||
local BYTE_UNDERSCORE = 0x5F -- '_'
|
||||
--- @type integer
|
||||
local BYTE_DOT = 0x2E -- '.'
|
||||
--- @type integer
|
||||
local BYTE_SLASH = 0x2F -- '/'
|
||||
--- @type integer
|
||||
local BYTE_BACKSLASH = 0x5C -- '\\'
|
||||
--- @type integer
|
||||
local BYTE_STAR = 0x2A -- '*'
|
||||
--- @type integer
|
||||
local BYTE_DQUOTE = 0x22 -- '"'
|
||||
--- @type integer
|
||||
local BYTE_SQUOTE = 0x27 -- '\''
|
||||
--- @type integer
|
||||
local BYTE_COMMA = 0x2C -- ','
|
||||
--- @type integer
|
||||
local BYTE_SEMI = 0x3B -- ';'
|
||||
local BYTE_UNDERSCORE = 0x5F ---@type integer -- '_'
|
||||
local BYTE_DOT = 0x2E ---@type integer -- '.'
|
||||
local BYTE_SLASH = 0x2F ---@type integer -- '/'
|
||||
local BYTE_BACKSLASH = 0x5C ---@type integer -- '\\'
|
||||
local BYTE_STAR = 0x2A ---@type integer -- '*'
|
||||
local BYTE_DQUOTE = 0x22 ---@type integer -- '"'
|
||||
local BYTE_SQUOTE = 0x27 ---@type integer -- '\''
|
||||
local BYTE_COMMA = 0x2C ---@type integer -- ','
|
||||
local BYTE_SEMI = 0x3B ---@type integer -- ';'
|
||||
|
||||
--- @type integer
|
||||
local BYTE_OPEN_PAREN = 0x28 -- '('
|
||||
--- @type integer
|
||||
local BYTE_OPEN_BRACE = 0x7B -- '{'
|
||||
--- @type integer
|
||||
local BYTE_OPEN_BRACK = 0x5B -- '['
|
||||
local BYTE_OPEN_PAREN = 0x28 ---@type integer -- '('
|
||||
local BYTE_OPEN_BRACE = 0x7B ---@type integer -- '{'
|
||||
local BYTE_OPEN_BRACK = 0x5B ---@type integer -- '['
|
||||
|
||||
--- @type integer
|
||||
local BYTE_LOWER_A = 0x61 -- 'a'
|
||||
--- @type integer
|
||||
local BYTE_LOWER_Z = 0x7A -- 'z'
|
||||
--- @type integer
|
||||
local BYTE_UPPER_A = 0x41 -- 'A'
|
||||
--- @type integer
|
||||
local BYTE_UPPER_Z = 0x5A -- 'Z'
|
||||
local BYTE_LOWER_A = 0x61 ---@type integer -- 'a'
|
||||
local BYTE_LOWER_Z = 0x7A ---@type integer -- 'z'
|
||||
local BYTE_UPPER_A = 0x41 ---@type integer -- 'A'
|
||||
local BYTE_UPPER_Z = 0x5A ---@type integer -- 'Z'
|
||||
|
||||
--- @type integer
|
||||
local BYTE_DIGIT_0 = 0x30 -- '0'
|
||||
--- @type integer
|
||||
local BYTE_DIGIT_9 = 0x39 -- '9'
|
||||
local BYTE_DIGIT_0 = 0x30 ---@type integer -- '0'
|
||||
local BYTE_DIGIT_9 = 0x39 ---@type integer -- '9'
|
||||
|
||||
-- ════════════════════════════════════════════════════════════════════════════
|
||||
-- Section -1: Bootstrap (path-setup at module load)
|
||||
@@ -145,47 +119,33 @@ local BYTE_DIGIT_9 = 0x39 -- '9'
|
||||
-- LPeg handles the high-level scanner; the byte-by-byte helpers in Section 1 handle classification primitives that LPeg's CPython-level cost would dominate.
|
||||
--
|
||||
-- If the require fails, fail loud with an actionable message. The build script (`update_deps.ps1`) builds lpeg.dll into `toolchain/lpeg/`; run it when the dll is missing.
|
||||
--- @type boolean, LpegMod|string
|
||||
local lpeg_ok, lpeg = pcall(require, "lpeg")
|
||||
local lpeg_ok, lpeg = pcall(require, "lpeg") ---@type boolean, LpegMod|string
|
||||
if not lpeg_ok then
|
||||
io.stderr:write("[duffle] require('lpeg') failed: ", lpeg, "\n")
|
||||
io.stderr:write("[duffle] lpeg.dll not found on package.cpath.\n")
|
||||
io.stderr:write("[duffle] Run 'scripts/update_deps.ps1' to build it into toolchain/lpeg/.\n")
|
||||
os.exit(1)
|
||||
end
|
||||
--- @type LpegCtor, LpegCtor, LpegCtor
|
||||
local P, S, R = lpeg.P, lpeg.S, lpeg.R
|
||||
local P, S, R = lpeg.P, lpeg.S, lpeg.R ---@type LpegCtor, LpegCtor, LpegCtor
|
||||
|
||||
-- Character class patterns
|
||||
--- @type LpegPattern
|
||||
local alpha_pat = R("AZ", "az") + P("_")
|
||||
--- @type LpegPattern
|
||||
local digit_pat = R("09")
|
||||
--- @type LpegPattern
|
||||
local lpeg_alnum_pat = alpha_pat + digit_pat
|
||||
local alpha_pat = R("AZ", "az") + P("_") ---@type LpegPattern
|
||||
local digit_pat = R("09") ---@type LpegPattern
|
||||
local lpeg_alnum_pat = alpha_pat + digit_pat ---@type LpegPattern
|
||||
|
||||
-- Identifier: alpha followed by zero+ alnum. Capture as a string.
|
||||
--- @type LpegPattern
|
||||
local lpeg_alpha_pat = alpha_pat
|
||||
--- @type LpegPattern
|
||||
local lpeg_ident_pat = lpeg.C(alpha_pat * lpeg_alnum_pat^0)
|
||||
local lpeg_alpha_pat = alpha_pat ---@type LpegPattern
|
||||
local lpeg_ident_pat = lpeg.C(alpha_pat * lpeg_alnum_pat^0) ---@type LpegPattern
|
||||
|
||||
--- @type LpegPattern
|
||||
local lpeg_str_pat = P('"') * (P(1) - S('"\\') + P('\\') * P(1))^0 * P('"') -- String literal: "..." with backslash escapes.
|
||||
--- @type LpegPattern
|
||||
local lpeg_chr_pat = P("'") * (P(1) - S("'\\") + P('\\') * P(1))^0 * P("'") -- Char literal: '...' with backslash escapes.
|
||||
--- @type LpegPattern
|
||||
local lpeg_line_cmt_pat = P("//") * (P(1) - S("\n"))^0 -- Line comment: // ... to end-of-line.
|
||||
--- @type LpegPattern
|
||||
local lpeg_block_cmt_pat = P("/*") * (P(1) - P("*/"))^0 * P("*/") -- Block comment: /* ... */ (no nesting per C standard).
|
||||
--- @type LpegPattern
|
||||
local lpeg_str_or_cmt_pat = lpeg_str_pat + lpeg_chr_pat + lpeg_line_cmt_pat + lpeg_block_cmt_pat -- String or comment (any of the four forms).
|
||||
local lpeg_str_pat = P('"') * (P(1) - S('"\\') + P('\\') * P(1))^0 * P('"') ---@type LpegPattern -- String literal: "..." with backslash escapes.
|
||||
local lpeg_chr_pat = P("'") * (P(1) - S("'\\") + P('\\') * P(1))^0 * P("'") ---@type LpegPattern -- Char literal: '...' with backslash escapes.
|
||||
local lpeg_line_cmt_pat = P("//") * (P(1) - S("\n"))^0 ---@type LpegPattern -- Line comment: // ... to end-of-line.
|
||||
local lpeg_block_cmt_pat = P("/*") * (P(1) - P("*/"))^0 * P("*/") ---@type LpegPattern -- Block comment: /* ... */ (no nesting per C standard).
|
||||
local lpeg_str_or_cmt_pat = lpeg_str_pat + lpeg_chr_pat + lpeg_line_cmt_pat + lpeg_block_cmt_pat ---@type LpegPattern -- String or comment (any of the four forms).
|
||||
|
||||
-- Whitespace + comment skipper: zero+ (whitespace run | string | comment).
|
||||
--- @type LpegPattern
|
||||
local ws_pat = S(" \t\n\r\v\f")
|
||||
--- @type LpegPattern
|
||||
local lpeg_ws_and_cmt_pat = (ws_pat + lpeg_str_or_cmt_pat)^0
|
||||
local ws_pat = S(" \t\n\r\v\f") ---@type LpegPattern
|
||||
local lpeg_ws_and_cmt_pat = (ws_pat + lpeg_str_or_cmt_pat)^0 ---@type LpegPattern
|
||||
|
||||
-- Generic "skip until target, but step over balanced groups" matcher.
|
||||
-- Used by scan_to_char for non-ident / non-bracket chars. We accept any single char except the target.
|
||||
@@ -258,10 +218,8 @@ function M.is_alnum(c) return M.is_alpha(c) or M.is_digit(c) end
|
||||
--- @param s string
|
||||
--- @return string
|
||||
function M.trim(s)
|
||||
--- @type integer
|
||||
local a = 1; while a <= #s and M.is_space_byte(s:byte(a)) do a = a + 1 end
|
||||
--- @type integer
|
||||
local b = #s; while b >= a and M.is_space_byte(s:byte(b)) do b = b - 1 end
|
||||
local a = 1; while a <= #s and M.is_space_byte(s:byte(a)) do a = a + 1 end ---@type integer
|
||||
local b = #s; while b >= a and M.is_space_byte(s:byte(b)) do b = b - 1 end ---@type integer
|
||||
return s:sub(a, b)
|
||||
end
|
||||
|
||||
@@ -271,8 +229,7 @@ end
|
||||
--- @param start integer -- optional 1-indexed start (default 1)
|
||||
--- @return integer|nil
|
||||
function M.find_byte(haystack, target, start)
|
||||
--- @type integer
|
||||
for pos = start or 1, #haystack do
|
||||
for pos = start or 1, #haystack do ---@type integer
|
||||
if haystack:byte(pos) == target then return pos end
|
||||
end
|
||||
return nil
|
||||
@@ -282,12 +239,9 @@ end
|
||||
--- @param path Path
|
||||
--- @return Path
|
||||
function M.dirname(path)
|
||||
--- @type integer
|
||||
local last_sep = 0
|
||||
--- @type integer
|
||||
for pos = 1, #path do
|
||||
--- @type integer
|
||||
local b = path:byte(pos)
|
||||
local last_sep = 0 ---@type integer
|
||||
for pos = 1, #path do ---@type integer
|
||||
local b = path:byte(pos) ---@type integer
|
||||
if b == BYTE_SLASH or b == BYTE_BACKSLASH then last_sep = pos end
|
||||
end
|
||||
if last_sep == 0 then return "." end
|
||||
@@ -298,20 +252,14 @@ end
|
||||
--- @param path Path
|
||||
--- @return string
|
||||
function M.basename_no_ext(path)
|
||||
--- @type integer
|
||||
local last_sep = 0
|
||||
--- @type integer
|
||||
for pos = 1, #path do
|
||||
--- @type integer
|
||||
local b = path:byte(pos)
|
||||
local last_sep = 0 ---@type integer
|
||||
for pos = 1, #path do ---@type integer
|
||||
local b = path:byte(pos) ---@type integer
|
||||
if b == BYTE_SLASH or b == BYTE_BACKSLASH then last_sep = pos end
|
||||
end
|
||||
--- @type integer
|
||||
local a = last_sep + 1
|
||||
--- @type integer
|
||||
local last_dot = #path + 1
|
||||
--- @type integer
|
||||
for pos = #path, a, -1 do
|
||||
local a = last_sep + 1 ---@type integer
|
||||
local last_dot = #path + 1 ---@type integer
|
||||
for pos = #path, a, -1 do ---@type integer
|
||||
if path:byte(pos) == BYTE_DOT then last_dot = pos; break end
|
||||
end
|
||||
return path:sub(a, last_dot - 1)
|
||||
@@ -322,12 +270,10 @@ end
|
||||
--- @param input string
|
||||
--- @return PathRoot
|
||||
local function parse_path_root(input)
|
||||
--- @type string|nil
|
||||
local drive = input:match("^(%a:)")
|
||||
local drive = input:match("^(%a:)") ---@type string|nil
|
||||
if drive then
|
||||
if input:sub(3, 3) == "/" then
|
||||
--- @type string
|
||||
local rest = input:sub(4)
|
||||
local rest = input:sub(4) ---@type string
|
||||
while rest:sub(1, 1) == "/" do rest = rest:sub(2) end
|
||||
return { kind = "drive_absolute", prefix = drive .. "/", rest = rest, anchored = true }
|
||||
end
|
||||
@@ -335,29 +281,22 @@ local function parse_path_root(input)
|
||||
end
|
||||
|
||||
if input:sub(1, 2) == "//" then
|
||||
--- @type integer
|
||||
local server_start = 3
|
||||
--- @type integer|nil
|
||||
local server_end = M.find_byte(input, BYTE_SLASH, server_start)
|
||||
local server_start = 3 ---@type integer
|
||||
local server_end = M.find_byte(input, BYTE_SLASH, server_start) ---@type integer|nil
|
||||
if not server_end or server_end == server_start then
|
||||
error("UNC path requires //server/share: " .. input, 3)
|
||||
end
|
||||
--- @type string
|
||||
local server = input:sub(server_start, server_end - 1)
|
||||
--- @type integer
|
||||
local share_start = server_end + 1
|
||||
local server = input:sub(server_start, server_end - 1) ---@type string
|
||||
local share_start = server_end + 1 ---@type integer
|
||||
while input:sub(share_start, share_start) == "/" do
|
||||
share_start = share_start + 1
|
||||
end
|
||||
--- @type integer
|
||||
local share_end = M.find_byte(input, BYTE_SLASH, share_start) or (#input + 1)
|
||||
local share_end = M.find_byte(input, BYTE_SLASH, share_start) or (#input + 1) ---@type integer
|
||||
if share_end == share_start then
|
||||
error("UNC path requires //server/share: " .. input, 3)
|
||||
end
|
||||
--- @type string
|
||||
local share = input:sub(share_start, share_end - 1)
|
||||
--- @type string
|
||||
local rest = input:sub(share_end + 1)
|
||||
local share = input:sub(share_start, share_end - 1) ---@type string
|
||||
local rest = input:sub(share_end + 1) ---@type string
|
||||
while rest:sub(1, 1) == "/" do rest = rest:sub(2) end
|
||||
return {
|
||||
kind = "unc_absolute",
|
||||
@@ -368,8 +307,7 @@ local function parse_path_root(input)
|
||||
end
|
||||
|
||||
if input:sub(1, 1) == "/" then
|
||||
--- @type string
|
||||
local rest = input:sub(2)
|
||||
local rest = input:sub(2) ---@type string
|
||||
while rest:sub(1, 1) == "/" do rest = rest:sub(2) end
|
||||
return { kind = "posix_absolute", prefix = "/", rest = rest, anchored = true }
|
||||
end
|
||||
@@ -384,12 +322,9 @@ function M.normalize_path(path)
|
||||
if type(path) ~= "string" then error("normalize_path requires a string path", 2) end
|
||||
if path == "" then return "" end
|
||||
|
||||
--- @type PathRoot
|
||||
local root = parse_path_root(path:gsub("\\", "/"))
|
||||
--- @type string[]
|
||||
local segments = {}
|
||||
--- @type string
|
||||
for segment in root.rest:gmatch("[^/]+") do
|
||||
local root = parse_path_root(path:gsub("\\", "/")) ---@type PathRoot
|
||||
local segments = {} ---@type string[]
|
||||
for segment in root.rest:gmatch("[^/]+") do ---@type string
|
||||
if segment == "." then
|
||||
-- no-op
|
||||
elseif segment == ".." then
|
||||
@@ -403,8 +338,7 @@ function M.normalize_path(path)
|
||||
end
|
||||
end
|
||||
|
||||
--- @type string
|
||||
local tail = table.concat(segments, "/")
|
||||
local tail = table.concat(segments, "/") ---@type string
|
||||
if root.kind == "relative" then return tail ~= "" and tail or "." end
|
||||
if root.kind == "drive_relative" then return root.prefix .. tail end
|
||||
if root.kind == "unc_absolute" then return tail ~= "" and (root.prefix .. "/" .. tail) or root.prefix end
|
||||
@@ -414,10 +348,8 @@ end
|
||||
--- @param path Path
|
||||
--- @return Path
|
||||
local function absolute_normalized_path(path)
|
||||
--- @type Path
|
||||
local normalized = M.normalize_path(path)
|
||||
--- @type PathRoot
|
||||
local root = parse_path_root(normalized)
|
||||
local normalized = M.normalize_path(path) ---@type Path
|
||||
local root = parse_path_root(normalized) ---@type PathRoot
|
||||
if root.kind == "drive_relative" then
|
||||
error("drive-relative path cannot be resolved without a per-drive cwd: " .. normalized, 3)
|
||||
end
|
||||
@@ -430,15 +362,12 @@ end
|
||||
--- @param path Path
|
||||
--- @return string
|
||||
function M.canonical_path_key(path)
|
||||
--- @type Path
|
||||
local normalized = M.normalize_path(path)
|
||||
--- @type PathRoot
|
||||
local root = parse_path_root(normalized)
|
||||
local normalized = M.normalize_path(path) ---@type Path
|
||||
local root = parse_path_root(normalized) ---@type PathRoot
|
||||
if root.kind == "drive_relative" then
|
||||
error("canonical_path_key cannot compare drive-relative path: " .. normalized, 2)
|
||||
end
|
||||
--- @type string
|
||||
local key = absolute_normalized_path(normalized):lower()
|
||||
local key = absolute_normalized_path(normalized):lower() ---@type string
|
||||
if #key > 3 and key:sub(-1) == "/" then key = key:sub(1, -2) end
|
||||
return key
|
||||
end
|
||||
@@ -453,11 +382,9 @@ end
|
||||
--- @param path Path
|
||||
--- @return string
|
||||
function M.read_file(path)
|
||||
--- @type file*|nil
|
||||
local f = io.open(path, "r")
|
||||
local f = io.open(path, "r") ---@type file*|nil
|
||||
if not f then error("Cannot open " .. path) end
|
||||
--- @type string
|
||||
local content = f:read("*a"); f:close()
|
||||
local content = f:read("*a"); f:close() ---@type string
|
||||
return content
|
||||
end
|
||||
|
||||
@@ -465,8 +392,7 @@ end
|
||||
--- @param content string
|
||||
--- @return nil
|
||||
function M.write_file(path, content)
|
||||
--- @type file*|nil
|
||||
local f = io.open(path, "w")
|
||||
local f = io.open(path, "w") ---@type file*|nil
|
||||
if not f then error("Cannot write " .. path) end
|
||||
f:write(content); f:close()
|
||||
end
|
||||
@@ -477,14 +403,12 @@ end
|
||||
--- @param content string
|
||||
--- @return nil
|
||||
function M.write_file_lf(path, content)
|
||||
--- @type file*|nil
|
||||
local f = io.open(path, "wb")
|
||||
local f = io.open(path, "wb") ---@type file*|nil
|
||||
if not f then error("Cannot write " .. path) end
|
||||
f:write(content); f:close()
|
||||
end
|
||||
|
||||
--- @type table<string, string> -- bag: input path -> absolute path
|
||||
local _absolute_path_cache = {}
|
||||
local _absolute_path_cache = {} ---@type table<string, string> -- bag: input path -> absolute path
|
||||
|
||||
--- Convert a (possibly relative) path to an absolute path, using CWD if needed.
|
||||
--- Normalizes forward slashes to backslashes on Windows.
|
||||
@@ -496,26 +420,21 @@ function M.to_absolute_path(path)
|
||||
if _absolute_path_cache[path] then return _absolute_path_cache[path] end
|
||||
if #path >= 2 and path:sub(2, 2) == ":" then
|
||||
-- Already absolute; normalize slashes for consistency.
|
||||
--- @type string
|
||||
local result = (path:gsub("/", "\\"))
|
||||
local result = (path:gsub("/", "\\")) ---@type string
|
||||
_absolute_path_cache[path] = result
|
||||
return result
|
||||
end
|
||||
--- @type string|nil
|
||||
local cwd = lfs.currentdir()
|
||||
local cwd = lfs.currentdir() ---@type string|nil
|
||||
if not cwd then _absolute_path_cache[path] = path; return path end
|
||||
cwd = cwd:gsub("/", "\\")
|
||||
--- @type string
|
||||
local tail = (path:gsub("/", "\\"))
|
||||
--- @type string
|
||||
local result = cwd .. "\\" .. tail
|
||||
local tail = (path:gsub("/", "\\")) ---@type string
|
||||
local result = cwd .. "\\" .. tail ---@type string
|
||||
_absolute_path_cache[path] = result
|
||||
return result
|
||||
end
|
||||
|
||||
-- Cache of directories already verified to exist in this process.
|
||||
--- @type table<string, boolean> -- bag: dir path -> already ensured
|
||||
local _ensured_dirs = {}
|
||||
local _ensured_dirs = {} ---@type table<string, boolean> -- bag: dir path -> already ensured
|
||||
|
||||
--- @param path Path
|
||||
--- @return nil
|
||||
@@ -533,10 +452,8 @@ end
|
||||
--- @param sources SourceFile[]
|
||||
--- @return table<string, SourceFile[]>
|
||||
function M.group_sources_by_dir(sources)
|
||||
--- @type table<string, SourceFile[]>
|
||||
local by_dir = {}
|
||||
--- @type integer, SourceFile
|
||||
for _, src in ipairs(sources) do
|
||||
local by_dir = {} ---@type table<string, SourceFile[]>
|
||||
for _, src in ipairs(sources) do ---@type integer, SourceFile
|
||||
by_dir[src.dir] = by_dir[src.dir] or {}
|
||||
table.insert(by_dir[src.dir], src)
|
||||
end
|
||||
@@ -567,8 +484,7 @@ function M.skip_ws_and_cmt(s, pos) return lpeg.match(lpeg_ws_and_cmt_pat, s, pos
|
||||
--- @param pos integer
|
||||
--- @return string|nil, integer
|
||||
function M.read_ident(s, pos)
|
||||
--- @type string|nil
|
||||
local result = lpeg.match(lpeg_ident_pat, s, pos)
|
||||
local result = lpeg.match(lpeg_ident_pat, s, pos) ---@type string|nil
|
||||
if result then return result, pos + #result end
|
||||
return nil, pos
|
||||
end
|
||||
@@ -581,21 +497,16 @@ end
|
||||
--- @param pos integer
|
||||
--- @return string|nil, integer
|
||||
function M.read_balanced(s, open_char, close_char, pos)
|
||||
--- @type integer
|
||||
local open_byte = open_char:byte()
|
||||
local open_byte = open_char:byte() ---@type integer
|
||||
if s:byte(pos) ~= open_byte then return nil, pos end
|
||||
-- scan: <open_char>
|
||||
pos = pos + 1
|
||||
-- scan: <open_char> <inner...>
|
||||
--- @type integer
|
||||
local len = #s
|
||||
--- @type integer
|
||||
local depth = 1
|
||||
--- @type integer
|
||||
local a = pos
|
||||
local len = #s ---@type integer
|
||||
local depth = 1 ---@type integer
|
||||
local a = pos ---@type integer
|
||||
while pos <= len and depth > 0 do
|
||||
--- @type integer
|
||||
local c = s:byte(pos)
|
||||
local c = s:byte(pos) ---@type integer
|
||||
if c == open_byte then
|
||||
depth = depth + 1
|
||||
pos = pos + 1
|
||||
@@ -606,8 +517,7 @@ function M.read_balanced(s, open_char, close_char, pos)
|
||||
pos = pos + 1
|
||||
-- scan: <open_char> <inner...> <close_char> (depth=depth)
|
||||
else
|
||||
--- @type integer
|
||||
local nx = M.skip_str_or_cmt(s, pos)
|
||||
local nx = M.skip_str_or_cmt(s, pos) ---@type integer
|
||||
if nx > pos then
|
||||
-- scan: <open_char> <inner...> <str|cmt>
|
||||
pos = nx
|
||||
@@ -641,20 +551,16 @@ M.read_brackets = function(s, pos) return M.read_balanced(s, "[", "]", pos) end
|
||||
--- @param start integer
|
||||
--- @return integer|nil
|
||||
function M.scan_to_char(s, target, start)
|
||||
--- @type integer
|
||||
local target_byte = target:byte()
|
||||
--- @type integer
|
||||
local pos = start
|
||||
local target_byte = target:byte() ---@type integer
|
||||
local pos = start ---@type integer
|
||||
while pos <= #s do
|
||||
--- @type integer
|
||||
local c = s:byte(pos)
|
||||
local c = s:byte(pos) ---@type integer
|
||||
if c == target_byte then return pos end -- scan: ... <target found> | <skipping to target>
|
||||
if c == BYTE_OPEN_PAREN then local _, a = M.read_balanced(s, "(", ")", pos); pos = a -- scan: ... ( <balanced> ) ...
|
||||
elseif c == BYTE_OPEN_BRACE then local _, a = M.read_balanced(s, "{", "}", pos); pos = a -- scan: ... { <balanced> } ...
|
||||
elseif c == BYTE_OPEN_BRACK then local _, a = M.read_balanced(s, "[", "]", pos); pos = a -- scan: ... [ <balanced> ] ...
|
||||
else
|
||||
--- @type integer
|
||||
local nx = M.skip_str_or_cmt(s, pos)
|
||||
local nx = M.skip_str_or_cmt(s, pos) ---@type integer
|
||||
pos = (nx > pos) and nx or (pos + 1)
|
||||
-- scan: ... <str|cmt skipped> ...
|
||||
end
|
||||
@@ -670,10 +576,8 @@ end
|
||||
--- @return integer|nil
|
||||
function M.skip_preprocessor_line(s, pos)
|
||||
if s:byte(pos) ~= 35 then return nil end -- '#'
|
||||
--- @type integer
|
||||
local scan = pos
|
||||
--- @type integer
|
||||
local len = #s
|
||||
local scan = pos ---@type integer
|
||||
local len = #s ---@type integer
|
||||
while scan <= len and s:byte(scan) ~= BYTE_NEWLINE do scan = scan + 1 end
|
||||
return scan + 1
|
||||
end
|
||||
@@ -689,8 +593,7 @@ end
|
||||
--- @param after_last integer
|
||||
--- @return boolean
|
||||
local function segment_has_newline(source, first, after_last)
|
||||
--- @type integer
|
||||
for pos = first, after_last - 1 do
|
||||
for pos = first, after_last - 1 do ---@type integer
|
||||
if source:byte(pos) == BYTE_NEWLINE then return true end
|
||||
end
|
||||
return false
|
||||
@@ -701,13 +604,11 @@ end
|
||||
--- @return integer|nil
|
||||
local function skip_directive_space(source, pos)
|
||||
while pos <= #source do
|
||||
--- @type integer
|
||||
local byte = source:byte(pos)
|
||||
local byte = source:byte(pos) ---@type integer
|
||||
if is_horizontal_space(byte) then
|
||||
pos = pos + 1
|
||||
elseif byte == BYTE_SLASH and source:byte(pos + 1) == BYTE_STAR then
|
||||
--- @type integer
|
||||
local after = M.skip_str_or_cmt(source, pos)
|
||||
local after = M.skip_str_or_cmt(source, pos) ---@type integer
|
||||
if after == pos or segment_has_newline(source, pos, after) then return nil end
|
||||
pos = after
|
||||
elseif byte == BYTE_SLASH and source:byte(pos + 1) == BYTE_SLASH then
|
||||
@@ -724,21 +625,14 @@ end
|
||||
--- @param source string
|
||||
--- @return string, integer[], integer[]
|
||||
local function splice_c_lines(source)
|
||||
--- @type string[]
|
||||
local logical_bytes = {}
|
||||
--- @type integer[]
|
||||
local physical_pos = {}
|
||||
--- @type integer[]
|
||||
local physical_line = {}
|
||||
--- @type integer
|
||||
local pos = 1
|
||||
--- @type integer
|
||||
local line = 1
|
||||
local logical_bytes = {} ---@type string[]
|
||||
local physical_pos = {} ---@type integer[]
|
||||
local physical_line = {} ---@type integer[]
|
||||
local pos = 1 ---@type integer
|
||||
local line = 1 ---@type integer
|
||||
while pos <= #source do
|
||||
--- @type integer
|
||||
local byte = source:byte(pos)
|
||||
--- @type integer|nil
|
||||
local splice_len = nil
|
||||
local byte = source:byte(pos) ---@type integer
|
||||
local splice_len = nil ---@type integer|nil
|
||||
if byte == BYTE_BACKSLASH and source:byte(pos + 1) == BYTE_NEWLINE then
|
||||
splice_len = 2
|
||||
elseif byte == BYTE_BACKSLASH and source:byte(pos + 1) == BYTE_CR and source:byte(pos + 2) == BYTE_NEWLINE then
|
||||
@@ -749,8 +643,7 @@ local function splice_c_lines(source)
|
||||
pos = pos + splice_len
|
||||
line = line + 1
|
||||
else
|
||||
--- @type integer
|
||||
local logical_pos = #logical_bytes + 1
|
||||
local logical_pos = #logical_bytes + 1 ---@type integer
|
||||
logical_bytes[logical_pos] = source:sub(pos, pos)
|
||||
physical_pos [logical_pos] = pos
|
||||
physical_line[logical_pos] = line
|
||||
@@ -774,17 +667,12 @@ function M.parse_direct_quoted_includes(source_text)
|
||||
|
||||
-- Each arm's effect on (pos, line_leading) is annotated at the branch site.
|
||||
-- Arm order: newline / horiz-space / '//' / '/*' / '"' / '\'' / '#' / default.
|
||||
--- @type string, integer[], integer[]
|
||||
local logical_text, physical_pos, physical_line = splice_c_lines(source_text)
|
||||
--- @type QuotedInclude[]
|
||||
local includes = {}
|
||||
--- @type integer
|
||||
local pos = 1
|
||||
--- @type boolean
|
||||
local line_leading = true
|
||||
local logical_text, physical_pos, physical_line = splice_c_lines(source_text) ---@type string, integer[], integer[]
|
||||
local includes = {} ---@type QuotedInclude[]
|
||||
local pos = 1 ---@type integer
|
||||
local line_leading = true ---@type boolean
|
||||
while pos <= #logical_text do
|
||||
--- @type integer
|
||||
local byte = logical_text:byte(pos)
|
||||
local byte = logical_text:byte(pos) ---@type integer
|
||||
if byte == BYTE_NEWLINE then
|
||||
-- line break; refresh leading-whitespace state for next line.
|
||||
line_leading = true
|
||||
@@ -794,22 +682,19 @@ function M.parse_direct_quoted_includes(source_text)
|
||||
pos = pos + 1
|
||||
elseif byte == BYTE_SLASH and logical_text:byte(pos + 1) == BYTE_SLASH then
|
||||
-- '//' line comment: skip_str_or_cmt walks to EOL on its own, so no separate newline scan is needed here.
|
||||
--- @type integer
|
||||
local after = M.skip_str_or_cmt(logical_text, pos)
|
||||
local after = M.skip_str_or_cmt(logical_text, pos) ---@type integer
|
||||
-- pos := after when the skipper agrees, else single-byte advance.
|
||||
pos = (after > pos) and after or (pos + 1)
|
||||
elseif byte == BYTE_SLASH and logical_text:byte(pos + 1) == BYTE_STAR then
|
||||
-- '/*' block comment.
|
||||
--- @type integer
|
||||
local after = M.skip_str_or_cmt(logical_text, pos)
|
||||
local after = M.skip_str_or_cmt(logical_text, pos) ---@type integer
|
||||
if after <= pos then
|
||||
-- skipper refused (unterminated /*). Treat this byte as ordinary content: step one, mark non-leading.
|
||||
line_leading = false
|
||||
pos = pos + 1
|
||||
else
|
||||
-- jump past the closing '*/'. The span may cross lines, so rescan for embedded '\n' to refresh line_leading.
|
||||
--- @type integer
|
||||
for scan = pos, after - 1 do
|
||||
for scan = pos, after - 1 do ---@type integer
|
||||
if logical_text:byte(scan) == BYTE_NEWLINE then line_leading = true end
|
||||
end
|
||||
pos = after
|
||||
@@ -817,17 +702,14 @@ function M.parse_direct_quoted_includes(source_text)
|
||||
elseif byte == BYTE_DQUOTE or byte == BYTE_SQUOTE then
|
||||
-- enter + leave the string literal in one skip; literal bodies cannot contain a directive regardless of what they look like.
|
||||
line_leading = false
|
||||
--- @type integer
|
||||
local after = M.skip_str_or_cmt(logical_text, pos)
|
||||
local after = M.skip_str_or_cmt(logical_text, pos) ---@type integer
|
||||
pos = (after > pos) and after or (pos + 1)
|
||||
elseif byte == 35 and line_leading then -- '#' at line head
|
||||
-- Sequential pre-checks; any one failing falls through to ::not_include:: (single-byte advance).
|
||||
-- Full success pushes the record and jumps to ::directive_done:: without ever entering the not-include path.
|
||||
-- (All locals are pre-declared at the top of this arm because Lua forbids a goto from crossing a local declaration into its scope.)
|
||||
--- @type integer, integer, integer|nil, string|nil, integer, integer
|
||||
local hash_pos, directive_line, scan, ident, after_ident, after_quote
|
||||
--- @type string, integer, integer
|
||||
local include_path, physical_first, physical_last
|
||||
local hash_pos, directive_line, scan, ident, after_ident, after_quote ---@type integer, integer, integer|nil, string|nil, integer, integer
|
||||
local include_path, physical_first, physical_last ---@type string, integer, integer
|
||||
hash_pos = pos
|
||||
directive_line = physical_line[hash_pos] or 1
|
||||
scan = skip_directive_space(logical_text, pos + 1)
|
||||
@@ -875,8 +757,7 @@ end
|
||||
--- @param wanted string
|
||||
--- @return boolean
|
||||
local function path_has_segment(path, wanted)
|
||||
--- @type string
|
||||
for segment in M.normalize_path(path):gmatch("[^/]+") do
|
||||
for segment in M.normalize_path(path):gmatch("[^/]+") do ---@type string
|
||||
if segment:lower() == wanted then return true end
|
||||
end
|
||||
return false
|
||||
@@ -887,16 +768,14 @@ end
|
||||
--- @return boolean
|
||||
local function canonical_key_is_within(candidate_key, root_key)
|
||||
if candidate_key == root_key then return true end
|
||||
--- @type string
|
||||
local prefix = root_key .. "/"
|
||||
local prefix = root_key .. "/" ---@type string
|
||||
return candidate_key:sub(1, #prefix) == prefix
|
||||
end
|
||||
|
||||
--- @param path Path
|
||||
--- @return SourceFile
|
||||
local function load_source_record(path)
|
||||
--- @type Path
|
||||
local normalized = absolute_normalized_path(path)
|
||||
local normalized = absolute_normalized_path(path) ---@type Path
|
||||
return {
|
||||
path = normalized,
|
||||
text = M.read_file(normalized),
|
||||
@@ -919,20 +798,13 @@ function M.resolve_source_corpus(options)
|
||||
error("resolve_source_corpus requires options.project_root", 2)
|
||||
end
|
||||
|
||||
--- @type Path
|
||||
local project_root = absolute_normalized_path(options.project_root)
|
||||
--- @type Path
|
||||
local code_root = M.normalize_path(project_root .. "/code")
|
||||
--- @type string
|
||||
local code_root_key = M.canonical_path_key(code_root)
|
||||
--- @type SourceFile
|
||||
local root = load_source_record(options.unity_root)
|
||||
--- @type SourceFile[]
|
||||
local source_order = { root }
|
||||
--- @type table<Path, SourceFile>
|
||||
local sources_by_path = { [M.canonical_path_key(root.path)] = root, }
|
||||
--- @type SourceResolver
|
||||
local resolver = {
|
||||
local project_root = absolute_normalized_path(options.project_root) ---@type Path
|
||||
local code_root = M.normalize_path(project_root .. "/code") ---@type Path
|
||||
local code_root_key = M.canonical_path_key(code_root) ---@type string
|
||||
local root = load_source_record(options.unity_root) ---@type SourceFile
|
||||
local source_order = { root } ---@type SourceFile[]
|
||||
local sources_by_path = { [M.canonical_path_key(root.path)] = root, } ---@type table<Path, SourceFile>
|
||||
local resolver = { ---@type SourceResolver
|
||||
resolved = {
|
||||
{
|
||||
include_path = nil,
|
||||
@@ -949,22 +821,14 @@ function M.resolve_source_corpus(options)
|
||||
shadowed = {},
|
||||
}
|
||||
|
||||
--- @type integer, QuotedInclude
|
||||
for _, include in ipairs(M.parse_direct_quoted_includes(root.text)) do
|
||||
--- @type Path
|
||||
local candidate_a = absolute_normalized_path(root.dir .. "/" .. include.path)
|
||||
--- @type Path
|
||||
local candidate_b = absolute_normalized_path(code_root .. "/" .. include.path)
|
||||
--- @type string
|
||||
local key_a = M.canonical_path_key(candidate_a)
|
||||
--- @type string
|
||||
local key_b = M.canonical_path_key(candidate_b)
|
||||
--- @type boolean
|
||||
local inside_a = canonical_key_is_within(key_a, code_root_key)
|
||||
--- @type boolean
|
||||
local inside_b = canonical_key_is_within(key_b, code_root_key)
|
||||
--- @type ResolverEvidence
|
||||
local evidence = {
|
||||
for _, include in ipairs(M.parse_direct_quoted_includes(root.text)) do ---@type integer, QuotedInclude
|
||||
local candidate_a = absolute_normalized_path(root.dir .. "/" .. include.path) ---@type Path
|
||||
local candidate_b = absolute_normalized_path(code_root .. "/" .. include.path) ---@type Path
|
||||
local key_a = M.canonical_path_key(candidate_a) ---@type string
|
||||
local key_b = M.canonical_path_key(candidate_b) ---@type string
|
||||
local inside_a = canonical_key_is_within(key_a, code_root_key) ---@type boolean
|
||||
local inside_b = canonical_key_is_within(key_b, code_root_key) ---@type boolean
|
||||
local evidence = { ---@type ResolverEvidence
|
||||
include_path = include.path,
|
||||
include_text = include.include_text,
|
||||
root_source = root.path,
|
||||
@@ -988,16 +852,11 @@ function M.resolve_source_corpus(options)
|
||||
resolver.skipped[#resolver.skipped + 1] = evidence
|
||||
else
|
||||
-- Boundary checks above deliberately precede every filesystem probe.
|
||||
--- @type boolean
|
||||
local exists_a = inside_a and lfs.attributes(candidate_a, "mode") == "file"
|
||||
--- @type boolean
|
||||
local exists_b = inside_b and ((key_b == key_a and exists_a) or lfs.attributes(candidate_b, "mode") == "file")
|
||||
--- @type Path|nil
|
||||
local selected = nil
|
||||
--- @type string|nil
|
||||
local selected_key = nil
|
||||
--- @type string|nil
|
||||
local disposition = nil
|
||||
local exists_a = inside_a and lfs.attributes(candidate_a, "mode") == "file" ---@type boolean
|
||||
local exists_b = inside_b and ((key_b == key_a and exists_a) or lfs.attributes(candidate_b, "mode") == "file") ---@type boolean
|
||||
local selected = nil ---@type Path|nil
|
||||
local selected_key = nil ---@type string|nil
|
||||
local disposition = nil ---@type string|nil
|
||||
if exists_a then
|
||||
selected = candidate_a
|
||||
selected_key = key_a
|
||||
@@ -1034,8 +893,7 @@ function M.resolve_source_corpus(options)
|
||||
evidence.duplicate_of = sources_by_path[selected_key].path
|
||||
resolver.skipped[#resolver.skipped + 1] = evidence
|
||||
else
|
||||
--- @type SourceFile
|
||||
local source = load_source_record(selected)
|
||||
local source = load_source_record(selected) ---@type SourceFile
|
||||
evidence.disposition = disposition
|
||||
source_order[#source_order + 1] = source
|
||||
sources_by_path[selected_key] = source
|
||||
@@ -1062,30 +920,23 @@ end
|
||||
--- @param body string
|
||||
--- @return string[]
|
||||
function M.split_top_level_commas(body)
|
||||
--- @type string[]
|
||||
local tokens = {}
|
||||
--- @type integer
|
||||
local pos = 1
|
||||
--- @type integer
|
||||
local body_len = #body
|
||||
--- @type integer
|
||||
local token_start = 1
|
||||
local tokens = {} ---@type string[]
|
||||
local pos = 1 ---@type integer
|
||||
local body_len = #body ---@type integer
|
||||
local token_start = 1 ---@type integer
|
||||
|
||||
-- True iff `chunk` contains any non-whitespace, non-comment, non-string content (i.e., real token material).
|
||||
-- Walks through ws + comments individually so a chunk like " /* trailing */ shift_lleft(...)" is correctly classified as having real content (the macro call).
|
||||
--- @param chunk string
|
||||
--- @return boolean
|
||||
local function has_real_content(chunk)
|
||||
--- @type integer
|
||||
local scan = 1
|
||||
--- @type integer
|
||||
local len = #chunk
|
||||
local scan = 1 ---@type integer
|
||||
local len = #chunk ---@type integer
|
||||
while scan <= len do
|
||||
if M.is_space_byte(chunk:byte(scan)) then
|
||||
scan = scan + 1
|
||||
else
|
||||
--- @type integer
|
||||
local nx = M.skip_str_or_cmt(chunk, scan)
|
||||
local nx = M.skip_str_or_cmt(chunk, scan) ---@type integer
|
||||
if nx > scan then
|
||||
scan = nx -- skipped a comment or string
|
||||
else
|
||||
@@ -1100,8 +951,7 @@ function M.split_top_level_commas(body)
|
||||
--- @return nil
|
||||
local function emit(end_pos)
|
||||
if end_pos >= token_start then
|
||||
--- @type string
|
||||
local chunk = body:sub(token_start, end_pos)
|
||||
local chunk = body:sub(token_start, end_pos) ---@type string
|
||||
if M.trim(chunk) ~= "" then
|
||||
if has_real_content(chunk) then
|
||||
tokens[#tokens + 1] = chunk
|
||||
@@ -1118,8 +968,7 @@ function M.split_top_level_commas(body)
|
||||
end
|
||||
|
||||
while pos <= body_len do
|
||||
--- @type integer
|
||||
local c = body:byte(pos)
|
||||
local c = body:byte(pos) ---@type integer
|
||||
if c == BYTE_OPEN_PAREN then local _, a = M.read_parens(body, pos); pos = a -- scan: ... ( <balanced> ...
|
||||
elseif c == BYTE_OPEN_BRACE then local _, a = M.read_braces(body, pos); pos = a -- scan: ... { <balanced> ...
|
||||
elseif c == BYTE_OPEN_BRACK then local _, a = M.read_brackets(body, pos); pos = a -- scan: ... ( <balanced> ...
|
||||
@@ -1139,8 +988,7 @@ function M.split_top_level_commas(body)
|
||||
pos = pos + 1
|
||||
token_start = pos
|
||||
else
|
||||
--- @type integer
|
||||
local nx = M.skip_str_or_cmt(body, pos)
|
||||
local nx = M.skip_str_or_cmt(body, pos) ---@type integer
|
||||
if nx > pos then
|
||||
-- scan: ... <str|cmt> ...
|
||||
-- Skipped a comment or string at top level: emit token break.
|
||||
@@ -1160,10 +1008,8 @@ end
|
||||
-- Section 4: tokenize_body + build_body_line_index (shared, memoized)
|
||||
-- ════════════════════════════════════════════════════════════════════════════
|
||||
|
||||
--- @type table<string, BodyToken[]> -- bag: body text -> tokens
|
||||
local _tokenize_body_cache = {}
|
||||
--- @type table<string, table<integer, integer>> -- bag: body text -> offset-to-line
|
||||
local _body_line_index_cache = {}
|
||||
local _tokenize_body_cache = {} ---@type table<string, BodyToken[]> -- bag: body text -> tokens
|
||||
local _body_line_index_cache = {} ---@type table<string, table<integer, integer>> -- bag: body text -> offset-to-line
|
||||
|
||||
--- Tokenize the body inner-text into a flat list of `{tok, rel}` pairs.
|
||||
--- `tok` is the trimmed token string; `rel` is the byte offset within `body`.
|
||||
@@ -1172,23 +1018,17 @@ local _body_line_index_cache = {}
|
||||
--- @return BodyToken[]
|
||||
function M.tokenize_body(body)
|
||||
if _tokenize_body_cache[body] ~= nil then return _tokenize_body_cache[body] end
|
||||
--- @type BodyToken[]
|
||||
local out = {}
|
||||
--- @type integer
|
||||
local len = #body
|
||||
--- @type integer
|
||||
local rel = 1
|
||||
local out = {} ---@type BodyToken[]
|
||||
local len = #body ---@type integer
|
||||
local rel = 1 ---@type integer
|
||||
while rel <= len do
|
||||
--- @type integer
|
||||
local ws_end = M.skip_ws_and_cmt(body, rel)
|
||||
local ws_end = M.skip_ws_and_cmt(body, rel) ---@type integer
|
||||
if ws_end > rel then rel = ws_end end
|
||||
if rel > len then break end
|
||||
|
||||
--- @type integer
|
||||
local scan = rel
|
||||
local scan = rel ---@type integer
|
||||
while scan <= len do
|
||||
--- @type integer
|
||||
local c = body:byte(scan)
|
||||
local c = body:byte(scan) ---@type integer
|
||||
-- Terminator bytes (delimit a token at the top level): ',' = 0x2C, '\n' = 0x0A, ';' = 0x3B.
|
||||
-- These also appear as separators between argument lists inside the parens/braces/brackets, so we stop the scan when we hit any of them.
|
||||
if c == BYTE_COMMA then break end
|
||||
@@ -1196,13 +1036,11 @@ function M.tokenize_body(body)
|
||||
if c == BYTE_SEMI then break end
|
||||
-- Line-comment '// ... \n' (0x2F 0x2F): skip to (and past) the next newline, or to end-of-body.
|
||||
if c == BYTE_SLASH and body:byte(scan + 1) == BYTE_SLASH then
|
||||
--- @type integer|nil
|
||||
local nl = M.find_byte(body, BYTE_NEWLINE, scan)
|
||||
local nl = M.find_byte(body, BYTE_NEWLINE, scan) ---@type integer|nil
|
||||
scan = nl and (nl + 1) or (len + 1)
|
||||
-- Block-comment '/* ... */' (0x2F 0x2A): skip to (and past) the matching '*/', or to end-of-body.
|
||||
elseif c == BYTE_SLASH and body:byte(scan + 1) == BYTE_STAR then
|
||||
--- @type integer|nil
|
||||
local close = body:find("*/", scan + 2, true)
|
||||
local close = body:find("*/", scan + 2, true) ---@type integer|nil
|
||||
scan = close and (close + 2) or (len + 1)
|
||||
-- Group opener bytes (consume the balanced group via the matching reader): '(' = 0x28, '{' = 0x7B, '[' = 0x5B.
|
||||
elseif c == BYTE_OPEN_PAREN then local _, a = M.read_parens (body, scan); scan = a
|
||||
@@ -1215,13 +1053,11 @@ function M.tokenize_body(body)
|
||||
scan = scan + 1
|
||||
end
|
||||
end
|
||||
--- @type string
|
||||
local tok = M.trim(body:sub(rel, scan - 1))
|
||||
local tok = M.trim(body:sub(rel, scan - 1)) ---@type string
|
||||
if tok ~= "" then out[#out + 1] = { tok = tok, rel = rel } end
|
||||
if scan <= len then
|
||||
scan = scan + 1
|
||||
--- @type integer
|
||||
local w = M.skip_ws_and_cmt(body, scan)
|
||||
local w = M.skip_ws_and_cmt(body, scan) ---@type integer
|
||||
if w > scan then scan = w end
|
||||
end
|
||||
rel = scan
|
||||
@@ -1236,14 +1072,10 @@ end
|
||||
--- @return table<integer, integer> -- bag: byte offset -> 1-based line
|
||||
function M.build_body_line_index(body)
|
||||
if _body_line_index_cache[body] ~= nil then return _body_line_index_cache[body] end
|
||||
--- @type table<integer, integer> -- bag: byte offset -> 1-based line
|
||||
local index = {}
|
||||
--- @type integer
|
||||
local len = #body
|
||||
--- @type integer
|
||||
local newline_count = 0
|
||||
--- @type integer
|
||||
for pos = 1, len do
|
||||
local index = {} ---@type table<integer, integer> -- bag: byte offset -> 1-based line
|
||||
local len = #body ---@type integer
|
||||
local newline_count = 0 ---@type integer
|
||||
for pos = 1, len do ---@type integer
|
||||
if pos > 1 then
|
||||
index[pos] = newline_count + 1
|
||||
end
|
||||
@@ -1265,31 +1097,20 @@ end
|
||||
--- @param metadata_path Path
|
||||
--- @return WordCounts
|
||||
function M.load_word_counts(metadata_path)
|
||||
--- @type WordCounts
|
||||
local counts = {}
|
||||
--- @type string
|
||||
local content = M.read_file(metadata_path)
|
||||
--- @type integer
|
||||
local len = #content
|
||||
--- @type integer
|
||||
local pos = 1
|
||||
--- @type string
|
||||
local prefix = "WORD_COUNT("
|
||||
local counts = {} ---@type WordCounts
|
||||
local content = M.read_file(metadata_path) ---@type string
|
||||
local len = #content ---@type integer
|
||||
local pos = 1 ---@type integer
|
||||
local prefix = "WORD_COUNT(" ---@type string
|
||||
while pos <= len do
|
||||
--- @type integer|nil
|
||||
local nl = M.find_byte(content, BYTE_NEWLINE, pos)
|
||||
--- @type integer
|
||||
local line_end = nl or (len + 1)
|
||||
--- @type string
|
||||
local line = content:sub(pos, line_end - 1)
|
||||
local nl = M.find_byte(content, BYTE_NEWLINE, pos) ---@type integer|nil
|
||||
local line_end = nl or (len + 1) ---@type integer
|
||||
local line = content:sub(pos, line_end - 1) ---@type string
|
||||
-- scan: WORD_COUNT(<name>, <N>)
|
||||
--- @type string
|
||||
local trimmed = M.trim(line)
|
||||
local trimmed = M.trim(line) ---@type string
|
||||
if trimmed:sub(1, #prefix) == prefix and trimmed:sub(-1) == ")" then
|
||||
--- @type string
|
||||
local inner = trimmed:sub(#prefix + 1, #trimmed - 1)
|
||||
--- @type integer|nil
|
||||
local comma = M.find_byte(inner, BYTE_COMMA, 1)
|
||||
local inner = trimmed:sub(#prefix + 1, #trimmed - 1) ---@type string
|
||||
local comma = M.find_byte(inner, BYTE_COMMA, 1) ---@type integer|nil
|
||||
if comma then
|
||||
counts[M.trim(inner:sub(1, comma - 1))] =
|
||||
tonumber(M.trim(inner:sub(comma + 1)))
|
||||
@@ -1307,12 +1128,9 @@ end
|
||||
--- @param source string
|
||||
--- @return LineIndexFn
|
||||
function M.LineIndex(source)
|
||||
--- @type integer[]
|
||||
local positions = {}
|
||||
--- @type integer
|
||||
local n = 0
|
||||
--- @type integer
|
||||
for pos = 1, #source do
|
||||
local positions = {} ---@type integer[]
|
||||
local n = 0 ---@type integer
|
||||
for pos = 1, #source do ---@type integer
|
||||
if source:byte(pos) == BYTE_NEWLINE then
|
||||
n = n + 1
|
||||
positions[n] = pos
|
||||
@@ -1322,11 +1140,9 @@ function M.LineIndex(source)
|
||||
--- @param query_pos integer
|
||||
--- @return integer
|
||||
local function line_of(query_pos)
|
||||
--- @type integer, integer
|
||||
local lo, hi = 1, n
|
||||
local lo, hi = 1, n ---@type integer, integer
|
||||
while lo <= hi do
|
||||
--- @type integer
|
||||
local mid = math.floor((lo + hi) / 2)
|
||||
local mid = math.floor((lo + hi) / 2) ---@type integer
|
||||
if positions[mid] <= query_pos then lo = mid + 1
|
||||
else hi = mid - 1 end
|
||||
end
|
||||
|
||||
+48
-96
@@ -109,8 +109,7 @@
|
||||
--- @field read_named_section fun(adapter: Elf32Adapter, sections: Elf32Section[], name: string): string|nil, string|nil
|
||||
--- @field collect_symbols fun(adapter: Elf32Adapter, sections: Elf32Section[]): table<string, Elf32Sym>|nil, string|nil
|
||||
|
||||
--- @type Elf32Mod
|
||||
local M = {}
|
||||
local M = {} ---@type Elf32Mod
|
||||
|
||||
-- ════════════════════════════════════════════════════════════════════════════
|
||||
-- Little-endian readers (bit-weighted accumulator, math.floor only)
|
||||
@@ -168,8 +167,7 @@ end
|
||||
--- @param off integer
|
||||
--- @return integer
|
||||
function M.read_u32_le(buf, off)
|
||||
--- @type integer
|
||||
local byte_off = off + 1
|
||||
local byte_off = off + 1 ---@type integer
|
||||
return buf:byte(byte_off)
|
||||
+ buf:byte(byte_off + 0x01) * 0x00000100
|
||||
+ buf:byte(byte_off + 0x02) * 0x00010000
|
||||
@@ -181,8 +179,7 @@ end
|
||||
--- @param off integer -- zero-based wire offset
|
||||
--- @return integer
|
||||
function M.read_u16_le(buf, off)
|
||||
--- @type integer
|
||||
local byte_off = off + 1
|
||||
local byte_off = off + 1 ---@type integer
|
||||
return buf:byte(byte_off) + buf:byte(byte_off + 0x01) * 0x00000100
|
||||
end
|
||||
|
||||
@@ -286,8 +283,7 @@ end
|
||||
--- @return string|nil
|
||||
function M.get_str(strtab, off)
|
||||
if off < 0 or off >= #strtab then return nil end
|
||||
--- @type integer|nil
|
||||
local end_pos = strtab:find("\0", off + 1, true)
|
||||
local end_pos = strtab:find("\0", off + 1, true) ---@type integer|nil
|
||||
if not end_pos then return nil end
|
||||
return strtab:sub(off + 1, end_pos - 1)
|
||||
end
|
||||
@@ -305,48 +301,36 @@ end
|
||||
--- @param adapter Elf32Adapter
|
||||
--- @return Elf32Header|nil, string|nil
|
||||
function M.parse_elf32_headers(adapter)
|
||||
--- @type boolean, string|nil
|
||||
local ok, err = M.validate_adapter(adapter)
|
||||
local ok, err = M.validate_adapter(adapter) ---@type boolean, string|nil
|
||||
if not ok then return nil, err end
|
||||
|
||||
-- 4-byte magic: 0x7F 'E' 'L' 'F'.
|
||||
-- The byte readers take the adapter explicitly.
|
||||
-- The production `Support.File` adapter is wrapped by the caller to drop its implicit `self` so the parser shape is flat pass-style.
|
||||
--- @type integer|nil
|
||||
local b1 = M.read_u8(adapter, 0)
|
||||
--- @type integer|nil
|
||||
local b2 = M.read_u8(adapter, 1)
|
||||
--- @type integer|nil
|
||||
local b3 = M.read_u8(adapter, 2)
|
||||
--- @type integer|nil
|
||||
local b4 = M.read_u8(adapter, 3)
|
||||
local b1 = M.read_u8(adapter, 0) ---@type integer|nil
|
||||
local b2 = M.read_u8(adapter, 1) ---@type integer|nil
|
||||
local b3 = M.read_u8(adapter, 2) ---@type integer|nil
|
||||
local b4 = M.read_u8(adapter, 3) ---@type integer|nil
|
||||
if not (b1 and b2 and b3 and b4)
|
||||
or not (b1 == 0x7f and b2 == 0x45 and b3 == 0x4c and b4 == 0x46) then
|
||||
return nil, "bad_magic"
|
||||
end
|
||||
|
||||
--- @type integer|nil
|
||||
local class = M.read_u8(adapter, M.ELF32_HEADER.class_offset)
|
||||
local class = M.read_u8(adapter, M.ELF32_HEADER.class_offset) ---@type integer|nil
|
||||
if class ~= M.ELFCLASS32 then
|
||||
return nil, "unsupported_elf_class"
|
||||
end
|
||||
|
||||
--- @type integer|nil
|
||||
local data = M.read_u8(adapter, M.ELF32_HEADER.endian_offset)
|
||||
local data = M.read_u8(adapter, M.ELF32_HEADER.endian_offset) ---@type integer|nil
|
||||
if data ~= M.ELFDATA2LSB then
|
||||
return nil, "unsupported_elf_data"
|
||||
end
|
||||
|
||||
--- @type integer|nil
|
||||
local e_entry = M.read_u32(adapter, M.ELF32_HEADER.e_entry_offset)
|
||||
--- @type integer|nil
|
||||
local e_shoff = M.read_u32(adapter, M.ELF32_HEADER.e_shoff_offset)
|
||||
--- @type integer|nil
|
||||
local e_shentsize = M.read_u16(adapter, M.ELF32_HEADER.e_shentsize_offset)
|
||||
--- @type integer|nil
|
||||
local e_shnum = M.read_u16(adapter, M.ELF32_HEADER.e_shnum_offset)
|
||||
--- @type integer|nil
|
||||
local e_shstrndx = M.read_u16(adapter, M.ELF32_HEADER.e_shstrndx_offset)
|
||||
local e_entry = M.read_u32(adapter, M.ELF32_HEADER.e_entry_offset) ---@type integer|nil
|
||||
local e_shoff = M.read_u32(adapter, M.ELF32_HEADER.e_shoff_offset) ---@type integer|nil
|
||||
local e_shentsize = M.read_u16(adapter, M.ELF32_HEADER.e_shentsize_offset) ---@type integer|nil
|
||||
local e_shnum = M.read_u16(adapter, M.ELF32_HEADER.e_shnum_offset) ---@type integer|nil
|
||||
local e_shstrndx = M.read_u16(adapter, M.ELF32_HEADER.e_shstrndx_offset) ---@type integer|nil
|
||||
if not (e_entry and e_shoff and e_shentsize and e_shnum and e_shstrndx) then
|
||||
return nil, "truncated_header"
|
||||
end
|
||||
@@ -367,8 +351,7 @@ end
|
||||
--- @param sh_off integer
|
||||
--- @return Elf32Section|nil, string|nil
|
||||
local function read_section_entry(adapter, sh_off)
|
||||
--- @type Elf32Section
|
||||
local entry = {
|
||||
local entry = { ---@type Elf32Section
|
||||
sh_name = M.read_u32(adapter, sh_off + M.ELF32_SECTION.sh_name_offset),
|
||||
sh_type = M.read_u32(adapter, sh_off + M.ELF32_SECTION.sh_type_offset),
|
||||
sh_flags = M.read_u32(adapter, sh_off + M.ELF32_SECTION.sh_flags_offset),
|
||||
@@ -395,21 +378,16 @@ end
|
||||
function M.walk_sections(adapter, hdr)
|
||||
if not hdr or hdr.error then return nil, hdr and hdr.error or "truncated_section_headers" end
|
||||
|
||||
--- @type integer
|
||||
local file_size = M.size(adapter)
|
||||
local file_size = M.size(adapter) ---@type integer
|
||||
if hdr.e_shoff + hdr.e_shnum * hdr.e_shentsize > file_size then
|
||||
return nil, "truncated_section_headers"
|
||||
end
|
||||
|
||||
-- Read every section header first; we need .shstrtab to resolve names.
|
||||
--- @type Elf32Section[]
|
||||
local sections = {}
|
||||
--- @type integer
|
||||
for i = 0, hdr.e_shnum - 1 do
|
||||
--- @type integer
|
||||
local sh_off = hdr.e_shoff + i * hdr.e_shentsize
|
||||
--- @type Elf32Section|nil, string|nil
|
||||
local entry, err = read_section_entry(adapter, sh_off)
|
||||
local sections = {} ---@type Elf32Section[]
|
||||
for i = 0, hdr.e_shnum - 1 do ---@type integer
|
||||
local sh_off = hdr.e_shoff + i * hdr.e_shentsize ---@type integer
|
||||
local entry, err = read_section_entry(adapter, sh_off) ---@type Elf32Section|nil, string|nil
|
||||
if not entry then return nil, err end
|
||||
sections[i + 1] = entry
|
||||
end
|
||||
@@ -418,20 +396,17 @@ function M.walk_sections(adapter, hdr)
|
||||
return nil, "missing_shstrtab"
|
||||
end
|
||||
|
||||
--- @type Elf32Section|nil
|
||||
local shstrtab = sections[hdr.e_shstrndx + 1]
|
||||
local shstrtab = sections[hdr.e_shstrndx + 1] ---@type Elf32Section|nil
|
||||
if not shstrtab or shstrtab.sh_type ~= M.SHT_STRTAB then
|
||||
return nil, "missing_shstrtab"
|
||||
end
|
||||
if shstrtab.sh_offset + shstrtab.sh_size > file_size then
|
||||
return nil, "truncated_section_headers"
|
||||
end
|
||||
--- @type string|nil
|
||||
local shstrtab_bytes = M.read_section_bytes(adapter, shstrtab)
|
||||
local shstrtab_bytes = M.read_section_bytes(adapter, shstrtab) ---@type string|nil
|
||||
if not shstrtab_bytes then return nil, "truncated_section_headers" end
|
||||
|
||||
--- @type integer, Elf32Section
|
||||
for _, s in ipairs(sections) do
|
||||
for _, s in ipairs(sections) do ---@type integer, Elf32Section
|
||||
s.name = M.get_str(shstrtab_bytes, s.sh_name) or ""
|
||||
end
|
||||
|
||||
@@ -444,15 +419,11 @@ end
|
||||
--- @param section Elf32Section
|
||||
--- @return string|nil
|
||||
function M.read_section_bytes(adapter, section)
|
||||
--- @type integer
|
||||
local size = section.sh_size
|
||||
local size = section.sh_size ---@type integer
|
||||
if size == 0 then return "" end
|
||||
--- @type string[]
|
||||
local out = {}
|
||||
--- @type integer
|
||||
for i = 0, size - 1 do
|
||||
--- @type integer|nil
|
||||
local b = M.read_u8(adapter, section.sh_offset + i)
|
||||
local out = {} ---@type string[]
|
||||
for i = 0, size - 1 do ---@type integer
|
||||
local b = M.read_u8(adapter, section.sh_offset + i) ---@type integer|nil
|
||||
if b == nil then return nil end
|
||||
out[#out + 1] = string.char(b)
|
||||
end
|
||||
@@ -467,11 +438,9 @@ end
|
||||
--- @return string|nil, string|nil
|
||||
function M.read_named_section(adapter, sections, name)
|
||||
if not sections then return nil, "missing_section" end
|
||||
--- @type integer, Elf32Section
|
||||
for _, s in ipairs(sections) do
|
||||
for _, s in ipairs(sections) do ---@type integer, Elf32Section
|
||||
if s.name == name then
|
||||
--- @type string|nil
|
||||
local bytes = M.read_section_bytes(adapter, s)
|
||||
local bytes = M.read_section_bytes(adapter, s) ---@type string|nil
|
||||
if not bytes then return nil, "truncated_section_data" end
|
||||
return bytes, nil
|
||||
end
|
||||
@@ -488,59 +457,42 @@ end
|
||||
--- @return table<string, Elf32Sym>|nil, string|nil
|
||||
function M.collect_symbols(adapter, sections)
|
||||
if not sections then return nil, "missing_sections" end
|
||||
--- @type table<string, Elf32Sym> -- bag: symbol name -> Elf32Sym
|
||||
local symbols = {}
|
||||
--- @type integer
|
||||
local file_size = M.size(adapter)
|
||||
--- @type integer, Elf32Section
|
||||
for _, s in ipairs(sections) do
|
||||
local symbols = {} ---@type table<string, Elf32Sym> -- bag: symbol name -> Elf32Sym
|
||||
local file_size = M.size(adapter) ---@type integer
|
||||
for _, s in ipairs(sections) do ---@type integer, Elf32Section
|
||||
if s.sh_type == M.SHT_SYMTAB then
|
||||
--- @type Elf32Section|nil
|
||||
local strtab = sections[s.sh_link + 1]
|
||||
local strtab = sections[s.sh_link + 1] ---@type Elf32Section|nil
|
||||
if not strtab or strtab.sh_type ~= M.SHT_STRTAB then
|
||||
return nil, "missing_symtab_strtab"
|
||||
end
|
||||
if strtab.sh_offset + strtab.sh_size > file_size then
|
||||
return nil, "truncated_section_headers"
|
||||
end
|
||||
--- @type string|nil
|
||||
local strtab_bytes = M.read_section_bytes(adapter, strtab)
|
||||
local strtab_bytes = M.read_section_bytes(adapter, strtab) ---@type string|nil
|
||||
if not strtab_bytes then return nil, "truncated_section_headers" end
|
||||
if s.sh_offset + s.sh_size > file_size then
|
||||
return nil, "truncated_section_headers"
|
||||
end
|
||||
--- @type string|nil
|
||||
local symtab_bytes = M.read_section_bytes(adapter, s)
|
||||
local symtab_bytes = M.read_section_bytes(adapter, s) ---@type string|nil
|
||||
if not symtab_bytes then return nil, "truncated_section_headers" end
|
||||
--- @type number
|
||||
local n = #symtab_bytes / M.ELF32_SYM.sym_entry_bytes
|
||||
--- @type integer
|
||||
for j = 0, n - 1 do
|
||||
--- @type integer
|
||||
local e = s.sh_offset + j * M.ELF32_SYM.sym_entry_bytes
|
||||
--- @type integer|nil
|
||||
local st_name = M.read_u32(adapter, e + M.ELF32_SYM.st_name)
|
||||
local n = #symtab_bytes / M.ELF32_SYM.sym_entry_bytes ---@type number
|
||||
for j = 0, n - 1 do ---@type integer
|
||||
local e = s.sh_offset + j * M.ELF32_SYM.sym_entry_bytes ---@type integer
|
||||
local st_name = M.read_u32(adapter, e + M.ELF32_SYM.st_name) ---@type integer|nil
|
||||
if st_name then
|
||||
--- @type integer|nil
|
||||
local st_value = M.read_u32(adapter, e + M.ELF32_SYM.st_value)
|
||||
--- @type integer|nil
|
||||
local st_size = M.read_u32(adapter, e + M.ELF32_SYM.st_size)
|
||||
--- @type integer|nil
|
||||
local st_info = M.read_u8(adapter, e + M.ELF32_SYM.st_info)
|
||||
local st_value = M.read_u32(adapter, e + M.ELF32_SYM.st_value) ---@type integer|nil
|
||||
local st_size = M.read_u32(adapter, e + M.ELF32_SYM.st_size) ---@type integer|nil
|
||||
local st_info = M.read_u8(adapter, e + M.ELF32_SYM.st_info) ---@type integer|nil
|
||||
-- st_shndx is at offset 14 (2 bytes) — derived from the layout
|
||||
-- the metaprogram reads too. Inline the read to keep the
|
||||
-- adapter as the only I/O surface.
|
||||
--- @type integer|nil
|
||||
local b1 = M.read_u8(adapter, e + 14)
|
||||
--- @type integer|nil
|
||||
local b2 = M.read_u8(adapter, e + 15)
|
||||
local b1 = M.read_u8(adapter, e + 14) ---@type integer|nil
|
||||
local b2 = M.read_u8(adapter, e + 15) ---@type integer|nil
|
||||
if not (b1 and b2) then
|
||||
return nil, "truncated_section_headers"
|
||||
end
|
||||
--- @type integer
|
||||
local st_shndx = b1 + b2 * 0x100
|
||||
--- @type string
|
||||
local name = M.get_str(strtab_bytes, st_name) or ""
|
||||
local st_shndx = b1 + b2 * 0x100 ---@type integer
|
||||
local name = M.get_str(strtab_bytes, st_name) or "" ---@type string
|
||||
if name ~= "" then
|
||||
symbols[name] = {
|
||||
value = st_value,
|
||||
|
||||
+151
-301
@@ -111,17 +111,14 @@
|
||||
--- @field sleb128_size fun(n: integer): integer
|
||||
--- @field read_line_unit_file_table fun(elf_path: string): table<string, integer>|nil, table<integer, string>|nil, table<integer, string>|nil
|
||||
|
||||
--- @type LfsMod
|
||||
local lfs = require("lfs")
|
||||
local lfs = require("lfs") ---@type LfsMod
|
||||
|
||||
-- scripts/elf32.lua contains format-constant tables + the byte-level walker.
|
||||
-- The this file re-exports `read_u32_le` / `read_u16_le` (and the DWARF32 terminator).
|
||||
-- read_u32_le is this module's reader; implementation in elf32.lua.
|
||||
--- @type Elf32Mod
|
||||
local E = require("elf32")
|
||||
local E = require("elf32") ---@type Elf32Mod
|
||||
|
||||
--- @type ElfDwarf
|
||||
local M = {}
|
||||
local M = {} ---@type ElfDwarf
|
||||
|
||||
-- ════════════════════════════════════════════════════════════════════════════
|
||||
-- DWARF tag + form constants
|
||||
@@ -200,8 +197,7 @@ M.DW_ATE = {
|
||||
}
|
||||
|
||||
-- DWARF5 §7.5.6 DW_FORM_implicit_const
|
||||
--- @type integer
|
||||
local DW_FORM_implicit_const = 0x21
|
||||
local DW_FORM_implicit_const = 0x21 ---@type integer
|
||||
|
||||
-- ════════════════════════════════════════════════════════════════════════════
|
||||
-- Format-constant tables
|
||||
@@ -378,13 +374,10 @@ end
|
||||
--- @return integer|nil
|
||||
--- @return integer
|
||||
function M.read_uleb128_at(buf, pos)
|
||||
--- @type integer, integer
|
||||
local value, shift = 0, 0
|
||||
--- @type integer
|
||||
local len = #buf
|
||||
local value, shift = 0, 0 ---@type integer, integer
|
||||
local len = #buf ---@type integer
|
||||
while pos < len do
|
||||
--- @type integer
|
||||
local b = buf:byte(pos + 1)
|
||||
local b = buf:byte(pos + 1) ---@type integer
|
||||
value = value + (b % 0x80) * (2 ^ shift)
|
||||
shift = shift + 7
|
||||
pos = pos + 1
|
||||
@@ -398,13 +391,10 @@ end
|
||||
--- @return integer|nil
|
||||
--- @return integer
|
||||
function M.read_sleb128_at(buf, pos)
|
||||
--- @type integer, integer
|
||||
local value, shift = 0, 0
|
||||
--- @type integer
|
||||
local len = #buf
|
||||
local value, shift = 0, 0 ---@type integer, integer
|
||||
local len = #buf ---@type integer
|
||||
while pos < len do
|
||||
--- @type integer
|
||||
local b = buf:byte(pos + 1)
|
||||
local b = buf:byte(pos + 1) ---@type integer
|
||||
value = value + (b % 0x80) * (2 ^ shift)
|
||||
shift = shift + 7
|
||||
pos = pos + 1
|
||||
@@ -423,33 +413,27 @@ end
|
||||
--- @param table_start integer
|
||||
--- @return integer|nil
|
||||
function M.find_abbrev_table_end(table_bytes, table_start)
|
||||
--- @type integer, integer
|
||||
local pos, len = table_start, #table_bytes
|
||||
local pos, len = table_start, #table_bytes ---@type integer, integer
|
||||
if pos >= len or table_bytes:byte(pos + 1) == 0 then return pos end
|
||||
while pos < len do
|
||||
--- @type integer|nil, integer
|
||||
local _code, code_end = M.read_uleb128_at(table_bytes, pos)
|
||||
local _code, code_end = M.read_uleb128_at(table_bytes, pos) ---@type integer|nil, integer
|
||||
if not _code then return nil end
|
||||
pos = code_end
|
||||
--- @type integer|nil, integer
|
||||
local _tag, tag_end = M.read_uleb128_at(table_bytes, pos)
|
||||
local _tag, tag_end = M.read_uleb128_at(table_bytes, pos) ---@type integer|nil, integer
|
||||
if not _tag then return nil end
|
||||
pos = tag_end
|
||||
if pos >= len then return nil end
|
||||
pos = pos + 1 -- has_children byte
|
||||
while pos < len do
|
||||
--- @type integer|nil, integer
|
||||
local attr, attr_end = M.read_uleb128_at(table_bytes, pos)
|
||||
local attr, attr_end = M.read_uleb128_at(table_bytes, pos) ---@type integer|nil, integer
|
||||
if not attr then return nil end
|
||||
pos = attr_end
|
||||
--- @type integer|nil, integer
|
||||
local form, form_end = M.read_uleb128_at(table_bytes, pos)
|
||||
local form, form_end = M.read_uleb128_at(table_bytes, pos) ---@type integer|nil, integer
|
||||
if not form then return nil end
|
||||
pos = form_end
|
||||
if attr == 0 and form == 0 then break end
|
||||
if form == DW_FORM_implicit_const then
|
||||
--- @type integer|nil, integer
|
||||
local _c, ce = M.read_sleb128_at(table_bytes, pos)
|
||||
local _c, ce = M.read_sleb128_at(table_bytes, pos) ---@type integer|nil, integer
|
||||
if not _c then return nil end
|
||||
pos = ce
|
||||
end
|
||||
@@ -466,10 +450,8 @@ end
|
||||
--- @param off integer
|
||||
--- @return string
|
||||
local function read_c_string_at(buf, off)
|
||||
--- @type integer
|
||||
local len = #buf
|
||||
--- @type integer
|
||||
local start = off
|
||||
local len = #buf ---@type integer
|
||||
local start = off ---@type integer
|
||||
while off < len and buf:byte(off + 1) ~= 0 do off = off + 1 end
|
||||
return buf:sub(start + 1, off)
|
||||
end
|
||||
@@ -482,41 +464,31 @@ end
|
||||
--- @return AbbrevDecl[]|nil
|
||||
--- @return string|nil
|
||||
local function parse_abbrev_table(table_bytes, table_start)
|
||||
--- @type integer|nil
|
||||
local table_end = M.find_abbrev_table_end(table_bytes, table_start)
|
||||
local table_end = M.find_abbrev_table_end(table_bytes, table_start) ---@type integer|nil
|
||||
if not table_end then return nil, "no terminator" end
|
||||
--- @type AbbrevDecl[]
|
||||
local decls = {}
|
||||
--- @type integer
|
||||
local pos = table_start
|
||||
local decls = {} ---@type AbbrevDecl[]
|
||||
local pos = table_start ---@type integer
|
||||
while pos < table_end do
|
||||
--- @type integer|nil, integer
|
||||
local code, code_end = M.read_uleb128_at(table_bytes, pos)
|
||||
local code, code_end = M.read_uleb128_at(table_bytes, pos) ---@type integer|nil, integer
|
||||
if not code then return nil, "truncated code" end
|
||||
pos = code_end
|
||||
--- @type integer|nil, integer
|
||||
local tag, tag_end = M.read_uleb128_at(table_bytes, pos)
|
||||
local tag, tag_end = M.read_uleb128_at(table_bytes, pos) ---@type integer|nil, integer
|
||||
if not tag then return nil, "truncated tag" end
|
||||
pos = tag_end
|
||||
--- @type integer
|
||||
local has_children = table_bytes:byte(pos + 1)
|
||||
local has_children = table_bytes:byte(pos + 1) ---@type integer
|
||||
pos = pos + 1
|
||||
--- @type AbbrevAttr[]
|
||||
local attrs = {}
|
||||
local attrs = {} ---@type AbbrevAttr[]
|
||||
while true do
|
||||
--- @type integer|nil, integer
|
||||
local attr, attr_end = M.read_uleb128_at(table_bytes, pos)
|
||||
local attr, attr_end = M.read_uleb128_at(table_bytes, pos) ---@type integer|nil, integer
|
||||
if not attr then return nil, "truncated attr" end
|
||||
pos = attr_end
|
||||
--- @type integer|nil, integer
|
||||
local form, form_end = M.read_uleb128_at(table_bytes, pos)
|
||||
local form, form_end = M.read_uleb128_at(table_bytes, pos) ---@type integer|nil, integer
|
||||
if not form then return nil, "truncated form" end
|
||||
pos = form_end
|
||||
if attr == 0 and form == 0 then break end
|
||||
attrs[#attrs + 1] = { name = attr, form = form }
|
||||
if form == DW_FORM_implicit_const then
|
||||
--- @type integer|nil, integer
|
||||
local _c, ce = M.read_sleb128_at(table_bytes, pos)
|
||||
local _c, ce = M.read_sleb128_at(table_bytes, pos) ---@type integer|nil, integer
|
||||
if not _c then return nil, "truncated const" end
|
||||
pos = ce
|
||||
end
|
||||
@@ -531,8 +503,7 @@ end
|
||||
-- For DW_FORM_strp we return the inline string resolved from `str_buf`.
|
||||
-- For DW_FORM_ref4 we return the absolute CU-relative offset.
|
||||
-- The caller decides whether to interpret that as a section offset.
|
||||
--- @type table<integer, fun(buf: string, str_buf: string, pos: integer): (string|integer|nil, integer)>
|
||||
local FORM_READERS = {
|
||||
local FORM_READERS = { ---@type table<integer, fun(buf: string, str_buf: string, pos: integer): (string|integer|nil, integer)>
|
||||
--- @param buf string
|
||||
--- @param _ string
|
||||
--- @param pos integer
|
||||
@@ -547,8 +518,7 @@ local FORM_READERS = {
|
||||
--- @return string
|
||||
--- @return integer
|
||||
[M.DW_FORM.string] = function(buf, _, pos)
|
||||
--- @type string
|
||||
local s = read_c_string_at(buf, pos)
|
||||
local s = read_c_string_at(buf, pos) ---@type string
|
||||
return s, pos + #s + 1
|
||||
end,
|
||||
--- @param buf string
|
||||
@@ -558,8 +528,7 @@ local FORM_READERS = {
|
||||
--- @return integer
|
||||
[M.DW_FORM.strp] = function(buf, str_buf, pos)
|
||||
-- DW_FORM_strp: 4-byte offset into .debug_str.
|
||||
--- @type integer
|
||||
local strp_off = M.read_u32_le(buf, pos)
|
||||
local strp_off = M.read_u32_le(buf, pos) ---@type integer
|
||||
return read_c_string_at(str_buf, strp_off), pos + 4
|
||||
end,
|
||||
--- @param buf string
|
||||
@@ -627,8 +596,7 @@ local FORM_READERS = {
|
||||
--- @return integer
|
||||
[M.DW_FORM.exprloc] = function(buf, _, pos)
|
||||
-- DW_FORM_exprloc: ULEB byte count + that many bytes of DW_OP_*.
|
||||
--- @type integer|nil, integer
|
||||
local len, ne = M.read_uleb128_at(buf, pos)
|
||||
local len, ne = M.read_uleb128_at(buf, pos) ---@type integer|nil, integer
|
||||
if not len then return nil, pos end
|
||||
return nil, ne + len
|
||||
end,
|
||||
@@ -654,8 +622,7 @@ local FORM_READERS = {
|
||||
-- then the high 4 to resolve the specific type within it.
|
||||
-- Return the low 4 as the primary value to preserve the (value, next_pos) shape;
|
||||
-- the high 4 is exposed via M.read_ref_sig8 (which returns both halves).
|
||||
--- @type integer, integer, integer
|
||||
local _, _, next_pos = M.read_ref_sig8(buf, pos)
|
||||
local _, _, next_pos = M.read_ref_sig8(buf, pos) ---@type integer, integer, integer
|
||||
return M.read_u32_le(buf, pos), next_pos
|
||||
end,
|
||||
}
|
||||
@@ -666,8 +633,7 @@ local FORM_READERS = {
|
||||
--- @return string|integer|nil
|
||||
--- @return integer
|
||||
local function read_form_value(buf, str_buf, pos, form)
|
||||
--- @type (fun(buf: string, str_buf: string, pos: integer): (string|integer|nil, integer))|nil
|
||||
local r = FORM_READERS[form]
|
||||
local r = FORM_READERS[form] ---@type (fun(buf: string, str_buf: string, pos: integer): (string|integer|nil, integer))|nil
|
||||
if not r then
|
||||
return nil, pos
|
||||
end
|
||||
@@ -701,21 +667,16 @@ function M.read_ref_sig8(buf, pos) return M.read_u32_le(buf, pos), M.read_u32_le
|
||||
--- @param target_sig_hi integer -- high 4 bytes (LE) of the desired signature
|
||||
--- @return integer|nil, integer|nil -- unit offset, type_offset within the unit
|
||||
function M.find_type_unit_by_signature(info, target_sig_lo, target_sig_hi)
|
||||
--- @type integer
|
||||
local pos = 0
|
||||
--- @type integer
|
||||
local section_len = #info
|
||||
local pos = 0 ---@type integer
|
||||
local section_len = #info ---@type integer
|
||||
while pos + 4 < section_len do
|
||||
--- @type integer
|
||||
local unit_length = M.read_u32_le(info, pos)
|
||||
local unit_length = M.read_u32_le(info, pos) ---@type integer
|
||||
if unit_length == 0xFFFFFFFF then
|
||||
return nil, nil -- DWARF64 not supported
|
||||
end
|
||||
-- unit_length is the body size, NOT including the 4-byte unit_length field itself.
|
||||
--- @type integer
|
||||
local body_start = pos + 4
|
||||
--- @type integer
|
||||
local body_end = body_start + unit_length
|
||||
local body_start = pos + 4 ---@type integer
|
||||
local body_end = body_start + unit_length ---@type integer
|
||||
if body_end > section_len then
|
||||
return nil, nil -- malformed
|
||||
end
|
||||
@@ -737,14 +698,11 @@ function M.find_type_unit_by_signature(info, target_sig_lo, target_sig_hi)
|
||||
-- byte 4-7: debug_abbrev_offset (4)
|
||||
-- byte 8-15: type_signature (8)
|
||||
-- byte 16-19: type_offset (4)
|
||||
--- @type integer
|
||||
local unit_type = info:byte(body_start + 2 + 1) -- 0-based +2 = unit_type in 1-indexed
|
||||
local unit_type = info:byte(body_start + 2 + 1) ---@type integer -- 0-based +2 = unit_type in 1-indexed
|
||||
if unit_type == 0x02 then -- DW_UT_type
|
||||
--- @type integer, integer, integer
|
||||
local sig_lo, sig_hi, _ = M.read_ref_sig8(info, body_start + 8) -- 0-based +8 = type_signature in 1-indexed
|
||||
local sig_lo, sig_hi, _ = M.read_ref_sig8(info, body_start + 8) ---@type integer, integer, integer -- 0-based +8 = type_signature in 1-indexed
|
||||
if sig_lo == target_sig_lo and sig_hi == target_sig_hi then
|
||||
--- @type integer
|
||||
local type_offset = M.read_u32_le(info, body_start + 16) -- 0-based +16 = type_offset in 1-indexed
|
||||
local type_offset = M.read_u32_le(info, body_start + 16) ---@type integer -- 0-based +16 = type_offset in 1-indexed
|
||||
return pos, type_offset
|
||||
end
|
||||
end
|
||||
@@ -796,16 +754,12 @@ end
|
||||
function M.read_elf_sections(elf_path, section_names)
|
||||
-- Initialize result with all requested names set to "" so callers can do `sections[X]
|
||||
-- or ""` for missing sections without nil-checks.
|
||||
--- @type table<string, string>
|
||||
local result = {}
|
||||
--- @type integer, string
|
||||
for _, name in ipairs(section_names) do result[name] = "" end
|
||||
local result = {} ---@type table<string, string>
|
||||
for _, name in ipairs(section_names) do result[name] = "" end ---@type integer, string
|
||||
|
||||
-- O(1) lookup set.
|
||||
--- @type table<string, boolean> -- bag: requested section name -> true
|
||||
local wanted = {}
|
||||
--- @type integer, string
|
||||
for _, name in ipairs(section_names) do wanted[name] = true end
|
||||
local wanted = {} ---@type table<string, boolean> -- bag: requested section name -> true
|
||||
for _, name in ipairs(section_names) do wanted[name] = true end ---@type integer, string
|
||||
|
||||
-- Existence check (lfs.attributes avoids an io.open-vs-fail race).
|
||||
if lfs.attributes(elf_path, "mode") ~= "file" then
|
||||
@@ -813,27 +767,23 @@ function M.read_elf_sections(elf_path, section_names)
|
||||
return result
|
||||
end
|
||||
|
||||
--- @type file*|nil
|
||||
local f = io.open(elf_path, "rb")
|
||||
local f = io.open(elf_path, "rb") ---@type file*|nil
|
||||
if not f then
|
||||
io.stderr:write(string.format("[elf_dwarf.read_elf_sections] io.open failed: %s\n", elf_path))
|
||||
return result
|
||||
end
|
||||
|
||||
--- @type integer
|
||||
local file_size
|
||||
local file_size ---@type integer
|
||||
do
|
||||
f:seek("end", 0)
|
||||
file_size = f:seek("cur", 0)
|
||||
end
|
||||
--- @type Elf32Adapter
|
||||
local adapter = {
|
||||
local adapter = { ---@type Elf32Adapter
|
||||
--- @param offset integer
|
||||
--- @return integer|nil
|
||||
read_u8_at = function(offset)
|
||||
f:seek("set", offset)
|
||||
--- @type string|nil
|
||||
local b = f:read(1)
|
||||
local b = f:read(1) ---@type string|nil
|
||||
if not b then return nil end
|
||||
return b:byte()
|
||||
end,
|
||||
@@ -841,10 +791,8 @@ function M.read_elf_sections(elf_path, section_names)
|
||||
--- @return integer|nil
|
||||
read_u16_at = function(offset)
|
||||
f:seek("set", offset)
|
||||
--- @type string|nil
|
||||
local b1 = f:read(1)
|
||||
--- @type string|nil
|
||||
local b2 = f:read(1)
|
||||
local b1 = f:read(1) ---@type string|nil
|
||||
local b2 = f:read(1) ---@type string|nil
|
||||
if not b1 or not b2 then return nil end
|
||||
return b1:byte() + b2:byte() * 0x100
|
||||
end,
|
||||
@@ -852,14 +800,10 @@ function M.read_elf_sections(elf_path, section_names)
|
||||
--- @return integer|nil
|
||||
read_u32_at = function(offset)
|
||||
f:seek("set", offset)
|
||||
--- @type string|nil
|
||||
local b1 = f:read(1)
|
||||
--- @type string|nil
|
||||
local b2 = f:read(1)
|
||||
--- @type string|nil
|
||||
local b3 = f:read(1)
|
||||
--- @type string|nil
|
||||
local b4 = f:read(1)
|
||||
local b1 = f:read(1) ---@type string|nil
|
||||
local b2 = f:read(1) ---@type string|nil
|
||||
local b3 = f:read(1) ---@type string|nil
|
||||
local b4 = f:read(1) ---@type string|nil
|
||||
if not b1 or not b2 or not b3 or not b4 then return nil end
|
||||
return b1:byte() + b2:byte() * 0x100
|
||||
+ b3:byte() * 0x10000 + b4:byte() * 0x1000000
|
||||
@@ -869,16 +813,14 @@ function M.read_elf_sections(elf_path, section_names)
|
||||
}
|
||||
|
||||
-- Delegate the header parse + section walk to E.*.
|
||||
--- @type Elf32Header|nil, string|nil
|
||||
local hdr, hdr_err = E.parse_elf32_headers(adapter)
|
||||
local hdr, hdr_err = E.parse_elf32_headers(adapter) ---@type Elf32Header|nil, string|nil
|
||||
if not hdr then
|
||||
io.stderr:write(string.format("[elf_dwarf.read_elf_sections] header parse failed: %s\n", tostring(hdr_err)))
|
||||
f:close()
|
||||
return result
|
||||
end
|
||||
|
||||
--- @type Elf32Section[]|nil, string|nil
|
||||
local sections, walk_err = E.walk_sections(adapter, hdr)
|
||||
local sections, walk_err = E.walk_sections(adapter, hdr) ---@type Elf32Section[]|nil, string|nil
|
||||
if not sections then
|
||||
io.stderr:write(string.format("[elf_dwarf.read_elf_sections] section walk failed: %s\n", tostring(walk_err)))
|
||||
f:close()
|
||||
@@ -886,11 +828,9 @@ function M.read_elf_sections(elf_path, section_names)
|
||||
end
|
||||
|
||||
-- Resolve the requested sections.
|
||||
--- @type integer, Elf32Section
|
||||
for _, s in ipairs(sections) do
|
||||
for _, s in ipairs(sections) do ---@type integer, Elf32Section
|
||||
if wanted[s.name] then
|
||||
--- @type string|nil
|
||||
local bytes = E.read_section_bytes(adapter, s)
|
||||
local bytes = E.read_section_bytes(adapter, s) ---@type string|nil
|
||||
if bytes then result[s.name] = bytes end
|
||||
end
|
||||
end
|
||||
@@ -912,31 +852,26 @@ end
|
||||
--- @param elf_path Path
|
||||
--- @return table<string, NmAddr>
|
||||
function M.read_nm(elf_path)
|
||||
--- @type table<string, NmAddr>
|
||||
local addrs = {}
|
||||
local addrs = {} ---@type table<string, NmAddr>
|
||||
|
||||
-- Existence check first; an empty or missing ELF returns an empty map.
|
||||
if lfs.attributes(elf_path, "mode") ~= "file" then return addrs end
|
||||
|
||||
--- @type file*|nil
|
||||
local f = io.open(elf_path, "rb")
|
||||
local f = io.open(elf_path, "rb") ---@type file*|nil
|
||||
if not f then return addrs end
|
||||
|
||||
-- Build the file adapter for E.*.
|
||||
--- @type integer
|
||||
local file_size
|
||||
local file_size ---@type integer
|
||||
do
|
||||
f:seek("end", 0)
|
||||
file_size = f:seek("cur", 0)
|
||||
end
|
||||
--- @type Elf32Adapter
|
||||
local adapter = {
|
||||
local adapter = { ---@type Elf32Adapter
|
||||
--- @param offset integer
|
||||
--- @return integer|nil
|
||||
read_u8_at = function(offset)
|
||||
f:seek("set", offset)
|
||||
--- @type string|nil
|
||||
local b = f:read(1)
|
||||
local b = f:read(1) ---@type string|nil
|
||||
if not b then return nil end
|
||||
return b:byte()
|
||||
end,
|
||||
@@ -944,10 +879,8 @@ function M.read_nm(elf_path)
|
||||
--- @return integer|nil
|
||||
read_u16_at = function(offset)
|
||||
f:seek("set", offset)
|
||||
--- @type string|nil
|
||||
local b1 = f:read(1)
|
||||
--- @type string|nil
|
||||
local b2 = f:read(1)
|
||||
local b1 = f:read(1) ---@type string|nil
|
||||
local b2 = f:read(1) ---@type string|nil
|
||||
if not b1 or not b2 then return nil end
|
||||
return b1:byte() + b2:byte() * 0x100
|
||||
end,
|
||||
@@ -955,14 +888,10 @@ function M.read_nm(elf_path)
|
||||
--- @return integer|nil
|
||||
read_u32_at = function(offset)
|
||||
f:seek("set", offset)
|
||||
--- @type string|nil
|
||||
local b1 = f:read(1)
|
||||
--- @type string|nil
|
||||
local b2 = f:read(1)
|
||||
--- @type string|nil
|
||||
local b3 = f:read(1)
|
||||
--- @type string|nil
|
||||
local b4 = f:read(1)
|
||||
local b1 = f:read(1) ---@type string|nil
|
||||
local b2 = f:read(1) ---@type string|nil
|
||||
local b3 = f:read(1) ---@type string|nil
|
||||
local b4 = f:read(1) ---@type string|nil
|
||||
if not b1 or not b2 or not b3 or not b4 then return nil end
|
||||
return b1:byte() + b2:byte() * 0x100
|
||||
+ b3:byte() * 0x10000 + b4:byte() * 0x1000000
|
||||
@@ -972,16 +901,14 @@ function M.read_nm(elf_path)
|
||||
}
|
||||
|
||||
-- Delegate the header + section walk to E.*.
|
||||
--- @type Elf32Header|nil, string|nil
|
||||
local hdr, hdr_err = E.parse_elf32_headers(adapter)
|
||||
local hdr, hdr_err = E.parse_elf32_headers(adapter) ---@type Elf32Header|nil, string|nil
|
||||
if not hdr then
|
||||
io.stderr:write(string.format("[elf_dwarf.read_nm] header parse failed: %s\n", tostring(hdr_err)))
|
||||
f:close()
|
||||
return addrs
|
||||
end
|
||||
|
||||
--- @type Elf32Section[]|nil, string|nil
|
||||
local sections, walk_err = E.walk_sections(adapter, hdr)
|
||||
local sections, walk_err = E.walk_sections(adapter, hdr) ---@type Elf32Section[]|nil, string|nil
|
||||
if not sections then
|
||||
io.stderr:write(string.format("[elf_dwarf.read_nm] section walk failed: %s\n", tostring(walk_err)))
|
||||
f:close()
|
||||
@@ -990,8 +917,7 @@ function M.read_nm(elf_path)
|
||||
|
||||
-- E.collect_symbols returns every defined symbol (no binding filter).
|
||||
-- The metaprogram then applies its STB_LOCAL / STB_GLOBAL + size>0 filter, matching `nm`'s default (external symbols only).
|
||||
--- @type table<string, Elf32Sym>|nil, string|nil
|
||||
local symbols, sym_err = E.collect_symbols(adapter, sections)
|
||||
local symbols, sym_err = E.collect_symbols(adapter, sections) ---@type table<string, Elf32Sym>|nil, string|nil
|
||||
if not symbols then
|
||||
io.stderr:write(string.format("[elf_dwarf.read_nm] symbol collection failed: %s\n", tostring(sym_err)))
|
||||
f:close()
|
||||
@@ -1000,12 +926,10 @@ function M.read_nm(elf_path)
|
||||
|
||||
f:close()
|
||||
|
||||
--- @type string, Elf32Sym
|
||||
for name, entry in pairs(symbols) do
|
||||
for name, entry in pairs(symbols) do ---@type string, Elf32Sym
|
||||
-- High nibble of st_info = binding (STB_LOCAL=0, STB_GLOBAL=1, STB_WEAK=2).
|
||||
-- math.floor(/16) is portable across LuaJIT 2.0/2.1 and plain Lua 5.x.
|
||||
--- @type integer
|
||||
local binding = math.floor(entry.info / 16)
|
||||
local binding = math.floor(entry.info / 16) ---@type integer
|
||||
if (binding == 0 or binding == 1) and entry.size > 0 then
|
||||
addrs[name] = { entry.value, entry.size }
|
||||
end
|
||||
@@ -1037,17 +961,14 @@ end
|
||||
-- Spec: DWARF5 §7.6 "Variable-Length Data" / Appendix C.
|
||||
|
||||
-- Top bit of each LEB128 byte. Set if more bytes follow in the encoding.
|
||||
--- @type integer
|
||||
local LEB_CONT_BIT = 0x80
|
||||
local LEB_CONT_BIT = 0x80 ---@type integer
|
||||
|
||||
-- Low 7 bits of each LEB128 byte. The actual data payload.
|
||||
--- @type integer
|
||||
local LEB_DATA_MASK = 0x7F
|
||||
local LEB_DATA_MASK = 0x7F ---@type integer
|
||||
|
||||
-- Bit 6 of the 7-bit data (i.e. 0x40). For SLEB128: the sign-bit position used by the decoder for sign extension.
|
||||
-- Encoders MUST stop when the next byte would be redundant AND the sign bit in the last byte matches the value's sign.
|
||||
--- @type integer
|
||||
local SLEB_SIGN_BIT = 0x40
|
||||
local SLEB_SIGN_BIT = 0x40 ---@type integer
|
||||
|
||||
--- ULEB128 (Unsigned Little-Endian Base 128) encoder. Returns the byte string for the non-negative integer `n`.
|
||||
--- Algorithm:
|
||||
@@ -1064,11 +985,9 @@ function M.uleb128(n)
|
||||
error("uleb128 requires non-negative number")
|
||||
end
|
||||
assert(n >= 0, "uleb128 requires non-negative input")
|
||||
--- @type string[]
|
||||
local bytes = {}
|
||||
local bytes = {} ---@type string[]
|
||||
repeat
|
||||
--- @type integer
|
||||
local b = n % (LEB_DATA_MASK + 1) -- extract low 7 bits
|
||||
local b = n % (LEB_DATA_MASK + 1) ---@type integer -- extract low 7 bits
|
||||
n = (n - b) / (LEB_DATA_MASK + 1) -- shift right by 7 bits
|
||||
if n > 0 then b = b + LEB_CONT_BIT end -- set continuation bit if more bytes follow
|
||||
bytes[#bytes + 1] = string.char(b)
|
||||
@@ -1087,13 +1006,10 @@ end
|
||||
--- @param n integer -- any integer (negative allowed)
|
||||
--- @return string
|
||||
function M.sleb128(n)
|
||||
--- @type string[]
|
||||
local bytes = {}
|
||||
--- @type boolean
|
||||
local more = true
|
||||
local bytes = {} ---@type string[]
|
||||
local more = true ---@type boolean
|
||||
while more do
|
||||
--- @type integer
|
||||
local b = n % (LEB_DATA_MASK + 1) -- extract low 7 bits
|
||||
local b = n % (LEB_DATA_MASK + 1) ---@type integer -- extract low 7 bits
|
||||
n = (n - b) / (LEB_DATA_MASK + 1) -- arithmetic shift right by 7
|
||||
-- Termination: remaining value bits fit in the sign bit of the last byte.
|
||||
if n == 0 and b < SLEB_SIGN_BIT then more = false end -- positive terminator
|
||||
@@ -1112,8 +1028,7 @@ end
|
||||
function M.uleb128_size(n)
|
||||
assert(n >= 0, "uleb128_size requires non-negative input")
|
||||
if n == 0 then return 1 end
|
||||
--- @type integer
|
||||
local bytes = 1
|
||||
local bytes = 1 ---@type integer
|
||||
while n >= 0x80 do
|
||||
n = (n - (n % (LEB_DATA_MASK + 1))) / (LEB_DATA_MASK + 1) -- arithmetic shift right by 7
|
||||
bytes = bytes + 1
|
||||
@@ -1129,15 +1044,11 @@ end
|
||||
--- @param n integer -- any integer (negative allowed)
|
||||
--- @return integer
|
||||
function M.sleb128_size(n)
|
||||
--- @type boolean
|
||||
local more = true
|
||||
--- @type integer
|
||||
local bytes = 0
|
||||
--- @type integer
|
||||
local v = n
|
||||
local more = true ---@type boolean
|
||||
local bytes = 0 ---@type integer
|
||||
local v = n ---@type integer
|
||||
while more do
|
||||
--- @type integer
|
||||
local b = v % (LEB_DATA_MASK + 1) -- extract low 7 bits
|
||||
local b = v % (LEB_DATA_MASK + 1) ---@type integer -- extract low 7 bits
|
||||
v = (v - b) / (LEB_DATA_MASK + 1) -- arithmetic shift right by 7
|
||||
if v == 0 and b < SLEB_SIGN_BIT then more = false end -- positive terminator
|
||||
if v == -1 and b >= SLEB_SIGN_BIT then more = false end -- negative terminator
|
||||
@@ -1179,23 +1090,17 @@ end
|
||||
--- @return table<integer, string>|nil
|
||||
--- @return table<integer, string>|nil
|
||||
function M.read_line_unit_file_table(elf_path)
|
||||
--- @type table<string, string>
|
||||
local sections = M.read_elf_sections(elf_path, { ".debug_line", ".debug_line_str" })
|
||||
--- @type string
|
||||
local line = sections[".debug_line"]
|
||||
--- @type string
|
||||
local lstr = sections[".debug_line_str"] or ""
|
||||
local sections = M.read_elf_sections(elf_path, { ".debug_line", ".debug_line_str" }) ---@type table<string, string>
|
||||
local line = sections[".debug_line"] ---@type string
|
||||
local lstr = sections[".debug_line_str"] or "" ---@type string
|
||||
if not line or line == "" then
|
||||
io.stderr:write("[elf_dwarf.read_line_unit_file_table] no .debug_line section in: " .. tostring(elf_path) .. "\n")
|
||||
return nil
|
||||
end
|
||||
|
||||
--- @type table<integer, string> -- bag: 1-based file index -> basename
|
||||
local basenames = {}
|
||||
--- @type table<string, integer> -- bag: basename -> 1-based file index
|
||||
local basename_to_index = {}
|
||||
--- @type table<integer, string> -- bag: 1-based file index -> full path
|
||||
local paths = {}
|
||||
local basenames = {} ---@type table<integer, string> -- bag: 1-based file index -> basename
|
||||
local basename_to_index = {} ---@type table<string, integer> -- bag: basename -> 1-based file index
|
||||
local paths = {} ---@type table<integer, string> -- bag: 1-based file index -> full path
|
||||
|
||||
--- Read one form-code's bytes from `buf` at position `p` according to `form`.
|
||||
--- Returns (value, after) where `value` is:
|
||||
@@ -1210,18 +1115,14 @@ function M.read_line_unit_file_table(elf_path)
|
||||
--- @return integer
|
||||
local function read_form(buf, lstr_buf, p, form)
|
||||
if form == M.DWARF5_DEBUG_LINE.form_line_strp then
|
||||
--- @type integer
|
||||
local strp = M.read_u32_le(buf, p)
|
||||
--- @type integer
|
||||
local end_pos = lstr_buf:find("\0", strp + 1, true) or (#lstr_buf + 1)
|
||||
local strp = M.read_u32_le(buf, p) ---@type integer
|
||||
local end_pos = lstr_buf:find("\0", strp + 1, true) or (#lstr_buf + 1) ---@type integer
|
||||
return lstr_buf:sub(strp + 1, end_pos - 1), p + M.DWARF5_DEBUG_LINE.form_strp_bytes
|
||||
elseif form == M.DWARF5_DEBUG_LINE.form_string then
|
||||
--- @type integer
|
||||
local nul = buf:find("\0", p + 1, true) or (#buf + 1)
|
||||
local nul = buf:find("\0", p + 1, true) or (#buf + 1) ---@type integer
|
||||
return buf:sub(p + 1, nul - 1), nul
|
||||
elseif form == M.DWARF5_DEBUG_LINE.form_udata then
|
||||
--- @type integer|nil, integer
|
||||
local v, after = M.read_uleb128_at(buf, p)
|
||||
local v, after = M.read_uleb128_at(buf, p) ---@type integer|nil, integer
|
||||
return v, after
|
||||
elseif form == M.DWARF5_DEBUG_LINE.form_data16 then
|
||||
return nil, p + M.DWARF5_DEBUG_LINE.form_data16_bytes
|
||||
@@ -1243,46 +1144,32 @@ function M.read_line_unit_file_table(elf_path)
|
||||
--- @return table<integer, string>
|
||||
--- @return table<integer, string>
|
||||
local function parse_dwarf3_unit(buf, content_start, body_end)
|
||||
--- @type integer
|
||||
local up = content_start
|
||||
local up = content_start ---@type integer
|
||||
-- 5 fixed bytes: min_insn, default_is, line_base (signed), line_range, opcode_base
|
||||
up = up + 5
|
||||
--- @type integer
|
||||
local opcode_base = buf:byte(content_start + 5)
|
||||
local opcode_base = buf:byte(content_start + 5) ---@type integer
|
||||
up = up + (opcode_base - 1) -- std_opcode_lengths
|
||||
--- @type string[]
|
||||
local dirs = {}
|
||||
local dirs = {} ---@type string[]
|
||||
while up < body_end do
|
||||
--- @type integer
|
||||
local nul = buf:find("\0", up + 1, true) or (body_end + 1)
|
||||
local nul = buf:find("\0", up + 1, true) or (body_end + 1) ---@type integer
|
||||
if nul > body_end then break end
|
||||
--- @type integer
|
||||
local len = nul - up - 1
|
||||
local len = nul - up - 1 ---@type integer
|
||||
if len == 0 then up = nul break end
|
||||
dirs[#dirs + 1] = buf:sub(up + 1, nul - 1)
|
||||
up = nul
|
||||
end
|
||||
--- @type table<integer, string> -- bag: 1-based unit file index -> basename
|
||||
local unit_basenames = {}
|
||||
--- @type table<integer, string> -- bag: 1-based unit file index -> full path
|
||||
local unit_paths = {}
|
||||
local unit_basenames = {} ---@type table<integer, string> -- bag: 1-based unit file index -> basename
|
||||
local unit_paths = {} ---@type table<integer, string> -- bag: 1-based unit file index -> full path
|
||||
while up < body_end do
|
||||
--- @type integer
|
||||
local nul = buf:find("\0", up + 1, true) or (body_end + 1)
|
||||
local nul = buf:find("\0", up + 1, true) or (body_end + 1) ---@type integer
|
||||
if nul > body_end or nul == up + 1 then up = nul break end
|
||||
--- @type string
|
||||
local path = buf:sub(up + 1, nul - 1)
|
||||
local path = buf:sub(up + 1, nul - 1) ---@type string
|
||||
up = nul
|
||||
--- @type integer|nil, integer
|
||||
local didx, up_next = M.read_uleb128_at(buf, up); up = up_next
|
||||
--- @type integer|nil, integer
|
||||
local _time, up_next2 = M.read_uleb128_at(buf, up); up = up_next2
|
||||
--- @type integer|nil, integer
|
||||
local _size, up_next3 = M.read_uleb128_at(buf, up); up = up_next3
|
||||
--- @type integer
|
||||
local idx = #unit_basenames + 1
|
||||
--- @type string
|
||||
local bs = path:match("[^/\\]+$") or path
|
||||
local didx, up_next = M.read_uleb128_at(buf, up); up = up_next ---@type integer|nil, integer
|
||||
local _time, up_next2 = M.read_uleb128_at(buf, up); up = up_next2 ---@type integer|nil, integer
|
||||
local _size, up_next3 = M.read_uleb128_at(buf, up); up = up_next3 ---@type integer|nil, integer
|
||||
local idx = #unit_basenames + 1 ---@type integer
|
||||
local bs = path:match("[^/\\]+$") or path ---@type string
|
||||
unit_paths[idx] = path
|
||||
unit_basenames[idx] = bs
|
||||
dirs[1] = dirs[1] or "" -- safety: gcc emits "" sentinel dir at 0
|
||||
@@ -1302,76 +1189,50 @@ function M.read_line_unit_file_table(elf_path)
|
||||
--- @return table<integer, string>
|
||||
--- @return table<integer, string>
|
||||
local function parse_dwarf5_unit(buf, lstr_buf, content_start, body_end)
|
||||
--- @type integer
|
||||
local up = content_start
|
||||
local up = content_start ---@type integer
|
||||
-- 6 fixed bytes: min_insn, max_ops_per_insn, default_is, line_base, line_range, opcode_base
|
||||
up = up + 6
|
||||
--- @type integer
|
||||
local opcode_base = buf:byte(content_start + 6)
|
||||
local opcode_base = buf:byte(content_start + 6) ---@type integer
|
||||
up = up + (opcode_base - 1) -- std_opcode_lengths
|
||||
-- directories
|
||||
--- @type integer|nil, integer
|
||||
local dir_format_count, after = M.read_uleb128_at(buf, up); up = after
|
||||
--- @type integer[]
|
||||
local dir_formats = {}
|
||||
--- @type integer
|
||||
for i = 1, dir_format_count do
|
||||
--- @type integer|nil, integer
|
||||
local f, a2 = M.read_uleb128_at(buf, up); up = a2
|
||||
local dir_format_count, after = M.read_uleb128_at(buf, up); up = after ---@type integer|nil, integer
|
||||
local dir_formats = {} ---@type integer[]
|
||||
for i = 1, dir_format_count do ---@type integer
|
||||
local f, a2 = M.read_uleb128_at(buf, up); up = a2 ---@type integer|nil, integer
|
||||
dir_formats[i] = f
|
||||
end
|
||||
--- @type integer|nil, integer
|
||||
local dir_count, a3 = M.read_uleb128_at(buf, up); up = a3
|
||||
--- @type string[]
|
||||
local dirs = {}
|
||||
--- @type integer
|
||||
for i = 1, dir_count do
|
||||
--- @type string
|
||||
local combined = ""
|
||||
--- @type integer
|
||||
for j = 1, dir_format_count do
|
||||
--- @type string|integer|nil, integer
|
||||
local v, a4 = read_form(buf, lstr_buf, up, dir_formats[j])
|
||||
local dir_count, a3 = M.read_uleb128_at(buf, up); up = a3 ---@type integer|nil, integer
|
||||
local dirs = {} ---@type string[]
|
||||
for i = 1, dir_count do ---@type integer
|
||||
local combined = "" ---@type string
|
||||
for j = 1, dir_format_count do ---@type integer
|
||||
local v, a4 = read_form(buf, lstr_buf, up, dir_formats[j]) ---@type string|integer|nil, integer
|
||||
up = a4
|
||||
if j == 1 and type(v) == "string" then combined = v end
|
||||
end
|
||||
dirs[i] = combined
|
||||
end
|
||||
-- file names
|
||||
--- @type integer|nil, integer
|
||||
local file_format_count, after2 = M.read_uleb128_at(buf, up); up = after2
|
||||
--- @type integer[]
|
||||
local file_formats = {}
|
||||
--- @type integer
|
||||
for i = 1, file_format_count do
|
||||
--- @type integer|nil, integer
|
||||
local f, a2 = M.read_uleb128_at(buf, up); up = a2
|
||||
local file_format_count, after2 = M.read_uleb128_at(buf, up); up = after2 ---@type integer|nil, integer
|
||||
local file_formats = {} ---@type integer[]
|
||||
for i = 1, file_format_count do ---@type integer
|
||||
local f, a2 = M.read_uleb128_at(buf, up); up = a2 ---@type integer|nil, integer
|
||||
file_formats[i] = f
|
||||
end
|
||||
--- @type integer|nil, integer
|
||||
local file_count, a3 = M.read_uleb128_at(buf, up); up = a3
|
||||
--- @type table<integer, string> -- bag: 1-based unit file index -> basename
|
||||
local unit_basenames = {}
|
||||
--- @type table<integer, string> -- bag: 1-based unit file index -> full path
|
||||
local unit_paths = {}
|
||||
--- @type integer
|
||||
for i = 1, file_count do
|
||||
--- @type string
|
||||
local combined = ""
|
||||
--- @type integer
|
||||
local didx = 0
|
||||
--- @type integer
|
||||
for j = 1, file_format_count do
|
||||
--- @type string|integer|nil, integer
|
||||
local v, a4 = read_form(buf, lstr_buf, up, file_formats[j])
|
||||
local file_count, a3 = M.read_uleb128_at(buf, up); up = a3 ---@type integer|nil, integer
|
||||
local unit_basenames = {} ---@type table<integer, string> -- bag: 1-based unit file index -> basename
|
||||
local unit_paths = {} ---@type table<integer, string> -- bag: 1-based unit file index -> full path
|
||||
for i = 1, file_count do ---@type integer
|
||||
local combined = "" ---@type string
|
||||
local didx = 0 ---@type integer
|
||||
for j = 1, file_format_count do ---@type integer
|
||||
local v, a4 = read_form(buf, lstr_buf, up, file_formats[j]) ---@type string|integer|nil, integer
|
||||
up = a4
|
||||
if j == 1 and type(v) == "string" then combined = v end
|
||||
if j == 2 and type(v) == "number" then didx = v end
|
||||
end
|
||||
--- @type integer
|
||||
local idx = #unit_basenames + 1
|
||||
--- @type string
|
||||
local bs = combined:match("[^/\\]+$") or combined
|
||||
local idx = #unit_basenames + 1 ---@type integer
|
||||
local bs = combined:match("[^/\\]+$") or combined ---@type string
|
||||
unit_paths[idx] = combined
|
||||
unit_basenames[idx] = bs
|
||||
if didx > 0 and dirs[didx] then
|
||||
@@ -1382,37 +1243,27 @@ function M.read_line_unit_file_table(elf_path)
|
||||
end
|
||||
|
||||
--- Walk every line-program unit in the section.
|
||||
--- @type integer
|
||||
local p = 0
|
||||
--- @type integer
|
||||
local section_end = #line
|
||||
local p = 0 ---@type integer
|
||||
local section_end = #line ---@type integer
|
||||
while p + 4 <= section_end do
|
||||
--- @type integer
|
||||
local unit_length = M.read_u32_le(line, p)
|
||||
local unit_length = M.read_u32_le(line, p) ---@type integer
|
||||
if unit_length == 0xFFFFFFFF then
|
||||
io.stderr:write("[elf_dwarf.read_line_unit_file_table] 64-bit DWARF (initial-length 0xFFFFFFFF); not supported\n")
|
||||
return nil
|
||||
end
|
||||
--- @type integer
|
||||
local body_start = p + 4
|
||||
--- @type integer
|
||||
local body_end = p + 4 + unit_length
|
||||
local body_start = p + 4 ---@type integer
|
||||
local body_end = p + 4 + unit_length ---@type integer
|
||||
if body_end > section_end then break end
|
||||
--- @type integer
|
||||
local version = M.read_u16_le(line, body_start)
|
||||
--- @type table<integer, string>|nil, table<integer, string>|nil
|
||||
local unit_basenames, unit_paths
|
||||
local version = M.read_u16_le(line, body_start) ---@type integer
|
||||
local unit_basenames, unit_paths ---@type table<integer, string>|nil, table<integer, string>|nil
|
||||
if version >= 5 then
|
||||
-- DWARF5 header: version(2) + addr_size(1) + seg_size(1) + header_length(4) + content
|
||||
--- @type integer
|
||||
local header_length_offset = body_start + 6 -- past version(2) + addr_size(1) + seg_size(1) - wait that's wrong; past hdr len is at +6
|
||||
--- @type integer
|
||||
local content_start = body_start + 8 -- past version(2) + addr_size(1) + seg_size(1) + header_length(4)
|
||||
local header_length_offset = body_start + 6 ---@type integer -- past version(2) + addr_size(1) + seg_size(1) - wait that's wrong; past hdr len is at +6
|
||||
local content_start = body_start + 8 ---@type integer -- past version(2) + addr_size(1) + seg_size(1) + header_length(4)
|
||||
unit_basenames, unit_paths = parse_dwarf5_unit(line, lstr, content_start, body_end)
|
||||
elseif version >= 2 then
|
||||
-- DWARF2/3/4 header: version(2) + header_length(4) + content
|
||||
--- @type integer
|
||||
local content_start = body_start + 6 -- past version(2) + header_length(4)
|
||||
local content_start = body_start + 6 ---@type integer -- past version(2) + header_length(4)
|
||||
unit_basenames, unit_paths = parse_dwarf3_unit(line, content_start, body_end)
|
||||
else
|
||||
io.stderr:write(string.format("[elf_dwarf.read_line_unit_file_table] unsupported DWARF version %d (offset 0x%x)\n", version, p))
|
||||
@@ -1426,10 +1277,9 @@ function M.read_line_unit_file_table(elf_path)
|
||||
-- For DWARF5 (crt0.s + C unit), each carries its own per-unit file-table map;
|
||||
-- the atom-side DW_LNS_set_file(N) refers to the C unit's indices, NOT crt0.s's.
|
||||
-- Since the C unit is the one with full include_directories + 12 entries, we can use it directly.
|
||||
--- @type integer, string
|
||||
for idx, bs in pairs(unit_basenames) do
|
||||
for idx, bs in pairs(unit_basenames) do ---@type integer, string
|
||||
basenames[idx] = bs
|
||||
paths[idx] = unit_paths[idx]
|
||||
paths [idx] = unit_paths[idx]
|
||||
basename_to_index[bs] = idx
|
||||
end
|
||||
p = body_end
|
||||
|
||||
+59
-118
@@ -10,10 +10,8 @@
|
||||
|
||||
-- Bootstrap follows the entry scripts; `scripts/duffle_paths.lua` sets package.path and package.cpath. See `ps1_meta.lua` for the rationale.
|
||||
-- `debug.getinfo(1, "S").source` locates this file for standalone and orchestrated runs, then `duffle_paths.lua` returns the loaded `duffle` module.
|
||||
--- @type string
|
||||
local _bootstrap_dir = debug.getinfo(1, "S").source:match("^@?(.*[/\\])") or "./"
|
||||
--- @type DuffleExport
|
||||
local duffle = dofile(_bootstrap_dir .. "../duffle_paths.lua")
|
||||
local _bootstrap_dir = debug.getinfo(1, "S").source:match("^@?(.*[/\\])") or "./" ---@type string
|
||||
local duffle = dofile(_bootstrap_dir .. "../duffle_paths.lua") ---@type DuffleExport
|
||||
|
||||
-- The annotation pass reads the source-derived registries from scan_source:
|
||||
-- * pipe_ctx.register_alias_registry — for atom_dbg_reg_default(R_X, ...) and atom_reg_types(R_X, ...) member-identity checks
|
||||
@@ -107,8 +105,7 @@ end
|
||||
--- @param findings Findings
|
||||
--- @return nil
|
||||
local function check_unique_annotation(_item, pipe_ctx, findings)
|
||||
--- @type string, integer
|
||||
for name, n in pairs(pipe_ctx.annot_counts) do
|
||||
for name, n in pairs(pipe_ctx.annot_counts) do ---@type string, integer
|
||||
if n > 1 then
|
||||
findings.errors[#findings.errors + 1] = {
|
||||
line = pipe_ctx.atom_index[name] and pipe_ctx.atom_index[name].line or 0,
|
||||
@@ -142,10 +139,8 @@ end
|
||||
--- @param findings Findings
|
||||
--- @return nil
|
||||
local function check_macro_word_drift(m, pipe_ctx, findings)
|
||||
--- @type WordCounts
|
||||
local wc = (pipe_ctx and pipe_ctx.word_counts) or {}
|
||||
--- @type integer|nil
|
||||
local declared = wc[m.name]
|
||||
local wc = (pipe_ctx and pipe_ctx.word_counts) or {} ---@type WordCounts
|
||||
local declared = wc[m.name] ---@type integer|nil
|
||||
if not declared then
|
||||
findings.errors[#findings.errors + 1] = {
|
||||
line = m.line,
|
||||
@@ -174,10 +169,8 @@ end
|
||||
--- @return nil
|
||||
local function check_semantic_reg_defaults(_src, pipe_ctx, findings)
|
||||
-- Detect duplicate defaults using the ordered occurrence list (the out.types hash only retains the last declaration).
|
||||
--- @type table<string, integer> -- bag: register ident -> first source line
|
||||
local seen_first_line = {}
|
||||
--- @type integer, RegTypeOccurrence
|
||||
for _, occ in ipairs(pipe_ctx.type_occurrences or {}) do
|
||||
local seen_first_line = {} ---@type table<string, integer> -- bag: register ident -> first source line
|
||||
for _, occ in ipairs(pipe_ctx.type_occurrences or {}) do ---@type integer, RegTypeOccurrence
|
||||
if seen_first_line[occ.reg] == nil then
|
||||
seen_first_line[occ.reg] = occ.source_line
|
||||
else
|
||||
@@ -189,12 +182,9 @@ local function check_semantic_reg_defaults(_src, pipe_ctx, findings)
|
||||
}
|
||||
end
|
||||
end
|
||||
--- @type table<string, AliasEntry>
|
||||
local reg_registry = pipe_ctx.register_alias_registry or {}
|
||||
--- @type table<string, TypeNameEntry>
|
||||
local type_registry = pipe_ctx.type_name_registry or {}
|
||||
--- @type string, RegTypeDefault
|
||||
for reg, def in pairs(pipe_ctx.types or {}) do
|
||||
local reg_registry = pipe_ctx.register_alias_registry or {} ---@type table<string, AliasEntry>
|
||||
local type_registry = pipe_ctx.type_name_registry or {} ---@type table<string, TypeNameEntry>
|
||||
for reg, def in pairs(pipe_ctx.types or {}) do ---@type string, RegTypeDefault
|
||||
if not reg_registry[reg] then
|
||||
findings.errors[#findings.errors + 1] = {
|
||||
line = def.source_line,
|
||||
@@ -229,15 +219,11 @@ end
|
||||
--- @param findings Findings
|
||||
--- @return nil
|
||||
local function check_atom_reg_types(_src, pipe_ctx, findings)
|
||||
--- @type table<string, AliasEntry>
|
||||
local reg_registry = pipe_ctx.register_alias_registry or {}
|
||||
--- @type table<string, TypeNameEntry>
|
||||
local type_registry = pipe_ctx.type_name_registry or {}
|
||||
--- @type integer, AtomInfoEntry
|
||||
for _, ai in ipairs(pipe_ctx.atom_infos_list or {}) do
|
||||
local reg_registry = pipe_ctx.register_alias_registry or {} ---@type table<string, AliasEntry>
|
||||
local type_registry = pipe_ctx.type_name_registry or {} ---@type table<string, TypeNameEntry>
|
||||
for _, ai in ipairs(pipe_ctx.atom_infos_list or {}) do ---@type integer, AtomInfoEntry
|
||||
if ai.reg_type_overrides then
|
||||
--- @type string, RegTypeOverride
|
||||
for reg, ov in pairs(ai.reg_type_overrides) do
|
||||
for reg, ov in pairs(ai.reg_type_overrides) do ---@type string, RegTypeOverride
|
||||
if not reg_registry[reg] then
|
||||
findings.errors[#findings.errors + 1] = {
|
||||
line = ai.info_line,
|
||||
@@ -265,13 +251,11 @@ end
|
||||
--- @param findings Findings
|
||||
--- @return nil
|
||||
local function check_atom_view_layout(_src, pipe_ctx, findings)
|
||||
--- @type string, AtomViewEntry
|
||||
for atom_name, view in pairs(pipe_ctx.atom_views or {}) do
|
||||
for atom_name, view in pairs(pipe_ctx.atom_views or {}) do ---@type string, AtomViewEntry
|
||||
if not view.binds_name then
|
||||
-- The atom had atom_reg_types but no atom_view; no layout check needed.
|
||||
else
|
||||
--- @type BindsEntry|nil
|
||||
local bs = pipe_ctx.binds_index[view.binds_name]
|
||||
local bs = pipe_ctx.binds_index[view.binds_name] ---@type BindsEntry|nil
|
||||
if not bs then
|
||||
findings.errors[#findings.errors + 1] = {
|
||||
line = view.info_line,
|
||||
@@ -297,16 +281,12 @@ end
|
||||
--- @param findings Findings
|
||||
--- @return nil
|
||||
local function check_binds_no_duplicate_fields(_src, pipe_ctx, findings)
|
||||
--- @type integer, BindsEntry
|
||||
for _, bs in ipairs(pipe_ctx.binds_list or {}) do
|
||||
--- @type table<string, integer> -- bag: field name -> occurrence count
|
||||
local seen = {}
|
||||
--- @type integer, TypeField
|
||||
for _, f in ipairs(bs.fields or {}) do
|
||||
for _, bs in ipairs(pipe_ctx.binds_list or {}) do ---@type integer, BindsEntry
|
||||
local seen = {} ---@type table<string, integer> -- bag: field name -> occurrence count
|
||||
for _, f in ipairs(bs.fields or {}) do ---@type integer, TypeField
|
||||
seen[f.name] = (seen[f.name] or 0) + 1
|
||||
end
|
||||
--- @type string, integer
|
||||
for name, count in pairs(seen) do
|
||||
for name, count in pairs(seen) do ---@type string, integer
|
||||
if count > 1 then
|
||||
findings.errors[#findings.errors + 1] = {
|
||||
line = bs.line,
|
||||
@@ -334,10 +314,8 @@ end
|
||||
--- @param findings Findings
|
||||
--- @return nil
|
||||
local function check_skip_marker(marker, _pipe_ctx, findings)
|
||||
--- @type string
|
||||
local kind = marker.marker_kind
|
||||
--- @type integer
|
||||
local line = marker.marker_line
|
||||
local kind = marker.marker_kind ---@type string
|
||||
local line = marker.marker_line ---@type integer
|
||||
-- Left `scan.debug_skip_markers` with production records for `atom_dbg_skip` only; other identifiers take the walker's unrelated branch.
|
||||
|
||||
if marker.has_parens then
|
||||
@@ -396,13 +374,10 @@ end
|
||||
local function check_wave_context_migration(_src, pipe_ctx, findings)
|
||||
if not (pipe_ctx.types and next(pipe_ctx.types)) then return end
|
||||
if not (pipe_ctx.atom_infos_list) then return end
|
||||
--- @type table<string, AliasEntry>
|
||||
local reg_registry = pipe_ctx.register_alias_registry or {}
|
||||
--- @type integer, AtomInfoEntry
|
||||
for _, ai in ipairs(pipe_ctx.atom_infos_list) do
|
||||
local reg_registry = pipe_ctx.register_alias_registry or {} ---@type table<string, AliasEntry>
|
||||
for _, ai in ipairs(pipe_ctx.atom_infos_list) do ---@type integer, AtomInfoEntry
|
||||
if ai.reg_type_overrides then
|
||||
--- @type string, RegTypeOverride
|
||||
for reg, _ in pairs(ai.reg_type_overrides) do
|
||||
for reg, _ in pairs(ai.reg_type_overrides) do ---@type string, RegTypeOverride
|
||||
if not reg_registry[reg] then
|
||||
findings.warnings[#findings.warnings + 1] = {
|
||||
line = 0,
|
||||
@@ -429,8 +404,7 @@ end
|
||||
--
|
||||
-- Adding a new check = 1 row here + 1 function above. The `validate()` dispatch loop never needs editing.
|
||||
|
||||
--- @type CheckRule[]
|
||||
local CHECK_RULES = {
|
||||
local CHECK_RULES = { ---@type CheckRule[]
|
||||
{ name = "atom_decl_exists", per_annot = check_atom_decl_exists },
|
||||
{ name = "binds_struct_exists", per_annot = check_binds_struct_exists },
|
||||
{ name = "unique_annotation", post = check_unique_annotation },
|
||||
@@ -453,12 +427,9 @@ local CHECK_RULES = {
|
||||
--- @param ctx PassCtx
|
||||
--- @return PipeCtx
|
||||
local function build_corpus_pipe_ctx(ctx)
|
||||
--- @type PipeCtx
|
||||
local view = duffle.corpus_view(ctx)
|
||||
--- @type table<string, integer> -- bag: atom name -> annotation count
|
||||
local annot_counts = {}
|
||||
--- @type integer, AtomInfoEntry
|
||||
for _, info in ipairs(view.atom_infos) do
|
||||
local view = duffle.corpus_view(ctx) ---@type PipeCtx
|
||||
local annot_counts = {} ---@type table<string, integer> -- bag: atom name -> annotation count
|
||||
for _, info in ipairs(view.atom_infos) do ---@type integer, AtomInfoEntry
|
||||
if info and info.atom_name then
|
||||
annot_counts[info.atom_name] = (annot_counts[info.atom_name] or 0) + 1
|
||||
end
|
||||
@@ -476,17 +447,13 @@ end
|
||||
--- @return AnnotatedResult
|
||||
local function validate(ctx, src, corpus_pipe_ctx)
|
||||
corpus_pipe_ctx = corpus_pipe_ctx or build_corpus_pipe_ctx(ctx)
|
||||
--- @type SourceScan
|
||||
local scan = src.scan
|
||||
local scan = src.scan ---@type SourceScan
|
||||
|
||||
-- Build a per-source pipe_ctx: shared lookups come from `corpus_pipe_ctx`, while declarations, bodies, types, views, defaults, and occurrences come from `src.scan`.
|
||||
--- @type table<string, integer> -- bag: register ident -> occurrence count
|
||||
local seen_defaults = {}; for reg, _ in pairs (scan.types or {}) do seen_defaults[reg] = (seen_defaults[reg] or 0) + 1 end
|
||||
--- @type AtomInfoEntry[]
|
||||
local atom_infos_list = {}; for _, ai in ipairs(scan.atom_infos or {}) do atom_infos_list[#atom_infos_list + 1] = ai end
|
||||
local seen_defaults = {}; for reg, _ in pairs (scan.types or {}) do seen_defaults[reg] = (seen_defaults[reg] or 0) + 1 end ---@type table<string, integer> -- bag: register ident -> occurrence count
|
||||
local atom_infos_list = {}; for _, ai in ipairs(scan.atom_infos or {}) do atom_infos_list[#atom_infos_list + 1] = ai end ---@type AtomInfoEntry[]
|
||||
|
||||
--- @type PipeCtx
|
||||
local pipe_ctx = {
|
||||
local pipe_ctx = { ---@type PipeCtx
|
||||
atom_index = {},
|
||||
binds_index = {},
|
||||
annot_counts = corpus_pipe_ctx.annot_counts,
|
||||
@@ -500,29 +467,23 @@ local function validate(ctx, src, corpus_pipe_ctx)
|
||||
register_alias_registry = corpus_pipe_ctx.register_alias_registry,
|
||||
type_name_registry = corpus_pipe_ctx.type_name_registry,
|
||||
}
|
||||
--- @type AtomEntry[]
|
||||
local atoms = {}
|
||||
--- @type integer, AtomEntry
|
||||
for _, a in ipairs(scan.atoms) do
|
||||
local atoms = {} ---@type AtomEntry[]
|
||||
for _, a in ipairs(scan.atoms) do ---@type integer, AtomEntry
|
||||
if a.kind == "atom" or a.kind == "atom_proc" then
|
||||
atoms[#atoms + 1] = a
|
||||
pipe_ctx.atom_index[a.raw_name or a.name] = a
|
||||
end
|
||||
end
|
||||
--- @type integer, BindsEntry
|
||||
for _, b in ipairs(scan.binds) do pipe_ctx.binds_index[b.name] = b end
|
||||
for _, b in ipairs(scan.binds) do pipe_ctx.binds_index[b.name] = b end ---@type integer, BindsEntry
|
||||
|
||||
-- Findings live in a single struct with three lists (errors / warnings / info).
|
||||
-- Each check writes to the list appropriate for its severity.
|
||||
--- @type Findings
|
||||
local findings = { errors = {}, warnings = {}, info = {} }
|
||||
local findings = { errors = {}, warnings = {}, info = {} } ---@type Findings
|
||||
|
||||
-- Lift parse-time errors already recorded in scan_source's atom_info payload into this pass's findings list.
|
||||
--- @type integer, AtomInfoEntry
|
||||
for _, info in ipairs(scan.atom_infos) do
|
||||
for _, info in ipairs(scan.atom_infos) do ---@type integer, AtomInfoEntry
|
||||
if info.errors then
|
||||
--- @type integer, string
|
||||
for _, msg in ipairs(info.errors) do
|
||||
for _, msg in ipairs(info.errors) do ---@type integer, string
|
||||
findings.errors[#findings.errors + 1] = {
|
||||
line = info.info_line,
|
||||
msg = string.format("'%s': %s", info.atom_name, msg),
|
||||
@@ -532,8 +493,7 @@ local function validate(ctx, src, corpus_pipe_ctx)
|
||||
end
|
||||
|
||||
-- THE per-annotation pipeline. ONE loop. CHECK_RULES dispatches per_annot rules.
|
||||
--- @type integer, AtomInfoEntry
|
||||
for _, info in ipairs(scan.atom_infos) do
|
||||
for _, info in ipairs(scan.atom_infos) do ---@type integer, AtomInfoEntry
|
||||
duffle.run_check_rules(CHECK_RULES, "per_annot", info, pipe_ctx, findings)
|
||||
end
|
||||
|
||||
@@ -542,17 +502,14 @@ local function validate(ctx, src, corpus_pipe_ctx)
|
||||
|
||||
-- scan_source records each marker in scan.debug_skip_markers; this loop validates each record independently and emits at most one error per marker.
|
||||
-- Valid markers stamp `debug_skip = true` on the following atom or component declaration, which downstream consumers read directly.
|
||||
--- @type DebugSkipMarker[]
|
||||
local skip_markers = scan.debug_skip_markers or {}
|
||||
--- @type integer, DebugSkipMarker
|
||||
for _, marker in ipairs(skip_markers) do
|
||||
local skip_markers = scan.debug_skip_markers or {} ---@type DebugSkipMarker[]
|
||||
for _, marker in ipairs(skip_markers) do ---@type integer, DebugSkipMarker
|
||||
duffle.run_check_rules(CHECK_RULES, "per_skip_marker", marker, pipe_ctx, findings)
|
||||
end
|
||||
|
||||
-- Per-macro rules (TAPE_WORDS vs WORD_COUNT drift).
|
||||
pipe_ctx.word_counts = corpus_pipe_ctx.word_counts
|
||||
--- @type integer, MacroEntry
|
||||
for _, m in ipairs(scan.macros) do
|
||||
for _, m in ipairs(scan.macros) do ---@type integer, MacroEntry
|
||||
duffle.run_check_rules(CHECK_RULES, "per_macro", m, pipe_ctx, findings)
|
||||
end
|
||||
|
||||
@@ -582,8 +539,7 @@ end
|
||||
-- M.run — orchestrator entry
|
||||
-- ════════════════════════════════════════════════════════════════════════════
|
||||
|
||||
--- @type AnnotationPass
|
||||
local M = {}
|
||||
local M = {} ---@type AnnotationPass
|
||||
|
||||
-- Expose `validate` for downstream passes (e.g. report.lua) that need to re-render the per-source results into a per-MODULE report.
|
||||
M.validate = validate
|
||||
@@ -591,47 +547,32 @@ M.validate = validate
|
||||
--- @param ctx PassCtx
|
||||
--- @return PassResult
|
||||
function M.run(ctx)
|
||||
--- @type PassOutputEntry[]
|
||||
local outputs = {}
|
||||
--- @type PassFinding[]
|
||||
local errors = {}
|
||||
--- @type PassFinding[]
|
||||
local warnings = {}
|
||||
local outputs = {} ---@type PassOutputEntry[]
|
||||
local errors = {} ---@type PassFinding[]
|
||||
local warnings = {} ---@type PassFinding[]
|
||||
|
||||
-- Build the shared pipe_ctx once for this run; every validate() call sees the same cross-source registries.
|
||||
-- The corpus owns the canonical cross-source registries; per-source scans retain body / declaration ownership.
|
||||
--- @type PipeCtx
|
||||
local corpus_pipe_ctx = build_corpus_pipe_ctx(ctx)
|
||||
--- @type Corpus
|
||||
local corpus = ctx.shared.corpus
|
||||
local corpus_pipe_ctx = build_corpus_pipe_ctx(ctx) ---@type PipeCtx
|
||||
local corpus = ctx.shared.corpus ---@type Corpus
|
||||
|
||||
-- Group `corpus.sources_by_dir` by module, validate every source in each bucket, and emit one errors.h per directory.
|
||||
--- @type table<string, SourceFile[]>
|
||||
local by_dir = (corpus and corpus.sources_by_dir) or {}
|
||||
local by_dir = (corpus and corpus.sources_by_dir) or {} ---@type table<string, SourceFile[]>
|
||||
|
||||
--- @type string, SourceFile[]
|
||||
for dir, dir_sources in pairs(by_dir) do
|
||||
--- @type string
|
||||
local dir_basename = dir:match("([^/\\]+)$") or dir
|
||||
--- @type integer
|
||||
local dir_atoms = 0
|
||||
--- @type PassFinding[]
|
||||
local dir_errors = {}
|
||||
--- @type PassFinding[]
|
||||
local dir_warnings = {}
|
||||
--- @type integer, SourceFile
|
||||
for _, src in ipairs(dir_sources) do
|
||||
--- @type AnnotatedResult
|
||||
local result = validate(ctx, src, corpus_pipe_ctx)
|
||||
for dir, dir_sources in pairs(by_dir) do ---@type string, SourceFile[]
|
||||
local dir_basename = dir:match("([^/\\]+)$") or dir ---@type string
|
||||
local dir_atoms = 0 ---@type integer
|
||||
local dir_errors = {} ---@type PassFinding[]
|
||||
local dir_warnings = {} ---@type PassFinding[]
|
||||
for _, src in ipairs(dir_sources) do ---@type integer, SourceFile
|
||||
local result = validate(ctx, src, corpus_pipe_ctx) ---@type AnnotatedResult
|
||||
result.source = src.path -- tag for downstream rendering
|
||||
dir_atoms = dir_atoms + #result.atoms
|
||||
--- @type integer, PassFinding
|
||||
for _, e in ipairs(result.errors) do
|
||||
for _, e in ipairs(result.errors) do ---@type integer, PassFinding
|
||||
dir_errors[#dir_errors + 1] = { line = e.line, msg = e.msg, source = src.path }
|
||||
errors [#errors + 1] = { line = e.line, msg = e.msg }
|
||||
end
|
||||
--- @type integer, PassFinding
|
||||
for _, w in ipairs(result.warnings) do
|
||||
for _, w in ipairs(result.warnings) do ---@type integer, PassFinding
|
||||
dir_warnings[#dir_warnings + 1] = { line = w.line, msg = w.msg }
|
||||
warnings [#warnings + 1] = { line = w.line, msg = w.msg }
|
||||
end
|
||||
|
||||
@@ -37,12 +37,9 @@
|
||||
-- Bootstrap: load `duffle_paths.lua` via `debug.getinfo(1, "S").source`
|
||||
-- (works both standalone + when require'd). `duffle_paths.lua` sets package.path then returns `require("duffle")`
|
||||
-- at the bottom, so the dofile value IS the duffle module.
|
||||
--- @type string
|
||||
local _bootstrap_dir = debug.getinfo(1, "S").source:match("^@?(.*[/\\])") or "./"
|
||||
--- @type DuffleExport
|
||||
local duffle = dofile(_bootstrap_dir .. "../duffle_paths.lua")
|
||||
--- @type ElfDwarfMod
|
||||
local elf_dwarf = require("elf_dwarf")
|
||||
local _bootstrap_dir = debug.getinfo(1, "S").source:match("^@?(.*[/\\])") or "./" ---@type string
|
||||
local duffle = dofile(_bootstrap_dir .. "../duffle_paths.lua") ---@type DuffleExport
|
||||
local elf_dwarf = require("elf_dwarf") ---@type ElfDwarfMod
|
||||
|
||||
-- ════════════════════════════════════════════════════════════════════════════
|
||||
-- Constants
|
||||
@@ -50,8 +47,7 @@ local elf_dwarf = require("elf_dwarf")
|
||||
|
||||
-- Format version emitted as the first line. Bump + add a migration test if the format changes;
|
||||
-- the gdb runtime loader rejects mismatches (E2).
|
||||
--- @type integer
|
||||
local FORMAT_VERSION = 1
|
||||
local FORMAT_VERSION = 1 ---@type integer
|
||||
|
||||
-- ════════════════════════════════════════════════════════════════════════════
|
||||
-- Type declarations
|
||||
@@ -107,23 +103,16 @@ local FORMAT_VERSION = 1
|
||||
--- @return WordMapEntry[]
|
||||
--- @return integer
|
||||
local function canonical_word_entries(atom)
|
||||
--- @type AtomPaths
|
||||
local paths = atom.paths or {}
|
||||
--- @type WordEvent[]
|
||||
local events = paths.word_events or {}
|
||||
--- @type EmissionItem[]
|
||||
local word_items = {}
|
||||
--- @type integer, EmissionItem
|
||||
for _, item in ipairs(paths.items or {}) do
|
||||
local paths = atom.paths or {} ---@type AtomPaths
|
||||
local events = paths.word_events or {} ---@type WordEvent[]
|
||||
local word_items = {} ---@type EmissionItem[]
|
||||
for _, item in ipairs(paths.items or {}) do ---@type integer, EmissionItem
|
||||
if item.kind == "word" then word_items[#word_items + 1] = item end
|
||||
end
|
||||
|
||||
--- @type WordMapEntry[]
|
||||
local entries = {}
|
||||
--- @type integer, WordEvent
|
||||
for index, event in ipairs(events) do
|
||||
--- @type EmissionItem
|
||||
local item = word_items[index] or {}
|
||||
local entries = {} ---@type WordMapEntry[]
|
||||
for index, event in ipairs(events) do ---@type integer, WordEvent
|
||||
local item = word_items[index] or {} ---@type EmissionItem
|
||||
entries[#entries + 1] = {
|
||||
pos = event.i or (index - 1),
|
||||
line = event.call_line or item.line or 0,
|
||||
@@ -149,20 +138,14 @@ end
|
||||
--- @return string[]
|
||||
--- @return integer
|
||||
local function emit_provenance_stanza(src, atom, wc)
|
||||
--- @type string[]
|
||||
local lines = {}
|
||||
--- @type string
|
||||
local rel_path = src.path:gsub("\\\\", "/")
|
||||
--- @type WordMapEntry[], integer
|
||||
local entries, total = canonical_word_entries(atom)
|
||||
local lines = {} ---@type string[]
|
||||
local rel_path = src.path:gsub("\\\\", "/") ---@type string
|
||||
local entries, total = canonical_word_entries(atom) ---@type WordMapEntry[], integer
|
||||
lines[#lines + 1] = string.format('ATOM %s "%s" 0', atom.raw_name or atom.name, rel_path)
|
||||
|
||||
--- @type integer, WordMapEntry
|
||||
for _, entry in ipairs(entries) do
|
||||
--- @type InvocationRecord|nil
|
||||
local inv = entry.invocation
|
||||
--- @type integer|nil
|
||||
local macro_count = inv and wc["mac_" .. inv.component_name]
|
||||
for _, entry in ipairs(entries) do ---@type integer, WordMapEntry
|
||||
local inv = entry.invocation ---@type InvocationRecord|nil
|
||||
local macro_count = inv and wc["mac_" .. inv.component_name] ---@type integer|nil
|
||||
if inv and macro_count ~= nil then
|
||||
lines[#lines + 1] = string.format('WORD %d CALL %s:%d MACRO %s "%s:%d" BODY %d'
|
||||
, entry.pos, rel_path, entry.line, inv.component_name
|
||||
@@ -182,8 +165,7 @@ end
|
||||
--- @param wc WordCounts
|
||||
--- @return string
|
||||
local function render_provenance(src, wc)
|
||||
--- @type string[]
|
||||
local lines = {}
|
||||
local lines = {} ---@type string[]
|
||||
lines[#lines + 1] = "# FORMAT_VERSION 1"
|
||||
lines[#lines + 1] = "# auto-generated by ps1_meta.lua (passes/atoms_source_map.lua) — DO NOT EDIT"
|
||||
lines[#lines + 1] = "# Per-.word provenance: maps each emitted .word to its call site (atom body"
|
||||
@@ -195,17 +177,13 @@ local function render_provenance(src, wc)
|
||||
--- @param atom AtomEntry
|
||||
--- @return nil
|
||||
local function append(atom)
|
||||
--- @type string[]
|
||||
local stanza = emit_provenance_stanza(src, atom, wc)
|
||||
--- @type integer, string
|
||||
for _, line in ipairs(stanza) do lines[#lines + 1] = line end
|
||||
local stanza = emit_provenance_stanza(src, atom, wc) ---@type string[]
|
||||
for _, line in ipairs(stanza) do lines[#lines + 1] = line end ---@type integer, string
|
||||
end
|
||||
--- @type integer, AtomEntry
|
||||
for _, atom in ipairs(src.scan.atoms or {}) do
|
||||
for _, atom in ipairs(src.scan.atoms or {}) do ---@type integer, AtomEntry
|
||||
if atom.paths then append(atom) end
|
||||
end
|
||||
--- @type integer, AtomEntry
|
||||
for _, atom in ipairs(src.scan.raw_atoms or {}) do
|
||||
for _, atom in ipairs(src.scan.raw_atoms or {}) do ---@type integer, AtomEntry
|
||||
if atom.paths then append(atom) end
|
||||
end
|
||||
|
||||
@@ -219,16 +197,12 @@ end
|
||||
--- @return string[]
|
||||
--- @return integer
|
||||
local function emit_atom_stanza(src, atom)
|
||||
--- @type string[]
|
||||
local lines = {}
|
||||
--- @type string
|
||||
local rel_path = src.path:gsub("\\\\", "/")
|
||||
--- @type WordMapEntry[], integer
|
||||
local entries, total = canonical_word_entries(atom)
|
||||
local lines = {} ---@type string[]
|
||||
local rel_path = src.path:gsub("\\\\", "/") ---@type string
|
||||
local entries, total = canonical_word_entries(atom) ---@type WordMapEntry[], integer
|
||||
|
||||
lines[#lines + 1] = string.format('ATOM %s "%s" 0', atom.raw_name or atom.name, rel_path)
|
||||
--- @type integer, WordMapEntry
|
||||
for _, entry in ipairs(entries) do
|
||||
for _, entry in ipairs(entries) do ---@type integer, WordMapEntry
|
||||
lines[#lines + 1] = string.format("WORD %d LINE %d TEXT %s",
|
||||
entry.pos, entry.line, entry.text)
|
||||
end
|
||||
@@ -242,25 +216,20 @@ end
|
||||
--- @param src SourceFile
|
||||
--- @return string
|
||||
local function render_source_map(src)
|
||||
--- @type string[]
|
||||
local lines = {}
|
||||
local lines = {} ---@type string[]
|
||||
lines[#lines + 1] = "# FORMAT_VERSION " .. FORMAT_VERSION
|
||||
lines[#lines + 1] = "# auto-generated by ps1_meta.lua (passes/atoms_source_map.lua) — DO NOT EDIT"
|
||||
|
||||
--- @param atom AtomEntry
|
||||
--- @return nil
|
||||
local function append(atom)
|
||||
--- @type string[]
|
||||
local stanza = emit_atom_stanza(src, atom)
|
||||
--- @type integer, string
|
||||
for _, line in ipairs(stanza) do lines[#lines + 1] = line end
|
||||
local stanza = emit_atom_stanza(src, atom) ---@type string[]
|
||||
for _, line in ipairs(stanza) do lines[#lines + 1] = line end ---@type integer, string
|
||||
end
|
||||
--- @type integer, AtomEntry
|
||||
for _, atom in ipairs(src.scan.atoms or {}) do
|
||||
for _, atom in ipairs(src.scan.atoms or {}) do ---@type integer, AtomEntry
|
||||
if atom.paths then append(atom) end
|
||||
end
|
||||
--- @type integer, AtomEntry
|
||||
for _, atom in ipairs(src.scan.raw_atoms or {}) do
|
||||
for _, atom in ipairs(src.scan.raw_atoms or {}) do ---@type integer, AtomEntry
|
||||
if atom.paths then append(atom) end
|
||||
end
|
||||
|
||||
@@ -283,28 +252,20 @@ end
|
||||
--- @param ctx PassCtx
|
||||
--- @return GdbAtomRecord[]
|
||||
local function build_atom_table(ctx)
|
||||
--- @type table<string, NmAddr>
|
||||
local addrs = elf_dwarf.read_nm(ctx.flags.elf_path)
|
||||
--- @type Corpus|nil
|
||||
local corpus = ctx.shared and ctx.shared.corpus
|
||||
--- @type GdbAtomRecord[]
|
||||
local matched = {}
|
||||
local addrs = elf_dwarf.read_nm(ctx.flags.elf_path) ---@type table<string, NmAddr>
|
||||
local corpus = ctx.shared and ctx.shared.corpus ---@type Corpus|nil
|
||||
local matched = {} ---@type GdbAtomRecord[]
|
||||
|
||||
--- @type integer, SourceFile
|
||||
for _, src in ipairs(corpus.source_order or {}) do
|
||||
--- @type string
|
||||
local file_base = src.path:match("([^/\\\\]+)$") or src.path
|
||||
for _, src in ipairs(corpus.source_order or {}) do ---@type integer, SourceFile
|
||||
local file_base = src.path:match("([^/\\\\]+)$") or src.path ---@type string
|
||||
--- @param atom AtomEntry
|
||||
--- @return nil
|
||||
local function append(atom)
|
||||
if not atom.paths then return end
|
||||
--- @type string
|
||||
local name = atom.raw_name or atom.name
|
||||
--- @type NmAddr|nil
|
||||
local info = addrs[name]
|
||||
local name = atom.raw_name or atom.name ---@type string
|
||||
local info = addrs[name] ---@type NmAddr|nil
|
||||
if not info then return end
|
||||
--- @type WordMapEntry[], integer
|
||||
local entries, total = canonical_word_entries(atom)
|
||||
local entries, total = canonical_word_entries(atom) ---@type WordMapEntry[], integer
|
||||
matched[#matched + 1] = {
|
||||
name = name,
|
||||
src_path = src.path,
|
||||
@@ -315,10 +276,8 @@ local function build_atom_table(ctx)
|
||||
entries = entries,
|
||||
}
|
||||
end
|
||||
--- @type integer, AtomEntry
|
||||
for _, atom in ipairs((src.scan or {}).atoms or {}) do append(atom) end
|
||||
--- @type integer, AtomEntry
|
||||
for _, atom in ipairs((src.scan or {}).raw_atoms or {}) do append(atom) end
|
||||
for _, atom in ipairs((src.scan or {}).atoms or {}) do append(atom) end ---@type integer, AtomEntry
|
||||
for _, atom in ipairs((src.scan or {}).raw_atoms or {}) do append(atom) end ---@type integer, AtomEntry
|
||||
end
|
||||
|
||||
-- Deterministic order: sort by address (matches `nm` output ordering).
|
||||
@@ -326,8 +285,7 @@ local function build_atom_table(ctx)
|
||||
--- @param b GdbAtomRecord
|
||||
--- @return boolean
|
||||
table.sort(matched, function(a, b) return a.addr < b.addr end)
|
||||
--- @type integer, GdbAtomRecord
|
||||
for i, a in ipairs(matched) do a.idx = i - 1 end
|
||||
for i, a in ipairs(matched) do a.idx = i - 1 end ---@type integer, GdbAtomRecord
|
||||
return matched
|
||||
end
|
||||
|
||||
@@ -345,8 +303,7 @@ local function append_gdb_commands(lines, matched)
|
||||
-- ── tape_atoms ──
|
||||
-- Hardcoded one printf per atom. No loop.
|
||||
lines[#lines + 1] = "define tape_atoms"
|
||||
--- @type integer, GdbAtomRecord
|
||||
for _, a in ipairs(matched) do
|
||||
for _, a in ipairs(matched) do ---@type integer, GdbAtomRecord
|
||||
-- gdb 12.1 quirk: literals in printf args require an attached target.
|
||||
-- Use the per-atom convenience vars set above as printf args.
|
||||
lines[#lines + 1] = string.format(' printf " %%-32s @ 0x%%08x %%4d words\\n", $__atom_name_%d, $__atom_addr_%d, $__atom_words_%d',
|
||||
@@ -361,8 +318,7 @@ local function append_gdb_commands(lines, matched)
|
||||
-- ── break_atom (generic) + per-atom break_atom_X ──
|
||||
lines[#lines + 1] = "define break_atom"
|
||||
lines[#lines + 1] = ' echo "Usage: break_atom_<exact_name> (pick from the list below)"'
|
||||
--- @type integer, GdbAtomRecord
|
||||
for _, a in ipairs(matched) do
|
||||
for _, a in ipairs(matched) do ---@type integer, GdbAtomRecord
|
||||
lines[#lines + 1] = string.format(' printf " break_atom_%%-32s\\n", $__atom_name_%d', a.idx)
|
||||
end
|
||||
lines[#lines + 1] = "end"
|
||||
@@ -371,8 +327,7 @@ local function append_gdb_commands(lines, matched)
|
||||
lines[#lines + 1] = "end"
|
||||
lines[#lines + 1] = ""
|
||||
|
||||
--- @type integer, GdbAtomRecord
|
||||
for _, a in ipairs(matched) do
|
||||
for _, a in ipairs(matched) do ---@type integer, GdbAtomRecord
|
||||
lines[#lines + 1] = string.format("define break_atom_%s", a.name)
|
||||
lines[#lines + 1] = string.format(" break *$__atom_addr_%d", a.idx)
|
||||
lines[#lines + 1] = string.format(' printf " Breakpoint set at %s (0x%%08x)\\n", $__atom_addr_%d', a.name, a.idx)
|
||||
@@ -386,8 +341,7 @@ local function append_gdb_commands(lines, matched)
|
||||
-- ── step_atom / next_atom ──
|
||||
-- Hardcoded one tbreak per atom. No loop.
|
||||
lines[#lines + 1] = "define step_atom"
|
||||
--- @type integer, GdbAtomRecord
|
||||
for _, a in ipairs(matched) do
|
||||
for _, a in ipairs(matched) do ---@type integer, GdbAtomRecord
|
||||
lines[#lines + 1] = string.format(" tbreak *$__atom_addr_%d", a.idx)
|
||||
end
|
||||
lines[#lines + 1] = " continue"
|
||||
@@ -410,8 +364,7 @@ local function append_gdb_commands(lines, matched)
|
||||
lines[#lines + 1] = "define where_in_atom"
|
||||
lines[#lines + 1] = " set $__pc = (unsigned int)$pc"
|
||||
lines[#lines + 1] = " set $__matched = 0"
|
||||
--- @type integer, GdbAtomRecord
|
||||
for _, a in ipairs(matched) do
|
||||
for _, a in ipairs(matched) do ---@type integer, GdbAtomRecord
|
||||
-- Precompute end_addr (gdb 12.1's expression evaluator chokes on `addr + words*4`).
|
||||
lines[#lines + 1] = string.format(" set $__end_%d = $__atom_addr_%d + $__atom_words_%d * 4", a.idx, a.idx, a.idx)
|
||||
lines[#lines + 1] = string.format(" if $__pc >= $__atom_addr_%d && $__pc < $__end_%d", a.idx, a.idx)
|
||||
@@ -420,18 +373,15 @@ local function append_gdb_commands(lines, matched)
|
||||
lines[#lines + 1] = string.format(" set $__word = ($__pc - $__atom_addr_%d) / 4", a.idx)
|
||||
lines[#lines + 1] = string.format(' printf "word: %%d/%%d\\n", $__word, $__atom_words_%d', a.idx)
|
||||
-- One inner-if per WORD entry. Each word's line + text hardcoded.
|
||||
--- @type integer, WordMapEntry
|
||||
for _, we in ipairs(a.entries) do
|
||||
for _, we in ipairs(a.entries) do ---@type integer, WordMapEntry
|
||||
lines[#lines + 1] = string.format(" if $__word == %d", we.pos)
|
||||
-- Escape TEXT for printf format string.
|
||||
--- @type string
|
||||
local escaped_text = we.text:gsub("%%", "%%%%"):gsub('"', '\\"')
|
||||
local escaped_text = we.text:gsub("%%", "%%%%"):gsub('"', '\\"') ---@type string
|
||||
lines[#lines + 1] = string.format(' printf "source: %%s:%%d %%s\\n", $__atom_file_%d, %d, "%s"', a.idx, we.line, escaped_text)
|
||||
lines[#lines + 1] = " end"
|
||||
end
|
||||
-- Fallback for words beyond the source map (shouldn't happen if nm matches).
|
||||
--- @type integer
|
||||
local max_word = 0
|
||||
local max_word = 0 ---@type integer
|
||||
if #a.entries > 0 then max_word = a.entries[#a.entries].pos end
|
||||
lines[#lines + 1] = string.format(' if $__word > %d', max_word)
|
||||
lines[#lines + 1] = ' printf "source: (no source-map entry for word %%d; map may be stale)\\n", $__word'
|
||||
@@ -456,8 +406,7 @@ local function append_gdb_commands(lines, matched)
|
||||
lines[#lines + 1] = " set $__in_atom = 0"
|
||||
lines[#lines + 1] = " set $__did_step = 0"
|
||||
lines[#lines + 1] = " set $__pc = (unsigned int)$pc"
|
||||
--- @type integer, GdbAtomRecord
|
||||
for _, a in ipairs(matched) do
|
||||
for _, a in ipairs(matched) do ---@type integer, GdbAtomRecord
|
||||
-- Precompute end_addr in the convenience var (single expression gdb handles).
|
||||
lines[#lines + 1] = string.format(" set $__end_%d = $__atom_addr_%d + $__atom_words_%d * 4", a.idx, a.idx, a.idx)
|
||||
lines[#lines + 1] = string.format(" if $__pc >= $__atom_addr_%d && $__pc < $__end_%d", a.idx, a.idx)
|
||||
@@ -494,8 +443,7 @@ end
|
||||
--- @return nil
|
||||
local function emit_gdb_runtime(ctx)
|
||||
if not (ctx.flags and ctx.flags.gdb_runtime) then return end
|
||||
--- @type string|nil
|
||||
local elf_path = ctx.flags.elf_path
|
||||
local elf_path = ctx.flags.elf_path ---@type string|nil
|
||||
if not elf_path or elf_path == "" then
|
||||
io.stderr:write("[atoms_source_map] --gdb-runtime requires --elf <elf>\n")
|
||||
return
|
||||
@@ -506,15 +454,13 @@ local function emit_gdb_runtime(ctx)
|
||||
return
|
||||
end
|
||||
|
||||
--- @type GdbAtomRecord[]
|
||||
local matched = build_atom_table(ctx)
|
||||
local matched = build_atom_table(ctx) ---@type GdbAtomRecord[]
|
||||
if #matched == 0 then
|
||||
io.stderr:write("[atoms_source_map] --gdb-runtime: no atoms matched against nm symbols (stale scan?).\n")
|
||||
return
|
||||
end
|
||||
|
||||
--- @type string[]
|
||||
local lines = {}
|
||||
local lines = {} ---@type string[]
|
||||
lines[#lines + 1] = "# Auto-generated by ps1_meta.lua (passes/atoms_source_map.lua)"
|
||||
lines[#lines + 1] = "# DO NOT EDIT — re-run ps1_meta.lua --atoms-source-map --gdb-runtime to regenerate"
|
||||
lines[#lines + 1] = "# Sourced by scripts/gdb/gdb_tape_atoms.gdb (the wrapper)."
|
||||
@@ -535,8 +481,7 @@ local function emit_gdb_runtime(ctx)
|
||||
|
||||
-- Per-atom convenience vars (used as printf args; literals aren't accepted
|
||||
-- without an attached target on gdb 12.1).
|
||||
--- @type integer, GdbAtomRecord
|
||||
for _, a in ipairs(matched) do
|
||||
for _, a in ipairs(matched) do ---@type integer, GdbAtomRecord
|
||||
lines[#lines + 1] = string.format('set $__atom_name_%d = "%s"', a.idx, gdb_escape(a.name))
|
||||
lines[#lines + 1] = string.format("set $__atom_addr_%d = 0x%x", a.idx, a.addr)
|
||||
lines[#lines + 1] = string.format("set $__atom_words_%d = %d", a.idx, a.words)
|
||||
@@ -552,8 +497,7 @@ local function emit_gdb_runtime(ctx)
|
||||
-- Confirmation line for the source operator.
|
||||
lines[#lines + 1] = 'printf "[gdb_tape_atoms] runtime loaded %d atoms from %s\\n", $__atom_count, $__elf_path'
|
||||
|
||||
--- @type string
|
||||
local out_path
|
||||
local out_path ---@type string
|
||||
-- Move out of `<out_root>/gdb_tape_atoms_runtime.gdb` to `<out_root>/../gdb_tape_atoms_runtime.gdb` when the conventional `<out_root>` is `<build>/gen`
|
||||
-- (any equivalent spelling — relative, absolute backslash, absolute forward-slash, trailing-separator variants).
|
||||
-- This puts the gdb runtime alongside the ELF at `build/` rather than under the report subdir.
|
||||
@@ -566,8 +510,7 @@ local function emit_gdb_runtime(ctx)
|
||||
if ends_with_gen_dir(ctx.out_root) then
|
||||
-- Strip the trailing `/gen` segment, then write the runtime script under `build/`.
|
||||
-- e.g. "C:/projects/Pikuma/ps1/build/gen" -> "C:/projects/Pikuma/ps1/build".
|
||||
--- @type string
|
||||
local parent = ctx.out_root:gsub("[/\\]gen[/\\]?$", "")
|
||||
local parent = ctx.out_root:gsub("[/\\]gen[/\\]?$", "") ---@type string
|
||||
out_path = parent .. "/gdb_tape_atoms_runtime.gdb"
|
||||
else
|
||||
out_path = ctx.out_root .. "/gdb_tape_atoms_runtime.gdb"
|
||||
@@ -581,8 +524,7 @@ end
|
||||
-- M — module exports
|
||||
-- ════════════════════════════════════════════════════════════════════════════
|
||||
|
||||
--- @type AtomSourceMapPass
|
||||
local M = {}
|
||||
local M = {} ---@type AtomSourceMapPass
|
||||
|
||||
-- Expose the pure render functions so `report.lua` and the focused tests can call them directly without triggering the file-emit path.
|
||||
M.render_source_map = render_source_map
|
||||
@@ -594,22 +536,15 @@ M.render_provenance = render_provenance
|
||||
function M.render_atom_source_map(atom)
|
||||
assert(type(atom) == "table", "render_atom_source_map: atom must be a table")
|
||||
assert(type(atom.paths) == "table", "render_atom_source_map: atom.paths must be a table")
|
||||
--- @type WordMapEntry[], integer
|
||||
local entries, total = canonical_word_entries(atom)
|
||||
--- @type string[]
|
||||
local lines = {}
|
||||
local entries, total = canonical_word_entries(atom) ---@type WordMapEntry[], integer
|
||||
local lines = {} ---@type string[]
|
||||
lines[#lines + 1] = string.format("ATOM %s %d", (atom.raw_name or atom.name), total)
|
||||
--- @type integer, WordMapEntry
|
||||
for _, entry in ipairs(entries) do
|
||||
--- @type string
|
||||
local word_line = string.format("WORD %d LINE %d TEXT %s",
|
||||
for _, entry in ipairs(entries) do ---@type integer, WordMapEntry
|
||||
local word_line = string.format("WORD %d LINE %d TEXT %s", ---@type string
|
||||
entry.pos, entry.line, entry.text)
|
||||
--- @type string[]
|
||||
local keys = {}
|
||||
--- @type integer
|
||||
for pos = 1, 16 do
|
||||
--- @type string|nil
|
||||
local k = entry.gpr_keys and entry.gpr_keys[pos]
|
||||
local keys = {} ---@type string[]
|
||||
for pos = 1, 16 do ---@type integer
|
||||
local k = entry.gpr_keys and entry.gpr_keys[pos] ---@type string|nil
|
||||
if type(k) == "string" and k:sub(1, 7) == "reguse:" then
|
||||
keys[#keys + 1] = k
|
||||
end
|
||||
@@ -635,17 +570,12 @@ function M.render_atom_provenance(atom, wc, rel_path)
|
||||
assert(type(atom) == "table", "render_atom_provenance: atom must be a table")
|
||||
assert(type(atom.paths) == "table", "render_atom_provenance: atom.paths must be a table")
|
||||
assert(type(rel_path) == "string", "render_atom_provenance: rel_path must be a string")
|
||||
--- @type WordMapEntry[], integer
|
||||
local entries, total = canonical_word_entries(atom)
|
||||
--- @type string[]
|
||||
local lines = {}
|
||||
local entries, total = canonical_word_entries(atom) ---@type WordMapEntry[], integer
|
||||
local lines = {} ---@type string[]
|
||||
lines[#lines + 1] = string.format("ATOM %s %d", (atom.raw_name or atom.name), total)
|
||||
--- @type integer, WordMapEntry
|
||||
for _, entry in ipairs(entries) do
|
||||
--- @type InvocationRecord|nil
|
||||
local inv = entry.invocation
|
||||
--- @type integer|nil
|
||||
local macro_count = inv and wc and wc["mac_" .. inv.component_name]
|
||||
for _, entry in ipairs(entries) do ---@type integer, WordMapEntry
|
||||
local inv = entry.invocation ---@type InvocationRecord|nil
|
||||
local macro_count = inv and wc and wc["mac_" .. inv.component_name] ---@type integer|nil
|
||||
if inv and macro_count ~= nil then
|
||||
lines[#lines + 1] = string.format('WORD %d CALL %s:%d MACRO %s "%s:%d" BODY %d'
|
||||
, entry.pos, rel_path, entry.line, inv.component_name, inv.def_path or "", inv.def_line or 0, entry.body_line)
|
||||
@@ -664,22 +594,17 @@ end
|
||||
--- @param ctx PassCtx
|
||||
--- @return PassResult
|
||||
function M.run(ctx)
|
||||
--- @type PassOutputEntry[]
|
||||
local outputs = {}
|
||||
--- @type PassFinding[]
|
||||
local errors = {}
|
||||
--- @type PassFinding[]
|
||||
local warnings = {}
|
||||
local outputs = {} ---@type PassOutputEntry[]
|
||||
local errors = {} ---@type PassFinding[]
|
||||
local warnings = {} ---@type PassFinding[]
|
||||
|
||||
--- @type Corpus|nil
|
||||
local corpus = ctx.shared and ctx.shared.corpus
|
||||
local corpus = ctx.shared and ctx.shared.corpus ---@type Corpus|nil
|
||||
if type(corpus) ~= "table" or type(corpus.source_order) ~= "table" then
|
||||
error("atoms_source_map.run requires ctx.shared.corpus.source_order (canonical corpus).", 0)
|
||||
end
|
||||
|
||||
-- Word counts come from `corpus.word_counts` (populated by word_count_eval + components passes).
|
||||
--- @type WordCounts
|
||||
local wc = corpus.word_counts or {}
|
||||
local wc = corpus.word_counts or {} ---@type WordCounts
|
||||
if not next(wc) then
|
||||
warnings[#warnings + 1] = {
|
||||
line = 0,
|
||||
|
||||
+71
-142
@@ -35,10 +35,8 @@
|
||||
--- @field run fun(ctx: PassCtx): AutoRegResult
|
||||
--- @field POOL GprIdent[]
|
||||
|
||||
--- @type string
|
||||
local _bootstrap_dir = debug.getinfo(1, "S").source:match("^@?(.*[/\\])") or "./"
|
||||
--- @type DuffleExport
|
||||
local duffle = dofile(_bootstrap_dir .. "../duffle_paths.lua")
|
||||
local _bootstrap_dir = debug.getinfo(1, "S").source:match("^@?(.*[/\\])") or "./" ---@type string
|
||||
local duffle = dofile(_bootstrap_dir .. "../duffle_paths.lua") ---@type DuffleExport
|
||||
|
||||
--- ════════════════════════════════════════════════════════════════════════════
|
||||
--- THE GPR ALLOCATION POOL — what's allocatable, and (more importantly) WHY
|
||||
@@ -56,8 +54,7 @@ local duffle = dofile(_bootstrap_dir .. "../duffle_paths.lua")
|
||||
--- R_K0/K1 (codes 26-27) — Kernel / interrupt handler reserves. Never touched by user code.
|
||||
--- R_GP/SP/FP/RA (codes 28-31) — R_SP/R_FP/R_RA are tape-runtime carriers between tape_enter and tape_exit; R_GP stays the host global pointer.
|
||||
---
|
||||
--- @type GprIdent[]
|
||||
local POOL = {
|
||||
local POOL = { ---@type GprIdent[]
|
||||
"R_V0", "R_V1",
|
||||
"R_T0", "R_T1", "R_T2", "R_T3",
|
||||
"R_T4", "R_T5", "R_T6", "R_T7",
|
||||
@@ -72,8 +69,7 @@ local POOL = {
|
||||
-- Only the POOL entries matter for auto_reg — non-pool aliases
|
||||
-- (R_AT=1, R_A0..A3=4..7, R_T8=24, R_T9=25, R_K0/K1=26..27, R_GP/SP/FP/RA=28..31)
|
||||
-- are deliberately omitted — see the comment block above for the WHY of each exclusion.
|
||||
--- @type table<integer, GprIdent> -- bag: MIPS GPR code -> POOL ident
|
||||
local INT_CODE_TO_POOL_GPR = {
|
||||
local INT_CODE_TO_POOL_GPR = { ---@type table<integer, GprIdent> -- bag: MIPS GPR code -> POOL ident
|
||||
[2] = "R_V0", [3] = "R_V1",
|
||||
[4] = "R_A0", [5] = "R_A1", [6] = "R_A2", [7] = "R_A3",
|
||||
[8] = "R_T0", [9] = "R_T1", [10] = "R_T2", [11] = "R_T3",
|
||||
@@ -87,10 +83,8 @@ local INT_CODE_TO_POOL_GPR = {
|
||||
--- @param tbl table<string, string> -- bag: key set only; values unused
|
||||
--- @return string[]
|
||||
local function stable_sort_keys(tbl)
|
||||
--- @type string[]
|
||||
local keys = {}
|
||||
--- @type string
|
||||
for k in pairs(tbl) do keys[#keys + 1] = k end
|
||||
local keys = {} ---@type string[]
|
||||
for k in pairs(tbl) do keys[#keys + 1] = k end ---@type string
|
||||
table.sort(keys)
|
||||
return keys
|
||||
end
|
||||
@@ -105,18 +99,12 @@ local function allocate_phase(phase_label, decls)
|
||||
-- Deep-copy POOL into a fresh sequence table. The original `table.unpack and table.unpack(POOL) or { unpack(POOL) }`
|
||||
-- idiom wraps the unpacked values in a single inner table under LuaJIT 5.1 (`table.unpack` is nil; the `or` returns one value),
|
||||
-- which corrupts the pool into `{ {R_T0, R_T1, ...} }` — making `table.remove(pool, 1)` return the inner table on iteration.
|
||||
--- @type GprIdent[]
|
||||
local pool = {}
|
||||
--- @type integer
|
||||
for i = 1, #POOL do pool[i] = POOL[i] end
|
||||
--- @type GprAllocMap
|
||||
local result = {}
|
||||
--- @type PassFinding[]
|
||||
local errors = {}
|
||||
--- @type integer, string
|
||||
for _, sym in ipairs(stable_sort_keys(decls)) do
|
||||
--- @type GprIdent|nil
|
||||
local next_gpr = table.remove(pool, 1)
|
||||
local pool = {} ---@type GprIdent[]
|
||||
for i = 1, #POOL do pool[i] = POOL[i] end ---@type integer
|
||||
local result = {} ---@type GprAllocMap
|
||||
local errors = {} ---@type PassFinding[]
|
||||
for _, sym in ipairs(stable_sort_keys(decls)) do ---@type integer, string
|
||||
local next_gpr = table.remove(pool, 1) ---@type GprIdent|nil
|
||||
if not next_gpr then
|
||||
errors[#errors + 1] = {
|
||||
line = 0,
|
||||
@@ -144,16 +132,12 @@ end
|
||||
--- @return table<GprIdent, boolean>
|
||||
--- @return table<string, GprIdent>
|
||||
local function build_user_pins(corpus)
|
||||
--- @type table<GprIdent, boolean> -- bag: pinned physical GPR -> true
|
||||
local user_pinned = {}
|
||||
--- @type table<string, GprIdent> -- bag: alias ident -> physical GPR
|
||||
local alias_to_gpr = {}
|
||||
local user_pinned = {} ---@type table<GprIdent, boolean> -- bag: pinned physical GPR -> true
|
||||
local alias_to_gpr = {} ---@type table<string, GprIdent> -- bag: alias ident -> physical GPR
|
||||
if not corpus.register_alias_registry then return user_pinned, alias_to_gpr end
|
||||
--- @type string, AliasEntry
|
||||
for alias_name, alias_entry in pairs(corpus.register_alias_registry) do
|
||||
for alias_name, alias_entry in pairs(corpus.register_alias_registry) do ---@type string, AliasEntry
|
||||
if alias_entry.has_atom_reg and alias_entry.code then
|
||||
--- @type GprIdent|nil
|
||||
local gpr = INT_CODE_TO_POOL_GPR[alias_entry.code]
|
||||
local gpr = INT_CODE_TO_POOL_GPR[alias_entry.code] ---@type GprIdent|nil
|
||||
if gpr then
|
||||
user_pinned[gpr] = true
|
||||
alias_to_gpr[alias_name] = gpr
|
||||
@@ -174,29 +158,22 @@ end
|
||||
--- @param alias_to_gpr table<string, GprIdent> -- bag: alias ident -> physical GPR
|
||||
--- @return table<GprIdent, integer>
|
||||
local function find_used_gprs(body_text, alias_to_gpr)
|
||||
--- @type table<GprIdent, integer> -- bag: physical GPR -> hit count
|
||||
local found = {}
|
||||
local found = {} ---@type table<GprIdent, integer> -- bag: physical GPR -> hit count
|
||||
-- (a) Hardcoded physical GPRs (R_T0..R_T7, R_V0..R_V1, R_A0..R_A3, R_S0..R_S7).
|
||||
--- @type GprIdent
|
||||
for gpr in body_text:gmatch("(R_T%d+|R_V%d+|R_A%d+|R_S%d+)") do
|
||||
for gpr in body_text:gmatch("(R_T%d+|R_V%d+|R_A%d+|R_S%d+)") do ---@type GprIdent
|
||||
found[gpr] = (found[gpr] or 0) + 1
|
||||
end
|
||||
-- (b) Alias references (R_<Alias>) resolved to physical GPRs via the registry.
|
||||
-- Sorted by name so the regex is byte-stable across runs.
|
||||
if alias_to_gpr and next(alias_to_gpr) then
|
||||
--- @type string[]
|
||||
local aliases = {}
|
||||
--- @type string
|
||||
for alias_name in pairs(alias_to_gpr) do
|
||||
local aliases = {} ---@type string[]
|
||||
for alias_name in pairs(alias_to_gpr) do ---@type string
|
||||
aliases[#aliases + 1] = alias_name
|
||||
end
|
||||
table.sort(aliases)
|
||||
--- @type string
|
||||
local pattern = "(" .. table.concat(aliases, "|") .. ")"
|
||||
--- @type string
|
||||
for alias_name in body_text:gmatch(pattern) do
|
||||
--- @type GprIdent|nil
|
||||
local gpr = alias_to_gpr[alias_name]
|
||||
local pattern = "(" .. table.concat(aliases, "|") .. ")" ---@type string
|
||||
for alias_name in body_text:gmatch(pattern) do ---@type string
|
||||
local gpr = alias_to_gpr[alias_name] ---@type GprIdent|nil
|
||||
if gpr and not found[gpr] then
|
||||
found[gpr] = 1
|
||||
end
|
||||
@@ -213,30 +190,24 @@ end
|
||||
--- @return string|nil
|
||||
local function emit_auto_reg_h(out_dir, dir, sources, mappings)
|
||||
if not mappings or next(mappings) == nil then return end
|
||||
--- @type string
|
||||
local out_path = out_dir .. "/" .. "auto_reg.h"
|
||||
local out_path = out_dir .. "/" .. "auto_reg.h" ---@type string
|
||||
duffle.ensure_dir(out_dir)
|
||||
--- @type string[]
|
||||
local lines = {
|
||||
local lines = { ---@type string[]
|
||||
"#ifdef INTELLISENSE_DIRECTIVES",
|
||||
"#pragma once",
|
||||
"#endif",
|
||||
"// Auto-generated by ps1_meta.lua (passes/auto_reg.lua) — DO NOT EDIT",
|
||||
"// Directory: " .. dir:gsub("/", "\\"),
|
||||
}
|
||||
--- @type integer, SourceFile
|
||||
for _, src in ipairs(sources) do
|
||||
for _, src in ipairs(sources) do ---@type integer, SourceFile
|
||||
lines[#lines + 1] = "// source: " .. src.path
|
||||
end
|
||||
lines[#lines + 1] = "// Per-phase register allocations resolved by the lua pass."
|
||||
lines[#lines + 1] = "// R_<Sym>_Code = <chosen GPR's _Code constant> for every marker in this directory."
|
||||
lines[#lines + 1] = ""
|
||||
--- @type integer, string
|
||||
for _, sym in ipairs(stable_sort_keys(mappings)) do
|
||||
--- @type GprIdent
|
||||
local gpr = mappings[sym]
|
||||
--- @type string
|
||||
local gpr_code = gpr .. "_Code"
|
||||
for _, sym in ipairs(stable_sort_keys(mappings)) do ---@type integer, string
|
||||
local gpr = mappings[sym] ---@type GprIdent
|
||||
local gpr_code = gpr .. "_Code" ---@type string
|
||||
lines[#lines + 1] = "#define " .. sym .. "_Code " .. gpr_code
|
||||
end
|
||||
lines[#lines + 1] = ""
|
||||
@@ -249,21 +220,16 @@ end
|
||||
-- Pass entry
|
||||
-- ════════════════════════════════════════════════════════════════════════════
|
||||
|
||||
--- @type AutoRegPass
|
||||
local M = {}
|
||||
local M = {} ---@type AutoRegPass
|
||||
|
||||
--- @param ctx PassCtx
|
||||
--- @return AutoRegResult
|
||||
function M.run(ctx)
|
||||
--- @type AutoRegOutput[]
|
||||
local outputs = {}
|
||||
--- @type PassFinding[]
|
||||
local errors = {}
|
||||
--- @type PassFinding[]
|
||||
local warnings = {}
|
||||
local outputs = {} ---@type AutoRegOutput[]
|
||||
local errors = {} ---@type PassFinding[]
|
||||
local warnings = {} ---@type PassFinding[]
|
||||
|
||||
--- @type Corpus|nil
|
||||
local corpus = ctx.shared and ctx.shared.corpus
|
||||
local corpus = ctx.shared and ctx.shared.corpus ---@type Corpus|nil
|
||||
if type(corpus) ~= "table" then
|
||||
error("auto_reg.run requires ctx.shared.corpus", 0)
|
||||
end
|
||||
@@ -273,23 +239,17 @@ function M.run(ctx)
|
||||
-- MUST NOT be allocated to any auto-reg marker — they're preserved across atoms by the wave-context discipline.
|
||||
-- The corpus's register_alias_registry is the source of truth for these opt-in pins.
|
||||
-- Body references to those aliases (via alias_to_gpr) are also excluded on a per-atom basis in step 2 below.
|
||||
--- @type table<GprIdent, boolean>, table<string, GprIdent>
|
||||
local user_pinned, alias_to_gpr = build_user_pins(corpus)
|
||||
local user_pinned, alias_to_gpr = build_user_pins(corpus) ---@type table<GprIdent, boolean>, table<string, GprIdent>
|
||||
|
||||
-- 1. Allocate phase pools first (phase declarations take precedence over per-atom declarations).
|
||||
--- @type table<string, GprAllocMap> -- bag: phase_label -> alloc map
|
||||
local phase_allocations = {}
|
||||
--- @type string, table<string, string>
|
||||
for phase_label, decls in pairs(corpus.phase_auto_regs or {}) do
|
||||
--- @type GprAllocMap, PassFinding[]
|
||||
local mapping, errs = allocate_phase(phase_label, decls)
|
||||
--- @type string, GprIdent
|
||||
for sym, gpr in pairs(mapping) do
|
||||
local phase_allocations = {} ---@type table<string, GprAllocMap> -- bag: phase_label -> alloc map
|
||||
for phase_label, decls in pairs(corpus.phase_auto_regs or {}) do ---@type string, table<string, string>
|
||||
local mapping, errs = allocate_phase(phase_label, decls) ---@type GprAllocMap, PassFinding[]
|
||||
for sym, gpr in pairs(mapping) do ---@type string, GprIdent
|
||||
phase_allocations[phase_label] = phase_allocations[phase_label] or {}
|
||||
phase_allocations[phase_label][sym] = gpr
|
||||
end
|
||||
--- @type integer, PassFinding
|
||||
for _, e in ipairs(errs) do
|
||||
for _, e in ipairs(errs) do ---@type integer, PassFinding
|
||||
errors[#errors + 1] = e
|
||||
end
|
||||
end
|
||||
@@ -298,22 +258,16 @@ function M.run(ctx)
|
||||
-- Otherwise, allocate a private pool for the atom.
|
||||
-- The phase membership is in `corpus.atom_phases[phase_label].atoms` (an array of atom names declared via `atom_phase(<phase>)`
|
||||
-- in the atom's `atom_info` line). Build a reverse map `atom_name -> phase_label` so the lookup is O(1) per atom scope.
|
||||
--- @type table<AtomName, string> -- bag: atom name -> phase label
|
||||
local atom_name_to_phase = {}
|
||||
--- @type string, AtomPhaseGroup
|
||||
for phase_label, entry in pairs(corpus.atom_phases or {}) do
|
||||
--- @type integer, AtomName
|
||||
for _, atom_name in ipairs(entry.atoms or {}) do
|
||||
local atom_name_to_phase = {} ---@type table<AtomName, string> -- bag: atom name -> phase label
|
||||
for phase_label, entry in pairs(corpus.atom_phases or {}) do ---@type string, AtomPhaseGroup
|
||||
for _, atom_name in ipairs(entry.atoms or {}) do ---@type integer, AtomName
|
||||
atom_name_to_phase[atom_name] = phase_label
|
||||
end
|
||||
end
|
||||
|
||||
--- @type table<AtomName, GprAllocMap> -- bag: atom scope -> alloc map
|
||||
local atom_allocations = {}
|
||||
--- @type AtomName, table<string, string>
|
||||
for atom_scope, decls in pairs(corpus.atom_auto_regs or {}) do
|
||||
--- @type string|nil
|
||||
local phase_label = atom_name_to_phase[atom_scope]
|
||||
local atom_allocations = {} ---@type table<AtomName, GprAllocMap> -- bag: atom scope -> alloc map
|
||||
for atom_scope, decls in pairs(corpus.atom_auto_regs or {}) do ---@type AtomName, table<string, string>
|
||||
local phase_label = atom_name_to_phase[atom_scope] ---@type string|nil
|
||||
-- Build the atom's source pool: start with the full POOL, subtract:
|
||||
-- (a) every GPR already committed (phase allocations + prior atom allocations)
|
||||
-- (b) every USER-PINNED GPR (wave-context carriers + file-scope pinned aliases)
|
||||
@@ -323,37 +277,26 @@ function M.run(ctx)
|
||||
-- the original `source_pool = phase_allocations[phase_label]` form used the phase
|
||||
-- allocation MAP as a pool, but that map has no array part, so `table.remove(source_pool, 1)`
|
||||
-- returned nil and every atom-with-phase marker errored with `phase_register_pool_exhausted`.
|
||||
--- @type table<GprIdent, boolean> -- bag: committed or body-referenced GPR -> true
|
||||
local used = {}
|
||||
--- @type integer, GprAllocMap
|
||||
for _, m in pairs(phase_allocations) do for _, gpr in pairs(m) do used[gpr] = true end end
|
||||
--- @type integer, GprAllocMap
|
||||
for _, m in pairs(atom_allocations) do for _, gpr in pairs(m) do used[gpr] = true end end
|
||||
local used = {} ---@type table<GprIdent, boolean> -- bag: committed or body-referenced GPR -> true
|
||||
for _, m in pairs(phase_allocations) do for _, gpr in pairs(m) do used[gpr] = true end end ---@type integer, GprAllocMap
|
||||
for _, m in pairs(atom_allocations) do for _, gpr in pairs(m) do used[gpr] = true end end ---@type integer, GprAllocMap
|
||||
-- (c) Body references — scan the atom body for hardcoded + alias-resolved GPRs.
|
||||
-- Folded into `used` so the source_pool exclusion is a single check.
|
||||
--- @type AtomEntry|nil
|
||||
local atom = corpus.atoms_by_name and corpus.atoms_by_name[atom_scope]
|
||||
local atom = corpus.atoms_by_name and corpus.atoms_by_name[atom_scope] ---@type AtomEntry|nil
|
||||
if atom and atom.body then
|
||||
--- @type table<GprIdent, integer>
|
||||
local body_used = find_used_gprs(atom.body, alias_to_gpr)
|
||||
--- @type GprIdent
|
||||
for gpr in pairs(body_used) do used[gpr] = true end
|
||||
local body_used = find_used_gprs(atom.body, alias_to_gpr) ---@type table<GprIdent, integer>
|
||||
for gpr in pairs(body_used) do used[gpr] = true end ---@type GprIdent
|
||||
end
|
||||
--- @type GprIdent[]
|
||||
local source_pool = {}
|
||||
--- @type integer, GprIdent
|
||||
for _, gpr in ipairs(POOL) do
|
||||
local source_pool = {} ---@type GprIdent[]
|
||||
for _, gpr in ipairs(POOL) do ---@type integer, GprIdent
|
||||
-- Exclude (a) prior commitments, (b) USER-PINNED GPRs (wave-context carriers declared via atom_reg + _Code defs, preserved across atoms globally).
|
||||
if not used[gpr] and not user_pinned[gpr] then
|
||||
source_pool[#source_pool + 1] = gpr
|
||||
end
|
||||
end
|
||||
--- @type GprAllocMap
|
||||
local result = {}
|
||||
--- @type integer, string
|
||||
for _, sym in ipairs(stable_sort_keys(decls)) do
|
||||
--- @type GprIdent|nil
|
||||
local next_gpr = table.remove(source_pool, 1)
|
||||
local result = {} ---@type GprAllocMap
|
||||
for _, sym in ipairs(stable_sort_keys(decls)) do ---@type integer, string
|
||||
local next_gpr = table.remove(source_pool, 1) ---@type GprIdent|nil
|
||||
if not next_gpr then
|
||||
errors[#errors + 1] = {
|
||||
line = 0,
|
||||
@@ -375,15 +318,11 @@ function M.run(ctx)
|
||||
-- This warning is kept as a defensive safety net for cases the body scanner might miss
|
||||
-- (e.g. macros that expand to register references the scanner cannot resolve).
|
||||
-- For each resolved (scope, sym) -> R_Tn mapping, scan the atom body source for used GPRs.
|
||||
--- @type AtomName, GprAllocMap
|
||||
for atom_scope, decls in pairs(atom_allocations) do
|
||||
--- @type AtomEntry|nil
|
||||
local atom = corpus.atoms_by_name and corpus.atoms_by_name[atom_scope]
|
||||
for atom_scope, decls in pairs(atom_allocations) do ---@type AtomName, GprAllocMap
|
||||
local atom = corpus.atoms_by_name and corpus.atoms_by_name[atom_scope] ---@type AtomEntry|nil
|
||||
if atom and atom.body then
|
||||
--- @type table<GprIdent, integer>
|
||||
local used_in_body = find_used_gprs(atom.body, alias_to_gpr)
|
||||
--- @type string, GprIdent
|
||||
for sym, allocated_gpr in pairs(decls) do
|
||||
local used_in_body = find_used_gprs(atom.body, alias_to_gpr) ---@type table<GprIdent, integer>
|
||||
for sym, allocated_gpr in pairs(decls) do ---@type string, GprIdent
|
||||
if used_in_body[allocated_gpr] and used_in_body[allocated_gpr] > 0 then
|
||||
warnings[#warnings + 1] = {
|
||||
line = atom.line or 0,
|
||||
@@ -398,37 +337,27 @@ function M.run(ctx)
|
||||
|
||||
-- 4. Emit per-directory gen/auto_reg.h.
|
||||
-- For each source directory that has atom_auto_regs or phase_auto_regs entries, emit one header.
|
||||
--- @type table<string, SourceFile[]>
|
||||
local sources_by_dir = corpus.sources_by_dir or {}
|
||||
--- @type string, SourceFile[]
|
||||
for dir, sources in pairs(sources_by_dir) do
|
||||
--- @type GprAllocMap
|
||||
local per_dir_mappings = {}
|
||||
--- @type integer, SourceFile
|
||||
for _, src in ipairs(sources) do
|
||||
local sources_by_dir = corpus.sources_by_dir or {} ---@type table<string, SourceFile[]>
|
||||
for dir, sources in pairs(sources_by_dir) do ---@type string, SourceFile[]
|
||||
local per_dir_mappings = {} ---@type GprAllocMap
|
||||
for _, src in ipairs(sources) do ---@type integer, SourceFile
|
||||
-- Collect every (sym -> gpr) entry that originated from a source in this directory.
|
||||
-- `src.scan.atom_auto_regs` is keyed by ATOM SCOPE NAME; `pairs(t)` iterates KEYS so `scope_name` here is the scope ident (e.g. "cube_g4_face").
|
||||
-- The previous `for _, scan_atom_auto` form silently assigned the VALUE (a `{sym = sym}` table) to the variable,
|
||||
-- which made `atom_allocations[scan_atom_auto]` a table-indexed lookup that never resolved.
|
||||
--- @type string
|
||||
for scope_name in pairs(src.scan and src.scan.atom_auto_regs or {}) do
|
||||
--- @type string, GprIdent
|
||||
for sym, gpr in pairs(atom_allocations[scope_name] or {}) do
|
||||
for scope_name in pairs(src.scan and src.scan.atom_auto_regs or {}) do ---@type string
|
||||
for sym, gpr in pairs(atom_allocations[scope_name] or {}) do ---@type string, GprIdent
|
||||
per_dir_mappings[sym] = gpr
|
||||
end
|
||||
end
|
||||
--- @type string
|
||||
for scope_name in pairs(src.scan and src.scan.phase_auto_regs or {}) do
|
||||
--- @type string, GprIdent
|
||||
for sym, gpr in pairs(phase_allocations[scope_name] or {}) do
|
||||
for scope_name in pairs(src.scan and src.scan.phase_auto_regs or {}) do ---@type string
|
||||
for sym, gpr in pairs(phase_allocations[scope_name] or {}) do ---@type string, GprIdent
|
||||
per_dir_mappings[sym] = gpr
|
||||
end
|
||||
end
|
||||
end
|
||||
--- @type string
|
||||
local out_dir = dir .. "/gen"
|
||||
--- @type string|nil
|
||||
local out_path = emit_auto_reg_h(out_dir, dir, sources, per_dir_mappings)
|
||||
local out_dir = dir .. "/gen" ---@type string
|
||||
local out_path = emit_auto_reg_h(out_dir, dir, sources, per_dir_mappings) ---@type string|nil
|
||||
if out_path then outputs[#outputs + 1] = { auto_reg_h = out_path } end
|
||||
end
|
||||
return { outputs = outputs, errors = errors, warnings = warnings }
|
||||
|
||||
+135
-270
@@ -22,42 +22,30 @@
|
||||
-- Uses `debug.getinfo` to find this file's own directory, so it works both standalone and when require'd from the orchestrator.
|
||||
-- Bootstrap: load `duffle_paths.lua` via `debug.getinfo(1, "S").source` (works both standalone + when require'd).
|
||||
-- duffle_paths.lua sets package.path then returns `require("duffle")` at the bottom, so the dofile value IS the duffle module.
|
||||
--- @type string
|
||||
local _bootstrap_dir = debug.getinfo(1, "S").source:match("^@?(.*[/\\])") or "./"
|
||||
--- @type DuffleExport
|
||||
local duffle = dofile(_bootstrap_dir .. "../duffle_paths.lua")
|
||||
local _bootstrap_dir = debug.getinfo(1, "S").source:match("^@?(.*[/\\])") or "./" ---@type string
|
||||
local duffle = dofile(_bootstrap_dir .. "../duffle_paths.lua") ---@type DuffleExport
|
||||
|
||||
-- ════════════════════════════════════════════════════════════════════════════
|
||||
-- Constants
|
||||
-- ════════════════════════════════════════════════════════════════════════════
|
||||
|
||||
-- Atom component declaration identifiers.
|
||||
--- @type string
|
||||
local ATOM_COMP_PROC = "MipsAtomComp_Proc_"
|
||||
--- @type string
|
||||
local MIPS_ATOM = "Slice_MipsCode" -- prefix on the function declaration that wraps an AtomComp_Proc_
|
||||
local ATOM_COMP_PROC = "MipsAtomComp_Proc_" ---@type string
|
||||
local MIPS_ATOM = "Slice_MipsCode" ---@type string -- prefix on the function declaration that wraps an AtomComp_Proc_
|
||||
|
||||
-- Component-name prefixes.
|
||||
--- @type string
|
||||
local AC_PREFIX = "ac_" -- arg to MipsAtomComp_(ac_X); the X is the atom name
|
||||
--- @type integer
|
||||
local AC_PREFIX_LEN = 3
|
||||
--- @type string
|
||||
local MAC_PREFIX = "mac_" -- prefix on generated macros; the rest is the atom name
|
||||
--- @type integer
|
||||
local MAC_PREFIX_LEN = 4
|
||||
local AC_PREFIX = "ac_" ---@type string -- arg to MipsAtomComp_(ac_X); the X is the atom name
|
||||
local AC_PREFIX_LEN = 3 ---@type integer
|
||||
local MAC_PREFIX = "mac_" ---@type string -- prefix on generated macros; the rest is the atom name
|
||||
local MAC_PREFIX_LEN = 4 ---@type integer
|
||||
|
||||
-- ASCII byte values used in tokenization.
|
||||
--- @type integer
|
||||
local BYTE_NEWLINE = 10
|
||||
--- @type integer
|
||||
local BYTE_SLASH = 47
|
||||
local BYTE_NEWLINE = 10 ---@type integer
|
||||
local BYTE_SLASH = 47 ---@type integer
|
||||
|
||||
-- Output gen subdirectory + filename (per-directory aggregation; the directory name is the namespace).
|
||||
--- @type string
|
||||
local GEN_SUBDIR = "gen"
|
||||
--- @type string
|
||||
local MACS_FILENAME = "macs.h"
|
||||
local GEN_SUBDIR = "gen" ---@type string
|
||||
local MACS_FILENAME = "macs.h" ---@type string
|
||||
|
||||
-- ════════════════════════════════════════════════════════════════════════════
|
||||
-- Type declarations
|
||||
@@ -100,8 +88,7 @@ local MACS_FILENAME = "macs.h"
|
||||
-- Local helpers (file I/O + path normalization)
|
||||
-- ════════════════════════════════════════════════════════════════════════════
|
||||
|
||||
--- @type ComponentsPass
|
||||
local M = {}
|
||||
local M = {} ---@type ComponentsPass
|
||||
|
||||
-- ════════════════════════════════════════════════════════════════════════════
|
||||
-- Back-walk helpers (composed into the entry point below: find_function_args_for)
|
||||
@@ -123,8 +110,7 @@ local M = {}
|
||||
--- @param before_pos integer
|
||||
--- @return string|nil
|
||||
local function find_function_args_for(source, name, before_pos)
|
||||
--- @type string|nil, string|nil
|
||||
local _, args_inner = duffle.find_function_decl_for(source, before_pos, #MIPS_ATOM)
|
||||
local _, args_inner = duffle.find_function_decl_for(source, before_pos, #MIPS_ATOM) ---@type string|nil, string|nil
|
||||
return args_inner
|
||||
end
|
||||
|
||||
@@ -140,31 +126,24 @@ end
|
||||
--- @return string[]|nil
|
||||
local function extract_arg_names(args_str)
|
||||
if not args_str or args_str == "" then return nil end
|
||||
--- @type string[]
|
||||
local names = {}
|
||||
--- @type string[]
|
||||
local tokens = duffle.split_top_level_commas(args_str)
|
||||
--- @type integer, string
|
||||
for _, tok in ipairs(tokens) do
|
||||
--- @type string
|
||||
local trimmed = duffle.trim(tok)
|
||||
local names = {} ---@type string[]
|
||||
local tokens = duffle.split_top_level_commas(args_str) ---@type string[]
|
||||
for _, tok in ipairs(tokens) do ---@type integer, string
|
||||
local trimmed = duffle.trim(tok) ---@type string
|
||||
if trimmed ~= "" then
|
||||
-- Strip trailing block comment (/* ... */) from the token, if present.
|
||||
-- split_top_level_commas only skips block comments at TOP LEVEL (between commas),
|
||||
-- not block comments embedded WITHIN a token between a parameter and a trailing comma.
|
||||
-- Without this strip, the identifier-walk below stops at the `/` of `*/` and returns
|
||||
-- the wrong name (or nothing). See `test_extract_arg_names_handles_trailing_block_comments`.
|
||||
--- @type integer
|
||||
local trimmed_end = #trimmed
|
||||
local trimmed_end = #trimmed ---@type integer
|
||||
if trimmed_end >= 2 and trimmed:sub(trimmed_end - 1, trimmed_end) == "*/" then
|
||||
-- Find the matching `/*` that opens the trailing comment.
|
||||
-- Walk back from the `*/` looking for `/*` (whitespace + `/*`).
|
||||
--- @type integer
|
||||
local close_pos = trimmed_end - 1 -- position of the second-to-last char
|
||||
local close_pos = trimmed_end - 1 ---@type integer -- position of the second-to-last char
|
||||
-- Walk back: skip trailing whitespace, then look for the `/*` opener.
|
||||
while close_pos > 1 do
|
||||
--- @type string
|
||||
local ch = trimmed:sub(close_pos, close_pos)
|
||||
local ch = trimmed:sub(close_pos, close_pos) ---@type string
|
||||
if ch == " " or ch == "\t" or ch == "\n" or ch == "\r" then
|
||||
close_pos = close_pos - 1
|
||||
else
|
||||
@@ -172,10 +151,8 @@ local function extract_arg_names(args_str)
|
||||
end
|
||||
end
|
||||
-- Now scan back from close_pos for the `/*` opener (slashes are at close_pos-1 and close_pos-2).
|
||||
--- @type integer|nil
|
||||
local opener_pos = nil
|
||||
--- @type integer
|
||||
local scan = close_pos - 3
|
||||
local opener_pos = nil ---@type integer|nil
|
||||
local scan = close_pos - 3 ---@type integer
|
||||
while scan >= 1 do
|
||||
if trimmed:sub(scan, scan + 1) == "/*" then
|
||||
opener_pos = scan
|
||||
@@ -194,11 +171,9 @@ local function extract_arg_names(args_str)
|
||||
trimmed_end = #trimmed
|
||||
if trimmed_end >= 4 and trimmed:sub(trimmed_end, trimmed_end) == "]" then
|
||||
-- Walk back: skip digits, expect `[`.
|
||||
--- @type integer
|
||||
local bracket_pos = trimmed_end - 1
|
||||
local bracket_pos = trimmed_end - 1 ---@type integer
|
||||
while bracket_pos > 1 do
|
||||
--- @type string
|
||||
local ch = trimmed:sub(bracket_pos, bracket_pos)
|
||||
local ch = trimmed:sub(bracket_pos, bracket_pos) ---@type string
|
||||
if ch >= "0" and ch <= "9" then
|
||||
bracket_pos = bracket_pos - 1
|
||||
else
|
||||
@@ -212,22 +187,18 @@ local function extract_arg_names(args_str)
|
||||
if trimmed == "" then goto continue end
|
||||
-- Find the identifier at the end: walk back over trailers (whitespace + `*` + `[]`),
|
||||
-- then walk back over the identifier chars (alnum + `_`).
|
||||
--- @type integer
|
||||
local ident_end = #trimmed
|
||||
local ident_end = #trimmed ---@type integer
|
||||
while ident_end > 0 do
|
||||
--- @type string
|
||||
local ch = trimmed:sub(ident_end, ident_end)
|
||||
local ch = trimmed:sub(ident_end, ident_end) ---@type string
|
||||
if ch == " " or ch == "\t" or ch == "*" or ch == "]" or ch == "[" then
|
||||
ident_end = ident_end - 1
|
||||
else
|
||||
break
|
||||
end
|
||||
end
|
||||
--- @type integer
|
||||
local ident_start = ident_end
|
||||
local ident_start = ident_end ---@type integer
|
||||
while ident_start > 0 do
|
||||
--- @type string
|
||||
local ch = trimmed:sub(ident_start, ident_start)
|
||||
local ch = trimmed:sub(ident_start, ident_start) ---@type string
|
||||
if duffle.is_alnum_byte(string.byte(ch)) or ch == "_" then
|
||||
ident_start = ident_start - 1
|
||||
else
|
||||
@@ -235,8 +206,7 @@ local function extract_arg_names(args_str)
|
||||
end
|
||||
end
|
||||
ident_start = ident_start + 1
|
||||
--- @type string
|
||||
local name = trimmed:sub(ident_start, ident_end)
|
||||
local name = trimmed:sub(ident_start, ident_end) ---@type string
|
||||
if name ~= "" then names[#names + 1] = name end
|
||||
::continue::
|
||||
end
|
||||
@@ -248,8 +218,7 @@ end
|
||||
--- @param args_str string|nil
|
||||
--- @return string[]|nil
|
||||
local function formal_arg_names(args_str)
|
||||
--- @type string[]|nil
|
||||
local names = extract_arg_names(args_str)
|
||||
local names = extract_arg_names(args_str) ---@type string[]|nil
|
||||
if not names then return nil end
|
||||
if names[1] == "ab" then table.remove(names, 1) end
|
||||
if #names == 0 then return nil end
|
||||
@@ -271,10 +240,8 @@ end
|
||||
--- @param scan SourceScan
|
||||
--- @return Component[]
|
||||
local function project_components(source, scan)
|
||||
--- @type Component[]
|
||||
local out = {}
|
||||
--- @type integer, AtomEntry
|
||||
for _, a in ipairs(scan.atoms) do
|
||||
local out = {} ---@type Component[]
|
||||
for _, a in ipairs(scan.atoms) do ---@type integer, AtomEntry
|
||||
-- Only `MipsAtomComp_(ac_X)` (kind="comp_bare") and `MipsAtomComp_Proc_(ac_X, ...)` (kind="comp_proc")
|
||||
-- are COMPONENTS — they get inlined via `mac_<name>` aliases inside atom bodies.
|
||||
-- `MipsAtom_Proc_` (kind="atom_proc") is an ATOM (ends with `mac_yield()`); it gets emitted via
|
||||
@@ -285,12 +252,10 @@ local function project_components(source, scan)
|
||||
-- Function-args lookup is meaningful for `MipsAtomComp_Proc_` components
|
||||
-- (the macro sits inside `FI_ Slice_MipsCode ac_X(...)`); the alias expansion
|
||||
-- discards the `ab` (atom-builder) arg the same way both forms do.
|
||||
--- @type string|nil
|
||||
local args = find_function_args_for(source, a.raw_name, a.ident_pos)
|
||||
local args = find_function_args_for(source, a.raw_name, a.ident_pos) ---@type string|nil
|
||||
-- Comment ownership: scan_source.lua stamps `declaration_comment` on the record by walking backward past any associated bare marker.
|
||||
-- The pass reads `declaration_comment` directly.
|
||||
--- @type string
|
||||
local comment = a.declaration_comment or ""
|
||||
local comment = a.declaration_comment or "" ---@type string
|
||||
out[#out + 1] = {
|
||||
line = a.line,
|
||||
name = a.name,
|
||||
@@ -321,31 +286,23 @@ end
|
||||
--- @param s string
|
||||
--- @return string
|
||||
local function convert_line_comments_to_block(s)
|
||||
--- @type string
|
||||
local result = s
|
||||
--- @type integer
|
||||
local pos = 1
|
||||
--- @type integer
|
||||
local len = #result
|
||||
local result = s ---@type string
|
||||
local pos = 1 ---@type integer
|
||||
local len = #result ---@type integer
|
||||
while pos <= len do
|
||||
--- @type boolean
|
||||
local is_double_slash = result:byte(pos) == BYTE_SLASH
|
||||
local is_double_slash = result:byte(pos) == BYTE_SLASH ---@type boolean
|
||||
and pos + 1 <= len and result:byte(pos + 1) == BYTE_SLASH
|
||||
if not is_double_slash then
|
||||
pos = pos + 1
|
||||
else
|
||||
-- Find end of line.
|
||||
--- @type integer
|
||||
local eol = pos
|
||||
local eol = pos ---@type integer
|
||||
while eol <= len and result:byte(eol) ~= BYTE_NEWLINE do
|
||||
eol = eol + 1
|
||||
end
|
||||
--- @type string
|
||||
local before = result:sub(1, pos - 1)
|
||||
--- @type string
|
||||
local comment = result:sub(pos + 2, eol - 1) -- skip the `//`
|
||||
--- @type string
|
||||
local after
|
||||
local before = result:sub(1, pos - 1) ---@type string
|
||||
local comment = result:sub(pos + 2, eol - 1) ---@type string -- skip the `//`
|
||||
local after ---@type string
|
||||
if eol <= len and result:byte(eol) == BYTE_NEWLINE then
|
||||
after = " */" .. result:sub(eol) -- keep the newline
|
||||
else
|
||||
@@ -382,14 +339,11 @@ end
|
||||
--- @param tok string
|
||||
--- @return string
|
||||
local function strip_leading_delay_marker(tok)
|
||||
--- @type string|nil
|
||||
local ident = duffle.read_ident(tok, 1)
|
||||
local ident = duffle.read_ident(tok, 1) ---@type string|nil
|
||||
if not ident or not duffle.DELAY_MARKERS[ident] then return tok end
|
||||
--- @type string
|
||||
local rest = tok:sub(#ident + 1):match("^%s*(.*)$") or ""
|
||||
local rest = tok:sub(#ident + 1):match("^%s*(.*)$") or "" ---@type string
|
||||
while rest:sub(1, 2) == "/*" do
|
||||
--- @type integer|nil
|
||||
local close = rest:find("*/", 3, true)
|
||||
local close = rest:find("*/", 3, true) ---@type integer|nil
|
||||
if not close then return "" end
|
||||
rest = rest:sub(close + 2):match("^%s*(.*)$") or ""
|
||||
end
|
||||
@@ -406,24 +360,17 @@ end
|
||||
local function word_count_rec(name, comp_by_name, wc, cache)
|
||||
if cache[name] ~= nil then return cache[name] end
|
||||
cache[name] = -1 -- mark in-progress (cycle detection)
|
||||
--- @type Component|nil
|
||||
local cc = comp_by_name[name]
|
||||
--- @type integer
|
||||
local n
|
||||
local cc = comp_by_name[name] ---@type Component|nil
|
||||
local n ---@type integer
|
||||
if cc then
|
||||
n = 0
|
||||
--- @type BodyToken[]
|
||||
local tokens = cc.body_tokens
|
||||
--- @type integer, BodyToken
|
||||
for _, t in ipairs(tokens) do
|
||||
--- @type string
|
||||
local trimmed = t.tok
|
||||
local tokens = cc.body_tokens ---@type BodyToken[]
|
||||
for _, t in ipairs(tokens) do ---@type integer, BodyToken
|
||||
local trimmed = t.tok ---@type string
|
||||
if trimmed ~= "" then
|
||||
--- @type string
|
||||
local work = trimmed
|
||||
local work = trimmed ---@type string
|
||||
while true do
|
||||
--- @type string|nil
|
||||
local marker = duffle.read_ident(work, 1)
|
||||
local marker = duffle.read_ident(work, 1) ---@type string|nil
|
||||
if marker and duffle.DELAY_MARKERS[marker] then
|
||||
work = strip_leading_delay_marker(work)
|
||||
if work == "" then break end
|
||||
@@ -432,8 +379,7 @@ local function word_count_rec(name, comp_by_name, wc, cache)
|
||||
end
|
||||
end
|
||||
if work ~= "" then
|
||||
--- @type string|nil
|
||||
local lookup = strip_mac_prefix(duffle.read_ident(work, 1))
|
||||
local lookup = strip_mac_prefix(duffle.read_ident(work, 1)) ---@type string|nil
|
||||
if lookup == "atom_label" or lookup == "atom_offset" then
|
||||
-- Pure metaprogram anchors; emit zero words.
|
||||
elseif lookup and comp_by_name[lookup] then
|
||||
@@ -466,16 +412,11 @@ end
|
||||
--- @param wc WordCounts
|
||||
--- @return table<string, integer> -- bag: bare component name -> word count
|
||||
local function count_all_components(components, wc)
|
||||
--- @type table<string, Component>
|
||||
local comp_by_name = {}
|
||||
--- @type integer, Component
|
||||
for _, cc in ipairs(components) do comp_by_name[cc.name] = cc end
|
||||
--- @type table<string, integer> -- bag: memo; -1 in-progress sentinel
|
||||
local cache = {}
|
||||
--- @type table<string, integer> -- bag: bare name -> word count
|
||||
local counts = {}
|
||||
--- @type integer, Component
|
||||
for _, c in ipairs(components) do
|
||||
local comp_by_name = {} ---@type table<string, Component>
|
||||
for _, cc in ipairs(components) do comp_by_name[cc.name] = cc end ---@type integer, Component
|
||||
local cache = {} ---@type table<string, integer> -- bag: memo; -1 in-progress sentinel
|
||||
local counts = {} ---@type table<string, integer> -- bag: bare name -> word count
|
||||
for _, c in ipairs(components) do ---@type integer, Component
|
||||
counts[c.name] = word_count_rec(c.name, comp_by_name, wc, cache)
|
||||
end
|
||||
return counts
|
||||
@@ -504,34 +445,23 @@ end
|
||||
local function component_meta_rec(name, comp_by_name, latency, cache)
|
||||
if cache[name] ~= nil then return cache[name] end
|
||||
cache[name] = { cycle_cost = -1, gp0_contrib = -1 }
|
||||
--- @type Component|nil
|
||||
local cc = comp_by_name[name]
|
||||
--- @type integer
|
||||
local cycle_cost
|
||||
--- @type integer
|
||||
local gp0_contrib
|
||||
local cc = comp_by_name[name] ---@type Component|nil
|
||||
local cycle_cost ---@type integer
|
||||
local gp0_contrib ---@type integer
|
||||
if cc then
|
||||
--- @type boolean
|
||||
local skip_cycle = (name == "yield")
|
||||
--- @type boolean
|
||||
local skip_gp0 = name:match("^insert_ot_tag") ~= nil
|
||||
local skip_cycle = (name == "yield") ---@type boolean
|
||||
local skip_gp0 = name:match("^insert_ot_tag") ~= nil ---@type boolean
|
||||
cycle_cost = 0
|
||||
gp0_contrib = 0
|
||||
if not skip_cycle or not skip_gp0 then
|
||||
--- @type BodyToken[]
|
||||
local tokens = cc.body_tokens
|
||||
--- @type integer, BodyToken
|
||||
for _, t in ipairs(tokens) do
|
||||
--- @type string
|
||||
local trimmed = t.tok
|
||||
local tokens = cc.body_tokens ---@type BodyToken[]
|
||||
for _, t in ipairs(tokens) do ---@type integer, BodyToken
|
||||
local trimmed = t.tok ---@type string
|
||||
if trimmed ~= "" then
|
||||
--- @type string|nil
|
||||
local ident = duffle.read_ident(trimmed, 1)
|
||||
local ident = duffle.read_ident(trimmed, 1) ---@type string|nil
|
||||
if ident and ident:sub(1, MAC_PREFIX_LEN) == MAC_PREFIX then
|
||||
--- @type string
|
||||
local nested = ident:sub(MAC_PREFIX_LEN + 1)
|
||||
--- @type ComponentMeta
|
||||
local nested_meta = component_meta_rec(nested, comp_by_name, latency, cache)
|
||||
local nested = ident:sub(MAC_PREFIX_LEN + 1) ---@type string
|
||||
local nested_meta = component_meta_rec(nested, comp_by_name, latency, cache) ---@type ComponentMeta
|
||||
if not skip_cycle then
|
||||
cycle_cost = cycle_cost + nested_meta.cycle_cost
|
||||
end
|
||||
@@ -540,10 +470,8 @@ local function component_meta_rec(name, comp_by_name, latency, cache)
|
||||
end
|
||||
else
|
||||
if not skip_cycle then
|
||||
--- @type InstructionRow|nil
|
||||
local isa = duffle.instr(ident)
|
||||
--- @type GteCommandRow|nil
|
||||
local gte = duffle.gte(ident)
|
||||
local isa = duffle.instr(ident) ---@type InstructionRow|nil
|
||||
local gte = duffle.gte(ident) ---@type GteCommandRow|nil
|
||||
cycle_cost = cycle_cost + ((isa and isa.cycles) or (gte and gte.cycles) or latency[ident] or 1)
|
||||
end
|
||||
if not skip_gp0 then
|
||||
@@ -578,16 +506,11 @@ end
|
||||
--- @param latency table<string, integer> -- bag: ident -> cycle cost
|
||||
--- @return ComponentMetaMap
|
||||
local function compute_components_metadata(components, latency)
|
||||
--- @type table<string, Component>
|
||||
local comp_by_name = {}
|
||||
--- @type integer, Component
|
||||
for _, cc in ipairs(components) do comp_by_name[cc.name] = cc end
|
||||
--- @type ComponentMetaMap
|
||||
local cache = {}
|
||||
--- @type ComponentMetaMap
|
||||
local out = {}
|
||||
--- @type integer, Component
|
||||
for _, c in ipairs(components) do
|
||||
local comp_by_name = {} ---@type table<string, Component>
|
||||
for _, cc in ipairs(components) do comp_by_name[cc.name] = cc end ---@type integer, Component
|
||||
local cache = {} ---@type ComponentMetaMap
|
||||
local out = {} ---@type ComponentMetaMap
|
||||
for _, c in ipairs(components) do ---@type integer, Component
|
||||
out[c.name] = component_meta_rec(c.name, comp_by_name, latency, cache)
|
||||
end
|
||||
return out
|
||||
@@ -602,15 +525,11 @@ end
|
||||
--- @param s string
|
||||
--- @return string[]
|
||||
local function split_comment_lines(s)
|
||||
--- @type string[]
|
||||
local out = {}
|
||||
--- @type integer
|
||||
local pos = 1
|
||||
--- @type integer
|
||||
local s_len = #s
|
||||
local out = {} ---@type string[]
|
||||
local pos = 1 ---@type integer
|
||||
local s_len = #s ---@type integer
|
||||
while pos <= s_len do
|
||||
--- @type integer|nil
|
||||
local nl = s:find("\n", pos, true)
|
||||
local nl = s:find("\n", pos, true) ---@type integer|nil
|
||||
if not nl then
|
||||
out[#out + 1] = s:sub(pos)
|
||||
break
|
||||
@@ -629,8 +548,7 @@ end
|
||||
--- @param args_str string|nil
|
||||
--- @return string
|
||||
local function signature_from_args(args_str)
|
||||
--- @type string[]|nil
|
||||
local names = formal_arg_names(args_str)
|
||||
local names = formal_arg_names(args_str) ---@type string[]|nil
|
||||
if names then
|
||||
return table.concat(names, ", ")
|
||||
end
|
||||
@@ -642,8 +560,7 @@ end
|
||||
--- @param lines string[]
|
||||
--- @return nil
|
||||
local function strip_trailing_continuation(lines)
|
||||
--- @type string
|
||||
local last = lines[#lines]
|
||||
local last = lines[#lines] ---@type string
|
||||
if last:sub(-2) == " \\" then
|
||||
lines[#lines] = last:sub(1, -3)
|
||||
end
|
||||
@@ -669,37 +586,30 @@ end
|
||||
--- @param tok string -- a single token from split_top_level_commas (already trimmed at the start, may contain trailing whitespace + block comment)
|
||||
--- @return boolean
|
||||
local function is_pure_delay_marker_token(tok)
|
||||
--- @type table<string, boolean> -- bag: delay-marker ident -> true
|
||||
local markers = duffle.DELAY_MARKERS
|
||||
local markers = duffle.DELAY_MARKERS ---@type table<string, boolean> -- bag: delay-marker ident -> true
|
||||
if type(markers) ~= "table" then return false end
|
||||
|
||||
-- Identify a leading delay-marker identifier (e.g. `GteDelay_`).
|
||||
--- @type integer
|
||||
local ident_end = 1
|
||||
local ident_end = 1 ---@type integer
|
||||
while ident_end <= #tok do
|
||||
--- @type string
|
||||
local ch = tok:sub(ident_end, ident_end)
|
||||
local ch = tok:sub(ident_end, ident_end) ---@type string
|
||||
if ch:match("[%w_]") then
|
||||
ident_end = ident_end + 1
|
||||
else
|
||||
break
|
||||
end
|
||||
end
|
||||
--- @type string
|
||||
local ident = tok:sub(1, ident_end - 1)
|
||||
local ident = tok:sub(1, ident_end - 1) ---@type string
|
||||
if not markers[ident] then return false end
|
||||
|
||||
-- Walk the remainder: only whitespace and block comments are allowed.
|
||||
--- @type integer
|
||||
local scan = ident_end
|
||||
local scan = ident_end ---@type integer
|
||||
while scan <= #tok do
|
||||
--- @type string
|
||||
local ch = tok:sub(scan, scan)
|
||||
local ch = tok:sub(scan, scan) ---@type string
|
||||
if ch:match("%s") then
|
||||
scan = scan + 1
|
||||
elseif ch == "/" and tok:sub(scan + 1, scan + 1) == "*" then
|
||||
--- @type integer|nil
|
||||
local close = tok:find("*/", scan + 2, true)
|
||||
local close = tok:find("*/", scan + 2, true) ---@type integer|nil
|
||||
if not close then return false end
|
||||
scan = close + 2
|
||||
else
|
||||
@@ -743,17 +653,14 @@ end
|
||||
--- @param tokens string[]
|
||||
--- @return nil
|
||||
local function emit_macro_body(lines, c, sig, tokens)
|
||||
--- @type integer
|
||||
for tok_idx = 1, #tokens do
|
||||
for tok_idx = 1, #tokens do ---@type integer
|
||||
tokens[tok_idx] = convert_line_comments_to_block(tokens[tok_idx])
|
||||
end
|
||||
if #tokens == 0 then return end
|
||||
lines[#lines + 1] = "#define mac_" .. c.name .. "(" .. sig .. ") \\"
|
||||
lines[#lines + 1] = "\t" .. tokens[1] .. " \\"
|
||||
--- @type integer
|
||||
for tok_idx = 2, #tokens do
|
||||
--- @type string
|
||||
local sep = token_skips_leading_comma(tokens[tok_idx]) and "\t" or ",\t"
|
||||
for tok_idx = 2, #tokens do ---@type integer
|
||||
local sep = token_skips_leading_comma(tokens[tok_idx]) and "\t" or ",\t" ---@type string
|
||||
lines[#lines + 1] = sep .. tokens[tok_idx] .. " \\"
|
||||
end
|
||||
strip_trailing_continuation(lines)
|
||||
@@ -768,8 +675,7 @@ end
|
||||
--- @param counts table<string, integer> -- bag: bare component name -> word count
|
||||
--- @return string[] -- list of lines for this component
|
||||
local function build_component_lines(c, counts)
|
||||
--- @type string[]
|
||||
local lines = {}
|
||||
local lines = {} ---@type string[]
|
||||
|
||||
-- Marker comment: emitted once for every skipped component.
|
||||
-- The marker is scanner-owned (declared by `atom_dbg_skip` immediately before the declaration in the source);
|
||||
@@ -779,21 +685,16 @@ local function build_component_lines(c, counts)
|
||||
end
|
||||
|
||||
if c.comment and c.comment ~= "" then
|
||||
--- @type integer, string
|
||||
for _, line in ipairs(split_comment_lines(c.comment)) do
|
||||
for _, line in ipairs(split_comment_lines(c.comment)) do ---@type integer, string
|
||||
lines[#lines + 1] = line
|
||||
end
|
||||
end
|
||||
|
||||
--- @type string[]
|
||||
local tokens = duffle.split_top_level_commas(c.body)
|
||||
--- @type integer
|
||||
for i = 1, #tokens do tokens[i] = duffle.trim(tokens[i]) end
|
||||
--- @type string
|
||||
local sig = signature_from_args(c.args)
|
||||
local tokens = duffle.split_top_level_commas(c.body) ---@type string[]
|
||||
for i = 1, #tokens do tokens[i] = duffle.trim(tokens[i]) end ---@type integer
|
||||
local sig = signature_from_args(c.args) ---@type string
|
||||
-- Direct lookup against the per-source precomputed `counts` table (built once by count_all_components).
|
||||
--- @type integer
|
||||
local n = counts[c.name]
|
||||
local n = counts[c.name] ---@type integer
|
||||
|
||||
if n > 0 then
|
||||
emit_macro_body(lines, c, sig, tokens)
|
||||
@@ -816,14 +717,11 @@ end
|
||||
--- @param sources SourceFile[] -- Sources contributing to this directory (for the header comment)
|
||||
--- @return string[]
|
||||
local function header_boilerplate(dir, sources)
|
||||
--- @type string[]
|
||||
local source_lines = { "// Directory: " .. duffle.to_absolute_path(dir) .. "/" }
|
||||
--- @type integer, SourceFile
|
||||
for _, src in ipairs(sources) do
|
||||
local source_lines = { "// Directory: " .. duffle.to_absolute_path(dir) .. "/" } ---@type string[]
|
||||
for _, src in ipairs(sources) do ---@type integer, SourceFile
|
||||
source_lines[#source_lines + 1] = "// source: " .. duffle.to_absolute_path(src.path)
|
||||
end
|
||||
--- @type string
|
||||
local source_blob = table.concat(source_lines, "\n")
|
||||
local source_blob = table.concat(source_lines, "\n") ---@type string
|
||||
return {
|
||||
-- #pragma once wrapped in #ifdef INTELLISENSE_DIRECTIVES, matching the convention in lottes_tape.h.
|
||||
-- The build does manual unity includes (the user controls include order), so the pragma is only active for IDE/tooling.
|
||||
@@ -851,10 +749,8 @@ end
|
||||
--- @return string -- Output directory
|
||||
--- @return string -- Full output path
|
||||
local function compute_macs_h_path(dir)
|
||||
--- @type string
|
||||
local out_dir = dir .. "/" .. GEN_SUBDIR
|
||||
--- @type string
|
||||
local out_path = out_dir .. "/" .. MACS_FILENAME
|
||||
local out_dir = dir .. "/" .. GEN_SUBDIR ---@type string
|
||||
local out_path = out_dir .. "/" .. MACS_FILENAME ---@type string
|
||||
return out_dir, out_path
|
||||
end
|
||||
|
||||
@@ -868,21 +764,16 @@ end
|
||||
--- @return string|nil -- Path to the written file (nil if no components)
|
||||
local function emit_component_macros_h(ctx, dir, sources, components, counts)
|
||||
if #components == 0 then return nil end
|
||||
--- @type string, string
|
||||
local out_dir, out_path = compute_macs_h_path(dir)
|
||||
--- @type string[]
|
||||
local lines = header_boilerplate(dir, sources)
|
||||
local out_dir, out_path = compute_macs_h_path(dir) ---@type string, string
|
||||
local lines = header_boilerplate(dir, sources) ---@type string[]
|
||||
|
||||
--- @type integer, Component
|
||||
for _, c in ipairs(components) do
|
||||
--- @type integer, string
|
||||
for _, l in ipairs(build_component_lines(c, counts)) do
|
||||
for _, c in ipairs(components) do ---@type integer, Component
|
||||
for _, l in ipairs(build_component_lines(c, counts)) do ---@type integer, string
|
||||
lines[#lines + 1] = l
|
||||
end
|
||||
end
|
||||
|
||||
--- @type string
|
||||
local content = table.concat(lines, "\n") .. "\n"
|
||||
local content = table.concat(lines, "\n") .. "\n" ---@type string
|
||||
duffle.ensure_dir(out_dir)
|
||||
duffle.write_file_lf(out_path, content)
|
||||
print(string.format(" -> %s", out_path))
|
||||
@@ -900,12 +791,9 @@ end
|
||||
--- @param counts table<string, integer> -- bag: bare component name -> word count
|
||||
--- @return nil
|
||||
local function update_canonical_word_counts(corpus, components, counts)
|
||||
--- @type WordCounts
|
||||
local wc = corpus.word_counts
|
||||
--- @type integer, Component
|
||||
for _, c in ipairs(components) do
|
||||
--- @type string
|
||||
local key = "mac_" .. c.name
|
||||
local wc = corpus.word_counts ---@type WordCounts
|
||||
for _, c in ipairs(components) do ---@type integer, Component
|
||||
local key = "mac_" .. c.name ---@type string
|
||||
if wc[key] == nil then
|
||||
wc[key] = counts[c.name]
|
||||
end
|
||||
@@ -932,15 +820,12 @@ end
|
||||
--- @param metadata ComponentMetaMap
|
||||
--- @return nil
|
||||
local function update_canonical_components(corpus, src, components, metadata)
|
||||
--- @type string
|
||||
local rel_path = src.path:gsub("\\", "/")
|
||||
--- @type integer, Component
|
||||
for _, c in ipairs(components) do
|
||||
local rel_path = src.path:gsub("\\", "/") ---@type string
|
||||
for _, c in ipairs(components) do ---@type integer, Component
|
||||
-- Keyed by bare name (e.g. `yield`, `load_tri_indices`).
|
||||
-- The atoms_source_map pass looks up components by bare name from the corpus;
|
||||
-- `mac_` prefix lives at the call-site identifier and is stripped before lookup.
|
||||
--- @type ComponentMeta|nil
|
||||
local m = metadata and metadata[c.name] or nil
|
||||
local m = metadata and metadata[c.name] or nil ---@type ComponentMeta|nil
|
||||
if corpus.components[c.name] == nil then
|
||||
corpus.components[c.name] = {
|
||||
name = c.name,
|
||||
@@ -954,13 +839,10 @@ local function update_canonical_components(corpus, src, components, metadata)
|
||||
else
|
||||
-- A second declaration of the same bare name: record a typed collision so static-analysis + the report can surface it.
|
||||
-- Identical-shape declarations (same path + line) reuse the first-wins entry without a collision record.
|
||||
--- @type ComponentDef
|
||||
local existing = corpus.components[c.name]
|
||||
local existing = corpus.components[c.name] ---@type ComponentDef
|
||||
if existing.path ~= rel_path or existing.line ~= c.line then
|
||||
--- @type string
|
||||
local kind = c.kind or "comp_bare"
|
||||
--- @type string
|
||||
local first_kind = existing.kind or "comp_bare"
|
||||
local kind = c.kind or "comp_bare" ---@type string
|
||||
local first_kind = existing.kind or "comp_bare" ---@type string
|
||||
corpus.collisions[#corpus.collisions + 1] = {
|
||||
kind = "component",
|
||||
name = c.name,
|
||||
@@ -983,10 +865,8 @@ end
|
||||
--- @param scan SourceScan
|
||||
--- @return nil
|
||||
local function update_canonical_component_body_index(corpus, src, components, scan)
|
||||
--- @type (fun(pos: integer): integer)|nil
|
||||
local line_of = scan and scan.line_of
|
||||
--- @type integer, Component
|
||||
for _, c in ipairs(components) do
|
||||
local line_of = scan and scan.line_of ---@type (fun(pos: integer): integer)|nil
|
||||
for _, c in ipairs(components) do ---@type integer, Component
|
||||
if corpus.component_body_index[c.name] == nil then
|
||||
corpus.component_body_index[c.name] = {
|
||||
body_tokens = c.body_tokens,
|
||||
@@ -1004,16 +884,12 @@ end
|
||||
--- @param ctx PassCtx
|
||||
--- @return PassResult
|
||||
function M.run(ctx)
|
||||
--- @type MacsOutput[]
|
||||
local outputs = {}
|
||||
--- @type PassFinding[]
|
||||
local errors = {}
|
||||
--- @type PassFinding[]
|
||||
local warnings = {}
|
||||
local outputs = {} ---@type MacsOutput[]
|
||||
local errors = {} ---@type PassFinding[]
|
||||
local warnings = {} ---@type PassFinding[]
|
||||
|
||||
-- Corpus ownership gate.
|
||||
--- @type Corpus|nil
|
||||
local corpus = ctx.shared and ctx.shared.corpus
|
||||
local corpus = ctx.shared and ctx.shared.corpus ---@type Corpus|nil
|
||||
if type(corpus) ~= "table" then
|
||||
error("components.run requires ctx.shared.corpus.", 0)
|
||||
end
|
||||
@@ -1034,22 +910,15 @@ function M.run(ctx)
|
||||
|
||||
-- Per-directory aggregation: every source in the same directory contributes to one `gen/macs.h`.
|
||||
-- The directory itself is the namespace. `corpus.sources_by_dir` preserves source-order within each bucket (matches `corpus.source_order`).
|
||||
--- @type table<string, SourceFile[]>
|
||||
local sources_by_dir = corpus.sources_by_dir or duffle.group_sources_by_dir(corpus.source_order)
|
||||
--- @type string, SourceFile[]
|
||||
for dir, sources in pairs(sources_by_dir) do
|
||||
local sources_by_dir = corpus.sources_by_dir or duffle.group_sources_by_dir(corpus.source_order) ---@type table<string, SourceFile[]>
|
||||
for dir, sources in pairs(sources_by_dir) do ---@type string, SourceFile[]
|
||||
-- Aggregate components from every source in this directory.
|
||||
-- `project_components` returns nil for sources with no `MipsAtomComp_` declarations; we skip those.
|
||||
--- @type Component[]
|
||||
local aggregated_components = {}
|
||||
--- @type table<SourceFile, ComponentMetaMap>
|
||||
local metadata_per_source = {}
|
||||
--- @type integer, SourceFile
|
||||
for _, src in ipairs(sources) do
|
||||
--- @type Component[]
|
||||
local per_source = project_components(src.text, src.scan) or {}
|
||||
--- @type integer, Component
|
||||
for _, c in ipairs(per_source) do
|
||||
local aggregated_components = {} ---@type Component[]
|
||||
local metadata_per_source = {} ---@type table<SourceFile, ComponentMetaMap>
|
||||
for _, src in ipairs(sources) do ---@type integer, SourceFile
|
||||
local per_source = project_components(src.text, src.scan) or {} ---@type Component[]
|
||||
for _, c in ipairs(per_source) do ---@type integer, Component
|
||||
aggregated_components[#aggregated_components + 1] = c
|
||||
end
|
||||
if #per_source > 0 then
|
||||
@@ -1059,18 +928,14 @@ function M.run(ctx)
|
||||
if #aggregated_components > 0 then
|
||||
-- Compute word counts across the aggregated set. `corpus.word_counts` carries the
|
||||
-- same-source + prior-directory entries so the recursive lookup sees both.
|
||||
--- @type table<string, integer> -- bag: bare name -> word count
|
||||
local counts = count_all_components(aggregated_components, corpus.word_counts)
|
||||
--- @type string|nil
|
||||
local macs_path = emit_component_macros_h(ctx, dir, sources, aggregated_components, counts)
|
||||
local counts = count_all_components(aggregated_components, corpus.word_counts) ---@type table<string, integer> -- bag: bare name -> word count
|
||||
local macs_path = emit_component_macros_h(ctx, dir, sources, aggregated_components, counts) ---@type string|nil
|
||||
if macs_path then
|
||||
outputs[#outputs + 1] = { macs_h = macs_path }
|
||||
-- Populate the projections AFTER disk emission (byte-identical `.macs.h` contract).
|
||||
update_canonical_word_counts(corpus, aggregated_components, counts)
|
||||
--- @type integer, SourceFile
|
||||
for _, src in ipairs(sources) do
|
||||
--- @type Component[]
|
||||
local per_source = project_components(src.text, src.scan) or {}
|
||||
for _, src in ipairs(sources) do ---@type integer, SourceFile
|
||||
local per_source = project_components(src.text, src.scan) or {} ---@type Component[]
|
||||
if #per_source > 0 then
|
||||
update_canonical_components(corpus, src, per_source, metadata_per_source[src])
|
||||
update_canonical_component_body_index(corpus, src, per_source, src.scan)
|
||||
|
||||
+423
-846
File diff suppressed because it is too large
Load Diff
@@ -117,16 +117,13 @@
|
||||
--- @class EmissionModelPass
|
||||
--- @field run fun(ctx: PassCtx): PassResult
|
||||
|
||||
--- @type EmissionModelPass
|
||||
local M = {}
|
||||
local M = {} ---@type EmissionModelPass
|
||||
|
||||
-- ─────────────────────────────────────────────────────────────────────────
|
||||
-- Bootstrap: load `duffle_paths.lua` via debug.getinfo so the module works standalone (run as `luajit passes/emission_model.lua`) and when require'd from the orchestrator.
|
||||
-- ─────────────────────────────────────────────────────────────────────────
|
||||
--- @type string
|
||||
local _bootstrap_dir = debug.getinfo(1, "S").source:match("^@?(.*[/\\])") or "./"
|
||||
--- @type DuffleExport
|
||||
local duffle = dofile(_bootstrap_dir .. "../duffle_paths.lua")
|
||||
local _bootstrap_dir = debug.getinfo(1, "S").source:match("^@?(.*[/\\])") or "./" ---@type string
|
||||
local duffle = dofile(_bootstrap_dir .. "../duffle_paths.lua") ---@type DuffleExport
|
||||
|
||||
-- ─────────────────────────────────────────────────────────────────────────
|
||||
-- Helpers
|
||||
@@ -149,8 +146,7 @@ local duffle = dofile(_bootstrap_dir .. "../duffle_paths.lua")
|
||||
--- @param corpus Corpus
|
||||
--- @return nil
|
||||
local function stamp_root_provenance(projection, atom_record, src, corpus)
|
||||
--- @type (fun(pos: integer): integer)|nil
|
||||
local root_line_of = src.scan and src.scan.line_of
|
||||
local root_line_of = src.scan and src.scan.line_of ---@type (fun(pos: integer): integer)|nil
|
||||
assert(type(root_line_of) == "function"
|
||||
, "emission_model: src.scan.line_of is required (canonical LineIndex closure over the source text) to stamp physical provenance")
|
||||
assert(type(atom_record.body_off) == "number"
|
||||
@@ -158,15 +154,11 @@ local function stamp_root_provenance(projection, atom_record, src, corpus)
|
||||
-- `root_body_line` is the physical source line of the ATOM HEADER byte containing the opening `{`; that byte is one byte BEFORE `atom_record.body_off`.
|
||||
-- The walker assigns line 2 to the body's first content line because line 1 is the trailing `\n` after `{`. Body-text line k therefore maps to `root_body_line + (k - 1)`.
|
||||
-- `body_off - 1` points at the opening `{`, whose line index identifies the header line. `body_off` points after `{` and would shift every word row forward by one line.
|
||||
--- @type integer
|
||||
local root_body_line = root_line_of(atom_record.body_off - 1) or atom_record.line or 0
|
||||
--- @type table<string, ComponentBodyEntry>
|
||||
local component_index = corpus.component_body_index or {}
|
||||
--- @type EmissionItem[]
|
||||
local word_items = {}
|
||||
local root_body_line = root_line_of(atom_record.body_off - 1) or atom_record.line or 0 ---@type integer
|
||||
local component_index = corpus.component_body_index or {} ---@type table<string, ComponentBodyEntry>
|
||||
local word_items = {} ---@type EmissionItem[]
|
||||
|
||||
--- @type integer, EmissionItem
|
||||
for _, item in ipairs(projection.items) do
|
||||
for _, item in ipairs(projection.items) do ---@type integer, EmissionItem
|
||||
if item.kind == "word" then word_items[#word_items + 1] = item end
|
||||
end
|
||||
|
||||
@@ -177,18 +169,14 @@ local function stamp_root_provenance(projection, atom_record, src, corpus)
|
||||
--- @param item EmissionItem
|
||||
--- @return integer
|
||||
local function body_line_for(event, item)
|
||||
--- @type integer[]
|
||||
local ids = event.invocation_ids or {}
|
||||
local ids = event.invocation_ids or {} ---@type integer[]
|
||||
-- The innermost open invocation identifies which line index the walker used.
|
||||
-- A component `line_of` makes `item.line` physical; the atom's `body_text` line index makes it body-relative.
|
||||
if ids and #ids > 0 then
|
||||
--- @type integer
|
||||
local inner_id = ids[#ids]
|
||||
--- @type InvocationRecord|nil
|
||||
local inner_inv = inner_id and projection.invocations[inner_id]
|
||||
local inner_id = ids[#ids] ---@type integer
|
||||
local inner_inv = inner_id and projection.invocations[inner_id] ---@type InvocationRecord|nil
|
||||
if inner_inv then
|
||||
--- @type ComponentBodyEntry|nil
|
||||
local component = component_index[inner_inv.component_name]
|
||||
local component = component_index[inner_inv.component_name] ---@type ComponentBodyEntry|nil
|
||||
if component and component.line_of then
|
||||
-- Walker used `comp.line_of`, which is the source's physical LineIndex. item.line is already physical.
|
||||
return item.line or 0
|
||||
@@ -203,10 +191,8 @@ local function stamp_root_provenance(projection, atom_record, src, corpus)
|
||||
-- Stamp the root source path onto invocation records whose `call_path` the walker left empty.
|
||||
-- The walker passes `body_entry.source` to `emit_invoke_begin`; `M.project_emission` creates the root `body_entry` with source `""`, leaving its `call_path` empty.
|
||||
-- This stamp gives every invocation a physical `call_path` matching `passes/atoms_source_map.lua`'s in-memory provenance projection.
|
||||
--- @type string
|
||||
local root_path = src.path or ""
|
||||
--- @type integer, InvocationRecord
|
||||
for _, inv in ipairs(projection.invocations) do
|
||||
local root_path = src.path or "" ---@type string
|
||||
for _, inv in ipairs(projection.invocations) do ---@type integer, InvocationRecord
|
||||
if inv.call_path == nil or inv.call_path == "" then
|
||||
inv.call_path = root_path
|
||||
end
|
||||
@@ -215,8 +201,7 @@ local function stamp_root_provenance(projection, atom_record, src, corpus)
|
||||
-- Normalize `inv.call_line` to a physical source line.
|
||||
-- * ROOT invocations (`parent_id == 0`) carry body-relative `call_line` values from `M.LineIndex(body_text)`; convert them once with `root_body_line`.
|
||||
-- * INNER invocations (`parent_id ~= 0`) carry physical `call_line` values from the component's `line_of`; retain them unchanged.
|
||||
--- @type integer, InvocationRecord
|
||||
for _, inv in ipairs(projection.invocations) do
|
||||
for _, inv in ipairs(projection.invocations) do ---@type integer, InvocationRecord
|
||||
if inv.parent_id == 0 then
|
||||
inv.call_line = (root_body_line or 0) + (inv.call_line or 1) - 1
|
||||
end
|
||||
@@ -225,21 +210,14 @@ local function stamp_root_provenance(projection, atom_record, src, corpus)
|
||||
-- Build `body_lines` for each invocation.
|
||||
-- `atoms_source_map` and `dwarf_injection` read `inv.body_lines[k]` directly from the invocation record created here.
|
||||
-- Component words already carry physical `item.line` values from the walker's COMPONENT line index, so `body_line_for` returns them unchanged.
|
||||
--- @type integer, InvocationRecord
|
||||
for _, inv in ipairs(projection.invocations) do
|
||||
--- @type integer
|
||||
local sw = inv.start_word
|
||||
--- @type integer
|
||||
local ew = inv.end_word
|
||||
--- @type integer[]
|
||||
local bls = {}
|
||||
--- @type integer
|
||||
for i = sw, ew do
|
||||
--- @type EmissionItem|nil
|
||||
local it = projection.items and projection.items[i]
|
||||
for _, inv in ipairs(projection.invocations) do ---@type integer, InvocationRecord
|
||||
local sw = inv.start_word ---@type integer
|
||||
local ew = inv.end_word ---@type integer
|
||||
local bls = {} ---@type integer[]
|
||||
for i = sw, ew do ---@type integer
|
||||
local it = projection.items and projection.items[i] ---@type EmissionItem|nil
|
||||
if it and it.kind == "word" then
|
||||
--- @type WordEvent
|
||||
local fake_event = { invocation_ids = { inv.id } }
|
||||
local fake_event = { invocation_ids = { inv.id } } ---@type WordEvent
|
||||
bls[#bls + 1] = body_line_for(fake_event, it) or 0
|
||||
end
|
||||
end
|
||||
@@ -249,21 +227,15 @@ local function stamp_root_provenance(projection, atom_record, src, corpus)
|
||||
-- Resolve each `word_event`'s physical `body_line` and `call_line`.
|
||||
-- For words inside an invocation, `we.call_line` identifies the OUTER atom source line containing the `mac_X(...)` token that triggered expansion.
|
||||
-- The root-invocation conversion above makes every `inv.call_line` physical; forward it directly and use each raw word's `body_line` as the fallback.
|
||||
--- @type integer, WordEvent
|
||||
for index, we in ipairs(projection.word_events) do
|
||||
--- @type EmissionItem
|
||||
local item = word_items[index] or {}
|
||||
--- @type integer
|
||||
local body_line = body_line_for(we, item)
|
||||
for index, we in ipairs(projection.word_events) do ---@type integer, WordEvent
|
||||
local item = word_items[index] or {} ---@type EmissionItem
|
||||
local body_line = body_line_for(we, item) ---@type integer
|
||||
item.line = body_line
|
||||
we.body_line = body_line
|
||||
|
||||
--- @type integer
|
||||
local call_line = body_line
|
||||
--- @type integer
|
||||
local outer_id = we.outermost_invocation_id or 0
|
||||
--- @type InvocationRecord|nil
|
||||
local outer_inv = projection.invocations[outer_id]
|
||||
local call_line = body_line ---@type integer
|
||||
local outer_id = we.outermost_invocation_id or 0 ---@type integer
|
||||
local outer_inv = projection.invocations[outer_id] ---@type InvocationRecord|nil
|
||||
if outer_inv then
|
||||
-- `outer_inv.call_line` is physical after the conversion loop above, so use it directly.
|
||||
call_line = outer_inv.call_line
|
||||
@@ -283,20 +255,15 @@ end
|
||||
--- @param corpus Corpus
|
||||
--- @return EmissionProjection
|
||||
local function project_atom(atom_record, src, corpus)
|
||||
--- @type string
|
||||
local body = atom_record.body or ""
|
||||
--- @type WordCounts
|
||||
local wc = corpus.word_counts or {}
|
||||
--- @type table<string, ComponentBodyEntry>
|
||||
local cbi = corpus.component_body_index or {}
|
||||
--- @type RegUseSchema|nil
|
||||
local schema = nil
|
||||
local body = atom_record.body or "" ---@type string
|
||||
local wc = corpus.word_counts or {} ---@type WordCounts
|
||||
local cbi = corpus.component_body_index or {} ---@type table<string, ComponentBodyEntry>
|
||||
local schema = nil ---@type RegUseSchema|nil
|
||||
if atom_record.reg_use_schema_name then
|
||||
schema = corpus.reg_use_schemas and corpus.reg_use_schemas[atom_record.reg_use_schema_name]
|
||||
end
|
||||
-- That construction site stamps `invocation.debug_skip` while appending each record to `proj.invocations`.
|
||||
--- @type EmissionProjection
|
||||
local proj = duffle.project_emission(body, cbi, wc, corpus.components, {
|
||||
local proj = duffle.project_emission(body, cbi, wc, corpus.components, { ---@type EmissionProjection
|
||||
reg_use_schema = schema,
|
||||
reg_use_param = atom_record.reg_use_param_name,
|
||||
atom_name = atom_record.name,
|
||||
@@ -308,14 +275,12 @@ local function project_atom(atom_record, src, corpus)
|
||||
msg = string.format("RegUse schema %q is missing", atom_record.reg_use_schema_name),
|
||||
}
|
||||
end
|
||||
--- @type integer, EmitError
|
||||
for _, err in ipairs(corpus.reg_use_errors or {}) do
|
||||
for _, err in ipairs(corpus.reg_use_errors or {}) do ---@type integer, EmitError
|
||||
if err.schema_name == atom_record.reg_use_schema_name then
|
||||
proj.errors[#proj.errors + 1] = err
|
||||
end
|
||||
end
|
||||
--- @type AtomPaths
|
||||
local paths = {
|
||||
local paths = { ---@type AtomPaths
|
||||
tokens = atom_record.body_tokens or {},
|
||||
line_in_body = duffle.build_body_line_index(body),
|
||||
items = proj.items,
|
||||
@@ -337,15 +302,11 @@ end
|
||||
--- @param ctx PassCtx -- { shared = { corpus = ... }, out_root, ... }
|
||||
--- @return PassResult
|
||||
function M.run(ctx)
|
||||
--- @type PassOutputEntry[]
|
||||
local outputs = {}
|
||||
--- @type EmitError[]
|
||||
local errors = {}
|
||||
--- @type EmitWarning[]
|
||||
local warnings = {}
|
||||
local outputs = {} ---@type PassOutputEntry[]
|
||||
local errors = {} ---@type EmitError[]
|
||||
local warnings = {} ---@type EmitWarning[]
|
||||
|
||||
--- @type Corpus|nil
|
||||
local corpus = ctx and ctx.shared and ctx.shared.corpus
|
||||
local corpus = ctx and ctx.shared and ctx.shared.corpus ---@type Corpus|nil
|
||||
if type(corpus) ~= "table" then error("emission_model: ctx.shared.corpus is required (canonical projection)", 0) end
|
||||
if type(corpus.source_order) ~= "table" then error("emission_model: ctx.shared.corpus.source_order is required", 0) end
|
||||
|
||||
@@ -356,15 +317,12 @@ function M.run(ctx)
|
||||
--- @return nil
|
||||
local function process_atom(atom, src)
|
||||
if not (atom and atom.body) then return end
|
||||
--- @type string
|
||||
local kind = atom.kind
|
||||
local kind = atom.kind ---@type string
|
||||
if kind ~= "atom" and kind ~= "atom_proc" and kind ~= "raw_atom" and kind ~= "comp_bare" and kind ~= "comp_proc" then
|
||||
return
|
||||
end
|
||||
--- @type EmissionProjection
|
||||
local proj = project_atom(atom, src, corpus)
|
||||
--- @type integer, EmitError
|
||||
for _, e in ipairs(proj.errors) do
|
||||
local proj = project_atom(atom, src, corpus) ---@type EmissionProjection
|
||||
for _, e in ipairs(proj.errors) do ---@type integer, EmitError
|
||||
-- Preserve `kind` (cycle / count_mismatch / unbalanced) so readers dispatch on the diagnostic class and leave the message string as display text.
|
||||
errors[#errors + 1] = {
|
||||
kind = e.kind,
|
||||
@@ -373,8 +331,7 @@ function M.run(ctx)
|
||||
source = e.source or src.path,
|
||||
}
|
||||
end
|
||||
--- @type integer, EmitWarning
|
||||
for _, w in ipairs(proj.warnings) do
|
||||
for _, w in ipairs(proj.warnings) do ---@type integer, EmitWarning
|
||||
warnings[#warnings + 1] = {
|
||||
kind = w.kind,
|
||||
line = w.line,
|
||||
@@ -386,16 +343,12 @@ function M.run(ctx)
|
||||
-- Walk `corpus.source_order`; within each source, visit atoms followed by raw_atoms.
|
||||
-- Recognized kinds (atom | atom_proc | raw_atom | comp_bare | comp_proc) each receive the atom.paths projection via duffle.project_emission.
|
||||
-- Components are macros inlined into atom bodies; focused tests and isolated component analyses consume atom.paths directly.
|
||||
--- @type integer, SourceFile
|
||||
for _, src in ipairs(corpus.source_order) do
|
||||
--- @type SourceScan
|
||||
local scan = src.scan or {}
|
||||
--- @type integer, AtomEntry
|
||||
for _, atom in ipairs(scan.atoms or {}) do
|
||||
for _, src in ipairs(corpus.source_order) do ---@type integer, SourceFile
|
||||
local scan = src.scan or {} ---@type SourceScan
|
||||
for _, atom in ipairs(scan.atoms or {}) do ---@type integer, AtomEntry
|
||||
process_atom(atom, src)
|
||||
end
|
||||
--- @type integer, AtomEntry
|
||||
for _, atom in ipairs(scan.raw_atoms or {}) do
|
||||
for _, atom in ipairs(scan.raw_atoms or {}) do ---@type integer, AtomEntry
|
||||
process_atom(atom, src)
|
||||
end
|
||||
end
|
||||
|
||||
+37
-74
@@ -20,24 +20,19 @@
|
||||
-- Uses `debug.getinfo` to find this file's own directory, so it works both standalone and when require'd from the orchestrator.
|
||||
-- Bootstrap: load `duffle_paths.lua` via `debug.getinfo(1, "S").source` (works both standalone + when require'd).
|
||||
-- duffle_paths.lua sets package.path then returns `require("duffle")` at the bottom, so the dofile value IS the duffle module.
|
||||
--- @type string
|
||||
local _bootstrap_dir = debug.getinfo(1, "S").source:match("^@?(.*[/\\])") or "./"
|
||||
--- @type DuffleExport
|
||||
local duffle = dofile(_bootstrap_dir .. "../duffle_paths.lua")
|
||||
local _bootstrap_dir = debug.getinfo(1, "S").source:match("^@?(.*[/\\])") or "./" ---@type string
|
||||
local duffle = dofile(_bootstrap_dir .. "../duffle_paths.lua") ---@type DuffleExport
|
||||
|
||||
-- ════════════════════════════════════════════════════════════════════════════
|
||||
-- Constants
|
||||
-- ════════════════════════════════════════════════════════════════════════════
|
||||
|
||||
-- Offset macro/enum naming prefixes (the emitted header uses these).
|
||||
--- @type string
|
||||
local OFFSET_MACRO_PREFIX = "_atom_offset_"
|
||||
--- @type string
|
||||
local OFFSET_ENUM_PREFIX = "atom_offset_"
|
||||
local OFFSET_MACRO_PREFIX = "_atom_offset_" ---@type string
|
||||
local OFFSET_ENUM_PREFIX = "atom_offset_" ---@type string
|
||||
|
||||
-- Column width for the `#define _atom_offset_F_T = N` alignment.
|
||||
--- @type integer
|
||||
local OFFSET_MACRO_COL = 44
|
||||
local OFFSET_MACRO_COL = 44 ---@type integer
|
||||
|
||||
-- ════════════════════════════════════════════════════════════════════════════
|
||||
-- Type declarations
|
||||
@@ -91,8 +86,7 @@ local OFFSET_MACRO_COL = 44
|
||||
-- MARKER_PROJECTORS is the marker-kind data table.
|
||||
-- The emission-model pass already records marker word positions + consuming-instruction context;
|
||||
-- this pass only projects those records into the label/branch lookup shape needed by offset computation.
|
||||
--- @type table<string, fun(state: MarkerProjectState, marker: EmissionMarker): nil>
|
||||
local MARKER_PROJECTORS = {
|
||||
local MARKER_PROJECTORS = { ---@type table<string, fun(state: MarkerProjectState, marker: EmissionMarker): nil>
|
||||
--- @param state MarkerProjectState
|
||||
--- @param marker EmissionMarker
|
||||
--- @return nil
|
||||
@@ -119,12 +113,9 @@ local MARKER_PROJECTORS = {
|
||||
--- @return table<string, integer>
|
||||
--- @return OffsetBranch[]
|
||||
local function project_markers(markers)
|
||||
--- @type MarkerProjectState
|
||||
local state = { labels = {}, branches = {} }
|
||||
--- @type integer, EmissionMarker
|
||||
for _, marker in ipairs(markers or {}) do
|
||||
--- @type (fun(state: MarkerProjectState, marker: EmissionMarker): nil)|nil
|
||||
local project = MARKER_PROJECTORS[marker.kind]
|
||||
local state = { labels = {}, branches = {} } ---@type MarkerProjectState
|
||||
for _, marker in ipairs(markers or {}) do ---@type integer, EmissionMarker
|
||||
local project = MARKER_PROJECTORS[marker.kind] ---@type (fun(state: MarkerProjectState, marker: EmissionMarker): nil)|nil
|
||||
if project then project(state, marker) end
|
||||
end
|
||||
return state.labels, state.branches
|
||||
@@ -149,20 +140,16 @@ end
|
||||
--- @param errors PassFinding[]
|
||||
--- @return BranchOffset[]
|
||||
local function compute_offsets(labels, branches, errors)
|
||||
--- @type BranchOffset[]
|
||||
local results = {}
|
||||
--- @type integer, OffsetBranch
|
||||
for _, br in ipairs(branches) do
|
||||
--- @type integer|nil
|
||||
local target = labels[br.target]
|
||||
local results = {} ---@type BranchOffset[]
|
||||
for _, br in ipairs(branches) do ---@type integer, OffsetBranch
|
||||
local target = labels[br.target] ---@type integer|nil
|
||||
if not target then
|
||||
errors[#errors + 1] = {
|
||||
line = br.line or 0,
|
||||
msg = "Branch target '" .. br.target .. "' has no atom_label (at word " .. br.branch_word .. ")",
|
||||
}
|
||||
else
|
||||
--- @type string|nil
|
||||
local consuming = br.consuming_encoder
|
||||
local consuming = br.consuming_encoder ---@type string|nil
|
||||
if consuming == nil or consuming == "" then
|
||||
errors[#errors + 1] = {
|
||||
line = br.line or 0,
|
||||
@@ -218,20 +205,16 @@ local function emit_atom_offsets(add, atom)
|
||||
if #atom.offsets == 0 then return end
|
||||
add("// --- atom: " .. atom.name .. " (" .. atom.total_words .. " words) ---")
|
||||
add("")
|
||||
--- @type OffsetConst[]
|
||||
local consts = {}
|
||||
--- @type integer, BranchOffset
|
||||
for _, r in ipairs(atom.offsets) do
|
||||
local consts = {} ---@type OffsetConst[]
|
||||
for _, r in ipairs(atom.offsets) do ---@type integer, BranchOffset
|
||||
consts[#consts + 1] = make_offset_const(r)
|
||||
end
|
||||
--- @type integer, OffsetConst
|
||||
for _, c in ipairs(consts) do
|
||||
for _, c in ipairs(consts) do ---@type integer, OffsetConst
|
||||
add("#define " .. pad_right(c.macro_name, OFFSET_MACRO_COL) .. " " .. c.value)
|
||||
end
|
||||
add("")
|
||||
add("enum {")
|
||||
--- @type integer, OffsetConst
|
||||
for _, c in ipairs(consts) do
|
||||
for _, c in ipairs(consts) do ---@type integer, OffsetConst
|
||||
add(" " .. c.enum_name .. " = " .. c.macro_name .. ",")
|
||||
end
|
||||
add("};")
|
||||
@@ -244,19 +227,16 @@ end
|
||||
--- @param atoms_data AtomData[]
|
||||
--- @return string
|
||||
local function generate_header(dir, sources, atoms_data)
|
||||
--- @type string
|
||||
local dir_basename = duffle.basename_no_ext(dir)
|
||||
local dir_basename = duffle.basename_no_ext(dir) ---@type string
|
||||
|
||||
--- @type string[]
|
||||
local lines = {}
|
||||
local lines = {} ---@type string[]
|
||||
--- @param s string
|
||||
--- @return nil
|
||||
local function add(s) lines[#lines + 1] = s end
|
||||
|
||||
add("// Auto-generated by ps1_meta.lua (passes/offsets.lua) — DO NOT EDIT")
|
||||
add("// Directory: " .. dir:gsub("/", "\\") .. "\\")
|
||||
--- @type integer, SourceFile
|
||||
for _, src in ipairs(sources) do
|
||||
for _, src in ipairs(sources) do ---@type integer, SourceFile
|
||||
add("// source: " .. src.path:gsub("/", "\\"))
|
||||
end
|
||||
add("#pragma once")
|
||||
@@ -264,8 +244,7 @@ local function generate_header(dir, sources, atoms_data)
|
||||
add("#pragma region " .. dir_basename)
|
||||
add("")
|
||||
add("")
|
||||
--- @type integer, AtomData
|
||||
for _, atom in ipairs(atoms_data) do
|
||||
for _, atom in ipairs(atoms_data) do ---@type integer, AtomData
|
||||
emit_atom_offsets(add, atom)
|
||||
end
|
||||
add("#pragma endregion " .. dir_basename)
|
||||
@@ -273,8 +252,7 @@ local function generate_header(dir, sources, atoms_data)
|
||||
return table.concat(lines, "\n") .. "\n"
|
||||
end
|
||||
|
||||
--- @type OffsetsPass
|
||||
local M = {}
|
||||
local M = {} ---@type OffsetsPass
|
||||
|
||||
--- (internal) Aggregate atoms from every source in one directory, render the per-directory `offsets.h`.
|
||||
--- Returns the offsets_h path if a header was written, or nil.
|
||||
@@ -284,17 +262,14 @@ local M = {}
|
||||
--- @param errors PassFinding[]
|
||||
--- @return string|nil
|
||||
local function process_directory(ctx, dir, sources, errors)
|
||||
--- @type AtomData[]
|
||||
local atoms_data = {}
|
||||
local atoms_data = {} ---@type AtomData[]
|
||||
|
||||
--- @param atom AtomEntry
|
||||
--- @return nil
|
||||
local function append_atom(atom)
|
||||
--- @type AtomPaths|nil
|
||||
local paths = atom and atom.paths
|
||||
local paths = atom and atom.paths ---@type AtomPaths|nil
|
||||
if not paths then return end
|
||||
--- @type table<string, integer>, OffsetBranch[]
|
||||
local labels, branches = project_markers(paths.markers)
|
||||
local labels, branches = project_markers(paths.markers) ---@type table<string, integer>, OffsetBranch[]
|
||||
atoms_data[#atoms_data + 1] = {
|
||||
name = atom.raw_name or atom.name,
|
||||
total_words = #(paths.word_events or {}),
|
||||
@@ -302,19 +277,14 @@ local function process_directory(ctx, dir, sources, errors)
|
||||
}
|
||||
end
|
||||
|
||||
--- @type integer, SourceFile
|
||||
for _, src in ipairs(sources) do
|
||||
--- @type SourceScan
|
||||
local scan = src.scan or {}
|
||||
--- @type integer, AtomEntry
|
||||
for _, atom in ipairs(scan.atoms or {}) do append_atom(atom) end
|
||||
--- @type integer, AtomEntry
|
||||
for _, atom in ipairs(scan.raw_atoms or {}) do append_atom(atom) end
|
||||
for _, src in ipairs(sources) do ---@type integer, SourceFile
|
||||
local scan = src.scan or {} ---@type SourceScan
|
||||
for _, atom in ipairs(scan.atoms or {}) do append_atom(atom) end ---@type integer, AtomEntry
|
||||
for _, atom in ipairs(scan.raw_atoms or {}) do append_atom(atom) end ---@type integer, AtomEntry
|
||||
end
|
||||
if #atoms_data == 0 then return nil end
|
||||
|
||||
--- @type string
|
||||
local out_path = dir .. "/gen/offsets.h"
|
||||
local out_path = dir .. "/gen/offsets.h" ---@type string
|
||||
duffle.ensure_dir(duffle.dirname(out_path))
|
||||
duffle.write_file(out_path, generate_header(dir, sources, atoms_data))
|
||||
return out_path
|
||||
@@ -326,15 +296,11 @@ end
|
||||
--- @param ctx PassCtx
|
||||
--- @return PassResult
|
||||
function M.run(ctx)
|
||||
--- @type OffsetOutput[]
|
||||
local outputs = {}
|
||||
--- @type PassFinding[]
|
||||
local errors = {}
|
||||
--- @type PassFinding[]
|
||||
local warnings = {}
|
||||
local outputs = {} ---@type OffsetOutput[]
|
||||
local errors = {} ---@type PassFinding[]
|
||||
local warnings = {} ---@type PassFinding[]
|
||||
|
||||
--- @type Corpus|nil
|
||||
local corpus = ctx.shared and ctx.shared.corpus
|
||||
local corpus = ctx.shared and ctx.shared.corpus ---@type Corpus|nil
|
||||
if type(corpus) ~= "table" then
|
||||
error("offsets.run requires ctx.shared.corpus", 0)
|
||||
end
|
||||
@@ -343,12 +309,9 @@ function M.run(ctx)
|
||||
end
|
||||
|
||||
-- Per-directory aggregation: every source in the same directory contributes to one `gen/offsets.h`.
|
||||
--- @type table<string, SourceFile[]>
|
||||
local sources_by_dir = corpus.sources_by_dir or duffle.group_sources_by_dir(corpus.source_order)
|
||||
--- @type string, SourceFile[]
|
||||
for dir, sources in pairs(sources_by_dir) do
|
||||
--- @type string|nil
|
||||
local out_path = process_directory(ctx, dir, sources, errors)
|
||||
local sources_by_dir = corpus.sources_by_dir or duffle.group_sources_by_dir(corpus.source_order) ---@type table<string, SourceFile[]>
|
||||
for dir, sources in pairs(sources_by_dir) do ---@type string, SourceFile[]
|
||||
local out_path = process_directory(ctx, dir, sources, errors) ---@type string|nil
|
||||
if out_path then
|
||||
outputs[#outputs + 1] = { offsets_h = out_path }
|
||||
end
|
||||
|
||||
+186
-372
@@ -18,16 +18,13 @@
|
||||
-- Uses `debug.getinfo` to find this file's own directory, so it works both standalone and when require'd from the orchestrator.
|
||||
-- Bootstrap: Load `duffle_paths.lua` via `debug.getinfo(1, "S").source` (works both standalone + when require'd).
|
||||
-- duffle_paths.lua sets package.path then returns `require("duffle")` at the bottom, so the dofile value IS the duffle module.
|
||||
--- @type string
|
||||
local _bootstrap_dir = debug.getinfo(1, "S").source:match("^@?(.*[/\\])") or "./"
|
||||
--- @type DuffleExport
|
||||
local duffle = dofile(_bootstrap_dir .. "../duffle_paths.lua")
|
||||
local _bootstrap_dir = debug.getinfo(1, "S").source:match("^@?(.*[/\\])") or "./" ---@type string
|
||||
local duffle = dofile(_bootstrap_dir .. "../duffle_paths.lua") ---@type DuffleExport
|
||||
|
||||
-- Load atoms_source_map for the `render_source_map` / `render_provenance` module functions (used by `render_module_atoms_md` to produce `<module>.atoms.md` without re-walking source tokens).
|
||||
-- The pass itself emits no per-source files anymore; we only consume the two pure renderers here.
|
||||
-- Defined BEFORE the renderer functions below so their upvalues resolve to this local (not the global `atoms_source_map`, which is nil).
|
||||
--- @type AtomSourceMapPass
|
||||
local atoms_source_map = dofile(_bootstrap_dir .. "atoms_source_map.lua")
|
||||
local atoms_source_map = dofile(_bootstrap_dir .. "atoms_source_map.lua") ---@type AtomSourceMapPass
|
||||
|
||||
-- ════════════════════════════════════════════════════════════════════════════
|
||||
-- Constants
|
||||
@@ -35,32 +32,22 @@ local atoms_source_map = dofile(_bootstrap_dir .. "atoms_source_map.lua")
|
||||
|
||||
-- Section separators used in the rendered text reports.
|
||||
-- The thin rules are hand-tuned to align with the per-section content width; do not change without also checking the section renderers below.
|
||||
--- @type string
|
||||
local RULE_THICK = "========================================================"
|
||||
--- @type string
|
||||
local SECTION_HEADER_ATOMS = "── Atoms ────────────────────────────────────────────────"
|
||||
--- @type string
|
||||
local SECTION_HEADER_ANNOTS = "── Annotations ──────────────────────────────────────────"
|
||||
--- @type string
|
||||
local SECTION_HEADER_BINDS = "── Binds_* structs ──────────────────────────────────────"
|
||||
--- @type string
|
||||
local SECTION_HEADER_MACROS = "── Macro word-count declarations ─────────────────────────"
|
||||
--- @type string
|
||||
local SECTION_HEADER_ERRORS = "── Errors ──────────────────────────────────────────────"
|
||||
--- @type string
|
||||
local SECTION_HEADER_WARNINGS = "── Warnings ────────────────────────────────────────────"
|
||||
local RULE_THICK = "========================================================" ---@type string
|
||||
local SECTION_HEADER_ATOMS = "── Atoms ────────────────────────────────────────────────" ---@type string
|
||||
local SECTION_HEADER_ANNOTS = "── Annotations ──────────────────────────────────────────" ---@type string
|
||||
local SECTION_HEADER_BINDS = "── Binds_* structs ──────────────────────────────────────" ---@type string
|
||||
local SECTION_HEADER_MACROS = "── Macro word-count declarations ─────────────────────────" ---@type string
|
||||
local SECTION_HEADER_ERRORS = "── Errors ──────────────────────────────────────────────" ---@type string
|
||||
local SECTION_HEADER_WARNINGS = "── Warnings ────────────────────────────────────────────" ---@type string
|
||||
|
||||
-- Lua pattern that captures the basename (last path segment) of a forward- or back-slash separated path.
|
||||
--- @type string
|
||||
local BASENAME_PATTERN = "([^/\\]+)$"
|
||||
local BASENAME_PATTERN = "([^/\\]+)$" ---@type string
|
||||
|
||||
-- Debug flag name — set to truthy in `_G` to enable verbose logging.
|
||||
--- @type string
|
||||
local DEBUG_FLAG = "_DEBUG_REPORT"
|
||||
local DEBUG_FLAG = "_DEBUG_REPORT" ---@type string
|
||||
|
||||
-- Pass identifier for log messages.
|
||||
--- @type string
|
||||
local PASS_NAME = "report"
|
||||
local PASS_NAME = "report" ---@type string
|
||||
|
||||
-- ════════════════════════════════════════════════════════════════════════════
|
||||
-- Type declarations
|
||||
@@ -245,18 +232,15 @@ end
|
||||
--- @param all_results ProjectSummaryRow[]
|
||||
--- @return string
|
||||
local function render_project_summary(all_results)
|
||||
--- @type string[]
|
||||
local lines = {
|
||||
local lines = { ---@type string[]
|
||||
"# Project summary",
|
||||
"> Auto-generated by ps1_meta.lua (passes/report.lua).",
|
||||
"",
|
||||
"| module | atoms | annots | binds | macros | findings | errors | warnings | info |",
|
||||
"|--------|-------|--------|-------|--------|----------|--------|----------|------|",
|
||||
}
|
||||
--- @type ProjectSummaryTotals
|
||||
local totals = { atoms = 0, annots = 0, binds = 0, macros = 0, findings = 0, errors = 0, warnings = 0, info = 0 }
|
||||
--- @type integer, ProjectSummaryRow
|
||||
for _, e in ipairs(all_results) do
|
||||
local totals = { atoms = 0, annots = 0, binds = 0, macros = 0, findings = 0, errors = 0, warnings = 0, info = 0 } ---@type ProjectSummaryTotals
|
||||
for _, e in ipairs(all_results) do ---@type integer, ProjectSummaryRow
|
||||
lines[#lines + 1] = string.format("| %s | %d | %d | %d | %d | %d | %d | %d | %d |"
|
||||
, e.module, e.atoms, e.annots, e.binds, e.macros, e.findings, e.errors, e.warnings, e.info)
|
||||
totals.atoms = totals.atoms + e.atoms
|
||||
@@ -281,29 +265,22 @@ end
|
||||
--- @param wc WordCounts
|
||||
--- @return string
|
||||
local function render_module_atoms_md(dir, dir_sources, wc)
|
||||
--- @type string
|
||||
local dir_basename = source_basename(dir)
|
||||
--- @type string[]
|
||||
local lines = {
|
||||
local dir_basename = source_basename(dir) ---@type string
|
||||
local lines = { ---@type string[]
|
||||
"# " .. dir_basename .. " — atoms (verbose source map)",
|
||||
"> Per-word call-site + provenance. Auto-generated.",
|
||||
"",
|
||||
}
|
||||
--- @type integer, SourceFile
|
||||
for _, src in ipairs(dir_sources) do
|
||||
--- @type string
|
||||
local src_name = source_basename(src.path)
|
||||
for _, src in ipairs(dir_sources) do ---@type integer, SourceFile
|
||||
local src_name = source_basename(src.path) ---@type string
|
||||
lines[#lines + 1] = "## " .. src_name
|
||||
lines[#lines + 1] = ""
|
||||
-- For each atom with a projection, render its sourcemap + provenance.
|
||||
--- @type AtomEntry[]
|
||||
local atoms_list = {}
|
||||
--- @type integer, AtomEntry
|
||||
for _, atom in ipairs((src.scan or {}).atoms or {}) do
|
||||
local atoms_list = {} ---@type AtomEntry[]
|
||||
for _, atom in ipairs((src.scan or {}).atoms or {}) do ---@type integer, AtomEntry
|
||||
if atom.paths then atoms_list[#atoms_list + 1] = atom end
|
||||
end
|
||||
--- @type integer, AtomEntry
|
||||
for _, atom in ipairs((src.scan or {}).raw_atoms or {}) do
|
||||
for _, atom in ipairs((src.scan or {}).raw_atoms or {}) do ---@type integer, AtomEntry
|
||||
if atom.paths then atoms_list[#atoms_list + 1] = atom end
|
||||
end
|
||||
if #atoms_list == 0 then
|
||||
@@ -312,10 +289,8 @@ local function render_module_atoms_md(dir, dir_sources, wc)
|
||||
else
|
||||
-- Per-source forward-slash path (same one `emit_atom_stanza` / `emit_provenance_stanza` would derive;
|
||||
-- computed once per `## <source>` heading and reused by each atom's `WORD N CALL ...` field).
|
||||
--- @type string
|
||||
local rel_path = src.path:gsub("\\\\", "/")
|
||||
--- @type integer, AtomEntry
|
||||
for _, atom in ipairs(atoms_list) do
|
||||
local rel_path = src.path:gsub("\\\\", "/") ---@type string
|
||||
for _, atom in ipairs(atoms_list) do ---@type integer, AtomEntry
|
||||
lines[#lines + 1] = string.format(
|
||||
"### atom: %s (line %d, %d words)",
|
||||
atom.name, atom.line or 0, #((atom.paths or {}).word_events or {}))
|
||||
@@ -342,18 +317,15 @@ end
|
||||
--- @param atom AtomEntry
|
||||
--- @return integer
|
||||
local function decl_words(atom)
|
||||
--- @type AtomPaths
|
||||
local p = atom.paths or {}
|
||||
local p = atom.paths or {} ---@type AtomPaths
|
||||
return #(p.word_events or {})
|
||||
end
|
||||
|
||||
--- @param decls AtomEntry[]|nil
|
||||
--- @return KindCounts
|
||||
local function count_kinds(decls)
|
||||
--- @type KindCounts
|
||||
local n = { atom = 0, atom_proc = 0, comp_bare = 0, comp_proc = 0 }
|
||||
--- @type integer, AtomEntry
|
||||
for _, a in ipairs(decls or {}) do
|
||||
local n = { atom = 0, atom_proc = 0, comp_bare = 0, comp_proc = 0 } ---@type KindCounts
|
||||
for _, a in ipairs(decls or {}) do ---@type integer, AtomEntry
|
||||
if n[a.kind] ~= nil then n[a.kind] = n[a.kind] + 1 end
|
||||
end
|
||||
return n
|
||||
@@ -369,10 +341,8 @@ end
|
||||
--- @param view ModuleView
|
||||
--- @return table<string, boolean>
|
||||
local function decl_names(view)
|
||||
--- @type table<string, boolean> -- bag: atom name -> true
|
||||
local names = {}
|
||||
--- @type integer, AtomEntry
|
||||
for _, a in ipairs(view.decls or {}) do
|
||||
local names = {} ---@type table<string, boolean> -- bag: atom name -> true
|
||||
for _, a in ipairs(view.decls or {}) do ---@type integer, AtomEntry
|
||||
if a.name then names[a.name] = true end
|
||||
end
|
||||
return names
|
||||
@@ -383,15 +353,12 @@ end
|
||||
--- @return boolean
|
||||
local function path_in_module(path, view)
|
||||
if type(path) ~= "string" or path == "" then return false end
|
||||
--- @type string
|
||||
local norm = path:gsub("\\", "/")
|
||||
--- @type string
|
||||
local dir = (view.dir or ""):gsub("\\", "/")
|
||||
local norm = path:gsub("\\", "/") ---@type string
|
||||
local dir = (view.dir or ""):gsub("\\", "/") ---@type string
|
||||
if dir ~= "" and (norm == dir or norm:sub(1, #dir + 1) == dir .. "/") then
|
||||
return true
|
||||
end
|
||||
--- @type integer, SourceFile
|
||||
for _, src in ipairs(view.sources or {}) do
|
||||
for _, src in ipairs(view.sources or {}) do ---@type integer, SourceFile
|
||||
if (src.path or ""):gsub("\\", "/") == norm then return true end
|
||||
end
|
||||
return false
|
||||
@@ -402,26 +369,18 @@ end
|
||||
--- @param corpus Corpus
|
||||
--- @return ModuleView
|
||||
local function build_module_view(dir, dir_sources, corpus)
|
||||
--- @type AtomEntry[]
|
||||
local decls = {}
|
||||
--- @type integer, SourceFile
|
||||
for _, src in ipairs(dir_sources or {}) do
|
||||
--- @type integer, AtomEntry
|
||||
for _, a in ipairs((src.scan and src.scan.atoms) or {}) do
|
||||
local decls = {} ---@type AtomEntry[]
|
||||
for _, src in ipairs(dir_sources or {}) do ---@type integer, SourceFile
|
||||
for _, a in ipairs((src.scan and src.scan.atoms) or {}) do ---@type integer, AtomEntry
|
||||
if not a.source_path then a.source_path = src.path end
|
||||
decls[#decls + 1] = a
|
||||
end
|
||||
end
|
||||
--- @type string
|
||||
local dir_basename = source_basename(dir)
|
||||
--- @type AtomAnalysis
|
||||
local sa = (corpus.static_analysis_results or {})[dir_basename] or {}
|
||||
--- @type RegUseSchema[]
|
||||
local schemas = {}
|
||||
--- @type string, RegUseSchema
|
||||
for name, schema in pairs(corpus.reg_use_schemas or {}) do
|
||||
--- @type integer, AtomEntry
|
||||
for _, a in ipairs(decls) do
|
||||
local dir_basename = source_basename(dir) ---@type string
|
||||
local sa = (corpus.static_analysis_results or {})[dir_basename] or {} ---@type AtomAnalysis
|
||||
local schemas = {} ---@type RegUseSchema[]
|
||||
for name, schema in pairs(corpus.reg_use_schemas or {}) do ---@type string, RegUseSchema
|
||||
for _, a in ipairs(decls) do ---@type integer, AtomEntry
|
||||
if a.reg_use_schema_name == name then
|
||||
schemas[#schemas + 1] = schema
|
||||
break
|
||||
@@ -445,10 +404,8 @@ local function render_section_declarations(add, view)
|
||||
if #view.decls == 0 then add("_(none)_"); add(""); return end
|
||||
add("| kind | name | source | line | words | min | max | branches | paths |")
|
||||
add("|------|------|--------|------|-------|-----|-----|----------|-------|")
|
||||
--- @type integer, AtomEntry
|
||||
for _, a in ipairs(view.decls) do
|
||||
--- @type AtomPaths
|
||||
local p = a.paths or {}
|
||||
for _, a in ipairs(view.decls) do ---@type integer, AtomEntry
|
||||
local p = a.paths or {} ---@type AtomPaths
|
||||
add(string.format("| %s | %s | %s | %d | %d | %s | %s | %s | %s |",
|
||||
a.kind or "?",
|
||||
a.name or "?",
|
||||
@@ -467,17 +424,12 @@ end
|
||||
--- @param view ModuleView
|
||||
--- @return nil
|
||||
local function render_section_components(add, view)
|
||||
--- @type ComponentReportRow[]
|
||||
local rows = {}
|
||||
--- @type table<string, ComponentBodyEntry>
|
||||
local index = (view.corpus and view.corpus.component_body_index) or {}
|
||||
--- @type integer, AtomEntry
|
||||
for _, a in ipairs(view.decls) do
|
||||
local rows = {} ---@type ComponentReportRow[]
|
||||
local index = (view.corpus and view.corpus.component_body_index) or {} ---@type table<string, ComponentBodyEntry>
|
||||
for _, a in ipairs(view.decls) do ---@type integer, AtomEntry
|
||||
if a.kind == "comp_bare" or a.kind == "comp_proc" then
|
||||
--- @type ComponentBodyEntry
|
||||
local idx = index[a.name] or {}
|
||||
--- @type string[]
|
||||
local args = idx.arg_names or {}
|
||||
local idx = index[a.name] or {} ---@type ComponentBodyEntry
|
||||
local args = idx.arg_names or {} ---@type string[]
|
||||
rows[#rows + 1] = {
|
||||
name = a.name,
|
||||
kind = a.kind,
|
||||
@@ -490,8 +442,7 @@ local function render_section_components(add, view)
|
||||
if #rows == 0 then add("_(none)_"); add(""); return end
|
||||
add("| name | kind | arg_names | words | map |")
|
||||
add("|------|------|-----------|-------|-----|")
|
||||
--- @type integer, ComponentReportRow
|
||||
for _, r in ipairs(rows) do
|
||||
for _, r in ipairs(rows) do ---@type integer, ComponentReportRow
|
||||
add(string.format("| %s | %s | %s | %d | %s |",
|
||||
r.name, r.kind, r.args ~= "" and r.args or "—", r.words, r.map))
|
||||
end
|
||||
@@ -502,38 +453,28 @@ end
|
||||
--- @param view ModuleView
|
||||
--- @return nil
|
||||
local function render_section_reguse(add, view)
|
||||
--- @type boolean
|
||||
local wrote = false
|
||||
--- @type integer, RegUseSchema
|
||||
for _, schema in ipairs(view.schemas or {}) do
|
||||
local wrote = false ---@type boolean
|
||||
for _, schema in ipairs(view.schemas or {}) do ---@type integer, RegUseSchema
|
||||
wrote = true
|
||||
add(string.format("### %s", schema.name or "?"))
|
||||
--- @type integer, RegUseSlot
|
||||
for _, slot in ipairs(schema.slots or {}) do
|
||||
--- @type string
|
||||
local aliases = table.concat(slot.aliases or { slot.name }, ", ")
|
||||
--- @type string
|
||||
local ro = slot.readonly and " readonly" or ""
|
||||
for _, slot in ipairs(schema.slots or {}) do ---@type integer, RegUseSlot
|
||||
local aliases = table.concat(slot.aliases or { slot.name }, ", ") ---@type string
|
||||
local ro = slot.readonly and " readonly" or "" ---@type string
|
||||
add(string.format("- slot `%s` aliases %s%s", slot.name, aliases, ro))
|
||||
end
|
||||
--- @type integer, AtomEntry
|
||||
for _, a in ipairs(view.decls) do
|
||||
for _, a in ipairs(view.decls) do ---@type integer, AtomEntry
|
||||
if a.reg_use_schema_name == schema.name then
|
||||
add(string.format("- bound `%s` param `%s`", a.name, a.reg_use_param_name or "?"))
|
||||
end
|
||||
end
|
||||
add("")
|
||||
end
|
||||
--- @type table<string, boolean> -- bag: schema name -> true
|
||||
local bound = {}
|
||||
--- @type integer, RegUseSchema
|
||||
for _, schema in ipairs(view.schemas or {}) do
|
||||
local bound = {} ---@type table<string, boolean> -- bag: schema name -> true
|
||||
for _, schema in ipairs(view.schemas or {}) do ---@type integer, RegUseSchema
|
||||
if schema.name then bound[schema.name] = true end
|
||||
end
|
||||
--- @type RegUseError[]
|
||||
local errors = {}
|
||||
--- @type integer, RegUseError
|
||||
for _, err in ipairs((view.corpus and view.corpus.reg_use_errors) or {}) do
|
||||
local errors = {} ---@type RegUseError[]
|
||||
for _, err in ipairs((view.corpus and view.corpus.reg_use_errors) or {}) do ---@type integer, RegUseError
|
||||
if bound[err.schema_name] or path_in_module(err.source_file, view) then
|
||||
errors[#errors + 1] = err
|
||||
end
|
||||
@@ -541,8 +482,7 @@ local function render_section_reguse(add, view)
|
||||
if #errors > 0 then
|
||||
wrote = true
|
||||
add("### parse errors")
|
||||
--- @type integer, RegUseError
|
||||
for _, err in ipairs(errors) do
|
||||
for _, err in ipairs(errors) do ---@type integer, RegUseError
|
||||
add(string.format("- `%s` %s", err.kind or "?", err.schema_name or ""))
|
||||
end
|
||||
add("")
|
||||
@@ -554,12 +494,9 @@ end
|
||||
--- @param view ModuleView
|
||||
--- @return nil
|
||||
local function render_section_annotations(add, view)
|
||||
--- @type AnnotReportRow[]
|
||||
local rows = {}
|
||||
--- @type integer, SourceFile
|
||||
for _, src in ipairs(view.sources) do
|
||||
--- @type integer, AtomInfoEntry
|
||||
for _, info in ipairs((src.scan and src.scan.atom_infos) or {}) do
|
||||
local rows = {} ---@type AnnotReportRow[]
|
||||
for _, src in ipairs(view.sources) do ---@type integer, SourceFile
|
||||
for _, info in ipairs((src.scan and src.scan.atom_infos) or {}) do ---@type integer, AtomInfoEntry
|
||||
rows[#rows + 1] = {
|
||||
source = source_basename(src.path),
|
||||
line = info.info_line or 0,
|
||||
@@ -574,8 +511,7 @@ local function render_section_annotations(add, view)
|
||||
if #rows == 0 then add("_(none)_"); add(""); return end
|
||||
add("| source | line | name | binds | reads | writes | phase |")
|
||||
add("|--------|------|------|-------|-------|--------|-------|")
|
||||
--- @type integer, AnnotReportRow
|
||||
for _, r in ipairs(rows) do
|
||||
for _, r in ipairs(rows) do ---@type integer, AnnotReportRow
|
||||
add(string.format("| %s | %d | %s | %s | %s | %s | %s |",
|
||||
r.source, r.line, r.name, r.binds, r.reads, r.writes, r.phase))
|
||||
end
|
||||
@@ -586,12 +522,9 @@ end
|
||||
--- @param view ModuleView
|
||||
--- @return nil
|
||||
local function render_section_component_annotations(add, view)
|
||||
--- @type CompAnnotReportRow[]
|
||||
local rows = {}
|
||||
--- @type integer, SourceFile
|
||||
for _, src in ipairs(view.sources) do
|
||||
--- @type integer, AtomInfoEntry
|
||||
for _, info in ipairs((src.scan and src.scan.component_atom_infos) or {}) do
|
||||
local rows = {} ---@type CompAnnotReportRow[]
|
||||
for _, src in ipairs(view.sources) do ---@type integer, SourceFile
|
||||
for _, info in ipairs((src.scan and src.scan.component_atom_infos) or {}) do ---@type integer, AtomInfoEntry
|
||||
rows[#rows + 1] = {
|
||||
source = source_basename(src.path),
|
||||
line = info.info_line or 0,
|
||||
@@ -604,8 +537,7 @@ local function render_section_component_annotations(add, view)
|
||||
if #rows == 0 then add("_(none)_"); add(""); return end
|
||||
add("| source | line | name | reads | writes |")
|
||||
add("|--------|------|------|-------|--------|")
|
||||
--- @type integer, CompAnnotReportRow
|
||||
for _, r in ipairs(rows) do
|
||||
for _, r in ipairs(rows) do ---@type integer, CompAnnotReportRow
|
||||
add(string.format("| %s | %d | %s | %s | %s |",
|
||||
r.source, r.line, r.name, r.reads, r.writes))
|
||||
end
|
||||
@@ -616,17 +548,13 @@ end
|
||||
--- @param view ModuleView
|
||||
--- @return nil
|
||||
local function render_section_binds(add, view)
|
||||
--- @type boolean
|
||||
local wrote = false
|
||||
--- @type integer, SourceFile
|
||||
for _, src in ipairs(view.sources) do
|
||||
--- @type integer, BindsEntry
|
||||
for _, b in ipairs((src.scan and src.scan.binds) or {}) do
|
||||
local wrote = false ---@type boolean
|
||||
for _, src in ipairs(view.sources) do ---@type integer, SourceFile
|
||||
for _, b in ipairs((src.scan and src.scan.binds) or {}) do ---@type integer, BindsEntry
|
||||
wrote = true
|
||||
add(string.format("### %s (%s:%s, %s bytes)",
|
||||
b.name, source_basename(src.path), tostring(b.line or 0), tostring(b.bytes or "—")))
|
||||
--- @type integer, TypeField
|
||||
for _, f in ipairs(b.fields or {}) do
|
||||
for _, f in ipairs(b.fields or {}) do ---@type integer, TypeField
|
||||
add(string.format("- `+%s %s`", tostring(f.offset or "?"), f.name or "?"))
|
||||
end
|
||||
add("")
|
||||
@@ -639,18 +567,12 @@ end
|
||||
--- @param view ModuleView
|
||||
--- @return nil
|
||||
local function render_section_phases(add, view)
|
||||
--- @type Corpus
|
||||
local corpus = view.corpus or {}
|
||||
--- @type table<string, boolean>
|
||||
local names = decl_names(view)
|
||||
--- @type boolean
|
||||
local wrote = false
|
||||
--- @type string, AtomPhaseGroup
|
||||
for phase, entry in pairs(corpus.atom_phases or {}) do
|
||||
--- @type string[]
|
||||
local here = {}
|
||||
--- @type integer, string
|
||||
for _, atom_name in ipairs(entry.atoms or {}) do
|
||||
local corpus = view.corpus or {} ---@type Corpus
|
||||
local names = decl_names(view) ---@type table<string, boolean>
|
||||
local wrote = false ---@type boolean
|
||||
for phase, entry in pairs(corpus.atom_phases or {}) do ---@type string, AtomPhaseGroup
|
||||
local here = {} ---@type string[]
|
||||
for _, atom_name in ipairs(entry.atoms or {}) do ---@type integer, string
|
||||
if names[atom_name] then here[#here + 1] = atom_name end
|
||||
end
|
||||
if #here > 0 then
|
||||
@@ -658,15 +580,13 @@ local function render_section_phases(add, view)
|
||||
add(string.format("- phase `%s`: %s", phase, table.concat(here, ", ")))
|
||||
end
|
||||
end
|
||||
--- @type string, AtomViewEntry
|
||||
for name, entry in pairs(corpus.atom_views or {}) do
|
||||
for name, entry in pairs(corpus.atom_views or {}) do ---@type string, AtomViewEntry
|
||||
if names[name] then
|
||||
wrote = true
|
||||
add(string.format("- view `%s` binds `%s`", name, entry.binds_name or "—"))
|
||||
end
|
||||
end
|
||||
--- @type string, AtomCtxEntry
|
||||
for name, entry in pairs(corpus.atom_ctxs or {}) do
|
||||
for name, entry in pairs(corpus.atom_ctxs or {}) do ---@type string, AtomCtxEntry
|
||||
if names[name] then
|
||||
wrote = true
|
||||
add(string.format("- ctx `%s` rbind `%s`", name, entry.rbind_atom or "—"))
|
||||
@@ -680,14 +600,10 @@ end
|
||||
--- @param view ModuleView
|
||||
--- @return nil
|
||||
local function render_section_aliases(add, view)
|
||||
--- @type string[]
|
||||
local names = {}
|
||||
--- @type table<string, AliasEntry>
|
||||
local seen = {}
|
||||
--- @type integer, SourceFile
|
||||
for _, src in ipairs(view.sources or {}) do
|
||||
--- @type string, AliasEntry
|
||||
for name, entry in pairs((src.scan and src.scan.register_alias_registry) or {}) do
|
||||
local names = {} ---@type string[]
|
||||
local seen = {} ---@type table<string, AliasEntry>
|
||||
for _, src in ipairs(view.sources or {}) do ---@type integer, SourceFile
|
||||
for name, entry in pairs((src.scan and src.scan.register_alias_registry) or {}) do ---@type string, AliasEntry
|
||||
if not seen[name] then
|
||||
seen[name] = entry
|
||||
names[#names + 1] = name
|
||||
@@ -698,10 +614,8 @@ local function render_section_aliases(add, view)
|
||||
if #names == 0 then add("_(none)_"); add(""); return end
|
||||
add("| alias | type |")
|
||||
add("|-------|------|")
|
||||
--- @type integer, string
|
||||
for _, name in ipairs(names) do
|
||||
--- @type AliasEntry|nil
|
||||
local e = seen[name]
|
||||
for _, name in ipairs(names) do ---@type integer, string
|
||||
local e = seen[name] ---@type AliasEntry|nil
|
||||
add(string.format("| %s | %s |", name, (e and e.default_type) or "—"))
|
||||
end
|
||||
add("")
|
||||
@@ -711,40 +625,30 @@ end
|
||||
--- @param view ModuleView
|
||||
--- @return nil
|
||||
local function render_section_autoreg(add, view)
|
||||
--- @type table<string, boolean>
|
||||
local allowed = decl_names(view)
|
||||
--- @type string, AtomPhaseGroup
|
||||
for phase, entry in pairs((view.corpus and view.corpus.atom_phases) or {}) do
|
||||
--- @type integer, string
|
||||
for _, atom_name in ipairs(entry.atoms or {}) do
|
||||
local allowed = decl_names(view) ---@type table<string, boolean>
|
||||
for phase, entry in pairs((view.corpus and view.corpus.atom_phases) or {}) do ---@type string, AtomPhaseGroup
|
||||
for _, atom_name in ipairs(entry.atoms or {}) do ---@type integer, string
|
||||
if allowed[atom_name] then allowed[phase] = true end
|
||||
end
|
||||
end
|
||||
--- @type boolean
|
||||
local wrote = false
|
||||
--- @type table<string, boolean> -- bag: label\\0scope -> already dumped
|
||||
local seen = {}
|
||||
local wrote = false ---@type boolean
|
||||
local seen = {} ---@type table<string, boolean> -- bag: label\\0scope -> already dumped
|
||||
--- @param label string
|
||||
--- @param table_map table<string, GprAllocMap>|nil
|
||||
--- @return nil
|
||||
local function dump(label, table_map)
|
||||
--- @type string[]
|
||||
local scopes = {}
|
||||
--- @type string
|
||||
for scope in pairs(table_map or {}) do
|
||||
local scopes = {} ---@type string[]
|
||||
for scope in pairs(table_map or {}) do ---@type string
|
||||
if allowed[scope] and not seen[label .. "\0" .. scope] then
|
||||
scopes[#scopes + 1] = scope
|
||||
end
|
||||
end
|
||||
table.sort(scopes)
|
||||
--- @type integer, string
|
||||
for _, scope in ipairs(scopes) do
|
||||
for _, scope in ipairs(scopes) do ---@type integer, string
|
||||
seen[label .. "\0" .. scope] = true
|
||||
wrote = true
|
||||
--- @type string[]
|
||||
local syms = {}
|
||||
--- @type string, string
|
||||
for sym, gpr in pairs(table_map[scope] or {}) do
|
||||
local syms = {} ---@type string[]
|
||||
for sym, gpr in pairs(table_map[scope] or {}) do ---@type string, string
|
||||
if type(gpr) == "string" and gpr ~= sym then
|
||||
syms[#syms + 1] = string.format("%s → %s", sym, gpr)
|
||||
else
|
||||
@@ -755,12 +659,10 @@ local function render_section_autoreg(add, view)
|
||||
add(string.format("- %s `%s`: %s", label, scope, table.concat(syms, ", ")))
|
||||
end
|
||||
end
|
||||
--- @type Corpus
|
||||
local corpus = view.corpus or {}
|
||||
local corpus = view.corpus or {} ---@type Corpus
|
||||
dump("atom", corpus.atom_auto_regs)
|
||||
dump("phase", corpus.phase_auto_regs)
|
||||
--- @type integer, SourceFile
|
||||
for _, src in ipairs(view.sources or {}) do
|
||||
for _, src in ipairs(view.sources or {}) do ---@type integer, SourceFile
|
||||
dump("atom", src.scan and src.scan.atom_auto_regs)
|
||||
dump("phase", src.scan and src.scan.phase_auto_regs)
|
||||
end
|
||||
@@ -772,25 +674,18 @@ end
|
||||
--- @param view ModuleView
|
||||
--- @return nil
|
||||
local function render_section_collisions(add, view)
|
||||
--- @type CorpusCollision[]
|
||||
local rows = {}
|
||||
--- @type integer, CorpusCollision
|
||||
for _, c in ipairs((view.corpus and view.corpus.collisions) or {}) do
|
||||
--- @type CollisionSite
|
||||
local first = c.first_site or {}
|
||||
--- @type CollisionSite
|
||||
local other = c.conflicting_site or {}
|
||||
local rows = {} ---@type CorpusCollision[]
|
||||
for _, c in ipairs((view.corpus and view.corpus.collisions) or {}) do ---@type integer, CorpusCollision
|
||||
local first = c.first_site or {} ---@type CollisionSite
|
||||
local other = c.conflicting_site or {} ---@type CollisionSite
|
||||
if path_in_module(first.path, view) or path_in_module(other.path, view) then
|
||||
rows[#rows + 1] = c
|
||||
end
|
||||
end
|
||||
if #rows == 0 then add("_(none)_"); add(""); return end
|
||||
--- @type integer, CorpusCollision
|
||||
for _, c in ipairs(rows) do
|
||||
--- @type CollisionSite
|
||||
local first = c.first_site or {}
|
||||
--- @type CollisionSite
|
||||
local other = c.conflicting_site or {}
|
||||
for _, c in ipairs(rows) do ---@type integer, CorpusCollision
|
||||
local first = c.first_site or {} ---@type CollisionSite
|
||||
local other = c.conflicting_site or {} ---@type CollisionSite
|
||||
add(string.format("- `%s` `%s` first %s:%s conflict %s:%s",
|
||||
c.kind or "?", c.name or "?",
|
||||
tostring(first.path or "?"), tostring(first.line or "?"),
|
||||
@@ -803,29 +698,22 @@ end
|
||||
--- @param view ModuleView
|
||||
--- @return nil
|
||||
local function render_section_findings(add, view)
|
||||
--- @type table<string, CheckFinding[]>
|
||||
local by_atom = {}
|
||||
--- @type integer, CheckFinding
|
||||
for _, f in ipairs(view.findings or {}) do
|
||||
--- @type string
|
||||
local key = f.atom or "?"
|
||||
local by_atom = {} ---@type table<string, CheckFinding[]>
|
||||
for _, f in ipairs(view.findings or {}) do ---@type integer, CheckFinding
|
||||
local key = f.atom or "?" ---@type string
|
||||
by_atom[key] = by_atom[key] or {}
|
||||
by_atom[key][#by_atom[key] + 1] = f
|
||||
end
|
||||
if next(by_atom) == nil then add("_(none)_"); add(""); return end
|
||||
--- @type table<string, boolean> -- bag: atom name already emitted
|
||||
local seen = {}
|
||||
local seen = {} ---@type table<string, boolean> -- bag: atom name already emitted
|
||||
--- @param name string
|
||||
--- @param fs CheckFinding[]
|
||||
--- @return nil
|
||||
local function emit(name, fs)
|
||||
add("### " .. name)
|
||||
--- @type integer, CheckFinding
|
||||
for _, f in ipairs(fs) do
|
||||
--- @type string
|
||||
local msg = f.msg or ""
|
||||
--- @type string|nil
|
||||
local slot = slot_suffix(f.gpr_key or f.producer_destination)
|
||||
for _, f in ipairs(fs) do ---@type integer, CheckFinding
|
||||
local msg = f.msg or "" ---@type string
|
||||
local slot = slot_suffix(f.gpr_key or f.producer_destination) ---@type string|nil
|
||||
if slot and not msg:find("(slot ", 1, true) then
|
||||
msg = msg .. " (slot " .. slot .. ")"
|
||||
end
|
||||
@@ -833,45 +721,34 @@ local function render_section_findings(add, view)
|
||||
end
|
||||
add("")
|
||||
end
|
||||
--- @type integer, AtomEntry
|
||||
for _, a in ipairs(view.decls) do
|
||||
for _, a in ipairs(view.decls) do ---@type integer, AtomEntry
|
||||
if by_atom[a.name] then
|
||||
seen[a.name] = true
|
||||
emit(a.name, by_atom[a.name])
|
||||
end
|
||||
end
|
||||
--- @type string[]
|
||||
local leftovers = {}
|
||||
--- @type string
|
||||
for name in pairs(by_atom) do
|
||||
local leftovers = {} ---@type string[]
|
||||
for name in pairs(by_atom) do ---@type string
|
||||
if not seen[name] then leftovers[#leftovers + 1] = name end
|
||||
end
|
||||
table.sort(leftovers)
|
||||
--- @type integer, string
|
||||
for _, name in ipairs(leftovers) do emit(name, by_atom[name]) end
|
||||
for _, name in ipairs(leftovers) do emit(name, by_atom[name]) end ---@type integer, string
|
||||
end
|
||||
|
||||
--- @param add fun(s: string): nil
|
||||
--- @param view ModuleView
|
||||
--- @return nil
|
||||
local function render_section_relations(add, view)
|
||||
--- @type boolean
|
||||
local wrote = false
|
||||
--- @type integer, AtomEntry
|
||||
for _, a in ipairs(view.decls) do
|
||||
--- @type AtomRelation[]
|
||||
local rels = (a.paths and a.paths.relations) or {}
|
||||
local wrote = false ---@type boolean
|
||||
for _, a in ipairs(view.decls) do ---@type integer, AtomEntry
|
||||
local rels = (a.paths and a.paths.relations) or {} ---@type AtomRelation[]
|
||||
if #rels > 0 then
|
||||
wrote = true
|
||||
add("### " .. a.name)
|
||||
--- @type integer, AtomRelation
|
||||
for _, rel in ipairs(rels) do
|
||||
--- @type string
|
||||
local dest = rel.destination or rel.producer_destination or "—"
|
||||
--- @type string|nil
|
||||
local slot = slot_suffix(dest)
|
||||
--- @type string
|
||||
local dest_s = tostring(dest)
|
||||
for _, rel in ipairs(rels) do ---@type integer, AtomRelation
|
||||
local dest = rel.destination or rel.producer_destination or "—" ---@type string
|
||||
local slot = slot_suffix(dest) ---@type string|nil
|
||||
local dest_s = tostring(dest) ---@type string
|
||||
if slot then dest_s = dest_s .. " (slot " .. slot .. ")" end
|
||||
add(string.format("- `%s` words %s → %s dest %s",
|
||||
rel.semantic or "?",
|
||||
@@ -885,13 +762,11 @@ local function render_section_relations(add, view)
|
||||
if not wrote then add("_(none)_"); add("") end
|
||||
end
|
||||
|
||||
--- @type table<string, boolean> -- bag: GPR key hidden unless an encoder wrote it
|
||||
local HIDDEN_UNLESS_WRITTEN = {
|
||||
local HIDDEN_UNLESS_WRITTEN = { ---@type table<string, boolean> -- bag: GPR key hidden unless an encoder wrote it
|
||||
R_AT = true, R_TapePtr = true, R_AtomJmp = true,
|
||||
}
|
||||
|
||||
--- @type table<string, boolean> -- bag: physical GPR alias -> true
|
||||
local PHYSICAL_GPR = {
|
||||
local PHYSICAL_GPR = { ---@type table<string, boolean> -- bag: physical GPR alias -> true
|
||||
R_T0 = true, R_T1 = true, R_T2 = true, R_T3 = true,
|
||||
R_T4 = true, R_T5 = true, R_T6 = true, R_T7 = true,
|
||||
R_V0 = true, R_V1 = true,
|
||||
@@ -901,10 +776,8 @@ local PHYSICAL_GPR = {
|
||||
--- @param key string
|
||||
--- @return boolean
|
||||
local function encoder_wrote_key(atom, key)
|
||||
--- @type integer, WordEvent
|
||||
for _, ev in ipairs((atom.paths and atom.paths.word_events) or {}) do
|
||||
--- @type integer|string, string
|
||||
for _, dest in pairs(ev.gpr_keys or {}) do
|
||||
for _, ev in ipairs((atom.paths and atom.paths.word_events) or {}) do ---@type integer, WordEvent
|
||||
for _, dest in pairs(ev.gpr_keys or {}) do ---@type integer|string, string
|
||||
if dest == key then return true end
|
||||
end
|
||||
end
|
||||
@@ -915,11 +788,9 @@ end
|
||||
--- @param atom AtomEntry
|
||||
--- @return string
|
||||
local function written_name_for(key, atom)
|
||||
--- @type string|nil
|
||||
local slot = key:match("^reguse:.+:(.+)$")
|
||||
local slot = key:match("^reguse:.+:(.+)$") ---@type string|nil
|
||||
if slot then
|
||||
--- @type string|nil
|
||||
local param = atom.reg_use_param_name
|
||||
local param = atom.reg_use_param_name ---@type string|nil
|
||||
if param and param ~= "" then return param .. "." .. slot end
|
||||
return slot
|
||||
end
|
||||
@@ -931,21 +802,15 @@ end
|
||||
--- @param view ModuleView
|
||||
--- @return string
|
||||
local function aliases_for_key(key, atom, view)
|
||||
--- @type string|nil
|
||||
local slot = key:match("^reguse:.+:(.+)$")
|
||||
local slot = key:match("^reguse:.+:(.+)$") ---@type string|nil
|
||||
if not slot then return "—" end
|
||||
--- @type string|nil
|
||||
local schema_name = atom.reg_use_schema_name
|
||||
--- @type RegUseSchema|nil
|
||||
local schema = view.corpus and view.corpus.reg_use_schemas and view.corpus.reg_use_schemas[schema_name]
|
||||
local schema_name = atom.reg_use_schema_name ---@type string|nil
|
||||
local schema = view.corpus and view.corpus.reg_use_schemas and view.corpus.reg_use_schemas[schema_name] ---@type RegUseSchema|nil
|
||||
if not schema then return "—" end
|
||||
--- @type integer, RegUseSlot
|
||||
for _, s in ipairs(schema.slots or {}) do
|
||||
for _, s in ipairs(schema.slots or {}) do ---@type integer, RegUseSlot
|
||||
if s.name == slot then
|
||||
--- @type string[]
|
||||
local names = {}
|
||||
--- @type integer, string
|
||||
for _, alias in ipairs(s.aliases or {}) do
|
||||
local names = {} ---@type string[]
|
||||
for _, alias in ipairs(s.aliases or {}) do ---@type integer, string
|
||||
if alias ~= slot then names[#names + 1] = alias end
|
||||
end
|
||||
if #names == 0 then
|
||||
@@ -964,25 +829,19 @@ end
|
||||
--- @return string
|
||||
local function physical_for_key(key, atom, view)
|
||||
if PHYSICAL_GPR[key] then return key end
|
||||
--- @type Corpus
|
||||
local corpus = view.corpus or {}
|
||||
--- @type AliasEntry|string|nil
|
||||
local alias = (corpus.register_alias_registry or {})[key]
|
||||
local corpus = view.corpus or {} ---@type Corpus
|
||||
local alias = (corpus.register_alias_registry or {})[key] ---@type AliasEntry|string|nil
|
||||
if type(alias) == "table" then
|
||||
--- @type string|nil
|
||||
local phys = alias.physical or alias.gpr or alias.code_name
|
||||
local phys = alias.physical or alias.gpr or alias.code_name ---@type string|nil
|
||||
if type(phys) == "string" and PHYSICAL_GPR[phys] then return phys end
|
||||
if type(alias.name) == "string" and PHYSICAL_GPR[alias.name] then return alias.name end
|
||||
elseif type(alias) == "string" and PHYSICAL_GPR[alias] then
|
||||
return alias
|
||||
end
|
||||
--- @type GprAllocMap|nil
|
||||
local atom_map = (corpus.atom_auto_regs or {})[atom.name]
|
||||
local atom_map = (corpus.atom_auto_regs or {})[atom.name] ---@type GprAllocMap|nil
|
||||
if type(atom_map) == "table" then
|
||||
--- @type string
|
||||
local slot = key:match("^reguse:.+:(.+)$") or key
|
||||
--- @type string|nil
|
||||
local bound = atom_map[slot] or atom_map["R_" .. slot]
|
||||
local slot = key:match("^reguse:.+:(.+)$") or key ---@type string
|
||||
local bound = atom_map[slot] or atom_map["R_" .. slot] ---@type string|nil
|
||||
if type(bound) == "string" and PHYSICAL_GPR[bound] then return bound end
|
||||
end
|
||||
return "—"
|
||||
@@ -992,21 +851,15 @@ end
|
||||
--- @param atom AtomEntry
|
||||
--- @return string
|
||||
local function last_relation_for(key, atom)
|
||||
--- @type AtomRelation|nil
|
||||
local last = nil
|
||||
--- @type integer, AtomRelation
|
||||
for _, rel in ipairs((atom.paths and atom.paths.relations) or {}) do
|
||||
--- @type string|nil
|
||||
local dest = rel.destination or rel.producer_destination
|
||||
local last = nil ---@type AtomRelation|nil
|
||||
for _, rel in ipairs((atom.paths and atom.paths.relations) or {}) do ---@type integer, AtomRelation
|
||||
local dest = rel.destination or rel.producer_destination ---@type string|nil
|
||||
if dest == key then last = rel end
|
||||
end
|
||||
if not last then return "—" end
|
||||
--- @type string
|
||||
local sem = last.semantic or "?"
|
||||
--- @type integer|nil
|
||||
local a = last.producer_word
|
||||
--- @type integer|nil
|
||||
local b = last.consumer_word
|
||||
local sem = last.semantic or "?" ---@type string
|
||||
local a = last.producer_word ---@type integer|nil
|
||||
local b = last.consumer_word ---@type integer|nil
|
||||
if a and b then return string.format("%s w%s→%s", sem, tostring(a), tostring(b)) end
|
||||
return sem
|
||||
end
|
||||
@@ -1015,16 +868,11 @@ end
|
||||
--- @param view ModuleView
|
||||
--- @return nil
|
||||
local function render_section_forward(add, view)
|
||||
--- @type boolean
|
||||
local wrote = false
|
||||
--- @type integer, AtomEntry
|
||||
for _, a in ipairs(view.decls) do
|
||||
--- @type table<string, GprLatticeSlot>|nil
|
||||
local gpr = a.paths and a.paths.forward_state and a.paths.forward_state.gpr_values
|
||||
--- @type string[]
|
||||
local keys = {}
|
||||
--- @type string
|
||||
for k in pairs(gpr or {}) do
|
||||
local wrote = false ---@type boolean
|
||||
for _, a in ipairs(view.decls) do ---@type integer, AtomEntry
|
||||
local gpr = a.paths and a.paths.forward_state and a.paths.forward_state.gpr_values ---@type table<string, GprLatticeSlot>|nil
|
||||
local keys = {} ---@type string[]
|
||||
for k in pairs(gpr or {}) do ---@type string
|
||||
if k == "R_0" then
|
||||
-- hidden
|
||||
elseif HIDDEN_UNLESS_WRITTEN[k] and not encoder_wrote_key(a, k) then
|
||||
@@ -1039,12 +887,9 @@ local function render_section_forward(add, view)
|
||||
add("| written | aliases | physical | lattice | last relation |")
|
||||
add("|---|---|---|---|---|")
|
||||
table.sort(keys)
|
||||
--- @type integer, string
|
||||
for _, k in ipairs(keys) do
|
||||
--- @type GprLatticeSlot|nil
|
||||
local slot = gpr[k]
|
||||
--- @type string
|
||||
local lattice = "—"
|
||||
for _, k in ipairs(keys) do ---@type integer, string
|
||||
local slot = gpr[k] ---@type GprLatticeSlot|nil
|
||||
local lattice = "—" ---@type string
|
||||
if slot and slot.kind == "constant" then
|
||||
lattice = tostring(slot.value)
|
||||
end
|
||||
@@ -1061,8 +906,7 @@ local function render_section_forward(add, view)
|
||||
if not wrote then add("_(none)_"); add("") end
|
||||
end
|
||||
|
||||
--- @type SectionRenderer[]
|
||||
local SECTION_RENDERERS = {
|
||||
local SECTION_RENDERERS = { ---@type SectionRenderer[]
|
||||
{ header = "## Declarations", render = render_section_declarations },
|
||||
{ header = "## Components", render = render_section_components },
|
||||
{ header = "## RegUse schemas", render = render_section_reguse },
|
||||
@@ -1083,10 +927,8 @@ local SECTION_RENDERERS = {
|
||||
--- @param view ModuleView
|
||||
--- @return string
|
||||
local function render_module_meta_report(view)
|
||||
--- @type string
|
||||
local dir_basename = source_basename(view.dir)
|
||||
--- @type string[]
|
||||
local lines = {
|
||||
local dir_basename = source_basename(view.dir) ---@type string
|
||||
local lines = { ---@type string[]
|
||||
"# " .. dir_basename .. " — atom meta report",
|
||||
"> Auto-generated by ps1_meta.lua (passes/report.lua). Do not edit.",
|
||||
"",
|
||||
@@ -1095,20 +937,15 @@ local function render_module_meta_report(view)
|
||||
--- @return nil
|
||||
local function add(s) lines[#lines + 1] = s end
|
||||
|
||||
--- @type KindCounts
|
||||
local kinds = count_kinds(view.decls)
|
||||
--- @type integer, integer, integer
|
||||
local n_annot, n_binds, n_macros = 0, 0, 0
|
||||
--- @type integer, SourceFile
|
||||
for _, src in ipairs(view.sources) do
|
||||
local kinds = count_kinds(view.decls) ---@type KindCounts
|
||||
local n_annot, n_binds, n_macros = 0, 0, 0 ---@type integer, integer, integer
|
||||
for _, src in ipairs(view.sources) do ---@type integer, SourceFile
|
||||
n_annot = n_annot + #((src.scan and src.scan.atom_infos) or {})
|
||||
n_binds = n_binds + #((src.scan and src.scan.binds) or {})
|
||||
n_macros = n_macros + #((src.scan and src.scan.macros) or {})
|
||||
end
|
||||
--- @type integer, integer, integer
|
||||
local n_err, n_warn, n_info = 0, 0, 0
|
||||
--- @type integer, CheckFinding
|
||||
for _, f in ipairs(view.findings or {}) do
|
||||
local n_err, n_warn, n_info = 0, 0, 0 ---@type integer, integer, integer
|
||||
for _, f in ipairs(view.findings or {}) do ---@type integer, CheckFinding
|
||||
if f.kind == "error" then n_err = n_err + 1
|
||||
elseif f.kind == "warning" then n_warn = n_warn + 1
|
||||
else n_info = n_info + 1
|
||||
@@ -1128,12 +965,10 @@ local function render_module_meta_report(view)
|
||||
add("")
|
||||
|
||||
add("## Sources"); add("")
|
||||
--- @type integer, SourceFile
|
||||
for _, s in ipairs(view.sources) do add("- `" .. s.path .. "`") end
|
||||
for _, s in ipairs(view.sources) do add("- `" .. s.path .. "`") end ---@type integer, SourceFile
|
||||
add("")
|
||||
|
||||
--- @type integer, SectionRenderer
|
||||
for _, row in ipairs(SECTION_RENDERERS) do
|
||||
for _, row in ipairs(SECTION_RENDERERS) do ---@type integer, SectionRenderer
|
||||
add(row.header); add("")
|
||||
row.render(add, view)
|
||||
end
|
||||
@@ -1147,8 +982,7 @@ end
|
||||
-- `once = true` means render once at the project level (not per-module).
|
||||
-- `basename(dir_basename)` yields the file's basename for that kind.
|
||||
-- `gather(ctx, dir, dir_sources [, all_modules])` returns the rendered string.
|
||||
--- @type ReportRenderer[]
|
||||
local REPORT_RENDERERS = {
|
||||
local REPORT_RENDERERS = { ---@type ReportRenderer[]
|
||||
{
|
||||
name = "atom_meta_report",
|
||||
ext = "md",
|
||||
@@ -1161,8 +995,7 @@ local REPORT_RENDERERS = {
|
||||
--- @param dir_sources SourceFile[]
|
||||
--- @return string
|
||||
gather = function(ctx, dir, dir_sources)
|
||||
--- @type Corpus
|
||||
local corpus = ctx.shared.corpus
|
||||
local corpus = ctx.shared.corpus ---@type Corpus
|
||||
return render_module_meta_report(build_module_view(dir, dir_sources, corpus))
|
||||
end,
|
||||
},
|
||||
@@ -1204,20 +1037,16 @@ local REPORT_RENDERERS = {
|
||||
-- M — public pass surface
|
||||
-- ════════════════════════════════════════════════════════════════════════════
|
||||
|
||||
--- @type ReportPass
|
||||
local M = {}
|
||||
local M = {} ---@type ReportPass
|
||||
|
||||
--- Run the report pass. Emits 1 `atom_meta_report.summary.md` per build + 2 `atom_meta_report.md` + 2 `atoms.md` files per module (duffle + gte_hello).
|
||||
--- Reads `corpus.static_analysis_results` (added in Phase 1) to populate per-module findings without re-running validate().
|
||||
--- @param ctx PassCtx
|
||||
--- @return PassResult
|
||||
function M.run(ctx)
|
||||
--- @type PassOutputEntry[]
|
||||
local outputs = {}
|
||||
--- @type Corpus|nil
|
||||
local corpus = ctx.shared and ctx.shared.corpus
|
||||
--- @type table<string, SourceFile[]>
|
||||
local by_dir = (corpus and corpus.sources_by_dir) or {}
|
||||
local outputs = {} ---@type PassOutputEntry[]
|
||||
local corpus = ctx.shared and ctx.shared.corpus ---@type Corpus|nil
|
||||
local by_dir = (corpus and corpus.sources_by_dir) or {} ---@type table<string, SourceFile[]>
|
||||
|
||||
-- `out_path_root`: when the conventional `out_root` is `build/gen` (any spelling — relative, absolute, separator variants).
|
||||
-- Write the md files to `build/` (parent of `gen/`) instead of nested under `gen/`.
|
||||
@@ -1228,49 +1057,37 @@ function M.run(ctx)
|
||||
return type(p) == "string" and (p:match("[/\\]gen[/\\]?$") ~= nil
|
||||
or p == "build/gen" or p == "build\\gen")
|
||||
end
|
||||
--- @type string
|
||||
local out_root_effective = ends_with_gen(ctx.out_root)
|
||||
local out_root_effective = ends_with_gen(ctx.out_root) ---@type string
|
||||
and ctx.out_root:gsub("[/\\]gen[/\\]?$", "")
|
||||
or ctx.out_root
|
||||
|
||||
duffle.ensure_dir(out_root_effective)
|
||||
|
||||
-- Aggregator for the project-wide `once = true` summary renderer.
|
||||
--- @type ProjectSummaryRow[]
|
||||
local all_modules = {}
|
||||
local all_modules = {} ---@type ProjectSummaryRow[]
|
||||
|
||||
--- @type string, SourceFile[]
|
||||
for dir, dir_sources in pairs(by_dir) do
|
||||
--- @type string
|
||||
local dir_basename = dir:match("([^/\\]+)$") or dir
|
||||
for dir, dir_sources in pairs(by_dir) do ---@type string, SourceFile[]
|
||||
local dir_basename = dir:match("([^/\\]+)$") or dir ---@type string
|
||||
|
||||
-- Per-renderer dispatch for the per-module renderers (once = false).
|
||||
--- @type integer, ReportRenderer
|
||||
for _, renderer in ipairs(REPORT_RENDERERS) do
|
||||
for _, renderer in ipairs(REPORT_RENDERERS) do ---@type integer, ReportRenderer
|
||||
if not renderer.once then
|
||||
--- @type string
|
||||
local body = renderer.gather(ctx, dir, dir_sources)
|
||||
--- @type string
|
||||
local out_path = out_root_effective .. "/" .. renderer.basename(dir_basename) .. "." .. renderer.ext
|
||||
local body = renderer.gather(ctx, dir, dir_sources) ---@type string
|
||||
local out_path = out_root_effective .. "/" .. renderer.basename(dir_basename) .. "." .. renderer.ext ---@type string
|
||||
duffle.write_file(out_path, body)
|
||||
outputs[#outputs + 1] = { kind = renderer.name, path = out_path }
|
||||
end
|
||||
end
|
||||
|
||||
--- @type ModuleView
|
||||
local view = build_module_view(dir, dir_sources, corpus)
|
||||
--- @type integer, integer, integer
|
||||
local n_annot, n_binds, n_macros = 0, 0, 0
|
||||
--- @type integer, SourceFile
|
||||
for _, src in ipairs(dir_sources) do
|
||||
local view = build_module_view(dir, dir_sources, corpus) ---@type ModuleView
|
||||
local n_annot, n_binds, n_macros = 0, 0, 0 ---@type integer, integer, integer
|
||||
for _, src in ipairs(dir_sources) do ---@type integer, SourceFile
|
||||
n_annot = n_annot + #((src.scan and src.scan.atom_infos) or {})
|
||||
n_binds = n_binds + #((src.scan and src.scan.binds) or {})
|
||||
n_macros = n_macros + #((src.scan and src.scan.macros) or {})
|
||||
end
|
||||
--- @type integer, integer, integer
|
||||
local n_err, n_warn, n_info = 0, 0, 0
|
||||
--- @type integer, CheckFinding
|
||||
for _, f in ipairs(view.findings or {}) do
|
||||
local n_err, n_warn, n_info = 0, 0, 0 ---@type integer, integer, integer
|
||||
for _, f in ipairs(view.findings or {}) do ---@type integer, CheckFinding
|
||||
if f.kind == "error" then n_err = n_err + 1
|
||||
elseif f.kind == "warning" then n_warn = n_warn + 1
|
||||
else n_info = n_info + 1
|
||||
@@ -1290,13 +1107,10 @@ function M.run(ctx)
|
||||
end
|
||||
|
||||
-- Project-wide renderer (once = true): write the summary file.
|
||||
--- @type integer, ReportRenderer
|
||||
for _, renderer in ipairs(REPORT_RENDERERS) do
|
||||
for _, renderer in ipairs(REPORT_RENDERERS) do ---@type integer, ReportRenderer
|
||||
if renderer.once then
|
||||
--- @type string
|
||||
local body = renderer.gather(ctx, nil, nil, all_modules)
|
||||
--- @type string
|
||||
local out_path = out_root_effective .. "/" .. renderer.basename("") .. "." .. renderer.ext
|
||||
local body = renderer.gather(ctx, nil, nil, all_modules) ---@type string
|
||||
local out_path = out_root_effective .. "/" .. renderer.basename("") .. "." .. renderer.ext ---@type string
|
||||
duffle.write_file(out_path, body)
|
||||
outputs[#outputs + 1] = { kind = renderer.name, path = out_path }
|
||||
end
|
||||
|
||||
+485
-970
File diff suppressed because it is too large
Load Diff
+662
-1324
File diff suppressed because it is too large
Load Diff
@@ -23,10 +23,8 @@
|
||||
-- Bootstrap: load `scripts/duffle_paths.lua` (sets package.path + package.cpath).
|
||||
-- Uses `debug.getinfo` to find this file's own directory, so it works both standalone and when require'd from the orchestrator.
|
||||
-- duffle_paths.lua sets package.path then returns `require("duffle")` at the bottom, so the dofile value IS the duffle module.
|
||||
--- @type string
|
||||
local _bootstrap_dir = debug.getinfo(1, "S").source:match("^@?(.*[/\\])") or "./"
|
||||
--- @type DuffleExport
|
||||
local duffle = dofile(_bootstrap_dir .. "../duffle_paths.lua")
|
||||
local _bootstrap_dir = debug.getinfo(1, "S").source:match("^@?(.*[/\\])") or "./" ---@type string
|
||||
local duffle = dofile(_bootstrap_dir .. "../duffle_paths.lua") ---@type DuffleExport
|
||||
|
||||
-- ════════════════════════════════════════════════════════════════════════════
|
||||
-- Type declarations
|
||||
@@ -46,8 +44,7 @@ local duffle = dofile(_bootstrap_dir .. "../duffle_paths.lua")
|
||||
-- Module exports
|
||||
-- ════════════════════════════════════════════════════════════════════════════
|
||||
|
||||
--- @type WordCountEval
|
||||
local M = {}
|
||||
local M = {} ---@type WordCountEval
|
||||
|
||||
-- ┌────────────────────────────────────────────────────────────────────┐
|
||||
-- │ Shared utility: count_token_words │
|
||||
@@ -61,15 +58,12 @@ local M = {}
|
||||
--- @param wc WordCounts -- the shared word-count table
|
||||
--- @return integer
|
||||
function M.count_token_words(token, wc)
|
||||
--- @type string
|
||||
local s = duffle.trim(token)
|
||||
local s = duffle.trim(token) ---@type string
|
||||
if s == "" then return 0 end
|
||||
--- @type string|nil, integer
|
||||
local name, after = duffle.read_ident(s, 1)
|
||||
local name, after = duffle.read_ident(s, 1) ---@type string|nil, integer
|
||||
if not name then return 1 end
|
||||
if wc[name] then return wc[name] end
|
||||
--- @type integer
|
||||
local paren_pos = duffle.skip_ws_and_cmt(s, after)
|
||||
local paren_pos = duffle.skip_ws_and_cmt(s, after) ---@type integer
|
||||
if s:sub(paren_pos, paren_pos) == "(" then
|
||||
io.stderr:write(" warning: unknown macro '" .. name .. "', assuming 1 word\n")
|
||||
end
|
||||
@@ -95,8 +89,7 @@ end
|
||||
--- @return PassResult
|
||||
function M.run(ctx)
|
||||
-- 1. Canonical-corpus ownership gate.
|
||||
--- @type Corpus|nil
|
||||
local corpus = ctx.shared and ctx.shared.corpus
|
||||
local corpus = ctx.shared and ctx.shared.corpus ---@type Corpus|nil
|
||||
if type(corpus) ~= "table" then
|
||||
error("word_count_eval.run requires ctx.shared.corpus (canonical corpus). The fixture must install the corpus before running this pass.", 0)
|
||||
end
|
||||
@@ -108,8 +101,7 @@ function M.run(ctx)
|
||||
|
||||
-- 3. Load authored metadata. Generated .macs.h files are NOT scanned
|
||||
-- (the pass computes their counts from the just-built bodies after disk emission; see passes/components.lua).
|
||||
--- @type WordCounts
|
||||
local wc = duffle.load_word_counts(ctx.metadata_path)
|
||||
local wc = duffle.load_word_counts(ctx.metadata_path) ---@type WordCounts
|
||||
|
||||
-- 4. Assign the count table. ONE assignment, no copy. The assignment creates no secondary alias.
|
||||
corpus.word_counts = wc
|
||||
|
||||
+92
-184
@@ -19,10 +19,8 @@
|
||||
-- fall back to `debug.getinfo(1, "S").source` when this file is being dofile()'d or require()'d (in which case `arg[0]` is the *caller's* path).
|
||||
-- That single statement: (a) sets `package.path` + `package.cpath`, (b) at the bottom returns `require("duffle")`.
|
||||
-- So the dofile's return value is the duffle module.
|
||||
--- @type boolean
|
||||
local _is_entry_script = arg and arg[0] and arg[0]:match("ps1_meta%.lua$") ~= nil
|
||||
--- @type string
|
||||
local _bootstrap_src
|
||||
local _is_entry_script = arg and arg[0] and arg[0]:match("ps1_meta%.lua$") ~= nil ---@type boolean
|
||||
local _bootstrap_src ---@type string
|
||||
if _is_entry_script then
|
||||
_bootstrap_src = arg[0]
|
||||
else
|
||||
@@ -30,33 +28,26 @@ else
|
||||
-- strip the leading "@" so the directory match works in both cases.
|
||||
_bootstrap_src = debug.getinfo(1, "S").source:sub(2)
|
||||
end
|
||||
--- @type DuffleExport
|
||||
local duffle = dofile((_bootstrap_src:match("(.*[/\\])") or "./") .. "duffle_paths.lua")
|
||||
local duffle = dofile((_bootstrap_src:match("(.*[/\\])") or "./") .. "duffle_paths.lua") ---@type DuffleExport
|
||||
|
||||
-- ════════════════════════════════════════════════════════════════════════════
|
||||
-- Constants
|
||||
-- ════════════════════════════════════════════════════════════════════════════
|
||||
|
||||
-- Exit codes (per the --help text and the post-build summary convention).
|
||||
--- @type integer
|
||||
local EXIT_OK = 0
|
||||
--- @type integer
|
||||
local EXIT_VALIDATION_ERRORS = 1
|
||||
--- @type integer
|
||||
local EXIT_INTERNAL_ERROR = 2
|
||||
local EXIT_OK = 0 ---@type integer
|
||||
local EXIT_VALIDATION_ERRORS = 1 ---@type integer
|
||||
local EXIT_INTERNAL_ERROR = 2 ---@type integer
|
||||
|
||||
-- Default --out-root value if not provided.
|
||||
--- @type string
|
||||
local DEFAULT_OUT_ROOT = "build/gen"
|
||||
local DEFAULT_OUT_ROOT = "build/gen" ---@type string
|
||||
|
||||
-- Sentinel for "all passes" in `PASS_FLAG_TO_NAME`. Distinguishes `--all` from the per-pass flags (which map to individual pass names).
|
||||
--- @type string
|
||||
local ALL_PASSES_SENTINEL = "__all__"
|
||||
local ALL_PASSES_SENTINEL = "__all__" ---@type string
|
||||
|
||||
-- Sentinel key for the pass-flag dispatcher in `FLAG_HANDLERS`.
|
||||
-- The actual pass names are looked up via `PASS_FLAG_TO_NAME`, not direct dispatch, so this key never matches a real flag.
|
||||
--- @type string
|
||||
local PASS_FLAG_DISPATCH_KEY = "__pass__"
|
||||
local PASS_FLAG_DISPATCH_KEY = "__pass__" ---@type string
|
||||
|
||||
-- ════════════════════════════════════════════════════════════════════════════
|
||||
-- Type declarations
|
||||
@@ -159,8 +150,7 @@ local PASS_FLAG_DISPATCH_KEY = "__pass__"
|
||||
-- A row without a `groups` entry is dependency-only: it runs only when a transitive dep requests it,
|
||||
-- but it remains directly requestable through its explicit CLI flag (e.g. --atoms-source-map, --scan-source).
|
||||
|
||||
--- @type table<string, PassDescriptor>
|
||||
local PASSES = {
|
||||
local PASSES = { ---@type table<string, PassDescriptor>
|
||||
["scan-source"] = {
|
||||
module = "passes.scan_source",
|
||||
kind = "shared", deps = {},
|
||||
@@ -230,13 +220,10 @@ local PASSES = {
|
||||
--- @param group_name string -- Build-phase group ("pre-link" | "post-link")
|
||||
--- @return string[] -- Sorted root pass names belonging to that group
|
||||
local function roots_for_group(group_name)
|
||||
--- @type string[]
|
||||
local names = {}
|
||||
--- @type string, PassDescriptor
|
||||
for name, pass in pairs(PASSES) do
|
||||
local names = {} ---@type string[]
|
||||
for name, pass in pairs(PASSES) do ---@type string, PassDescriptor
|
||||
if pass.groups then
|
||||
--- @type integer, string
|
||||
for _, g in ipairs(pass.groups) do
|
||||
for _, g in ipairs(pass.groups) do ---@type integer, string
|
||||
if g == group_name then
|
||||
names[#names + 1] = name
|
||||
break
|
||||
@@ -255,14 +242,12 @@ end
|
||||
--- @param group_name string
|
||||
--- @return nil
|
||||
local function request_roots_for_group(args, group_name)
|
||||
--- @type string[]
|
||||
local roots = roots_for_group(group_name)
|
||||
local roots = roots_for_group(group_name) ---@type string[]
|
||||
if #roots == 0 then
|
||||
error(string.format("ps1_meta: build-phase group %q has zero roots in PASSES; check PASSES rows for a `groups = { %q }` field"
|
||||
, group_name, group_name))
|
||||
end
|
||||
--- @type integer, string
|
||||
for _, name in ipairs(roots) do
|
||||
for _, name in ipairs(roots) do ---@type integer, string
|
||||
args.requested_set[#args.requested_set + 1] = name
|
||||
end
|
||||
end
|
||||
@@ -270,8 +255,7 @@ end
|
||||
-- Pass-kind taxonomy: findings always print. No pass kind stops the build.
|
||||
-- Report severity is independent from process exit policy.
|
||||
-- Adding a new pass kind requires listing it here explicitly; an unknown kind must not silently fall back to "true".
|
||||
--- @type table<string, boolean> -- bag: pass kind -> stop-on-error
|
||||
local PASS_KIND_STOP_ON_ERROR = {
|
||||
local PASS_KIND_STOP_ON_ERROR = { ---@type table<string, boolean> -- bag: pass kind -> stop-on-error
|
||||
["shared"] = false,
|
||||
["header-output"] = false,
|
||||
["validation"] = false,
|
||||
@@ -283,8 +267,7 @@ local PASS_KIND_STOP_ON_ERROR = {
|
||||
-- Per-pass flags (e.g. --word-counts); phase flags (--pre-link, --post-link, --all) are within FLAG_HANDLERS because they own side effects or invoke group-derivation logic.
|
||||
-- dwarf-injection is *also* a per-pass opt-in flag, but its selection + opt-in state are both owned by the explicit FLAG_HANDLERS entry below
|
||||
-- (it sets args.flags.dwarf_injection and appends "dwarf-injection" to requested_set), so it is intentionally absent from this table.
|
||||
--- @type table<string, string> -- bag: CLI flag -> pass name or ALL_PASSES_SENTINEL
|
||||
local PASS_FLAG_TO_NAME = {
|
||||
local PASS_FLAG_TO_NAME = { ---@type table<string, string> -- bag: CLI flag -> pass name or ALL_PASSES_SENTINEL
|
||||
["--word-counts"] = "word-counts",
|
||||
["--components"] = "components",
|
||||
["--validate"] = "annotation",
|
||||
@@ -301,21 +284,17 @@ local PASS_FLAG_TO_NAME = {
|
||||
--- @param args ParsedArgs
|
||||
--- @return nil
|
||||
local function request_all_passes(args)
|
||||
--- @type string[]
|
||||
local names = {}
|
||||
--- @type string
|
||||
for name in pairs(PASSES) do names[#names + 1] = name end
|
||||
local names = {} ---@type string[]
|
||||
for name in pairs(PASSES) do names[#names + 1] = name end ---@type string
|
||||
table.sort(names)
|
||||
--- @type integer, string
|
||||
for _, n in ipairs(names) do
|
||||
for _, n in ipairs(names) do ---@type integer, string
|
||||
args.requested_set[#args.requested_set + 1] = n
|
||||
end
|
||||
end
|
||||
|
||||
-- Per-flag handlers. Each handler takes (args, argv, arg_idx) and returns the new arg_idx (so multi-arg flags like --source FILE advance it).
|
||||
-- Returning nil + os.exit() handles termination flags (--help).
|
||||
--- @type table<string, FlagHandler>
|
||||
local FLAG_HANDLERS = {}
|
||||
local FLAG_HANDLERS = {} ---@type table<string, FlagHandler>
|
||||
|
||||
-- ════════════════════════════════════════════════════════════════════════════
|
||||
-- CLI parsing
|
||||
@@ -372,8 +351,7 @@ EXAMPLES:
|
||||
]])
|
||||
end
|
||||
|
||||
--- @type table<string, string> -- bag: flag -> value metavar
|
||||
local FLAG_VALUE_NAMES = {
|
||||
local FLAG_VALUE_NAMES = { ---@type table<string, string> -- bag: flag -> value metavar
|
||||
["--source"] = "FILE",
|
||||
["--unity-root"] = "FILE",
|
||||
["--metadata"] = "PATH",
|
||||
@@ -388,10 +366,8 @@ local FLAG_VALUE_NAMES = {
|
||||
--- @return string
|
||||
--- @return integer
|
||||
local function require_flag_value(argv, arg_idx, flag)
|
||||
--- @type string|nil
|
||||
local value = argv[arg_idx + 1]
|
||||
--- @type boolean
|
||||
local next_known = type(value) == "string"
|
||||
local value = argv[arg_idx + 1] ---@type string|nil
|
||||
local next_known = type(value) == "string" ---@type boolean
|
||||
and (FLAG_HANDLERS[value] ~= nil or PASS_FLAG_TO_NAME[value] ~= nil)
|
||||
if value == nil or next_known then
|
||||
io.stderr:write("ps1_meta: " .. flag .. " requires " .. FLAG_VALUE_NAMES[flag] .. "\n")
|
||||
@@ -417,8 +393,7 @@ FLAG_HANDLERS["--verbose"] = function(args) args.verbose = true end
|
||||
--- @param arg_idx integer
|
||||
--- @return integer
|
||||
FLAG_HANDLERS["--source"] = function(args, argv, arg_idx)
|
||||
--- @type string, integer
|
||||
local value, value_idx = require_flag_value(argv, arg_idx, "--source")
|
||||
local value, value_idx = require_flag_value(argv, arg_idx, "--source") ---@type string, integer
|
||||
args.sources[#args.sources + 1] = value
|
||||
return value_idx
|
||||
end
|
||||
@@ -427,8 +402,7 @@ end
|
||||
--- @param arg_idx integer
|
||||
--- @return integer
|
||||
FLAG_HANDLERS["--unity-root"] = function(args, argv, arg_idx)
|
||||
--- @type string, integer
|
||||
local value, value_idx = require_flag_value(argv, arg_idx, "--unity-root")
|
||||
local value, value_idx = require_flag_value(argv, arg_idx, "--unity-root") ---@type string, integer
|
||||
args.unity_root = value
|
||||
return value_idx
|
||||
end
|
||||
@@ -437,8 +411,7 @@ end
|
||||
--- @param arg_idx integer
|
||||
--- @return integer
|
||||
FLAG_HANDLERS["--metadata"] = function(args, argv, arg_idx)
|
||||
--- @type string, integer
|
||||
local value, value_idx = require_flag_value(argv, arg_idx, "--metadata")
|
||||
local value, value_idx = require_flag_value(argv, arg_idx, "--metadata") ---@type string, integer
|
||||
args.metadata = value
|
||||
return value_idx
|
||||
end
|
||||
@@ -447,8 +420,7 @@ end
|
||||
--- @param arg_idx integer
|
||||
--- @return integer
|
||||
FLAG_HANDLERS["--out-root"] = function(args, argv, arg_idx)
|
||||
--- @type string, integer
|
||||
local value, value_idx = require_flag_value(argv, arg_idx, "--out-root")
|
||||
local value, value_idx = require_flag_value(argv, arg_idx, "--out-root") ---@type string, integer
|
||||
args.out_root = value
|
||||
return value_idx
|
||||
end
|
||||
@@ -457,8 +429,7 @@ end
|
||||
--- @param arg_idx integer
|
||||
--- @return integer
|
||||
FLAG_HANDLERS["--project-root"] = function(args, argv, arg_idx)
|
||||
--- @type string, integer
|
||||
local value, value_idx = require_flag_value(argv, arg_idx, "--project-root")
|
||||
local value, value_idx = require_flag_value(argv, arg_idx, "--project-root") ---@type string, integer
|
||||
args.project_root = value
|
||||
return value_idx
|
||||
end
|
||||
@@ -476,8 +447,7 @@ end
|
||||
--- @param arg_idx integer
|
||||
--- @return integer
|
||||
FLAG_HANDLERS["--elf"] = function(args, argv, arg_idx)
|
||||
--- @type string, integer
|
||||
local value, value_idx = require_flag_value(argv, arg_idx, "--elf")
|
||||
local value, value_idx = require_flag_value(argv, arg_idx, "--elf") ---@type string, integer
|
||||
args.flags = args.flags or {}
|
||||
args.flags.elf_path = value
|
||||
return value_idx
|
||||
@@ -517,8 +487,7 @@ end
|
||||
--- @param a string
|
||||
--- @return nil
|
||||
FLAG_HANDLERS[PASS_FLAG_DISPATCH_KEY] = function(args, a)
|
||||
--- @type string|nil
|
||||
local name = PASS_FLAG_TO_NAME[a]
|
||||
local name = PASS_FLAG_TO_NAME[a] ---@type string|nil
|
||||
if name == ALL_PASSES_SENTINEL then
|
||||
request_all_passes(args)
|
||||
return
|
||||
@@ -530,8 +499,7 @@ end
|
||||
--- @param argv string[]
|
||||
--- @return ParsedArgs
|
||||
local function parse_args(argv)
|
||||
--- @type ParsedArgs
|
||||
local args = {
|
||||
local args = { ---@type ParsedArgs
|
||||
requested_set = {},
|
||||
sources = {},
|
||||
unity_root = nil,
|
||||
@@ -541,13 +509,10 @@ local function parse_args(argv)
|
||||
verbose = false,
|
||||
}
|
||||
|
||||
--- @type integer
|
||||
local pos = 1
|
||||
local pos = 1 ---@type integer
|
||||
while pos <= #argv do
|
||||
--- @type string
|
||||
local a = argv[pos]
|
||||
--- @type FlagHandler|nil
|
||||
local handler = FLAG_HANDLERS[a]
|
||||
local a = argv[pos] ---@type string
|
||||
local handler = FLAG_HANDLERS[a] ---@type FlagHandler|nil
|
||||
if handler then
|
||||
pos = handler(args, argv, pos) or pos
|
||||
elseif PASS_FLAG_TO_NAME[a] then
|
||||
@@ -572,17 +537,14 @@ local function parse_args(argv)
|
||||
-- `<repo>/code/duffle/word_count.metadata.h` is the canonical metadata location.
|
||||
-- `project_root` names `<repo>`; the resolver derives `<project_root>/code` separately.
|
||||
if not args.project_root then
|
||||
--- @type string
|
||||
local metadata_dir = duffle.dirname(duffle.normalize_path(args.metadata))
|
||||
--- @type string
|
||||
local code_root = duffle.dirname(metadata_dir)
|
||||
local metadata_dir = duffle.dirname(duffle.normalize_path(args.metadata)) ---@type string
|
||||
local code_root = duffle.dirname(metadata_dir) ---@type string
|
||||
args.project_root = duffle.dirname(code_root)
|
||||
else
|
||||
args.project_root = duffle.normalize_path(args.project_root)
|
||||
end
|
||||
|
||||
--- @type boolean
|
||||
local has_unity = type(args.unity_root) == "string" and args.unity_root ~= ""
|
||||
local has_unity = type(args.unity_root) == "string" and args.unity_root ~= "" ---@type boolean
|
||||
if has_unity and #args.sources > 0 then
|
||||
io.stderr:write("ps1_meta: --unity-root FILE and --source FILE are mutually exclusive\n")
|
||||
os.exit(EXIT_INTERNAL_ERROR)
|
||||
@@ -595,14 +557,10 @@ local function parse_args(argv)
|
||||
-- Post-link opt-ins (--gdb-runtime, --dwarf-injection) write output that depends on the linked ELF.
|
||||
-- Without --elf the metaprogram can't satisfy those requests, so refuse loud and early.
|
||||
-- This covers the explicit --post-link batch, --dwarf-injection by itself, and --gdb-runtime by itself.
|
||||
--- @type PassFlags
|
||||
local flags = args.flags or {}
|
||||
--- @type string|nil
|
||||
local elf_path = flags.elf_path
|
||||
--- @type boolean
|
||||
local has_elf = type(elf_path) == "string" and #elf_path > 0
|
||||
--- @type boolean
|
||||
local post_links = flags.gdb_runtime or flags.dwarf_injection
|
||||
local flags = args.flags or {} ---@type PassFlags
|
||||
local elf_path = flags.elf_path ---@type string|nil
|
||||
local has_elf = type(elf_path) == "string" and #elf_path > 0 ---@type boolean
|
||||
local post_links = flags.gdb_runtime or flags.dwarf_injection ---@type boolean
|
||||
if post_links and not has_elf then
|
||||
io.stderr:write("ps1_meta: --elf PATH is required for post-link output\n")
|
||||
os.exit(EXIT_INTERNAL_ERROR)
|
||||
@@ -621,12 +579,9 @@ end
|
||||
--- @param args ParsedArgs
|
||||
--- @return PassCtx
|
||||
local function build_ctx(args)
|
||||
--- @type string
|
||||
local normalized_project_root = duffle.normalize_path(args.project_root)
|
||||
--- @type string
|
||||
local project_root = normalized_project_root
|
||||
--- @type boolean
|
||||
local project_root_is_absolute = normalized_project_root:match("^%a:/")
|
||||
local normalized_project_root = duffle.normalize_path(args.project_root) ---@type string
|
||||
local project_root = normalized_project_root ---@type string
|
||||
local project_root_is_absolute = normalized_project_root:match("^%a:/") ---@type boolean
|
||||
or normalized_project_root:sub(1, 2) == "//"
|
||||
or normalized_project_root:sub(1, 1) == "/"
|
||||
if not project_root_is_absolute then
|
||||
@@ -637,11 +592,9 @@ local function build_ctx(args)
|
||||
-- Do not route POSIX/UNC/drive-absolute paths through to_absolute_path.
|
||||
duffle.canonical_path_key(project_root)
|
||||
end
|
||||
--- @type Corpus
|
||||
local resolution
|
||||
local resolution ---@type Corpus
|
||||
if args.unity_root then
|
||||
--- @type boolean, Corpus|string
|
||||
local ok_resolve, resolved = pcall(duffle.resolve_source_corpus, {
|
||||
local ok_resolve, resolved = pcall(duffle.resolve_source_corpus, { ---@type boolean, Corpus|string
|
||||
unity_root = args.unity_root,
|
||||
project_root = project_root,
|
||||
})
|
||||
@@ -651,45 +604,35 @@ local function build_ctx(args)
|
||||
end
|
||||
resolution = resolved
|
||||
else
|
||||
--- @type SourceFile[]
|
||||
local source_order = {}
|
||||
--- @type table<Path, SourceFile>
|
||||
local sources_by_path = {}
|
||||
--- @type SourceResolver
|
||||
local resolver = {
|
||||
local source_order = {} ---@type SourceFile[]
|
||||
local sources_by_path = {} ---@type table<Path, SourceFile>
|
||||
local resolver = { ---@type SourceResolver
|
||||
resolved = {},
|
||||
skipped = {},
|
||||
shadowed = {},
|
||||
}
|
||||
--- @type integer, string
|
||||
for _, input_path in ipairs(args.sources) do
|
||||
--- @type string
|
||||
local path = duffle.normalize_path(input_path)
|
||||
--- @type boolean, string
|
||||
local key_ok, key_or_error = pcall(duffle.canonical_path_key, path)
|
||||
for _, input_path in ipairs(args.sources) do ---@type integer, string
|
||||
local path = duffle.normalize_path(input_path) ---@type string
|
||||
local key_ok, key_or_error = pcall(duffle.canonical_path_key, path) ---@type boolean, string
|
||||
if not key_ok then
|
||||
error("ps1_meta: invalid --source " .. input_path .. ": " .. tostring(key_or_error), 0)
|
||||
end
|
||||
--- @type file*|nil
|
||||
local file = io.open(path, "r")
|
||||
local file = io.open(path, "r") ---@type file*|nil
|
||||
if not file then
|
||||
io.stderr:write("ps1_meta: cannot open --source " .. input_path .. "\n")
|
||||
os.exit(EXIT_INTERNAL_ERROR)
|
||||
end
|
||||
--- @type string
|
||||
local text = file:read("*a")
|
||||
local text = file:read("*a") ---@type string
|
||||
file:close()
|
||||
|
||||
--- @type SourceFile
|
||||
local source = {
|
||||
local source = { ---@type SourceFile
|
||||
path = path,
|
||||
text = text,
|
||||
dir = duffle.dirname(path),
|
||||
basename = duffle.basename_no_ext(path),
|
||||
}
|
||||
source_order[#source_order + 1] = source
|
||||
--- @type string
|
||||
local key = key_or_error
|
||||
local key = key_or_error ---@type string
|
||||
if not sources_by_path[key] then sources_by_path[key] = source end
|
||||
resolver.resolved[#resolver.resolved + 1] = {
|
||||
include_path = path,
|
||||
@@ -713,8 +656,7 @@ local function build_ctx(args)
|
||||
}
|
||||
end
|
||||
|
||||
--- @type Corpus
|
||||
local corpus = {
|
||||
local corpus = { ---@type Corpus
|
||||
unity_root = resolution.unity_root,
|
||||
project_root = resolution.project_root,
|
||||
code_root = resolution.code_root,
|
||||
@@ -735,8 +677,7 @@ local function build_ctx(args)
|
||||
collisions = {},
|
||||
resolver = resolution.resolver,
|
||||
}
|
||||
--- @type PassCtx
|
||||
local ctx = {
|
||||
local ctx = { ---@type PassCtx
|
||||
metadata_path = args.metadata,
|
||||
shared = { corpus = corpus },
|
||||
out_root = args.out_root,
|
||||
@@ -765,21 +706,15 @@ end
|
||||
--- Keeping these blocks local makes the topological sort self-contained.
|
||||
local function topo_sort(passes, requested_set)
|
||||
-- Dependency closure: include every pass transitively required by `requested_set`.
|
||||
--- @type table<string, boolean> -- bag: pass name -> needed
|
||||
local needed = {}
|
||||
--- @type integer, string
|
||||
for _, name in ipairs(requested_set) do needed[name] = true end
|
||||
--- @type boolean
|
||||
local changed = true
|
||||
local needed = {} ---@type table<string, boolean> -- bag: pass name -> needed
|
||||
for _, name in ipairs(requested_set) do needed[name] = true end ---@type integer, string
|
||||
local changed = true ---@type boolean
|
||||
while changed do
|
||||
changed = false
|
||||
--- @type string, boolean
|
||||
for name, _ in pairs(needed) do
|
||||
--- @type PassDescriptor
|
||||
local pass = passes[name]
|
||||
for name, _ in pairs(needed) do ---@type string, boolean
|
||||
local pass = passes[name] ---@type PassDescriptor
|
||||
if not pass then error("unknown pass '" .. name .. "' requested") end
|
||||
--- @type integer, string
|
||||
for _, dep in ipairs(pass.deps) do
|
||||
for _, dep in ipairs(pass.deps) do ---@type integer, string
|
||||
if not needed[dep] then
|
||||
needed[dep] = true
|
||||
changed = true
|
||||
@@ -789,14 +724,10 @@ local function topo_sort(passes, requested_set)
|
||||
end
|
||||
|
||||
-- In-degree calculation: count each needed pass's needed dependencies.
|
||||
--- @type table<string, integer> -- bag: pass name -> in-degree
|
||||
local in_degree = {}
|
||||
--- @type string, boolean
|
||||
for name, _ in pairs(needed) do in_degree[name] = 0 end
|
||||
--- @type string, boolean
|
||||
for name, _ in pairs(needed) do
|
||||
--- @type integer, string
|
||||
for _, dep in ipairs(passes[name].deps) do
|
||||
local in_degree = {} ---@type table<string, integer> -- bag: pass name -> in-degree
|
||||
for name, _ in pairs(needed) do in_degree[name] = 0 end ---@type string, boolean
|
||||
for name, _ in pairs(needed) do ---@type string, boolean
|
||||
for _, dep in ipairs(passes[name].deps) do ---@type integer, string
|
||||
if needed[dep] then
|
||||
in_degree[name] = in_degree[name] + 1
|
||||
end
|
||||
@@ -804,27 +735,21 @@ local function topo_sort(passes, requested_set)
|
||||
end
|
||||
|
||||
-- Ready-queue seeding: add zero-in-degree passes in deterministic order.
|
||||
--- @type string[]
|
||||
local ready = {}
|
||||
--- @type string, integer
|
||||
for name, deg in pairs(in_degree) do
|
||||
local ready = {} ---@type string[]
|
||||
for name, deg in pairs(in_degree) do ---@type string, integer
|
||||
if deg == 0 then ready[#ready + 1] = name end
|
||||
end
|
||||
table.sort(ready)
|
||||
|
||||
-- Ready-queue drain: decrement dependents when each pass is emitted.
|
||||
-- Newly-zero-degree passes are inserted back into the ready queue (kept sorted).
|
||||
--- @type string[]
|
||||
local order = {}
|
||||
local order = {} ---@type string[]
|
||||
while #ready > 0 do
|
||||
--- @type string
|
||||
local just_finished = table.remove(ready, 1)
|
||||
local just_finished = table.remove(ready, 1) ---@type string
|
||||
order[#order + 1] = just_finished
|
||||
--- @type string, boolean
|
||||
for name, _ in pairs(needed) do
|
||||
for name, _ in pairs(needed) do ---@type string, boolean
|
||||
if name ~= just_finished then
|
||||
--- @type integer, string
|
||||
for _, dep in ipairs(passes[name].deps) do
|
||||
for _, dep in ipairs(passes[name].deps) do ---@type integer, string
|
||||
if dep == just_finished then
|
||||
in_degree[name] = in_degree[name] - 1
|
||||
if in_degree[name] == 0 then
|
||||
@@ -840,13 +765,10 @@ local function topo_sort(passes, requested_set)
|
||||
-- Cycle detection: if `order` doesn't include all needed passes, some are stuck with in_degree > 0
|
||||
-- (the cycle closed on itself before Kahn could process them).
|
||||
-- Without this check, a fully-closed cycle (e.g. A -> B -> A) would silently return an empty order list, leaving the orchestrator to dispatch nothing.
|
||||
--- @type integer
|
||||
local needed_count = 0
|
||||
--- @type string
|
||||
for _ in pairs(needed) do needed_count = needed_count + 1 end -- count hash entries; Lua's #t doesn't work
|
||||
local needed_count = 0 ---@type integer
|
||||
for _ in pairs(needed) do needed_count = needed_count + 1 end ---@type string -- count hash entries; Lua's #t doesn't work
|
||||
if #order ~= needed_count then
|
||||
--- @type string, integer
|
||||
for name, deg in pairs(in_degree) do
|
||||
for name, deg in pairs(in_degree) do ---@type string, integer
|
||||
if deg > 0 then
|
||||
error("dependency cycle detected involving pass '" .. name .. "'")
|
||||
end
|
||||
@@ -867,11 +789,9 @@ end
|
||||
--- @param result PassResult
|
||||
--- @return boolean
|
||||
local function report_validation_errors(pass_name, pass, result)
|
||||
--- @type boolean
|
||||
local has_errors = result.errors and #result.errors > 0
|
||||
local has_errors = result.errors and #result.errors > 0 ---@type boolean
|
||||
if not has_errors then return false end
|
||||
--- @type integer, PassFinding
|
||||
for _, e in ipairs(result.errors) do
|
||||
for _, e in ipairs(result.errors) do ---@type integer, PassFinding
|
||||
io.stderr:write(string.format("[%s] line %d: %s\n", pass_name, e.line or 0, e.msg or ""))
|
||||
end
|
||||
return PASS_KIND_STOP_ON_ERROR[pass.kind] == true
|
||||
@@ -882,16 +802,11 @@ end
|
||||
--- @param order string[]
|
||||
--- @return boolean -- true if any validation errors were reported
|
||||
local function dispatch_passes(ctx, order)
|
||||
--- @type boolean
|
||||
local had_errors = false
|
||||
--- @type integer, string
|
||||
for _, pass_name in ipairs(order) do
|
||||
--- @type PassDescriptor
|
||||
local pass = PASSES[pass_name]
|
||||
--- @type PassModule
|
||||
local mod = require(pass.module)
|
||||
--- @type PassResult
|
||||
local result = mod.run(ctx)
|
||||
local had_errors = false ---@type boolean
|
||||
for _, pass_name in ipairs(order) do ---@type integer, string
|
||||
local pass = PASSES[pass_name] ---@type PassDescriptor
|
||||
local mod = require(pass.module) ---@type PassModule
|
||||
local result = mod.run(ctx) ---@type PassResult
|
||||
if report_validation_errors(pass_name, pass, result) then
|
||||
had_errors = true
|
||||
end
|
||||
@@ -903,20 +818,14 @@ end
|
||||
--- @param argv string[]
|
||||
--- @return nil
|
||||
local function main(argv)
|
||||
--- @type boolean, string|nil
|
||||
local ok, err = pcall(function()
|
||||
--- @type ParsedArgs
|
||||
local args = parse_args(argv)
|
||||
--- @type PassCtx
|
||||
local ctx = build_ctx(args)
|
||||
local ok, err = pcall(function() ---@type boolean, string|nil
|
||||
local args = parse_args(argv) ---@type ParsedArgs
|
||||
local ctx = build_ctx(args) ---@type PassCtx
|
||||
|
||||
--- @type string[]
|
||||
local requested = args.requested_set
|
||||
--- @type string[]
|
||||
local closed = topo_sort(PASSES, requested)
|
||||
local requested = args.requested_set ---@type string[]
|
||||
local closed = topo_sort(PASSES, requested) ---@type string[]
|
||||
|
||||
--- @type boolean
|
||||
local had_errors = dispatch_passes(ctx, closed)
|
||||
local had_errors = dispatch_passes(ctx, closed) ---@type boolean
|
||||
if had_errors then os.exit(EXIT_VALIDATION_ERRORS) end
|
||||
end)
|
||||
|
||||
@@ -931,8 +840,7 @@ end
|
||||
-- Module export for in-process consumers (tests that dofile this script).
|
||||
-- The conditional `main(...)` call below only fires when this file is invoked as the entry script (arg[0] ends in "ps1_meta.lua");
|
||||
-- in dofile() mode (test's arg[0] does not match), main() is skipped and the chunk returns `_M` to the caller.
|
||||
--- @type Ps1MetaMod
|
||||
local _M = {
|
||||
local _M = { ---@type Ps1MetaMod
|
||||
PASSES = PASSES,
|
||||
PASS_KIND_STOP_ON_ERROR = PASS_KIND_STOP_ON_ERROR,
|
||||
parse_args = parse_args,
|
||||
|
||||
Reference in New Issue
Block a user