mirror of
https://github.com/Ed94/pikuma_ps1.git
synced 2026-08-25 02:20:33 +00:00
Improving program model, (better structs, better path awareness ties to those structs).
This commit is contained in:
+4
-6
@@ -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
@@ -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,
|
||||
|
||||
@@ -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,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.
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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,
|
||||
}
|
||||
|
||||
@@ -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
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
+472
-432
File diff suppressed because it is too large
Load Diff
@@ -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
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user