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:
+25
-33
@@ -3,36 +3,32 @@
|
|||||||
--- @class DuffleExport
|
--- @class DuffleExport
|
||||||
--- bag: open module-export keys from duffle_scan / duffle_isa / duffle_emit
|
--- bag: open module-export keys from duffle_scan / duffle_isa / duffle_emit
|
||||||
|
|
||||||
--- @type DuffleExport
|
local scan = require("duffle_scan") ---@type DuffleExport
|
||||||
local scan = require("duffle_scan")
|
local isa = require("duffle_isa") ---@type DuffleExport
|
||||||
--- @type DuffleExport
|
local emit = require("duffle_emit") ---@type DuffleExport
|
||||||
local isa = require("duffle_isa")
|
local M = {} ---@type DuffleExport
|
||||||
--- @type DuffleExport
|
|
||||||
local emit = require("duffle_emit")
|
|
||||||
--- @type DuffleExport
|
|
||||||
local M = {}
|
|
||||||
|
|
||||||
--- @alias Path string
|
--- @alias Path string
|
||||||
--- @alias LineNum integer
|
--- @alias LineNum integer
|
||||||
--- @alias ByteOff integer
|
--- @alias ByteOff integer
|
||||||
--- @alias MacroName string
|
--- @alias MacroName string
|
||||||
--- @alias AtomName string
|
--- @alias AtomName string
|
||||||
--- @alias Severity string
|
--- @alias Severity string
|
||||||
|
|
||||||
--- @class SourceFile
|
--- @class SourceFile
|
||||||
--- @field path Path
|
--- @field path Path
|
||||||
--- @field text string
|
--- @field text string
|
||||||
--- @field dir string
|
--- @field dir string
|
||||||
--- @field basename string
|
--- @field basename string
|
||||||
--- @field scan SourceScan|nil
|
--- @field scan SourceScan|nil
|
||||||
|
|
||||||
--- @class CorpusView
|
--- @class CorpusView
|
||||||
--- @field register_alias_registry table<string, AliasEntry>
|
--- @field register_alias_registry table<string, AliasEntry>
|
||||||
--- @field type_name_registry table<string, TypeNameEntry>
|
--- @field type_name_registry table<string, TypeNameEntry>
|
||||||
--- @field atom_views table<AtomName, AtomViewEntry>
|
--- @field atom_views table<AtomName, AtomViewEntry>
|
||||||
--- @field atom_ctxs table<AtomName, AtomCtxEntry>
|
--- @field atom_ctxs table<AtomName, AtomCtxEntry>
|
||||||
--- @field atom_phases table<string, AtomPhaseGroup>
|
--- @field atom_phases table<string, AtomPhaseGroup>
|
||||||
--- @field binds_by_name table<string, BindsEntry>
|
--- @field binds_by_name table<string, BindsEntry>
|
||||||
--- @field atoms_by_name table<AtomName, AtomEntry>
|
--- @field atoms_by_name table<AtomName, AtomEntry>
|
||||||
--- @field atom_infos AtomInfoEntry[]
|
--- @field atom_infos AtomInfoEntry[]
|
||||||
--- @field components table<string, ComponentDef>
|
--- @field components table<string, ComponentDef>
|
||||||
@@ -42,12 +38,11 @@ local M = {}
|
|||||||
--- @field source_order SourceFile[]
|
--- @field source_order SourceFile[]
|
||||||
--- @field collisions CorpusCollision[]
|
--- @field collisions CorpusCollision[]
|
||||||
|
|
||||||
--- @param src DuffleExport
|
--- @param src DuffleExport
|
||||||
--- @param label string
|
--- @param label string
|
||||||
--- @return nil
|
--- @return nil
|
||||||
local function merge(src, label)
|
local function merge(src, label)
|
||||||
--- @type string, any
|
for k, v in pairs(src) do ---@type string, any
|
||||||
for k, v in pairs(src) do
|
|
||||||
if M[k] ~= nil and M[k] ~= v then
|
if M[k] ~= nil and M[k] ~= v then
|
||||||
error("duffle facade name collision on " .. tostring(k) .. " from " .. label, 0)
|
error("duffle facade name collision on " .. tostring(k) .. " from " .. label, 0)
|
||||||
end
|
end
|
||||||
@@ -62,8 +57,7 @@ merge(emit, "duffle_emit")
|
|||||||
--- @param ctx PassCtx
|
--- @param ctx PassCtx
|
||||||
--- @return CorpusView
|
--- @return CorpusView
|
||||||
function M.corpus_view(ctx)
|
function M.corpus_view(ctx)
|
||||||
--- @type Corpus
|
local corpus = ctx and ctx.shared and ctx.shared.corpus ---@type Corpus
|
||||||
local corpus = ctx and ctx.shared and ctx.shared.corpus
|
|
||||||
if not corpus then error("requires ctx.shared.corpus", 0) end
|
if not corpus then error("requires ctx.shared.corpus", 0) end
|
||||||
return {
|
return {
|
||||||
register_alias_registry = corpus.register_alias_registry or {},
|
register_alias_registry = corpus.register_alias_registry or {},
|
||||||
@@ -83,17 +77,15 @@ function M.corpus_view(ctx)
|
|||||||
}
|
}
|
||||||
end
|
end
|
||||||
|
|
||||||
--- @param rules CheckRule[]
|
--- @param rules CheckRule[]
|
||||||
--- @param phase string
|
--- @param phase string
|
||||||
--- @param item AtomEntry|SourceFile
|
--- @param item AtomEntry|SourceFile
|
||||||
--- @param pipe_ctx PipeCtx
|
--- @param pipe_ctx PipeCtx
|
||||||
--- @param findings CheckFinding[]
|
--- @param findings CheckFinding[]
|
||||||
--- @return nil
|
--- @return nil
|
||||||
function M.run_check_rules(rules, phase, item, pipe_ctx, findings)
|
function M.run_check_rules(rules, phase, item, pipe_ctx, findings)
|
||||||
--- @type integer, CheckRule
|
for _, rule in ipairs(rules) do ---@type integer, CheckRule
|
||||||
for _, rule in ipairs(rules) do
|
local fn = rule[phase] ---@type (fun(item: AtomEntry|SourceFile, pipe_ctx: PipeCtx, findings: CheckFinding[]): nil)|nil
|
||||||
--- @type (fun(item: AtomEntry|SourceFile, pipe_ctx: PipeCtx, findings: CheckFinding[]): nil)|nil
|
|
||||||
local fn = rule[phase]
|
|
||||||
if fn then fn(item, pipe_ctx, findings) end
|
if fn then fn(item, pipe_ctx, findings) end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -106,8 +106,7 @@
|
|||||||
--- @field HARDWARE_RELATIONS HardwareRelationRow[]
|
--- @field HARDWARE_RELATIONS HardwareRelationRow[]
|
||||||
--- @field CU2_TRANSITION_POLICY Cu2TransitionPolicy
|
--- @field CU2_TRANSITION_POLICY Cu2TransitionPolicy
|
||||||
|
|
||||||
--- @type DuffleIsa
|
local M = {} ---@type DuffleIsa
|
||||||
local M = {}
|
|
||||||
|
|
||||||
-- Section 7: domain tables
|
-- Section 7: domain tables
|
||||||
-- ════════════════════════════════════════════════════════════════════════════
|
-- ════════════════════════════════════════════════════════════════════════════
|
||||||
@@ -422,11 +421,9 @@ function M.gte (ident) return M.GTE_COMMAND[M.gte_canon(ident)] end
|
|||||||
local function build_alias_map()
|
local function build_alias_map()
|
||||||
--- @type table<string, string> -- bag: alias or canon -> canon
|
--- @type table<string, string> -- bag: alias or canon -> canon
|
||||||
M.ALIAS_TO_CANONICAL = {}
|
M.ALIAS_TO_CANONICAL = {}
|
||||||
--- @type string, GteCommandRow
|
for canon, row in pairs(M.GTE_COMMAND) do ---@type string, GteCommandRow
|
||||||
for canon, row in pairs(M.GTE_COMMAND) do
|
|
||||||
M.ALIAS_TO_CANONICAL[canon] = canon
|
M.ALIAS_TO_CANONICAL[canon] = canon
|
||||||
--- @type integer, string
|
for _, alias in ipairs(row.aliases or {}) do ---@type integer, string
|
||||||
for _, alias in ipairs(row.aliases or {}) do
|
|
||||||
M.ALIAS_TO_CANONICAL[alias] = canon
|
M.ALIAS_TO_CANONICAL[alias] = canon
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
+10
-20
@@ -19,12 +19,10 @@
|
|||||||
--- @class DufflePaths
|
--- @class DufflePaths
|
||||||
--- @field setup fun(): nil
|
--- @field setup fun(): nil
|
||||||
|
|
||||||
--- @type DufflePaths
|
local M = {} ---@type DufflePaths
|
||||||
local M = {}
|
|
||||||
|
|
||||||
-- Cache key for the repo root. Stored in `package.loaded` (process-global) so all 8 entry scripts + passes scripts share one resolution.
|
-- Cache key for the repo root. Stored in `package.loaded` (process-global) so all 8 entry scripts + passes scripts share one resolution.
|
||||||
--- @type string
|
local CACHE_KEY = "__duffle_repo_root__" ---@type string
|
||||||
local CACHE_KEY = "__duffle_repo_root__"
|
|
||||||
|
|
||||||
--- Resolve the repo root from this script's own path. Zero shell spawn.
|
--- Resolve the repo root from this script's own path. Zero shell spawn.
|
||||||
--- `duffle_paths.lua` always lives at `<repo>/scripts/duffle_paths.lua`, so the repo root is the parent of the directory containing this script.
|
--- `duffle_paths.lua` always lives at `<repo>/scripts/duffle_paths.lua`, so the repo root is the parent of the directory containing this script.
|
||||||
@@ -35,17 +33,14 @@ local CACHE_KEY = "__duffle_repo_root__"
|
|||||||
local function find_repo_root()
|
local function find_repo_root()
|
||||||
if package.loaded[CACHE_KEY] then return package.loaded[CACHE_KEY] end
|
if package.loaded[CACHE_KEY] then return package.loaded[CACHE_KEY] end
|
||||||
|
|
||||||
--- @type string
|
local source = debug.getinfo(1, "S").source ---@type string
|
||||||
local source = debug.getinfo(1, "S").source
|
|
||||||
-- Strip the leading `@` (Lua's dofile marker) and the trailing `/duffle_paths.lua` filename.
|
-- Strip the leading `@` (Lua's dofile marker) and the trailing `/duffle_paths.lua` filename.
|
||||||
-- What remains is the directory containing this script, i.e. `<repo>/scripts/`.
|
-- What remains is the directory containing this script, i.e. `<repo>/scripts/`.
|
||||||
--- @type string|nil
|
local scripts_dir = source and source:match("^@?(.*)[/\\]duffle_paths%.lua$") ---@type string|nil
|
||||||
local scripts_dir = source and source:match("^@?(.*)[/\\]duffle_paths%.lua$")
|
|
||||||
if not scripts_dir then return nil end
|
if not scripts_dir then return nil end
|
||||||
|
|
||||||
-- The repo root is the parent of `scripts/`. Strip the trailing `scripts/` (with or without trailing slash).
|
-- The repo root is the parent of `scripts/`. Strip the trailing `scripts/` (with or without trailing slash).
|
||||||
--- @type string
|
local root = scripts_dir:gsub("scripts[\\/]?$", "") ---@type string
|
||||||
local root = scripts_dir:gsub("scripts[\\/]?$", "")
|
|
||||||
root = root:gsub("\\", "/")
|
root = root:gsub("\\", "/")
|
||||||
if root == "" then root = "./" end
|
if root == "" then root = "./" end
|
||||||
if not root:match("/$") then root = root .. "/" end
|
if not root:match("/$") then root = root .. "/" end
|
||||||
@@ -60,8 +55,7 @@ end
|
|||||||
--- lpeg is built by `update_deps.ps1` to `toolchain/lpeg/`, which we wire into `package.cpath` here (so `require("lpeg")` from `duffle.lua` resolves without any global state).
|
--- lpeg is built by `update_deps.ps1` to `toolchain/lpeg/`, which we wire into `package.cpath` here (so `require("lpeg")` from `duffle.lua` resolves without any global state).
|
||||||
--- @return nil
|
--- @return nil
|
||||||
function M.setup()
|
function M.setup()
|
||||||
--- @type string|nil
|
local repo_root = find_repo_root() ---@type string|nil
|
||||||
local repo_root = find_repo_root()
|
|
||||||
if not repo_root then
|
if not repo_root then
|
||||||
-- Unreachable in practice: find_repo_root() derives the repo root from this script's own source path via debug.getinfo(1, "S").source (no subprocess, no git CLI, <1ms).
|
-- Unreachable in practice: find_repo_root() derives the repo root from this script's own source path via debug.getinfo(1, "S").source (no subprocess, no git CLI, <1ms).
|
||||||
-- A nil return means the source path did not match the expected <repo>/scripts/duffle_paths.lua layout — a packaging bug, not a "missing git repo" condition.
|
-- A nil return means the source path did not match the expected <repo>/scripts/duffle_paths.lua layout — a packaging bug, not a "missing git repo" condition.
|
||||||
@@ -69,10 +63,8 @@ function M.setup()
|
|||||||
os.exit(2)
|
os.exit(2)
|
||||||
end
|
end
|
||||||
|
|
||||||
--- @type string
|
local scripts_dir = repo_root .. "scripts/" ---@type string
|
||||||
local scripts_dir = repo_root .. "scripts/"
|
local passes_dir = repo_root .. "scripts/passes/" ---@type string
|
||||||
--- @type string
|
|
||||||
local passes_dir = repo_root .. "scripts/passes/"
|
|
||||||
package.path = scripts_dir .. "?.lua;"
|
package.path = scripts_dir .. "?.lua;"
|
||||||
.. scripts_dir .. "?/init.lua;"
|
.. scripts_dir .. "?/init.lua;"
|
||||||
.. passes_dir .. "?.lua;"
|
.. passes_dir .. "?.lua;"
|
||||||
@@ -82,10 +74,8 @@ function M.setup()
|
|||||||
-- lpeg: built by `update_deps.ps1` to `toolchain/lpeg/lpeg.dll`.
|
-- lpeg: built by `update_deps.ps1` to `toolchain/lpeg/lpeg.dll`.
|
||||||
-- lfs: compiled from pcsx-redux's vendored luafilesystem source to `toolchain/lfs/lfs.dll`.
|
-- lfs: compiled from pcsx-redux's vendored luafilesystem source to `toolchain/lfs/lfs.dll`.
|
||||||
-- Wire both directories into cpath so `require("lpeg")` and `require("lfs")` resolve.
|
-- Wire both directories into cpath so `require("lpeg")` and `require("lfs")` resolve.
|
||||||
--- @type string
|
local lpeg_dir = repo_root .. "toolchain/lpeg/" ---@type string
|
||||||
local lpeg_dir = repo_root .. "toolchain/lpeg/"
|
local lfs_dir = repo_root .. "toolchain/lfs/" ---@type string
|
||||||
--- @type string
|
|
||||||
local lfs_dir = repo_root .. "toolchain/lfs/"
|
|
||||||
package.cpath = lpeg_dir .. "?.dll;"
|
package.cpath = lpeg_dir .. "?.dll;"
|
||||||
.. lfs_dir .. "?.dll;"
|
.. lfs_dir .. "?.dll;"
|
||||||
.. package.cpath
|
.. package.cpath
|
||||||
|
|||||||
+217
-401
File diff suppressed because it is too large
Load Diff
+49
-97
@@ -109,8 +109,7 @@
|
|||||||
--- @field read_named_section fun(adapter: Elf32Adapter, sections: Elf32Section[], name: string): string|nil, string|nil
|
--- @field read_named_section fun(adapter: Elf32Adapter, sections: Elf32Section[], name: string): string|nil, string|nil
|
||||||
--- @field collect_symbols fun(adapter: Elf32Adapter, sections: Elf32Section[]): table<string, Elf32Sym>|nil, string|nil
|
--- @field collect_symbols fun(adapter: Elf32Adapter, sections: Elf32Section[]): table<string, Elf32Sym>|nil, string|nil
|
||||||
|
|
||||||
--- @type Elf32Mod
|
local M = {} ---@type Elf32Mod
|
||||||
local M = {}
|
|
||||||
|
|
||||||
-- ════════════════════════════════════════════════════════════════════════════
|
-- ════════════════════════════════════════════════════════════════════════════
|
||||||
-- Little-endian readers (bit-weighted accumulator, math.floor only)
|
-- Little-endian readers (bit-weighted accumulator, math.floor only)
|
||||||
@@ -168,8 +167,7 @@ end
|
|||||||
--- @param off integer
|
--- @param off integer
|
||||||
--- @return integer
|
--- @return integer
|
||||||
function M.read_u32_le(buf, off)
|
function M.read_u32_le(buf, off)
|
||||||
--- @type integer
|
local byte_off = off + 1 ---@type integer
|
||||||
local byte_off = off + 1
|
|
||||||
return buf:byte(byte_off)
|
return buf:byte(byte_off)
|
||||||
+ buf:byte(byte_off + 0x01) * 0x00000100
|
+ buf:byte(byte_off + 0x01) * 0x00000100
|
||||||
+ buf:byte(byte_off + 0x02) * 0x00010000
|
+ buf:byte(byte_off + 0x02) * 0x00010000
|
||||||
@@ -181,8 +179,7 @@ end
|
|||||||
--- @param off integer -- zero-based wire offset
|
--- @param off integer -- zero-based wire offset
|
||||||
--- @return integer
|
--- @return integer
|
||||||
function M.read_u16_le(buf, off)
|
function M.read_u16_le(buf, off)
|
||||||
--- @type integer
|
local byte_off = off + 1 ---@type integer
|
||||||
local byte_off = off + 1
|
|
||||||
return buf:byte(byte_off) + buf:byte(byte_off + 0x01) * 0x00000100
|
return buf:byte(byte_off) + buf:byte(byte_off + 0x01) * 0x00000100
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -282,12 +279,11 @@ end
|
|||||||
--- Extract a NUL-terminated C string from `strtab` at zero-based offset `off`.
|
--- Extract a NUL-terminated C string from `strtab` at zero-based offset `off`.
|
||||||
--- Returns nil if `off` is out of range or the string is not NUL-terminated.
|
--- Returns nil if `off` is out of range or the string is not NUL-terminated.
|
||||||
--- @param strtab string
|
--- @param strtab string
|
||||||
--- @param off integer
|
--- @param off integer
|
||||||
--- @return string|nil
|
--- @return string|nil
|
||||||
function M.get_str(strtab, off)
|
function M.get_str(strtab, off)
|
||||||
if off < 0 or off >= #strtab then return nil end
|
if off < 0 or off >= #strtab then return nil end
|
||||||
--- @type integer|nil
|
local end_pos = strtab:find("\0", off + 1, true) ---@type integer|nil
|
||||||
local end_pos = strtab:find("\0", off + 1, true)
|
|
||||||
if not end_pos then return nil end
|
if not end_pos then return nil end
|
||||||
return strtab:sub(off + 1, end_pos - 1)
|
return strtab:sub(off + 1, end_pos - 1)
|
||||||
end
|
end
|
||||||
@@ -305,48 +301,36 @@ end
|
|||||||
--- @param adapter Elf32Adapter
|
--- @param adapter Elf32Adapter
|
||||||
--- @return Elf32Header|nil, string|nil
|
--- @return Elf32Header|nil, string|nil
|
||||||
function M.parse_elf32_headers(adapter)
|
function M.parse_elf32_headers(adapter)
|
||||||
--- @type boolean, string|nil
|
local ok, err = M.validate_adapter(adapter) ---@type boolean, string|nil
|
||||||
local ok, err = M.validate_adapter(adapter)
|
|
||||||
if not ok then return nil, err end
|
if not ok then return nil, err end
|
||||||
|
|
||||||
-- 4-byte magic: 0x7F 'E' 'L' 'F'.
|
-- 4-byte magic: 0x7F 'E' 'L' 'F'.
|
||||||
-- The byte readers take the adapter explicitly.
|
-- The byte readers take the adapter explicitly.
|
||||||
-- The production `Support.File` adapter is wrapped by the caller to drop its implicit `self` so the parser shape is flat pass-style.
|
-- The production `Support.File` adapter is wrapped by the caller to drop its implicit `self` so the parser shape is flat pass-style.
|
||||||
--- @type integer|nil
|
local b1 = M.read_u8(adapter, 0) ---@type integer|nil
|
||||||
local b1 = M.read_u8(adapter, 0)
|
local b2 = M.read_u8(adapter, 1) ---@type integer|nil
|
||||||
--- @type integer|nil
|
local b3 = M.read_u8(adapter, 2) ---@type integer|nil
|
||||||
local b2 = M.read_u8(adapter, 1)
|
local b4 = M.read_u8(adapter, 3) ---@type integer|nil
|
||||||
--- @type integer|nil
|
|
||||||
local b3 = M.read_u8(adapter, 2)
|
|
||||||
--- @type integer|nil
|
|
||||||
local b4 = M.read_u8(adapter, 3)
|
|
||||||
if not (b1 and b2 and b3 and b4)
|
if not (b1 and b2 and b3 and b4)
|
||||||
or not (b1 == 0x7f and b2 == 0x45 and b3 == 0x4c and b4 == 0x46) then
|
or not (b1 == 0x7f and b2 == 0x45 and b3 == 0x4c and b4 == 0x46) then
|
||||||
return nil, "bad_magic"
|
return nil, "bad_magic"
|
||||||
end
|
end
|
||||||
|
|
||||||
--- @type integer|nil
|
local class = M.read_u8(adapter, M.ELF32_HEADER.class_offset) ---@type integer|nil
|
||||||
local class = M.read_u8(adapter, M.ELF32_HEADER.class_offset)
|
|
||||||
if class ~= M.ELFCLASS32 then
|
if class ~= M.ELFCLASS32 then
|
||||||
return nil, "unsupported_elf_class"
|
return nil, "unsupported_elf_class"
|
||||||
end
|
end
|
||||||
|
|
||||||
--- @type integer|nil
|
local data = M.read_u8(adapter, M.ELF32_HEADER.endian_offset) ---@type integer|nil
|
||||||
local data = M.read_u8(adapter, M.ELF32_HEADER.endian_offset)
|
|
||||||
if data ~= M.ELFDATA2LSB then
|
if data ~= M.ELFDATA2LSB then
|
||||||
return nil, "unsupported_elf_data"
|
return nil, "unsupported_elf_data"
|
||||||
end
|
end
|
||||||
|
|
||||||
--- @type integer|nil
|
local e_entry = M.read_u32(adapter, M.ELF32_HEADER.e_entry_offset) ---@type integer|nil
|
||||||
local e_entry = M.read_u32(adapter, M.ELF32_HEADER.e_entry_offset)
|
local e_shoff = M.read_u32(adapter, M.ELF32_HEADER.e_shoff_offset) ---@type integer|nil
|
||||||
--- @type integer|nil
|
local e_shentsize = M.read_u16(adapter, M.ELF32_HEADER.e_shentsize_offset) ---@type integer|nil
|
||||||
local e_shoff = M.read_u32(adapter, M.ELF32_HEADER.e_shoff_offset)
|
local e_shnum = M.read_u16(adapter, M.ELF32_HEADER.e_shnum_offset) ---@type integer|nil
|
||||||
--- @type integer|nil
|
local e_shstrndx = M.read_u16(adapter, M.ELF32_HEADER.e_shstrndx_offset) ---@type integer|nil
|
||||||
local e_shentsize = M.read_u16(adapter, M.ELF32_HEADER.e_shentsize_offset)
|
|
||||||
--- @type integer|nil
|
|
||||||
local e_shnum = M.read_u16(adapter, M.ELF32_HEADER.e_shnum_offset)
|
|
||||||
--- @type integer|nil
|
|
||||||
local e_shstrndx = M.read_u16(adapter, M.ELF32_HEADER.e_shstrndx_offset)
|
|
||||||
if not (e_entry and e_shoff and e_shentsize and e_shnum and e_shstrndx) then
|
if not (e_entry and e_shoff and e_shentsize and e_shnum and e_shstrndx) then
|
||||||
return nil, "truncated_header"
|
return nil, "truncated_header"
|
||||||
end
|
end
|
||||||
@@ -367,8 +351,7 @@ end
|
|||||||
--- @param sh_off integer
|
--- @param sh_off integer
|
||||||
--- @return Elf32Section|nil, string|nil
|
--- @return Elf32Section|nil, string|nil
|
||||||
local function read_section_entry(adapter, sh_off)
|
local function read_section_entry(adapter, sh_off)
|
||||||
--- @type Elf32Section
|
local entry = { ---@type Elf32Section
|
||||||
local entry = {
|
|
||||||
sh_name = M.read_u32(adapter, sh_off + M.ELF32_SECTION.sh_name_offset),
|
sh_name = M.read_u32(adapter, sh_off + M.ELF32_SECTION.sh_name_offset),
|
||||||
sh_type = M.read_u32(adapter, sh_off + M.ELF32_SECTION.sh_type_offset),
|
sh_type = M.read_u32(adapter, sh_off + M.ELF32_SECTION.sh_type_offset),
|
||||||
sh_flags = M.read_u32(adapter, sh_off + M.ELF32_SECTION.sh_flags_offset),
|
sh_flags = M.read_u32(adapter, sh_off + M.ELF32_SECTION.sh_flags_offset),
|
||||||
@@ -395,21 +378,16 @@ end
|
|||||||
function M.walk_sections(adapter, hdr)
|
function M.walk_sections(adapter, hdr)
|
||||||
if not hdr or hdr.error then return nil, hdr and hdr.error or "truncated_section_headers" end
|
if not hdr or hdr.error then return nil, hdr and hdr.error or "truncated_section_headers" end
|
||||||
|
|
||||||
--- @type integer
|
local file_size = M.size(adapter) ---@type integer
|
||||||
local file_size = M.size(adapter)
|
|
||||||
if hdr.e_shoff + hdr.e_shnum * hdr.e_shentsize > file_size then
|
if hdr.e_shoff + hdr.e_shnum * hdr.e_shentsize > file_size then
|
||||||
return nil, "truncated_section_headers"
|
return nil, "truncated_section_headers"
|
||||||
end
|
end
|
||||||
|
|
||||||
-- Read every section header first; we need .shstrtab to resolve names.
|
-- Read every section header first; we need .shstrtab to resolve names.
|
||||||
--- @type Elf32Section[]
|
local sections = {} ---@type Elf32Section[]
|
||||||
local sections = {}
|
for i = 0, hdr.e_shnum - 1 do ---@type integer
|
||||||
--- @type integer
|
local sh_off = hdr.e_shoff + i * hdr.e_shentsize ---@type integer
|
||||||
for i = 0, hdr.e_shnum - 1 do
|
local entry, err = read_section_entry(adapter, sh_off) ---@type Elf32Section|nil, string|nil
|
||||||
--- @type integer
|
|
||||||
local sh_off = hdr.e_shoff + i * hdr.e_shentsize
|
|
||||||
--- @type Elf32Section|nil, string|nil
|
|
||||||
local entry, err = read_section_entry(adapter, sh_off)
|
|
||||||
if not entry then return nil, err end
|
if not entry then return nil, err end
|
||||||
sections[i + 1] = entry
|
sections[i + 1] = entry
|
||||||
end
|
end
|
||||||
@@ -418,20 +396,17 @@ function M.walk_sections(adapter, hdr)
|
|||||||
return nil, "missing_shstrtab"
|
return nil, "missing_shstrtab"
|
||||||
end
|
end
|
||||||
|
|
||||||
--- @type Elf32Section|nil
|
local shstrtab = sections[hdr.e_shstrndx + 1] ---@type Elf32Section|nil
|
||||||
local shstrtab = sections[hdr.e_shstrndx + 1]
|
|
||||||
if not shstrtab or shstrtab.sh_type ~= M.SHT_STRTAB then
|
if not shstrtab or shstrtab.sh_type ~= M.SHT_STRTAB then
|
||||||
return nil, "missing_shstrtab"
|
return nil, "missing_shstrtab"
|
||||||
end
|
end
|
||||||
if shstrtab.sh_offset + shstrtab.sh_size > file_size then
|
if shstrtab.sh_offset + shstrtab.sh_size > file_size then
|
||||||
return nil, "truncated_section_headers"
|
return nil, "truncated_section_headers"
|
||||||
end
|
end
|
||||||
--- @type string|nil
|
local shstrtab_bytes = M.read_section_bytes(adapter, shstrtab) ---@type string|nil
|
||||||
local shstrtab_bytes = M.read_section_bytes(adapter, shstrtab)
|
|
||||||
if not shstrtab_bytes then return nil, "truncated_section_headers" end
|
if not shstrtab_bytes then return nil, "truncated_section_headers" end
|
||||||
|
|
||||||
--- @type integer, Elf32Section
|
for _, s in ipairs(sections) do ---@type integer, Elf32Section
|
||||||
for _, s in ipairs(sections) do
|
|
||||||
s.name = M.get_str(shstrtab_bytes, s.sh_name) or ""
|
s.name = M.get_str(shstrtab_bytes, s.sh_name) or ""
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -444,15 +419,11 @@ end
|
|||||||
--- @param section Elf32Section
|
--- @param section Elf32Section
|
||||||
--- @return string|nil
|
--- @return string|nil
|
||||||
function M.read_section_bytes(adapter, section)
|
function M.read_section_bytes(adapter, section)
|
||||||
--- @type integer
|
local size = section.sh_size ---@type integer
|
||||||
local size = section.sh_size
|
|
||||||
if size == 0 then return "" end
|
if size == 0 then return "" end
|
||||||
--- @type string[]
|
local out = {} ---@type string[]
|
||||||
local out = {}
|
for i = 0, size - 1 do ---@type integer
|
||||||
--- @type integer
|
local b = M.read_u8(adapter, section.sh_offset + i) ---@type integer|nil
|
||||||
for i = 0, size - 1 do
|
|
||||||
--- @type integer|nil
|
|
||||||
local b = M.read_u8(adapter, section.sh_offset + i)
|
|
||||||
if b == nil then return nil end
|
if b == nil then return nil end
|
||||||
out[#out + 1] = string.char(b)
|
out[#out + 1] = string.char(b)
|
||||||
end
|
end
|
||||||
@@ -467,11 +438,9 @@ end
|
|||||||
--- @return string|nil, string|nil
|
--- @return string|nil, string|nil
|
||||||
function M.read_named_section(adapter, sections, name)
|
function M.read_named_section(adapter, sections, name)
|
||||||
if not sections then return nil, "missing_section" end
|
if not sections then return nil, "missing_section" end
|
||||||
--- @type integer, Elf32Section
|
for _, s in ipairs(sections) do ---@type integer, Elf32Section
|
||||||
for _, s in ipairs(sections) do
|
|
||||||
if s.name == name then
|
if s.name == name then
|
||||||
--- @type string|nil
|
local bytes = M.read_section_bytes(adapter, s) ---@type string|nil
|
||||||
local bytes = M.read_section_bytes(adapter, s)
|
|
||||||
if not bytes then return nil, "truncated_section_data" end
|
if not bytes then return nil, "truncated_section_data" end
|
||||||
return bytes, nil
|
return bytes, nil
|
||||||
end
|
end
|
||||||
@@ -488,59 +457,42 @@ end
|
|||||||
--- @return table<string, Elf32Sym>|nil, string|nil
|
--- @return table<string, Elf32Sym>|nil, string|nil
|
||||||
function M.collect_symbols(adapter, sections)
|
function M.collect_symbols(adapter, sections)
|
||||||
if not sections then return nil, "missing_sections" end
|
if not sections then return nil, "missing_sections" end
|
||||||
--- @type table<string, Elf32Sym> -- bag: symbol name -> Elf32Sym
|
local symbols = {} ---@type table<string, Elf32Sym> -- bag: symbol name -> Elf32Sym
|
||||||
local symbols = {}
|
local file_size = M.size(adapter) ---@type integer
|
||||||
--- @type integer
|
for _, s in ipairs(sections) do ---@type integer, Elf32Section
|
||||||
local file_size = M.size(adapter)
|
|
||||||
--- @type integer, Elf32Section
|
|
||||||
for _, s in ipairs(sections) do
|
|
||||||
if s.sh_type == M.SHT_SYMTAB then
|
if s.sh_type == M.SHT_SYMTAB then
|
||||||
--- @type Elf32Section|nil
|
local strtab = sections[s.sh_link + 1] ---@type Elf32Section|nil
|
||||||
local strtab = sections[s.sh_link + 1]
|
|
||||||
if not strtab or strtab.sh_type ~= M.SHT_STRTAB then
|
if not strtab or strtab.sh_type ~= M.SHT_STRTAB then
|
||||||
return nil, "missing_symtab_strtab"
|
return nil, "missing_symtab_strtab"
|
||||||
end
|
end
|
||||||
if strtab.sh_offset + strtab.sh_size > file_size then
|
if strtab.sh_offset + strtab.sh_size > file_size then
|
||||||
return nil, "truncated_section_headers"
|
return nil, "truncated_section_headers"
|
||||||
end
|
end
|
||||||
--- @type string|nil
|
local strtab_bytes = M.read_section_bytes(adapter, strtab) ---@type string|nil
|
||||||
local strtab_bytes = M.read_section_bytes(adapter, strtab)
|
|
||||||
if not strtab_bytes then return nil, "truncated_section_headers" end
|
if not strtab_bytes then return nil, "truncated_section_headers" end
|
||||||
if s.sh_offset + s.sh_size > file_size then
|
if s.sh_offset + s.sh_size > file_size then
|
||||||
return nil, "truncated_section_headers"
|
return nil, "truncated_section_headers"
|
||||||
end
|
end
|
||||||
--- @type string|nil
|
local symtab_bytes = M.read_section_bytes(adapter, s) ---@type string|nil
|
||||||
local symtab_bytes = M.read_section_bytes(adapter, s)
|
|
||||||
if not symtab_bytes then return nil, "truncated_section_headers" end
|
if not symtab_bytes then return nil, "truncated_section_headers" end
|
||||||
--- @type number
|
local n = #symtab_bytes / M.ELF32_SYM.sym_entry_bytes ---@type number
|
||||||
local n = #symtab_bytes / M.ELF32_SYM.sym_entry_bytes
|
for j = 0, n - 1 do ---@type integer
|
||||||
--- @type integer
|
local e = s.sh_offset + j * M.ELF32_SYM.sym_entry_bytes ---@type integer
|
||||||
for j = 0, n - 1 do
|
local st_name = M.read_u32(adapter, e + M.ELF32_SYM.st_name) ---@type integer|nil
|
||||||
--- @type integer
|
|
||||||
local e = s.sh_offset + j * M.ELF32_SYM.sym_entry_bytes
|
|
||||||
--- @type integer|nil
|
|
||||||
local st_name = M.read_u32(adapter, e + M.ELF32_SYM.st_name)
|
|
||||||
if st_name then
|
if st_name then
|
||||||
--- @type integer|nil
|
local st_value = M.read_u32(adapter, e + M.ELF32_SYM.st_value) ---@type integer|nil
|
||||||
local st_value = M.read_u32(adapter, e + M.ELF32_SYM.st_value)
|
local st_size = M.read_u32(adapter, e + M.ELF32_SYM.st_size) ---@type integer|nil
|
||||||
--- @type integer|nil
|
local st_info = M.read_u8(adapter, e + M.ELF32_SYM.st_info) ---@type integer|nil
|
||||||
local st_size = M.read_u32(adapter, e + M.ELF32_SYM.st_size)
|
|
||||||
--- @type integer|nil
|
|
||||||
local st_info = M.read_u8(adapter, e + M.ELF32_SYM.st_info)
|
|
||||||
-- st_shndx is at offset 14 (2 bytes) — derived from the layout
|
-- st_shndx is at offset 14 (2 bytes) — derived from the layout
|
||||||
-- the metaprogram reads too. Inline the read to keep the
|
-- the metaprogram reads too. Inline the read to keep the
|
||||||
-- adapter as the only I/O surface.
|
-- adapter as the only I/O surface.
|
||||||
--- @type integer|nil
|
local b1 = M.read_u8(adapter, e + 14) ---@type integer|nil
|
||||||
local b1 = M.read_u8(adapter, e + 14)
|
local b2 = M.read_u8(adapter, e + 15) ---@type integer|nil
|
||||||
--- @type integer|nil
|
|
||||||
local b2 = M.read_u8(adapter, e + 15)
|
|
||||||
if not (b1 and b2) then
|
if not (b1 and b2) then
|
||||||
return nil, "truncated_section_headers"
|
return nil, "truncated_section_headers"
|
||||||
end
|
end
|
||||||
--- @type integer
|
local st_shndx = b1 + b2 * 0x100 ---@type integer
|
||||||
local st_shndx = b1 + b2 * 0x100
|
local name = M.get_str(strtab_bytes, st_name) or "" ---@type string
|
||||||
--- @type string
|
|
||||||
local name = M.get_str(strtab_bytes, st_name) or ""
|
|
||||||
if name ~= "" then
|
if name ~= "" then
|
||||||
symbols[name] = {
|
symbols[name] = {
|
||||||
value = st_value,
|
value = st_value,
|
||||||
|
|||||||
+200
-350
@@ -84,44 +84,41 @@
|
|||||||
--- @field attrs AbbrevAttr[]
|
--- @field attrs AbbrevAttr[]
|
||||||
|
|
||||||
--- @class ElfDwarf
|
--- @class ElfDwarf
|
||||||
--- @field DW_TAG table<string, integer> -- bag: tag name -> encoding
|
--- @field DW_TAG table<string, integer> -- bag: tag name -> encoding
|
||||||
--- @field DW_AT table<string, integer> -- bag: attr name -> encoding
|
--- @field DW_AT table<string, integer> -- bag: attr name -> encoding
|
||||||
--- @field DW_FORM table<string, integer> -- bag: form name -> encoding
|
--- @field DW_FORM table<string, integer> -- bag: form name -> encoding
|
||||||
--- @field DW_ATE table<string, integer> -- bag: base-type encoding name -> code
|
--- @field DW_ATE table<string, integer> -- bag: base-type encoding name -> code
|
||||||
--- @field MIPS_BYTES_PER_WORD integer
|
--- @field MIPS_BYTES_PER_WORD integer
|
||||||
--- @field dw_dwarf32_terminator integer
|
--- @field dw_dwarf32_terminator integer
|
||||||
--- @field DWARF4_ARANGES Dwarf4Aranges
|
--- @field DWARF4_ARANGES Dwarf4Aranges
|
||||||
--- @field DWARF5_RNGLISTS Dwarf5Rnglists
|
--- @field DWARF5_RNGLISTS Dwarf5Rnglists
|
||||||
--- @field DWARF_LINE_OPS DwarfLineOps
|
--- @field DWARF_LINE_OPS DwarfLineOps
|
||||||
--- @field DWARF5_DEBUG_LINE Dwarf5DebugLine
|
--- @field DWARF5_DEBUG_LINE Dwarf5DebugLine
|
||||||
--- @field read_u32_le fun(buf: string, off: integer): integer
|
--- @field read_u32_le fun(buf: string, off: integer): integer
|
||||||
--- @field read_u16_le fun(buf: string, off: integer): integer
|
--- @field read_u16_le fun(buf: string, off: integer): integer
|
||||||
--- @field read_uleb128_at fun(buf: string, pos: integer): integer|nil, integer
|
--- @field read_uleb128_at fun(buf: string, pos: integer): integer|nil, integer
|
||||||
--- @field read_sleb128_at fun(buf: string, pos: integer): integer|nil, integer
|
--- @field read_sleb128_at fun(buf: string, pos: integer): integer|nil, integer
|
||||||
--- @field find_abbrev_table_end fun(table_bytes: string, table_start: integer): integer|nil
|
--- @field find_abbrev_table_end fun(table_bytes: string, table_start: integer): integer|nil
|
||||||
--- @field read_ref_sig8 fun(buf: string, pos: integer): integer, integer, integer
|
--- @field read_ref_sig8 fun(buf: string, pos: integer): integer, integer, integer
|
||||||
--- @field find_type_unit_by_signature fun(info: string, target_sig_lo: integer, target_sig_hi: integer): integer|nil, integer|nil
|
--- @field find_type_unit_by_signature fun(info: string, target_sig_lo: integer, target_sig_hi: integer): integer|nil, integer|nil
|
||||||
--- @field write_u32_le fun(value: integer): string
|
--- @field write_u32_le fun(value: integer): string
|
||||||
--- @field write_u16_le fun(value: integer): string
|
--- @field write_u16_le fun(value: integer): string
|
||||||
--- @field read_elf_sections fun(elf_path: Path, section_names: string[]): table<string, string>
|
--- @field read_elf_sections fun(elf_path: Path, section_names: string[]): table<string, string>
|
||||||
--- @field read_nm fun(elf_path: Path): table<string, NmAddr>
|
--- @field read_nm fun(elf_path: Path): table<string, NmAddr>
|
||||||
--- @field uleb128 fun(n: integer): string
|
--- @field uleb128 fun(n: integer): string
|
||||||
--- @field sleb128 fun(n: integer): string
|
--- @field sleb128 fun(n: integer): string
|
||||||
--- @field uleb128_size fun(n: integer): integer
|
--- @field uleb128_size fun(n: integer): integer
|
||||||
--- @field sleb128_size fun(n: integer): integer
|
--- @field sleb128_size fun(n: integer): integer
|
||||||
--- @field read_line_unit_file_table fun(elf_path: string): table<string, integer>|nil, table<integer, string>|nil, table<integer, string>|nil
|
--- @field read_line_unit_file_table fun(elf_path: string): table<string, integer>|nil, table<integer, string>|nil, table<integer, string>|nil
|
||||||
|
|
||||||
--- @type LfsMod
|
local lfs = require("lfs") ---@type LfsMod
|
||||||
local lfs = require("lfs")
|
|
||||||
|
|
||||||
-- scripts/elf32.lua contains format-constant tables + the byte-level walker.
|
-- scripts/elf32.lua contains format-constant tables + the byte-level walker.
|
||||||
-- The this file re-exports `read_u32_le` / `read_u16_le` (and the DWARF32 terminator).
|
-- The this file re-exports `read_u32_le` / `read_u16_le` (and the DWARF32 terminator).
|
||||||
-- read_u32_le is this module's reader; implementation in elf32.lua.
|
-- read_u32_le is this module's reader; implementation in elf32.lua.
|
||||||
--- @type Elf32Mod
|
local E = require("elf32") ---@type Elf32Mod
|
||||||
local E = require("elf32")
|
|
||||||
|
|
||||||
--- @type ElfDwarf
|
local M = {} ---@type ElfDwarf
|
||||||
local M = {}
|
|
||||||
|
|
||||||
-- ════════════════════════════════════════════════════════════════════════════
|
-- ════════════════════════════════════════════════════════════════════════════
|
||||||
-- DWARF tag + form constants
|
-- DWARF tag + form constants
|
||||||
@@ -200,8 +197,7 @@ M.DW_ATE = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
-- DWARF5 §7.5.6 DW_FORM_implicit_const
|
-- DWARF5 §7.5.6 DW_FORM_implicit_const
|
||||||
--- @type integer
|
local DW_FORM_implicit_const = 0x21 ---@type integer
|
||||||
local DW_FORM_implicit_const = 0x21
|
|
||||||
|
|
||||||
-- ════════════════════════════════════════════════════════════════════════════
|
-- ════════════════════════════════════════════════════════════════════════════
|
||||||
-- Format-constant tables
|
-- Format-constant tables
|
||||||
@@ -378,13 +374,10 @@ end
|
|||||||
--- @return integer|nil
|
--- @return integer|nil
|
||||||
--- @return integer
|
--- @return integer
|
||||||
function M.read_uleb128_at(buf, pos)
|
function M.read_uleb128_at(buf, pos)
|
||||||
--- @type integer, integer
|
local value, shift = 0, 0 ---@type integer, integer
|
||||||
local value, shift = 0, 0
|
local len = #buf ---@type integer
|
||||||
--- @type integer
|
|
||||||
local len = #buf
|
|
||||||
while pos < len do
|
while pos < len do
|
||||||
--- @type integer
|
local b = buf:byte(pos + 1) ---@type integer
|
||||||
local b = buf:byte(pos + 1)
|
|
||||||
value = value + (b % 0x80) * (2 ^ shift)
|
value = value + (b % 0x80) * (2 ^ shift)
|
||||||
shift = shift + 7
|
shift = shift + 7
|
||||||
pos = pos + 1
|
pos = pos + 1
|
||||||
@@ -398,13 +391,10 @@ end
|
|||||||
--- @return integer|nil
|
--- @return integer|nil
|
||||||
--- @return integer
|
--- @return integer
|
||||||
function M.read_sleb128_at(buf, pos)
|
function M.read_sleb128_at(buf, pos)
|
||||||
--- @type integer, integer
|
local value, shift = 0, 0 ---@type integer, integer
|
||||||
local value, shift = 0, 0
|
local len = #buf ---@type integer
|
||||||
--- @type integer
|
|
||||||
local len = #buf
|
|
||||||
while pos < len do
|
while pos < len do
|
||||||
--- @type integer
|
local b = buf:byte(pos + 1) ---@type integer
|
||||||
local b = buf:byte(pos + 1)
|
|
||||||
value = value + (b % 0x80) * (2 ^ shift)
|
value = value + (b % 0x80) * (2 ^ shift)
|
||||||
shift = shift + 7
|
shift = shift + 7
|
||||||
pos = pos + 1
|
pos = pos + 1
|
||||||
@@ -423,33 +413,27 @@ end
|
|||||||
--- @param table_start integer
|
--- @param table_start integer
|
||||||
--- @return integer|nil
|
--- @return integer|nil
|
||||||
function M.find_abbrev_table_end(table_bytes, table_start)
|
function M.find_abbrev_table_end(table_bytes, table_start)
|
||||||
--- @type integer, integer
|
local pos, len = table_start, #table_bytes ---@type integer, integer
|
||||||
local pos, len = table_start, #table_bytes
|
|
||||||
if pos >= len or table_bytes:byte(pos + 1) == 0 then return pos end
|
if pos >= len or table_bytes:byte(pos + 1) == 0 then return pos end
|
||||||
while pos < len do
|
while pos < len do
|
||||||
--- @type integer|nil, integer
|
local _code, code_end = M.read_uleb128_at(table_bytes, pos) ---@type integer|nil, integer
|
||||||
local _code, code_end = M.read_uleb128_at(table_bytes, pos)
|
|
||||||
if not _code then return nil end
|
if not _code then return nil end
|
||||||
pos = code_end
|
pos = code_end
|
||||||
--- @type integer|nil, integer
|
local _tag, tag_end = M.read_uleb128_at(table_bytes, pos) ---@type integer|nil, integer
|
||||||
local _tag, tag_end = M.read_uleb128_at(table_bytes, pos)
|
|
||||||
if not _tag then return nil end
|
if not _tag then return nil end
|
||||||
pos = tag_end
|
pos = tag_end
|
||||||
if pos >= len then return nil end
|
if pos >= len then return nil end
|
||||||
pos = pos + 1 -- has_children byte
|
pos = pos + 1 -- has_children byte
|
||||||
while pos < len do
|
while pos < len do
|
||||||
--- @type integer|nil, integer
|
local attr, attr_end = M.read_uleb128_at(table_bytes, pos) ---@type integer|nil, integer
|
||||||
local attr, attr_end = M.read_uleb128_at(table_bytes, pos)
|
|
||||||
if not attr then return nil end
|
if not attr then return nil end
|
||||||
pos = attr_end
|
pos = attr_end
|
||||||
--- @type integer|nil, integer
|
local form, form_end = M.read_uleb128_at(table_bytes, pos) ---@type integer|nil, integer
|
||||||
local form, form_end = M.read_uleb128_at(table_bytes, pos)
|
|
||||||
if not form then return nil end
|
if not form then return nil end
|
||||||
pos = form_end
|
pos = form_end
|
||||||
if attr == 0 and form == 0 then break end
|
if attr == 0 and form == 0 then break end
|
||||||
if form == DW_FORM_implicit_const then
|
if form == DW_FORM_implicit_const then
|
||||||
--- @type integer|nil, integer
|
local _c, ce = M.read_sleb128_at(table_bytes, pos) ---@type integer|nil, integer
|
||||||
local _c, ce = M.read_sleb128_at(table_bytes, pos)
|
|
||||||
if not _c then return nil end
|
if not _c then return nil end
|
||||||
pos = ce
|
pos = ce
|
||||||
end
|
end
|
||||||
@@ -466,10 +450,8 @@ end
|
|||||||
--- @param off integer
|
--- @param off integer
|
||||||
--- @return string
|
--- @return string
|
||||||
local function read_c_string_at(buf, off)
|
local function read_c_string_at(buf, off)
|
||||||
--- @type integer
|
local len = #buf ---@type integer
|
||||||
local len = #buf
|
local start = off ---@type integer
|
||||||
--- @type integer
|
|
||||||
local start = off
|
|
||||||
while off < len and buf:byte(off + 1) ~= 0 do off = off + 1 end
|
while off < len and buf:byte(off + 1) ~= 0 do off = off + 1 end
|
||||||
return buf:sub(start + 1, off)
|
return buf:sub(start + 1, off)
|
||||||
end
|
end
|
||||||
@@ -482,41 +464,31 @@ end
|
|||||||
--- @return AbbrevDecl[]|nil
|
--- @return AbbrevDecl[]|nil
|
||||||
--- @return string|nil
|
--- @return string|nil
|
||||||
local function parse_abbrev_table(table_bytes, table_start)
|
local function parse_abbrev_table(table_bytes, table_start)
|
||||||
--- @type integer|nil
|
local table_end = M.find_abbrev_table_end(table_bytes, table_start) ---@type integer|nil
|
||||||
local table_end = M.find_abbrev_table_end(table_bytes, table_start)
|
|
||||||
if not table_end then return nil, "no terminator" end
|
if not table_end then return nil, "no terminator" end
|
||||||
--- @type AbbrevDecl[]
|
local decls = {} ---@type AbbrevDecl[]
|
||||||
local decls = {}
|
local pos = table_start ---@type integer
|
||||||
--- @type integer
|
|
||||||
local pos = table_start
|
|
||||||
while pos < table_end do
|
while pos < table_end do
|
||||||
--- @type integer|nil, integer
|
local code, code_end = M.read_uleb128_at(table_bytes, pos) ---@type integer|nil, integer
|
||||||
local code, code_end = M.read_uleb128_at(table_bytes, pos)
|
|
||||||
if not code then return nil, "truncated code" end
|
if not code then return nil, "truncated code" end
|
||||||
pos = code_end
|
pos = code_end
|
||||||
--- @type integer|nil, integer
|
local tag, tag_end = M.read_uleb128_at(table_bytes, pos) ---@type integer|nil, integer
|
||||||
local tag, tag_end = M.read_uleb128_at(table_bytes, pos)
|
|
||||||
if not tag then return nil, "truncated tag" end
|
if not tag then return nil, "truncated tag" end
|
||||||
pos = tag_end
|
pos = tag_end
|
||||||
--- @type integer
|
local has_children = table_bytes:byte(pos + 1) ---@type integer
|
||||||
local has_children = table_bytes:byte(pos + 1)
|
|
||||||
pos = pos + 1
|
pos = pos + 1
|
||||||
--- @type AbbrevAttr[]
|
local attrs = {} ---@type AbbrevAttr[]
|
||||||
local attrs = {}
|
|
||||||
while true do
|
while true do
|
||||||
--- @type integer|nil, integer
|
local attr, attr_end = M.read_uleb128_at(table_bytes, pos) ---@type integer|nil, integer
|
||||||
local attr, attr_end = M.read_uleb128_at(table_bytes, pos)
|
|
||||||
if not attr then return nil, "truncated attr" end
|
if not attr then return nil, "truncated attr" end
|
||||||
pos = attr_end
|
pos = attr_end
|
||||||
--- @type integer|nil, integer
|
local form, form_end = M.read_uleb128_at(table_bytes, pos) ---@type integer|nil, integer
|
||||||
local form, form_end = M.read_uleb128_at(table_bytes, pos)
|
|
||||||
if not form then return nil, "truncated form" end
|
if not form then return nil, "truncated form" end
|
||||||
pos = form_end
|
pos = form_end
|
||||||
if attr == 0 and form == 0 then break end
|
if attr == 0 and form == 0 then break end
|
||||||
attrs[#attrs + 1] = { name = attr, form = form }
|
attrs[#attrs + 1] = { name = attr, form = form }
|
||||||
if form == DW_FORM_implicit_const then
|
if form == DW_FORM_implicit_const then
|
||||||
--- @type integer|nil, integer
|
local _c, ce = M.read_sleb128_at(table_bytes, pos) ---@type integer|nil, integer
|
||||||
local _c, ce = M.read_sleb128_at(table_bytes, pos)
|
|
||||||
if not _c then return nil, "truncated const" end
|
if not _c then return nil, "truncated const" end
|
||||||
pos = ce
|
pos = ce
|
||||||
end
|
end
|
||||||
@@ -531,10 +503,9 @@ end
|
|||||||
-- For DW_FORM_strp we return the inline string resolved from `str_buf`.
|
-- For DW_FORM_strp we return the inline string resolved from `str_buf`.
|
||||||
-- For DW_FORM_ref4 we return the absolute CU-relative offset.
|
-- For DW_FORM_ref4 we return the absolute CU-relative offset.
|
||||||
-- The caller decides whether to interpret that as a section offset.
|
-- The caller decides whether to interpret that as a section offset.
|
||||||
--- @type table<integer, fun(buf: string, str_buf: string, pos: integer): (string|integer|nil, integer)>
|
local FORM_READERS = { ---@type table<integer, fun(buf: string, str_buf: string, pos: integer): (string|integer|nil, integer)>
|
||||||
local FORM_READERS = {
|
|
||||||
--- @param buf string
|
--- @param buf string
|
||||||
--- @param _ string
|
--- @param _ string
|
||||||
--- @param pos integer
|
--- @param pos integer
|
||||||
--- @return integer
|
--- @return integer
|
||||||
--- @return integer
|
--- @return integer
|
||||||
@@ -542,28 +513,26 @@ local FORM_READERS = {
|
|||||||
return M.read_u32_le(buf, pos), pos + 4
|
return M.read_u32_le(buf, pos), pos + 4
|
||||||
end,
|
end,
|
||||||
--- @param buf string
|
--- @param buf string
|
||||||
--- @param _ string
|
--- @param _ string
|
||||||
--- @param pos integer
|
--- @param pos integer
|
||||||
--- @return string
|
--- @return string
|
||||||
--- @return integer
|
--- @return integer
|
||||||
[M.DW_FORM.string] = function(buf, _, pos)
|
[M.DW_FORM.string] = function(buf, _, pos)
|
||||||
--- @type string
|
local s = read_c_string_at(buf, pos) ---@type string
|
||||||
local s = read_c_string_at(buf, pos)
|
|
||||||
return s, pos + #s + 1
|
return s, pos + #s + 1
|
||||||
end,
|
end,
|
||||||
--- @param buf string
|
--- @param buf string
|
||||||
--- @param str_buf string
|
--- @param str_buf string
|
||||||
--- @param pos integer
|
--- @param pos integer
|
||||||
--- @return string
|
--- @return string
|
||||||
--- @return integer
|
--- @return integer
|
||||||
[M.DW_FORM.strp] = function(buf, str_buf, pos)
|
[M.DW_FORM.strp] = function(buf, str_buf, pos)
|
||||||
-- DW_FORM_strp: 4-byte offset into .debug_str.
|
-- DW_FORM_strp: 4-byte offset into .debug_str.
|
||||||
--- @type integer
|
local strp_off = M.read_u32_le(buf, pos) ---@type integer
|
||||||
local strp_off = M.read_u32_le(buf, pos)
|
|
||||||
return read_c_string_at(str_buf, strp_off), pos + 4
|
return read_c_string_at(str_buf, strp_off), pos + 4
|
||||||
end,
|
end,
|
||||||
--- @param buf string
|
--- @param buf string
|
||||||
--- @param _ string
|
--- @param _ string
|
||||||
--- @param pos integer
|
--- @param pos integer
|
||||||
--- @return integer|nil
|
--- @return integer|nil
|
||||||
--- @return integer
|
--- @return integer
|
||||||
@@ -579,7 +548,7 @@ local FORM_READERS = {
|
|||||||
return buf:byte(pos + 1), pos + 1
|
return buf:byte(pos + 1), pos + 1
|
||||||
end,
|
end,
|
||||||
--- @param buf string
|
--- @param buf string
|
||||||
--- @param _ string
|
--- @param _ string
|
||||||
--- @param pos integer
|
--- @param pos integer
|
||||||
--- @return integer
|
--- @return integer
|
||||||
--- @return integer
|
--- @return integer
|
||||||
@@ -587,7 +556,7 @@ local FORM_READERS = {
|
|||||||
return M.read_u16_le(buf, pos), pos + 2
|
return M.read_u16_le(buf, pos), pos + 2
|
||||||
end,
|
end,
|
||||||
--- @param buf string
|
--- @param buf string
|
||||||
--- @param _ string
|
--- @param _ string
|
||||||
--- @param pos integer
|
--- @param pos integer
|
||||||
--- @return integer
|
--- @return integer
|
||||||
--- @return integer
|
--- @return integer
|
||||||
@@ -595,7 +564,7 @@ local FORM_READERS = {
|
|||||||
return M.read_u32_le(buf, pos), pos + 4
|
return M.read_u32_le(buf, pos), pos + 4
|
||||||
end,
|
end,
|
||||||
--- @param buf string
|
--- @param buf string
|
||||||
--- @param _ string
|
--- @param _ string
|
||||||
--- @param pos integer
|
--- @param pos integer
|
||||||
--- @return integer
|
--- @return integer
|
||||||
--- @return integer
|
--- @return integer
|
||||||
@@ -603,7 +572,7 @@ local FORM_READERS = {
|
|||||||
return M.read_u32_le(buf, pos), pos + 4
|
return M.read_u32_le(buf, pos), pos + 4
|
||||||
end,
|
end,
|
||||||
--- @param buf string
|
--- @param buf string
|
||||||
--- @param _ string
|
--- @param _ string
|
||||||
--- @param pos integer
|
--- @param pos integer
|
||||||
--- @return integer
|
--- @return integer
|
||||||
--- @return integer
|
--- @return integer
|
||||||
@@ -612,8 +581,8 @@ local FORM_READERS = {
|
|||||||
-- on DWARF5 32-bit it's always 4 bytes).
|
-- on DWARF5 32-bit it's always 4 bytes).
|
||||||
return M.read_u32_le(buf, pos), pos + 4
|
return M.read_u32_le(buf, pos), pos + 4
|
||||||
end,
|
end,
|
||||||
--- @param _ string
|
--- @param _ string
|
||||||
--- @param _ string
|
--- @param _ string
|
||||||
--- @param pos integer
|
--- @param pos integer
|
||||||
--- @return integer
|
--- @return integer
|
||||||
--- @return integer
|
--- @return integer
|
||||||
@@ -627,8 +596,7 @@ local FORM_READERS = {
|
|||||||
--- @return integer
|
--- @return integer
|
||||||
[M.DW_FORM.exprloc] = function(buf, _, pos)
|
[M.DW_FORM.exprloc] = function(buf, _, pos)
|
||||||
-- DW_FORM_exprloc: ULEB byte count + that many bytes of DW_OP_*.
|
-- DW_FORM_exprloc: ULEB byte count + that many bytes of DW_OP_*.
|
||||||
--- @type integer|nil, integer
|
local len, ne = M.read_uleb128_at(buf, pos) ---@type integer|nil, integer
|
||||||
local len, ne = M.read_uleb128_at(buf, pos)
|
|
||||||
if not len then return nil, pos end
|
if not len then return nil, pos end
|
||||||
return nil, ne + len
|
return nil, ne + len
|
||||||
end,
|
end,
|
||||||
@@ -642,7 +610,7 @@ local FORM_READERS = {
|
|||||||
return nil, pos
|
return nil, pos
|
||||||
end,
|
end,
|
||||||
--- @param buf string
|
--- @param buf string
|
||||||
--- @param _ string
|
--- @param _ string
|
||||||
--- @param pos integer
|
--- @param pos integer
|
||||||
--- @return integer
|
--- @return integer
|
||||||
--- @return integer
|
--- @return integer
|
||||||
@@ -654,20 +622,18 @@ local FORM_READERS = {
|
|||||||
-- then the high 4 to resolve the specific type within it.
|
-- then the high 4 to resolve the specific type within it.
|
||||||
-- Return the low 4 as the primary value to preserve the (value, next_pos) shape;
|
-- Return the low 4 as the primary value to preserve the (value, next_pos) shape;
|
||||||
-- the high 4 is exposed via M.read_ref_sig8 (which returns both halves).
|
-- the high 4 is exposed via M.read_ref_sig8 (which returns both halves).
|
||||||
--- @type integer, integer, integer
|
local _, _, next_pos = M.read_ref_sig8(buf, pos) ---@type integer, integer, integer
|
||||||
local _, _, next_pos = M.read_ref_sig8(buf, pos)
|
|
||||||
return M.read_u32_le(buf, pos), next_pos
|
return M.read_u32_le(buf, pos), next_pos
|
||||||
end,
|
end,
|
||||||
}
|
}
|
||||||
--- @param buf string
|
--- @param buf string
|
||||||
--- @param str_buf string
|
--- @param str_buf string
|
||||||
--- @param pos integer
|
--- @param pos integer
|
||||||
--- @param form integer
|
--- @param form integer
|
||||||
--- @return string|integer|nil
|
--- @return string|integer|nil
|
||||||
--- @return integer
|
--- @return integer
|
||||||
local function read_form_value(buf, str_buf, pos, form)
|
local function read_form_value(buf, str_buf, pos, form)
|
||||||
--- @type (fun(buf: string, str_buf: string, pos: integer): (string|integer|nil, integer))|nil
|
local r = FORM_READERS[form] ---@type (fun(buf: string, str_buf: string, pos: integer): (string|integer|nil, integer))|nil
|
||||||
local r = FORM_READERS[form]
|
|
||||||
if not r then
|
if not r then
|
||||||
return nil, pos
|
return nil, pos
|
||||||
end
|
end
|
||||||
@@ -701,21 +667,16 @@ function M.read_ref_sig8(buf, pos) return M.read_u32_le(buf, pos), M.read_u32_le
|
|||||||
--- @param target_sig_hi integer -- high 4 bytes (LE) of the desired signature
|
--- @param target_sig_hi integer -- high 4 bytes (LE) of the desired signature
|
||||||
--- @return integer|nil, integer|nil -- unit offset, type_offset within the unit
|
--- @return integer|nil, integer|nil -- unit offset, type_offset within the unit
|
||||||
function M.find_type_unit_by_signature(info, target_sig_lo, target_sig_hi)
|
function M.find_type_unit_by_signature(info, target_sig_lo, target_sig_hi)
|
||||||
--- @type integer
|
local pos = 0 ---@type integer
|
||||||
local pos = 0
|
local section_len = #info ---@type integer
|
||||||
--- @type integer
|
|
||||||
local section_len = #info
|
|
||||||
while pos + 4 < section_len do
|
while pos + 4 < section_len do
|
||||||
--- @type integer
|
local unit_length = M.read_u32_le(info, pos) ---@type integer
|
||||||
local unit_length = M.read_u32_le(info, pos)
|
|
||||||
if unit_length == 0xFFFFFFFF then
|
if unit_length == 0xFFFFFFFF then
|
||||||
return nil, nil -- DWARF64 not supported
|
return nil, nil -- DWARF64 not supported
|
||||||
end
|
end
|
||||||
-- unit_length is the body size, NOT including the 4-byte unit_length field itself.
|
-- unit_length is the body size, NOT including the 4-byte unit_length field itself.
|
||||||
--- @type integer
|
local body_start = pos + 4 ---@type integer
|
||||||
local body_start = pos + 4
|
local body_end = body_start + unit_length ---@type integer
|
||||||
--- @type integer
|
|
||||||
local body_end = body_start + unit_length
|
|
||||||
if body_end > section_len then
|
if body_end > section_len then
|
||||||
return nil, nil -- malformed
|
return nil, nil -- malformed
|
||||||
end
|
end
|
||||||
@@ -737,14 +698,11 @@ function M.find_type_unit_by_signature(info, target_sig_lo, target_sig_hi)
|
|||||||
-- byte 4-7: debug_abbrev_offset (4)
|
-- byte 4-7: debug_abbrev_offset (4)
|
||||||
-- byte 8-15: type_signature (8)
|
-- byte 8-15: type_signature (8)
|
||||||
-- byte 16-19: type_offset (4)
|
-- byte 16-19: type_offset (4)
|
||||||
--- @type integer
|
local unit_type = info:byte(body_start + 2 + 1) ---@type integer -- 0-based +2 = unit_type in 1-indexed
|
||||||
local unit_type = info:byte(body_start + 2 + 1) -- 0-based +2 = unit_type in 1-indexed
|
|
||||||
if unit_type == 0x02 then -- DW_UT_type
|
if unit_type == 0x02 then -- DW_UT_type
|
||||||
--- @type integer, integer, integer
|
local sig_lo, sig_hi, _ = M.read_ref_sig8(info, body_start + 8) ---@type integer, integer, integer -- 0-based +8 = type_signature in 1-indexed
|
||||||
local sig_lo, sig_hi, _ = M.read_ref_sig8(info, body_start + 8) -- 0-based +8 = type_signature in 1-indexed
|
|
||||||
if sig_lo == target_sig_lo and sig_hi == target_sig_hi then
|
if sig_lo == target_sig_lo and sig_hi == target_sig_hi then
|
||||||
--- @type integer
|
local type_offset = M.read_u32_le(info, body_start + 16) ---@type integer -- 0-based +16 = type_offset in 1-indexed
|
||||||
local type_offset = M.read_u32_le(info, body_start + 16) -- 0-based +16 = type_offset in 1-indexed
|
|
||||||
return pos, type_offset
|
return pos, type_offset
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
@@ -796,16 +754,12 @@ end
|
|||||||
function M.read_elf_sections(elf_path, section_names)
|
function M.read_elf_sections(elf_path, section_names)
|
||||||
-- Initialize result with all requested names set to "" so callers can do `sections[X]
|
-- Initialize result with all requested names set to "" so callers can do `sections[X]
|
||||||
-- or ""` for missing sections without nil-checks.
|
-- or ""` for missing sections without nil-checks.
|
||||||
--- @type table<string, string>
|
local result = {} ---@type table<string, string>
|
||||||
local result = {}
|
for _, name in ipairs(section_names) do result[name] = "" end ---@type integer, string
|
||||||
--- @type integer, string
|
|
||||||
for _, name in ipairs(section_names) do result[name] = "" end
|
|
||||||
|
|
||||||
-- O(1) lookup set.
|
-- O(1) lookup set.
|
||||||
--- @type table<string, boolean> -- bag: requested section name -> true
|
local wanted = {} ---@type table<string, boolean> -- bag: requested section name -> true
|
||||||
local wanted = {}
|
for _, name in ipairs(section_names) do wanted[name] = true end ---@type integer, string
|
||||||
--- @type integer, string
|
|
||||||
for _, name in ipairs(section_names) do wanted[name] = true end
|
|
||||||
|
|
||||||
-- Existence check (lfs.attributes avoids an io.open-vs-fail race).
|
-- Existence check (lfs.attributes avoids an io.open-vs-fail race).
|
||||||
if lfs.attributes(elf_path, "mode") ~= "file" then
|
if lfs.attributes(elf_path, "mode") ~= "file" then
|
||||||
@@ -813,27 +767,23 @@ function M.read_elf_sections(elf_path, section_names)
|
|||||||
return result
|
return result
|
||||||
end
|
end
|
||||||
|
|
||||||
--- @type file*|nil
|
local f = io.open(elf_path, "rb") ---@type file*|nil
|
||||||
local f = io.open(elf_path, "rb")
|
|
||||||
if not f then
|
if not f then
|
||||||
io.stderr:write(string.format("[elf_dwarf.read_elf_sections] io.open failed: %s\n", elf_path))
|
io.stderr:write(string.format("[elf_dwarf.read_elf_sections] io.open failed: %s\n", elf_path))
|
||||||
return result
|
return result
|
||||||
end
|
end
|
||||||
|
|
||||||
--- @type integer
|
local file_size ---@type integer
|
||||||
local file_size
|
|
||||||
do
|
do
|
||||||
f:seek("end", 0)
|
f:seek("end", 0)
|
||||||
file_size = f:seek("cur", 0)
|
file_size = f:seek("cur", 0)
|
||||||
end
|
end
|
||||||
--- @type Elf32Adapter
|
local adapter = { ---@type Elf32Adapter
|
||||||
local adapter = {
|
|
||||||
--- @param offset integer
|
--- @param offset integer
|
||||||
--- @return integer|nil
|
--- @return integer|nil
|
||||||
read_u8_at = function(offset)
|
read_u8_at = function(offset)
|
||||||
f:seek("set", offset)
|
f:seek("set", offset)
|
||||||
--- @type string|nil
|
local b = f:read(1) ---@type string|nil
|
||||||
local b = f:read(1)
|
|
||||||
if not b then return nil end
|
if not b then return nil end
|
||||||
return b:byte()
|
return b:byte()
|
||||||
end,
|
end,
|
||||||
@@ -841,10 +791,8 @@ function M.read_elf_sections(elf_path, section_names)
|
|||||||
--- @return integer|nil
|
--- @return integer|nil
|
||||||
read_u16_at = function(offset)
|
read_u16_at = function(offset)
|
||||||
f:seek("set", offset)
|
f:seek("set", offset)
|
||||||
--- @type string|nil
|
local b1 = f:read(1) ---@type string|nil
|
||||||
local b1 = f:read(1)
|
local b2 = f:read(1) ---@type string|nil
|
||||||
--- @type string|nil
|
|
||||||
local b2 = f:read(1)
|
|
||||||
if not b1 or not b2 then return nil end
|
if not b1 or not b2 then return nil end
|
||||||
return b1:byte() + b2:byte() * 0x100
|
return b1:byte() + b2:byte() * 0x100
|
||||||
end,
|
end,
|
||||||
@@ -852,14 +800,10 @@ function M.read_elf_sections(elf_path, section_names)
|
|||||||
--- @return integer|nil
|
--- @return integer|nil
|
||||||
read_u32_at = function(offset)
|
read_u32_at = function(offset)
|
||||||
f:seek("set", offset)
|
f:seek("set", offset)
|
||||||
--- @type string|nil
|
local b1 = f:read(1) ---@type string|nil
|
||||||
local b1 = f:read(1)
|
local b2 = f:read(1) ---@type string|nil
|
||||||
--- @type string|nil
|
local b3 = f:read(1) ---@type string|nil
|
||||||
local b2 = f:read(1)
|
local b4 = f:read(1) ---@type string|nil
|
||||||
--- @type string|nil
|
|
||||||
local b3 = f:read(1)
|
|
||||||
--- @type string|nil
|
|
||||||
local b4 = f:read(1)
|
|
||||||
if not b1 or not b2 or not b3 or not b4 then return nil end
|
if not b1 or not b2 or not b3 or not b4 then return nil end
|
||||||
return b1:byte() + b2:byte() * 0x100
|
return b1:byte() + b2:byte() * 0x100
|
||||||
+ b3:byte() * 0x10000 + b4:byte() * 0x1000000
|
+ b3:byte() * 0x10000 + b4:byte() * 0x1000000
|
||||||
@@ -869,16 +813,14 @@ function M.read_elf_sections(elf_path, section_names)
|
|||||||
}
|
}
|
||||||
|
|
||||||
-- Delegate the header parse + section walk to E.*.
|
-- Delegate the header parse + section walk to E.*.
|
||||||
--- @type Elf32Header|nil, string|nil
|
local hdr, hdr_err = E.parse_elf32_headers(adapter) ---@type Elf32Header|nil, string|nil
|
||||||
local hdr, hdr_err = E.parse_elf32_headers(adapter)
|
|
||||||
if not hdr then
|
if not hdr then
|
||||||
io.stderr:write(string.format("[elf_dwarf.read_elf_sections] header parse failed: %s\n", tostring(hdr_err)))
|
io.stderr:write(string.format("[elf_dwarf.read_elf_sections] header parse failed: %s\n", tostring(hdr_err)))
|
||||||
f:close()
|
f:close()
|
||||||
return result
|
return result
|
||||||
end
|
end
|
||||||
|
|
||||||
--- @type Elf32Section[]|nil, string|nil
|
local sections, walk_err = E.walk_sections(adapter, hdr) ---@type Elf32Section[]|nil, string|nil
|
||||||
local sections, walk_err = E.walk_sections(adapter, hdr)
|
|
||||||
if not sections then
|
if not sections then
|
||||||
io.stderr:write(string.format("[elf_dwarf.read_elf_sections] section walk failed: %s\n", tostring(walk_err)))
|
io.stderr:write(string.format("[elf_dwarf.read_elf_sections] section walk failed: %s\n", tostring(walk_err)))
|
||||||
f:close()
|
f:close()
|
||||||
@@ -886,11 +828,9 @@ function M.read_elf_sections(elf_path, section_names)
|
|||||||
end
|
end
|
||||||
|
|
||||||
-- Resolve the requested sections.
|
-- Resolve the requested sections.
|
||||||
--- @type integer, Elf32Section
|
for _, s in ipairs(sections) do ---@type integer, Elf32Section
|
||||||
for _, s in ipairs(sections) do
|
|
||||||
if wanted[s.name] then
|
if wanted[s.name] then
|
||||||
--- @type string|nil
|
local bytes = E.read_section_bytes(adapter, s) ---@type string|nil
|
||||||
local bytes = E.read_section_bytes(adapter, s)
|
|
||||||
if bytes then result[s.name] = bytes end
|
if bytes then result[s.name] = bytes end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
@@ -912,31 +852,26 @@ end
|
|||||||
--- @param elf_path Path
|
--- @param elf_path Path
|
||||||
--- @return table<string, NmAddr>
|
--- @return table<string, NmAddr>
|
||||||
function M.read_nm(elf_path)
|
function M.read_nm(elf_path)
|
||||||
--- @type table<string, NmAddr>
|
local addrs = {} ---@type table<string, NmAddr>
|
||||||
local addrs = {}
|
|
||||||
|
|
||||||
-- Existence check first; an empty or missing ELF returns an empty map.
|
-- Existence check first; an empty or missing ELF returns an empty map.
|
||||||
if lfs.attributes(elf_path, "mode") ~= "file" then return addrs end
|
if lfs.attributes(elf_path, "mode") ~= "file" then return addrs end
|
||||||
|
|
||||||
--- @type file*|nil
|
local f = io.open(elf_path, "rb") ---@type file*|nil
|
||||||
local f = io.open(elf_path, "rb")
|
|
||||||
if not f then return addrs end
|
if not f then return addrs end
|
||||||
|
|
||||||
-- Build the file adapter for E.*.
|
-- Build the file adapter for E.*.
|
||||||
--- @type integer
|
local file_size ---@type integer
|
||||||
local file_size
|
|
||||||
do
|
do
|
||||||
f:seek("end", 0)
|
f:seek("end", 0)
|
||||||
file_size = f:seek("cur", 0)
|
file_size = f:seek("cur", 0)
|
||||||
end
|
end
|
||||||
--- @type Elf32Adapter
|
local adapter = { ---@type Elf32Adapter
|
||||||
local adapter = {
|
|
||||||
--- @param offset integer
|
--- @param offset integer
|
||||||
--- @return integer|nil
|
--- @return integer|nil
|
||||||
read_u8_at = function(offset)
|
read_u8_at = function(offset)
|
||||||
f:seek("set", offset)
|
f:seek("set", offset)
|
||||||
--- @type string|nil
|
local b = f:read(1) ---@type string|nil
|
||||||
local b = f:read(1)
|
|
||||||
if not b then return nil end
|
if not b then return nil end
|
||||||
return b:byte()
|
return b:byte()
|
||||||
end,
|
end,
|
||||||
@@ -944,10 +879,8 @@ function M.read_nm(elf_path)
|
|||||||
--- @return integer|nil
|
--- @return integer|nil
|
||||||
read_u16_at = function(offset)
|
read_u16_at = function(offset)
|
||||||
f:seek("set", offset)
|
f:seek("set", offset)
|
||||||
--- @type string|nil
|
local b1 = f:read(1) ---@type string|nil
|
||||||
local b1 = f:read(1)
|
local b2 = f:read(1) ---@type string|nil
|
||||||
--- @type string|nil
|
|
||||||
local b2 = f:read(1)
|
|
||||||
if not b1 or not b2 then return nil end
|
if not b1 or not b2 then return nil end
|
||||||
return b1:byte() + b2:byte() * 0x100
|
return b1:byte() + b2:byte() * 0x100
|
||||||
end,
|
end,
|
||||||
@@ -955,14 +888,10 @@ function M.read_nm(elf_path)
|
|||||||
--- @return integer|nil
|
--- @return integer|nil
|
||||||
read_u32_at = function(offset)
|
read_u32_at = function(offset)
|
||||||
f:seek("set", offset)
|
f:seek("set", offset)
|
||||||
--- @type string|nil
|
local b1 = f:read(1) ---@type string|nil
|
||||||
local b1 = f:read(1)
|
local b2 = f:read(1) ---@type string|nil
|
||||||
--- @type string|nil
|
local b3 = f:read(1) ---@type string|nil
|
||||||
local b2 = f:read(1)
|
local b4 = f:read(1) ---@type string|nil
|
||||||
--- @type string|nil
|
|
||||||
local b3 = f:read(1)
|
|
||||||
--- @type string|nil
|
|
||||||
local b4 = f:read(1)
|
|
||||||
if not b1 or not b2 or not b3 or not b4 then return nil end
|
if not b1 or not b2 or not b3 or not b4 then return nil end
|
||||||
return b1:byte() + b2:byte() * 0x100
|
return b1:byte() + b2:byte() * 0x100
|
||||||
+ b3:byte() * 0x10000 + b4:byte() * 0x1000000
|
+ b3:byte() * 0x10000 + b4:byte() * 0x1000000
|
||||||
@@ -972,16 +901,14 @@ function M.read_nm(elf_path)
|
|||||||
}
|
}
|
||||||
|
|
||||||
-- Delegate the header + section walk to E.*.
|
-- Delegate the header + section walk to E.*.
|
||||||
--- @type Elf32Header|nil, string|nil
|
local hdr, hdr_err = E.parse_elf32_headers(adapter) ---@type Elf32Header|nil, string|nil
|
||||||
local hdr, hdr_err = E.parse_elf32_headers(adapter)
|
|
||||||
if not hdr then
|
if not hdr then
|
||||||
io.stderr:write(string.format("[elf_dwarf.read_nm] header parse failed: %s\n", tostring(hdr_err)))
|
io.stderr:write(string.format("[elf_dwarf.read_nm] header parse failed: %s\n", tostring(hdr_err)))
|
||||||
f:close()
|
f:close()
|
||||||
return addrs
|
return addrs
|
||||||
end
|
end
|
||||||
|
|
||||||
--- @type Elf32Section[]|nil, string|nil
|
local sections, walk_err = E.walk_sections(adapter, hdr) ---@type Elf32Section[]|nil, string|nil
|
||||||
local sections, walk_err = E.walk_sections(adapter, hdr)
|
|
||||||
if not sections then
|
if not sections then
|
||||||
io.stderr:write(string.format("[elf_dwarf.read_nm] section walk failed: %s\n", tostring(walk_err)))
|
io.stderr:write(string.format("[elf_dwarf.read_nm] section walk failed: %s\n", tostring(walk_err)))
|
||||||
f:close()
|
f:close()
|
||||||
@@ -990,8 +917,7 @@ function M.read_nm(elf_path)
|
|||||||
|
|
||||||
-- E.collect_symbols returns every defined symbol (no binding filter).
|
-- E.collect_symbols returns every defined symbol (no binding filter).
|
||||||
-- The metaprogram then applies its STB_LOCAL / STB_GLOBAL + size>0 filter, matching `nm`'s default (external symbols only).
|
-- The metaprogram then applies its STB_LOCAL / STB_GLOBAL + size>0 filter, matching `nm`'s default (external symbols only).
|
||||||
--- @type table<string, Elf32Sym>|nil, string|nil
|
local symbols, sym_err = E.collect_symbols(adapter, sections) ---@type table<string, Elf32Sym>|nil, string|nil
|
||||||
local symbols, sym_err = E.collect_symbols(adapter, sections)
|
|
||||||
if not symbols then
|
if not symbols then
|
||||||
io.stderr:write(string.format("[elf_dwarf.read_nm] symbol collection failed: %s\n", tostring(sym_err)))
|
io.stderr:write(string.format("[elf_dwarf.read_nm] symbol collection failed: %s\n", tostring(sym_err)))
|
||||||
f:close()
|
f:close()
|
||||||
@@ -1000,12 +926,10 @@ function M.read_nm(elf_path)
|
|||||||
|
|
||||||
f:close()
|
f:close()
|
||||||
|
|
||||||
--- @type string, Elf32Sym
|
for name, entry in pairs(symbols) do ---@type string, Elf32Sym
|
||||||
for name, entry in pairs(symbols) do
|
|
||||||
-- High nibble of st_info = binding (STB_LOCAL=0, STB_GLOBAL=1, STB_WEAK=2).
|
-- High nibble of st_info = binding (STB_LOCAL=0, STB_GLOBAL=1, STB_WEAK=2).
|
||||||
-- math.floor(/16) is portable across LuaJIT 2.0/2.1 and plain Lua 5.x.
|
-- math.floor(/16) is portable across LuaJIT 2.0/2.1 and plain Lua 5.x.
|
||||||
--- @type integer
|
local binding = math.floor(entry.info / 16) ---@type integer
|
||||||
local binding = math.floor(entry.info / 16)
|
|
||||||
if (binding == 0 or binding == 1) and entry.size > 0 then
|
if (binding == 0 or binding == 1) and entry.size > 0 then
|
||||||
addrs[name] = { entry.value, entry.size }
|
addrs[name] = { entry.value, entry.size }
|
||||||
end
|
end
|
||||||
@@ -1037,17 +961,14 @@ end
|
|||||||
-- Spec: DWARF5 §7.6 "Variable-Length Data" / Appendix C.
|
-- Spec: DWARF5 §7.6 "Variable-Length Data" / Appendix C.
|
||||||
|
|
||||||
-- Top bit of each LEB128 byte. Set if more bytes follow in the encoding.
|
-- Top bit of each LEB128 byte. Set if more bytes follow in the encoding.
|
||||||
--- @type integer
|
local LEB_CONT_BIT = 0x80 ---@type integer
|
||||||
local LEB_CONT_BIT = 0x80
|
|
||||||
|
|
||||||
-- Low 7 bits of each LEB128 byte. The actual data payload.
|
-- Low 7 bits of each LEB128 byte. The actual data payload.
|
||||||
--- @type integer
|
local LEB_DATA_MASK = 0x7F ---@type integer
|
||||||
local LEB_DATA_MASK = 0x7F
|
|
||||||
|
|
||||||
-- Bit 6 of the 7-bit data (i.e. 0x40). For SLEB128: the sign-bit position used by the decoder for sign extension.
|
-- Bit 6 of the 7-bit data (i.e. 0x40). For SLEB128: the sign-bit position used by the decoder for sign extension.
|
||||||
-- Encoders MUST stop when the next byte would be redundant AND the sign bit in the last byte matches the value's sign.
|
-- Encoders MUST stop when the next byte would be redundant AND the sign bit in the last byte matches the value's sign.
|
||||||
--- @type integer
|
local SLEB_SIGN_BIT = 0x40 ---@type integer
|
||||||
local SLEB_SIGN_BIT = 0x40
|
|
||||||
|
|
||||||
--- ULEB128 (Unsigned Little-Endian Base 128) encoder. Returns the byte string for the non-negative integer `n`.
|
--- ULEB128 (Unsigned Little-Endian Base 128) encoder. Returns the byte string for the non-negative integer `n`.
|
||||||
--- Algorithm:
|
--- Algorithm:
|
||||||
@@ -1064,11 +985,9 @@ function M.uleb128(n)
|
|||||||
error("uleb128 requires non-negative number")
|
error("uleb128 requires non-negative number")
|
||||||
end
|
end
|
||||||
assert(n >= 0, "uleb128 requires non-negative input")
|
assert(n >= 0, "uleb128 requires non-negative input")
|
||||||
--- @type string[]
|
local bytes = {} ---@type string[]
|
||||||
local bytes = {}
|
|
||||||
repeat
|
repeat
|
||||||
--- @type integer
|
local b = n % (LEB_DATA_MASK + 1) ---@type integer -- extract low 7 bits
|
||||||
local b = n % (LEB_DATA_MASK + 1) -- extract low 7 bits
|
|
||||||
n = (n - b) / (LEB_DATA_MASK + 1) -- shift right by 7 bits
|
n = (n - b) / (LEB_DATA_MASK + 1) -- shift right by 7 bits
|
||||||
if n > 0 then b = b + LEB_CONT_BIT end -- set continuation bit if more bytes follow
|
if n > 0 then b = b + LEB_CONT_BIT end -- set continuation bit if more bytes follow
|
||||||
bytes[#bytes + 1] = string.char(b)
|
bytes[#bytes + 1] = string.char(b)
|
||||||
@@ -1087,13 +1006,10 @@ end
|
|||||||
--- @param n integer -- any integer (negative allowed)
|
--- @param n integer -- any integer (negative allowed)
|
||||||
--- @return string
|
--- @return string
|
||||||
function M.sleb128(n)
|
function M.sleb128(n)
|
||||||
--- @type string[]
|
local bytes = {} ---@type string[]
|
||||||
local bytes = {}
|
local more = true ---@type boolean
|
||||||
--- @type boolean
|
|
||||||
local more = true
|
|
||||||
while more do
|
while more do
|
||||||
--- @type integer
|
local b = n % (LEB_DATA_MASK + 1) ---@type integer -- extract low 7 bits
|
||||||
local b = n % (LEB_DATA_MASK + 1) -- extract low 7 bits
|
|
||||||
n = (n - b) / (LEB_DATA_MASK + 1) -- arithmetic shift right by 7
|
n = (n - b) / (LEB_DATA_MASK + 1) -- arithmetic shift right by 7
|
||||||
-- Termination: remaining value bits fit in the sign bit of the last byte.
|
-- Termination: remaining value bits fit in the sign bit of the last byte.
|
||||||
if n == 0 and b < SLEB_SIGN_BIT then more = false end -- positive terminator
|
if n == 0 and b < SLEB_SIGN_BIT then more = false end -- positive terminator
|
||||||
@@ -1112,8 +1028,7 @@ end
|
|||||||
function M.uleb128_size(n)
|
function M.uleb128_size(n)
|
||||||
assert(n >= 0, "uleb128_size requires non-negative input")
|
assert(n >= 0, "uleb128_size requires non-negative input")
|
||||||
if n == 0 then return 1 end
|
if n == 0 then return 1 end
|
||||||
--- @type integer
|
local bytes = 1 ---@type integer
|
||||||
local bytes = 1
|
|
||||||
while n >= 0x80 do
|
while n >= 0x80 do
|
||||||
n = (n - (n % (LEB_DATA_MASK + 1))) / (LEB_DATA_MASK + 1) -- arithmetic shift right by 7
|
n = (n - (n % (LEB_DATA_MASK + 1))) / (LEB_DATA_MASK + 1) -- arithmetic shift right by 7
|
||||||
bytes = bytes + 1
|
bytes = bytes + 1
|
||||||
@@ -1129,15 +1044,11 @@ end
|
|||||||
--- @param n integer -- any integer (negative allowed)
|
--- @param n integer -- any integer (negative allowed)
|
||||||
--- @return integer
|
--- @return integer
|
||||||
function M.sleb128_size(n)
|
function M.sleb128_size(n)
|
||||||
--- @type boolean
|
local more = true ---@type boolean
|
||||||
local more = true
|
local bytes = 0 ---@type integer
|
||||||
--- @type integer
|
local v = n ---@type integer
|
||||||
local bytes = 0
|
|
||||||
--- @type integer
|
|
||||||
local v = n
|
|
||||||
while more do
|
while more do
|
||||||
--- @type integer
|
local b = v % (LEB_DATA_MASK + 1) ---@type integer -- extract low 7 bits
|
||||||
local b = v % (LEB_DATA_MASK + 1) -- extract low 7 bits
|
|
||||||
v = (v - b) / (LEB_DATA_MASK + 1) -- arithmetic shift right by 7
|
v = (v - b) / (LEB_DATA_MASK + 1) -- arithmetic shift right by 7
|
||||||
if v == 0 and b < SLEB_SIGN_BIT then more = false end -- positive terminator
|
if v == 0 and b < SLEB_SIGN_BIT then more = false end -- positive terminator
|
||||||
if v == -1 and b >= SLEB_SIGN_BIT then more = false end -- negative terminator
|
if v == -1 and b >= SLEB_SIGN_BIT then more = false end -- negative terminator
|
||||||
@@ -1179,49 +1090,39 @@ end
|
|||||||
--- @return table<integer, string>|nil
|
--- @return table<integer, string>|nil
|
||||||
--- @return table<integer, string>|nil
|
--- @return table<integer, string>|nil
|
||||||
function M.read_line_unit_file_table(elf_path)
|
function M.read_line_unit_file_table(elf_path)
|
||||||
--- @type table<string, string>
|
local sections = M.read_elf_sections(elf_path, { ".debug_line", ".debug_line_str" }) ---@type table<string, string>
|
||||||
local sections = M.read_elf_sections(elf_path, { ".debug_line", ".debug_line_str" })
|
local line = sections[".debug_line"] ---@type string
|
||||||
--- @type string
|
local lstr = sections[".debug_line_str"] or "" ---@type string
|
||||||
local line = sections[".debug_line"]
|
|
||||||
--- @type string
|
|
||||||
local lstr = sections[".debug_line_str"] or ""
|
|
||||||
if not line or line == "" then
|
if not line or line == "" then
|
||||||
io.stderr:write("[elf_dwarf.read_line_unit_file_table] no .debug_line section in: " .. tostring(elf_path) .. "\n")
|
io.stderr:write("[elf_dwarf.read_line_unit_file_table] no .debug_line section in: " .. tostring(elf_path) .. "\n")
|
||||||
return nil
|
return nil
|
||||||
end
|
end
|
||||||
|
|
||||||
--- @type table<integer, string> -- bag: 1-based file index -> basename
|
local basenames = {} ---@type table<integer, string> -- bag: 1-based file index -> basename
|
||||||
local basenames = {}
|
local basename_to_index = {} ---@type table<string, integer> -- bag: basename -> 1-based file index
|
||||||
--- @type table<string, integer> -- bag: basename -> 1-based file index
|
local paths = {} ---@type table<integer, string> -- bag: 1-based file index -> full path
|
||||||
local basename_to_index = {}
|
|
||||||
--- @type table<integer, string> -- bag: 1-based file index -> full path
|
|
||||||
local paths = {}
|
|
||||||
|
|
||||||
--- Read one form-code's bytes from `buf` at position `p` according to `form`.
|
--- Read one form-code's bytes from `buf` at position `p` according to `form`.
|
||||||
--- Returns (value, after) where `value` is:
|
--- Returns (value, after) where `value` is:
|
||||||
--- * the resolved string (DW_FORM_line_strp / DW_FORM_string)
|
--- * the resolved string (DW_FORM_line_strp / DW_FORM_string)
|
||||||
--- * the ULEB128 number (DW_FORM_udata)
|
--- * the ULEB128 number (DW_FORM_udata)
|
||||||
--- * nil + skip-bytes (DW_FORM_data16; we don't surface the MD5)
|
--- * nil + skip-bytes (DW_FORM_data16; we don't surface the MD5)
|
||||||
--- @param buf string
|
--- @param buf string
|
||||||
--- @param lstr_buf string
|
--- @param lstr_buf string
|
||||||
--- @param p integer
|
--- @param p integer
|
||||||
--- @param form integer
|
--- @param form integer
|
||||||
--- @return string|integer|nil
|
--- @return string|integer|nil
|
||||||
--- @return integer
|
--- @return integer
|
||||||
local function read_form(buf, lstr_buf, p, form)
|
local function read_form(buf, lstr_buf, p, form)
|
||||||
if form == M.DWARF5_DEBUG_LINE.form_line_strp then
|
if form == M.DWARF5_DEBUG_LINE.form_line_strp then
|
||||||
--- @type integer
|
local strp = M.read_u32_le(buf, p) ---@type integer
|
||||||
local strp = M.read_u32_le(buf, p)
|
local end_pos = lstr_buf:find("\0", strp + 1, true) or (#lstr_buf + 1) ---@type integer
|
||||||
--- @type integer
|
|
||||||
local end_pos = lstr_buf:find("\0", strp + 1, true) or (#lstr_buf + 1)
|
|
||||||
return lstr_buf:sub(strp + 1, end_pos - 1), p + M.DWARF5_DEBUG_LINE.form_strp_bytes
|
return lstr_buf:sub(strp + 1, end_pos - 1), p + M.DWARF5_DEBUG_LINE.form_strp_bytes
|
||||||
elseif form == M.DWARF5_DEBUG_LINE.form_string then
|
elseif form == M.DWARF5_DEBUG_LINE.form_string then
|
||||||
--- @type integer
|
local nul = buf:find("\0", p + 1, true) or (#buf + 1) ---@type integer
|
||||||
local nul = buf:find("\0", p + 1, true) or (#buf + 1)
|
|
||||||
return buf:sub(p + 1, nul - 1), nul
|
return buf:sub(p + 1, nul - 1), nul
|
||||||
elseif form == M.DWARF5_DEBUG_LINE.form_udata then
|
elseif form == M.DWARF5_DEBUG_LINE.form_udata then
|
||||||
--- @type integer|nil, integer
|
local v, after = M.read_uleb128_at(buf, p) ---@type integer|nil, integer
|
||||||
local v, after = M.read_uleb128_at(buf, p)
|
|
||||||
return v, after
|
return v, after
|
||||||
elseif form == M.DWARF5_DEBUG_LINE.form_data16 then
|
elseif form == M.DWARF5_DEBUG_LINE.form_data16 then
|
||||||
return nil, p + M.DWARF5_DEBUG_LINE.form_data16_bytes
|
return nil, p + M.DWARF5_DEBUG_LINE.form_data16_bytes
|
||||||
@@ -1237,52 +1138,38 @@ function M.read_line_unit_file_table(elf_path)
|
|||||||
--- Parse one DWARF-version-3-style unit (DWARF3/4 line program; gcc default in the PS1 toolchain still emits DWARF3 for line programs in `-g` mode).
|
--- Parse one DWARF-version-3-style unit (DWARF3/4 line program; gcc default in the PS1 toolchain still emits DWARF3 for line programs in `-g` mode).
|
||||||
--- Layout: null-terminated directory list, then path(null) + dir_idx(ULEB) + time(ULEB) + size(ULEB) file entries terminated by an empty null.
|
--- Layout: null-terminated directory list, then path(null) + dir_idx(ULEB) + time(ULEB) + size(ULEB) file entries terminated by an empty null.
|
||||||
--- `content_start` = zero-based wire offset of the first byte of program-header content (after version + header_length fields).
|
--- `content_start` = zero-based wire offset of the first byte of program-header content (after version + header_length fields).
|
||||||
--- @param buf string
|
--- @param buf string
|
||||||
--- @param content_start integer
|
--- @param content_start integer
|
||||||
--- @param body_end integer
|
--- @param body_end integer
|
||||||
--- @return table<integer, string>
|
--- @return table<integer, string>
|
||||||
--- @return table<integer, string>
|
--- @return table<integer, string>
|
||||||
local function parse_dwarf3_unit(buf, content_start, body_end)
|
local function parse_dwarf3_unit(buf, content_start, body_end)
|
||||||
--- @type integer
|
local up = content_start ---@type integer
|
||||||
local up = content_start
|
|
||||||
-- 5 fixed bytes: min_insn, default_is, line_base (signed), line_range, opcode_base
|
-- 5 fixed bytes: min_insn, default_is, line_base (signed), line_range, opcode_base
|
||||||
up = up + 5
|
up = up + 5
|
||||||
--- @type integer
|
local opcode_base = buf:byte(content_start + 5) ---@type integer
|
||||||
local opcode_base = buf:byte(content_start + 5)
|
|
||||||
up = up + (opcode_base - 1) -- std_opcode_lengths
|
up = up + (opcode_base - 1) -- std_opcode_lengths
|
||||||
--- @type string[]
|
local dirs = {} ---@type string[]
|
||||||
local dirs = {}
|
|
||||||
while up < body_end do
|
while up < body_end do
|
||||||
--- @type integer
|
local nul = buf:find("\0", up + 1, true) or (body_end + 1) ---@type integer
|
||||||
local nul = buf:find("\0", up + 1, true) or (body_end + 1)
|
|
||||||
if nul > body_end then break end
|
if nul > body_end then break end
|
||||||
--- @type integer
|
local len = nul - up - 1 ---@type integer
|
||||||
local len = nul - up - 1
|
|
||||||
if len == 0 then up = nul break end
|
if len == 0 then up = nul break end
|
||||||
dirs[#dirs + 1] = buf:sub(up + 1, nul - 1)
|
dirs[#dirs + 1] = buf:sub(up + 1, nul - 1)
|
||||||
up = nul
|
up = nul
|
||||||
end
|
end
|
||||||
--- @type table<integer, string> -- bag: 1-based unit file index -> basename
|
local unit_basenames = {} ---@type table<integer, string> -- bag: 1-based unit file index -> basename
|
||||||
local unit_basenames = {}
|
local unit_paths = {} ---@type table<integer, string> -- bag: 1-based unit file index -> full path
|
||||||
--- @type table<integer, string> -- bag: 1-based unit file index -> full path
|
|
||||||
local unit_paths = {}
|
|
||||||
while up < body_end do
|
while up < body_end do
|
||||||
--- @type integer
|
local nul = buf:find("\0", up + 1, true) or (body_end + 1) ---@type integer
|
||||||
local nul = buf:find("\0", up + 1, true) or (body_end + 1)
|
|
||||||
if nul > body_end or nul == up + 1 then up = nul break end
|
if nul > body_end or nul == up + 1 then up = nul break end
|
||||||
--- @type string
|
local path = buf:sub(up + 1, nul - 1) ---@type string
|
||||||
local path = buf:sub(up + 1, nul - 1)
|
|
||||||
up = nul
|
up = nul
|
||||||
--- @type integer|nil, integer
|
local didx, up_next = M.read_uleb128_at(buf, up); up = up_next ---@type integer|nil, integer
|
||||||
local didx, up_next = M.read_uleb128_at(buf, up); up = up_next
|
local _time, up_next2 = M.read_uleb128_at(buf, up); up = up_next2 ---@type integer|nil, integer
|
||||||
--- @type integer|nil, integer
|
local _size, up_next3 = M.read_uleb128_at(buf, up); up = up_next3 ---@type integer|nil, integer
|
||||||
local _time, up_next2 = M.read_uleb128_at(buf, up); up = up_next2
|
local idx = #unit_basenames + 1 ---@type integer
|
||||||
--- @type integer|nil, integer
|
local bs = path:match("[^/\\]+$") or path ---@type string
|
||||||
local _size, up_next3 = M.read_uleb128_at(buf, up); up = up_next3
|
|
||||||
--- @type integer
|
|
||||||
local idx = #unit_basenames + 1
|
|
||||||
--- @type string
|
|
||||||
local bs = path:match("[^/\\]+$") or path
|
|
||||||
unit_paths[idx] = path
|
unit_paths[idx] = path
|
||||||
unit_basenames[idx] = bs
|
unit_basenames[idx] = bs
|
||||||
dirs[1] = dirs[1] or "" -- safety: gcc emits "" sentinel dir at 0
|
dirs[1] = dirs[1] or "" -- safety: gcc emits "" sentinel dir at 0
|
||||||
@@ -1295,83 +1182,57 @@ function M.read_line_unit_file_table(elf_path)
|
|||||||
|
|
||||||
--- Parse one DWARF-version-5-style unit (DWARF5 line program; used by modern gcc with `-gdwarf-5`).
|
--- Parse one DWARF-version-5-style unit (DWARF5 line program; used by modern gcc with `-gdwarf-5`).
|
||||||
--- `content_start` is the first byte of program-header content (after the 8 fixed bytes version+addr_size+seg_size+header_length).
|
--- `content_start` is the first byte of program-header content (after the 8 fixed bytes version+addr_size+seg_size+header_length).
|
||||||
--- @param buf string
|
--- @param buf string
|
||||||
--- @param lstr_buf string
|
--- @param lstr_buf string
|
||||||
--- @param content_start integer
|
--- @param content_start integer
|
||||||
--- @param body_end integer
|
--- @param body_end integer
|
||||||
--- @return table<integer, string>
|
--- @return table<integer, string>
|
||||||
--- @return table<integer, string>
|
--- @return table<integer, string>
|
||||||
local function parse_dwarf5_unit(buf, lstr_buf, content_start, body_end)
|
local function parse_dwarf5_unit(buf, lstr_buf, content_start, body_end)
|
||||||
--- @type integer
|
local up = content_start ---@type integer
|
||||||
local up = content_start
|
|
||||||
-- 6 fixed bytes: min_insn, max_ops_per_insn, default_is, line_base, line_range, opcode_base
|
-- 6 fixed bytes: min_insn, max_ops_per_insn, default_is, line_base, line_range, opcode_base
|
||||||
up = up + 6
|
up = up + 6
|
||||||
--- @type integer
|
local opcode_base = buf:byte(content_start + 6) ---@type integer
|
||||||
local opcode_base = buf:byte(content_start + 6)
|
|
||||||
up = up + (opcode_base - 1) -- std_opcode_lengths
|
up = up + (opcode_base - 1) -- std_opcode_lengths
|
||||||
-- directories
|
-- directories
|
||||||
--- @type integer|nil, integer
|
local dir_format_count, after = M.read_uleb128_at(buf, up); up = after ---@type integer|nil, integer
|
||||||
local dir_format_count, after = M.read_uleb128_at(buf, up); up = after
|
local dir_formats = {} ---@type integer[]
|
||||||
--- @type integer[]
|
for i = 1, dir_format_count do ---@type integer
|
||||||
local dir_formats = {}
|
local f, a2 = M.read_uleb128_at(buf, up); up = a2 ---@type integer|nil, integer
|
||||||
--- @type integer
|
|
||||||
for i = 1, dir_format_count do
|
|
||||||
--- @type integer|nil, integer
|
|
||||||
local f, a2 = M.read_uleb128_at(buf, up); up = a2
|
|
||||||
dir_formats[i] = f
|
dir_formats[i] = f
|
||||||
end
|
end
|
||||||
--- @type integer|nil, integer
|
local dir_count, a3 = M.read_uleb128_at(buf, up); up = a3 ---@type integer|nil, integer
|
||||||
local dir_count, a3 = M.read_uleb128_at(buf, up); up = a3
|
local dirs = {} ---@type string[]
|
||||||
--- @type string[]
|
for i = 1, dir_count do ---@type integer
|
||||||
local dirs = {}
|
local combined = "" ---@type string
|
||||||
--- @type integer
|
for j = 1, dir_format_count do ---@type integer
|
||||||
for i = 1, dir_count do
|
local v, a4 = read_form(buf, lstr_buf, up, dir_formats[j]) ---@type string|integer|nil, integer
|
||||||
--- @type string
|
|
||||||
local combined = ""
|
|
||||||
--- @type integer
|
|
||||||
for j = 1, dir_format_count do
|
|
||||||
--- @type string|integer|nil, integer
|
|
||||||
local v, a4 = read_form(buf, lstr_buf, up, dir_formats[j])
|
|
||||||
up = a4
|
up = a4
|
||||||
if j == 1 and type(v) == "string" then combined = v end
|
if j == 1 and type(v) == "string" then combined = v end
|
||||||
end
|
end
|
||||||
dirs[i] = combined
|
dirs[i] = combined
|
||||||
end
|
end
|
||||||
-- file names
|
-- file names
|
||||||
--- @type integer|nil, integer
|
local file_format_count, after2 = M.read_uleb128_at(buf, up); up = after2 ---@type integer|nil, integer
|
||||||
local file_format_count, after2 = M.read_uleb128_at(buf, up); up = after2
|
local file_formats = {} ---@type integer[]
|
||||||
--- @type integer[]
|
for i = 1, file_format_count do ---@type integer
|
||||||
local file_formats = {}
|
local f, a2 = M.read_uleb128_at(buf, up); up = a2 ---@type integer|nil, integer
|
||||||
--- @type integer
|
|
||||||
for i = 1, file_format_count do
|
|
||||||
--- @type integer|nil, integer
|
|
||||||
local f, a2 = M.read_uleb128_at(buf, up); up = a2
|
|
||||||
file_formats[i] = f
|
file_formats[i] = f
|
||||||
end
|
end
|
||||||
--- @type integer|nil, integer
|
local file_count, a3 = M.read_uleb128_at(buf, up); up = a3 ---@type integer|nil, integer
|
||||||
local file_count, a3 = M.read_uleb128_at(buf, up); up = a3
|
local unit_basenames = {} ---@type table<integer, string> -- bag: 1-based unit file index -> basename
|
||||||
--- @type table<integer, string> -- bag: 1-based unit file index -> basename
|
local unit_paths = {} ---@type table<integer, string> -- bag: 1-based unit file index -> full path
|
||||||
local unit_basenames = {}
|
for i = 1, file_count do ---@type integer
|
||||||
--- @type table<integer, string> -- bag: 1-based unit file index -> full path
|
local combined = "" ---@type string
|
||||||
local unit_paths = {}
|
local didx = 0 ---@type integer
|
||||||
--- @type integer
|
for j = 1, file_format_count do ---@type integer
|
||||||
for i = 1, file_count do
|
local v, a4 = read_form(buf, lstr_buf, up, file_formats[j]) ---@type string|integer|nil, integer
|
||||||
--- @type string
|
|
||||||
local combined = ""
|
|
||||||
--- @type integer
|
|
||||||
local didx = 0
|
|
||||||
--- @type integer
|
|
||||||
for j = 1, file_format_count do
|
|
||||||
--- @type string|integer|nil, integer
|
|
||||||
local v, a4 = read_form(buf, lstr_buf, up, file_formats[j])
|
|
||||||
up = a4
|
up = a4
|
||||||
if j == 1 and type(v) == "string" then combined = v end
|
if j == 1 and type(v) == "string" then combined = v end
|
||||||
if j == 2 and type(v) == "number" then didx = v end
|
if j == 2 and type(v) == "number" then didx = v end
|
||||||
end
|
end
|
||||||
--- @type integer
|
local idx = #unit_basenames + 1 ---@type integer
|
||||||
local idx = #unit_basenames + 1
|
local bs = combined:match("[^/\\]+$") or combined ---@type string
|
||||||
--- @type string
|
|
||||||
local bs = combined:match("[^/\\]+$") or combined
|
|
||||||
unit_paths[idx] = combined
|
unit_paths[idx] = combined
|
||||||
unit_basenames[idx] = bs
|
unit_basenames[idx] = bs
|
||||||
if didx > 0 and dirs[didx] then
|
if didx > 0 and dirs[didx] then
|
||||||
@@ -1382,37 +1243,27 @@ function M.read_line_unit_file_table(elf_path)
|
|||||||
end
|
end
|
||||||
|
|
||||||
--- Walk every line-program unit in the section.
|
--- Walk every line-program unit in the section.
|
||||||
--- @type integer
|
local p = 0 ---@type integer
|
||||||
local p = 0
|
local section_end = #line ---@type integer
|
||||||
--- @type integer
|
|
||||||
local section_end = #line
|
|
||||||
while p + 4 <= section_end do
|
while p + 4 <= section_end do
|
||||||
--- @type integer
|
local unit_length = M.read_u32_le(line, p) ---@type integer
|
||||||
local unit_length = M.read_u32_le(line, p)
|
|
||||||
if unit_length == 0xFFFFFFFF then
|
if unit_length == 0xFFFFFFFF then
|
||||||
io.stderr:write("[elf_dwarf.read_line_unit_file_table] 64-bit DWARF (initial-length 0xFFFFFFFF); not supported\n")
|
io.stderr:write("[elf_dwarf.read_line_unit_file_table] 64-bit DWARF (initial-length 0xFFFFFFFF); not supported\n")
|
||||||
return nil
|
return nil
|
||||||
end
|
end
|
||||||
--- @type integer
|
local body_start = p + 4 ---@type integer
|
||||||
local body_start = p + 4
|
local body_end = p + 4 + unit_length ---@type integer
|
||||||
--- @type integer
|
|
||||||
local body_end = p + 4 + unit_length
|
|
||||||
if body_end > section_end then break end
|
if body_end > section_end then break end
|
||||||
--- @type integer
|
local version = M.read_u16_le(line, body_start) ---@type integer
|
||||||
local version = M.read_u16_le(line, body_start)
|
local unit_basenames, unit_paths ---@type table<integer, string>|nil, table<integer, string>|nil
|
||||||
--- @type table<integer, string>|nil, table<integer, string>|nil
|
|
||||||
local unit_basenames, unit_paths
|
|
||||||
if version >= 5 then
|
if version >= 5 then
|
||||||
-- DWARF5 header: version(2) + addr_size(1) + seg_size(1) + header_length(4) + content
|
-- DWARF5 header: version(2) + addr_size(1) + seg_size(1) + header_length(4) + content
|
||||||
--- @type integer
|
local header_length_offset = body_start + 6 ---@type integer -- past version(2) + addr_size(1) + seg_size(1) - wait that's wrong; past hdr len is at +6
|
||||||
local header_length_offset = body_start + 6 -- past version(2) + addr_size(1) + seg_size(1) - wait that's wrong; past hdr len is at +6
|
local content_start = body_start + 8 ---@type integer -- past version(2) + addr_size(1) + seg_size(1) + header_length(4)
|
||||||
--- @type integer
|
|
||||||
local content_start = body_start + 8 -- past version(2) + addr_size(1) + seg_size(1) + header_length(4)
|
|
||||||
unit_basenames, unit_paths = parse_dwarf5_unit(line, lstr, content_start, body_end)
|
unit_basenames, unit_paths = parse_dwarf5_unit(line, lstr, content_start, body_end)
|
||||||
elseif version >= 2 then
|
elseif version >= 2 then
|
||||||
-- DWARF2/3/4 header: version(2) + header_length(4) + content
|
-- DWARF2/3/4 header: version(2) + header_length(4) + content
|
||||||
--- @type integer
|
local content_start = body_start + 6 ---@type integer -- past version(2) + header_length(4)
|
||||||
local content_start = body_start + 6 -- past version(2) + header_length(4)
|
|
||||||
unit_basenames, unit_paths = parse_dwarf3_unit(line, content_start, body_end)
|
unit_basenames, unit_paths = parse_dwarf3_unit(line, content_start, body_end)
|
||||||
else
|
else
|
||||||
io.stderr:write(string.format("[elf_dwarf.read_line_unit_file_table] unsupported DWARF version %d (offset 0x%x)\n", version, p))
|
io.stderr:write(string.format("[elf_dwarf.read_line_unit_file_table] unsupported DWARF version %d (offset 0x%x)\n", version, p))
|
||||||
@@ -1426,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;
|
-- For DWARF5 (crt0.s + C unit), each carries its own per-unit file-table map;
|
||||||
-- the atom-side DW_LNS_set_file(N) refers to the C unit's indices, NOT crt0.s's.
|
-- the atom-side DW_LNS_set_file(N) refers to the C unit's indices, NOT crt0.s's.
|
||||||
-- Since the C unit is the one with full include_directories + 12 entries, we can use it directly.
|
-- Since the C unit is the one with full include_directories + 12 entries, we can use it directly.
|
||||||
--- @type integer, string
|
for idx, bs in pairs(unit_basenames) do ---@type integer, string
|
||||||
for idx, bs in pairs(unit_basenames) do
|
basenames[idx] = bs
|
||||||
basenames[idx] = bs
|
paths [idx] = unit_paths[idx]
|
||||||
paths[idx] = unit_paths[idx]
|
|
||||||
basename_to_index[bs] = idx
|
basename_to_index[bs] = idx
|
||||||
end
|
end
|
||||||
p = body_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.
|
-- Bootstrap follows the entry scripts; `scripts/duffle_paths.lua` sets package.path and package.cpath. See `ps1_meta.lua` for the rationale.
|
||||||
-- `debug.getinfo(1, "S").source` locates this file for standalone and orchestrated runs, then `duffle_paths.lua` returns the loaded `duffle` module.
|
-- `debug.getinfo(1, "S").source` locates this file for standalone and orchestrated runs, then `duffle_paths.lua` returns the loaded `duffle` module.
|
||||||
--- @type string
|
local _bootstrap_dir = debug.getinfo(1, "S").source:match("^@?(.*[/\\])") or "./" ---@type string
|
||||||
local _bootstrap_dir = debug.getinfo(1, "S").source:match("^@?(.*[/\\])") or "./"
|
local duffle = dofile(_bootstrap_dir .. "../duffle_paths.lua") ---@type DuffleExport
|
||||||
--- @type DuffleExport
|
|
||||||
local duffle = dofile(_bootstrap_dir .. "../duffle_paths.lua")
|
|
||||||
|
|
||||||
-- The annotation pass reads the source-derived registries from scan_source:
|
-- The annotation pass reads the source-derived registries from scan_source:
|
||||||
-- * pipe_ctx.register_alias_registry — for atom_dbg_reg_default(R_X, ...) and atom_reg_types(R_X, ...) member-identity checks
|
-- * pipe_ctx.register_alias_registry — for atom_dbg_reg_default(R_X, ...) and atom_reg_types(R_X, ...) member-identity checks
|
||||||
@@ -107,8 +105,7 @@ end
|
|||||||
--- @param findings Findings
|
--- @param findings Findings
|
||||||
--- @return nil
|
--- @return nil
|
||||||
local function check_unique_annotation(_item, pipe_ctx, findings)
|
local function check_unique_annotation(_item, pipe_ctx, findings)
|
||||||
--- @type string, integer
|
for name, n in pairs(pipe_ctx.annot_counts) do ---@type string, integer
|
||||||
for name, n in pairs(pipe_ctx.annot_counts) do
|
|
||||||
if n > 1 then
|
if n > 1 then
|
||||||
findings.errors[#findings.errors + 1] = {
|
findings.errors[#findings.errors + 1] = {
|
||||||
line = pipe_ctx.atom_index[name] and pipe_ctx.atom_index[name].line or 0,
|
line = pipe_ctx.atom_index[name] and pipe_ctx.atom_index[name].line or 0,
|
||||||
@@ -142,10 +139,8 @@ end
|
|||||||
--- @param findings Findings
|
--- @param findings Findings
|
||||||
--- @return nil
|
--- @return nil
|
||||||
local function check_macro_word_drift(m, pipe_ctx, findings)
|
local function check_macro_word_drift(m, pipe_ctx, findings)
|
||||||
--- @type WordCounts
|
local wc = (pipe_ctx and pipe_ctx.word_counts) or {} ---@type WordCounts
|
||||||
local wc = (pipe_ctx and pipe_ctx.word_counts) or {}
|
local declared = wc[m.name] ---@type integer|nil
|
||||||
--- @type integer|nil
|
|
||||||
local declared = wc[m.name]
|
|
||||||
if not declared then
|
if not declared then
|
||||||
findings.errors[#findings.errors + 1] = {
|
findings.errors[#findings.errors + 1] = {
|
||||||
line = m.line,
|
line = m.line,
|
||||||
@@ -174,10 +169,8 @@ end
|
|||||||
--- @return nil
|
--- @return nil
|
||||||
local function check_semantic_reg_defaults(_src, pipe_ctx, findings)
|
local function check_semantic_reg_defaults(_src, pipe_ctx, findings)
|
||||||
-- Detect duplicate defaults using the ordered occurrence list (the out.types hash only retains the last declaration).
|
-- Detect duplicate defaults using the ordered occurrence list (the out.types hash only retains the last declaration).
|
||||||
--- @type table<string, integer> -- bag: register ident -> first source line
|
local seen_first_line = {} ---@type table<string, integer> -- bag: register ident -> first source line
|
||||||
local seen_first_line = {}
|
for _, occ in ipairs(pipe_ctx.type_occurrences or {}) do ---@type integer, RegTypeOccurrence
|
||||||
--- @type integer, RegTypeOccurrence
|
|
||||||
for _, occ in ipairs(pipe_ctx.type_occurrences or {}) do
|
|
||||||
if seen_first_line[occ.reg] == nil then
|
if seen_first_line[occ.reg] == nil then
|
||||||
seen_first_line[occ.reg] = occ.source_line
|
seen_first_line[occ.reg] = occ.source_line
|
||||||
else
|
else
|
||||||
@@ -189,12 +182,9 @@ local function check_semantic_reg_defaults(_src, pipe_ctx, findings)
|
|||||||
}
|
}
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
--- @type table<string, AliasEntry>
|
local reg_registry = pipe_ctx.register_alias_registry or {} ---@type table<string, AliasEntry>
|
||||||
local reg_registry = pipe_ctx.register_alias_registry or {}
|
local type_registry = pipe_ctx.type_name_registry or {} ---@type table<string, TypeNameEntry>
|
||||||
--- @type table<string, TypeNameEntry>
|
for reg, def in pairs(pipe_ctx.types or {}) do ---@type string, RegTypeDefault
|
||||||
local type_registry = pipe_ctx.type_name_registry or {}
|
|
||||||
--- @type string, RegTypeDefault
|
|
||||||
for reg, def in pairs(pipe_ctx.types or {}) do
|
|
||||||
if not reg_registry[reg] then
|
if not reg_registry[reg] then
|
||||||
findings.errors[#findings.errors + 1] = {
|
findings.errors[#findings.errors + 1] = {
|
||||||
line = def.source_line,
|
line = def.source_line,
|
||||||
@@ -229,15 +219,11 @@ end
|
|||||||
--- @param findings Findings
|
--- @param findings Findings
|
||||||
--- @return nil
|
--- @return nil
|
||||||
local function check_atom_reg_types(_src, pipe_ctx, findings)
|
local function check_atom_reg_types(_src, pipe_ctx, findings)
|
||||||
--- @type table<string, AliasEntry>
|
local reg_registry = pipe_ctx.register_alias_registry or {} ---@type table<string, AliasEntry>
|
||||||
local reg_registry = pipe_ctx.register_alias_registry or {}
|
local type_registry = pipe_ctx.type_name_registry or {} ---@type table<string, TypeNameEntry>
|
||||||
--- @type table<string, TypeNameEntry>
|
for _, ai in ipairs(pipe_ctx.atom_infos_list or {}) do ---@type integer, AtomInfoEntry
|
||||||
local type_registry = pipe_ctx.type_name_registry or {}
|
|
||||||
--- @type integer, AtomInfoEntry
|
|
||||||
for _, ai in ipairs(pipe_ctx.atom_infos_list or {}) do
|
|
||||||
if ai.reg_type_overrides then
|
if ai.reg_type_overrides then
|
||||||
--- @type string, RegTypeOverride
|
for reg, ov in pairs(ai.reg_type_overrides) do ---@type string, RegTypeOverride
|
||||||
for reg, ov in pairs(ai.reg_type_overrides) do
|
|
||||||
if not reg_registry[reg] then
|
if not reg_registry[reg] then
|
||||||
findings.errors[#findings.errors + 1] = {
|
findings.errors[#findings.errors + 1] = {
|
||||||
line = ai.info_line,
|
line = ai.info_line,
|
||||||
@@ -265,13 +251,11 @@ end
|
|||||||
--- @param findings Findings
|
--- @param findings Findings
|
||||||
--- @return nil
|
--- @return nil
|
||||||
local function check_atom_view_layout(_src, pipe_ctx, findings)
|
local function check_atom_view_layout(_src, pipe_ctx, findings)
|
||||||
--- @type string, AtomViewEntry
|
for atom_name, view in pairs(pipe_ctx.atom_views or {}) do ---@type string, AtomViewEntry
|
||||||
for atom_name, view in pairs(pipe_ctx.atom_views or {}) do
|
|
||||||
if not view.binds_name then
|
if not view.binds_name then
|
||||||
-- The atom had atom_reg_types but no atom_view; no layout check needed.
|
-- The atom had atom_reg_types but no atom_view; no layout check needed.
|
||||||
else
|
else
|
||||||
--- @type BindsEntry|nil
|
local bs = pipe_ctx.binds_index[view.binds_name] ---@type BindsEntry|nil
|
||||||
local bs = pipe_ctx.binds_index[view.binds_name]
|
|
||||||
if not bs then
|
if not bs then
|
||||||
findings.errors[#findings.errors + 1] = {
|
findings.errors[#findings.errors + 1] = {
|
||||||
line = view.info_line,
|
line = view.info_line,
|
||||||
@@ -297,16 +281,12 @@ end
|
|||||||
--- @param findings Findings
|
--- @param findings Findings
|
||||||
--- @return nil
|
--- @return nil
|
||||||
local function check_binds_no_duplicate_fields(_src, pipe_ctx, findings)
|
local function check_binds_no_duplicate_fields(_src, pipe_ctx, findings)
|
||||||
--- @type integer, BindsEntry
|
for _, bs in ipairs(pipe_ctx.binds_list or {}) do ---@type integer, BindsEntry
|
||||||
for _, bs in ipairs(pipe_ctx.binds_list or {}) do
|
local seen = {} ---@type table<string, integer> -- bag: field name -> occurrence count
|
||||||
--- @type table<string, integer> -- bag: field name -> occurrence count
|
for _, f in ipairs(bs.fields or {}) do ---@type integer, TypeField
|
||||||
local seen = {}
|
|
||||||
--- @type integer, TypeField
|
|
||||||
for _, f in ipairs(bs.fields or {}) do
|
|
||||||
seen[f.name] = (seen[f.name] or 0) + 1
|
seen[f.name] = (seen[f.name] or 0) + 1
|
||||||
end
|
end
|
||||||
--- @type string, integer
|
for name, count in pairs(seen) do ---@type string, integer
|
||||||
for name, count in pairs(seen) do
|
|
||||||
if count > 1 then
|
if count > 1 then
|
||||||
findings.errors[#findings.errors + 1] = {
|
findings.errors[#findings.errors + 1] = {
|
||||||
line = bs.line,
|
line = bs.line,
|
||||||
@@ -334,10 +314,8 @@ end
|
|||||||
--- @param findings Findings
|
--- @param findings Findings
|
||||||
--- @return nil
|
--- @return nil
|
||||||
local function check_skip_marker(marker, _pipe_ctx, findings)
|
local function check_skip_marker(marker, _pipe_ctx, findings)
|
||||||
--- @type string
|
local kind = marker.marker_kind ---@type string
|
||||||
local kind = marker.marker_kind
|
local line = marker.marker_line ---@type integer
|
||||||
--- @type integer
|
|
||||||
local line = marker.marker_line
|
|
||||||
-- Left `scan.debug_skip_markers` with production records for `atom_dbg_skip` only; other identifiers take the walker's unrelated branch.
|
-- Left `scan.debug_skip_markers` with production records for `atom_dbg_skip` only; other identifiers take the walker's unrelated branch.
|
||||||
|
|
||||||
if marker.has_parens then
|
if marker.has_parens then
|
||||||
@@ -396,13 +374,10 @@ end
|
|||||||
local function check_wave_context_migration(_src, pipe_ctx, findings)
|
local function check_wave_context_migration(_src, pipe_ctx, findings)
|
||||||
if not (pipe_ctx.types and next(pipe_ctx.types)) then return end
|
if not (pipe_ctx.types and next(pipe_ctx.types)) then return end
|
||||||
if not (pipe_ctx.atom_infos_list) then return end
|
if not (pipe_ctx.atom_infos_list) then return end
|
||||||
--- @type table<string, AliasEntry>
|
local reg_registry = pipe_ctx.register_alias_registry or {} ---@type table<string, AliasEntry>
|
||||||
local reg_registry = pipe_ctx.register_alias_registry or {}
|
for _, ai in ipairs(pipe_ctx.atom_infos_list) do ---@type integer, AtomInfoEntry
|
||||||
--- @type integer, AtomInfoEntry
|
|
||||||
for _, ai in ipairs(pipe_ctx.atom_infos_list) do
|
|
||||||
if ai.reg_type_overrides then
|
if ai.reg_type_overrides then
|
||||||
--- @type string, RegTypeOverride
|
for reg, _ in pairs(ai.reg_type_overrides) do ---@type string, RegTypeOverride
|
||||||
for reg, _ in pairs(ai.reg_type_overrides) do
|
|
||||||
if not reg_registry[reg] then
|
if not reg_registry[reg] then
|
||||||
findings.warnings[#findings.warnings + 1] = {
|
findings.warnings[#findings.warnings + 1] = {
|
||||||
line = 0,
|
line = 0,
|
||||||
@@ -429,8 +404,7 @@ end
|
|||||||
--
|
--
|
||||||
-- Adding a new check = 1 row here + 1 function above. The `validate()` dispatch loop never needs editing.
|
-- Adding a new check = 1 row here + 1 function above. The `validate()` dispatch loop never needs editing.
|
||||||
|
|
||||||
--- @type CheckRule[]
|
local CHECK_RULES = { ---@type CheckRule[]
|
||||||
local CHECK_RULES = {
|
|
||||||
{ name = "atom_decl_exists", per_annot = check_atom_decl_exists },
|
{ name = "atom_decl_exists", per_annot = check_atom_decl_exists },
|
||||||
{ name = "binds_struct_exists", per_annot = check_binds_struct_exists },
|
{ name = "binds_struct_exists", per_annot = check_binds_struct_exists },
|
||||||
{ name = "unique_annotation", post = check_unique_annotation },
|
{ name = "unique_annotation", post = check_unique_annotation },
|
||||||
@@ -453,12 +427,9 @@ local CHECK_RULES = {
|
|||||||
--- @param ctx PassCtx
|
--- @param ctx PassCtx
|
||||||
--- @return PipeCtx
|
--- @return PipeCtx
|
||||||
local function build_corpus_pipe_ctx(ctx)
|
local function build_corpus_pipe_ctx(ctx)
|
||||||
--- @type PipeCtx
|
local view = duffle.corpus_view(ctx) ---@type PipeCtx
|
||||||
local view = duffle.corpus_view(ctx)
|
local annot_counts = {} ---@type table<string, integer> -- bag: atom name -> annotation count
|
||||||
--- @type table<string, integer> -- bag: atom name -> annotation count
|
for _, info in ipairs(view.atom_infos) do ---@type integer, AtomInfoEntry
|
||||||
local annot_counts = {}
|
|
||||||
--- @type integer, AtomInfoEntry
|
|
||||||
for _, info in ipairs(view.atom_infos) do
|
|
||||||
if info and info.atom_name then
|
if info and info.atom_name then
|
||||||
annot_counts[info.atom_name] = (annot_counts[info.atom_name] or 0) + 1
|
annot_counts[info.atom_name] = (annot_counts[info.atom_name] or 0) + 1
|
||||||
end
|
end
|
||||||
@@ -476,17 +447,13 @@ end
|
|||||||
--- @return AnnotatedResult
|
--- @return AnnotatedResult
|
||||||
local function validate(ctx, src, corpus_pipe_ctx)
|
local function validate(ctx, src, corpus_pipe_ctx)
|
||||||
corpus_pipe_ctx = corpus_pipe_ctx or build_corpus_pipe_ctx(ctx)
|
corpus_pipe_ctx = corpus_pipe_ctx or build_corpus_pipe_ctx(ctx)
|
||||||
--- @type SourceScan
|
local scan = src.scan ---@type SourceScan
|
||||||
local scan = src.scan
|
|
||||||
|
|
||||||
-- Build a per-source pipe_ctx: shared lookups come from `corpus_pipe_ctx`, while declarations, bodies, types, views, defaults, and occurrences come from `src.scan`.
|
-- Build a per-source pipe_ctx: shared lookups come from `corpus_pipe_ctx`, while declarations, bodies, types, views, defaults, and occurrences come from `src.scan`.
|
||||||
--- @type table<string, integer> -- bag: register ident -> occurrence count
|
local seen_defaults = {}; for reg, _ in pairs (scan.types or {}) do seen_defaults[reg] = (seen_defaults[reg] or 0) + 1 end ---@type table<string, integer> -- bag: register ident -> occurrence count
|
||||||
local seen_defaults = {}; for reg, _ in pairs (scan.types or {}) do seen_defaults[reg] = (seen_defaults[reg] or 0) + 1 end
|
local atom_infos_list = {}; for _, ai in ipairs(scan.atom_infos or {}) do atom_infos_list[#atom_infos_list + 1] = ai end ---@type AtomInfoEntry[]
|
||||||
--- @type AtomInfoEntry[]
|
|
||||||
local atom_infos_list = {}; for _, ai in ipairs(scan.atom_infos or {}) do atom_infos_list[#atom_infos_list + 1] = ai end
|
|
||||||
|
|
||||||
--- @type PipeCtx
|
local pipe_ctx = { ---@type PipeCtx
|
||||||
local pipe_ctx = {
|
|
||||||
atom_index = {},
|
atom_index = {},
|
||||||
binds_index = {},
|
binds_index = {},
|
||||||
annot_counts = corpus_pipe_ctx.annot_counts,
|
annot_counts = corpus_pipe_ctx.annot_counts,
|
||||||
@@ -500,29 +467,23 @@ local function validate(ctx, src, corpus_pipe_ctx)
|
|||||||
register_alias_registry = corpus_pipe_ctx.register_alias_registry,
|
register_alias_registry = corpus_pipe_ctx.register_alias_registry,
|
||||||
type_name_registry = corpus_pipe_ctx.type_name_registry,
|
type_name_registry = corpus_pipe_ctx.type_name_registry,
|
||||||
}
|
}
|
||||||
--- @type AtomEntry[]
|
local atoms = {} ---@type AtomEntry[]
|
||||||
local atoms = {}
|
for _, a in ipairs(scan.atoms) do ---@type integer, AtomEntry
|
||||||
--- @type integer, AtomEntry
|
|
||||||
for _, a in ipairs(scan.atoms) do
|
|
||||||
if a.kind == "atom" or a.kind == "atom_proc" then
|
if a.kind == "atom" or a.kind == "atom_proc" then
|
||||||
atoms[#atoms + 1] = a
|
atoms[#atoms + 1] = a
|
||||||
pipe_ctx.atom_index[a.raw_name or a.name] = a
|
pipe_ctx.atom_index[a.raw_name or a.name] = a
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
--- @type integer, BindsEntry
|
for _, b in ipairs(scan.binds) do pipe_ctx.binds_index[b.name] = b end ---@type integer, BindsEntry
|
||||||
for _, b in ipairs(scan.binds) do pipe_ctx.binds_index[b.name] = b end
|
|
||||||
|
|
||||||
-- Findings live in a single struct with three lists (errors / warnings / info).
|
-- Findings live in a single struct with three lists (errors / warnings / info).
|
||||||
-- Each check writes to the list appropriate for its severity.
|
-- Each check writes to the list appropriate for its severity.
|
||||||
--- @type Findings
|
local findings = { errors = {}, warnings = {}, info = {} } ---@type Findings
|
||||||
local findings = { errors = {}, warnings = {}, info = {} }
|
|
||||||
|
|
||||||
-- Lift parse-time errors already recorded in scan_source's atom_info payload into this pass's findings list.
|
-- Lift parse-time errors already recorded in scan_source's atom_info payload into this pass's findings list.
|
||||||
--- @type integer, AtomInfoEntry
|
for _, info in ipairs(scan.atom_infos) do ---@type integer, AtomInfoEntry
|
||||||
for _, info in ipairs(scan.atom_infos) do
|
|
||||||
if info.errors then
|
if info.errors then
|
||||||
--- @type integer, string
|
for _, msg in ipairs(info.errors) do ---@type integer, string
|
||||||
for _, msg in ipairs(info.errors) do
|
|
||||||
findings.errors[#findings.errors + 1] = {
|
findings.errors[#findings.errors + 1] = {
|
||||||
line = info.info_line,
|
line = info.info_line,
|
||||||
msg = string.format("'%s': %s", info.atom_name, msg),
|
msg = string.format("'%s': %s", info.atom_name, msg),
|
||||||
@@ -532,8 +493,7 @@ local function validate(ctx, src, corpus_pipe_ctx)
|
|||||||
end
|
end
|
||||||
|
|
||||||
-- THE per-annotation pipeline. ONE loop. CHECK_RULES dispatches per_annot rules.
|
-- THE per-annotation pipeline. ONE loop. CHECK_RULES dispatches per_annot rules.
|
||||||
--- @type integer, AtomInfoEntry
|
for _, info in ipairs(scan.atom_infos) do ---@type integer, AtomInfoEntry
|
||||||
for _, info in ipairs(scan.atom_infos) do
|
|
||||||
duffle.run_check_rules(CHECK_RULES, "per_annot", info, pipe_ctx, findings)
|
duffle.run_check_rules(CHECK_RULES, "per_annot", info, pipe_ctx, findings)
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -542,17 +502,14 @@ local function validate(ctx, src, corpus_pipe_ctx)
|
|||||||
|
|
||||||
-- scan_source records each marker in scan.debug_skip_markers; this loop validates each record independently and emits at most one error per marker.
|
-- scan_source records each marker in scan.debug_skip_markers; this loop validates each record independently and emits at most one error per marker.
|
||||||
-- Valid markers stamp `debug_skip = true` on the following atom or component declaration, which downstream consumers read directly.
|
-- Valid markers stamp `debug_skip = true` on the following atom or component declaration, which downstream consumers read directly.
|
||||||
--- @type DebugSkipMarker[]
|
local skip_markers = scan.debug_skip_markers or {} ---@type DebugSkipMarker[]
|
||||||
local skip_markers = scan.debug_skip_markers or {}
|
for _, marker in ipairs(skip_markers) do ---@type integer, DebugSkipMarker
|
||||||
--- @type integer, DebugSkipMarker
|
|
||||||
for _, marker in ipairs(skip_markers) do
|
|
||||||
duffle.run_check_rules(CHECK_RULES, "per_skip_marker", marker, pipe_ctx, findings)
|
duffle.run_check_rules(CHECK_RULES, "per_skip_marker", marker, pipe_ctx, findings)
|
||||||
end
|
end
|
||||||
|
|
||||||
-- Per-macro rules (TAPE_WORDS vs WORD_COUNT drift).
|
-- Per-macro rules (TAPE_WORDS vs WORD_COUNT drift).
|
||||||
pipe_ctx.word_counts = corpus_pipe_ctx.word_counts
|
pipe_ctx.word_counts = corpus_pipe_ctx.word_counts
|
||||||
--- @type integer, MacroEntry
|
for _, m in ipairs(scan.macros) do ---@type integer, MacroEntry
|
||||||
for _, m in ipairs(scan.macros) do
|
|
||||||
duffle.run_check_rules(CHECK_RULES, "per_macro", m, pipe_ctx, findings)
|
duffle.run_check_rules(CHECK_RULES, "per_macro", m, pipe_ctx, findings)
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -582,8 +539,7 @@ end
|
|||||||
-- M.run — orchestrator entry
|
-- M.run — orchestrator entry
|
||||||
-- ════════════════════════════════════════════════════════════════════════════
|
-- ════════════════════════════════════════════════════════════════════════════
|
||||||
|
|
||||||
--- @type AnnotationPass
|
local M = {} ---@type AnnotationPass
|
||||||
local M = {}
|
|
||||||
|
|
||||||
-- Expose `validate` for downstream passes (e.g. report.lua) that need to re-render the per-source results into a per-MODULE report.
|
-- Expose `validate` for downstream passes (e.g. report.lua) that need to re-render the per-source results into a per-MODULE report.
|
||||||
M.validate = validate
|
M.validate = validate
|
||||||
@@ -591,47 +547,32 @@ M.validate = validate
|
|||||||
--- @param ctx PassCtx
|
--- @param ctx PassCtx
|
||||||
--- @return PassResult
|
--- @return PassResult
|
||||||
function M.run(ctx)
|
function M.run(ctx)
|
||||||
--- @type PassOutputEntry[]
|
local outputs = {} ---@type PassOutputEntry[]
|
||||||
local outputs = {}
|
local errors = {} ---@type PassFinding[]
|
||||||
--- @type PassFinding[]
|
local warnings = {} ---@type PassFinding[]
|
||||||
local errors = {}
|
|
||||||
--- @type PassFinding[]
|
|
||||||
local warnings = {}
|
|
||||||
|
|
||||||
-- Build the shared pipe_ctx once for this run; every validate() call sees the same cross-source registries.
|
-- Build the shared pipe_ctx once for this run; every validate() call sees the same cross-source registries.
|
||||||
-- The corpus owns the canonical cross-source registries; per-source scans retain body / declaration ownership.
|
-- The corpus owns the canonical cross-source registries; per-source scans retain body / declaration ownership.
|
||||||
--- @type PipeCtx
|
local corpus_pipe_ctx = build_corpus_pipe_ctx(ctx) ---@type PipeCtx
|
||||||
local corpus_pipe_ctx = build_corpus_pipe_ctx(ctx)
|
local corpus = ctx.shared.corpus ---@type Corpus
|
||||||
--- @type Corpus
|
|
||||||
local corpus = ctx.shared.corpus
|
|
||||||
|
|
||||||
-- Group `corpus.sources_by_dir` by module, validate every source in each bucket, and emit one errors.h per directory.
|
-- Group `corpus.sources_by_dir` by module, validate every source in each bucket, and emit one errors.h per directory.
|
||||||
--- @type table<string, SourceFile[]>
|
local by_dir = (corpus and corpus.sources_by_dir) or {} ---@type table<string, SourceFile[]>
|
||||||
local by_dir = (corpus and corpus.sources_by_dir) or {}
|
|
||||||
|
|
||||||
--- @type string, SourceFile[]
|
for dir, dir_sources in pairs(by_dir) do ---@type string, SourceFile[]
|
||||||
for dir, dir_sources in pairs(by_dir) do
|
local dir_basename = dir:match("([^/\\]+)$") or dir ---@type string
|
||||||
--- @type string
|
local dir_atoms = 0 ---@type integer
|
||||||
local dir_basename = dir:match("([^/\\]+)$") or dir
|
local dir_errors = {} ---@type PassFinding[]
|
||||||
--- @type integer
|
local dir_warnings = {} ---@type PassFinding[]
|
||||||
local dir_atoms = 0
|
for _, src in ipairs(dir_sources) do ---@type integer, SourceFile
|
||||||
--- @type PassFinding[]
|
local result = validate(ctx, src, corpus_pipe_ctx) ---@type AnnotatedResult
|
||||||
local dir_errors = {}
|
|
||||||
--- @type PassFinding[]
|
|
||||||
local dir_warnings = {}
|
|
||||||
--- @type integer, SourceFile
|
|
||||||
for _, src in ipairs(dir_sources) do
|
|
||||||
--- @type AnnotatedResult
|
|
||||||
local result = validate(ctx, src, corpus_pipe_ctx)
|
|
||||||
result.source = src.path -- tag for downstream rendering
|
result.source = src.path -- tag for downstream rendering
|
||||||
dir_atoms = dir_atoms + #result.atoms
|
dir_atoms = dir_atoms + #result.atoms
|
||||||
--- @type integer, PassFinding
|
for _, e in ipairs(result.errors) do ---@type integer, PassFinding
|
||||||
for _, e in ipairs(result.errors) do
|
|
||||||
dir_errors[#dir_errors + 1] = { line = e.line, msg = e.msg, source = src.path }
|
dir_errors[#dir_errors + 1] = { line = e.line, msg = e.msg, source = src.path }
|
||||||
errors [#errors + 1] = { line = e.line, msg = e.msg }
|
errors [#errors + 1] = { line = e.line, msg = e.msg }
|
||||||
end
|
end
|
||||||
--- @type integer, PassFinding
|
for _, w in ipairs(result.warnings) do ---@type integer, PassFinding
|
||||||
for _, w in ipairs(result.warnings) do
|
|
||||||
dir_warnings[#dir_warnings + 1] = { line = w.line, msg = w.msg }
|
dir_warnings[#dir_warnings + 1] = { line = w.line, msg = w.msg }
|
||||||
warnings [#warnings + 1] = { line = w.line, msg = w.msg }
|
warnings [#warnings + 1] = { line = w.line, msg = w.msg }
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -37,12 +37,9 @@
|
|||||||
-- Bootstrap: load `duffle_paths.lua` via `debug.getinfo(1, "S").source`
|
-- Bootstrap: load `duffle_paths.lua` via `debug.getinfo(1, "S").source`
|
||||||
-- (works both standalone + when require'd). `duffle_paths.lua` sets package.path then returns `require("duffle")`
|
-- (works both standalone + when require'd). `duffle_paths.lua` sets package.path then returns `require("duffle")`
|
||||||
-- at the bottom, so the dofile value IS the duffle module.
|
-- at the bottom, so the dofile value IS the duffle module.
|
||||||
--- @type string
|
local _bootstrap_dir = debug.getinfo(1, "S").source:match("^@?(.*[/\\])") or "./" ---@type string
|
||||||
local _bootstrap_dir = debug.getinfo(1, "S").source:match("^@?(.*[/\\])") or "./"
|
local duffle = dofile(_bootstrap_dir .. "../duffle_paths.lua") ---@type DuffleExport
|
||||||
--- @type DuffleExport
|
local elf_dwarf = require("elf_dwarf") ---@type ElfDwarfMod
|
||||||
local duffle = dofile(_bootstrap_dir .. "../duffle_paths.lua")
|
|
||||||
--- @type ElfDwarfMod
|
|
||||||
local elf_dwarf = require("elf_dwarf")
|
|
||||||
|
|
||||||
-- ════════════════════════════════════════════════════════════════════════════
|
-- ════════════════════════════════════════════════════════════════════════════
|
||||||
-- Constants
|
-- Constants
|
||||||
@@ -50,8 +47,7 @@ local elf_dwarf = require("elf_dwarf")
|
|||||||
|
|
||||||
-- Format version emitted as the first line. Bump + add a migration test if the format changes;
|
-- Format version emitted as the first line. Bump + add a migration test if the format changes;
|
||||||
-- the gdb runtime loader rejects mismatches (E2).
|
-- the gdb runtime loader rejects mismatches (E2).
|
||||||
--- @type integer
|
local FORMAT_VERSION = 1 ---@type integer
|
||||||
local FORMAT_VERSION = 1
|
|
||||||
|
|
||||||
-- ════════════════════════════════════════════════════════════════════════════
|
-- ════════════════════════════════════════════════════════════════════════════
|
||||||
-- Type declarations
|
-- Type declarations
|
||||||
@@ -107,23 +103,16 @@ local FORMAT_VERSION = 1
|
|||||||
--- @return WordMapEntry[]
|
--- @return WordMapEntry[]
|
||||||
--- @return integer
|
--- @return integer
|
||||||
local function canonical_word_entries(atom)
|
local function canonical_word_entries(atom)
|
||||||
--- @type AtomPaths
|
local paths = atom.paths or {} ---@type AtomPaths
|
||||||
local paths = atom.paths or {}
|
local events = paths.word_events or {} ---@type WordEvent[]
|
||||||
--- @type WordEvent[]
|
local word_items = {} ---@type EmissionItem[]
|
||||||
local events = paths.word_events or {}
|
for _, item in ipairs(paths.items or {}) do ---@type integer, EmissionItem
|
||||||
--- @type EmissionItem[]
|
|
||||||
local word_items = {}
|
|
||||||
--- @type integer, EmissionItem
|
|
||||||
for _, item in ipairs(paths.items or {}) do
|
|
||||||
if item.kind == "word" then word_items[#word_items + 1] = item end
|
if item.kind == "word" then word_items[#word_items + 1] = item end
|
||||||
end
|
end
|
||||||
|
|
||||||
--- @type WordMapEntry[]
|
local entries = {} ---@type WordMapEntry[]
|
||||||
local entries = {}
|
for index, event in ipairs(events) do ---@type integer, WordEvent
|
||||||
--- @type integer, WordEvent
|
local item = word_items[index] or {} ---@type EmissionItem
|
||||||
for index, event in ipairs(events) do
|
|
||||||
--- @type EmissionItem
|
|
||||||
local item = word_items[index] or {}
|
|
||||||
entries[#entries + 1] = {
|
entries[#entries + 1] = {
|
||||||
pos = event.i or (index - 1),
|
pos = event.i or (index - 1),
|
||||||
line = event.call_line or item.line or 0,
|
line = event.call_line or item.line or 0,
|
||||||
@@ -149,20 +138,14 @@ end
|
|||||||
--- @return string[]
|
--- @return string[]
|
||||||
--- @return integer
|
--- @return integer
|
||||||
local function emit_provenance_stanza(src, atom, wc)
|
local function emit_provenance_stanza(src, atom, wc)
|
||||||
--- @type string[]
|
local lines = {} ---@type string[]
|
||||||
local lines = {}
|
local rel_path = src.path:gsub("\\\\", "/") ---@type string
|
||||||
--- @type string
|
local entries, total = canonical_word_entries(atom) ---@type WordMapEntry[], integer
|
||||||
local rel_path = src.path:gsub("\\\\", "/")
|
|
||||||
--- @type WordMapEntry[], integer
|
|
||||||
local entries, total = canonical_word_entries(atom)
|
|
||||||
lines[#lines + 1] = string.format('ATOM %s "%s" 0', atom.raw_name or atom.name, rel_path)
|
lines[#lines + 1] = string.format('ATOM %s "%s" 0', atom.raw_name or atom.name, rel_path)
|
||||||
|
|
||||||
--- @type integer, WordMapEntry
|
for _, entry in ipairs(entries) do ---@type integer, WordMapEntry
|
||||||
for _, entry in ipairs(entries) do
|
local inv = entry.invocation ---@type InvocationRecord|nil
|
||||||
--- @type InvocationRecord|nil
|
local macro_count = inv and wc["mac_" .. inv.component_name] ---@type integer|nil
|
||||||
local inv = entry.invocation
|
|
||||||
--- @type integer|nil
|
|
||||||
local macro_count = inv and wc["mac_" .. inv.component_name]
|
|
||||||
if inv and macro_count ~= nil then
|
if inv and macro_count ~= nil then
|
||||||
lines[#lines + 1] = string.format('WORD %d CALL %s:%d MACRO %s "%s:%d" BODY %d'
|
lines[#lines + 1] = string.format('WORD %d CALL %s:%d MACRO %s "%s:%d" BODY %d'
|
||||||
, entry.pos, rel_path, entry.line, inv.component_name
|
, entry.pos, rel_path, entry.line, inv.component_name
|
||||||
@@ -182,8 +165,7 @@ end
|
|||||||
--- @param wc WordCounts
|
--- @param wc WordCounts
|
||||||
--- @return string
|
--- @return string
|
||||||
local function render_provenance(src, wc)
|
local function render_provenance(src, wc)
|
||||||
--- @type string[]
|
local lines = {} ---@type string[]
|
||||||
local lines = {}
|
|
||||||
lines[#lines + 1] = "# FORMAT_VERSION 1"
|
lines[#lines + 1] = "# FORMAT_VERSION 1"
|
||||||
lines[#lines + 1] = "# auto-generated by ps1_meta.lua (passes/atoms_source_map.lua) — DO NOT EDIT"
|
lines[#lines + 1] = "# auto-generated by ps1_meta.lua (passes/atoms_source_map.lua) — DO NOT EDIT"
|
||||||
lines[#lines + 1] = "# Per-.word provenance: maps each emitted .word to its call site (atom body"
|
lines[#lines + 1] = "# Per-.word provenance: maps each emitted .word to its call site (atom body"
|
||||||
@@ -195,17 +177,13 @@ local function render_provenance(src, wc)
|
|||||||
--- @param atom AtomEntry
|
--- @param atom AtomEntry
|
||||||
--- @return nil
|
--- @return nil
|
||||||
local function append(atom)
|
local function append(atom)
|
||||||
--- @type string[]
|
local stanza = emit_provenance_stanza(src, atom, wc) ---@type string[]
|
||||||
local stanza = emit_provenance_stanza(src, atom, wc)
|
for _, line in ipairs(stanza) do lines[#lines + 1] = line end ---@type integer, string
|
||||||
--- @type integer, string
|
|
||||||
for _, line in ipairs(stanza) do lines[#lines + 1] = line end
|
|
||||||
end
|
end
|
||||||
--- @type integer, AtomEntry
|
for _, atom in ipairs(src.scan.atoms or {}) do ---@type integer, AtomEntry
|
||||||
for _, atom in ipairs(src.scan.atoms or {}) do
|
|
||||||
if atom.paths then append(atom) end
|
if atom.paths then append(atom) end
|
||||||
end
|
end
|
||||||
--- @type integer, AtomEntry
|
for _, atom in ipairs(src.scan.raw_atoms or {}) do ---@type integer, AtomEntry
|
||||||
for _, atom in ipairs(src.scan.raw_atoms or {}) do
|
|
||||||
if atom.paths then append(atom) end
|
if atom.paths then append(atom) end
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -219,16 +197,12 @@ end
|
|||||||
--- @return string[]
|
--- @return string[]
|
||||||
--- @return integer
|
--- @return integer
|
||||||
local function emit_atom_stanza(src, atom)
|
local function emit_atom_stanza(src, atom)
|
||||||
--- @type string[]
|
local lines = {} ---@type string[]
|
||||||
local lines = {}
|
local rel_path = src.path:gsub("\\\\", "/") ---@type string
|
||||||
--- @type string
|
local entries, total = canonical_word_entries(atom) ---@type WordMapEntry[], integer
|
||||||
local rel_path = src.path:gsub("\\\\", "/")
|
|
||||||
--- @type WordMapEntry[], integer
|
|
||||||
local entries, total = canonical_word_entries(atom)
|
|
||||||
|
|
||||||
lines[#lines + 1] = string.format('ATOM %s "%s" 0', atom.raw_name or atom.name, rel_path)
|
lines[#lines + 1] = string.format('ATOM %s "%s" 0', atom.raw_name or atom.name, rel_path)
|
||||||
--- @type integer, WordMapEntry
|
for _, entry in ipairs(entries) do ---@type integer, WordMapEntry
|
||||||
for _, entry in ipairs(entries) do
|
|
||||||
lines[#lines + 1] = string.format("WORD %d LINE %d TEXT %s",
|
lines[#lines + 1] = string.format("WORD %d LINE %d TEXT %s",
|
||||||
entry.pos, entry.line, entry.text)
|
entry.pos, entry.line, entry.text)
|
||||||
end
|
end
|
||||||
@@ -242,25 +216,20 @@ end
|
|||||||
--- @param src SourceFile
|
--- @param src SourceFile
|
||||||
--- @return string
|
--- @return string
|
||||||
local function render_source_map(src)
|
local function render_source_map(src)
|
||||||
--- @type string[]
|
local lines = {} ---@type string[]
|
||||||
local lines = {}
|
|
||||||
lines[#lines + 1] = "# FORMAT_VERSION " .. FORMAT_VERSION
|
lines[#lines + 1] = "# FORMAT_VERSION " .. FORMAT_VERSION
|
||||||
lines[#lines + 1] = "# auto-generated by ps1_meta.lua (passes/atoms_source_map.lua) — DO NOT EDIT"
|
lines[#lines + 1] = "# auto-generated by ps1_meta.lua (passes/atoms_source_map.lua) — DO NOT EDIT"
|
||||||
|
|
||||||
--- @param atom AtomEntry
|
--- @param atom AtomEntry
|
||||||
--- @return nil
|
--- @return nil
|
||||||
local function append(atom)
|
local function append(atom)
|
||||||
--- @type string[]
|
local stanza = emit_atom_stanza(src, atom) ---@type string[]
|
||||||
local stanza = emit_atom_stanza(src, atom)
|
for _, line in ipairs(stanza) do lines[#lines + 1] = line end ---@type integer, string
|
||||||
--- @type integer, string
|
|
||||||
for _, line in ipairs(stanza) do lines[#lines + 1] = line end
|
|
||||||
end
|
end
|
||||||
--- @type integer, AtomEntry
|
for _, atom in ipairs(src.scan.atoms or {}) do ---@type integer, AtomEntry
|
||||||
for _, atom in ipairs(src.scan.atoms or {}) do
|
|
||||||
if atom.paths then append(atom) end
|
if atom.paths then append(atom) end
|
||||||
end
|
end
|
||||||
--- @type integer, AtomEntry
|
for _, atom in ipairs(src.scan.raw_atoms or {}) do ---@type integer, AtomEntry
|
||||||
for _, atom in ipairs(src.scan.raw_atoms or {}) do
|
|
||||||
if atom.paths then append(atom) end
|
if atom.paths then append(atom) end
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -283,28 +252,20 @@ end
|
|||||||
--- @param ctx PassCtx
|
--- @param ctx PassCtx
|
||||||
--- @return GdbAtomRecord[]
|
--- @return GdbAtomRecord[]
|
||||||
local function build_atom_table(ctx)
|
local function build_atom_table(ctx)
|
||||||
--- @type table<string, NmAddr>
|
local addrs = elf_dwarf.read_nm(ctx.flags.elf_path) ---@type table<string, NmAddr>
|
||||||
local addrs = elf_dwarf.read_nm(ctx.flags.elf_path)
|
local corpus = ctx.shared and ctx.shared.corpus ---@type Corpus|nil
|
||||||
--- @type Corpus|nil
|
local matched = {} ---@type GdbAtomRecord[]
|
||||||
local corpus = ctx.shared and ctx.shared.corpus
|
|
||||||
--- @type GdbAtomRecord[]
|
|
||||||
local matched = {}
|
|
||||||
|
|
||||||
--- @type integer, SourceFile
|
for _, src in ipairs(corpus.source_order or {}) do ---@type integer, SourceFile
|
||||||
for _, src in ipairs(corpus.source_order or {}) do
|
local file_base = src.path:match("([^/\\\\]+)$") or src.path ---@type string
|
||||||
--- @type string
|
|
||||||
local file_base = src.path:match("([^/\\\\]+)$") or src.path
|
|
||||||
--- @param atom AtomEntry
|
--- @param atom AtomEntry
|
||||||
--- @return nil
|
--- @return nil
|
||||||
local function append(atom)
|
local function append(atom)
|
||||||
if not atom.paths then return end
|
if not atom.paths then return end
|
||||||
--- @type string
|
local name = atom.raw_name or atom.name ---@type string
|
||||||
local name = atom.raw_name or atom.name
|
local info = addrs[name] ---@type NmAddr|nil
|
||||||
--- @type NmAddr|nil
|
|
||||||
local info = addrs[name]
|
|
||||||
if not info then return end
|
if not info then return end
|
||||||
--- @type WordMapEntry[], integer
|
local entries, total = canonical_word_entries(atom) ---@type WordMapEntry[], integer
|
||||||
local entries, total = canonical_word_entries(atom)
|
|
||||||
matched[#matched + 1] = {
|
matched[#matched + 1] = {
|
||||||
name = name,
|
name = name,
|
||||||
src_path = src.path,
|
src_path = src.path,
|
||||||
@@ -315,10 +276,8 @@ local function build_atom_table(ctx)
|
|||||||
entries = entries,
|
entries = entries,
|
||||||
}
|
}
|
||||||
end
|
end
|
||||||
--- @type integer, AtomEntry
|
for _, atom in ipairs((src.scan or {}).atoms or {}) do append(atom) end ---@type integer, AtomEntry
|
||||||
for _, atom in ipairs((src.scan or {}).atoms or {}) do append(atom) end
|
for _, atom in ipairs((src.scan or {}).raw_atoms or {}) do append(atom) end ---@type integer, AtomEntry
|
||||||
--- @type integer, AtomEntry
|
|
||||||
for _, atom in ipairs((src.scan or {}).raw_atoms or {}) do append(atom) end
|
|
||||||
end
|
end
|
||||||
|
|
||||||
-- Deterministic order: sort by address (matches `nm` output ordering).
|
-- Deterministic order: sort by address (matches `nm` output ordering).
|
||||||
@@ -326,8 +285,7 @@ local function build_atom_table(ctx)
|
|||||||
--- @param b GdbAtomRecord
|
--- @param b GdbAtomRecord
|
||||||
--- @return boolean
|
--- @return boolean
|
||||||
table.sort(matched, function(a, b) return a.addr < b.addr end)
|
table.sort(matched, function(a, b) return a.addr < b.addr end)
|
||||||
--- @type integer, GdbAtomRecord
|
for i, a in ipairs(matched) do a.idx = i - 1 end ---@type integer, GdbAtomRecord
|
||||||
for i, a in ipairs(matched) do a.idx = i - 1 end
|
|
||||||
return matched
|
return matched
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -345,8 +303,7 @@ local function append_gdb_commands(lines, matched)
|
|||||||
-- ── tape_atoms ──
|
-- ── tape_atoms ──
|
||||||
-- Hardcoded one printf per atom. No loop.
|
-- Hardcoded one printf per atom. No loop.
|
||||||
lines[#lines + 1] = "define tape_atoms"
|
lines[#lines + 1] = "define tape_atoms"
|
||||||
--- @type integer, GdbAtomRecord
|
for _, a in ipairs(matched) do ---@type integer, GdbAtomRecord
|
||||||
for _, a in ipairs(matched) do
|
|
||||||
-- gdb 12.1 quirk: literals in printf args require an attached target.
|
-- gdb 12.1 quirk: literals in printf args require an attached target.
|
||||||
-- Use the per-atom convenience vars set above as printf args.
|
-- Use the per-atom convenience vars set above as printf args.
|
||||||
lines[#lines + 1] = string.format(' printf " %%-32s @ 0x%%08x %%4d words\\n", $__atom_name_%d, $__atom_addr_%d, $__atom_words_%d',
|
lines[#lines + 1] = string.format(' printf " %%-32s @ 0x%%08x %%4d words\\n", $__atom_name_%d, $__atom_addr_%d, $__atom_words_%d',
|
||||||
@@ -361,8 +318,7 @@ local function append_gdb_commands(lines, matched)
|
|||||||
-- ── break_atom (generic) + per-atom break_atom_X ──
|
-- ── break_atom (generic) + per-atom break_atom_X ──
|
||||||
lines[#lines + 1] = "define break_atom"
|
lines[#lines + 1] = "define break_atom"
|
||||||
lines[#lines + 1] = ' echo "Usage: break_atom_<exact_name> (pick from the list below)"'
|
lines[#lines + 1] = ' echo "Usage: break_atom_<exact_name> (pick from the list below)"'
|
||||||
--- @type integer, GdbAtomRecord
|
for _, a in ipairs(matched) do ---@type integer, GdbAtomRecord
|
||||||
for _, a in ipairs(matched) do
|
|
||||||
lines[#lines + 1] = string.format(' printf " break_atom_%%-32s\\n", $__atom_name_%d', a.idx)
|
lines[#lines + 1] = string.format(' printf " break_atom_%%-32s\\n", $__atom_name_%d', a.idx)
|
||||||
end
|
end
|
||||||
lines[#lines + 1] = "end"
|
lines[#lines + 1] = "end"
|
||||||
@@ -371,8 +327,7 @@ local function append_gdb_commands(lines, matched)
|
|||||||
lines[#lines + 1] = "end"
|
lines[#lines + 1] = "end"
|
||||||
lines[#lines + 1] = ""
|
lines[#lines + 1] = ""
|
||||||
|
|
||||||
--- @type integer, GdbAtomRecord
|
for _, a in ipairs(matched) do ---@type integer, GdbAtomRecord
|
||||||
for _, a in ipairs(matched) do
|
|
||||||
lines[#lines + 1] = string.format("define break_atom_%s", a.name)
|
lines[#lines + 1] = string.format("define break_atom_%s", a.name)
|
||||||
lines[#lines + 1] = string.format(" break *$__atom_addr_%d", a.idx)
|
lines[#lines + 1] = string.format(" break *$__atom_addr_%d", a.idx)
|
||||||
lines[#lines + 1] = string.format(' printf " Breakpoint set at %s (0x%%08x)\\n", $__atom_addr_%d', a.name, a.idx)
|
lines[#lines + 1] = string.format(' printf " Breakpoint set at %s (0x%%08x)\\n", $__atom_addr_%d', a.name, a.idx)
|
||||||
@@ -386,8 +341,7 @@ local function append_gdb_commands(lines, matched)
|
|||||||
-- ── step_atom / next_atom ──
|
-- ── step_atom / next_atom ──
|
||||||
-- Hardcoded one tbreak per atom. No loop.
|
-- Hardcoded one tbreak per atom. No loop.
|
||||||
lines[#lines + 1] = "define step_atom"
|
lines[#lines + 1] = "define step_atom"
|
||||||
--- @type integer, GdbAtomRecord
|
for _, a in ipairs(matched) do ---@type integer, GdbAtomRecord
|
||||||
for _, a in ipairs(matched) do
|
|
||||||
lines[#lines + 1] = string.format(" tbreak *$__atom_addr_%d", a.idx)
|
lines[#lines + 1] = string.format(" tbreak *$__atom_addr_%d", a.idx)
|
||||||
end
|
end
|
||||||
lines[#lines + 1] = " continue"
|
lines[#lines + 1] = " continue"
|
||||||
@@ -410,8 +364,7 @@ local function append_gdb_commands(lines, matched)
|
|||||||
lines[#lines + 1] = "define where_in_atom"
|
lines[#lines + 1] = "define where_in_atom"
|
||||||
lines[#lines + 1] = " set $__pc = (unsigned int)$pc"
|
lines[#lines + 1] = " set $__pc = (unsigned int)$pc"
|
||||||
lines[#lines + 1] = " set $__matched = 0"
|
lines[#lines + 1] = " set $__matched = 0"
|
||||||
--- @type integer, GdbAtomRecord
|
for _, a in ipairs(matched) do ---@type integer, GdbAtomRecord
|
||||||
for _, a in ipairs(matched) do
|
|
||||||
-- Precompute end_addr (gdb 12.1's expression evaluator chokes on `addr + words*4`).
|
-- Precompute end_addr (gdb 12.1's expression evaluator chokes on `addr + words*4`).
|
||||||
lines[#lines + 1] = string.format(" set $__end_%d = $__atom_addr_%d + $__atom_words_%d * 4", a.idx, a.idx, a.idx)
|
lines[#lines + 1] = string.format(" set $__end_%d = $__atom_addr_%d + $__atom_words_%d * 4", a.idx, a.idx, a.idx)
|
||||||
lines[#lines + 1] = string.format(" if $__pc >= $__atom_addr_%d && $__pc < $__end_%d", a.idx, a.idx)
|
lines[#lines + 1] = string.format(" if $__pc >= $__atom_addr_%d && $__pc < $__end_%d", a.idx, a.idx)
|
||||||
@@ -420,18 +373,15 @@ local function append_gdb_commands(lines, matched)
|
|||||||
lines[#lines + 1] = string.format(" set $__word = ($__pc - $__atom_addr_%d) / 4", a.idx)
|
lines[#lines + 1] = string.format(" set $__word = ($__pc - $__atom_addr_%d) / 4", a.idx)
|
||||||
lines[#lines + 1] = string.format(' printf "word: %%d/%%d\\n", $__word, $__atom_words_%d', a.idx)
|
lines[#lines + 1] = string.format(' printf "word: %%d/%%d\\n", $__word, $__atom_words_%d', a.idx)
|
||||||
-- One inner-if per WORD entry. Each word's line + text hardcoded.
|
-- One inner-if per WORD entry. Each word's line + text hardcoded.
|
||||||
--- @type integer, WordMapEntry
|
for _, we in ipairs(a.entries) do ---@type integer, WordMapEntry
|
||||||
for _, we in ipairs(a.entries) do
|
|
||||||
lines[#lines + 1] = string.format(" if $__word == %d", we.pos)
|
lines[#lines + 1] = string.format(" if $__word == %d", we.pos)
|
||||||
-- Escape TEXT for printf format string.
|
-- Escape TEXT for printf format string.
|
||||||
--- @type string
|
local escaped_text = we.text:gsub("%%", "%%%%"):gsub('"', '\\"') ---@type string
|
||||||
local escaped_text = we.text:gsub("%%", "%%%%"):gsub('"', '\\"')
|
|
||||||
lines[#lines + 1] = string.format(' printf "source: %%s:%%d %%s\\n", $__atom_file_%d, %d, "%s"', a.idx, we.line, escaped_text)
|
lines[#lines + 1] = string.format(' printf "source: %%s:%%d %%s\\n", $__atom_file_%d, %d, "%s"', a.idx, we.line, escaped_text)
|
||||||
lines[#lines + 1] = " end"
|
lines[#lines + 1] = " end"
|
||||||
end
|
end
|
||||||
-- Fallback for words beyond the source map (shouldn't happen if nm matches).
|
-- Fallback for words beyond the source map (shouldn't happen if nm matches).
|
||||||
--- @type integer
|
local max_word = 0 ---@type integer
|
||||||
local max_word = 0
|
|
||||||
if #a.entries > 0 then max_word = a.entries[#a.entries].pos end
|
if #a.entries > 0 then max_word = a.entries[#a.entries].pos end
|
||||||
lines[#lines + 1] = string.format(' if $__word > %d', max_word)
|
lines[#lines + 1] = string.format(' if $__word > %d', max_word)
|
||||||
lines[#lines + 1] = ' printf "source: (no source-map entry for word %%d; map may be stale)\\n", $__word'
|
lines[#lines + 1] = ' printf "source: (no source-map entry for word %%d; map may be stale)\\n", $__word'
|
||||||
@@ -456,8 +406,7 @@ local function append_gdb_commands(lines, matched)
|
|||||||
lines[#lines + 1] = " set $__in_atom = 0"
|
lines[#lines + 1] = " set $__in_atom = 0"
|
||||||
lines[#lines + 1] = " set $__did_step = 0"
|
lines[#lines + 1] = " set $__did_step = 0"
|
||||||
lines[#lines + 1] = " set $__pc = (unsigned int)$pc"
|
lines[#lines + 1] = " set $__pc = (unsigned int)$pc"
|
||||||
--- @type integer, GdbAtomRecord
|
for _, a in ipairs(matched) do ---@type integer, GdbAtomRecord
|
||||||
for _, a in ipairs(matched) do
|
|
||||||
-- Precompute end_addr in the convenience var (single expression gdb handles).
|
-- Precompute end_addr in the convenience var (single expression gdb handles).
|
||||||
lines[#lines + 1] = string.format(" set $__end_%d = $__atom_addr_%d + $__atom_words_%d * 4", a.idx, a.idx, a.idx)
|
lines[#lines + 1] = string.format(" set $__end_%d = $__atom_addr_%d + $__atom_words_%d * 4", a.idx, a.idx, a.idx)
|
||||||
lines[#lines + 1] = string.format(" if $__pc >= $__atom_addr_%d && $__pc < $__end_%d", a.idx, a.idx)
|
lines[#lines + 1] = string.format(" if $__pc >= $__atom_addr_%d && $__pc < $__end_%d", a.idx, a.idx)
|
||||||
@@ -494,8 +443,7 @@ end
|
|||||||
--- @return nil
|
--- @return nil
|
||||||
local function emit_gdb_runtime(ctx)
|
local function emit_gdb_runtime(ctx)
|
||||||
if not (ctx.flags and ctx.flags.gdb_runtime) then return end
|
if not (ctx.flags and ctx.flags.gdb_runtime) then return end
|
||||||
--- @type string|nil
|
local elf_path = ctx.flags.elf_path ---@type string|nil
|
||||||
local elf_path = ctx.flags.elf_path
|
|
||||||
if not elf_path or elf_path == "" then
|
if not elf_path or elf_path == "" then
|
||||||
io.stderr:write("[atoms_source_map] --gdb-runtime requires --elf <elf>\n")
|
io.stderr:write("[atoms_source_map] --gdb-runtime requires --elf <elf>\n")
|
||||||
return
|
return
|
||||||
@@ -506,15 +454,13 @@ local function emit_gdb_runtime(ctx)
|
|||||||
return
|
return
|
||||||
end
|
end
|
||||||
|
|
||||||
--- @type GdbAtomRecord[]
|
local matched = build_atom_table(ctx) ---@type GdbAtomRecord[]
|
||||||
local matched = build_atom_table(ctx)
|
|
||||||
if #matched == 0 then
|
if #matched == 0 then
|
||||||
io.stderr:write("[atoms_source_map] --gdb-runtime: no atoms matched against nm symbols (stale scan?).\n")
|
io.stderr:write("[atoms_source_map] --gdb-runtime: no atoms matched against nm symbols (stale scan?).\n")
|
||||||
return
|
return
|
||||||
end
|
end
|
||||||
|
|
||||||
--- @type string[]
|
local lines = {} ---@type string[]
|
||||||
local lines = {}
|
|
||||||
lines[#lines + 1] = "# Auto-generated by ps1_meta.lua (passes/atoms_source_map.lua)"
|
lines[#lines + 1] = "# Auto-generated by ps1_meta.lua (passes/atoms_source_map.lua)"
|
||||||
lines[#lines + 1] = "# DO NOT EDIT — re-run ps1_meta.lua --atoms-source-map --gdb-runtime to regenerate"
|
lines[#lines + 1] = "# DO NOT EDIT — re-run ps1_meta.lua --atoms-source-map --gdb-runtime to regenerate"
|
||||||
lines[#lines + 1] = "# Sourced by scripts/gdb/gdb_tape_atoms.gdb (the wrapper)."
|
lines[#lines + 1] = "# Sourced by scripts/gdb/gdb_tape_atoms.gdb (the wrapper)."
|
||||||
@@ -535,8 +481,7 @@ local function emit_gdb_runtime(ctx)
|
|||||||
|
|
||||||
-- Per-atom convenience vars (used as printf args; literals aren't accepted
|
-- Per-atom convenience vars (used as printf args; literals aren't accepted
|
||||||
-- without an attached target on gdb 12.1).
|
-- without an attached target on gdb 12.1).
|
||||||
--- @type integer, GdbAtomRecord
|
for _, a in ipairs(matched) do ---@type integer, GdbAtomRecord
|
||||||
for _, a in ipairs(matched) do
|
|
||||||
lines[#lines + 1] = string.format('set $__atom_name_%d = "%s"', a.idx, gdb_escape(a.name))
|
lines[#lines + 1] = string.format('set $__atom_name_%d = "%s"', a.idx, gdb_escape(a.name))
|
||||||
lines[#lines + 1] = string.format("set $__atom_addr_%d = 0x%x", a.idx, a.addr)
|
lines[#lines + 1] = string.format("set $__atom_addr_%d = 0x%x", a.idx, a.addr)
|
||||||
lines[#lines + 1] = string.format("set $__atom_words_%d = %d", a.idx, a.words)
|
lines[#lines + 1] = string.format("set $__atom_words_%d = %d", a.idx, a.words)
|
||||||
@@ -552,8 +497,7 @@ local function emit_gdb_runtime(ctx)
|
|||||||
-- Confirmation line for the source operator.
|
-- Confirmation line for the source operator.
|
||||||
lines[#lines + 1] = 'printf "[gdb_tape_atoms] runtime loaded %d atoms from %s\\n", $__atom_count, $__elf_path'
|
lines[#lines + 1] = 'printf "[gdb_tape_atoms] runtime loaded %d atoms from %s\\n", $__atom_count, $__elf_path'
|
||||||
|
|
||||||
--- @type string
|
local out_path ---@type string
|
||||||
local out_path
|
|
||||||
-- Move out of `<out_root>/gdb_tape_atoms_runtime.gdb` to `<out_root>/../gdb_tape_atoms_runtime.gdb` when the conventional `<out_root>` is `<build>/gen`
|
-- Move out of `<out_root>/gdb_tape_atoms_runtime.gdb` to `<out_root>/../gdb_tape_atoms_runtime.gdb` when the conventional `<out_root>` is `<build>/gen`
|
||||||
-- (any equivalent spelling — relative, absolute backslash, absolute forward-slash, trailing-separator variants).
|
-- (any equivalent spelling — relative, absolute backslash, absolute forward-slash, trailing-separator variants).
|
||||||
-- This puts the gdb runtime alongside the ELF at `build/` rather than under the report subdir.
|
-- This puts the gdb runtime alongside the ELF at `build/` rather than under the report subdir.
|
||||||
@@ -566,8 +510,7 @@ local function emit_gdb_runtime(ctx)
|
|||||||
if ends_with_gen_dir(ctx.out_root) then
|
if ends_with_gen_dir(ctx.out_root) then
|
||||||
-- Strip the trailing `/gen` segment, then write the runtime script under `build/`.
|
-- Strip the trailing `/gen` segment, then write the runtime script under `build/`.
|
||||||
-- e.g. "C:/projects/Pikuma/ps1/build/gen" -> "C:/projects/Pikuma/ps1/build".
|
-- e.g. "C:/projects/Pikuma/ps1/build/gen" -> "C:/projects/Pikuma/ps1/build".
|
||||||
--- @type string
|
local parent = ctx.out_root:gsub("[/\\]gen[/\\]?$", "") ---@type string
|
||||||
local parent = ctx.out_root:gsub("[/\\]gen[/\\]?$", "")
|
|
||||||
out_path = parent .. "/gdb_tape_atoms_runtime.gdb"
|
out_path = parent .. "/gdb_tape_atoms_runtime.gdb"
|
||||||
else
|
else
|
||||||
out_path = ctx.out_root .. "/gdb_tape_atoms_runtime.gdb"
|
out_path = ctx.out_root .. "/gdb_tape_atoms_runtime.gdb"
|
||||||
@@ -581,8 +524,7 @@ end
|
|||||||
-- M — module exports
|
-- M — module exports
|
||||||
-- ════════════════════════════════════════════════════════════════════════════
|
-- ════════════════════════════════════════════════════════════════════════════
|
||||||
|
|
||||||
--- @type AtomSourceMapPass
|
local M = {} ---@type AtomSourceMapPass
|
||||||
local M = {}
|
|
||||||
|
|
||||||
-- Expose the pure render functions so `report.lua` and the focused tests can call them directly without triggering the file-emit path.
|
-- Expose the pure render functions so `report.lua` and the focused tests can call them directly without triggering the file-emit path.
|
||||||
M.render_source_map = render_source_map
|
M.render_source_map = render_source_map
|
||||||
@@ -594,22 +536,15 @@ M.render_provenance = render_provenance
|
|||||||
function M.render_atom_source_map(atom)
|
function M.render_atom_source_map(atom)
|
||||||
assert(type(atom) == "table", "render_atom_source_map: atom must be a table")
|
assert(type(atom) == "table", "render_atom_source_map: atom must be a table")
|
||||||
assert(type(atom.paths) == "table", "render_atom_source_map: atom.paths must be a table")
|
assert(type(atom.paths) == "table", "render_atom_source_map: atom.paths must be a table")
|
||||||
--- @type WordMapEntry[], integer
|
local entries, total = canonical_word_entries(atom) ---@type WordMapEntry[], integer
|
||||||
local entries, total = canonical_word_entries(atom)
|
local lines = {} ---@type string[]
|
||||||
--- @type string[]
|
|
||||||
local lines = {}
|
|
||||||
lines[#lines + 1] = string.format("ATOM %s %d", (atom.raw_name or atom.name), total)
|
lines[#lines + 1] = string.format("ATOM %s %d", (atom.raw_name or atom.name), total)
|
||||||
--- @type integer, WordMapEntry
|
for _, entry in ipairs(entries) do ---@type integer, WordMapEntry
|
||||||
for _, entry in ipairs(entries) do
|
local word_line = string.format("WORD %d LINE %d TEXT %s", ---@type string
|
||||||
--- @type string
|
|
||||||
local word_line = string.format("WORD %d LINE %d TEXT %s",
|
|
||||||
entry.pos, entry.line, entry.text)
|
entry.pos, entry.line, entry.text)
|
||||||
--- @type string[]
|
local keys = {} ---@type string[]
|
||||||
local keys = {}
|
for pos = 1, 16 do ---@type integer
|
||||||
--- @type integer
|
local k = entry.gpr_keys and entry.gpr_keys[pos] ---@type string|nil
|
||||||
for pos = 1, 16 do
|
|
||||||
--- @type string|nil
|
|
||||||
local k = entry.gpr_keys and entry.gpr_keys[pos]
|
|
||||||
if type(k) == "string" and k:sub(1, 7) == "reguse:" then
|
if type(k) == "string" and k:sub(1, 7) == "reguse:" then
|
||||||
keys[#keys + 1] = k
|
keys[#keys + 1] = k
|
||||||
end
|
end
|
||||||
@@ -635,17 +570,12 @@ function M.render_atom_provenance(atom, wc, rel_path)
|
|||||||
assert(type(atom) == "table", "render_atom_provenance: atom must be a table")
|
assert(type(atom) == "table", "render_atom_provenance: atom must be a table")
|
||||||
assert(type(atom.paths) == "table", "render_atom_provenance: atom.paths must be a table")
|
assert(type(atom.paths) == "table", "render_atom_provenance: atom.paths must be a table")
|
||||||
assert(type(rel_path) == "string", "render_atom_provenance: rel_path must be a string")
|
assert(type(rel_path) == "string", "render_atom_provenance: rel_path must be a string")
|
||||||
--- @type WordMapEntry[], integer
|
local entries, total = canonical_word_entries(atom) ---@type WordMapEntry[], integer
|
||||||
local entries, total = canonical_word_entries(atom)
|
local lines = {} ---@type string[]
|
||||||
--- @type string[]
|
|
||||||
local lines = {}
|
|
||||||
lines[#lines + 1] = string.format("ATOM %s %d", (atom.raw_name or atom.name), total)
|
lines[#lines + 1] = string.format("ATOM %s %d", (atom.raw_name or atom.name), total)
|
||||||
--- @type integer, WordMapEntry
|
for _, entry in ipairs(entries) do ---@type integer, WordMapEntry
|
||||||
for _, entry in ipairs(entries) do
|
local inv = entry.invocation ---@type InvocationRecord|nil
|
||||||
--- @type InvocationRecord|nil
|
local macro_count = inv and wc and wc["mac_" .. inv.component_name] ---@type integer|nil
|
||||||
local inv = entry.invocation
|
|
||||||
--- @type integer|nil
|
|
||||||
local macro_count = inv and wc and wc["mac_" .. inv.component_name]
|
|
||||||
if inv and macro_count ~= nil then
|
if inv and macro_count ~= nil then
|
||||||
lines[#lines + 1] = string.format('WORD %d CALL %s:%d MACRO %s "%s:%d" BODY %d'
|
lines[#lines + 1] = string.format('WORD %d CALL %s:%d MACRO %s "%s:%d" BODY %d'
|
||||||
, entry.pos, rel_path, entry.line, inv.component_name, inv.def_path or "", inv.def_line or 0, entry.body_line)
|
, entry.pos, rel_path, entry.line, inv.component_name, inv.def_path or "", inv.def_line or 0, entry.body_line)
|
||||||
@@ -664,22 +594,17 @@ end
|
|||||||
--- @param ctx PassCtx
|
--- @param ctx PassCtx
|
||||||
--- @return PassResult
|
--- @return PassResult
|
||||||
function M.run(ctx)
|
function M.run(ctx)
|
||||||
--- @type PassOutputEntry[]
|
local outputs = {} ---@type PassOutputEntry[]
|
||||||
local outputs = {}
|
local errors = {} ---@type PassFinding[]
|
||||||
--- @type PassFinding[]
|
local warnings = {} ---@type PassFinding[]
|
||||||
local errors = {}
|
|
||||||
--- @type PassFinding[]
|
|
||||||
local warnings = {}
|
|
||||||
|
|
||||||
--- @type Corpus|nil
|
local corpus = ctx.shared and ctx.shared.corpus ---@type Corpus|nil
|
||||||
local corpus = ctx.shared and ctx.shared.corpus
|
|
||||||
if type(corpus) ~= "table" or type(corpus.source_order) ~= "table" then
|
if type(corpus) ~= "table" or type(corpus.source_order) ~= "table" then
|
||||||
error("atoms_source_map.run requires ctx.shared.corpus.source_order (canonical corpus).", 0)
|
error("atoms_source_map.run requires ctx.shared.corpus.source_order (canonical corpus).", 0)
|
||||||
end
|
end
|
||||||
|
|
||||||
-- Word counts come from `corpus.word_counts` (populated by word_count_eval + components passes).
|
-- Word counts come from `corpus.word_counts` (populated by word_count_eval + components passes).
|
||||||
--- @type WordCounts
|
local wc = corpus.word_counts or {} ---@type WordCounts
|
||||||
local wc = corpus.word_counts or {}
|
|
||||||
if not next(wc) then
|
if not next(wc) then
|
||||||
warnings[#warnings + 1] = {
|
warnings[#warnings + 1] = {
|
||||||
line = 0,
|
line = 0,
|
||||||
|
|||||||
+71
-142
@@ -35,10 +35,8 @@
|
|||||||
--- @field run fun(ctx: PassCtx): AutoRegResult
|
--- @field run fun(ctx: PassCtx): AutoRegResult
|
||||||
--- @field POOL GprIdent[]
|
--- @field POOL GprIdent[]
|
||||||
|
|
||||||
--- @type string
|
local _bootstrap_dir = debug.getinfo(1, "S").source:match("^@?(.*[/\\])") or "./" ---@type string
|
||||||
local _bootstrap_dir = debug.getinfo(1, "S").source:match("^@?(.*[/\\])") or "./"
|
local duffle = dofile(_bootstrap_dir .. "../duffle_paths.lua") ---@type DuffleExport
|
||||||
--- @type DuffleExport
|
|
||||||
local duffle = dofile(_bootstrap_dir .. "../duffle_paths.lua")
|
|
||||||
|
|
||||||
--- ════════════════════════════════════════════════════════════════════════════
|
--- ════════════════════════════════════════════════════════════════════════════
|
||||||
--- THE GPR ALLOCATION POOL — what's allocatable, and (more importantly) WHY
|
--- THE GPR ALLOCATION POOL — what's allocatable, and (more importantly) WHY
|
||||||
@@ -56,8 +54,7 @@ local duffle = dofile(_bootstrap_dir .. "../duffle_paths.lua")
|
|||||||
--- R_K0/K1 (codes 26-27) — Kernel / interrupt handler reserves. Never touched by user code.
|
--- R_K0/K1 (codes 26-27) — Kernel / interrupt handler reserves. Never touched by user code.
|
||||||
--- R_GP/SP/FP/RA (codes 28-31) — R_SP/R_FP/R_RA are tape-runtime carriers between tape_enter and tape_exit; R_GP stays the host global pointer.
|
--- R_GP/SP/FP/RA (codes 28-31) — R_SP/R_FP/R_RA are tape-runtime carriers between tape_enter and tape_exit; R_GP stays the host global pointer.
|
||||||
---
|
---
|
||||||
--- @type GprIdent[]
|
local POOL = { ---@type GprIdent[]
|
||||||
local POOL = {
|
|
||||||
"R_V0", "R_V1",
|
"R_V0", "R_V1",
|
||||||
"R_T0", "R_T1", "R_T2", "R_T3",
|
"R_T0", "R_T1", "R_T2", "R_T3",
|
||||||
"R_T4", "R_T5", "R_T6", "R_T7",
|
"R_T4", "R_T5", "R_T6", "R_T7",
|
||||||
@@ -72,8 +69,7 @@ local POOL = {
|
|||||||
-- Only the POOL entries matter for auto_reg — non-pool aliases
|
-- Only the POOL entries matter for auto_reg — non-pool aliases
|
||||||
-- (R_AT=1, R_A0..A3=4..7, R_T8=24, R_T9=25, R_K0/K1=26..27, R_GP/SP/FP/RA=28..31)
|
-- (R_AT=1, R_A0..A3=4..7, R_T8=24, R_T9=25, R_K0/K1=26..27, R_GP/SP/FP/RA=28..31)
|
||||||
-- are deliberately omitted — see the comment block above for the WHY of each exclusion.
|
-- are deliberately omitted — see the comment block above for the WHY of each exclusion.
|
||||||
--- @type table<integer, GprIdent> -- bag: MIPS GPR code -> POOL ident
|
local INT_CODE_TO_POOL_GPR = { ---@type table<integer, GprIdent> -- bag: MIPS GPR code -> POOL ident
|
||||||
local INT_CODE_TO_POOL_GPR = {
|
|
||||||
[2] = "R_V0", [3] = "R_V1",
|
[2] = "R_V0", [3] = "R_V1",
|
||||||
[4] = "R_A0", [5] = "R_A1", [6] = "R_A2", [7] = "R_A3",
|
[4] = "R_A0", [5] = "R_A1", [6] = "R_A2", [7] = "R_A3",
|
||||||
[8] = "R_T0", [9] = "R_T1", [10] = "R_T2", [11] = "R_T3",
|
[8] = "R_T0", [9] = "R_T1", [10] = "R_T2", [11] = "R_T3",
|
||||||
@@ -87,10 +83,8 @@ local INT_CODE_TO_POOL_GPR = {
|
|||||||
--- @param tbl table<string, string> -- bag: key set only; values unused
|
--- @param tbl table<string, string> -- bag: key set only; values unused
|
||||||
--- @return string[]
|
--- @return string[]
|
||||||
local function stable_sort_keys(tbl)
|
local function stable_sort_keys(tbl)
|
||||||
--- @type string[]
|
local keys = {} ---@type string[]
|
||||||
local keys = {}
|
for k in pairs(tbl) do keys[#keys + 1] = k end ---@type string
|
||||||
--- @type string
|
|
||||||
for k in pairs(tbl) do keys[#keys + 1] = k end
|
|
||||||
table.sort(keys)
|
table.sort(keys)
|
||||||
return keys
|
return keys
|
||||||
end
|
end
|
||||||
@@ -105,18 +99,12 @@ local function allocate_phase(phase_label, decls)
|
|||||||
-- Deep-copy POOL into a fresh sequence table. The original `table.unpack and table.unpack(POOL) or { unpack(POOL) }`
|
-- Deep-copy POOL into a fresh sequence table. The original `table.unpack and table.unpack(POOL) or { unpack(POOL) }`
|
||||||
-- idiom wraps the unpacked values in a single inner table under LuaJIT 5.1 (`table.unpack` is nil; the `or` returns one value),
|
-- idiom wraps the unpacked values in a single inner table under LuaJIT 5.1 (`table.unpack` is nil; the `or` returns one value),
|
||||||
-- which corrupts the pool into `{ {R_T0, R_T1, ...} }` — making `table.remove(pool, 1)` return the inner table on iteration.
|
-- which corrupts the pool into `{ {R_T0, R_T1, ...} }` — making `table.remove(pool, 1)` return the inner table on iteration.
|
||||||
--- @type GprIdent[]
|
local pool = {} ---@type GprIdent[]
|
||||||
local pool = {}
|
for i = 1, #POOL do pool[i] = POOL[i] end ---@type integer
|
||||||
--- @type integer
|
local result = {} ---@type GprAllocMap
|
||||||
for i = 1, #POOL do pool[i] = POOL[i] end
|
local errors = {} ---@type PassFinding[]
|
||||||
--- @type GprAllocMap
|
for _, sym in ipairs(stable_sort_keys(decls)) do ---@type integer, string
|
||||||
local result = {}
|
local next_gpr = table.remove(pool, 1) ---@type GprIdent|nil
|
||||||
--- @type PassFinding[]
|
|
||||||
local errors = {}
|
|
||||||
--- @type integer, string
|
|
||||||
for _, sym in ipairs(stable_sort_keys(decls)) do
|
|
||||||
--- @type GprIdent|nil
|
|
||||||
local next_gpr = table.remove(pool, 1)
|
|
||||||
if not next_gpr then
|
if not next_gpr then
|
||||||
errors[#errors + 1] = {
|
errors[#errors + 1] = {
|
||||||
line = 0,
|
line = 0,
|
||||||
@@ -144,16 +132,12 @@ end
|
|||||||
--- @return table<GprIdent, boolean>
|
--- @return table<GprIdent, boolean>
|
||||||
--- @return table<string, GprIdent>
|
--- @return table<string, GprIdent>
|
||||||
local function build_user_pins(corpus)
|
local function build_user_pins(corpus)
|
||||||
--- @type table<GprIdent, boolean> -- bag: pinned physical GPR -> true
|
local user_pinned = {} ---@type table<GprIdent, boolean> -- bag: pinned physical GPR -> true
|
||||||
local user_pinned = {}
|
local alias_to_gpr = {} ---@type table<string, GprIdent> -- bag: alias ident -> physical GPR
|
||||||
--- @type table<string, GprIdent> -- bag: alias ident -> physical GPR
|
|
||||||
local alias_to_gpr = {}
|
|
||||||
if not corpus.register_alias_registry then return user_pinned, alias_to_gpr end
|
if not corpus.register_alias_registry then return user_pinned, alias_to_gpr end
|
||||||
--- @type string, AliasEntry
|
for alias_name, alias_entry in pairs(corpus.register_alias_registry) do ---@type string, AliasEntry
|
||||||
for alias_name, alias_entry in pairs(corpus.register_alias_registry) do
|
|
||||||
if alias_entry.has_atom_reg and alias_entry.code then
|
if alias_entry.has_atom_reg and alias_entry.code then
|
||||||
--- @type GprIdent|nil
|
local gpr = INT_CODE_TO_POOL_GPR[alias_entry.code] ---@type GprIdent|nil
|
||||||
local gpr = INT_CODE_TO_POOL_GPR[alias_entry.code]
|
|
||||||
if gpr then
|
if gpr then
|
||||||
user_pinned[gpr] = true
|
user_pinned[gpr] = true
|
||||||
alias_to_gpr[alias_name] = gpr
|
alias_to_gpr[alias_name] = gpr
|
||||||
@@ -174,29 +158,22 @@ end
|
|||||||
--- @param alias_to_gpr table<string, GprIdent> -- bag: alias ident -> physical GPR
|
--- @param alias_to_gpr table<string, GprIdent> -- bag: alias ident -> physical GPR
|
||||||
--- @return table<GprIdent, integer>
|
--- @return table<GprIdent, integer>
|
||||||
local function find_used_gprs(body_text, alias_to_gpr)
|
local function find_used_gprs(body_text, alias_to_gpr)
|
||||||
--- @type table<GprIdent, integer> -- bag: physical GPR -> hit count
|
local found = {} ---@type table<GprIdent, integer> -- bag: physical GPR -> hit count
|
||||||
local found = {}
|
|
||||||
-- (a) Hardcoded physical GPRs (R_T0..R_T7, R_V0..R_V1, R_A0..R_A3, R_S0..R_S7).
|
-- (a) Hardcoded physical GPRs (R_T0..R_T7, R_V0..R_V1, R_A0..R_A3, R_S0..R_S7).
|
||||||
--- @type GprIdent
|
for gpr in body_text:gmatch("(R_T%d+|R_V%d+|R_A%d+|R_S%d+)") do ---@type GprIdent
|
||||||
for gpr in body_text:gmatch("(R_T%d+|R_V%d+|R_A%d+|R_S%d+)") do
|
|
||||||
found[gpr] = (found[gpr] or 0) + 1
|
found[gpr] = (found[gpr] or 0) + 1
|
||||||
end
|
end
|
||||||
-- (b) Alias references (R_<Alias>) resolved to physical GPRs via the registry.
|
-- (b) Alias references (R_<Alias>) resolved to physical GPRs via the registry.
|
||||||
-- Sorted by name so the regex is byte-stable across runs.
|
-- Sorted by name so the regex is byte-stable across runs.
|
||||||
if alias_to_gpr and next(alias_to_gpr) then
|
if alias_to_gpr and next(alias_to_gpr) then
|
||||||
--- @type string[]
|
local aliases = {} ---@type string[]
|
||||||
local aliases = {}
|
for alias_name in pairs(alias_to_gpr) do ---@type string
|
||||||
--- @type string
|
|
||||||
for alias_name in pairs(alias_to_gpr) do
|
|
||||||
aliases[#aliases + 1] = alias_name
|
aliases[#aliases + 1] = alias_name
|
||||||
end
|
end
|
||||||
table.sort(aliases)
|
table.sort(aliases)
|
||||||
--- @type string
|
local pattern = "(" .. table.concat(aliases, "|") .. ")" ---@type string
|
||||||
local pattern = "(" .. table.concat(aliases, "|") .. ")"
|
for alias_name in body_text:gmatch(pattern) do ---@type string
|
||||||
--- @type string
|
local gpr = alias_to_gpr[alias_name] ---@type GprIdent|nil
|
||||||
for alias_name in body_text:gmatch(pattern) do
|
|
||||||
--- @type GprIdent|nil
|
|
||||||
local gpr = alias_to_gpr[alias_name]
|
|
||||||
if gpr and not found[gpr] then
|
if gpr and not found[gpr] then
|
||||||
found[gpr] = 1
|
found[gpr] = 1
|
||||||
end
|
end
|
||||||
@@ -213,30 +190,24 @@ end
|
|||||||
--- @return string|nil
|
--- @return string|nil
|
||||||
local function emit_auto_reg_h(out_dir, dir, sources, mappings)
|
local function emit_auto_reg_h(out_dir, dir, sources, mappings)
|
||||||
if not mappings or next(mappings) == nil then return end
|
if not mappings or next(mappings) == nil then return end
|
||||||
--- @type string
|
local out_path = out_dir .. "/" .. "auto_reg.h" ---@type string
|
||||||
local out_path = out_dir .. "/" .. "auto_reg.h"
|
|
||||||
duffle.ensure_dir(out_dir)
|
duffle.ensure_dir(out_dir)
|
||||||
--- @type string[]
|
local lines = { ---@type string[]
|
||||||
local lines = {
|
|
||||||
"#ifdef INTELLISENSE_DIRECTIVES",
|
"#ifdef INTELLISENSE_DIRECTIVES",
|
||||||
"#pragma once",
|
"#pragma once",
|
||||||
"#endif",
|
"#endif",
|
||||||
"// Auto-generated by ps1_meta.lua (passes/auto_reg.lua) — DO NOT EDIT",
|
"// Auto-generated by ps1_meta.lua (passes/auto_reg.lua) — DO NOT EDIT",
|
||||||
"// Directory: " .. dir:gsub("/", "\\"),
|
"// Directory: " .. dir:gsub("/", "\\"),
|
||||||
}
|
}
|
||||||
--- @type integer, SourceFile
|
for _, src in ipairs(sources) do ---@type integer, SourceFile
|
||||||
for _, src in ipairs(sources) do
|
|
||||||
lines[#lines + 1] = "// source: " .. src.path
|
lines[#lines + 1] = "// source: " .. src.path
|
||||||
end
|
end
|
||||||
lines[#lines + 1] = "// Per-phase register allocations resolved by the lua pass."
|
lines[#lines + 1] = "// Per-phase register allocations resolved by the lua pass."
|
||||||
lines[#lines + 1] = "// R_<Sym>_Code = <chosen GPR's _Code constant> for every marker in this directory."
|
lines[#lines + 1] = "// R_<Sym>_Code = <chosen GPR's _Code constant> for every marker in this directory."
|
||||||
lines[#lines + 1] = ""
|
lines[#lines + 1] = ""
|
||||||
--- @type integer, string
|
for _, sym in ipairs(stable_sort_keys(mappings)) do ---@type integer, string
|
||||||
for _, sym in ipairs(stable_sort_keys(mappings)) do
|
local gpr = mappings[sym] ---@type GprIdent
|
||||||
--- @type GprIdent
|
local gpr_code = gpr .. "_Code" ---@type string
|
||||||
local gpr = mappings[sym]
|
|
||||||
--- @type string
|
|
||||||
local gpr_code = gpr .. "_Code"
|
|
||||||
lines[#lines + 1] = "#define " .. sym .. "_Code " .. gpr_code
|
lines[#lines + 1] = "#define " .. sym .. "_Code " .. gpr_code
|
||||||
end
|
end
|
||||||
lines[#lines + 1] = ""
|
lines[#lines + 1] = ""
|
||||||
@@ -249,21 +220,16 @@ end
|
|||||||
-- Pass entry
|
-- Pass entry
|
||||||
-- ════════════════════════════════════════════════════════════════════════════
|
-- ════════════════════════════════════════════════════════════════════════════
|
||||||
|
|
||||||
--- @type AutoRegPass
|
local M = {} ---@type AutoRegPass
|
||||||
local M = {}
|
|
||||||
|
|
||||||
--- @param ctx PassCtx
|
--- @param ctx PassCtx
|
||||||
--- @return AutoRegResult
|
--- @return AutoRegResult
|
||||||
function M.run(ctx)
|
function M.run(ctx)
|
||||||
--- @type AutoRegOutput[]
|
local outputs = {} ---@type AutoRegOutput[]
|
||||||
local outputs = {}
|
local errors = {} ---@type PassFinding[]
|
||||||
--- @type PassFinding[]
|
local warnings = {} ---@type PassFinding[]
|
||||||
local errors = {}
|
|
||||||
--- @type PassFinding[]
|
|
||||||
local warnings = {}
|
|
||||||
|
|
||||||
--- @type Corpus|nil
|
local corpus = ctx.shared and ctx.shared.corpus ---@type Corpus|nil
|
||||||
local corpus = ctx.shared and ctx.shared.corpus
|
|
||||||
if type(corpus) ~= "table" then
|
if type(corpus) ~= "table" then
|
||||||
error("auto_reg.run requires ctx.shared.corpus", 0)
|
error("auto_reg.run requires ctx.shared.corpus", 0)
|
||||||
end
|
end
|
||||||
@@ -273,23 +239,17 @@ function M.run(ctx)
|
|||||||
-- MUST NOT be allocated to any auto-reg marker — they're preserved across atoms by the wave-context discipline.
|
-- MUST NOT be allocated to any auto-reg marker — they're preserved across atoms by the wave-context discipline.
|
||||||
-- The corpus's register_alias_registry is the source of truth for these opt-in pins.
|
-- The corpus's register_alias_registry is the source of truth for these opt-in pins.
|
||||||
-- Body references to those aliases (via alias_to_gpr) are also excluded on a per-atom basis in step 2 below.
|
-- Body references to those aliases (via alias_to_gpr) are also excluded on a per-atom basis in step 2 below.
|
||||||
--- @type table<GprIdent, boolean>, table<string, GprIdent>
|
local user_pinned, alias_to_gpr = build_user_pins(corpus) ---@type table<GprIdent, boolean>, table<string, GprIdent>
|
||||||
local user_pinned, alias_to_gpr = build_user_pins(corpus)
|
|
||||||
|
|
||||||
-- 1. Allocate phase pools first (phase declarations take precedence over per-atom declarations).
|
-- 1. Allocate phase pools first (phase declarations take precedence over per-atom declarations).
|
||||||
--- @type table<string, GprAllocMap> -- bag: phase_label -> alloc map
|
local phase_allocations = {} ---@type table<string, GprAllocMap> -- bag: phase_label -> alloc map
|
||||||
local phase_allocations = {}
|
for phase_label, decls in pairs(corpus.phase_auto_regs or {}) do ---@type string, table<string, string>
|
||||||
--- @type string, table<string, string>
|
local mapping, errs = allocate_phase(phase_label, decls) ---@type GprAllocMap, PassFinding[]
|
||||||
for phase_label, decls in pairs(corpus.phase_auto_regs or {}) do
|
for sym, gpr in pairs(mapping) do ---@type string, GprIdent
|
||||||
--- @type GprAllocMap, PassFinding[]
|
|
||||||
local mapping, errs = allocate_phase(phase_label, decls)
|
|
||||||
--- @type string, GprIdent
|
|
||||||
for sym, gpr in pairs(mapping) do
|
|
||||||
phase_allocations[phase_label] = phase_allocations[phase_label] or {}
|
phase_allocations[phase_label] = phase_allocations[phase_label] or {}
|
||||||
phase_allocations[phase_label][sym] = gpr
|
phase_allocations[phase_label][sym] = gpr
|
||||||
end
|
end
|
||||||
--- @type integer, PassFinding
|
for _, e in ipairs(errs) do ---@type integer, PassFinding
|
||||||
for _, e in ipairs(errs) do
|
|
||||||
errors[#errors + 1] = e
|
errors[#errors + 1] = e
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
@@ -298,22 +258,16 @@ function M.run(ctx)
|
|||||||
-- Otherwise, allocate a private pool for the atom.
|
-- Otherwise, allocate a private pool for the atom.
|
||||||
-- The phase membership is in `corpus.atom_phases[phase_label].atoms` (an array of atom names declared via `atom_phase(<phase>)`
|
-- The phase membership is in `corpus.atom_phases[phase_label].atoms` (an array of atom names declared via `atom_phase(<phase>)`
|
||||||
-- in the atom's `atom_info` line). Build a reverse map `atom_name -> phase_label` so the lookup is O(1) per atom scope.
|
-- in the atom's `atom_info` line). Build a reverse map `atom_name -> phase_label` so the lookup is O(1) per atom scope.
|
||||||
--- @type table<AtomName, string> -- bag: atom name -> phase label
|
local atom_name_to_phase = {} ---@type table<AtomName, string> -- bag: atom name -> phase label
|
||||||
local atom_name_to_phase = {}
|
for phase_label, entry in pairs(corpus.atom_phases or {}) do ---@type string, AtomPhaseGroup
|
||||||
--- @type string, AtomPhaseGroup
|
for _, atom_name in ipairs(entry.atoms or {}) do ---@type integer, AtomName
|
||||||
for phase_label, entry in pairs(corpus.atom_phases or {}) do
|
|
||||||
--- @type integer, AtomName
|
|
||||||
for _, atom_name in ipairs(entry.atoms or {}) do
|
|
||||||
atom_name_to_phase[atom_name] = phase_label
|
atom_name_to_phase[atom_name] = phase_label
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
--- @type table<AtomName, GprAllocMap> -- bag: atom scope -> alloc map
|
local atom_allocations = {} ---@type table<AtomName, GprAllocMap> -- bag: atom scope -> alloc map
|
||||||
local atom_allocations = {}
|
for atom_scope, decls in pairs(corpus.atom_auto_regs or {}) do ---@type AtomName, table<string, string>
|
||||||
--- @type AtomName, table<string, string>
|
local phase_label = atom_name_to_phase[atom_scope] ---@type string|nil
|
||||||
for atom_scope, decls in pairs(corpus.atom_auto_regs or {}) do
|
|
||||||
--- @type string|nil
|
|
||||||
local phase_label = atom_name_to_phase[atom_scope]
|
|
||||||
-- Build the atom's source pool: start with the full POOL, subtract:
|
-- Build the atom's source pool: start with the full POOL, subtract:
|
||||||
-- (a) every GPR already committed (phase allocations + prior atom allocations)
|
-- (a) every GPR already committed (phase allocations + prior atom allocations)
|
||||||
-- (b) every USER-PINNED GPR (wave-context carriers + file-scope pinned aliases)
|
-- (b) every USER-PINNED GPR (wave-context carriers + file-scope pinned aliases)
|
||||||
@@ -323,37 +277,26 @@ function M.run(ctx)
|
|||||||
-- the original `source_pool = phase_allocations[phase_label]` form used the phase
|
-- the original `source_pool = phase_allocations[phase_label]` form used the phase
|
||||||
-- allocation MAP as a pool, but that map has no array part, so `table.remove(source_pool, 1)`
|
-- allocation MAP as a pool, but that map has no array part, so `table.remove(source_pool, 1)`
|
||||||
-- returned nil and every atom-with-phase marker errored with `phase_register_pool_exhausted`.
|
-- returned nil and every atom-with-phase marker errored with `phase_register_pool_exhausted`.
|
||||||
--- @type table<GprIdent, boolean> -- bag: committed or body-referenced GPR -> true
|
local used = {} ---@type table<GprIdent, boolean> -- bag: committed or body-referenced GPR -> true
|
||||||
local used = {}
|
for _, m in pairs(phase_allocations) do for _, gpr in pairs(m) do used[gpr] = true end end ---@type integer, GprAllocMap
|
||||||
--- @type integer, GprAllocMap
|
for _, m in pairs(atom_allocations) do for _, gpr in pairs(m) do used[gpr] = true end end ---@type integer, GprAllocMap
|
||||||
for _, m in pairs(phase_allocations) do for _, gpr in pairs(m) do used[gpr] = true end end
|
|
||||||
--- @type integer, GprAllocMap
|
|
||||||
for _, m in pairs(atom_allocations) do for _, gpr in pairs(m) do used[gpr] = true end end
|
|
||||||
-- (c) Body references — scan the atom body for hardcoded + alias-resolved GPRs.
|
-- (c) Body references — scan the atom body for hardcoded + alias-resolved GPRs.
|
||||||
-- Folded into `used` so the source_pool exclusion is a single check.
|
-- Folded into `used` so the source_pool exclusion is a single check.
|
||||||
--- @type AtomEntry|nil
|
local atom = corpus.atoms_by_name and corpus.atoms_by_name[atom_scope] ---@type AtomEntry|nil
|
||||||
local atom = corpus.atoms_by_name and corpus.atoms_by_name[atom_scope]
|
|
||||||
if atom and atom.body then
|
if atom and atom.body then
|
||||||
--- @type table<GprIdent, integer>
|
local body_used = find_used_gprs(atom.body, alias_to_gpr) ---@type table<GprIdent, integer>
|
||||||
local body_used = find_used_gprs(atom.body, alias_to_gpr)
|
for gpr in pairs(body_used) do used[gpr] = true end ---@type GprIdent
|
||||||
--- @type GprIdent
|
|
||||||
for gpr in pairs(body_used) do used[gpr] = true end
|
|
||||||
end
|
end
|
||||||
--- @type GprIdent[]
|
local source_pool = {} ---@type GprIdent[]
|
||||||
local source_pool = {}
|
for _, gpr in ipairs(POOL) do ---@type integer, GprIdent
|
||||||
--- @type integer, GprIdent
|
|
||||||
for _, gpr in ipairs(POOL) do
|
|
||||||
-- Exclude (a) prior commitments, (b) USER-PINNED GPRs (wave-context carriers declared via atom_reg + _Code defs, preserved across atoms globally).
|
-- Exclude (a) prior commitments, (b) USER-PINNED GPRs (wave-context carriers declared via atom_reg + _Code defs, preserved across atoms globally).
|
||||||
if not used[gpr] and not user_pinned[gpr] then
|
if not used[gpr] and not user_pinned[gpr] then
|
||||||
source_pool[#source_pool + 1] = gpr
|
source_pool[#source_pool + 1] = gpr
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
--- @type GprAllocMap
|
local result = {} ---@type GprAllocMap
|
||||||
local result = {}
|
for _, sym in ipairs(stable_sort_keys(decls)) do ---@type integer, string
|
||||||
--- @type integer, string
|
local next_gpr = table.remove(source_pool, 1) ---@type GprIdent|nil
|
||||||
for _, sym in ipairs(stable_sort_keys(decls)) do
|
|
||||||
--- @type GprIdent|nil
|
|
||||||
local next_gpr = table.remove(source_pool, 1)
|
|
||||||
if not next_gpr then
|
if not next_gpr then
|
||||||
errors[#errors + 1] = {
|
errors[#errors + 1] = {
|
||||||
line = 0,
|
line = 0,
|
||||||
@@ -375,15 +318,11 @@ function M.run(ctx)
|
|||||||
-- This warning is kept as a defensive safety net for cases the body scanner might miss
|
-- This warning is kept as a defensive safety net for cases the body scanner might miss
|
||||||
-- (e.g. macros that expand to register references the scanner cannot resolve).
|
-- (e.g. macros that expand to register references the scanner cannot resolve).
|
||||||
-- For each resolved (scope, sym) -> R_Tn mapping, scan the atom body source for used GPRs.
|
-- For each resolved (scope, sym) -> R_Tn mapping, scan the atom body source for used GPRs.
|
||||||
--- @type AtomName, GprAllocMap
|
for atom_scope, decls in pairs(atom_allocations) do ---@type AtomName, GprAllocMap
|
||||||
for atom_scope, decls in pairs(atom_allocations) do
|
local atom = corpus.atoms_by_name and corpus.atoms_by_name[atom_scope] ---@type AtomEntry|nil
|
||||||
--- @type AtomEntry|nil
|
|
||||||
local atom = corpus.atoms_by_name and corpus.atoms_by_name[atom_scope]
|
|
||||||
if atom and atom.body then
|
if atom and atom.body then
|
||||||
--- @type table<GprIdent, integer>
|
local used_in_body = find_used_gprs(atom.body, alias_to_gpr) ---@type table<GprIdent, integer>
|
||||||
local used_in_body = find_used_gprs(atom.body, alias_to_gpr)
|
for sym, allocated_gpr in pairs(decls) do ---@type string, GprIdent
|
||||||
--- @type string, GprIdent
|
|
||||||
for sym, allocated_gpr in pairs(decls) do
|
|
||||||
if used_in_body[allocated_gpr] and used_in_body[allocated_gpr] > 0 then
|
if used_in_body[allocated_gpr] and used_in_body[allocated_gpr] > 0 then
|
||||||
warnings[#warnings + 1] = {
|
warnings[#warnings + 1] = {
|
||||||
line = atom.line or 0,
|
line = atom.line or 0,
|
||||||
@@ -398,37 +337,27 @@ function M.run(ctx)
|
|||||||
|
|
||||||
-- 4. Emit per-directory gen/auto_reg.h.
|
-- 4. Emit per-directory gen/auto_reg.h.
|
||||||
-- For each source directory that has atom_auto_regs or phase_auto_regs entries, emit one header.
|
-- For each source directory that has atom_auto_regs or phase_auto_regs entries, emit one header.
|
||||||
--- @type table<string, SourceFile[]>
|
local sources_by_dir = corpus.sources_by_dir or {} ---@type table<string, SourceFile[]>
|
||||||
local sources_by_dir = corpus.sources_by_dir or {}
|
for dir, sources in pairs(sources_by_dir) do ---@type string, SourceFile[]
|
||||||
--- @type string, SourceFile[]
|
local per_dir_mappings = {} ---@type GprAllocMap
|
||||||
for dir, sources in pairs(sources_by_dir) do
|
for _, src in ipairs(sources) do ---@type integer, SourceFile
|
||||||
--- @type GprAllocMap
|
|
||||||
local per_dir_mappings = {}
|
|
||||||
--- @type integer, SourceFile
|
|
||||||
for _, src in ipairs(sources) do
|
|
||||||
-- Collect every (sym -> gpr) entry that originated from a source in this directory.
|
-- Collect every (sym -> gpr) entry that originated from a source in this directory.
|
||||||
-- `src.scan.atom_auto_regs` is keyed by ATOM SCOPE NAME; `pairs(t)` iterates KEYS so `scope_name` here is the scope ident (e.g. "cube_g4_face").
|
-- `src.scan.atom_auto_regs` is keyed by ATOM SCOPE NAME; `pairs(t)` iterates KEYS so `scope_name` here is the scope ident (e.g. "cube_g4_face").
|
||||||
-- The previous `for _, scan_atom_auto` form silently assigned the VALUE (a `{sym = sym}` table) to the variable,
|
-- The previous `for _, scan_atom_auto` form silently assigned the VALUE (a `{sym = sym}` table) to the variable,
|
||||||
-- which made `atom_allocations[scan_atom_auto]` a table-indexed lookup that never resolved.
|
-- which made `atom_allocations[scan_atom_auto]` a table-indexed lookup that never resolved.
|
||||||
--- @type string
|
for scope_name in pairs(src.scan and src.scan.atom_auto_regs or {}) do ---@type string
|
||||||
for scope_name in pairs(src.scan and src.scan.atom_auto_regs or {}) do
|
for sym, gpr in pairs(atom_allocations[scope_name] or {}) do ---@type string, GprIdent
|
||||||
--- @type string, GprIdent
|
|
||||||
for sym, gpr in pairs(atom_allocations[scope_name] or {}) do
|
|
||||||
per_dir_mappings[sym] = gpr
|
per_dir_mappings[sym] = gpr
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
--- @type string
|
for scope_name in pairs(src.scan and src.scan.phase_auto_regs or {}) do ---@type string
|
||||||
for scope_name in pairs(src.scan and src.scan.phase_auto_regs or {}) do
|
for sym, gpr in pairs(phase_allocations[scope_name] or {}) do ---@type string, GprIdent
|
||||||
--- @type string, GprIdent
|
|
||||||
for sym, gpr in pairs(phase_allocations[scope_name] or {}) do
|
|
||||||
per_dir_mappings[sym] = gpr
|
per_dir_mappings[sym] = gpr
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
--- @type string
|
local out_dir = dir .. "/gen" ---@type string
|
||||||
local out_dir = dir .. "/gen"
|
local out_path = emit_auto_reg_h(out_dir, dir, sources, per_dir_mappings) ---@type string|nil
|
||||||
--- @type string|nil
|
|
||||||
local out_path = emit_auto_reg_h(out_dir, dir, sources, per_dir_mappings)
|
|
||||||
if out_path then outputs[#outputs + 1] = { auto_reg_h = out_path } end
|
if out_path then outputs[#outputs + 1] = { auto_reg_h = out_path } end
|
||||||
end
|
end
|
||||||
return { outputs = outputs, errors = errors, warnings = warnings }
|
return { outputs = outputs, errors = errors, warnings = warnings }
|
||||||
|
|||||||
+135
-270
@@ -22,42 +22,30 @@
|
|||||||
-- Uses `debug.getinfo` to find this file's own directory, so it works both standalone and when require'd from the orchestrator.
|
-- Uses `debug.getinfo` to find this file's own directory, so it works both standalone and when require'd from the orchestrator.
|
||||||
-- Bootstrap: load `duffle_paths.lua` via `debug.getinfo(1, "S").source` (works both standalone + when require'd).
|
-- Bootstrap: load `duffle_paths.lua` via `debug.getinfo(1, "S").source` (works both standalone + when require'd).
|
||||||
-- duffle_paths.lua sets package.path then returns `require("duffle")` at the bottom, so the dofile value IS the duffle module.
|
-- duffle_paths.lua sets package.path then returns `require("duffle")` at the bottom, so the dofile value IS the duffle module.
|
||||||
--- @type string
|
local _bootstrap_dir = debug.getinfo(1, "S").source:match("^@?(.*[/\\])") or "./" ---@type string
|
||||||
local _bootstrap_dir = debug.getinfo(1, "S").source:match("^@?(.*[/\\])") or "./"
|
local duffle = dofile(_bootstrap_dir .. "../duffle_paths.lua") ---@type DuffleExport
|
||||||
--- @type DuffleExport
|
|
||||||
local duffle = dofile(_bootstrap_dir .. "../duffle_paths.lua")
|
|
||||||
|
|
||||||
-- ════════════════════════════════════════════════════════════════════════════
|
-- ════════════════════════════════════════════════════════════════════════════
|
||||||
-- Constants
|
-- Constants
|
||||||
-- ════════════════════════════════════════════════════════════════════════════
|
-- ════════════════════════════════════════════════════════════════════════════
|
||||||
|
|
||||||
-- Atom component declaration identifiers.
|
-- Atom component declaration identifiers.
|
||||||
--- @type string
|
local ATOM_COMP_PROC = "MipsAtomComp_Proc_" ---@type string
|
||||||
local ATOM_COMP_PROC = "MipsAtomComp_Proc_"
|
local MIPS_ATOM = "Slice_MipsCode" ---@type string -- prefix on the function declaration that wraps an AtomComp_Proc_
|
||||||
--- @type string
|
|
||||||
local MIPS_ATOM = "Slice_MipsCode" -- prefix on the function declaration that wraps an AtomComp_Proc_
|
|
||||||
|
|
||||||
-- Component-name prefixes.
|
-- Component-name prefixes.
|
||||||
--- @type string
|
local AC_PREFIX = "ac_" ---@type string -- arg to MipsAtomComp_(ac_X); the X is the atom name
|
||||||
local AC_PREFIX = "ac_" -- arg to MipsAtomComp_(ac_X); the X is the atom name
|
local AC_PREFIX_LEN = 3 ---@type integer
|
||||||
--- @type integer
|
local MAC_PREFIX = "mac_" ---@type string -- prefix on generated macros; the rest is the atom name
|
||||||
local AC_PREFIX_LEN = 3
|
local MAC_PREFIX_LEN = 4 ---@type integer
|
||||||
--- @type string
|
|
||||||
local MAC_PREFIX = "mac_" -- prefix on generated macros; the rest is the atom name
|
|
||||||
--- @type integer
|
|
||||||
local MAC_PREFIX_LEN = 4
|
|
||||||
|
|
||||||
-- ASCII byte values used in tokenization.
|
-- ASCII byte values used in tokenization.
|
||||||
--- @type integer
|
local BYTE_NEWLINE = 10 ---@type integer
|
||||||
local BYTE_NEWLINE = 10
|
local BYTE_SLASH = 47 ---@type integer
|
||||||
--- @type integer
|
|
||||||
local BYTE_SLASH = 47
|
|
||||||
|
|
||||||
-- Output gen subdirectory + filename (per-directory aggregation; the directory name is the namespace).
|
-- Output gen subdirectory + filename (per-directory aggregation; the directory name is the namespace).
|
||||||
--- @type string
|
local GEN_SUBDIR = "gen" ---@type string
|
||||||
local GEN_SUBDIR = "gen"
|
local MACS_FILENAME = "macs.h" ---@type string
|
||||||
--- @type string
|
|
||||||
local MACS_FILENAME = "macs.h"
|
|
||||||
|
|
||||||
-- ════════════════════════════════════════════════════════════════════════════
|
-- ════════════════════════════════════════════════════════════════════════════
|
||||||
-- Type declarations
|
-- Type declarations
|
||||||
@@ -100,8 +88,7 @@ local MACS_FILENAME = "macs.h"
|
|||||||
-- Local helpers (file I/O + path normalization)
|
-- Local helpers (file I/O + path normalization)
|
||||||
-- ════════════════════════════════════════════════════════════════════════════
|
-- ════════════════════════════════════════════════════════════════════════════
|
||||||
|
|
||||||
--- @type ComponentsPass
|
local M = {} ---@type ComponentsPass
|
||||||
local M = {}
|
|
||||||
|
|
||||||
-- ════════════════════════════════════════════════════════════════════════════
|
-- ════════════════════════════════════════════════════════════════════════════
|
||||||
-- Back-walk helpers (composed into the entry point below: find_function_args_for)
|
-- Back-walk helpers (composed into the entry point below: find_function_args_for)
|
||||||
@@ -123,8 +110,7 @@ local M = {}
|
|||||||
--- @param before_pos integer
|
--- @param before_pos integer
|
||||||
--- @return string|nil
|
--- @return string|nil
|
||||||
local function find_function_args_for(source, name, before_pos)
|
local function find_function_args_for(source, name, before_pos)
|
||||||
--- @type string|nil, string|nil
|
local _, args_inner = duffle.find_function_decl_for(source, before_pos, #MIPS_ATOM) ---@type string|nil, string|nil
|
||||||
local _, args_inner = duffle.find_function_decl_for(source, before_pos, #MIPS_ATOM)
|
|
||||||
return args_inner
|
return args_inner
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -140,31 +126,24 @@ end
|
|||||||
--- @return string[]|nil
|
--- @return string[]|nil
|
||||||
local function extract_arg_names(args_str)
|
local function extract_arg_names(args_str)
|
||||||
if not args_str or args_str == "" then return nil end
|
if not args_str or args_str == "" then return nil end
|
||||||
--- @type string[]
|
local names = {} ---@type string[]
|
||||||
local names = {}
|
local tokens = duffle.split_top_level_commas(args_str) ---@type string[]
|
||||||
--- @type string[]
|
for _, tok in ipairs(tokens) do ---@type integer, string
|
||||||
local tokens = duffle.split_top_level_commas(args_str)
|
local trimmed = duffle.trim(tok) ---@type string
|
||||||
--- @type integer, string
|
|
||||||
for _, tok in ipairs(tokens) do
|
|
||||||
--- @type string
|
|
||||||
local trimmed = duffle.trim(tok)
|
|
||||||
if trimmed ~= "" then
|
if trimmed ~= "" then
|
||||||
-- Strip trailing block comment (/* ... */) from the token, if present.
|
-- Strip trailing block comment (/* ... */) from the token, if present.
|
||||||
-- split_top_level_commas only skips block comments at TOP LEVEL (between commas),
|
-- split_top_level_commas only skips block comments at TOP LEVEL (between commas),
|
||||||
-- not block comments embedded WITHIN a token between a parameter and a trailing comma.
|
-- not block comments embedded WITHIN a token between a parameter and a trailing comma.
|
||||||
-- Without this strip, the identifier-walk below stops at the `/` of `*/` and returns
|
-- Without this strip, the identifier-walk below stops at the `/` of `*/` and returns
|
||||||
-- the wrong name (or nothing). See `test_extract_arg_names_handles_trailing_block_comments`.
|
-- the wrong name (or nothing). See `test_extract_arg_names_handles_trailing_block_comments`.
|
||||||
--- @type integer
|
local trimmed_end = #trimmed ---@type integer
|
||||||
local trimmed_end = #trimmed
|
|
||||||
if trimmed_end >= 2 and trimmed:sub(trimmed_end - 1, trimmed_end) == "*/" then
|
if trimmed_end >= 2 and trimmed:sub(trimmed_end - 1, trimmed_end) == "*/" then
|
||||||
-- Find the matching `/*` that opens the trailing comment.
|
-- Find the matching `/*` that opens the trailing comment.
|
||||||
-- Walk back from the `*/` looking for `/*` (whitespace + `/*`).
|
-- Walk back from the `*/` looking for `/*` (whitespace + `/*`).
|
||||||
--- @type integer
|
local close_pos = trimmed_end - 1 ---@type integer -- position of the second-to-last char
|
||||||
local close_pos = trimmed_end - 1 -- position of the second-to-last char
|
|
||||||
-- Walk back: skip trailing whitespace, then look for the `/*` opener.
|
-- Walk back: skip trailing whitespace, then look for the `/*` opener.
|
||||||
while close_pos > 1 do
|
while close_pos > 1 do
|
||||||
--- @type string
|
local ch = trimmed:sub(close_pos, close_pos) ---@type string
|
||||||
local ch = trimmed:sub(close_pos, close_pos)
|
|
||||||
if ch == " " or ch == "\t" or ch == "\n" or ch == "\r" then
|
if ch == " " or ch == "\t" or ch == "\n" or ch == "\r" then
|
||||||
close_pos = close_pos - 1
|
close_pos = close_pos - 1
|
||||||
else
|
else
|
||||||
@@ -172,10 +151,8 @@ local function extract_arg_names(args_str)
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
-- Now scan back from close_pos for the `/*` opener (slashes are at close_pos-1 and close_pos-2).
|
-- Now scan back from close_pos for the `/*` opener (slashes are at close_pos-1 and close_pos-2).
|
||||||
--- @type integer|nil
|
local opener_pos = nil ---@type integer|nil
|
||||||
local opener_pos = nil
|
local scan = close_pos - 3 ---@type integer
|
||||||
--- @type integer
|
|
||||||
local scan = close_pos - 3
|
|
||||||
while scan >= 1 do
|
while scan >= 1 do
|
||||||
if trimmed:sub(scan, scan + 1) == "/*" then
|
if trimmed:sub(scan, scan + 1) == "/*" then
|
||||||
opener_pos = scan
|
opener_pos = scan
|
||||||
@@ -194,11 +171,9 @@ local function extract_arg_names(args_str)
|
|||||||
trimmed_end = #trimmed
|
trimmed_end = #trimmed
|
||||||
if trimmed_end >= 4 and trimmed:sub(trimmed_end, trimmed_end) == "]" then
|
if trimmed_end >= 4 and trimmed:sub(trimmed_end, trimmed_end) == "]" then
|
||||||
-- Walk back: skip digits, expect `[`.
|
-- Walk back: skip digits, expect `[`.
|
||||||
--- @type integer
|
local bracket_pos = trimmed_end - 1 ---@type integer
|
||||||
local bracket_pos = trimmed_end - 1
|
|
||||||
while bracket_pos > 1 do
|
while bracket_pos > 1 do
|
||||||
--- @type string
|
local ch = trimmed:sub(bracket_pos, bracket_pos) ---@type string
|
||||||
local ch = trimmed:sub(bracket_pos, bracket_pos)
|
|
||||||
if ch >= "0" and ch <= "9" then
|
if ch >= "0" and ch <= "9" then
|
||||||
bracket_pos = bracket_pos - 1
|
bracket_pos = bracket_pos - 1
|
||||||
else
|
else
|
||||||
@@ -212,22 +187,18 @@ local function extract_arg_names(args_str)
|
|||||||
if trimmed == "" then goto continue end
|
if trimmed == "" then goto continue end
|
||||||
-- Find the identifier at the end: walk back over trailers (whitespace + `*` + `[]`),
|
-- Find the identifier at the end: walk back over trailers (whitespace + `*` + `[]`),
|
||||||
-- then walk back over the identifier chars (alnum + `_`).
|
-- then walk back over the identifier chars (alnum + `_`).
|
||||||
--- @type integer
|
local ident_end = #trimmed ---@type integer
|
||||||
local ident_end = #trimmed
|
|
||||||
while ident_end > 0 do
|
while ident_end > 0 do
|
||||||
--- @type string
|
local ch = trimmed:sub(ident_end, ident_end) ---@type string
|
||||||
local ch = trimmed:sub(ident_end, ident_end)
|
|
||||||
if ch == " " or ch == "\t" or ch == "*" or ch == "]" or ch == "[" then
|
if ch == " " or ch == "\t" or ch == "*" or ch == "]" or ch == "[" then
|
||||||
ident_end = ident_end - 1
|
ident_end = ident_end - 1
|
||||||
else
|
else
|
||||||
break
|
break
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
--- @type integer
|
local ident_start = ident_end ---@type integer
|
||||||
local ident_start = ident_end
|
|
||||||
while ident_start > 0 do
|
while ident_start > 0 do
|
||||||
--- @type string
|
local ch = trimmed:sub(ident_start, ident_start) ---@type string
|
||||||
local ch = trimmed:sub(ident_start, ident_start)
|
|
||||||
if duffle.is_alnum_byte(string.byte(ch)) or ch == "_" then
|
if duffle.is_alnum_byte(string.byte(ch)) or ch == "_" then
|
||||||
ident_start = ident_start - 1
|
ident_start = ident_start - 1
|
||||||
else
|
else
|
||||||
@@ -235,8 +206,7 @@ local function extract_arg_names(args_str)
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
ident_start = ident_start + 1
|
ident_start = ident_start + 1
|
||||||
--- @type string
|
local name = trimmed:sub(ident_start, ident_end) ---@type string
|
||||||
local name = trimmed:sub(ident_start, ident_end)
|
|
||||||
if name ~= "" then names[#names + 1] = name end
|
if name ~= "" then names[#names + 1] = name end
|
||||||
::continue::
|
::continue::
|
||||||
end
|
end
|
||||||
@@ -248,8 +218,7 @@ end
|
|||||||
--- @param args_str string|nil
|
--- @param args_str string|nil
|
||||||
--- @return string[]|nil
|
--- @return string[]|nil
|
||||||
local function formal_arg_names(args_str)
|
local function formal_arg_names(args_str)
|
||||||
--- @type string[]|nil
|
local names = extract_arg_names(args_str) ---@type string[]|nil
|
||||||
local names = extract_arg_names(args_str)
|
|
||||||
if not names then return nil end
|
if not names then return nil end
|
||||||
if names[1] == "ab" then table.remove(names, 1) end
|
if names[1] == "ab" then table.remove(names, 1) end
|
||||||
if #names == 0 then return nil end
|
if #names == 0 then return nil end
|
||||||
@@ -271,10 +240,8 @@ end
|
|||||||
--- @param scan SourceScan
|
--- @param scan SourceScan
|
||||||
--- @return Component[]
|
--- @return Component[]
|
||||||
local function project_components(source, scan)
|
local function project_components(source, scan)
|
||||||
--- @type Component[]
|
local out = {} ---@type Component[]
|
||||||
local out = {}
|
for _, a in ipairs(scan.atoms) do ---@type integer, AtomEntry
|
||||||
--- @type integer, AtomEntry
|
|
||||||
for _, a in ipairs(scan.atoms) do
|
|
||||||
-- Only `MipsAtomComp_(ac_X)` (kind="comp_bare") and `MipsAtomComp_Proc_(ac_X, ...)` (kind="comp_proc")
|
-- Only `MipsAtomComp_(ac_X)` (kind="comp_bare") and `MipsAtomComp_Proc_(ac_X, ...)` (kind="comp_proc")
|
||||||
-- are COMPONENTS — they get inlined via `mac_<name>` aliases inside atom bodies.
|
-- are COMPONENTS — they get inlined via `mac_<name>` aliases inside atom bodies.
|
||||||
-- `MipsAtom_Proc_` (kind="atom_proc") is an ATOM (ends with `mac_yield()`); it gets emitted via
|
-- `MipsAtom_Proc_` (kind="atom_proc") is an ATOM (ends with `mac_yield()`); it gets emitted via
|
||||||
@@ -285,12 +252,10 @@ local function project_components(source, scan)
|
|||||||
-- Function-args lookup is meaningful for `MipsAtomComp_Proc_` components
|
-- Function-args lookup is meaningful for `MipsAtomComp_Proc_` components
|
||||||
-- (the macro sits inside `FI_ Slice_MipsCode ac_X(...)`); the alias expansion
|
-- (the macro sits inside `FI_ Slice_MipsCode ac_X(...)`); the alias expansion
|
||||||
-- discards the `ab` (atom-builder) arg the same way both forms do.
|
-- discards the `ab` (atom-builder) arg the same way both forms do.
|
||||||
--- @type string|nil
|
local args = find_function_args_for(source, a.raw_name, a.ident_pos) ---@type string|nil
|
||||||
local args = find_function_args_for(source, a.raw_name, a.ident_pos)
|
|
||||||
-- Comment ownership: scan_source.lua stamps `declaration_comment` on the record by walking backward past any associated bare marker.
|
-- Comment ownership: scan_source.lua stamps `declaration_comment` on the record by walking backward past any associated bare marker.
|
||||||
-- The pass reads `declaration_comment` directly.
|
-- The pass reads `declaration_comment` directly.
|
||||||
--- @type string
|
local comment = a.declaration_comment or "" ---@type string
|
||||||
local comment = a.declaration_comment or ""
|
|
||||||
out[#out + 1] = {
|
out[#out + 1] = {
|
||||||
line = a.line,
|
line = a.line,
|
||||||
name = a.name,
|
name = a.name,
|
||||||
@@ -321,31 +286,23 @@ end
|
|||||||
--- @param s string
|
--- @param s string
|
||||||
--- @return string
|
--- @return string
|
||||||
local function convert_line_comments_to_block(s)
|
local function convert_line_comments_to_block(s)
|
||||||
--- @type string
|
local result = s ---@type string
|
||||||
local result = s
|
local pos = 1 ---@type integer
|
||||||
--- @type integer
|
local len = #result ---@type integer
|
||||||
local pos = 1
|
|
||||||
--- @type integer
|
|
||||||
local len = #result
|
|
||||||
while pos <= len do
|
while pos <= len do
|
||||||
--- @type boolean
|
local is_double_slash = result:byte(pos) == BYTE_SLASH ---@type boolean
|
||||||
local is_double_slash = result:byte(pos) == BYTE_SLASH
|
|
||||||
and pos + 1 <= len and result:byte(pos + 1) == BYTE_SLASH
|
and pos + 1 <= len and result:byte(pos + 1) == BYTE_SLASH
|
||||||
if not is_double_slash then
|
if not is_double_slash then
|
||||||
pos = pos + 1
|
pos = pos + 1
|
||||||
else
|
else
|
||||||
-- Find end of line.
|
-- Find end of line.
|
||||||
--- @type integer
|
local eol = pos ---@type integer
|
||||||
local eol = pos
|
|
||||||
while eol <= len and result:byte(eol) ~= BYTE_NEWLINE do
|
while eol <= len and result:byte(eol) ~= BYTE_NEWLINE do
|
||||||
eol = eol + 1
|
eol = eol + 1
|
||||||
end
|
end
|
||||||
--- @type string
|
local before = result:sub(1, pos - 1) ---@type string
|
||||||
local before = result:sub(1, pos - 1)
|
local comment = result:sub(pos + 2, eol - 1) ---@type string -- skip the `//`
|
||||||
--- @type string
|
local after ---@type string
|
||||||
local comment = result:sub(pos + 2, eol - 1) -- skip the `//`
|
|
||||||
--- @type string
|
|
||||||
local after
|
|
||||||
if eol <= len and result:byte(eol) == BYTE_NEWLINE then
|
if eol <= len and result:byte(eol) == BYTE_NEWLINE then
|
||||||
after = " */" .. result:sub(eol) -- keep the newline
|
after = " */" .. result:sub(eol) -- keep the newline
|
||||||
else
|
else
|
||||||
@@ -382,14 +339,11 @@ end
|
|||||||
--- @param tok string
|
--- @param tok string
|
||||||
--- @return string
|
--- @return string
|
||||||
local function strip_leading_delay_marker(tok)
|
local function strip_leading_delay_marker(tok)
|
||||||
--- @type string|nil
|
local ident = duffle.read_ident(tok, 1) ---@type string|nil
|
||||||
local ident = duffle.read_ident(tok, 1)
|
|
||||||
if not ident or not duffle.DELAY_MARKERS[ident] then return tok end
|
if not ident or not duffle.DELAY_MARKERS[ident] then return tok end
|
||||||
--- @type string
|
local rest = tok:sub(#ident + 1):match("^%s*(.*)$") or "" ---@type string
|
||||||
local rest = tok:sub(#ident + 1):match("^%s*(.*)$") or ""
|
|
||||||
while rest:sub(1, 2) == "/*" do
|
while rest:sub(1, 2) == "/*" do
|
||||||
--- @type integer|nil
|
local close = rest:find("*/", 3, true) ---@type integer|nil
|
||||||
local close = rest:find("*/", 3, true)
|
|
||||||
if not close then return "" end
|
if not close then return "" end
|
||||||
rest = rest:sub(close + 2):match("^%s*(.*)$") or ""
|
rest = rest:sub(close + 2):match("^%s*(.*)$") or ""
|
||||||
end
|
end
|
||||||
@@ -406,24 +360,17 @@ end
|
|||||||
local function word_count_rec(name, comp_by_name, wc, cache)
|
local function word_count_rec(name, comp_by_name, wc, cache)
|
||||||
if cache[name] ~= nil then return cache[name] end
|
if cache[name] ~= nil then return cache[name] end
|
||||||
cache[name] = -1 -- mark in-progress (cycle detection)
|
cache[name] = -1 -- mark in-progress (cycle detection)
|
||||||
--- @type Component|nil
|
local cc = comp_by_name[name] ---@type Component|nil
|
||||||
local cc = comp_by_name[name]
|
local n ---@type integer
|
||||||
--- @type integer
|
|
||||||
local n
|
|
||||||
if cc then
|
if cc then
|
||||||
n = 0
|
n = 0
|
||||||
--- @type BodyToken[]
|
local tokens = cc.body_tokens ---@type BodyToken[]
|
||||||
local tokens = cc.body_tokens
|
for _, t in ipairs(tokens) do ---@type integer, BodyToken
|
||||||
--- @type integer, BodyToken
|
local trimmed = t.tok ---@type string
|
||||||
for _, t in ipairs(tokens) do
|
|
||||||
--- @type string
|
|
||||||
local trimmed = t.tok
|
|
||||||
if trimmed ~= "" then
|
if trimmed ~= "" then
|
||||||
--- @type string
|
local work = trimmed ---@type string
|
||||||
local work = trimmed
|
|
||||||
while true do
|
while true do
|
||||||
--- @type string|nil
|
local marker = duffle.read_ident(work, 1) ---@type string|nil
|
||||||
local marker = duffle.read_ident(work, 1)
|
|
||||||
if marker and duffle.DELAY_MARKERS[marker] then
|
if marker and duffle.DELAY_MARKERS[marker] then
|
||||||
work = strip_leading_delay_marker(work)
|
work = strip_leading_delay_marker(work)
|
||||||
if work == "" then break end
|
if work == "" then break end
|
||||||
@@ -432,8 +379,7 @@ local function word_count_rec(name, comp_by_name, wc, cache)
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
if work ~= "" then
|
if work ~= "" then
|
||||||
--- @type string|nil
|
local lookup = strip_mac_prefix(duffle.read_ident(work, 1)) ---@type string|nil
|
||||||
local lookup = strip_mac_prefix(duffle.read_ident(work, 1))
|
|
||||||
if lookup == "atom_label" or lookup == "atom_offset" then
|
if lookup == "atom_label" or lookup == "atom_offset" then
|
||||||
-- Pure metaprogram anchors; emit zero words.
|
-- Pure metaprogram anchors; emit zero words.
|
||||||
elseif lookup and comp_by_name[lookup] then
|
elseif lookup and comp_by_name[lookup] then
|
||||||
@@ -466,16 +412,11 @@ end
|
|||||||
--- @param wc WordCounts
|
--- @param wc WordCounts
|
||||||
--- @return table<string, integer> -- bag: bare component name -> word count
|
--- @return table<string, integer> -- bag: bare component name -> word count
|
||||||
local function count_all_components(components, wc)
|
local function count_all_components(components, wc)
|
||||||
--- @type table<string, Component>
|
local comp_by_name = {} ---@type table<string, Component>
|
||||||
local comp_by_name = {}
|
for _, cc in ipairs(components) do comp_by_name[cc.name] = cc end ---@type integer, Component
|
||||||
--- @type integer, Component
|
local cache = {} ---@type table<string, integer> -- bag: memo; -1 in-progress sentinel
|
||||||
for _, cc in ipairs(components) do comp_by_name[cc.name] = cc end
|
local counts = {} ---@type table<string, integer> -- bag: bare name -> word count
|
||||||
--- @type table<string, integer> -- bag: memo; -1 in-progress sentinel
|
for _, c in ipairs(components) do ---@type integer, Component
|
||||||
local cache = {}
|
|
||||||
--- @type table<string, integer> -- bag: bare name -> word count
|
|
||||||
local counts = {}
|
|
||||||
--- @type integer, Component
|
|
||||||
for _, c in ipairs(components) do
|
|
||||||
counts[c.name] = word_count_rec(c.name, comp_by_name, wc, cache)
|
counts[c.name] = word_count_rec(c.name, comp_by_name, wc, cache)
|
||||||
end
|
end
|
||||||
return counts
|
return counts
|
||||||
@@ -504,34 +445,23 @@ end
|
|||||||
local function component_meta_rec(name, comp_by_name, latency, cache)
|
local function component_meta_rec(name, comp_by_name, latency, cache)
|
||||||
if cache[name] ~= nil then return cache[name] end
|
if cache[name] ~= nil then return cache[name] end
|
||||||
cache[name] = { cycle_cost = -1, gp0_contrib = -1 }
|
cache[name] = { cycle_cost = -1, gp0_contrib = -1 }
|
||||||
--- @type Component|nil
|
local cc = comp_by_name[name] ---@type Component|nil
|
||||||
local cc = comp_by_name[name]
|
local cycle_cost ---@type integer
|
||||||
--- @type integer
|
local gp0_contrib ---@type integer
|
||||||
local cycle_cost
|
|
||||||
--- @type integer
|
|
||||||
local gp0_contrib
|
|
||||||
if cc then
|
if cc then
|
||||||
--- @type boolean
|
local skip_cycle = (name == "yield") ---@type boolean
|
||||||
local skip_cycle = (name == "yield")
|
local skip_gp0 = name:match("^insert_ot_tag") ~= nil ---@type boolean
|
||||||
--- @type boolean
|
|
||||||
local skip_gp0 = name:match("^insert_ot_tag") ~= nil
|
|
||||||
cycle_cost = 0
|
cycle_cost = 0
|
||||||
gp0_contrib = 0
|
gp0_contrib = 0
|
||||||
if not skip_cycle or not skip_gp0 then
|
if not skip_cycle or not skip_gp0 then
|
||||||
--- @type BodyToken[]
|
local tokens = cc.body_tokens ---@type BodyToken[]
|
||||||
local tokens = cc.body_tokens
|
for _, t in ipairs(tokens) do ---@type integer, BodyToken
|
||||||
--- @type integer, BodyToken
|
local trimmed = t.tok ---@type string
|
||||||
for _, t in ipairs(tokens) do
|
|
||||||
--- @type string
|
|
||||||
local trimmed = t.tok
|
|
||||||
if trimmed ~= "" then
|
if trimmed ~= "" then
|
||||||
--- @type string|nil
|
local ident = duffle.read_ident(trimmed, 1) ---@type string|nil
|
||||||
local ident = duffle.read_ident(trimmed, 1)
|
|
||||||
if ident and ident:sub(1, MAC_PREFIX_LEN) == MAC_PREFIX then
|
if ident and ident:sub(1, MAC_PREFIX_LEN) == MAC_PREFIX then
|
||||||
--- @type string
|
local nested = ident:sub(MAC_PREFIX_LEN + 1) ---@type string
|
||||||
local nested = ident:sub(MAC_PREFIX_LEN + 1)
|
local nested_meta = component_meta_rec(nested, comp_by_name, latency, cache) ---@type ComponentMeta
|
||||||
--- @type ComponentMeta
|
|
||||||
local nested_meta = component_meta_rec(nested, comp_by_name, latency, cache)
|
|
||||||
if not skip_cycle then
|
if not skip_cycle then
|
||||||
cycle_cost = cycle_cost + nested_meta.cycle_cost
|
cycle_cost = cycle_cost + nested_meta.cycle_cost
|
||||||
end
|
end
|
||||||
@@ -540,10 +470,8 @@ local function component_meta_rec(name, comp_by_name, latency, cache)
|
|||||||
end
|
end
|
||||||
else
|
else
|
||||||
if not skip_cycle then
|
if not skip_cycle then
|
||||||
--- @type InstructionRow|nil
|
local isa = duffle.instr(ident) ---@type InstructionRow|nil
|
||||||
local isa = duffle.instr(ident)
|
local gte = duffle.gte(ident) ---@type GteCommandRow|nil
|
||||||
--- @type GteCommandRow|nil
|
|
||||||
local gte = duffle.gte(ident)
|
|
||||||
cycle_cost = cycle_cost + ((isa and isa.cycles) or (gte and gte.cycles) or latency[ident] or 1)
|
cycle_cost = cycle_cost + ((isa and isa.cycles) or (gte and gte.cycles) or latency[ident] or 1)
|
||||||
end
|
end
|
||||||
if not skip_gp0 then
|
if not skip_gp0 then
|
||||||
@@ -578,16 +506,11 @@ end
|
|||||||
--- @param latency table<string, integer> -- bag: ident -> cycle cost
|
--- @param latency table<string, integer> -- bag: ident -> cycle cost
|
||||||
--- @return ComponentMetaMap
|
--- @return ComponentMetaMap
|
||||||
local function compute_components_metadata(components, latency)
|
local function compute_components_metadata(components, latency)
|
||||||
--- @type table<string, Component>
|
local comp_by_name = {} ---@type table<string, Component>
|
||||||
local comp_by_name = {}
|
for _, cc in ipairs(components) do comp_by_name[cc.name] = cc end ---@type integer, Component
|
||||||
--- @type integer, Component
|
local cache = {} ---@type ComponentMetaMap
|
||||||
for _, cc in ipairs(components) do comp_by_name[cc.name] = cc end
|
local out = {} ---@type ComponentMetaMap
|
||||||
--- @type ComponentMetaMap
|
for _, c in ipairs(components) do ---@type integer, Component
|
||||||
local cache = {}
|
|
||||||
--- @type ComponentMetaMap
|
|
||||||
local out = {}
|
|
||||||
--- @type integer, Component
|
|
||||||
for _, c in ipairs(components) do
|
|
||||||
out[c.name] = component_meta_rec(c.name, comp_by_name, latency, cache)
|
out[c.name] = component_meta_rec(c.name, comp_by_name, latency, cache)
|
||||||
end
|
end
|
||||||
return out
|
return out
|
||||||
@@ -602,15 +525,11 @@ end
|
|||||||
--- @param s string
|
--- @param s string
|
||||||
--- @return string[]
|
--- @return string[]
|
||||||
local function split_comment_lines(s)
|
local function split_comment_lines(s)
|
||||||
--- @type string[]
|
local out = {} ---@type string[]
|
||||||
local out = {}
|
local pos = 1 ---@type integer
|
||||||
--- @type integer
|
local s_len = #s ---@type integer
|
||||||
local pos = 1
|
|
||||||
--- @type integer
|
|
||||||
local s_len = #s
|
|
||||||
while pos <= s_len do
|
while pos <= s_len do
|
||||||
--- @type integer|nil
|
local nl = s:find("\n", pos, true) ---@type integer|nil
|
||||||
local nl = s:find("\n", pos, true)
|
|
||||||
if not nl then
|
if not nl then
|
||||||
out[#out + 1] = s:sub(pos)
|
out[#out + 1] = s:sub(pos)
|
||||||
break
|
break
|
||||||
@@ -629,8 +548,7 @@ end
|
|||||||
--- @param args_str string|nil
|
--- @param args_str string|nil
|
||||||
--- @return string
|
--- @return string
|
||||||
local function signature_from_args(args_str)
|
local function signature_from_args(args_str)
|
||||||
--- @type string[]|nil
|
local names = formal_arg_names(args_str) ---@type string[]|nil
|
||||||
local names = formal_arg_names(args_str)
|
|
||||||
if names then
|
if names then
|
||||||
return table.concat(names, ", ")
|
return table.concat(names, ", ")
|
||||||
end
|
end
|
||||||
@@ -642,8 +560,7 @@ end
|
|||||||
--- @param lines string[]
|
--- @param lines string[]
|
||||||
--- @return nil
|
--- @return nil
|
||||||
local function strip_trailing_continuation(lines)
|
local function strip_trailing_continuation(lines)
|
||||||
--- @type string
|
local last = lines[#lines] ---@type string
|
||||||
local last = lines[#lines]
|
|
||||||
if last:sub(-2) == " \\" then
|
if last:sub(-2) == " \\" then
|
||||||
lines[#lines] = last:sub(1, -3)
|
lines[#lines] = last:sub(1, -3)
|
||||||
end
|
end
|
||||||
@@ -669,37 +586,30 @@ end
|
|||||||
--- @param tok string -- a single token from split_top_level_commas (already trimmed at the start, may contain trailing whitespace + block comment)
|
--- @param tok string -- a single token from split_top_level_commas (already trimmed at the start, may contain trailing whitespace + block comment)
|
||||||
--- @return boolean
|
--- @return boolean
|
||||||
local function is_pure_delay_marker_token(tok)
|
local function is_pure_delay_marker_token(tok)
|
||||||
--- @type table<string, boolean> -- bag: delay-marker ident -> true
|
local markers = duffle.DELAY_MARKERS ---@type table<string, boolean> -- bag: delay-marker ident -> true
|
||||||
local markers = duffle.DELAY_MARKERS
|
|
||||||
if type(markers) ~= "table" then return false end
|
if type(markers) ~= "table" then return false end
|
||||||
|
|
||||||
-- Identify a leading delay-marker identifier (e.g. `GteDelay_`).
|
-- Identify a leading delay-marker identifier (e.g. `GteDelay_`).
|
||||||
--- @type integer
|
local ident_end = 1 ---@type integer
|
||||||
local ident_end = 1
|
|
||||||
while ident_end <= #tok do
|
while ident_end <= #tok do
|
||||||
--- @type string
|
local ch = tok:sub(ident_end, ident_end) ---@type string
|
||||||
local ch = tok:sub(ident_end, ident_end)
|
|
||||||
if ch:match("[%w_]") then
|
if ch:match("[%w_]") then
|
||||||
ident_end = ident_end + 1
|
ident_end = ident_end + 1
|
||||||
else
|
else
|
||||||
break
|
break
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
--- @type string
|
local ident = tok:sub(1, ident_end - 1) ---@type string
|
||||||
local ident = tok:sub(1, ident_end - 1)
|
|
||||||
if not markers[ident] then return false end
|
if not markers[ident] then return false end
|
||||||
|
|
||||||
-- Walk the remainder: only whitespace and block comments are allowed.
|
-- Walk the remainder: only whitespace and block comments are allowed.
|
||||||
--- @type integer
|
local scan = ident_end ---@type integer
|
||||||
local scan = ident_end
|
|
||||||
while scan <= #tok do
|
while scan <= #tok do
|
||||||
--- @type string
|
local ch = tok:sub(scan, scan) ---@type string
|
||||||
local ch = tok:sub(scan, scan)
|
|
||||||
if ch:match("%s") then
|
if ch:match("%s") then
|
||||||
scan = scan + 1
|
scan = scan + 1
|
||||||
elseif ch == "/" and tok:sub(scan + 1, scan + 1) == "*" then
|
elseif ch == "/" and tok:sub(scan + 1, scan + 1) == "*" then
|
||||||
--- @type integer|nil
|
local close = tok:find("*/", scan + 2, true) ---@type integer|nil
|
||||||
local close = tok:find("*/", scan + 2, true)
|
|
||||||
if not close then return false end
|
if not close then return false end
|
||||||
scan = close + 2
|
scan = close + 2
|
||||||
else
|
else
|
||||||
@@ -743,17 +653,14 @@ end
|
|||||||
--- @param tokens string[]
|
--- @param tokens string[]
|
||||||
--- @return nil
|
--- @return nil
|
||||||
local function emit_macro_body(lines, c, sig, tokens)
|
local function emit_macro_body(lines, c, sig, tokens)
|
||||||
--- @type integer
|
for tok_idx = 1, #tokens do ---@type integer
|
||||||
for tok_idx = 1, #tokens do
|
|
||||||
tokens[tok_idx] = convert_line_comments_to_block(tokens[tok_idx])
|
tokens[tok_idx] = convert_line_comments_to_block(tokens[tok_idx])
|
||||||
end
|
end
|
||||||
if #tokens == 0 then return end
|
if #tokens == 0 then return end
|
||||||
lines[#lines + 1] = "#define mac_" .. c.name .. "(" .. sig .. ") \\"
|
lines[#lines + 1] = "#define mac_" .. c.name .. "(" .. sig .. ") \\"
|
||||||
lines[#lines + 1] = "\t" .. tokens[1] .. " \\"
|
lines[#lines + 1] = "\t" .. tokens[1] .. " \\"
|
||||||
--- @type integer
|
for tok_idx = 2, #tokens do ---@type integer
|
||||||
for tok_idx = 2, #tokens do
|
local sep = token_skips_leading_comma(tokens[tok_idx]) and "\t" or ",\t" ---@type string
|
||||||
--- @type string
|
|
||||||
local sep = token_skips_leading_comma(tokens[tok_idx]) and "\t" or ",\t"
|
|
||||||
lines[#lines + 1] = sep .. tokens[tok_idx] .. " \\"
|
lines[#lines + 1] = sep .. tokens[tok_idx] .. " \\"
|
||||||
end
|
end
|
||||||
strip_trailing_continuation(lines)
|
strip_trailing_continuation(lines)
|
||||||
@@ -768,8 +675,7 @@ end
|
|||||||
--- @param counts table<string, integer> -- bag: bare component name -> word count
|
--- @param counts table<string, integer> -- bag: bare component name -> word count
|
||||||
--- @return string[] -- list of lines for this component
|
--- @return string[] -- list of lines for this component
|
||||||
local function build_component_lines(c, counts)
|
local function build_component_lines(c, counts)
|
||||||
--- @type string[]
|
local lines = {} ---@type string[]
|
||||||
local lines = {}
|
|
||||||
|
|
||||||
-- Marker comment: emitted once for every skipped component.
|
-- Marker comment: emitted once for every skipped component.
|
||||||
-- The marker is scanner-owned (declared by `atom_dbg_skip` immediately before the declaration in the source);
|
-- The marker is scanner-owned (declared by `atom_dbg_skip` immediately before the declaration in the source);
|
||||||
@@ -779,21 +685,16 @@ local function build_component_lines(c, counts)
|
|||||||
end
|
end
|
||||||
|
|
||||||
if c.comment and c.comment ~= "" then
|
if c.comment and c.comment ~= "" then
|
||||||
--- @type integer, string
|
for _, line in ipairs(split_comment_lines(c.comment)) do ---@type integer, string
|
||||||
for _, line in ipairs(split_comment_lines(c.comment)) do
|
|
||||||
lines[#lines + 1] = line
|
lines[#lines + 1] = line
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
--- @type string[]
|
local tokens = duffle.split_top_level_commas(c.body) ---@type string[]
|
||||||
local tokens = duffle.split_top_level_commas(c.body)
|
for i = 1, #tokens do tokens[i] = duffle.trim(tokens[i]) end ---@type integer
|
||||||
--- @type integer
|
local sig = signature_from_args(c.args) ---@type string
|
||||||
for i = 1, #tokens do tokens[i] = duffle.trim(tokens[i]) end
|
|
||||||
--- @type string
|
|
||||||
local sig = signature_from_args(c.args)
|
|
||||||
-- Direct lookup against the per-source precomputed `counts` table (built once by count_all_components).
|
-- Direct lookup against the per-source precomputed `counts` table (built once by count_all_components).
|
||||||
--- @type integer
|
local n = counts[c.name] ---@type integer
|
||||||
local n = counts[c.name]
|
|
||||||
|
|
||||||
if n > 0 then
|
if n > 0 then
|
||||||
emit_macro_body(lines, c, sig, tokens)
|
emit_macro_body(lines, c, sig, tokens)
|
||||||
@@ -816,14 +717,11 @@ end
|
|||||||
--- @param sources SourceFile[] -- Sources contributing to this directory (for the header comment)
|
--- @param sources SourceFile[] -- Sources contributing to this directory (for the header comment)
|
||||||
--- @return string[]
|
--- @return string[]
|
||||||
local function header_boilerplate(dir, sources)
|
local function header_boilerplate(dir, sources)
|
||||||
--- @type string[]
|
local source_lines = { "// Directory: " .. duffle.to_absolute_path(dir) .. "/" } ---@type string[]
|
||||||
local source_lines = { "// Directory: " .. duffle.to_absolute_path(dir) .. "/" }
|
for _, src in ipairs(sources) do ---@type integer, SourceFile
|
||||||
--- @type integer, SourceFile
|
|
||||||
for _, src in ipairs(sources) do
|
|
||||||
source_lines[#source_lines + 1] = "// source: " .. duffle.to_absolute_path(src.path)
|
source_lines[#source_lines + 1] = "// source: " .. duffle.to_absolute_path(src.path)
|
||||||
end
|
end
|
||||||
--- @type string
|
local source_blob = table.concat(source_lines, "\n") ---@type string
|
||||||
local source_blob = table.concat(source_lines, "\n")
|
|
||||||
return {
|
return {
|
||||||
-- #pragma once wrapped in #ifdef INTELLISENSE_DIRECTIVES, matching the convention in lottes_tape.h.
|
-- #pragma once wrapped in #ifdef INTELLISENSE_DIRECTIVES, matching the convention in lottes_tape.h.
|
||||||
-- The build does manual unity includes (the user controls include order), so the pragma is only active for IDE/tooling.
|
-- The build does manual unity includes (the user controls include order), so the pragma is only active for IDE/tooling.
|
||||||
@@ -851,10 +749,8 @@ end
|
|||||||
--- @return string -- Output directory
|
--- @return string -- Output directory
|
||||||
--- @return string -- Full output path
|
--- @return string -- Full output path
|
||||||
local function compute_macs_h_path(dir)
|
local function compute_macs_h_path(dir)
|
||||||
--- @type string
|
local out_dir = dir .. "/" .. GEN_SUBDIR ---@type string
|
||||||
local out_dir = dir .. "/" .. GEN_SUBDIR
|
local out_path = out_dir .. "/" .. MACS_FILENAME ---@type string
|
||||||
--- @type string
|
|
||||||
local out_path = out_dir .. "/" .. MACS_FILENAME
|
|
||||||
return out_dir, out_path
|
return out_dir, out_path
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -868,21 +764,16 @@ end
|
|||||||
--- @return string|nil -- Path to the written file (nil if no components)
|
--- @return string|nil -- Path to the written file (nil if no components)
|
||||||
local function emit_component_macros_h(ctx, dir, sources, components, counts)
|
local function emit_component_macros_h(ctx, dir, sources, components, counts)
|
||||||
if #components == 0 then return nil end
|
if #components == 0 then return nil end
|
||||||
--- @type string, string
|
local out_dir, out_path = compute_macs_h_path(dir) ---@type string, string
|
||||||
local out_dir, out_path = compute_macs_h_path(dir)
|
local lines = header_boilerplate(dir, sources) ---@type string[]
|
||||||
--- @type string[]
|
|
||||||
local lines = header_boilerplate(dir, sources)
|
|
||||||
|
|
||||||
--- @type integer, Component
|
for _, c in ipairs(components) do ---@type integer, Component
|
||||||
for _, c in ipairs(components) do
|
for _, l in ipairs(build_component_lines(c, counts)) do ---@type integer, string
|
||||||
--- @type integer, string
|
|
||||||
for _, l in ipairs(build_component_lines(c, counts)) do
|
|
||||||
lines[#lines + 1] = l
|
lines[#lines + 1] = l
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
--- @type string
|
local content = table.concat(lines, "\n") .. "\n" ---@type string
|
||||||
local content = table.concat(lines, "\n") .. "\n"
|
|
||||||
duffle.ensure_dir(out_dir)
|
duffle.ensure_dir(out_dir)
|
||||||
duffle.write_file_lf(out_path, content)
|
duffle.write_file_lf(out_path, content)
|
||||||
print(string.format(" -> %s", out_path))
|
print(string.format(" -> %s", out_path))
|
||||||
@@ -900,12 +791,9 @@ end
|
|||||||
--- @param counts table<string, integer> -- bag: bare component name -> word count
|
--- @param counts table<string, integer> -- bag: bare component name -> word count
|
||||||
--- @return nil
|
--- @return nil
|
||||||
local function update_canonical_word_counts(corpus, components, counts)
|
local function update_canonical_word_counts(corpus, components, counts)
|
||||||
--- @type WordCounts
|
local wc = corpus.word_counts ---@type WordCounts
|
||||||
local wc = corpus.word_counts
|
for _, c in ipairs(components) do ---@type integer, Component
|
||||||
--- @type integer, Component
|
local key = "mac_" .. c.name ---@type string
|
||||||
for _, c in ipairs(components) do
|
|
||||||
--- @type string
|
|
||||||
local key = "mac_" .. c.name
|
|
||||||
if wc[key] == nil then
|
if wc[key] == nil then
|
||||||
wc[key] = counts[c.name]
|
wc[key] = counts[c.name]
|
||||||
end
|
end
|
||||||
@@ -932,15 +820,12 @@ end
|
|||||||
--- @param metadata ComponentMetaMap
|
--- @param metadata ComponentMetaMap
|
||||||
--- @return nil
|
--- @return nil
|
||||||
local function update_canonical_components(corpus, src, components, metadata)
|
local function update_canonical_components(corpus, src, components, metadata)
|
||||||
--- @type string
|
local rel_path = src.path:gsub("\\", "/") ---@type string
|
||||||
local rel_path = src.path:gsub("\\", "/")
|
for _, c in ipairs(components) do ---@type integer, Component
|
||||||
--- @type integer, Component
|
|
||||||
for _, c in ipairs(components) do
|
|
||||||
-- Keyed by bare name (e.g. `yield`, `load_tri_indices`).
|
-- Keyed by bare name (e.g. `yield`, `load_tri_indices`).
|
||||||
-- The atoms_source_map pass looks up components by bare name from the corpus;
|
-- The atoms_source_map pass looks up components by bare name from the corpus;
|
||||||
-- `mac_` prefix lives at the call-site identifier and is stripped before lookup.
|
-- `mac_` prefix lives at the call-site identifier and is stripped before lookup.
|
||||||
--- @type ComponentMeta|nil
|
local m = metadata and metadata[c.name] or nil ---@type ComponentMeta|nil
|
||||||
local m = metadata and metadata[c.name] or nil
|
|
||||||
if corpus.components[c.name] == nil then
|
if corpus.components[c.name] == nil then
|
||||||
corpus.components[c.name] = {
|
corpus.components[c.name] = {
|
||||||
name = c.name,
|
name = c.name,
|
||||||
@@ -954,13 +839,10 @@ local function update_canonical_components(corpus, src, components, metadata)
|
|||||||
else
|
else
|
||||||
-- A second declaration of the same bare name: record a typed collision so static-analysis + the report can surface it.
|
-- A second declaration of the same bare name: record a typed collision so static-analysis + the report can surface it.
|
||||||
-- Identical-shape declarations (same path + line) reuse the first-wins entry without a collision record.
|
-- Identical-shape declarations (same path + line) reuse the first-wins entry without a collision record.
|
||||||
--- @type ComponentDef
|
local existing = corpus.components[c.name] ---@type ComponentDef
|
||||||
local existing = corpus.components[c.name]
|
|
||||||
if existing.path ~= rel_path or existing.line ~= c.line then
|
if existing.path ~= rel_path or existing.line ~= c.line then
|
||||||
--- @type string
|
local kind = c.kind or "comp_bare" ---@type string
|
||||||
local kind = c.kind or "comp_bare"
|
local first_kind = existing.kind or "comp_bare" ---@type string
|
||||||
--- @type string
|
|
||||||
local first_kind = existing.kind or "comp_bare"
|
|
||||||
corpus.collisions[#corpus.collisions + 1] = {
|
corpus.collisions[#corpus.collisions + 1] = {
|
||||||
kind = "component",
|
kind = "component",
|
||||||
name = c.name,
|
name = c.name,
|
||||||
@@ -983,10 +865,8 @@ end
|
|||||||
--- @param scan SourceScan
|
--- @param scan SourceScan
|
||||||
--- @return nil
|
--- @return nil
|
||||||
local function update_canonical_component_body_index(corpus, src, components, scan)
|
local function update_canonical_component_body_index(corpus, src, components, scan)
|
||||||
--- @type (fun(pos: integer): integer)|nil
|
local line_of = scan and scan.line_of ---@type (fun(pos: integer): integer)|nil
|
||||||
local line_of = scan and scan.line_of
|
for _, c in ipairs(components) do ---@type integer, Component
|
||||||
--- @type integer, Component
|
|
||||||
for _, c in ipairs(components) do
|
|
||||||
if corpus.component_body_index[c.name] == nil then
|
if corpus.component_body_index[c.name] == nil then
|
||||||
corpus.component_body_index[c.name] = {
|
corpus.component_body_index[c.name] = {
|
||||||
body_tokens = c.body_tokens,
|
body_tokens = c.body_tokens,
|
||||||
@@ -1004,16 +884,12 @@ end
|
|||||||
--- @param ctx PassCtx
|
--- @param ctx PassCtx
|
||||||
--- @return PassResult
|
--- @return PassResult
|
||||||
function M.run(ctx)
|
function M.run(ctx)
|
||||||
--- @type MacsOutput[]
|
local outputs = {} ---@type MacsOutput[]
|
||||||
local outputs = {}
|
local errors = {} ---@type PassFinding[]
|
||||||
--- @type PassFinding[]
|
local warnings = {} ---@type PassFinding[]
|
||||||
local errors = {}
|
|
||||||
--- @type PassFinding[]
|
|
||||||
local warnings = {}
|
|
||||||
|
|
||||||
-- Corpus ownership gate.
|
-- Corpus ownership gate.
|
||||||
--- @type Corpus|nil
|
local corpus = ctx.shared and ctx.shared.corpus ---@type Corpus|nil
|
||||||
local corpus = ctx.shared and ctx.shared.corpus
|
|
||||||
if type(corpus) ~= "table" then
|
if type(corpus) ~= "table" then
|
||||||
error("components.run requires ctx.shared.corpus.", 0)
|
error("components.run requires ctx.shared.corpus.", 0)
|
||||||
end
|
end
|
||||||
@@ -1034,22 +910,15 @@ function M.run(ctx)
|
|||||||
|
|
||||||
-- Per-directory aggregation: every source in the same directory contributes to one `gen/macs.h`.
|
-- Per-directory aggregation: every source in the same directory contributes to one `gen/macs.h`.
|
||||||
-- The directory itself is the namespace. `corpus.sources_by_dir` preserves source-order within each bucket (matches `corpus.source_order`).
|
-- The directory itself is the namespace. `corpus.sources_by_dir` preserves source-order within each bucket (matches `corpus.source_order`).
|
||||||
--- @type table<string, SourceFile[]>
|
local sources_by_dir = corpus.sources_by_dir or duffle.group_sources_by_dir(corpus.source_order) ---@type table<string, SourceFile[]>
|
||||||
local sources_by_dir = corpus.sources_by_dir or duffle.group_sources_by_dir(corpus.source_order)
|
for dir, sources in pairs(sources_by_dir) do ---@type string, SourceFile[]
|
||||||
--- @type string, SourceFile[]
|
|
||||||
for dir, sources in pairs(sources_by_dir) do
|
|
||||||
-- Aggregate components from every source in this directory.
|
-- Aggregate components from every source in this directory.
|
||||||
-- `project_components` returns nil for sources with no `MipsAtomComp_` declarations; we skip those.
|
-- `project_components` returns nil for sources with no `MipsAtomComp_` declarations; we skip those.
|
||||||
--- @type Component[]
|
local aggregated_components = {} ---@type Component[]
|
||||||
local aggregated_components = {}
|
local metadata_per_source = {} ---@type table<SourceFile, ComponentMetaMap>
|
||||||
--- @type table<SourceFile, ComponentMetaMap>
|
for _, src in ipairs(sources) do ---@type integer, SourceFile
|
||||||
local metadata_per_source = {}
|
local per_source = project_components(src.text, src.scan) or {} ---@type Component[]
|
||||||
--- @type integer, SourceFile
|
for _, c in ipairs(per_source) do ---@type integer, Component
|
||||||
for _, src in ipairs(sources) do
|
|
||||||
--- @type Component[]
|
|
||||||
local per_source = project_components(src.text, src.scan) or {}
|
|
||||||
--- @type integer, Component
|
|
||||||
for _, c in ipairs(per_source) do
|
|
||||||
aggregated_components[#aggregated_components + 1] = c
|
aggregated_components[#aggregated_components + 1] = c
|
||||||
end
|
end
|
||||||
if #per_source > 0 then
|
if #per_source > 0 then
|
||||||
@@ -1059,18 +928,14 @@ function M.run(ctx)
|
|||||||
if #aggregated_components > 0 then
|
if #aggregated_components > 0 then
|
||||||
-- Compute word counts across the aggregated set. `corpus.word_counts` carries the
|
-- Compute word counts across the aggregated set. `corpus.word_counts` carries the
|
||||||
-- same-source + prior-directory entries so the recursive lookup sees both.
|
-- same-source + prior-directory entries so the recursive lookup sees both.
|
||||||
--- @type table<string, integer> -- bag: bare name -> word count
|
local counts = count_all_components(aggregated_components, corpus.word_counts) ---@type table<string, integer> -- bag: bare name -> word count
|
||||||
local counts = count_all_components(aggregated_components, corpus.word_counts)
|
local macs_path = emit_component_macros_h(ctx, dir, sources, aggregated_components, counts) ---@type string|nil
|
||||||
--- @type string|nil
|
|
||||||
local macs_path = emit_component_macros_h(ctx, dir, sources, aggregated_components, counts)
|
|
||||||
if macs_path then
|
if macs_path then
|
||||||
outputs[#outputs + 1] = { macs_h = macs_path }
|
outputs[#outputs + 1] = { macs_h = macs_path }
|
||||||
-- Populate the projections AFTER disk emission (byte-identical `.macs.h` contract).
|
-- Populate the projections AFTER disk emission (byte-identical `.macs.h` contract).
|
||||||
update_canonical_word_counts(corpus, aggregated_components, counts)
|
update_canonical_word_counts(corpus, aggregated_components, counts)
|
||||||
--- @type integer, SourceFile
|
for _, src in ipairs(sources) do ---@type integer, SourceFile
|
||||||
for _, src in ipairs(sources) do
|
local per_source = project_components(src.text, src.scan) or {} ---@type Component[]
|
||||||
--- @type Component[]
|
|
||||||
local per_source = project_components(src.text, src.scan) or {}
|
|
||||||
if #per_source > 0 then
|
if #per_source > 0 then
|
||||||
update_canonical_components(corpus, src, per_source, metadata_per_source[src])
|
update_canonical_components(corpus, src, per_source, metadata_per_source[src])
|
||||||
update_canonical_component_body_index(corpus, src, per_source, src.scan)
|
update_canonical_component_body_index(corpus, src, per_source, src.scan)
|
||||||
|
|||||||
+423
-846
File diff suppressed because it is too large
Load Diff
@@ -117,16 +117,13 @@
|
|||||||
--- @class EmissionModelPass
|
--- @class EmissionModelPass
|
||||||
--- @field run fun(ctx: PassCtx): PassResult
|
--- @field run fun(ctx: PassCtx): PassResult
|
||||||
|
|
||||||
--- @type EmissionModelPass
|
local M = {} ---@type EmissionModelPass
|
||||||
local M = {}
|
|
||||||
|
|
||||||
-- ─────────────────────────────────────────────────────────────────────────
|
-- ─────────────────────────────────────────────────────────────────────────
|
||||||
-- Bootstrap: load `duffle_paths.lua` via debug.getinfo so the module works standalone (run as `luajit passes/emission_model.lua`) and when require'd from the orchestrator.
|
-- Bootstrap: load `duffle_paths.lua` via debug.getinfo so the module works standalone (run as `luajit passes/emission_model.lua`) and when require'd from the orchestrator.
|
||||||
-- ─────────────────────────────────────────────────────────────────────────
|
-- ─────────────────────────────────────────────────────────────────────────
|
||||||
--- @type string
|
local _bootstrap_dir = debug.getinfo(1, "S").source:match("^@?(.*[/\\])") or "./" ---@type string
|
||||||
local _bootstrap_dir = debug.getinfo(1, "S").source:match("^@?(.*[/\\])") or "./"
|
local duffle = dofile(_bootstrap_dir .. "../duffle_paths.lua") ---@type DuffleExport
|
||||||
--- @type DuffleExport
|
|
||||||
local duffle = dofile(_bootstrap_dir .. "../duffle_paths.lua")
|
|
||||||
|
|
||||||
-- ─────────────────────────────────────────────────────────────────────────
|
-- ─────────────────────────────────────────────────────────────────────────
|
||||||
-- Helpers
|
-- Helpers
|
||||||
@@ -149,8 +146,7 @@ local duffle = dofile(_bootstrap_dir .. "../duffle_paths.lua")
|
|||||||
--- @param corpus Corpus
|
--- @param corpus Corpus
|
||||||
--- @return nil
|
--- @return nil
|
||||||
local function stamp_root_provenance(projection, atom_record, src, corpus)
|
local function stamp_root_provenance(projection, atom_record, src, corpus)
|
||||||
--- @type (fun(pos: integer): integer)|nil
|
local root_line_of = src.scan and src.scan.line_of ---@type (fun(pos: integer): integer)|nil
|
||||||
local root_line_of = src.scan and src.scan.line_of
|
|
||||||
assert(type(root_line_of) == "function"
|
assert(type(root_line_of) == "function"
|
||||||
, "emission_model: src.scan.line_of is required (canonical LineIndex closure over the source text) to stamp physical provenance")
|
, "emission_model: src.scan.line_of is required (canonical LineIndex closure over the source text) to stamp physical provenance")
|
||||||
assert(type(atom_record.body_off) == "number"
|
assert(type(atom_record.body_off) == "number"
|
||||||
@@ -158,15 +154,11 @@ local function stamp_root_provenance(projection, atom_record, src, corpus)
|
|||||||
-- `root_body_line` is the physical source line of the ATOM HEADER byte containing the opening `{`; that byte is one byte BEFORE `atom_record.body_off`.
|
-- `root_body_line` is the physical source line of the ATOM HEADER byte containing the opening `{`; that byte is one byte BEFORE `atom_record.body_off`.
|
||||||
-- The walker assigns line 2 to the body's first content line because line 1 is the trailing `\n` after `{`. Body-text line k therefore maps to `root_body_line + (k - 1)`.
|
-- The walker assigns line 2 to the body's first content line because line 1 is the trailing `\n` after `{`. Body-text line k therefore maps to `root_body_line + (k - 1)`.
|
||||||
-- `body_off - 1` points at the opening `{`, whose line index identifies the header line. `body_off` points after `{` and would shift every word row forward by one line.
|
-- `body_off - 1` points at the opening `{`, whose line index identifies the header line. `body_off` points after `{` and would shift every word row forward by one line.
|
||||||
--- @type integer
|
local root_body_line = root_line_of(atom_record.body_off - 1) or atom_record.line or 0 ---@type integer
|
||||||
local root_body_line = root_line_of(atom_record.body_off - 1) or atom_record.line or 0
|
local component_index = corpus.component_body_index or {} ---@type table<string, ComponentBodyEntry>
|
||||||
--- @type table<string, ComponentBodyEntry>
|
local word_items = {} ---@type EmissionItem[]
|
||||||
local component_index = corpus.component_body_index or {}
|
|
||||||
--- @type EmissionItem[]
|
|
||||||
local word_items = {}
|
|
||||||
|
|
||||||
--- @type integer, EmissionItem
|
for _, item in ipairs(projection.items) do ---@type integer, EmissionItem
|
||||||
for _, item in ipairs(projection.items) do
|
|
||||||
if item.kind == "word" then word_items[#word_items + 1] = item end
|
if item.kind == "word" then word_items[#word_items + 1] = item end
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -177,18 +169,14 @@ local function stamp_root_provenance(projection, atom_record, src, corpus)
|
|||||||
--- @param item EmissionItem
|
--- @param item EmissionItem
|
||||||
--- @return integer
|
--- @return integer
|
||||||
local function body_line_for(event, item)
|
local function body_line_for(event, item)
|
||||||
--- @type integer[]
|
local ids = event.invocation_ids or {} ---@type integer[]
|
||||||
local ids = event.invocation_ids or {}
|
|
||||||
-- The innermost open invocation identifies which line index the walker used.
|
-- The innermost open invocation identifies which line index the walker used.
|
||||||
-- A component `line_of` makes `item.line` physical; the atom's `body_text` line index makes it body-relative.
|
-- A component `line_of` makes `item.line` physical; the atom's `body_text` line index makes it body-relative.
|
||||||
if ids and #ids > 0 then
|
if ids and #ids > 0 then
|
||||||
--- @type integer
|
local inner_id = ids[#ids] ---@type integer
|
||||||
local inner_id = ids[#ids]
|
local inner_inv = inner_id and projection.invocations[inner_id] ---@type InvocationRecord|nil
|
||||||
--- @type InvocationRecord|nil
|
|
||||||
local inner_inv = inner_id and projection.invocations[inner_id]
|
|
||||||
if inner_inv then
|
if inner_inv then
|
||||||
--- @type ComponentBodyEntry|nil
|
local component = component_index[inner_inv.component_name] ---@type ComponentBodyEntry|nil
|
||||||
local component = component_index[inner_inv.component_name]
|
|
||||||
if component and component.line_of then
|
if component and component.line_of then
|
||||||
-- Walker used `comp.line_of`, which is the source's physical LineIndex. item.line is already physical.
|
-- Walker used `comp.line_of`, which is the source's physical LineIndex. item.line is already physical.
|
||||||
return item.line or 0
|
return item.line or 0
|
||||||
@@ -203,10 +191,8 @@ local function stamp_root_provenance(projection, atom_record, src, corpus)
|
|||||||
-- Stamp the root source path onto invocation records whose `call_path` the walker left empty.
|
-- Stamp the root source path onto invocation records whose `call_path` the walker left empty.
|
||||||
-- The walker passes `body_entry.source` to `emit_invoke_begin`; `M.project_emission` creates the root `body_entry` with source `""`, leaving its `call_path` empty.
|
-- The walker passes `body_entry.source` to `emit_invoke_begin`; `M.project_emission` creates the root `body_entry` with source `""`, leaving its `call_path` empty.
|
||||||
-- This stamp gives every invocation a physical `call_path` matching `passes/atoms_source_map.lua`'s in-memory provenance projection.
|
-- This stamp gives every invocation a physical `call_path` matching `passes/atoms_source_map.lua`'s in-memory provenance projection.
|
||||||
--- @type string
|
local root_path = src.path or "" ---@type string
|
||||||
local root_path = src.path or ""
|
for _, inv in ipairs(projection.invocations) do ---@type integer, InvocationRecord
|
||||||
--- @type integer, InvocationRecord
|
|
||||||
for _, inv in ipairs(projection.invocations) do
|
|
||||||
if inv.call_path == nil or inv.call_path == "" then
|
if inv.call_path == nil or inv.call_path == "" then
|
||||||
inv.call_path = root_path
|
inv.call_path = root_path
|
||||||
end
|
end
|
||||||
@@ -215,8 +201,7 @@ local function stamp_root_provenance(projection, atom_record, src, corpus)
|
|||||||
-- Normalize `inv.call_line` to a physical source line.
|
-- Normalize `inv.call_line` to a physical source line.
|
||||||
-- * ROOT invocations (`parent_id == 0`) carry body-relative `call_line` values from `M.LineIndex(body_text)`; convert them once with `root_body_line`.
|
-- * ROOT invocations (`parent_id == 0`) carry body-relative `call_line` values from `M.LineIndex(body_text)`; convert them once with `root_body_line`.
|
||||||
-- * INNER invocations (`parent_id ~= 0`) carry physical `call_line` values from the component's `line_of`; retain them unchanged.
|
-- * INNER invocations (`parent_id ~= 0`) carry physical `call_line` values from the component's `line_of`; retain them unchanged.
|
||||||
--- @type integer, InvocationRecord
|
for _, inv in ipairs(projection.invocations) do ---@type integer, InvocationRecord
|
||||||
for _, inv in ipairs(projection.invocations) do
|
|
||||||
if inv.parent_id == 0 then
|
if inv.parent_id == 0 then
|
||||||
inv.call_line = (root_body_line or 0) + (inv.call_line or 1) - 1
|
inv.call_line = (root_body_line or 0) + (inv.call_line or 1) - 1
|
||||||
end
|
end
|
||||||
@@ -225,21 +210,14 @@ local function stamp_root_provenance(projection, atom_record, src, corpus)
|
|||||||
-- Build `body_lines` for each invocation.
|
-- Build `body_lines` for each invocation.
|
||||||
-- `atoms_source_map` and `dwarf_injection` read `inv.body_lines[k]` directly from the invocation record created here.
|
-- `atoms_source_map` and `dwarf_injection` read `inv.body_lines[k]` directly from the invocation record created here.
|
||||||
-- Component words already carry physical `item.line` values from the walker's COMPONENT line index, so `body_line_for` returns them unchanged.
|
-- Component words already carry physical `item.line` values from the walker's COMPONENT line index, so `body_line_for` returns them unchanged.
|
||||||
--- @type integer, InvocationRecord
|
for _, inv in ipairs(projection.invocations) do ---@type integer, InvocationRecord
|
||||||
for _, inv in ipairs(projection.invocations) do
|
local sw = inv.start_word ---@type integer
|
||||||
--- @type integer
|
local ew = inv.end_word ---@type integer
|
||||||
local sw = inv.start_word
|
local bls = {} ---@type integer[]
|
||||||
--- @type integer
|
for i = sw, ew do ---@type integer
|
||||||
local ew = inv.end_word
|
local it = projection.items and projection.items[i] ---@type EmissionItem|nil
|
||||||
--- @type integer[]
|
|
||||||
local bls = {}
|
|
||||||
--- @type integer
|
|
||||||
for i = sw, ew do
|
|
||||||
--- @type EmissionItem|nil
|
|
||||||
local it = projection.items and projection.items[i]
|
|
||||||
if it and it.kind == "word" then
|
if it and it.kind == "word" then
|
||||||
--- @type WordEvent
|
local fake_event = { invocation_ids = { inv.id } } ---@type WordEvent
|
||||||
local fake_event = { invocation_ids = { inv.id } }
|
|
||||||
bls[#bls + 1] = body_line_for(fake_event, it) or 0
|
bls[#bls + 1] = body_line_for(fake_event, it) or 0
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
@@ -249,21 +227,15 @@ local function stamp_root_provenance(projection, atom_record, src, corpus)
|
|||||||
-- Resolve each `word_event`'s physical `body_line` and `call_line`.
|
-- Resolve each `word_event`'s physical `body_line` and `call_line`.
|
||||||
-- For words inside an invocation, `we.call_line` identifies the OUTER atom source line containing the `mac_X(...)` token that triggered expansion.
|
-- For words inside an invocation, `we.call_line` identifies the OUTER atom source line containing the `mac_X(...)` token that triggered expansion.
|
||||||
-- The root-invocation conversion above makes every `inv.call_line` physical; forward it directly and use each raw word's `body_line` as the fallback.
|
-- The root-invocation conversion above makes every `inv.call_line` physical; forward it directly and use each raw word's `body_line` as the fallback.
|
||||||
--- @type integer, WordEvent
|
for index, we in ipairs(projection.word_events) do ---@type integer, WordEvent
|
||||||
for index, we in ipairs(projection.word_events) do
|
local item = word_items[index] or {} ---@type EmissionItem
|
||||||
--- @type EmissionItem
|
local body_line = body_line_for(we, item) ---@type integer
|
||||||
local item = word_items[index] or {}
|
|
||||||
--- @type integer
|
|
||||||
local body_line = body_line_for(we, item)
|
|
||||||
item.line = body_line
|
item.line = body_line
|
||||||
we.body_line = body_line
|
we.body_line = body_line
|
||||||
|
|
||||||
--- @type integer
|
local call_line = body_line ---@type integer
|
||||||
local call_line = body_line
|
local outer_id = we.outermost_invocation_id or 0 ---@type integer
|
||||||
--- @type integer
|
local outer_inv = projection.invocations[outer_id] ---@type InvocationRecord|nil
|
||||||
local outer_id = we.outermost_invocation_id or 0
|
|
||||||
--- @type InvocationRecord|nil
|
|
||||||
local outer_inv = projection.invocations[outer_id]
|
|
||||||
if outer_inv then
|
if outer_inv then
|
||||||
-- `outer_inv.call_line` is physical after the conversion loop above, so use it directly.
|
-- `outer_inv.call_line` is physical after the conversion loop above, so use it directly.
|
||||||
call_line = outer_inv.call_line
|
call_line = outer_inv.call_line
|
||||||
@@ -283,20 +255,15 @@ end
|
|||||||
--- @param corpus Corpus
|
--- @param corpus Corpus
|
||||||
--- @return EmissionProjection
|
--- @return EmissionProjection
|
||||||
local function project_atom(atom_record, src, corpus)
|
local function project_atom(atom_record, src, corpus)
|
||||||
--- @type string
|
local body = atom_record.body or "" ---@type string
|
||||||
local body = atom_record.body or ""
|
local wc = corpus.word_counts or {} ---@type WordCounts
|
||||||
--- @type WordCounts
|
local cbi = corpus.component_body_index or {} ---@type table<string, ComponentBodyEntry>
|
||||||
local wc = corpus.word_counts or {}
|
local schema = nil ---@type RegUseSchema|nil
|
||||||
--- @type table<string, ComponentBodyEntry>
|
|
||||||
local cbi = corpus.component_body_index or {}
|
|
||||||
--- @type RegUseSchema|nil
|
|
||||||
local schema = nil
|
|
||||||
if atom_record.reg_use_schema_name then
|
if atom_record.reg_use_schema_name then
|
||||||
schema = corpus.reg_use_schemas and corpus.reg_use_schemas[atom_record.reg_use_schema_name]
|
schema = corpus.reg_use_schemas and corpus.reg_use_schemas[atom_record.reg_use_schema_name]
|
||||||
end
|
end
|
||||||
-- That construction site stamps `invocation.debug_skip` while appending each record to `proj.invocations`.
|
-- That construction site stamps `invocation.debug_skip` while appending each record to `proj.invocations`.
|
||||||
--- @type EmissionProjection
|
local proj = duffle.project_emission(body, cbi, wc, corpus.components, { ---@type EmissionProjection
|
||||||
local proj = duffle.project_emission(body, cbi, wc, corpus.components, {
|
|
||||||
reg_use_schema = schema,
|
reg_use_schema = schema,
|
||||||
reg_use_param = atom_record.reg_use_param_name,
|
reg_use_param = atom_record.reg_use_param_name,
|
||||||
atom_name = atom_record.name,
|
atom_name = atom_record.name,
|
||||||
@@ -308,14 +275,12 @@ local function project_atom(atom_record, src, corpus)
|
|||||||
msg = string.format("RegUse schema %q is missing", atom_record.reg_use_schema_name),
|
msg = string.format("RegUse schema %q is missing", atom_record.reg_use_schema_name),
|
||||||
}
|
}
|
||||||
end
|
end
|
||||||
--- @type integer, EmitError
|
for _, err in ipairs(corpus.reg_use_errors or {}) do ---@type integer, EmitError
|
||||||
for _, err in ipairs(corpus.reg_use_errors or {}) do
|
|
||||||
if err.schema_name == atom_record.reg_use_schema_name then
|
if err.schema_name == atom_record.reg_use_schema_name then
|
||||||
proj.errors[#proj.errors + 1] = err
|
proj.errors[#proj.errors + 1] = err
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
--- @type AtomPaths
|
local paths = { ---@type AtomPaths
|
||||||
local paths = {
|
|
||||||
tokens = atom_record.body_tokens or {},
|
tokens = atom_record.body_tokens or {},
|
||||||
line_in_body = duffle.build_body_line_index(body),
|
line_in_body = duffle.build_body_line_index(body),
|
||||||
items = proj.items,
|
items = proj.items,
|
||||||
@@ -337,15 +302,11 @@ end
|
|||||||
--- @param ctx PassCtx -- { shared = { corpus = ... }, out_root, ... }
|
--- @param ctx PassCtx -- { shared = { corpus = ... }, out_root, ... }
|
||||||
--- @return PassResult
|
--- @return PassResult
|
||||||
function M.run(ctx)
|
function M.run(ctx)
|
||||||
--- @type PassOutputEntry[]
|
local outputs = {} ---@type PassOutputEntry[]
|
||||||
local outputs = {}
|
local errors = {} ---@type EmitError[]
|
||||||
--- @type EmitError[]
|
local warnings = {} ---@type EmitWarning[]
|
||||||
local errors = {}
|
|
||||||
--- @type EmitWarning[]
|
|
||||||
local warnings = {}
|
|
||||||
|
|
||||||
--- @type Corpus|nil
|
local corpus = ctx and ctx.shared and ctx.shared.corpus ---@type Corpus|nil
|
||||||
local corpus = ctx and ctx.shared and ctx.shared.corpus
|
|
||||||
if type(corpus) ~= "table" then error("emission_model: ctx.shared.corpus is required (canonical projection)", 0) end
|
if type(corpus) ~= "table" then error("emission_model: ctx.shared.corpus is required (canonical projection)", 0) end
|
||||||
if type(corpus.source_order) ~= "table" then error("emission_model: ctx.shared.corpus.source_order is required", 0) end
|
if type(corpus.source_order) ~= "table" then error("emission_model: ctx.shared.corpus.source_order is required", 0) end
|
||||||
|
|
||||||
@@ -356,15 +317,12 @@ function M.run(ctx)
|
|||||||
--- @return nil
|
--- @return nil
|
||||||
local function process_atom(atom, src)
|
local function process_atom(atom, src)
|
||||||
if not (atom and atom.body) then return end
|
if not (atom and atom.body) then return end
|
||||||
--- @type string
|
local kind = atom.kind ---@type string
|
||||||
local kind = atom.kind
|
|
||||||
if kind ~= "atom" and kind ~= "atom_proc" and kind ~= "raw_atom" and kind ~= "comp_bare" and kind ~= "comp_proc" then
|
if kind ~= "atom" and kind ~= "atom_proc" and kind ~= "raw_atom" and kind ~= "comp_bare" and kind ~= "comp_proc" then
|
||||||
return
|
return
|
||||||
end
|
end
|
||||||
--- @type EmissionProjection
|
local proj = project_atom(atom, src, corpus) ---@type EmissionProjection
|
||||||
local proj = project_atom(atom, src, corpus)
|
for _, e in ipairs(proj.errors) do ---@type integer, EmitError
|
||||||
--- @type integer, EmitError
|
|
||||||
for _, e in ipairs(proj.errors) do
|
|
||||||
-- Preserve `kind` (cycle / count_mismatch / unbalanced) so readers dispatch on the diagnostic class and leave the message string as display text.
|
-- Preserve `kind` (cycle / count_mismatch / unbalanced) so readers dispatch on the diagnostic class and leave the message string as display text.
|
||||||
errors[#errors + 1] = {
|
errors[#errors + 1] = {
|
||||||
kind = e.kind,
|
kind = e.kind,
|
||||||
@@ -373,8 +331,7 @@ function M.run(ctx)
|
|||||||
source = e.source or src.path,
|
source = e.source or src.path,
|
||||||
}
|
}
|
||||||
end
|
end
|
||||||
--- @type integer, EmitWarning
|
for _, w in ipairs(proj.warnings) do ---@type integer, EmitWarning
|
||||||
for _, w in ipairs(proj.warnings) do
|
|
||||||
warnings[#warnings + 1] = {
|
warnings[#warnings + 1] = {
|
||||||
kind = w.kind,
|
kind = w.kind,
|
||||||
line = w.line,
|
line = w.line,
|
||||||
@@ -386,16 +343,12 @@ function M.run(ctx)
|
|||||||
-- Walk `corpus.source_order`; within each source, visit atoms followed by raw_atoms.
|
-- Walk `corpus.source_order`; within each source, visit atoms followed by raw_atoms.
|
||||||
-- Recognized kinds (atom | atom_proc | raw_atom | comp_bare | comp_proc) each receive the atom.paths projection via duffle.project_emission.
|
-- Recognized kinds (atom | atom_proc | raw_atom | comp_bare | comp_proc) each receive the atom.paths projection via duffle.project_emission.
|
||||||
-- Components are macros inlined into atom bodies; focused tests and isolated component analyses consume atom.paths directly.
|
-- Components are macros inlined into atom bodies; focused tests and isolated component analyses consume atom.paths directly.
|
||||||
--- @type integer, SourceFile
|
for _, src in ipairs(corpus.source_order) do ---@type integer, SourceFile
|
||||||
for _, src in ipairs(corpus.source_order) do
|
local scan = src.scan or {} ---@type SourceScan
|
||||||
--- @type SourceScan
|
for _, atom in ipairs(scan.atoms or {}) do ---@type integer, AtomEntry
|
||||||
local scan = src.scan or {}
|
|
||||||
--- @type integer, AtomEntry
|
|
||||||
for _, atom in ipairs(scan.atoms or {}) do
|
|
||||||
process_atom(atom, src)
|
process_atom(atom, src)
|
||||||
end
|
end
|
||||||
--- @type integer, AtomEntry
|
for _, atom in ipairs(scan.raw_atoms or {}) do ---@type integer, AtomEntry
|
||||||
for _, atom in ipairs(scan.raw_atoms or {}) do
|
|
||||||
process_atom(atom, src)
|
process_atom(atom, src)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
+37
-74
@@ -20,24 +20,19 @@
|
|||||||
-- Uses `debug.getinfo` to find this file's own directory, so it works both standalone and when require'd from the orchestrator.
|
-- Uses `debug.getinfo` to find this file's own directory, so it works both standalone and when require'd from the orchestrator.
|
||||||
-- Bootstrap: load `duffle_paths.lua` via `debug.getinfo(1, "S").source` (works both standalone + when require'd).
|
-- Bootstrap: load `duffle_paths.lua` via `debug.getinfo(1, "S").source` (works both standalone + when require'd).
|
||||||
-- duffle_paths.lua sets package.path then returns `require("duffle")` at the bottom, so the dofile value IS the duffle module.
|
-- duffle_paths.lua sets package.path then returns `require("duffle")` at the bottom, so the dofile value IS the duffle module.
|
||||||
--- @type string
|
local _bootstrap_dir = debug.getinfo(1, "S").source:match("^@?(.*[/\\])") or "./" ---@type string
|
||||||
local _bootstrap_dir = debug.getinfo(1, "S").source:match("^@?(.*[/\\])") or "./"
|
local duffle = dofile(_bootstrap_dir .. "../duffle_paths.lua") ---@type DuffleExport
|
||||||
--- @type DuffleExport
|
|
||||||
local duffle = dofile(_bootstrap_dir .. "../duffle_paths.lua")
|
|
||||||
|
|
||||||
-- ════════════════════════════════════════════════════════════════════════════
|
-- ════════════════════════════════════════════════════════════════════════════
|
||||||
-- Constants
|
-- Constants
|
||||||
-- ════════════════════════════════════════════════════════════════════════════
|
-- ════════════════════════════════════════════════════════════════════════════
|
||||||
|
|
||||||
-- Offset macro/enum naming prefixes (the emitted header uses these).
|
-- Offset macro/enum naming prefixes (the emitted header uses these).
|
||||||
--- @type string
|
local OFFSET_MACRO_PREFIX = "_atom_offset_" ---@type string
|
||||||
local OFFSET_MACRO_PREFIX = "_atom_offset_"
|
local OFFSET_ENUM_PREFIX = "atom_offset_" ---@type string
|
||||||
--- @type string
|
|
||||||
local OFFSET_ENUM_PREFIX = "atom_offset_"
|
|
||||||
|
|
||||||
-- Column width for the `#define _atom_offset_F_T = N` alignment.
|
-- Column width for the `#define _atom_offset_F_T = N` alignment.
|
||||||
--- @type integer
|
local OFFSET_MACRO_COL = 44 ---@type integer
|
||||||
local OFFSET_MACRO_COL = 44
|
|
||||||
|
|
||||||
-- ════════════════════════════════════════════════════════════════════════════
|
-- ════════════════════════════════════════════════════════════════════════════
|
||||||
-- Type declarations
|
-- Type declarations
|
||||||
@@ -91,8 +86,7 @@ local OFFSET_MACRO_COL = 44
|
|||||||
-- MARKER_PROJECTORS is the marker-kind data table.
|
-- MARKER_PROJECTORS is the marker-kind data table.
|
||||||
-- The emission-model pass already records marker word positions + consuming-instruction context;
|
-- The emission-model pass already records marker word positions + consuming-instruction context;
|
||||||
-- this pass only projects those records into the label/branch lookup shape needed by offset computation.
|
-- this pass only projects those records into the label/branch lookup shape needed by offset computation.
|
||||||
--- @type table<string, fun(state: MarkerProjectState, marker: EmissionMarker): nil>
|
local MARKER_PROJECTORS = { ---@type table<string, fun(state: MarkerProjectState, marker: EmissionMarker): nil>
|
||||||
local MARKER_PROJECTORS = {
|
|
||||||
--- @param state MarkerProjectState
|
--- @param state MarkerProjectState
|
||||||
--- @param marker EmissionMarker
|
--- @param marker EmissionMarker
|
||||||
--- @return nil
|
--- @return nil
|
||||||
@@ -119,12 +113,9 @@ local MARKER_PROJECTORS = {
|
|||||||
--- @return table<string, integer>
|
--- @return table<string, integer>
|
||||||
--- @return OffsetBranch[]
|
--- @return OffsetBranch[]
|
||||||
local function project_markers(markers)
|
local function project_markers(markers)
|
||||||
--- @type MarkerProjectState
|
local state = { labels = {}, branches = {} } ---@type MarkerProjectState
|
||||||
local state = { labels = {}, branches = {} }
|
for _, marker in ipairs(markers or {}) do ---@type integer, EmissionMarker
|
||||||
--- @type integer, EmissionMarker
|
local project = MARKER_PROJECTORS[marker.kind] ---@type (fun(state: MarkerProjectState, marker: EmissionMarker): nil)|nil
|
||||||
for _, marker in ipairs(markers or {}) do
|
|
||||||
--- @type (fun(state: MarkerProjectState, marker: EmissionMarker): nil)|nil
|
|
||||||
local project = MARKER_PROJECTORS[marker.kind]
|
|
||||||
if project then project(state, marker) end
|
if project then project(state, marker) end
|
||||||
end
|
end
|
||||||
return state.labels, state.branches
|
return state.labels, state.branches
|
||||||
@@ -149,20 +140,16 @@ end
|
|||||||
--- @param errors PassFinding[]
|
--- @param errors PassFinding[]
|
||||||
--- @return BranchOffset[]
|
--- @return BranchOffset[]
|
||||||
local function compute_offsets(labels, branches, errors)
|
local function compute_offsets(labels, branches, errors)
|
||||||
--- @type BranchOffset[]
|
local results = {} ---@type BranchOffset[]
|
||||||
local results = {}
|
for _, br in ipairs(branches) do ---@type integer, OffsetBranch
|
||||||
--- @type integer, OffsetBranch
|
local target = labels[br.target] ---@type integer|nil
|
||||||
for _, br in ipairs(branches) do
|
|
||||||
--- @type integer|nil
|
|
||||||
local target = labels[br.target]
|
|
||||||
if not target then
|
if not target then
|
||||||
errors[#errors + 1] = {
|
errors[#errors + 1] = {
|
||||||
line = br.line or 0,
|
line = br.line or 0,
|
||||||
msg = "Branch target '" .. br.target .. "' has no atom_label (at word " .. br.branch_word .. ")",
|
msg = "Branch target '" .. br.target .. "' has no atom_label (at word " .. br.branch_word .. ")",
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
--- @type string|nil
|
local consuming = br.consuming_encoder ---@type string|nil
|
||||||
local consuming = br.consuming_encoder
|
|
||||||
if consuming == nil or consuming == "" then
|
if consuming == nil or consuming == "" then
|
||||||
errors[#errors + 1] = {
|
errors[#errors + 1] = {
|
||||||
line = br.line or 0,
|
line = br.line or 0,
|
||||||
@@ -218,20 +205,16 @@ local function emit_atom_offsets(add, atom)
|
|||||||
if #atom.offsets == 0 then return end
|
if #atom.offsets == 0 then return end
|
||||||
add("// --- atom: " .. atom.name .. " (" .. atom.total_words .. " words) ---")
|
add("// --- atom: " .. atom.name .. " (" .. atom.total_words .. " words) ---")
|
||||||
add("")
|
add("")
|
||||||
--- @type OffsetConst[]
|
local consts = {} ---@type OffsetConst[]
|
||||||
local consts = {}
|
for _, r in ipairs(atom.offsets) do ---@type integer, BranchOffset
|
||||||
--- @type integer, BranchOffset
|
|
||||||
for _, r in ipairs(atom.offsets) do
|
|
||||||
consts[#consts + 1] = make_offset_const(r)
|
consts[#consts + 1] = make_offset_const(r)
|
||||||
end
|
end
|
||||||
--- @type integer, OffsetConst
|
for _, c in ipairs(consts) do ---@type integer, OffsetConst
|
||||||
for _, c in ipairs(consts) do
|
|
||||||
add("#define " .. pad_right(c.macro_name, OFFSET_MACRO_COL) .. " " .. c.value)
|
add("#define " .. pad_right(c.macro_name, OFFSET_MACRO_COL) .. " " .. c.value)
|
||||||
end
|
end
|
||||||
add("")
|
add("")
|
||||||
add("enum {")
|
add("enum {")
|
||||||
--- @type integer, OffsetConst
|
for _, c in ipairs(consts) do ---@type integer, OffsetConst
|
||||||
for _, c in ipairs(consts) do
|
|
||||||
add(" " .. c.enum_name .. " = " .. c.macro_name .. ",")
|
add(" " .. c.enum_name .. " = " .. c.macro_name .. ",")
|
||||||
end
|
end
|
||||||
add("};")
|
add("};")
|
||||||
@@ -244,19 +227,16 @@ end
|
|||||||
--- @param atoms_data AtomData[]
|
--- @param atoms_data AtomData[]
|
||||||
--- @return string
|
--- @return string
|
||||||
local function generate_header(dir, sources, atoms_data)
|
local function generate_header(dir, sources, atoms_data)
|
||||||
--- @type string
|
local dir_basename = duffle.basename_no_ext(dir) ---@type string
|
||||||
local dir_basename = duffle.basename_no_ext(dir)
|
|
||||||
|
|
||||||
--- @type string[]
|
local lines = {} ---@type string[]
|
||||||
local lines = {}
|
|
||||||
--- @param s string
|
--- @param s string
|
||||||
--- @return nil
|
--- @return nil
|
||||||
local function add(s) lines[#lines + 1] = s end
|
local function add(s) lines[#lines + 1] = s end
|
||||||
|
|
||||||
add("// Auto-generated by ps1_meta.lua (passes/offsets.lua) — DO NOT EDIT")
|
add("// Auto-generated by ps1_meta.lua (passes/offsets.lua) — DO NOT EDIT")
|
||||||
add("// Directory: " .. dir:gsub("/", "\\") .. "\\")
|
add("// Directory: " .. dir:gsub("/", "\\") .. "\\")
|
||||||
--- @type integer, SourceFile
|
for _, src in ipairs(sources) do ---@type integer, SourceFile
|
||||||
for _, src in ipairs(sources) do
|
|
||||||
add("// source: " .. src.path:gsub("/", "\\"))
|
add("// source: " .. src.path:gsub("/", "\\"))
|
||||||
end
|
end
|
||||||
add("#pragma once")
|
add("#pragma once")
|
||||||
@@ -264,8 +244,7 @@ local function generate_header(dir, sources, atoms_data)
|
|||||||
add("#pragma region " .. dir_basename)
|
add("#pragma region " .. dir_basename)
|
||||||
add("")
|
add("")
|
||||||
add("")
|
add("")
|
||||||
--- @type integer, AtomData
|
for _, atom in ipairs(atoms_data) do ---@type integer, AtomData
|
||||||
for _, atom in ipairs(atoms_data) do
|
|
||||||
emit_atom_offsets(add, atom)
|
emit_atom_offsets(add, atom)
|
||||||
end
|
end
|
||||||
add("#pragma endregion " .. dir_basename)
|
add("#pragma endregion " .. dir_basename)
|
||||||
@@ -273,8 +252,7 @@ local function generate_header(dir, sources, atoms_data)
|
|||||||
return table.concat(lines, "\n") .. "\n"
|
return table.concat(lines, "\n") .. "\n"
|
||||||
end
|
end
|
||||||
|
|
||||||
--- @type OffsetsPass
|
local M = {} ---@type OffsetsPass
|
||||||
local M = {}
|
|
||||||
|
|
||||||
--- (internal) Aggregate atoms from every source in one directory, render the per-directory `offsets.h`.
|
--- (internal) Aggregate atoms from every source in one directory, render the per-directory `offsets.h`.
|
||||||
--- Returns the offsets_h path if a header was written, or nil.
|
--- Returns the offsets_h path if a header was written, or nil.
|
||||||
@@ -284,17 +262,14 @@ local M = {}
|
|||||||
--- @param errors PassFinding[]
|
--- @param errors PassFinding[]
|
||||||
--- @return string|nil
|
--- @return string|nil
|
||||||
local function process_directory(ctx, dir, sources, errors)
|
local function process_directory(ctx, dir, sources, errors)
|
||||||
--- @type AtomData[]
|
local atoms_data = {} ---@type AtomData[]
|
||||||
local atoms_data = {}
|
|
||||||
|
|
||||||
--- @param atom AtomEntry
|
--- @param atom AtomEntry
|
||||||
--- @return nil
|
--- @return nil
|
||||||
local function append_atom(atom)
|
local function append_atom(atom)
|
||||||
--- @type AtomPaths|nil
|
local paths = atom and atom.paths ---@type AtomPaths|nil
|
||||||
local paths = atom and atom.paths
|
|
||||||
if not paths then return end
|
if not paths then return end
|
||||||
--- @type table<string, integer>, OffsetBranch[]
|
local labels, branches = project_markers(paths.markers) ---@type table<string, integer>, OffsetBranch[]
|
||||||
local labels, branches = project_markers(paths.markers)
|
|
||||||
atoms_data[#atoms_data + 1] = {
|
atoms_data[#atoms_data + 1] = {
|
||||||
name = atom.raw_name or atom.name,
|
name = atom.raw_name or atom.name,
|
||||||
total_words = #(paths.word_events or {}),
|
total_words = #(paths.word_events or {}),
|
||||||
@@ -302,19 +277,14 @@ local function process_directory(ctx, dir, sources, errors)
|
|||||||
}
|
}
|
||||||
end
|
end
|
||||||
|
|
||||||
--- @type integer, SourceFile
|
for _, src in ipairs(sources) do ---@type integer, SourceFile
|
||||||
for _, src in ipairs(sources) do
|
local scan = src.scan or {} ---@type SourceScan
|
||||||
--- @type SourceScan
|
for _, atom in ipairs(scan.atoms or {}) do append_atom(atom) end ---@type integer, AtomEntry
|
||||||
local scan = src.scan or {}
|
for _, atom in ipairs(scan.raw_atoms or {}) do append_atom(atom) end ---@type integer, AtomEntry
|
||||||
--- @type integer, AtomEntry
|
|
||||||
for _, atom in ipairs(scan.atoms or {}) do append_atom(atom) end
|
|
||||||
--- @type integer, AtomEntry
|
|
||||||
for _, atom in ipairs(scan.raw_atoms or {}) do append_atom(atom) end
|
|
||||||
end
|
end
|
||||||
if #atoms_data == 0 then return nil end
|
if #atoms_data == 0 then return nil end
|
||||||
|
|
||||||
--- @type string
|
local out_path = dir .. "/gen/offsets.h" ---@type string
|
||||||
local out_path = dir .. "/gen/offsets.h"
|
|
||||||
duffle.ensure_dir(duffle.dirname(out_path))
|
duffle.ensure_dir(duffle.dirname(out_path))
|
||||||
duffle.write_file(out_path, generate_header(dir, sources, atoms_data))
|
duffle.write_file(out_path, generate_header(dir, sources, atoms_data))
|
||||||
return out_path
|
return out_path
|
||||||
@@ -326,15 +296,11 @@ end
|
|||||||
--- @param ctx PassCtx
|
--- @param ctx PassCtx
|
||||||
--- @return PassResult
|
--- @return PassResult
|
||||||
function M.run(ctx)
|
function M.run(ctx)
|
||||||
--- @type OffsetOutput[]
|
local outputs = {} ---@type OffsetOutput[]
|
||||||
local outputs = {}
|
local errors = {} ---@type PassFinding[]
|
||||||
--- @type PassFinding[]
|
local warnings = {} ---@type PassFinding[]
|
||||||
local errors = {}
|
|
||||||
--- @type PassFinding[]
|
|
||||||
local warnings = {}
|
|
||||||
|
|
||||||
--- @type Corpus|nil
|
local corpus = ctx.shared and ctx.shared.corpus ---@type Corpus|nil
|
||||||
local corpus = ctx.shared and ctx.shared.corpus
|
|
||||||
if type(corpus) ~= "table" then
|
if type(corpus) ~= "table" then
|
||||||
error("offsets.run requires ctx.shared.corpus", 0)
|
error("offsets.run requires ctx.shared.corpus", 0)
|
||||||
end
|
end
|
||||||
@@ -343,12 +309,9 @@ function M.run(ctx)
|
|||||||
end
|
end
|
||||||
|
|
||||||
-- Per-directory aggregation: every source in the same directory contributes to one `gen/offsets.h`.
|
-- Per-directory aggregation: every source in the same directory contributes to one `gen/offsets.h`.
|
||||||
--- @type table<string, SourceFile[]>
|
local sources_by_dir = corpus.sources_by_dir or duffle.group_sources_by_dir(corpus.source_order) ---@type table<string, SourceFile[]>
|
||||||
local sources_by_dir = corpus.sources_by_dir or duffle.group_sources_by_dir(corpus.source_order)
|
for dir, sources in pairs(sources_by_dir) do ---@type string, SourceFile[]
|
||||||
--- @type string, SourceFile[]
|
local out_path = process_directory(ctx, dir, sources, errors) ---@type string|nil
|
||||||
for dir, sources in pairs(sources_by_dir) do
|
|
||||||
--- @type string|nil
|
|
||||||
local out_path = process_directory(ctx, dir, sources, errors)
|
|
||||||
if out_path then
|
if out_path then
|
||||||
outputs[#outputs + 1] = { offsets_h = out_path }
|
outputs[#outputs + 1] = { offsets_h = out_path }
|
||||||
end
|
end
|
||||||
|
|||||||
+186
-372
@@ -18,16 +18,13 @@
|
|||||||
-- Uses `debug.getinfo` to find this file's own directory, so it works both standalone and when require'd from the orchestrator.
|
-- Uses `debug.getinfo` to find this file's own directory, so it works both standalone and when require'd from the orchestrator.
|
||||||
-- Bootstrap: Load `duffle_paths.lua` via `debug.getinfo(1, "S").source` (works both standalone + when require'd).
|
-- Bootstrap: Load `duffle_paths.lua` via `debug.getinfo(1, "S").source` (works both standalone + when require'd).
|
||||||
-- duffle_paths.lua sets package.path then returns `require("duffle")` at the bottom, so the dofile value IS the duffle module.
|
-- duffle_paths.lua sets package.path then returns `require("duffle")` at the bottom, so the dofile value IS the duffle module.
|
||||||
--- @type string
|
local _bootstrap_dir = debug.getinfo(1, "S").source:match("^@?(.*[/\\])") or "./" ---@type string
|
||||||
local _bootstrap_dir = debug.getinfo(1, "S").source:match("^@?(.*[/\\])") or "./"
|
local duffle = dofile(_bootstrap_dir .. "../duffle_paths.lua") ---@type DuffleExport
|
||||||
--- @type DuffleExport
|
|
||||||
local duffle = dofile(_bootstrap_dir .. "../duffle_paths.lua")
|
|
||||||
|
|
||||||
-- Load atoms_source_map for the `render_source_map` / `render_provenance` module functions (used by `render_module_atoms_md` to produce `<module>.atoms.md` without re-walking source tokens).
|
-- Load atoms_source_map for the `render_source_map` / `render_provenance` module functions (used by `render_module_atoms_md` to produce `<module>.atoms.md` without re-walking source tokens).
|
||||||
-- The pass itself emits no per-source files anymore; we only consume the two pure renderers here.
|
-- The pass itself emits no per-source files anymore; we only consume the two pure renderers here.
|
||||||
-- Defined BEFORE the renderer functions below so their upvalues resolve to this local (not the global `atoms_source_map`, which is nil).
|
-- Defined BEFORE the renderer functions below so their upvalues resolve to this local (not the global `atoms_source_map`, which is nil).
|
||||||
--- @type AtomSourceMapPass
|
local atoms_source_map = dofile(_bootstrap_dir .. "atoms_source_map.lua") ---@type AtomSourceMapPass
|
||||||
local atoms_source_map = dofile(_bootstrap_dir .. "atoms_source_map.lua")
|
|
||||||
|
|
||||||
-- ════════════════════════════════════════════════════════════════════════════
|
-- ════════════════════════════════════════════════════════════════════════════
|
||||||
-- Constants
|
-- Constants
|
||||||
@@ -35,32 +32,22 @@ local atoms_source_map = dofile(_bootstrap_dir .. "atoms_source_map.lua")
|
|||||||
|
|
||||||
-- Section separators used in the rendered text reports.
|
-- Section separators used in the rendered text reports.
|
||||||
-- The thin rules are hand-tuned to align with the per-section content width; do not change without also checking the section renderers below.
|
-- The thin rules are hand-tuned to align with the per-section content width; do not change without also checking the section renderers below.
|
||||||
--- @type string
|
local RULE_THICK = "========================================================" ---@type string
|
||||||
local RULE_THICK = "========================================================"
|
local SECTION_HEADER_ATOMS = "── Atoms ────────────────────────────────────────────────" ---@type string
|
||||||
--- @type string
|
local SECTION_HEADER_ANNOTS = "── Annotations ──────────────────────────────────────────" ---@type string
|
||||||
local SECTION_HEADER_ATOMS = "── Atoms ────────────────────────────────────────────────"
|
local SECTION_HEADER_BINDS = "── Binds_* structs ──────────────────────────────────────" ---@type string
|
||||||
--- @type string
|
local SECTION_HEADER_MACROS = "── Macro word-count declarations ─────────────────────────" ---@type string
|
||||||
local SECTION_HEADER_ANNOTS = "── Annotations ──────────────────────────────────────────"
|
local SECTION_HEADER_ERRORS = "── Errors ──────────────────────────────────────────────" ---@type string
|
||||||
--- @type string
|
local SECTION_HEADER_WARNINGS = "── Warnings ────────────────────────────────────────────" ---@type string
|
||||||
local SECTION_HEADER_BINDS = "── Binds_* structs ──────────────────────────────────────"
|
|
||||||
--- @type string
|
|
||||||
local SECTION_HEADER_MACROS = "── Macro word-count declarations ─────────────────────────"
|
|
||||||
--- @type string
|
|
||||||
local SECTION_HEADER_ERRORS = "── Errors ──────────────────────────────────────────────"
|
|
||||||
--- @type string
|
|
||||||
local SECTION_HEADER_WARNINGS = "── Warnings ────────────────────────────────────────────"
|
|
||||||
|
|
||||||
-- Lua pattern that captures the basename (last path segment) of a forward- or back-slash separated path.
|
-- Lua pattern that captures the basename (last path segment) of a forward- or back-slash separated path.
|
||||||
--- @type string
|
local BASENAME_PATTERN = "([^/\\]+)$" ---@type string
|
||||||
local BASENAME_PATTERN = "([^/\\]+)$"
|
|
||||||
|
|
||||||
-- Debug flag name — set to truthy in `_G` to enable verbose logging.
|
-- Debug flag name — set to truthy in `_G` to enable verbose logging.
|
||||||
--- @type string
|
local DEBUG_FLAG = "_DEBUG_REPORT" ---@type string
|
||||||
local DEBUG_FLAG = "_DEBUG_REPORT"
|
|
||||||
|
|
||||||
-- Pass identifier for log messages.
|
-- Pass identifier for log messages.
|
||||||
--- @type string
|
local PASS_NAME = "report" ---@type string
|
||||||
local PASS_NAME = "report"
|
|
||||||
|
|
||||||
-- ════════════════════════════════════════════════════════════════════════════
|
-- ════════════════════════════════════════════════════════════════════════════
|
||||||
-- Type declarations
|
-- Type declarations
|
||||||
@@ -245,18 +232,15 @@ end
|
|||||||
--- @param all_results ProjectSummaryRow[]
|
--- @param all_results ProjectSummaryRow[]
|
||||||
--- @return string
|
--- @return string
|
||||||
local function render_project_summary(all_results)
|
local function render_project_summary(all_results)
|
||||||
--- @type string[]
|
local lines = { ---@type string[]
|
||||||
local lines = {
|
|
||||||
"# Project summary",
|
"# Project summary",
|
||||||
"> Auto-generated by ps1_meta.lua (passes/report.lua).",
|
"> Auto-generated by ps1_meta.lua (passes/report.lua).",
|
||||||
"",
|
"",
|
||||||
"| module | atoms | annots | binds | macros | findings | errors | warnings | info |",
|
"| module | atoms | annots | binds | macros | findings | errors | warnings | info |",
|
||||||
"|--------|-------|--------|-------|--------|----------|--------|----------|------|",
|
"|--------|-------|--------|-------|--------|----------|--------|----------|------|",
|
||||||
}
|
}
|
||||||
--- @type ProjectSummaryTotals
|
local totals = { atoms = 0, annots = 0, binds = 0, macros = 0, findings = 0, errors = 0, warnings = 0, info = 0 } ---@type ProjectSummaryTotals
|
||||||
local totals = { atoms = 0, annots = 0, binds = 0, macros = 0, findings = 0, errors = 0, warnings = 0, info = 0 }
|
for _, e in ipairs(all_results) do ---@type integer, ProjectSummaryRow
|
||||||
--- @type integer, ProjectSummaryRow
|
|
||||||
for _, e in ipairs(all_results) do
|
|
||||||
lines[#lines + 1] = string.format("| %s | %d | %d | %d | %d | %d | %d | %d | %d |"
|
lines[#lines + 1] = string.format("| %s | %d | %d | %d | %d | %d | %d | %d | %d |"
|
||||||
, e.module, e.atoms, e.annots, e.binds, e.macros, e.findings, e.errors, e.warnings, e.info)
|
, e.module, e.atoms, e.annots, e.binds, e.macros, e.findings, e.errors, e.warnings, e.info)
|
||||||
totals.atoms = totals.atoms + e.atoms
|
totals.atoms = totals.atoms + e.atoms
|
||||||
@@ -281,29 +265,22 @@ end
|
|||||||
--- @param wc WordCounts
|
--- @param wc WordCounts
|
||||||
--- @return string
|
--- @return string
|
||||||
local function render_module_atoms_md(dir, dir_sources, wc)
|
local function render_module_atoms_md(dir, dir_sources, wc)
|
||||||
--- @type string
|
local dir_basename = source_basename(dir) ---@type string
|
||||||
local dir_basename = source_basename(dir)
|
local lines = { ---@type string[]
|
||||||
--- @type string[]
|
|
||||||
local lines = {
|
|
||||||
"# " .. dir_basename .. " — atoms (verbose source map)",
|
"# " .. dir_basename .. " — atoms (verbose source map)",
|
||||||
"> Per-word call-site + provenance. Auto-generated.",
|
"> Per-word call-site + provenance. Auto-generated.",
|
||||||
"",
|
"",
|
||||||
}
|
}
|
||||||
--- @type integer, SourceFile
|
for _, src in ipairs(dir_sources) do ---@type integer, SourceFile
|
||||||
for _, src in ipairs(dir_sources) do
|
local src_name = source_basename(src.path) ---@type string
|
||||||
--- @type string
|
|
||||||
local src_name = source_basename(src.path)
|
|
||||||
lines[#lines + 1] = "## " .. src_name
|
lines[#lines + 1] = "## " .. src_name
|
||||||
lines[#lines + 1] = ""
|
lines[#lines + 1] = ""
|
||||||
-- For each atom with a projection, render its sourcemap + provenance.
|
-- For each atom with a projection, render its sourcemap + provenance.
|
||||||
--- @type AtomEntry[]
|
local atoms_list = {} ---@type AtomEntry[]
|
||||||
local atoms_list = {}
|
for _, atom in ipairs((src.scan or {}).atoms or {}) do ---@type integer, AtomEntry
|
||||||
--- @type integer, AtomEntry
|
|
||||||
for _, atom in ipairs((src.scan or {}).atoms or {}) do
|
|
||||||
if atom.paths then atoms_list[#atoms_list + 1] = atom end
|
if atom.paths then atoms_list[#atoms_list + 1] = atom end
|
||||||
end
|
end
|
||||||
--- @type integer, AtomEntry
|
for _, atom in ipairs((src.scan or {}).raw_atoms or {}) do ---@type integer, AtomEntry
|
||||||
for _, atom in ipairs((src.scan or {}).raw_atoms or {}) do
|
|
||||||
if atom.paths then atoms_list[#atoms_list + 1] = atom end
|
if atom.paths then atoms_list[#atoms_list + 1] = atom end
|
||||||
end
|
end
|
||||||
if #atoms_list == 0 then
|
if #atoms_list == 0 then
|
||||||
@@ -312,10 +289,8 @@ local function render_module_atoms_md(dir, dir_sources, wc)
|
|||||||
else
|
else
|
||||||
-- Per-source forward-slash path (same one `emit_atom_stanza` / `emit_provenance_stanza` would derive;
|
-- Per-source forward-slash path (same one `emit_atom_stanza` / `emit_provenance_stanza` would derive;
|
||||||
-- computed once per `## <source>` heading and reused by each atom's `WORD N CALL ...` field).
|
-- computed once per `## <source>` heading and reused by each atom's `WORD N CALL ...` field).
|
||||||
--- @type string
|
local rel_path = src.path:gsub("\\\\", "/") ---@type string
|
||||||
local rel_path = src.path:gsub("\\\\", "/")
|
for _, atom in ipairs(atoms_list) do ---@type integer, AtomEntry
|
||||||
--- @type integer, AtomEntry
|
|
||||||
for _, atom in ipairs(atoms_list) do
|
|
||||||
lines[#lines + 1] = string.format(
|
lines[#lines + 1] = string.format(
|
||||||
"### atom: %s (line %d, %d words)",
|
"### atom: %s (line %d, %d words)",
|
||||||
atom.name, atom.line or 0, #((atom.paths or {}).word_events or {}))
|
atom.name, atom.line or 0, #((atom.paths or {}).word_events or {}))
|
||||||
@@ -342,18 +317,15 @@ end
|
|||||||
--- @param atom AtomEntry
|
--- @param atom AtomEntry
|
||||||
--- @return integer
|
--- @return integer
|
||||||
local function decl_words(atom)
|
local function decl_words(atom)
|
||||||
--- @type AtomPaths
|
local p = atom.paths or {} ---@type AtomPaths
|
||||||
local p = atom.paths or {}
|
|
||||||
return #(p.word_events or {})
|
return #(p.word_events or {})
|
||||||
end
|
end
|
||||||
|
|
||||||
--- @param decls AtomEntry[]|nil
|
--- @param decls AtomEntry[]|nil
|
||||||
--- @return KindCounts
|
--- @return KindCounts
|
||||||
local function count_kinds(decls)
|
local function count_kinds(decls)
|
||||||
--- @type KindCounts
|
local n = { atom = 0, atom_proc = 0, comp_bare = 0, comp_proc = 0 } ---@type KindCounts
|
||||||
local n = { atom = 0, atom_proc = 0, comp_bare = 0, comp_proc = 0 }
|
for _, a in ipairs(decls or {}) do ---@type integer, AtomEntry
|
||||||
--- @type integer, AtomEntry
|
|
||||||
for _, a in ipairs(decls or {}) do
|
|
||||||
if n[a.kind] ~= nil then n[a.kind] = n[a.kind] + 1 end
|
if n[a.kind] ~= nil then n[a.kind] = n[a.kind] + 1 end
|
||||||
end
|
end
|
||||||
return n
|
return n
|
||||||
@@ -369,10 +341,8 @@ end
|
|||||||
--- @param view ModuleView
|
--- @param view ModuleView
|
||||||
--- @return table<string, boolean>
|
--- @return table<string, boolean>
|
||||||
local function decl_names(view)
|
local function decl_names(view)
|
||||||
--- @type table<string, boolean> -- bag: atom name -> true
|
local names = {} ---@type table<string, boolean> -- bag: atom name -> true
|
||||||
local names = {}
|
for _, a in ipairs(view.decls or {}) do ---@type integer, AtomEntry
|
||||||
--- @type integer, AtomEntry
|
|
||||||
for _, a in ipairs(view.decls or {}) do
|
|
||||||
if a.name then names[a.name] = true end
|
if a.name then names[a.name] = true end
|
||||||
end
|
end
|
||||||
return names
|
return names
|
||||||
@@ -383,15 +353,12 @@ end
|
|||||||
--- @return boolean
|
--- @return boolean
|
||||||
local function path_in_module(path, view)
|
local function path_in_module(path, view)
|
||||||
if type(path) ~= "string" or path == "" then return false end
|
if type(path) ~= "string" or path == "" then return false end
|
||||||
--- @type string
|
local norm = path:gsub("\\", "/") ---@type string
|
||||||
local norm = path:gsub("\\", "/")
|
local dir = (view.dir or ""):gsub("\\", "/") ---@type string
|
||||||
--- @type string
|
|
||||||
local dir = (view.dir or ""):gsub("\\", "/")
|
|
||||||
if dir ~= "" and (norm == dir or norm:sub(1, #dir + 1) == dir .. "/") then
|
if dir ~= "" and (norm == dir or norm:sub(1, #dir + 1) == dir .. "/") then
|
||||||
return true
|
return true
|
||||||
end
|
end
|
||||||
--- @type integer, SourceFile
|
for _, src in ipairs(view.sources or {}) do ---@type integer, SourceFile
|
||||||
for _, src in ipairs(view.sources or {}) do
|
|
||||||
if (src.path or ""):gsub("\\", "/") == norm then return true end
|
if (src.path or ""):gsub("\\", "/") == norm then return true end
|
||||||
end
|
end
|
||||||
return false
|
return false
|
||||||
@@ -402,26 +369,18 @@ end
|
|||||||
--- @param corpus Corpus
|
--- @param corpus Corpus
|
||||||
--- @return ModuleView
|
--- @return ModuleView
|
||||||
local function build_module_view(dir, dir_sources, corpus)
|
local function build_module_view(dir, dir_sources, corpus)
|
||||||
--- @type AtomEntry[]
|
local decls = {} ---@type AtomEntry[]
|
||||||
local decls = {}
|
for _, src in ipairs(dir_sources or {}) do ---@type integer, SourceFile
|
||||||
--- @type integer, SourceFile
|
for _, a in ipairs((src.scan and src.scan.atoms) or {}) do ---@type integer, AtomEntry
|
||||||
for _, src in ipairs(dir_sources or {}) do
|
|
||||||
--- @type integer, AtomEntry
|
|
||||||
for _, a in ipairs((src.scan and src.scan.atoms) or {}) do
|
|
||||||
if not a.source_path then a.source_path = src.path end
|
if not a.source_path then a.source_path = src.path end
|
||||||
decls[#decls + 1] = a
|
decls[#decls + 1] = a
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
--- @type string
|
local dir_basename = source_basename(dir) ---@type string
|
||||||
local dir_basename = source_basename(dir)
|
local sa = (corpus.static_analysis_results or {})[dir_basename] or {} ---@type AtomAnalysis
|
||||||
--- @type AtomAnalysis
|
local schemas = {} ---@type RegUseSchema[]
|
||||||
local sa = (corpus.static_analysis_results or {})[dir_basename] or {}
|
for name, schema in pairs(corpus.reg_use_schemas or {}) do ---@type string, RegUseSchema
|
||||||
--- @type RegUseSchema[]
|
for _, a in ipairs(decls) do ---@type integer, AtomEntry
|
||||||
local schemas = {}
|
|
||||||
--- @type string, RegUseSchema
|
|
||||||
for name, schema in pairs(corpus.reg_use_schemas or {}) do
|
|
||||||
--- @type integer, AtomEntry
|
|
||||||
for _, a in ipairs(decls) do
|
|
||||||
if a.reg_use_schema_name == name then
|
if a.reg_use_schema_name == name then
|
||||||
schemas[#schemas + 1] = schema
|
schemas[#schemas + 1] = schema
|
||||||
break
|
break
|
||||||
@@ -445,10 +404,8 @@ local function render_section_declarations(add, view)
|
|||||||
if #view.decls == 0 then add("_(none)_"); add(""); return end
|
if #view.decls == 0 then add("_(none)_"); add(""); return end
|
||||||
add("| kind | name | source | line | words | min | max | branches | paths |")
|
add("| kind | name | source | line | words | min | max | branches | paths |")
|
||||||
add("|------|------|--------|------|-------|-----|-----|----------|-------|")
|
add("|------|------|--------|------|-------|-----|-----|----------|-------|")
|
||||||
--- @type integer, AtomEntry
|
for _, a in ipairs(view.decls) do ---@type integer, AtomEntry
|
||||||
for _, a in ipairs(view.decls) do
|
local p = a.paths or {} ---@type AtomPaths
|
||||||
--- @type AtomPaths
|
|
||||||
local p = a.paths or {}
|
|
||||||
add(string.format("| %s | %s | %s | %d | %d | %s | %s | %s | %s |",
|
add(string.format("| %s | %s | %s | %d | %d | %s | %s | %s | %s |",
|
||||||
a.kind or "?",
|
a.kind or "?",
|
||||||
a.name or "?",
|
a.name or "?",
|
||||||
@@ -467,17 +424,12 @@ end
|
|||||||
--- @param view ModuleView
|
--- @param view ModuleView
|
||||||
--- @return nil
|
--- @return nil
|
||||||
local function render_section_components(add, view)
|
local function render_section_components(add, view)
|
||||||
--- @type ComponentReportRow[]
|
local rows = {} ---@type ComponentReportRow[]
|
||||||
local rows = {}
|
local index = (view.corpus and view.corpus.component_body_index) or {} ---@type table<string, ComponentBodyEntry>
|
||||||
--- @type table<string, ComponentBodyEntry>
|
for _, a in ipairs(view.decls) do ---@type integer, AtomEntry
|
||||||
local index = (view.corpus and view.corpus.component_body_index) or {}
|
|
||||||
--- @type integer, AtomEntry
|
|
||||||
for _, a in ipairs(view.decls) do
|
|
||||||
if a.kind == "comp_bare" or a.kind == "comp_proc" then
|
if a.kind == "comp_bare" or a.kind == "comp_proc" then
|
||||||
--- @type ComponentBodyEntry
|
local idx = index[a.name] or {} ---@type ComponentBodyEntry
|
||||||
local idx = index[a.name] or {}
|
local args = idx.arg_names or {} ---@type string[]
|
||||||
--- @type string[]
|
|
||||||
local args = idx.arg_names or {}
|
|
||||||
rows[#rows + 1] = {
|
rows[#rows + 1] = {
|
||||||
name = a.name,
|
name = a.name,
|
||||||
kind = a.kind,
|
kind = a.kind,
|
||||||
@@ -490,8 +442,7 @@ local function render_section_components(add, view)
|
|||||||
if #rows == 0 then add("_(none)_"); add(""); return end
|
if #rows == 0 then add("_(none)_"); add(""); return end
|
||||||
add("| name | kind | arg_names | words | map |")
|
add("| name | kind | arg_names | words | map |")
|
||||||
add("|------|------|-----------|-------|-----|")
|
add("|------|------|-----------|-------|-----|")
|
||||||
--- @type integer, ComponentReportRow
|
for _, r in ipairs(rows) do ---@type integer, ComponentReportRow
|
||||||
for _, r in ipairs(rows) do
|
|
||||||
add(string.format("| %s | %s | %s | %d | %s |",
|
add(string.format("| %s | %s | %s | %d | %s |",
|
||||||
r.name, r.kind, r.args ~= "" and r.args or "—", r.words, r.map))
|
r.name, r.kind, r.args ~= "" and r.args or "—", r.words, r.map))
|
||||||
end
|
end
|
||||||
@@ -502,38 +453,28 @@ end
|
|||||||
--- @param view ModuleView
|
--- @param view ModuleView
|
||||||
--- @return nil
|
--- @return nil
|
||||||
local function render_section_reguse(add, view)
|
local function render_section_reguse(add, view)
|
||||||
--- @type boolean
|
local wrote = false ---@type boolean
|
||||||
local wrote = false
|
for _, schema in ipairs(view.schemas or {}) do ---@type integer, RegUseSchema
|
||||||
--- @type integer, RegUseSchema
|
|
||||||
for _, schema in ipairs(view.schemas or {}) do
|
|
||||||
wrote = true
|
wrote = true
|
||||||
add(string.format("### %s", schema.name or "?"))
|
add(string.format("### %s", schema.name or "?"))
|
||||||
--- @type integer, RegUseSlot
|
for _, slot in ipairs(schema.slots or {}) do ---@type integer, RegUseSlot
|
||||||
for _, slot in ipairs(schema.slots or {}) do
|
local aliases = table.concat(slot.aliases or { slot.name }, ", ") ---@type string
|
||||||
--- @type string
|
local ro = slot.readonly and " readonly" or "" ---@type string
|
||||||
local aliases = table.concat(slot.aliases or { slot.name }, ", ")
|
|
||||||
--- @type string
|
|
||||||
local ro = slot.readonly and " readonly" or ""
|
|
||||||
add(string.format("- slot `%s` aliases %s%s", slot.name, aliases, ro))
|
add(string.format("- slot `%s` aliases %s%s", slot.name, aliases, ro))
|
||||||
end
|
end
|
||||||
--- @type integer, AtomEntry
|
for _, a in ipairs(view.decls) do ---@type integer, AtomEntry
|
||||||
for _, a in ipairs(view.decls) do
|
|
||||||
if a.reg_use_schema_name == schema.name then
|
if a.reg_use_schema_name == schema.name then
|
||||||
add(string.format("- bound `%s` param `%s`", a.name, a.reg_use_param_name or "?"))
|
add(string.format("- bound `%s` param `%s`", a.name, a.reg_use_param_name or "?"))
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
add("")
|
add("")
|
||||||
end
|
end
|
||||||
--- @type table<string, boolean> -- bag: schema name -> true
|
local bound = {} ---@type table<string, boolean> -- bag: schema name -> true
|
||||||
local bound = {}
|
for _, schema in ipairs(view.schemas or {}) do ---@type integer, RegUseSchema
|
||||||
--- @type integer, RegUseSchema
|
|
||||||
for _, schema in ipairs(view.schemas or {}) do
|
|
||||||
if schema.name then bound[schema.name] = true end
|
if schema.name then bound[schema.name] = true end
|
||||||
end
|
end
|
||||||
--- @type RegUseError[]
|
local errors = {} ---@type RegUseError[]
|
||||||
local errors = {}
|
for _, err in ipairs((view.corpus and view.corpus.reg_use_errors) or {}) do ---@type integer, RegUseError
|
||||||
--- @type integer, RegUseError
|
|
||||||
for _, err in ipairs((view.corpus and view.corpus.reg_use_errors) or {}) do
|
|
||||||
if bound[err.schema_name] or path_in_module(err.source_file, view) then
|
if bound[err.schema_name] or path_in_module(err.source_file, view) then
|
||||||
errors[#errors + 1] = err
|
errors[#errors + 1] = err
|
||||||
end
|
end
|
||||||
@@ -541,8 +482,7 @@ local function render_section_reguse(add, view)
|
|||||||
if #errors > 0 then
|
if #errors > 0 then
|
||||||
wrote = true
|
wrote = true
|
||||||
add("### parse errors")
|
add("### parse errors")
|
||||||
--- @type integer, RegUseError
|
for _, err in ipairs(errors) do ---@type integer, RegUseError
|
||||||
for _, err in ipairs(errors) do
|
|
||||||
add(string.format("- `%s` %s", err.kind or "?", err.schema_name or ""))
|
add(string.format("- `%s` %s", err.kind or "?", err.schema_name or ""))
|
||||||
end
|
end
|
||||||
add("")
|
add("")
|
||||||
@@ -554,12 +494,9 @@ end
|
|||||||
--- @param view ModuleView
|
--- @param view ModuleView
|
||||||
--- @return nil
|
--- @return nil
|
||||||
local function render_section_annotations(add, view)
|
local function render_section_annotations(add, view)
|
||||||
--- @type AnnotReportRow[]
|
local rows = {} ---@type AnnotReportRow[]
|
||||||
local rows = {}
|
for _, src in ipairs(view.sources) do ---@type integer, SourceFile
|
||||||
--- @type integer, SourceFile
|
for _, info in ipairs((src.scan and src.scan.atom_infos) or {}) do ---@type integer, AtomInfoEntry
|
||||||
for _, src in ipairs(view.sources) do
|
|
||||||
--- @type integer, AtomInfoEntry
|
|
||||||
for _, info in ipairs((src.scan and src.scan.atom_infos) or {}) do
|
|
||||||
rows[#rows + 1] = {
|
rows[#rows + 1] = {
|
||||||
source = source_basename(src.path),
|
source = source_basename(src.path),
|
||||||
line = info.info_line or 0,
|
line = info.info_line or 0,
|
||||||
@@ -574,8 +511,7 @@ local function render_section_annotations(add, view)
|
|||||||
if #rows == 0 then add("_(none)_"); add(""); return end
|
if #rows == 0 then add("_(none)_"); add(""); return end
|
||||||
add("| source | line | name | binds | reads | writes | phase |")
|
add("| source | line | name | binds | reads | writes | phase |")
|
||||||
add("|--------|------|------|-------|-------|--------|-------|")
|
add("|--------|------|------|-------|-------|--------|-------|")
|
||||||
--- @type integer, AnnotReportRow
|
for _, r in ipairs(rows) do ---@type integer, AnnotReportRow
|
||||||
for _, r in ipairs(rows) do
|
|
||||||
add(string.format("| %s | %d | %s | %s | %s | %s | %s |",
|
add(string.format("| %s | %d | %s | %s | %s | %s | %s |",
|
||||||
r.source, r.line, r.name, r.binds, r.reads, r.writes, r.phase))
|
r.source, r.line, r.name, r.binds, r.reads, r.writes, r.phase))
|
||||||
end
|
end
|
||||||
@@ -586,12 +522,9 @@ end
|
|||||||
--- @param view ModuleView
|
--- @param view ModuleView
|
||||||
--- @return nil
|
--- @return nil
|
||||||
local function render_section_component_annotations(add, view)
|
local function render_section_component_annotations(add, view)
|
||||||
--- @type CompAnnotReportRow[]
|
local rows = {} ---@type CompAnnotReportRow[]
|
||||||
local rows = {}
|
for _, src in ipairs(view.sources) do ---@type integer, SourceFile
|
||||||
--- @type integer, SourceFile
|
for _, info in ipairs((src.scan and src.scan.component_atom_infos) or {}) do ---@type integer, AtomInfoEntry
|
||||||
for _, src in ipairs(view.sources) do
|
|
||||||
--- @type integer, AtomInfoEntry
|
|
||||||
for _, info in ipairs((src.scan and src.scan.component_atom_infos) or {}) do
|
|
||||||
rows[#rows + 1] = {
|
rows[#rows + 1] = {
|
||||||
source = source_basename(src.path),
|
source = source_basename(src.path),
|
||||||
line = info.info_line or 0,
|
line = info.info_line or 0,
|
||||||
@@ -604,8 +537,7 @@ local function render_section_component_annotations(add, view)
|
|||||||
if #rows == 0 then add("_(none)_"); add(""); return end
|
if #rows == 0 then add("_(none)_"); add(""); return end
|
||||||
add("| source | line | name | reads | writes |")
|
add("| source | line | name | reads | writes |")
|
||||||
add("|--------|------|------|-------|--------|")
|
add("|--------|------|------|-------|--------|")
|
||||||
--- @type integer, CompAnnotReportRow
|
for _, r in ipairs(rows) do ---@type integer, CompAnnotReportRow
|
||||||
for _, r in ipairs(rows) do
|
|
||||||
add(string.format("| %s | %d | %s | %s | %s |",
|
add(string.format("| %s | %d | %s | %s | %s |",
|
||||||
r.source, r.line, r.name, r.reads, r.writes))
|
r.source, r.line, r.name, r.reads, r.writes))
|
||||||
end
|
end
|
||||||
@@ -616,17 +548,13 @@ end
|
|||||||
--- @param view ModuleView
|
--- @param view ModuleView
|
||||||
--- @return nil
|
--- @return nil
|
||||||
local function render_section_binds(add, view)
|
local function render_section_binds(add, view)
|
||||||
--- @type boolean
|
local wrote = false ---@type boolean
|
||||||
local wrote = false
|
for _, src in ipairs(view.sources) do ---@type integer, SourceFile
|
||||||
--- @type integer, SourceFile
|
for _, b in ipairs((src.scan and src.scan.binds) or {}) do ---@type integer, BindsEntry
|
||||||
for _, src in ipairs(view.sources) do
|
|
||||||
--- @type integer, BindsEntry
|
|
||||||
for _, b in ipairs((src.scan and src.scan.binds) or {}) do
|
|
||||||
wrote = true
|
wrote = true
|
||||||
add(string.format("### %s (%s:%s, %s bytes)",
|
add(string.format("### %s (%s:%s, %s bytes)",
|
||||||
b.name, source_basename(src.path), tostring(b.line or 0), tostring(b.bytes or "—")))
|
b.name, source_basename(src.path), tostring(b.line or 0), tostring(b.bytes or "—")))
|
||||||
--- @type integer, TypeField
|
for _, f in ipairs(b.fields or {}) do ---@type integer, TypeField
|
||||||
for _, f in ipairs(b.fields or {}) do
|
|
||||||
add(string.format("- `+%s %s`", tostring(f.offset or "?"), f.name or "?"))
|
add(string.format("- `+%s %s`", tostring(f.offset or "?"), f.name or "?"))
|
||||||
end
|
end
|
||||||
add("")
|
add("")
|
||||||
@@ -639,18 +567,12 @@ end
|
|||||||
--- @param view ModuleView
|
--- @param view ModuleView
|
||||||
--- @return nil
|
--- @return nil
|
||||||
local function render_section_phases(add, view)
|
local function render_section_phases(add, view)
|
||||||
--- @type Corpus
|
local corpus = view.corpus or {} ---@type Corpus
|
||||||
local corpus = view.corpus or {}
|
local names = decl_names(view) ---@type table<string, boolean>
|
||||||
--- @type table<string, boolean>
|
local wrote = false ---@type boolean
|
||||||
local names = decl_names(view)
|
for phase, entry in pairs(corpus.atom_phases or {}) do ---@type string, AtomPhaseGroup
|
||||||
--- @type boolean
|
local here = {} ---@type string[]
|
||||||
local wrote = false
|
for _, atom_name in ipairs(entry.atoms or {}) do ---@type integer, string
|
||||||
--- @type string, AtomPhaseGroup
|
|
||||||
for phase, entry in pairs(corpus.atom_phases or {}) do
|
|
||||||
--- @type string[]
|
|
||||||
local here = {}
|
|
||||||
--- @type integer, string
|
|
||||||
for _, atom_name in ipairs(entry.atoms or {}) do
|
|
||||||
if names[atom_name] then here[#here + 1] = atom_name end
|
if names[atom_name] then here[#here + 1] = atom_name end
|
||||||
end
|
end
|
||||||
if #here > 0 then
|
if #here > 0 then
|
||||||
@@ -658,15 +580,13 @@ local function render_section_phases(add, view)
|
|||||||
add(string.format("- phase `%s`: %s", phase, table.concat(here, ", ")))
|
add(string.format("- phase `%s`: %s", phase, table.concat(here, ", ")))
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
--- @type string, AtomViewEntry
|
for name, entry in pairs(corpus.atom_views or {}) do ---@type string, AtomViewEntry
|
||||||
for name, entry in pairs(corpus.atom_views or {}) do
|
|
||||||
if names[name] then
|
if names[name] then
|
||||||
wrote = true
|
wrote = true
|
||||||
add(string.format("- view `%s` binds `%s`", name, entry.binds_name or "—"))
|
add(string.format("- view `%s` binds `%s`", name, entry.binds_name or "—"))
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
--- @type string, AtomCtxEntry
|
for name, entry in pairs(corpus.atom_ctxs or {}) do ---@type string, AtomCtxEntry
|
||||||
for name, entry in pairs(corpus.atom_ctxs or {}) do
|
|
||||||
if names[name] then
|
if names[name] then
|
||||||
wrote = true
|
wrote = true
|
||||||
add(string.format("- ctx `%s` rbind `%s`", name, entry.rbind_atom or "—"))
|
add(string.format("- ctx `%s` rbind `%s`", name, entry.rbind_atom or "—"))
|
||||||
@@ -680,14 +600,10 @@ end
|
|||||||
--- @param view ModuleView
|
--- @param view ModuleView
|
||||||
--- @return nil
|
--- @return nil
|
||||||
local function render_section_aliases(add, view)
|
local function render_section_aliases(add, view)
|
||||||
--- @type string[]
|
local names = {} ---@type string[]
|
||||||
local names = {}
|
local seen = {} ---@type table<string, AliasEntry>
|
||||||
--- @type table<string, AliasEntry>
|
for _, src in ipairs(view.sources or {}) do ---@type integer, SourceFile
|
||||||
local seen = {}
|
for name, entry in pairs((src.scan and src.scan.register_alias_registry) or {}) do ---@type string, AliasEntry
|
||||||
--- @type integer, SourceFile
|
|
||||||
for _, src in ipairs(view.sources or {}) do
|
|
||||||
--- @type string, AliasEntry
|
|
||||||
for name, entry in pairs((src.scan and src.scan.register_alias_registry) or {}) do
|
|
||||||
if not seen[name] then
|
if not seen[name] then
|
||||||
seen[name] = entry
|
seen[name] = entry
|
||||||
names[#names + 1] = name
|
names[#names + 1] = name
|
||||||
@@ -698,10 +614,8 @@ local function render_section_aliases(add, view)
|
|||||||
if #names == 0 then add("_(none)_"); add(""); return end
|
if #names == 0 then add("_(none)_"); add(""); return end
|
||||||
add("| alias | type |")
|
add("| alias | type |")
|
||||||
add("|-------|------|")
|
add("|-------|------|")
|
||||||
--- @type integer, string
|
for _, name in ipairs(names) do ---@type integer, string
|
||||||
for _, name in ipairs(names) do
|
local e = seen[name] ---@type AliasEntry|nil
|
||||||
--- @type AliasEntry|nil
|
|
||||||
local e = seen[name]
|
|
||||||
add(string.format("| %s | %s |", name, (e and e.default_type) or "—"))
|
add(string.format("| %s | %s |", name, (e and e.default_type) or "—"))
|
||||||
end
|
end
|
||||||
add("")
|
add("")
|
||||||
@@ -711,40 +625,30 @@ end
|
|||||||
--- @param view ModuleView
|
--- @param view ModuleView
|
||||||
--- @return nil
|
--- @return nil
|
||||||
local function render_section_autoreg(add, view)
|
local function render_section_autoreg(add, view)
|
||||||
--- @type table<string, boolean>
|
local allowed = decl_names(view) ---@type table<string, boolean>
|
||||||
local allowed = decl_names(view)
|
for phase, entry in pairs((view.corpus and view.corpus.atom_phases) or {}) do ---@type string, AtomPhaseGroup
|
||||||
--- @type string, AtomPhaseGroup
|
for _, atom_name in ipairs(entry.atoms or {}) do ---@type integer, string
|
||||||
for phase, entry in pairs((view.corpus and view.corpus.atom_phases) or {}) do
|
|
||||||
--- @type integer, string
|
|
||||||
for _, atom_name in ipairs(entry.atoms or {}) do
|
|
||||||
if allowed[atom_name] then allowed[phase] = true end
|
if allowed[atom_name] then allowed[phase] = true end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
--- @type boolean
|
local wrote = false ---@type boolean
|
||||||
local wrote = false
|
local seen = {} ---@type table<string, boolean> -- bag: label\\0scope -> already dumped
|
||||||
--- @type table<string, boolean> -- bag: label\\0scope -> already dumped
|
|
||||||
local seen = {}
|
|
||||||
--- @param label string
|
--- @param label string
|
||||||
--- @param table_map table<string, GprAllocMap>|nil
|
--- @param table_map table<string, GprAllocMap>|nil
|
||||||
--- @return nil
|
--- @return nil
|
||||||
local function dump(label, table_map)
|
local function dump(label, table_map)
|
||||||
--- @type string[]
|
local scopes = {} ---@type string[]
|
||||||
local scopes = {}
|
for scope in pairs(table_map or {}) do ---@type string
|
||||||
--- @type string
|
|
||||||
for scope in pairs(table_map or {}) do
|
|
||||||
if allowed[scope] and not seen[label .. "\0" .. scope] then
|
if allowed[scope] and not seen[label .. "\0" .. scope] then
|
||||||
scopes[#scopes + 1] = scope
|
scopes[#scopes + 1] = scope
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
table.sort(scopes)
|
table.sort(scopes)
|
||||||
--- @type integer, string
|
for _, scope in ipairs(scopes) do ---@type integer, string
|
||||||
for _, scope in ipairs(scopes) do
|
|
||||||
seen[label .. "\0" .. scope] = true
|
seen[label .. "\0" .. scope] = true
|
||||||
wrote = true
|
wrote = true
|
||||||
--- @type string[]
|
local syms = {} ---@type string[]
|
||||||
local syms = {}
|
for sym, gpr in pairs(table_map[scope] or {}) do ---@type string, string
|
||||||
--- @type string, string
|
|
||||||
for sym, gpr in pairs(table_map[scope] or {}) do
|
|
||||||
if type(gpr) == "string" and gpr ~= sym then
|
if type(gpr) == "string" and gpr ~= sym then
|
||||||
syms[#syms + 1] = string.format("%s → %s", sym, gpr)
|
syms[#syms + 1] = string.format("%s → %s", sym, gpr)
|
||||||
else
|
else
|
||||||
@@ -755,12 +659,10 @@ local function render_section_autoreg(add, view)
|
|||||||
add(string.format("- %s `%s`: %s", label, scope, table.concat(syms, ", ")))
|
add(string.format("- %s `%s`: %s", label, scope, table.concat(syms, ", ")))
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
--- @type Corpus
|
local corpus = view.corpus or {} ---@type Corpus
|
||||||
local corpus = view.corpus or {}
|
|
||||||
dump("atom", corpus.atom_auto_regs)
|
dump("atom", corpus.atom_auto_regs)
|
||||||
dump("phase", corpus.phase_auto_regs)
|
dump("phase", corpus.phase_auto_regs)
|
||||||
--- @type integer, SourceFile
|
for _, src in ipairs(view.sources or {}) do ---@type integer, SourceFile
|
||||||
for _, src in ipairs(view.sources or {}) do
|
|
||||||
dump("atom", src.scan and src.scan.atom_auto_regs)
|
dump("atom", src.scan and src.scan.atom_auto_regs)
|
||||||
dump("phase", src.scan and src.scan.phase_auto_regs)
|
dump("phase", src.scan and src.scan.phase_auto_regs)
|
||||||
end
|
end
|
||||||
@@ -772,25 +674,18 @@ end
|
|||||||
--- @param view ModuleView
|
--- @param view ModuleView
|
||||||
--- @return nil
|
--- @return nil
|
||||||
local function render_section_collisions(add, view)
|
local function render_section_collisions(add, view)
|
||||||
--- @type CorpusCollision[]
|
local rows = {} ---@type CorpusCollision[]
|
||||||
local rows = {}
|
for _, c in ipairs((view.corpus and view.corpus.collisions) or {}) do ---@type integer, CorpusCollision
|
||||||
--- @type integer, CorpusCollision
|
local first = c.first_site or {} ---@type CollisionSite
|
||||||
for _, c in ipairs((view.corpus and view.corpus.collisions) or {}) do
|
local other = c.conflicting_site or {} ---@type CollisionSite
|
||||||
--- @type CollisionSite
|
|
||||||
local first = c.first_site or {}
|
|
||||||
--- @type CollisionSite
|
|
||||||
local other = c.conflicting_site or {}
|
|
||||||
if path_in_module(first.path, view) or path_in_module(other.path, view) then
|
if path_in_module(first.path, view) or path_in_module(other.path, view) then
|
||||||
rows[#rows + 1] = c
|
rows[#rows + 1] = c
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
if #rows == 0 then add("_(none)_"); add(""); return end
|
if #rows == 0 then add("_(none)_"); add(""); return end
|
||||||
--- @type integer, CorpusCollision
|
for _, c in ipairs(rows) do ---@type integer, CorpusCollision
|
||||||
for _, c in ipairs(rows) do
|
local first = c.first_site or {} ---@type CollisionSite
|
||||||
--- @type CollisionSite
|
local other = c.conflicting_site or {} ---@type CollisionSite
|
||||||
local first = c.first_site or {}
|
|
||||||
--- @type CollisionSite
|
|
||||||
local other = c.conflicting_site or {}
|
|
||||||
add(string.format("- `%s` `%s` first %s:%s conflict %s:%s",
|
add(string.format("- `%s` `%s` first %s:%s conflict %s:%s",
|
||||||
c.kind or "?", c.name or "?",
|
c.kind or "?", c.name or "?",
|
||||||
tostring(first.path or "?"), tostring(first.line or "?"),
|
tostring(first.path or "?"), tostring(first.line or "?"),
|
||||||
@@ -803,29 +698,22 @@ end
|
|||||||
--- @param view ModuleView
|
--- @param view ModuleView
|
||||||
--- @return nil
|
--- @return nil
|
||||||
local function render_section_findings(add, view)
|
local function render_section_findings(add, view)
|
||||||
--- @type table<string, CheckFinding[]>
|
local by_atom = {} ---@type table<string, CheckFinding[]>
|
||||||
local by_atom = {}
|
for _, f in ipairs(view.findings or {}) do ---@type integer, CheckFinding
|
||||||
--- @type integer, CheckFinding
|
local key = f.atom or "?" ---@type string
|
||||||
for _, f in ipairs(view.findings or {}) do
|
|
||||||
--- @type string
|
|
||||||
local key = f.atom or "?"
|
|
||||||
by_atom[key] = by_atom[key] or {}
|
by_atom[key] = by_atom[key] or {}
|
||||||
by_atom[key][#by_atom[key] + 1] = f
|
by_atom[key][#by_atom[key] + 1] = f
|
||||||
end
|
end
|
||||||
if next(by_atom) == nil then add("_(none)_"); add(""); return end
|
if next(by_atom) == nil then add("_(none)_"); add(""); return end
|
||||||
--- @type table<string, boolean> -- bag: atom name already emitted
|
local seen = {} ---@type table<string, boolean> -- bag: atom name already emitted
|
||||||
local seen = {}
|
|
||||||
--- @param name string
|
--- @param name string
|
||||||
--- @param fs CheckFinding[]
|
--- @param fs CheckFinding[]
|
||||||
--- @return nil
|
--- @return nil
|
||||||
local function emit(name, fs)
|
local function emit(name, fs)
|
||||||
add("### " .. name)
|
add("### " .. name)
|
||||||
--- @type integer, CheckFinding
|
for _, f in ipairs(fs) do ---@type integer, CheckFinding
|
||||||
for _, f in ipairs(fs) do
|
local msg = f.msg or "" ---@type string
|
||||||
--- @type string
|
local slot = slot_suffix(f.gpr_key or f.producer_destination) ---@type string|nil
|
||||||
local msg = f.msg or ""
|
|
||||||
--- @type string|nil
|
|
||||||
local slot = slot_suffix(f.gpr_key or f.producer_destination)
|
|
||||||
if slot and not msg:find("(slot ", 1, true) then
|
if slot and not msg:find("(slot ", 1, true) then
|
||||||
msg = msg .. " (slot " .. slot .. ")"
|
msg = msg .. " (slot " .. slot .. ")"
|
||||||
end
|
end
|
||||||
@@ -833,45 +721,34 @@ local function render_section_findings(add, view)
|
|||||||
end
|
end
|
||||||
add("")
|
add("")
|
||||||
end
|
end
|
||||||
--- @type integer, AtomEntry
|
for _, a in ipairs(view.decls) do ---@type integer, AtomEntry
|
||||||
for _, a in ipairs(view.decls) do
|
|
||||||
if by_atom[a.name] then
|
if by_atom[a.name] then
|
||||||
seen[a.name] = true
|
seen[a.name] = true
|
||||||
emit(a.name, by_atom[a.name])
|
emit(a.name, by_atom[a.name])
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
--- @type string[]
|
local leftovers = {} ---@type string[]
|
||||||
local leftovers = {}
|
for name in pairs(by_atom) do ---@type string
|
||||||
--- @type string
|
|
||||||
for name in pairs(by_atom) do
|
|
||||||
if not seen[name] then leftovers[#leftovers + 1] = name end
|
if not seen[name] then leftovers[#leftovers + 1] = name end
|
||||||
end
|
end
|
||||||
table.sort(leftovers)
|
table.sort(leftovers)
|
||||||
--- @type integer, string
|
for _, name in ipairs(leftovers) do emit(name, by_atom[name]) end ---@type integer, string
|
||||||
for _, name in ipairs(leftovers) do emit(name, by_atom[name]) end
|
|
||||||
end
|
end
|
||||||
|
|
||||||
--- @param add fun(s: string): nil
|
--- @param add fun(s: string): nil
|
||||||
--- @param view ModuleView
|
--- @param view ModuleView
|
||||||
--- @return nil
|
--- @return nil
|
||||||
local function render_section_relations(add, view)
|
local function render_section_relations(add, view)
|
||||||
--- @type boolean
|
local wrote = false ---@type boolean
|
||||||
local wrote = false
|
for _, a in ipairs(view.decls) do ---@type integer, AtomEntry
|
||||||
--- @type integer, AtomEntry
|
local rels = (a.paths and a.paths.relations) or {} ---@type AtomRelation[]
|
||||||
for _, a in ipairs(view.decls) do
|
|
||||||
--- @type AtomRelation[]
|
|
||||||
local rels = (a.paths and a.paths.relations) or {}
|
|
||||||
if #rels > 0 then
|
if #rels > 0 then
|
||||||
wrote = true
|
wrote = true
|
||||||
add("### " .. a.name)
|
add("### " .. a.name)
|
||||||
--- @type integer, AtomRelation
|
for _, rel in ipairs(rels) do ---@type integer, AtomRelation
|
||||||
for _, rel in ipairs(rels) do
|
local dest = rel.destination or rel.producer_destination or "—" ---@type string
|
||||||
--- @type string
|
local slot = slot_suffix(dest) ---@type string|nil
|
||||||
local dest = rel.destination or rel.producer_destination or "—"
|
local dest_s = tostring(dest) ---@type string
|
||||||
--- @type string|nil
|
|
||||||
local slot = slot_suffix(dest)
|
|
||||||
--- @type string
|
|
||||||
local dest_s = tostring(dest)
|
|
||||||
if slot then dest_s = dest_s .. " (slot " .. slot .. ")" end
|
if slot then dest_s = dest_s .. " (slot " .. slot .. ")" end
|
||||||
add(string.format("- `%s` words %s → %s dest %s",
|
add(string.format("- `%s` words %s → %s dest %s",
|
||||||
rel.semantic or "?",
|
rel.semantic or "?",
|
||||||
@@ -885,13 +762,11 @@ local function render_section_relations(add, view)
|
|||||||
if not wrote then add("_(none)_"); add("") end
|
if not wrote then add("_(none)_"); add("") end
|
||||||
end
|
end
|
||||||
|
|
||||||
--- @type table<string, boolean> -- bag: GPR key hidden unless an encoder wrote it
|
local HIDDEN_UNLESS_WRITTEN = { ---@type table<string, boolean> -- bag: GPR key hidden unless an encoder wrote it
|
||||||
local HIDDEN_UNLESS_WRITTEN = {
|
|
||||||
R_AT = true, R_TapePtr = true, R_AtomJmp = true,
|
R_AT = true, R_TapePtr = true, R_AtomJmp = true,
|
||||||
}
|
}
|
||||||
|
|
||||||
--- @type table<string, boolean> -- bag: physical GPR alias -> true
|
local PHYSICAL_GPR = { ---@type table<string, boolean> -- bag: physical GPR alias -> true
|
||||||
local PHYSICAL_GPR = {
|
|
||||||
R_T0 = true, R_T1 = true, R_T2 = true, R_T3 = true,
|
R_T0 = true, R_T1 = true, R_T2 = true, R_T3 = true,
|
||||||
R_T4 = true, R_T5 = true, R_T6 = true, R_T7 = true,
|
R_T4 = true, R_T5 = true, R_T6 = true, R_T7 = true,
|
||||||
R_V0 = true, R_V1 = true,
|
R_V0 = true, R_V1 = true,
|
||||||
@@ -901,10 +776,8 @@ local PHYSICAL_GPR = {
|
|||||||
--- @param key string
|
--- @param key string
|
||||||
--- @return boolean
|
--- @return boolean
|
||||||
local function encoder_wrote_key(atom, key)
|
local function encoder_wrote_key(atom, key)
|
||||||
--- @type integer, WordEvent
|
for _, ev in ipairs((atom.paths and atom.paths.word_events) or {}) do ---@type integer, WordEvent
|
||||||
for _, ev in ipairs((atom.paths and atom.paths.word_events) or {}) do
|
for _, dest in pairs(ev.gpr_keys or {}) do ---@type integer|string, string
|
||||||
--- @type integer|string, string
|
|
||||||
for _, dest in pairs(ev.gpr_keys or {}) do
|
|
||||||
if dest == key then return true end
|
if dest == key then return true end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
@@ -915,11 +788,9 @@ end
|
|||||||
--- @param atom AtomEntry
|
--- @param atom AtomEntry
|
||||||
--- @return string
|
--- @return string
|
||||||
local function written_name_for(key, atom)
|
local function written_name_for(key, atom)
|
||||||
--- @type string|nil
|
local slot = key:match("^reguse:.+:(.+)$") ---@type string|nil
|
||||||
local slot = key:match("^reguse:.+:(.+)$")
|
|
||||||
if slot then
|
if slot then
|
||||||
--- @type string|nil
|
local param = atom.reg_use_param_name ---@type string|nil
|
||||||
local param = atom.reg_use_param_name
|
|
||||||
if param and param ~= "" then return param .. "." .. slot end
|
if param and param ~= "" then return param .. "." .. slot end
|
||||||
return slot
|
return slot
|
||||||
end
|
end
|
||||||
@@ -931,21 +802,15 @@ end
|
|||||||
--- @param view ModuleView
|
--- @param view ModuleView
|
||||||
--- @return string
|
--- @return string
|
||||||
local function aliases_for_key(key, atom, view)
|
local function aliases_for_key(key, atom, view)
|
||||||
--- @type string|nil
|
local slot = key:match("^reguse:.+:(.+)$") ---@type string|nil
|
||||||
local slot = key:match("^reguse:.+:(.+)$")
|
|
||||||
if not slot then return "—" end
|
if not slot then return "—" end
|
||||||
--- @type string|nil
|
local schema_name = atom.reg_use_schema_name ---@type string|nil
|
||||||
local schema_name = atom.reg_use_schema_name
|
local schema = view.corpus and view.corpus.reg_use_schemas and view.corpus.reg_use_schemas[schema_name] ---@type RegUseSchema|nil
|
||||||
--- @type RegUseSchema|nil
|
|
||||||
local schema = view.corpus and view.corpus.reg_use_schemas and view.corpus.reg_use_schemas[schema_name]
|
|
||||||
if not schema then return "—" end
|
if not schema then return "—" end
|
||||||
--- @type integer, RegUseSlot
|
for _, s in ipairs(schema.slots or {}) do ---@type integer, RegUseSlot
|
||||||
for _, s in ipairs(schema.slots or {}) do
|
|
||||||
if s.name == slot then
|
if s.name == slot then
|
||||||
--- @type string[]
|
local names = {} ---@type string[]
|
||||||
local names = {}
|
for _, alias in ipairs(s.aliases or {}) do ---@type integer, string
|
||||||
--- @type integer, string
|
|
||||||
for _, alias in ipairs(s.aliases or {}) do
|
|
||||||
if alias ~= slot then names[#names + 1] = alias end
|
if alias ~= slot then names[#names + 1] = alias end
|
||||||
end
|
end
|
||||||
if #names == 0 then
|
if #names == 0 then
|
||||||
@@ -964,25 +829,19 @@ end
|
|||||||
--- @return string
|
--- @return string
|
||||||
local function physical_for_key(key, atom, view)
|
local function physical_for_key(key, atom, view)
|
||||||
if PHYSICAL_GPR[key] then return key end
|
if PHYSICAL_GPR[key] then return key end
|
||||||
--- @type Corpus
|
local corpus = view.corpus or {} ---@type Corpus
|
||||||
local corpus = view.corpus or {}
|
local alias = (corpus.register_alias_registry or {})[key] ---@type AliasEntry|string|nil
|
||||||
--- @type AliasEntry|string|nil
|
|
||||||
local alias = (corpus.register_alias_registry or {})[key]
|
|
||||||
if type(alias) == "table" then
|
if type(alias) == "table" then
|
||||||
--- @type string|nil
|
local phys = alias.physical or alias.gpr or alias.code_name ---@type string|nil
|
||||||
local phys = alias.physical or alias.gpr or alias.code_name
|
|
||||||
if type(phys) == "string" and PHYSICAL_GPR[phys] then return phys end
|
if type(phys) == "string" and PHYSICAL_GPR[phys] then return phys end
|
||||||
if type(alias.name) == "string" and PHYSICAL_GPR[alias.name] then return alias.name end
|
if type(alias.name) == "string" and PHYSICAL_GPR[alias.name] then return alias.name end
|
||||||
elseif type(alias) == "string" and PHYSICAL_GPR[alias] then
|
elseif type(alias) == "string" and PHYSICAL_GPR[alias] then
|
||||||
return alias
|
return alias
|
||||||
end
|
end
|
||||||
--- @type GprAllocMap|nil
|
local atom_map = (corpus.atom_auto_regs or {})[atom.name] ---@type GprAllocMap|nil
|
||||||
local atom_map = (corpus.atom_auto_regs or {})[atom.name]
|
|
||||||
if type(atom_map) == "table" then
|
if type(atom_map) == "table" then
|
||||||
--- @type string
|
local slot = key:match("^reguse:.+:(.+)$") or key ---@type string
|
||||||
local slot = key:match("^reguse:.+:(.+)$") or key
|
local bound = atom_map[slot] or atom_map["R_" .. slot] ---@type string|nil
|
||||||
--- @type string|nil
|
|
||||||
local bound = atom_map[slot] or atom_map["R_" .. slot]
|
|
||||||
if type(bound) == "string" and PHYSICAL_GPR[bound] then return bound end
|
if type(bound) == "string" and PHYSICAL_GPR[bound] then return bound end
|
||||||
end
|
end
|
||||||
return "—"
|
return "—"
|
||||||
@@ -992,21 +851,15 @@ end
|
|||||||
--- @param atom AtomEntry
|
--- @param atom AtomEntry
|
||||||
--- @return string
|
--- @return string
|
||||||
local function last_relation_for(key, atom)
|
local function last_relation_for(key, atom)
|
||||||
--- @type AtomRelation|nil
|
local last = nil ---@type AtomRelation|nil
|
||||||
local last = nil
|
for _, rel in ipairs((atom.paths and atom.paths.relations) or {}) do ---@type integer, AtomRelation
|
||||||
--- @type integer, AtomRelation
|
local dest = rel.destination or rel.producer_destination ---@type string|nil
|
||||||
for _, rel in ipairs((atom.paths and atom.paths.relations) or {}) do
|
|
||||||
--- @type string|nil
|
|
||||||
local dest = rel.destination or rel.producer_destination
|
|
||||||
if dest == key then last = rel end
|
if dest == key then last = rel end
|
||||||
end
|
end
|
||||||
if not last then return "—" end
|
if not last then return "—" end
|
||||||
--- @type string
|
local sem = last.semantic or "?" ---@type string
|
||||||
local sem = last.semantic or "?"
|
local a = last.producer_word ---@type integer|nil
|
||||||
--- @type integer|nil
|
local b = last.consumer_word ---@type integer|nil
|
||||||
local a = last.producer_word
|
|
||||||
--- @type integer|nil
|
|
||||||
local b = last.consumer_word
|
|
||||||
if a and b then return string.format("%s w%s→%s", sem, tostring(a), tostring(b)) end
|
if a and b then return string.format("%s w%s→%s", sem, tostring(a), tostring(b)) end
|
||||||
return sem
|
return sem
|
||||||
end
|
end
|
||||||
@@ -1015,16 +868,11 @@ end
|
|||||||
--- @param view ModuleView
|
--- @param view ModuleView
|
||||||
--- @return nil
|
--- @return nil
|
||||||
local function render_section_forward(add, view)
|
local function render_section_forward(add, view)
|
||||||
--- @type boolean
|
local wrote = false ---@type boolean
|
||||||
local wrote = false
|
for _, a in ipairs(view.decls) do ---@type integer, AtomEntry
|
||||||
--- @type integer, AtomEntry
|
local gpr = a.paths and a.paths.forward_state and a.paths.forward_state.gpr_values ---@type table<string, GprLatticeSlot>|nil
|
||||||
for _, a in ipairs(view.decls) do
|
local keys = {} ---@type string[]
|
||||||
--- @type table<string, GprLatticeSlot>|nil
|
for k in pairs(gpr or {}) do ---@type string
|
||||||
local gpr = a.paths and a.paths.forward_state and a.paths.forward_state.gpr_values
|
|
||||||
--- @type string[]
|
|
||||||
local keys = {}
|
|
||||||
--- @type string
|
|
||||||
for k in pairs(gpr or {}) do
|
|
||||||
if k == "R_0" then
|
if k == "R_0" then
|
||||||
-- hidden
|
-- hidden
|
||||||
elseif HIDDEN_UNLESS_WRITTEN[k] and not encoder_wrote_key(a, k) then
|
elseif HIDDEN_UNLESS_WRITTEN[k] and not encoder_wrote_key(a, k) then
|
||||||
@@ -1039,12 +887,9 @@ local function render_section_forward(add, view)
|
|||||||
add("| written | aliases | physical | lattice | last relation |")
|
add("| written | aliases | physical | lattice | last relation |")
|
||||||
add("|---|---|---|---|---|")
|
add("|---|---|---|---|---|")
|
||||||
table.sort(keys)
|
table.sort(keys)
|
||||||
--- @type integer, string
|
for _, k in ipairs(keys) do ---@type integer, string
|
||||||
for _, k in ipairs(keys) do
|
local slot = gpr[k] ---@type GprLatticeSlot|nil
|
||||||
--- @type GprLatticeSlot|nil
|
local lattice = "—" ---@type string
|
||||||
local slot = gpr[k]
|
|
||||||
--- @type string
|
|
||||||
local lattice = "—"
|
|
||||||
if slot and slot.kind == "constant" then
|
if slot and slot.kind == "constant" then
|
||||||
lattice = tostring(slot.value)
|
lattice = tostring(slot.value)
|
||||||
end
|
end
|
||||||
@@ -1061,8 +906,7 @@ local function render_section_forward(add, view)
|
|||||||
if not wrote then add("_(none)_"); add("") end
|
if not wrote then add("_(none)_"); add("") end
|
||||||
end
|
end
|
||||||
|
|
||||||
--- @type SectionRenderer[]
|
local SECTION_RENDERERS = { ---@type SectionRenderer[]
|
||||||
local SECTION_RENDERERS = {
|
|
||||||
{ header = "## Declarations", render = render_section_declarations },
|
{ header = "## Declarations", render = render_section_declarations },
|
||||||
{ header = "## Components", render = render_section_components },
|
{ header = "## Components", render = render_section_components },
|
||||||
{ header = "## RegUse schemas", render = render_section_reguse },
|
{ header = "## RegUse schemas", render = render_section_reguse },
|
||||||
@@ -1083,10 +927,8 @@ local SECTION_RENDERERS = {
|
|||||||
--- @param view ModuleView
|
--- @param view ModuleView
|
||||||
--- @return string
|
--- @return string
|
||||||
local function render_module_meta_report(view)
|
local function render_module_meta_report(view)
|
||||||
--- @type string
|
local dir_basename = source_basename(view.dir) ---@type string
|
||||||
local dir_basename = source_basename(view.dir)
|
local lines = { ---@type string[]
|
||||||
--- @type string[]
|
|
||||||
local lines = {
|
|
||||||
"# " .. dir_basename .. " — atom meta report",
|
"# " .. dir_basename .. " — atom meta report",
|
||||||
"> Auto-generated by ps1_meta.lua (passes/report.lua). Do not edit.",
|
"> Auto-generated by ps1_meta.lua (passes/report.lua). Do not edit.",
|
||||||
"",
|
"",
|
||||||
@@ -1095,20 +937,15 @@ local function render_module_meta_report(view)
|
|||||||
--- @return nil
|
--- @return nil
|
||||||
local function add(s) lines[#lines + 1] = s end
|
local function add(s) lines[#lines + 1] = s end
|
||||||
|
|
||||||
--- @type KindCounts
|
local kinds = count_kinds(view.decls) ---@type KindCounts
|
||||||
local kinds = count_kinds(view.decls)
|
local n_annot, n_binds, n_macros = 0, 0, 0 ---@type integer, integer, integer
|
||||||
--- @type integer, integer, integer
|
for _, src in ipairs(view.sources) do ---@type integer, SourceFile
|
||||||
local n_annot, n_binds, n_macros = 0, 0, 0
|
|
||||||
--- @type integer, SourceFile
|
|
||||||
for _, src in ipairs(view.sources) do
|
|
||||||
n_annot = n_annot + #((src.scan and src.scan.atom_infos) or {})
|
n_annot = n_annot + #((src.scan and src.scan.atom_infos) or {})
|
||||||
n_binds = n_binds + #((src.scan and src.scan.binds) or {})
|
n_binds = n_binds + #((src.scan and src.scan.binds) or {})
|
||||||
n_macros = n_macros + #((src.scan and src.scan.macros) or {})
|
n_macros = n_macros + #((src.scan and src.scan.macros) or {})
|
||||||
end
|
end
|
||||||
--- @type integer, integer, integer
|
local n_err, n_warn, n_info = 0, 0, 0 ---@type integer, integer, integer
|
||||||
local n_err, n_warn, n_info = 0, 0, 0
|
for _, f in ipairs(view.findings or {}) do ---@type integer, CheckFinding
|
||||||
--- @type integer, CheckFinding
|
|
||||||
for _, f in ipairs(view.findings or {}) do
|
|
||||||
if f.kind == "error" then n_err = n_err + 1
|
if f.kind == "error" then n_err = n_err + 1
|
||||||
elseif f.kind == "warning" then n_warn = n_warn + 1
|
elseif f.kind == "warning" then n_warn = n_warn + 1
|
||||||
else n_info = n_info + 1
|
else n_info = n_info + 1
|
||||||
@@ -1128,12 +965,10 @@ local function render_module_meta_report(view)
|
|||||||
add("")
|
add("")
|
||||||
|
|
||||||
add("## Sources"); add("")
|
add("## Sources"); add("")
|
||||||
--- @type integer, SourceFile
|
for _, s in ipairs(view.sources) do add("- `" .. s.path .. "`") end ---@type integer, SourceFile
|
||||||
for _, s in ipairs(view.sources) do add("- `" .. s.path .. "`") end
|
|
||||||
add("")
|
add("")
|
||||||
|
|
||||||
--- @type integer, SectionRenderer
|
for _, row in ipairs(SECTION_RENDERERS) do ---@type integer, SectionRenderer
|
||||||
for _, row in ipairs(SECTION_RENDERERS) do
|
|
||||||
add(row.header); add("")
|
add(row.header); add("")
|
||||||
row.render(add, view)
|
row.render(add, view)
|
||||||
end
|
end
|
||||||
@@ -1147,8 +982,7 @@ end
|
|||||||
-- `once = true` means render once at the project level (not per-module).
|
-- `once = true` means render once at the project level (not per-module).
|
||||||
-- `basename(dir_basename)` yields the file's basename for that kind.
|
-- `basename(dir_basename)` yields the file's basename for that kind.
|
||||||
-- `gather(ctx, dir, dir_sources [, all_modules])` returns the rendered string.
|
-- `gather(ctx, dir, dir_sources [, all_modules])` returns the rendered string.
|
||||||
--- @type ReportRenderer[]
|
local REPORT_RENDERERS = { ---@type ReportRenderer[]
|
||||||
local REPORT_RENDERERS = {
|
|
||||||
{
|
{
|
||||||
name = "atom_meta_report",
|
name = "atom_meta_report",
|
||||||
ext = "md",
|
ext = "md",
|
||||||
@@ -1161,8 +995,7 @@ local REPORT_RENDERERS = {
|
|||||||
--- @param dir_sources SourceFile[]
|
--- @param dir_sources SourceFile[]
|
||||||
--- @return string
|
--- @return string
|
||||||
gather = function(ctx, dir, dir_sources)
|
gather = function(ctx, dir, dir_sources)
|
||||||
--- @type Corpus
|
local corpus = ctx.shared.corpus ---@type Corpus
|
||||||
local corpus = ctx.shared.corpus
|
|
||||||
return render_module_meta_report(build_module_view(dir, dir_sources, corpus))
|
return render_module_meta_report(build_module_view(dir, dir_sources, corpus))
|
||||||
end,
|
end,
|
||||||
},
|
},
|
||||||
@@ -1204,20 +1037,16 @@ local REPORT_RENDERERS = {
|
|||||||
-- M — public pass surface
|
-- M — public pass surface
|
||||||
-- ════════════════════════════════════════════════════════════════════════════
|
-- ════════════════════════════════════════════════════════════════════════════
|
||||||
|
|
||||||
--- @type ReportPass
|
local M = {} ---@type ReportPass
|
||||||
local M = {}
|
|
||||||
|
|
||||||
--- Run the report pass. Emits 1 `atom_meta_report.summary.md` per build + 2 `atom_meta_report.md` + 2 `atoms.md` files per module (duffle + gte_hello).
|
--- Run the report pass. Emits 1 `atom_meta_report.summary.md` per build + 2 `atom_meta_report.md` + 2 `atoms.md` files per module (duffle + gte_hello).
|
||||||
--- Reads `corpus.static_analysis_results` (added in Phase 1) to populate per-module findings without re-running validate().
|
--- Reads `corpus.static_analysis_results` (added in Phase 1) to populate per-module findings without re-running validate().
|
||||||
--- @param ctx PassCtx
|
--- @param ctx PassCtx
|
||||||
--- @return PassResult
|
--- @return PassResult
|
||||||
function M.run(ctx)
|
function M.run(ctx)
|
||||||
--- @type PassOutputEntry[]
|
local outputs = {} ---@type PassOutputEntry[]
|
||||||
local outputs = {}
|
local corpus = ctx.shared and ctx.shared.corpus ---@type Corpus|nil
|
||||||
--- @type Corpus|nil
|
local by_dir = (corpus and corpus.sources_by_dir) or {} ---@type table<string, SourceFile[]>
|
||||||
local corpus = ctx.shared and ctx.shared.corpus
|
|
||||||
--- @type table<string, SourceFile[]>
|
|
||||||
local by_dir = (corpus and corpus.sources_by_dir) or {}
|
|
||||||
|
|
||||||
-- `out_path_root`: when the conventional `out_root` is `build/gen` (any spelling — relative, absolute, separator variants).
|
-- `out_path_root`: when the conventional `out_root` is `build/gen` (any spelling — relative, absolute, separator variants).
|
||||||
-- Write the md files to `build/` (parent of `gen/`) instead of nested under `gen/`.
|
-- Write the md files to `build/` (parent of `gen/`) instead of nested under `gen/`.
|
||||||
@@ -1228,49 +1057,37 @@ function M.run(ctx)
|
|||||||
return type(p) == "string" and (p:match("[/\\]gen[/\\]?$") ~= nil
|
return type(p) == "string" and (p:match("[/\\]gen[/\\]?$") ~= nil
|
||||||
or p == "build/gen" or p == "build\\gen")
|
or p == "build/gen" or p == "build\\gen")
|
||||||
end
|
end
|
||||||
--- @type string
|
local out_root_effective = ends_with_gen(ctx.out_root) ---@type string
|
||||||
local out_root_effective = ends_with_gen(ctx.out_root)
|
|
||||||
and ctx.out_root:gsub("[/\\]gen[/\\]?$", "")
|
and ctx.out_root:gsub("[/\\]gen[/\\]?$", "")
|
||||||
or ctx.out_root
|
or ctx.out_root
|
||||||
|
|
||||||
duffle.ensure_dir(out_root_effective)
|
duffle.ensure_dir(out_root_effective)
|
||||||
|
|
||||||
-- Aggregator for the project-wide `once = true` summary renderer.
|
-- Aggregator for the project-wide `once = true` summary renderer.
|
||||||
--- @type ProjectSummaryRow[]
|
local all_modules = {} ---@type ProjectSummaryRow[]
|
||||||
local all_modules = {}
|
|
||||||
|
|
||||||
--- @type string, SourceFile[]
|
for dir, dir_sources in pairs(by_dir) do ---@type string, SourceFile[]
|
||||||
for dir, dir_sources in pairs(by_dir) do
|
local dir_basename = dir:match("([^/\\]+)$") or dir ---@type string
|
||||||
--- @type string
|
|
||||||
local dir_basename = dir:match("([^/\\]+)$") or dir
|
|
||||||
|
|
||||||
-- Per-renderer dispatch for the per-module renderers (once = false).
|
-- Per-renderer dispatch for the per-module renderers (once = false).
|
||||||
--- @type integer, ReportRenderer
|
for _, renderer in ipairs(REPORT_RENDERERS) do ---@type integer, ReportRenderer
|
||||||
for _, renderer in ipairs(REPORT_RENDERERS) do
|
|
||||||
if not renderer.once then
|
if not renderer.once then
|
||||||
--- @type string
|
local body = renderer.gather(ctx, dir, dir_sources) ---@type string
|
||||||
local body = renderer.gather(ctx, dir, dir_sources)
|
local out_path = out_root_effective .. "/" .. renderer.basename(dir_basename) .. "." .. renderer.ext ---@type string
|
||||||
--- @type string
|
|
||||||
local out_path = out_root_effective .. "/" .. renderer.basename(dir_basename) .. "." .. renderer.ext
|
|
||||||
duffle.write_file(out_path, body)
|
duffle.write_file(out_path, body)
|
||||||
outputs[#outputs + 1] = { kind = renderer.name, path = out_path }
|
outputs[#outputs + 1] = { kind = renderer.name, path = out_path }
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
--- @type ModuleView
|
local view = build_module_view(dir, dir_sources, corpus) ---@type ModuleView
|
||||||
local view = build_module_view(dir, dir_sources, corpus)
|
local n_annot, n_binds, n_macros = 0, 0, 0 ---@type integer, integer, integer
|
||||||
--- @type integer, integer, integer
|
for _, src in ipairs(dir_sources) do ---@type integer, SourceFile
|
||||||
local n_annot, n_binds, n_macros = 0, 0, 0
|
|
||||||
--- @type integer, SourceFile
|
|
||||||
for _, src in ipairs(dir_sources) do
|
|
||||||
n_annot = n_annot + #((src.scan and src.scan.atom_infos) or {})
|
n_annot = n_annot + #((src.scan and src.scan.atom_infos) or {})
|
||||||
n_binds = n_binds + #((src.scan and src.scan.binds) or {})
|
n_binds = n_binds + #((src.scan and src.scan.binds) or {})
|
||||||
n_macros = n_macros + #((src.scan and src.scan.macros) or {})
|
n_macros = n_macros + #((src.scan and src.scan.macros) or {})
|
||||||
end
|
end
|
||||||
--- @type integer, integer, integer
|
local n_err, n_warn, n_info = 0, 0, 0 ---@type integer, integer, integer
|
||||||
local n_err, n_warn, n_info = 0, 0, 0
|
for _, f in ipairs(view.findings or {}) do ---@type integer, CheckFinding
|
||||||
--- @type integer, CheckFinding
|
|
||||||
for _, f in ipairs(view.findings or {}) do
|
|
||||||
if f.kind == "error" then n_err = n_err + 1
|
if f.kind == "error" then n_err = n_err + 1
|
||||||
elseif f.kind == "warning" then n_warn = n_warn + 1
|
elseif f.kind == "warning" then n_warn = n_warn + 1
|
||||||
else n_info = n_info + 1
|
else n_info = n_info + 1
|
||||||
@@ -1290,13 +1107,10 @@ function M.run(ctx)
|
|||||||
end
|
end
|
||||||
|
|
||||||
-- Project-wide renderer (once = true): write the summary file.
|
-- Project-wide renderer (once = true): write the summary file.
|
||||||
--- @type integer, ReportRenderer
|
for _, renderer in ipairs(REPORT_RENDERERS) do ---@type integer, ReportRenderer
|
||||||
for _, renderer in ipairs(REPORT_RENDERERS) do
|
|
||||||
if renderer.once then
|
if renderer.once then
|
||||||
--- @type string
|
local body = renderer.gather(ctx, nil, nil, all_modules) ---@type string
|
||||||
local body = renderer.gather(ctx, nil, nil, all_modules)
|
local out_path = out_root_effective .. "/" .. renderer.basename("") .. "." .. renderer.ext ---@type string
|
||||||
--- @type string
|
|
||||||
local out_path = out_root_effective .. "/" .. renderer.basename("") .. "." .. renderer.ext
|
|
||||||
duffle.write_file(out_path, body)
|
duffle.write_file(out_path, body)
|
||||||
outputs[#outputs + 1] = { kind = renderer.name, path = out_path }
|
outputs[#outputs + 1] = { kind = renderer.name, path = out_path }
|
||||||
end
|
end
|
||||||
|
|||||||
+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).
|
-- Bootstrap: load `scripts/duffle_paths.lua` (sets package.path + package.cpath).
|
||||||
-- Uses `debug.getinfo` to find this file's own directory, so it works both standalone and when require'd from the orchestrator.
|
-- Uses `debug.getinfo` to find this file's own directory, so it works both standalone and when require'd from the orchestrator.
|
||||||
-- duffle_paths.lua sets package.path then returns `require("duffle")` at the bottom, so the dofile value IS the duffle module.
|
-- duffle_paths.lua sets package.path then returns `require("duffle")` at the bottom, so the dofile value IS the duffle module.
|
||||||
--- @type string
|
local _bootstrap_dir = debug.getinfo(1, "S").source:match("^@?(.*[/\\])") or "./" ---@type string
|
||||||
local _bootstrap_dir = debug.getinfo(1, "S").source:match("^@?(.*[/\\])") or "./"
|
local duffle = dofile(_bootstrap_dir .. "../duffle_paths.lua") ---@type DuffleExport
|
||||||
--- @type DuffleExport
|
|
||||||
local duffle = dofile(_bootstrap_dir .. "../duffle_paths.lua")
|
|
||||||
|
|
||||||
-- ════════════════════════════════════════════════════════════════════════════
|
-- ════════════════════════════════════════════════════════════════════════════
|
||||||
-- Type declarations
|
-- Type declarations
|
||||||
@@ -46,8 +44,7 @@ local duffle = dofile(_bootstrap_dir .. "../duffle_paths.lua")
|
|||||||
-- Module exports
|
-- Module exports
|
||||||
-- ════════════════════════════════════════════════════════════════════════════
|
-- ════════════════════════════════════════════════════════════════════════════
|
||||||
|
|
||||||
--- @type WordCountEval
|
local M = {} ---@type WordCountEval
|
||||||
local M = {}
|
|
||||||
|
|
||||||
-- ┌────────────────────────────────────────────────────────────────────┐
|
-- ┌────────────────────────────────────────────────────────────────────┐
|
||||||
-- │ Shared utility: count_token_words │
|
-- │ Shared utility: count_token_words │
|
||||||
@@ -61,15 +58,12 @@ local M = {}
|
|||||||
--- @param wc WordCounts -- the shared word-count table
|
--- @param wc WordCounts -- the shared word-count table
|
||||||
--- @return integer
|
--- @return integer
|
||||||
function M.count_token_words(token, wc)
|
function M.count_token_words(token, wc)
|
||||||
--- @type string
|
local s = duffle.trim(token) ---@type string
|
||||||
local s = duffle.trim(token)
|
|
||||||
if s == "" then return 0 end
|
if s == "" then return 0 end
|
||||||
--- @type string|nil, integer
|
local name, after = duffle.read_ident(s, 1) ---@type string|nil, integer
|
||||||
local name, after = duffle.read_ident(s, 1)
|
|
||||||
if not name then return 1 end
|
if not name then return 1 end
|
||||||
if wc[name] then return wc[name] end
|
if wc[name] then return wc[name] end
|
||||||
--- @type integer
|
local paren_pos = duffle.skip_ws_and_cmt(s, after) ---@type integer
|
||||||
local paren_pos = duffle.skip_ws_and_cmt(s, after)
|
|
||||||
if s:sub(paren_pos, paren_pos) == "(" then
|
if s:sub(paren_pos, paren_pos) == "(" then
|
||||||
io.stderr:write(" warning: unknown macro '" .. name .. "', assuming 1 word\n")
|
io.stderr:write(" warning: unknown macro '" .. name .. "', assuming 1 word\n")
|
||||||
end
|
end
|
||||||
@@ -95,8 +89,7 @@ end
|
|||||||
--- @return PassResult
|
--- @return PassResult
|
||||||
function M.run(ctx)
|
function M.run(ctx)
|
||||||
-- 1. Canonical-corpus ownership gate.
|
-- 1. Canonical-corpus ownership gate.
|
||||||
--- @type Corpus|nil
|
local corpus = ctx.shared and ctx.shared.corpus ---@type Corpus|nil
|
||||||
local corpus = ctx.shared and ctx.shared.corpus
|
|
||||||
if type(corpus) ~= "table" then
|
if type(corpus) ~= "table" then
|
||||||
error("word_count_eval.run requires ctx.shared.corpus (canonical corpus). The fixture must install the corpus before running this pass.", 0)
|
error("word_count_eval.run requires ctx.shared.corpus (canonical corpus). The fixture must install the corpus before running this pass.", 0)
|
||||||
end
|
end
|
||||||
@@ -108,8 +101,7 @@ function M.run(ctx)
|
|||||||
|
|
||||||
-- 3. Load authored metadata. Generated .macs.h files are NOT scanned
|
-- 3. Load authored metadata. Generated .macs.h files are NOT scanned
|
||||||
-- (the pass computes their counts from the just-built bodies after disk emission; see passes/components.lua).
|
-- (the pass computes their counts from the just-built bodies after disk emission; see passes/components.lua).
|
||||||
--- @type WordCounts
|
local wc = duffle.load_word_counts(ctx.metadata_path) ---@type WordCounts
|
||||||
local wc = duffle.load_word_counts(ctx.metadata_path)
|
|
||||||
|
|
||||||
-- 4. Assign the count table. ONE assignment, no copy. The assignment creates no secondary alias.
|
-- 4. Assign the count table. ONE assignment, no copy. The assignment creates no secondary alias.
|
||||||
corpus.word_counts = wc
|
corpus.word_counts = wc
|
||||||
|
|||||||
+93
-185
@@ -19,10 +19,8 @@
|
|||||||
-- fall back to `debug.getinfo(1, "S").source` when this file is being dofile()'d or require()'d (in which case `arg[0]` is the *caller's* path).
|
-- fall back to `debug.getinfo(1, "S").source` when this file is being dofile()'d or require()'d (in which case `arg[0]` is the *caller's* path).
|
||||||
-- That single statement: (a) sets `package.path` + `package.cpath`, (b) at the bottom returns `require("duffle")`.
|
-- That single statement: (a) sets `package.path` + `package.cpath`, (b) at the bottom returns `require("duffle")`.
|
||||||
-- So the dofile's return value is the duffle module.
|
-- So the dofile's return value is the duffle module.
|
||||||
--- @type boolean
|
local _is_entry_script = arg and arg[0] and arg[0]:match("ps1_meta%.lua$") ~= nil ---@type boolean
|
||||||
local _is_entry_script = arg and arg[0] and arg[0]:match("ps1_meta%.lua$") ~= nil
|
local _bootstrap_src ---@type string
|
||||||
--- @type string
|
|
||||||
local _bootstrap_src
|
|
||||||
if _is_entry_script then
|
if _is_entry_script then
|
||||||
_bootstrap_src = arg[0]
|
_bootstrap_src = arg[0]
|
||||||
else
|
else
|
||||||
@@ -30,33 +28,26 @@ else
|
|||||||
-- strip the leading "@" so the directory match works in both cases.
|
-- strip the leading "@" so the directory match works in both cases.
|
||||||
_bootstrap_src = debug.getinfo(1, "S").source:sub(2)
|
_bootstrap_src = debug.getinfo(1, "S").source:sub(2)
|
||||||
end
|
end
|
||||||
--- @type DuffleExport
|
local duffle = dofile((_bootstrap_src:match("(.*[/\\])") or "./") .. "duffle_paths.lua") ---@type DuffleExport
|
||||||
local duffle = dofile((_bootstrap_src:match("(.*[/\\])") or "./") .. "duffle_paths.lua")
|
|
||||||
|
|
||||||
-- ════════════════════════════════════════════════════════════════════════════
|
-- ════════════════════════════════════════════════════════════════════════════
|
||||||
-- Constants
|
-- Constants
|
||||||
-- ════════════════════════════════════════════════════════════════════════════
|
-- ════════════════════════════════════════════════════════════════════════════
|
||||||
|
|
||||||
-- Exit codes (per the --help text and the post-build summary convention).
|
-- Exit codes (per the --help text and the post-build summary convention).
|
||||||
--- @type integer
|
local EXIT_OK = 0 ---@type integer
|
||||||
local EXIT_OK = 0
|
local EXIT_VALIDATION_ERRORS = 1 ---@type integer
|
||||||
--- @type integer
|
local EXIT_INTERNAL_ERROR = 2 ---@type integer
|
||||||
local EXIT_VALIDATION_ERRORS = 1
|
|
||||||
--- @type integer
|
|
||||||
local EXIT_INTERNAL_ERROR = 2
|
|
||||||
|
|
||||||
-- Default --out-root value if not provided.
|
-- Default --out-root value if not provided.
|
||||||
--- @type string
|
local DEFAULT_OUT_ROOT = "build/gen" ---@type string
|
||||||
local DEFAULT_OUT_ROOT = "build/gen"
|
|
||||||
|
|
||||||
-- Sentinel for "all passes" in `PASS_FLAG_TO_NAME`. Distinguishes `--all` from the per-pass flags (which map to individual pass names).
|
-- Sentinel for "all passes" in `PASS_FLAG_TO_NAME`. Distinguishes `--all` from the per-pass flags (which map to individual pass names).
|
||||||
--- @type string
|
local ALL_PASSES_SENTINEL = "__all__" ---@type string
|
||||||
local ALL_PASSES_SENTINEL = "__all__"
|
|
||||||
|
|
||||||
-- Sentinel key for the pass-flag dispatcher in `FLAG_HANDLERS`.
|
-- Sentinel key for the pass-flag dispatcher in `FLAG_HANDLERS`.
|
||||||
-- The actual pass names are looked up via `PASS_FLAG_TO_NAME`, not direct dispatch, so this key never matches a real flag.
|
-- The actual pass names are looked up via `PASS_FLAG_TO_NAME`, not direct dispatch, so this key never matches a real flag.
|
||||||
--- @type string
|
local PASS_FLAG_DISPATCH_KEY = "__pass__" ---@type string
|
||||||
local PASS_FLAG_DISPATCH_KEY = "__pass__"
|
|
||||||
|
|
||||||
-- ════════════════════════════════════════════════════════════════════════════
|
-- ════════════════════════════════════════════════════════════════════════════
|
||||||
-- Type declarations
|
-- Type declarations
|
||||||
@@ -159,8 +150,7 @@ local PASS_FLAG_DISPATCH_KEY = "__pass__"
|
|||||||
-- A row without a `groups` entry is dependency-only: it runs only when a transitive dep requests it,
|
-- A row without a `groups` entry is dependency-only: it runs only when a transitive dep requests it,
|
||||||
-- but it remains directly requestable through its explicit CLI flag (e.g. --atoms-source-map, --scan-source).
|
-- but it remains directly requestable through its explicit CLI flag (e.g. --atoms-source-map, --scan-source).
|
||||||
|
|
||||||
--- @type table<string, PassDescriptor>
|
local PASSES = { ---@type table<string, PassDescriptor>
|
||||||
local PASSES = {
|
|
||||||
["scan-source"] = {
|
["scan-source"] = {
|
||||||
module = "passes.scan_source",
|
module = "passes.scan_source",
|
||||||
kind = "shared", deps = {},
|
kind = "shared", deps = {},
|
||||||
@@ -230,13 +220,10 @@ local PASSES = {
|
|||||||
--- @param group_name string -- Build-phase group ("pre-link" | "post-link")
|
--- @param group_name string -- Build-phase group ("pre-link" | "post-link")
|
||||||
--- @return string[] -- Sorted root pass names belonging to that group
|
--- @return string[] -- Sorted root pass names belonging to that group
|
||||||
local function roots_for_group(group_name)
|
local function roots_for_group(group_name)
|
||||||
--- @type string[]
|
local names = {} ---@type string[]
|
||||||
local names = {}
|
for name, pass in pairs(PASSES) do ---@type string, PassDescriptor
|
||||||
--- @type string, PassDescriptor
|
|
||||||
for name, pass in pairs(PASSES) do
|
|
||||||
if pass.groups then
|
if pass.groups then
|
||||||
--- @type integer, string
|
for _, g in ipairs(pass.groups) do ---@type integer, string
|
||||||
for _, g in ipairs(pass.groups) do
|
|
||||||
if g == group_name then
|
if g == group_name then
|
||||||
names[#names + 1] = name
|
names[#names + 1] = name
|
||||||
break
|
break
|
||||||
@@ -251,18 +238,16 @@ end
|
|||||||
--- Append every root belonging to `group_name` to `args.requested_set`.
|
--- Append every root belonging to `group_name` to `args.requested_set`.
|
||||||
--- Errors loudly if no PASSES row declares the group, so a typo'd or future-removed group name
|
--- Errors loudly if no PASSES row declares the group, so a typo'd or future-removed group name
|
||||||
--- cannot silently fall through to pre-link (or any other default) and dispatch nothing.
|
--- cannot silently fall through to pre-link (or any other default) and dispatch nothing.
|
||||||
--- @param args ParsedArgs
|
--- @param args ParsedArgs
|
||||||
--- @param group_name string
|
--- @param group_name string
|
||||||
--- @return nil
|
--- @return nil
|
||||||
local function request_roots_for_group(args, group_name)
|
local function request_roots_for_group(args, group_name)
|
||||||
--- @type string[]
|
local roots = roots_for_group(group_name) ---@type string[]
|
||||||
local roots = roots_for_group(group_name)
|
|
||||||
if #roots == 0 then
|
if #roots == 0 then
|
||||||
error(string.format("ps1_meta: build-phase group %q has zero roots in PASSES; check PASSES rows for a `groups = { %q }` field"
|
error(string.format("ps1_meta: build-phase group %q has zero roots in PASSES; check PASSES rows for a `groups = { %q }` field"
|
||||||
, group_name, group_name))
|
, group_name, group_name))
|
||||||
end
|
end
|
||||||
--- @type integer, string
|
for _, name in ipairs(roots) do ---@type integer, string
|
||||||
for _, name in ipairs(roots) do
|
|
||||||
args.requested_set[#args.requested_set + 1] = name
|
args.requested_set[#args.requested_set + 1] = name
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
@@ -270,8 +255,7 @@ end
|
|||||||
-- Pass-kind taxonomy: findings always print. No pass kind stops the build.
|
-- Pass-kind taxonomy: findings always print. No pass kind stops the build.
|
||||||
-- Report severity is independent from process exit policy.
|
-- Report severity is independent from process exit policy.
|
||||||
-- Adding a new pass kind requires listing it here explicitly; an unknown kind must not silently fall back to "true".
|
-- Adding a new pass kind requires listing it here explicitly; an unknown kind must not silently fall back to "true".
|
||||||
--- @type table<string, boolean> -- bag: pass kind -> stop-on-error
|
local PASS_KIND_STOP_ON_ERROR = { ---@type table<string, boolean> -- bag: pass kind -> stop-on-error
|
||||||
local PASS_KIND_STOP_ON_ERROR = {
|
|
||||||
["shared"] = false,
|
["shared"] = false,
|
||||||
["header-output"] = false,
|
["header-output"] = false,
|
||||||
["validation"] = false,
|
["validation"] = false,
|
||||||
@@ -283,8 +267,7 @@ local PASS_KIND_STOP_ON_ERROR = {
|
|||||||
-- Per-pass flags (e.g. --word-counts); phase flags (--pre-link, --post-link, --all) are within FLAG_HANDLERS because they own side effects or invoke group-derivation logic.
|
-- Per-pass flags (e.g. --word-counts); phase flags (--pre-link, --post-link, --all) are within FLAG_HANDLERS because they own side effects or invoke group-derivation logic.
|
||||||
-- dwarf-injection is *also* a per-pass opt-in flag, but its selection + opt-in state are both owned by the explicit FLAG_HANDLERS entry below
|
-- dwarf-injection is *also* a per-pass opt-in flag, but its selection + opt-in state are both owned by the explicit FLAG_HANDLERS entry below
|
||||||
-- (it sets args.flags.dwarf_injection and appends "dwarf-injection" to requested_set), so it is intentionally absent from this table.
|
-- (it sets args.flags.dwarf_injection and appends "dwarf-injection" to requested_set), so it is intentionally absent from this table.
|
||||||
--- @type table<string, string> -- bag: CLI flag -> pass name or ALL_PASSES_SENTINEL
|
local PASS_FLAG_TO_NAME = { ---@type table<string, string> -- bag: CLI flag -> pass name or ALL_PASSES_SENTINEL
|
||||||
local PASS_FLAG_TO_NAME = {
|
|
||||||
["--word-counts"] = "word-counts",
|
["--word-counts"] = "word-counts",
|
||||||
["--components"] = "components",
|
["--components"] = "components",
|
||||||
["--validate"] = "annotation",
|
["--validate"] = "annotation",
|
||||||
@@ -301,21 +284,17 @@ local PASS_FLAG_TO_NAME = {
|
|||||||
--- @param args ParsedArgs
|
--- @param args ParsedArgs
|
||||||
--- @return nil
|
--- @return nil
|
||||||
local function request_all_passes(args)
|
local function request_all_passes(args)
|
||||||
--- @type string[]
|
local names = {} ---@type string[]
|
||||||
local names = {}
|
for name in pairs(PASSES) do names[#names + 1] = name end ---@type string
|
||||||
--- @type string
|
|
||||||
for name in pairs(PASSES) do names[#names + 1] = name end
|
|
||||||
table.sort(names)
|
table.sort(names)
|
||||||
--- @type integer, string
|
for _, n in ipairs(names) do ---@type integer, string
|
||||||
for _, n in ipairs(names) do
|
|
||||||
args.requested_set[#args.requested_set + 1] = n
|
args.requested_set[#args.requested_set + 1] = n
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
-- Per-flag handlers. Each handler takes (args, argv, arg_idx) and returns the new arg_idx (so multi-arg flags like --source FILE advance it).
|
-- Per-flag handlers. Each handler takes (args, argv, arg_idx) and returns the new arg_idx (so multi-arg flags like --source FILE advance it).
|
||||||
-- Returning nil + os.exit() handles termination flags (--help).
|
-- Returning nil + os.exit() handles termination flags (--help).
|
||||||
--- @type table<string, FlagHandler>
|
local FLAG_HANDLERS = {} ---@type table<string, FlagHandler>
|
||||||
local FLAG_HANDLERS = {}
|
|
||||||
|
|
||||||
-- ════════════════════════════════════════════════════════════════════════════
|
-- ════════════════════════════════════════════════════════════════════════════
|
||||||
-- CLI parsing
|
-- CLI parsing
|
||||||
@@ -372,8 +351,7 @@ EXAMPLES:
|
|||||||
]])
|
]])
|
||||||
end
|
end
|
||||||
|
|
||||||
--- @type table<string, string> -- bag: flag -> value metavar
|
local FLAG_VALUE_NAMES = { ---@type table<string, string> -- bag: flag -> value metavar
|
||||||
local FLAG_VALUE_NAMES = {
|
|
||||||
["--source"] = "FILE",
|
["--source"] = "FILE",
|
||||||
["--unity-root"] = "FILE",
|
["--unity-root"] = "FILE",
|
||||||
["--metadata"] = "PATH",
|
["--metadata"] = "PATH",
|
||||||
@@ -388,10 +366,8 @@ local FLAG_VALUE_NAMES = {
|
|||||||
--- @return string
|
--- @return string
|
||||||
--- @return integer
|
--- @return integer
|
||||||
local function require_flag_value(argv, arg_idx, flag)
|
local function require_flag_value(argv, arg_idx, flag)
|
||||||
--- @type string|nil
|
local value = argv[arg_idx + 1] ---@type string|nil
|
||||||
local value = argv[arg_idx + 1]
|
local next_known = type(value) == "string" ---@type boolean
|
||||||
--- @type boolean
|
|
||||||
local next_known = type(value) == "string"
|
|
||||||
and (FLAG_HANDLERS[value] ~= nil or PASS_FLAG_TO_NAME[value] ~= nil)
|
and (FLAG_HANDLERS[value] ~= nil or PASS_FLAG_TO_NAME[value] ~= nil)
|
||||||
if value == nil or next_known then
|
if value == nil or next_known then
|
||||||
io.stderr:write("ps1_meta: " .. flag .. " requires " .. FLAG_VALUE_NAMES[flag] .. "\n")
|
io.stderr:write("ps1_meta: " .. flag .. " requires " .. FLAG_VALUE_NAMES[flag] .. "\n")
|
||||||
@@ -417,8 +393,7 @@ FLAG_HANDLERS["--verbose"] = function(args) args.verbose = true end
|
|||||||
--- @param arg_idx integer
|
--- @param arg_idx integer
|
||||||
--- @return integer
|
--- @return integer
|
||||||
FLAG_HANDLERS["--source"] = function(args, argv, arg_idx)
|
FLAG_HANDLERS["--source"] = function(args, argv, arg_idx)
|
||||||
--- @type string, integer
|
local value, value_idx = require_flag_value(argv, arg_idx, "--source") ---@type string, integer
|
||||||
local value, value_idx = require_flag_value(argv, arg_idx, "--source")
|
|
||||||
args.sources[#args.sources + 1] = value
|
args.sources[#args.sources + 1] = value
|
||||||
return value_idx
|
return value_idx
|
||||||
end
|
end
|
||||||
@@ -427,8 +402,7 @@ end
|
|||||||
--- @param arg_idx integer
|
--- @param arg_idx integer
|
||||||
--- @return integer
|
--- @return integer
|
||||||
FLAG_HANDLERS["--unity-root"] = function(args, argv, arg_idx)
|
FLAG_HANDLERS["--unity-root"] = function(args, argv, arg_idx)
|
||||||
--- @type string, integer
|
local value, value_idx = require_flag_value(argv, arg_idx, "--unity-root") ---@type string, integer
|
||||||
local value, value_idx = require_flag_value(argv, arg_idx, "--unity-root")
|
|
||||||
args.unity_root = value
|
args.unity_root = value
|
||||||
return value_idx
|
return value_idx
|
||||||
end
|
end
|
||||||
@@ -437,8 +411,7 @@ end
|
|||||||
--- @param arg_idx integer
|
--- @param arg_idx integer
|
||||||
--- @return integer
|
--- @return integer
|
||||||
FLAG_HANDLERS["--metadata"] = function(args, argv, arg_idx)
|
FLAG_HANDLERS["--metadata"] = function(args, argv, arg_idx)
|
||||||
--- @type string, integer
|
local value, value_idx = require_flag_value(argv, arg_idx, "--metadata") ---@type string, integer
|
||||||
local value, value_idx = require_flag_value(argv, arg_idx, "--metadata")
|
|
||||||
args.metadata = value
|
args.metadata = value
|
||||||
return value_idx
|
return value_idx
|
||||||
end
|
end
|
||||||
@@ -447,8 +420,7 @@ end
|
|||||||
--- @param arg_idx integer
|
--- @param arg_idx integer
|
||||||
--- @return integer
|
--- @return integer
|
||||||
FLAG_HANDLERS["--out-root"] = function(args, argv, arg_idx)
|
FLAG_HANDLERS["--out-root"] = function(args, argv, arg_idx)
|
||||||
--- @type string, integer
|
local value, value_idx = require_flag_value(argv, arg_idx, "--out-root") ---@type string, integer
|
||||||
local value, value_idx = require_flag_value(argv, arg_idx, "--out-root")
|
|
||||||
args.out_root = value
|
args.out_root = value
|
||||||
return value_idx
|
return value_idx
|
||||||
end
|
end
|
||||||
@@ -457,8 +429,7 @@ end
|
|||||||
--- @param arg_idx integer
|
--- @param arg_idx integer
|
||||||
--- @return integer
|
--- @return integer
|
||||||
FLAG_HANDLERS["--project-root"] = function(args, argv, arg_idx)
|
FLAG_HANDLERS["--project-root"] = function(args, argv, arg_idx)
|
||||||
--- @type string, integer
|
local value, value_idx = require_flag_value(argv, arg_idx, "--project-root") ---@type string, integer
|
||||||
local value, value_idx = require_flag_value(argv, arg_idx, "--project-root")
|
|
||||||
args.project_root = value
|
args.project_root = value
|
||||||
return value_idx
|
return value_idx
|
||||||
end
|
end
|
||||||
@@ -476,8 +447,7 @@ end
|
|||||||
--- @param arg_idx integer
|
--- @param arg_idx integer
|
||||||
--- @return integer
|
--- @return integer
|
||||||
FLAG_HANDLERS["--elf"] = function(args, argv, arg_idx)
|
FLAG_HANDLERS["--elf"] = function(args, argv, arg_idx)
|
||||||
--- @type string, integer
|
local value, value_idx = require_flag_value(argv, arg_idx, "--elf") ---@type string, integer
|
||||||
local value, value_idx = require_flag_value(argv, arg_idx, "--elf")
|
|
||||||
args.flags = args.flags or {}
|
args.flags = args.flags or {}
|
||||||
args.flags.elf_path = value
|
args.flags.elf_path = value
|
||||||
return value_idx
|
return value_idx
|
||||||
@@ -517,8 +487,7 @@ end
|
|||||||
--- @param a string
|
--- @param a string
|
||||||
--- @return nil
|
--- @return nil
|
||||||
FLAG_HANDLERS[PASS_FLAG_DISPATCH_KEY] = function(args, a)
|
FLAG_HANDLERS[PASS_FLAG_DISPATCH_KEY] = function(args, a)
|
||||||
--- @type string|nil
|
local name = PASS_FLAG_TO_NAME[a] ---@type string|nil
|
||||||
local name = PASS_FLAG_TO_NAME[a]
|
|
||||||
if name == ALL_PASSES_SENTINEL then
|
if name == ALL_PASSES_SENTINEL then
|
||||||
request_all_passes(args)
|
request_all_passes(args)
|
||||||
return
|
return
|
||||||
@@ -530,8 +499,7 @@ end
|
|||||||
--- @param argv string[]
|
--- @param argv string[]
|
||||||
--- @return ParsedArgs
|
--- @return ParsedArgs
|
||||||
local function parse_args(argv)
|
local function parse_args(argv)
|
||||||
--- @type ParsedArgs
|
local args = { ---@type ParsedArgs
|
||||||
local args = {
|
|
||||||
requested_set = {},
|
requested_set = {},
|
||||||
sources = {},
|
sources = {},
|
||||||
unity_root = nil,
|
unity_root = nil,
|
||||||
@@ -541,13 +509,10 @@ local function parse_args(argv)
|
|||||||
verbose = false,
|
verbose = false,
|
||||||
}
|
}
|
||||||
|
|
||||||
--- @type integer
|
local pos = 1 ---@type integer
|
||||||
local pos = 1
|
|
||||||
while pos <= #argv do
|
while pos <= #argv do
|
||||||
--- @type string
|
local a = argv[pos] ---@type string
|
||||||
local a = argv[pos]
|
local handler = FLAG_HANDLERS[a] ---@type FlagHandler|nil
|
||||||
--- @type FlagHandler|nil
|
|
||||||
local handler = FLAG_HANDLERS[a]
|
|
||||||
if handler then
|
if handler then
|
||||||
pos = handler(args, argv, pos) or pos
|
pos = handler(args, argv, pos) or pos
|
||||||
elseif PASS_FLAG_TO_NAME[a] then
|
elseif PASS_FLAG_TO_NAME[a] then
|
||||||
@@ -572,17 +537,14 @@ local function parse_args(argv)
|
|||||||
-- `<repo>/code/duffle/word_count.metadata.h` is the canonical metadata location.
|
-- `<repo>/code/duffle/word_count.metadata.h` is the canonical metadata location.
|
||||||
-- `project_root` names `<repo>`; the resolver derives `<project_root>/code` separately.
|
-- `project_root` names `<repo>`; the resolver derives `<project_root>/code` separately.
|
||||||
if not args.project_root then
|
if not args.project_root then
|
||||||
--- @type string
|
local metadata_dir = duffle.dirname(duffle.normalize_path(args.metadata)) ---@type string
|
||||||
local metadata_dir = duffle.dirname(duffle.normalize_path(args.metadata))
|
local code_root = duffle.dirname(metadata_dir) ---@type string
|
||||||
--- @type string
|
|
||||||
local code_root = duffle.dirname(metadata_dir)
|
|
||||||
args.project_root = duffle.dirname(code_root)
|
args.project_root = duffle.dirname(code_root)
|
||||||
else
|
else
|
||||||
args.project_root = duffle.normalize_path(args.project_root)
|
args.project_root = duffle.normalize_path(args.project_root)
|
||||||
end
|
end
|
||||||
|
|
||||||
--- @type boolean
|
local has_unity = type(args.unity_root) == "string" and args.unity_root ~= "" ---@type boolean
|
||||||
local has_unity = type(args.unity_root) == "string" and args.unity_root ~= ""
|
|
||||||
if has_unity and #args.sources > 0 then
|
if has_unity and #args.sources > 0 then
|
||||||
io.stderr:write("ps1_meta: --unity-root FILE and --source FILE are mutually exclusive\n")
|
io.stderr:write("ps1_meta: --unity-root FILE and --source FILE are mutually exclusive\n")
|
||||||
os.exit(EXIT_INTERNAL_ERROR)
|
os.exit(EXIT_INTERNAL_ERROR)
|
||||||
@@ -595,14 +557,10 @@ local function parse_args(argv)
|
|||||||
-- Post-link opt-ins (--gdb-runtime, --dwarf-injection) write output that depends on the linked ELF.
|
-- Post-link opt-ins (--gdb-runtime, --dwarf-injection) write output that depends on the linked ELF.
|
||||||
-- Without --elf the metaprogram can't satisfy those requests, so refuse loud and early.
|
-- Without --elf the metaprogram can't satisfy those requests, so refuse loud and early.
|
||||||
-- This covers the explicit --post-link batch, --dwarf-injection by itself, and --gdb-runtime by itself.
|
-- This covers the explicit --post-link batch, --dwarf-injection by itself, and --gdb-runtime by itself.
|
||||||
--- @type PassFlags
|
local flags = args.flags or {} ---@type PassFlags
|
||||||
local flags = args.flags or {}
|
local elf_path = flags.elf_path ---@type string|nil
|
||||||
--- @type string|nil
|
local has_elf = type(elf_path) == "string" and #elf_path > 0 ---@type boolean
|
||||||
local elf_path = flags.elf_path
|
local post_links = flags.gdb_runtime or flags.dwarf_injection ---@type boolean
|
||||||
--- @type boolean
|
|
||||||
local has_elf = type(elf_path) == "string" and #elf_path > 0
|
|
||||||
--- @type boolean
|
|
||||||
local post_links = flags.gdb_runtime or flags.dwarf_injection
|
|
||||||
if post_links and not has_elf then
|
if post_links and not has_elf then
|
||||||
io.stderr:write("ps1_meta: --elf PATH is required for post-link output\n")
|
io.stderr:write("ps1_meta: --elf PATH is required for post-link output\n")
|
||||||
os.exit(EXIT_INTERNAL_ERROR)
|
os.exit(EXIT_INTERNAL_ERROR)
|
||||||
@@ -621,12 +579,9 @@ end
|
|||||||
--- @param args ParsedArgs
|
--- @param args ParsedArgs
|
||||||
--- @return PassCtx
|
--- @return PassCtx
|
||||||
local function build_ctx(args)
|
local function build_ctx(args)
|
||||||
--- @type string
|
local normalized_project_root = duffle.normalize_path(args.project_root) ---@type string
|
||||||
local normalized_project_root = duffle.normalize_path(args.project_root)
|
local project_root = normalized_project_root ---@type string
|
||||||
--- @type string
|
local project_root_is_absolute = normalized_project_root:match("^%a:/") ---@type boolean
|
||||||
local project_root = normalized_project_root
|
|
||||||
--- @type boolean
|
|
||||||
local project_root_is_absolute = normalized_project_root:match("^%a:/")
|
|
||||||
or normalized_project_root:sub(1, 2) == "//"
|
or normalized_project_root:sub(1, 2) == "//"
|
||||||
or normalized_project_root:sub(1, 1) == "/"
|
or normalized_project_root:sub(1, 1) == "/"
|
||||||
if not project_root_is_absolute then
|
if not project_root_is_absolute then
|
||||||
@@ -637,11 +592,9 @@ local function build_ctx(args)
|
|||||||
-- Do not route POSIX/UNC/drive-absolute paths through to_absolute_path.
|
-- Do not route POSIX/UNC/drive-absolute paths through to_absolute_path.
|
||||||
duffle.canonical_path_key(project_root)
|
duffle.canonical_path_key(project_root)
|
||||||
end
|
end
|
||||||
--- @type Corpus
|
local resolution ---@type Corpus
|
||||||
local resolution
|
|
||||||
if args.unity_root then
|
if args.unity_root then
|
||||||
--- @type boolean, Corpus|string
|
local ok_resolve, resolved = pcall(duffle.resolve_source_corpus, { ---@type boolean, Corpus|string
|
||||||
local ok_resolve, resolved = pcall(duffle.resolve_source_corpus, {
|
|
||||||
unity_root = args.unity_root,
|
unity_root = args.unity_root,
|
||||||
project_root = project_root,
|
project_root = project_root,
|
||||||
})
|
})
|
||||||
@@ -651,45 +604,35 @@ local function build_ctx(args)
|
|||||||
end
|
end
|
||||||
resolution = resolved
|
resolution = resolved
|
||||||
else
|
else
|
||||||
--- @type SourceFile[]
|
local source_order = {} ---@type SourceFile[]
|
||||||
local source_order = {}
|
local sources_by_path = {} ---@type table<Path, SourceFile>
|
||||||
--- @type table<Path, SourceFile>
|
local resolver = { ---@type SourceResolver
|
||||||
local sources_by_path = {}
|
|
||||||
--- @type SourceResolver
|
|
||||||
local resolver = {
|
|
||||||
resolved = {},
|
resolved = {},
|
||||||
skipped = {},
|
skipped = {},
|
||||||
shadowed = {},
|
shadowed = {},
|
||||||
}
|
}
|
||||||
--- @type integer, string
|
for _, input_path in ipairs(args.sources) do ---@type integer, string
|
||||||
for _, input_path in ipairs(args.sources) do
|
local path = duffle.normalize_path(input_path) ---@type string
|
||||||
--- @type string
|
local key_ok, key_or_error = pcall(duffle.canonical_path_key, path) ---@type boolean, string
|
||||||
local path = duffle.normalize_path(input_path)
|
|
||||||
--- @type boolean, string
|
|
||||||
local key_ok, key_or_error = pcall(duffle.canonical_path_key, path)
|
|
||||||
if not key_ok then
|
if not key_ok then
|
||||||
error("ps1_meta: invalid --source " .. input_path .. ": " .. tostring(key_or_error), 0)
|
error("ps1_meta: invalid --source " .. input_path .. ": " .. tostring(key_or_error), 0)
|
||||||
end
|
end
|
||||||
--- @type file*|nil
|
local file = io.open(path, "r") ---@type file*|nil
|
||||||
local file = io.open(path, "r")
|
|
||||||
if not file then
|
if not file then
|
||||||
io.stderr:write("ps1_meta: cannot open --source " .. input_path .. "\n")
|
io.stderr:write("ps1_meta: cannot open --source " .. input_path .. "\n")
|
||||||
os.exit(EXIT_INTERNAL_ERROR)
|
os.exit(EXIT_INTERNAL_ERROR)
|
||||||
end
|
end
|
||||||
--- @type string
|
local text = file:read("*a") ---@type string
|
||||||
local text = file:read("*a")
|
|
||||||
file:close()
|
file:close()
|
||||||
|
|
||||||
--- @type SourceFile
|
local source = { ---@type SourceFile
|
||||||
local source = {
|
|
||||||
path = path,
|
path = path,
|
||||||
text = text,
|
text = text,
|
||||||
dir = duffle.dirname(path),
|
dir = duffle.dirname(path),
|
||||||
basename = duffle.basename_no_ext(path),
|
basename = duffle.basename_no_ext(path),
|
||||||
}
|
}
|
||||||
source_order[#source_order + 1] = source
|
source_order[#source_order + 1] = source
|
||||||
--- @type string
|
local key = key_or_error ---@type string
|
||||||
local key = key_or_error
|
|
||||||
if not sources_by_path[key] then sources_by_path[key] = source end
|
if not sources_by_path[key] then sources_by_path[key] = source end
|
||||||
resolver.resolved[#resolver.resolved + 1] = {
|
resolver.resolved[#resolver.resolved + 1] = {
|
||||||
include_path = path,
|
include_path = path,
|
||||||
@@ -713,8 +656,7 @@ local function build_ctx(args)
|
|||||||
}
|
}
|
||||||
end
|
end
|
||||||
|
|
||||||
--- @type Corpus
|
local corpus = { ---@type Corpus
|
||||||
local corpus = {
|
|
||||||
unity_root = resolution.unity_root,
|
unity_root = resolution.unity_root,
|
||||||
project_root = resolution.project_root,
|
project_root = resolution.project_root,
|
||||||
code_root = resolution.code_root,
|
code_root = resolution.code_root,
|
||||||
@@ -735,8 +677,7 @@ local function build_ctx(args)
|
|||||||
collisions = {},
|
collisions = {},
|
||||||
resolver = resolution.resolver,
|
resolver = resolution.resolver,
|
||||||
}
|
}
|
||||||
--- @type PassCtx
|
local ctx = { ---@type PassCtx
|
||||||
local ctx = {
|
|
||||||
metadata_path = args.metadata,
|
metadata_path = args.metadata,
|
||||||
shared = { corpus = corpus },
|
shared = { corpus = corpus },
|
||||||
out_root = args.out_root,
|
out_root = args.out_root,
|
||||||
@@ -765,21 +706,15 @@ end
|
|||||||
--- Keeping these blocks local makes the topological sort self-contained.
|
--- Keeping these blocks local makes the topological sort self-contained.
|
||||||
local function topo_sort(passes, requested_set)
|
local function topo_sort(passes, requested_set)
|
||||||
-- Dependency closure: include every pass transitively required by `requested_set`.
|
-- Dependency closure: include every pass transitively required by `requested_set`.
|
||||||
--- @type table<string, boolean> -- bag: pass name -> needed
|
local needed = {} ---@type table<string, boolean> -- bag: pass name -> needed
|
||||||
local needed = {}
|
for _, name in ipairs(requested_set) do needed[name] = true end ---@type integer, string
|
||||||
--- @type integer, string
|
local changed = true ---@type boolean
|
||||||
for _, name in ipairs(requested_set) do needed[name] = true end
|
|
||||||
--- @type boolean
|
|
||||||
local changed = true
|
|
||||||
while changed do
|
while changed do
|
||||||
changed = false
|
changed = false
|
||||||
--- @type string, boolean
|
for name, _ in pairs(needed) do ---@type string, boolean
|
||||||
for name, _ in pairs(needed) do
|
local pass = passes[name] ---@type PassDescriptor
|
||||||
--- @type PassDescriptor
|
|
||||||
local pass = passes[name]
|
|
||||||
if not pass then error("unknown pass '" .. name .. "' requested") end
|
if not pass then error("unknown pass '" .. name .. "' requested") end
|
||||||
--- @type integer, string
|
for _, dep in ipairs(pass.deps) do ---@type integer, string
|
||||||
for _, dep in ipairs(pass.deps) do
|
|
||||||
if not needed[dep] then
|
if not needed[dep] then
|
||||||
needed[dep] = true
|
needed[dep] = true
|
||||||
changed = true
|
changed = true
|
||||||
@@ -789,14 +724,10 @@ local function topo_sort(passes, requested_set)
|
|||||||
end
|
end
|
||||||
|
|
||||||
-- In-degree calculation: count each needed pass's needed dependencies.
|
-- In-degree calculation: count each needed pass's needed dependencies.
|
||||||
--- @type table<string, integer> -- bag: pass name -> in-degree
|
local in_degree = {} ---@type table<string, integer> -- bag: pass name -> in-degree
|
||||||
local in_degree = {}
|
for name, _ in pairs(needed) do in_degree[name] = 0 end ---@type string, boolean
|
||||||
--- @type string, boolean
|
for name, _ in pairs(needed) do ---@type string, boolean
|
||||||
for name, _ in pairs(needed) do in_degree[name] = 0 end
|
for _, dep in ipairs(passes[name].deps) do ---@type integer, string
|
||||||
--- @type string, boolean
|
|
||||||
for name, _ in pairs(needed) do
|
|
||||||
--- @type integer, string
|
|
||||||
for _, dep in ipairs(passes[name].deps) do
|
|
||||||
if needed[dep] then
|
if needed[dep] then
|
||||||
in_degree[name] = in_degree[name] + 1
|
in_degree[name] = in_degree[name] + 1
|
||||||
end
|
end
|
||||||
@@ -804,27 +735,21 @@ local function topo_sort(passes, requested_set)
|
|||||||
end
|
end
|
||||||
|
|
||||||
-- Ready-queue seeding: add zero-in-degree passes in deterministic order.
|
-- Ready-queue seeding: add zero-in-degree passes in deterministic order.
|
||||||
--- @type string[]
|
local ready = {} ---@type string[]
|
||||||
local ready = {}
|
for name, deg in pairs(in_degree) do ---@type string, integer
|
||||||
--- @type string, integer
|
|
||||||
for name, deg in pairs(in_degree) do
|
|
||||||
if deg == 0 then ready[#ready + 1] = name end
|
if deg == 0 then ready[#ready + 1] = name end
|
||||||
end
|
end
|
||||||
table.sort(ready)
|
table.sort(ready)
|
||||||
|
|
||||||
-- Ready-queue drain: decrement dependents when each pass is emitted.
|
-- Ready-queue drain: decrement dependents when each pass is emitted.
|
||||||
-- Newly-zero-degree passes are inserted back into the ready queue (kept sorted).
|
-- Newly-zero-degree passes are inserted back into the ready queue (kept sorted).
|
||||||
--- @type string[]
|
local order = {} ---@type string[]
|
||||||
local order = {}
|
|
||||||
while #ready > 0 do
|
while #ready > 0 do
|
||||||
--- @type string
|
local just_finished = table.remove(ready, 1) ---@type string
|
||||||
local just_finished = table.remove(ready, 1)
|
|
||||||
order[#order + 1] = just_finished
|
order[#order + 1] = just_finished
|
||||||
--- @type string, boolean
|
for name, _ in pairs(needed) do ---@type string, boolean
|
||||||
for name, _ in pairs(needed) do
|
|
||||||
if name ~= just_finished then
|
if name ~= just_finished then
|
||||||
--- @type integer, string
|
for _, dep in ipairs(passes[name].deps) do ---@type integer, string
|
||||||
for _, dep in ipairs(passes[name].deps) do
|
|
||||||
if dep == just_finished then
|
if dep == just_finished then
|
||||||
in_degree[name] = in_degree[name] - 1
|
in_degree[name] = in_degree[name] - 1
|
||||||
if in_degree[name] == 0 then
|
if in_degree[name] == 0 then
|
||||||
@@ -840,13 +765,10 @@ local function topo_sort(passes, requested_set)
|
|||||||
-- Cycle detection: if `order` doesn't include all needed passes, some are stuck with in_degree > 0
|
-- Cycle detection: if `order` doesn't include all needed passes, some are stuck with in_degree > 0
|
||||||
-- (the cycle closed on itself before Kahn could process them).
|
-- (the cycle closed on itself before Kahn could process them).
|
||||||
-- Without this check, a fully-closed cycle (e.g. A -> B -> A) would silently return an empty order list, leaving the orchestrator to dispatch nothing.
|
-- Without this check, a fully-closed cycle (e.g. A -> B -> A) would silently return an empty order list, leaving the orchestrator to dispatch nothing.
|
||||||
--- @type integer
|
local needed_count = 0 ---@type integer
|
||||||
local needed_count = 0
|
for _ in pairs(needed) do needed_count = needed_count + 1 end ---@type string -- count hash entries; Lua's #t doesn't work
|
||||||
--- @type string
|
|
||||||
for _ in pairs(needed) do needed_count = needed_count + 1 end -- count hash entries; Lua's #t doesn't work
|
|
||||||
if #order ~= needed_count then
|
if #order ~= needed_count then
|
||||||
--- @type string, integer
|
for name, deg in pairs(in_degree) do ---@type string, integer
|
||||||
for name, deg in pairs(in_degree) do
|
|
||||||
if deg > 0 then
|
if deg > 0 then
|
||||||
error("dependency cycle detected involving pass '" .. name .. "'")
|
error("dependency cycle detected involving pass '" .. name .. "'")
|
||||||
end
|
end
|
||||||
@@ -867,11 +789,9 @@ end
|
|||||||
--- @param result PassResult
|
--- @param result PassResult
|
||||||
--- @return boolean
|
--- @return boolean
|
||||||
local function report_validation_errors(pass_name, pass, result)
|
local function report_validation_errors(pass_name, pass, result)
|
||||||
--- @type boolean
|
local has_errors = result.errors and #result.errors > 0 ---@type boolean
|
||||||
local has_errors = result.errors and #result.errors > 0
|
|
||||||
if not has_errors then return false end
|
if not has_errors then return false end
|
||||||
--- @type integer, PassFinding
|
for _, e in ipairs(result.errors) do ---@type integer, PassFinding
|
||||||
for _, e in ipairs(result.errors) do
|
|
||||||
io.stderr:write(string.format("[%s] line %d: %s\n", pass_name, e.line or 0, e.msg or ""))
|
io.stderr:write(string.format("[%s] line %d: %s\n", pass_name, e.line or 0, e.msg or ""))
|
||||||
end
|
end
|
||||||
return PASS_KIND_STOP_ON_ERROR[pass.kind] == true
|
return PASS_KIND_STOP_ON_ERROR[pass.kind] == true
|
||||||
@@ -882,16 +802,11 @@ end
|
|||||||
--- @param order string[]
|
--- @param order string[]
|
||||||
--- @return boolean -- true if any validation errors were reported
|
--- @return boolean -- true if any validation errors were reported
|
||||||
local function dispatch_passes(ctx, order)
|
local function dispatch_passes(ctx, order)
|
||||||
--- @type boolean
|
local had_errors = false ---@type boolean
|
||||||
local had_errors = false
|
for _, pass_name in ipairs(order) do ---@type integer, string
|
||||||
--- @type integer, string
|
local pass = PASSES[pass_name] ---@type PassDescriptor
|
||||||
for _, pass_name in ipairs(order) do
|
local mod = require(pass.module) ---@type PassModule
|
||||||
--- @type PassDescriptor
|
local result = mod.run(ctx) ---@type PassResult
|
||||||
local pass = PASSES[pass_name]
|
|
||||||
--- @type PassModule
|
|
||||||
local mod = require(pass.module)
|
|
||||||
--- @type PassResult
|
|
||||||
local result = mod.run(ctx)
|
|
||||||
if report_validation_errors(pass_name, pass, result) then
|
if report_validation_errors(pass_name, pass, result) then
|
||||||
had_errors = true
|
had_errors = true
|
||||||
end
|
end
|
||||||
@@ -903,20 +818,14 @@ end
|
|||||||
--- @param argv string[]
|
--- @param argv string[]
|
||||||
--- @return nil
|
--- @return nil
|
||||||
local function main(argv)
|
local function main(argv)
|
||||||
--- @type boolean, string|nil
|
local ok, err = pcall(function() ---@type boolean, string|nil
|
||||||
local ok, err = pcall(function()
|
local args = parse_args(argv) ---@type ParsedArgs
|
||||||
--- @type ParsedArgs
|
local ctx = build_ctx(args) ---@type PassCtx
|
||||||
local args = parse_args(argv)
|
|
||||||
--- @type PassCtx
|
|
||||||
local ctx = build_ctx(args)
|
|
||||||
|
|
||||||
--- @type string[]
|
local requested = args.requested_set ---@type string[]
|
||||||
local requested = args.requested_set
|
local closed = topo_sort(PASSES, requested) ---@type string[]
|
||||||
--- @type string[]
|
|
||||||
local closed = topo_sort(PASSES, requested)
|
|
||||||
|
|
||||||
--- @type boolean
|
local had_errors = dispatch_passes(ctx, closed) ---@type boolean
|
||||||
local had_errors = dispatch_passes(ctx, closed)
|
|
||||||
if had_errors then os.exit(EXIT_VALIDATION_ERRORS) end
|
if had_errors then os.exit(EXIT_VALIDATION_ERRORS) end
|
||||||
end)
|
end)
|
||||||
|
|
||||||
@@ -931,8 +840,7 @@ end
|
|||||||
-- Module export for in-process consumers (tests that dofile this script).
|
-- Module export for in-process consumers (tests that dofile this script).
|
||||||
-- The conditional `main(...)` call below only fires when this file is invoked as the entry script (arg[0] ends in "ps1_meta.lua");
|
-- The conditional `main(...)` call below only fires when this file is invoked as the entry script (arg[0] ends in "ps1_meta.lua");
|
||||||
-- in dofile() mode (test's arg[0] does not match), main() is skipped and the chunk returns `_M` to the caller.
|
-- in dofile() mode (test's arg[0] does not match), main() is skipped and the chunk returns `_M` to the caller.
|
||||||
--- @type Ps1MetaMod
|
local _M = { ---@type Ps1MetaMod
|
||||||
local _M = {
|
|
||||||
PASSES = PASSES,
|
PASSES = PASSES,
|
||||||
PASS_KIND_STOP_ON_ERROR = PASS_KIND_STOP_ON_ERROR,
|
PASS_KIND_STOP_ON_ERROR = PASS_KIND_STOP_ON_ERROR,
|
||||||
parse_args = parse_args,
|
parse_args = parse_args,
|
||||||
|
|||||||
Reference in New Issue
Block a user