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