mirror of
https://github.com/Ed94/pikuma_ps1.git
synced 2026-07-23 16:07:50 +00:00
Fixes atom component gdb stepping. New phase/ctx annotations for atoms. Attempt at type views on registers (gdb pretty print failures).
Needs heavy curation and problably simplicication.
This commit is contained in:
Vendored
+4
-4
@@ -24,7 +24,7 @@
|
||||
"osx": {
|
||||
"gdbpath": "gdb"
|
||||
},
|
||||
"executable": "${workspaceRoot}/build/hello_psyq.elf",
|
||||
"executable": "${workspaceRoot}/build/hello_gte.elf",
|
||||
"setupCommands": [
|
||||
{ "text": "set mi-async off" },
|
||||
{ "text": "set remotetimeout 0" },
|
||||
@@ -33,7 +33,7 @@
|
||||
],
|
||||
"autorun": [
|
||||
"monitor reset shellhalt",
|
||||
"load hello_psyq.elf",
|
||||
"load hello_gte.elf",
|
||||
"source scripts/gdb/gdb_tape_atoms.gdb",
|
||||
"tbreak main",
|
||||
"continue"
|
||||
@@ -59,7 +59,7 @@
|
||||
"osx": {
|
||||
"gdbpath": "gdb"
|
||||
},
|
||||
"executable": "${workspaceRoot}/build/hello_gpu.elf",
|
||||
"executable": "${workspaceRoot}/build/hello_gte.elf",
|
||||
"setupCommands": [
|
||||
{ "text": "set mi-async off" },
|
||||
{ "text": "set remotetimeout 0" },
|
||||
@@ -68,7 +68,7 @@
|
||||
],
|
||||
"autorun": [
|
||||
"monitor reset shellhalt",
|
||||
"load hello_gpu.elf",
|
||||
"load hello_gte.elf",
|
||||
"tbreak main",
|
||||
"continue"
|
||||
]
|
||||
|
||||
+36
-2
@@ -63,11 +63,19 @@
|
||||
/* ============================================================================
|
||||
* atom_reads(...) / atom_writes(...)
|
||||
*
|
||||
* Used during the static analysis pass of the metaprogram to do
|
||||
* Used during the static analysis pass of the metaprogram to do
|
||||
* ============================================================================*/
|
||||
#define atom_reads(...) (__VA_ARGS__)
|
||||
#define atom_writes(...) (__VA_ARGS__)
|
||||
|
||||
/* ----------------------------------------------------------------------------
|
||||
* atom_reg (per-enum opt-in marker for the DWARF register-alias registry)
|
||||
*
|
||||
* The bare `atom_reg` token adjacent to an enum entry in mips.h / lottes_tape.h flags that alias as debug-visible for scan_source's register_alias_registry.
|
||||
* The C preprocessor strips it to a comment so no runtime symbol is created; the Lua scanner reads the bare token.
|
||||
* ----------------------------------------------------------------------------*/
|
||||
#define atom_reg /* atom_reg: opt the preceding enum entry into the DWARF registry */
|
||||
|
||||
/* ============================================================================
|
||||
* atom_info :
|
||||
* MipsAtom_(cube_tri) atom_info(
|
||||
@@ -84,12 +92,38 @@
|
||||
/* ----------------------------------------------------------------------------
|
||||
* DEBUG SOURCE-STEP MARKERS
|
||||
*
|
||||
* Place atom_dbg_skip_over() before a MipsAtom_, MipsAtomComp_, or MipsAtomComp_Proc_.
|
||||
* Place atom_dbg_skip_over() before a MipsAtom_, MipsAtomComp_, or MipsAtomComp_Proc_.
|
||||
* The following declaration kind determines whether the marker selects a whole atom or a component inline view.
|
||||
* The source scanner associates the marker with that declaration; placement diagnostics are handled by the annotation pass.
|
||||
* ----------------------------------------------------------------------------*/
|
||||
#define atom_dbg_skip_over() /* atom_dbg_skip_over: skip the following atom or component source view */
|
||||
|
||||
/* ----------------------------------------------------------------------------
|
||||
* Typed-view annotations (Registry for DWARF RR_<R_X> chain resolution)
|
||||
* atom_type(<T>) -- overloaded:
|
||||
* (a) enum-site default: `R_Foo = R_Tn, atom_reg atom_type(T)`
|
||||
* Sets the per-alias default typed view in the register_alias_registry.
|
||||
* Consumed by the DWARF chain step (e) when no per-atom atom_ctx / atom_phase / atom_type callsite provides a stronger resolution.
|
||||
* (b) callsite override: `atom_reads(R_Foo atom_type(T), ...)` Overrides the per-alias default for THIS atom only.
|
||||
* Last-write-wins per R_Name; conflict -> error.
|
||||
* atom_ctx(<atom_name>) -- atom-info sub-call:
|
||||
* Propagate another atom's atom.rbind.fields (its Binds_* typed fields) into THIS atom's typed-view resolution.
|
||||
* The named atom must be an rbind atom (have `atom_bind(Binds_X)` in its `atom_info`).
|
||||
* Used as the escape hatch when atom_phase is not the natural correlation.
|
||||
* atom_phase(<label>) -- atom-info sub-call:
|
||||
* Free-form C-identifier label for grouping atoms.
|
||||
* Within a phase, the FIRST atom in source-order that owns its own atom.rbind provides
|
||||
* the Binds_* field types used by all other atoms in the same phase.
|
||||
* The preferred correlation mechanism; atom_ctx is the escape hatch for non-natural cases.
|
||||
*
|
||||
* All three expand to C comments
|
||||
* (the bare-token convention matching `atom_reg` and `atom_dbg_skip_over`).
|
||||
* The Lua scanner reads the bare tokens in source-as-written; the C preprocessor strips them.
|
||||
* ----------------------------------------------------------------------------*/
|
||||
#define atom_type(T) /* atom_type: associate <T> with the preceding enum entry (enum site) or this register (atom-info site) */
|
||||
#define atom_ctx(atom_name) /* atom_ctx: propagate <atom_name>'s Binds_* field types into this atom's typed views */
|
||||
#define atom_phase(label) /* atom_phase: tag this atom with <label> for grouped typed-view resolution */
|
||||
|
||||
/* ----------------------------------------------------------------------------
|
||||
* atom_bind(Binds_X) -- rbind sub-call of atom_info
|
||||
*
|
||||
|
||||
@@ -32,16 +32,16 @@ typedef Slice_MipsCode MipsAtom;
|
||||
|
||||
/* Register aliases */
|
||||
enum {
|
||||
R_AtomJmp = R_T9,
|
||||
R_TapePtr = R_T8, /* The Instruction Stream Pointer */
|
||||
R_InCursor = R_T4, /* Input data cursor */
|
||||
R_AtomJmp = R_T9 atom_reg, /* debug-visible; tape yield handshake scratch */
|
||||
R_TapePtr = R_T8 atom_reg, /* The Instruction Stream Pointer */
|
||||
R_InCursor = R_T4,
|
||||
|
||||
R_PrimCursor = R_T7, /* VRAM output cursor (primitive buffer) */
|
||||
R_FaceCursor = R_T4, /* Input data cursor (indices/faces) */
|
||||
R_VertBase = R_T5, /* Base address of the vertex array */
|
||||
R_OtBase = R_T6, /* Base address of the Ordering Table */
|
||||
R_PrimCursor = R_T7 atom_reg atom_type(U4 *), /* VRAM output cursor (primitive buffer) */
|
||||
R_FaceCursor = R_T4 atom_reg atom_type(V4_S2 *), /* Cube face-index cursor (V4_S2*); floor context switches to V3_S2* via atom_phase */
|
||||
R_VertBase = R_T5 atom_reg atom_type(V3_S2 *), /* Base address of the vertex array */
|
||||
R_OtBase = R_T6 atom_reg atom_type(U4 *), /* Base address of the Ordering Table */
|
||||
|
||||
/* Stringification codes for the GCC inline assembler clobber lists */
|
||||
/* Stringification codes for the GCC inline assembler clobber lists. */
|
||||
#define R_TapePtr_Code R_T8_Code
|
||||
#define R_InCursor_Code R_T4_Code
|
||||
|
||||
|
||||
@@ -22,7 +22,7 @@ typedef Struct_(Binds_CubeTri) {
|
||||
V3_S2* VertBase;
|
||||
U4* OtBase;
|
||||
};
|
||||
internal MipsAtom_(rbind_cube_g4_face) atom_info(atom_bind(Binds_CubeTri)
|
||||
internal MipsAtom_(rbind_cube_g4_face) atom_info(atom_bind(Binds_CubeTri), atom_phase(cube_g4)
|
||||
, atom_reads(R_TapePtr)
|
||||
, atom_writes(R_PrimCursor, R_FaceCursor, R_VertBase, R_OtBase)
|
||||
){
|
||||
@@ -37,7 +37,7 @@ internal MipsAtom_(rbind_cube_g4_face) atom_info(atom_bind(Binds_CubeTri)
|
||||
|
||||
// cube_g4_face — Draw one cube face (Gouraud-shaded quad) via the GTE tape pipeline
|
||||
internal
|
||||
MipsAtom_(cube_g4_face) atom_info(
|
||||
MipsAtom_(cube_g4_face) atom_info(atom_phase(cube_g4),
|
||||
atom_reads( R_PrimCursor, R_FaceCursor, R_VertBase, R_OtBase),
|
||||
atom_writes(R_PrimCursor, R_FaceCursor)
|
||||
){
|
||||
@@ -90,7 +90,7 @@ typedef Struct_(Binds_FloorTri) {
|
||||
U4* OtBase;
|
||||
};
|
||||
internal
|
||||
MipsAtom_(rbind_floor_f3_face) atom_info(atom_bind(Binds_FloorTri)
|
||||
MipsAtom_(rbind_floor_f3_face) atom_info(atom_bind(Binds_FloorTri), atom_phase(floor_f3)
|
||||
, atom_reads(R_TapePtr)
|
||||
, atom_writes(R_PrimCursor, R_FaceCursor, R_VertBase, R_OtBase)
|
||||
){
|
||||
@@ -105,7 +105,7 @@ MipsAtom_(rbind_floor_f3_face) atom_info(atom_bind(Binds_FloorTri)
|
||||
|
||||
internal
|
||||
atom_dbg_skip_over()
|
||||
MipsAtom_(floor_f3_face) atom_info(
|
||||
MipsAtom_(floor_f3_face) atom_info(atom_phase(floor_f3)
|
||||
, atom_reads( R_PrimCursor, R_FaceCursor, R_VertBase, R_OtBase)
|
||||
, atom_writes(R_PrimCursor, R_FaceCursor)
|
||||
) {
|
||||
|
||||
+9
-25
@@ -8,7 +8,7 @@
|
||||
--- - **C-language scanner** (`skip_ws_and_cmt`, `skip_str_or_cmt`, `read_ident`, `read_parens`, `read_braces`, `read_brackets`, `read_balanced`, `scan_to_char`, `split_top_level_commas`).
|
||||
--- - **Word-count loader** (`load_word_counts` for `WORD_COUNT(...)` metadata files).
|
||||
--- - **Line lookup** (`LineIndex` returns an O(log N) `line_of(pos)` closure for source-mapping).
|
||||
--- - **Domain tables** (`WAVE_CONTEXT_REGS`, `TAPE_ATOM_MACROS`, `GTE_PIPELINE_LATENCY`, `GP0_CMD_SIZE`, `GP0_CMD_BY_SHAPE`, `GP0_MACRO_CONTRIB`, `INSTRUCTION_LATENCY`).
|
||||
--- - **Domain tables** (`TAPE_ATOM_MACROS`, `GTE_PIPELINE_LATENCY`, `GP0_CMD_SIZE`, `GP0_CMD_BY_SHAPE`, `GP0_MACRO_CONTRIB`, `INSTRUCTION_LATENCY`).
|
||||
--- - **Process-bootstrap helper** (`setup_package_path`replaces the 8-line `arg[0]`-resolution boilerplate duplicated across 7 entry scripts)
|
||||
---
|
||||
--- **Conventions**: tabs (1/level), EmmyLua annotations, no regex.
|
||||
@@ -74,21 +74,14 @@ local BYTE_DIGIT_9 = 0x39 -- '9'
|
||||
-- Section -1: Bootstrap (path-setup at module load)
|
||||
-- ════════════════════════════════════════════════════════════════════════════
|
||||
--
|
||||
-- Path setup is done by `scripts/duffle_paths.lua`, which derives the repo
|
||||
-- root from `debug.getinfo(1, "S").source` (NO subprocess, ~0ms) and
|
||||
-- then calls `require("duffle")`. The prior `io.popen("git rev-parse ...")`
|
||||
-- approach in this section was removed during F'' because:
|
||||
--
|
||||
-- 1. Every entry script + every passes script now uses
|
||||
-- `dofile("duffle_paths.lua")` (14 call sites; verified via grep).
|
||||
-- The `find_repo_root` / `setup_package_path` defined here was dead
|
||||
-- code in practice.
|
||||
-- Path setup is done by `scripts/duffle_paths.lua`, which derives the repo root from `debug.getinfo(1, "S").source` (NO subprocess, ~0ms) and then calls `require("duffle")`.
|
||||
-- The prior `io.popen("git rev-parse ...")` approach in this section was removed during F'' because:
|
||||
-- 1. Every entry script + every passes script now uses `dofile("duffle_paths.lua")` (14 call sites; verified via grep).
|
||||
-- The `find_repo_root` / `setup_package_path` defined here was dead code in practice.
|
||||
-- 2. `git rev-parse` costs ~100-180ms per subprocess spawn on Windows.
|
||||
-- `debug.getinfo` is <1ms. There's no reason to keep the slow path
|
||||
-- even as a "fallback".
|
||||
-- `debug.getinfo` is <1ms. There's no reason to keep the slow path even as a "fallback".
|
||||
--
|
||||
-- If a future use case ever needs to load `duffle.lua` WITHOUT going
|
||||
-- through `duffle_paths.lua`, set `package.path` manually before `require`.
|
||||
-- If a future use case ever needs to load `duffle.lua` WITHOUT going through `duffle_paths.lua`, set `package.path` manually before `require`.
|
||||
-- See `docs/guide_metaprogram_ssdl.md` §"I/O primitives" for the pattern.
|
||||
|
||||
-- ════════════════════════════════════════════════════════════════════════════
|
||||
@@ -434,8 +427,7 @@ function M.skip_preprocessor_line(s, pos)
|
||||
return scan + 1
|
||||
end
|
||||
|
||||
-- Split a brace-body into top-level comma-separated tokens. Honors nested
|
||||
-- parens/braces/brackets and skips strings/comments.
|
||||
-- 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
|
||||
@@ -694,13 +686,6 @@ end
|
||||
-- Section 7: domain tables
|
||||
-- ════════════════════════════════════════════════════════════════════════════
|
||||
|
||||
M.WAVE_CONTEXT_REGS = {
|
||||
["R_PrimCursor"] = { alias = "R_T7", size = 4, role = "output cursor (prim arena)" },
|
||||
["R_FaceCursor"] = { alias = "R_T4", size = 4, role = "input cursor (face array)" },
|
||||
["R_VertBase"] = { alias = "R_T5", size = 4, role = "base pointer (vertex array)" },
|
||||
["R_OtBase"] = { alias = "R_T6", size = 4, role = "base pointer (ordering table)" },
|
||||
}
|
||||
|
||||
-- The annotation DSL has been reduced to a single annotation macro:
|
||||
-- atom_info(atom_bind(Binds_X), atom_reads(...), atom_writes(...))
|
||||
-- All phase / region / cadence / async / resource / group tokens have been dropped.
|
||||
@@ -713,8 +698,7 @@ M.TAPE_ATOM_MACROS = {
|
||||
-- GTE pipeline-fill latency table.
|
||||
--
|
||||
-- For each `gte_cmdw_*` macro in code/duffle/gte.h, the minimum number of consecutive COP2 "nop" words that MUST appear
|
||||
-- before the command issues so that any preceding `lwc2`/`swc2`/C2 state writes have retired before the GTE starts
|
||||
-- reading its input registers.
|
||||
-- before the command issues so that any preceding `lwc2`/`swc2`/C2 state writes have retired before the GTE starts reading its input registers.
|
||||
--
|
||||
-- The check (`scripts/passes/static_analysis.lua :: check_gte_pipeline_fill`) walks each atom body,
|
||||
-- counts the consecutive nop words before every `gte_cmdw_*` invocation, and reports a finding if the count is below this minimum.
|
||||
|
||||
+42
-44
@@ -41,7 +41,7 @@ M.DW_TAG = {
|
||||
}
|
||||
|
||||
M.DW_AT = {
|
||||
name = 0x03,
|
||||
name = 0x03,
|
||||
low_pc = 0x11,
|
||||
high_pc = 0x12,
|
||||
language = 0x13,
|
||||
@@ -214,11 +214,9 @@ M.DWARF_LINE_OPS = {
|
||||
---
|
||||
--- **Convention:** offsets are 1-indexed (matching Lua `string.sub`).
|
||||
---
|
||||
--- **Byte weights** are written as `0x100`, `0x10000`, `0x1000000` (i.e.
|
||||
--- 2^8, 2^16, 2^24) so the LE byte positions are visually explicit:
|
||||
--- **Byte weights** are written as `0x100`, `0x10000`, `0x1000000` (i.e. 2^8, 2^16, 2^24) so the LE byte positions are visually explicit:
|
||||
--- byte 0 contributes its value directly; byte 1 is shifted left by 8
|
||||
--- (= 0x100); byte 2 by 16 (= 0x10000); byte 3 by 24 (= 0x1000000).
|
||||
---
|
||||
--- @param buf string
|
||||
--- @param off integer -- 1-indexed
|
||||
--- @return integer
|
||||
@@ -238,9 +236,12 @@ function M.read_u16_le(buf, off)
|
||||
return buf:byte(off) + buf:byte(off + 0x01) * 0x00000100
|
||||
end
|
||||
|
||||
-- Pure-Lua 5.3 LEB128 readers (no `bit` library). `2^shift` arithmetic matches
|
||||
-- the existing F' parser. Offsets are 0-based; returns (value, next_pos).
|
||||
local function read_uleb128_at(buf, pos)
|
||||
-- Pure-Lua 5.3 LEB128 readers (no `bit` library). `2^shift` arithmetic matches the existing F' parser.
|
||||
-- Offsets are 0-based; returns (value, next_pos).
|
||||
-- Track A Task 10: promoted from `local function` to M.* exports so passes/dwarf_injection.lua
|
||||
-- can import them as file-scope locals per the 2nd-caller lift precedent
|
||||
-- (the uleb128 + sleb128 encoders were promoted the same way).
|
||||
function M.read_uleb128_at(buf, pos)
|
||||
local value, shift = 0, 0
|
||||
local len = #buf
|
||||
while pos < len do
|
||||
@@ -253,7 +254,7 @@ local function read_uleb128_at(buf, pos)
|
||||
return nil, pos
|
||||
end
|
||||
|
||||
local function read_sleb128_at(buf, pos)
|
||||
function M.read_sleb128_at(buf, pos)
|
||||
local value, shift = 0, 0
|
||||
local len = #buf
|
||||
while pos < len do
|
||||
@@ -270,30 +271,30 @@ local function read_sleb128_at(buf, pos)
|
||||
end
|
||||
|
||||
-- Find the 0-based offset of the table-terminator byte (a single 0) for the abbrev table starting at `table_start`.
|
||||
-- Returns nil on truncated input. Walks declaration headers
|
||||
-- (code, tag, has_children, attr/form pairs, DW_FORM_implicit_const constant) until it finds a 0 byte that follows a complete declaration. Mirrors dwarf_injection.lua :: find_abbrev_table_end.
|
||||
local function find_abbrev_table_end(table_bytes, table_start)
|
||||
-- Returns nil on truncated input. Walks declaration headers
|
||||
-- (code, tag, has_children, attr/form pairs, DW_FORM_implicit_const constant) until it finds a 0 byte that follows a complete declaration.
|
||||
function M.find_abbrev_table_end(table_bytes, table_start)
|
||||
local pos, len = table_start, #table_bytes
|
||||
if pos >= len or table_bytes:byte(pos + 1) == 0 then return pos end
|
||||
if pos >= len or table_bytes:byte(pos + 1) == 0 then return pos end
|
||||
while pos < len do
|
||||
local _code, code_end = read_uleb128_at(table_bytes, pos)
|
||||
local _code, code_end = M.read_uleb128_at(table_bytes, pos)
|
||||
if not _code then return nil end
|
||||
pos = code_end
|
||||
local _tag, tag_end = read_uleb128_at(table_bytes, pos)
|
||||
local _tag, tag_end = M.read_uleb128_at(table_bytes, pos)
|
||||
if not _tag then return nil end
|
||||
pos = tag_end
|
||||
if pos >= len then return nil end
|
||||
pos = pos + 1 -- has_children byte
|
||||
while pos < len do
|
||||
local attr, attr_end = read_uleb128_at(table_bytes, pos)
|
||||
local attr, attr_end = M.read_uleb128_at(table_bytes, pos)
|
||||
if not attr then return nil end
|
||||
pos = attr_end
|
||||
local form, form_end = read_uleb128_at(table_bytes, pos)
|
||||
local form, form_end = M.read_uleb128_at(table_bytes, pos)
|
||||
if not form then return nil end
|
||||
pos = form_end
|
||||
if attr == 0 and form == 0 then break end
|
||||
if form == DW_FORM_implicit_const then
|
||||
local _c, ce = read_sleb128_at(table_bytes, pos)
|
||||
local _c, ce = M.read_sleb128_at(table_bytes, pos)
|
||||
if not _c then return nil end
|
||||
pos = ce
|
||||
end
|
||||
@@ -317,31 +318,31 @@ end
|
||||
-- {code, tag, has_children, attrs={ {name, form}, ... }}.
|
||||
-- Stops at the table terminator.
|
||||
local function parse_abbrev_table(table_bytes, table_start)
|
||||
local table_end = find_abbrev_table_end(table_bytes, table_start)
|
||||
local table_end = M.find_abbrev_table_end(table_bytes, table_start)
|
||||
if not table_end then return nil, "no terminator" end
|
||||
local decls = {}
|
||||
local pos = table_start
|
||||
while pos < table_end do
|
||||
local code, code_end = read_uleb128_at(table_bytes, pos)
|
||||
local code, code_end = M.read_uleb128_at(table_bytes, pos)
|
||||
if not code then return nil, "truncated code" end
|
||||
pos = code_end
|
||||
local tag, tag_end = read_uleb128_at(table_bytes, pos)
|
||||
local tag, tag_end = M.read_uleb128_at(table_bytes, pos)
|
||||
if not tag then return nil, "truncated tag" end
|
||||
pos = tag_end
|
||||
local has_children = table_bytes:byte(pos + 1)
|
||||
pos = pos + 1
|
||||
local attrs = {}
|
||||
while true do
|
||||
local attr, attr_end = read_uleb128_at(table_bytes, pos)
|
||||
local attr, attr_end = M.read_uleb128_at(table_bytes, pos)
|
||||
if not attr then return nil, "truncated attr" end
|
||||
pos = attr_end
|
||||
local form, form_end = read_uleb128_at(table_bytes, pos)
|
||||
local form, form_end = M.read_uleb128_at(table_bytes, pos)
|
||||
if not form then return nil, "truncated form" end
|
||||
pos = form_end
|
||||
if attr == 0 and form == 0 then break end
|
||||
attrs[#attrs + 1] = { name = attr, form = form }
|
||||
if form == DW_FORM_implicit_const then
|
||||
local _c, ce = read_sleb128_at(table_bytes, pos)
|
||||
local _c, ce = M.read_sleb128_at(table_bytes, pos)
|
||||
if not _c then return nil, "truncated const" end
|
||||
pos = ce
|
||||
end
|
||||
@@ -366,7 +367,7 @@ local function read_form_value(buf, str_buf, pos, form)
|
||||
-- DW_FORM_strp: 4-byte offset into .debug_str.
|
||||
local strp_off = M.read_u32_le(buf, pos + 1)
|
||||
return read_c_string_at(str_buf, strp_off), pos + 4
|
||||
elseif form == M.DW_FORM.udata then return read_uleb128_at(buf, pos)
|
||||
elseif form == M.DW_FORM.udata then return M.read_uleb128_at(buf, pos)
|
||||
elseif form == M.DW_FORM.data1 then return buf:byte(pos + 1), pos + 1
|
||||
elseif form == M.DW_FORM.data2 then return M.read_u16_le(buf, pos + 1), pos + 2
|
||||
elseif form == M.DW_FORM.data4 then return M.read_u32_le(buf, pos + 1), pos + 4
|
||||
@@ -379,7 +380,7 @@ local function read_form_value(buf, str_buf, pos, form)
|
||||
return 1, pos
|
||||
elseif form == M.DW_FORM.exprloc then
|
||||
-- DW_FORM_exprloc: ULEB byte count + that many bytes of DW_OP_*.
|
||||
local len, ne = read_uleb128_at(buf, pos)
|
||||
local len, ne = M.read_uleb128_at(buf, pos)
|
||||
if not len then return nil, pos end
|
||||
return nil, ne + len
|
||||
elseif form == DW_FORM_implicit_const then
|
||||
@@ -392,8 +393,7 @@ local function read_form_value(buf, str_buf, pos, form)
|
||||
end
|
||||
end
|
||||
|
||||
-- Index the .debug_info + .debug_abbrev sections of an existing ELF and collect one entry
|
||||
-- per "interesting" type DIE in the FIRST compilation unit.
|
||||
-- Index the .debug_info + .debug_abbrev sections of an existing ELF and collect one entry per "interesting" type DIE in the FIRST compilation unit.
|
||||
-- The index supports typed-views:
|
||||
-- index = {
|
||||
-- by_name = { ["V4_S2"] = {kind="structure_type", die_offset, byte_size, fields={...}},
|
||||
@@ -435,7 +435,7 @@ function M.index_main_cu_types(info, abbrev, str_buf, abbrev_offset, cu_start)
|
||||
local unit_end = pos + 4 + ul
|
||||
if unit_end > #info then break end
|
||||
local unit_abbrev = M.read_u32_le(info, pos + 9)
|
||||
if unit_abbrev == abbrev_offset then
|
||||
if unit_abbrev == abbrev_offset then
|
||||
cu_start = pos
|
||||
cu_end_excl = unit_end
|
||||
break
|
||||
@@ -455,12 +455,12 @@ function M.index_main_cu_types(info, abbrev, str_buf, abbrev_offset, cu_start)
|
||||
-- Root DIE is the compile_unit; skip past it.
|
||||
if pos_cursor >= cu_end_excl then return nil, "no DIE bytes" end
|
||||
local root_decl_code
|
||||
root_decl_code, pos_cursor = read_uleb128_at(info, pos_cursor)
|
||||
root_decl_code, pos_cursor = M.read_uleb128_at(info, pos_cursor)
|
||||
if not root_decl_code then return nil, "truncated root DIE code" end
|
||||
local root_decl = abbrev_by_code[root_decl_code]
|
||||
if not root_decl then return nil, "unknown root DIE abbrev" end
|
||||
-- Skip root DIE attributes.
|
||||
for _, attr in ipairs(root_decl.attrs) do
|
||||
for _, attr in ipairs(root_decl.attrs) do
|
||||
local _, ne = read_form_value(info, str_buf, pos_cursor, attr.form)
|
||||
if not ne then return nil, "truncated root attr" end
|
||||
pos_cursor = ne
|
||||
@@ -502,7 +502,7 @@ function M.index_main_cu_types(info, abbrev, str_buf, abbrev_offset, cu_start)
|
||||
local die_encoding = nil
|
||||
local die_type_ref = nil
|
||||
for ai, attr in ipairs(decl.attrs) do
|
||||
local before = pos
|
||||
local before = pos
|
||||
local val, ne = read_form_value(info, str_buf, pos, attr.form)
|
||||
if not ne then
|
||||
return nil, "truncated DIE attr"
|
||||
@@ -510,8 +510,7 @@ function M.index_main_cu_types(info, abbrev, str_buf, abbrev_offset, cu_start)
|
||||
pos = ne
|
||||
if attr.name == M.DW_AT.name then
|
||||
die_name = val
|
||||
elseif attr.name == M.DW_AT.byte_size and
|
||||
(decl.tag == M.DW_TAG.base_type or decl.tag == M.DW_TAG.structure_type) then
|
||||
elseif attr.name == M.DW_AT.byte_size and (decl.tag == M.DW_TAG.base_type or decl.tag == M.DW_TAG.structure_type) then
|
||||
die_byte_size = val
|
||||
elseif attr.name == M.DW_AT.encoding and decl.tag == M.DW_TAG.base_type then
|
||||
die_encoding = val
|
||||
@@ -539,9 +538,9 @@ function M.index_main_cu_types(info, abbrev, str_buf, abbrev_offset, cu_start)
|
||||
local v, ne = read_form_value(info, str_buf, pos, a.form)
|
||||
if not ne then return nil, "truncated member attr" end
|
||||
pos = ne
|
||||
if a.name == M.DW_AT.name then mname = v
|
||||
elseif a.name == M.DW_AT.type then mtype_ref = v
|
||||
elseif a.name == M.DW_AT.data_member_location then moffset = v end
|
||||
if a.name == M.DW_AT.name then mname = v
|
||||
elseif a.name == M.DW_AT.type then mtype_ref = v
|
||||
elseif a.name == M.DW_AT.data_member_location then moffset = v end
|
||||
end
|
||||
fields[#fields + 1] = { name = mname, type_offset = mtype_ref, offset = moffset }
|
||||
end
|
||||
@@ -575,7 +574,7 @@ function M.index_main_cu_types(info, abbrev, str_buf, abbrev_offset, cu_start)
|
||||
end
|
||||
end
|
||||
-- Find the terminator (0 byte) of this table.
|
||||
local term = find_abbrev_table_end(abbrev, scan_pos)
|
||||
local term = M.find_abbrev_table_end(abbrev, scan_pos)
|
||||
if not term then break end
|
||||
scan_pos = term + 1
|
||||
::continue::
|
||||
@@ -594,11 +593,12 @@ function M.index_main_cu_types(info, abbrev, str_buf, abbrev_offset, cu_start)
|
||||
local die_name, die_byte_size, die_encoding, die_type_ref, pos_after_attrs
|
||||
= read_result[1], read_result[2], read_result[3], read_result[4], read_result[5]
|
||||
n_visited = n_visited + 1
|
||||
local is_type = (decl.tag == M.DW_TAG.base_type
|
||||
or decl.tag == M.DW_TAG.structure_type
|
||||
or decl.tag == M.DW_TAG.typedef
|
||||
or decl.tag == M.DW_TAG.pointer_type
|
||||
or decl.tag == M.DW_TAG.const_type)
|
||||
local is_type = (
|
||||
decl.tag == M.DW_TAG.base_type
|
||||
or decl.tag == M.DW_TAG.structure_type
|
||||
or decl.tag == M.DW_TAG.typedef
|
||||
or decl.tag == M.DW_TAG.pointer_type
|
||||
or decl.tag == M.DW_TAG.const_type)
|
||||
|
||||
local member_fields = nil
|
||||
pos_cursor = pos_after_attrs
|
||||
@@ -716,7 +716,6 @@ end
|
||||
|
||||
--- Return a 4-byte little-endian byte string for `value`.
|
||||
--- Caller concatenates with `..` if composing multi-word blobs.
|
||||
---
|
||||
--- **Byte weights** written as `0x100` etc. (see `M.read_u32_le` for rationale).
|
||||
--- @param value integer -- 0 ≤ value ≤ 0xFFFFFFFF
|
||||
--- @return string
|
||||
@@ -857,7 +856,6 @@ end
|
||||
--- - 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.
|
||||
--- - `st_size > 0` filter excludes undefined/imported symbols.
|
||||
---
|
||||
--- @param elf_path Path
|
||||
--- @return table<string, {integer, integer}>
|
||||
function M.read_nm(elf_path)
|
||||
|
||||
+81
-121
@@ -3,7 +3,7 @@
|
||||
--- Validates `MipsAtom_(name) atom_info(atom_bind(Binds_X), atom_reads(...), atom_writes(...)) { ... }` declarations in source files.
|
||||
--- Also reads: `Binds_*` struct declarations (`typedef Struct_(Binds_X) { ... };`)
|
||||
---
|
||||
--- Source scanning: done ONCE upstream by `duffle.scan_source()` (ps1_meta.lua pre-scans each source and stashes the result in `src.scan`).
|
||||
--- Source scanning: done ONCE upstream by `duffle.scan_source()` (ps1_meta.lua pre-scans each source and stashes the result in `src.scan`).
|
||||
---
|
||||
--- Writes:
|
||||
--- - `<ctx.out_root>/<dir_basename>.errors.h` — one per module, with `#error` directives on findings (the C compile will surface the error)
|
||||
@@ -21,30 +21,9 @@ local duffle = dofile(_bootstrap_dir .. "../duffle_paths.lua")
|
||||
local write_file = duffle.write_file
|
||||
local ensure_dir = duffle.ensure_dir
|
||||
|
||||
-- Domain tables (single source of truth in duffle.lua).
|
||||
local WAVE_CONTEXT_REGS = duffle.WAVE_CONTEXT_REGS
|
||||
|
||||
local function is_wave_context_reg(n) return WAVE_CONTEXT_REGS[n] ~= nil end
|
||||
|
||||
-- Closed sets. atom_dbg_reg_default and atom_reg_types MUST target one of these registers;
|
||||
-- atom_view MUST reference a real Binds_* struct.
|
||||
local SEMANTIC_DEFAULT_REGS = {
|
||||
["R_TapePtr"] = true, ["R_AtomJmp"] = true,
|
||||
["R_PrimCursor"] = true, ["R_FaceCursor"] = true,
|
||||
["R_VertBase"] = true, ["R_OtBase"] = true,
|
||||
}
|
||||
|
||||
local COMPUTE_REG_PREFIX = "R_"
|
||||
local COMPUTE_REG_RE = -- permits R_T0..R_T3 + caller-trash (not wave-context)
|
||||
"^R_T[0-3]$"
|
||||
|
||||
-- Compute-register types are restricted to byte-width primitives.
|
||||
-- Pointer depth is also bounded typed pointer chains live in the Binds_* struct, not in a per-atom override.
|
||||
local ALLOWED_COMPUTE_TYPES = {
|
||||
["U4"] = true, ["S4"] = true,
|
||||
["U2"] = true, ["S2"] = true,
|
||||
["U1"] = true, ["S1"] = true,
|
||||
}
|
||||
-- The annotation pass now consults the source-derived registries built by scan_source:
|
||||
-- * pipe_ctx.register_alias_registry — for atom_dbg_reg_default(R_X, ...) and atom_reg_types(R_X, ...) member-identity checks
|
||||
-- * pipe_ctx.type_name_registry — for atom_dbg_reg_default(<T>, ...) and atom_reg_types(<T>, ...) type-identity checks
|
||||
|
||||
-- ════════════════════════════════════════════════════════════════════════════
|
||||
-- Type declarations
|
||||
@@ -179,50 +158,6 @@ local function check_binds_struct_exists(a, pipe_ctx, findings)
|
||||
}
|
||||
end
|
||||
|
||||
--- Check: Binds_* struct fields must correspond to known wave-context registers.
|
||||
--- Also checks that all `atom_writes(...)` entries are wave-context registers.
|
||||
--- @param a AtomAnnotation
|
||||
--- @param pipe_ctx PipeCtx
|
||||
--- @param findings Findings
|
||||
local function check_binds_field_wave_context(a, pipe_ctx, findings)
|
||||
if not (a.binds and pipe_ctx.binds_index[a.binds]) then return end
|
||||
local bs = pipe_ctx.binds_index[a.binds]
|
||||
|
||||
for _, f in ipairs(bs.fields) do
|
||||
local candidate = "R_" .. f.name
|
||||
if not is_wave_context_reg(candidate) then
|
||||
findings.warnings[#findings.warnings + 1] = {
|
||||
line = bs.line,
|
||||
msg = string.format("%s field '%s' doesn't match a known wave-context register (candidate '%s')", a.binds, f.name, candidate),
|
||||
}
|
||||
end
|
||||
end
|
||||
|
||||
for _, w in ipairs(a.writes) do
|
||||
if not is_wave_context_reg(w) then
|
||||
findings.warnings[#findings.warnings + 1] = {
|
||||
line = a.line,
|
||||
msg = string.format("%s writes '%s' which is not a known wave-context register", a.name, w),
|
||||
}
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
--- Check: atom_reads(...) entries should be wave-context registers (or R_TapePtr for rbind).
|
||||
--- @param a AtomAnnotation
|
||||
--- @param pipe_ctx PipeCtx
|
||||
--- @param findings Findings
|
||||
local function check_reads_wave_context(a, pipe_ctx, findings)
|
||||
for _, r in ipairs(a.reads) do
|
||||
if not is_wave_context_reg(r) and r ~= "R_TapePtr" then
|
||||
findings.warnings[#findings.warnings + 1] = {
|
||||
line = a.line,
|
||||
msg = string.format("atom '%s' reads '%s' which is not a known wave-context register", a.name, r),
|
||||
}
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
--- Check: TAPE_WORDS(mac_X, N) ↔ WORD_COUNT(mac_X, N) drift.
|
||||
--- Three outcomes: missing (error), mismatch (error), match (info).
|
||||
--- @param m MacroEntry
|
||||
@@ -250,28 +185,14 @@ local function check_macro_word_drift(m, wc, findings)
|
||||
}
|
||||
end
|
||||
|
||||
---- Check: atom_dbg_reg_default(R_X, <type>) must target a wave-context register and the type name must be a recognized C type
|
||||
--- (we currently allow the types actually used in the codebase; pointer depth must be 0 or 1).
|
||||
--- Also detects duplicate defaults for the same register.
|
||||
--- Check: atom_dbg_reg_default(R_X, <type>) must target a register declared as a debug-visible alias in `pipe_ctx.register_alias_registry`,
|
||||
--- with a type name found in `pipe_ctx.type_name_registry`.
|
||||
--- Pointer depth is still bounded to 0 or 1. Duplicate defaults are still detected.
|
||||
--- @param _src SourceFile -- unused (kept for the per_source shape)
|
||||
--- @param pipe_ctx PipeCtx
|
||||
--- @param findings Findings
|
||||
-- Known type names accepted in atom_dbg_reg_default and the Binds_*-via-atom_view resolution path.
|
||||
local KNOWN_REG_DEFAULT_TYPES = {
|
||||
["U4"] = true, ["S4"] = true,
|
||||
["U2"] = true, ["S2"] = true,
|
||||
["U1"] = true, ["S1"] = true,
|
||||
["MipsCode"] = true, ["MipsAtom"] = true,
|
||||
["V2_S2"] = true, ["V2_S4"] = true,
|
||||
["V3_S2"] = true, ["V3_S4"] = true,
|
||||
["V4_S2"] = true, ["V4_S4"] = true,
|
||||
["M3_S2"] = true,
|
||||
["void"] = true,
|
||||
}
|
||||
|
||||
local function check_semantic_reg_defaults(_src, pipe_ctx, findings)
|
||||
-- Detect duplicate defaults using the ordered occurrence list (the
|
||||
-- out.types hash only retains the last declaration).
|
||||
-- Detect duplicate defaults using the ordered occurrence list (the out.types hash only retains the last declaration).
|
||||
local seen_first_line = {}
|
||||
for _, occ in ipairs(pipe_ctx.type_occurrences or {}) do
|
||||
if seen_first_line[occ.reg] == nil then
|
||||
@@ -285,12 +206,14 @@ local function check_semantic_reg_defaults(_src, pipe_ctx, findings)
|
||||
}
|
||||
end
|
||||
end
|
||||
local reg_registry = pipe_ctx.register_alias_registry or {}
|
||||
local type_registry = pipe_ctx.type_name_registry or {}
|
||||
for reg, def in pairs(pipe_ctx.types or {}) do
|
||||
if not SEMANTIC_DEFAULT_REGS[reg] then
|
||||
if not reg_registry[reg] then
|
||||
findings.errors[#findings.errors + 1] = {
|
||||
line = def.source_line,
|
||||
msg = string.format(
|
||||
"atom_dbg_reg_default at line %d references unknown register %q; expected one of R_TapePtr, R_AtomJmp, R_PrimCursor, R_FaceCursor, R_VertBase, R_OtBase",
|
||||
"atom_dbg_reg_default at line %d references unknown register %q (not in register_alias_registry)",
|
||||
def.source_line, reg),
|
||||
}
|
||||
end
|
||||
@@ -302,39 +225,43 @@ local function check_semantic_reg_defaults(_src, pipe_ctx, findings)
|
||||
def.source_line, reg, def.pointer_depth or -1),
|
||||
}
|
||||
end
|
||||
if not def.type_name or not KNOWN_REG_DEFAULT_TYPES[def.type_name] then
|
||||
if not def.type_name or not type_registry[def.type_name] then
|
||||
findings.errors[#findings.errors + 1] = {
|
||||
line = def.source_line,
|
||||
msg = string.format(
|
||||
"atom_dbg_reg_default at line %d for %q uses unknown type %q (allowed: byte-width primitives, V*_S*, M3_S2, MipsCode, void)",
|
||||
"atom_dbg_reg_default at line %d for %q uses unknown type %q (not in type_name_registry)",
|
||||
def.source_line, reg, tostring(def.type_name)),
|
||||
}
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
--- Check: atom_reg_types(R_X, <type>) entries must point to a compute register
|
||||
--- (R_T0..R_T3) with a recognized fundamental type.
|
||||
--- Check: atom_reg_types(R_X, <type>) entries must point to a register declared in `pipe_ctx.register_alias_registry`, with a type name found in `pipe_ctx.type_name_registry`.
|
||||
--- The alias ident `R_<n>` now encodes the GPR identity only for entries that are explicitly opted in via the bare `atom_reg` marker.
|
||||
--- R_T0..R_T3 are intentionally NOT auto-included (per the prototype principle: no auto-include of wave-context; explicit opt-in only).
|
||||
--- The check fires for any R_T0..R_T3 reference that hasn't been opted in via `#define atom_reg`.
|
||||
--- @param _src SourceFile
|
||||
--- @param pipe_ctx PipeCtx
|
||||
--- @param findings Findings
|
||||
local function check_atom_reg_types(_src, pipe_ctx, findings)
|
||||
local reg_registry = pipe_ctx.register_alias_registry or {}
|
||||
local type_registry = pipe_ctx.type_name_registry or {}
|
||||
for _, ai in ipairs(pipe_ctx.atom_infos_list or {}) do
|
||||
if ai.reg_type_overrides then
|
||||
for reg, ov in pairs(ai.reg_type_overrides) do
|
||||
if not reg:match(COMPUTE_REG_RE) then
|
||||
if not reg_registry[reg] then
|
||||
findings.errors[#findings.errors + 1] = {
|
||||
line = ai.info_line,
|
||||
msg = string.format(
|
||||
"atom '%s' has atom_reg_types for %q; compute-register types are restricted to R_T0..R_T3",
|
||||
ai.atom_name, reg),
|
||||
"atom '%s' has atom_reg_types for %q; compute-register types are restricted to opt-in aliases (%q not in register_alias_registry)",
|
||||
ai.atom_name, reg, reg),
|
||||
}
|
||||
end
|
||||
if not ov.type_name or not ALLOWED_COMPUTE_TYPES[ov.type_name] then
|
||||
if not ov.type_name or not type_registry[ov.type_name] then
|
||||
findings.errors[#findings.errors + 1] = {
|
||||
line = ai.info_line,
|
||||
msg = string.format(
|
||||
"atom '%s' atom_reg_types for %q uses unknown compute type %q (allowed: U1/U2/U4/S1/S2/S4)",
|
||||
"atom '%s' atom_reg_types for %q uses unknown compute type %q (not in type_name_registry)",
|
||||
ai.atom_name, reg, tostring(ov.type_name)),
|
||||
}
|
||||
end
|
||||
@@ -397,16 +324,14 @@ local function check_binds_no_duplicate_fields(_src, pipe_ctx, findings)
|
||||
end
|
||||
|
||||
-- Check: skip-over markers must satisfy shape + placement constraints.
|
||||
--- Walks the priority list once; at most one error is appended per marker so that
|
||||
--- a single source-level defect does not cascade into multiple findings.
|
||||
--- Walks the priority list once; at most one error is appended per marker so that a single source-level defect does not cascade into multiple findings.
|
||||
--- Priority order (first defect wins):
|
||||
--- 1. has_parens == false -> requires parentheses: marker()
|
||||
--- 2. args ~= "" -> takes no arguments
|
||||
--- 3. superseded_by_marker_line -> duplicate marker (cite superseding line)
|
||||
--- 4. pending + no target_kind -> dangling (no following declaration)
|
||||
--- 5. unsupported target_kind -> marker precedes an unrelated declaration
|
||||
--- Valid markers before whole-atom / bare-component / proc-component declarations
|
||||
--- emit no error and remain in src.scan.skip_over.atoms / .components.
|
||||
--- 5. unsupported target_kind -> marker precedes an unrelated declaration
|
||||
--- Valid markers before whole-atom / bare-component / proc-component declarations emit no error and remain in src.scan.skip_over.atoms / .components.
|
||||
--- @param marker SkipOverMarker
|
||||
--- @param _pipe_ctx PipeCtx -- unused today; kept for plex-shape consistency with per_annot
|
||||
--- @param findings Findings
|
||||
@@ -460,30 +385,62 @@ local function check_skip_marker(marker, _pipe_ctx, findings)
|
||||
end
|
||||
end
|
||||
|
||||
--- Migration warning emitted alongside the new registry-membership check.
|
||||
---
|
||||
--- R_TapePtr / R_AtomJmp / R_PrimCursor / R_FaceCursor / R_VertBase / R_OtBase
|
||||
--- are the wave-context aliases opted in via `#define atom_reg` in lottes_tape.h (Task 21).
|
||||
--- Any source referencing an R_X that's NOT in the registry will trip the new check; a single pass-level info entry
|
||||
--- (emitted only when at least one such rejection lands in this source) tells users where to look.
|
||||
---
|
||||
--- Track A Task 13 added the proper `enum_alias_membership` per_source rule;
|
||||
--- this is the stop-gap until users migrate off raw C-ABI register names.
|
||||
--- @param _src SourceFile
|
||||
--- @param pipe_ctx PipeCtx
|
||||
--- @param findings Findings
|
||||
local function check_wave_context_migration(_src, pipe_ctx, findings)
|
||||
if not (pipe_ctx.types and next(pipe_ctx.types)) then return end
|
||||
if not (pipe_ctx.atom_infos_list) then return end
|
||||
local reg_registry = pipe_ctx.register_alias_registry or {}
|
||||
for _, ai in ipairs(pipe_ctx.atom_infos_list) do
|
||||
if ai.reg_type_overrides then
|
||||
for reg, _ in pairs(ai.reg_type_overrides) do
|
||||
if not reg_registry[reg] then
|
||||
findings.warnings[#findings.warnings + 1] = {
|
||||
line = 0,
|
||||
msg = "wave-context removed; opt in via #define atom_reg in mips.h "
|
||||
.. "(every R_<alias> that should be visible to the annotation pass "
|
||||
.. "must be enum-declared with the bare atom_reg marker; see Track A Task 21)",
|
||||
}
|
||||
return
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
-- ════════════════════════════════════════════════════════════════════════════
|
||||
-- CHECK_RULES — data-driven check dispatch (the plex pattern)
|
||||
-- ════════════════════════════════════════════════════════════════════════════
|
||||
--
|
||||
-- Each rule entry picks one of four "shapes" of dispatch:
|
||||
-- per_annot(annot, pipe_ctx, findings) — runs once per AtomAnnotation
|
||||
-- post(pipe_ctx, findings) — runs once after all per_annot calls complete (full-corpus aggregation)
|
||||
-- per_macro(macro, wc, findings) — runs once per TAPE_WORDS / _Pragma macro declaration
|
||||
-- per_skip_marker(marker, pipe_ctx, findings) — runs once per src.scan.skip_over.markers entry
|
||||
-- per_annot(annot, pipe_ctx, findings) -- runs once per AtomAnnotation
|
||||
-- post(pipe_ctx, findings) -- runs once after all per_annot calls complete (full-corpus aggregation)
|
||||
-- per_macro(macro, wc, findings) -- runs once per TAPE_WORDS / _Pragma macro declaration
|
||||
-- per_skip_marker(marker, pipe_ctx, findings) -- runs once per src.scan.skip_over.markers entry
|
||||
--
|
||||
-- Adding a new check = 1 row here + 1 function above. The `validate()` dispatch loop never needs editing.
|
||||
|
||||
local CHECK_RULES = {
|
||||
{ name = "atom_decl_exists", per_annot = check_atom_decl_exists },
|
||||
{ name = "binds_struct_exists", per_annot = check_binds_struct_exists },
|
||||
{ name = "binds_field_wave_context", per_annot = check_binds_field_wave_context },
|
||||
{ name = "reads_wave_context", per_annot = check_reads_wave_context },
|
||||
{ name = "unique_annotation", post = check_unique_annotation },
|
||||
{ name = "macro_word_drift", per_macro = check_macro_word_drift },
|
||||
{ name = "skip_marker_validation", per_skip_marker = check_skip_marker },
|
||||
{ name = "skip_marker_validation", per_skip_marker = check_skip_marker },
|
||||
{ name = "semantic_reg_defaults", per_source = check_semantic_reg_defaults },
|
||||
{ name = "atom_reg_types", per_source = check_atom_reg_types },
|
||||
{ name = "atom_view_layout", per_source = check_atom_view_layout },
|
||||
{ name = "binds_no_duplicate_fields", per_source = check_binds_no_duplicate_fields },
|
||||
{ name = "wave_context_migration", per_source = check_wave_context_migration },
|
||||
}
|
||||
|
||||
-- ════════════════════════════════════════════════════════════════════════════
|
||||
@@ -536,15 +493,19 @@ local function validate(ctx, src)
|
||||
end
|
||||
|
||||
local pipe_ctx = {
|
||||
atom_index = {},
|
||||
binds_index = {},
|
||||
annot_counts = {},
|
||||
types = scan.types or {},
|
||||
type_occurrences = scan.type_occurrences or {},
|
||||
atom_views = scan.atom_views or {},
|
||||
seen_defaults = seen_defaults,
|
||||
atom_infos_list = atom_infos_list,
|
||||
binds_list = scan.binds or {},
|
||||
atom_index = {},
|
||||
binds_index = {},
|
||||
annot_counts = {},
|
||||
types = scan.types or {},
|
||||
type_occurrences = scan.type_occurrences or {},
|
||||
atom_views = scan.atom_views or {},
|
||||
seen_defaults = seen_defaults,
|
||||
atom_infos_list = atom_infos_list,
|
||||
binds_list = scan.binds or {},
|
||||
-- Project the source-derived registries from the scan payload so per_source checks consult them instead of the deleted
|
||||
-- SEMANTIC_DEFAULT_REGS / KNOWN_REG_DEFAULT_TYPES / etc.
|
||||
register_alias_registry = scan.register_alias_registry or {},
|
||||
type_name_registry = scan.type_name_registry or {},
|
||||
}
|
||||
for _, a in ipairs(atoms) do pipe_ctx.atom_index [a.name] = a end
|
||||
for _, b in ipairs(scan.binds) do pipe_ctx.binds_index[b.name] = b end
|
||||
@@ -691,8 +652,7 @@ function M.run(ctx)
|
||||
local errors = {}
|
||||
local warnings = {}
|
||||
|
||||
-- Per-DIRECTORY (per-module) aggregation. Group sources by `src.dir`,
|
||||
-- validate every source in the dir, then emit ONE errors.h per dir.
|
||||
-- Per-DIRECTORY (per-module) aggregation. Group sources by `src.dir`, validate every source in the dir, then emit ONE errors.h per dir.
|
||||
-- `ctx.by_dir` is pre-computed in build_ctx (shared across all passes).
|
||||
local by_dir = ctx.by_dir or duffle.group_sources_by_dir(ctx.sources)
|
||||
|
||||
|
||||
@@ -170,17 +170,23 @@ end
|
||||
|
||||
--- Compute per-word provenance entries for an atom. Mirrors `compute_word_entries` but additionally classifies each emitted `.word` as either:
|
||||
--- - `RAW` — emitted by a direct instruction token (no component provenance)
|
||||
--- - `MACRO X` — emitted by a `mac_X(...)` component invocation,
|
||||
--- with the component's definition file:line resolved from `ctx.shared.components`.
|
||||
--- - `MACRO X` — emitted by a `mac_X(...)` component invocation, with the component's definition file:line resolved from `ctx.shared.components`.
|
||||
---
|
||||
--- Returns a list of `{pos, line, text, comp_name, comp_line, comp_path}` entries + the total word count.
|
||||
--- `comp_name` is nil for RAW rows.
|
||||
--- Returns a list of `{pos, line, text, comp_name, comp_line, comp_path, body_line}` entries + the total word count.
|
||||
--- `comp_name` is nil for RAW rows. `body_line` is the line of THIS specific word in the macro body (component source file, not the caller's source);
|
||||
--- it differs from `comp_line` (= the macro signature line) for every body word whose macro-body token is on a different physical line.
|
||||
--- `body_line` is `nil` for RAW rows and for component words whose component declaration could not be indexed (older pass combinations / external macros).
|
||||
---
|
||||
--- The per-word body-line lookup mirrors `passes/dwarf_injection.lua :: compute_invocation_body_lines`:
|
||||
--- walk the component's pre-tokenized body in lockstep with `count_token_words` and attribute the source line via `src.scan.line_of(...)` to each emitted `.word`.
|
||||
--- Atom labels (`atom_label(...)`) emit 0 `.word`s and are skipped to stay aligned with the macro-side word-counting contract.
|
||||
--- @param atom table -- one entry of scan.atoms / scan.raw_atoms
|
||||
--- @param src table -- SourceFile (has .scan with .line_of(), .path)
|
||||
--- @param wc table -- shared.word_counts
|
||||
--- @param comp table -- shared.components map: bare_name -> {name=, line=, path=, kind=}
|
||||
--- @param comp_body_index table -- per-source component body index: bare_name -> {body_off, body_tokens, line_of}
|
||||
--- @return table[], integer
|
||||
local function compute_provenance_entries(atom, src, wc, comp)
|
||||
local function compute_provenance_entries(atom, src, wc, comp, comp_body_index)
|
||||
local entries = {}
|
||||
local pos = 0
|
||||
for _, t in ipairs(atom.body_tokens) do
|
||||
@@ -207,10 +213,41 @@ local function compute_provenance_entries(atom, src, wc, comp)
|
||||
comp_kind = comp[bare].kind
|
||||
end
|
||||
|
||||
-- Per-word body lines: lazily allocate from the indexed component body the first time we see a `mac_X(...)` call to a given component.
|
||||
-- We allocate ONE full body_lines vector per (call) and consume it sequentially;
|
||||
-- if a single atom calls the same component more than once, each call refetches its own vector.
|
||||
-- (Today no atom calls the same `mac_X(...)` twice, but the refetch keeps the semantics correct even if that changes.)
|
||||
local body_lines = nil
|
||||
local function fetch_body_lines()
|
||||
if not (bare and comp_body_index) then return nil end
|
||||
local idx = comp_body_index[bare]
|
||||
if not (idx and idx.body_tokens and idx.line_of) then return nil end
|
||||
local lines = {}
|
||||
for _, bt in ipairs(idx.body_tokens) do
|
||||
local bt_tok = duffle.trim(bt.tok or "")
|
||||
if bt_tok ~= "" then
|
||||
local leading = duffle.read_ident(bt_tok, 1)
|
||||
local bt_words
|
||||
if leading == "atom_label" or leading == "atom_offset" then
|
||||
bt_words = 0
|
||||
else
|
||||
bt_words = count_token_words(bt_tok, wc)
|
||||
end
|
||||
if bt_words > 0 then
|
||||
local body_line = idx.line_of(idx.body_off + bt.rel)
|
||||
for _ = 1, bt_words do lines[#lines + 1] = body_line end
|
||||
end
|
||||
end
|
||||
end
|
||||
return lines
|
||||
end
|
||||
|
||||
if words > 0 then
|
||||
local line = src.scan.line_of(atom.body_off + rel)
|
||||
local text = duffle.trim(tok):gsub("[\t\r\n]+", " ")
|
||||
for _ = 1, words do
|
||||
-- Fetch body_lines ONCE per token (one mac_X(...) call exhausts N body words).
|
||||
if comp_name then body_lines = fetch_body_lines() end
|
||||
for i = 1, words do
|
||||
entries[#entries + 1] = {
|
||||
pos = pos,
|
||||
line = line,
|
||||
@@ -219,6 +256,7 @@ local function compute_provenance_entries(atom, src, wc, comp)
|
||||
comp_line = comp_line,
|
||||
comp_path = comp_path,
|
||||
comp_kind = comp_kind,
|
||||
body_line = body_lines and body_lines[i],
|
||||
}
|
||||
pos = pos + 1
|
||||
end
|
||||
@@ -228,26 +266,35 @@ local function compute_provenance_entries(atom, src, wc, comp)
|
||||
end
|
||||
|
||||
--- Render one atom's provenance stanza. Format:
|
||||
--- `WORD N CALL <src-path>:<src-line> MACRO <name> "<def-path>:<def-line>"` (for component words)
|
||||
--- `WORD N CALL <src-path>:<src-line> RAW` (for direct instructions)
|
||||
--- `WORD N CALL <src-path>:<src-line> MACRO <name> "<def-path>:<def-line>" [BODY <line>]` (for component words)
|
||||
--- `WORD N CALL <src-path>:<src-line> RAW` (for direct instructions)
|
||||
--- `BODY <line>` is the source line of THIS specific word within the macro body
|
||||
--- (lottes_tape.h:N where N is the per-word body line).
|
||||
--- Absent for RAW rows and for component rows whose component declaration could not be indexed (older pass combinations / external macros).
|
||||
--- Downstream consumers (dwarf_injection, tests) fall back to DefLine / comp_line when BODY is absent.
|
||||
--- Returns (lines, total_words).
|
||||
--- @param src table
|
||||
--- @param atom table
|
||||
--- @param wc table
|
||||
--- @param comp table -- shared.components map
|
||||
--- @param comp_body_index table -- per-source component body index: bare_name -> {body_off, body_tokens, line_of}
|
||||
--- @return string[], integer
|
||||
local function emit_provenance_stanza(src, atom, wc, comp)
|
||||
local function emit_provenance_stanza(src, atom, wc, comp, comp_body_index)
|
||||
local lines = {}
|
||||
local rel_path = src.path:gsub("\\", "/")
|
||||
local entries, total = compute_provenance_entries(atom, src, wc, comp)
|
||||
local entries, total = compute_provenance_entries(atom, src, wc, comp, comp_body_index)
|
||||
|
||||
-- ATOM header line with placeholder total (patched after we know it).
|
||||
lines[#lines + 1] = string.format('ATOM %s "%s" 0', atom.raw_name or atom.name, rel_path)
|
||||
|
||||
for _, pe in ipairs(entries) do
|
||||
if pe.comp_name then
|
||||
lines[#lines + 1] = string.format('WORD %d CALL %s:%d MACRO %s "%s:%d"',
|
||||
pe.pos, rel_path, pe.line, pe.comp_name, pe.comp_path, pe.comp_line)
|
||||
local body_suffix = ""
|
||||
if pe.body_line then
|
||||
body_suffix = " BODY " .. tostring(pe.body_line)
|
||||
end
|
||||
lines[#lines + 1] = string.format('WORD %d CALL %s:%d MACRO %s "%s:%d"%s',
|
||||
pe.pos, rel_path, pe.line, pe.comp_name, pe.comp_path, pe.comp_line, body_suffix)
|
||||
else
|
||||
lines[#lines + 1] = string.format("WORD %d CALL %s:%d RAW", pe.pos, rel_path, pe.line)
|
||||
end
|
||||
@@ -259,26 +306,38 @@ local function emit_provenance_stanza(src, atom, wc, comp)
|
||||
return lines, total
|
||||
end
|
||||
|
||||
--- Build a per-source component body index keyed by the bare component name (e.g. `gte_load_tri_verts`).
|
||||
--- Each entry holds the data we need to map each emitted `.word` to its actual source line within the macro body:
|
||||
--- body_off -- byte offset of the `{` (start of body) in the component's source file.
|
||||
--- body_tokens -- list of {tok, rel} pairs; `rel` is the byte offset within the body.
|
||||
--- line_of -- closure resolving byte offsets in the component's source file to lines.
|
||||
--- Only `comp_bare` + `comp_proc` declarations contribute (a macro invocation can only resolve to one of those).
|
||||
--- First declaration wins (subsequent redeclarations would collide; today's sources declare each component exactly once).
|
||||
--- Render the full provenance file content for one source (one `.atoms.provenance.txt` per source).
|
||||
--- @param src table
|
||||
--- @param wc table
|
||||
--- @param comp table -- shared.components map
|
||||
--- @param comp_body_index table -- cross-source component body index (built once in M.run; may be empty)
|
||||
--- @return string
|
||||
local function render_provenance(src, wc, comp)
|
||||
local function render_provenance(src, wc, comp, comp_body_index)
|
||||
local lines = {}
|
||||
lines[#lines + 1] = "# FORMAT_VERSION 1"
|
||||
lines[#lines + 1] = "# auto-generated by ps1_meta.lua (passes/atoms_source_map.lua) — DO NOT EDIT"
|
||||
lines[#lines + 1] = "# Per-.word provenance: maps each emitted .word to its call site (atom body"
|
||||
lines[#lines + 1] = "# file:line) and, when the word was emitted by a `mac_X(...)` component invocation,"
|
||||
lines[#lines + 1] = "# the component's definition file:line. Used by dwarf_injection to synthesize"
|
||||
lines[#lines + 1] = "# DW_TAG_inlined_subroutine instances for component step-into."
|
||||
lines[#lines + 1] = "# the component's definition file:line + the per-word BODY line. Used by"
|
||||
lines[#lines + 1] = "# dwarf_injection to synthesize DW_TAG_inlined_subroutine instances + per-word"
|
||||
lines[#lines + 1] = "# line program rows for native source-level step into component bodies."
|
||||
|
||||
-- The cross-source component body index is passed in from M.run (one global lookup shared across every source's provenance file).
|
||||
-- A per-source lookup would miss every component whose declaration is in another source (e.g. `gte_load_tri_verts` is declared in `lottes_tape.h` but invoked from `hello_gte_tape.c`).
|
||||
|
||||
for _, atom in ipairs(src.scan.atoms or {}) do
|
||||
local stanza = emit_provenance_stanza(src, atom, wc, comp)
|
||||
local stanza = emit_provenance_stanza(src, atom, wc, comp, comp_body_index)
|
||||
for _, line in ipairs(stanza) do lines[#lines + 1] = line end
|
||||
end
|
||||
for _, atom in ipairs(src.scan.raw_atoms or {}) do
|
||||
local stanza = emit_provenance_stanza(src, atom, wc, comp)
|
||||
local stanza = emit_provenance_stanza(src, atom, wc, comp, comp_body_index)
|
||||
for _, line in ipairs(stanza) do lines[#lines + 1] = line end
|
||||
end
|
||||
|
||||
@@ -648,9 +707,44 @@ end
|
||||
|
||||
local M = {}
|
||||
|
||||
--- Build the cross-source component body index used by `render_provenance` to attribute each emitted `.word` to its actual line within the macro body.
|
||||
---
|
||||
--- Components are declared in one source (the header that contains `MipsAtomComp_(ac_X)` / `MipsAtomComp_Proc_(ac_X, ...)`)
|
||||
--- but invoked from many source files (every atom body that calls `mac_X(...)`).
|
||||
--- The body_offset + body_tokens + line_of live with the declaration source, so a per-source index would miss invocations from other sources.
|
||||
---
|
||||
--- The cross-source index is keyed by the bare component name (`gte_load_tri_verts`, NOT `ac_gte_load_tri_verts`) — `strip_mac_prefix_from_token` strips the `mac_` prefix
|
||||
--- from call-site identifiers and yields that exact bare name; matching it here keeps the lookup aligned with the `ctx.shared.components` map's keying convention.
|
||||
--- First declaration wins (subsequent redeclarations would collide; today's sources declare each component exactly once).
|
||||
--- @param ctx PassCtx
|
||||
--- @return table<string, table> -- {[comp_name] = {body_off, body_tokens, line_of}}
|
||||
local function build_cross_source_component_body_index(ctx)
|
||||
local index = {}
|
||||
for _, src in ipairs(ctx.sources or {}) do
|
||||
if src.scan and src.scan.atoms then
|
||||
local line_of = src.scan.line_of
|
||||
for _, atom in ipairs(src.scan.atoms) do
|
||||
if atom.kind == "comp_bare" or atom.kind == "comp_proc" then
|
||||
-- Prefer `atom.name` (stripped of `ac_` prefix); fall back to `raw_name`
|
||||
-- only if the stripped name is absent (defensive — current scan-source always sets both).
|
||||
local name = atom.name or atom.raw_name
|
||||
if name and not index[name] then
|
||||
index[name] = {
|
||||
body_off = atom.body_off,
|
||||
body_tokens = atom.body_tokens,
|
||||
line_of = line_of,
|
||||
}
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
return index
|
||||
end
|
||||
|
||||
--- Pass entry: emit one `<out_root>/<basename>.atoms.sourcemap.txt` per source file that contains at least one `MipsAtom_(name)` / `MipsCode code_<name>` declaration.
|
||||
--- Also emits `<out_root>/<basename>.atoms.provenance.txt`:
|
||||
--- per-.word provenance with `mac_X(...)` component resolution back to the component's definition file:line.
|
||||
--- per-.word provenance with `mac_X(...)` component resolution back to the component's definition file:line + the per-word body line.
|
||||
--- Optionally also emit `<ctx.out_root>/gdb_tape_atoms_runtime.gdb` when `ctx.flags.gdb_runtime` is true.
|
||||
--- @param ctx PassCtx
|
||||
--- @return PassResult
|
||||
@@ -674,6 +768,12 @@ function M.run(ctx)
|
||||
-- If absent, all words fall through as RAW (correct behavior — provenance is additive).
|
||||
local comp = (ctx.shared and ctx.shared.components) or {}
|
||||
|
||||
-- Cross-source component body index.
|
||||
-- Built ONCE so every source's provenance writer can resolve `mac_X(...)` invocations back to the macro's body tokens (regardless of which source declared the component).
|
||||
-- Per-source copies were insufficient — the atom file (`hello_gte_tape.c`) does not contain the `MipsAtomComp_(...)` declarations,
|
||||
-- so the body data would be missing for every component invocation the atom file emitted.
|
||||
local comp_body_index = build_cross_source_component_body_index(ctx)
|
||||
|
||||
-- Always emit the canonical text form (per-source).
|
||||
for _, src in ipairs(ctx.sources) do
|
||||
if src.scan then
|
||||
@@ -689,7 +789,7 @@ function M.run(ctx)
|
||||
-- (2) atoms.provenance.txt — per-.word provenance with `mac_X(...)` component resolution back to 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.
|
||||
local prov_path = ctx.out_root .. "/" .. basename .. ".atoms.provenance.txt"
|
||||
local prov_body = render_provenance(src, wc, comp)
|
||||
local prov_body = render_provenance(src, wc, comp, comp_body_index)
|
||||
|
||||
if not ctx.dry_run then
|
||||
duffle.ensure_dir(duffle.dirname(sourcemap_path))
|
||||
|
||||
+585
-427
File diff suppressed because it is too large
Load Diff
+1148
-87
File diff suppressed because it is too large
Load Diff
@@ -1,13 +1,22 @@
|
||||
--- passes/static_analysis.lua — Per-atom static-analysis checks.
|
||||
---
|
||||
--- The 5 checks currently shipped:
|
||||
--- 1. **GTE pipeline-fill** — every `gte_cmdw_*` invocation must be preceded by the minimum number of `nop` words
|
||||
--- (per `duffle.GTE_PIPELINE_LATENCY`) so the COP2 pipeline latency is fully retired before the command issues.
|
||||
--- 2. **mac_yield uniformity** — every atom body must contain exactly one `mac_yield()` call (control transfer pattern).
|
||||
--- 3. **ABI handoff** — every `atom_bind(Binds_X)` must reference a `typedef Struct_(Binds_X) { ... }` declaration.
|
||||
--- 4. **GPU port-store shape** — per-shape (`f3`/`f4`/`g4`/etc.) the sum of `mac_format_X_color` + `mac_gte_store_X_*` +
|
||||
--- `mac_insert_ot_tag_X` words must equal the GP0 cmd's expected packet size.
|
||||
--- 5. **per-atom cycle budget** — sum each atom body's instruction latencies (per `duffle.INSTRUCTION_LATENCY`); report total.
|
||||
--- The 9 checks currently shipped:
|
||||
--- Per-atom rules:
|
||||
--- 1. **GTE pipeline-fill** — every `gte_cmdw_*` invocation must be preceded by the minimum number of `nop` words
|
||||
--- (per `duffle.GTE_PIPELINE_LATENCY`) so the COP2 pipeline latency is fully retired before the command issues.
|
||||
--- 2. **mac_yield uniformity** — every atom body must contain exactly one `mac_yield()` call (control transfer pattern).
|
||||
--- 3. **ABI handoff** — every `atom_bind(Binds_X)` must reference a `typedef Struct_(Binds_X) { ... }` declaration.
|
||||
--- 4. **GPU port-store shape** — per-shape (`f3`/`f4`/`g4`/etc.) the sum of `mac_format_X_color` + `mac_gte_store_X_*` +
|
||||
--- `mac_insert_ot_tag_X` words must equal the GP0 cmd's expected packet size.
|
||||
--- 5. **per-atom cycle budget** — sum each atom body's instruction latencies (per `duffle.INSTRUCTION_LATENCY`); report total.
|
||||
--- Per-source rules (registry-driven, added 2026-07-16):
|
||||
--- 6. **enum_alias_membership** — every `R_X` referenced from `atom_dbg_reg_default`, `atom_reg_types`,
|
||||
--- `atom_rtype(...)`, `atom_reads`, or `atom_writes` must be in `scan.register_alias_registry`. Missing -> warning.
|
||||
--- 7. **atom_rtype_consistency** — every `reg_type_overrides[R_X].type_name` must resolve in `scan.type_name_registry`. Missing -> error.
|
||||
--- 8. **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). Missing -> warning (build continues).
|
||||
--- 9. **reads_writes_alias_membership** — distinct check name duplicating #6's reads/writes coverage so the report can
|
||||
--- attribute failures to a precedence class. Missing -> warning (build continues).
|
||||
---
|
||||
--- The orchestrator (`ps1_meta.lua`) wires this module in via the PASSES table:
|
||||
--- `["static-analysis"] = { module = "passes.static_analysis", kind = "validation", deps = {"word-counts", "components"},
|
||||
@@ -24,7 +33,7 @@
|
||||
-- Bootstrap: load `duffle_paths.lua` via `debug.getinfo(1, "S").source` (works both standalone + when require'd).
|
||||
-- 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 duffle = dofile(_bootstrap_dir .. "../duffle_paths.lua")
|
||||
|
||||
-- ════════════════════════════════════════════════════════════════════════════
|
||||
-- Constants
|
||||
@@ -503,8 +512,8 @@ local function check_gpu_portstore_shape(atom, pipe_ctx, findings)
|
||||
local saw_format = false
|
||||
local saw_prim_write = false
|
||||
|
||||
-- Reads from tc_entry fields pre-computed by classify_tokens (R3 lift). Eliminates 4 per-token
|
||||
-- string matches (mac_format_X_color + mac_gte_store_<shape> + mac_insert_ot_tag_<shape> + R_PrimCursor)
|
||||
-- Reads from tc_entry fields pre-computed by classify_tokens (R3 lift).
|
||||
-- Eliminates 4 per-token string matches (mac_format_X_color + mac_gte_store_<shape> + mac_insert_ot_tag_<shape> + R_PrimCursor)
|
||||
for tok_idx = 1, #tokens do
|
||||
local tc_entry = tc[tok_idx]
|
||||
local shape = tc_entry.mac_format_shape
|
||||
@@ -628,7 +637,7 @@ local function analyze_atom_paths(atom)
|
||||
end
|
||||
end
|
||||
-- For literal-offset branches (label == false), the taken path would jump to a non-tracked address; conservatively omit.
|
||||
-- Return (succ, nil) -- the second value is the terminator marker (nil = not a terminator).
|
||||
-- Return (succ, nil), the second value is the terminator marker (nil = not a terminator).
|
||||
return succ, nil
|
||||
end
|
||||
-- Normal token: just the next one
|
||||
@@ -732,21 +741,273 @@ local function check_per_atom_cycle_budget(atom, pipe_ctx, findings)
|
||||
end
|
||||
end
|
||||
|
||||
-- ════════════════════════════════════════════════════════════════════════════
|
||||
-- Check #6: enum_alias_membership (Track A Task 13)
|
||||
-- ════════════════════════════════════════════════════════════════════════════
|
||||
|
||||
-- Every R_X referenced from a debug-visible surface — atom_dbg_reg_default, atom_reg_types, atom_rtype sub-entries, atom_reads, atom_writes;
|
||||
-- MUST be present in `pipe_ctx.register_alias_registry`.
|
||||
-- The registry is the source-derived answer to "is this R_X a real, opt-in alias?"
|
||||
-- (populated by scan_source's `parse_enum_aliases` from `enum { R_X = N atom_reg }` declarations).
|
||||
-- Per-source rule (called once per source via the CHECK_RULES dispatch).
|
||||
-- Signature matches the per_source shape established by check_semantic_reg_defaults.
|
||||
--
|
||||
-- Severity: WARNING (build continues).
|
||||
-- The rule is intentionally permissive because the production `code/duffle/` and `code/gte_hello/`
|
||||
-- sources use R_* aliases in atom_reads / atom_writes that may not yet be opted in via the
|
||||
-- bare `atom_reg` marker. R_TapePtr / R_AtomJmp / R_PrimCursor / R_FaceCursor / R_VertBase / R_OtBase ARE opted in (lottes_tape.h Task 21).
|
||||
-- Raw C-ABI aliases like R_T0..R_T3 are intentionally NOT auto-included (per the prototype principle:
|
||||
-- no auto-include of wave-context; explicit opt-in only). Warnings keep the build green
|
||||
-- and surface the migration gap so users see which atoms still need opt-in registration.
|
||||
local function check_enum_alias_membership(_src, pipe_ctx, findings)
|
||||
local reg_registry = pipe_ctx.register_alias_registry or {}
|
||||
|
||||
-- (a) atom_dbg_reg_default(R_X, T) -- pipe_ctx.types.
|
||||
-- source_line is on every entry; emit the diagnostic against the default declaration's own line so the report's
|
||||
-- "Findings by atom" section can attribute the failure to the marker location.
|
||||
for reg, def in pairs(pipe_ctx.types or {}) do
|
||||
if not reg_registry[reg] then
|
||||
findings[#findings + 1] = {
|
||||
atom = "", line = def.source_line or 0,
|
||||
check = "enum_alias_membership", kind = "warning",
|
||||
msg = string.format(
|
||||
"atom_dbg_reg_default at line %d references unknown register %q (not in register_alias_registry)",
|
||||
def.source_line or 0, reg),
|
||||
}
|
||||
end
|
||||
end
|
||||
|
||||
-- (b) atom_reg_types(R_X, T) + (c) atom_rtype(R_X, T) sub-entries both populate `ai.reg_type_overrides` (Track A Task 5 merge).
|
||||
-- (d) atom_reads(R_X) + (e) atom_writes(R_X) populate the reads/writes arrays.
|
||||
-- All four are checked against the same registry; the per-rule dispatch iterates `ai` once and covers all three locations
|
||||
-- so we don't re-walk atom_infos for each sub-check.
|
||||
for _, ai in ipairs(pipe_ctx.atom_infos_list or {}) do
|
||||
local info_line = ai.info_line or 0
|
||||
local atom_name = ai.atom_name or ""
|
||||
if ai.reg_type_overrides then
|
||||
for reg in pairs(ai.reg_type_overrides) do
|
||||
if not reg_registry[reg] then
|
||||
findings[#findings + 1] = {
|
||||
atom = atom_name, line = info_line,
|
||||
check = "enum_alias_membership", kind = "warning",
|
||||
msg = string.format(
|
||||
"atom '%s' at line %d has reg_type_overrides for %q; the alias is not in register_alias_registry",
|
||||
atom_name, info_line, reg),
|
||||
}
|
||||
end
|
||||
end
|
||||
end
|
||||
for _, reg in ipairs(ai.reads or {}) do
|
||||
if not reg_registry[reg] then
|
||||
findings[#findings + 1] = {
|
||||
atom = atom_name, line = info_line,
|
||||
check = "enum_alias_membership", kind = "warning",
|
||||
msg = string.format(
|
||||
"atom '%s' at line %d has atom_reads for %q; the alias is not in register_alias_registry",
|
||||
atom_name, info_line, reg),
|
||||
}
|
||||
end
|
||||
end
|
||||
for _, reg in ipairs(ai.writes or {}) do
|
||||
if not reg_registry[reg] then
|
||||
findings[#findings + 1] = {
|
||||
atom = atom_name, line = info_line,
|
||||
check = "enum_alias_membership", kind = "warning",
|
||||
msg = string.format(
|
||||
"atom '%s' at line %d has atom_writes for %q; the alias is not in register_alias_registry",
|
||||
atom_name, info_line, reg),
|
||||
}
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
-- ════════════════════════════════════════════════════════════════════════════
|
||||
-- Check #7: atom_type_consistency (Track A Task 13; legacy alias atom_rtype_consistency)
|
||||
-- ════════════════════════════════════════════════════════════════════════════
|
||||
|
||||
-- Every `reg_type_overrides[R_X].type_name` (populated by BOTH `atom_reg_types(R_X, <type>)`
|
||||
-- and `atom_type(R_X, <type>)` sub-entries inside atom_reads/atom_writes — legacy `atom_rtype` is
|
||||
-- accepted as a transparent alias) MUST resolve to a `type_name_registry` entry.
|
||||
-- The registry is the source-derived answer to "is this type name declared in this translation unit?"
|
||||
-- (populated by `typedef Struct_(...)`, `typedef Enum_(...)`, `typedef ... TSet_(...)` declarations).
|
||||
-- Missing type names are errors (the build stops) so the user adds the typedef before re-running.
|
||||
-- Per-source rule.
|
||||
local function check_atom_rtype_consistency(_src, pipe_ctx, findings)
|
||||
local type_registry = pipe_ctx.type_name_registry or {}
|
||||
for _, ai in ipairs(pipe_ctx.atom_infos_list or {}) do
|
||||
local info_line = ai.info_line or 0
|
||||
local atom_name = ai.atom_name or ""
|
||||
if ai.reg_type_overrides then
|
||||
for reg, ov in pairs(ai.reg_type_overrides) do
|
||||
if not ov.type_name or not type_registry[ov.type_name] then
|
||||
findings[#findings + 1] = {
|
||||
atom = atom_name, line = info_line,
|
||||
check = "atom_rtype_consistency", kind = "error",
|
||||
msg = string.format(
|
||||
"atom '%s' at line %d reg_type_overrides[%q] uses unknown type %q (not in type_name_registry)",
|
||||
atom_name, info_line, reg, tostring(ov.type_name)),
|
||||
}
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
-- ════════════════════════════════════════════════════════════════════════════
|
||||
-- Check #8: binds_no_substruct_deref (Track A Task 13)
|
||||
-- ════════════════════════════════════════════════════════════════════════════
|
||||
|
||||
-- For every `load_word(R_A, R_B, O_(<Type>, <Field>))` and matching `store_word(...)` call in every atom body,
|
||||
-- the `<Field>` MUST resolve to a leaf scalar of `<Type>`. A "leaf scalar" is:
|
||||
-- * a non-struct field with `pointer_depth >= 1` (pointer-to-struct IS a leaf — the field is a pointer; the pointee is unrelated), OR
|
||||
-- * a non-struct field whose type_name resolves to a typedef / enum / builtin in `type_name_registry`.
|
||||
-- A nested struct member (pointer_depth == 0 AND type_name resolves to a `kind = "struct"` registry entry) is NOT a leaf scalar and is flagged.
|
||||
-- The check also flags fields whose Type has no `fields` table (typedefs and enums don't have fields — any Field reference against them is bogus)
|
||||
-- and fields whose name doesn't appear in the resolved Type's fields array.
|
||||
--
|
||||
-- Walks every atom's pre-computed `paths.tok_class`
|
||||
-- (set by `classify_tokens` once per atom in validate()) and uses the `o_arg1` / `o_arg2` captures instead of re-matching the token string.
|
||||
-- Resolution consults `pipe_ctx.type_name_registry`
|
||||
-- (Binds_* structs are registered there by scan_source's `register_struct_type`, so a unified lookup works for both Binds_* and non-Binds structs).
|
||||
--
|
||||
-- Severity: warning (build continues) — this catches a category of bugs
|
||||
-- (passing a struct by value through the tape payload) where the symptom is runtime corruption, not a compile error.
|
||||
-- Look up a field by name in a type's `fields` array. Returns the matching field entry, or nil if not found.
|
||||
-- Extracted to keep check_binds_no_substruct_deref's nesting depth <= 5 (the project convention; this is the 5th nesting level:
|
||||
-- function -> for-atom -> for-token -> if-load/store -> if-type-resolves -> [helper]).
|
||||
local function find_field_by_name(type_entry, field_name)
|
||||
for _, f in ipairs(type_entry.fields or {}) do
|
||||
if f.name == field_name then return f end
|
||||
end
|
||||
return nil
|
||||
end
|
||||
|
||||
-- True iff a (field, type_registry) pair is a leaf scalar (safe to dereference as a tape-payload field).
|
||||
-- Pointer-to-X is always leaf; non-pointer struct members are NOT leaf.
|
||||
local function is_field_leaf(field, type_registry)
|
||||
if field.pointer_depth and field.pointer_depth > 0 then
|
||||
return true
|
||||
end
|
||||
local ftype_entry = type_registry[field.type_name]
|
||||
if ftype_entry and ftype_entry.kind == "struct" then
|
||||
return false
|
||||
end
|
||||
return true
|
||||
end
|
||||
|
||||
local function check_binds_no_substruct_deref(_src, pipe_ctx, findings)
|
||||
local type_registry = pipe_ctx.type_name_registry or {}
|
||||
for _, a in ipairs(pipe_ctx.atoms or {}) do
|
||||
local tc = a.paths and a.paths.tok_class or {}
|
||||
local tokens = a.paths and a.paths.tokens or {}
|
||||
local line_in_body = a.paths and a.paths.line_in_body or {}
|
||||
for ti = 1, #tokens do
|
||||
local tc_entry = tc[ti]
|
||||
if (tc_entry.is_load_word or tc_entry.is_store_word)
|
||||
and tc_entry.o_arg1 and tc_entry.o_arg2 then
|
||||
local type_name = tc_entry.o_arg1
|
||||
local field_name = tc_entry.o_arg2
|
||||
local body_line = a.line + (line_in_body[tokens[ti].rel] or 0)
|
||||
|
||||
local type_entry = type_registry[type_name]
|
||||
if not type_entry or not type_entry.fields then
|
||||
findings[#findings + 1] = {
|
||||
atom = a.name, line = body_line,
|
||||
check = "binds_no_substruct_deref", kind = "warning",
|
||||
msg = string.format(
|
||||
"atom '%s' at line %d O_(%s, %s) refers to type %q which has no fields table in type_name_registry",
|
||||
a.name, body_line, type_name, field_name, type_name),
|
||||
}
|
||||
else
|
||||
local field = find_field_by_name(type_entry, field_name)
|
||||
if not field then
|
||||
findings[#findings + 1] = {
|
||||
atom = a.name, line = body_line,
|
||||
check = "binds_no_substruct_deref", kind = "warning",
|
||||
msg = string.format(
|
||||
"atom '%s' at line %d O_(%s, %s) does not resolve to a field of %s",
|
||||
a.name, body_line, type_name, field_name, type_name),
|
||||
}
|
||||
elseif not is_field_leaf(field, type_registry) then
|
||||
findings[#findings + 1] = {
|
||||
atom = a.name, line = body_line,
|
||||
check = "binds_no_substruct_deref", kind = "warning",
|
||||
msg = string.format(
|
||||
"atom '%s' at line %d O_(%s, %s) dereferences a non-pointer struct field of type %q; nested struct members are forbidden",
|
||||
a.name, body_line, type_name, field_name, field.type_name),
|
||||
}
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
-- ════════════════════════════════════════════════════════════════════════════
|
||||
-- Check #9: reads_writes_alias_membership (Track A Task 13)
|
||||
-- ════════════════════════════════════════════════════════════════════════════
|
||||
|
||||
-- For every `atom_reads(R_X)` and `atom_writes(R_X)` entry in every `atom_infos` entry, the `R_X` MUST be present in `pipe_ctx.register_alias_registry`.
|
||||
-- This DUPLICATES `enum_alias_membership`'s coverage of the reads/writes arrays;
|
||||
-- the distinct check name is intentional so the report can attribute the failure to a precedence-class (warnings vs errors) — the production reads/writes
|
||||
-- paths are intentionally permissive at the warning level even when the registry-driven check is strict at the error level.
|
||||
-- Per-source rule. Severity: warning (build continues).
|
||||
local function check_reads_writes_alias_membership(_src, pipe_ctx, findings)
|
||||
local reg_registry = pipe_ctx.register_alias_registry or {}
|
||||
for _, ai in ipairs(pipe_ctx.atom_infos_list or {}) do
|
||||
local info_line = ai.info_line or 0
|
||||
local atom_name = ai.atom_name or ""
|
||||
for _, reg in ipairs(ai.reads or {}) do
|
||||
if not reg_registry[reg] then
|
||||
findings[#findings + 1] = {
|
||||
atom = atom_name, line = info_line,
|
||||
check = "reads_writes_alias_membership", kind = "warning",
|
||||
msg = string.format(
|
||||
"atom '%s' at line %d atom_reads for %q; the alias is not in register_alias_registry",
|
||||
atom_name, info_line, reg),
|
||||
}
|
||||
end
|
||||
end
|
||||
for _, reg in ipairs(ai.writes or {}) do
|
||||
if not reg_registry[reg] then
|
||||
findings[#findings + 1] = {
|
||||
atom = atom_name, line = info_line,
|
||||
check = "reads_writes_alias_membership", kind = "warning",
|
||||
msg = string.format(
|
||||
"atom '%s' at line %d atom_writes for %q; the alias is not in register_alias_registry",
|
||||
atom_name, info_line, reg),
|
||||
}
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
-- ════════════════════════════════════════════════════════════════════════════
|
||||
-- CHECK_RULES — data-driven check dispatch (Muratori: data over control flow)
|
||||
-- ════════════════════════════════════════════════════════════════════════════
|
||||
|
||||
-- Each rule is a table entry: { name, per_atom }.
|
||||
-- `per_atom(atom, pipe_ctx, findings)` runs once per atom inside validate()'s single loop.
|
||||
-- Adding a new check = 1 row here + 1 check_* function. No validate() edit required.
|
||||
-- Each rule is a table entry: { name, <dispatch> }.
|
||||
-- Dispatch shapes:
|
||||
-- per_atom(atom, pipe_ctx, findings) — runs once per atom inside validate()'s single loop
|
||||
-- post(pipe_ctx, findings) — runs once after all per-atom calls complete
|
||||
-- per_macro(macro, wc, findings) — runs once per TAPE_WORDS / _Pragma macro declaration
|
||||
-- per_skip_marker(marker, pipe_ctx, findings) — runs once per src.scan.skip_over.markers entry
|
||||
-- per_source(src, pipe_ctx, findings) — runs once per source AFTER the per-atom loop completes
|
||||
-- (added for the registry-driven rule set; same CHECK_RULES table — no parallel dispatch)
|
||||
-- Adding a new check = 1 row here + 1 check_* function. validate() is updated only to invoke the per_source dispatch loop (the per_atom dispatch loop never changes).
|
||||
-- This is the plex pattern: the iteration is in ONE place (validate), the variation is in DATA (this table).
|
||||
|
||||
local CHECK_RULES = {
|
||||
{ name = "gte_pipeline_fill", per_atom = check_gte_pipeline_fill },
|
||||
{ name = "mac_yield_uniformity", per_atom = check_mac_yield_uniformity },
|
||||
{ name = "abi_handoff", per_atom = check_abi_handoff },
|
||||
{ name = "gpu_portstore_shape", per_atom = check_gpu_portstore_shape },
|
||||
{ name = "per_atom_cycle_budget", per_atom = check_per_atom_cycle_budget },
|
||||
{ name = "gte_pipeline_fill", per_atom = check_gte_pipeline_fill },
|
||||
{ name = "mac_yield_uniformity", per_atom = check_mac_yield_uniformity },
|
||||
{ name = "abi_handoff", per_atom = check_abi_handoff },
|
||||
{ name = "gpu_portstore_shape", per_atom = check_gpu_portstore_shape },
|
||||
{ name = "per_atom_cycle_budget", per_atom = check_per_atom_cycle_budget },
|
||||
{ name = "enum_alias_membership", per_source = check_enum_alias_membership },
|
||||
{ name = "atom_rtype_consistency", per_source = check_atom_rtype_consistency },
|
||||
{ name = "binds_no_substruct_deref", per_source = check_binds_no_substruct_deref },
|
||||
{ name = "reads_writes_alias_membership",per_source = check_reads_writes_alias_membership},
|
||||
}
|
||||
|
||||
-- ════════════════════════════════════════════════════════════════════════════
|
||||
@@ -770,17 +1031,28 @@ local function validate(ctx, src)
|
||||
-- pipe_ctx: the cross-atom shared state for the per-atom pipeline (Fleury "expose structure").
|
||||
-- Pre-allocated here, mutated by each per-atom check call below.
|
||||
-- Replaces the per-check local tables that used to live inside each check_* function body.
|
||||
-- info_by_atom — atom_name -> atom_info (built once; check_abi_handoff reads it)
|
||||
-- binds_index — Binds_X -> binds struct (built once; check_abi_handoff reads it)
|
||||
-- unknown_seen — macro_name -> first atom line (accumulated across atoms; check_per_atom_cycle_budget dedups)
|
||||
-- info_by_atom — atom_name -> atom_info (built once; check_abi_handoff reads it)
|
||||
-- binds_index — Binds_X -> binds struct (built once; check_abi_handoff reads it)
|
||||
-- unknown_seen — macro_name -> first atom line (accumulated across atoms; check_per_atom_cycle_budget dedups)
|
||||
-- atoms — full atom list (used by check_binds_no_substruct_deref's per-source body walk)
|
||||
-- types — R_X -> default-type info from atom_dbg_reg_default (check_enum_alias_membership source a)
|
||||
-- atom_infos_list — flat list of atom_info entries (checks #6/#7/#9 iterate it)
|
||||
-- register_alias_registry — R_X -> {name, code, has_atom_reg, source_line} from parse_enum_aliases
|
||||
-- type_name_registry — T -> {name, kind, fields, ...} from parse_typedef_binds
|
||||
-- All registry fields are READ from src.scan (the dep-closed scan-source payload); this pass never re-parses.
|
||||
local info_by_atom = {}
|
||||
for _, info in ipairs(atom_infos) do
|
||||
info_by_atom[info.atom_name] = info
|
||||
end
|
||||
local pipe_ctx = {
|
||||
info_by_atom = info_by_atom,
|
||||
binds_index = binds_index,
|
||||
unknown_seen = {},
|
||||
info_by_atom = info_by_atom,
|
||||
binds_index = binds_index,
|
||||
unknown_seen = {},
|
||||
atoms = atoms,
|
||||
types = scan.types or {},
|
||||
atom_infos_list = atom_infos or {},
|
||||
register_alias_registry = scan.register_alias_registry or {},
|
||||
type_name_registry = scan.type_name_registry or {},
|
||||
}
|
||||
|
||||
-- THE per-atom pipeline. ONE iteration of atoms; the 5 check_* functions + analyze_atom_paths
|
||||
@@ -788,6 +1060,7 @@ local function validate(ctx, src)
|
||||
-- Plex move: every piece of state derived from an atom body lives on `atom.paths` (the per-atom mega-struct);
|
||||
-- readers (analyze_atom_paths, the 5 checks, the renderers) all consume `atom.paths`, not the raw `atoms` list.
|
||||
-- Stage 1B: each check_* now takes `(atom, ...)` instead of `(atoms, findings)` — no more single-atom `{a}` shim.
|
||||
-- Per-source rules run once after this loop completes (no parallel dispatch table).
|
||||
local findings = {}
|
||||
for _, a in ipairs(atoms) do
|
||||
a.paths = a.paths or {}
|
||||
@@ -798,13 +1071,20 @@ local function validate(ctx, src)
|
||||
-- analyze_atom_paths fills the *cycles / branches / has_loops / unknown_macros* fields of a.paths.
|
||||
analyze_atom_paths(a)
|
||||
|
||||
-- Run all checks on this one atom via the CHECK_RULES data table (Muratori: data over control flow).
|
||||
-- Run all per-atom checks on this one atom via the CHECK_RULES data table (Muratori: data over control flow).
|
||||
-- Adding a new check = 1 row in CHECK_RULES; this loop never needs editing.
|
||||
for _, rule in ipairs(CHECK_RULES) do
|
||||
rule.per_atom(a, pipe_ctx, findings)
|
||||
if rule.per_atom then rule.per_atom(a, pipe_ctx, findings) end
|
||||
end
|
||||
end
|
||||
|
||||
-- Per-source dispatch. Run once per source AFTER the per-atom loop;
|
||||
-- consults pipe_ctx's cross-atom registries (register_alias_registry, type_name_registry).
|
||||
-- Same CHECK_RULES table; no parallel dispatch table.
|
||||
for _, rule in ipairs(CHECK_RULES) do
|
||||
if rule.per_source then rule.per_source(src, pipe_ctx, findings) end
|
||||
end
|
||||
|
||||
local errors = {}
|
||||
local warnings = {}
|
||||
local info = {}
|
||||
|
||||
Reference in New Issue
Block a user