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
+4 -6
View File
@@ -31,9 +31,8 @@ local M = {} ---@type DuffleExport
--- @field binds_by_name table<string, BindsEntry>
--- @field atoms_by_name table<AtomName, AtomEntry>
--- @field atom_infos AtomInfoEntry[]
--- @field components table<string, ComponentDef>
--- @field components table<string, Component>
--- @field component_atom_infos AtomInfoEntry[]|nil
--- @field component_body_index table<string, ComponentBodyEntry>
--- @field tape_chains table<string, TapeChain>|nil
--- @field source_order SourceFile[]
--- @field collisions CorpusCollision[]
@@ -70,7 +69,6 @@ function M.corpus_view(ctx)
atom_infos = corpus.atom_infos or {},
components = corpus.components or {},
component_atom_infos = corpus.component_atom_infos or {},
component_body_index = corpus.component_body_index or {},
tape_chains = corpus.tape_chains or {},
source_order = corpus.source_order or {},
collisions = corpus.collisions or {},
@@ -80,12 +78,12 @@ end
--- @param rules CheckRule[]
--- @param phase string
--- @param item AtomEntry|SourceFile
--- @param pipe_ctx PipeCtx
--- @param findings CheckFinding[]
--- @param pipe_ctx PassScratch
--- @param findings Finding[]
--- @return nil
function M.run_check_rules(rules, phase, item, pipe_ctx, findings)
for _, rule in ipairs(rules) do ---@type integer, CheckRule
local fn = rule[phase] ---@type (fun(item: AtomEntry|SourceFile, pipe_ctx: PipeCtx, findings: CheckFinding[]): nil)|nil
local fn = rule[phase] ---@type (fun(item: AtomEntry|SourceFile, pipe_ctx: PassScratch, findings: Finding[]): nil)|nil
if fn then fn(item, pipe_ctx, findings) end
end
end
+59 -69
View File
@@ -11,20 +11,9 @@ for k, v in pairs(isa) do M[k] = v end ---@type string, any
-- Shared, memoized helpers: a single emitted-word event stream that every downstream pass reads from,
-- built once from the pre-tokenized bodies.
--- @class ComponentBodyEntry
--- @field body_tokens BodyToken[]
--- @field body_off integer -- byte offset of body[1] in `source`
--- @field line_of fun(pos:integer):integer -- byte-offset → 1-based line number in `source`
--- @field source string -- absolute path of the source containing the declaration
--- @field declaration integer -- 1-based line number of the MipsAtomComp_(ac_X) declaration
--- @field kind string -- "comp_bare" | "comp_proc"
--- @field arg_names string[]|nil
--- @field sub_map table<string, string>|nil -- bag: formal -> substituted operand
--- @class EmissionWalkCtx
--- @field component_index table<string, ComponentBodyEntry>
--- @field components table<string, Component>
--- @field word_counts WordCounts
--- @field components table<string, ComponentDef>
--- @field reg_use_schema RegUseSchema|nil
--- @field reg_use_param string|nil
--- @field atom_name AtomName|nil
@@ -39,10 +28,12 @@ for k, v in pairs(isa) do M[k] = v end ---@type string, any
--- @field atom_name AtomName|nil
--- @field schema_name string|nil
-- Finding: see ps1_meta.lua
--- @class DuffleEmit
-- The cross-source component-body index is owned by the corpus (`corpus.component_body_index`, populated by `passes/components.lua`).
-- The cross-source component row is owned by the corpus (`corpus.components`, populated by `passes/components.lua`).
-- Consumers (`passes/static_analysis.lua`, `passes/emission_model.lua`) read it directly; per-pass memoization helpers stay out of scope.
-- ASCII byte constants used by split_call_args (kept local to keep Section 8 self-contained).
@@ -119,18 +110,18 @@ local E_MAC_PREFIX_LEN = 4 ---@type integer
--- * Known `mac_X(...)` calls: Recursively expand the indexed component body, including nested components. Every event from the expansion carries:
--- - `source` / `line` = the COMPONENT'S source path + the line of the token within the component body (i.e. "definition site").
--- - `call_source` / `call_line` = the ROOT atom's source path + call-site line, PRESERVED across recursion so nested events still point at the original root.
--- * Unknown `mac_X` (not in `component_index`): fall back to `word_counts[ident]` if present; otherwise emit one opaque event so the cycle budget accounts for the word.
--- * Unknown `mac_X` (not in `components`): fall back to `word_counts[ident]` if present; otherwise emit one opaque event so the cycle budget accounts for the word.
--- * Marker Tokens (`atom_label(...)` / `atom_offset(...)`): Zero events (they are pure metaprogram hints).
---
--- Cycle protection: a per-expansion `visiting` set tracks components currently on the expansion stack;
--- a re-entry produces a deterministic `{kind = "cycle", ...}` error and aborts that branch (does NOT hang, does NOT recurse).
--- a re-entry produces a deterministic `{check = "cycle", ...}` error and aborts that branch (does NOT hang, does NOT recurse).
---
--- Pure: reads `body_entry` / `component_index` / `word_counts`. Memoization is the caller's responsibility.
--- Pure: reads `body_entry` / `components` / `word_counts`. Memoization is the caller's responsibility.
--- Callers wanting `word_events` / `word_event_errors` precomputed for many atoms should memoize them per atom.
--- @param body_entry ComponentBodyEntry
--- @param component_index table<string, ComponentBodyEntry>
--- @param body_entry Component
--- @param components table<string, Component>
--- @param word_counts WordCounts
--- @return WordEvent[], EmitError[]
--- @return WordEvent[], Finding[]
-- ════════════════════════════════════════════════════════════════════════════
-- Section 11: project_emission (per-atom emission projection)
@@ -141,7 +132,7 @@ local E_MAC_PREFIX_LEN = 4 ---@type integer
-- The items stream is the single ordered source of truth; `word_events` and `markers` are dense views over it.
--
-- The helper below operates on a body string (not a body_entry) so the pass can call it without depending on the older SourceScan / body_off conventions.
-- component_index argument is reserved for recursive component expansion.
-- `components` is the recursive expansion map (`corpus.components`).
-- word_counts table is authored-metadata + current-component count table.
--- @class EmissionProjection
@@ -149,8 +140,8 @@ local E_MAC_PREFIX_LEN = 4 ---@type integer
--- @field word_events WordEvent[]
--- @field markers EmissionMarker[]
--- @field invocations InvocationRecord[] -- dense view of items where kind == "invoke_begin"|"invoke_end"
--- @field errors EmitError[]
--- @field warnings EmitWarning[]
--- @field errors Finding[]
--- @field warnings Finding[]
--- @class InvocationRecord
--- Lives at `atom.paths.invocations[*]`. Constructed once at the single invocation-construction site
@@ -171,7 +162,7 @@ local E_MAC_PREFIX_LEN = 4 ---@type integer
--- @field end_word integer -- 1-based items index of the `invoke_end` item (set by `emit_invoke_end`)
--- @field word_count integer -- Number of `word` items emitted between `start_word` and `end_word` (inclusive)
--- @field debug_skip boolean -- `debug_skip` stamp; true iff `corpus.components[name].debug_skip` is true at construction. Always boolean (never `nil`).
--- @field errors EmitError[]
--- @field errors Finding[]
-- Internal recursive walker. The items stream holds every emitted event in order; `word_events`, `markers`,
-- `invocations`, `errors`, `warnings` are dense views / side outputs appended alongside.
@@ -187,9 +178,9 @@ local E_MAC_PREFIX_LEN = 4 ---@type integer
-- * Unknown uncounted macros emit one opaque word + one warning. Unknown metadata-backed macros (entry in `word_counts`) emit the declared word count, no warning.
-- * Cycle detection uses an active DFS stack (`visiting`); a cycle appends a construction error to BOTH the projection errors and the cycle invocation's own errors,
-- then breaks out without recursing (the cycle entry still receives an invocation ID + paired `invoke_begin` / `invoke_end` items, so the boundary invariant is preserved).
-- * Component declared-count mismatch (declared vs. measured) is a construction error (kind = "count_mismatch"); recorded on the invocation record and pass-level errors list.
-- * Component declared-count mismatch (declared vs. measured) is a construction error (check = "count_mismatch"); recorded on the invocation record and pass-level errors list.
-- * Final boundary check: if any invocation is still open at end of walk, surface a "unbalanced" construction error.
--- @param root_body_entry ComponentBodyEntry
--- @param root_body_entry Component
--- @param ctx_table EmissionWalkCtx
--- @return EmissionProjection
local function _project_emission_inner(root_body_entry, ctx_table)
@@ -197,8 +188,8 @@ local function _project_emission_inner(root_body_entry, ctx_table)
local word_events = {} ---@type WordEvent[]
local markers = {} ---@type EmissionMarker[]
local invocations = {} ---@type InvocationRecord[]
local errors = {} ---@type EmitError[]
local warnings = {} ---@type EmitWarning[]
local errors = {} ---@type Finding[]
local warnings = {} ---@type Finding[]
local word_idx = 0 ---@type integer
local invocation_stack = {} ---@type InvocationRecord[] -- stack of currently-open invocation records
@@ -282,7 +273,8 @@ local function _project_emission_inner(root_body_entry, ctx_table)
gpr_keys[pos] = key
if unresolved then
errors[#errors + 1] = {
kind = "reguse_unresolved",
kind = "error",
check = "reguse_unresolved",
line = line,
msg = string.format("RegUse operand %q does not resolve in schema %q",
effective, (reg_use_schema and reg_use_schema.name) or "?"),
@@ -294,7 +286,8 @@ local function _project_emission_inner(root_body_entry, ctx_table)
for _, wpos in ipairs(row.writes) do ---@type integer, integer
if wpos == pos then
errors[#errors + 1] = {
kind = "reguse_const_write",
kind = "error",
check = "reguse_const_write",
line = line,
msg = string.format("RegUse slot %q is Reg const; %s writes it",
slot, encoder),
@@ -541,15 +534,14 @@ local function _project_emission_inner(root_body_entry, ctx_table)
-- The stamp is resolved from the `corpus.components[name]` registry (passed in via `ctx_table.components` by `emission_model.run`),
-- Unmarked components stamp `false` (not `nil`) so consumers can dispatch on the boolean without nil checks.
--
-- The walker has already found the component body in `ctx_table.component_index[component_name]`, so the matching entry MUST exist in `ctx_table.components[component_name]`
-- (both registries are populated from the same source by the components pass).
-- The walker has already found the component body in `ctx_table.components[component_name]`.
-- A missing entry is a corpus-plumbing bug; we fail loudly here rather than silently stamp `false` and mask the regression.
local components = ctx_table.components ---@type table<string, ComponentDef>|nil
local component_def = components and components[component_name] or nil ---@type ComponentDef|nil
local components = ctx_table.components ---@type table<string, Component>|nil
local component_def = components and components[component_name] or nil ---@type Component|nil
if not component_def then
error("duffle.emit_invoke_begin: component " .. string.format("%q", component_name)
.. " is present in `component_index` (the walker matched a `mac_" .. component_name .. "()` call) but absent from `components` (the canonical corpus.components registry). "
.. "This is a corpus-plumbing bug — the components pass must populate corpus.components[name] for every component it puts in corpus.component_body_index[name]. "
.. " is present in the walk (the walker matched a `mac_" .. component_name .. "()` call) but absent from `components` (the canonical corpus.components registry). "
.. "This is a corpus-plumbing bug — the components pass must populate corpus.components[name] for every expanded component. "
.. "The emission pass refuses to silently stamp `debug_skip = false` for a missing registry entry."
, 0
)
@@ -621,9 +613,10 @@ local function _project_emission_inner(root_body_entry, ctx_table)
local canon = M.gte_canon(ident) ---@type string
if canon ~= ident and wc and wc[canon] then return wc[canon] end
warnings[#warnings + 1] = {
kind = "uncounted",
kind = "warning",
check = "uncounted",
line = tok_line,
msg = string.format("project_emission: opaque word emitted for %q (no entry in word_counts or component_index)",
msg = string.format("project_emission: opaque word emitted for %q (no entry in word_counts or components)",
ident),
}
return 1
@@ -634,19 +627,19 @@ local function _project_emission_inner(root_body_entry, ctx_table)
-- walk_root_call_text: Outermost `mac_X(...)` token text (preserved across recursion).
-- walk_immediate_call_text: IMMEDIATE outer `mac_X(...)` token text for words emitted in this body — nil for the root atom body.
-- Two trackers are propagated as separate parameters so words deep inside nested expansions correctly identify both their immediate call site and the outermost call site.
--- @param body_entry ComponentBodyEntry
--- @param body_entry Component
--- @param walk_parent_inv_id integer
--- @param walk_root_call_text string|nil
--- @param walk_immediate_call_text string|nil
--- @param sub_map table<string, string>|nil
--- @return nil
local function walk_body_entry(body_entry, walk_parent_inv_id,
walk_root_call_text, walk_immediate_call_text)
walk_root_call_text, walk_immediate_call_text, sub_map)
local tokens = body_entry.body_tokens or {} ---@type BodyToken[]
local body_off = body_entry.body_off or 0 ---@type integer
local line_of = body_entry.line_of or M.LineIndex("") ---@type LineIndexFn
local def_source = body_entry.source or "" ---@type string
local def_line = body_entry.declaration or 0 ---@type integer
local sub_map = body_entry.sub_map ---@type table<string, string>|nil
local def_line = body_entry.line or 0 ---@type integer
-- Per-token dispatch: each matched branch returns; only the fall-through
-- "opaque word" emit handles direct encoders + mac_X-without-component.
--- @param bt BodyToken
@@ -716,7 +709,7 @@ local function _project_emission_inner(root_body_entry, ctx_table)
end
if ident:sub(1, 4) == "mac_" then
local bare = ident:sub(5) ---@type string
local comp = ctx_table.component_index[bare] ---@type ComponentBodyEntry|nil
local comp = ctx_table.components[bare] ---@type Component|nil
if comp then
local invocation_root_call_text = walk_root_call_text or tok ---@type string
if ctx_table.visiting[bare] then
@@ -724,8 +717,9 @@ local function _project_emission_inner(root_body_entry, ctx_table)
local inv = emit_invoke_begin(comp.kind or "comp_bare", bare, tok, invocation_root_call_text, def_source, tok_line) ---@type InvocationRecord
inv.parent_id = walk_parent_inv_id
inv.call_text = tok
local err = { ---@type EmitError
kind = "cycle",
local err = { ---@type Finding
kind = "error",
check = "cycle",
msg = string.format("project_emission: component cycle detected: %q", bare),
source = def_source,
line = tok_line,
@@ -741,12 +735,12 @@ local function _project_emission_inner(root_body_entry, ctx_table)
inv.parent_id = walk_parent_inv_id
inv.call_text = tok
inv.def_path = comp.source
inv.def_line = comp.declaration
inv.def_line = comp.line
-- Propagate trackers into the recursive walk:
-- immediate_call_text = this call's tok (the IMMEDIATE outer call for words emitted in this body)
-- root_call_text = the OUTERMOST call (immutable across the recursion)
local formal_names = ctx_table.component_index[bare] ---@type string[]|nil
and ctx_table.component_index[bare].arg_names
-- child_map = per-invocation formal substitution; stays on the walk stack
local formal_names = comp.arg_names ---@type string[]|nil
local child_map = nil ---@type table<string, string>|nil
if formal_names then
child_map = {}
@@ -754,17 +748,11 @@ local function _project_emission_inner(root_body_entry, ctx_table)
child_map[fname] = apply_sub(sub_map, args[i])
end
end
walk_body_entry({
body_tokens = comp.body_tokens or {},
body_off = comp.body_off or 0,
line_of = comp.line_of,
source = comp.source,
declaration = comp.declaration,
sub_map = child_map,
},
walk_body_entry(comp,
inv.id,
invocation_root_call_text,
tok)
tok,
child_map)
ctx_table.visiting[bare] = nil
emit_invoke_end(inv)
-- Count `word` items inside [start_word, end_word].
@@ -780,8 +768,9 @@ local function _project_emission_inner(root_body_entry, ctx_table)
-- We compare against the measured word count.
local declared = ctx_table.word_counts["mac_" .. bare] ---@type integer|nil
if declared and wc_inside ~= declared then
local err = { ---@type EmitError
kind = "count_mismatch",
local err = { ---@type Finding
kind = "error",
check = "count_mismatch",
msg = string.format("project_emission: mac_%s declared=%d measured=%d", bare, declared, wc_inside),
source = def_source,
line = tok_line,
@@ -791,7 +780,7 @@ local function _project_emission_inner(root_body_entry, ctx_table)
end
return
end
-- mac_X NOT in component_index: fall through to opaque emit.
-- mac_X NOT in components: fall through to opaque emit.
end
-- Direct encoder, or mac_X-without-component: resolve count + emit n words.
-- Resolve_count may emit a warning if the count is unresolved.
@@ -816,13 +805,14 @@ local function _project_emission_inner(root_body_entry, ctx_table)
-- Walk first; the pass caller stamps the root call site for direct words after the projection returns.
-- For nested words the def_path / def_line already point at the component source and MUST be preserved (the stamping helper checks for that).
walk_body_entry(root_body_entry, 0, nil, nil)
walk_body_entry(root_body_entry, 0, nil, nil, nil)
-- Boundary check: every invoke_begin must have a matching invoke_end.
-- If anything is still open, surface a hard error.
if #invocation_stack > 0 then
errors[#errors + 1] = {
kind = "unbalanced",
kind = "error",
check = "unbalanced",
msg = string.format("project_emission: invocation boundaries not balanced (%d unclosed invocation(s) at end of walk)", #invocation_stack),
}
end
@@ -850,7 +840,7 @@ end
--- * `mac_X(...)` calls: emit `invoke_begin` (zero-width), recurse into the component body, emit `invoke_end` (zero-width).
--- The component body's words land between the begin/end pair; one invocation record is allocated per call (monotonic ID per atom).
--- * Unknown uncounted macros emit 1 opaque word + one warning per occurrence.
--- * Tokens whose count cannot be resolved (e.g. `mac_unknown` not in word_counts and not in component_index) surface one
--- * Tokens whose count cannot be resolved (e.g. `mac_unknown` not in word_counts and not in components) surface one
--- warning; cycle + count-mismatch + boundary violations are construction errors on `pass.errors`.
---
--- Every emitted `word` carries: `i` (0-based word index), `encoder`, `args` (top-level args), `def_path`, `def_line`,
@@ -860,15 +850,15 @@ end
--- for the open invocation stack at that word.
---
--- @param body_text string -- the raw atom body string
--- @param component_index table<string, ComponentBodyEntry>
--- @param component_index table<string, Component>
--- @param word_counts WordCounts
--- @param components table<string, ComponentDef>
--- @param components table<string, Component>
--- `invocation.debug_skip`. A missing or non-table `components` raises a fail-loud error rather than silently falling back.
--- @return EmissionProjection
--- @param reg_use_ctx RegUseCtx|nil
function M.project_emission(body_text, component_index, word_counts, components, reg_use_ctx)
-- The recursive walk delegates to `_project_emission_inner` so component bodies (which arrive as
-- `{body_tokens, body_off, line_of, source, declaration}` records from `corpus.component_body_index`)
-- The recursive walk delegates to `_project_emission_inner` so component bodies
-- (the `corpus.components` row: body_tokens / body_off / line_of / source / line)
-- re-enter the same walker with the same shared output state.
--
-- The walker is body-relative: it builds `line_of` from `body_text` and stamps body-relative line numbers (1..N)
@@ -898,17 +888,17 @@ function M.project_emission(body_text, component_index, word_counts, components,
end
local tokens = M.tokenize_body(body_text) ---@type BodyToken[]
local comps = components or component_index or {} ---@type table<string, Component>
return _project_emission_inner({
body_tokens = tokens,
body_off = 0,
line_of = M.LineIndex(body_text),
source = "",
declaration = 0,
line = 0,
},
{
component_index = component_index or {},
components = comps,
word_counts = word_counts or {},
components = components,
reg_use_schema = reg_use_ctx and reg_use_ctx.reg_use_schema,
reg_use_param = reg_use_ctx and reg_use_ctx.reg_use_param,
atom_name = reg_use_ctx and reg_use_ctx.atom_name,
+44
View File
@@ -88,7 +88,14 @@
--- @field visibility_kind string
--- @field evidence HardwareRelationEvidence
--- @class GprRole
--- @field name string
--- @field pool boolean
--- @field optional boolean
--- @field carrier boolean
--- @class DuffleIsa
--- @field GPR_ROLE table<string, GprRole>
--- @field TAPE_ATOM_MACROS table<string, TapeAtomMacroRow>
--- @field DELAY_MARKERS table<string, boolean>
--- @field INSTRUCTION table<string, InstructionRow>
@@ -111,6 +118,43 @@ local M = {} ---@type DuffleIsa
-- Section 7: domain tables
-- ════════════════════════════════════════════════════════════════════════════
-- One GprRole row per name. Construction order is the auto_reg pool order,
-- then R_AT, then the three carriers. Index by name into M.GPR_ROLE.
--- @type table<string, GprRole>
M.GPR_ROLE = {
{ name = "R_V0", pool = true, optional = true, carrier = false },
{ name = "R_V1", pool = true, optional = true, carrier = false },
{ name = "R_T0", pool = true, optional = true, carrier = false },
{ name = "R_T1", pool = true, optional = true, carrier = false },
{ name = "R_T2", pool = true, optional = true, carrier = false },
{ name = "R_T3", pool = true, optional = true, carrier = false },
{ name = "R_T4", pool = true, optional = true, carrier = false },
{ name = "R_T5", pool = true, optional = true, carrier = false },
{ name = "R_T6", pool = true, optional = true, carrier = false },
{ name = "R_T7", pool = true, optional = true, carrier = false },
{ name = "R_A0", pool = true, optional = true, carrier = false },
{ name = "R_A1", pool = true, optional = true, carrier = false },
{ name = "R_A2", pool = true, optional = true, carrier = false },
{ name = "R_A3", pool = true, optional = true, carrier = false },
{ name = "R_S0", pool = true, optional = true, carrier = false },
{ name = "R_S1", pool = true, optional = true, carrier = false },
{ name = "R_S2", pool = true, optional = true, carrier = false },
{ name = "R_S3", pool = true, optional = true, carrier = false },
{ name = "R_S4", pool = true, optional = true, carrier = false },
{ name = "R_S5", pool = true, optional = true, carrier = false },
{ name = "R_S6", pool = true, optional = true, carrier = false },
{ name = "R_S7", pool = true, optional = true, carrier = false },
{ name = "R_T8", pool = true, optional = true, carrier = false },
{ name = "R_T9", pool = true, optional = true, carrier = false },
{ name = "R_AT", pool = false, optional = true, carrier = false },
{ name = "R_TapePtr", pool = false, optional = true, carrier = true },
{ name = "R_AtomJmp", pool = false, optional = true, carrier = true },
{ name = "R_ScratchBase", pool = false, optional = true, carrier = true },
}
for _, row in ipairs(M.GPR_ROLE) do ---@type integer, GprRole
M.GPR_ROLE[row.name] = row
end
-- atom_info sub-calls: atom_bind, atom_reads, atom_writes, atom_view, atom_reg_types, atom_ctx, atom_phase.
--- @type table<string, TapeAtomMacroRow>
M.TAPE_ATOM_MACROS = {
+59
View File
@@ -59,6 +59,10 @@
--- @field unity_root Path
--- @field project_root Path
--- @class ExactResolveOptions
--- @field sources string[]
--- @field project_root Path
--- @alias LineIndexFn fun(query_pos: integer): integer
--- @class DuffleScan
@@ -914,6 +918,61 @@ function M.resolve_source_corpus(options)
}
end
--- Exact `--source` corpus. No include expansion. Paths stay as given (normalized).
--- Same return shape as resolve_source_corpus.
--- @param options ExactResolveOptions
--- @return Corpus
function M.resolve_exact_sources(options)
if type(options) ~= "table" then error("resolve_exact_sources requires options", 2) end
if type(options.project_root) ~= "string" or options.project_root == "" then
error("resolve_exact_sources requires options.project_root", 2)
end
local sources = options.sources ---@type string[]|nil
if type(sources) ~= "table" then error("resolve_exact_sources requires options.sources", 2) end
local project_root = options.project_root ---@type Path
local code_root = M.normalize_path(project_root .. "/code") ---@type Path
local source_order = {} ---@type SourceFile[]
local sources_by_path = {} ---@type table<Path, SourceFile>
local resolver = { ---@type SourceResolver
resolved = {},
skipped = {},
shadowed = {},
}
for _, input_path in ipairs(sources) do ---@type integer, string
local path = M.normalize_path(input_path) ---@type string
M.canonical_path_key(path)
local source = { ---@type SourceFile
path = path,
text = M.read_file(path),
dir = M.dirname(path),
basename = M.basename_no_ext(path),
}
source_order[#source_order + 1] = source
local key = M.canonical_path_key(path) ---@type string
if not sources_by_path[key] then sources_by_path[key] = source end
resolver.resolved[#resolver.resolved + 1] = {
include_path = path,
include_text = nil,
root_source = path,
root_line = 1,
candidate_a = path,
candidate_b = nil,
selected_path = path,
disposition = "exact",
}
end
return {
unity_root = nil,
project_root = project_root,
code_root = code_root,
source_order = source_order,
sources_by_path = sources_by_path,
sources_by_dir = M.group_sources_by_dir(source_order),
resolver = resolver,
}
end
-- Split a brace-body into top-level comma-separated tokens. Honors nested parens/braces/brackets and skips strings/comments.
-- Splits at top-level NEWLINES and SEMICOLONS too, AND emits a token break after a top-level comment/string.
-- Pure-comment / pure-string chunks contribute 0 words.
+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
+23 -54
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,7 +56,6 @@ 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
@@ -70,6 +69,11 @@ local MACS_FILENAME = "macs.h" ---@type string
--- @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 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.
+32 -29
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",
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,18 +321,22 @@ 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,
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,
kind = "warning",
check = w.check,
line = w.line,
msg = w.msg,
}
+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
+24 -42
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
@@ -425,10 +406,10 @@ end
--- @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>
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,
+49 -58
View File
@@ -76,8 +76,7 @@ local PASS_FLAG_DISPATCH_KEY = "__pass__" ---@type string
--- @field atom_ctxs table<AtomName, AtomCtxEntry>
--- @field atom_phases table<string, AtomPhaseGroup>
--- @field word_counts WordCounts
--- @field components table<string, ComponentDef>
--- @field component_body_index table<string, ComponentBodyEntry>
--- @field components table<string, Component>
--- @field collisions CorpusCollision[]
--- @field resolver SourceResolver
--- @field component_atom_infos AtomInfoEntry[]|nil
@@ -104,9 +103,43 @@ local PASS_FLAG_DISPATCH_KEY = "__pass__" ---@type string
--- @field flags PassFlags -- CLI flags + per-pass stash
--- @field verbose boolean -- If true, log diagnostic info
--- @class PassFinding
--- @field line integer -- Source line (or 0 for pass-level)
--- @field msg string -- Finding message
--- CheckName: see static_analysis.lua. AtomName: see duffle.lua.
--- @class Finding
--- @field line integer
--- @field msg string
--- @field kind string|nil -- error | warning | info
--- @field atom AtomName|nil
--- @field check CheckName|nil
--- @field source string|nil -- optional; emit/reguse path
--- @field schema_name string|nil -- optional; emit/reguse
--- @class PassScratch
--- @field corpus Corpus|nil
--- @field info_by_atom table<string, AtomInfoEntry>|nil
--- @field binds_index table<string, BindsEntry>|nil
--- @field atom_index table<string, AtomEntry>|nil
--- @field annot_counts table<string, integer>|nil -- bag
--- @field types table<string, RegTypeDefault>|nil
--- @field atom_views table<string, AtomViewEntry>|nil
--- @field seen_defaults table<string, integer>|nil -- bag
--- @field seen_field table<string, integer>|nil -- bag
--- @field _scan SourceScan|nil
--- @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
--- @field unknown_seen table<string, integer>|nil -- bag
--- @field atoms AtomEntry[]|nil
--- @field components_by_name table<string, Component>|nil
--- @field atoms_by_name table<string, AtomEntry>|nil
--- @field tape_chains table<string, string[]>|nil
--- @field source_order SourceFile[]|nil
--- @field component_atom_infos AtomInfoEntry[]|nil
--- @field atom_infos_all AtomInfoEntry[]|nil
--- @field gte_cr_alias_groups GteCrAliasGroup[]|nil
--- @field line_for_word_event (fun(ev: WordEvent): integer)|nil
--- @class PassOutputEntry
--- @field kind string
@@ -114,9 +147,9 @@ local PASS_FLAG_DISPATCH_KEY = "__pass__" ---@type string
--- @class PassResult
--- @field outputs PassOutputEntry[]
--- @field errors PassFinding[] -- Build-stops (per-pass kind policy)
--- @field warnings PassFinding[] -- Informational
--- @field info CheckFinding[]|nil -- static_analysis only
--- @field errors Finding[] -- Build-stops (per-pass kind policy)
--- @field warnings Finding[] -- Informational
--- @field info Finding[]|nil -- static_analysis only
--- @class ParsedArgs
--- @field requested_set string[] -- Pass names to run (explicit --all expanded)
@@ -604,56 +637,15 @@ local function build_ctx(args)
end
resolution = resolved
else
local source_order = {} ---@type SourceFile[]
local sources_by_path = {} ---@type table<Path, SourceFile>
local resolver = { ---@type SourceResolver
resolved = {},
skipped = {},
shadowed = {},
}
for _, input_path in ipairs(args.sources) do ---@type integer, string
local path = duffle.normalize_path(input_path) ---@type string
local key_ok, key_or_error = pcall(duffle.canonical_path_key, path) ---@type boolean, string
if not key_ok then
error("ps1_meta: invalid --source " .. input_path .. ": " .. tostring(key_or_error), 0)
end
local file = io.open(path, "r") ---@type file*|nil
if not file then
io.stderr:write("ps1_meta: cannot open --source " .. input_path .. "\n")
local ok_exact, exact = pcall(duffle.resolve_exact_sources, { ---@type boolean, Corpus|string
sources = args.sources,
project_root = project_root,
})
if not ok_exact then
io.stderr:write("ps1_meta: cannot resolve --source: " .. tostring(exact) .. "\n")
os.exit(EXIT_INTERNAL_ERROR)
end
local text = file:read("*a") ---@type string
file:close()
local source = { ---@type SourceFile
path = path,
text = text,
dir = duffle.dirname(path),
basename = duffle.basename_no_ext(path),
}
source_order[#source_order + 1] = source
local key = key_or_error ---@type string
if not sources_by_path[key] then sources_by_path[key] = source end
resolver.resolved[#resolver.resolved + 1] = {
include_path = path,
include_text = nil,
root_source = nil,
root_line = nil,
candidate_a = path,
candidate_b = nil,
selected_path = path,
disposition = "exact",
}
end
resolution = {
unity_root = nil,
project_root = project_root,
code_root = duffle.normalize_path(project_root .. "/code"),
source_order = source_order,
sources_by_path = sources_by_path,
sources_by_dir = duffle.group_sources_by_dir(source_order),
resolver = resolver,
}
resolution = exact
end
local corpus = { ---@type Corpus
@@ -673,7 +665,6 @@ local function build_ctx(args)
atom_phases = {},
word_counts = {},
components = {},
component_body_index = {},
collisions = {},
resolver = resolution.resolver,
}
@@ -791,7 +782,7 @@ end
local function report_validation_errors(pass_name, pass, result)
local has_errors = result.errors and #result.errors > 0 ---@type boolean
if not has_errors then return false end
for _, e in ipairs(result.errors) do ---@type integer, PassFinding
for _, e in ipairs(result.errors) do ---@type integer, Finding
io.stderr:write(string.format("[%s] line %d: %s\n", pass_name, e.line or 0, e.msg or ""))
end
return PASS_KIND_STOP_ON_ERROR[pass.kind] == true