wip: going over all codepaths.

This commit is contained in:
ed
2026-08-16 10:35:32 -04:00
parent 3faccfc283
commit 581b00b960
8 changed files with 3843 additions and 3324 deletions
+34 -2979
View File
File diff suppressed because it is too large Load Diff
+915
View File
@@ -0,0 +1,915 @@
--- duffle_emit.lua — project_emission + decl finders.
local scan = require("duffle_scan")
local isa = require("duffle_isa")
local M = {}
for k, v in pairs(scan) do M[k] = v end
for k, v in pairs(isa) do M[k] = v end
-- Section 8: Cross-source component-body index + word-event expansion
-- ════════════════════════════════════════════════════════════════════════════
--
-- 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 table -- pre-tokenized {{tok=string, rel=integer}, ...}
--- @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"
-- The cross-source component-body index is owned by the corpus (`corpus.component_body_index`, 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).
local E_BYTE_OPEN_PAREN = 0x28
local E_BYTE_OPEN_BRACE = 0x7B
local E_BYTE_OPEN_BRACK = 0x5B
local E_BYTE_DQUOTE = 0x22
local E_BYTE_SQUOTE = 0x27
local E_BYTE_COMMA = 0x2C
-- Map an open-delimiter byte to its matching close string for read_balanced.
local E_OPEN_CLOSE = {
[E_BYTE_OPEN_PAREN] = ")",
[E_BYTE_OPEN_BRACE] = "}",
[E_BYTE_OPEN_BRACK] = "]",
}
--- Split the INSIDE of a `f(...)` call on top-level commas.
--- Honors nested parens / braces / brackets and skips strings / comments.
--- Returns a list of trimmed argument strings in source order.
--- (Mirrors split_top_level_commas but for paren-body args; intentionally distinct so a caller's brace-body split isn't confused with an arg list.)
--- @param inner string
--- @return string[]
local function split_call_args(inner)
local args = {}
if not inner or inner == "" then return args end
local pos = 1
local len = #inner
local start = 1
while pos <= len do
local c = inner:byte(pos)
local close = E_OPEN_CLOSE[c]
if close then
local _, after = M.read_balanced(inner, string.char(c), close, pos)
pos = after
elseif c == E_BYTE_DQUOTE or c == E_BYTE_SQUOTE then
pos = M.skip_str_or_cmt(inner, pos)
elseif c == E_BYTE_COMMA then
args[#args + 1] = M.trim(inner:sub(start, pos - 1))
start = pos + 1
pos = pos + 1
else
pos = pos + 1
end
end
if start <= len then args[#args + 1] = M.trim(inner:sub(start, len)) end
return args
end
--- Extract the leading identifier + top-level args list from a token string.
--- Returns (ident, args). For tokens without a `(...)` call, args is `{}`.
--- @param tok string
--- @return string, string[]
local function token_ident_and_args(tok)
local ident, after = M.read_ident(tok, 1)
if not ident then return "?", {} end
local paren_pos = M.skip_ws_and_cmt(tok, after)
if tok:sub(paren_pos, paren_pos) ~= "(" then return ident, {} end
local inner = M.read_parens(tok, paren_pos)
if not inner then return ident, {} end
return ident, split_call_args(inner)
end
-- The macro-name prefix that marks a `mac_X(...)` component invocation.
local E_MAC_PREFIX = "mac_"
local E_MAC_PREFIX_LEN = 4
--- Expand a body entry into the flat sequence of emitted machine-word events.
---
--- Semantics (one event per emitted machine word):
--- * Direct one-word encoders `load_word`, `add_ui`, `nop`, `gte_lw`, ...: One event with `ident` = leading ident, `args` = parsed top-level args.
--- * `nop2` (2-word pseudo-instruction): Two events, both with `ident = "nop"` so the recognized "this slot is a no-op" semantic is visible to downstream analyses.
--- * Any other N-word token in `word_counts`: N events sharing the same `ident` + `args` so useful CPU words retire slots in the cycle budget.
--- * 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.
--- * 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).
---
--- Pure: reads `body_entry` / `component_index` / `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 table -- `{body_tokens, body_off, line_of, source, declaration}` (declaration = root atom's atom.line)
--- @param component_index table -- the bare-name → ComponentBodyEntry map from M.get_component_body_index
--- @param word_counts table -- macro name → emitted-word count (from `ctx.shared.word_counts`)
--- @return WordEvent[], WordEventError[]
-- ════════════════════════════════════════════════════════════════════════════
-- Section 11: project_emission (per-atom emission projection)
-- ════════════════════════════════════════════════════════════════════════════
--
-- Per-atom emission projection is owned by `passes/emission_model.lua`.
-- The projection is built from the root atom body only; invocation ancestry recursively expands nested components.
-- 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.
-- word_counts table is authored-metadata + current-component count table.
--- @class EmissionProjection
--- @field items table[] -- Ordered stream of word|label|offset|invoke_begin|invoke_end
--- @field word_events table[] -- Dense view of items where kind == "word"
--- @field markers table[] -- Dense view of items where kind == "label"|"offset"
--- @field invocations InvocationRecord[] -- dense view of items where kind == "invoke_begin"|"invoke_end"
--- @field errors table[] -- Token-resolution failures surfaced without fail-loud
--- @field warnings table[] -- Opaque warnings (e.g. unknown uncounted macro)
--- @class InvocationRecord
--- Lives at `atom.paths.invocations[*]`. Constructed once at the single invocation-construction site
--- (`emit_invoke_begin` inside `_project_emission_inner`); `invoke_begin` / `invoke_end` markers in the items stream share the same `id`.
--- @field id integer -- 1-based, monotonic per-atom invocation id (0 is reserved for "no open invocation")
--- @field parent_id integer -- 0 for the outermost (root) call; otherwise the id of the immediately enclosing invocation
--- @field kind string -- "comp_bare" | "comp_proc" (component form that triggered the expansion)
--- @field component_name string -- Bare component name without the `mac_` prefix
--- @field call_text string -- Immediate `mac_X(...)` token text (or root call text for the outermost entry)
--- @field root_call_text string -- IMMUTABLE outermost `mac_X(...)` token text for every word emitted in this call's expansion
--- @field call_path string -- Source path of the call site (root atom source for direct calls, component source for nested expansions)
--- @field call_line integer -- Source line of the call site
--- @field def_path string -- Source path of the component definition
--- @field def_line integer -- Source line of the component declaration
--- @field start_pos integer -- 0-based emitted-word position of the FIRST word inside this invocation (the value of `word_idx` AT `emit_invoke_begin` time, BEFORE the first word is emitted). Words emitted inside this invocation occupy `start_pos..start_pos+#body_lines-1` (inclusive, 0-based). Downstream DWARF/provenance consumers MUST read this; do NOT reconstruct it from `start_word` (which is the 1-based items index including `invoke_begin`/`invoke_end` markers).
--- @field end_pos integer -- 0-based position of the LAST word inside this invocation (set by `emit_invoke_end` to `word_idx - 1` AFTER all body words are emitted).
--- @field start_word integer -- 1-based items index of the `invoke_begin` item
--- @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 table[] -- Per-invocation construction errors (cycle / count_mismatch); does not include pass-level errors
-- 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.
--
-- Output rules:
-- * `word` items record: `invocation_ids` (innermost last) and `outermost_invocation_id` (0 if no invocation is open).
-- * `invoke_begin` / `invoke_end` items are zero-width at the current word index; the same `word_index` is recorded on both.
-- * `root_call_text` is the outermost `mac_X(...)` token text for every word emitted inside a component expansion;
-- it is `nil` for direct words emitted from the root atom body.
-- * `call_text` is the IMMEDIATE top-level token spelling for the word (for nested words this is the inner `mac_X(...)` token;
-- for direct words it is the trimmed encoder token).
-- * `def_path` / `def_line` are the definition site of the current body (component source for nested words; root atom source for direct words, filled in by the pass caller).
-- * 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.
-- * Final boundary check: if any invocation is still open at end of walk, surface a "unbalanced" construction error.
local function _project_emission_inner(root_body_entry, ctx_table)
local items = {}
local word_events = {}
local markers = {}
local invocations = {}
local errors = {}
local warnings = {}
local word_idx = 0
local invocation_stack = {} -- stack of currently-open invocation records
local next_inv_id = 0
local reg_use_schema = ctx_table.reg_use_schema
local reg_use_param = ctx_table.reg_use_param
local atom_name = ctx_table.atom_name
local slot_readonly = {}
if reg_use_schema then
for _, slot in ipairs(reg_use_schema.slots or {}) do
slot_readonly[slot.name] = slot.readonly == true
end
end
local function apply_sub(sub_map, operand)
if not (sub_map and type(operand) == "string") then return operand end
if sub_map[operand] then return sub_map[operand] end
local dot = operand:find(".", 1, true)
if dot then
local head = operand:sub(1, dot - 1)
local mapped = sub_map[head]
if type(mapped) == "string" then
return mapped .. operand:sub(dot)
end
end
return operand
end
local function resolve_gpr_key(operand)
if type(operand) ~= "string" then return nil end
if operand:sub(1, 2) == "R_" then return operand end
if not (reg_use_schema and reg_use_param) then return nil end
local prefix = reg_use_param .. "."
if operand:sub(1, #prefix) ~= prefix then return nil end
local member_path = operand:sub(#prefix + 1)
local slot = reg_use_schema.alias_to_slot[member_path]
if not slot then return nil, member_path end
return "reguse:" .. atom_name .. ":" .. slot, nil, slot
end
local function open_invocation_ids_snapshot()
local ids = {}
for _, inv in ipairs(invocation_stack) do
ids[#ids + 1] = inv.id
end
return ids
end
local function emit_word(encoder, args, line, word_call_text,
def_source_now, def_line_now,
immediate_call_text, root_call_text_w, sub_map)
local inv_ids = open_invocation_ids_snapshot()
local outermost = inv_ids[1] or 0
-- For words emitted at the root atom body, `immediate_call_text` is nil and the walker's `word_call_text` (the word's own token, e.g. "nop") becomes the effective call_text.
-- For words emitted inside a component expansion, `immediate_call_text` is the immediate outer `mac_X(...)` token text;
-- The call that triggered the body expansion we're currently walking.
local eff_call_text = immediate_call_text or word_call_text
local eff_root_call_text = root_call_text_w
local gpr_keys = nil
if reg_use_schema or sub_map then
gpr_keys = {}
for pos, arg in ipairs(args or {}) do
local effective = apply_sub(sub_map, arg)
local key, unresolved, slot = resolve_gpr_key(effective)
gpr_keys[pos] = key
if unresolved then
errors[#errors + 1] = {
kind = "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 "?"),
}
end
if key and slot and slot_readonly[slot] then
local row = M.instr(encoder)
if row and row.writes then
for _, wpos in ipairs(row.writes) do
if wpos == pos then
errors[#errors + 1] = {
kind = "reguse_const_write",
line = line,
msg = string.format("RegUse slot %q is Reg const; %s writes it",
slot, encoder),
}
end
end
end
end
end
end
if not reg_use_schema then
gpr_keys = nil
end
items[#items + 1] = {
kind = "word",
encoder = encoder,
args = args,
i = word_idx,
word_count = 1,
line = line,
call_text = eff_call_text,
root_call_text = eff_root_call_text,
invocation_ids = inv_ids,
outermost_invocation_id = outermost,
gpr_keys = gpr_keys,
}
word_events[#word_events + 1] = {
i = word_idx,
encoder = encoder,
args = args,
def_path = def_source_now or "",
def_line = def_line_now or 0,
call_text = eff_call_text,
root_call_text = eff_root_call_text,
invocation_ids = inv_ids,
outermost_invocation_id = outermost,
word_count = 1,
gpr_keys = gpr_keys,
}
word_idx = word_idx + 1
end
local function emit_marker(kind, name, target, line,
immediate_call_text, root_call_text_w,
consuming_encoder, consuming_arg_pos)
local inv_ids = open_invocation_ids_snapshot()
local outermost = inv_ids[1] or 0
-- Markers carry the open invocation stack snapshot. `call_text` / `root_call_text` belong to words, not markers — markers are zero-width and skip per-word call-site attribution.
-- `consuming_encoder` + `consuming_arg_pos` carry the surrounding control-transfer instruction context
-- (e.g. `branch_le_zero` consuming its 3rd argument, or `jump` / `call_addr` consuming their only argument).
-- `passes/offsets.lua` reads these to dispatch per-consuming-instruction offset encoding.
-- nil for top-level markers (where the marker is the entire token — no surrounding consuming instruction).
local it = {
kind = kind,
name = name,
line = line,
word_index = word_idx,
invocation_ids = inv_ids,
outermost_invocation_id = outermost,
}
if target ~= nil then it.target = target end
if consuming_encoder then it.consuming_encoder = consuming_encoder end
if consuming_arg_pos then it.consuming_arg_pos = consuming_arg_pos end
items[#items + 1] = it
markers[#markers + 1] = {
kind = kind,
name = name,
line = line,
word_index = word_idx,
target = target,
consuming_encoder = consuming_encoder,
consuming_arg_pos = consuming_arg_pos,
}
end
-- Count top-level commas in `tok` between position `from_pos` (inclusive) and `to_pos` (exclusive).
-- Tracks paren depth so commas inside nested () don't count. Skips string literals + comments.
-- Used by `emit_embedded_markers` to compute `consuming_arg_pos` for each embedded marker.
local function count_top_level_commas(tok, from_pos, to_pos)
local depth = 0
local count = 0
local i = from_pos
while i < to_pos do
local c = tok:sub(i, i)
if c == "'" or c == '"' then
local next_pos = M.skip_str_or_cmt(tok, i)
i = (next_pos > i) and next_pos or (i + 1)
elseif c == "/" and tok:sub(i + 1, i + 1) == "/" then
-- line comment: skip to end of line
local nl = tok:find("\n", i, true)
i = (nl and nl + 1) or (#tok + 1)
elseif c == "/" and tok:sub(i + 1, i + 1) == "*" then
-- block comment: skip to matching */
local close = tok:find("*/", i + 2, true)
i = (close and close + 2) or (#tok + 1)
elseif c == "(" then
depth = depth + 1
i = i + 1
elseif c == ")" then
depth = depth - 1
i = i + 1
elseif c == "," and depth == 0 then
count = count + 1
i = i + 1
else
i = i + 1
end
end
return count
end
-- Find the position of the consuming instruction's open paren (the `(` that starts the consuming instruction's argument list).
-- Returns nil if the token's leading text isn't an ident followed by `(` (e.g. the ident is at the start of a non-instruction token).
local function find_consuming_paren(tok)
local i = 1
while i <= #tok do
local c = tok:sub(i, i)
if c == "(" then return i end
if not c:match("[%w_]") and c ~= " " then return nil end
i = i + 1
end
return nil
end
local function emit_embedded_markers(tok, tok_line, consuming_encoder)
-- When called with a non-nil `consuming_encoder`, the marker is nested inside that instruction's argument list.
-- We compute each marker's arg position by counting top-level commas between the consuming instruction's `(` and the marker's start.
local consuming_paren = nil
if consuming_encoder then consuming_paren = find_consuming_paren(tok) end
local pos = 1
while pos <= #tok do
-- Trim leading whitespace and comments before each scan.
pos = M.skip_ws_and_cmt(tok, pos)
if pos > #tok then break end
local ident, after = M.read_ident(tok, pos)
if not ident then
-- Not an ident: token is a string or comment; skip or one-step.
local next_pos = M.skip_str_or_cmt(tok, pos)
pos = (next_pos > pos) and next_pos or (pos + 1)
goto continue_loop
end
if M.DELAY_MARKERS[ident] then
local arg_pos = nil
if consuming_encoder and consuming_paren then
arg_pos = count_top_level_commas(tok, consuming_paren + 1, pos) + 1
end
emit_marker("delay", ident, nil, tok_line, nil, nil, consuming_encoder, arg_pos)
pos = after
goto continue_loop
end
if ident ~= "atom_label" and ident ~= "atom_offset" then
-- Ordinary ident; nothing to emit, step past the ident only.
pos = after
goto continue_loop
end
-- Marker ident: parse the (...) arguments.
local open = M.skip_ws_and_cmt(tok, after)
local inner, after_paren = M.read_parens(tok, open)
if not inner then
-- (...) Unreadable: fall back to non-marker behavior.
pos = after
goto continue_loop
end
-- Commit: label takes 1 arg, offset takes 2.
-- For embedded markers, propagate the consuming_encoder + the marker's arg position
-- (1-based) so `passes/offsets.lua` can dispatch per-consuming-instruction offset encoding.
-- Top-level markers (no consuming_encoder) get nil for both — the offsets pass treats
-- them as branch-equivalent for backward compatibility.
local arg_pos = nil
if consuming_encoder and consuming_paren then
arg_pos = count_top_level_commas(tok, consuming_paren + 1, pos) + 1
end
local args = split_call_args(inner)
if ident == "atom_label" then emit_marker("label", args[1] or "", nil, tok_line, nil, nil, consuming_encoder, arg_pos)
else emit_marker("offset", args[1] or "", args[2] or "", tok_line, nil, nil, consuming_encoder, arg_pos)
end
pos = after_paren
::continue_loop::
end
end
local function emit_invoke_begin(inv_kind, component_name, call_text,
root_call_text, call_path, call_line)
next_inv_id = next_inv_id + 1
-- Invocation-level debug_skip stamp: Emission pass owns `atom.paths.invocations[*].debug_skip`.
-- 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).
-- 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
local component_def = components and components[component_name] or 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]. "
.. "The emission pass refuses to silently stamp `debug_skip = false` for a missing registry entry."
, 0
)
end
local debug_skip_stamp = component_def.debug_skip == true
local inv = {
id = next_inv_id,
parent_id = 0, -- patched below by caller
kind = inv_kind,
component_name = component_name,
call_text = call_text,
root_call_text = root_call_text,
call_path = call_path,
call_line = call_line,
def_path = nil, -- patched below after component lookup
def_line = nil,
-- 0-based emitted-word position. `word_idx` is the monotonic 0-based counter of `word` items emitted so far in this walk —
-- BEFORE this invocation's first word is emitted, it equals the position of the first word inside the invocation.
-- `start_word` (1-based items index of `invoke_begin`) is kept for items-walking consumers (Annotation pass bounds checks),
-- but DWARF / provenance rows MUST read `start_pos` because those rows are 1-based over the dense `word_events` stream (which has no `invoke_begin` items).
start_pos = word_idx,
start_word = #items + 1, -- 1-based items index of invoke_begin
end_pos = nil, -- patched by emit_invoke_end
end_word = nil, -- patched by emit_invoke_end
word_count = 0,
debug_skip = debug_skip_stamp,
errors = {},
}
invocations[#invocations + 1] = inv
items [#items + 1] = {
kind = "invoke_begin",
invocation_id = inv.id,
word_index = word_idx,
invocation_ids = open_invocation_ids_snapshot(),
}
invocation_stack[#invocation_stack + 1] = inv
return inv
end
local function emit_invoke_end(inv)
-- 0-based emitted-word position of the LAST word inside this invocation.
-- After the last body word was emitted, `word_idx` was incremented past it, so `word_idx - 1` is the 0-based position of the last word.
inv.end_pos = word_idx - 1
inv.end_word = #items + 1 -- 1-based items index of invoke_end
items[#items + 1] = {
kind = "invoke_end",
invocation_id = inv.id,
word_index = word_idx,
invocation_ids = open_invocation_ids_snapshot(),
}
for i = #invocation_stack, 1, -1 do
if invocation_stack[i] == inv then
table.remove(invocation_stack, i)
break
end
end
end
-- Resolve the per-token word count. If unresolved, surface ONE warning
-- and fall back to 1 opaque word so the cycle budget still accounts for the slot.
local function resolve_count(ident, tok_line)
local wc = ctx_table.word_counts
if wc and wc[ident] then return wc[ident] end
local canon = M.gte_canon(ident)
if canon ~= ident and wc and wc[canon] then return wc[canon] end
warnings[#warnings + 1] = {
kind = "uncounted",
line = tok_line,
msg = string.format("project_emission: opaque word emitted for %q (no entry in word_counts or component_index)",
ident),
}
return 1
end
-- Recursive walker: walk one body entry, possibly descending into components.
-- walk_parent_inv_id: Invocation ID of the enclosing call (0 for the root call).
-- 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.
local function walk_body_entry(body_entry, walk_parent_inv_id,
walk_root_call_text, walk_immediate_call_text)
local tokens = body_entry.body_tokens or {}
local body_off = body_entry.body_off or 0
local line_of = body_entry.line_of or M.LineIndex("")
local def_source = body_entry.source or ""
local def_line = body_entry.declaration or 0
local sub_map = body_entry.sub_map
-- Per-token dispatch: each matched branch returns; only the fall-through
-- "opaque word" emit handles direct encoders + mac_X-without-component.
local function process_token(bt)
local tok = M.trim(bt.tok or "")
if tok == "" then return end
local ident, after = M.read_ident(tok, 1)
if not ident then ident = "?" end
local _, args = token_ident_and_args(tok)
local tok_line = line_of(body_off + bt.rel) or 0
if M.DELAY_MARKERS[ident] then
emit_marker("delay", ident, nil, tok_line)
local rest = M.trim(tok:sub(after or (#tok + 1)))
if rest ~= "" then
process_token({ tok = rest, rel = bt.rel })
end
return
end
-- embedded markers live only in non-marker tokens.
-- Pass `ident` as the consuming instruction so `emit_embedded_markers` can compute each marker's arg position + record the consuming_encoder for the offsets pass.
-- Canonicalize `jump_rel` to `branch_equal` (its preprocessor-expanded form) so the `consuming_encoder` metadata in marker records is canonical.
-- `jump_rel`: unconditional jump alias from `code/duffle/mips.h`.
local consuming_encoder_for_markers = (ident == "jump_rel") and "branch_equal" or ident
if ident ~= "atom_label" and ident ~= "atom_offset" then
emit_embedded_markers(tok, tok_line, consuming_encoder_for_markers)
end
-- atom_label / atom_offset: terminal markers, no further descent.
-- Top-level markers (the marker IS the entire token) have no consuming instruction;
-- nil for both `consuming_encoder` and `consuming_arg_pos`.
-- The offsets pass treats these as branch-equivalent for backward compatibility.
-- TODO(Ed): Review this don't want legacy cruft here..
if ident == "atom_label" then emit_marker("label", args[1] or "", nil, tok_line); return
elseif ident == "atom_offset" then emit_marker("offset", args[1] or "", args[2] or "", tok_line); return
end
if ident:sub(1, 4) == "mac_" then
local bare = ident:sub(5)
local comp = ctx_table.component_index[bare]
if comp then
local invocation_root_call_text = walk_root_call_text or tok
if ctx_table.visiting[bare] then
-- Cycle: still allocate inv_id, emit zero-width begin/end, record the cycle error; do NOT recurse.
local inv = emit_invoke_begin(comp.kind or "comp_bare", bare, tok, invocation_root_call_text, def_source, tok_line)
inv.parent_id = walk_parent_inv_id
inv.call_text = tok
local err = {
kind = "cycle",
msg = string.format("project_emission: component cycle detected: %q", bare),
source = def_source,
line = tok_line,
}
inv.errors[#inv.errors + 1] = err
errors [#errors + 1] = err
emit_invoke_end(inv)
return
end
-- First visit: descend + count + count_mismatch-check below.
ctx_table.visiting[bare] = true
local inv = emit_invoke_begin(comp.kind or "comp_bare", bare, tok, invocation_root_call_text, def_source, tok_line)
inv.parent_id = walk_parent_inv_id
inv.call_text = tok
inv.def_path = comp.source
inv.def_line = comp.declaration
-- 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]
and ctx_table.component_index[bare].arg_names
local child_map = nil
if formal_names then
child_map = {}
for i, fname in ipairs(formal_names) do
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,
},
inv.id,
invocation_root_call_text,
tok)
ctx_table.visiting[bare] = nil
emit_invoke_end(inv)
-- Count `word` items inside [start_word, end_word].
local wc_inside = 0
for i = inv.start_word, inv.end_word do
local it = items[i]
if it and it.kind == "word" then
wc_inside = wc_inside + 1
end
end
inv.word_count = wc_inside
-- count_mismatch is a construction error: word_counts["mac_X"] is the declared count populated by the components pass;
-- We compare against the measured word count.
local declared = ctx_table.word_counts["mac_" .. bare]
if declared and wc_inside ~= declared then
local err = {
kind = "count_mismatch",
msg = string.format("project_emission: mac_%s declared=%d measured=%d", bare, declared, wc_inside),
source = def_source,
line = tok_line,
}
inv.errors[#inv.errors + 1] = err
errors [#errors + 1] = err
end
return
end
-- mac_X NOT in component_index: 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.
local n = resolve_count(ident, tok_line)
local out_ident = (ident == "nop2") and "nop" or ident
for _ = 1, n do
emit_word(out_ident, args, tok_line, tok, def_source, def_line, walk_immediate_call_text, walk_root_call_text, sub_map)
end
end
for _, bt in ipairs(tokens) do
process_token(bt)
end
end
-- Initialize the per-walk mutable context.
-- `visiting` is the active DFS component stack; `root_call_path` / `root_call_line` are preserved across recursion so nested words always point at the
-- ORIGINAL root atom call site.
ctx_table.visiting = ctx_table.visiting or {}
ctx_table.root_call_path = ctx_table.root_call_path or ""
ctx_table.root_call_line = ctx_table.root_call_line or 0
-- 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)
-- 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",
msg = string.format("project_emission: invocation boundaries not balanced (%d unclosed invocation(s) at end of walk)", #invocation_stack),
}
end
return {
items = items,
word_events = word_events,
markers = markers,
invocations = invocations,
errors = errors,
warnings = warnings,
}
end
--- Project a body string into the per-atom emission projection.
---
--- Semantics:
--- * Direct one-word tokens (`nop`, `add_ui`, ...): one `word` item, encoder = ident, word_count = 1.
--- * Metadata-backed N-word tokens (`nop2`, `mask_upper`, ...): N `word` items, all sharing the same encoder + word_count = 1.
--- `nop2` is normalized to encoder `nop` (per the spec).
--- * `atom_label(F)` markers: one `label` item with `name = "F"`, `word_index = current word_idx`; zero-width (does NOT advance word_idx).
--- * `atom_offset(B, T)` markers: one `offset` item with `name = "B"`, `target = "T"`, `word_index = current word_idx`; zero-width.
--- * Delay markers (`GteDelay_` / `LdSlot_` / `BdSlot_` / `DmaSlot_`): one `delay` item; zero-width. The following encoder is the next token.
--- * `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
--- 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`,
--- `call_text` (the immediate token spelling), `root_call_text` (outermost `mac_X(...)` text), `word_count` (always 1),
--- `invocation_ids` (innermost last), `outermost_invocation_id`.
--- Markers carry: `kind`, `name`, `line`, `word_index`, `target` (only for offset kind), plus `invocation_ids` / `outermost_invocation_id`
--- for the open invocation stack at that word.
---
--- @param body_text string -- the raw atom body string
--- @param component_index table -- bare-name → component record (corpus.component_body_index)
--- @param word_counts table -- macro name → emitted word count
--- @param components table -- bare-name → component definition (corpus.components); REQUIRED — consumed at the invocation-construction site to stamp
--- `invocation.debug_skip`. A missing or non-table `components` raises a fail-loud error rather than silently falling back.
--- @return EmissionProjection
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`)
-- 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)
-- into `item.line` and `invocation.call_line`. `passes/emission_model.lua::stamp_root_provenance` performs the single
-- conversion from body-relative to physical source line at the close site, using the source's `line_of` closure that
-- the pass forwarded. One owner of the line state.
if type(components) ~= "table" then
error("duffle.project_emission: `components` is required "
.. "(bare-name -> component definition, e.g. corpus.components); "
.. "got " .. type(components) .. ". "
.. "The emission pass MUST forward the corpus registry "
.. "so the invocation-construction site can stamp `debug_skip` "
.. "without a second pass, source parse, or parallel lookup.",
0)
end
if type(body_text) ~= "string" or body_text == "" then
-- Empty body: still return a valid (empty) projection.
return {
items = {},
word_events = {},
markers = {},
invocations = {},
errors = {},
warnings = {},
}
end
local tokens = M.tokenize_body(body_text)
return _project_emission_inner({
body_tokens = tokens,
body_off = 0,
line_of = M.LineIndex(body_text),
source = "",
declaration = 0,
},
{
component_index = component_index or {},
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,
schema_name = reg_use_ctx and reg_use_ctx.schema_name,
})
end
-------------------------------------------------------------------------------
-- find_function_decl_for — backward walk for MipsAtomComp_Proc_ name extraction.
--
-- After the `sym` arg was dropped from MipsAtomComp_Proc_, the component name
-- is derived from the preceding `FI_ Slice_MipsCode ac_X(args)` function
-- declaration. This function walks backward from `before_pos` to find it.
--
-- Returns (raw_name, args_inner) or (nil, nil).
-- raw_name — e.g. "ac_load_word_imm"
-- args_inner — e.g. "AtomBuilder_R ab, Reg dst, U4 imm"
--
-- The walk finds the LAST "Slice_MipsCode" before before_pos, then skips
-- whitespace + qualifiers (FI_, atom_dbg_skip, comments) until it finds an
-- ident followed by "(". That ident is the function name; the parens contents
-- are the args.
-------------------------------------------------------------------------------
function M.find_function_decl_for(source, before_pos, slice_mips_code_len)
local search_pos = 1
local last_match = nil
while true do
local found = source:find("Slice_MipsCode", search_pos, true)
if not found or found >= before_pos then break end
last_match = found
search_pos = found + slice_mips_code_len
end
if not last_match then return nil, nil end
local pos = last_match + slice_mips_code_len
while pos < before_pos do
-- skip whitespace
while pos <= #source do
local c = source:sub(pos, pos)
if c == " " or c == "\t" or c == "\n" or c == "\r" then
pos = pos + 1
else
break
end
end
if pos > #source then break end
-- skip line comments
if source:sub(pos, pos + 1) == "//" then
while pos <= #source and source:sub(pos, pos) ~= "\n" do pos = pos + 1 end
pos = pos + 1
goto continue
end
-- skip block comments
if source:sub(pos, pos + 1) == "/*" then
local close = source:find("*/", pos + 2, true)
if not close then break end
pos = close + 2
goto continue
end
-- try to read an ident
local ident, ident_end = M.read_ident(source, pos)
if not ident then break end
-- check if the next non-ws char after ident is "("
local next_pos = M.skip_ws_and_cmt(source, ident_end)
if source:sub(next_pos, next_pos) == "(" then
local inner = M.read_parens(source, next_pos)
if inner then
return ident, inner
end
end
-- ident not followed by "(" — it's a qualifier (FI_, atom_dbg_skip, etc); skip it
pos = ident_end
::continue::
end
return nil, nil
end
-------------------------------------------------------------------------------
-- find_atom_proc_decl_for — backward walk for MipsAtom_Proc_ name extraction.
--
-- The atom name is the preceding `MipsAtom* ident(args)` function ident.
-- This function walks backward from `before_pos` to find it.
--
-- Returns (raw_name, args_inner, func_ident, after_paren) or (nil, nil).
-- raw_name — the function ident as written
-- args_inner — e.g. "AtomArena_R aa, U4 r_scratch, ..."
-- after_paren — source position after the function `)`
--
-- The walk finds the LAST "MipsAtom*" before before_pos, then skips
-- whitespace + qualifiers (internal, I_, FI_, comments) until it finds an
-- ident followed by "(". That ident is the name. The parens contents are the args.
-------------------------------------------------------------------------------
function M.find_atom_proc_decl_for(source, before_pos, mips_atom_ptr_len)
local search_pos = 1
local last_match = nil
while true do
-- plain=true: "*" is literal, no escaping needed
local found = source:find("MipsAtom*", search_pos, true)
if not found or found >= before_pos then break end
last_match = found
search_pos = found + mips_atom_ptr_len
end
if not last_match then return nil, nil end
local pos = last_match + mips_atom_ptr_len
while pos < before_pos do
-- skip whitespace
while pos <= #source do
local c = source:sub(pos, pos)
if c == " " or c == "\t" or c == "\n" or c == "\r" then
pos = pos + 1
else
break
end
end
if pos > #source then break end
-- skip line comments
if source:sub(pos, pos + 1) == "//" then
while pos <= #source and source:sub(pos, pos) ~= "\n" do pos = pos + 1 end
pos = pos + 1
goto continue
end
-- skip block comments
if source:sub(pos, pos + 1) == "/*" then
local close = source:find("*/", pos + 2, true)
if not close then break end
pos = close + 2
goto continue
end
-- try to read an ident
local ident, ident_end = M.read_ident(source, pos)
if not ident then break end
-- check if the next non-ws char after ident is "("
local next_pos = M.skip_ws_and_cmt(source, ident_end)
if source:sub(next_pos, next_pos) == "(" then
local inner, after_paren = M.read_parens(source, next_pos)
if inner then
return ident, inner, ident, after_paren
end
end
-- ident not followed by "(" — it's a qualifier; skip it
pos = ident_end
::continue::
end
return nil, nil
end
return M
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
+15 -47
View File
@@ -115,7 +115,7 @@ end
--- Post-loop: Needs full-corpus `annot_counts` from pipe_ctx. --- Post-loop: Needs full-corpus `annot_counts` from pipe_ctx.
--- @param pipe_ctx PipeCtx --- @param pipe_ctx PipeCtx
--- @param findings Findings --- @param findings Findings
local function check_unique_annotation(pipe_ctx, findings) local function check_unique_annotation(_item, pipe_ctx, findings)
for name, n in pairs(pipe_ctx.annot_counts) do for name, n in pairs(pipe_ctx.annot_counts) do
if n > 1 then if n > 1 then
findings.errors[#findings.errors + 1] = { findings.errors[#findings.errors + 1] = {
@@ -147,7 +147,8 @@ end
--- @param m MacroEntry --- @param m MacroEntry
--- @param wc table<string, integer> -- Shared word-count table (from ctx.shared.word_counts) --- @param wc table<string, integer> -- Shared word-count table (from ctx.shared.word_counts)
--- @param findings Findings --- @param findings Findings
local function check_macro_word_drift(m, wc, findings) local function check_macro_word_drift(m, pipe_ctx, findings)
local wc = (pipe_ctx and pipe_ctx.word_counts) or {}
local declared = wc[m.name] local declared = wc[m.name]
if not declared then if not declared then
findings.errors[#findings.errors + 1] = { findings.errors[#findings.errors + 1] = {
@@ -429,40 +430,17 @@ local CHECK_RULES = {
--- @param ctx PassCtx --- @param ctx PassCtx
--- @return PipeCtx --- @return PipeCtx
local function build_corpus_pipe_ctx(ctx) local function build_corpus_pipe_ctx(ctx)
local corpus = ctx.shared and ctx.shared.corpus local view = duffle.corpus_view(ctx)
if not corpus then
error("annotation requires ctx.shared.corpus "
.. "(the canonical corpus is the source of truth; "
.. "no per-source fallback is supported)", 0)
end
-- `corpus.atom_infos` preserves source order and duplicates; I precompute counts here for `check_unique_annotation` and the per-source checks.
local annot_counts = {} local annot_counts = {}
for _, info in ipairs(corpus.atom_infos or {}) do for _, info in ipairs(view.atom_infos) do
if info and info.atom_name then if info and info.atom_name then
annot_counts[info.atom_name] = (annot_counts[info.atom_name] or 0) + 1 annot_counts[info.atom_name] = (annot_counts[info.atom_name] or 0) + 1
end end
end end
view.annot_counts = annot_counts
-- Every consumer of these fields observes mutations via the canonical corpus without independently mutable registry construction. view.atom_infos_list = view.atom_infos
return { view.word_counts = ctx.shared.corpus.word_counts or {}
-- Cross-source lookup tables from corpus. return view
register_alias_registry = corpus.register_alias_registry or {},
type_name_registry = corpus.type_name_registry or {},
atom_views = corpus.atom_views or {},
atom_ctxs = corpus.atom_ctxs or {},
atom_phases = corpus.atom_phases or {},
binds_by_name = corpus.binds_by_name or {},
atoms_by_name = corpus.atoms_by_name or {},
-- Corpus-wide ordered list of atom_info records (source-order + duplicates).
atom_infos_list = corpus.atom_infos or {},
-- Corpus-wide annotation count aggregation (post-rule consumes this).
annot_counts = annot_counts,
-- Corpus-wide collisions (recorded by scan_source.merge_corpus_registries).
collisions = corpus.collisions or {},
-- `check_macro_word_drift` reads `corpus.word_counts`, populated by word_count_eval.run.
word_counts = corpus.word_counts or {},
}
end end
--- Validate one source against its pre-scanned SourceScan payload + the corpus-wide pipe_ctx. --- Validate one source against its pre-scanned SourceScan payload + the corpus-wide pipe_ctx.
@@ -536,38 +514,28 @@ local function validate(ctx, src, corpus_pipe_ctx)
-- THE per-annotation pipeline. ONE loop. CHECK_RULES dispatches per_annot rules. -- THE per-annotation pipeline. ONE loop. CHECK_RULES dispatches per_annot rules.
for _, a in ipairs(annots) do for _, a in ipairs(annots) do
for _, rule in ipairs(CHECK_RULES) do duffle.run_check_rules(CHECK_RULES, "per_annot", a, pipe_ctx, findings)
if rule.per_annot then rule.per_annot(a, pipe_ctx, findings) end
end
end end
-- Post-loop rules (one-shot checks that need full-corpus aggregation in pipe_ctx). -- Post-loop rules (one-shot checks that need full-corpus aggregation in pipe_ctx).
for _, rule in ipairs(CHECK_RULES) do duffle.run_check_rules(CHECK_RULES, "post", nil, pipe_ctx, findings)
if rule.post then rule.post(pipe_ctx, findings) end
end
-- scan_source records each marker in scan.debug_skip_markers; this loop validates each record independently and emits at most one error per marker. -- scan_source records each marker in scan.debug_skip_markers; this loop validates each record independently and emits at most one error per marker.
-- Valid markers stamp `debug_skip = true` on the following atom or component declaration, which downstream consumers read directly. -- Valid markers stamp `debug_skip = true` on the following atom or component declaration, which downstream consumers read directly.
local skip_markers = scan.debug_skip_markers or {} local skip_markers = scan.debug_skip_markers or {}
for _, marker in ipairs(skip_markers) do for _, marker in ipairs(skip_markers) do
for _, rule in ipairs(CHECK_RULES) do duffle.run_check_rules(CHECK_RULES, "per_skip_marker", marker, pipe_ctx, findings)
if rule.per_skip_marker then rule.per_skip_marker(marker, pipe_ctx, findings) end
end
end end
-- Per-macro rules (TAPE_WORDS vs WORD_COUNT drift). -- Per-macro rules (TAPE_WORDS vs WORD_COUNT drift).
local wc = corpus_pipe_ctx.word_counts pipe_ctx.word_counts = corpus_pipe_ctx.word_counts
for _, m in ipairs(scan.macros) do for _, m in ipairs(scan.macros) do
for _, rule in ipairs(CHECK_RULES) do duffle.run_check_rules(CHECK_RULES, "per_macro", m, pipe_ctx, findings)
if rule.per_macro then rule.per_macro(m, wc, findings) end
end
end end
-- Per-source rules (reg defaults, atom_view layout, compute-register type overrides, Binds_* field uniqueness). -- Per-source rules (reg defaults, atom_view layout, compute-register type overrides, Binds_* field uniqueness).
-- Each per_source rule sees the full scan payload via pipe_ctx. -- Each per_source rule sees the full scan payload via pipe_ctx.
for _, rule in ipairs(CHECK_RULES) do duffle.run_check_rules(CHECK_RULES, "per_source", src, pipe_ctx, findings)
if rule.per_source then rule.per_source(src, pipe_ctx, findings) end
end
-- Information summary (always emitted). -- Information summary (always emitted).
findings.info[#findings.info + 1] = { findings.info[#findings.info + 1] = {
+4 -2
View File
@@ -372,8 +372,10 @@ local function cycle_cost_rec(name, comp_by_name, latency, cache)
local nested = ident:sub(MAC_PREFIX_LEN + 1) local nested = ident:sub(MAC_PREFIX_LEN + 1)
n = n + cycle_cost_rec(nested, comp_by_name, latency, cache) n = n + cycle_cost_rec(nested, comp_by_name, latency, cache)
else else
-- Leaf instruction or pseudo-macro. Look up in INSTRUCTION_LATENCY; default 1. -- Leaf instruction or pseudo-macro.
n = n + (latency[ident] or 1) local isa = duffle.instr(ident)
local gte = duffle.gte(ident)
n = n + ((isa and isa.cycles) or (gte and gte.cycles) or latency[ident] or 1)
end end
end end
end end
+190 -171
View File
@@ -1319,159 +1319,49 @@ local function parse_atom_info_after_decl(source, after_paren, raw_name, line_of
return info_after return info_after
end end
--- Parse: `MipsAtom_(<name>) [atom_info(<binds>, <reads>, <writes>)] { <body> }` local DECL_FORMS = {
--- @param source string MipsAtom_ = {
--- @param pos integer kind = "atom", name = "paren_ident", body = "braces_after",
--- @param ident_end integer info_dest = "atom_infos", strip = false,
--- @param line_of fun(pos: integer): integer },
--- @param out SourceScan MipsAtom_Proc_ = {
--- @return integer kind = "atom_proc", name = "backward_atom_proc", body = "last_brace_in_args",
local function parse_mips_atom(source, pos, ident_end, line_of, out) info_dest = "atom_infos", strip = false, after = "reguse_hook",
local inner, after_paren, open_paren = read_parens_after(source, ident_end) },
if not inner then return after_paren end MipsAtomComp_ = {
kind = "comp_bare", name = "paren_ident", body = "braces_after",
info_dest = "component_atom_infos", strip = "ac_",
},
MipsAtomComp_Proc_ = {
kind = "comp_proc", name = "backward_fi", body = "last_brace_in_args",
info_dest = nil, strip = "ac_",
},
MipsAtomComp_ProcMap_ = {
kind = "comp_proc", name = "backward_fi", body = "comma_arg_2",
info_dest = nil, strip = "ac_", after = "map_command_hook",
},
}
local raw_name = duffle.read_ident(inner, 1) local function last_brace_body(inner, open_paren)
-- Lookahead for atom_info(...) between `)` and `{`.
local brace_search_pos = parse_atom_info_after_decl(source, after_paren, raw_name, line_of, out, out.atom_infos)
local body, after_brace, body_off = find_body_braces(source, brace_search_pos, open_paren + 1)
if not body then return after_brace end
if raw_name and raw_name ~= "" then
register_atom(out, "atom", line_of(pos), raw_name, body, body_off, raw_name, pos, after_paren, source)
end
return after_brace
end
--- Parse: `MipsAtomComp_(<name>) { <body> }`
--- @param source string
--- @param pos integer
--- @param ident_end integer
--- @param line_of fun(pos: integer): integer
--- @param out SourceScan
--- @return integer
local function parse_mips_atom_comp(source, pos, ident_end, line_of, out)
local inner, after_paren, open_paren = read_parens_after(source, ident_end)
if not inner then return after_paren end
local raw_name = duffle.read_ident(inner, 1)
if not raw_name then return open_paren + 1 end
out.component_atom_infos = out.component_atom_infos or {}
local brace_search_pos = parse_atom_info_after_decl(source, after_paren, strip_ac_prefix(raw_name), line_of, out, out.component_atom_infos)
local body, after_brace, body_off = find_body_braces(source, brace_search_pos, open_paren + 1)
if not body then return after_brace end
local name = strip_ac_prefix(raw_name)
register_atom(out, "comp_bare", line_of(pos), name, body, body_off, raw_name, pos, after_paren, source)
return after_brace
end
--- Parse: `MipsAtomComp_Proc_(<name>, { <body> })` — body is inside the LAST `{` in args.
--- @param source string
--- @param pos integer
--- @param ident_end integer
--- @param line_of fun(pos: integer): integer
--- @param out SourceScan
--- @return integer
local function parse_mips_atom_comp_proc(source, pos, ident_end, line_of, out)
local inner, after_paren, open_paren = read_parens_after(source, ident_end)
if not inner then return after_paren end
-- Find the LAST `{` in inner (the body brace, not any potential embedded braces in expressions).
local last_brace_pos = nil local last_brace_pos = nil
for search_pos = #inner, 1, -1 do for search_pos = #inner, 1, -1 do
if inner:sub(search_pos, search_pos) == "{" then last_brace_pos = search_pos; break end if inner:sub(search_pos, search_pos) == "{" then
last_brace_pos = search_pos
break
end end
if not last_brace_pos then return after_paren end end
if not last_brace_pos then return nil end
-- Use duffle.read_braces to find the matching close brace.
-- Uses `read_balanced` for delimiter-depth tracking.
-- If close_pos is past the end of inner, the brace didn't match (malformed input); skip.
local body, close_pos = duffle.read_braces(inner, last_brace_pos) local body, close_pos = duffle.read_braces(inner, last_brace_pos)
if close_pos > #inner + 1 then return after_paren end if close_pos > #inner + 1 then return nil end
return body, open_paren + 2 + last_brace_pos
-- The component name is derived from the preceding function declaration
-- (`FI_ Slice_MipsCode ac_X(...)`), not from the first macro arg (which
-- is now `ab`). The backward walk finds the function decl before open_paren.
local raw_name = duffle.find_function_decl_for(source, open_paren, SLICE_MIPS_CODE_LEN)
if not raw_name then raw_name = "?" end
local name = strip_ac_prefix(raw_name)
-- Position of body[1] in source = open_paren + 1 (start of inner) + last_brace_pos + 1 (past '{').
local body_off = open_paren + 2 + last_brace_pos
register_atom(out, "comp_proc", line_of(pos), name, body, body_off, raw_name, pos, after_paren, source)
return after_paren
end end
--- Parse: `MipsAtomComp_ProcMap_(ab, command)` — body is the one command (second arg). local function reguse_hook(source, pos, line_of, out, extras)
--- Reuses the proc name walk. Kind is `comp_proc`. The C expansion wraps
--- `atom_dbg_skip MipsAtomComp_Proc_(ab, {command })`; source-as-written is the map.
--- @param source string
--- @param pos integer
--- @param ident_end integer
--- @param line_of fun(pos: integer): integer
--- @param out SourceScan
--- @return integer
local function parse_mips_atom_comp_proc_map(source, pos, ident_end, line_of, out)
local inner, after_paren, open_paren = read_parens_after(source, ident_end)
if not inner then return after_paren end
local args = duffle.split_top_level_commas(inner)
if #args < 2 then return after_paren end
local command = duffle.trim(args[2])
if command == "" then return after_paren end
local raw_name = duffle.find_function_decl_for(source, open_paren, SLICE_MIPS_CODE_LEN)
if not raw_name then raw_name = "?" end
local name = strip_ac_prefix(raw_name)
local body_off = open_paren + 1 + (inner:find(command, 1, true) or 1) - 1
register_atom(out, "comp_proc", line_of(pos), name, command, body_off, raw_name, pos, after_paren, source)
local entry = out.atoms[#out.atoms] local entry = out.atoms[#out.atoms]
entry.map_command = command if not entry then return end
return after_paren local reg_use_schema_name, reg_use_param_name
end if extras.args_inner then
local arg_tokens = duffle.split_top_level_commas(extras.args_inner)
--- Parse: `MipsAtom_Proc_(aa, { body })` — body is inside the LAST `{` in args.
--- Kind is `atom_proc`. The name is the preceding function ident as written.
--- Offsets walk this kind. Components do not emit a `mac_*` alias for it.
--- @param source string
--- @param pos integer
--- @param ident_end integer
--- @param line_of fun(pos: integer): integer
--- @param out SourceScan
--- @return integer
local function parse_mips_atom_proc(source, pos, ident_end, line_of, out)
local inner, after_paren, open_paren = read_parens_after(source, ident_end)
if not inner then return after_paren end
-- Find the LAST `{` in inner (the body brace, not any potential embedded braces in expressions).
local last_brace_pos = nil
for search_pos = #inner, 1, -1 do
if inner:sub(search_pos, search_pos) == "{" then last_brace_pos = search_pos; break end
end
if not last_brace_pos then return after_paren end
-- Use duffle.read_braces to find the matching close brace.
-- Uses `read_balanced` for delimiter-depth tracking.
-- If close_pos is past the end of inner, the brace didn't match (malformed input); skip.
local body, close_pos = duffle.read_braces(inner, last_brace_pos)
if close_pos > #inner + 1 then return after_paren end
-- The atom name is the preceding function ident as written
-- (`internal MipsAtom* X(...)`). The first macro arg is the arena.
local raw_name, args_inner, func_ident, after_func_paren =
duffle.find_atom_proc_decl_for(source, open_paren, MIPS_ATOM_PTR_LEN)
if not raw_name then raw_name = "?" end
local name = strip_ac_prefix(raw_name)
if after_func_paren then
parse_atom_info_after_decl(source, after_func_paren, name, line_of, out, out.atom_infos)
else
parse_atom_info_after_decl(source, pos, name, line_of, out, out.atom_infos)
end
local reg_use_schema_name = nil
local reg_use_param_name = nil
if args_inner then
local arg_tokens = duffle.split_top_level_commas(args_inner)
for _, tok in ipairs(arg_tokens) do for _, tok in ipairs(arg_tokens) do
local trimmed = duffle.trim(tok) local trimmed = duffle.trim(tok)
local schema_suffix, param = trimmed:match("RegUse_([%w_]+)%s+([%w_]+)$") local schema_suffix, param = trimmed:match("RegUse_([%w_]+)%s+([%w_]+)$")
@@ -1489,28 +1379,102 @@ local function parse_mips_atom_proc(source, pos, ident_end, line_of, out)
end end
end end
end end
-- Position of body[1] in source = open_paren + 1 (start of inner) + last_brace_pos + 1 (past '{').
local body_off = open_paren + 2 + last_brace_pos
register_atom(out, "atom_proc", line_of(pos), name, body, body_off, raw_name, pos, after_paren, source)
local entry = out.atoms[#out.atoms]
entry.reg_use_schema_name = reg_use_schema_name entry.reg_use_schema_name = reg_use_schema_name
entry.reg_use_param_name = reg_use_param_name entry.reg_use_param_name = reg_use_param_name
if reg_use_schema_name and func_ident then if reg_use_schema_name and extras.func_ident then
local expected = "RegUse_" .. func_ident local expected = "RegUse_" .. extras.func_ident
if reg_use_schema_name ~= expected then if reg_use_schema_name ~= expected then
out.reg_use_errors[#out.reg_use_errors + 1] = { out.reg_use_errors[#out.reg_use_errors + 1] = {
kind = "reguse_name_mismatch", kind = "reguse_name_mismatch",
schema_name = reg_use_schema_name, schema_name = reg_use_schema_name,
func_ident = func_ident, func_ident = extras.func_ident,
source_line = line_of(pos), source_line = line_of(pos),
} }
end end
end end
end
local function parse_decl_form(source, pos, ident_end, line_of, out)
local ident = duffle.read_ident(source, pos)
local form = ident and DECL_FORMS[ident]
if not form then return ident_end end
local inner, after_paren, open_paren = read_parens_after(source, ident_end)
if not inner then return after_paren end
local extras = {}
local raw_name
if form.name == "paren_ident" then
raw_name = duffle.read_ident(inner, 1)
if form.strip and not raw_name then return open_paren + 1 end
elseif form.name == "backward_fi" then
raw_name = duffle.find_function_decl_for(source, open_paren, SLICE_MIPS_CODE_LEN)
elseif form.name == "backward_atom_proc" then
raw_name, extras.args_inner, extras.func_ident, extras.after_func_paren =
duffle.find_atom_proc_decl_for(source, open_paren, MIPS_ATOM_PTR_LEN)
end
if form.name == "paren_ident" and form.strip and not raw_name then
return open_paren + 1
end
if not raw_name then raw_name = "?" end
local name = form.strip and strip_ac_prefix(raw_name) or raw_name
if form.info_dest == "component_atom_infos" then
out.component_atom_infos = out.component_atom_infos or {}
end
local info_dest = form.info_dest and out[form.info_dest]
local body, body_off, resume
if form.body == "braces_after" then
local brace_search = after_paren
if info_dest then
brace_search = parse_atom_info_after_decl(
source, after_paren, name, line_of, out, info_dest)
end
local after_brace
body, after_brace, body_off = find_body_braces(source, brace_search, open_paren + 1)
if not body then return after_brace end
resume = after_brace
elseif form.body == "last_brace_in_args" then
body, body_off = last_brace_body(inner, open_paren)
if not body then return after_paren end
resume = after_paren
if form.info_dest then
if extras.after_func_paren then
parse_atom_info_after_decl(
source, extras.after_func_paren, name, line_of, out, out.atom_infos)
else
parse_atom_info_after_decl(source, pos, name, line_of, out, out.atom_infos)
end
end
elseif form.body == "comma_arg_2" then
local args = duffle.split_top_level_commas(inner)
if #args < 2 then return after_paren end
body = duffle.trim(args[2])
if body == "" then return after_paren end
body_off = open_paren + 1 + (inner:find(body, 1, true) or 1) - 1
resume = after_paren
else
return after_paren return after_paren
end end
local skip_register = form.name == "paren_ident" and not form.strip
and (raw_name == "?" or raw_name == "")
if not skip_register then
register_atom(out, form.kind, line_of(pos), name, body, body_off,
raw_name, pos, after_paren, source)
end
if form.after == "reguse_hook" then
reguse_hook(source, pos, line_of, out, extras)
elseif form.after == "map_command_hook" then
local entry = out.atoms[#out.atoms]
if entry then entry.map_command = body end
end
return resume
end
--- Parse: `MipsCode code_<name> { <body> }` (raw atom form — offsets pass only). --- Parse: `MipsCode code_<name> { <body> }` (raw atom form — offsets pass only).
--- @param source string --- @param source string
--- @param pos integer --- @param pos integer
@@ -2219,11 +2183,11 @@ end
-- Adding a new construct = 1 row here + 1 parser function above. -- Adding a new construct = 1 row here + 1 parser function above.
local DECL_PARSERS = { local DECL_PARSERS = {
MipsAtom_ = parse_mips_atom, MipsAtom_ = parse_decl_form,
MipsAtom_Proc_ = parse_mips_atom_proc, MipsAtom_Proc_ = parse_decl_form,
MipsAtomComp_ = parse_mips_atom_comp, MipsAtomComp_ = parse_decl_form,
MipsAtomComp_Proc_ = parse_mips_atom_comp_proc, MipsAtomComp_Proc_ = parse_decl_form,
MipsAtomComp_ProcMap_ = parse_mips_atom_comp_proc_map, MipsAtomComp_ProcMap_ = parse_decl_form,
-- `atom_dbg_skip` is the only debug-skip parser entry. Every other -- `atom_dbg_skip` is the only debug-skip parser entry. Every other
-- identifier follows the ordinary unrelated-token path; there is no alias. -- identifier follows the ordinary unrelated-token path; there is no alias.
atom_dbg_skip = parse_dbg_skip_marker, atom_dbg_skip = parse_dbg_skip_marker,
@@ -2243,17 +2207,11 @@ local DECL_PARSERS = {
-- Only the bare `atom_dbg_skip` marker reaches `parse_dbg_skip_marker`. -- Only the bare `atom_dbg_skip` marker reaches `parse_dbg_skip_marker`.
-- Unknown identifiers follow the same unrelated-token path as every other unsupported source token. -- Unknown identifiers follow the same unrelated-token path as every other unsupported source token.
local TAPE_SKIP_MACROS = { local function tape_skip_ident(ident)
MipsAtom_ = true, return ident and (DECL_FORMS[ident] or ident == "Struct_" or ident == "Enum_")
MipsAtom_Proc_ = true, end
MipsAtomComp_ = true,
MipsAtomComp_Proc_ = true,
MipsAtomComp_ProcMap_ = true,
Struct_ = true,
Enum_ = true,
}
local function collect_addrs_assigns(text) local function collect_addrs_assigns_REMOVED(text)
local addrs = {} local addrs = {}
local pos = 1 local pos = 1
local n = #text local n = #text
@@ -2337,7 +2295,7 @@ local function scan_tape_chains(source)
pos = duffle.skip_ws_and_cmt(source, pos) pos = duffle.skip_ws_and_cmt(source, pos)
if pos > n then break end if pos > n then break end
local ident, ident_end = duffle.read_ident(source, pos) local ident, ident_end = duffle.read_ident(source, pos)
if ident and TAPE_SKIP_MACROS[ident] then if tape_skip_ident(ident) then
local after = duffle.skip_ws_and_cmt(source, ident_end) local after = duffle.skip_ws_and_cmt(source, ident_end)
if source:sub(after, after) == "(" then if source:sub(after, after) == "(" then
local _, after_p = duffle.read_parens(source, after) local _, after_p = duffle.read_parens(source, after)
@@ -2418,6 +2376,10 @@ local function scan_source(source, source_file, code_macros, code_macro_bodies)
-- See `propagate_type_sizes()` below. -- See `propagate_type_sizes()` below.
type_name_registry = {}, type_name_registry = {},
reg_use_schemas = {}, reg_use_schemas = {},
tape_chains = {},
_addrs = {},
_chain = nil,
_brace_depth = 0,
reg_use_errors = {}, reg_use_errors = {},
-- Shared `R_*_Code -> integer code` registry -- Shared `R_*_Code -> integer code` registry
-- (passed in from M.run pass 1; same reference so preprocessor intercept writes are visible to the enum-value resolver). -- (passed in from M.run pass 1; same reference so preprocessor intercept writes are visible to the enum-value resolver).
@@ -2452,6 +2414,48 @@ local function scan_source(source, source_file, code_macros, code_macro_bodies)
local parser = DECL_PARSERS[ident] local parser = DECL_PARSERS[ident]
if parser then if parser then
pos = parser(source, pos, ident_end, line_of, out) pos = parser(source, pos, ident_end, line_of, out)
elseif ident == "addrs" then
local after = duffle.skip_ws_and_cmt(source, ident_end)
if source:sub(after, after) == "[" then
local inner, after_br = duffle.read_brackets(source, after)
local idx = inner and tonumber(duffle.trim(inner))
after_br = duffle.skip_ws_and_cmt(source, after_br or after)
if idx and source:sub(after_br, after_br) == "=" then
local rhs = duffle.skip_ws_and_cmt(source, after_br + 1)
local rhs_ident = duffle.read_ident(source, rhs)
if rhs_ident then out._addrs[idx] = rhs_ident end
pos = rhs
else
pos = after_br or (after + 1)
end
else
pos = ident_end
end
elseif ident == "tb_emit_" or ident == "tb_emit" then
local after = duffle.skip_ws_and_cmt(source, ident_end)
if source:sub(after, after) == "(" then
local inner, after_p = duffle.read_parens(source, after)
local name
if ident == "tb_emit_" then
name = duffle.trim(inner or ""):match("^([%w_]+)")
else
local args = duffle.split_top_level_commas(inner or "")
local last = duffle.trim(args[#args] or "")
local idx = last:match("^addrs%s*%[%s*(%d+)%s*%]$")
if idx then
name = out._addrs[tonumber(idx)]
else
name = last:match("([%w_]+)$")
end
end
if name then
out._chain = out._chain or {}
out._chain[#out._chain + 1] = name
end
pos = after_p or (after + 1)
else
pos = ident_end
end
else else
-- Unsupported identifiers follow the unrelated-token path. If a -- Unsupported identifiers follow the unrelated-token path. If a
-- pending marker is still open, consume it so it cannot drift to a -- pending marker is still open, consume it so it cannot drift to a
@@ -2470,12 +2474,22 @@ local function scan_source(source, source_file, code_macros, code_macro_bodies)
else else
local markers = out.debug_skip_markers local markers = out.debug_skip_markers
local marker = markers[#markers] local marker = markers[#markers]
if marker and marker.pending and marker.proc_prelude then
local c = source:sub(pos, pos) local c = source:sub(pos, pos)
if marker and marker.pending and marker.proc_prelude then
if c == "{" or c == ";" then if c == "{" or c == ";" then
attach_debug_skip_marker(out, "unrelated") attach_debug_skip_marker(out, "unrelated")
end end
end end
if c == "{" then
out._brace_depth = out._brace_depth + 1
elseif c == "}" then
if out._brace_depth == 1 and out._chain and #out._chain > 0 then
out.tape_chains[#out.tape_chains + 1] = out._chain
end
out._chain = nil
out._brace_depth = out._brace_depth - 1
if out._brace_depth < 0 then out._brace_depth = 0 end
end
pos = pos + 1 pos = pos + 1
end end
end end
@@ -2486,7 +2500,12 @@ local function scan_source(source, source_file, code_macros, code_macro_bodies)
-- Runs AFTER the source walk so all typedef / Struct_ / Enum_ declarations have been parsed into `out.type_name_registry`. -- Runs AFTER the source walk so all typedef / Struct_ / Enum_ declarations have been parsed into `out.type_name_registry`.
-- Mutates each entry's `byte_size` field in place; fields with pointer_depth > 0 already carry byte_size = 4 from parse time and are unaffected. -- Mutates each entry's `byte_size` field in place; fields with pointer_depth > 0 already carry byte_size = 4 from parse time and are unaffected.
propagate_type_sizes(out) propagate_type_sizes(out)
out.tape_chains = scan_tape_chains(source) if out._chain and #out._chain > 0 then
out.tape_chains[#out.tape_chains + 1] = out._chain
end
out._addrs = nil
out._chain = nil
out._brace_depth = nil
return out return out
end end
+55 -124
View File
@@ -227,19 +227,7 @@ end
--- @field o_arg2 string|nil -- Second arg of O_(<a>, <b>) captures --- @field o_arg2 string|nil -- Second arg of O_(<a>, <b>) captures
--- @field s_arg1 string|nil -- Arg of S_(<a>) captures; nil for non-S_ tokens --- @field s_arg1 string|nil -- Arg of S_(<a>) captures; nil for non-S_ tokens
-- The set of MIPS instruction idents that have a load-delay slot. -- Load-delay idents are M.INSTRUCTION rows with kind == "load".
-- Per MIPS I R3000A: `lw`, `lh`, `lhu`, `lb`, `lbu`, `lwc2` (gte_lw).
-- Note: `lui` (load_upper_i) does NOT have a load delay on MIPS I — it's an ALU op, not a load.
-- The `load_imm_*` macros are lui + ori sequences with no per-component load delay either.
local LOAD_INSTRUCTION_IDENTS = {
load_word = true,
load_half = true,
load_half_u = true,
load_byte = true,
load_byte_u = true,
gte_lw = true,
gte_lwc2 = true,
}
-- Patterns for O_(<arg1>, <arg2>) and S_(<arg>) captures. -- Patterns for O_(<arg1>, <arg2>) and S_(<arg>) captures.
-- UNANCHORED, the substring can appea anywhere in the token (e.g., `load_word(R_T0, R_TapePtr, O_(Binds_X, field))` matches at position ~24). -- UNANCHORED, the substring can appea anywhere in the token (e.g., `load_word(R_T0, R_TapePtr, O_(Binds_X, field))` matches at position ~24).
@@ -256,21 +244,6 @@ local BRANCH_PATTERN = "^branch_[%w_]+%s*%("
-- The C preprocessor expands it BEFORE the metaprogram sees the source, but for source-level metadata consistency we still match it here and classify it as a branch_equal. -- The C preprocessor expands it BEFORE the metaprogram sees the source, but for source-level metadata consistency we still match it here and classify it as a branch_equal.
-- This keeps `consuming_encoder` canonical for any downstream tooling that consults the metadata field. -- This keeps `consuming_encoder` canonical for any downstream tooling that consults the metadata field.
local JUMP_REL_PATTERN = "^jump_rel%s*%(" local JUMP_REL_PATTERN = "^jump_rel%s*%("
local UNCOND_JUMP_PATTERNS = {
"^%f[%w]jump%f[%W]",
"^%f[%w]call_addr%f[%W]",
}
local TERMINAL_JUMP_PATTERNS = {
"^%f[%w]jump_reg%f[%W]",
"^%f[%w]call_reg%f[%W]",
"^%f[%w]jump_link%f[%W]",
}
local function matches_any(tok, patterns)
for i = 1, #patterns do
if tok:match(patterns[i]) then return true end
end
return false
end
local function classify_tokens(tokens) local function classify_tokens(tokens)
local n = #tokens local n = #tokens
@@ -303,7 +276,8 @@ local function classify_tokens(tokens)
local is_unconditional_jump = false local is_unconditional_jump = false
local is_terminal_jump = false local is_terminal_jump = false
local branch_label = nil local branch_label = nil
local is_load = LOAD_INSTRUCTION_IDENTS[ident] == true local isa = duffle.instr(ident)
local is_load = isa and isa.kind == "load"
local is_store_word = ident == "store_word" local is_store_word = ident == "store_word"
-- Per-check pre-computes (R3 lift). -- Per-check pre-computes (R3 lift).
@@ -319,18 +293,18 @@ local function classify_tokens(tokens)
if ident == "atom_label" then if ident == "atom_label" then
is_atom_label = true is_atom_label = true
label_name = tok:match("^atom_label%s*%(%s*([%w_]+)%s*%)") label_name = tok:match("^atom_label%s*%(%s*([%w_]+)%s*%)")
elseif tok:match(BRANCH_PATTERN) or tok:match(JUMP_REL_PATTERN) then elseif (isa and isa.kind == "branch") or tok:match(BRANCH_PATTERN) or tok:match(JUMP_REL_PATTERN) then
-- Conditional branch OR `jump_rel` (the within-atom-safe unconditional jump alias). -- Conditional branch OR `jump_rel` (the within-atom-safe unconditional jump alias).
-- Both encode a 16-bit signed relative word offset. -- Both encode a 16-bit signed relative word offset.
is_branch = true is_branch = true
branch_label = tok:match("atom_offset%s*%([^,]+,%s*([%w_]+)%s*%)") or false branch_label = tok:match("atom_offset%s*%([^,]+,%s*([%w_]+)%s*%)") or false
elseif matches_any(tok, UNCOND_JUMP_PATTERNS) then elseif ident == "jump" or ident == "call_addr" then
-- Unconditional absolute jump / call: `jump(off)` / `call_addr(off)`. -- Unconditional absolute jump / call: `jump(off)` / `call_addr(off)`.
-- One immediate offset field; can carry an `atom_offset(F, T)` marker (the offsets pass dispatches on `consuming_encoder` — see `passes/offsets.lua::compute_offsets`). -- One immediate offset field; can carry an `atom_offset(F, T)` marker (the offsets pass dispatches on `consuming_encoder` — see `passes/offsets.lua::compute_offsets`).
is_branch = true is_branch = true
is_unconditional_jump = true is_unconditional_jump = true
branch_label = tok:match("atom_offset%s*%([^,]+,%s*([%w_]+)%s*%)") or false branch_label = tok:match("atom_offset%s*%([^,]+,%s*([%w_]+)%s*%)") or false
elseif matches_any(tok, TERMINAL_JUMP_PATTERNS) then elseif ident == "jump_reg" or ident == "call_reg" or ident == "jump_link" then
-- Register-form jump / call: no offset field; `atom_offset` is invalid here (the offsets pass will error if one is supplied). -- Register-form jump / call: no offset field; `atom_offset` is invalid here (the offsets pass will error if one is supplied).
-- Transfers control OUT of the current atom — the CFG treats this as a path terminator. -- Transfers control OUT of the current atom — the CFG treats this as a path terminator.
is_terminal_jump = true is_terminal_jump = true
@@ -427,8 +401,7 @@ end
local function is_gte_command(consumer_event) local function is_gte_command(consumer_event)
local tok = consumer_event.encoder or consumer_event.ident or "" local tok = consumer_event.encoder or consumer_event.ident or ""
if tok:sub(1, 9) == "gte_cmdw_" then return true end if tok:sub(1, 9) == "gte_cmdw_" then return true end
local aliases = duffle.GTE_COMMAND_ALIASES or {} return duffle.gte(tok) ~= nil
return aliases[tok] ~= nil
end end
-- True iff `consumer_word` falls inside the COP2 command's input set OR inside the producer's `fanout_to` set (for IRGB writes). -- True iff `consumer_word` falls inside the COP2 command's input set OR inside the producer's `fanout_to` set (for IRGB writes).
@@ -443,12 +416,11 @@ local function is_cop2_consumer_of(consumer_event, destination, producer_rel)
if pos == destination then return true end if pos == destination then return true end
end end
-- Match via the command's input set: the consumer encoder resolves to a `gte_cmdw_*` -- Match via the command's input set: the consumer encoder resolves to a `gte_cmdw_*`
-- short form whose `duffle.GTE_COMMAND_INPUTS` entry includes the destination (or a fan-out target). -- short form whose GTE_COMMAND.inputs includes the destination (or a fan-out target).
local aliases = duffle.GTE_COMMAND_ALIASES or {} local gte = duffle.gte(consumer_token)
local canonical = aliases[consumer_token] or consumer_token local canonical = duffle.gte_canon(consumer_token)
if canonical:sub(1, 9) == "gte_cmdw_" or aliases[consumer_token] then if gte or canonical:sub(1, 9) == "gte_cmdw_" then
local inputs = duffle.GTE_COMMAND_INPUTS or {} local cmd_inputs = gte and gte.inputs
local cmd_inputs = inputs[canonical]
if cmd_inputs then if cmd_inputs then
-- Direct hit. -- Direct hit.
for _, in_reg in ipairs(cmd_inputs) do for _, in_reg in ipairs(cmd_inputs) do
@@ -659,8 +631,11 @@ local function apply_gpr_effects(ev, forward_state)
local ev_ident = ev.encoder or ev.ident local ev_ident = ev.encoder or ev.ident
local ev_args = ev.args or {} local ev_args = ev.args or {}
local gpr_values = forward_state.gpr_values local gpr_values = forward_state.gpr_values
local effects = duffle.INSTRUCTION_GPR_EFFECTS or {} local isa = duffle.instr(ev_ident)
local row = effects[ev_ident] local row = isa and (isa.reads or isa.writes) and isa or nil
if row == nil and duffle.gte(ev_ident) then
row = { reads = {}, writes = {} }
end
if row == nil then if row == nil then
for pos, operand in ipairs(ev_args) do for pos, operand in ipairs(ev_args) do
local key = gpr_identity(ev, pos) or operand local key = gpr_identity(ev, pos) or operand
@@ -671,7 +646,7 @@ local function apply_gpr_effects(ev, forward_state)
return return
end end
local value_rule = (duffle.GPR_VALUE_RULES or {})[ev_ident] local value_rule = isa and isa.value
local value = value_rule and evaluate_gpr_value_rule(value_rule, ev_args, gpr_values) or nil local value = value_rule and evaluate_gpr_value_rule(value_rule, ev_args, gpr_values) or nil
for _, position in ipairs(row.writes or {}) do for _, position in ipairs(row.writes or {}) do
local destination = gpr_identity(ev, position) local destination = gpr_identity(ev, position)
@@ -688,8 +663,7 @@ end
-- Look up the alias of a GTE command ident. -- Look up the alias of a GTE command ident.
-- Defaults to the input ident so unknown idents surface rather than silently inheriting a 0-cycle command input set. -- Defaults to the input ident so unknown idents surface rather than silently inheriting a 0-cycle command input set.
local function canonical_command(ident) local function canonical_command(ident)
local aliases = duffle.GTE_COMMAND_ALIASES or {} return duffle.gte_canon(ident)
return aliases[ident] or ident
end end
-- True for a COP2/GTE use that can make a pending SR.CU2 transition observable. -- True for a COP2/GTE use that can make a pending SR.CU2 transition observable.
@@ -1041,8 +1015,8 @@ local function analyze_hardware_relations(atom)
local canonical = canonical_command(ev_ident) local canonical = canonical_command(ev_ident)
if canonical:sub(1, 9) == "gte_cmdw_" then if canonical:sub(1, 9) == "gte_cmdw_" then
-- Update the post-command role state. -- Update the post-command role state.
local outputs = duffle.GTE_COMMAND_OUTPUTS or {} local gte_row = duffle.gte(canonical)
local cmd_outputs = outputs[canonical] local cmd_outputs = gte_row and gte_row.outputs
if cmd_outputs then if cmd_outputs then
for _, out in ipairs(cmd_outputs) do for _, out in ipairs(cmd_outputs) do
if out.register then if out.register then
@@ -1058,8 +1032,7 @@ local function analyze_hardware_relations(atom)
end end
end end
-- Stage post-command latch relations for every measured output. -- Stage post-command latch relations for every measured output.
local latch_table = duffle.GTE_COMMAND_LATCH_WINDOWS or {} local cmd_latches = gte_row and gte_row.latch
local cmd_latches = latch_table[canonical]
if cmd_latches then if cmd_latches then
for _, latch in ipairs(cmd_latches) do for _, latch in ipairs(cmd_latches) do
if latch.register and latch.required then if latch.register and latch.required then
@@ -1237,7 +1210,6 @@ local function check_hazard_nop_use(atom, _pipe_ctx, findings)
local forward = atom.paths and atom.paths.forward_state local forward = atom.paths and atom.paths.forward_state
local events = atom.paths.word_events or {} local events = atom.paths.word_events or {}
-- GPR effects table used to resolve load destinations when classifying load-delay-slot nops. -- GPR effects table used to resolve load destinations when classifying load-delay-slot nops.
local gpr_effects = duffle.INSTRUCTION_GPR_EFFECTS or {}
if not events or #events == 0 then return end if not events or #events == 0 then return end
-- Runtime-helper atoms / components (e.g. tape_exit, ac_yield) carry `debug_skip = true` from the bare -- Runtime-helper atoms / components (e.g. tape_exit, ac_yield) carry `debug_skip = true` from the bare
-- `atom_dbg_skip` marker; their structural nops are part of the fixed handshake and not author choices. -- `atom_dbg_skip` marker; their structural nops are part of the fixed handshake and not author choices.
@@ -1260,8 +1232,10 @@ local function check_hazard_nop_use(atom, _pipe_ctx, findings)
-- Every BD-slot nop is structural; this check never reports on it. -- Every BD-slot nop is structural; this check never reports on it.
-- (The earlier `if not suppressed then is_bd_slot = true end` form inverted the suppression - the `mac_yield()` handshake's `jump_reg(R_AtomJmp)` was incorrectly flagged.) -- (The earlier `if not suppressed then is_bd_slot = true end` form inverted the suppression - the `mac_yield()` handshake's `jump_reg(R_AtomJmp)` was incorrectly flagged.)
local prev_ident = prev_ev.encoder or "" local prev_ident = prev_ev.encoder or ""
local bd_policies = duffle.CONTROL_TRANSFER_DELAY_SLOT_POLICIES or {} local prev_isa = duffle.instr(prev_ident)
local is_bd_slot = bd_policies[prev_ident] ~= nil local is_bd_slot = prev_isa
and (prev_isa.kind == "branch" or prev_isa.kind == "jump" or prev_isa.kind == "call")
and prev_isa.delay_slot ~= false
if not is_bd_slot then if not is_bd_slot then
-- Find a pending modeled relation that this nop would retire. -- Find a pending modeled relation that this nop would retire.
local retired = nil local retired = nil
@@ -1280,11 +1254,10 @@ local function check_hazard_nop_use(atom, _pipe_ctx, findings)
if f_word > ev_word then if f_word > ev_word then
local f_ident = future_ev.encoder or future_ev.ident or "" local f_ident = future_ev.encoder or future_ev.ident or ""
local f_args = future_ev.args or {} local f_args = future_ev.args or {}
local aliases = duffle.GTE_COMMAND_ALIASES or {} local f_gte = duffle.gte(f_ident)
local canonical = aliases[f_ident] or f_ident local canonical = duffle.gte_canon(f_ident)
if canonical:sub(1, 9) == "gte_cmdw_" then if canonical:sub(1, 9) == "gte_cmdw_" then
local inputs = duffle.GTE_COMMAND_INPUTS or {} local cmd_inputs = f_gte and f_gte.inputs
local cmd_inputs = inputs[canonical]
if cmd_inputs then if cmd_inputs then
for _, in_reg in ipairs(cmd_inputs) do for _, in_reg in ipairs(cmd_inputs) do
if in_reg == retired.destination then if in_reg == retired.destination then
@@ -1323,20 +1296,10 @@ local function check_hazard_nop_use(atom, _pipe_ctx, findings)
-- This `nop` is structurally required; classifying it as `modeled-required` is the correct signal -- This `nop` is structurally required; classifying it as `modeled-required` is the correct signal
-- (removing it would make the following instruction read the OLD value of the loaded register, a load-use hazard). -- (removing it would make the following instruction read the OLD value of the loaded register, a load-use hazard).
-- The `load_delay_violations` check (Concern 3) catches the actual read-side error; here we suppress the `modeled-redundant` misclassification. -- The `load_delay_violations` check (Concern 3) catches the actual read-side error; here we suppress the `modeled-redundant` misclassification.
-- The set of load instructions mirrors the LOAD_INSTRUCTION_IDENTS in `check_load_delay_slots`. local is_load_delay = prev_isa and prev_isa.kind == "load"
local load_idents = {
load_word = true,
load_half = true,
load_half_u = true,
load_byte = true,
load_byte_u = true,
gte_lw = true,
gte_lwc2 = true
}
local is_load_delay = load_idents[prev_ident] == true
if is_load_delay then if is_load_delay then
-- Determine the destination register from the load's `writes` field. -- Determine the destination register from the load's `writes` field.
local prev_writes = gpr_effects[prev_ident] and gpr_effects[prev_ident].writes or {} local prev_writes = prev_isa.writes or {}
local dest_pos = prev_writes[1] local dest_pos = prev_writes[1]
local load_dest = dest_pos and (gpr_identity(prev_ev, dest_pos) or (prev_ev.args or {})[dest_pos]) or "<load-destination>" local load_dest = dest_pos and (gpr_identity(prev_ev, dest_pos) or (prev_ev.args or {})[dest_pos]) or "<load-destination>"
local authored = dest_pos and (prev_ev.args or {})[dest_pos] or load_dest local authored = dest_pos and (prev_ev.args or {})[dest_pos] or load_dest
@@ -1383,8 +1346,7 @@ local function check_hazard_nop_use(atom, _pipe_ctx, findings)
-- Update the pending snapshot for the next iteration. -- Update the pending snapshot for the next iteration.
-- The replay is observation-only; we mirror the walker's staging -- The replay is observation-only; we mirror the walker's staging
-- behavior for MTC2 / CTC2 / LWC2 / MFC2 / CFC2 / MFC0 / command_latch. -- behavior for MTC2 / CTC2 / LWC2 / MFC2 / CFC2 / MFC0 / command_latch.
local aliases = duffle.GTE_COMMAND_ALIASES or {} local canonical = duffle.gte_canon(ev_ident)
local canonical = aliases[ev_ident] or ev_ident
if ev_ident == "gte_mv_to_data_r" or ev_ident == "gte_mv_to_ctrl_r" then if ev_ident == "gte_mv_to_data_r" or ev_ident == "gte_mv_to_ctrl_r" then
local relations_table = duffle.HARDWARE_RELATIONS or {} local relations_table = duffle.HARDWARE_RELATIONS or {}
for _, row in ipairs(relations_table) do for _, row in ipairs(relations_table) do
@@ -1407,8 +1369,7 @@ local function check_hazard_nop_use(atom, _pipe_ctx, findings)
end end
elseif canonical:sub(1, 9) == "gte_cmdw_" then elseif canonical:sub(1, 9) == "gte_cmdw_" then
-- Command: stage post-command latch relations (same as the walker). -- Command: stage post-command latch relations (same as the walker).
local latch_table = duffle.GTE_COMMAND_LATCH_WINDOWS or {} local cmd_latches = (duffle.gte(canonical) or {}).latch
local cmd_latches = latch_table[canonical]
if cmd_latches then if cmd_latches then
for _, latch in ipairs(cmd_latches) do for _, latch in ipairs(cmd_latches) do
if latch.register and latch.required then if latch.register and latch.required then
@@ -1459,13 +1420,16 @@ local function check_control_transfer_delay_slot_use(atom, pipe_ctx, findings)
-- Runtime-helper atoms / components (e.g. tape_exit, ac_yield) carry `debug_skip = true` from the bare -- Runtime-helper atoms / components (e.g. tape_exit, ac_yield) carry `debug_skip = true` from the bare
-- `atom_dbg_skip` marker; their structural BD slots are part of the fixed handshake. -- `atom_dbg_skip` marker; their structural BD slots are part of the fixed handshake.
if is_runtime_helper(atom) then return end if is_runtime_helper(atom) then return end
local policies = duffle.CONTROL_TRANSFER_DELAY_SLOT_POLICIES or {}
for event_idx, event in ipairs(events) do for event_idx, event in ipairs(events) do
-- Canonical word_events use `encoder` as the leading identifier of the emitting token). -- Canonical word_events use `encoder` as the leading identifier of the emitting token).
-- Focused inputs may supply `ident` when constructing isolated events. -- Focused inputs may supply `ident` when constructing isolated events.
local event_ident = event.encoder or event.ident local event_ident = event.encoder or event.ident
local slot_ident_field = event.encoder and "encoder" or "ident" local slot_ident_field = event.encoder and "encoder" or "ident"
local policy = policies[event_ident] local event_isa = duffle.instr(event_ident)
local policy = event_isa
and (event_isa.kind == "branch" or event_isa.kind == "jump" or event_isa.kind == "call")
and event_isa.delay_slot ~= false
and { family = event_isa.kind, suppress_arg1 = event_isa.suppress_arg1 }
if policy then if policy then
local arg1 = event.args and event.args[1] or nil local arg1 = event.args and event.args[1] or nil
local suppressed = policy.suppress_arg1 and policy.suppress_arg1[arg1] or nil local suppressed = policy.suppress_arg1 and policy.suppress_arg1[arg1] or nil
@@ -1518,7 +1482,6 @@ local function check_load_delay_slots(atom, pipe_ctx, findings)
local events = p.word_events or {} local events = p.word_events or {}
if #events == 0 then return end if #events == 0 then return end
local gpr_effects = duffle.INSTRUCTION_GPR_EFFECTS or {}
local read_positions = duffle.OPERAND_READ_POSITIONS or {} local read_positions = duffle.OPERAND_READ_POSITIONS or {}
-- volatile_until[reg] = 1-based word_events index; the slot AFTER which the register is safe. -- volatile_until[reg] = 1-based word_events index; the slot AFTER which the register is safe.
-- `nil` means "not currently volatile". -- `nil` means "not currently volatile".
@@ -1530,7 +1493,7 @@ local function check_load_delay_slots(atom, pipe_ctx, findings)
-- and for genuine RMW ops like `add rt, rs, rt` where position 1 IS both read+written) is not a "read" for load-delay purposes: -- and for genuine RMW ops like `add rt, rs, rt` where position 1 IS both read+written) is not a "read" for load-delay purposes:
-- The write shadows whatever value the register previously held. Only positions that are reads WITHOUT a co-occurring write to the same register count as net reads. -- The write shadows whatever value the register previously held. Only positions that are reads WITHOUT a co-occurring write to the same register count as net reads.
local function net_reads(event_ident, args) local function net_reads(event_ident, args)
local effect = gpr_effects[event_ident] local effect = duffle.instr(event_ident)
local positions = read_positions[event_ident] local positions = read_positions[event_ident]
if not positions then return {} end if not positions then return {} end
local writes_set = {} local writes_set = {}
@@ -1547,7 +1510,8 @@ local function check_load_delay_slots(atom, pipe_ctx, findings)
for event_idx, event in ipairs(events) do for event_idx, event in ipairs(events) do
local event_ident = event.encoder or event.ident local event_ident = event.encoder or event.ident
local args = event.args or {} local args = event.args or {}
local is_load = LOAD_INSTRUCTION_IDENTS[event_ident] == true local event_isa = duffle.instr(event_ident)
local is_load = event_isa and event_isa.kind == "load"
-- (1) Is this event reading a register that's still volatile from a previous load? -- (1) Is this event reading a register that's still volatile from a previous load?
-- Skip the load instruction itself (the load's own argument list may "read" its destination via `OPERAND_READ_POSITIONS`: -- Skip the load instruction itself (the load's own argument list may "read" its destination via `OPERAND_READ_POSITIONS`:
@@ -1577,7 +1541,7 @@ local function check_load_delay_slots(atom, pipe_ctx, findings)
end end
-- (2) Update the volatile set based on what this event writes. -- (2) Update the volatile set based on what this event writes.
local effect = gpr_effects[event_ident] local effect = event_isa
if effect and effect.writes then if effect and effect.writes then
for _, pos in ipairs(effect.writes) do for _, pos in ipairs(effect.writes) do
local reg = gpr_identity(event, pos) local reg = gpr_identity(event, pos)
@@ -2168,7 +2132,9 @@ local function analyze_atom_paths(atom, pipe_ctx)
unknown_set[ident] = true unknown_set[ident] = true
end end
else else
cost = duffle.INSTRUCTION_LATENCY[ident] local isa = duffle.instr(ident)
local gte = duffle.gte(ident)
cost = (isa and isa.cycles) or (gte and gte.cycles)
if cost == nil then if cost == nil then
cost = duffle.UNKNOWN_INSTRUCTION_CYCLES cost = duffle.UNKNOWN_INSTRUCTION_CYCLES
unknown_set[ident] = true unknown_set[ident] = true
@@ -2929,12 +2895,12 @@ end
-- from duffle.lua. Only fires on parseable integer literals; register names, -- from duffle.lua. Only fires on parseable integer literals; register names,
-- O_(...) offsets, atom_offset(...) markers, and enum tokens are skipped. -- O_(...) offsets, atom_offset(...) markers, and enum tokens are skipped.
local function check_immediate_field_width(atom, pipe_ctx, findings) local function check_immediate_field_width(atom, pipe_ctx, findings)
local widths = duffle.IMMEDIATE_FIELD_WIDTHS or {}
local events = atom.paths and atom.paths.word_events or {} local events = atom.paths and atom.paths.word_events or {}
local line_for_word_event = pipe_ctx.line_for_word_event local line_for_word_event = pipe_ctx.line_for_word_event
for _, ev in ipairs(events) do for _, ev in ipairs(events) do
local ev_ident = ev.encoder or ev.ident or "?" local ev_ident = ev.encoder or ev.ident or "?"
local rules = widths[ev_ident] local isa = duffle.instr(ev_ident)
local rules = isa and isa.imm
if rules then if rules then
local ev_args = ev.args or {} local ev_args = ev.args or {}
local ev_line = line_for_word_event and line_for_word_event(ev) or atom.line local ev_line = line_for_word_event and line_for_word_event(ev) or atom.line
@@ -3040,7 +3006,7 @@ local function collect_gpr_traffic(tokens)
and ident ~= "nop" and ident ~= "atom_label" and ident ~= "atom_offset" and ident ~= "nop" and ident ~= "atom_label" and ident ~= "atom_offset"
then then
local args = token_arg_list(tok) local args = token_arg_list(tok)
local fx = (duffle.INSTRUCTION_GPR_EFFECTS or {})[ident] local fx = duffle.instr(ident)
if fx then if fx then
for _, pos in ipairs(fx.reads or {}) do for _, pos in ipairs(fx.reads or {}) do
local g = arg_as_gpr(args[pos]) local g = arg_as_gpr(args[pos])
@@ -3228,42 +3194,11 @@ local CHECK_RULES = {
--- @param ctx PassCtx --- @param ctx PassCtx
--- @return PipeCtx --- @return PipeCtx
local function build_corpus_pipe_ctx(ctx) local function build_corpus_pipe_ctx(ctx)
local corpus = ctx.shared and ctx.shared.corpus local view = duffle.corpus_view(ctx)
if not corpus then view.components_by_name = view.components
error("static_analysis requires ctx.shared.corpus " view.atom_infos_list = view.atom_infos
.. "(the canonical corpus is the source of truth; " view.gte_cr_alias_groups = duffle.GTE_CR_ALIAS_GROUPS or {}
.. "no per-source fallback is supported)", 0) return view
end
-- The pipe_ctx views REFERENCE the corpus tables directly (no copies).
-- Every consumer observes mutations through the corpus tables directly.
return {
-- Cross-source lookup tables.
register_alias_registry = corpus.register_alias_registry or {},
type_name_registry = corpus.type_name_registry or {},
atom_views = corpus.atom_views or {},
atom_ctxs = corpus.atom_ctxs or {},
atom_phases = corpus.atom_phases or {},
binds_by_name = corpus.binds_by_name or {},
atoms_by_name = corpus.atoms_by_name or {},
-- Per-component metadata (cycle_cost + gp0_contrib) auto-derived from the original
-- `MipsAtomComp_` body by `passes/components.lua::compute_components_metadata`.
-- Keyed by bare name (e.g. `format_f3_color`, `gte_store_f3`); the `mac_` prefix at call sites is stripped before lookup.
components_by_name = corpus.components or {},
atoms_by_name = corpus.atoms_by_name or {},
tape_chains = corpus.tape_chains or {},
source_order = corpus.source_order or {},
component_atom_infos = corpus.component_atom_infos or {},
atom_infos = corpus.atom_infos or {},
-- Corpus-wide ordered list of atom_info records (source-order + duplicates).
atom_infos_list = corpus.atom_infos or {},
-- Corpus-wide collisions (recorded by scan_source.merge_corpus_registries).
collisions = corpus.collisions or {},
-- GTE control-register alias groups (from `duffle.GTE_CR_ALIAS_GROUPS`).
-- The three new per_atom checks (gte_cr_alias_writes, rtdiagonal_completeness,
-- gte_cr_TR_naming) read from this view. `duffle` is exposed alongside so
-- `find_alias_pair_for` can resolve alias → group without a separate registry.
gte_cr_alias_groups = duffle.GTE_CR_ALIAS_GROUPS or {},
}
end end
local function validate(ctx, src, corpus_pipe_ctx) local function validate(ctx, src, corpus_pipe_ctx)
@@ -3360,17 +3295,13 @@ local function validate(ctx, src, corpus_pipe_ctx)
-- Run all per-atom checks on this one atom via the CHECK_RULES data table. -- Run all per-atom checks on this one atom via the CHECK_RULES data table.
-- Adding a new check = 1 row in CHECK_RULES; this loop never needs editing. -- Adding a new check = 1 row in CHECK_RULES; this loop never needs editing.
for _, rule in ipairs(CHECK_RULES) do duffle.run_check_rules(CHECK_RULES, "per_atom", a, pipe_ctx, findings)
if rule.per_atom then rule.per_atom(a, pipe_ctx, findings) end
end
end end
-- Per-source dispatch. Run once per source AFTER the per-atom loop; -- Per-source dispatch. Run once per source AFTER the per-atom loop;
-- consults pipe_ctx's cross-atom registries (register_alias_registry, type_name_registry). -- consults pipe_ctx's cross-atom registries (register_alias_registry, type_name_registry).
-- Same CHECK_RULES table; no parallel dispatch table. -- Same CHECK_RULES table; no parallel dispatch table.
for _, rule in ipairs(CHECK_RULES) do duffle.run_check_rules(CHECK_RULES, "per_source", src, pipe_ctx, findings)
if rule.per_source then rule.per_source(src, pipe_ctx, findings) end
end
-- Three-way severity binning: per-finding severity is set by the check via `f.kind`. -- Three-way severity binning: per-finding severity is set by the check via `f.kind`.
-- "error" / "warning" / "info" are all distinct; info findings are NEVER folded into warnings. -- "error" / "warning" / "info" are all distinct; info findings are NEVER folded into warnings.