utilizing trailing type annotations more

This commit is contained in:
ed
2026-08-19 23:35:57 -04:00
parent 449216967b
commit 2a087f735e
18 changed files with 2785 additions and 5468 deletions
+75 -150
View File
@@ -37,12 +37,9 @@
-- Bootstrap: load `duffle_paths.lua` via `debug.getinfo(1, "S").source`
-- (works both standalone + when require'd). `duffle_paths.lua` sets package.path then returns `require("duffle")`
-- at the bottom, so the dofile value IS the duffle module.
--- @type string
local _bootstrap_dir = debug.getinfo(1, "S").source:match("^@?(.*[/\\])") or "./"
--- @type DuffleExport
local duffle = dofile(_bootstrap_dir .. "../duffle_paths.lua")
--- @type ElfDwarfMod
local elf_dwarf = require("elf_dwarf")
local _bootstrap_dir = debug.getinfo(1, "S").source:match("^@?(.*[/\\])") or "./" ---@type string
local duffle = dofile(_bootstrap_dir .. "../duffle_paths.lua") ---@type DuffleExport
local elf_dwarf = require("elf_dwarf") ---@type ElfDwarfMod
-- ════════════════════════════════════════════════════════════════════════════
-- Constants
@@ -50,8 +47,7 @@ local elf_dwarf = require("elf_dwarf")
-- Format version emitted as the first line. Bump + add a migration test if the format changes;
-- the gdb runtime loader rejects mismatches (E2).
--- @type integer
local FORMAT_VERSION = 1
local FORMAT_VERSION = 1 ---@type integer
-- ════════════════════════════════════════════════════════════════════════════
-- Type declarations
@@ -107,23 +103,16 @@ local FORMAT_VERSION = 1
--- @return WordMapEntry[]
--- @return integer
local function canonical_word_entries(atom)
--- @type AtomPaths
local paths = atom.paths or {}
--- @type WordEvent[]
local events = paths.word_events or {}
--- @type EmissionItem[]
local word_items = {}
--- @type integer, EmissionItem
for _, item in ipairs(paths.items or {}) do
local paths = atom.paths or {} ---@type AtomPaths
local events = paths.word_events or {} ---@type WordEvent[]
local word_items = {} ---@type EmissionItem[]
for _, item in ipairs(paths.items or {}) do ---@type integer, EmissionItem
if item.kind == "word" then word_items[#word_items + 1] = item end
end
--- @type WordMapEntry[]
local entries = {}
--- @type integer, WordEvent
for index, event in ipairs(events) do
--- @type EmissionItem
local item = word_items[index] or {}
local entries = {} ---@type WordMapEntry[]
for index, event in ipairs(events) do ---@type integer, WordEvent
local item = word_items[index] or {} ---@type EmissionItem
entries[#entries + 1] = {
pos = event.i or (index - 1),
line = event.call_line or item.line or 0,
@@ -149,20 +138,14 @@ end
--- @return string[]
--- @return integer
local function emit_provenance_stanza(src, atom, wc)
--- @type string[]
local lines = {}
--- @type string
local rel_path = src.path:gsub("\\\\", "/")
--- @type WordMapEntry[], integer
local entries, total = canonical_word_entries(atom)
local lines = {} ---@type string[]
local rel_path = src.path:gsub("\\\\", "/") ---@type string
local entries, total = canonical_word_entries(atom) ---@type WordMapEntry[], integer
lines[#lines + 1] = string.format('ATOM %s "%s" 0', atom.raw_name or atom.name, rel_path)
--- @type integer, WordMapEntry
for _, entry in ipairs(entries) do
--- @type InvocationRecord|nil
local inv = entry.invocation
--- @type integer|nil
local macro_count = inv and wc["mac_" .. inv.component_name]
for _, entry in ipairs(entries) do ---@type integer, WordMapEntry
local inv = entry.invocation ---@type InvocationRecord|nil
local macro_count = inv and wc["mac_" .. inv.component_name] ---@type integer|nil
if inv and macro_count ~= nil then
lines[#lines + 1] = string.format('WORD %d CALL %s:%d MACRO %s "%s:%d" BODY %d'
, entry.pos, rel_path, entry.line, inv.component_name
@@ -182,8 +165,7 @@ end
--- @param wc WordCounts
--- @return string
local function render_provenance(src, wc)
--- @type string[]
local lines = {}
local lines = {} ---@type string[]
lines[#lines + 1] = "# FORMAT_VERSION 1"
lines[#lines + 1] = "# auto-generated by ps1_meta.lua (passes/atoms_source_map.lua) — DO NOT EDIT"
lines[#lines + 1] = "# Per-.word provenance: maps each emitted .word to its call site (atom body"
@@ -195,17 +177,13 @@ local function render_provenance(src, wc)
--- @param atom AtomEntry
--- @return nil
local function append(atom)
--- @type string[]
local stanza = emit_provenance_stanza(src, atom, wc)
--- @type integer, string
for _, line in ipairs(stanza) do lines[#lines + 1] = line end
local stanza = emit_provenance_stanza(src, atom, wc) ---@type string[]
for _, line in ipairs(stanza) do lines[#lines + 1] = line end ---@type integer, string
end
--- @type integer, AtomEntry
for _, atom in ipairs(src.scan.atoms or {}) do
for _, atom in ipairs(src.scan.atoms or {}) do ---@type integer, AtomEntry
if atom.paths then append(atom) end
end
--- @type integer, AtomEntry
for _, atom in ipairs(src.scan.raw_atoms or {}) do
for _, atom in ipairs(src.scan.raw_atoms or {}) do ---@type integer, AtomEntry
if atom.paths then append(atom) end
end
@@ -219,16 +197,12 @@ end
--- @return string[]
--- @return integer
local function emit_atom_stanza(src, atom)
--- @type string[]
local lines = {}
--- @type string
local rel_path = src.path:gsub("\\\\", "/")
--- @type WordMapEntry[], integer
local entries, total = canonical_word_entries(atom)
local lines = {} ---@type string[]
local rel_path = src.path:gsub("\\\\", "/") ---@type string
local entries, total = canonical_word_entries(atom) ---@type WordMapEntry[], integer
lines[#lines + 1] = string.format('ATOM %s "%s" 0', atom.raw_name or atom.name, rel_path)
--- @type integer, WordMapEntry
for _, entry in ipairs(entries) do
for _, entry in ipairs(entries) do ---@type integer, WordMapEntry
lines[#lines + 1] = string.format("WORD %d LINE %d TEXT %s",
entry.pos, entry.line, entry.text)
end
@@ -242,25 +216,20 @@ end
--- @param src SourceFile
--- @return string
local function render_source_map(src)
--- @type string[]
local lines = {}
local lines = {} ---@type string[]
lines[#lines + 1] = "# FORMAT_VERSION " .. FORMAT_VERSION
lines[#lines + 1] = "# auto-generated by ps1_meta.lua (passes/atoms_source_map.lua) — DO NOT EDIT"
--- @param atom AtomEntry
--- @return nil
local function append(atom)
--- @type string[]
local stanza = emit_atom_stanza(src, atom)
--- @type integer, string
for _, line in ipairs(stanza) do lines[#lines + 1] = line end
local stanza = emit_atom_stanza(src, atom) ---@type string[]
for _, line in ipairs(stanza) do lines[#lines + 1] = line end ---@type integer, string
end
--- @type integer, AtomEntry
for _, atom in ipairs(src.scan.atoms or {}) do
for _, atom in ipairs(src.scan.atoms or {}) do ---@type integer, AtomEntry
if atom.paths then append(atom) end
end
--- @type integer, AtomEntry
for _, atom in ipairs(src.scan.raw_atoms or {}) do
for _, atom in ipairs(src.scan.raw_atoms or {}) do ---@type integer, AtomEntry
if atom.paths then append(atom) end
end
@@ -283,28 +252,20 @@ end
--- @param ctx PassCtx
--- @return GdbAtomRecord[]
local function build_atom_table(ctx)
--- @type table<string, NmAddr>
local addrs = elf_dwarf.read_nm(ctx.flags.elf_path)
--- @type Corpus|nil
local corpus = ctx.shared and ctx.shared.corpus
--- @type GdbAtomRecord[]
local matched = {}
local addrs = elf_dwarf.read_nm(ctx.flags.elf_path) ---@type table<string, NmAddr>
local corpus = ctx.shared and ctx.shared.corpus ---@type Corpus|nil
local matched = {} ---@type GdbAtomRecord[]
--- @type integer, SourceFile
for _, src in ipairs(corpus.source_order or {}) do
--- @type string
local file_base = src.path:match("([^/\\\\]+)$") or src.path
for _, src in ipairs(corpus.source_order or {}) do ---@type integer, SourceFile
local file_base = src.path:match("([^/\\\\]+)$") or src.path ---@type string
--- @param atom AtomEntry
--- @return nil
local function append(atom)
if not atom.paths then return end
--- @type string
local name = atom.raw_name or atom.name
--- @type NmAddr|nil
local info = addrs[name]
local name = atom.raw_name or atom.name ---@type string
local info = addrs[name] ---@type NmAddr|nil
if not info then return end
--- @type WordMapEntry[], integer
local entries, total = canonical_word_entries(atom)
local entries, total = canonical_word_entries(atom) ---@type WordMapEntry[], integer
matched[#matched + 1] = {
name = name,
src_path = src.path,
@@ -315,10 +276,8 @@ local function build_atom_table(ctx)
entries = entries,
}
end
--- @type integer, AtomEntry
for _, atom in ipairs((src.scan or {}).atoms or {}) do append(atom) end
--- @type integer, AtomEntry
for _, atom in ipairs((src.scan or {}).raw_atoms or {}) do append(atom) end
for _, atom in ipairs((src.scan or {}).atoms or {}) do append(atom) end ---@type integer, AtomEntry
for _, atom in ipairs((src.scan or {}).raw_atoms or {}) do append(atom) end ---@type integer, AtomEntry
end
-- Deterministic order: sort by address (matches `nm` output ordering).
@@ -326,8 +285,7 @@ local function build_atom_table(ctx)
--- @param b GdbAtomRecord
--- @return boolean
table.sort(matched, function(a, b) return a.addr < b.addr end)
--- @type integer, GdbAtomRecord
for i, a in ipairs(matched) do a.idx = i - 1 end
for i, a in ipairs(matched) do a.idx = i - 1 end ---@type integer, GdbAtomRecord
return matched
end
@@ -345,8 +303,7 @@ local function append_gdb_commands(lines, matched)
-- ── tape_atoms ──
-- Hardcoded one printf per atom. No loop.
lines[#lines + 1] = "define tape_atoms"
--- @type integer, GdbAtomRecord
for _, a in ipairs(matched) do
for _, a in ipairs(matched) do ---@type integer, GdbAtomRecord
-- gdb 12.1 quirk: literals in printf args require an attached target.
-- Use the per-atom convenience vars set above as printf args.
lines[#lines + 1] = string.format(' printf " %%-32s @ 0x%%08x %%4d words\\n", $__atom_name_%d, $__atom_addr_%d, $__atom_words_%d',
@@ -361,8 +318,7 @@ local function append_gdb_commands(lines, matched)
-- ── break_atom (generic) + per-atom break_atom_X ──
lines[#lines + 1] = "define break_atom"
lines[#lines + 1] = ' echo "Usage: break_atom_<exact_name> (pick from the list below)"'
--- @type integer, GdbAtomRecord
for _, a in ipairs(matched) do
for _, a in ipairs(matched) do ---@type integer, GdbAtomRecord
lines[#lines + 1] = string.format(' printf " break_atom_%%-32s\\n", $__atom_name_%d', a.idx)
end
lines[#lines + 1] = "end"
@@ -371,8 +327,7 @@ local function append_gdb_commands(lines, matched)
lines[#lines + 1] = "end"
lines[#lines + 1] = ""
--- @type integer, GdbAtomRecord
for _, a in ipairs(matched) do
for _, a in ipairs(matched) do ---@type integer, GdbAtomRecord
lines[#lines + 1] = string.format("define break_atom_%s", a.name)
lines[#lines + 1] = string.format(" break *$__atom_addr_%d", a.idx)
lines[#lines + 1] = string.format(' printf " Breakpoint set at %s (0x%%08x)\\n", $__atom_addr_%d', a.name, a.idx)
@@ -386,8 +341,7 @@ local function append_gdb_commands(lines, matched)
-- ── step_atom / next_atom ──
-- Hardcoded one tbreak per atom. No loop.
lines[#lines + 1] = "define step_atom"
--- @type integer, GdbAtomRecord
for _, a in ipairs(matched) do
for _, a in ipairs(matched) do ---@type integer, GdbAtomRecord
lines[#lines + 1] = string.format(" tbreak *$__atom_addr_%d", a.idx)
end
lines[#lines + 1] = " continue"
@@ -410,8 +364,7 @@ local function append_gdb_commands(lines, matched)
lines[#lines + 1] = "define where_in_atom"
lines[#lines + 1] = " set $__pc = (unsigned int)$pc"
lines[#lines + 1] = " set $__matched = 0"
--- @type integer, GdbAtomRecord
for _, a in ipairs(matched) do
for _, a in ipairs(matched) do ---@type integer, GdbAtomRecord
-- Precompute end_addr (gdb 12.1's expression evaluator chokes on `addr + words*4`).
lines[#lines + 1] = string.format(" set $__end_%d = $__atom_addr_%d + $__atom_words_%d * 4", a.idx, a.idx, a.idx)
lines[#lines + 1] = string.format(" if $__pc >= $__atom_addr_%d && $__pc < $__end_%d", a.idx, a.idx)
@@ -420,18 +373,15 @@ local function append_gdb_commands(lines, matched)
lines[#lines + 1] = string.format(" set $__word = ($__pc - $__atom_addr_%d) / 4", a.idx)
lines[#lines + 1] = string.format(' printf "word: %%d/%%d\\n", $__word, $__atom_words_%d', a.idx)
-- One inner-if per WORD entry. Each word's line + text hardcoded.
--- @type integer, WordMapEntry
for _, we in ipairs(a.entries) do
for _, we in ipairs(a.entries) do ---@type integer, WordMapEntry
lines[#lines + 1] = string.format(" if $__word == %d", we.pos)
-- Escape TEXT for printf format string.
--- @type string
local escaped_text = we.text:gsub("%%", "%%%%"):gsub('"', '\\"')
local escaped_text = we.text:gsub("%%", "%%%%"):gsub('"', '\\"') ---@type string
lines[#lines + 1] = string.format(' printf "source: %%s:%%d %%s\\n", $__atom_file_%d, %d, "%s"', a.idx, we.line, escaped_text)
lines[#lines + 1] = " end"
end
-- Fallback for words beyond the source map (shouldn't happen if nm matches).
--- @type integer
local max_word = 0
local max_word = 0 ---@type integer
if #a.entries > 0 then max_word = a.entries[#a.entries].pos end
lines[#lines + 1] = string.format(' if $__word > %d', max_word)
lines[#lines + 1] = ' printf "source: (no source-map entry for word %%d; map may be stale)\\n", $__word'
@@ -456,8 +406,7 @@ local function append_gdb_commands(lines, matched)
lines[#lines + 1] = " set $__in_atom = 0"
lines[#lines + 1] = " set $__did_step = 0"
lines[#lines + 1] = " set $__pc = (unsigned int)$pc"
--- @type integer, GdbAtomRecord
for _, a in ipairs(matched) do
for _, a in ipairs(matched) do ---@type integer, GdbAtomRecord
-- Precompute end_addr in the convenience var (single expression gdb handles).
lines[#lines + 1] = string.format(" set $__end_%d = $__atom_addr_%d + $__atom_words_%d * 4", a.idx, a.idx, a.idx)
lines[#lines + 1] = string.format(" if $__pc >= $__atom_addr_%d && $__pc < $__end_%d", a.idx, a.idx)
@@ -494,8 +443,7 @@ end
--- @return nil
local function emit_gdb_runtime(ctx)
if not (ctx.flags and ctx.flags.gdb_runtime) then return end
--- @type string|nil
local elf_path = ctx.flags.elf_path
local elf_path = ctx.flags.elf_path ---@type string|nil
if not elf_path or elf_path == "" then
io.stderr:write("[atoms_source_map] --gdb-runtime requires --elf <elf>\n")
return
@@ -506,15 +454,13 @@ local function emit_gdb_runtime(ctx)
return
end
--- @type GdbAtomRecord[]
local matched = build_atom_table(ctx)
local matched = build_atom_table(ctx) ---@type GdbAtomRecord[]
if #matched == 0 then
io.stderr:write("[atoms_source_map] --gdb-runtime: no atoms matched against nm symbols (stale scan?).\n")
return
end
--- @type string[]
local lines = {}
local lines = {} ---@type string[]
lines[#lines + 1] = "# Auto-generated by ps1_meta.lua (passes/atoms_source_map.lua)"
lines[#lines + 1] = "# DO NOT EDIT — re-run ps1_meta.lua --atoms-source-map --gdb-runtime to regenerate"
lines[#lines + 1] = "# Sourced by scripts/gdb/gdb_tape_atoms.gdb (the wrapper)."
@@ -535,8 +481,7 @@ local function emit_gdb_runtime(ctx)
-- Per-atom convenience vars (used as printf args; literals aren't accepted
-- without an attached target on gdb 12.1).
--- @type integer, GdbAtomRecord
for _, a in ipairs(matched) do
for _, a in ipairs(matched) do ---@type integer, GdbAtomRecord
lines[#lines + 1] = string.format('set $__atom_name_%d = "%s"', a.idx, gdb_escape(a.name))
lines[#lines + 1] = string.format("set $__atom_addr_%d = 0x%x", a.idx, a.addr)
lines[#lines + 1] = string.format("set $__atom_words_%d = %d", a.idx, a.words)
@@ -552,8 +497,7 @@ local function emit_gdb_runtime(ctx)
-- Confirmation line for the source operator.
lines[#lines + 1] = 'printf "[gdb_tape_atoms] runtime loaded %d atoms from %s\\n", $__atom_count, $__elf_path'
--- @type string
local out_path
local out_path ---@type string
-- Move out of `<out_root>/gdb_tape_atoms_runtime.gdb` to `<out_root>/../gdb_tape_atoms_runtime.gdb` when the conventional `<out_root>` is `<build>/gen`
-- (any equivalent spelling — relative, absolute backslash, absolute forward-slash, trailing-separator variants).
-- This puts the gdb runtime alongside the ELF at `build/` rather than under the report subdir.
@@ -566,8 +510,7 @@ local function emit_gdb_runtime(ctx)
if ends_with_gen_dir(ctx.out_root) then
-- Strip the trailing `/gen` segment, then write the runtime script under `build/`.
-- e.g. "C:/projects/Pikuma/ps1/build/gen" -> "C:/projects/Pikuma/ps1/build".
--- @type string
local parent = ctx.out_root:gsub("[/\\]gen[/\\]?$", "")
local parent = ctx.out_root:gsub("[/\\]gen[/\\]?$", "") ---@type string
out_path = parent .. "/gdb_tape_atoms_runtime.gdb"
else
out_path = ctx.out_root .. "/gdb_tape_atoms_runtime.gdb"
@@ -581,8 +524,7 @@ end
-- M — module exports
-- ════════════════════════════════════════════════════════════════════════════
--- @type AtomSourceMapPass
local M = {}
local M = {} ---@type AtomSourceMapPass
-- Expose the pure render functions so `report.lua` and the focused tests can call them directly without triggering the file-emit path.
M.render_source_map = render_source_map
@@ -594,22 +536,15 @@ M.render_provenance = render_provenance
function M.render_atom_source_map(atom)
assert(type(atom) == "table", "render_atom_source_map: atom must be a table")
assert(type(atom.paths) == "table", "render_atom_source_map: atom.paths must be a table")
--- @type WordMapEntry[], integer
local entries, total = canonical_word_entries(atom)
--- @type string[]
local lines = {}
local entries, total = canonical_word_entries(atom) ---@type WordMapEntry[], integer
local lines = {} ---@type string[]
lines[#lines + 1] = string.format("ATOM %s %d", (atom.raw_name or atom.name), total)
--- @type integer, WordMapEntry
for _, entry in ipairs(entries) do
--- @type string
local word_line = string.format("WORD %d LINE %d TEXT %s",
for _, entry in ipairs(entries) do ---@type integer, WordMapEntry
local word_line = string.format("WORD %d LINE %d TEXT %s", ---@type string
entry.pos, entry.line, entry.text)
--- @type string[]
local keys = {}
--- @type integer
for pos = 1, 16 do
--- @type string|nil
local k = entry.gpr_keys and entry.gpr_keys[pos]
local keys = {} ---@type string[]
for pos = 1, 16 do ---@type integer
local k = entry.gpr_keys and entry.gpr_keys[pos] ---@type string|nil
if type(k) == "string" and k:sub(1, 7) == "reguse:" then
keys[#keys + 1] = k
end
@@ -635,17 +570,12 @@ function M.render_atom_provenance(atom, wc, rel_path)
assert(type(atom) == "table", "render_atom_provenance: atom must be a table")
assert(type(atom.paths) == "table", "render_atom_provenance: atom.paths must be a table")
assert(type(rel_path) == "string", "render_atom_provenance: rel_path must be a string")
--- @type WordMapEntry[], integer
local entries, total = canonical_word_entries(atom)
--- @type string[]
local lines = {}
local entries, total = canonical_word_entries(atom) ---@type WordMapEntry[], integer
local lines = {} ---@type string[]
lines[#lines + 1] = string.format("ATOM %s %d", (atom.raw_name or atom.name), total)
--- @type integer, WordMapEntry
for _, entry in ipairs(entries) do
--- @type InvocationRecord|nil
local inv = entry.invocation
--- @type integer|nil
local macro_count = inv and wc and wc["mac_" .. inv.component_name]
for _, entry in ipairs(entries) do ---@type integer, WordMapEntry
local inv = entry.invocation ---@type InvocationRecord|nil
local macro_count = inv and wc and wc["mac_" .. inv.component_name] ---@type integer|nil
if inv and macro_count ~= nil then
lines[#lines + 1] = string.format('WORD %d CALL %s:%d MACRO %s "%s:%d" BODY %d'
, entry.pos, rel_path, entry.line, inv.component_name, inv.def_path or "", inv.def_line or 0, entry.body_line)
@@ -664,22 +594,17 @@ end
--- @param ctx PassCtx
--- @return PassResult
function M.run(ctx)
--- @type PassOutputEntry[]
local outputs = {}
--- @type PassFinding[]
local errors = {}
--- @type PassFinding[]
local warnings = {}
local outputs = {} ---@type PassOutputEntry[]
local errors = {} ---@type PassFinding[]
local warnings = {} ---@type PassFinding[]
--- @type Corpus|nil
local corpus = ctx.shared and ctx.shared.corpus
local corpus = ctx.shared and ctx.shared.corpus ---@type Corpus|nil
if type(corpus) ~= "table" or type(corpus.source_order) ~= "table" then
error("atoms_source_map.run requires ctx.shared.corpus.source_order (canonical corpus).", 0)
end
-- Word counts come from `corpus.word_counts` (populated by word_count_eval + components passes).
--- @type WordCounts
local wc = corpus.word_counts or {}
local wc = corpus.word_counts or {} ---@type WordCounts
if not next(wc) then
warnings[#warnings + 1] = {
line = 0,