mirror of
https://github.com/Ed94/pikuma_ps1.git
synced 2026-07-31 11:50:11 +00:00
Review pass.
This commit is contained in:
@@ -103,7 +103,7 @@ FI_ void farena_init(FArena_R arena, Slice mem) { assert(arena != nullptr);
|
||||
arena->used = 0;
|
||||
}
|
||||
FI_ FArena farena_make(Slice mem) { FArena a; farena_init(& a, mem); return a; }
|
||||
I_ Slice farena_push(FArena_R arena, U4 amount, Opt_farena o) {
|
||||
I_ Slice farena_push(FArena_R arena, U4 amount, Opt_farena o) {
|
||||
if (amount == 0) { return (Slice){}; }
|
||||
U4 desired = amount * (o.type_width == 0 ? 1 : o.type_width);
|
||||
U4 to_commit = align_pow2(desired, o.alignment ? o.alignment : MEM_ALIGNMENT_DEFAULT);
|
||||
|
||||
@@ -384,12 +384,6 @@ void update(PrimitiveArena* pa, U4* ordering_buf)
|
||||
TapeBuilder tb = tb_make_old(& tape_arena); tb_scope(& tb) {
|
||||
// Skip set_gte_world atom for diagnostics to isolate the triangle loop
|
||||
for (U4 i = 0; i < Floor_num_faces; i++) {
|
||||
// =======================================================
|
||||
// SWAP EMIT TO TEST DIFFERENT PARTS OF THE PIPELINE:
|
||||
// =======================================================
|
||||
// 1. code_diag_yield -> Tests Tape Engine jump logic
|
||||
// 2. code_diag_color -> Tests OT and Prim Arena memory
|
||||
// 3. code_diag_gte -> Tests Vertex arrays and GTE Math
|
||||
// tb_emit(& tb, code_diag_yield);
|
||||
// tb_emit(& tb, code_diag_color);
|
||||
// tb_emit(& tb, code_diag_gte);
|
||||
|
||||
+5
-170
@@ -357,22 +357,6 @@ function M.write_file_lf(path, content)
|
||||
f:write(content); f:close()
|
||||
end
|
||||
|
||||
--- Return `{path, ...}` for files in `out_root` whose basename matches `pattern` (Lua pattern, NOT regex — `%.` not `\.`).
|
||||
--- Empty list if `out_root` doesn't exist or matches nothing.
|
||||
--- @param out_root Path
|
||||
--- @param pattern string -- Lua pattern matched against basename only
|
||||
--- @return string[]
|
||||
function M.list_dir(out_root, pattern)
|
||||
local files = {}
|
||||
if lfs.attributes(out_root, "mode") ~= "directory" then return files end
|
||||
for entry in lfs.dir(out_root) do
|
||||
if entry:match(pattern) then
|
||||
files[#files + 1] = out_root .. "\\" .. entry
|
||||
end
|
||||
end
|
||||
return files
|
||||
end
|
||||
|
||||
local _absolute_path_cache = {}
|
||||
|
||||
--- Convert a (possibly relative) path to an absolute path, using CWD if needed.
|
||||
@@ -409,10 +393,6 @@ function M.ensure_dir(path)
|
||||
if lfs.attributes(path, "mode") ~= "directory" then lfs.mkdir(path) end
|
||||
end
|
||||
|
||||
-- Test helper: clear the cache (used by tests + between process runs).
|
||||
-- Not normally needed since Lua state is per-process.
|
||||
function M._reset_ensured_dirs() _ensured_dirs = {} end
|
||||
|
||||
--- Group a list of `SourceFile`-shaped records by their `dir` field.
|
||||
--- Used by the annotation / static-analysis / report passes to partition sources into per-DIRECTORY (per-module) buckets before emitting per-module reports.
|
||||
--- Insertion order preserved within each bucket (matches source order in `ctx.sources`).
|
||||
@@ -813,10 +793,8 @@ end
|
||||
|
||||
-- Split a brace-body into top-level comma-separated tokens. Honors nested parens/braces/brackets and skips strings/comments.
|
||||
--
|
||||
-- FIX (2026-07-09): split at top-level NEWLINES and SEMICOLONS too, AND emit a token break after a top-level comment/string.
|
||||
-- Previous behavior glued the macro call after a comment into the same token, so `word_count_of_token` only saw the
|
||||
-- leading ident (often nil after stripping the comment), undercounting the body.
|
||||
-- Pure-comment / pure-string chunks (which now appear between real statements) are filtered out so they contribute 0 words instead of 1.
|
||||
-- Splits at top-level NEWLINES and SEMICOLONS too, AND emits a token break after a top-level comment/string.
|
||||
-- Pure-comment / pure-string chunks contribute 0 words.
|
||||
function M.split_top_level_commas(body)
|
||||
local tokens = {}
|
||||
local pos = 1
|
||||
@@ -829,7 +807,7 @@ function M.split_top_level_commas(body)
|
||||
local scan = 1
|
||||
local len = #chunk
|
||||
while scan <= len do
|
||||
if M.is_space(chunk:sub(scan, scan)) then
|
||||
if M.is_space_byte(chunk:byte(scan)) then
|
||||
scan = scan + 1
|
||||
else
|
||||
local nx = M.skip_str_or_cmt(chunk, scan)
|
||||
@@ -980,51 +958,6 @@ function M.build_body_line_index(body)
|
||||
return index
|
||||
end
|
||||
|
||||
--- Find the end of a marker call (`atom_label(...)` or `atom_offset(...)`).
|
||||
--- Returns the position past the closing `)`, or nil if the token isn't a marker call.
|
||||
--- @param tok string
|
||||
--- @return integer|nil
|
||||
function M.find_marker_call_end(tok)
|
||||
local ident, after = M.read_ident(tok, 1)
|
||||
if not ident then return nil end
|
||||
if ident ~= "atom_label" and ident ~= "atom_offset" then return nil end
|
||||
local paren_pos = M.skip_ws_and_cmt(tok, after)
|
||||
if tok:sub(paren_pos, paren_pos) ~= "(" then return nil end
|
||||
local _, close = M.read_parens(tok, paren_pos)
|
||||
return close
|
||||
end
|
||||
|
||||
--- True iff `tok` is an atom-label or atom-offset marker call.
|
||||
--- Sibling helper to M.find_marker_call_end; uses the same string constants.
|
||||
--- @param tok string
|
||||
--- @return boolean
|
||||
function M.is_marker_token(tok)
|
||||
local leading = M.read_ident(tok, 1)
|
||||
return leading == "atom_label" or leading == "atom_offset"
|
||||
end
|
||||
|
||||
--- Count words contributed by the non-marker portion of `tok` (after the marker's closing `)`).
|
||||
--- Returns 0 if `tok` isn't a marker call or has no trailing content.
|
||||
---
|
||||
--- `count_token_words_fn` is injected by the caller rather than imported here because the dependency arrow already points the other way:
|
||||
--- `passes/offsets.lua` and `passes/atoms_source_map.lua` both `require("word_count_eval")` and pass its `count_token_words` as the 3rd argument to this function,
|
||||
--- while `word_count_eval` itself loads `duffle` via `duffle_paths.lua` (see `passes/word_count_eval.lua` near the top of the file)
|
||||
--- and calls `duffle.trim` / `duffle.read_ident` / `duffle.skip_ws_and_cmt` from `M.count_token_words`. Importing `word_count_eval`
|
||||
--- from this module would reverse that direction and form a recursive require cycle.
|
||||
--- The callback keeps the marker-syntax helpers (`find_marker_call_end`, `is_marker_token`, this function)
|
||||
--- shared in `duffle` without making the foundational utility depend on a pass module.
|
||||
--- @param tok string
|
||||
--- @param word_counts table
|
||||
--- @param count_token_words_fn fun(tok: string, wc: table): integer
|
||||
--- @return integer
|
||||
function M.count_marker_rest(tok, word_counts, count_token_words_fn)
|
||||
local marker_end = M.find_marker_call_end(tok)
|
||||
if not marker_end or marker_end >= #tok then return 0 end
|
||||
local rest = M.trim(tok:sub(marker_end))
|
||||
if rest == "" then return 0 end
|
||||
return count_token_words_fn(rest, word_counts)
|
||||
end
|
||||
|
||||
-- ════════════════════════════════════════════════════════════════════════════
|
||||
-- Section 5: load_word_counts
|
||||
-- ════════════════════════════════════════════════════════════════════════════
|
||||
@@ -2046,7 +1979,7 @@ M.GPR_VALUE_RULES = {
|
||||
--
|
||||
-- Consumers:
|
||||
-- * passes/static_analysis.lua::check_control_transfer_delay_slot_use
|
||||
-- No other pass consumes this table as of 2026-07-23.
|
||||
|
||||
M.CONTROL_TRANSFER_DELAY_SLOT_POLICIES = {
|
||||
branch_equal = { family = "branch" },
|
||||
branch_ne = { family = "branch" },
|
||||
@@ -2081,21 +2014,6 @@ M.CONTROL_TRANSFER_DELAY_SLOT_POLICIES = {
|
||||
--- @field declaration integer -- 1-based line number of the MipsAtomComp_(ac_X) declaration
|
||||
--- @field kind string -- "comp_bare" | "comp_proc"
|
||||
|
||||
--- @class WordEvent
|
||||
--- @field word integer -- 0-based word index across the entire expansion (root atom + recursed bodies)
|
||||
--- @field ident string -- leading identifier of the emitting token (for nop2 → "nop")
|
||||
--- @field args string[] -- top-level comma-split args of the emitting token (trimmed)
|
||||
--- @field source string -- where the token is defined (component source for recursed; atom source for root)
|
||||
--- @field line integer -- source line of the token (within `source`)
|
||||
--- @field call_source string -- always the ROOT atom's source path (preserved across recursion)
|
||||
--- @field call_line integer -- root atom's call-site line (preserved across recursion)
|
||||
|
||||
--- @class WordEventError
|
||||
--- @field kind string -- "cycle" (currently the only error kind)
|
||||
--- @field msg string -- deterministic human-readable description
|
||||
--- @field source string -- path of the source containing the offending token
|
||||
--- @field line integer -- 1-based line of the offending token within `source`
|
||||
|
||||
-- The cross-source component-body index is owned by the canonical corpus
|
||||
-- (`corpus.component_body_index`, populated by `passes/components.lua`).
|
||||
-- Consumers (`passes/static_analysis.lua`, `passes/emission_model.lua`) read it directly;
|
||||
@@ -2196,7 +2114,7 @@ local E_MAC_PREFIX_LEN = 4
|
||||
-- The items stream is the single ordered source of truth; `word_events` and `markers` are dense views over it (never a separate walk).
|
||||
--
|
||||
-- The helper below operates on a body string (not a body_entry) so the canonical pass can call it without depending on the older SourceScan / body_off conventions.
|
||||
-- component_index argument is accepted for parity with `expand_word_events` and is reserved for recursive component expansion.
|
||||
-- component_index argument is reserved for recursive component expansion.
|
||||
-- word_counts table is the canonical authored-metadata + current-component count table.
|
||||
|
||||
--- @class EmissionProjection
|
||||
@@ -2608,87 +2526,4 @@ function M.project_emission(body_text, component_index, word_counts)
|
||||
})
|
||||
end
|
||||
|
||||
function M.expand_word_events(body_entry, component_index, word_counts)
|
||||
local events = {}
|
||||
local errors = {}
|
||||
|
||||
-- `word_idx` is 0-based across the entire expansion (root atom body + every recursed component body).
|
||||
-- Each emitted machine word consumes one slot.
|
||||
local word_idx = 0
|
||||
|
||||
local root_call_source = body_entry.source
|
||||
local root_call_line = body_entry.declaration or 0
|
||||
|
||||
local function expand(tokens, body_off, line_of, def_source, call_source, call_line, visiting)
|
||||
for _, bt in ipairs(tokens) do
|
||||
local tok = M.trim(bt.tok or "")
|
||||
if tok ~= "" then
|
||||
local ident, args = token_ident_and_args(tok)
|
||||
local tok_line = (line_of and line_of(body_off + bt.rel)) or 0
|
||||
|
||||
if ident == "atom_label" or ident == "atom_offset" then
|
||||
-- Marker: zero events.
|
||||
else
|
||||
-- Strip the `mac_` prefix to look up the component by its BARE name.
|
||||
local bare = nil
|
||||
if ident:sub(1, E_MAC_PREFIX_LEN) == E_MAC_PREFIX then
|
||||
bare = ident:sub(E_MAC_PREFIX_LEN + 1)
|
||||
end
|
||||
|
||||
if bare and component_index and component_index[bare] then
|
||||
if visiting[bare] then
|
||||
-- Cycle: this component is already on the expansion stack.
|
||||
errors[#errors + 1] = {
|
||||
kind = "cycle",
|
||||
msg = string.format("component cycle detected involving %q", bare),
|
||||
source = def_source,
|
||||
line = tok_line,
|
||||
}
|
||||
else
|
||||
visiting[bare] = true
|
||||
local inner = component_index[bare]
|
||||
-- `call_source` / `call_line` (the ROOT atom site) are PRESERVED — we do NOT update them when recursing.
|
||||
-- Nested events keep pointing at the original root atom.
|
||||
expand(inner.body_tokens, inner.body_off, inner.line_of,
|
||||
inner.source, call_source, call_line, visiting)
|
||||
visiting[bare] = nil
|
||||
end
|
||||
else
|
||||
-- Direct token (or unknown `mac_X` falling back). Emit `n` events.
|
||||
local n = 1
|
||||
if word_counts and word_counts[ident] then n = word_counts[ident] end
|
||||
local out_ident = (ident == "nop2") and "nop" or ident
|
||||
for _ = 1, n do
|
||||
word_idx = word_idx + 1
|
||||
events[#events + 1] = {
|
||||
word = word_idx - 1,
|
||||
ident = out_ident,
|
||||
args = args,
|
||||
source = def_source,
|
||||
line = tok_line,
|
||||
call_source = call_source,
|
||||
call_line = call_line,
|
||||
}
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
-- Initial call: the root atom body. `def_source` and `call_source` both start at the atom's source;
|
||||
|
||||
-- `call_line` starts at the atom's declaration line (every event from the root body inherits this).
|
||||
expand(
|
||||
body_entry.body_tokens,
|
||||
body_entry.body_off or 0,
|
||||
body_entry.line_of,
|
||||
body_entry.source,
|
||||
root_call_source,
|
||||
root_call_line,
|
||||
{})
|
||||
|
||||
return events, errors
|
||||
end
|
||||
|
||||
return M
|
||||
|
||||
@@ -11,11 +11,10 @@
|
||||
--- ```
|
||||
---
|
||||
--- That small bootstrap: (a) locates this helper via `arg[0]` / `debug.getinfo`,
|
||||
--- (b) loads it (which sets `package.path` + `package.cpath` via cached `git rev-parse`),
|
||||
--- (b) loads it (which sets `package.path` + `package.cpath`),
|
||||
--- (c) at the bottom calls `require("duffle")` (now resolvable since `package.path` was just set) and returns the duffle M.
|
||||
--- Net effect: the caller gets the duffle module in one statement; no separate `dofile(...)` + `require("duffle")` dance.
|
||||
---
|
||||
--- Replaces the prior 2-line (entry) or 4-line (pass) pattern that had the call site do its own path resolution + duplicated setup.
|
||||
|
||||
local M = {}
|
||||
|
||||
@@ -27,9 +26,6 @@ local CACHE_KEY = "__duffle_repo_root__"
|
||||
--- parent of the directory containing this script. We derive it directly from `debug.getinfo(1, "S").source`
|
||||
--- (returns `@<path>` for the currently-running chunk).
|
||||
---
|
||||
--- Replaces the prior `io.popen("git rev-parse --show-toplevel")` approach, which cost ~100-180ms per
|
||||
--- LuaJIT process on Windows due to git's CLI startup. The path-derive approach costs <1ms.
|
||||
---
|
||||
--- If `debug.getinfo` can't parse this script's path (shouldn't happen — dofile always populates source),
|
||||
--- return nil and let `M.setup()` fail loud.
|
||||
--- @return string|nil
|
||||
@@ -61,7 +57,12 @@ end
|
||||
function M.setup()
|
||||
local repo_root = find_repo_root()
|
||||
if not repo_root then
|
||||
io.stderr:write("[duffle_paths] git rev-parse failed -- not in a git repo?\n")
|
||||
-- Unreachable in practice: find_repo_root() derives the repo root from this script's
|
||||
-- own source path via debug.getinfo(1, "S").source (no subprocess, no git CLI, <1ms).
|
||||
-- A nil return means the source path did not match the expected
|
||||
-- <repo>/scripts/duffle_paths.lua layout — a packaging bug, not a "missing git repo"
|
||||
-- condition. os.exit(2) is retained so a real failure surfaces loud rather than
|
||||
-- silently producing an unconfigured module table.
|
||||
os.exit(2)
|
||||
end
|
||||
|
||||
|
||||
+2
-130
@@ -616,7 +616,7 @@ end
|
||||
--- - ELF32 symtab entry = 16 bytes (`st_name:4 + st_value:4 + st_size:4 + st_info:1 + st_other:1 + st_shndx:2`); offsets within each entry are zero-based wire offsets.
|
||||
--- - Direct Lua `string.byte`/`string.sub`/`string.find` boundaries receive `+ 1`.
|
||||
--- - We filter on STB_GLOBAL (high nibble of st_info = 1) to match `nm`'s default (external symbols only). STB_WEAK excluded.
|
||||
--- - We strip the `code_` prefix to match the previous `read_nm` output.
|
||||
--- - The `code_` prefix is stripped (MipsAtom_ macros emit bare atom names, no `code_` prefix).
|
||||
--- - `st_size > 0` filter excludes undefined/imported symbols.
|
||||
--- @param elf_path Path
|
||||
--- @return table<string, {integer, integer}>
|
||||
@@ -654,7 +654,7 @@ function M.read_nm(elf_path)
|
||||
-- Extract the name from .strtab (null-terminated C string).
|
||||
local name_end = strtab:find("\0", st_name_off + 1, true) or (st_name_off + 1)
|
||||
local name = strtab:sub(st_name_off + 1, name_end - 1)
|
||||
-- Filter: keep all symbol-table symbols (atoms emit their name as the bare `<name>` since the `code_` prefix was removed from the MipsAtom_ macro).
|
||||
-- Filter: keep all symbol-table symbols (atoms emit their name as the bare `<name>` — MipsAtom_ macros strip the `code_` prefix).
|
||||
-- The atoms_source_map pass already filters out non-atom symbols via the source-map.txt cross-ref.
|
||||
if name and #name > 0 then
|
||||
local st_value = M.read_u32_le(symtab, entry_off + SYM_ST_VALUE)
|
||||
@@ -791,132 +791,4 @@ end
|
||||
-- I/O helpers: atoms source-map + native directory glob
|
||||
-- ════════════════════════════════════════════════════════════════════════════
|
||||
|
||||
--- Parse a FORMAT_VERSION <expected_version> atoms-meta file (sourcemap or provenance).
|
||||
--- Shared by M.parse_source_map_file + M.parse_provenance_file.
|
||||
--- The two callers differ only in how they parse WORD lines; that's `extract_word(line)`.
|
||||
--- Returns the standard `{name -> {total, words}}` shape.
|
||||
--- Returns `{}` on format-version mismatch (and logs to stderr).
|
||||
--- @param path string
|
||||
--- @param expected_version integer
|
||||
--- @param extract_word fun(line: string): table|nil -- caller-supplied per-line parser
|
||||
--- @return table<string, table>
|
||||
function M.parse_atom_records(path, expected_version, extract_word)
|
||||
local out = {}
|
||||
local cur_name, cur_words = nil, {}
|
||||
for raw in io.lines(path) do
|
||||
local line = raw
|
||||
if line:match("^#") then
|
||||
local ver = line:match("^# FORMAT_VERSION%s+(%d+)")
|
||||
if ver and tonumber(ver) ~= expected_version then
|
||||
io.stderr:write(string.format(
|
||||
"[elf_dwarf.parse_atom_records] version mismatch (got %s, expected %d) in %s\n",
|
||||
ver, expected_version, path))
|
||||
return {}
|
||||
end
|
||||
-- skip other comments
|
||||
elseif line:sub(1, 4) == "ATOM" then
|
||||
-- ATOM <name> "<abs-source-path>" <total>
|
||||
local _, _, name = line:find("ATOM%s+(%S+)%s+\"[^\"]*\"%s+(%d+)")
|
||||
if name then
|
||||
cur_name = name
|
||||
cur_words = {}
|
||||
out[name] = { total = 0, words = cur_words }
|
||||
end
|
||||
elseif line == "ENDATOM" then
|
||||
-- Update the recorded total from the entries count
|
||||
-- (matches the `lines[1] = lines[1]:gsub(" 0$", " " .. total)` patch in atoms_source_map.lua:170).
|
||||
if cur_name and out[cur_name] then
|
||||
out[cur_name].total = #cur_words
|
||||
end
|
||||
cur_name, cur_words = nil, {}
|
||||
elseif line:sub(1, 4) == "WORD" and cur_name then
|
||||
local field = extract_word(line)
|
||||
if field then
|
||||
cur_words[#cur_words + 1] = field
|
||||
end
|
||||
end
|
||||
end
|
||||
return out
|
||||
end
|
||||
|
||||
--- Parse a FORMAT_VERSION <expected_version> `*.atoms.sourcemap.txt` file.
|
||||
--- Returns `{name -> {total = N, words = {{pos, line}, ...}}}`.
|
||||
--- Returns `{}` on format-version mismatch (and logs to stderr).
|
||||
---
|
||||
--- **Wire format** (emitted by `passes/atoms_source_map.lua`):
|
||||
--- ```
|
||||
--- # FORMAT_VERSION <n>
|
||||
--- ATOM <name> "<abs-source-path>" <total>
|
||||
--- WORD <n> LINE <line> TEXT <text...>
|
||||
--- ...
|
||||
--- ENDATOM
|
||||
--- ```
|
||||
---
|
||||
--- **Conventions:** the in-memory shape uses `{pos, line, text}`
|
||||
--- (`atoms_source_map.lua:142`); the `.txt` file uses `WORD <n>` so the parser maps `n` → `pos` field name.
|
||||
--- @param sm_path Path
|
||||
--- @param expected_version integer -- expected FORMAT_VERSION line
|
||||
--- @return table<string, table>
|
||||
function M.parse_source_map_file(sm_path, expected_version)
|
||||
return M.parse_atom_records(sm_path, expected_version, function(line)
|
||||
local _, n, _, src_line = line:find("WORD%s+(%d+)%s+LINE%s+(%d+)")
|
||||
if n and src_line then
|
||||
return { pos = tonumber(n), line = tonumber(src_line) }
|
||||
end
|
||||
end)
|
||||
end
|
||||
|
||||
--- Parse a FORMAT_VERSION <expected_version> `*.atoms.provenance.txt` file.
|
||||
--- Returns `{name -> {total = N, words = {{pos, call_file, call_line, comp_name, comp_file, comp_line}, ...}}}`.
|
||||
--- Returns `{}` on format-version mismatch (and logs to stderr).
|
||||
---
|
||||
--- **Wire format** (emitted by `passes/atoms_source_map.lua`):
|
||||
--- ```
|
||||
--- # FORMAT_VERSION <n>
|
||||
--- ATOM <name> "<abs-source-path>" <total>
|
||||
--- WORD <n> CALL <src-file>:<src-line> RAW
|
||||
--- WORD <n> CALL <src-file>:<src-line> MACRO <comp_name> "<comp-file>:<comp-line>"
|
||||
--- ...
|
||||
--- ENDATOM
|
||||
--- ```
|
||||
---
|
||||
--- **Used by** `passes/dwarf_injection.lua` to:
|
||||
--- - group consecutive MACRO rows into component invocations (one `DW_TAG_inlined_subroutine` each)
|
||||
--- - emit abstract `DW_TAG_subprogram` per unique component name
|
||||
--- - extend `.debug_line` so stepping into a `mac_X(...)` lands on the component's source line.
|
||||
--- @param prov_path string -- path to *.atoms.provenance.txt
|
||||
--- @param expected_version integer -- expected FORMAT_VERSION line
|
||||
--- @return table<string, table>
|
||||
function M.parse_provenance_file(prov_path, expected_version)
|
||||
return M.parse_atom_records(prov_path, expected_version, function(line)
|
||||
-- Two accepted shapes:
|
||||
-- WORD <n> CALL <call-file>:<call-line> RAW
|
||||
-- WORD <n> CALL <call-file>:<call-line> MACRO <comp_name> "<comp-file>:<comp-line>"
|
||||
local pos, call_file, call_line, comp_name, comp_file, comp_line =
|
||||
line:match('WORD%s+(%d+)%s+CALL%s+(.-):(%d+)%s+MACRO%s+(%S+)%s+"([^"]*):(%d+)"')
|
||||
if pos then
|
||||
return {
|
||||
pos = tonumber(pos),
|
||||
call_file = call_file,
|
||||
call_line = tonumber(call_line),
|
||||
comp_name = comp_name,
|
||||
comp_file = comp_file,
|
||||
comp_line = tonumber(comp_line),
|
||||
}
|
||||
end
|
||||
-- RAW row.
|
||||
local raw_pos, raw_file, raw_line = line:match('WORD%s+(%d+)%s+CALL%s+(.-):(%d+)%s+RAW')
|
||||
if raw_pos then
|
||||
return {
|
||||
pos = tonumber(raw_pos),
|
||||
call_file = raw_file,
|
||||
call_line = tonumber(raw_line),
|
||||
comp_name = nil,
|
||||
comp_file = nil,
|
||||
comp_line = nil,
|
||||
}
|
||||
end
|
||||
end)
|
||||
end
|
||||
|
||||
return M
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
---
|
||||
--- Writes:
|
||||
--- - `<ctx.out_root>/<dir_basename>.errors.h` — one per module, with `#error` directives on findings (the C compile will surface the error)
|
||||
--- - The annotations.txt report is rendered by `passes/report.lua` from the per-module results stashed in `ctx.flags._annot_results`
|
||||
--- - The annotations.txt report is rendered by `passes/report.lua` from the canonical `corpus.sources_by_dir` projection (re-validating each source via `M.validate()`).
|
||||
---
|
||||
--- **Conventions**: tabs (1/level), EmmyLua annotations, no regex, Lua 5.3 compatible
|
||||
|
||||
@@ -45,7 +45,6 @@ local ensure_dir = duffle.ensure_dir
|
||||
--- @field project_root string
|
||||
--- @field upstream table<string, table>
|
||||
--- @field flags table
|
||||
--- @field flags._annot_results table[] -- stashed by annotation pass; consumed by report.lua
|
||||
--- @field dry_run boolean
|
||||
--- @field verbose boolean
|
||||
|
||||
@@ -500,9 +499,10 @@ end
|
||||
--- Validate one source against its pre-scanned SourceScan payload + the corpus-wide pipe_ctx.
|
||||
--- @param ctx PassCtx
|
||||
--- @param src SourceFile
|
||||
--- @param corpus_pipe_ctx PipeCtx -- built once per pass from corpus registries
|
||||
--- @param corpus_pipe_ctx PipeCtx|nil -- built once per pass from corpus registries; nil = self-build (canonical projection).
|
||||
--- @return AnnotatedResult
|
||||
local function validate(ctx, src, corpus_pipe_ctx)
|
||||
corpus_pipe_ctx = corpus_pipe_ctx or build_corpus_pipe_ctx(ctx)
|
||||
local scan = src.scan
|
||||
|
||||
-- Project the pre-scanned atoms to the AtomEntry shape this pass needs.
|
||||
@@ -668,17 +668,6 @@ local function emit_module_errors_h(ctx, dir_basename, atoms_count, errors, sour
|
||||
return out_path
|
||||
end
|
||||
|
||||
--- Stash aggregated per-module results for the report pass to consume.
|
||||
local function emit_module_annotations_stub(ctx, dir, dir_basename, atoms_count)
|
||||
ctx.flags = ctx.flags or {}
|
||||
ctx.flags._annot_results = ctx.flags._annot_results or {}
|
||||
ctx.flags._annot_results[#ctx.flags._annot_results + 1] = {
|
||||
dir = dir,
|
||||
dir_basename = dir_basename,
|
||||
atoms_count = atoms_count,
|
||||
}
|
||||
end
|
||||
|
||||
-- ════════════════════════════════════════════════════════════════════════════
|
||||
-- M.run — orchestrator entry
|
||||
-- ════════════════════════════════════════════════════════════════════════════
|
||||
@@ -713,13 +702,9 @@ function M.run(ctx)
|
||||
local dir_atoms = 0
|
||||
local dir_errors = {}
|
||||
local dir_warnings = {}
|
||||
-- Per-source validate() results, cached for the report pass (it reads from this instead of re-validating each source).
|
||||
ctx.flags = ctx.flags or {}
|
||||
ctx.flags._annot_source_results = ctx.flags._annot_source_results or {}
|
||||
for _, src in ipairs(dir_sources) do
|
||||
local result = validate(ctx, src, corpus_pipe_ctx)
|
||||
result.source = src.path -- tag for downstream rendering
|
||||
ctx.flags._annot_source_results[src.path] = result -- stash so report.lua reads from cache instead of re-running validate()
|
||||
dir_atoms = dir_atoms + #result.atoms
|
||||
for _, e in ipairs(result.errors) do
|
||||
dir_errors[#dir_errors + 1] = { line = e.line, msg = e.msg, source = src.path }
|
||||
@@ -735,8 +720,6 @@ function M.run(ctx)
|
||||
if err_path then
|
||||
table.insert(outputs, { errors_h = err_path })
|
||||
end
|
||||
|
||||
emit_module_annotations_stub(ctx, dir, dir_basename, dir_atoms)
|
||||
end
|
||||
|
||||
return { outputs = outputs, errors = errors, warnings = warnings }
|
||||
|
||||
@@ -63,7 +63,7 @@ local FORMAT_VERSION = 1
|
||||
--- @class AtomSourceMapCtx
|
||||
--- @field shared table -- `ctx.shared`
|
||||
--- @field shared.corpus table -- canonical source-order corpus
|
||||
--- @field shared.word_counts table -- identity alias of `corpus.word_counts`
|
||||
--- @field shared.word_counts table
|
||||
--- @field out_root string -- output root (e.g. "build/gen")
|
||||
--- @field dry_run boolean -- if true, compute but don't write
|
||||
--- @field flags table -- `ctx.flags`; reads `flags.gdb_runtime` + `flags.elf_path`
|
||||
@@ -391,31 +391,6 @@ local function append_gdb_commands(lines, matched)
|
||||
lines[#lines + 1] = "end"
|
||||
lines[#lines + 1] = ""
|
||||
|
||||
-- ── show_c2 ──
|
||||
-- GTE data regs (COP2). pcsx-redux's gdb stub doesn't expose COP2 (only 72 regs: 32 GPR + COP0 + FPR).
|
||||
-- curl http://localhost:8080/api/v1/lua/gte
|
||||
-- We keep the command definition as a stub that points the user at the plugin.
|
||||
lines[#lines + 1] = "define show_c2"
|
||||
lines[#lines + 1] = ' echo "[gdb_tape_atoms] show_c2: gdb stub does not expose COP2 in this build."'
|
||||
lines[#lines + 1] = ' echo "[gdb_tape_atoms] Use scripts/pcsx_debug_helper.zip + curl http://localhost:8080/api/v1/lua/gte"'
|
||||
lines[#lines + 1] = ' echo "[gdb_tape_atoms] (or pcsx-redux Debug > Registers window for a native view)"'
|
||||
lines[#lines + 1] = "end"
|
||||
lines[#lines + 1] = "document show_c2"
|
||||
lines[#lines + 1] = " Stub. The gdb stub in this pcsx-redux build does not expose COP2 regs."
|
||||
lines[#lines + 1] = " For GTE data + control state, use the pcsx_debug_helper Lua plugin or the"
|
||||
lines[#lines + 1] = " pcsx-redux Debug > Registers window."
|
||||
lines[#lines + 1] = "end"
|
||||
lines[#lines + 1] = ""
|
||||
|
||||
-- ── show_c2ctl ──
|
||||
lines[#lines + 1] = "define show_c2ctl"
|
||||
lines[#lines + 1] = ' echo "[gdb_tape_atoms] show_c2ctl: see show_c2 for the same workaround."'
|
||||
lines[#lines + 1] = "end"
|
||||
lines[#lines + 1] = "document show_c2ctl"
|
||||
lines[#lines + 1] = " Stub. Same workaround as show_c2."
|
||||
lines[#lines + 1] = "end"
|
||||
lines[#lines + 1] = ""
|
||||
|
||||
-- ── wave_ctx ──
|
||||
lines[#lines + 1] = "define wave_ctx"
|
||||
lines[#lines + 1] = ' printf "$t4 = R_FaceCursor 0x%08x\\n", $t4'
|
||||
@@ -533,7 +508,9 @@ function M.run(ctx)
|
||||
for _, src in ipairs(corpus.source_order) do
|
||||
local has_projection = false
|
||||
for _, atom in ipairs((src.scan or {}).atoms or {}) do
|
||||
if atom.paths then has_projection = true; break end
|
||||
if (atom.kind == "atom" or atom.kind == "raw_atom") and atom.paths then
|
||||
has_projection = true; break
|
||||
end
|
||||
end
|
||||
if not has_projection then
|
||||
for _, atom in ipairs((src.scan or {}).raw_atoms or {}) do
|
||||
|
||||
@@ -11,15 +11,6 @@
|
||||
--- **Conventions**: tabs (1/level), EmmyLua annotations, no regex,
|
||||
--- Lua 5.3 compatible.
|
||||
|
||||
--- @class Component
|
||||
--- @field name string
|
||||
--- @field body string
|
||||
--- @field args string|nil
|
||||
--- @field line integer
|
||||
--- @field comment string|nil
|
||||
|
||||
--- @class M
|
||||
|
||||
-- ════════════════════════════════════════════════════════════════════════════
|
||||
-- Module-scope requires + package.path setup
|
||||
-- ════════════════════════════════════════════════════════════════════════════
|
||||
@@ -31,7 +22,6 @@
|
||||
-- duffle_paths.lua sets package.path then returns `require("duffle")` at the bottom, so the dofile value IS the duffle module.
|
||||
local _bootstrap_dir = debug.getinfo(1, "S").source:match("^@?(.*[/\\])") or "./"
|
||||
local duffle = dofile(_bootstrap_dir .. "../duffle_paths.lua")
|
||||
local word_count_eval = require("word_count_eval")
|
||||
|
||||
-- ════════════════════════════════════════════════════════════════════════════
|
||||
-- Constants
|
||||
@@ -247,7 +237,7 @@ local function extract_arg_names(args_str)
|
||||
local ident_start = ident_end
|
||||
while ident_start > 0 do
|
||||
local ch = trimmed:sub(ident_start, ident_start)
|
||||
if duffle.is_alnum(ch) or ch == "_" then
|
||||
if duffle.is_alnum_byte(string.byte(ch)) or ch == "_" then
|
||||
ident_start = ident_start - 1
|
||||
else
|
||||
break
|
||||
@@ -640,7 +630,7 @@ end
|
||||
|
||||
--- (internal) Populate the canonical `corpus.component_body_index` projection with this source's body index entries.
|
||||
--- First declaration wins; later declarations are dropped (no separate collision record: the components collision is already surfaced by `update_canonical_components`).
|
||||
--- The pass does NOT write to `ctx.shared.component_body_index` (the legacy corpus owns this projection).
|
||||
--- The pass does NOT write to `ctx.shared.component_body_index` (the corpus owns this projection).
|
||||
--- @param corpus table -- the canonical corpus
|
||||
--- @param src SourceFile
|
||||
--- @param components Component[]
|
||||
|
||||
@@ -46,10 +46,8 @@ local lfs = require("lfs")
|
||||
|
||||
-- File-scope aliases to elf_dwarf helpers; the canonical implementations live in scripts/elf_dwarf.lua.
|
||||
-- ELF decoding helpers come from `elf_dwarf.lua`.
|
||||
local read_uleb128_at = elf_dwarf.read_uleb128_at
|
||||
local read_sleb128_at = elf_dwarf.read_sleb128_at
|
||||
local find_abbrev_table_end = elf_dwarf.find_abbrev_table_end
|
||||
-- Note: uleb128 + sleb128 are encoders; read_uleb128_at + read_sleb128_at are decoders. Different functions.
|
||||
-- Local DWARF opcode constants + length-prefixed integers (uleb128 + sleb128 encoders are in elf_dwarf.lua).
|
||||
local uleb128 = elf_dwarf.uleb128
|
||||
local sleb128 = elf_dwarf.sleb128
|
||||
|
||||
@@ -207,14 +205,13 @@ local DW_FORM_udata = 0x0F -- ULEB128 (DW_AT_byte_size for struct_typ
|
||||
local DW_FORM_implicit_const = 0x21 -- DWARF5 §7.5.6: abbrev declaration carries a SLEB constant (used by the abbrev-table walker)
|
||||
local DW_FORM_sec_offset = 0x17 -- 4-byte section-relative offset (into .debug_loclists / .debug_rnglists)
|
||||
|
||||
local DW_OP_reg0 = 0x50
|
||||
-- DW_OP_regN = 0x50 + N; the variable's value resides in that register.
|
||||
local DW_OP_piece = 0x93 -- followed by ULEB128 byte count (per DWARF5 §7.7.5)
|
||||
-- DW_OP_reg0 + DW_OP_piece are declared canonically above (lines 114-116) alongside the other DWARF5 §7.7.3 loclist opcodes.
|
||||
|
||||
|
||||
local DW_ATE_unsigned = 0x07 -- DWARF5 §7.8.1: DW_ATE_unsigned (used for U4 base type)
|
||||
|
||||
local DW_LANG_C99 = 0x0C -- = 12 (C99); use as a safe "C-like" placeholder
|
||||
-- (DW_LANG_Mips_Assembler = 0x8001 was used in the, but we want this CU to look like a C TU so VSCode's Variables pane treats it as code.)
|
||||
-- No DW_AT_language attribute is emitted (see build_debug_info_section's abbrev 100).
|
||||
|
||||
-- R_<name> → MIPS GPR lookups go through the merged register_alias_registry
|
||||
-- (collected by collect_per_source_registries from ctx.sources[*].scan.register_alias_registry).
|
||||
@@ -616,13 +613,14 @@ local function build_atom_sequence(atom)
|
||||
-- If entry 1 is the first word of a component invocation, emit the component-definition row at the same PC immediately after.
|
||||
-- The def line is always a statement target unless the inv is explicitly skip-over (atom_dbg_skip_over);
|
||||
-- the whole-atom skip path takes the early-return at line ~497 above so atom_is_stmt is irrelevant here.
|
||||
-- For the body row, prefer `body_lines[1]` (the actual source line of the first body word in the macro's expansion)
|
||||
-- For the body row, prefer `body_lines[1]` (the actual source line of the first body word in the macro's expansion)
|
||||
-- over `comp_line` (the macro signature line).
|
||||
-- When `body_lines` is absent (the component was not indexed by the scan, e.g. an external macro),
|
||||
-- fall back to `comp_line` so the line program remains valid.
|
||||
-- Canonical contract: the emission-model pass populates `body_lines` for every invocation;
|
||||
-- absence now is an error (older emitters / external macros are no longer supported).
|
||||
if inv1 and 1 == inv1.start_pos + 1 then
|
||||
local comp_file_idx_1 = resolve_provenance_file_index(inv1.comp_file)
|
||||
local body_line_1 = (inv1.body_lines and inv1.body_lines[1]) or inv1.comp_line
|
||||
assert(inv1.body_lines, "missing body_lines: emitter did not run emission-model")
|
||||
local body_line_1 = inv1.body_lines[1]
|
||||
emit_row(comp_file_idx_1, body_line_1, not inv1.skip_over)
|
||||
end
|
||||
|
||||
@@ -648,18 +646,20 @@ local function build_atom_sequence(atom)
|
||||
emit_row(call_file_idx, inv.call_line, atom_is_stmt)
|
||||
-- 2) component body row at this PC.
|
||||
-- Prefer `body_lines[1]` over `comp_line` so gdb's `step` lands on the macro's actual body line (not the signature line above `{ ... }`).
|
||||
-- Fallback to `comp_line` when the component isn't indexed.
|
||||
-- Canonical contract: `body_lines` is always populated by the emission-model pass.
|
||||
local body_file_idx = resolve_provenance_file_index(inv.comp_file)
|
||||
local body_line_1 = (inv.body_lines and inv.body_lines[1]) or inv.comp_line
|
||||
assert(inv.body_lines, "missing body_lines: emitter did not run emission-model")
|
||||
local body_line_1 = inv.body_lines[1]
|
||||
emit_row(body_file_idx, body_line_1, not inv.skip_over)
|
||||
elseif inv then
|
||||
-- Subsequent word of an invocation: 1-based offset into body_lines:
|
||||
-- word 1 of the invocation corresponds to body_lines[1], word 2 -> body_lines[2], etc.
|
||||
-- 1-based offset = idx - inv.start_pos (since idx = inv.start_pos + i for the i-th body word).
|
||||
-- When `body_lines` is missing (older emitters / external macros), fall back to comp_line.
|
||||
-- Canonical contract: `body_lines` is always populated by the emission-model pass.
|
||||
local body_file_idx = resolve_provenance_file_index(inv.comp_file)
|
||||
local words_into = idx - inv.start_pos -- 1-based word position in invocation
|
||||
local body_line_i = (inv.body_lines and inv.body_lines[words_into]) or inv.comp_line
|
||||
assert(inv.body_lines, "missing body_lines: emitter did not run emission-model")
|
||||
local body_line_i = inv.body_lines[words_into]
|
||||
emit_row(body_file_idx, body_line_i, not inv.skip_over)
|
||||
else
|
||||
-- RAW word: single call-site row. emit_row restores is_stmt after a selected component range before exposing this adjacent row.
|
||||
@@ -1283,7 +1283,7 @@ end
|
||||
--- Abbrev 100 (DW_TAG_compile_unit, with children):
|
||||
--- DW_AT_name (DW_FORM_strp) -- CU name
|
||||
--- DW_AT_comp_dir (DW_FORM_strp) -- compilation dir
|
||||
--- DW_AT_language (DW_FORM_data1) -- DW_LANG_C99
|
||||
--- DW_AT_language (DW_FORM_data1) -- one byte language code
|
||||
--- Abbrev 101 (DW_TAG_subprogram, with children):
|
||||
--- DW_AT_name (DW_FORM_string) -- atom function name
|
||||
--- DW_AT_low_pc (DW_FORM_addr) -- atom.addr
|
||||
@@ -2099,8 +2099,6 @@ end
|
||||
|
||||
--- Build the new .debug_info: SPLICE inserted DIEs into the MAIN CU as children.
|
||||
---
|
||||
--- This function appended a DETACHED synthetic CU to the end of .debug_info.
|
||||
--- That put every atom DIE in a separate CU from the one GDB selected for PC lookup, so `RR_PrimCursor` + `bind_args` never appeared in scope.
|
||||
--- This implementation instead:
|
||||
--- 1. Builds the inserted-children bytes (base_type, struct_types, subprograms with their RR_* + bind_args children) via build_inserted_children.
|
||||
--- 2. Patches the main CU's `unit_length` field to account for the inserted bytes.
|
||||
@@ -2109,7 +2107,7 @@ end
|
||||
--- 5. Replaces the main CU's final byte (the root children-terminator, 0) with: inserted_children_bytes + a single 0 byte (root terminator preserved).
|
||||
---
|
||||
--- The crt CU (everything before main_cu_start) is preserved.
|
||||
--- **No detached synthetic CU is appended.**
|
||||
|
||||
--- @param existing string -- existing .debug_info section bytes
|
||||
--- @param main_cu_start integer -- 0-based offset of the main CU's unit_length field
|
||||
--- @param main_cu_end_excl integer -- 0-based offset of the first byte AFTER the main CU
|
||||
@@ -2156,21 +2154,11 @@ end
|
||||
--- Atoms don't have stack frames. The .debug_loc section describes per-instruction location adjustments for call-frame-based variables;
|
||||
--- We use DW_OP_regN which is register-based and doesn't need .debug_loc entries).
|
||||
--- The section itself must not be empty OR gdb may complain; the DW_LLE_end_of_list marker (per DWARF5 §7.7) is a single byte 0x00.
|
||||
-- local function build_debug_loc_section() return string.char(0x00) end
|
||||
--
|
||||
-- The actual .debug_loc emission is `string.char(DW_LLE_end_of_list)` inlined
|
||||
-- at the per-section writers below (the dispatch is in M.run, not a table).
|
||||
|
||||
local SECTION_BUILDERS = {
|
||||
debug_line = build_dwarf_line_section,
|
||||
debug_aranges = build_dwarf_aranges_section,
|
||||
debug_rnglists = build_dwarf_rnglists_section,
|
||||
debug_abbrev = build_debug_abbrev_section,
|
||||
debug_info = build_debug_info_section,
|
||||
debug_str = build_debug_str_section,
|
||||
debug_loc = function() return string.char(DW_LLE_end_of_list) end,
|
||||
-- debug_loclists is fed directly in M.run (it needs the merged registries for the R_TapePtr GPR lookup;
|
||||
-- the SECTION_BUILDERS table cannot carry that context, so the dispatch is inlined in the per-run writers loop).
|
||||
}
|
||||
|
||||
-- Per-section output path resolver (mirrors SECTION_BUILDERS).
|
||||
-- Per-section output path resolver.
|
||||
-- Returns the on-disk path for the section's `.bin` blob.
|
||||
local SECTION_WRITERS = {
|
||||
debug_line = function(out_root, basename) return out_root .. "\\" .. basename .. ".dwarf_line.bin" end,
|
||||
@@ -2244,10 +2232,10 @@ function M.run(ctx)
|
||||
|
||||
-- Read the existing DWARF sections directly (no subprocess; lfs + io.open + manual ELF32 section-header walk).
|
||||
-- We need all 8 sections: .debug_line / .debug_aranges / .debug_rnglists get extended
|
||||
-- (additional rows appended to the existing unit), and .debug_info / .debug_abbrev / .debug_str / .debug_loc / .debug_loclists
|
||||
-- (additional rows appended to the existing unit), and .debug_info / .debug_abbrev / .debug_str / .debug_loc / .debug_loclists
|
||||
-- get spliced (the main CU's unit_length is patched;
|
||||
-- no new compile unit is appended; debug_loc/debug_loclists may not exist in the source ELF so we add-section them on splice).
|
||||
-- The dispatch (SECTION_BUILDERS) handles each.
|
||||
-- The per-section dispatch is inlined in the writers loop below.
|
||||
local existing_sections = elf_dwarf.read_elf_sections(elf_path, {
|
||||
".debug_line", ".debug_aranges", ".debug_rnglists",
|
||||
".debug_info", ".debug_abbrev", ".debug_str",
|
||||
@@ -2303,6 +2291,7 @@ function M.run(ctx)
|
||||
--
|
||||
-- .debug_str now also receives the new RR_<R_Name> entries
|
||||
-- (the merged registry drives the strings table to keep .debug_str and .debug_info in sync).
|
||||
-- (the per-section dispatch is inlined in the writers loop below)
|
||||
local basename = ctx.basename or duffle.basename_no_ext(elf_path) or DEFAULT_BASENAME
|
||||
if ctx.out_root and ctx.out_root ~= "" then
|
||||
duffle.ensure_dir(ctx.out_root)
|
||||
|
||||
@@ -135,14 +135,21 @@ function M.run(ctx)
|
||||
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"`) receive the canonical `atom.paths` projection; component declarations
|
||||
-- (`comp_bare` / `comp_proc`) are recursively expanded by atom projections and do not get an independent projection themselves.
|
||||
-- Test-only fixtures that need per-component word events may still consume `duffle.expand_word_events`
|
||||
-- (which remains available; emission-model owns the canonical per-atom projection).
|
||||
-- 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.
|
||||
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") then
|
||||
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.
|
||||
|
||||
@@ -48,7 +48,7 @@ local OFFSET_MACRO_COL = 44
|
||||
--- @class PassCtx
|
||||
--- @field shared table -- cross-pass shared state
|
||||
--- @field shared.corpus table -- canonical corpus projection
|
||||
--- @field shared.word_counts table -- compatibility alias to corpus.word_counts
|
||||
--- @field shared.word_counts table
|
||||
--- @field out_root string -- output root (e.g. "build/gen")
|
||||
--- @field dry_run boolean -- if true, compute but don't write
|
||||
|
||||
|
||||
+27
-19
@@ -5,8 +5,10 @@
|
||||
--- - `build/gen/<dir_basename>.annotations.txt` — one per source-directory containing atoms; aggregates across all sources in the directory.
|
||||
--- - `build/gen/annotation_validation.txt` — the project summary.
|
||||
---
|
||||
--- The annotation pass stashes per-MODULE summary entries in `ctx.flags._annot_results` (set by `passes/annotation.lua`).
|
||||
--- This pass re-validates each source via `annotation.validate()` to get the detailed per-source results needed for the report.
|
||||
--- The annotation pass emits `errors.h` files per module and the canonical
|
||||
--- `corpus.sources_by_dir` projection groups sources by directory. This pass
|
||||
--- iterates the canonical dir projection directly and re-validates each source
|
||||
--- via `annotation.validate()` to get the detailed per-source results.
|
||||
---
|
||||
--- **Conventions**: tabs (1/level), EmmyLua annotations, no regex,
|
||||
--- Lua 5.3 compatible.
|
||||
@@ -25,6 +27,13 @@
|
||||
local _bootstrap_dir = debug.getinfo(1, "S").source:match("^@?(.*[/\\])") or "./"
|
||||
local duffle = dofile(_bootstrap_dir .. "../duffle_paths.lua")
|
||||
|
||||
-- Load the annotation pass so we can re-validate each source against the
|
||||
-- canonical corpus projection. The annotation pass exposes `M.validate`,
|
||||
-- which returns the per-source AnnotationResult (atoms / annots / macros /
|
||||
-- binds / errors / warnings) that the report pass renders into the
|
||||
-- per-module `<dir_basename>.annotations.txt` output.
|
||||
local annotation = dofile(_bootstrap_dir .. "annotation.lua")
|
||||
|
||||
-- ════════════════════════════════════════════════════════════════════════════
|
||||
-- Constants
|
||||
-- ════════════════════════════════════════════════════════════════════════════
|
||||
@@ -67,7 +76,6 @@ local PASS_NAME = "report"
|
||||
--- @field project_root string -- project root (e.g. "code/")
|
||||
--- @field upstream table<string, table> -- per-pass upstream outputs
|
||||
--- @field flags table -- CLI flags + per-pass stash
|
||||
--- @field flags._annot_results ModuleEntry[] -- stashed by annotation pass
|
||||
--- @field dry_run boolean -- if true, compute but don't write
|
||||
--- @field verbose boolean -- if true, log diagnostic info
|
||||
|
||||
@@ -377,21 +385,22 @@ end
|
||||
-- Orchestration helpers
|
||||
-- ════════════════════════════════════════════════════════════════════════════
|
||||
|
||||
--- (internal) Pull per-source validate() results from the annotation pass's stash.
|
||||
--- The annotation pass runs first in the dep chain and caches results in `ctx.flags._annot_source_results`;
|
||||
--- we read from there instead of re-validating each source.
|
||||
--- (internal) Re-validate every source in a directory against the canonical
|
||||
--- corpus projection. Calls `annotation.validate()` per source to produce the
|
||||
--- per-source AnnotationResult (atoms / annots / macros / binds / errors /
|
||||
--- warnings) that the report renderer consumes. This is the canonical path —
|
||||
--- no private stash; each report pass run is reproducible from the corpus.
|
||||
--- Returns the list of module results + the flat list of all results (for the project-wide summary).
|
||||
--- @param ctx PassCtx
|
||||
--- @param dir_sources SourceFile[]
|
||||
--- @return AnnotationResult[], AnnotationResult[]
|
||||
local function lookup_module_results(ctx, dir_sources)
|
||||
local src_cache = (ctx.flags and ctx.flags._annot_source_results) or {}
|
||||
local module_results = {}
|
||||
local all_results = {}
|
||||
for _, src in ipairs(dir_sources) do
|
||||
local result = src_cache[src.path]
|
||||
if result then
|
||||
result.source = src.path -- defensive (annotation tags it too; this guards against cache misses from earlier iterations)
|
||||
if src.scan then
|
||||
local result = annotation.validate(ctx, src, nil)
|
||||
result.source = src.path -- tag for downstream rendering
|
||||
module_results[#module_results + 1] = result
|
||||
all_results[#all_results + 1] = result
|
||||
end
|
||||
@@ -435,29 +444,28 @@ function M.run(ctx)
|
||||
local errors = {}
|
||||
local warnings = {}
|
||||
|
||||
local module_entries = (ctx.flags and ctx.flags._annot_results) or {}
|
||||
-- Read module grouping from `corpus.sources_by_dir` (the canonical projection).
|
||||
-- Module grouping comes from `corpus.sources_by_dir`.
|
||||
-- Module grouping comes from `corpus.sources_by_dir` (the canonical projection).
|
||||
-- Iterate it directly; no private cache, no per-pass stash.
|
||||
local corpus = ctx.shared and ctx.shared.corpus
|
||||
local by_dir = (corpus and corpus.sources_by_dir) or {}
|
||||
|
||||
if not ctx.dry_run then duffle.ensure_dir(ctx.out_root) end
|
||||
|
||||
local all_results_for_summary = {}
|
||||
for _, entry in ipairs(module_entries) do
|
||||
debug_log("entry: dir=%s basename=%s atoms_count=%d dir_sources=%d\n", entry.dir, entry.dir_basename, entry.atoms_count, #(by_dir[entry.dir] or {}))
|
||||
for dir, dir_sources in pairs(by_dir) do
|
||||
local dir_basename = dir:match("([^/\\]+)$") or dir
|
||||
debug_log("dir=%s basename=%s sources=%d\n", dir, dir_basename, #dir_sources)
|
||||
|
||||
if entry.atoms_count > 0 or #(by_dir[entry.dir] or {}) > 0 then
|
||||
local dir_sources = by_dir[entry.dir] or {}
|
||||
if #dir_sources > 0 then
|
||||
local module_results, all_results = lookup_module_results(ctx, dir_sources)
|
||||
for _, r in ipairs(all_results) do
|
||||
all_results_for_summary[#all_results_for_summary + 1] = r
|
||||
end
|
||||
|
||||
if module_has_content(module_results) then
|
||||
local out_path = ctx.out_root .. "/" .. entry.dir_basename .. ".annotations.txt"
|
||||
local out_path = ctx.out_root .. "/" .. dir_basename .. ".annotations.txt"
|
||||
if not ctx.dry_run then
|
||||
duffle.write_file(out_path, render_module_report(entry.dir, dir_sources, module_results))
|
||||
duffle.write_file(out_path, render_module_report(dir, dir_sources, module_results))
|
||||
end
|
||||
outputs[#outputs + 1] = { annotations_txt = out_path }
|
||||
else
|
||||
|
||||
@@ -2029,8 +2029,8 @@ function M.run(ctx)
|
||||
local code_macros = {}
|
||||
local code_macro_bodies = {}
|
||||
|
||||
-- Resolve the canonical source list. The corpus owns the authoritative source_order; no legacy alias is consulted and no per-source fallback synthesis is performed.
|
||||
-- A context without `ctx.shared.corpus` is rejected with an explicit canonical-corpus message so callers migrate to the canonical context (no production compatibility layer).
|
||||
-- Resolve the canonical source list. The corpus owns the authoritative source_order.
|
||||
-- A context without `ctx.shared.corpus` is rejected with an explicit canonical-corpus error.
|
||||
ctx.shared = ctx.shared or {}
|
||||
local corpus = ctx.shared.corpus
|
||||
if not corpus or type(corpus.source_order) ~= "table" then
|
||||
|
||||
@@ -15,7 +15,7 @@
|
||||
--- 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).
|
||||
--- `jump_reg(R_AtomJmp)` is suppressed by policy (the fixed `mac_yield()` handshake).
|
||||
--- `nop2` needs no special case: `expand_word_events` emits two `nop` events for it, so the first expansion is the hardware delay slot.
|
||||
--- `nop2` needs no special case: emission-model emits two `nop` events for it, so the first expansion is the hardware delay slot.
|
||||
--- `atom_label` also needs no special case (zero events).
|
||||
--- 3. mac_yield uniformity: Every atom body must contain exactly one `mac_yield()` call (control transfer pattern).
|
||||
--- 4. Binding handoff: Every `atom_bind(Binds_X)` must reference a `typedef Struct_(Binds_X) { ... }` declaration.
|
||||
@@ -30,10 +30,6 @@
|
||||
--- 10. binds_no_substruct_deref: Every `load_word(R_A, R_B, O_(Type, Field))` and `store_word(...)` in every atom body must reference a leaf scalar
|
||||
--- (pointer-to-struct counts as leaf; nested struct members do NOT).
|
||||
---
|
||||
--- `enum_alias_membership` already iterates `ai.reads` and `ai.writes` against
|
||||
--- `corpus.register_alias_registry`, so a duplicate precedence-class check
|
||||
--- only produced duplicate findings. The CHECK_RULES row and the helper
|
||||
--- function are gone; no public/private surface retains that name.)
|
||||
---
|
||||
--- Findings carry an explicit `kind` ("error" / "warning" / "info").
|
||||
--- The renderer maintains three independent severity collections; `info` is never folded into warnings.
|
||||
@@ -714,10 +710,9 @@ local function analyze_hardware_relations(atom)
|
||||
local ev_line = ev.body_line or ev.line or ev.def_line or 0
|
||||
local ev_source = ev.def_path or ev.source or ""
|
||||
local ev_args = ev.args or {}
|
||||
-- `word_events` use `i` as the 0-based word index across the entire expansion);
|
||||
-- The legacy `duffle.expand_word_events` walker emits `word`.
|
||||
-- Either is accepted; unknown defaults to 0 (the producer's own word).
|
||||
local ev_word = ev.i or ev.word or 0
|
||||
-- `word_events` use `i` as the 0-based word index across the entire expansion.
|
||||
-- Default to 0 if the field is absent (the producer's own word).
|
||||
local ev_word = ev.i or 0
|
||||
|
||||
-- Read a Status source, then apply current-event GPR writes, then consume a pending CU2 transition at the first relevant COP2 use.
|
||||
-- Both operations are part of this one event walk.
|
||||
@@ -1032,7 +1027,7 @@ local function check_gte_result_position(atom, _pipe_ctx, findings)
|
||||
-- For each word event whose encoder is `gte_mv_from_data_r`, look up the register being read in `forward_state.post_command_roles`.
|
||||
-- If a role is set, the reader's register must match the role's register (the registered "latest_<role>" target).
|
||||
for _, ev in ipairs(events) do
|
||||
local ev_ident = ev.encoder or ev.ident
|
||||
local ev_ident = ev.encoder
|
||||
if ev_ident == "gte_mv_from_data_r" then
|
||||
local args = ev.args or {}
|
||||
local reg = args[2]
|
||||
@@ -1101,14 +1096,14 @@ local function check_hazard_nop_use(atom, _pipe_ctx, findings)
|
||||
local pending_snapshot = {}
|
||||
local prev_ev = nil
|
||||
for event_idx, ev in ipairs(events) do
|
||||
local ev_ident = ev.encoder or ev.ident or ""
|
||||
local ev_ident = ev.encoder or ""
|
||||
local ev_args = ev.args or {}
|
||||
local ev_word = ev.i or ev.word or 0
|
||||
local ev_word = ev.i or 0
|
||||
|
||||
-- Classify the nop BEFORE its event is applied to the pending state.
|
||||
if ev_ident == "nop" and prev_ev ~= nil then
|
||||
-- Skip BD-slot nops: they are exclusively owned by control_transfer_delay_slot_use.
|
||||
local prev_ident = prev_ev.encoder or prev_ev.ident or ""
|
||||
local prev_ident = prev_ev.encoder or ""
|
||||
local prev_args = prev_ev.args or {}
|
||||
local bd_policies = duffle.CONTROL_TRANSFER_DELAY_SLOT_POLICIES or {}
|
||||
local is_bd_slot = false
|
||||
@@ -1246,8 +1241,10 @@ end
|
||||
-- ─────────────────────────────────────────────────────────────────────────
|
||||
-- Check #1c: control-transfer delay-slot use.
|
||||
--
|
||||
-- Reads `atom.paths.word_events` (the semantic emitted-word stream from `duffle.expand_word_events`).
|
||||
-- For each event whose `ident` is in `duffle.CONTROL_TRANSFER_DELAY_SLOT_POLICIES`, inspect the next emitted event in the SAME `events` array.
|
||||
-- Reads `atom.paths.word_events` (the semantic emitted-word stream from
|
||||
-- `passes/emission_model.lua`). For each event whose `encoder` is in
|
||||
-- `duffle.CONTROL_TRANSFER_DELAY_SLOT_POLICIES`, inspect the next emitted
|
||||
-- event in the SAME `events` array.
|
||||
-- The next event is the hardware delay-slot word (the duffle pipeline already absorbs the BD-slot into the branch's cost in `analyze_atom_paths`.
|
||||
-- This check observes, it does not reschedule.
|
||||
--
|
||||
@@ -1260,7 +1257,7 @@ end
|
||||
--
|
||||
-- `pipe_ctx` is unused; the uniform `(atom, pipe_ctx, findings)` signature is preserved so the check plugs into
|
||||
-- the existing CHECK_RULES dispatch without modifying the per-atom loop or analyze_atom_paths.
|
||||
-- `expand_word_events` already normalizes `nop2` to two `nop` events and `atom_label` to zero events, so no special-case branching is needed for either.
|
||||
-- `passes/emission_model` already normalizes `nop2` to two `nop` events and `atom_label` to zero events, so no special-case branching is needed for either.
|
||||
-- ─────────────────────────────────────────────────────────────────────────
|
||||
|
||||
local function check_control_transfer_delay_slot_use(atom, pipe_ctx, findings)
|
||||
@@ -1921,10 +1918,6 @@ local function check_binds_no_substruct_deref(_src, pipe_ctx, findings)
|
||||
end
|
||||
end
|
||||
|
||||
-- Because enum_alias_membership (Check #8) already iterates ai.reads / ai.writes against corpus.register_alias_registry.
|
||||
-- The duplicate row produced redundant findings for the same off-registry register.
|
||||
-- Neither a check function nor a CHECK_RULES row retains the name.
|
||||
-- If a future regression reintroduces either, the test_canonical_corpus.lua grep sweep will surface it.
|
||||
|
||||
-- ════════════════════════════════════════════════════════════════════════════
|
||||
-- CHECK_RULES — data-driven check dispatch (Muratori: data over control flow)
|
||||
@@ -1995,8 +1988,9 @@ end
|
||||
|
||||
local function validate(ctx, src, corpus_pipe_ctx)
|
||||
local scan = src.scan
|
||||
-- Read the canonical corpus word_counts for the defensive fallback path below
|
||||
-- (test-only: Focused tests that bypass emission-model and feed static_analysis still need word_events produced by the legacy `duffle.expand_word_events` walker).
|
||||
-- Read the canonical corpus word_counts for the per-atom pipeline
|
||||
-- (atom.paths.word_events is the canonical projection).
|
||||
|
||||
local corpus = (ctx.shared and ctx.shared.corpus) or {}
|
||||
|
||||
-- Read atoms + binds + atom_infos from the pre-scanned SourceScan payload.
|
||||
@@ -2050,36 +2044,27 @@ local function validate(ctx, src, corpus_pipe_ctx)
|
||||
---
|
||||
--- Body, token, and emission projections come from here (`paths.tokens = body_tokens`, `paths.line_in_body = build_body_line_index` `paths.word_events`
|
||||
--- and related fields are owned by `passes/emission_model.lua` pass (per-atom emission projection).
|
||||
--- This pass reads: `paths.tokens`, `paths.line_in_body` ` paths.items`, `paths.word_events` from the canonical projection,
|
||||
--- then computes `paths.tok_class`, `paths.cycles_min/max`, `paths.branches`, `paths.paths`, `paths.has_loops`, `paths.unknown_macros`
|
||||
--- This pass reads: `paths.tokens`, `paths.line_in_body` ` paths.items`, `paths.word_events` from the canonical projection,
|
||||
--- then computes `paths.tok_class`, `paths.cycles_min/max`, `paths.branches`, `paths.paths`, `paths.has_loops`, `paths.unknown_macros`
|
||||
--- via `classify_tokens` + `analyze_atom_paths`.
|
||||
--- No re-walk of body text or body_tokens happens here.
|
||||
---
|
||||
--- Isolated component checks may supply a component body directly.
|
||||
--- Such inputs may omit an emission projection and require this pass to populate `paths.word_events` through `duffle.expand_word_events`.
|
||||
--- Normal callers run emission-model first.
|
||||
--- Canonical contract: `atom.paths` and `atom.paths.word_events` MUST be
|
||||
--- populated by `passes/emission_model.run(ctx)` before this pass runs.
|
||||
--- The `atom.paths.word_events` projection is owned by the emission-model pass; static-analysis reads it directly.
|
||||
local findings = {}
|
||||
for _, a in ipairs(atoms) do
|
||||
a.paths = a.paths or {}
|
||||
if a.paths == nil then
|
||||
error("static_analysis: a.paths is nil; emit emission-model first")
|
||||
end
|
||||
if a.paths.word_events == nil then
|
||||
error("static_analysis: a.paths.word_events is nil; emit emission-model first")
|
||||
end
|
||||
-- `paths.tokens` / `paths.line_in_body` / `paths.items` / `paths.word_events` are populated by `passes/emission_model.lua`.
|
||||
-- Supply tokens when no emission projection is present.
|
||||
if a.paths.tokens == nil then a.paths.tokens = a.body_tokens end
|
||||
a.paths.tok_class = classify_tokens(a.paths.tokens)
|
||||
|
||||
-- Supply word events when no emission projection is present.
|
||||
if a.paths.word_events == nil then
|
||||
local body_entry = {
|
||||
body_tokens = a.body_tokens,
|
||||
body_off = a.body_off,
|
||||
line_of = src.scan.line_of,
|
||||
source = src.path,
|
||||
declaration = a.line,
|
||||
}
|
||||
a.paths.word_events = duffle.expand_word_events(body_entry,
|
||||
pipe_ctx.component_body_index,
|
||||
corpus.word_counts or {})
|
||||
end
|
||||
|
||||
-- analyze_atom_paths fills the *cycles / branches / has_loops / unknown_macros* fields of a.paths.
|
||||
analyze_atom_paths(a)
|
||||
|
||||
|
||||
+2
-62
@@ -20,7 +20,7 @@
|
||||
-- Bootstrap: load `duffle_paths.lua` via this script's own path.
|
||||
-- Use `arg[0]` when this file is the entry script (`arg[0]` ends in "ps1_meta.lua");
|
||||
-- fall back to `debug.getinfo(1, "S").source` when this file is being dofile()'d or require()'d (in which case `arg[0]` is the *caller's* path, not ours).
|
||||
-- That single statement: (a) sets `package.path` + `package.cpath` (via cached `git rev-parse`), (b) at the bottom returns `require("duffle")`.
|
||||
-- That single statement: (a) sets `package.path` + `package.cpath`, (b) at the bottom returns `require("duffle")`.
|
||||
-- So the dofile's return value is the duffle module.
|
||||
local _is_entry_script = arg and arg[0] and arg[0]:match("ps1_meta%.lua$") ~= nil
|
||||
local _bootstrap_src
|
||||
@@ -63,12 +63,6 @@ local PASS_FLAG_DISPATCH_KEY = "__pass__"
|
||||
--- @field deps string[] -- names of upstream passes
|
||||
--- @field groups string[]? -- OPTIONAL build-phase groups this pass is a root of
|
||||
--- -- (e.g. { "pre-link" }, { "post-link" }); absent ⇒ dependency-only
|
||||
--- @field desc string -- human description (used by --help + ASCII graph)
|
||||
--- @field out PassOutput[] -- output paths (used by --dry-run + report)
|
||||
|
||||
--- @class PassOutput
|
||||
--- @field kind string -- "header" | "report"
|
||||
--- @field path_template string -- e.g. "<source_dir>/gen/<basename>.macs.h"
|
||||
|
||||
--- @class SourceFile
|
||||
--- @field path string -- absolute path to the source file
|
||||
@@ -82,15 +76,10 @@ local PASS_FLAG_DISPATCH_KEY = "__pass__"
|
||||
--- @field shared.corpus table -- canonical authored-source/project projection
|
||||
--- @field out_root string -- output root (e.g. "build/gen")
|
||||
--- @field project_root string -- PS1 repository root
|
||||
--- @field upstream table<string, table> -- per-pass output accumulator
|
||||
--- @field flags table -- CLI flags + per-pass stash
|
||||
--- @field dry_run boolean -- if true, compute but don't write
|
||||
--- @field verbose boolean -- if true, log diagnostic info
|
||||
|
||||
--- @class PassOutputEntry
|
||||
--- @field [string] string -- dynamic shape; key is the output kind
|
||||
-- (e.g. "macs_h", "offsets_h", "errors_h", "annotations_txt", "static_analysis_txt", "summary_txt"), value is the path
|
||||
|
||||
--- @class Finding
|
||||
--- @field line integer -- source line (or 0 for pass-level)
|
||||
--- @field msg string -- finding message
|
||||
@@ -125,46 +114,31 @@ local PASSES = {
|
||||
["scan-source"] = {
|
||||
module = "passes.scan_source",
|
||||
kind = "shared", deps = {},
|
||||
desc = "Walk each source once; produce the fat SourceScan payload for downstream passes",
|
||||
out = {},
|
||||
},
|
||||
["word-counts"] = {
|
||||
module = "passes.word_count_eval",
|
||||
kind = "shared", deps = {},
|
||||
desc = "Build the shared metadata table (metadata.h + .macs.h)",
|
||||
out = {},
|
||||
},
|
||||
components = {
|
||||
module = "passes.components",
|
||||
kind = "header-output",
|
||||
deps = {"scan-source", "word-counts"},
|
||||
desc = "Emit mac_X macros from MipsAtomComp_ declarations",
|
||||
out = { { kind = "header", path_template = "<source_dir>/gen/<basename>.macs.h" } },
|
||||
},
|
||||
["emission-model"] = {
|
||||
module = "passes.emission_model",
|
||||
kind = "validation",
|
||||
deps = {"components"},
|
||||
desc = "Build canonical per-atom words, markers, and invocation ancestry",
|
||||
out = {},
|
||||
},
|
||||
annotation = {
|
||||
module = "passes.annotation",
|
||||
kind = "validation",
|
||||
deps = {"scan-source", "word-counts"},
|
||||
desc = "Validate atom DSL usage; emit errors.h + annotations.txt",
|
||||
out = {
|
||||
{ kind = "report", path_template = "<out_root>/<basename>.errors.h" },
|
||||
{ kind = "report", path_template = "<out_root>/<basename>.annotations.txt" },
|
||||
},
|
||||
},
|
||||
offsets = {
|
||||
module = "passes.offsets",
|
||||
kind = "header-output",
|
||||
deps = {"scan-source", "word-counts", "components", "emission-model"},
|
||||
groups = { "pre-link" },
|
||||
desc = "Compute branch offsets for atom_label / atom_offset",
|
||||
out = { { kind = "header", path_template = "<source_dir>/gen/<basename>.offsets.h" } },
|
||||
},
|
||||
["static-analysis"] = {
|
||||
module = "passes.static_analysis",
|
||||
@@ -173,43 +147,23 @@ local PASSES = {
|
||||
-- Report severity is independent from process exit policy.
|
||||
kind = "diagnostic",
|
||||
deps = {"scan-source", "word-counts", "components", "emission-model"},
|
||||
desc = "Static analysis: GTE pipeline-fill, mac_yield uniformity, ABI handoff, GPU port-store shape, per-atom cycle budget, type consistency",
|
||||
out = { { kind = "report", path_template = "<out_root>/<basename>.static_analysis.txt" } },
|
||||
},
|
||||
["atoms-source-map"] = {
|
||||
module = "passes.atoms_source_map",
|
||||
kind = "header-output",
|
||||
deps = {"word-counts", "components", "emission-model"},
|
||||
desc = "Emit gen/<basename>.atoms.sourcemap.txt (per-.word C source line map for gdb debugging) AND gen/<basename>.atoms.provenance.txt (per-.word provenance; each word tagged with its call-site file:line and, when emitted by a mac_X(...) component invocation, the component's definition file:line). Consumed by passes/dwarf_injection.lua to synthesize DW_TAG_inlined_subroutine instances for source-level Step Into on component invocations.",
|
||||
out = {
|
||||
{ kind = "report", path_template = "<out_root>/<basename>.atoms.sourcemap.txt" },
|
||||
{ kind = "report", path_template = "<out_root>/<basename>.atoms.provenance.txt" },
|
||||
},
|
||||
},
|
||||
["dwarf-injection"] = {
|
||||
module = "passes.dwarf_injection",
|
||||
kind = "shared",
|
||||
deps = {"scan-source", "atoms-source-map"},
|
||||
groups = { "post-link" },
|
||||
desc = "Inject per-atom .debug_line + .debug_aranges (F') + per-atom .debug_info subprogram + per-wave-context-reg .debug_info variables (G') into the ELF (post-link; writes 7 section .bin blobs plus one deterministic .gdbinit sidecar). (rbind composite) reads ctx.sources[i].scan to find atom_bind(Binds_X) atoms + their Binds_X struct fields; emits per-Binds_X DW_TAG_structure_type DIEs + per-rbind-atom DW_TAG_variable 'bind_args' DIEs with piece-chain DW_OP_bregN/DW_OP_piece location expressions.",
|
||||
out = {
|
||||
{ kind = "report", path_template = "<out_root>/<basename>.dwarf_line.bin" },
|
||||
{ kind = "report", path_template = "<out_root>/<basename>.dwarf_aranges.bin" },
|
||||
{ kind = "report", path_template = "<out_root>/<basename>.dwarf_rnglists.bin" },
|
||||
{ kind = "report", path_template = "<out_root>/<basename>.dwarf_abbrev.bin" },
|
||||
{ kind = "report", path_template = "<out_root>/<basename>.dwarf_info.bin" },
|
||||
{ kind = "report", path_template = "<out_root>/<basename>.dwarf_str.bin" },
|
||||
{ kind = "report", path_template = "<out_root>/<basename>.dwarf_loc.bin" },
|
||||
{ kind = "report", path_template = "<out_root>/<basename>.gdbinit" },
|
||||
},
|
||||
},
|
||||
report = {
|
||||
module = "passes.report",
|
||||
kind = "report",
|
||||
deps = {"annotation", "static-analysis"},
|
||||
groups = { "pre-link" },
|
||||
desc = "Render the per-project summary",
|
||||
out = { { kind = "report", path_template = "<out_root>/annotation_validation.txt" } },
|
||||
},
|
||||
}
|
||||
|
||||
@@ -557,7 +511,7 @@ local function build_ctx(args)
|
||||
or normalized_project_root:sub(1, 1) == "/"
|
||||
if not project_root_is_absolute then
|
||||
-- canonical_path_key validates ordinary relative paths and rejects
|
||||
-- drive-relative paths before the legacy display-path helper is used.
|
||||
-- drive-relative paths before the absolute-path rewrite is performed.
|
||||
duffle.canonical_path_key(normalized_project_root)
|
||||
project_root = duffle.normalize_path(duffle.to_absolute_path(normalized_project_root))
|
||||
else
|
||||
@@ -654,7 +608,6 @@ local function build_ctx(args)
|
||||
local ctx = {
|
||||
metadata_path = args.metadata,
|
||||
shared = { corpus = corpus },
|
||||
upstream = {},
|
||||
out_root = args.out_root,
|
||||
project_root = corpus.project_root,
|
||||
flags = args.flags or {},
|
||||
@@ -782,16 +735,6 @@ end
|
||||
-- Main Orchestrator
|
||||
-- ════════════════════════════════════════════════════════════════════════════
|
||||
|
||||
--- (internal) Push a pass's outputs + warnings into `ctx.upstream[name]` for downstream passes to consume.
|
||||
--- @param ctx PassCtx
|
||||
--- @param pass_name string
|
||||
--- @param result PassResult
|
||||
local function accumulate_pass_result(ctx, pass_name, result)
|
||||
ctx.upstream[pass_name] = ctx.upstream[pass_name] or {}
|
||||
for _, out in ipairs(result.outputs or {}) do table.insert(ctx.upstream[pass_name], out) end
|
||||
for _, warn in ipairs(result.warnings or {}) do table.insert(ctx.upstream[pass_name], warn) end
|
||||
end
|
||||
|
||||
--- (internal) If the pass's kind is in PASS_KIND_STOP_ON_ERROR and it reported errors, write each error to stderr.
|
||||
--- Returns true if any validation errors were reported.
|
||||
--- @param pass_name string
|
||||
@@ -812,14 +755,11 @@ end
|
||||
--- @param order string[]
|
||||
--- @return boolean -- true if any validation errors were reported
|
||||
local function dispatch_passes(ctx, order)
|
||||
ctx.shared = ctx.shared or {}
|
||||
local had_errors = false
|
||||
for _, pass_name in ipairs(order) do
|
||||
local pass = PASSES[pass_name]
|
||||
-- io.stderr:write(string.format("[ps1_meta] %-22s running\n", pass_name))
|
||||
local mod = require(pass.module)
|
||||
local result = mod.run(ctx)
|
||||
accumulate_pass_result(ctx, pass_name, result)
|
||||
if report_validation_errors(pass_name, pass, result) then
|
||||
had_errors = true
|
||||
end
|
||||
|
||||
Reference in New Issue
Block a user