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
+59 -118
View File
@@ -10,10 +10,8 @@
-- Bootstrap follows the entry scripts; `scripts/duffle_paths.lua` sets package.path and package.cpath. See `ps1_meta.lua` for the rationale.
-- `debug.getinfo(1, "S").source` locates this file for standalone and orchestrated runs, then `duffle_paths.lua` returns the loaded `duffle` module.
--- @type string
local _bootstrap_dir = debug.getinfo(1, "S").source:match("^@?(.*[/\\])") or "./"
--- @type DuffleExport
local duffle = dofile(_bootstrap_dir .. "../duffle_paths.lua")
local _bootstrap_dir = debug.getinfo(1, "S").source:match("^@?(.*[/\\])") or "./" ---@type string
local duffle = dofile(_bootstrap_dir .. "../duffle_paths.lua") ---@type DuffleExport
-- The annotation pass reads the source-derived registries from scan_source:
-- * pipe_ctx.register_alias_registry — for atom_dbg_reg_default(R_X, ...) and atom_reg_types(R_X, ...) member-identity checks
@@ -107,8 +105,7 @@ end
--- @param findings Findings
--- @return nil
local function check_unique_annotation(_item, pipe_ctx, findings)
--- @type string, integer
for name, n in pairs(pipe_ctx.annot_counts) do
for name, n in pairs(pipe_ctx.annot_counts) do ---@type string, integer
if n > 1 then
findings.errors[#findings.errors + 1] = {
line = pipe_ctx.atom_index[name] and pipe_ctx.atom_index[name].line or 0,
@@ -142,10 +139,8 @@ end
--- @param findings Findings
--- @return nil
local function check_macro_word_drift(m, pipe_ctx, findings)
--- @type WordCounts
local wc = (pipe_ctx and pipe_ctx.word_counts) or {}
--- @type integer|nil
local declared = wc[m.name]
local wc = (pipe_ctx and pipe_ctx.word_counts) or {} ---@type WordCounts
local declared = wc[m.name] ---@type integer|nil
if not declared then
findings.errors[#findings.errors + 1] = {
line = m.line,
@@ -174,10 +169,8 @@ end
--- @return nil
local function check_semantic_reg_defaults(_src, pipe_ctx, findings)
-- Detect duplicate defaults using the ordered occurrence list (the out.types hash only retains the last declaration).
--- @type table<string, integer> -- bag: register ident -> first source line
local seen_first_line = {}
--- @type integer, RegTypeOccurrence
for _, occ in ipairs(pipe_ctx.type_occurrences or {}) do
local seen_first_line = {} ---@type table<string, integer> -- bag: register ident -> first source line
for _, occ in ipairs(pipe_ctx.type_occurrences or {}) do ---@type integer, RegTypeOccurrence
if seen_first_line[occ.reg] == nil then
seen_first_line[occ.reg] = occ.source_line
else
@@ -189,12 +182,9 @@ local function check_semantic_reg_defaults(_src, pipe_ctx, findings)
}
end
end
--- @type table<string, AliasEntry>
local reg_registry = pipe_ctx.register_alias_registry or {}
--- @type table<string, TypeNameEntry>
local type_registry = pipe_ctx.type_name_registry or {}
--- @type string, RegTypeDefault
for reg, def in pairs(pipe_ctx.types or {}) do
local reg_registry = pipe_ctx.register_alias_registry or {} ---@type table<string, AliasEntry>
local type_registry = pipe_ctx.type_name_registry or {} ---@type table<string, TypeNameEntry>
for reg, def in pairs(pipe_ctx.types or {}) do ---@type string, RegTypeDefault
if not reg_registry[reg] then
findings.errors[#findings.errors + 1] = {
line = def.source_line,
@@ -229,15 +219,11 @@ end
--- @param findings Findings
--- @return nil
local function check_atom_reg_types(_src, pipe_ctx, findings)
--- @type table<string, AliasEntry>
local reg_registry = pipe_ctx.register_alias_registry or {}
--- @type table<string, TypeNameEntry>
local type_registry = pipe_ctx.type_name_registry or {}
--- @type integer, AtomInfoEntry
for _, ai in ipairs(pipe_ctx.atom_infos_list or {}) do
local reg_registry = pipe_ctx.register_alias_registry or {} ---@type table<string, AliasEntry>
local type_registry = pipe_ctx.type_name_registry or {} ---@type table<string, TypeNameEntry>
for _, ai in ipairs(pipe_ctx.atom_infos_list or {}) do ---@type integer, AtomInfoEntry
if ai.reg_type_overrides then
--- @type string, RegTypeOverride
for reg, ov in pairs(ai.reg_type_overrides) do
for reg, ov in pairs(ai.reg_type_overrides) do ---@type string, RegTypeOverride
if not reg_registry[reg] then
findings.errors[#findings.errors + 1] = {
line = ai.info_line,
@@ -265,13 +251,11 @@ end
--- @param findings Findings
--- @return nil
local function check_atom_view_layout(_src, pipe_ctx, findings)
--- @type string, AtomViewEntry
for atom_name, view in pairs(pipe_ctx.atom_views or {}) do
for atom_name, view in pairs(pipe_ctx.atom_views or {}) do ---@type string, AtomViewEntry
if not view.binds_name then
-- The atom had atom_reg_types but no atom_view; no layout check needed.
else
--- @type BindsEntry|nil
local bs = pipe_ctx.binds_index[view.binds_name]
local bs = pipe_ctx.binds_index[view.binds_name] ---@type BindsEntry|nil
if not bs then
findings.errors[#findings.errors + 1] = {
line = view.info_line,
@@ -297,16 +281,12 @@ end
--- @param findings Findings
--- @return nil
local function check_binds_no_duplicate_fields(_src, pipe_ctx, findings)
--- @type integer, BindsEntry
for _, bs in ipairs(pipe_ctx.binds_list or {}) do
--- @type table<string, integer> -- bag: field name -> occurrence count
local seen = {}
--- @type integer, TypeField
for _, f in ipairs(bs.fields or {}) do
for _, bs in ipairs(pipe_ctx.binds_list or {}) do ---@type integer, BindsEntry
local seen = {} ---@type table<string, integer> -- bag: field name -> occurrence count
for _, f in ipairs(bs.fields or {}) do ---@type integer, TypeField
seen[f.name] = (seen[f.name] or 0) + 1
end
--- @type string, integer
for name, count in pairs(seen) do
for name, count in pairs(seen) do ---@type string, integer
if count > 1 then
findings.errors[#findings.errors + 1] = {
line = bs.line,
@@ -334,10 +314,8 @@ end
--- @param findings Findings
--- @return nil
local function check_skip_marker(marker, _pipe_ctx, findings)
--- @type string
local kind = marker.marker_kind
--- @type integer
local line = marker.marker_line
local kind = marker.marker_kind ---@type string
local line = marker.marker_line ---@type integer
-- Left `scan.debug_skip_markers` with production records for `atom_dbg_skip` only; other identifiers take the walker's unrelated branch.
if marker.has_parens then
@@ -396,13 +374,10 @@ end
local function check_wave_context_migration(_src, pipe_ctx, findings)
if not (pipe_ctx.types and next(pipe_ctx.types)) then return end
if not (pipe_ctx.atom_infos_list) then return end
--- @type table<string, AliasEntry>
local reg_registry = pipe_ctx.register_alias_registry or {}
--- @type integer, AtomInfoEntry
for _, ai in ipairs(pipe_ctx.atom_infos_list) do
local reg_registry = pipe_ctx.register_alias_registry or {} ---@type table<string, AliasEntry>
for _, ai in ipairs(pipe_ctx.atom_infos_list) do ---@type integer, AtomInfoEntry
if ai.reg_type_overrides then
--- @type string, RegTypeOverride
for reg, _ in pairs(ai.reg_type_overrides) do
for reg, _ in pairs(ai.reg_type_overrides) do ---@type string, RegTypeOverride
if not reg_registry[reg] then
findings.warnings[#findings.warnings + 1] = {
line = 0,
@@ -429,8 +404,7 @@ end
--
-- Adding a new check = 1 row here + 1 function above. The `validate()` dispatch loop never needs editing.
--- @type CheckRule[]
local CHECK_RULES = {
local CHECK_RULES = { ---@type CheckRule[]
{ name = "atom_decl_exists", per_annot = check_atom_decl_exists },
{ name = "binds_struct_exists", per_annot = check_binds_struct_exists },
{ name = "unique_annotation", post = check_unique_annotation },
@@ -453,12 +427,9 @@ local CHECK_RULES = {
--- @param ctx PassCtx
--- @return PipeCtx
local function build_corpus_pipe_ctx(ctx)
--- @type PipeCtx
local view = duffle.corpus_view(ctx)
--- @type table<string, integer> -- bag: atom name -> annotation count
local annot_counts = {}
--- @type integer, AtomInfoEntry
for _, info in ipairs(view.atom_infos) do
local view = duffle.corpus_view(ctx) ---@type PipeCtx
local annot_counts = {} ---@type table<string, integer> -- bag: atom name -> annotation count
for _, info in ipairs(view.atom_infos) do ---@type integer, AtomInfoEntry
if info and info.atom_name then
annot_counts[info.atom_name] = (annot_counts[info.atom_name] or 0) + 1
end
@@ -476,17 +447,13 @@ end
--- @return AnnotatedResult
local function validate(ctx, src, corpus_pipe_ctx)
corpus_pipe_ctx = corpus_pipe_ctx or build_corpus_pipe_ctx(ctx)
--- @type SourceScan
local scan = src.scan
local scan = src.scan ---@type SourceScan
-- Build a per-source pipe_ctx: shared lookups come from `corpus_pipe_ctx`, while declarations, bodies, types, views, defaults, and occurrences come from `src.scan`.
--- @type table<string, integer> -- bag: register ident -> occurrence count
local seen_defaults = {}; for reg, _ in pairs (scan.types or {}) do seen_defaults[reg] = (seen_defaults[reg] or 0) + 1 end
--- @type AtomInfoEntry[]
local atom_infos_list = {}; for _, ai in ipairs(scan.atom_infos or {}) do atom_infos_list[#atom_infos_list + 1] = ai end
local seen_defaults = {}; for reg, _ in pairs (scan.types or {}) do seen_defaults[reg] = (seen_defaults[reg] or 0) + 1 end ---@type table<string, integer> -- bag: register ident -> occurrence count
local atom_infos_list = {}; for _, ai in ipairs(scan.atom_infos or {}) do atom_infos_list[#atom_infos_list + 1] = ai end ---@type AtomInfoEntry[]
--- @type PipeCtx
local pipe_ctx = {
local pipe_ctx = { ---@type PipeCtx
atom_index = {},
binds_index = {},
annot_counts = corpus_pipe_ctx.annot_counts,
@@ -500,29 +467,23 @@ local function validate(ctx, src, corpus_pipe_ctx)
register_alias_registry = corpus_pipe_ctx.register_alias_registry,
type_name_registry = corpus_pipe_ctx.type_name_registry,
}
--- @type AtomEntry[]
local atoms = {}
--- @type integer, AtomEntry
for _, a in ipairs(scan.atoms) do
local atoms = {} ---@type AtomEntry[]
for _, a in ipairs(scan.atoms) do ---@type integer, AtomEntry
if a.kind == "atom" or a.kind == "atom_proc" then
atoms[#atoms + 1] = a
pipe_ctx.atom_index[a.raw_name or a.name] = a
end
end
--- @type integer, BindsEntry
for _, b in ipairs(scan.binds) do pipe_ctx.binds_index[b.name] = b end
for _, b in ipairs(scan.binds) do pipe_ctx.binds_index[b.name] = b end ---@type integer, BindsEntry
-- Findings live in a single struct with three lists (errors / warnings / info).
-- Each check writes to the list appropriate for its severity.
--- @type Findings
local findings = { errors = {}, warnings = {}, info = {} }
local findings = { errors = {}, warnings = {}, info = {} } ---@type Findings
-- Lift parse-time errors already recorded in scan_source's atom_info payload into this pass's findings list.
--- @type integer, AtomInfoEntry
for _, info in ipairs(scan.atom_infos) do
for _, info in ipairs(scan.atom_infos) do ---@type integer, AtomInfoEntry
if info.errors then
--- @type integer, string
for _, msg in ipairs(info.errors) do
for _, msg in ipairs(info.errors) do ---@type integer, string
findings.errors[#findings.errors + 1] = {
line = info.info_line,
msg = string.format("'%s': %s", info.atom_name, msg),
@@ -532,8 +493,7 @@ local function validate(ctx, src, corpus_pipe_ctx)
end
-- THE per-annotation pipeline. ONE loop. CHECK_RULES dispatches per_annot rules.
--- @type integer, AtomInfoEntry
for _, info in ipairs(scan.atom_infos) do
for _, info in ipairs(scan.atom_infos) do ---@type integer, AtomInfoEntry
duffle.run_check_rules(CHECK_RULES, "per_annot", info, pipe_ctx, findings)
end
@@ -542,17 +502,14 @@ local function validate(ctx, src, corpus_pipe_ctx)
-- scan_source records each marker in scan.debug_skip_markers; this loop validates each record independently and emits at most one error per marker.
-- Valid markers stamp `debug_skip = true` on the following atom or component declaration, which downstream consumers read directly.
--- @type DebugSkipMarker[]
local skip_markers = scan.debug_skip_markers or {}
--- @type integer, DebugSkipMarker
for _, marker in ipairs(skip_markers) do
local skip_markers = scan.debug_skip_markers or {} ---@type DebugSkipMarker[]
for _, marker in ipairs(skip_markers) do ---@type integer, DebugSkipMarker
duffle.run_check_rules(CHECK_RULES, "per_skip_marker", marker, pipe_ctx, findings)
end
-- Per-macro rules (TAPE_WORDS vs WORD_COUNT drift).
pipe_ctx.word_counts = corpus_pipe_ctx.word_counts
--- @type integer, MacroEntry
for _, m in ipairs(scan.macros) do
for _, m in ipairs(scan.macros) do ---@type integer, MacroEntry
duffle.run_check_rules(CHECK_RULES, "per_macro", m, pipe_ctx, findings)
end
@@ -582,8 +539,7 @@ end
-- M.run — orchestrator entry
-- ════════════════════════════════════════════════════════════════════════════
--- @type AnnotationPass
local M = {}
local M = {} ---@type AnnotationPass
-- Expose `validate` for downstream passes (e.g. report.lua) that need to re-render the per-source results into a per-MODULE report.
M.validate = validate
@@ -591,47 +547,32 @@ M.validate = validate
--- @param ctx PassCtx
--- @return PassResult
function M.run(ctx)
--- @type PassOutputEntry[]
local outputs = {}
--- @type PassFinding[]
local errors = {}
--- @type PassFinding[]
local warnings = {}
local outputs = {} ---@type PassOutputEntry[]
local errors = {} ---@type PassFinding[]
local warnings = {} ---@type PassFinding[]
-- Build the shared pipe_ctx once for this run; every validate() call sees the same cross-source registries.
-- The corpus owns the canonical cross-source registries; per-source scans retain body / declaration ownership.
--- @type PipeCtx
local corpus_pipe_ctx = build_corpus_pipe_ctx(ctx)
--- @type Corpus
local corpus = ctx.shared.corpus
local corpus_pipe_ctx = build_corpus_pipe_ctx(ctx) ---@type PipeCtx
local corpus = ctx.shared.corpus ---@type Corpus
-- Group `corpus.sources_by_dir` by module, validate every source in each bucket, and emit one errors.h per directory.
--- @type table<string, SourceFile[]>
local by_dir = (corpus and corpus.sources_by_dir) or {}
local by_dir = (corpus and corpus.sources_by_dir) or {} ---@type table<string, SourceFile[]>
--- @type string, SourceFile[]
for dir, dir_sources in pairs(by_dir) do
--- @type string
local dir_basename = dir:match("([^/\\]+)$") or dir
--- @type integer
local dir_atoms = 0
--- @type PassFinding[]
local dir_errors = {}
--- @type PassFinding[]
local dir_warnings = {}
--- @type integer, SourceFile
for _, src in ipairs(dir_sources) do
--- @type AnnotatedResult
local result = validate(ctx, src, corpus_pipe_ctx)
for dir, dir_sources in pairs(by_dir) do ---@type string, SourceFile[]
local dir_basename = dir:match("([^/\\]+)$") or dir ---@type string
local dir_atoms = 0 ---@type integer
local dir_errors = {} ---@type PassFinding[]
local dir_warnings = {} ---@type PassFinding[]
for _, src in ipairs(dir_sources) do ---@type integer, SourceFile
local result = validate(ctx, src, corpus_pipe_ctx) ---@type AnnotatedResult
result.source = src.path -- tag for downstream rendering
dir_atoms = dir_atoms + #result.atoms
--- @type integer, PassFinding
for _, e in ipairs(result.errors) do
for _, e in ipairs(result.errors) do ---@type integer, PassFinding
dir_errors[#dir_errors + 1] = { line = e.line, msg = e.msg, source = src.path }
errors [#errors + 1] = { line = e.line, msg = e.msg }
end
--- @type integer, PassFinding
for _, w in ipairs(result.warnings) do
for _, w in ipairs(result.warnings) do ---@type integer, PassFinding
dir_warnings[#dir_warnings + 1] = { line = w.line, msg = w.msg }
warnings [#warnings + 1] = { line = w.line, msg = w.msg }
end
+75 -150
View File
@@ -37,12 +37,9 @@
-- Bootstrap: load `duffle_paths.lua` via `debug.getinfo(1, "S").source`
-- (works both standalone + when require'd). `duffle_paths.lua` sets package.path then returns `require("duffle")`
-- at the bottom, so the dofile value IS the duffle module.
--- @type string
local _bootstrap_dir = debug.getinfo(1, "S").source:match("^@?(.*[/\\])") or "./"
--- @type DuffleExport
local duffle = dofile(_bootstrap_dir .. "../duffle_paths.lua")
--- @type ElfDwarfMod
local elf_dwarf = require("elf_dwarf")
local _bootstrap_dir = debug.getinfo(1, "S").source:match("^@?(.*[/\\])") or "./" ---@type string
local duffle = dofile(_bootstrap_dir .. "../duffle_paths.lua") ---@type DuffleExport
local elf_dwarf = require("elf_dwarf") ---@type ElfDwarfMod
-- ════════════════════════════════════════════════════════════════════════════
-- Constants
@@ -50,8 +47,7 @@ local elf_dwarf = require("elf_dwarf")
-- Format version emitted as the first line. Bump + add a migration test if the format changes;
-- the gdb runtime loader rejects mismatches (E2).
--- @type integer
local FORMAT_VERSION = 1
local FORMAT_VERSION = 1 ---@type integer
-- ════════════════════════════════════════════════════════════════════════════
-- Type declarations
@@ -107,23 +103,16 @@ local FORMAT_VERSION = 1
--- @return WordMapEntry[]
--- @return integer
local function canonical_word_entries(atom)
--- @type AtomPaths
local paths = atom.paths or {}
--- @type WordEvent[]
local events = paths.word_events or {}
--- @type EmissionItem[]
local word_items = {}
--- @type integer, EmissionItem
for _, item in ipairs(paths.items or {}) do
local paths = atom.paths or {} ---@type AtomPaths
local events = paths.word_events or {} ---@type WordEvent[]
local word_items = {} ---@type EmissionItem[]
for _, item in ipairs(paths.items or {}) do ---@type integer, EmissionItem
if item.kind == "word" then word_items[#word_items + 1] = item end
end
--- @type WordMapEntry[]
local entries = {}
--- @type integer, WordEvent
for index, event in ipairs(events) do
--- @type EmissionItem
local item = word_items[index] or {}
local entries = {} ---@type WordMapEntry[]
for index, event in ipairs(events) do ---@type integer, WordEvent
local item = word_items[index] or {} ---@type EmissionItem
entries[#entries + 1] = {
pos = event.i or (index - 1),
line = event.call_line or item.line or 0,
@@ -149,20 +138,14 @@ end
--- @return string[]
--- @return integer
local function emit_provenance_stanza(src, atom, wc)
--- @type string[]
local lines = {}
--- @type string
local rel_path = src.path:gsub("\\\\", "/")
--- @type WordMapEntry[], integer
local entries, total = canonical_word_entries(atom)
local lines = {} ---@type string[]
local rel_path = src.path:gsub("\\\\", "/") ---@type string
local entries, total = canonical_word_entries(atom) ---@type WordMapEntry[], integer
lines[#lines + 1] = string.format('ATOM %s "%s" 0', atom.raw_name or atom.name, rel_path)
--- @type integer, WordMapEntry
for _, entry in ipairs(entries) do
--- @type InvocationRecord|nil
local inv = entry.invocation
--- @type integer|nil
local macro_count = inv and wc["mac_" .. inv.component_name]
for _, entry in ipairs(entries) do ---@type integer, WordMapEntry
local inv = entry.invocation ---@type InvocationRecord|nil
local macro_count = inv and wc["mac_" .. inv.component_name] ---@type integer|nil
if inv and macro_count ~= nil then
lines[#lines + 1] = string.format('WORD %d CALL %s:%d MACRO %s "%s:%d" BODY %d'
, entry.pos, rel_path, entry.line, inv.component_name
@@ -182,8 +165,7 @@ end
--- @param wc WordCounts
--- @return string
local function render_provenance(src, wc)
--- @type string[]
local lines = {}
local lines = {} ---@type string[]
lines[#lines + 1] = "# FORMAT_VERSION 1"
lines[#lines + 1] = "# auto-generated by ps1_meta.lua (passes/atoms_source_map.lua) — DO NOT EDIT"
lines[#lines + 1] = "# Per-.word provenance: maps each emitted .word to its call site (atom body"
@@ -195,17 +177,13 @@ local function render_provenance(src, wc)
--- @param atom AtomEntry
--- @return nil
local function append(atom)
--- @type string[]
local stanza = emit_provenance_stanza(src, atom, wc)
--- @type integer, string
for _, line in ipairs(stanza) do lines[#lines + 1] = line end
local stanza = emit_provenance_stanza(src, atom, wc) ---@type string[]
for _, line in ipairs(stanza) do lines[#lines + 1] = line end ---@type integer, string
end
--- @type integer, AtomEntry
for _, atom in ipairs(src.scan.atoms or {}) do
for _, atom in ipairs(src.scan.atoms or {}) do ---@type integer, AtomEntry
if atom.paths then append(atom) end
end
--- @type integer, AtomEntry
for _, atom in ipairs(src.scan.raw_atoms or {}) do
for _, atom in ipairs(src.scan.raw_atoms or {}) do ---@type integer, AtomEntry
if atom.paths then append(atom) end
end
@@ -219,16 +197,12 @@ end
--- @return string[]
--- @return integer
local function emit_atom_stanza(src, atom)
--- @type string[]
local lines = {}
--- @type string
local rel_path = src.path:gsub("\\\\", "/")
--- @type WordMapEntry[], integer
local entries, total = canonical_word_entries(atom)
local lines = {} ---@type string[]
local rel_path = src.path:gsub("\\\\", "/") ---@type string
local entries, total = canonical_word_entries(atom) ---@type WordMapEntry[], integer
lines[#lines + 1] = string.format('ATOM %s "%s" 0', atom.raw_name or atom.name, rel_path)
--- @type integer, WordMapEntry
for _, entry in ipairs(entries) do
for _, entry in ipairs(entries) do ---@type integer, WordMapEntry
lines[#lines + 1] = string.format("WORD %d LINE %d TEXT %s",
entry.pos, entry.line, entry.text)
end
@@ -242,25 +216,20 @@ end
--- @param src SourceFile
--- @return string
local function render_source_map(src)
--- @type string[]
local lines = {}
local lines = {} ---@type string[]
lines[#lines + 1] = "# FORMAT_VERSION " .. FORMAT_VERSION
lines[#lines + 1] = "# auto-generated by ps1_meta.lua (passes/atoms_source_map.lua) — DO NOT EDIT"
--- @param atom AtomEntry
--- @return nil
local function append(atom)
--- @type string[]
local stanza = emit_atom_stanza(src, atom)
--- @type integer, string
for _, line in ipairs(stanza) do lines[#lines + 1] = line end
local stanza = emit_atom_stanza(src, atom) ---@type string[]
for _, line in ipairs(stanza) do lines[#lines + 1] = line end ---@type integer, string
end
--- @type integer, AtomEntry
for _, atom in ipairs(src.scan.atoms or {}) do
for _, atom in ipairs(src.scan.atoms or {}) do ---@type integer, AtomEntry
if atom.paths then append(atom) end
end
--- @type integer, AtomEntry
for _, atom in ipairs(src.scan.raw_atoms or {}) do
for _, atom in ipairs(src.scan.raw_atoms or {}) do ---@type integer, AtomEntry
if atom.paths then append(atom) end
end
@@ -283,28 +252,20 @@ end
--- @param ctx PassCtx
--- @return GdbAtomRecord[]
local function build_atom_table(ctx)
--- @type table<string, NmAddr>
local addrs = elf_dwarf.read_nm(ctx.flags.elf_path)
--- @type Corpus|nil
local corpus = ctx.shared and ctx.shared.corpus
--- @type GdbAtomRecord[]
local matched = {}
local addrs = elf_dwarf.read_nm(ctx.flags.elf_path) ---@type table<string, NmAddr>
local corpus = ctx.shared and ctx.shared.corpus ---@type Corpus|nil
local matched = {} ---@type GdbAtomRecord[]
--- @type integer, SourceFile
for _, src in ipairs(corpus.source_order or {}) do
--- @type string
local file_base = src.path:match("([^/\\\\]+)$") or src.path
for _, src in ipairs(corpus.source_order or {}) do ---@type integer, SourceFile
local file_base = src.path:match("([^/\\\\]+)$") or src.path ---@type string
--- @param atom AtomEntry
--- @return nil
local function append(atom)
if not atom.paths then return end
--- @type string
local name = atom.raw_name or atom.name
--- @type NmAddr|nil
local info = addrs[name]
local name = atom.raw_name or atom.name ---@type string
local info = addrs[name] ---@type NmAddr|nil
if not info then return end
--- @type WordMapEntry[], integer
local entries, total = canonical_word_entries(atom)
local entries, total = canonical_word_entries(atom) ---@type WordMapEntry[], integer
matched[#matched + 1] = {
name = name,
src_path = src.path,
@@ -315,10 +276,8 @@ local function build_atom_table(ctx)
entries = entries,
}
end
--- @type integer, AtomEntry
for _, atom in ipairs((src.scan or {}).atoms or {}) do append(atom) end
--- @type integer, AtomEntry
for _, atom in ipairs((src.scan or {}).raw_atoms or {}) do append(atom) end
for _, atom in ipairs((src.scan or {}).atoms or {}) do append(atom) end ---@type integer, AtomEntry
for _, atom in ipairs((src.scan or {}).raw_atoms or {}) do append(atom) end ---@type integer, AtomEntry
end
-- Deterministic order: sort by address (matches `nm` output ordering).
@@ -326,8 +285,7 @@ local function build_atom_table(ctx)
--- @param b GdbAtomRecord
--- @return boolean
table.sort(matched, function(a, b) return a.addr < b.addr end)
--- @type integer, GdbAtomRecord
for i, a in ipairs(matched) do a.idx = i - 1 end
for i, a in ipairs(matched) do a.idx = i - 1 end ---@type integer, GdbAtomRecord
return matched
end
@@ -345,8 +303,7 @@ local function append_gdb_commands(lines, matched)
-- ── tape_atoms ──
-- Hardcoded one printf per atom. No loop.
lines[#lines + 1] = "define tape_atoms"
--- @type integer, GdbAtomRecord
for _, a in ipairs(matched) do
for _, a in ipairs(matched) do ---@type integer, GdbAtomRecord
-- gdb 12.1 quirk: literals in printf args require an attached target.
-- Use the per-atom convenience vars set above as printf args.
lines[#lines + 1] = string.format(' printf " %%-32s @ 0x%%08x %%4d words\\n", $__atom_name_%d, $__atom_addr_%d, $__atom_words_%d',
@@ -361,8 +318,7 @@ local function append_gdb_commands(lines, matched)
-- ── break_atom (generic) + per-atom break_atom_X ──
lines[#lines + 1] = "define break_atom"
lines[#lines + 1] = ' echo "Usage: break_atom_<exact_name> (pick from the list below)"'
--- @type integer, GdbAtomRecord
for _, a in ipairs(matched) do
for _, a in ipairs(matched) do ---@type integer, GdbAtomRecord
lines[#lines + 1] = string.format(' printf " break_atom_%%-32s\\n", $__atom_name_%d', a.idx)
end
lines[#lines + 1] = "end"
@@ -371,8 +327,7 @@ local function append_gdb_commands(lines, matched)
lines[#lines + 1] = "end"
lines[#lines + 1] = ""
--- @type integer, GdbAtomRecord
for _, a in ipairs(matched) do
for _, a in ipairs(matched) do ---@type integer, GdbAtomRecord
lines[#lines + 1] = string.format("define break_atom_%s", a.name)
lines[#lines + 1] = string.format(" break *$__atom_addr_%d", a.idx)
lines[#lines + 1] = string.format(' printf " Breakpoint set at %s (0x%%08x)\\n", $__atom_addr_%d', a.name, a.idx)
@@ -386,8 +341,7 @@ local function append_gdb_commands(lines, matched)
-- ── step_atom / next_atom ──
-- Hardcoded one tbreak per atom. No loop.
lines[#lines + 1] = "define step_atom"
--- @type integer, GdbAtomRecord
for _, a in ipairs(matched) do
for _, a in ipairs(matched) do ---@type integer, GdbAtomRecord
lines[#lines + 1] = string.format(" tbreak *$__atom_addr_%d", a.idx)
end
lines[#lines + 1] = " continue"
@@ -410,8 +364,7 @@ local function append_gdb_commands(lines, matched)
lines[#lines + 1] = "define where_in_atom"
lines[#lines + 1] = " set $__pc = (unsigned int)$pc"
lines[#lines + 1] = " set $__matched = 0"
--- @type integer, GdbAtomRecord
for _, a in ipairs(matched) do
for _, a in ipairs(matched) do ---@type integer, GdbAtomRecord
-- Precompute end_addr (gdb 12.1's expression evaluator chokes on `addr + words*4`).
lines[#lines + 1] = string.format(" set $__end_%d = $__atom_addr_%d + $__atom_words_%d * 4", a.idx, a.idx, a.idx)
lines[#lines + 1] = string.format(" if $__pc >= $__atom_addr_%d && $__pc < $__end_%d", a.idx, a.idx)
@@ -420,18 +373,15 @@ local function append_gdb_commands(lines, matched)
lines[#lines + 1] = string.format(" set $__word = ($__pc - $__atom_addr_%d) / 4", a.idx)
lines[#lines + 1] = string.format(' printf "word: %%d/%%d\\n", $__word, $__atom_words_%d', a.idx)
-- One inner-if per WORD entry. Each word's line + text hardcoded.
--- @type integer, WordMapEntry
for _, we in ipairs(a.entries) do
for _, we in ipairs(a.entries) do ---@type integer, WordMapEntry
lines[#lines + 1] = string.format(" if $__word == %d", we.pos)
-- Escape TEXT for printf format string.
--- @type string
local escaped_text = we.text:gsub("%%", "%%%%"):gsub('"', '\\"')
local escaped_text = we.text:gsub("%%", "%%%%"):gsub('"', '\\"') ---@type string
lines[#lines + 1] = string.format(' printf "source: %%s:%%d %%s\\n", $__atom_file_%d, %d, "%s"', a.idx, we.line, escaped_text)
lines[#lines + 1] = " end"
end
-- Fallback for words beyond the source map (shouldn't happen if nm matches).
--- @type integer
local max_word = 0
local max_word = 0 ---@type integer
if #a.entries > 0 then max_word = a.entries[#a.entries].pos end
lines[#lines + 1] = string.format(' if $__word > %d', max_word)
lines[#lines + 1] = ' printf "source: (no source-map entry for word %%d; map may be stale)\\n", $__word'
@@ -456,8 +406,7 @@ local function append_gdb_commands(lines, matched)
lines[#lines + 1] = " set $__in_atom = 0"
lines[#lines + 1] = " set $__did_step = 0"
lines[#lines + 1] = " set $__pc = (unsigned int)$pc"
--- @type integer, GdbAtomRecord
for _, a in ipairs(matched) do
for _, a in ipairs(matched) do ---@type integer, GdbAtomRecord
-- Precompute end_addr in the convenience var (single expression gdb handles).
lines[#lines + 1] = string.format(" set $__end_%d = $__atom_addr_%d + $__atom_words_%d * 4", a.idx, a.idx, a.idx)
lines[#lines + 1] = string.format(" if $__pc >= $__atom_addr_%d && $__pc < $__end_%d", a.idx, a.idx)
@@ -494,8 +443,7 @@ end
--- @return nil
local function emit_gdb_runtime(ctx)
if not (ctx.flags and ctx.flags.gdb_runtime) then return end
--- @type string|nil
local elf_path = ctx.flags.elf_path
local elf_path = ctx.flags.elf_path ---@type string|nil
if not elf_path or elf_path == "" then
io.stderr:write("[atoms_source_map] --gdb-runtime requires --elf <elf>\n")
return
@@ -506,15 +454,13 @@ local function emit_gdb_runtime(ctx)
return
end
--- @type GdbAtomRecord[]
local matched = build_atom_table(ctx)
local matched = build_atom_table(ctx) ---@type GdbAtomRecord[]
if #matched == 0 then
io.stderr:write("[atoms_source_map] --gdb-runtime: no atoms matched against nm symbols (stale scan?).\n")
return
end
--- @type string[]
local lines = {}
local lines = {} ---@type string[]
lines[#lines + 1] = "# Auto-generated by ps1_meta.lua (passes/atoms_source_map.lua)"
lines[#lines + 1] = "# DO NOT EDIT — re-run ps1_meta.lua --atoms-source-map --gdb-runtime to regenerate"
lines[#lines + 1] = "# Sourced by scripts/gdb/gdb_tape_atoms.gdb (the wrapper)."
@@ -535,8 +481,7 @@ local function emit_gdb_runtime(ctx)
-- Per-atom convenience vars (used as printf args; literals aren't accepted
-- without an attached target on gdb 12.1).
--- @type integer, GdbAtomRecord
for _, a in ipairs(matched) do
for _, a in ipairs(matched) do ---@type integer, GdbAtomRecord
lines[#lines + 1] = string.format('set $__atom_name_%d = "%s"', a.idx, gdb_escape(a.name))
lines[#lines + 1] = string.format("set $__atom_addr_%d = 0x%x", a.idx, a.addr)
lines[#lines + 1] = string.format("set $__atom_words_%d = %d", a.idx, a.words)
@@ -552,8 +497,7 @@ local function emit_gdb_runtime(ctx)
-- Confirmation line for the source operator.
lines[#lines + 1] = 'printf "[gdb_tape_atoms] runtime loaded %d atoms from %s\\n", $__atom_count, $__elf_path'
--- @type string
local out_path
local out_path ---@type string
-- Move out of `<out_root>/gdb_tape_atoms_runtime.gdb` to `<out_root>/../gdb_tape_atoms_runtime.gdb` when the conventional `<out_root>` is `<build>/gen`
-- (any equivalent spelling — relative, absolute backslash, absolute forward-slash, trailing-separator variants).
-- This puts the gdb runtime alongside the ELF at `build/` rather than under the report subdir.
@@ -566,8 +510,7 @@ local function emit_gdb_runtime(ctx)
if ends_with_gen_dir(ctx.out_root) then
-- Strip the trailing `/gen` segment, then write the runtime script under `build/`.
-- e.g. "C:/projects/Pikuma/ps1/build/gen" -> "C:/projects/Pikuma/ps1/build".
--- @type string
local parent = ctx.out_root:gsub("[/\\]gen[/\\]?$", "")
local parent = ctx.out_root:gsub("[/\\]gen[/\\]?$", "") ---@type string
out_path = parent .. "/gdb_tape_atoms_runtime.gdb"
else
out_path = ctx.out_root .. "/gdb_tape_atoms_runtime.gdb"
@@ -581,8 +524,7 @@ end
-- M — module exports
-- ════════════════════════════════════════════════════════════════════════════
--- @type AtomSourceMapPass
local M = {}
local M = {} ---@type AtomSourceMapPass
-- Expose the pure render functions so `report.lua` and the focused tests can call them directly without triggering the file-emit path.
M.render_source_map = render_source_map
@@ -594,22 +536,15 @@ M.render_provenance = render_provenance
function M.render_atom_source_map(atom)
assert(type(atom) == "table", "render_atom_source_map: atom must be a table")
assert(type(atom.paths) == "table", "render_atom_source_map: atom.paths must be a table")
--- @type WordMapEntry[], integer
local entries, total = canonical_word_entries(atom)
--- @type string[]
local lines = {}
local entries, total = canonical_word_entries(atom) ---@type WordMapEntry[], integer
local lines = {} ---@type string[]
lines[#lines + 1] = string.format("ATOM %s %d", (atom.raw_name or atom.name), total)
--- @type integer, WordMapEntry
for _, entry in ipairs(entries) do
--- @type string
local word_line = string.format("WORD %d LINE %d TEXT %s",
for _, entry in ipairs(entries) do ---@type integer, WordMapEntry
local word_line = string.format("WORD %d LINE %d TEXT %s", ---@type string
entry.pos, entry.line, entry.text)
--- @type string[]
local keys = {}
--- @type integer
for pos = 1, 16 do
--- @type string|nil
local k = entry.gpr_keys and entry.gpr_keys[pos]
local keys = {} ---@type string[]
for pos = 1, 16 do ---@type integer
local k = entry.gpr_keys and entry.gpr_keys[pos] ---@type string|nil
if type(k) == "string" and k:sub(1, 7) == "reguse:" then
keys[#keys + 1] = k
end
@@ -635,17 +570,12 @@ function M.render_atom_provenance(atom, wc, rel_path)
assert(type(atom) == "table", "render_atom_provenance: atom must be a table")
assert(type(atom.paths) == "table", "render_atom_provenance: atom.paths must be a table")
assert(type(rel_path) == "string", "render_atom_provenance: rel_path must be a string")
--- @type WordMapEntry[], integer
local entries, total = canonical_word_entries(atom)
--- @type string[]
local lines = {}
local entries, total = canonical_word_entries(atom) ---@type WordMapEntry[], integer
local lines = {} ---@type string[]
lines[#lines + 1] = string.format("ATOM %s %d", (atom.raw_name or atom.name), total)
--- @type integer, WordMapEntry
for _, entry in ipairs(entries) do
--- @type InvocationRecord|nil
local inv = entry.invocation
--- @type integer|nil
local macro_count = inv and wc and wc["mac_" .. inv.component_name]
for _, entry in ipairs(entries) do ---@type integer, WordMapEntry
local inv = entry.invocation ---@type InvocationRecord|nil
local macro_count = inv and wc and wc["mac_" .. inv.component_name] ---@type integer|nil
if inv and macro_count ~= nil then
lines[#lines + 1] = string.format('WORD %d CALL %s:%d MACRO %s "%s:%d" BODY %d'
, entry.pos, rel_path, entry.line, inv.component_name, inv.def_path or "", inv.def_line or 0, entry.body_line)
@@ -664,22 +594,17 @@ end
--- @param ctx PassCtx
--- @return PassResult
function M.run(ctx)
--- @type PassOutputEntry[]
local outputs = {}
--- @type PassFinding[]
local errors = {}
--- @type PassFinding[]
local warnings = {}
local outputs = {} ---@type PassOutputEntry[]
local errors = {} ---@type PassFinding[]
local warnings = {} ---@type PassFinding[]
--- @type Corpus|nil
local corpus = ctx.shared and ctx.shared.corpus
local corpus = ctx.shared and ctx.shared.corpus ---@type Corpus|nil
if type(corpus) ~= "table" or type(corpus.source_order) ~= "table" then
error("atoms_source_map.run requires ctx.shared.corpus.source_order (canonical corpus).", 0)
end
-- Word counts come from `corpus.word_counts` (populated by word_count_eval + components passes).
--- @type WordCounts
local wc = corpus.word_counts or {}
local wc = corpus.word_counts or {} ---@type WordCounts
if not next(wc) then
warnings[#warnings + 1] = {
line = 0,
+71 -142
View File
@@ -35,10 +35,8 @@
--- @field run fun(ctx: PassCtx): AutoRegResult
--- @field POOL GprIdent[]
--- @type string
local _bootstrap_dir = debug.getinfo(1, "S").source:match("^@?(.*[/\\])") or "./"
--- @type DuffleExport
local duffle = dofile(_bootstrap_dir .. "../duffle_paths.lua")
local _bootstrap_dir = debug.getinfo(1, "S").source:match("^@?(.*[/\\])") or "./" ---@type string
local duffle = dofile(_bootstrap_dir .. "../duffle_paths.lua") ---@type DuffleExport
--- ════════════════════════════════════════════════════════════════════════════
--- THE GPR ALLOCATION POOL — what's allocatable, and (more importantly) WHY
@@ -56,8 +54,7 @@ local duffle = dofile(_bootstrap_dir .. "../duffle_paths.lua")
--- R_K0/K1 (codes 26-27) — Kernel / interrupt handler reserves. Never touched by user code.
--- R_GP/SP/FP/RA (codes 28-31) — R_SP/R_FP/R_RA are tape-runtime carriers between tape_enter and tape_exit; R_GP stays the host global pointer.
---
--- @type GprIdent[]
local POOL = {
local POOL = { ---@type GprIdent[]
"R_V0", "R_V1",
"R_T0", "R_T1", "R_T2", "R_T3",
"R_T4", "R_T5", "R_T6", "R_T7",
@@ -72,8 +69,7 @@ local POOL = {
-- Only the POOL entries matter for auto_reg — non-pool aliases
-- (R_AT=1, R_A0..A3=4..7, R_T8=24, R_T9=25, R_K0/K1=26..27, R_GP/SP/FP/RA=28..31)
-- are deliberately omitted — see the comment block above for the WHY of each exclusion.
--- @type table<integer, GprIdent> -- bag: MIPS GPR code -> POOL ident
local INT_CODE_TO_POOL_GPR = {
local INT_CODE_TO_POOL_GPR = { ---@type table<integer, GprIdent> -- bag: MIPS GPR code -> POOL ident
[2] = "R_V0", [3] = "R_V1",
[4] = "R_A0", [5] = "R_A1", [6] = "R_A2", [7] = "R_A3",
[8] = "R_T0", [9] = "R_T1", [10] = "R_T2", [11] = "R_T3",
@@ -87,10 +83,8 @@ local INT_CODE_TO_POOL_GPR = {
--- @param tbl table<string, string> -- bag: key set only; values unused
--- @return string[]
local function stable_sort_keys(tbl)
--- @type string[]
local keys = {}
--- @type string
for k in pairs(tbl) do keys[#keys + 1] = k end
local keys = {} ---@type string[]
for k in pairs(tbl) do keys[#keys + 1] = k end ---@type string
table.sort(keys)
return keys
end
@@ -105,18 +99,12 @@ local function allocate_phase(phase_label, decls)
-- Deep-copy POOL into a fresh sequence table. The original `table.unpack and table.unpack(POOL) or { unpack(POOL) }`
-- idiom wraps the unpacked values in a single inner table under LuaJIT 5.1 (`table.unpack` is nil; the `or` returns one value),
-- which corrupts the pool into `{ {R_T0, R_T1, ...} }` — making `table.remove(pool, 1)` return the inner table on iteration.
--- @type GprIdent[]
local pool = {}
--- @type integer
for i = 1, #POOL do pool[i] = POOL[i] end
--- @type GprAllocMap
local result = {}
--- @type PassFinding[]
local errors = {}
--- @type integer, string
for _, sym in ipairs(stable_sort_keys(decls)) do
--- @type GprIdent|nil
local next_gpr = table.remove(pool, 1)
local pool = {} ---@type GprIdent[]
for i = 1, #POOL do pool[i] = POOL[i] end ---@type integer
local result = {} ---@type GprAllocMap
local errors = {} ---@type PassFinding[]
for _, sym in ipairs(stable_sort_keys(decls)) do ---@type integer, string
local next_gpr = table.remove(pool, 1) ---@type GprIdent|nil
if not next_gpr then
errors[#errors + 1] = {
line = 0,
@@ -144,16 +132,12 @@ end
--- @return table<GprIdent, boolean>
--- @return table<string, GprIdent>
local function build_user_pins(corpus)
--- @type table<GprIdent, boolean> -- bag: pinned physical GPR -> true
local user_pinned = {}
--- @type table<string, GprIdent> -- bag: alias ident -> physical GPR
local alias_to_gpr = {}
local user_pinned = {} ---@type table<GprIdent, boolean> -- bag: pinned physical GPR -> true
local alias_to_gpr = {} ---@type table<string, GprIdent> -- bag: alias ident -> physical GPR
if not corpus.register_alias_registry then return user_pinned, alias_to_gpr end
--- @type string, AliasEntry
for alias_name, alias_entry in pairs(corpus.register_alias_registry) do
for alias_name, alias_entry in pairs(corpus.register_alias_registry) do ---@type string, AliasEntry
if alias_entry.has_atom_reg and alias_entry.code then
--- @type GprIdent|nil
local gpr = INT_CODE_TO_POOL_GPR[alias_entry.code]
local gpr = INT_CODE_TO_POOL_GPR[alias_entry.code] ---@type GprIdent|nil
if gpr then
user_pinned[gpr] = true
alias_to_gpr[alias_name] = gpr
@@ -174,29 +158,22 @@ end
--- @param alias_to_gpr table<string, GprIdent> -- bag: alias ident -> physical GPR
--- @return table<GprIdent, integer>
local function find_used_gprs(body_text, alias_to_gpr)
--- @type table<GprIdent, integer> -- bag: physical GPR -> hit count
local found = {}
local found = {} ---@type table<GprIdent, integer> -- bag: physical GPR -> hit count
-- (a) Hardcoded physical GPRs (R_T0..R_T7, R_V0..R_V1, R_A0..R_A3, R_S0..R_S7).
--- @type GprIdent
for gpr in body_text:gmatch("(R_T%d+|R_V%d+|R_A%d+|R_S%d+)") do
for gpr in body_text:gmatch("(R_T%d+|R_V%d+|R_A%d+|R_S%d+)") do ---@type GprIdent
found[gpr] = (found[gpr] or 0) + 1
end
-- (b) Alias references (R_<Alias>) resolved to physical GPRs via the registry.
-- Sorted by name so the regex is byte-stable across runs.
if alias_to_gpr and next(alias_to_gpr) then
--- @type string[]
local aliases = {}
--- @type string
for alias_name in pairs(alias_to_gpr) do
local aliases = {} ---@type string[]
for alias_name in pairs(alias_to_gpr) do ---@type string
aliases[#aliases + 1] = alias_name
end
table.sort(aliases)
--- @type string
local pattern = "(" .. table.concat(aliases, "|") .. ")"
--- @type string
for alias_name in body_text:gmatch(pattern) do
--- @type GprIdent|nil
local gpr = alias_to_gpr[alias_name]
local pattern = "(" .. table.concat(aliases, "|") .. ")" ---@type string
for alias_name in body_text:gmatch(pattern) do ---@type string
local gpr = alias_to_gpr[alias_name] ---@type GprIdent|nil
if gpr and not found[gpr] then
found[gpr] = 1
end
@@ -213,30 +190,24 @@ end
--- @return string|nil
local function emit_auto_reg_h(out_dir, dir, sources, mappings)
if not mappings or next(mappings) == nil then return end
--- @type string
local out_path = out_dir .. "/" .. "auto_reg.h"
local out_path = out_dir .. "/" .. "auto_reg.h" ---@type string
duffle.ensure_dir(out_dir)
--- @type string[]
local lines = {
local lines = { ---@type string[]
"#ifdef INTELLISENSE_DIRECTIVES",
"#pragma once",
"#endif",
"// Auto-generated by ps1_meta.lua (passes/auto_reg.lua) — DO NOT EDIT",
"// Directory: " .. dir:gsub("/", "\\"),
}
--- @type integer, SourceFile
for _, src in ipairs(sources) do
for _, src in ipairs(sources) do ---@type integer, SourceFile
lines[#lines + 1] = "// source: " .. src.path
end
lines[#lines + 1] = "// Per-phase register allocations resolved by the lua pass."
lines[#lines + 1] = "// R_<Sym>_Code = <chosen GPR's _Code constant> for every marker in this directory."
lines[#lines + 1] = ""
--- @type integer, string
for _, sym in ipairs(stable_sort_keys(mappings)) do
--- @type GprIdent
local gpr = mappings[sym]
--- @type string
local gpr_code = gpr .. "_Code"
for _, sym in ipairs(stable_sort_keys(mappings)) do ---@type integer, string
local gpr = mappings[sym] ---@type GprIdent
local gpr_code = gpr .. "_Code" ---@type string
lines[#lines + 1] = "#define " .. sym .. "_Code " .. gpr_code
end
lines[#lines + 1] = ""
@@ -249,21 +220,16 @@ end
-- Pass entry
-- ════════════════════════════════════════════════════════════════════════════
--- @type AutoRegPass
local M = {}
local M = {} ---@type AutoRegPass
--- @param ctx PassCtx
--- @return AutoRegResult
function M.run(ctx)
--- @type AutoRegOutput[]
local outputs = {}
--- @type PassFinding[]
local errors = {}
--- @type PassFinding[]
local warnings = {}
local outputs = {} ---@type AutoRegOutput[]
local errors = {} ---@type PassFinding[]
local warnings = {} ---@type PassFinding[]
--- @type Corpus|nil
local corpus = ctx.shared and ctx.shared.corpus
local corpus = ctx.shared and ctx.shared.corpus ---@type Corpus|nil
if type(corpus) ~= "table" then
error("auto_reg.run requires ctx.shared.corpus", 0)
end
@@ -273,23 +239,17 @@ function M.run(ctx)
-- MUST NOT be allocated to any auto-reg marker — they're preserved across atoms by the wave-context discipline.
-- The corpus's register_alias_registry is the source of truth for these opt-in pins.
-- Body references to those aliases (via alias_to_gpr) are also excluded on a per-atom basis in step 2 below.
--- @type table<GprIdent, boolean>, table<string, GprIdent>
local user_pinned, alias_to_gpr = build_user_pins(corpus)
local user_pinned, alias_to_gpr = build_user_pins(corpus) ---@type table<GprIdent, boolean>, table<string, GprIdent>
-- 1. Allocate phase pools first (phase declarations take precedence over per-atom declarations).
--- @type table<string, GprAllocMap> -- bag: phase_label -> alloc map
local phase_allocations = {}
--- @type string, table<string, string>
for phase_label, decls in pairs(corpus.phase_auto_regs or {}) do
--- @type GprAllocMap, PassFinding[]
local mapping, errs = allocate_phase(phase_label, decls)
--- @type string, GprIdent
for sym, gpr in pairs(mapping) do
local phase_allocations = {} ---@type table<string, GprAllocMap> -- bag: phase_label -> alloc map
for phase_label, decls in pairs(corpus.phase_auto_regs or {}) do ---@type string, table<string, string>
local mapping, errs = allocate_phase(phase_label, decls) ---@type GprAllocMap, PassFinding[]
for sym, gpr in pairs(mapping) do ---@type string, GprIdent
phase_allocations[phase_label] = phase_allocations[phase_label] or {}
phase_allocations[phase_label][sym] = gpr
end
--- @type integer, PassFinding
for _, e in ipairs(errs) do
for _, e in ipairs(errs) do ---@type integer, PassFinding
errors[#errors + 1] = e
end
end
@@ -298,22 +258,16 @@ function M.run(ctx)
-- Otherwise, allocate a private pool for the atom.
-- The phase membership is in `corpus.atom_phases[phase_label].atoms` (an array of atom names declared via `atom_phase(<phase>)`
-- in the atom's `atom_info` line). Build a reverse map `atom_name -> phase_label` so the lookup is O(1) per atom scope.
--- @type table<AtomName, string> -- bag: atom name -> phase label
local atom_name_to_phase = {}
--- @type string, AtomPhaseGroup
for phase_label, entry in pairs(corpus.atom_phases or {}) do
--- @type integer, AtomName
for _, atom_name in ipairs(entry.atoms or {}) do
local atom_name_to_phase = {} ---@type table<AtomName, string> -- bag: atom name -> phase label
for phase_label, entry in pairs(corpus.atom_phases or {}) do ---@type string, AtomPhaseGroup
for _, atom_name in ipairs(entry.atoms or {}) do ---@type integer, AtomName
atom_name_to_phase[atom_name] = phase_label
end
end
--- @type table<AtomName, GprAllocMap> -- bag: atom scope -> alloc map
local atom_allocations = {}
--- @type AtomName, table<string, string>
for atom_scope, decls in pairs(corpus.atom_auto_regs or {}) do
--- @type string|nil
local phase_label = atom_name_to_phase[atom_scope]
local atom_allocations = {} ---@type table<AtomName, GprAllocMap> -- bag: atom scope -> alloc map
for atom_scope, decls in pairs(corpus.atom_auto_regs or {}) do ---@type AtomName, table<string, string>
local phase_label = atom_name_to_phase[atom_scope] ---@type string|nil
-- Build the atom's source pool: start with the full POOL, subtract:
-- (a) every GPR already committed (phase allocations + prior atom allocations)
-- (b) every USER-PINNED GPR (wave-context carriers + file-scope pinned aliases)
@@ -323,37 +277,26 @@ function M.run(ctx)
-- the original `source_pool = phase_allocations[phase_label]` form used the phase
-- allocation MAP as a pool, but that map has no array part, so `table.remove(source_pool, 1)`
-- returned nil and every atom-with-phase marker errored with `phase_register_pool_exhausted`.
--- @type table<GprIdent, boolean> -- bag: committed or body-referenced GPR -> true
local used = {}
--- @type integer, GprAllocMap
for _, m in pairs(phase_allocations) do for _, gpr in pairs(m) do used[gpr] = true end end
--- @type integer, GprAllocMap
for _, m in pairs(atom_allocations) do for _, gpr in pairs(m) do used[gpr] = true end end
local used = {} ---@type table<GprIdent, boolean> -- bag: committed or body-referenced GPR -> true
for _, m in pairs(phase_allocations) do for _, gpr in pairs(m) do used[gpr] = true end end ---@type integer, GprAllocMap
for _, m in pairs(atom_allocations) do for _, gpr in pairs(m) do used[gpr] = true end end ---@type integer, GprAllocMap
-- (c) Body references — scan the atom body for hardcoded + alias-resolved GPRs.
-- Folded into `used` so the source_pool exclusion is a single check.
--- @type AtomEntry|nil
local atom = corpus.atoms_by_name and corpus.atoms_by_name[atom_scope]
local atom = corpus.atoms_by_name and corpus.atoms_by_name[atom_scope] ---@type AtomEntry|nil
if atom and atom.body then
--- @type table<GprIdent, integer>
local body_used = find_used_gprs(atom.body, alias_to_gpr)
--- @type GprIdent
for gpr in pairs(body_used) do used[gpr] = true end
local body_used = find_used_gprs(atom.body, alias_to_gpr) ---@type table<GprIdent, integer>
for gpr in pairs(body_used) do used[gpr] = true end ---@type GprIdent
end
--- @type GprIdent[]
local source_pool = {}
--- @type integer, GprIdent
for _, gpr in ipairs(POOL) do
local source_pool = {} ---@type GprIdent[]
for _, gpr in ipairs(POOL) do ---@type integer, GprIdent
-- Exclude (a) prior commitments, (b) USER-PINNED GPRs (wave-context carriers declared via atom_reg + _Code defs, preserved across atoms globally).
if not used[gpr] and not user_pinned[gpr] then
source_pool[#source_pool + 1] = gpr
end
end
--- @type GprAllocMap
local result = {}
--- @type integer, string
for _, sym in ipairs(stable_sort_keys(decls)) do
--- @type GprIdent|nil
local next_gpr = table.remove(source_pool, 1)
local result = {} ---@type GprAllocMap
for _, sym in ipairs(stable_sort_keys(decls)) do ---@type integer, string
local next_gpr = table.remove(source_pool, 1) ---@type GprIdent|nil
if not next_gpr then
errors[#errors + 1] = {
line = 0,
@@ -375,15 +318,11 @@ function M.run(ctx)
-- This warning is kept as a defensive safety net for cases the body scanner might miss
-- (e.g. macros that expand to register references the scanner cannot resolve).
-- For each resolved (scope, sym) -> R_Tn mapping, scan the atom body source for used GPRs.
--- @type AtomName, GprAllocMap
for atom_scope, decls in pairs(atom_allocations) do
--- @type AtomEntry|nil
local atom = corpus.atoms_by_name and corpus.atoms_by_name[atom_scope]
for atom_scope, decls in pairs(atom_allocations) do ---@type AtomName, GprAllocMap
local atom = corpus.atoms_by_name and corpus.atoms_by_name[atom_scope] ---@type AtomEntry|nil
if atom and atom.body then
--- @type table<GprIdent, integer>
local used_in_body = find_used_gprs(atom.body, alias_to_gpr)
--- @type string, GprIdent
for sym, allocated_gpr in pairs(decls) do
local used_in_body = find_used_gprs(atom.body, alias_to_gpr) ---@type table<GprIdent, integer>
for sym, allocated_gpr in pairs(decls) do ---@type string, GprIdent
if used_in_body[allocated_gpr] and used_in_body[allocated_gpr] > 0 then
warnings[#warnings + 1] = {
line = atom.line or 0,
@@ -398,37 +337,27 @@ function M.run(ctx)
-- 4. Emit per-directory gen/auto_reg.h.
-- For each source directory that has atom_auto_regs or phase_auto_regs entries, emit one header.
--- @type table<string, SourceFile[]>
local sources_by_dir = corpus.sources_by_dir or {}
--- @type string, SourceFile[]
for dir, sources in pairs(sources_by_dir) do
--- @type GprAllocMap
local per_dir_mappings = {}
--- @type integer, SourceFile
for _, src in ipairs(sources) do
local sources_by_dir = corpus.sources_by_dir or {} ---@type table<string, SourceFile[]>
for dir, sources in pairs(sources_by_dir) do ---@type string, SourceFile[]
local per_dir_mappings = {} ---@type GprAllocMap
for _, src in ipairs(sources) do ---@type integer, SourceFile
-- Collect every (sym -> gpr) entry that originated from a source in this directory.
-- `src.scan.atom_auto_regs` is keyed by ATOM SCOPE NAME; `pairs(t)` iterates KEYS so `scope_name` here is the scope ident (e.g. "cube_g4_face").
-- The previous `for _, scan_atom_auto` form silently assigned the VALUE (a `{sym = sym}` table) to the variable,
-- which made `atom_allocations[scan_atom_auto]` a table-indexed lookup that never resolved.
--- @type string
for scope_name in pairs(src.scan and src.scan.atom_auto_regs or {}) do
--- @type string, GprIdent
for sym, gpr in pairs(atom_allocations[scope_name] or {}) do
for scope_name in pairs(src.scan and src.scan.atom_auto_regs or {}) do ---@type string
for sym, gpr in pairs(atom_allocations[scope_name] or {}) do ---@type string, GprIdent
per_dir_mappings[sym] = gpr
end
end
--- @type string
for scope_name in pairs(src.scan and src.scan.phase_auto_regs or {}) do
--- @type string, GprIdent
for sym, gpr in pairs(phase_allocations[scope_name] or {}) do
for scope_name in pairs(src.scan and src.scan.phase_auto_regs or {}) do ---@type string
for sym, gpr in pairs(phase_allocations[scope_name] or {}) do ---@type string, GprIdent
per_dir_mappings[sym] = gpr
end
end
end
--- @type string
local out_dir = dir .. "/gen"
--- @type string|nil
local out_path = emit_auto_reg_h(out_dir, dir, sources, per_dir_mappings)
local out_dir = dir .. "/gen" ---@type string
local out_path = emit_auto_reg_h(out_dir, dir, sources, per_dir_mappings) ---@type string|nil
if out_path then outputs[#outputs + 1] = { auto_reg_h = out_path } end
end
return { outputs = outputs, errors = errors, warnings = warnings }
+135 -270
View File
@@ -22,42 +22,30 @@
-- Uses `debug.getinfo` to find this file's own directory, so it works both standalone and when require'd from the orchestrator.
-- Bootstrap: load `duffle_paths.lua` via `debug.getinfo(1, "S").source` (works both standalone + when require'd).
-- duffle_paths.lua sets package.path then returns `require("duffle")` at the bottom, so the dofile value IS the duffle module.
--- @type string
local _bootstrap_dir = debug.getinfo(1, "S").source:match("^@?(.*[/\\])") or "./"
--- @type DuffleExport
local duffle = dofile(_bootstrap_dir .. "../duffle_paths.lua")
local _bootstrap_dir = debug.getinfo(1, "S").source:match("^@?(.*[/\\])") or "./" ---@type string
local duffle = dofile(_bootstrap_dir .. "../duffle_paths.lua") ---@type DuffleExport
-- ════════════════════════════════════════════════════════════════════════════
-- Constants
-- ════════════════════════════════════════════════════════════════════════════
-- Atom component declaration identifiers.
--- @type string
local ATOM_COMP_PROC = "MipsAtomComp_Proc_"
--- @type string
local MIPS_ATOM = "Slice_MipsCode" -- prefix on the function declaration that wraps an AtomComp_Proc_
local ATOM_COMP_PROC = "MipsAtomComp_Proc_" ---@type string
local MIPS_ATOM = "Slice_MipsCode" ---@type string -- prefix on the function declaration that wraps an AtomComp_Proc_
-- Component-name prefixes.
--- @type string
local AC_PREFIX = "ac_" -- arg to MipsAtomComp_(ac_X); the X is the atom name
--- @type integer
local AC_PREFIX_LEN = 3
--- @type string
local MAC_PREFIX = "mac_" -- prefix on generated macros; the rest is the atom name
--- @type integer
local MAC_PREFIX_LEN = 4
local AC_PREFIX = "ac_" ---@type string -- arg to MipsAtomComp_(ac_X); the X is the atom name
local AC_PREFIX_LEN = 3 ---@type integer
local MAC_PREFIX = "mac_" ---@type string -- prefix on generated macros; the rest is the atom name
local MAC_PREFIX_LEN = 4 ---@type integer
-- ASCII byte values used in tokenization.
--- @type integer
local BYTE_NEWLINE = 10
--- @type integer
local BYTE_SLASH = 47
local BYTE_NEWLINE = 10 ---@type integer
local BYTE_SLASH = 47 ---@type integer
-- Output gen subdirectory + filename (per-directory aggregation; the directory name is the namespace).
--- @type string
local GEN_SUBDIR = "gen"
--- @type string
local MACS_FILENAME = "macs.h"
local GEN_SUBDIR = "gen" ---@type string
local MACS_FILENAME = "macs.h" ---@type string
-- ════════════════════════════════════════════════════════════════════════════
-- Type declarations
@@ -100,8 +88,7 @@ local MACS_FILENAME = "macs.h"
-- Local helpers (file I/O + path normalization)
-- ════════════════════════════════════════════════════════════════════════════
--- @type ComponentsPass
local M = {}
local M = {} ---@type ComponentsPass
-- ════════════════════════════════════════════════════════════════════════════
-- Back-walk helpers (composed into the entry point below: find_function_args_for)
@@ -123,8 +110,7 @@ local M = {}
--- @param before_pos integer
--- @return string|nil
local function find_function_args_for(source, name, before_pos)
--- @type string|nil, string|nil
local _, args_inner = duffle.find_function_decl_for(source, before_pos, #MIPS_ATOM)
local _, args_inner = duffle.find_function_decl_for(source, before_pos, #MIPS_ATOM) ---@type string|nil, string|nil
return args_inner
end
@@ -140,31 +126,24 @@ end
--- @return string[]|nil
local function extract_arg_names(args_str)
if not args_str or args_str == "" then return nil end
--- @type string[]
local names = {}
--- @type string[]
local tokens = duffle.split_top_level_commas(args_str)
--- @type integer, string
for _, tok in ipairs(tokens) do
--- @type string
local trimmed = duffle.trim(tok)
local names = {} ---@type string[]
local tokens = duffle.split_top_level_commas(args_str) ---@type string[]
for _, tok in ipairs(tokens) do ---@type integer, string
local trimmed = duffle.trim(tok) ---@type string
if trimmed ~= "" then
-- Strip trailing block comment (/* ... */) from the token, if present.
-- split_top_level_commas only skips block comments at TOP LEVEL (between commas),
-- not block comments embedded WITHIN a token between a parameter and a trailing comma.
-- Without this strip, the identifier-walk below stops at the `/` of `*/` and returns
-- the wrong name (or nothing). See `test_extract_arg_names_handles_trailing_block_comments`.
--- @type integer
local trimmed_end = #trimmed
local trimmed_end = #trimmed ---@type integer
if trimmed_end >= 2 and trimmed:sub(trimmed_end - 1, trimmed_end) == "*/" then
-- Find the matching `/*` that opens the trailing comment.
-- Walk back from the `*/` looking for `/*` (whitespace + `/*`).
--- @type integer
local close_pos = trimmed_end - 1 -- position of the second-to-last char
local close_pos = trimmed_end - 1 ---@type integer -- position of the second-to-last char
-- Walk back: skip trailing whitespace, then look for the `/*` opener.
while close_pos > 1 do
--- @type string
local ch = trimmed:sub(close_pos, close_pos)
local ch = trimmed:sub(close_pos, close_pos) ---@type string
if ch == " " or ch == "\t" or ch == "\n" or ch == "\r" then
close_pos = close_pos - 1
else
@@ -172,10 +151,8 @@ local function extract_arg_names(args_str)
end
end
-- Now scan back from close_pos for the `/*` opener (slashes are at close_pos-1 and close_pos-2).
--- @type integer|nil
local opener_pos = nil
--- @type integer
local scan = close_pos - 3
local opener_pos = nil ---@type integer|nil
local scan = close_pos - 3 ---@type integer
while scan >= 1 do
if trimmed:sub(scan, scan + 1) == "/*" then
opener_pos = scan
@@ -194,11 +171,9 @@ local function extract_arg_names(args_str)
trimmed_end = #trimmed
if trimmed_end >= 4 and trimmed:sub(trimmed_end, trimmed_end) == "]" then
-- Walk back: skip digits, expect `[`.
--- @type integer
local bracket_pos = trimmed_end - 1
local bracket_pos = trimmed_end - 1 ---@type integer
while bracket_pos > 1 do
--- @type string
local ch = trimmed:sub(bracket_pos, bracket_pos)
local ch = trimmed:sub(bracket_pos, bracket_pos) ---@type string
if ch >= "0" and ch <= "9" then
bracket_pos = bracket_pos - 1
else
@@ -212,22 +187,18 @@ local function extract_arg_names(args_str)
if trimmed == "" then goto continue end
-- Find the identifier at the end: walk back over trailers (whitespace + `*` + `[]`),
-- then walk back over the identifier chars (alnum + `_`).
--- @type integer
local ident_end = #trimmed
local ident_end = #trimmed ---@type integer
while ident_end > 0 do
--- @type string
local ch = trimmed:sub(ident_end, ident_end)
local ch = trimmed:sub(ident_end, ident_end) ---@type string
if ch == " " or ch == "\t" or ch == "*" or ch == "]" or ch == "[" then
ident_end = ident_end - 1
else
break
end
end
--- @type integer
local ident_start = ident_end
local ident_start = ident_end ---@type integer
while ident_start > 0 do
--- @type string
local ch = trimmed:sub(ident_start, ident_start)
local ch = trimmed:sub(ident_start, ident_start) ---@type string
if duffle.is_alnum_byte(string.byte(ch)) or ch == "_" then
ident_start = ident_start - 1
else
@@ -235,8 +206,7 @@ local function extract_arg_names(args_str)
end
end
ident_start = ident_start + 1
--- @type string
local name = trimmed:sub(ident_start, ident_end)
local name = trimmed:sub(ident_start, ident_end) ---@type string
if name ~= "" then names[#names + 1] = name end
::continue::
end
@@ -248,8 +218,7 @@ end
--- @param args_str string|nil
--- @return string[]|nil
local function formal_arg_names(args_str)
--- @type string[]|nil
local names = extract_arg_names(args_str)
local names = extract_arg_names(args_str) ---@type string[]|nil
if not names then return nil end
if names[1] == "ab" then table.remove(names, 1) end
if #names == 0 then return nil end
@@ -271,10 +240,8 @@ end
--- @param scan SourceScan
--- @return Component[]
local function project_components(source, scan)
--- @type Component[]
local out = {}
--- @type integer, AtomEntry
for _, a in ipairs(scan.atoms) do
local out = {} ---@type Component[]
for _, a in ipairs(scan.atoms) do ---@type integer, AtomEntry
-- Only `MipsAtomComp_(ac_X)` (kind="comp_bare") and `MipsAtomComp_Proc_(ac_X, ...)` (kind="comp_proc")
-- are COMPONENTS — they get inlined via `mac_<name>` aliases inside atom bodies.
-- `MipsAtom_Proc_` (kind="atom_proc") is an ATOM (ends with `mac_yield()`); it gets emitted via
@@ -285,12 +252,10 @@ local function project_components(source, scan)
-- Function-args lookup is meaningful for `MipsAtomComp_Proc_` components
-- (the macro sits inside `FI_ Slice_MipsCode ac_X(...)`); the alias expansion
-- discards the `ab` (atom-builder) arg the same way both forms do.
--- @type string|nil
local args = find_function_args_for(source, a.raw_name, a.ident_pos)
local args = find_function_args_for(source, a.raw_name, a.ident_pos) ---@type string|nil
-- Comment ownership: scan_source.lua stamps `declaration_comment` on the record by walking backward past any associated bare marker.
-- The pass reads `declaration_comment` directly.
--- @type string
local comment = a.declaration_comment or ""
local comment = a.declaration_comment or "" ---@type string
out[#out + 1] = {
line = a.line,
name = a.name,
@@ -321,31 +286,23 @@ end
--- @param s string
--- @return string
local function convert_line_comments_to_block(s)
--- @type string
local result = s
--- @type integer
local pos = 1
--- @type integer
local len = #result
local result = s ---@type string
local pos = 1 ---@type integer
local len = #result ---@type integer
while pos <= len do
--- @type boolean
local is_double_slash = result:byte(pos) == BYTE_SLASH
local is_double_slash = result:byte(pos) == BYTE_SLASH ---@type boolean
and pos + 1 <= len and result:byte(pos + 1) == BYTE_SLASH
if not is_double_slash then
pos = pos + 1
else
-- Find end of line.
--- @type integer
local eol = pos
local eol = pos ---@type integer
while eol <= len and result:byte(eol) ~= BYTE_NEWLINE do
eol = eol + 1
end
--- @type string
local before = result:sub(1, pos - 1)
--- @type string
local comment = result:sub(pos + 2, eol - 1) -- skip the `//`
--- @type string
local after
local before = result:sub(1, pos - 1) ---@type string
local comment = result:sub(pos + 2, eol - 1) ---@type string -- skip the `//`
local after ---@type string
if eol <= len and result:byte(eol) == BYTE_NEWLINE then
after = " */" .. result:sub(eol) -- keep the newline
else
@@ -382,14 +339,11 @@ end
--- @param tok string
--- @return string
local function strip_leading_delay_marker(tok)
--- @type string|nil
local ident = duffle.read_ident(tok, 1)
local ident = duffle.read_ident(tok, 1) ---@type string|nil
if not ident or not duffle.DELAY_MARKERS[ident] then return tok end
--- @type string
local rest = tok:sub(#ident + 1):match("^%s*(.*)$") or ""
local rest = tok:sub(#ident + 1):match("^%s*(.*)$") or "" ---@type string
while rest:sub(1, 2) == "/*" do
--- @type integer|nil
local close = rest:find("*/", 3, true)
local close = rest:find("*/", 3, true) ---@type integer|nil
if not close then return "" end
rest = rest:sub(close + 2):match("^%s*(.*)$") or ""
end
@@ -406,24 +360,17 @@ end
local function word_count_rec(name, comp_by_name, wc, cache)
if cache[name] ~= nil then return cache[name] end
cache[name] = -1 -- mark in-progress (cycle detection)
--- @type Component|nil
local cc = comp_by_name[name]
--- @type integer
local n
local cc = comp_by_name[name] ---@type Component|nil
local n ---@type integer
if cc then
n = 0
--- @type BodyToken[]
local tokens = cc.body_tokens
--- @type integer, BodyToken
for _, t in ipairs(tokens) do
--- @type string
local trimmed = t.tok
local tokens = cc.body_tokens ---@type BodyToken[]
for _, t in ipairs(tokens) do ---@type integer, BodyToken
local trimmed = t.tok ---@type string
if trimmed ~= "" then
--- @type string
local work = trimmed
local work = trimmed ---@type string
while true do
--- @type string|nil
local marker = duffle.read_ident(work, 1)
local marker = duffle.read_ident(work, 1) ---@type string|nil
if marker and duffle.DELAY_MARKERS[marker] then
work = strip_leading_delay_marker(work)
if work == "" then break end
@@ -432,8 +379,7 @@ local function word_count_rec(name, comp_by_name, wc, cache)
end
end
if work ~= "" then
--- @type string|nil
local lookup = strip_mac_prefix(duffle.read_ident(work, 1))
local lookup = strip_mac_prefix(duffle.read_ident(work, 1)) ---@type string|nil
if lookup == "atom_label" or lookup == "atom_offset" then
-- Pure metaprogram anchors; emit zero words.
elseif lookup and comp_by_name[lookup] then
@@ -466,16 +412,11 @@ end
--- @param wc WordCounts
--- @return table<string, integer> -- bag: bare component name -> word count
local function count_all_components(components, wc)
--- @type table<string, Component>
local comp_by_name = {}
--- @type integer, Component
for _, cc in ipairs(components) do comp_by_name[cc.name] = cc end
--- @type table<string, integer> -- bag: memo; -1 in-progress sentinel
local cache = {}
--- @type table<string, integer> -- bag: bare name -> word count
local counts = {}
--- @type integer, Component
for _, c in ipairs(components) do
local comp_by_name = {} ---@type table<string, Component>
for _, cc in ipairs(components) do comp_by_name[cc.name] = cc end ---@type integer, Component
local cache = {} ---@type table<string, integer> -- bag: memo; -1 in-progress sentinel
local counts = {} ---@type table<string, integer> -- bag: bare name -> word count
for _, c in ipairs(components) do ---@type integer, Component
counts[c.name] = word_count_rec(c.name, comp_by_name, wc, cache)
end
return counts
@@ -504,34 +445,23 @@ end
local function component_meta_rec(name, comp_by_name, latency, cache)
if cache[name] ~= nil then return cache[name] end
cache[name] = { cycle_cost = -1, gp0_contrib = -1 }
--- @type Component|nil
local cc = comp_by_name[name]
--- @type integer
local cycle_cost
--- @type integer
local gp0_contrib
local cc = comp_by_name[name] ---@type Component|nil
local cycle_cost ---@type integer
local gp0_contrib ---@type integer
if cc then
--- @type boolean
local skip_cycle = (name == "yield")
--- @type boolean
local skip_gp0 = name:match("^insert_ot_tag") ~= nil
local skip_cycle = (name == "yield") ---@type boolean
local skip_gp0 = name:match("^insert_ot_tag") ~= nil ---@type boolean
cycle_cost = 0
gp0_contrib = 0
if not skip_cycle or not skip_gp0 then
--- @type BodyToken[]
local tokens = cc.body_tokens
--- @type integer, BodyToken
for _, t in ipairs(tokens) do
--- @type string
local trimmed = t.tok
local tokens = cc.body_tokens ---@type BodyToken[]
for _, t in ipairs(tokens) do ---@type integer, BodyToken
local trimmed = t.tok ---@type string
if trimmed ~= "" then
--- @type string|nil
local ident = duffle.read_ident(trimmed, 1)
local ident = duffle.read_ident(trimmed, 1) ---@type string|nil
if ident and ident:sub(1, MAC_PREFIX_LEN) == MAC_PREFIX then
--- @type string
local nested = ident:sub(MAC_PREFIX_LEN + 1)
--- @type ComponentMeta
local nested_meta = component_meta_rec(nested, comp_by_name, latency, cache)
local nested = ident:sub(MAC_PREFIX_LEN + 1) ---@type string
local nested_meta = component_meta_rec(nested, comp_by_name, latency, cache) ---@type ComponentMeta
if not skip_cycle then
cycle_cost = cycle_cost + nested_meta.cycle_cost
end
@@ -540,10 +470,8 @@ local function component_meta_rec(name, comp_by_name, latency, cache)
end
else
if not skip_cycle then
--- @type InstructionRow|nil
local isa = duffle.instr(ident)
--- @type GteCommandRow|nil
local gte = duffle.gte(ident)
local isa = duffle.instr(ident) ---@type InstructionRow|nil
local gte = duffle.gte(ident) ---@type GteCommandRow|nil
cycle_cost = cycle_cost + ((isa and isa.cycles) or (gte and gte.cycles) or latency[ident] or 1)
end
if not skip_gp0 then
@@ -578,16 +506,11 @@ end
--- @param latency table<string, integer> -- bag: ident -> cycle cost
--- @return ComponentMetaMap
local function compute_components_metadata(components, latency)
--- @type table<string, Component>
local comp_by_name = {}
--- @type integer, Component
for _, cc in ipairs(components) do comp_by_name[cc.name] = cc end
--- @type ComponentMetaMap
local cache = {}
--- @type ComponentMetaMap
local out = {}
--- @type integer, Component
for _, c in ipairs(components) do
local comp_by_name = {} ---@type table<string, Component>
for _, cc in ipairs(components) do comp_by_name[cc.name] = cc end ---@type integer, Component
local cache = {} ---@type ComponentMetaMap
local out = {} ---@type ComponentMetaMap
for _, c in ipairs(components) do ---@type integer, Component
out[c.name] = component_meta_rec(c.name, comp_by_name, latency, cache)
end
return out
@@ -602,15 +525,11 @@ end
--- @param s string
--- @return string[]
local function split_comment_lines(s)
--- @type string[]
local out = {}
--- @type integer
local pos = 1
--- @type integer
local s_len = #s
local out = {} ---@type string[]
local pos = 1 ---@type integer
local s_len = #s ---@type integer
while pos <= s_len do
--- @type integer|nil
local nl = s:find("\n", pos, true)
local nl = s:find("\n", pos, true) ---@type integer|nil
if not nl then
out[#out + 1] = s:sub(pos)
break
@@ -629,8 +548,7 @@ end
--- @param args_str string|nil
--- @return string
local function signature_from_args(args_str)
--- @type string[]|nil
local names = formal_arg_names(args_str)
local names = formal_arg_names(args_str) ---@type string[]|nil
if names then
return table.concat(names, ", ")
end
@@ -642,8 +560,7 @@ end
--- @param lines string[]
--- @return nil
local function strip_trailing_continuation(lines)
--- @type string
local last = lines[#lines]
local last = lines[#lines] ---@type string
if last:sub(-2) == " \\" then
lines[#lines] = last:sub(1, -3)
end
@@ -669,37 +586,30 @@ end
--- @param tok string -- a single token from split_top_level_commas (already trimmed at the start, may contain trailing whitespace + block comment)
--- @return boolean
local function is_pure_delay_marker_token(tok)
--- @type table<string, boolean> -- bag: delay-marker ident -> true
local markers = duffle.DELAY_MARKERS
local markers = duffle.DELAY_MARKERS ---@type table<string, boolean> -- bag: delay-marker ident -> true
if type(markers) ~= "table" then return false end
-- Identify a leading delay-marker identifier (e.g. `GteDelay_`).
--- @type integer
local ident_end = 1
local ident_end = 1 ---@type integer
while ident_end <= #tok do
--- @type string
local ch = tok:sub(ident_end, ident_end)
local ch = tok:sub(ident_end, ident_end) ---@type string
if ch:match("[%w_]") then
ident_end = ident_end + 1
else
break
end
end
--- @type string
local ident = tok:sub(1, ident_end - 1)
local ident = tok:sub(1, ident_end - 1) ---@type string
if not markers[ident] then return false end
-- Walk the remainder: only whitespace and block comments are allowed.
--- @type integer
local scan = ident_end
local scan = ident_end ---@type integer
while scan <= #tok do
--- @type string
local ch = tok:sub(scan, scan)
local ch = tok:sub(scan, scan) ---@type string
if ch:match("%s") then
scan = scan + 1
elseif ch == "/" and tok:sub(scan + 1, scan + 1) == "*" then
--- @type integer|nil
local close = tok:find("*/", scan + 2, true)
local close = tok:find("*/", scan + 2, true) ---@type integer|nil
if not close then return false end
scan = close + 2
else
@@ -743,17 +653,14 @@ end
--- @param tokens string[]
--- @return nil
local function emit_macro_body(lines, c, sig, tokens)
--- @type integer
for tok_idx = 1, #tokens do
for tok_idx = 1, #tokens do ---@type integer
tokens[tok_idx] = convert_line_comments_to_block(tokens[tok_idx])
end
if #tokens == 0 then return end
lines[#lines + 1] = "#define mac_" .. c.name .. "(" .. sig .. ") \\"
lines[#lines + 1] = "\t" .. tokens[1] .. " \\"
--- @type integer
for tok_idx = 2, #tokens do
--- @type string
local sep = token_skips_leading_comma(tokens[tok_idx]) and "\t" or ",\t"
for tok_idx = 2, #tokens do ---@type integer
local sep = token_skips_leading_comma(tokens[tok_idx]) and "\t" or ",\t" ---@type string
lines[#lines + 1] = sep .. tokens[tok_idx] .. " \\"
end
strip_trailing_continuation(lines)
@@ -768,8 +675,7 @@ end
--- @param counts table<string, integer> -- bag: bare component name -> word count
--- @return string[] -- list of lines for this component
local function build_component_lines(c, counts)
--- @type string[]
local lines = {}
local lines = {} ---@type string[]
-- Marker comment: emitted once for every skipped component.
-- The marker is scanner-owned (declared by `atom_dbg_skip` immediately before the declaration in the source);
@@ -779,21 +685,16 @@ local function build_component_lines(c, counts)
end
if c.comment and c.comment ~= "" then
--- @type integer, string
for _, line in ipairs(split_comment_lines(c.comment)) do
for _, line in ipairs(split_comment_lines(c.comment)) do ---@type integer, string
lines[#lines + 1] = line
end
end
--- @type string[]
local tokens = duffle.split_top_level_commas(c.body)
--- @type integer
for i = 1, #tokens do tokens[i] = duffle.trim(tokens[i]) end
--- @type string
local sig = signature_from_args(c.args)
local tokens = duffle.split_top_level_commas(c.body) ---@type string[]
for i = 1, #tokens do tokens[i] = duffle.trim(tokens[i]) end ---@type integer
local sig = signature_from_args(c.args) ---@type string
-- Direct lookup against the per-source precomputed `counts` table (built once by count_all_components).
--- @type integer
local n = counts[c.name]
local n = counts[c.name] ---@type integer
if n > 0 then
emit_macro_body(lines, c, sig, tokens)
@@ -816,14 +717,11 @@ end
--- @param sources SourceFile[] -- Sources contributing to this directory (for the header comment)
--- @return string[]
local function header_boilerplate(dir, sources)
--- @type string[]
local source_lines = { "// Directory: " .. duffle.to_absolute_path(dir) .. "/" }
--- @type integer, SourceFile
for _, src in ipairs(sources) do
local source_lines = { "// Directory: " .. duffle.to_absolute_path(dir) .. "/" } ---@type string[]
for _, src in ipairs(sources) do ---@type integer, SourceFile
source_lines[#source_lines + 1] = "// source: " .. duffle.to_absolute_path(src.path)
end
--- @type string
local source_blob = table.concat(source_lines, "\n")
local source_blob = table.concat(source_lines, "\n") ---@type string
return {
-- #pragma once wrapped in #ifdef INTELLISENSE_DIRECTIVES, matching the convention in lottes_tape.h.
-- The build does manual unity includes (the user controls include order), so the pragma is only active for IDE/tooling.
@@ -851,10 +749,8 @@ end
--- @return string -- Output directory
--- @return string -- Full output path
local function compute_macs_h_path(dir)
--- @type string
local out_dir = dir .. "/" .. GEN_SUBDIR
--- @type string
local out_path = out_dir .. "/" .. MACS_FILENAME
local out_dir = dir .. "/" .. GEN_SUBDIR ---@type string
local out_path = out_dir .. "/" .. MACS_FILENAME ---@type string
return out_dir, out_path
end
@@ -868,21 +764,16 @@ end
--- @return string|nil -- Path to the written file (nil if no components)
local function emit_component_macros_h(ctx, dir, sources, components, counts)
if #components == 0 then return nil end
--- @type string, string
local out_dir, out_path = compute_macs_h_path(dir)
--- @type string[]
local lines = header_boilerplate(dir, sources)
local out_dir, out_path = compute_macs_h_path(dir) ---@type string, string
local lines = header_boilerplate(dir, sources) ---@type string[]
--- @type integer, Component
for _, c in ipairs(components) do
--- @type integer, string
for _, l in ipairs(build_component_lines(c, counts)) do
for _, c in ipairs(components) do ---@type integer, Component
for _, l in ipairs(build_component_lines(c, counts)) do ---@type integer, string
lines[#lines + 1] = l
end
end
--- @type string
local content = table.concat(lines, "\n") .. "\n"
local content = table.concat(lines, "\n") .. "\n" ---@type string
duffle.ensure_dir(out_dir)
duffle.write_file_lf(out_path, content)
print(string.format(" -> %s", out_path))
@@ -900,12 +791,9 @@ end
--- @param counts table<string, integer> -- bag: bare component name -> word count
--- @return nil
local function update_canonical_word_counts(corpus, components, counts)
--- @type WordCounts
local wc = corpus.word_counts
--- @type integer, Component
for _, c in ipairs(components) do
--- @type string
local key = "mac_" .. c.name
local wc = corpus.word_counts ---@type WordCounts
for _, c in ipairs(components) do ---@type integer, Component
local key = "mac_" .. c.name ---@type string
if wc[key] == nil then
wc[key] = counts[c.name]
end
@@ -932,15 +820,12 @@ end
--- @param metadata ComponentMetaMap
--- @return nil
local function update_canonical_components(corpus, src, components, metadata)
--- @type string
local rel_path = src.path:gsub("\\", "/")
--- @type integer, Component
for _, c in ipairs(components) do
local rel_path = src.path:gsub("\\", "/") ---@type string
for _, c in ipairs(components) do ---@type integer, Component
-- Keyed by bare name (e.g. `yield`, `load_tri_indices`).
-- The atoms_source_map pass looks up components by bare name from the corpus;
-- `mac_` prefix lives at the call-site identifier and is stripped before lookup.
--- @type ComponentMeta|nil
local m = metadata and metadata[c.name] or nil
local m = metadata and metadata[c.name] or nil ---@type ComponentMeta|nil
if corpus.components[c.name] == nil then
corpus.components[c.name] = {
name = c.name,
@@ -954,13 +839,10 @@ local function update_canonical_components(corpus, src, components, metadata)
else
-- A second declaration of the same bare name: record a typed collision so static-analysis + the report can surface it.
-- Identical-shape declarations (same path + line) reuse the first-wins entry without a collision record.
--- @type ComponentDef
local existing = corpus.components[c.name]
local existing = corpus.components[c.name] ---@type ComponentDef
if existing.path ~= rel_path or existing.line ~= c.line then
--- @type string
local kind = c.kind or "comp_bare"
--- @type string
local first_kind = existing.kind or "comp_bare"
local kind = c.kind or "comp_bare" ---@type string
local first_kind = existing.kind or "comp_bare" ---@type string
corpus.collisions[#corpus.collisions + 1] = {
kind = "component",
name = c.name,
@@ -983,10 +865,8 @@ end
--- @param scan SourceScan
--- @return nil
local function update_canonical_component_body_index(corpus, src, components, scan)
--- @type (fun(pos: integer): integer)|nil
local line_of = scan and scan.line_of
--- @type integer, Component
for _, c in ipairs(components) do
local line_of = scan and scan.line_of ---@type (fun(pos: integer): integer)|nil
for _, c in ipairs(components) do ---@type integer, Component
if corpus.component_body_index[c.name] == nil then
corpus.component_body_index[c.name] = {
body_tokens = c.body_tokens,
@@ -1004,16 +884,12 @@ end
--- @param ctx PassCtx
--- @return PassResult
function M.run(ctx)
--- @type MacsOutput[]
local outputs = {}
--- @type PassFinding[]
local errors = {}
--- @type PassFinding[]
local warnings = {}
local outputs = {} ---@type MacsOutput[]
local errors = {} ---@type PassFinding[]
local warnings = {} ---@type PassFinding[]
-- Corpus ownership gate.
--- @type Corpus|nil
local corpus = ctx.shared and ctx.shared.corpus
local corpus = ctx.shared and ctx.shared.corpus ---@type Corpus|nil
if type(corpus) ~= "table" then
error("components.run requires ctx.shared.corpus.", 0)
end
@@ -1034,22 +910,15 @@ function M.run(ctx)
-- Per-directory aggregation: every source in the same directory contributes to one `gen/macs.h`.
-- The directory itself is the namespace. `corpus.sources_by_dir` preserves source-order within each bucket (matches `corpus.source_order`).
--- @type table<string, SourceFile[]>
local sources_by_dir = corpus.sources_by_dir or duffle.group_sources_by_dir(corpus.source_order)
--- @type string, SourceFile[]
for dir, sources in pairs(sources_by_dir) do
local sources_by_dir = corpus.sources_by_dir or duffle.group_sources_by_dir(corpus.source_order) ---@type table<string, SourceFile[]>
for dir, sources in pairs(sources_by_dir) do ---@type string, SourceFile[]
-- Aggregate components from every source in this directory.
-- `project_components` returns nil for sources with no `MipsAtomComp_` declarations; we skip those.
--- @type Component[]
local aggregated_components = {}
--- @type table<SourceFile, ComponentMetaMap>
local metadata_per_source = {}
--- @type integer, SourceFile
for _, src in ipairs(sources) do
--- @type Component[]
local per_source = project_components(src.text, src.scan) or {}
--- @type integer, Component
for _, c in ipairs(per_source) do
local aggregated_components = {} ---@type Component[]
local metadata_per_source = {} ---@type table<SourceFile, ComponentMetaMap>
for _, src in ipairs(sources) do ---@type integer, SourceFile
local per_source = project_components(src.text, src.scan) or {} ---@type Component[]
for _, c in ipairs(per_source) do ---@type integer, Component
aggregated_components[#aggregated_components + 1] = c
end
if #per_source > 0 then
@@ -1059,18 +928,14 @@ function M.run(ctx)
if #aggregated_components > 0 then
-- Compute word counts across the aggregated set. `corpus.word_counts` carries the
-- same-source + prior-directory entries so the recursive lookup sees both.
--- @type table<string, integer> -- bag: bare name -> word count
local counts = count_all_components(aggregated_components, corpus.word_counts)
--- @type string|nil
local macs_path = emit_component_macros_h(ctx, dir, sources, aggregated_components, counts)
local counts = count_all_components(aggregated_components, corpus.word_counts) ---@type table<string, integer> -- bag: bare name -> word count
local macs_path = emit_component_macros_h(ctx, dir, sources, aggregated_components, counts) ---@type string|nil
if macs_path then
outputs[#outputs + 1] = { macs_h = macs_path }
-- Populate the projections AFTER disk emission (byte-identical `.macs.h` contract).
update_canonical_word_counts(corpus, aggregated_components, counts)
--- @type integer, SourceFile
for _, src in ipairs(sources) do
--- @type Component[]
local per_source = project_components(src.text, src.scan) or {}
for _, src in ipairs(sources) do ---@type integer, SourceFile
local per_source = project_components(src.text, src.scan) or {} ---@type Component[]
if #per_source > 0 then
update_canonical_components(corpus, src, per_source, metadata_per_source[src])
update_canonical_component_body_index(corpus, src, per_source, src.scan)
File diff suppressed because it is too large Load Diff
+47 -94
View File
@@ -117,16 +117,13 @@
--- @class EmissionModelPass
--- @field run fun(ctx: PassCtx): PassResult
--- @type EmissionModelPass
local M = {}
local M = {} ---@type EmissionModelPass
-- ─────────────────────────────────────────────────────────────────────────
-- Bootstrap: load `duffle_paths.lua` via debug.getinfo so the module works standalone (run as `luajit passes/emission_model.lua`) and when require'd from the orchestrator.
-- ─────────────────────────────────────────────────────────────────────────
--- @type string
local _bootstrap_dir = debug.getinfo(1, "S").source:match("^@?(.*[/\\])") or "./"
--- @type DuffleExport
local duffle = dofile(_bootstrap_dir .. "../duffle_paths.lua")
local _bootstrap_dir = debug.getinfo(1, "S").source:match("^@?(.*[/\\])") or "./" ---@type string
local duffle = dofile(_bootstrap_dir .. "../duffle_paths.lua") ---@type DuffleExport
-- ─────────────────────────────────────────────────────────────────────────
-- Helpers
@@ -149,8 +146,7 @@ local duffle = dofile(_bootstrap_dir .. "../duffle_paths.lua")
--- @param corpus Corpus
--- @return nil
local function stamp_root_provenance(projection, atom_record, src, corpus)
--- @type (fun(pos: integer): integer)|nil
local root_line_of = src.scan and src.scan.line_of
local root_line_of = src.scan and src.scan.line_of ---@type (fun(pos: integer): integer)|nil
assert(type(root_line_of) == "function"
, "emission_model: src.scan.line_of is required (canonical LineIndex closure over the source text) to stamp physical provenance")
assert(type(atom_record.body_off) == "number"
@@ -158,15 +154,11 @@ local function stamp_root_provenance(projection, atom_record, src, corpus)
-- `root_body_line` is the physical source line of the ATOM HEADER byte containing the opening `{`; that byte is one byte BEFORE `atom_record.body_off`.
-- The walker assigns line 2 to the body's first content line because line 1 is the trailing `\n` after `{`. Body-text line k therefore maps to `root_body_line + (k - 1)`.
-- `body_off - 1` points at the opening `{`, whose line index identifies the header line. `body_off` points after `{` and would shift every word row forward by one line.
--- @type integer
local root_body_line = root_line_of(atom_record.body_off - 1) or atom_record.line or 0
--- @type table<string, ComponentBodyEntry>
local component_index = corpus.component_body_index or {}
--- @type EmissionItem[]
local word_items = {}
local root_body_line = root_line_of(atom_record.body_off - 1) or atom_record.line or 0 ---@type integer
local component_index = corpus.component_body_index or {} ---@type table<string, ComponentBodyEntry>
local word_items = {} ---@type EmissionItem[]
--- @type integer, EmissionItem
for _, item in ipairs(projection.items) do
for _, item in ipairs(projection.items) do ---@type integer, EmissionItem
if item.kind == "word" then word_items[#word_items + 1] = item end
end
@@ -177,18 +169,14 @@ local function stamp_root_provenance(projection, atom_record, src, corpus)
--- @param item EmissionItem
--- @return integer
local function body_line_for(event, item)
--- @type integer[]
local ids = event.invocation_ids or {}
local ids = event.invocation_ids or {} ---@type integer[]
-- The innermost open invocation identifies which line index the walker used.
-- A component `line_of` makes `item.line` physical; the atom's `body_text` line index makes it body-relative.
if ids and #ids > 0 then
--- @type integer
local inner_id = ids[#ids]
--- @type InvocationRecord|nil
local inner_inv = inner_id and projection.invocations[inner_id]
local inner_id = ids[#ids] ---@type integer
local inner_inv = inner_id and projection.invocations[inner_id] ---@type InvocationRecord|nil
if inner_inv then
--- @type ComponentBodyEntry|nil
local component = component_index[inner_inv.component_name]
local component = component_index[inner_inv.component_name] ---@type ComponentBodyEntry|nil
if component and component.line_of then
-- Walker used `comp.line_of`, which is the source's physical LineIndex. item.line is already physical.
return item.line or 0
@@ -203,10 +191,8 @@ local function stamp_root_provenance(projection, atom_record, src, corpus)
-- Stamp the root source path onto invocation records whose `call_path` the walker left empty.
-- The walker passes `body_entry.source` to `emit_invoke_begin`; `M.project_emission` creates the root `body_entry` with source `""`, leaving its `call_path` empty.
-- This stamp gives every invocation a physical `call_path` matching `passes/atoms_source_map.lua`'s in-memory provenance projection.
--- @type string
local root_path = src.path or ""
--- @type integer, InvocationRecord
for _, inv in ipairs(projection.invocations) do
local root_path = src.path or "" ---@type string
for _, inv in ipairs(projection.invocations) do ---@type integer, InvocationRecord
if inv.call_path == nil or inv.call_path == "" then
inv.call_path = root_path
end
@@ -215,8 +201,7 @@ local function stamp_root_provenance(projection, atom_record, src, corpus)
-- Normalize `inv.call_line` to a physical source line.
-- * ROOT invocations (`parent_id == 0`) carry body-relative `call_line` values from `M.LineIndex(body_text)`; convert them once with `root_body_line`.
-- * INNER invocations (`parent_id ~= 0`) carry physical `call_line` values from the component's `line_of`; retain them unchanged.
--- @type integer, InvocationRecord
for _, inv in ipairs(projection.invocations) do
for _, inv in ipairs(projection.invocations) do ---@type integer, InvocationRecord
if inv.parent_id == 0 then
inv.call_line = (root_body_line or 0) + (inv.call_line or 1) - 1
end
@@ -225,21 +210,14 @@ local function stamp_root_provenance(projection, atom_record, src, corpus)
-- Build `body_lines` for each invocation.
-- `atoms_source_map` and `dwarf_injection` read `inv.body_lines[k]` directly from the invocation record created here.
-- Component words already carry physical `item.line` values from the walker's COMPONENT line index, so `body_line_for` returns them unchanged.
--- @type integer, InvocationRecord
for _, inv in ipairs(projection.invocations) do
--- @type integer
local sw = inv.start_word
--- @type integer
local ew = inv.end_word
--- @type integer[]
local bls = {}
--- @type integer
for i = sw, ew do
--- @type EmissionItem|nil
local it = projection.items and projection.items[i]
for _, inv in ipairs(projection.invocations) do ---@type integer, InvocationRecord
local sw = inv.start_word ---@type integer
local ew = inv.end_word ---@type integer
local bls = {} ---@type integer[]
for i = sw, ew do ---@type integer
local it = projection.items and projection.items[i] ---@type EmissionItem|nil
if it and it.kind == "word" then
--- @type WordEvent
local fake_event = { invocation_ids = { inv.id } }
local fake_event = { invocation_ids = { inv.id } } ---@type WordEvent
bls[#bls + 1] = body_line_for(fake_event, it) or 0
end
end
@@ -249,21 +227,15 @@ local function stamp_root_provenance(projection, atom_record, src, corpus)
-- Resolve each `word_event`'s physical `body_line` and `call_line`.
-- For words inside an invocation, `we.call_line` identifies the OUTER atom source line containing the `mac_X(...)` token that triggered expansion.
-- The root-invocation conversion above makes every `inv.call_line` physical; forward it directly and use each raw word's `body_line` as the fallback.
--- @type integer, WordEvent
for index, we in ipairs(projection.word_events) do
--- @type EmissionItem
local item = word_items[index] or {}
--- @type integer
local body_line = body_line_for(we, item)
for index, we in ipairs(projection.word_events) do ---@type integer, WordEvent
local item = word_items[index] or {} ---@type EmissionItem
local body_line = body_line_for(we, item) ---@type integer
item.line = body_line
we.body_line = body_line
--- @type integer
local call_line = body_line
--- @type integer
local outer_id = we.outermost_invocation_id or 0
--- @type InvocationRecord|nil
local outer_inv = projection.invocations[outer_id]
local call_line = body_line ---@type integer
local outer_id = we.outermost_invocation_id or 0 ---@type integer
local outer_inv = projection.invocations[outer_id] ---@type InvocationRecord|nil
if outer_inv then
-- `outer_inv.call_line` is physical after the conversion loop above, so use it directly.
call_line = outer_inv.call_line
@@ -283,20 +255,15 @@ end
--- @param corpus Corpus
--- @return EmissionProjection
local function project_atom(atom_record, src, corpus)
--- @type string
local body = atom_record.body or ""
--- @type WordCounts
local wc = corpus.word_counts or {}
--- @type table<string, ComponentBodyEntry>
local cbi = corpus.component_body_index or {}
--- @type RegUseSchema|nil
local schema = nil
local body = atom_record.body or "" ---@type string
local wc = corpus.word_counts or {} ---@type WordCounts
local cbi = corpus.component_body_index or {} ---@type table<string, ComponentBodyEntry>
local schema = nil ---@type RegUseSchema|nil
if atom_record.reg_use_schema_name then
schema = corpus.reg_use_schemas and corpus.reg_use_schemas[atom_record.reg_use_schema_name]
end
-- That construction site stamps `invocation.debug_skip` while appending each record to `proj.invocations`.
--- @type EmissionProjection
local proj = duffle.project_emission(body, cbi, wc, corpus.components, {
local proj = duffle.project_emission(body, cbi, wc, corpus.components, { ---@type EmissionProjection
reg_use_schema = schema,
reg_use_param = atom_record.reg_use_param_name,
atom_name = atom_record.name,
@@ -308,14 +275,12 @@ local function project_atom(atom_record, src, corpus)
msg = string.format("RegUse schema %q is missing", atom_record.reg_use_schema_name),
}
end
--- @type integer, EmitError
for _, err in ipairs(corpus.reg_use_errors or {}) do
for _, err in ipairs(corpus.reg_use_errors or {}) do ---@type integer, EmitError
if err.schema_name == atom_record.reg_use_schema_name then
proj.errors[#proj.errors + 1] = err
end
end
--- @type AtomPaths
local paths = {
local paths = { ---@type AtomPaths
tokens = atom_record.body_tokens or {},
line_in_body = duffle.build_body_line_index(body),
items = proj.items,
@@ -337,15 +302,11 @@ end
--- @param ctx PassCtx -- { shared = { corpus = ... }, out_root, ... }
--- @return PassResult
function M.run(ctx)
--- @type PassOutputEntry[]
local outputs = {}
--- @type EmitError[]
local errors = {}
--- @type EmitWarning[]
local warnings = {}
local outputs = {} ---@type PassOutputEntry[]
local errors = {} ---@type EmitError[]
local warnings = {} ---@type EmitWarning[]
--- @type Corpus|nil
local corpus = ctx and ctx.shared and ctx.shared.corpus
local corpus = ctx and ctx.shared and ctx.shared.corpus ---@type Corpus|nil
if type(corpus) ~= "table" then error("emission_model: ctx.shared.corpus is required (canonical projection)", 0) end
if type(corpus.source_order) ~= "table" then error("emission_model: ctx.shared.corpus.source_order is required", 0) end
@@ -356,15 +317,12 @@ function M.run(ctx)
--- @return nil
local function process_atom(atom, src)
if not (atom and atom.body) then return end
--- @type string
local kind = atom.kind
local kind = atom.kind ---@type string
if kind ~= "atom" and kind ~= "atom_proc" and kind ~= "raw_atom" and kind ~= "comp_bare" and kind ~= "comp_proc" then
return
end
--- @type EmissionProjection
local proj = project_atom(atom, src, corpus)
--- @type integer, EmitError
for _, e in ipairs(proj.errors) do
local proj = project_atom(atom, src, corpus) ---@type EmissionProjection
for _, e in ipairs(proj.errors) do ---@type integer, EmitError
-- Preserve `kind` (cycle / count_mismatch / unbalanced) so readers dispatch on the diagnostic class and leave the message string as display text.
errors[#errors + 1] = {
kind = e.kind,
@@ -373,8 +331,7 @@ function M.run(ctx)
source = e.source or src.path,
}
end
--- @type integer, EmitWarning
for _, w in ipairs(proj.warnings) do
for _, w in ipairs(proj.warnings) do ---@type integer, EmitWarning
warnings[#warnings + 1] = {
kind = w.kind,
line = w.line,
@@ -386,16 +343,12 @@ function M.run(ctx)
-- Walk `corpus.source_order`; within each source, visit atoms followed by raw_atoms.
-- Recognized kinds (atom | atom_proc | raw_atom | comp_bare | comp_proc) each receive the atom.paths projection via duffle.project_emission.
-- Components are macros inlined into atom bodies; focused tests and isolated component analyses consume atom.paths directly.
--- @type integer, SourceFile
for _, src in ipairs(corpus.source_order) do
--- @type SourceScan
local scan = src.scan or {}
--- @type integer, AtomEntry
for _, atom in ipairs(scan.atoms or {}) do
for _, src in ipairs(corpus.source_order) do ---@type integer, SourceFile
local scan = src.scan or {} ---@type SourceScan
for _, atom in ipairs(scan.atoms or {}) do ---@type integer, AtomEntry
process_atom(atom, src)
end
--- @type integer, AtomEntry
for _, atom in ipairs(scan.raw_atoms or {}) do
for _, atom in ipairs(scan.raw_atoms or {}) do ---@type integer, AtomEntry
process_atom(atom, src)
end
end
+37 -74
View File
@@ -20,24 +20,19 @@
-- Uses `debug.getinfo` to find this file's own directory, so it works both standalone and when require'd from the orchestrator.
-- Bootstrap: load `duffle_paths.lua` via `debug.getinfo(1, "S").source` (works both standalone + when require'd).
-- duffle_paths.lua sets package.path then returns `require("duffle")` at the bottom, so the dofile value IS the duffle module.
--- @type string
local _bootstrap_dir = debug.getinfo(1, "S").source:match("^@?(.*[/\\])") or "./"
--- @type DuffleExport
local duffle = dofile(_bootstrap_dir .. "../duffle_paths.lua")
local _bootstrap_dir = debug.getinfo(1, "S").source:match("^@?(.*[/\\])") or "./" ---@type string
local duffle = dofile(_bootstrap_dir .. "../duffle_paths.lua") ---@type DuffleExport
-- ════════════════════════════════════════════════════════════════════════════
-- Constants
-- ════════════════════════════════════════════════════════════════════════════
-- Offset macro/enum naming prefixes (the emitted header uses these).
--- @type string
local OFFSET_MACRO_PREFIX = "_atom_offset_"
--- @type string
local OFFSET_ENUM_PREFIX = "atom_offset_"
local OFFSET_MACRO_PREFIX = "_atom_offset_" ---@type string
local OFFSET_ENUM_PREFIX = "atom_offset_" ---@type string
-- Column width for the `#define _atom_offset_F_T = N` alignment.
--- @type integer
local OFFSET_MACRO_COL = 44
local OFFSET_MACRO_COL = 44 ---@type integer
-- ════════════════════════════════════════════════════════════════════════════
-- Type declarations
@@ -91,8 +86,7 @@ local OFFSET_MACRO_COL = 44
-- MARKER_PROJECTORS is the marker-kind data table.
-- The emission-model pass already records marker word positions + consuming-instruction context;
-- this pass only projects those records into the label/branch lookup shape needed by offset computation.
--- @type table<string, fun(state: MarkerProjectState, marker: EmissionMarker): nil>
local MARKER_PROJECTORS = {
local MARKER_PROJECTORS = { ---@type table<string, fun(state: MarkerProjectState, marker: EmissionMarker): nil>
--- @param state MarkerProjectState
--- @param marker EmissionMarker
--- @return nil
@@ -119,12 +113,9 @@ local MARKER_PROJECTORS = {
--- @return table<string, integer>
--- @return OffsetBranch[]
local function project_markers(markers)
--- @type MarkerProjectState
local state = { labels = {}, branches = {} }
--- @type integer, EmissionMarker
for _, marker in ipairs(markers or {}) do
--- @type (fun(state: MarkerProjectState, marker: EmissionMarker): nil)|nil
local project = MARKER_PROJECTORS[marker.kind]
local state = { labels = {}, branches = {} } ---@type MarkerProjectState
for _, marker in ipairs(markers or {}) do ---@type integer, EmissionMarker
local project = MARKER_PROJECTORS[marker.kind] ---@type (fun(state: MarkerProjectState, marker: EmissionMarker): nil)|nil
if project then project(state, marker) end
end
return state.labels, state.branches
@@ -149,20 +140,16 @@ end
--- @param errors PassFinding[]
--- @return BranchOffset[]
local function compute_offsets(labels, branches, errors)
--- @type BranchOffset[]
local results = {}
--- @type integer, OffsetBranch
for _, br in ipairs(branches) do
--- @type integer|nil
local target = labels[br.target]
local results = {} ---@type BranchOffset[]
for _, br in ipairs(branches) do ---@type integer, OffsetBranch
local target = labels[br.target] ---@type integer|nil
if not target then
errors[#errors + 1] = {
line = br.line or 0,
msg = "Branch target '" .. br.target .. "' has no atom_label (at word " .. br.branch_word .. ")",
}
else
--- @type string|nil
local consuming = br.consuming_encoder
local consuming = br.consuming_encoder ---@type string|nil
if consuming == nil or consuming == "" then
errors[#errors + 1] = {
line = br.line or 0,
@@ -218,20 +205,16 @@ local function emit_atom_offsets(add, atom)
if #atom.offsets == 0 then return end
add("// --- atom: " .. atom.name .. " (" .. atom.total_words .. " words) ---")
add("")
--- @type OffsetConst[]
local consts = {}
--- @type integer, BranchOffset
for _, r in ipairs(atom.offsets) do
local consts = {} ---@type OffsetConst[]
for _, r in ipairs(atom.offsets) do ---@type integer, BranchOffset
consts[#consts + 1] = make_offset_const(r)
end
--- @type integer, OffsetConst
for _, c in ipairs(consts) do
for _, c in ipairs(consts) do ---@type integer, OffsetConst
add("#define " .. pad_right(c.macro_name, OFFSET_MACRO_COL) .. " " .. c.value)
end
add("")
add("enum {")
--- @type integer, OffsetConst
for _, c in ipairs(consts) do
for _, c in ipairs(consts) do ---@type integer, OffsetConst
add(" " .. c.enum_name .. " = " .. c.macro_name .. ",")
end
add("};")
@@ -244,19 +227,16 @@ end
--- @param atoms_data AtomData[]
--- @return string
local function generate_header(dir, sources, atoms_data)
--- @type string
local dir_basename = duffle.basename_no_ext(dir)
local dir_basename = duffle.basename_no_ext(dir) ---@type string
--- @type string[]
local lines = {}
local lines = {} ---@type string[]
--- @param s string
--- @return nil
local function add(s) lines[#lines + 1] = s end
add("// Auto-generated by ps1_meta.lua (passes/offsets.lua) — DO NOT EDIT")
add("// Directory: " .. dir:gsub("/", "\\") .. "\\")
--- @type integer, SourceFile
for _, src in ipairs(sources) do
for _, src in ipairs(sources) do ---@type integer, SourceFile
add("// source: " .. src.path:gsub("/", "\\"))
end
add("#pragma once")
@@ -264,8 +244,7 @@ local function generate_header(dir, sources, atoms_data)
add("#pragma region " .. dir_basename)
add("")
add("")
--- @type integer, AtomData
for _, atom in ipairs(atoms_data) do
for _, atom in ipairs(atoms_data) do ---@type integer, AtomData
emit_atom_offsets(add, atom)
end
add("#pragma endregion " .. dir_basename)
@@ -273,8 +252,7 @@ local function generate_header(dir, sources, atoms_data)
return table.concat(lines, "\n") .. "\n"
end
--- @type OffsetsPass
local M = {}
local M = {} ---@type OffsetsPass
--- (internal) Aggregate atoms from every source in one directory, render the per-directory `offsets.h`.
--- Returns the offsets_h path if a header was written, or nil.
@@ -284,17 +262,14 @@ local M = {}
--- @param errors PassFinding[]
--- @return string|nil
local function process_directory(ctx, dir, sources, errors)
--- @type AtomData[]
local atoms_data = {}
local atoms_data = {} ---@type AtomData[]
--- @param atom AtomEntry
--- @return nil
local function append_atom(atom)
--- @type AtomPaths|nil
local paths = atom and atom.paths
local paths = atom and atom.paths ---@type AtomPaths|nil
if not paths then return end
--- @type table<string, integer>, OffsetBranch[]
local labels, branches = project_markers(paths.markers)
local labels, branches = project_markers(paths.markers) ---@type table<string, integer>, OffsetBranch[]
atoms_data[#atoms_data + 1] = {
name = atom.raw_name or atom.name,
total_words = #(paths.word_events or {}),
@@ -302,19 +277,14 @@ local function process_directory(ctx, dir, sources, errors)
}
end
--- @type integer, SourceFile
for _, src in ipairs(sources) do
--- @type SourceScan
local scan = src.scan or {}
--- @type integer, AtomEntry
for _, atom in ipairs(scan.atoms or {}) do append_atom(atom) end
--- @type integer, AtomEntry
for _, atom in ipairs(scan.raw_atoms or {}) do append_atom(atom) end
for _, src in ipairs(sources) do ---@type integer, SourceFile
local scan = src.scan or {} ---@type SourceScan
for _, atom in ipairs(scan.atoms or {}) do append_atom(atom) end ---@type integer, AtomEntry
for _, atom in ipairs(scan.raw_atoms or {}) do append_atom(atom) end ---@type integer, AtomEntry
end
if #atoms_data == 0 then return nil end
--- @type string
local out_path = dir .. "/gen/offsets.h"
local out_path = dir .. "/gen/offsets.h" ---@type string
duffle.ensure_dir(duffle.dirname(out_path))
duffle.write_file(out_path, generate_header(dir, sources, atoms_data))
return out_path
@@ -326,15 +296,11 @@ end
--- @param ctx PassCtx
--- @return PassResult
function M.run(ctx)
--- @type OffsetOutput[]
local outputs = {}
--- @type PassFinding[]
local errors = {}
--- @type PassFinding[]
local warnings = {}
local outputs = {} ---@type OffsetOutput[]
local errors = {} ---@type PassFinding[]
local warnings = {} ---@type PassFinding[]
--- @type Corpus|nil
local corpus = ctx.shared and ctx.shared.corpus
local corpus = ctx.shared and ctx.shared.corpus ---@type Corpus|nil
if type(corpus) ~= "table" then
error("offsets.run requires ctx.shared.corpus", 0)
end
@@ -343,12 +309,9 @@ function M.run(ctx)
end
-- Per-directory aggregation: every source in the same directory contributes to one `gen/offsets.h`.
--- @type table<string, SourceFile[]>
local sources_by_dir = corpus.sources_by_dir or duffle.group_sources_by_dir(corpus.source_order)
--- @type string, SourceFile[]
for dir, sources in pairs(sources_by_dir) do
--- @type string|nil
local out_path = process_directory(ctx, dir, sources, errors)
local sources_by_dir = corpus.sources_by_dir or duffle.group_sources_by_dir(corpus.source_order) ---@type table<string, SourceFile[]>
for dir, sources in pairs(sources_by_dir) do ---@type string, SourceFile[]
local out_path = process_directory(ctx, dir, sources, errors) ---@type string|nil
if out_path then
outputs[#outputs + 1] = { offsets_h = out_path }
end
+186 -372
View File
@@ -18,16 +18,13 @@
-- Uses `debug.getinfo` to find this file's own directory, so it works both standalone and when require'd from the orchestrator.
-- Bootstrap: Load `duffle_paths.lua` via `debug.getinfo(1, "S").source` (works both standalone + when require'd).
-- duffle_paths.lua sets package.path then returns `require("duffle")` at the bottom, so the dofile value IS the duffle module.
--- @type string
local _bootstrap_dir = debug.getinfo(1, "S").source:match("^@?(.*[/\\])") or "./"
--- @type DuffleExport
local duffle = dofile(_bootstrap_dir .. "../duffle_paths.lua")
local _bootstrap_dir = debug.getinfo(1, "S").source:match("^@?(.*[/\\])") or "./" ---@type string
local duffle = dofile(_bootstrap_dir .. "../duffle_paths.lua") ---@type DuffleExport
-- Load atoms_source_map for the `render_source_map` / `render_provenance` module functions (used by `render_module_atoms_md` to produce `<module>.atoms.md` without re-walking source tokens).
-- The pass itself emits no per-source files anymore; we only consume the two pure renderers here.
-- Defined BEFORE the renderer functions below so their upvalues resolve to this local (not the global `atoms_source_map`, which is nil).
--- @type AtomSourceMapPass
local atoms_source_map = dofile(_bootstrap_dir .. "atoms_source_map.lua")
local atoms_source_map = dofile(_bootstrap_dir .. "atoms_source_map.lua") ---@type AtomSourceMapPass
-- ════════════════════════════════════════════════════════════════════════════
-- Constants
@@ -35,32 +32,22 @@ local atoms_source_map = dofile(_bootstrap_dir .. "atoms_source_map.lua")
-- Section separators used in the rendered text reports.
-- The thin rules are hand-tuned to align with the per-section content width; do not change without also checking the section renderers below.
--- @type string
local RULE_THICK = "========================================================"
--- @type string
local SECTION_HEADER_ATOMS = "── Atoms ────────────────────────────────────────────────"
--- @type string
local SECTION_HEADER_ANNOTS = "── Annotations ──────────────────────────────────────────"
--- @type string
local SECTION_HEADER_BINDS = "── Binds_* structs ──────────────────────────────────────"
--- @type string
local SECTION_HEADER_MACROS = "── Macro word-count declarations ─────────────────────────"
--- @type string
local SECTION_HEADER_ERRORS = "── Errors ──────────────────────────────────────────────"
--- @type string
local SECTION_HEADER_WARNINGS = "── Warnings ────────────────────────────────────────────"
local RULE_THICK = "========================================================" ---@type string
local SECTION_HEADER_ATOMS = "── Atoms ────────────────────────────────────────────────" ---@type string
local SECTION_HEADER_ANNOTS = "── Annotations ──────────────────────────────────────────" ---@type string
local SECTION_HEADER_BINDS = "── Binds_* structs ──────────────────────────────────────" ---@type string
local SECTION_HEADER_MACROS = "── Macro word-count declarations ─────────────────────────" ---@type string
local SECTION_HEADER_ERRORS = "── Errors ──────────────────────────────────────────────" ---@type string
local SECTION_HEADER_WARNINGS = "── Warnings ────────────────────────────────────────────" ---@type string
-- Lua pattern that captures the basename (last path segment) of a forward- or back-slash separated path.
--- @type string
local BASENAME_PATTERN = "([^/\\]+)$"
local BASENAME_PATTERN = "([^/\\]+)$" ---@type string
-- Debug flag name — set to truthy in `_G` to enable verbose logging.
--- @type string
local DEBUG_FLAG = "_DEBUG_REPORT"
local DEBUG_FLAG = "_DEBUG_REPORT" ---@type string
-- Pass identifier for log messages.
--- @type string
local PASS_NAME = "report"
local PASS_NAME = "report" ---@type string
-- ════════════════════════════════════════════════════════════════════════════
-- Type declarations
@@ -245,18 +232,15 @@ end
--- @param all_results ProjectSummaryRow[]
--- @return string
local function render_project_summary(all_results)
--- @type string[]
local lines = {
local lines = { ---@type string[]
"# Project summary",
"> Auto-generated by ps1_meta.lua (passes/report.lua).",
"",
"| module | atoms | annots | binds | macros | findings | errors | warnings | info |",
"|--------|-------|--------|-------|--------|----------|--------|----------|------|",
}
--- @type ProjectSummaryTotals
local totals = { atoms = 0, annots = 0, binds = 0, macros = 0, findings = 0, errors = 0, warnings = 0, info = 0 }
--- @type integer, ProjectSummaryRow
for _, e in ipairs(all_results) do
local totals = { atoms = 0, annots = 0, binds = 0, macros = 0, findings = 0, errors = 0, warnings = 0, info = 0 } ---@type ProjectSummaryTotals
for _, e in ipairs(all_results) do ---@type integer, ProjectSummaryRow
lines[#lines + 1] = string.format("| %s | %d | %d | %d | %d | %d | %d | %d | %d |"
, e.module, e.atoms, e.annots, e.binds, e.macros, e.findings, e.errors, e.warnings, e.info)
totals.atoms = totals.atoms + e.atoms
@@ -281,29 +265,22 @@ end
--- @param wc WordCounts
--- @return string
local function render_module_atoms_md(dir, dir_sources, wc)
--- @type string
local dir_basename = source_basename(dir)
--- @type string[]
local lines = {
local dir_basename = source_basename(dir) ---@type string
local lines = { ---@type string[]
"# " .. dir_basename .. " — atoms (verbose source map)",
"> Per-word call-site + provenance. Auto-generated.",
"",
}
--- @type integer, SourceFile
for _, src in ipairs(dir_sources) do
--- @type string
local src_name = source_basename(src.path)
for _, src in ipairs(dir_sources) do ---@type integer, SourceFile
local src_name = source_basename(src.path) ---@type string
lines[#lines + 1] = "## " .. src_name
lines[#lines + 1] = ""
-- For each atom with a projection, render its sourcemap + provenance.
--- @type AtomEntry[]
local atoms_list = {}
--- @type integer, AtomEntry
for _, atom in ipairs((src.scan or {}).atoms or {}) do
local atoms_list = {} ---@type AtomEntry[]
for _, atom in ipairs((src.scan or {}).atoms or {}) do ---@type integer, AtomEntry
if atom.paths then atoms_list[#atoms_list + 1] = atom end
end
--- @type integer, AtomEntry
for _, atom in ipairs((src.scan or {}).raw_atoms or {}) do
for _, atom in ipairs((src.scan or {}).raw_atoms or {}) do ---@type integer, AtomEntry
if atom.paths then atoms_list[#atoms_list + 1] = atom end
end
if #atoms_list == 0 then
@@ -312,10 +289,8 @@ local function render_module_atoms_md(dir, dir_sources, wc)
else
-- Per-source forward-slash path (same one `emit_atom_stanza` / `emit_provenance_stanza` would derive;
-- computed once per `## <source>` heading and reused by each atom's `WORD N CALL ...` field).
--- @type string
local rel_path = src.path:gsub("\\\\", "/")
--- @type integer, AtomEntry
for _, atom in ipairs(atoms_list) do
local rel_path = src.path:gsub("\\\\", "/") ---@type string
for _, atom in ipairs(atoms_list) do ---@type integer, AtomEntry
lines[#lines + 1] = string.format(
"### atom: %s (line %d, %d words)",
atom.name, atom.line or 0, #((atom.paths or {}).word_events or {}))
@@ -342,18 +317,15 @@ end
--- @param atom AtomEntry
--- @return integer
local function decl_words(atom)
--- @type AtomPaths
local p = atom.paths or {}
local p = atom.paths or {} ---@type AtomPaths
return #(p.word_events or {})
end
--- @param decls AtomEntry[]|nil
--- @return KindCounts
local function count_kinds(decls)
--- @type KindCounts
local n = { atom = 0, atom_proc = 0, comp_bare = 0, comp_proc = 0 }
--- @type integer, AtomEntry
for _, a in ipairs(decls or {}) do
local n = { atom = 0, atom_proc = 0, comp_bare = 0, comp_proc = 0 } ---@type KindCounts
for _, a in ipairs(decls or {}) do ---@type integer, AtomEntry
if n[a.kind] ~= nil then n[a.kind] = n[a.kind] + 1 end
end
return n
@@ -369,10 +341,8 @@ end
--- @param view ModuleView
--- @return table<string, boolean>
local function decl_names(view)
--- @type table<string, boolean> -- bag: atom name -> true
local names = {}
--- @type integer, AtomEntry
for _, a in ipairs(view.decls or {}) do
local names = {} ---@type table<string, boolean> -- bag: atom name -> true
for _, a in ipairs(view.decls or {}) do ---@type integer, AtomEntry
if a.name then names[a.name] = true end
end
return names
@@ -383,15 +353,12 @@ end
--- @return boolean
local function path_in_module(path, view)
if type(path) ~= "string" or path == "" then return false end
--- @type string
local norm = path:gsub("\\", "/")
--- @type string
local dir = (view.dir or ""):gsub("\\", "/")
local norm = path:gsub("\\", "/") ---@type string
local dir = (view.dir or ""):gsub("\\", "/") ---@type string
if dir ~= "" and (norm == dir or norm:sub(1, #dir + 1) == dir .. "/") then
return true
end
--- @type integer, SourceFile
for _, src in ipairs(view.sources or {}) do
for _, src in ipairs(view.sources or {}) do ---@type integer, SourceFile
if (src.path or ""):gsub("\\", "/") == norm then return true end
end
return false
@@ -402,26 +369,18 @@ end
--- @param corpus Corpus
--- @return ModuleView
local function build_module_view(dir, dir_sources, corpus)
--- @type AtomEntry[]
local decls = {}
--- @type integer, SourceFile
for _, src in ipairs(dir_sources or {}) do
--- @type integer, AtomEntry
for _, a in ipairs((src.scan and src.scan.atoms) or {}) do
local decls = {} ---@type AtomEntry[]
for _, src in ipairs(dir_sources or {}) do ---@type integer, SourceFile
for _, a in ipairs((src.scan and src.scan.atoms) or {}) do ---@type integer, AtomEntry
if not a.source_path then a.source_path = src.path end
decls[#decls + 1] = a
end
end
--- @type string
local dir_basename = source_basename(dir)
--- @type AtomAnalysis
local sa = (corpus.static_analysis_results or {})[dir_basename] or {}
--- @type RegUseSchema[]
local schemas = {}
--- @type string, RegUseSchema
for name, schema in pairs(corpus.reg_use_schemas or {}) do
--- @type integer, AtomEntry
for _, a in ipairs(decls) do
local dir_basename = source_basename(dir) ---@type string
local sa = (corpus.static_analysis_results or {})[dir_basename] or {} ---@type AtomAnalysis
local schemas = {} ---@type RegUseSchema[]
for name, schema in pairs(corpus.reg_use_schemas or {}) do ---@type string, RegUseSchema
for _, a in ipairs(decls) do ---@type integer, AtomEntry
if a.reg_use_schema_name == name then
schemas[#schemas + 1] = schema
break
@@ -445,10 +404,8 @@ local function render_section_declarations(add, view)
if #view.decls == 0 then add("_(none)_"); add(""); return end
add("| kind | name | source | line | words | min | max | branches | paths |")
add("|------|------|--------|------|-------|-----|-----|----------|-------|")
--- @type integer, AtomEntry
for _, a in ipairs(view.decls) do
--- @type AtomPaths
local p = a.paths or {}
for _, a in ipairs(view.decls) do ---@type integer, AtomEntry
local p = a.paths or {} ---@type AtomPaths
add(string.format("| %s | %s | %s | %d | %d | %s | %s | %s | %s |",
a.kind or "?",
a.name or "?",
@@ -467,17 +424,12 @@ end
--- @param view ModuleView
--- @return nil
local function render_section_components(add, view)
--- @type ComponentReportRow[]
local rows = {}
--- @type table<string, ComponentBodyEntry>
local index = (view.corpus and view.corpus.component_body_index) or {}
--- @type integer, AtomEntry
for _, a in ipairs(view.decls) do
local rows = {} ---@type ComponentReportRow[]
local index = (view.corpus and view.corpus.component_body_index) or {} ---@type table<string, ComponentBodyEntry>
for _, a in ipairs(view.decls) do ---@type integer, AtomEntry
if a.kind == "comp_bare" or a.kind == "comp_proc" then
--- @type ComponentBodyEntry
local idx = index[a.name] or {}
--- @type string[]
local args = idx.arg_names or {}
local idx = index[a.name] or {} ---@type ComponentBodyEntry
local args = idx.arg_names or {} ---@type string[]
rows[#rows + 1] = {
name = a.name,
kind = a.kind,
@@ -490,8 +442,7 @@ local function render_section_components(add, view)
if #rows == 0 then add("_(none)_"); add(""); return end
add("| name | kind | arg_names | words | map |")
add("|------|------|-----------|-------|-----|")
--- @type integer, ComponentReportRow
for _, r in ipairs(rows) do
for _, r in ipairs(rows) do ---@type integer, ComponentReportRow
add(string.format("| %s | %s | %s | %d | %s |",
r.name, r.kind, r.args ~= "" and r.args or "", r.words, r.map))
end
@@ -502,38 +453,28 @@ end
--- @param view ModuleView
--- @return nil
local function render_section_reguse(add, view)
--- @type boolean
local wrote = false
--- @type integer, RegUseSchema
for _, schema in ipairs(view.schemas or {}) do
local wrote = false ---@type boolean
for _, schema in ipairs(view.schemas or {}) do ---@type integer, RegUseSchema
wrote = true
add(string.format("### %s", schema.name or "?"))
--- @type integer, RegUseSlot
for _, slot in ipairs(schema.slots or {}) do
--- @type string
local aliases = table.concat(slot.aliases or { slot.name }, ", ")
--- @type string
local ro = slot.readonly and " readonly" or ""
for _, slot in ipairs(schema.slots or {}) do ---@type integer, RegUseSlot
local aliases = table.concat(slot.aliases or { slot.name }, ", ") ---@type string
local ro = slot.readonly and " readonly" or "" ---@type string
add(string.format("- slot `%s` aliases %s%s", slot.name, aliases, ro))
end
--- @type integer, AtomEntry
for _, a in ipairs(view.decls) do
for _, a in ipairs(view.decls) do ---@type integer, AtomEntry
if a.reg_use_schema_name == schema.name then
add(string.format("- bound `%s` param `%s`", a.name, a.reg_use_param_name or "?"))
end
end
add("")
end
--- @type table<string, boolean> -- bag: schema name -> true
local bound = {}
--- @type integer, RegUseSchema
for _, schema in ipairs(view.schemas or {}) do
local bound = {} ---@type table<string, boolean> -- bag: schema name -> true
for _, schema in ipairs(view.schemas or {}) do ---@type integer, RegUseSchema
if schema.name then bound[schema.name] = true end
end
--- @type RegUseError[]
local errors = {}
--- @type integer, RegUseError
for _, err in ipairs((view.corpus and view.corpus.reg_use_errors) or {}) do
local errors = {} ---@type RegUseError[]
for _, err in ipairs((view.corpus and view.corpus.reg_use_errors) or {}) do ---@type integer, RegUseError
if bound[err.schema_name] or path_in_module(err.source_file, view) then
errors[#errors + 1] = err
end
@@ -541,8 +482,7 @@ local function render_section_reguse(add, view)
if #errors > 0 then
wrote = true
add("### parse errors")
--- @type integer, RegUseError
for _, err in ipairs(errors) do
for _, err in ipairs(errors) do ---@type integer, RegUseError
add(string.format("- `%s` %s", err.kind or "?", err.schema_name or ""))
end
add("")
@@ -554,12 +494,9 @@ end
--- @param view ModuleView
--- @return nil
local function render_section_annotations(add, view)
--- @type AnnotReportRow[]
local rows = {}
--- @type integer, SourceFile
for _, src in ipairs(view.sources) do
--- @type integer, AtomInfoEntry
for _, info in ipairs((src.scan and src.scan.atom_infos) or {}) do
local rows = {} ---@type AnnotReportRow[]
for _, src in ipairs(view.sources) do ---@type integer, SourceFile
for _, info in ipairs((src.scan and src.scan.atom_infos) or {}) do ---@type integer, AtomInfoEntry
rows[#rows + 1] = {
source = source_basename(src.path),
line = info.info_line or 0,
@@ -574,8 +511,7 @@ local function render_section_annotations(add, view)
if #rows == 0 then add("_(none)_"); add(""); return end
add("| source | line | name | binds | reads | writes | phase |")
add("|--------|------|------|-------|-------|--------|-------|")
--- @type integer, AnnotReportRow
for _, r in ipairs(rows) do
for _, r in ipairs(rows) do ---@type integer, AnnotReportRow
add(string.format("| %s | %d | %s | %s | %s | %s | %s |",
r.source, r.line, r.name, r.binds, r.reads, r.writes, r.phase))
end
@@ -586,12 +522,9 @@ end
--- @param view ModuleView
--- @return nil
local function render_section_component_annotations(add, view)
--- @type CompAnnotReportRow[]
local rows = {}
--- @type integer, SourceFile
for _, src in ipairs(view.sources) do
--- @type integer, AtomInfoEntry
for _, info in ipairs((src.scan and src.scan.component_atom_infos) or {}) do
local rows = {} ---@type CompAnnotReportRow[]
for _, src in ipairs(view.sources) do ---@type integer, SourceFile
for _, info in ipairs((src.scan and src.scan.component_atom_infos) or {}) do ---@type integer, AtomInfoEntry
rows[#rows + 1] = {
source = source_basename(src.path),
line = info.info_line or 0,
@@ -604,8 +537,7 @@ local function render_section_component_annotations(add, view)
if #rows == 0 then add("_(none)_"); add(""); return end
add("| source | line | name | reads | writes |")
add("|--------|------|------|-------|--------|")
--- @type integer, CompAnnotReportRow
for _, r in ipairs(rows) do
for _, r in ipairs(rows) do ---@type integer, CompAnnotReportRow
add(string.format("| %s | %d | %s | %s | %s |",
r.source, r.line, r.name, r.reads, r.writes))
end
@@ -616,17 +548,13 @@ end
--- @param view ModuleView
--- @return nil
local function render_section_binds(add, view)
--- @type boolean
local wrote = false
--- @type integer, SourceFile
for _, src in ipairs(view.sources) do
--- @type integer, BindsEntry
for _, b in ipairs((src.scan and src.scan.binds) or {}) do
local wrote = false ---@type boolean
for _, src in ipairs(view.sources) do ---@type integer, SourceFile
for _, b in ipairs((src.scan and src.scan.binds) or {}) do ---@type integer, BindsEntry
wrote = true
add(string.format("### %s (%s:%s, %s bytes)",
b.name, source_basename(src.path), tostring(b.line or 0), tostring(b.bytes or "")))
--- @type integer, TypeField
for _, f in ipairs(b.fields or {}) do
for _, f in ipairs(b.fields or {}) do ---@type integer, TypeField
add(string.format("- `+%s %s`", tostring(f.offset or "?"), f.name or "?"))
end
add("")
@@ -639,18 +567,12 @@ end
--- @param view ModuleView
--- @return nil
local function render_section_phases(add, view)
--- @type Corpus
local corpus = view.corpus or {}
--- @type table<string, boolean>
local names = decl_names(view)
--- @type boolean
local wrote = false
--- @type string, AtomPhaseGroup
for phase, entry in pairs(corpus.atom_phases or {}) do
--- @type string[]
local here = {}
--- @type integer, string
for _, atom_name in ipairs(entry.atoms or {}) do
local corpus = view.corpus or {} ---@type Corpus
local names = decl_names(view) ---@type table<string, boolean>
local wrote = false ---@type boolean
for phase, entry in pairs(corpus.atom_phases or {}) do ---@type string, AtomPhaseGroup
local here = {} ---@type string[]
for _, atom_name in ipairs(entry.atoms or {}) do ---@type integer, string
if names[atom_name] then here[#here + 1] = atom_name end
end
if #here > 0 then
@@ -658,15 +580,13 @@ local function render_section_phases(add, view)
add(string.format("- phase `%s`: %s", phase, table.concat(here, ", ")))
end
end
--- @type string, AtomViewEntry
for name, entry in pairs(corpus.atom_views or {}) do
for name, entry in pairs(corpus.atom_views or {}) do ---@type string, AtomViewEntry
if names[name] then
wrote = true
add(string.format("- view `%s` binds `%s`", name, entry.binds_name or ""))
end
end
--- @type string, AtomCtxEntry
for name, entry in pairs(corpus.atom_ctxs or {}) do
for name, entry in pairs(corpus.atom_ctxs or {}) do ---@type string, AtomCtxEntry
if names[name] then
wrote = true
add(string.format("- ctx `%s` rbind `%s`", name, entry.rbind_atom or ""))
@@ -680,14 +600,10 @@ end
--- @param view ModuleView
--- @return nil
local function render_section_aliases(add, view)
--- @type string[]
local names = {}
--- @type table<string, AliasEntry>
local seen = {}
--- @type integer, SourceFile
for _, src in ipairs(view.sources or {}) do
--- @type string, AliasEntry
for name, entry in pairs((src.scan and src.scan.register_alias_registry) or {}) do
local names = {} ---@type string[]
local seen = {} ---@type table<string, AliasEntry>
for _, src in ipairs(view.sources or {}) do ---@type integer, SourceFile
for name, entry in pairs((src.scan and src.scan.register_alias_registry) or {}) do ---@type string, AliasEntry
if not seen[name] then
seen[name] = entry
names[#names + 1] = name
@@ -698,10 +614,8 @@ local function render_section_aliases(add, view)
if #names == 0 then add("_(none)_"); add(""); return end
add("| alias | type |")
add("|-------|------|")
--- @type integer, string
for _, name in ipairs(names) do
--- @type AliasEntry|nil
local e = seen[name]
for _, name in ipairs(names) do ---@type integer, string
local e = seen[name] ---@type AliasEntry|nil
add(string.format("| %s | %s |", name, (e and e.default_type) or ""))
end
add("")
@@ -711,40 +625,30 @@ end
--- @param view ModuleView
--- @return nil
local function render_section_autoreg(add, view)
--- @type table<string, boolean>
local allowed = decl_names(view)
--- @type string, AtomPhaseGroup
for phase, entry in pairs((view.corpus and view.corpus.atom_phases) or {}) do
--- @type integer, string
for _, atom_name in ipairs(entry.atoms or {}) do
local allowed = decl_names(view) ---@type table<string, boolean>
for phase, entry in pairs((view.corpus and view.corpus.atom_phases) or {}) do ---@type string, AtomPhaseGroup
for _, atom_name in ipairs(entry.atoms or {}) do ---@type integer, string
if allowed[atom_name] then allowed[phase] = true end
end
end
--- @type boolean
local wrote = false
--- @type table<string, boolean> -- bag: label\\0scope -> already dumped
local seen = {}
local wrote = false ---@type boolean
local seen = {} ---@type table<string, boolean> -- bag: label\\0scope -> already dumped
--- @param label string
--- @param table_map table<string, GprAllocMap>|nil
--- @return nil
local function dump(label, table_map)
--- @type string[]
local scopes = {}
--- @type string
for scope in pairs(table_map or {}) do
local scopes = {} ---@type string[]
for scope in pairs(table_map or {}) do ---@type string
if allowed[scope] and not seen[label .. "\0" .. scope] then
scopes[#scopes + 1] = scope
end
end
table.sort(scopes)
--- @type integer, string
for _, scope in ipairs(scopes) do
for _, scope in ipairs(scopes) do ---@type integer, string
seen[label .. "\0" .. scope] = true
wrote = true
--- @type string[]
local syms = {}
--- @type string, string
for sym, gpr in pairs(table_map[scope] or {}) do
local syms = {} ---@type string[]
for sym, gpr in pairs(table_map[scope] or {}) do ---@type string, string
if type(gpr) == "string" and gpr ~= sym then
syms[#syms + 1] = string.format("%s → %s", sym, gpr)
else
@@ -755,12 +659,10 @@ local function render_section_autoreg(add, view)
add(string.format("- %s `%s`: %s", label, scope, table.concat(syms, ", ")))
end
end
--- @type Corpus
local corpus = view.corpus or {}
local corpus = view.corpus or {} ---@type Corpus
dump("atom", corpus.atom_auto_regs)
dump("phase", corpus.phase_auto_regs)
--- @type integer, SourceFile
for _, src in ipairs(view.sources or {}) do
for _, src in ipairs(view.sources or {}) do ---@type integer, SourceFile
dump("atom", src.scan and src.scan.atom_auto_regs)
dump("phase", src.scan and src.scan.phase_auto_regs)
end
@@ -772,25 +674,18 @@ end
--- @param view ModuleView
--- @return nil
local function render_section_collisions(add, view)
--- @type CorpusCollision[]
local rows = {}
--- @type integer, CorpusCollision
for _, c in ipairs((view.corpus and view.corpus.collisions) or {}) do
--- @type CollisionSite
local first = c.first_site or {}
--- @type CollisionSite
local other = c.conflicting_site or {}
local rows = {} ---@type CorpusCollision[]
for _, c in ipairs((view.corpus and view.corpus.collisions) or {}) do ---@type integer, CorpusCollision
local first = c.first_site or {} ---@type CollisionSite
local other = c.conflicting_site or {} ---@type CollisionSite
if path_in_module(first.path, view) or path_in_module(other.path, view) then
rows[#rows + 1] = c
end
end
if #rows == 0 then add("_(none)_"); add(""); return end
--- @type integer, CorpusCollision
for _, c in ipairs(rows) do
--- @type CollisionSite
local first = c.first_site or {}
--- @type CollisionSite
local other = c.conflicting_site or {}
for _, c in ipairs(rows) do ---@type integer, CorpusCollision
local first = c.first_site or {} ---@type CollisionSite
local other = c.conflicting_site or {} ---@type CollisionSite
add(string.format("- `%s` `%s` first %s:%s conflict %s:%s",
c.kind or "?", c.name or "?",
tostring(first.path or "?"), tostring(first.line or "?"),
@@ -803,29 +698,22 @@ end
--- @param view ModuleView
--- @return nil
local function render_section_findings(add, view)
--- @type table<string, CheckFinding[]>
local by_atom = {}
--- @type integer, CheckFinding
for _, f in ipairs(view.findings or {}) do
--- @type string
local key = f.atom or "?"
local by_atom = {} ---@type table<string, CheckFinding[]>
for _, f in ipairs(view.findings or {}) do ---@type integer, CheckFinding
local key = f.atom or "?" ---@type string
by_atom[key] = by_atom[key] or {}
by_atom[key][#by_atom[key] + 1] = f
end
if next(by_atom) == nil then add("_(none)_"); add(""); return end
--- @type table<string, boolean> -- bag: atom name already emitted
local seen = {}
local seen = {} ---@type table<string, boolean> -- bag: atom name already emitted
--- @param name string
--- @param fs CheckFinding[]
--- @return nil
local function emit(name, fs)
add("### " .. name)
--- @type integer, CheckFinding
for _, f in ipairs(fs) do
--- @type string
local msg = f.msg or ""
--- @type string|nil
local slot = slot_suffix(f.gpr_key or f.producer_destination)
for _, f in ipairs(fs) do ---@type integer, CheckFinding
local msg = f.msg or "" ---@type string
local slot = slot_suffix(f.gpr_key or f.producer_destination) ---@type string|nil
if slot and not msg:find("(slot ", 1, true) then
msg = msg .. " (slot " .. slot .. ")"
end
@@ -833,45 +721,34 @@ local function render_section_findings(add, view)
end
add("")
end
--- @type integer, AtomEntry
for _, a in ipairs(view.decls) do
for _, a in ipairs(view.decls) do ---@type integer, AtomEntry
if by_atom[a.name] then
seen[a.name] = true
emit(a.name, by_atom[a.name])
end
end
--- @type string[]
local leftovers = {}
--- @type string
for name in pairs(by_atom) do
local leftovers = {} ---@type string[]
for name in pairs(by_atom) do ---@type string
if not seen[name] then leftovers[#leftovers + 1] = name end
end
table.sort(leftovers)
--- @type integer, string
for _, name in ipairs(leftovers) do emit(name, by_atom[name]) end
for _, name in ipairs(leftovers) do emit(name, by_atom[name]) end ---@type integer, string
end
--- @param add fun(s: string): nil
--- @param view ModuleView
--- @return nil
local function render_section_relations(add, view)
--- @type boolean
local wrote = false
--- @type integer, AtomEntry
for _, a in ipairs(view.decls) do
--- @type AtomRelation[]
local rels = (a.paths and a.paths.relations) or {}
local wrote = false ---@type boolean
for _, a in ipairs(view.decls) do ---@type integer, AtomEntry
local rels = (a.paths and a.paths.relations) or {} ---@type AtomRelation[]
if #rels > 0 then
wrote = true
add("### " .. a.name)
--- @type integer, AtomRelation
for _, rel in ipairs(rels) do
--- @type string
local dest = rel.destination or rel.producer_destination or ""
--- @type string|nil
local slot = slot_suffix(dest)
--- @type string
local dest_s = tostring(dest)
for _, rel in ipairs(rels) do ---@type integer, AtomRelation
local dest = rel.destination or rel.producer_destination or "" ---@type string
local slot = slot_suffix(dest) ---@type string|nil
local dest_s = tostring(dest) ---@type string
if slot then dest_s = dest_s .. " (slot " .. slot .. ")" end
add(string.format("- `%s` words %s → %s dest %s",
rel.semantic or "?",
@@ -885,13 +762,11 @@ local function render_section_relations(add, view)
if not wrote then add("_(none)_"); add("") end
end
--- @type table<string, boolean> -- bag: GPR key hidden unless an encoder wrote it
local HIDDEN_UNLESS_WRITTEN = {
local HIDDEN_UNLESS_WRITTEN = { ---@type table<string, boolean> -- bag: GPR key hidden unless an encoder wrote it
R_AT = true, R_TapePtr = true, R_AtomJmp = true,
}
--- @type table<string, boolean> -- bag: physical GPR alias -> true
local PHYSICAL_GPR = {
local PHYSICAL_GPR = { ---@type table<string, boolean> -- bag: physical GPR alias -> true
R_T0 = true, R_T1 = true, R_T2 = true, R_T3 = true,
R_T4 = true, R_T5 = true, R_T6 = true, R_T7 = true,
R_V0 = true, R_V1 = true,
@@ -901,10 +776,8 @@ local PHYSICAL_GPR = {
--- @param key string
--- @return boolean
local function encoder_wrote_key(atom, key)
--- @type integer, WordEvent
for _, ev in ipairs((atom.paths and atom.paths.word_events) or {}) do
--- @type integer|string, string
for _, dest in pairs(ev.gpr_keys or {}) do
for _, ev in ipairs((atom.paths and atom.paths.word_events) or {}) do ---@type integer, WordEvent
for _, dest in pairs(ev.gpr_keys or {}) do ---@type integer|string, string
if dest == key then return true end
end
end
@@ -915,11 +788,9 @@ end
--- @param atom AtomEntry
--- @return string
local function written_name_for(key, atom)
--- @type string|nil
local slot = key:match("^reguse:.+:(.+)$")
local slot = key:match("^reguse:.+:(.+)$") ---@type string|nil
if slot then
--- @type string|nil
local param = atom.reg_use_param_name
local param = atom.reg_use_param_name ---@type string|nil
if param and param ~= "" then return param .. "." .. slot end
return slot
end
@@ -931,21 +802,15 @@ end
--- @param view ModuleView
--- @return string
local function aliases_for_key(key, atom, view)
--- @type string|nil
local slot = key:match("^reguse:.+:(.+)$")
local slot = key:match("^reguse:.+:(.+)$") ---@type string|nil
if not slot then return "" end
--- @type string|nil
local schema_name = atom.reg_use_schema_name
--- @type RegUseSchema|nil
local schema = view.corpus and view.corpus.reg_use_schemas and view.corpus.reg_use_schemas[schema_name]
local schema_name = atom.reg_use_schema_name ---@type string|nil
local schema = view.corpus and view.corpus.reg_use_schemas and view.corpus.reg_use_schemas[schema_name] ---@type RegUseSchema|nil
if not schema then return "" end
--- @type integer, RegUseSlot
for _, s in ipairs(schema.slots or {}) do
for _, s in ipairs(schema.slots or {}) do ---@type integer, RegUseSlot
if s.name == slot then
--- @type string[]
local names = {}
--- @type integer, string
for _, alias in ipairs(s.aliases or {}) do
local names = {} ---@type string[]
for _, alias in ipairs(s.aliases or {}) do ---@type integer, string
if alias ~= slot then names[#names + 1] = alias end
end
if #names == 0 then
@@ -964,25 +829,19 @@ end
--- @return string
local function physical_for_key(key, atom, view)
if PHYSICAL_GPR[key] then return key end
--- @type Corpus
local corpus = view.corpus or {}
--- @type AliasEntry|string|nil
local alias = (corpus.register_alias_registry or {})[key]
local corpus = view.corpus or {} ---@type Corpus
local alias = (corpus.register_alias_registry or {})[key] ---@type AliasEntry|string|nil
if type(alias) == "table" then
--- @type string|nil
local phys = alias.physical or alias.gpr or alias.code_name
local phys = alias.physical or alias.gpr or alias.code_name ---@type string|nil
if type(phys) == "string" and PHYSICAL_GPR[phys] then return phys end
if type(alias.name) == "string" and PHYSICAL_GPR[alias.name] then return alias.name end
elseif type(alias) == "string" and PHYSICAL_GPR[alias] then
return alias
end
--- @type GprAllocMap|nil
local atom_map = (corpus.atom_auto_regs or {})[atom.name]
local atom_map = (corpus.atom_auto_regs or {})[atom.name] ---@type GprAllocMap|nil
if type(atom_map) == "table" then
--- @type string
local slot = key:match("^reguse:.+:(.+)$") or key
--- @type string|nil
local bound = atom_map[slot] or atom_map["R_" .. slot]
local slot = key:match("^reguse:.+:(.+)$") or key ---@type string
local bound = atom_map[slot] or atom_map["R_" .. slot] ---@type string|nil
if type(bound) == "string" and PHYSICAL_GPR[bound] then return bound end
end
return ""
@@ -992,21 +851,15 @@ end
--- @param atom AtomEntry
--- @return string
local function last_relation_for(key, atom)
--- @type AtomRelation|nil
local last = nil
--- @type integer, AtomRelation
for _, rel in ipairs((atom.paths and atom.paths.relations) or {}) do
--- @type string|nil
local dest = rel.destination or rel.producer_destination
local last = nil ---@type AtomRelation|nil
for _, rel in ipairs((atom.paths and atom.paths.relations) or {}) do ---@type integer, AtomRelation
local dest = rel.destination or rel.producer_destination ---@type string|nil
if dest == key then last = rel end
end
if not last then return "" end
--- @type string
local sem = last.semantic or "?"
--- @type integer|nil
local a = last.producer_word
--- @type integer|nil
local b = last.consumer_word
local sem = last.semantic or "?" ---@type string
local a = last.producer_word ---@type integer|nil
local b = last.consumer_word ---@type integer|nil
if a and b then return string.format("%s w%s→%s", sem, tostring(a), tostring(b)) end
return sem
end
@@ -1015,16 +868,11 @@ end
--- @param view ModuleView
--- @return nil
local function render_section_forward(add, view)
--- @type boolean
local wrote = false
--- @type integer, AtomEntry
for _, a in ipairs(view.decls) do
--- @type table<string, GprLatticeSlot>|nil
local gpr = a.paths and a.paths.forward_state and a.paths.forward_state.gpr_values
--- @type string[]
local keys = {}
--- @type string
for k in pairs(gpr or {}) do
local wrote = false ---@type boolean
for _, a in ipairs(view.decls) do ---@type integer, AtomEntry
local gpr = a.paths and a.paths.forward_state and a.paths.forward_state.gpr_values ---@type table<string, GprLatticeSlot>|nil
local keys = {} ---@type string[]
for k in pairs(gpr or {}) do ---@type string
if k == "R_0" then
-- hidden
elseif HIDDEN_UNLESS_WRITTEN[k] and not encoder_wrote_key(a, k) then
@@ -1039,12 +887,9 @@ local function render_section_forward(add, view)
add("| written | aliases | physical | lattice | last relation |")
add("|---|---|---|---|---|")
table.sort(keys)
--- @type integer, string
for _, k in ipairs(keys) do
--- @type GprLatticeSlot|nil
local slot = gpr[k]
--- @type string
local lattice = ""
for _, k in ipairs(keys) do ---@type integer, string
local slot = gpr[k] ---@type GprLatticeSlot|nil
local lattice = "" ---@type string
if slot and slot.kind == "constant" then
lattice = tostring(slot.value)
end
@@ -1061,8 +906,7 @@ local function render_section_forward(add, view)
if not wrote then add("_(none)_"); add("") end
end
--- @type SectionRenderer[]
local SECTION_RENDERERS = {
local SECTION_RENDERERS = { ---@type SectionRenderer[]
{ header = "## Declarations", render = render_section_declarations },
{ header = "## Components", render = render_section_components },
{ header = "## RegUse schemas", render = render_section_reguse },
@@ -1083,10 +927,8 @@ local SECTION_RENDERERS = {
--- @param view ModuleView
--- @return string
local function render_module_meta_report(view)
--- @type string
local dir_basename = source_basename(view.dir)
--- @type string[]
local lines = {
local dir_basename = source_basename(view.dir) ---@type string
local lines = { ---@type string[]
"# " .. dir_basename .. " — atom meta report",
"> Auto-generated by ps1_meta.lua (passes/report.lua). Do not edit.",
"",
@@ -1095,20 +937,15 @@ local function render_module_meta_report(view)
--- @return nil
local function add(s) lines[#lines + 1] = s end
--- @type KindCounts
local kinds = count_kinds(view.decls)
--- @type integer, integer, integer
local n_annot, n_binds, n_macros = 0, 0, 0
--- @type integer, SourceFile
for _, src in ipairs(view.sources) do
local kinds = count_kinds(view.decls) ---@type KindCounts
local n_annot, n_binds, n_macros = 0, 0, 0 ---@type integer, integer, integer
for _, src in ipairs(view.sources) do ---@type integer, SourceFile
n_annot = n_annot + #((src.scan and src.scan.atom_infos) or {})
n_binds = n_binds + #((src.scan and src.scan.binds) or {})
n_macros = n_macros + #((src.scan and src.scan.macros) or {})
end
--- @type integer, integer, integer
local n_err, n_warn, n_info = 0, 0, 0
--- @type integer, CheckFinding
for _, f in ipairs(view.findings or {}) do
local n_err, n_warn, n_info = 0, 0, 0 ---@type integer, integer, integer
for _, f in ipairs(view.findings or {}) do ---@type integer, CheckFinding
if f.kind == "error" then n_err = n_err + 1
elseif f.kind == "warning" then n_warn = n_warn + 1
else n_info = n_info + 1
@@ -1128,12 +965,10 @@ local function render_module_meta_report(view)
add("")
add("## Sources"); add("")
--- @type integer, SourceFile
for _, s in ipairs(view.sources) do add("- `" .. s.path .. "`") end
for _, s in ipairs(view.sources) do add("- `" .. s.path .. "`") end ---@type integer, SourceFile
add("")
--- @type integer, SectionRenderer
for _, row in ipairs(SECTION_RENDERERS) do
for _, row in ipairs(SECTION_RENDERERS) do ---@type integer, SectionRenderer
add(row.header); add("")
row.render(add, view)
end
@@ -1147,8 +982,7 @@ end
-- `once = true` means render once at the project level (not per-module).
-- `basename(dir_basename)` yields the file's basename for that kind.
-- `gather(ctx, dir, dir_sources [, all_modules])` returns the rendered string.
--- @type ReportRenderer[]
local REPORT_RENDERERS = {
local REPORT_RENDERERS = { ---@type ReportRenderer[]
{
name = "atom_meta_report",
ext = "md",
@@ -1161,8 +995,7 @@ local REPORT_RENDERERS = {
--- @param dir_sources SourceFile[]
--- @return string
gather = function(ctx, dir, dir_sources)
--- @type Corpus
local corpus = ctx.shared.corpus
local corpus = ctx.shared.corpus ---@type Corpus
return render_module_meta_report(build_module_view(dir, dir_sources, corpus))
end,
},
@@ -1204,20 +1037,16 @@ local REPORT_RENDERERS = {
-- M — public pass surface
-- ════════════════════════════════════════════════════════════════════════════
--- @type ReportPass
local M = {}
local M = {} ---@type ReportPass
--- Run the report pass. Emits 1 `atom_meta_report.summary.md` per build + 2 `atom_meta_report.md` + 2 `atoms.md` files per module (duffle + gte_hello).
--- Reads `corpus.static_analysis_results` (added in Phase 1) to populate per-module findings without re-running validate().
--- @param ctx PassCtx
--- @return PassResult
function M.run(ctx)
--- @type PassOutputEntry[]
local outputs = {}
--- @type Corpus|nil
local corpus = ctx.shared and ctx.shared.corpus
--- @type table<string, SourceFile[]>
local by_dir = (corpus and corpus.sources_by_dir) or {}
local outputs = {} ---@type PassOutputEntry[]
local corpus = ctx.shared and ctx.shared.corpus ---@type Corpus|nil
local by_dir = (corpus and corpus.sources_by_dir) or {} ---@type table<string, SourceFile[]>
-- `out_path_root`: when the conventional `out_root` is `build/gen` (any spelling — relative, absolute, separator variants).
-- Write the md files to `build/` (parent of `gen/`) instead of nested under `gen/`.
@@ -1228,49 +1057,37 @@ function M.run(ctx)
return type(p) == "string" and (p:match("[/\\]gen[/\\]?$") ~= nil
or p == "build/gen" or p == "build\\gen")
end
--- @type string
local out_root_effective = ends_with_gen(ctx.out_root)
local out_root_effective = ends_with_gen(ctx.out_root) ---@type string
and ctx.out_root:gsub("[/\\]gen[/\\]?$", "")
or ctx.out_root
duffle.ensure_dir(out_root_effective)
-- Aggregator for the project-wide `once = true` summary renderer.
--- @type ProjectSummaryRow[]
local all_modules = {}
local all_modules = {} ---@type ProjectSummaryRow[]
--- @type string, SourceFile[]
for dir, dir_sources in pairs(by_dir) do
--- @type string
local dir_basename = dir:match("([^/\\]+)$") or dir
for dir, dir_sources in pairs(by_dir) do ---@type string, SourceFile[]
local dir_basename = dir:match("([^/\\]+)$") or dir ---@type string
-- Per-renderer dispatch for the per-module renderers (once = false).
--- @type integer, ReportRenderer
for _, renderer in ipairs(REPORT_RENDERERS) do
for _, renderer in ipairs(REPORT_RENDERERS) do ---@type integer, ReportRenderer
if not renderer.once then
--- @type string
local body = renderer.gather(ctx, dir, dir_sources)
--- @type string
local out_path = out_root_effective .. "/" .. renderer.basename(dir_basename) .. "." .. renderer.ext
local body = renderer.gather(ctx, dir, dir_sources) ---@type string
local out_path = out_root_effective .. "/" .. renderer.basename(dir_basename) .. "." .. renderer.ext ---@type string
duffle.write_file(out_path, body)
outputs[#outputs + 1] = { kind = renderer.name, path = out_path }
end
end
--- @type ModuleView
local view = build_module_view(dir, dir_sources, corpus)
--- @type integer, integer, integer
local n_annot, n_binds, n_macros = 0, 0, 0
--- @type integer, SourceFile
for _, src in ipairs(dir_sources) do
local view = build_module_view(dir, dir_sources, corpus) ---@type ModuleView
local n_annot, n_binds, n_macros = 0, 0, 0 ---@type integer, integer, integer
for _, src in ipairs(dir_sources) do ---@type integer, SourceFile
n_annot = n_annot + #((src.scan and src.scan.atom_infos) or {})
n_binds = n_binds + #((src.scan and src.scan.binds) or {})
n_macros = n_macros + #((src.scan and src.scan.macros) or {})
end
--- @type integer, integer, integer
local n_err, n_warn, n_info = 0, 0, 0
--- @type integer, CheckFinding
for _, f in ipairs(view.findings or {}) do
local n_err, n_warn, n_info = 0, 0, 0 ---@type integer, integer, integer
for _, f in ipairs(view.findings or {}) do ---@type integer, CheckFinding
if f.kind == "error" then n_err = n_err + 1
elseif f.kind == "warning" then n_warn = n_warn + 1
else n_info = n_info + 1
@@ -1290,13 +1107,10 @@ function M.run(ctx)
end
-- Project-wide renderer (once = true): write the summary file.
--- @type integer, ReportRenderer
for _, renderer in ipairs(REPORT_RENDERERS) do
for _, renderer in ipairs(REPORT_RENDERERS) do ---@type integer, ReportRenderer
if renderer.once then
--- @type string
local body = renderer.gather(ctx, nil, nil, all_modules)
--- @type string
local out_path = out_root_effective .. "/" .. renderer.basename("") .. "." .. renderer.ext
local body = renderer.gather(ctx, nil, nil, all_modules) ---@type string
local out_path = out_root_effective .. "/" .. renderer.basename("") .. "." .. renderer.ext ---@type string
duffle.write_file(out_path, body)
outputs[#outputs + 1] = { kind = renderer.name, path = out_path }
end
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
+8 -16
View File
@@ -23,10 +23,8 @@
-- Bootstrap: load `scripts/duffle_paths.lua` (sets package.path + package.cpath).
-- Uses `debug.getinfo` to find this file's own directory, so it works both standalone and when require'd from the orchestrator.
-- duffle_paths.lua sets package.path then returns `require("duffle")` at the bottom, so the dofile value IS the duffle module.
--- @type string
local _bootstrap_dir = debug.getinfo(1, "S").source:match("^@?(.*[/\\])") or "./"
--- @type DuffleExport
local duffle = dofile(_bootstrap_dir .. "../duffle_paths.lua")
local _bootstrap_dir = debug.getinfo(1, "S").source:match("^@?(.*[/\\])") or "./" ---@type string
local duffle = dofile(_bootstrap_dir .. "../duffle_paths.lua") ---@type DuffleExport
-- ════════════════════════════════════════════════════════════════════════════
-- Type declarations
@@ -46,8 +44,7 @@ local duffle = dofile(_bootstrap_dir .. "../duffle_paths.lua")
-- Module exports
-- ════════════════════════════════════════════════════════════════════════════
--- @type WordCountEval
local M = {}
local M = {} ---@type WordCountEval
-- ┌────────────────────────────────────────────────────────────────────┐
-- │ Shared utility: count_token_words │
@@ -61,15 +58,12 @@ local M = {}
--- @param wc WordCounts -- the shared word-count table
--- @return integer
function M.count_token_words(token, wc)
--- @type string
local s = duffle.trim(token)
local s = duffle.trim(token) ---@type string
if s == "" then return 0 end
--- @type string|nil, integer
local name, after = duffle.read_ident(s, 1)
local name, after = duffle.read_ident(s, 1) ---@type string|nil, integer
if not name then return 1 end
if wc[name] then return wc[name] end
--- @type integer
local paren_pos = duffle.skip_ws_and_cmt(s, after)
local paren_pos = duffle.skip_ws_and_cmt(s, after) ---@type integer
if s:sub(paren_pos, paren_pos) == "(" then
io.stderr:write(" warning: unknown macro '" .. name .. "', assuming 1 word\n")
end
@@ -95,8 +89,7 @@ end
--- @return PassResult
function M.run(ctx)
-- 1. Canonical-corpus ownership gate.
--- @type Corpus|nil
local corpus = ctx.shared and ctx.shared.corpus
local corpus = ctx.shared and ctx.shared.corpus ---@type Corpus|nil
if type(corpus) ~= "table" then
error("word_count_eval.run requires ctx.shared.corpus (canonical corpus). The fixture must install the corpus before running this pass.", 0)
end
@@ -108,8 +101,7 @@ function M.run(ctx)
-- 3. Load authored metadata. Generated .macs.h files are NOT scanned
-- (the pass computes their counts from the just-built bodies after disk emission; see passes/components.lua).
--- @type WordCounts
local wc = duffle.load_word_counts(ctx.metadata_path)
local wc = duffle.load_word_counts(ctx.metadata_path) ---@type WordCounts
-- 4. Assign the count table. ONE assignment, no copy. The assignment creates no secondary alias.
corpus.word_counts = wc