Improving program model, (better structs, better path awareness ties to those structs).

This commit is contained in:
ed
2026-08-20 14:05:49 -04:00
parent de13bc3ce9
commit 223d1832eb
16 changed files with 1018 additions and 965 deletions
+36 -58
View File
@@ -21,16 +21,8 @@ local duffle = dofile(_bootstrap_dir .. "../duffle_paths.lua")
-- Type declarations
-- ════════════════════════════════════════════════════════════════════════════
-- SourceFile, PassCtx, PassResult, PassShared, Corpus: see ps1_meta.lua
-- SourceScan, AtomEntry, BindsEntry, RegTypeDefault, AtomViewEntry: see scan_source.lua
--- @class AtomAnnotation
--- @field atom_name string -- Atom name (scan.atom_infos row)
--- @field info_line integer -- Source line of the atom_info call
--- @field binds string|nil -- Binds_X name if any
--- @field reads string[] -- R_* names (read targets)
--- @field writes string[] -- R_* names (write targets)
--- @field errors string[]|nil -- Parse-time errors from scan_source (atom_info body malformed)
-- SourceFile, PassCtx, PassResult, PassShared, Corpus, Finding: see ps1_meta.lua
-- SourceScan, AtomEntry, AtomInfoEntry, BindsEntry, RegTypeDefault, AtomViewEntry: see scan_source.lua
--- @class RegTypeOccurrence
--- @field reg string
@@ -38,44 +30,30 @@ local duffle = dofile(_bootstrap_dir .. "../duffle_paths.lua")
--- @field source_line integer
--- @class Findings
--- @field errors PassFinding[]
--- @field warnings PassFinding[]
--- @field info PassFinding[]
--- @field errors Finding[]
--- @field warnings Finding[]
--- @field info Finding[]
--- @class PipeCtx
--- @field atom_index table<string, AtomEntry> -- raw_name or name -> scan.atoms row (kind atom/atom_proc)
--- @field binds_index table<string, BindsEntry>
--- @field annot_counts table<string, integer> -- bag: atom name -> annotation count
--- @field types table<string, RegTypeDefault>
--- @field atom_views table<string, AtomViewEntry>
--- @field seen_defaults table<string, integer> -- bag: register ident -> occurrence count
--- @field seen_field table<string, integer> -- bag: leftover field-count slot
--- @field _scan SourceScan
--- @field word_counts WordCounts|nil
--- @field register_alias_registry table<string, AliasEntry>|nil
--- @field type_name_registry table<string, TypeNameEntry>|nil
--- @field type_occurrences RegTypeOccurrence[]|nil
--- @field atom_infos_list AtomInfoEntry[]|nil
--- @field binds_list BindsEntry[]|nil
-- PassScratch: see ps1_meta.lua
--- @class AnnotatedResult
--- @field atoms AtomEntry[]
--- @field annots AtomAnnotation[]
--- @field annots AtomInfoEntry[]
--- @field macros MacroEntry[]
--- @field binds BindsEntry[]
--- @field errors PassFinding[]
--- @field warnings PassFinding[]
--- @field info PassFinding[]
--- @field errors Finding[]
--- @field warnings Finding[]
--- @field info Finding[]
--- @field source string|nil
--- @class CheckRule
--- @field per_annot (fun(item: AtomAnnotation, pipe_ctx: PipeCtx, findings: Findings): nil)|nil
--- @field per_annot (fun(item: AtomInfoEntry, pipe_ctx: PassScratch, findings: Findings): nil)|nil
--- @class SourceScan
--- @field type_occurrences RegTypeOccurrence[]|nil
--- @class AnnotationPass
--- @field validate fun(ctx: PassCtx, src: SourceFile, corpus_pipe_ctx: PipeCtx|nil): AnnotatedResult
--- @field validate fun(ctx: PassCtx, src: SourceFile, corpus_pipe_ctx: PassScratch|nil): AnnotatedResult
--- @field run fun(ctx: PassCtx): PassResult
-- ════════════════════════════════════════════════════════════════════════════
@@ -85,8 +63,8 @@ local duffle = dofile(_bootstrap_dir .. "../duffle_paths.lua")
--- `macro_word_drift` writes errors[] for missing or mismatched metadata and info[] for a match.
--- Check: Every annotated atom must have a matching MipsAtom_(name) declaration.
--- @param info AtomAnnotation
--- @param pipe_ctx PipeCtx
--- @param info AtomInfoEntry
--- @param pipe_ctx PassScratch
--- @param findings Findings
--- @return nil
local function check_atom_decl_exists(info, pipe_ctx, findings)
@@ -100,8 +78,8 @@ end
--- Check: Every atom may have AT MOST ONE annotation.
--- Post-loop: Needs full-corpus `annot_counts` from pipe_ctx.
--- @param _item AtomAnnotation|nil
--- @param pipe_ctx PipeCtx
--- @param _item AtomInfoEntry|nil
--- @param pipe_ctx PassScratch
--- @param findings Findings
--- @return nil
local function check_unique_annotation(_item, pipe_ctx, findings)
@@ -117,8 +95,8 @@ end
--- Check: BIND atoms must reference a real Binds_* struct.
--- I keep this as a warning so the annotation pass can report the common test-fixture case; `check_abi_handoff` in static analysis supplies the build-stopping error.
--- @param info AtomAnnotation
--- @param pipe_ctx PipeCtx
--- @param info AtomInfoEntry
--- @param pipe_ctx PassScratch
--- @param findings Findings
--- @return nil
local function check_binds_struct_exists(info, pipe_ctx, findings)
@@ -135,7 +113,7 @@ end
--- Check: TAPE_WORDS(mac_X, N) ↔ WORD_COUNT(mac_X, N) drift.
--- Three outcomes: missing (error), mismatch (error), match (info).
--- @param m MacroEntry
--- @param pipe_ctx PipeCtx
--- @param pipe_ctx PassScratch
--- @param findings Findings
--- @return nil
local function check_macro_word_drift(m, pipe_ctx, findings)
@@ -164,7 +142,7 @@ end
--- Check: atom_dbg_reg_default(R_X, <type>) targets an alias in `pipe_ctx.register_alias_registry` and a type in `pipe_ctx.type_name_registry`.
--- Pointer depth remains bounded to 0 or 1, and duplicate defaults remain errors.
--- @param _src SourceFile -- unused (kept for the per_source shape)
--- @param pipe_ctx PipeCtx
--- @param pipe_ctx PassScratch
--- @param findings Findings
--- @return nil
local function check_semantic_reg_defaults(_src, pipe_ctx, findings)
@@ -215,7 +193,7 @@ end
--- Check: atom_reg_types(R_X, <type>) entries target an alias in `pipe_ctx.register_alias_registry` and a type in `pipe_ctx.type_name_registry`.
--- A bare `atom_reg` marker opts the `R_<n>` alias into GPR identity; references to R_T0..R_T3 require the same explicit marker.
--- @param _src SourceFile
--- @param pipe_ctx PipeCtx
--- @param pipe_ctx PassScratch
--- @param findings Findings
--- @return nil
local function check_atom_reg_types(_src, pipe_ctx, findings)
@@ -247,7 +225,7 @@ end
--- Check: atom_view(Binds_X) entries reference a Binds_* struct with at least one field.
--- @param _src SourceFile
--- @param pipe_ctx PipeCtx
--- @param pipe_ctx PassScratch
--- @param findings Findings
--- @return nil
local function check_atom_view_layout(_src, pipe_ctx, findings)
@@ -277,7 +255,7 @@ end
--- Check: Binds_* structs require unique field names because atom_view uses those names for typed-field lookup in gdb.
--- @param _src SourceFile
--- @param pipe_ctx PipeCtx
--- @param pipe_ctx PassScratch
--- @param findings Findings
--- @return nil
local function check_binds_no_duplicate_fields(_src, pipe_ctx, findings)
@@ -310,7 +288,7 @@ end
--- 6. unsupported target_kind -> marker precedes an unrelated declaration
--- Valid markers stamp `debug_skip` on whole-atom, bare-component, and proc-component declaration records in scan_source.lua.
--- @param marker DebugSkipMarker
--- @param _pipe_ctx PipeCtx -- Unused; kept for consistency with per_annot
--- @param _pipe_ctx PassScratch -- Unused; kept for consistency with per_annot
--- @param findings Findings
--- @return nil
local function check_skip_marker(marker, _pipe_ctx, findings)
@@ -368,7 +346,7 @@ end
--- Warn when a source references an unregistered alias.
--- When a source uses an unregistered R_X, this check emits one pass-level info entry for that source and directs C-ABI register names to explicit alias registration.
--- @param _src SourceFile
--- @param pipe_ctx PipeCtx
--- @param pipe_ctx PassScratch
--- @param findings Findings
--- @return nil
local function check_wave_context_migration(_src, pipe_ctx, findings)
@@ -425,9 +403,9 @@ local CHECK_RULES = { ---@type CheckRule[]
--- Builds one pass-wide pipe_ctx from the merged `corpus.*` registries and source-ordered `corpus.atom_infos`; per-source declarations and bodies remain in `src.scan`.
--- The module ownership contract above requires callers to construct `ctx.shared.corpus` through `build_ctx`; the error message below enforces that gate.
--- @param ctx PassCtx
--- @return PipeCtx
--- @return PassScratch
local function build_corpus_pipe_ctx(ctx)
local view = duffle.corpus_view(ctx) ---@type PipeCtx
local view = duffle.corpus_view(ctx) ---@type PassScratch
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
@@ -443,7 +421,7 @@ end
--- Validate one source against its pre-scanned SourceScan payload + the corpus-wide pipe_ctx.
--- @param ctx PassCtx
--- @param src SourceFile
--- @param corpus_pipe_ctx PipeCtx|nil -- Built once per pass from corpus registries; nil builds the same projection here.
--- @param corpus_pipe_ctx PassScratch|nil -- Built once per pass from corpus registries; nil builds the same projection here.
--- @return AnnotatedResult
local function validate(ctx, src, corpus_pipe_ctx)
corpus_pipe_ctx = corpus_pipe_ctx or build_corpus_pipe_ctx(ctx)
@@ -453,7 +431,7 @@ local function validate(ctx, src, corpus_pipe_ctx)
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[]
local pipe_ctx = { ---@type PipeCtx
local pipe_ctx = { ---@type PassScratch
atom_index = {},
binds_index = {},
annot_counts = corpus_pipe_ctx.annot_counts,
@@ -548,12 +526,12 @@ M.validate = validate
--- @return PassResult
function M.run(ctx)
local outputs = {} ---@type PassOutputEntry[]
local errors = {} ---@type PassFinding[]
local warnings = {} ---@type PassFinding[]
local errors = {} ---@type Finding[]
local warnings = {} ---@type Finding[]
-- 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.
local corpus_pipe_ctx = build_corpus_pipe_ctx(ctx) ---@type PipeCtx
local corpus_pipe_ctx = build_corpus_pipe_ctx(ctx) ---@type PassScratch
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.
@@ -562,17 +540,17 @@ function M.run(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[]
local dir_errors = {} ---@type Finding[]
local dir_warnings = {} ---@type Finding[]
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
for _, e in ipairs(result.errors) do ---@type integer, PassFinding
for _, e in ipairs(result.errors) do ---@type integer, Finding
dir_errors[#dir_errors + 1] = { line = e.line, msg = e.msg, source = src.path }
errors [#errors + 1] = { line = e.line, msg = e.msg }
end
for _, w in ipairs(result.warnings) do ---@type integer, PassFinding
for _, w in ipairs(result.warnings) do ---@type integer, Finding
dir_warnings[#dir_warnings + 1] = { line = w.line, msg = w.msg }
warnings [#warnings + 1] = { line = w.line, msg = w.msg }
end
+2 -2
View File
@@ -595,8 +595,8 @@ end
--- @return PassResult
function M.run(ctx)
local outputs = {} ---@type PassOutputEntry[]
local errors = {} ---@type PassFinding[]
local warnings = {} ---@type PassFinding[]
local errors = {} ---@type Finding[]
local warnings = {} ---@type Finding[]
local corpus = ctx.shared and ctx.shared.corpus ---@type Corpus|nil
if type(corpus) ~= "table" or type(corpus.source_order) ~= "table" then
+15 -17
View File
@@ -28,8 +28,8 @@
--- @class AutoRegResult
--- @field outputs AutoRegOutput[]
--- @field errors PassFinding[]
--- @field warnings PassFinding[]
--- @field errors Finding[]
--- @field warnings Finding[]
--- @class AutoRegPass
--- @field run fun(ctx: PassCtx): AutoRegResult
@@ -37,6 +37,7 @@
local _bootstrap_dir = debug.getinfo(1, "S").source:match("^@?(.*[/\\])") or "./" ---@type string
local duffle = dofile(_bootstrap_dir .. "../duffle_paths.lua") ---@type DuffleExport
local isa = require("duffle_isa") ---@type DuffleIsa
--- ════════════════════════════════════════════════════════════════════════════
--- THE GPR ALLOCATION POOL — what's allocatable, and (more importantly) WHY
@@ -54,15 +55,12 @@ 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.
---
local POOL = { ---@type GprIdent[]
"R_V0", "R_V1",
"R_T0", "R_T1", "R_T2", "R_T3",
"R_T4", "R_T5", "R_T6", "R_T7",
"R_A0", "R_A1", "R_A2", "R_A3",
"R_S0", "R_S1", "R_S2", "R_S3",
"R_S4", "R_S5", "R_S6", "R_S7",
"R_T8", "R_T9",
}
local POOL = {} ---@type GprIdent[]
for _, row in ipairs(isa.GPR_ROLE) do ---@type integer, GprRole
if row.pool then
POOL[#POOL + 1] = row.name
end
end
-- Map from integer MIPS GPR code (the `code` field on AliasEntry) to the physical GPR ident in POOL.
-- The standard MIPS O32 ABI register numbering matches mips.h's R_*_Code #defines (mips.h).
@@ -94,7 +92,7 @@ end
--- @param phase_label string
--- @param decls table<string, string> -- bag: auto-reg symbol -> decl payload
--- @return GprAllocMap
--- @return PassFinding[]
--- @return Finding[]
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),
@@ -102,7 +100,7 @@ local function allocate_phase(phase_label, decls)
local pool = {} ---@type GprIdent[]
for i = 1, #POOL do pool[i] = POOL[i] end ---@type integer
local result = {} ---@type GprAllocMap
local errors = {} ---@type PassFinding[]
local errors = {} ---@type Finding[]
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
@@ -226,8 +224,8 @@ local M = {} ---@type AutoRegPass
--- @return AutoRegResult
function M.run(ctx)
local outputs = {} ---@type AutoRegOutput[]
local errors = {} ---@type PassFinding[]
local warnings = {} ---@type PassFinding[]
local errors = {} ---@type Finding[]
local warnings = {} ---@type Finding[]
local corpus = ctx.shared and ctx.shared.corpus ---@type Corpus|nil
if type(corpus) ~= "table" then
@@ -244,12 +242,12 @@ function M.run(ctx)
-- 1. Allocate phase pools first (phase declarations take precedence over per-atom declarations).
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[]
local mapping, errs = allocate_phase(phase_label, decls) ---@type GprAllocMap, Finding[]
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
for _, e in ipairs(errs) do ---@type integer, PassFinding
for _, e in ipairs(errs) do ---@type integer, Finding
errors[#errors + 1] = e
end
end
+35 -66
View File
@@ -1,6 +1,6 @@
--- passes/components.lua — Component-macro header generator.
---
--- Ownership: `corpus.word_counts`, `corpus.components`, and `corpus.component_body_index`.
--- Ownership: `corpus.word_counts` and `corpus.components`.
--- Scanner owns `declaration_comment` and `debug_skip` on each declaration record; this pass projects both forward.
---
--- Reads the pre-scanned SourceScan payload from `duffle.scan_source` for `MipsAtomComp_(ac_X)` and `MipsAtomComp_Proc_(ac_X, { body })` declarations (kind="comp_bare" / "comp_proc"),
@@ -56,20 +56,24 @@ local MACS_FILENAME = "macs.h" ---@type string
-- SourceScan, AtomEntry, CorpusCollision, CollisionSite: see scan_source.lua
-- BodyToken: see emission_model.lua
-- WordCounts: see word_count_eval.lua
-- ComponentBodyEntry: see duffle_emit.lua
-- InstructionRow, GteCommandRow: see duffle_isa.lua
--- @class Component
--- @field name string -- Atom name (without `ac_` prefix)
--- @field body string -- Brace-delimited body (without the braces)
--- @field body_off integer|nil -- Byte offset of body[1] in source
--- @field body_tokens BodyToken[]|nil
--- @field args string|nil -- Function-args string (function form only)
--- @field arg_names string[]|nil -- Formal names with leading `ab` dropped
--- @field line integer -- Source line of the declaration
--- @field comment string|nil -- Scanner-owned `declaration_comment`; the components pass reads it from the scanner record
--- @field kind string -- "comp_bare" | "comp_proc" (atom_proc is NOT a component — see `project_components`)
--- @field debug_skip boolean -- Mirror of `a.debug_skip` (scanner-owned); true iff a bare `atom_dbg_skip` marker immediately preceded the declaration
--- @field name string -- Atom name (without `ac_` prefix)
--- @field body string -- Brace-delimited body (without the braces)
--- @field body_off integer|nil -- Byte offset of body[1] in source
--- @field body_tokens BodyToken[]|nil
--- @field args string|nil -- Function-args string (function form only)
--- @field arg_names string[]|nil -- Formal names with leading `ab` dropped
--- @field line integer -- Source line of the declaration
--- @field comment string|nil -- Scanner-owned `declaration_comment`; the components pass reads it from the scanner record
--- @field kind string -- "comp_bare" | "comp_proc" (atom_proc is NOT a component — see `project_components`)
--- @field debug_skip boolean -- Mirror of `a.debug_skip` (scanner-owned); true iff a bare `atom_dbg_skip` marker immediately preceded the declaration
--- @field path string|nil -- Slash-normalized source path (collision sites)
--- @field source string|nil -- Absolute source path (emit)
--- @field line_of (fun(pos: integer): integer)|nil
--- @field cycle_cost integer|nil -- From metadata[c.name]; nil when the body was not costed
--- @field gp0_contrib integer|nil -- From metadata[c.name]; nil when the body was not costed
--- @class ComponentMeta
--- @field cycle_cost integer
@@ -800,16 +804,7 @@ local function update_canonical_word_counts(corpus, components, counts)
end
end
--- @class ComponentDef
--- @field name string -- Bare name (without ac_/mac_ prefix)
--- @field line integer -- Definition source line (line of `MipsAtomComp_(ac_X)` / `MipsAtomComp_Proc_(ac_X, ...)`)
--- @field path string -- Absolute source path of the definition
--- @field kind string -- "comp_bare" | "comp_proc" (atom_proc is NOT a component)
--- @field debug_skip boolean -- Mirror of the scanner-owned `a.debug_skip`; consumers read this directly
--- @field cycle_cost integer|nil -- From metadata[c.name]; nil when the body was not costed
--- @field gp0_contrib integer|nil -- From metadata[c.name]; nil when the body was not costed
--- (internal) Populate `corpus.components` with this source's components-by-name map.
--- (internal) Populate `corpus.components` with this source's one component row per bare name.
--- First declaration wins; later declarations of the same bare name are dropped and recorded as a collision via `corpus.collisions` (kind = "component").
--- The pass does NOT write to `ctx.shared.components`.
--- No parallel skip map is built here; consumers that need the per-component skip state read `corpus.components[name].debug_skip` directly.
@@ -818,28 +813,29 @@ end
--- @param src SourceFile
--- @param components Component[]
--- @param metadata ComponentMetaMap
--- @param scan SourceScan
--- @return nil
local function update_canonical_components(corpus, src, components, metadata)
local rel_path = src.path:gsub("\\", "/") ---@type string
for _, c in ipairs(components) do ---@type integer, Component
local function update_canonical_components(corpus, src, components, metadata, scan)
local rel_path = src.path:gsub("\\", "/") ---@type string
local line_of = scan and scan.line_of ---@type (fun(pos: integer): integer)|nil
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.
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,
line = c.line,
path = rel_path,
kind = c.kind or "comp_bare",
debug_skip = c.debug_skip == true,
cycle_cost = m and m.cycle_cost or nil,
gp0_contrib = m and m.gp0_contrib or nil,
}
c.path = rel_path
c.source = src.path
c.line_of = line_of
c.kind = c.kind or "comp_bare"
c.debug_skip = c.debug_skip == true
c.cycle_cost = m and m.cycle_cost or nil
c.gp0_contrib = m and m.gp0_contrib or nil
corpus.components[c.name] = c
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.
local existing = corpus.components[c.name] ---@type ComponentDef
local existing = corpus.components[c.name] ---@type Component
if existing.path ~= rel_path or existing.line ~= c.line then
local kind = c.kind or "comp_bare" ---@type string
local first_kind = existing.kind or "comp_bare" ---@type string
@@ -856,37 +852,12 @@ local function update_canonical_components(corpus, src, components, metadata)
end
end
--- (internal) Populate `corpus.component_body_index` with this source's body index entries.
--- First declaration wins; later declarations are dropped (no separate collision record: the components collision is already surfaced by `update_canonical_components`).
--- The pass writes to `corpus.component_body_index` only (the corpus owns this projection).
--- @param corpus Corpus
--- @param src SourceFile
--- @param components Component[]
--- @param scan SourceScan
--- @return nil
local function update_canonical_component_body_index(corpus, src, components, scan)
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,
body_off = c.body_off,
line_of = line_of,
source = src.path,
declaration = c.line,
kind = c.kind,
arg_names = c.arg_names,
}
end
end
end
--- @param ctx PassCtx
--- @return PassResult
function M.run(ctx)
local outputs = {} ---@type MacsOutput[]
local errors = {} ---@type PassFinding[]
local warnings = {} ---@type PassFinding[]
local errors = {} ---@type Finding[]
local warnings = {} ---@type Finding[]
-- Corpus ownership gate.
local corpus = ctx.shared and ctx.shared.corpus ---@type Corpus|nil
@@ -904,8 +875,7 @@ function M.run(ctx)
-- Projection ownership:
-- * `corpus.word_counts["mac_"..name]` — current component count
-- * `corpus.components[name]` — bare-name component definition
-- * `corpus.component_body_index[name]` — body / line_of / source index
-- * `corpus.components[name]` — one row: body, line_of, source, cost
-- The pass writes to the corpus only; consumers read from the corpus directly.
-- Per-directory aggregation: every source in the same directory contributes to one `gen/macs.h`.
@@ -937,8 +907,7 @@ function M.run(ctx)
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)
update_canonical_components(corpus, src, per_source, metadata_per_source[src], src.scan)
end
end
end
+1 -1
View File
@@ -450,7 +450,7 @@ local DEFAULT_BASENAME = "hello_gte" ---@type string
--- DieSchema / FORM_WRITERS / atom-table records live on this file.
--- SourceFile, Path, AtomName: see duffle.lua.
--- Corpus, PassCtx, PassResult, PassFlags, PassShared, PassFinding: see ps1_meta.lua.
--- Corpus, PassCtx, PassResult, PassFlags, PassShared, Finding: see ps1_meta.lua.
--- AtomEntry, AliasEntry, TypeNameEntry, TypeField, AtomInfoEntry, BindsEntry,
--- AtomViewEntry, AtomCtxEntry, AtomPhaseGroup, SourceScan, RegTypeOverride: see scan_source.lua.
--- AtomPaths, WordEvent, BodyToken: see emission_model.lua.
+38 -35
View File
@@ -1,7 +1,7 @@
--- passes/emission_model.lua: Per-atom emission projection.
---
--- The `emission-model` pass owns `atom.paths`, the canonical per-atom mutable surface for atoms and raw atoms with bodies in `ctx.shared.corpus.source_order`.
--- For each atom, the pass invokes `duffle.project_emission(body_text, component_index, word_counts, components)`.
--- For each atom, the pass invokes `duffle.project_emission(body_text, components, word_counts, components)`.
--- It stores the ordered `items` stream plus the dense `word_events` / `markers` / `invocations` views on `atom.paths`.
---
--- Public boundary:
@@ -92,17 +92,7 @@
--- @field consuming_encoder string|nil
--- @field consuming_arg_pos integer|nil
--- @class EmitError
--- @field kind string
--- @field line integer|nil
--- @field msg string
--- @field source string|nil
--- @field schema_name string|nil
--- @class EmitWarning
--- @field kind string
--- @field line integer|nil
--- @field msg string
-- Finding: see ps1_meta.lua
--- @class AtomPaths
--- @field tokens BodyToken[]
@@ -111,8 +101,8 @@
--- @field word_events WordEvent[]
--- @field markers EmissionMarker[]
--- @field invocations InvocationRecord[]
--- @field errors EmitError[]
--- @field warnings EmitWarning[]
--- @field errors Finding[]
--- @field warnings Finding[]
--- @class EmissionModelPass
--- @field run fun(ctx: PassCtx): PassResult
@@ -136,7 +126,7 @@ local duffle = dofile(_bootstrap_dir .. "../duffle_paths.lua")
-- * ROOT invocations (`inv.parent_id == 0`) receive body-relative `call_line` values directly from `M.LineIndex(body_text)` in the walker.
-- The source `line_of` closure supplies physical lines at the close site, so this function converts each root value exactly once.
-- * INNER invocations (`inv.parent_id ~= 0`) receive physical `call_line` values directly from the COMPONENT's `line_of` in the walker.
-- Recursive descent forwards that closure through `corpus.component_body_index[name].line_of`; those values arrive physical and remain unchanged.
-- Recursive descent forwards that closure through `corpus.components[name].line_of`; those values arrive physical and remain unchanged.
--
-- After this function, every `inv.call_line` is physical. DWARF and provenance output read it directly.
-- The word-event loop forwards the already-physical `outer_inv.call_line` into `we.call_line` for words inside an invocation.
@@ -155,7 +145,7 @@ local function stamp_root_provenance(projection, atom_record, src, corpus)
-- 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.
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 components = corpus.components or {} ---@type table<string, Component>
local word_items = {} ---@type EmissionItem[]
for _, item in ipairs(projection.items) do ---@type integer, EmissionItem
@@ -176,7 +166,7 @@ local function stamp_root_provenance(projection, atom_record, src, corpus)
local inner_id = ids[#ids] ---@type integer
local inner_inv = inner_id and projection.invocations[inner_id] ---@type InvocationRecord|nil
if inner_inv then
local component = component_index[inner_inv.component_name] ---@type ComponentBodyEntry|nil
local component = components[inner_inv.component_name] ---@type Component|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
@@ -257,13 +247,13 @@ end
local function project_atom(atom_record, src, corpus)
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 comps = corpus.components or {} ---@type table<string, Component>
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`.
local proj = duffle.project_emission(body, cbi, wc, corpus.components, { ---@type EmissionProjection
local proj = duffle.project_emission(body, comps, wc, comps, { ---@type EmissionProjection
reg_use_schema = schema,
reg_use_param = atom_record.reg_use_param_name,
atom_name = atom_record.name,
@@ -271,13 +261,22 @@ local function project_atom(atom_record, src, corpus)
})
if atom_record.reg_use_schema_name and not schema then
proj.errors[#proj.errors + 1] = {
kind = "reguse_missing_schema",
msg = string.format("RegUse schema %q is missing", atom_record.reg_use_schema_name),
kind = "error",
check = "reguse_missing_schema",
msg = string.format("RegUse schema %q is missing", atom_record.reg_use_schema_name),
schema_name = atom_record.reg_use_schema_name,
}
end
for _, err in ipairs(corpus.reg_use_errors or {}) do ---@type integer, EmitError
for _, err in ipairs(corpus.reg_use_errors or {}) do ---@type integer, RegUseError
if err.schema_name == atom_record.reg_use_schema_name then
proj.errors[#proj.errors + 1] = err
proj.errors[#proj.errors + 1] = {
kind = "error",
check = err.kind,
line = err.line or err.source_line or 0,
msg = err.msg or "",
source = err.source or err.source_file,
schema_name = err.schema_name,
}
end
end
local paths = { ---@type AtomPaths
@@ -303,8 +302,8 @@ end
--- @return PassResult
function M.run(ctx)
local outputs = {} ---@type PassOutputEntry[]
local errors = {} ---@type EmitError[]
local warnings = {} ---@type EmitWarning[]
local errors = {} ---@type Finding[]
local warnings = {} ---@type Finding[]
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
@@ -322,20 +321,24 @@ function M.run(ctx)
return
end
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.
for _, e in ipairs(proj.errors) do ---@type integer, Finding
-- Finding.kind is severity. Finding.check holds the diagnostic code
-- (cycle / count_mismatch / unbalanced / reguse_*).
errors[#errors + 1] = {
kind = e.kind,
line = e.line,
msg = e.msg,
source = e.source or src.path,
kind = "error",
check = e.check,
line = e.line,
msg = e.msg,
source = e.source or src.path,
schema_name = e.schema_name,
}
end
for _, w in ipairs(proj.warnings) do ---@type integer, EmitWarning
for _, w in ipairs(proj.warnings) do ---@type integer, Finding
warnings[#warnings + 1] = {
kind = w.kind,
line = w.line,
msg = w.msg,
kind = "warning",
check = w.check,
line = w.line,
msg = w.msg,
}
end
end
+4 -4
View File
@@ -137,7 +137,7 @@ end
--- missing `consuming_encoder` -> ERROR. A lone top-level `atom_offset` is not a branch.
--- @param labels table<string, integer>
--- @param branches OffsetBranch[]
--- @param errors PassFinding[]
--- @param errors Finding[]
--- @return BranchOffset[]
local function compute_offsets(labels, branches, errors)
local results = {} ---@type BranchOffset[]
@@ -259,7 +259,7 @@ local M = {} ---@type OffsetsPass
--- @param ctx PassCtx
--- @param dir string
--- @param sources SourceFile[]
--- @param errors PassFinding[]
--- @param errors Finding[]
--- @return string|nil
local function process_directory(ctx, dir, sources, errors)
local atoms_data = {} ---@type AtomData[]
@@ -297,8 +297,8 @@ end
--- @return PassResult
function M.run(ctx)
local outputs = {} ---@type OffsetOutput[]
local errors = {} ---@type PassFinding[]
local warnings = {} ---@type PassFinding[]
local errors = {} ---@type Finding[]
local warnings = {} ---@type Finding[]
local corpus = ctx.shared and ctx.shared.corpus ---@type Corpus|nil
if type(corpus) ~= "table" then
+26 -44
View File
@@ -54,36 +54,17 @@ local PASS_NAME = "report" ---@type string
-- ════════════════════════════════════════════════════════════════════════════
-- SourceFile: see duffle.lua
-- PassCtx, PassResult, PassFinding, PassOutputEntry: see ps1_meta.lua
-- PassCtx, PassResult, Finding, PassOutputEntry: see ps1_meta.lua
-- AtomEntry, SourceScan, BindsEntry, AtomInfoEntry, AliasEntry,
-- AtomPhaseGroup, AtomViewEntry, AtomCtxEntry, CorpusCollision: see scan_source.lua
-- CheckFinding, AtomAnalysis: see static_analysis.lua
-- AtomAnalysis: see static_analysis.lua
-- WordCounts: see word_count_eval.lua
-- ComponentBodyEntry: see duffle_emit.lua
-- Component: see components.lua
-- AtomPaths, WordEvent: see emission_model.lua
-- GprAllocMap: see auto_reg.lua
-- Shapes produced by `passes/annotation.lua`'s `M.validate()`.
--- @class AnnotEntry
--- @field line integer -- Source line
--- @field macro string -- Macro name (e.g. "atom_reads")
--- @field name string -- Atom name (if a `name(...)` was given)
--- @field kind string -- "atom_info" | "atom_bind" | ...
--- @field binds string|nil -- Binds_X name if any
--- @field reads string[] -- R_* names (read targets)
--- @field writes string[] -- R_* names (write targets)
--- @field error string|nil -- Error message if annotation was malformed
--- @class BindsField
--- @field name string -- Field name
--- @field offset integer -- Byte offset within the Binds_X struct
--- @class BindsStruct
--- @field name string -- Struct name (e.g. "Binds_Floor")
--- @field line integer -- Source line of the typedef
--- @field bytes integer -- Total byte size
--- @field fields BindsField[] -- The field list
-- Binds_* rows are BindsEntry (scan_source.lua). Report reads src.scan.binds.
--- @class MacroEntry
--- @field name string -- Macro name (e.g. "WORD_COUNT(my_macro, 4)")
@@ -93,12 +74,12 @@ local PASS_NAME = "report" ---@type string
--- @class AnnotationResult
--- @field source string -- Set by this pass; original source path
--- @field atoms AtomEntry[] -- Atom declarations in this source
--- @field annots AnnotEntry[] -- Annotation entries
--- @field annots AtomInfoEntry[] -- Annotation entries
--- @field macros MacroEntry[] -- Macro word-count declarations
--- @field binds BindsStruct[] -- Binds_* struct declarations
--- @field errors PassFinding[] -- Errors from validation
--- @field warnings PassFinding[] -- Warnings from validation
--- @field info PassFinding[] -- Info summary (not rendered here)
--- @field binds BindsEntry[] -- Binds_* struct declarations
--- @field errors Finding[] -- Errors from validation
--- @field warnings Finding[] -- Warnings from validation
--- @field info Finding[] -- Info summary (not rendered here)
--- @class ModuleEntry
--- @field dir string -- Absolute directory path
@@ -196,7 +177,7 @@ local PASS_NAME = "report" ---@type string
--- @field sources SourceFile[]
--- @field decls AtomEntry[]
--- @field schemas RegUseSchema[]
--- @field findings CheckFinding[]
--- @field findings Finding[]
--- @field corpus Corpus
--- @class SectionRenderer
@@ -424,11 +405,11 @@ end
--- @param view ModuleView
--- @return nil
local function render_section_components(add, view)
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
local rows = {} ---@type ComponentReportRow[]
local index = (view.corpus and view.corpus.components) or {} ---@type table<string, Component>
for _, a in ipairs(view.decls) do ---@type integer, AtomEntry
if a.kind == "comp_bare" or a.kind == "comp_proc" then
local idx = index[a.name] or {} ---@type ComponentBodyEntry
local idx = index[a.name] or {} ---@type Component
local args = idx.arg_names or {} ---@type string[]
rows[#rows + 1] = {
name = a.name,
@@ -698,8 +679,8 @@ end
--- @param view ModuleView
--- @return nil
local function render_section_findings(add, view)
local by_atom = {} ---@type table<string, CheckFinding[]>
for _, f in ipairs(view.findings or {}) do ---@type integer, CheckFinding
local by_atom = {} ---@type table<string, Finding[]>
for _, f in ipairs(view.findings or {}) do ---@type integer, Finding
local key = f.atom or "?" ---@type string
by_atom[key] = by_atom[key] or {}
by_atom[key][#by_atom[key] + 1] = f
@@ -707,11 +688,11 @@ local function render_section_findings(add, view)
if next(by_atom) == nil then add("_(none)_"); add(""); return end
local seen = {} ---@type table<string, boolean> -- bag: atom name already emitted
--- @param name string
--- @param fs CheckFinding[]
--- @param fs Finding[]
--- @return nil
local function emit(name, fs)
add("### " .. name)
for _, f in ipairs(fs) do ---@type integer, CheckFinding
for _, f in ipairs(fs) do ---@type integer, Finding
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
@@ -766,11 +747,12 @@ local HIDDEN_UNLESS_WRITTEN = { ---@type table<string, boolean> -- bag: GPR key
R_AT = true, R_TapePtr = true, R_AtomJmp = true,
}
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,
}
local PHYSICAL_GPR = {} ---@type table<string, boolean> -- bag: physical GPR alias -> true
for _, row in ipairs(duffle.GPR_ROLE) do ---@type integer, GprRole
if row.pool then
PHYSICAL_GPR[row.name] = true
end
end
--- @param atom AtomEntry
--- @param key string
@@ -945,7 +927,7 @@ local function render_module_meta_report(view)
n_macros = n_macros + #((src.scan and src.scan.macros) or {})
end
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
for _, f in ipairs(view.findings or {}) do ---@type integer, Finding
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
@@ -1087,7 +1069,7 @@ function M.run(ctx)
n_macros = n_macros + #((src.scan and src.scan.macros) or {})
end
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
for _, f in ipairs(view.findings or {}) do ---@type integer, Finding
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
+1
View File
@@ -118,6 +118,7 @@ local parse_enum_int_literal ---@type fun(text: string, start: integer): (intege
--- @field ctx_atom string|nil
--- @field phase string|nil
--- @field info_line integer
--- @field errors string[]|nil -- parse-time atom_info errors
--- @class BindsEntry
--- @field line integer
File diff suppressed because it is too large Load Diff
+2 -2
View File
@@ -4,13 +4,13 @@
--- 1. **Public utility** `M.count_token_words(token, wc)`: Used by `passes/offsets.lua`, `passes/annotation.lua`, and other passes.
--- 2. **Pass entry** `M.run(ctx)`: Loads the authored `word_count.metadata.h` into `ctx.shared.corpus.word_counts` for downstream passes.
--- The generated `.macs.h` files are OUTPUT artifacts and are NOT inputs to this pass;
--- Current component counts are owned by `passes/components.lua` (which populates `corpus.word_counts` and `corpus.component_body_index`
--- Current component counts are owned by `passes/components.lua` (which populates `corpus.word_counts` and `corpus.components`
--- AFTER computing each current count from the just-built body + `corpus.word_counts`).
---
--- **Canonical contract**:
--- * `ctx.shared.corpus.word_counts` is the count table.
--- * `corpus.word_counts` is the sole count table. Consumers read `corpus.word_counts` directly.
--- * `ctx.shared.components` and `ctx.shared.component_body_index` are NOT created by this pass (projections only).
--- * `ctx.shared.components` is NOT created by this pass (projections only).
--- * No `.macs.h` recursive discovery (no `scan_dir`, no scan cache, no `_invalidate_scan_cache`).
---
--- **Conventions**: tabs (1/level), EmmyLua annotations, no regex,