Curation pass: reduce nested conditional branching in some defnitions.

This commit is contained in:
ed
2026-07-25 13:00:36 -04:00
parent 653e18ee28
commit 590ff1e2ec
6 changed files with 310 additions and 312 deletions
+172 -153
View File
@@ -442,7 +442,7 @@ function M.read_balanced(s, open_char, close_char, pos)
local c = s:byte(pos)
if c == open_byte then
depth = depth + 1
pos = pos + 1
pos = pos + 1
-- scan: <open_char> <inner...> <open_char> (depth=depth)
elseif c == close_char:byte() then
depth = depth - 1
@@ -572,65 +572,87 @@ function M.parse_direct_quoted_includes(source_text)
error("parse_direct_quoted_includes requires source text", 2)
end
-- Each arm's effect on (pos, line_leading) is annotated at the branch site.
-- Arm order: newline / horiz-space / '//' / '/*' / '"' / '\'' / '#' / default.
local logical_text, physical_pos, physical_line = splice_c_lines(source_text)
local includes = {}
local pos = 1
local line_leading = true
while pos <= #logical_text do
local byte = logical_text:byte(pos)
if byte == BYTE_NEWLINE then
if byte == BYTE_NEWLINE then
-- line break; refresh leading-whitespace state for next line.
line_leading = true
pos = pos + 1
elseif is_horizontal_space(byte) then
-- ordinary inter-token whitespace; preserve current leading-ness.
pos = pos + 1
elseif byte == BYTE_SLASH and logical_text:byte(pos + 1) == BYTE_SLASH then
-- '//' line comment: skip_str_or_cmt walks to EOL on its own, so no separate newline scan is needed here.
local after = M.skip_str_or_cmt(logical_text, pos)
-- pos := after when the skipper agrees, else single-byte advance.
pos = (after > pos) and after or (pos + 1)
elseif byte == BYTE_SLASH and logical_text:byte(pos + 1) == BYTE_STAR then
-- '/*' block comment.
local after = M.skip_str_or_cmt(logical_text, pos)
if after == pos then
if after <= pos then
-- skipper refused (unterminated /*). Treat this byte as ordinary content: step one, mark non-leading.
line_leading = false
pos = pos + 1
pos = pos + 1
else
-- jump past the closing '*/'. The span may cross lines, so rescan for embedded '\n' to refresh line_leading.
for scan = pos, after - 1 do
if logical_text:byte(scan) == BYTE_NEWLINE then line_leading = true end
end
pos = after
end
elseif byte == BYTE_DQUOTE or byte == BYTE_SQUOTE then
-- enter+leave the string literal in one skip; literal bodies cannot contain a directive regardless of what they look like.
line_leading = false
local after = M.skip_str_or_cmt(logical_text, pos)
pos = (after > pos) and after or (pos + 1)
elseif byte == 35 and line_leading then -- '#'
local hash_pos = pos
local directive_line = physical_line[hash_pos] or 1
local scan = skip_directive_space(logical_text, pos + 1)
local ident, after_ident
if scan then ident, after_ident = M.read_ident(logical_text, scan) end
if ident == "include" then
scan = skip_directive_space(logical_text, after_ident)
end
if ident == "include" and scan and logical_text:byte(scan) == BYTE_DQUOTE then
local after_quote = M.skip_str_or_cmt(logical_text, scan)
if after_quote > scan and logical_text:byte(after_quote - 1) == BYTE_DQUOTE then
local include_path = logical_text:sub(scan + 1, after_quote - 2)
local physical_first = physical_pos[hash_pos]
local physical_last = physical_pos[after_quote - 1]
includes[#includes + 1] = {
path = include_path,
include_path = include_path,
include_text = M.trim(source_text:sub(physical_first, physical_last)),
line = directive_line,
}
pos = after_quote
else
pos = pos + 1
end
else
pos = pos + 1
local after = M.skip_str_or_cmt(logical_text, pos)
pos = (after > pos) and after or (pos + 1)
elseif byte == 35 and line_leading then -- '#' at line head
-- Sequential pre-checks; any one failing falls through to ::not_include:: (single-byte advance).
-- Full success pushes the record and jumps to ::directive_done:: without ever entering the not-include path.
-- (All locals are pre-declared at the top of this arm because Lua forbids a goto from crossing a local declaration into its scope.)
local hash_pos, directive_line, scan, ident, after_ident, after_quote
local include_path, physical_first, physical_last
hash_pos = pos
directive_line = physical_line[hash_pos] or 1
scan = skip_directive_space(logical_text, pos + 1)
if not scan then goto not_include end
ident, after_ident = M.read_ident(logical_text, scan)
if ident ~= "include" then goto not_include end
scan = skip_directive_space(logical_text, after_ident)
if not scan then goto not_include end
if logical_text:byte(scan) ~= BYTE_DQUOTE then goto not_include end
after_quote = M.skip_str_or_cmt(logical_text, scan)
if not (after_quote > scan and logical_text:byte(after_quote - 1) == BYTE_DQUOTE) then
goto not_include
end
-- success: build the include record; pos jumps past closing '"'.
include_path = logical_text:sub(scan + 1, after_quote - 2)
physical_first = physical_pos[hash_pos]
physical_last = physical_pos[after_quote - 1]
includes[#includes + 1] = {
path = include_path,
include_path = include_path,
include_text = M.trim(source_text:sub(physical_first, physical_last)),
line = directive_line,
}
pos = after_quote
goto directive_done
::not_include::
-- any pre-check failure: '#' is ordinary content; advance one.
pos = pos + 1
::directive_done::
-- '#' at line head clears the leading-whitespace state.
line_leading = false
else
-- ordinary source character; mark non-leading, advance one.
line_leading = false
pos = pos + 1
end
@@ -2226,31 +2248,36 @@ local function _project_emission_inner(root_body_entry, ctx_table)
local function emit_embedded_markers(tok, tok_line)
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 ident then
if ident == "atom_label" or ident == "atom_offset" then
local open = M.skip_ws_and_cmt(tok, after)
local inner, after_paren = M.read_parens(tok, open)
if inner then
local args = split_top_level_args(inner)
if ident == "atom_label" then
emit_marker("label", args[1] or "", nil, tok_line)
else
emit_marker("offset", args[1] or "", args[2] or "", tok_line)
end
pos = after_paren
else
pos = after
end
else
pos = after
end
else
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 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.
local args = split_top_level_args(inner)
if ident == "atom_label" then emit_marker("label", args[1] or "", nil, tok_line)
else emit_marker("offset", args[1] or "", args[2] or "", tok_line)
end
pos = after_paren
::continue_loop::
end
end
@@ -2336,108 +2363,100 @@ local function _project_emission_inner(root_body_entry, ctx_table)
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
for _, bt in ipairs(tokens) do
-- 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
local ident = M.read_ident(tok, 1) or "?"
local _, args = token_ident_and_args(tok)
local tok_line = line_of(body_off + bt.rel) or 0
if ident ~= "atom_label" and ident ~= "atom_offset" then emit_embedded_markers(tok, tok_line) end
if ident == "atom_label" then emit_marker("label", args[1] or "", nil, tok_line)
elseif ident == "atom_offset" then emit_marker("offset", args[1] or "", args[2] or "", tok_line)
elseif 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: this component is already on the expansion stack.
-- Still allocate an ID, still emit invoke_begin / invoke_end (zero-width), still record the cycle error — but 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)
else
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 component body:
-- * `immediate_call_text` for the recursive walk is THIS call's token text (the immediate outer call for any word emitted inside this body).
-- * `root_call_text` is the OUTERMOST call. Immutable value computed before this invocation was opened.
local inner_immediate_call_text = tok
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,
}
, inv.id
, invocation_root_call_text
, inner_immediate_call_text
)
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
-- Declared-vs-measured 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
end
else
-- mac_X is NOT a known component. Resolve the count from `word_counts`; if unresolved, emit 1 opaque word + one warning.
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)
end
end
else
-- Direct encoder (non-mac_X). Resolve count; emit N words.
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)
end
end
if tok == "" then return end
local ident = M.read_ident(tok, 1) or "?"
local _, args = token_ident_and_args(tok)
local tok_line = line_of(body_off + bt.rel) or 0
-- embedded markers live only in non-marker tokens.
if ident ~= "atom_label" and ident ~= "atom_offset" then emit_embedded_markers(tok, tok_line) end
-- atom_label / atom_offset: terminal markers, no further descent.
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)
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,
},
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)
end
end
for _, bt in ipairs(tokens) do
process_token(bt)
end
end
-4
View File
@@ -516,19 +516,15 @@ function M.run(ctx)
end
if has_projection then
local basename = duffle.basename_no_ext(src.path)
-- (1) atoms.sourcemap.txt — format-1 per-word call-site map.
local sourcemap_path = ctx.out_root .. "/" .. basename .. ".atoms.sourcemap.txt"
local sourcemap_body = render_source_map(src)
-- (2) atoms.provenance.txt — format-1 per-word definition/body map.
local prov_path = ctx.out_root .. "/" .. basename .. ".atoms.provenance.txt"
local prov_body = render_provenance(src, wc)
duffle.ensure_dir(duffle.dirname(sourcemap_path))
duffle.write_file_lf(sourcemap_path, sourcemap_body)
duffle.write_file_lf(prov_path, prov_body)
outputs[#outputs + 1] = { kind = "report", path = sourcemap_path }
outputs[#outputs + 1] = { kind = "report", path = prov_path }
end
+15 -25
View File
@@ -144,8 +144,7 @@ local function preceding_comment_block(source, pos)
local scan_pos = pos
local pieces = {}
while true do
-- Skip whitespace (space/tab/newline/CR) backward from `scan_pos`,
-- returning the position of the first non-whitespace char.
-- skip whitespace backward; land on the next non-ws character.
local non_ws = scan_pos - 1
while non_ws > 0 do
local ch = source:sub(non_ws, non_ws)
@@ -156,13 +155,9 @@ local function preceding_comment_block(source, pos)
end
end
if non_ws == 0 then break end
local is_block_close = non_ws >= 2 and source:sub(non_ws - 1, non_ws) == "*/"
local is_line_end = source:sub(non_ws, non_ws) == "\n" or source:sub(non_ws, non_ws) == "\r"
if is_block_close then
-- Find the opening `/*` for a block comment whose `*/` ends at `non_ws`.
-- Walk back from `non_ws` over `/*` candidates.
if non_ws >= 2 and source:sub(non_ws - 1, non_ws) == "*/" then
-- block comment close: find the opening /* by walking back over /* candidates
-- in source[1..non_ws-1].
local prefix = source:sub(1, non_ws - 1)
local open_at = nil
for scan = #prefix - 1, 1, -1 do
@@ -172,33 +167,28 @@ local function preceding_comment_block(source, pos)
end
end
if not open_at then break end
-- Walk back from `open_at` over leading spaces + tabs to include the indentation before the `/*`.
-- include the indentation before the /* by walking back over leading spaces + tabs.
local block_start = open_at
while block_start > 1 do
local ch = source:sub(block_start - 1, block_start - 1)
if ch == " " or ch == "\t" then
block_start = block_start - 1
else
break
end
if ch ~= " " and ch ~= "\t" then break end
block_start = block_start - 1
end
table.insert(pieces, 1, source:sub(block_start, non_ws))
scan_pos = block_start
elseif is_line_end then
-- Walk back from `non_ws` to the start of the source line (the most recent `\n` or position 1).
else
-- line comment path: must end in newline, must start with //.
local ch = source:sub(non_ws, non_ws)
if ch ~= "\n" and ch ~= "\r" then break end
-- walk back from non_ws to the start of the source line (most recent \n or position 1).
local line_start = non_ws
while line_start > 1 and source:sub(line_start - 1, line_start - 1) ~= "\n" do
line_start = line_start - 1
end
local line = source:sub(line_start, non_ws)
if line:sub(1, 2) == "//" then
table.insert(pieces, 1, line)
scan_pos = line_start - 1
else
break
end
else
break
if line:sub(1, 2) ~= "//" then break end
table.insert(pieces, 1, line)
scan_pos = line_start - 1
end
end
if #pieces == 0 then return "" end
+83 -71
View File
@@ -700,85 +700,97 @@ end
--- @param skip_over table -- {atoms = {[symbol] = association}, components = {[file|name] = association}}
--- @return table[] -- list of {name, addr, size_bytes, words, entries, invocations, skip_over?}
local function build_atom_table(corpus, addrs, skip_over)
-- Cross-ref: keep only atoms present in BOTH the nm symbol table AND
-- the canonical corpus projection. Output is sorted by ascending addr.
local atoms_by_name = corpus.atoms_by_name or {}
-- Cross-ref: keep only the atoms that exist in BOTH the nm symbol table AND the canonical corpus projection.
-- Address-ascending sort + lexical Stable tie-breaker: declaration order, then symbol address.
local out = {}
for name, info in pairs(addrs) do
-- Per-atom ingest. Returns nil if the atom is absent from the corpus
-- (caller skips it via the `if atom then ...` guard).
local function ingest_atom(name, info)
local atom_record = atoms_by_name[name]
if atom_record then
local paths = atom_record.paths or {}
local word_events = paths.word_events or {}
local invocations_proj = paths.invocations or {}
if not atom_record then return nil end
-- Build the dense entries list from `word_events`. `word_events[i].i` is the 0-based `.word` position;
-- `call_line` is the root atom's physical source line for that word (stamped by emission_model).
local entries = {}
for idx, ev in ipairs(word_events) do
entries[#entries + 1] = {
pos = ev.i or (idx - 1),
line = ev.call_line or 0,
text = ev.call_text or "",
local paths = atom_record.paths or {}
local word_events = paths.word_events or {}
local invocations_proj = paths.invocations or {}
-- Build the dense entries list from `word_events`.
-- `word_events[i].i` = the 0-based `.word` position
-- `call_line` = the root atom's physical source line for that word
-- (stamped by emission_model)
local entries = {}
for idx, ev in ipairs(word_events) do
entries[#entries + 1] = {
pos = ev.i or (idx - 1),
line = ev.call_line or 0,
text = ev.call_text or "",
}
end
local atom = {
name = name,
addr = info[1],
size_bytes = info[2],
words = #word_events,
entries = entries,
skip_over = skip_over.atoms[name] ~= nil,
}
-- Group consecutive `word_events` rows whose outermost invocation is the SAME
-- format-1 invocation into a single `atom.invocations` entry. Rows sharing the
-- same comp_name / call_file / call_line / comp_file / comp_line are part of
-- the same group. Rows outside any invocation flush cur_inv.
if #invocations_proj > 0 then
local invocations = {}
-- Process one word_event row against the current group state.
-- Returns the (possibly updated) cur_inv.
local function row(ev, cur_inv)
local outer_id = ev.outermost_invocation_id
local outer_inv = outer_id and invocations_proj[outer_id] or nil
if not (outer_inv and outer_inv.component_name) then
-- raw row: flush any pending cur_inv; no new group starts.
if cur_inv then invocations[#invocations + 1] = cur_inv end
return nil
end
local inv_key = outer_inv.component_name
.. "|" .. (outer_inv.call_path or "")
.. "|" .. tostring(outer_inv.call_line or 0)
.. "|" .. (outer_inv.def_path or "")
.. "|" .. tostring(outer_inv.def_line or 0)
local ev_pos = ev.i or 0
if cur_inv and cur_inv.key == inv_key then
-- same group: extend range + append body line.
cur_inv.end_pos = ev_pos
cur_inv.body_lines[#cur_inv.body_lines + 1] = ev.body_line or 0
return cur_inv
end
-- key changed (or no current group): flush + start new.
if cur_inv then invocations[#invocations + 1] = cur_inv end
return {
key = inv_key,
comp_name = outer_inv.component_name,
call_file = outer_inv.call_path or "",
call_line = outer_inv.call_line or 0,
comp_file = outer_inv.def_path or "",
comp_line = outer_inv.def_line or 0,
start_pos = ev_pos,
end_pos = ev_pos,
skip_over = skip_over.components[normalize_debug_path(outer_inv.def_path or ""):lower() .. "\0" .. outer_inv.component_name] ~= nil,
body_lines = { ev.body_line or 0 },
}
end
local atom = {
name = name,
addr = info[1],
size_bytes = info[2],
words = #word_events,
entries = entries,
skip_over = skip_over.atoms[name] ~= nil,
}
-- Group consecutive `word_events` rows whose outermost invocation
-- is the SAME format-1 invocation into a single `atom.invocations`
-- entry. Keep entries grouped by outermost invocation
-- (two consecutive rows with the same comp_name/call_file/call_line/
-- comp_file/comp_line are part of the same invocation).
if #invocations_proj > 0 then
local invocations = {}
local cur_inv = nil
for _, ev in ipairs(word_events) do
local outer_id = ev.outermost_invocation_id
local outer_inv = outer_id and invocations_proj[outer_id] or nil
if outer_inv and outer_inv.component_name then
local inv_key = outer_inv.component_name
.. "|" .. (outer_inv.call_path or "")
.. "|" .. tostring(outer_inv.call_line or 0)
.. "|" .. (outer_inv.def_path or "")
.. "|" .. tostring(outer_inv.def_line or 0)
local ev_pos = ev.i or 0
if cur_inv and cur_inv.key == inv_key then
cur_inv.end_pos = ev_pos
cur_inv.body_lines[#cur_inv.body_lines + 1] = ev.body_line or 0
else
if cur_inv then invocations[#invocations + 1] = cur_inv end
cur_inv = {
key = inv_key,
comp_name = outer_inv.component_name,
call_file = outer_inv.call_path or "",
call_line = outer_inv.call_line or 0,
comp_file = outer_inv.def_path or "",
comp_line = outer_inv.def_line or 0,
start_pos = ev_pos,
end_pos = ev_pos,
skip_over = skip_over.components[normalize_debug_path(outer_inv.def_path or ""):lower()
.. "\0" .. outer_inv.component_name] ~= nil,
body_lines = { ev.body_line or 0 },
}
end
else
-- RAW row: flush the current invocation.
if cur_inv then invocations[#invocations + 1] = cur_inv; cur_inv = nil end
end
end
if cur_inv then invocations[#invocations + 1] = cur_inv end
atom.invocations = invocations
local cur_inv = nil
for _, ev in ipairs(word_events) do
cur_inv = row(ev, cur_inv)
end
out[#out + 1] = atom
if cur_inv then invocations[#invocations + 1] = cur_inv end
atom.invocations = invocations
end
return atom
end
local out = {}
for name, info in pairs(addrs) do
local atom = ingest_atom(name, info)
if atom then out[#out + 1] = atom end
end
table.sort(out, function(a, b) return a.addr < b.addr end)
return out
+32 -49
View File
@@ -134,60 +134,43 @@ function M.run(ctx)
if type(corpus) ~= "table" then error("emission_model: ctx.shared.corpus is required (canonical projection)", 0) end
if type(corpus.source_order) ~= "table" then error("emission_model: ctx.shared.corpus.source_order is required", 0) end
-- Walk every source in canonical source order; for each source, iterate atoms.
-- Atom declarations (`kind == "atom"` / `"raw_atom"`) AND component declarations
-- (`comp_bare` / `comp_proc`) each receive the canonical `atom.paths` projection.
-- Components are macros inlined into atom bodies; focused tests and isolated
-- component analyses read them from `atom.paths` on the component record.
-- The per-atom emission projection is produced by `duffle.project_emission` (this pass).
-- Test-only fixtures may consume `atom.paths.word_events` directly from the emission-model pass output.
-- Project once, collect errors + warnings for one atom.
-- Kind must be one of: atom | raw_atom | comp_bare | comp_proc.
local function process_atom(atom, src)
if not (atom and atom.body) then return end
local kind = atom.kind
if kind ~= "atom" and kind ~= "raw_atom" and kind ~= "comp_bare" and kind ~= "comp_proc" then
return
end
local proj = project_atom(atom, src, corpus)
for _, e in ipairs(proj.errors) do
-- Preserve `kind` (cycle / count_mismatch / unbalanced) so readers can dispatch on the diagnostic class without re-parsing the message string.
errors[#errors + 1] = {
kind = e.kind,
line = e.line,
msg = e.msg,
source = e.source or src.path,
}
end
for _, w in ipairs(proj.warnings) do
warnings[#warnings + 1] = {
kind = w.kind,
line = w.line,
msg = w.msg,
}
end
end
-- Walk every source in canonical order; for each source, iterate atoms + raw_atoms.
-- Recognized kinds (atom | raw_atom | comp_bare | comp_proc) each receive the atom.paths projection via duffle.project_emission.
-- Components are macros inlined into atom bodies; focused tests and isolated component analyses consume atom.paths directly.
for _, src in ipairs(corpus.source_order) do
local scan = src.scan or {}
for _, atom in ipairs(scan.atoms or {}) do
if atom and atom.body and (
atom.kind == "atom" or
atom.kind == "raw_atom" or
atom.kind == "comp_bare" or
atom.kind == "comp_proc"
) then
local proj = project_atom(atom, src, corpus)
for _, e in ipairs(proj.errors) do
-- Preserve `kind` (cycle / count_mismatch / unbalanced) so readers can dispatch on the diagnostic class without re-parsing the message string.
errors[#errors + 1] = {
kind = e.kind,
line = e.line,
msg = e.msg,
source = e.source or src.path,
}
end
for _, w in ipairs(proj.warnings) do
warnings[#warnings + 1] = {
kind = w.kind,
line = w.line,
msg = w.msg,
}
end
end
process_atom(atom, src)
end
for _, atom in ipairs(scan.raw_atoms or {}) do
if atom and atom.body then
local proj = project_atom(atom, src, corpus)
for _, e in ipairs(proj.errors) do
errors[#errors + 1] = {
kind = e.kind,
line = e.line,
msg = e.msg,
source = e.source or src.path,
}
end
for _, w in ipairs(proj.warnings) do
warnings[#warnings + 1] = {
kind = w.kind,
line = w.line,
msg = w.msg,
}
end
end
process_atom(atom, src)
end
end
+8 -10
View File
@@ -1,16 +1,14 @@
--- passes/static_analysis.lua — Per-atom static-analysis checks.
---
--- Per-atom rules:
--- 1. transfer_hazards: A single forward walker (`analyze_hardware_relations`) reads `atom.paths.word_events`
--- once per atom. For each emitted word event it (a) inspects pending CPU/COP0/COP2/GTE relations against
--- the event as CONSUMER (recording a hazard on `atom.paths.hazards` when the producer→consumer gap is below
--- the required retire-slot count), (b) applies the event's GPR value effects (`duffle.INSTRUCTION_GPR_EFFECTS`)
--- to `atom.paths.forward_state.gpr_values`, applies bounded constant propagation, and stages
--- matching relation rows as PRODUCERS (with `destination_match` filters, e.g. for the IRGB fan-out). The
--- `transfer_hazards` CHECK_RULES reader projects `atom.paths.hazards` into per-atom findings without
--- re-walking source. The walker runs once per atom before the per-atom dispatch; the reader runs inside
--- the same dispatch.
--- 2. control_transfer_delay_slot_use: For every emitted branch/jump/call encoder in `duffle.CONTROL_TRANSFER_DELAY_SLOT_POLICIES`
--- 1. transfer_hazards: A single forward walker (`analyze_hardware_relations`) reads `atom.paths.word_events` once per atom.
--- For each emitted word event it (a) inspects pending CPU/COP0/COP2/GTE relations against the event as CONSUMER
--- (recording a hazard on `atom.paths.hazards` when the producer→consumer gap is below the required retire-slot count),
--- (b) applies the event's GPR value effects (`duffle.INSTRUCTION_GPR_EFFECTS`) to `atom.paths.forward_state.gpr_values`,
--- applies bounded constant propagation, and stages matching relation rows as PRODUCERS (with `destination_match` filters, e.g. for the IRGB fan-out).
--- The `transfer_hazards` CHECK_RULES reader projects `atom.paths.hazards` into per-atom findings without re-walking source.
--- The walker runs once per atom before the per-atom dispatch; the reader runs inside the same dispatch.
--- 2. control_transfer_delay_slot_use: For every emitted branch/jump/call encoder in `duffle.CONTROL_TRANSFER_DELAY_SLOT_POLICIES`
--- (the six `branch_*` encoders plus `jump` / `jump_reg` / `jump_link` / `call_reg` / `call_addr`),
--- inspect the next emitted event in `atom.paths.word_events`.
--- Emit an `info`-severity finding when the successor is `nop` or absent (the next emitted word IS the hardware delay slot).