From 590ff1e2eced9acf6c3be34459822b2e2e569be4 Mon Sep 17 00:00:00 2001 From: Ed_ Date: Sat, 25 Jul 2026 13:00:36 -0400 Subject: [PATCH] Curation pass: reduce nested conditional branching in some defnitions. --- scripts/duffle.lua | 325 +++++++++++++++------------- scripts/passes/atoms_source_map.lua | 4 - scripts/passes/components.lua | 40 ++-- scripts/passes/dwarf_injection.lua | 154 +++++++------ scripts/passes/emission_model.lua | 81 +++---- scripts/passes/static_analysis.lua | 18 +- 6 files changed, 310 insertions(+), 312 deletions(-) diff --git a/scripts/duffle.lua b/scripts/duffle.lua index 76e75c1..d610201 100644 --- a/scripts/duffle.lua +++ b/scripts/duffle.lua @@ -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: (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 diff --git a/scripts/passes/atoms_source_map.lua b/scripts/passes/atoms_source_map.lua index 85bdda3..cbd8edc 100644 --- a/scripts/passes/atoms_source_map.lua +++ b/scripts/passes/atoms_source_map.lua @@ -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 diff --git a/scripts/passes/components.lua b/scripts/passes/components.lua index 97b5fac..6026d80 100644 --- a/scripts/passes/components.lua +++ b/scripts/passes/components.lua @@ -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 diff --git a/scripts/passes/dwarf_injection.lua b/scripts/passes/dwarf_injection.lua index 651f2ea..73da3fd 100644 --- a/scripts/passes/dwarf_injection.lua +++ b/scripts/passes/dwarf_injection.lua @@ -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 diff --git a/scripts/passes/emission_model.lua b/scripts/passes/emission_model.lua index 352411f..eebd655 100644 --- a/scripts/passes/emission_model.lua +++ b/scripts/passes/emission_model.lua @@ -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 diff --git a/scripts/passes/static_analysis.lua b/scripts/passes/static_analysis.lua index 3784f23..a764122 100644 --- a/scripts/passes/static_analysis.lua +++ b/scripts/passes/static_analysis.lua @@ -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).