mirror of
https://github.com/Ed94/pikuma_ps1.git
synced 2026-08-04 22:58:47 +00:00
Curation pass: reduce nested conditional branching in some defnitions.
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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).
|
||||
|
||||
Reference in New Issue
Block a user