mirror of
https://github.com/Ed94/pikuma_ps1.git
synced 2026-08-05 15:18:49 +00:00
Proof-reading lua metaprogram (part 1)
This commit is contained in:
+131
-178
@@ -1,18 +1,12 @@
|
||||
--- duffle.lua — shared primitives + domain tables for the tape-atom metaprograms.
|
||||
---
|
||||
--- One ownership statement, then the rest is signal:
|
||||
--- * **Character classification** (`is_space`, `is_alpha`, `is_alnum`, `is_digit`, plus the byte-fast `_byte` variants).
|
||||
--- * **String / path primitives** (`trim`, `dirname`, `basename_no_ext`, `normalize_path`, `canonical_path_key`, `find_byte`).
|
||||
--- * **I/O primitives** (`read_file`, `write_file`, `ensure_dir`).
|
||||
--- * **Corpus resolution** (`parse_direct_quoted_includes`, `resolve_source_corpus`).
|
||||
--- * **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** (`TAPE_ATOM_MACROS`, `GTE_PIPELINE_LATENCY`, `GP0_CMD_SIZE`, `GP0_CMD_BY_SHAPE`,
|
||||
--- `INSTRUCTION_LATENCY`).
|
||||
---
|
||||
--- **Conventions**: tabs (1/level), EmmyLua annotations, no regex.
|
||||
--- * Character classification: `is_space`, `is_alpha`, `is_alnum`, `is_digit`, plus the byte-fast `_byte` variants.
|
||||
--- * String / path primitives: `trim`, `dirname`, `basename_no_ext`, `normalize_path`, `canonical_path_key`, `find_byte`.
|
||||
--- * I/O primitives: `read_file`, `write_file`, `ensure_dir`.
|
||||
--- * Corpus resolution: `parse_direct_quoted_includes`, `resolve_source_corpus`.
|
||||
--- * 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: `TAPE_ATOM_MACROS`, `GTE_PIPELINE_LATENCY`, `GP0_CMD_SIZE`, `GP0_CMD_BY_SHAPE`, `INSTRUCTION_LATENCY`.
|
||||
|
||||
local M = {}
|
||||
|
||||
@@ -24,7 +18,7 @@ local lfs = require("lfs")
|
||||
-- Cross-file type aliases
|
||||
-- ════════════════════════════════════════════════════════════════════════════
|
||||
|
||||
--- @alias Path string -- absolute or CWD-relative file path
|
||||
--- @alias Path string -- Absolute or CWD-relative file path
|
||||
--- @alias LineNum integer -- 1-indexed source line number
|
||||
--- @alias ByteOff integer -- 0-indexed byte offset within a source string
|
||||
--- @alias MacroName string -- lower_snake_case macro identifier (e.g. "mac_yield")
|
||||
@@ -32,10 +26,10 @@ local lfs = require("lfs")
|
||||
--- @alias Severity string -- "error" | "warning" | "info"
|
||||
|
||||
--- @class SourceFile
|
||||
--- @field path Path -- absolute path to the source file
|
||||
--- @field text string -- the full source text
|
||||
--- @field dir string -- the directory containing the source
|
||||
--- @field basename string -- filename without extension
|
||||
--- @field path Path -- Absolute path to the source file
|
||||
--- @field text string -- Full source text
|
||||
--- @field dir string -- Directory containing the source
|
||||
--- @field basename string -- Filename without extension
|
||||
|
||||
-- ════════════════════════════════════════════════════════════════════════════
|
||||
-- ASCII byte constants
|
||||
@@ -73,20 +67,12 @@ local BYTE_DIGIT_9 = 0x39 -- '9'
|
||||
-- ════════════════════════════════════════════════════════════════════════════
|
||||
-- Section -1: Bootstrap (path-setup at module load)
|
||||
-- ════════════════════════════════════════════════════════════════════════════
|
||||
--
|
||||
-- Path setup runs through `scripts/duffle_paths.lua`, which derives the repo root from `debug.getinfo(1, "S").source`
|
||||
-- (no subprocess, ~0ms) and then calls `require("duffle")`.
|
||||
-- Entry and pass scripts load `duffle_paths.lua` first; a `find_repo_root` / `setup_package_path` defined here was dead code in practice.
|
||||
-- `git rev-parse` costs ~100-180ms per subprocess spawn on Windows; `debug.getinfo` is <1ms, so we keep only the fast path.
|
||||
--
|
||||
-- To load `duffle.lua` outside `duffle_paths.lua`, set `package.path` manually before `require`.
|
||||
-- See `docs/guide_metaprogram_ssdl.md` §"I/O primitives" for the pattern.
|
||||
|
||||
-- ════════════════════════════════════════════════════════════════════════════
|
||||
-- Section 0: LPeg patterns (compiled once at module load)
|
||||
-- ════════════════════════════════════════════════════════════════════════════
|
||||
--
|
||||
-- LPeg is a required dependency (PEG library, no regex). It's loaded via `package.cpath` — `duffle_paths.lua` wires the path to `toolchain/lpeg/lpeg.dll`.
|
||||
-- LPeg is a required dependency (PEG library). It's loaded via `package.cpath` — `duffle_paths.lua` wires the path to `toolchain/lpeg/lpeg.dll`.
|
||||
-- LPeg handles the high-level scanner; the byte-by-byte helpers in Section 1 handle classification primitives that LPeg's CPython-level cost would dominate.
|
||||
--
|
||||
-- If the require fails, fail loud with an actionable message. The build script (`update_deps.ps1`) builds lpeg.dll into `toolchain/lpeg/`; run it when the dll is missing.
|
||||
@@ -100,32 +86,26 @@ end
|
||||
local P, S, R = lpeg.P, lpeg.S, lpeg.R
|
||||
|
||||
-- Character class patterns
|
||||
local alpha_pat = R("AZ", "az") + P("_")
|
||||
local digit_pat = R("09")
|
||||
local lpeg_alnum_pat = alpha_pat + digit_pat
|
||||
local alpha_pat = R("AZ", "az") + P("_")
|
||||
local digit_pat = R("09")
|
||||
local lpeg_alnum_pat = alpha_pat + digit_pat
|
||||
|
||||
-- Identifier: alpha followed by zero+ alnum. Capture as a string.
|
||||
local lpeg_alpha_pat = alpha_pat
|
||||
local lpeg_ident_pat = lpeg.C(alpha_pat * lpeg_alnum_pat^0)
|
||||
|
||||
-- String literal: "..." with backslash escapes.
|
||||
local lpeg_str_pat = P('"') * (P(1) - S('"\\') + P('\\') * P(1))^0 * P('"')
|
||||
-- Char literal: '...' with backslash escapes.
|
||||
local lpeg_chr_pat = P("'") * (P(1) - S("'\\") + P('\\') * P(1))^0 * P("'")
|
||||
-- Line comment: // ... to end-of-line.
|
||||
local lpeg_line_cmt_pat = P("//") * (P(1) - S("\n"))^0
|
||||
-- Block comment: /* ... */ (no nesting per C standard).
|
||||
local lpeg_block_cmt_pat = P("/*") * (P(1) - P("*/"))^0 * P("*/")
|
||||
-- String or comment (any of the four forms).
|
||||
local lpeg_str_or_cmt_pat = lpeg_str_pat + lpeg_chr_pat + lpeg_line_cmt_pat + lpeg_block_cmt_pat
|
||||
local lpeg_str_pat = P('"') * (P(1) - S('"\\') + P('\\') * P(1))^0 * P('"') -- String literal: "..." with backslash escapes.
|
||||
local lpeg_chr_pat = P("'") * (P(1) - S("'\\") + P('\\') * P(1))^0 * P("'") -- Char literal: '...' with backslash escapes.
|
||||
local lpeg_line_cmt_pat = P("//") * (P(1) - S("\n"))^0 -- Line comment: // ... to end-of-line.
|
||||
local lpeg_block_cmt_pat = P("/*") * (P(1) - P("*/"))^0 * P("*/") -- Block comment: /* ... */ (no nesting per C standard).
|
||||
local lpeg_str_or_cmt_pat = lpeg_str_pat + lpeg_chr_pat + lpeg_line_cmt_pat + lpeg_block_cmt_pat -- String or comment (any of the four forms).
|
||||
|
||||
-- Whitespace + comment skipper: zero+ (whitespace run | string | comment).
|
||||
local ws_pat = S(" \t\n\r\v\f")
|
||||
local lpeg_ws_and_cmt_pat = (ws_pat + lpeg_str_or_cmt_pat)^0
|
||||
|
||||
-- Generic "skip until target, but step over balanced groups" matcher.
|
||||
-- Used by scan_to_char for non-ident / non-bracket chars.
|
||||
-- We accept any single char except the target.
|
||||
-- Used by scan_to_char for non-ident / non-bracket chars. We accept any single char except the target.
|
||||
-- The balanced-group stepping is handled by the caller (via read_balanced).
|
||||
local lpeg_scan_to_target_pat = function(target) return (P(1) - P(target))^0 end
|
||||
|
||||
@@ -148,12 +128,10 @@ end
|
||||
|
||||
-- Single digit.
|
||||
function M.is_digit_byte(b) return b and b >= BYTE_DIGIT_0 and b <= BYTE_DIGIT_9 end
|
||||
|
||||
-- Letter OR digit OR underscore.
|
||||
function M.is_alnum_byte(b) return M.is_alpha_byte(b) or M.is_digit_byte(b) end
|
||||
|
||||
-- String-based wrappers (kept for callers that already have a single-char string;
|
||||
-- the byte versions are what the hot loops should call).
|
||||
-- String-based wrappers (kept for callers that already have a single-char string; the byte versions are what the hot loops should call).
|
||||
function M.is_space(c)
|
||||
if type(c) == "number" then return M.is_space_byte(c) end
|
||||
return c == " " or c == "\t" or c == "\n" or c == "\r" or c == "\v" or c == "\f"
|
||||
@@ -184,8 +162,8 @@ end
|
||||
|
||||
--- Linear-search for a single-byte target in a string.
|
||||
--- @param haystack string
|
||||
--- @param target integer -- byte value
|
||||
--- @param start integer -- optional 1-indexed start (default 1)
|
||||
--- @param target integer -- byte value
|
||||
--- @param start integer -- optional 1-indexed start (default 1)
|
||||
--- @return integer|nil
|
||||
function M.find_byte(haystack, target, start)
|
||||
for pos = start or 1, #haystack do
|
||||
@@ -309,7 +287,7 @@ local function absolute_normalized_path(path)
|
||||
end
|
||||
|
||||
--- Return the normalized absolute, Windows-case-folded comparison key for a path.
|
||||
--- -Ordinary relative paths resolve against the process cwd. Drive-relative paths are rejected because LuaFileSystem does not expose Windows per-drive current directories.
|
||||
--- Ordinary relative paths resolve against the process cwd. Drive-relative paths are rejected because LuaFileSystem does not expose Windows per-drive current directories.
|
||||
--- @param path Path
|
||||
--- @return string
|
||||
function M.canonical_path_key(path)
|
||||
@@ -603,7 +581,7 @@ function M.parse_direct_quoted_includes(source_text)
|
||||
pos = after
|
||||
end
|
||||
elseif byte == BYTE_DQUOTE or byte == BYTE_SQUOTE then
|
||||
-- enter+leave the string literal in one skip; literal bodies cannot contain a directive regardless of what they look like.
|
||||
-- enter + leave the string literal in one skip; literal bodies cannot contain a directive regardless of what they look like.
|
||||
line_leading = false
|
||||
local after = M.skip_str_or_cmt(logical_text, pos)
|
||||
pos = (after > pos) and after or (pos + 1)
|
||||
@@ -799,18 +777,17 @@ function M.resolve_source_corpus(options)
|
||||
end
|
||||
|
||||
return {
|
||||
unity_root = root.path,
|
||||
project_root = project_root,
|
||||
code_root = code_root,
|
||||
source_order = source_order,
|
||||
unity_root = root.path,
|
||||
project_root = project_root,
|
||||
code_root = code_root,
|
||||
source_order = source_order,
|
||||
sources_by_path = sources_by_path,
|
||||
sources_by_dir = M.group_sources_by_dir(source_order),
|
||||
resolver = resolver,
|
||||
sources_by_dir = M.group_sources_by_dir(source_order),
|
||||
resolver = resolver,
|
||||
}
|
||||
end
|
||||
|
||||
-- Split a brace-body into top-level comma-separated tokens. Honors nested parens/braces/brackets and skips strings/comments.
|
||||
--
|
||||
-- Splits at top-level NEWLINES and SEMICOLONS too, AND emits a token break after a top-level comment/string.
|
||||
-- Pure-comment / pure-string chunks contribute 0 words.
|
||||
function M.split_top_level_commas(body)
|
||||
@@ -846,7 +823,7 @@ function M.split_top_level_commas(body)
|
||||
if has_real_content(chunk) then
|
||||
tokens[#tokens + 1] = chunk
|
||||
elseif #tokens > 0 then
|
||||
-- Pure comment/string chunk at top level.
|
||||
-- comment/string chunk at top level.
|
||||
-- Append it to the LAST token so emit-context callers (components.lua build_component_lines) can convert
|
||||
-- `// trailing comment` to `/* */` and emit it with the macro body.
|
||||
-- count_token_words only inspects the leading ident, so a trailing comment does not affect the count.
|
||||
@@ -920,8 +897,7 @@ function M.tokenize_body(body)
|
||||
while scan <= len do
|
||||
local c = body:byte(scan)
|
||||
-- Terminator bytes (delimit a token at the top level): ',' = 0x2C, '\n' = 0x0A, ';' = 0x3B.
|
||||
-- These also appear as separators between argument lists inside the parens/braces/brackets,
|
||||
-- so we stop the scan when we hit any of them.
|
||||
-- These also appear as separators between argument lists inside the parens/braces/brackets, so we stop the scan when we hit any of them.
|
||||
if c == BYTE_COMMA then break end
|
||||
if c == BYTE_NEWLINE then break end
|
||||
if c == BYTE_SEMI then break end
|
||||
@@ -966,7 +942,7 @@ function M.build_body_line_index(body)
|
||||
local index = {}
|
||||
local len = #body
|
||||
local newline_count = 0
|
||||
for pos = 1, len do
|
||||
for pos = 1, len do
|
||||
if pos > 1 then
|
||||
index[pos] = newline_count + 1
|
||||
end
|
||||
@@ -1088,18 +1064,18 @@ M.GTE_COMMAND_ALIASES = {
|
||||
-- * "Store delays are counted in numbers of clock cycles (not in numbers of opcodes).
|
||||
-- For 3 cycle delay, one must usually insert 3 cached opcodes (or one uncached opcode)."
|
||||
--
|
||||
-- Per PSX-SPX `docs/psx-spx/docs/gtepipelinetimings.md` (the per-instruction input-latch measurement, which is the same
|
||||
-- phenomenon modeled from the command side), the values are:
|
||||
-- rtps: every data register, every control register (RT/TR/OFX/OFY/H/DQA/DQB)
|
||||
-- rtpt: same superset (rtpt reads V0..V2, the RT matrix, the TR vector, OFX/OFY, H, DQA, DQB)
|
||||
-- nclip: SXY0, SXY1, SXY2 (no RT/TR/OFX inputs)
|
||||
-- Per PSX-SPX `docs/psx-spx/docs/gtepipelinetimings.md` (the per-instruction input-latch measurement, which is the same phenomenon modeled from the command side),
|
||||
-- the values are:
|
||||
-- rtps: every data register, every control register (RT / TR / OFX / OFY / H / DQA / DQB)
|
||||
-- rtpt: same superset (rtpt reads V0..V2, the RT matrix, the TR vector, OFX / OFY, H, DQA, DQB)
|
||||
-- nclip: SXY0, SXY1, SXY2 (no RT / TR / OFX inputs)
|
||||
-- mvmva: variable (depends on the chosen mx / v / cv selector); treated conservatively as the union of all RT + TR + BK + IR columns
|
||||
-- (the data inputs the command can read).
|
||||
-- op: IR1, IR2, IR3 (cross-product output, atomic; consumers treat as fan-out only)
|
||||
-- avsz3/avsz4: SZ0..SZ3 + ZSF3/ZSF4
|
||||
-- avsz3 / avsz4: SZ0..SZ3 + ZSF3/ZSF4
|
||||
--
|
||||
-- We model the data-register + control-register superset. Every relevant input is in this set per PSX-SPX `gtepipelinetimings.md`;
|
||||
-- the per-input latching values there describe the same number's command-side view
|
||||
-- The per-input latching values there describe the same number's command-side view
|
||||
-- (a recent mtc2/ctc2 to that register must retire the same number of cycles before the command issues).
|
||||
-- Anything outside this set is safe to clobber immediately after a prior command.
|
||||
M.GTE_COMMAND_INPUTS = {
|
||||
@@ -1133,7 +1109,7 @@ M.GTE_COMMAND_INPUTS = {
|
||||
"gte_cr_OFX", "gte_cr_OFY", "gte_cr_H",
|
||||
"gte_cr_DQA", "gte_cr_DQB",
|
||||
},
|
||||
-- NCLIP: reads SXY0/SXY1/SXY2 only (per PSX-SPX gtepipelinetimings.md §12.6).
|
||||
-- NCLIP: reads SXY0 / SXY1 / SXY2 only (per PSX-SPX gtepipelinetimings.md §12.6).
|
||||
["gte_cmdw_nclip"] = {
|
||||
"C2_SXY0", "C2_SXY1", "C2_SXY2",
|
||||
},
|
||||
@@ -1163,7 +1139,6 @@ M.GTE_COMMAND_INPUTS = {
|
||||
}
|
||||
|
||||
-- GTE command output-set + semantic role table.
|
||||
--
|
||||
-- For each command, the set of C2 data registers the command writes as outputs, paired with the SEMANTIC ROLE of each output.
|
||||
-- The semantic role is the basis for the `_post_<cmd>` contract validation.
|
||||
-- The contract says "after <cmd>, the latest screen-XY is C2_SXY2" (C2_SXY0 is wrong; the FIFO side effects leave SXY0 as an older FIFO entry, never the newest).
|
||||
@@ -1220,14 +1195,14 @@ M.GTE_COMMAND_OUTPUTS = {
|
||||
["gte_cmdw_avsz4"] = {
|
||||
{ register = "C2_OTZ", role = "otz" },
|
||||
},
|
||||
-- OP (outer product): writes IR1/IR2/IR3 (color-conversion fan-out).
|
||||
-- OP (outer product): writes IR1 / IR2 / IR3 (color-conversion fan-out).
|
||||
["gte_cmdw_op"] = {
|
||||
{ register = "C2_IR1", role = "latest_color" },
|
||||
{ register = "C2_IR2", role = "latest_color" },
|
||||
{ register = "C2_IR3", role = "latest_color" },
|
||||
},
|
||||
-- MVMVA: same shape as OP from the role perspective; the single
|
||||
-- MAC result is written to C2_IR1/IR2/IR3.
|
||||
-- MAC result is written to C2_IR1 / IR2 / IR3.
|
||||
["gte_cmdw_mvmva"] = {
|
||||
{ register = "C2_IR1", role = "latest_color" },
|
||||
{ register = "C2_IR2", role = "latest_color" },
|
||||
@@ -1241,17 +1216,16 @@ M.GTE_COMMAND_OUTPUTS = {
|
||||
-- A subsequent MTC2/CTC2 overwrite of one of those outputs before the latch window expires is a hazard:
|
||||
-- the latched value in the pipeline gets overwritten by the CPU before the pipeline consumes it.
|
||||
--
|
||||
-- This relation is the command -> register direction (the command is the producer; MTC2/CTC2 is the consumer).
|
||||
-- It is the inverse of the MTC2 -> command input propagation (register -> command direction), which is staged by the
|
||||
-- producer step of `analyze_hardware_relations`.
|
||||
-- This relation is the command -> register direction (the command is the producer; MTC2 / CTC2 is the consumer).
|
||||
-- It is the inverse of the MTC2 -> command input propagation (register -> command direction), which is staged by the producer step of `analyze_hardware_relations`.
|
||||
--
|
||||
-- The schema mirrors the producer-side relations (`direction`, `evidence`, `violation_kind`); `required` counts the
|
||||
-- emitted words strictly between the command's last output word and the overwrite.
|
||||
-- The schema mirrors the producer-side relations (`direction`, `evidence`, `violation_kind`);
|
||||
-- `required` counts the emitted words strictly between the command's last output word and the overwrite.
|
||||
-- `required = 0` permits the immediately following overwrite; `required = 4` requires four intervening words.
|
||||
--
|
||||
-- Per PSX-SPX `gtepipelinetimings.md` the per-command input latching measurements are the same numbers inverted.
|
||||
-- They describe when a recent MTC2/CTC2 must retire before the command issues; this table describes when a recent
|
||||
-- command's outputs latch into the pipeline before a later MTC2/CTC2 overwrites them.
|
||||
-- They describe when a recent MTC2 / CTC2 must retire before the command issues.
|
||||
-- This table describes when a recent command's outputs latch into the pipeline before a later MTC2/CTC2 overwrites them.
|
||||
--
|
||||
-- Consumers:
|
||||
-- * passes/static_analysis.lua::analyze_hardware_relations (stages post-command latch relations in `pending` after a GTE command).
|
||||
@@ -1266,9 +1240,7 @@ M.GTE_COMMAND_LATCH_WINDOWS = {
|
||||
{ register = "C2_OTZ", required = 4 },
|
||||
{ register = "C2_IR0", required = 4 },
|
||||
},
|
||||
-- RTPT: same latching as RTPS (the LAST projection in SXY2 is the
|
||||
-- newest one; the earlier SXY0 / SXY1 entries are part of the
|
||||
-- batched triple).
|
||||
-- RTPT: same latching as RTPS (the LAST projection in SXY2 is the newest one; the earlier SXY0 / SXY1 entries are part of the batched triple).
|
||||
["gte_cmdw_rtpt"] = {
|
||||
{ register = "C2_SXY0", required = 4 },
|
||||
{ register = "C2_SXY1", required = 4 },
|
||||
@@ -1287,7 +1259,7 @@ M.GTE_COMMAND_LATCH_WINDOWS = {
|
||||
["gte_cmdw_avsz4"] = {
|
||||
{ register = "C2_OTZ", required = 4 },
|
||||
},
|
||||
-- OP / MVMVA: IR1/IR2/IR3 latch for 4 emitted words.
|
||||
-- OP / MVMVA: IR1 / IR2 / IR3 latch for 4 emitted words.
|
||||
["gte_cmdw_op"] = {
|
||||
{ register = "C2_IR1", required = 4 },
|
||||
{ register = "C2_IR2", required = 4 },
|
||||
@@ -1300,18 +1272,14 @@ M.GTE_COMMAND_LATCH_WINDOWS = {
|
||||
},
|
||||
}
|
||||
|
||||
-- GTE component result contracts were removed: the `_post_<cmd>` naming convention was a soft convention
|
||||
-- (the user did not want it formalized via static-analysis enforcement). A proper `atom_info` directive for ordering semantics is a future TODO.
|
||||
|
||||
-- Operand-class table for the COP2->GPR load-delay check.
|
||||
--
|
||||
-- Maps each emitting-token ident to the set of GPR operand positions it reads.
|
||||
-- Covers the current encoder vocabulary (`code/duffle/mips.h` + `code/duffle/gte.h`); add rows here as new encoders land.
|
||||
--
|
||||
-- Semantics:
|
||||
-- * A "GPR operand position" is the textual slot in the macro's argument list, 1-based; e.g. `load_word(rt, base, off)` has
|
||||
-- positional operands 1 (rt), 2 (base), 3 (off). The table reads operands 1 + 2 + 3 to find what GPRs the macro touches.
|
||||
-- * The check tracks one entry per destination GPR per MFC2/CFC2 event.
|
||||
-- * The check tracks one entry per destination GPR per MFC2 / CFC2 event.
|
||||
-- A subsequent event counts as a "use" iff any of its read operand positions reference that destination GPR's ident (e.g. `R_T0`).
|
||||
-- * Branch delay slots are out of scope (MIPS control-flow; tracked separately).
|
||||
M.OPERAND_READ_POSITIONS = {
|
||||
@@ -1324,13 +1292,13 @@ M.OPERAND_READ_POSITIONS = {
|
||||
["sub_s"] = {1, 2, 3},
|
||||
["sub_u"] = {1, 2, 3},
|
||||
["and_i"] = {1, 2},
|
||||
["and"] = {1, 2, 3},
|
||||
["and"] = {1, 2, 3},
|
||||
["or_i"] = {1, 2},
|
||||
["or_i_self"] = {1},
|
||||
["or"] = {1, 2, 3},
|
||||
["or_self"] = {1, 2},
|
||||
["or"] = {1, 2, 3},
|
||||
["or_self"] = {1, 2},
|
||||
["xor_i"] = {1, 2},
|
||||
["xor"] = {1, 2, 3},
|
||||
["xor"] = {1, 2, 3},
|
||||
["slt_s"] = {1, 2, 3},
|
||||
["slt_u"] = {1, 2, 3},
|
||||
["slt_si"] = {1, 2},
|
||||
@@ -1431,7 +1399,6 @@ M.GP0_CMD_BY_SHAPE = {
|
||||
}
|
||||
|
||||
-- Per-instruction cycle cost (best-case, no stalls). Used by the static-analysis pass to emit per-atom cycle budgets.
|
||||
--
|
||||
-- GTE command values are the GTE instruction's intrinsic cycles — the latency after any pre-cmd `nop2` has retired.
|
||||
-- When the source emits `nop2, gte_cmdw_X`, the nops' cycles are added separately (1+1) plus the gte_cmdw_X value here:
|
||||
-- rtpt = 23 + 2 nops = 25 total cycles (PSX-SPX says 23 cycles for the cmd itself; the nops are pre-fill)
|
||||
@@ -1445,8 +1412,8 @@ M.GP0_CMD_BY_SHAPE = {
|
||||
-- PSX-SPX reports the GTE intrinsic cycles as the total execution time of the command itself (rtpt=23, rtps=15, nclip=8, etc.).
|
||||
-- The pre-fill nops are a codebase convention for retiring preceding C2 writes.
|
||||
-- See `docs/psx-spx/docs/geometrytransformationenginegte.md` for per-command cycle counts and
|
||||
-- `docs/psx-spx/docs/gtepipelinetimings.md` for the hardware-verified input-latch boundaries (most inputs become
|
||||
-- safe to clobber after 0-4 cycles).
|
||||
-- `docs/psx-spx/docs/gtepipelinetimings.md` for the hardware-verified input-latch boundaries
|
||||
-- (most inputs become safe to clobber after 0-4 cycles).
|
||||
--
|
||||
-- Per-macro cycle costs (`mac_yield`, `mac_pack_color_word`, ...) and per-macro prim-buffer contributions
|
||||
-- (`mac_format_*_color`, `mac_gte_store_*`, `mac_insert_ot_tag_*`) are NOT hardcoded here.
|
||||
@@ -1488,18 +1455,17 @@ M.INSTRUCTION_LATENCY = {
|
||||
["load_byte_u"] = 1, ["load_byte"] = 1,
|
||||
["load_upper_i"] = 1,
|
||||
-- 2-word loads (lui + ori) used for >16-bit immediates
|
||||
["load_imm"] = 2,
|
||||
["load_imm_1w"] = 1,
|
||||
["load_imm_1w_s0"] = 1,
|
||||
["load_imm_2w"] = 2,
|
||||
["load_imm"] = 2,
|
||||
["load_imm_1w"] = 1,
|
||||
["load_imm_1w_s0"] = 1,
|
||||
["load_imm_2w"] = 2,
|
||||
["load_imm_2w_addi_forced"] = 2,
|
||||
["load_imm_2w_ori_forced"] = 2,
|
||||
-- Stores (1 cycle each)
|
||||
["store_word"] = 1,
|
||||
["store_half"] = 1,
|
||||
["store_byte"] = 1,
|
||||
-- Branches (branch + BD slot nop = 2 cycles; the BD slot's nop is
|
||||
-- counted as part of the branch's cost)
|
||||
-- Branches (branch + BD slot nop = 2 cycles; the BD slot's nop is counted as part of the branch's cost)
|
||||
["branch_equal"] = 2, ["branch_ne"] = 2,
|
||||
["branch_le_zero"] = 2, ["branch_lt_zero"] = 2,
|
||||
["branch_ge_zero"] = 2, ["branch_gt_zero"] = 2,
|
||||
@@ -1510,8 +1476,8 @@ M.INSTRUCTION_LATENCY = {
|
||||
["jump"] = 2, ["jump_reg"] = 2,
|
||||
["jump_link"] = 2, ["call_reg"] = 2,
|
||||
["call_addr"] = 2,
|
||||
-- COP2 transfers (mtc2/mfc2/ctc2/cfc2 = 1 cycle + COP2 latency; the
|
||||
-- COP2 latency is usually absorbed by subsequent nops or by the next
|
||||
-- COP2 transfers (mtc2/mfc2/ctc2/cfc2 = 1 cycle + COP2 latency;
|
||||
-- The COP2 latency is usually absorbed by subsequent nops or by the next
|
||||
-- GTE command's pre-fill nops, so we count 1)
|
||||
["gte_mv_to_data_r"] = 1,
|
||||
["gte_mv_from_data_r"] = 1,
|
||||
@@ -1568,8 +1534,7 @@ M.UNKNOWN_INSTRUCTION_CYCLES = 1
|
||||
|
||||
-- Hardware-relation policy table.
|
||||
--
|
||||
-- The forward walker in `passes/static_analysis.lua::analyze_hardware_relations` reads every emitted word_event,
|
||||
-- matches its `encoder` against `row.token`, and:
|
||||
-- The forward walker in `passes/static_analysis.lua::analyze_hardware_relations` reads every emitted word_event, matches its `encoder` against `row.token`, and:
|
||||
-- * stages the event as a producer in `atom.paths.forward_state`; or
|
||||
-- * matches it as a consumer against pending producers and records a hazard on `atom.paths.hazards` when the gap is below `visibility.required`.
|
||||
--
|
||||
@@ -1589,8 +1554,8 @@ M.UNKNOWN_INSTRUCTION_CYCLES = 1
|
||||
-- and is reserved for future "self-retires" relations.
|
||||
--
|
||||
-- Evidence:
|
||||
-- * `evidence.confidence` is one of `"exact"`, `"conservative"`, `"unknown"`. The severity comes from `violation_kind`; a hardware
|
||||
-- measurement that the vendor caveats may still classify as `"conservative"` even when the underlying timing is numerically known.
|
||||
-- * `evidence.confidence` is one of `"exact"`, `"conservative"`, `"unknown"`. The severity comes from `violation_kind`;
|
||||
-- A hardware measurement that the vendor caveats may still classify as `"conservative"` even when the underlying timing is numerically known.
|
||||
-- * `evidence.source` is the upstream reference (file + line range) the row is sourced from. New rows must carry this citation.
|
||||
--
|
||||
-- Consumers:
|
||||
@@ -1702,13 +1667,10 @@ M.HARDWARE_RELATIONS = {
|
||||
-- The memory-side timing is not measured by the vendored GTE latch experiment, so this relation has no numeric retirement threshold.
|
||||
-- The LWC2 destination has TWO retirement regimes (per PSX-SPX):
|
||||
-- * GTE-command consumer (`gte_cmdw_*`): the GTE pipeline LATCHES the LWC2 result, so a `gte_cmdw_*`
|
||||
-- in the very next slot uses the latched value. Gap = 0 is allowed.
|
||||
-- (Per `docs/psx-spx/docs/gtepipelinetimings.md:271-274`.)
|
||||
-- * Any other consumer: standard MIPS load delay applies. Gap = 1 required.
|
||||
-- (Per `docs/psx-spx/docs/cpuspecifications.md:407-419`.)
|
||||
-- Two separate relations so the walker can dispatch by consumer type and emit
|
||||
-- different severities (the GTE-command path is `info` because the latch is intentional;
|
||||
-- the non-GTE-consumer path is `error` because the missing nop is a real bug).
|
||||
-- in the very next slot uses the latched value. Gap = 0 is allowed. (Per `docs/psx-spx/docs/gtepipelinetimings.md:271-274`.)
|
||||
-- * Any other consumer: standard MIPS load delay applies. Gap = 1 required. (Per `docs/psx-spx/docs/cpuspecifications.md:407-419`.)
|
||||
-- Two separate relations so the walker can dispatch by consumer type and emit different severities
|
||||
-- (the GTE-command path is `info` because the latch is intentional; the non-GTE-consumer path is `error` because the missing nop is a real bug).
|
||||
{
|
||||
id = "lwc2_to_gte_command",
|
||||
semantic = "LWC2_to_GTE",
|
||||
@@ -2070,14 +2032,14 @@ local E_MAC_PREFIX_LEN = 4
|
||||
--- Expand a body entry into the flat sequence of emitted machine-word events.
|
||||
---
|
||||
--- Semantics (one event per emitted machine word):
|
||||
--- * **Direct one-word encoders** (`load_word`, `add_ui`, `nop`, `gte_lw`, ...): one event with `ident` = leading ident, `args` = parsed top-level args.
|
||||
--- * **`nop2`** (2-word pseudo-instruction): two events, both with `ident = "nop"` so the recognized "this slot is a no-op" semantic is visible to downstream analyses.
|
||||
--- * **Any other N-word token** in `word_counts`: N events sharing the same `ident` + `args` so useful CPU words retire slots in the cycle budget.
|
||||
--- * **Known `mac_X(...)` calls**: recursively expand the indexed component body, including nested components. Every event from the expansion carries:
|
||||
--- * Direct one-word encoders `load_word`, `add_ui`, `nop`, `gte_lw`, ...: One event with `ident` = leading ident, `args` = parsed top-level args.
|
||||
--- * `nop2` (2-word pseudo-instruction): Two events, both with `ident = "nop"` so the recognized "this slot is a no-op" semantic is visible to downstream analyses.
|
||||
--- * Any other N-word token in `word_counts`: N events sharing the same `ident` + `args` so useful CPU words retire slots in the cycle budget.
|
||||
--- * Known `mac_X(...)` calls: Recursively expand the indexed component body, including nested components. Every event from the expansion carries:
|
||||
--- - `source` / `line` = the COMPONENT'S source path + the line of the token within the component body (i.e. "definition site").
|
||||
--- - `call_source` / `call_line` = the ROOT atom's source path + call-site line, PRESERVED across recursion so nested events still point at the original root.
|
||||
--- * **Unknown `mac_X`** (not in `component_index`): fall back to `word_counts[ident]` if present; otherwise emit one opaque event so the cycle budget accounts for the word.
|
||||
--- * **Marker tokens** (`atom_label(...)` / `atom_offset(...)`): zero events (they are pure metaprogram hints, not emitted machine words).
|
||||
--- * Unknown `mac_X` (not in `component_index`): fall back to `word_counts[ident]` if present; otherwise emit one opaque event so the cycle budget accounts for the word.
|
||||
--- * Marker Tokens (`atom_label(...)` / `atom_offset(...)`): Zero events (they are pure metaprogram hints).
|
||||
---
|
||||
--- Cycle protection: a per-expansion `visiting` set tracks components currently on the expansion stack; a re-entry produces a deterministic `{kind = "cycle", ...}` error and aborts that branch (does NOT hang, does NOT recurse).
|
||||
---
|
||||
@@ -2101,12 +2063,12 @@ local E_MAC_PREFIX_LEN = 4
|
||||
-- word_counts table is authored-metadata + current-component count table.
|
||||
|
||||
--- @class EmissionProjection
|
||||
--- @field items table[] -- ordered stream of word|label|offset|invoke_begin|invoke_end
|
||||
--- @field word_events table[] -- dense view of items where kind == "word"
|
||||
--- @field markers table[] -- dense view of items where kind == "label"|"offset"
|
||||
--- @field items table[] -- Ordered stream of word|label|offset|invoke_begin|invoke_end
|
||||
--- @field word_events table[] -- Dense view of items where kind == "word"
|
||||
--- @field markers table[] -- Dense view of items where kind == "label"|"offset"
|
||||
--- @field invocations InvocationRecord[] -- dense view of items where kind == "invoke_begin"|"invoke_end"
|
||||
--- @field errors table[] -- token-resolution failures surfaced without fail-loud
|
||||
--- @field warnings table[] -- opaque warnings (e.g. unknown uncounted macro)
|
||||
--- @field errors table[] -- Token-resolution failures surfaced without fail-loud
|
||||
--- @field warnings table[] -- Opaque warnings (e.g. unknown uncounted macro)
|
||||
|
||||
--- @class InvocationRecord
|
||||
--- Lives at `atom.paths.invocations[*]`. Constructed once at the single invocation-construction site
|
||||
@@ -2114,20 +2076,20 @@ local E_MAC_PREFIX_LEN = 4
|
||||
--- @field id integer -- 1-based, monotonic per-atom invocation id (0 is reserved for "no open invocation")
|
||||
--- @field parent_id integer -- 0 for the outermost (root) call; otherwise the id of the immediately enclosing invocation
|
||||
--- @field kind string -- "comp_bare" | "comp_proc" (component form that triggered the expansion)
|
||||
--- @field component_name string -- the bare component name without the `mac_` prefix
|
||||
--- @field call_text string -- the immediate `mac_X(...)` token text (or root call text for the outermost entry)
|
||||
--- @field root_call_text string -- the IMMUTABLE outermost `mac_X(...)` token text for every word emitted in this call's expansion
|
||||
--- @field call_path string -- source path of the call site (root atom source for direct calls, component source for nested expansions)
|
||||
--- @field call_line integer -- source line of the call site
|
||||
--- @field def_path string -- source path of the component definition
|
||||
--- @field def_line integer -- source line of the component declaration
|
||||
--- @field component_name string -- Bare component name without the `mac_` prefix
|
||||
--- @field call_text string -- Immediate `mac_X(...)` token text (or root call text for the outermost entry)
|
||||
--- @field root_call_text string -- IMMUTABLE outermost `mac_X(...)` token text for every word emitted in this call's expansion
|
||||
--- @field call_path string -- Source path of the call site (root atom source for direct calls, component source for nested expansions)
|
||||
--- @field call_line integer -- Source line of the call site
|
||||
--- @field def_path string -- Source path of the component definition
|
||||
--- @field def_line integer -- Source line of the component declaration
|
||||
--- @field start_pos integer -- 0-based emitted-word position of the FIRST word inside this invocation (the value of `word_idx` AT `emit_invoke_begin` time, BEFORE the first word is emitted). Words emitted inside this invocation occupy `start_pos..start_pos+#body_lines-1` (inclusive, 0-based). Downstream DWARF/provenance consumers MUST read this; do NOT reconstruct it from `start_word` (which is the 1-based items index including `invoke_begin`/`invoke_end` markers).
|
||||
--- @field end_pos integer -- 0-based position of the LAST word inside this invocation (set by `emit_invoke_end` to `word_idx - 1` AFTER all body words are emitted).
|
||||
--- @field start_word integer -- 1-based items index of the `invoke_begin` item
|
||||
--- @field end_word integer -- 1-based items index of the `invoke_end` item (set by `emit_invoke_end`)
|
||||
--- @field word_count integer -- number of `word` items emitted between `start_word` and `end_word` (inclusive)
|
||||
--- @field word_count integer -- Number of `word` items emitted between `start_word` and `end_word` (inclusive)
|
||||
--- @field debug_skip boolean -- `debug_skip` stamp; true iff `corpus.components[name].debug_skip` is true at construction. Always boolean (never `nil`).
|
||||
--- @field errors table[] -- per-invocation construction errors (cycle / count_mismatch); does not include pass-level errors
|
||||
--- @field errors table[] -- Per-invocation construction errors (cycle / count_mismatch); does not include pass-level errors
|
||||
|
||||
-- Internal recursive walker. The items stream holds every emitted event in order; `word_events`, `markers`,
|
||||
-- `invocations`, `errors`, `warnings` are dense views / side outputs appended alongside.
|
||||
@@ -2271,15 +2233,13 @@ local function _project_emission_inner(root_body_entry, ctx_table)
|
||||
return count
|
||||
end
|
||||
|
||||
-- Find the position of the consuming instruction's open paren (the `(` that
|
||||
-- starts the consuming instruction's argument list). Returns nil if the token's
|
||||
-- leading text isn't an ident followed by `(` (e.g. the ident is at the start of a
|
||||
-- non-instruction token).
|
||||
-- Find the position of the consuming instruction's open paren (the `(` that starts the consuming instruction's argument list).
|
||||
-- Returns nil if the token's leading text isn't an ident followed by `(` (e.g. the ident is at the start of a non-instruction token).
|
||||
local function find_consuming_paren(tok)
|
||||
local i = 1
|
||||
while i <= #tok do
|
||||
local c = tok:sub(i, i)
|
||||
if c == "(" then return i end
|
||||
if c == "(" then return i end
|
||||
if not c:match("[%w_]") and c ~= " " then return nil end
|
||||
i = i + 1
|
||||
end
|
||||
@@ -2287,33 +2247,32 @@ local function _project_emission_inner(root_body_entry, ctx_table)
|
||||
end
|
||||
|
||||
local function emit_embedded_markers(tok, tok_line, consuming_encoder)
|
||||
-- When called with a non-nil `consuming_encoder`, the marker is nested inside that
|
||||
-- instruction's argument list. We compute each marker's arg position by counting
|
||||
-- top-level commas between the consuming instruction's `(` and the marker's start.
|
||||
-- When called with a non-nil `consuming_encoder`, the marker is nested inside that instruction's argument list.
|
||||
-- We compute each marker's arg position by counting top-level commas between the consuming instruction's `(` and the marker's start.
|
||||
local consuming_paren = nil
|
||||
if consuming_encoder then consuming_paren = find_consuming_paren(tok) end
|
||||
local pos = 1
|
||||
while pos <= #tok do
|
||||
-- trim leading whitespace and comments before each scan.
|
||||
-- Trim leading whitespace and comments before each scan.
|
||||
pos = M.skip_ws_and_cmt(tok, pos)
|
||||
if pos > #tok then break end
|
||||
local ident, after = M.read_ident(tok, pos)
|
||||
if not ident then
|
||||
-- not an ident: token is a string or comment; skip or one-step.
|
||||
-- Not an ident: token is a string or comment; skip or one-step.
|
||||
local next_pos = M.skip_str_or_cmt(tok, pos)
|
||||
pos = (next_pos > pos) and next_pos or (pos + 1)
|
||||
goto continue_loop
|
||||
end
|
||||
if ident ~= "atom_label" and ident ~= "atom_offset" then
|
||||
-- ordinary ident; nothing to emit, step past the ident only.
|
||||
-- Ordinary ident; nothing to emit, step past the ident only.
|
||||
pos = after
|
||||
goto continue_loop
|
||||
end
|
||||
-- marker ident: parse the (...) arguments.
|
||||
-- Marker ident: parse the (...) arguments.
|
||||
local open = M.skip_ws_and_cmt(tok, after)
|
||||
local inner, after_paren = M.read_parens(tok, open)
|
||||
if not inner then
|
||||
-- (...) unreadable: fall back to non-marker behavior.
|
||||
-- (...) Unreadable: fall back to non-marker behavior.
|
||||
pos = after
|
||||
goto continue_loop
|
||||
end
|
||||
@@ -2327,10 +2286,8 @@ local function _project_emission_inner(root_body_entry, ctx_table)
|
||||
arg_pos = count_top_level_commas(tok, consuming_paren + 1, pos) + 1
|
||||
end
|
||||
local args = split_top_level_args(inner)
|
||||
if ident == "atom_label" then
|
||||
emit_marker("label", args[1] or "", nil, tok_line, nil, nil, consuming_encoder, arg_pos)
|
||||
else
|
||||
emit_marker("offset", args[1] or "", args[2] or "", tok_line, nil, nil, consuming_encoder, arg_pos)
|
||||
if ident == "atom_label" then emit_marker("label", args[1] or "", nil, tok_line, nil, nil, consuming_encoder, arg_pos)
|
||||
else emit_marker("offset", args[1] or "", args[2] or "", tok_line, nil, nil, consuming_encoder, arg_pos)
|
||||
end
|
||||
pos = after_paren
|
||||
::continue_loop::
|
||||
@@ -2342,14 +2299,13 @@ local function _project_emission_inner(root_body_entry, ctx_table)
|
||||
next_inv_id = next_inv_id + 1
|
||||
-- Invocation-level debug_skip stamp: Emission pass owns `atom.paths.invocations[*].debug_skip`.
|
||||
-- The stamp is resolved from the `corpus.components[name]` registry (passed in via `ctx_table.components` by `emission_model.run`),
|
||||
-- NOT from a parallel skip map, source-text re-parse, or second pass over `invocations`.
|
||||
-- Unmarked components stamp `false` (not `nil`) so consumers can dispatch on the boolean without nil checks.
|
||||
--
|
||||
-- The walker has already found the component body in `ctx_table.component_index[component_name]`, so the matching entry MUST exist in `ctx_table.components[component_name]`
|
||||
-- (both registries are populated from the same source by the components pass).
|
||||
-- A missing entry is a corpus-plumbing bug; we fail loudly here rather than silently stamp `false` and mask the regression.
|
||||
local components = ctx_table.components
|
||||
local component_def = components and components[component_name] or nil
|
||||
local components = ctx_table.components
|
||||
local component_def = components and components[component_name] or nil
|
||||
if not component_def then
|
||||
error("duffle.emit_invoke_begin: component " .. string.format("%q", component_name)
|
||||
.. " is present in `component_index` (the walker matched a `mac_" .. component_name .. "()` call) but absent from `components` (the canonical corpus.components registry). "
|
||||
@@ -2413,7 +2369,6 @@ local function _project_emission_inner(root_body_entry, ctx_table)
|
||||
end
|
||||
|
||||
-- Resolve the per-token word count. If unresolved, surface ONE warning
|
||||
-- (NOT an error; the build does not fail-loud on an uncounted opaque word)
|
||||
-- and fall back to 1 opaque word so the cycle budget still accounts for the slot.
|
||||
local function resolve_count(ident, tok_line)
|
||||
local wc = ctx_table.word_counts
|
||||
@@ -2437,10 +2392,10 @@ local function _project_emission_inner(root_body_entry, ctx_table)
|
||||
end
|
||||
|
||||
-- Recursive walker: walk one body entry, possibly descending into components.
|
||||
-- `walk_parent_inv_id` is the invocation ID of the enclosing call (0 for the root call).
|
||||
-- `walk_root_call_text` is the outermost `mac_X(...)` token text (preserved across recursion).
|
||||
-- `walk_immediate_call_text` is the IMMEDIATE outer `mac_X(...)` token text for words emitted in this body — nil for the root atom body.
|
||||
-- The two trackers are propagated as separate parameters so words deep inside nested expansions correctly identify both their immediate call site and the outermost call site.
|
||||
-- walk_parent_inv_id: Invocation ID of the enclosing call (0 for the root call).
|
||||
-- walk_root_call_text: Outermost `mac_X(...)` token text (preserved across recursion).
|
||||
-- walk_immediate_call_text: IMMEDIATE outer `mac_X(...)` token text for words emitted in this body — nil for the root atom body.
|
||||
-- Two trackers are propagated as separate parameters so words deep inside nested expansions correctly identify both their immediate call site and the outermost call site.
|
||||
local function walk_body_entry(body_entry, walk_parent_inv_id,
|
||||
walk_root_call_text, walk_immediate_call_text)
|
||||
local tokens = body_entry.body_tokens or {}
|
||||
@@ -2457,20 +2412,18 @@ local function _project_emission_inner(root_body_entry, ctx_table)
|
||||
local _, args = token_ident_and_args(tok)
|
||||
local tok_line = line_of(body_off + bt.rel) or 0
|
||||
-- embedded markers live only in non-marker tokens.
|
||||
-- Pass `ident` as the consuming instruction so `emit_embedded_markers` can compute
|
||||
-- each marker's arg position + record the consuming_encoder for the offsets pass.
|
||||
-- Canonicalize `jump_rel` to `branch_equal` (its preprocessor-expanded form) so the
|
||||
-- `consuming_encoder` metadata in marker records is canonical. `jump_rel` is the within-atom-safe
|
||||
-- unconditional jump alias from `code/duffle/mips.h`; the C preprocessor expands it BEFORE
|
||||
-- the metaprogram sees the source, but the raw token ident is still `jump_rel` here.
|
||||
-- Pass `ident` as the consuming instruction so `emit_embedded_markers` can compute each marker's arg position + record the consuming_encoder for the offsets pass.
|
||||
-- Canonicalize `jump_rel` to `branch_equal` (its preprocessor-expanded form) so the `consuming_encoder` metadata in marker records is canonical.
|
||||
-- `jump_rel`: unconditional jump alias from `code/duffle/mips.h`.
|
||||
local consuming_encoder_for_markers = (ident == "jump_rel") and "branch_equal" or ident
|
||||
if ident ~= "atom_label" and ident ~= "atom_offset" then
|
||||
emit_embedded_markers(tok, tok_line, consuming_encoder_for_markers)
|
||||
end
|
||||
-- atom_label / atom_offset: terminal markers, no further descent.
|
||||
-- Top-level markers (the marker IS the entire token) have no consuming instruction;
|
||||
-- nil for both `consuming_encoder` and `consuming_arg_pos`. The offsets pass treats
|
||||
-- these as branch-equivalent for backward compatibility.
|
||||
-- nil for both `consuming_encoder` and `consuming_arg_pos`.
|
||||
-- The offsets pass treats these as branch-equivalent for backward compatibility.
|
||||
-- TODO(Ed): Review this don't want legacy cruft here..
|
||||
if ident == "atom_label" then emit_marker("label", args[1] or "", nil, tok_line); return
|
||||
elseif ident == "atom_offset" then emit_marker("offset", args[1] or "", args[2] or "", tok_line); return
|
||||
end
|
||||
@@ -2480,7 +2433,7 @@ local function _project_emission_inner(root_body_entry, ctx_table)
|
||||
if comp then
|
||||
local invocation_root_call_text = walk_root_call_text or tok
|
||||
if ctx_table.visiting[bare] then
|
||||
-- cycle: still allocate inv_id, emit zero-width begin/end, record the cycle error; do NOT recurse.
|
||||
-- Cycle: still allocate inv_id, emit zero-width begin/end, record the cycle error; do NOT recurse.
|
||||
local inv = emit_invoke_begin(comp.kind or "comp_bare", bare, tok, invocation_root_call_text, def_source, tok_line)
|
||||
inv.parent_id = walk_parent_inv_id
|
||||
inv.call_text = tok
|
||||
@@ -2495,16 +2448,16 @@ local function _project_emission_inner(root_body_entry, ctx_table)
|
||||
emit_invoke_end(inv)
|
||||
return
|
||||
end
|
||||
-- first visit: descend + count + count_mismatch-check below.
|
||||
-- First visit: descend + count + count_mismatch-check below.
|
||||
ctx_table.visiting[bare] = true
|
||||
local inv = emit_invoke_begin(comp.kind or "comp_bare", bare, tok, invocation_root_call_text, def_source, tok_line)
|
||||
inv.parent_id = walk_parent_inv_id
|
||||
inv.call_text = tok
|
||||
inv.def_path = comp.source
|
||||
inv.def_line = comp.declaration
|
||||
-- propagate trackers into the recursive walk:
|
||||
-- Propagate trackers into the recursive walk:
|
||||
-- immediate_call_text = this call's tok (the IMMEDIATE outer call for words emitted in this body)
|
||||
-- root_call_text = the OUTERMOST call (immutable across the recursion)
|
||||
-- root_call_text = the OUTERMOST call (immutable across the recursion)
|
||||
walk_body_entry({
|
||||
body_tokens = comp.body_tokens or {},
|
||||
body_off = comp.body_off or 0,
|
||||
@@ -2517,7 +2470,7 @@ local function _project_emission_inner(root_body_entry, ctx_table)
|
||||
tok)
|
||||
ctx_table.visiting[bare] = nil
|
||||
emit_invoke_end(inv)
|
||||
-- count `word` items inside [start_word, end_word].
|
||||
-- Count `word` items inside [start_word, end_word].
|
||||
local wc_inside = 0
|
||||
for i = inv.start_word, inv.end_word do
|
||||
local it = items[i]
|
||||
@@ -2543,9 +2496,9 @@ local function _project_emission_inner(root_body_entry, ctx_table)
|
||||
end
|
||||
-- mac_X NOT in component_index: fall through to opaque emit.
|
||||
end
|
||||
-- direct encoder, or mac_X-without-component: resolve count + emit n words.
|
||||
-- resolve_count may emit a warning if the count is unresolved.
|
||||
local n = resolve_count(ident, tok_line)
|
||||
-- Direct encoder, or mac_X-without-component: resolve count + emit n words.
|
||||
-- Resolve_count may emit a warning if the count is unresolved.
|
||||
local n = resolve_count(ident, tok_line)
|
||||
local out_ident = (ident == "nop2") and "nop" or ident
|
||||
for _ = 1, n do
|
||||
emit_word(out_ident, args, tok_line, tok, def_source, def_line, walk_immediate_call_text, walk_root_call_text)
|
||||
@@ -2560,9 +2513,9 @@ local function _project_emission_inner(root_body_entry, ctx_table)
|
||||
-- Initialize the per-walk mutable context.
|
||||
-- `visiting` is the active DFS component stack; `root_call_path` / `root_call_line` are preserved across recursion so nested words always point at the
|
||||
-- ORIGINAL root atom call site.
|
||||
ctx_table.visiting = ctx_table.visiting or {}
|
||||
ctx_table.root_call_path = ctx_table.root_call_path or ""
|
||||
ctx_table.root_call_line = ctx_table.root_call_line or 0
|
||||
ctx_table.visiting = ctx_table.visiting or {}
|
||||
ctx_table.root_call_path = ctx_table.root_call_path or ""
|
||||
ctx_table.root_call_line = ctx_table.root_call_line or 0
|
||||
|
||||
-- Walk first; the pass caller stamps the root call site for direct words after the projection returns.
|
||||
-- For nested words the def_path / def_line already point at the component source and MUST be preserved (the stamping helper checks for that).
|
||||
|
||||
+68
-84
@@ -1,13 +1,8 @@
|
||||
--- elf_dwarf.lua — ELF32 + DWARF + atoms source-map utilities.
|
||||
--- All ELF32 + DWARF-specific code lives here.
|
||||
---
|
||||
--- **What this module contains:**
|
||||
--- - **Format-constant tables** (the byte-offset / opcode / size encyclopedias for ELF32, DWARF4 aranges, DWARF5 rnglists, DWARF line-program, MIPS).
|
||||
--- Every constant carries a spec:` comment naming the spec section that defines it.
|
||||
--- - **I/O helpers**: little-endian byte read/write, ELF32 section walker, nm symbol reader, source-map parser, native directory glob.
|
||||
---
|
||||
--- **Conventions:** tabs (1/level), EmmyLua annotations, no regex,
|
||||
--- Lua 5.3 compatible.
|
||||
|
||||
-- ════════════════════════════════════════════════════════════════════════════
|
||||
-- Native dependencies
|
||||
@@ -60,19 +55,19 @@ M.DW_AT = {
|
||||
}
|
||||
|
||||
M.DW_FORM = {
|
||||
addr = 0x01,
|
||||
data1 = 0x0B,
|
||||
data2 = 0x05,
|
||||
data4 = 0x06,
|
||||
string = 0x08,
|
||||
strp = 0x0E,
|
||||
exprloc = 0x18,
|
||||
ref4 = 0x13,
|
||||
udata = 0x0F,
|
||||
ref_sig8 = 0x20,
|
||||
addr = 0x01,
|
||||
data1 = 0x0B,
|
||||
data2 = 0x05,
|
||||
data4 = 0x06,
|
||||
string = 0x08,
|
||||
strp = 0x0E,
|
||||
exprloc = 0x18,
|
||||
ref4 = 0x13,
|
||||
udata = 0x0F,
|
||||
ref_sig8 = 0x20,
|
||||
implicit_const = 0x21,
|
||||
flag_present = 0x19,
|
||||
sec_offset = 0x17,
|
||||
flag_present = 0x19,
|
||||
sec_offset = 0x17,
|
||||
}
|
||||
|
||||
M.DW_ATE = {
|
||||
@@ -104,13 +99,9 @@ M.MIPS_BYTES_PER_WORD = 0x04
|
||||
-- ----------------------------------------------------------------------------
|
||||
-- ELF32 (System V ABI gABI v1.2)
|
||||
-- ----------------------------------------------------------------------------
|
||||
--- **Wire-offset contract:** format offsets, fixed-width reader offsets, LEB/parser cursors,
|
||||
--- and section-relative values are zero-based wire offsets. Only Lua string APIs receive
|
||||
--- a `+ 1` conversion at their boundary (`byte`, `sub`, and `find`).
|
||||
---
|
||||
--- ELF/DWARF field offsets are expressed in hex so they map directly to the
|
||||
--- zero-based byte positions in the binary file.
|
||||
|
||||
--- **Wire-offset contract:** format offsets, fixed-width reader offsets, LEB/parser cursors, and section-relative values are zero-based wire offsets.
|
||||
--- Only Lua string APIs receive a `+ 1` conversion at their boundary (`byte`, `sub`, and `find`).
|
||||
--- ELF/DWARF field offsets are expressed in hex so they map directly to the zero-based byte positions in the binary file.
|
||||
|
||||
--- spec: System V ABI gABI v1.2 §"ELF Header" (Table 1) + §"Section Header Table"
|
||||
M.ELF32 = {
|
||||
@@ -192,8 +183,7 @@ M.DWARF_LINE_OPS = {
|
||||
DW_LNE_end_sequence = 1, -- spec: §6.2.5.3
|
||||
DW_LNE_set_address = 2, -- spec: §6.2.5.3
|
||||
-- Standard opcode header (§6.2.5.1)
|
||||
-- opcode_base + line_range are 1-byte header fields; hex so they map
|
||||
-- directly to the line-program header byte sequence.
|
||||
-- opcode_base + line_range are 1-byte header fields; hex so they map directly to the line-program header byte sequence.
|
||||
-- line_base stays signed decimal (=-5) since 0xFB obscures the spec semantics.
|
||||
opcode_base = 0x0D,
|
||||
line_base = -5,
|
||||
@@ -249,12 +239,10 @@ M.DWARF5_DEBUG_LINE = {
|
||||
--- Read a 4-byte little-endian unsigned integer from `buf` at zero-based wire offset `off`.
|
||||
--- Equivalent to `string.unpack("<I4", buf, off + 1)` but avoids the table-return shape + works under LuaJIT 2.1
|
||||
--- (which has partial `string.unpack` coverage).
|
||||
---
|
||||
--- **Convention:** `off` is a zero-based wire offset; `+ 1` is applied only at the `string.byte` boundary.
|
||||
---
|
||||
--- **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).
|
||||
--- 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 -- zero-based wire offset
|
||||
--- @return integer
|
||||
@@ -278,12 +266,11 @@ end
|
||||
|
||||
-- Pure-Lua 5.3 LEB128 readers (no `bit` library). `2^shift` arithmetic matches the existing parser.
|
||||
-- Offsets are 0-based; returns (value, next_pos).
|
||||
-- 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
|
||||
-- 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
|
||||
local len = #buf
|
||||
while pos < len do
|
||||
local b = buf:byte(pos + 1)
|
||||
value = value + (b % 0x80) * (2 ^ shift)
|
||||
@@ -427,13 +414,13 @@ local function read_form_value(buf, str_buf, pos, form)
|
||||
-- The constant is declared in the abbrev; no value bytes in the DIE.
|
||||
return nil, pos
|
||||
elseif form == M.DW_FORM.ref_sig8 then
|
||||
-- DW_FORM_ref_sig8 (DWARF5 §7.4.2): an 8-byte value identifying a type
|
||||
-- by signature. The low 4 bytes (LE) are the type signature (content hash);
|
||||
-- the high 4 bytes (LE) are a CU-relative offset into the matching type unit.
|
||||
-- Consumers use the low 4 to look up the type unit (see M.find_type_unit_by_signature)
|
||||
-- then the high 4 to resolve the specific type within it.
|
||||
-- Return the low 4 as the primary value to preserve the (value, next_pos) shape;
|
||||
-- the high 4 is exposed via M.read_ref_sig8 (which returns both halves).
|
||||
-- DW_FORM_ref_sig8 (DWARF5 §7.4.2): An 8-byte value identifying a type by signature.
|
||||
-- The low 4 bytes (LE) are the type signature (content hash);
|
||||
-- The high 4 bytes (LE) are a CU-relative offset into the matching type unit.
|
||||
-- Consumers use the low 4 to look up the type unit (see M.find_type_unit_by_signature)
|
||||
-- then the high 4 to resolve the specific type within it.
|
||||
-- Return the low 4 as the primary value to preserve the (value, next_pos) shape;
|
||||
-- the high 4 is exposed via M.read_ref_sig8 (which returns both halves).
|
||||
local _, _, next_pos = M.read_ref_sig8(buf, pos)
|
||||
return M.read_u32_le(buf, pos), next_pos
|
||||
else
|
||||
@@ -470,11 +457,11 @@ end
|
||||
-- @param target_sig_hi integer -- high 4 bytes (LE) of the desired signature
|
||||
-- @return integer|nil, integer|nil -- unit offset, type_offset within the unit
|
||||
function M.find_type_unit_by_signature(info, target_sig_lo, target_sig_hi)
|
||||
local pos = 0
|
||||
local pos = 0
|
||||
local section_len = #info
|
||||
while pos + 4 < section_len do
|
||||
local unit_length = M.read_u32_le(info, pos)
|
||||
if unit_length == 0xFFFFFFFF then
|
||||
if unit_length == 0xFFFFFFFF then
|
||||
return nil, nil -- DWARF64 not supported
|
||||
end
|
||||
-- unit_length is the body size, NOT including the 4-byte unit_length field itself.
|
||||
@@ -503,9 +490,9 @@ function M.find_type_unit_by_signature(info, target_sig_lo, target_sig_hi)
|
||||
-- byte 8-15: type_signature (8)
|
||||
-- byte 16-19: type_offset (4)
|
||||
local unit_type = info:byte(body_start + 2 + 1) -- 0-based +2 = unit_type in 1-indexed
|
||||
if unit_type == 0x02 then -- DW_UT_type
|
||||
if unit_type == 0x02 then -- DW_UT_type
|
||||
local sig_lo, sig_hi, _ = M.read_ref_sig8(info, body_start + 8) -- 0-based +8 = type_signature in 1-indexed
|
||||
if sig_lo == target_sig_lo and sig_hi == target_sig_hi then
|
||||
if sig_lo == target_sig_lo and sig_hi == target_sig_hi then
|
||||
local type_offset = M.read_u32_le(info, body_start + 16) -- 0-based +16 = type_offset in 1-indexed
|
||||
return pos, type_offset
|
||||
end
|
||||
@@ -571,14 +558,14 @@ function M.read_elf_sections(elf_path, section_names)
|
||||
return result
|
||||
end
|
||||
|
||||
local f = io.open(elf_path, "rb")
|
||||
local f = io.open(elf_path, "rb")
|
||||
if not f then
|
||||
io.stderr:write(string.format("[elf_dwarf.read_elf_sections] io.open failed: %s\n", elf_path))
|
||||
return result
|
||||
end
|
||||
|
||||
-- Read the ELF32 header.
|
||||
local header = f:read(M.ELF32.header_bytes)
|
||||
local header = f:read(M.ELF32.header_bytes)
|
||||
if not header or #header < M.ELF32.header_bytes then
|
||||
io.stderr:write("[elf_dwarf.read_elf_sections] ELF too small for ELF32 header\n")
|
||||
f:close()
|
||||
@@ -610,7 +597,7 @@ function M.read_elf_sections(elf_path, section_names)
|
||||
|
||||
-- Read the section-header string table (.shstrtab) so we can resolve section names from their `sh_name` offsets.
|
||||
f:seek("set", e_shoff + e_shstrndx * e_shentsize)
|
||||
local strtab_hdr = f:read(e_shentsize)
|
||||
local strtab_hdr = f:read(e_shentsize)
|
||||
if not strtab_hdr or #strtab_hdr < e_shentsize then
|
||||
io.stderr:write("[elf_dwarf.read_elf_sections] could not read .shstrtab header\n")
|
||||
f:close()
|
||||
@@ -629,7 +616,7 @@ function M.read_elf_sections(elf_path, section_names)
|
||||
|
||||
for sh_idx = 0, e_shnum - 1 do
|
||||
f:seek("set", e_shoff + sh_idx * e_shentsize)
|
||||
local sh = f:read(e_shentsize)
|
||||
local sh = f:read(e_shentsize)
|
||||
if not sh or #sh < e_shentsize then break end
|
||||
local sh_name = M.read_u32_le(sh, M.ELF32.sh_name_offset)
|
||||
local sh_offset = M.read_u32_le(sh, M.ELF32.sh_offset_offset)
|
||||
@@ -679,15 +666,14 @@ function M.read_nm(elf_path)
|
||||
local SYM_ST_INFO = 0x0C
|
||||
local n_syms = #symtab / SYM_ENTRY_BYTES
|
||||
for i = 0, n_syms - 1 do
|
||||
local entry_off = i * SYM_ENTRY_BYTES
|
||||
local st_info = symtab:byte(entry_off + SYM_ST_INFO + 1)
|
||||
local entry_off = i * SYM_ENTRY_BYTES
|
||||
local st_info = symtab:byte(entry_off + SYM_ST_INFO + 1)
|
||||
-- High nibble = binding (STB_LOCAL=0, STB_GLOBAL=1, STB_WEAK=2).
|
||||
-- Use math.floor(/16) instead of bit.rshift for LuaJIT 2.1 compat
|
||||
-- (LuaJIT's `>>` is 5.3+, but math.floor(x/16) works on all versions).
|
||||
local binding = math.floor(st_info / 16)
|
||||
if binding == 0 or binding == 1 then -- STB_LOCAL or STB_GLOBAL
|
||||
local st_size = M.read_u32_le(symtab, entry_off + SYM_ST_SIZE)
|
||||
if st_size > 0 then
|
||||
-- Use math.floor(/16) instead of bit.rshift for LuaJIT 2.1 compat (LuaJIT's `>>` is 5.3+, but math.floor(x/16) works on all versions).
|
||||
local binding = math.floor(st_info / 16)
|
||||
if binding == 0 or binding == 1 then -- STB_LOCAL or STB_GLOBAL
|
||||
local st_size = M.read_u32_le(symtab, entry_off + SYM_ST_SIZE)
|
||||
if st_size > 0 then
|
||||
local st_name_off = M.read_u32_le(symtab, entry_off + SYM_ST_NAME)
|
||||
-- Extract the name from .strtab (null-terminated C string).
|
||||
local name_end = strtab:find("\0", st_name_off + 1, true) or (st_name_off + 1)
|
||||
@@ -779,8 +765,8 @@ function M.sleb128(n)
|
||||
local b = n % (LEB_DATA_MASK + 1) -- extract low 7 bits
|
||||
n = (n - b) / (LEB_DATA_MASK + 1) -- arithmetic shift right by 7
|
||||
-- Termination: remaining value bits fit in the sign bit of the last byte.
|
||||
if n == 0 and b < SLEB_SIGN_BIT then more = false end -- positive terminator
|
||||
if n == -1 and b >= SLEB_SIGN_BIT then more = false end -- negative terminator
|
||||
if n == 0 and b < SLEB_SIGN_BIT then more = false end -- positive terminator
|
||||
if n == -1 and b >= SLEB_SIGN_BIT then more = false end -- negative terminator
|
||||
if more then b = b + LEB_CONT_BIT end
|
||||
bytes[#bytes + 1] = string.char(b)
|
||||
end
|
||||
@@ -811,14 +797,14 @@ end
|
||||
--- @param n integer -- any integer (negative allowed)
|
||||
--- @return integer
|
||||
function M.sleb128_size(n)
|
||||
local more = true
|
||||
local more = true
|
||||
local bytes = 0
|
||||
local v = n
|
||||
local v = n
|
||||
while more do
|
||||
local b = v % (LEB_DATA_MASK + 1) -- extract low 7 bits
|
||||
v = (v - b) / (LEB_DATA_MASK + 1) -- arithmetic shift right by 7
|
||||
if v == 0 and b < SLEB_SIGN_BIT then more = false end -- positive terminator
|
||||
if v == -1 and b >= SLEB_SIGN_BIT then more = false end -- negative terminator
|
||||
if v == 0 and b < SLEB_SIGN_BIT then more = false end -- positive terminator
|
||||
if v == -1 and b >= SLEB_SIGN_BIT then more = false end -- negative terminator
|
||||
if more then b = b + LEB_CONT_BIT end
|
||||
bytes = bytes + 1
|
||||
end
|
||||
@@ -855,47 +841,45 @@ end
|
||||
--- (which replaced the former hardcoded `ATOM_SOURCE_FILE_INDEX` + `PROVENANCE_BASENAME_TO_FILE_INDEX` table per `conductor/tracks/dwarf_file_index_lookup_20260731/`)
|
||||
--- consult the map directly.
|
||||
---
|
||||
--- @param elf_path string -- absolute path to the post-link ELF (typically the gcc-emitted `.elf` BEFORE dwarf_injector's splice;
|
||||
--- both shapes work since the splice preserves `.debug_line`)
|
||||
--- @param elf_path string -- absolute path to the post-link ELF (typically the gcc-emitted `.elf` BEFORE dwarf_injector's splice; both shapes work since the splice preserves `.debug_line`)
|
||||
--- @return table|nil, table|nil, table|nil
|
||||
--- basename_to_index: { [basename] = 1-based-per-unit-file-index, ... }
|
||||
--- basenames: { [1-based-per-unit-file-index] = basename, ... }
|
||||
--- paths: { [1-based-per-unit-file-index] = full path (mixed slashes), ... }
|
||||
--- basename_to_index: { [basename] = 1-based-per-unit-file-index, ... }
|
||||
--- basenames: { [1-based-per-unit-file-index] = basename, ... }
|
||||
--- paths: { [1-based-per-unit-file-index] = full path (mixed slashes), ... }
|
||||
function M.read_line_unit_file_table(elf_path)
|
||||
local sections = M.read_elf_sections(elf_path, { ".debug_line", ".debug_line_str" })
|
||||
local line = sections[".debug_line"]
|
||||
local lstr = sections[".debug_line_str"] or ""
|
||||
local line = sections[".debug_line"]
|
||||
local lstr = sections[".debug_line_str"] or ""
|
||||
if not line or line == "" then
|
||||
io.stderr:write("[elf_dwarf.read_line_unit_file_table] no .debug_line section in: " .. tostring(elf_path) .. "\n")
|
||||
return nil
|
||||
end
|
||||
|
||||
local basenames = {}
|
||||
local basenames = {}
|
||||
local basename_to_index = {}
|
||||
local paths = {}
|
||||
local paths = {}
|
||||
|
||||
--- Read one form-code's bytes from `buf` at position `p` according to `form`.
|
||||
--- Returns (value, after) where `value` is:
|
||||
--- * the resolved string (DW_FORM_line_strp / DW_FORM_string)
|
||||
--- * the ULEB128 number (DW_FORM_udata)
|
||||
--- * nil + skip-bytes (DW_FORM_data16; we don't surface the MD5)
|
||||
--- * the ULEB128 number (DW_FORM_udata)
|
||||
--- * nil + skip-bytes (DW_FORM_data16; we don't surface the MD5)
|
||||
local function read_form(buf, lstr_buf, p, form)
|
||||
if form == M.DWARF5_DEBUG_LINE.form_line_strp then
|
||||
local strp = M.read_u32_le(buf, p)
|
||||
local strp = M.read_u32_le(buf, p)
|
||||
local end_pos = lstr_buf:find("\0", strp + 1, true) or (#lstr_buf + 1)
|
||||
return lstr_buf:sub(strp + 1, end_pos - 1), p + M.DWARF5_DEBUG_LINE.form_strp_bytes
|
||||
elseif form == M.DWARF5_DEBUG_LINE.form_string then
|
||||
local nul = buf:find("\0", p + 1, true) or (#buf + 1)
|
||||
return buf:sub(p + 1, nul - 1), nul
|
||||
elseif form == M.DWARF5_DEBUG_LINE.form_udata then
|
||||
local v, after = M.read_uleb128_at(buf, p)
|
||||
local v, after = M.read_uleb128_at(buf, p)
|
||||
return v, after
|
||||
elseif form == M.DWARF5_DEBUG_LINE.form_data16 then
|
||||
return nil, p + M.DWARF5_DEBUG_LINE.form_data16_bytes
|
||||
else
|
||||
-- Unsupported form in a directory/file-table entry: best-effort skip.
|
||||
-- We do NOT stderr-write because the crt0.s DWARF5 line unit (gcc-as emitted) uses DW_FORM_addr (0x01) for what is effectively a path entry,
|
||||
-- which is non-standard.
|
||||
-- We do NOT stderr-write because the crt0.s DWARF5 line unit (gcc-as emitted) uses DW_FORM_addr (0x01) for what is effectively a path entry, which is non-standard.
|
||||
-- The C-unit's DWARF3 paths are read via the parallel DWARF3 path and never see this error.
|
||||
-- Callers should consult `basename_to_index` for the paths they care about and ignore this unit if it produced none.
|
||||
return nil, p
|
||||
@@ -916,9 +900,9 @@ function M.read_line_unit_file_table(elf_path)
|
||||
local dirs = {}
|
||||
while up < body_end do
|
||||
local nul = buf:find("\0", up + 1, true) or (body_end + 1)
|
||||
if nul > body_end then break end
|
||||
if nul > body_end then break end
|
||||
local len = nul - up - 1
|
||||
if len == 0 then up = nul break end
|
||||
if len == 0 then up = nul break end
|
||||
dirs[#dirs + 1] = buf:sub(up + 1, nul - 1)
|
||||
up = nul
|
||||
end
|
||||
@@ -926,14 +910,14 @@ function M.read_line_unit_file_table(elf_path)
|
||||
local unit_paths = {}
|
||||
while up < body_end do
|
||||
local nul = buf:find("\0", up + 1, true) or (body_end + 1)
|
||||
if nul > body_end or nul == up + 1 then up = nul break end
|
||||
if nul > body_end or nul == up + 1 then up = nul break end
|
||||
local path = buf:sub(up + 1, nul - 1)
|
||||
up = nul
|
||||
local didx, up_next = M.read_uleb128_at(buf, up); up = up_next
|
||||
local _time, up_next2 = M.read_uleb128_at(buf, up); up = up_next2
|
||||
local _size, up_next3 = M.read_uleb128_at(buf, up); up = up_next3
|
||||
local didx, up_next = M.read_uleb128_at(buf, up); up = up_next
|
||||
local _time, up_next2 = M.read_uleb128_at(buf, up); up = up_next2
|
||||
local _size, up_next3 = M.read_uleb128_at(buf, up); up = up_next3
|
||||
local idx = #unit_basenames + 1
|
||||
local bs = path:match("[^/\\]+$") or path
|
||||
local bs = path:match("[^/\\]+$") or path
|
||||
unit_paths[idx] = path
|
||||
unit_basenames[idx] = bs
|
||||
dirs[1] = dirs[1] or "" -- safety: gcc emits "" sentinel dir at 0
|
||||
@@ -1006,7 +990,7 @@ function M.read_line_unit_file_table(elf_path)
|
||||
local section_end = #line
|
||||
while p + 4 <= section_end do
|
||||
local unit_length = M.read_u32_le(line, p)
|
||||
if unit_length == 0xFFFFFFFF then
|
||||
if unit_length == 0xFFFFFFFF then
|
||||
io.stderr:write("[elf_dwarf.read_line_unit_file_table] 64-bit DWARF (initial-length 0xFFFFFFFF); not supported\n")
|
||||
return nil
|
||||
end
|
||||
|
||||
@@ -22,11 +22,11 @@ local duffle = dofile(_bootstrap_dir .. "../duffle_paths.lua")
|
||||
-- ════════════════════════════════════════════════════════════════════════════
|
||||
|
||||
--- @class SourceFile
|
||||
--- @field path string -- absolute path to the source file
|
||||
--- @field text string -- the full source text
|
||||
--- @field dir string -- the directory containing the source
|
||||
--- @field basename string -- filename without extension
|
||||
--- @field scan table -- pre-scanned SourceScan payload (from duffle.scan_source)
|
||||
--- @field path string -- Absolute path to the source file
|
||||
--- @field text string -- Full source text
|
||||
--- @field dir string -- Directory containing the source
|
||||
--- @field basename string -- Filename without extension
|
||||
--- @field scan table -- Pre-scanned SourceScan payload (from duffle.scan_source)
|
||||
|
||||
--- @class PassCtx
|
||||
--- @field sources SourceFile[]
|
||||
@@ -45,28 +45,28 @@ local duffle = dofile(_bootstrap_dir .. "../duffle_paths.lua")
|
||||
--- @field warnings table[]
|
||||
|
||||
--- @class AtomAnnotation
|
||||
--- @field line integer -- source line of the atom_info call
|
||||
--- @field macro string -- the macro name (always "atom_info" in the new shape)
|
||||
--- @field name string -- the atom name
|
||||
--- @field kind string -- always "info"
|
||||
--- @field line integer -- Source line of the atom_info call
|
||||
--- @field macro string -- Macro name (always "atom_info" in the new shape)
|
||||
--- @field name string -- Atom name
|
||||
--- @field kind string -- Always "info"
|
||||
--- @field binds string|nil -- Binds_X name if any
|
||||
--- @field reads string[] -- R_* names (read targets)
|
||||
--- @field writes string[] -- R_* names (write targets)
|
||||
--- @field errors string[]|nil -- parse-time errors from scan_source (atom_info body malformed)
|
||||
--- @field errors string[]|nil -- Parse-time errors from scan_source (atom_info body malformed)
|
||||
|
||||
--- @class DebugSkipMarker -- sub-shape of scan_source.lua's @class DebugSkipMarker
|
||||
--- @field marker_kind string -- exact marker ident read from source. Only "atom_dbg_skip" (bare) is positive.
|
||||
--- @class DebugSkipMarker -- Sub-shape of scan_source.lua's @class DebugSkipMarker
|
||||
--- @field marker_kind string -- Exact marker ident read from source. Only "atom_dbg_skip" (bare) is positive.
|
||||
--- @field marker_line integer
|
||||
--- @field args string|nil -- trimmed text inside the parens (nil when has_parens is false)
|
||||
--- @field args string|nil -- Trimmed text inside the parens (nil when has_parens is false)
|
||||
--- @field has_parens boolean
|
||||
--- @field is_bare boolean -- true iff marker_kind == "atom_dbg_skip" AND has_parens == false (the only positive form)
|
||||
--- @field pending boolean -- true while awaiting the following declaration
|
||||
--- @field superseded_by_marker_line integer|nil -- set on a marker that was bumped out of the pending slot
|
||||
--- @field superseded_by_marker_line integer|nil -- Set on a marker that was bumped out of the pending slot
|
||||
--- @field target_kind string|nil -- "atom" | "comp_bare" | "comp_proc" | "unrelated" once observed
|
||||
|
||||
--- @class Finding
|
||||
--- @field line integer -- source line (or 0 for pass-level)
|
||||
--- @field msg string -- finding message
|
||||
--- @field line integer -- Source line (or 0 for pass-level)
|
||||
--- @field msg string -- Finding message
|
||||
|
||||
--- @class Findings
|
||||
--- @field errors Finding[]
|
||||
@@ -74,14 +74,14 @@ local duffle = dofile(_bootstrap_dir .. "../duffle_paths.lua")
|
||||
--- @field info Finding[]
|
||||
|
||||
--- @class PipeCtx
|
||||
--- @field atom_index table<string, AtomAnnotation> -- name -> AtomAnnotation (only kind=="atom")
|
||||
--- @field binds_index table<string, BindsStruct> -- name -> BindsStruct
|
||||
--- @field annot_counts table<string, integer> -- name -> annotation count (for unique_annotation check)
|
||||
--- @field types table<string, RegTypeDefault> -- from scan_source
|
||||
--- @field atom_views table<string, AtomViewEntry> -- from scan_source
|
||||
--- @field seen_defaults table<string, integer> -- duplicate atom_dbg_reg_default detection
|
||||
--- @field atom_index table<string, AtomAnnotation> -- Name -> AtomAnnotation (only kind=="atom")
|
||||
--- @field binds_index table<string, BindsStruct> -- Name -> BindsStruct
|
||||
--- @field annot_counts table<string, integer> -- Name -> annotation count (for unique_annotation check)
|
||||
--- @field types table<string, RegTypeDefault> -- From scan_source
|
||||
--- @field atom_views table<string, AtomViewEntry> -- From scan_source
|
||||
--- @field seen_defaults table<string, integer> -- Duplicate atom_dbg_reg_default detection
|
||||
--- @field seen_field table<string, integer> -- Binds_* -> count of fields (set/checked by check_binds_no_duplicate_fields)
|
||||
--- @field _scan SourceScan -- full scan payload (typed-view sub-calls live here)
|
||||
--- @field _scan SourceScan -- Full scan payload (typed-view sub-calls live here)
|
||||
|
||||
--- @class AnnotatedResult
|
||||
--- @field atoms AtomEntry[]
|
||||
@@ -95,11 +95,10 @@ local duffle = dofile(_bootstrap_dir .. "../duffle_paths.lua")
|
||||
-- ════════════════════════════════════════════════════════════════════════════
|
||||
-- Per-check functions (the CHECK_RULES table's payload)
|
||||
-- ════════════════════════════════════════════════════════════════════════════
|
||||
--
|
||||
--- The dispatcher in `validate()` routes each result by convention: existence checks write errors[] and shape checks write warnings[].
|
||||
--- `macro_word_drift` writes errors[] for missing or mismatched metadata and info[] for a match.
|
||||
|
||||
--- Check: every annotated atom must have a matching MipsAtom_(name) declaration.
|
||||
--- Check: Every annotated atom must have a matching MipsAtom_(name) declaration.
|
||||
--- @param a AtomAnnotation
|
||||
--- @param pipe_ctx PipeCtx
|
||||
--- @param findings Findings
|
||||
@@ -112,8 +111,8 @@ local function check_atom_decl_exists(a, pipe_ctx, findings)
|
||||
end
|
||||
end
|
||||
|
||||
--- Check: every atom may have AT MOST ONE annotation.
|
||||
--- Post-loop: needs full-corpus `annot_counts` from pipe_ctx.
|
||||
--- Check: Every atom may have AT MOST ONE annotation.
|
||||
--- Post-loop: Needs full-corpus `annot_counts` from pipe_ctx.
|
||||
--- @param pipe_ctx PipeCtx
|
||||
--- @param findings Findings
|
||||
local function check_unique_annotation(pipe_ctx, findings)
|
||||
@@ -146,7 +145,7 @@ end
|
||||
--- Check: TAPE_WORDS(mac_X, N) ↔ WORD_COUNT(mac_X, N) drift.
|
||||
--- Three outcomes: missing (error), mismatch (error), match (info).
|
||||
--- @param m MacroEntry
|
||||
--- @param wc table<string, integer> -- the shared word-count table (from ctx.shared.word_counts)
|
||||
--- @param wc table<string, integer> -- Shared word-count table (from ctx.shared.word_counts)
|
||||
--- @param findings Findings
|
||||
local function check_macro_word_drift(m, wc, findings)
|
||||
local declared = wc[m.name]
|
||||
@@ -304,7 +303,7 @@ local function check_binds_no_duplicate_fields(_src, pipe_ctx, findings)
|
||||
end
|
||||
end
|
||||
|
||||
-- Check: debug-skip markers must satisfy shape + placement constraints.
|
||||
-- Check: Debug-skip markers must satisfy shape + placement constraints.
|
||||
--- Walks the priority list once; each marker produces at most one error, so one source defect yields one finding.
|
||||
--- Priority order (first defect wins):
|
||||
--- 1. marker_kind ~= "atom_dbg_skip" -> legacy/renamed spelling (use `atom_dbg_skip`)
|
||||
@@ -315,12 +314,11 @@ end
|
||||
--- 6. unsupported target_kind -> marker precedes an unrelated declaration
|
||||
--- Valid markers stamp `debug_skip` on whole-atom, bare-component, and proc-component declaration records in scan_source.lua.
|
||||
--- @param marker DebugSkipMarker
|
||||
--- @param _pipe_ctx PipeCtx -- unused today; kept for plex-shape consistency with per_annot
|
||||
--- @param _pipe_ctx PipeCtx -- Unused; kept for consistency with per_annot // TODO(Ed): Remove?
|
||||
--- @param findings Findings
|
||||
local function check_skip_marker(marker, _pipe_ctx, findings)
|
||||
local kind = marker.marker_kind
|
||||
local line = marker.marker_line
|
||||
|
||||
-- Left `scan.debug_skip_markers` with production records for `atom_dbg_skip` only; other identifiers take the walker's unrelated branch.
|
||||
|
||||
if marker.has_parens then
|
||||
@@ -371,8 +369,6 @@ local function check_skip_marker(marker, _pipe_ctx, findings)
|
||||
end
|
||||
|
||||
--- Warn when a source references an unregistered alias.
|
||||
---
|
||||
--- R_TapePtr, R_AtomJmp, R_PrimCursor, R_FaceCursor, R_VertBase, and R_OtBase opt in through `#define atom_reg` in lottes_tape.h.
|
||||
--- When a source uses an unregistered R_X, this check emits one pass-level info entry for that source and directs C-ABI register names to explicit alias registration.
|
||||
--- @param _src SourceFile
|
||||
--- @param pipe_ctx PipeCtx
|
||||
@@ -426,8 +422,7 @@ local CHECK_RULES = {
|
||||
-- ════════════════════════════════════════════════════════════════════════════
|
||||
-- Validation
|
||||
-- ════════════════════════════════════════════════════════════════════════════
|
||||
--
|
||||
-- Pure check: read from src.scan, run validations, emit findings. The scan was done once upstream.
|
||||
-- Pure check: Read from src.scan, run validations, emit findings. The scan was done once upstream.
|
||||
|
||||
--- Builds one pass-wide pipe_ctx from the merged `corpus.*` registries and source-ordered `corpus.atom_infos`; per-source declarations and bodies remain in `src.scan`.
|
||||
--- The module ownership contract above requires callers to construct `ctx.shared.corpus` through `build_ctx`; the error message below enforces that gate.
|
||||
@@ -473,7 +468,7 @@ 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|nil -- built once per pass from corpus registries; nil builds the same projection here.
|
||||
--- @param corpus_pipe_ctx PipeCtx|nil -- Built once per pass from corpus registries; nil builds the same projection here.
|
||||
--- @return AnnotatedResult
|
||||
local function validate(ctx, src, corpus_pipe_ctx)
|
||||
corpus_pipe_ctx = corpus_pipe_ctx or build_corpus_pipe_ctx(ctx)
|
||||
@@ -503,14 +498,8 @@ local function validate(ctx, src, corpus_pipe_ctx)
|
||||
end
|
||||
|
||||
-- Build a per-source pipe_ctx: shared lookups come from `corpus_pipe_ctx`, while declarations, bodies, types, views, defaults, and occurrences come from `src.scan`.
|
||||
local seen_defaults = {}
|
||||
for reg, _ in pairs(scan.types or {}) do
|
||||
seen_defaults[reg] = (seen_defaults[reg] or 0) + 1
|
||||
end
|
||||
local atom_infos_list = {}
|
||||
for _, ai in ipairs(scan.atom_infos or {}) do
|
||||
atom_infos_list[#atom_infos_list + 1] = ai
|
||||
end
|
||||
local seen_defaults = {}; for reg, _ in pairs (scan.types or {}) do seen_defaults[reg] = (seen_defaults[reg] or 0) + 1 end
|
||||
local atom_infos_list = {}; for _, ai in ipairs(scan.atom_infos or {}) do atom_infos_list[#atom_infos_list + 1] = ai end
|
||||
|
||||
local pipe_ctx = {
|
||||
atom_index = {},
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
--- passes/atoms_source_map.lua — Per-.word source-line map emitter for tape atoms.
|
||||
---
|
||||
--- Writer: this pass, given `atom.paths` (the per-atom mutable surface owned by `emission_model`). Readers:
|
||||
--- `passes/dwarf_injection.lua` (synthesizes DW_TAG_inlined_subroutine + per-word line program rows) and the gdb-runtime
|
||||
--- wrapper at `scripts/gdb/gdb_tape_atoms.gdb` (loads the source map via `source <path>`).
|
||||
--- `passes/dwarf_injection.lua` (synthesizes DW_TAG_inlined_subroutine + per-word line program rows) and
|
||||
--- the gdb-runtime wrapper at `scripts/gdb/gdb_tape_atoms.gdb` (loads the source map via `source <path>`).
|
||||
---
|
||||
--- Inputs from `atom.paths`: the ordered `items` stream, dense `word_events`, `invocations` views. Outputs:
|
||||
--- one `WORD N LINE L TEXT T` line per emitted `.word`, plus the per-word provenance form that DWARF synthesis consumes.
|
||||
@@ -28,7 +28,6 @@
|
||||
--- ...
|
||||
--- ENDATOM
|
||||
--- ```
|
||||
---
|
||||
--- Marker records are zero-width in `atom.paths.items`, so they emit no WORD rows in the dense word view.
|
||||
|
||||
-- ════════════════════════════════════════════════════════════════════════════
|
||||
@@ -39,8 +38,8 @@
|
||||
-- (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 elf_dwarf = require("elf_dwarf")
|
||||
local duffle = dofile(_bootstrap_dir .. "../duffle_paths.lua")
|
||||
local elf_dwarf = require("elf_dwarf")
|
||||
|
||||
-- ════════════════════════════════════════════════════════════════════════════
|
||||
-- Constants
|
||||
@@ -69,8 +68,8 @@ local FORMAT_VERSION = 1
|
||||
--- @param atom table
|
||||
--- @return table[], integer
|
||||
local function canonical_word_entries(atom)
|
||||
local paths = atom.paths or {}
|
||||
local events = paths.word_events or {}
|
||||
local paths = atom.paths or {}
|
||||
local events = paths.word_events or {}
|
||||
local word_items = {}
|
||||
for _, item in ipairs(paths.items or {}) do
|
||||
if item.kind == "word" then word_items[#word_items + 1] = item end
|
||||
@@ -95,41 +94,38 @@ end
|
||||
--- Render one atom's provenance stanza. Format 1 line shapes:
|
||||
--- `WORD N CALL <src-path>:<src-line> MACRO <name> "<def-path>:<def-line>" BODY <line>` (component invocation)
|
||||
--- `WORD N CALL <src-path>:<src-line> RAW` (raw `.word` outside any mac_* component)
|
||||
--- Component identity comes from the outermost invocation record; the count-table lookup confirms the component was
|
||||
--- declared in `corpus.word_counts` (populated by word_count_eval + components passes).
|
||||
--- @param src table
|
||||
--- Component identity comes from the outermost invocation record; the count-table lookup confirms the component was declared in `corpus.word_counts`
|
||||
--- (populated by word_count_eval + components passes).
|
||||
--- @param src table
|
||||
--- @param atom table
|
||||
--- @param wc table -- identity alias of corpus.word_counts
|
||||
--- @param wc table -- identity alias of corpus.word_counts
|
||||
--- @return string[], integer
|
||||
local function emit_provenance_stanza(src, atom, wc)
|
||||
local lines = {}
|
||||
local rel_path = src.path:gsub("\\\\", "/")
|
||||
local lines = {}
|
||||
local rel_path = src.path:gsub("\\\\", "/")
|
||||
local entries, total = canonical_word_entries(atom)
|
||||
|
||||
lines[#lines + 1] = string.format('ATOM %s "%s" 0', atom.raw_name or atom.name, rel_path)
|
||||
|
||||
for _, entry in ipairs(entries) do
|
||||
local inv = entry.invocation
|
||||
local inv = entry.invocation
|
||||
local macro_count = inv and wc["mac_" .. inv.component_name]
|
||||
if inv and macro_count ~= nil then
|
||||
lines[#lines + 1] = string.format(
|
||||
'WORD %d CALL %s:%d MACRO %s "%s:%d" BODY %d',
|
||||
entry.pos, rel_path, entry.line, inv.component_name,
|
||||
inv.def_path or "", inv.def_line or 0, entry.body_line)
|
||||
lines[#lines + 1] = string.format('WORD %d CALL %s:%d MACRO %s "%s:%d" BODY %d'
|
||||
, entry.pos, rel_path, entry.line, inv.component_name
|
||||
, inv.def_path or "", inv.def_line or 0, entry.body_line)
|
||||
else
|
||||
lines[#lines + 1] = string.format(
|
||||
"WORD %d CALL %s:%d RAW", entry.pos, rel_path, entry.line)
|
||||
lines[#lines + 1] = string.format("WORD %d CALL %s:%d RAW", entry.pos, rel_path, entry.line)
|
||||
end
|
||||
end
|
||||
|
||||
lines[1] = lines[1]:gsub(" 0$", " " .. tostring(total))
|
||||
lines[1] = lines[1]:gsub(" 0$", " " .. tostring(total))
|
||||
lines[#lines + 1] = "ENDATOM"
|
||||
return lines, total
|
||||
end
|
||||
|
||||
--- Render the full provenance file content for one source.
|
||||
--- @param src table
|
||||
--- @param wc table
|
||||
--- @param wc table
|
||||
--- @return string
|
||||
local function render_provenance(src, wc)
|
||||
local lines = {}
|
||||
@@ -162,8 +158,8 @@ end
|
||||
--- @param wc table
|
||||
--- @return string[], integer
|
||||
local function emit_atom_stanza(src, atom)
|
||||
local lines = {}
|
||||
local rel_path = src.path:gsub("\\\\", "/")
|
||||
local lines = {}
|
||||
local rel_path = src.path:gsub("\\\\", "/")
|
||||
local entries, total = canonical_word_entries(atom)
|
||||
|
||||
lines[#lines + 1] = string.format('ATOM %s "%s" 0', atom.raw_name or atom.name, rel_path)
|
||||
@@ -179,10 +175,10 @@ end
|
||||
--- Render the full source map file content for one source (one .atoms.sourcemap.txt per source). Mirrors offsets.lua's
|
||||
--- `project_atoms` shape: scan.atoms + scan.raw_atoms, no kind filter.
|
||||
--- @param src table
|
||||
--- @param wc table
|
||||
--- @param wc table
|
||||
--- @return string
|
||||
local function render_source_map(src)
|
||||
local lines = {}
|
||||
local lines = {}
|
||||
lines[#lines + 1] = "# FORMAT_VERSION " .. FORMAT_VERSION
|
||||
lines[#lines + 1] = "# auto-generated by ps1_meta.lua (passes/atoms_source_map.lua) — DO NOT EDIT"
|
||||
|
||||
@@ -216,16 +212,16 @@ end
|
||||
--- @param ctx PassCtx
|
||||
--- @return table[] -- list of {idx, name, src_path, file_base, addr, size_bytes, words, entries}
|
||||
local function build_atom_table(ctx)
|
||||
local addrs = elf_dwarf.read_nm(ctx.flags.elf_path)
|
||||
local corpus = ctx.shared and ctx.shared.corpus
|
||||
local addrs = elf_dwarf.read_nm(ctx.flags.elf_path)
|
||||
local corpus = ctx.shared and ctx.shared.corpus
|
||||
local matched = {}
|
||||
|
||||
for _, src in ipairs(corpus.source_order or {}) do
|
||||
local file_base = src.path:match("([^/\\\\]+)$") or src.path
|
||||
local function append(atom)
|
||||
if not atom.paths then return end
|
||||
local name = atom.raw_name or atom.name
|
||||
local info = addrs[name]
|
||||
local name = atom.raw_name or atom.name
|
||||
local info = addrs[name]
|
||||
if not info then return end
|
||||
local entries, total = canonical_word_entries(atom)
|
||||
matched[#matched + 1] = {
|
||||
@@ -248,9 +244,10 @@ local function build_atom_table(ctx)
|
||||
return matched
|
||||
end
|
||||
|
||||
--- Append the 9 gdb command definitions to `lines`. Pure gdb scripting — addresses come from `nm`, the convenience
|
||||
--- vars set in `emit_gdb_runtime` provide printf args, and each command is a static sequence of `printf` / `tbreak` /
|
||||
--- `if ... end` blocks. The Lua pass emits N atoms' worth of lines; runtime iteration is gdb's job.
|
||||
--- Append the 9 gdb command definitions to `lines`. Pure gdb scripting — addresses come from `nm`,
|
||||
--- the convenience vars set in `emit_gdb_runtime` provide printf args, and
|
||||
--- each command is a static sequence of `printf` / `tbreak` / `if ... end` blocks.
|
||||
--- The Lua pass emits N atoms' worth of lines; runtime iteration is gdb's job.
|
||||
---
|
||||
--- Why hardcoded per-atom: gdb's `$` substitution doesn't concat inside var names — `$__atom_name_$__i` in a `while`
|
||||
--- loop resolves to one literal identifier, not `name_i`. Compile-time emission is the only path.
|
||||
@@ -395,8 +392,7 @@ local function append_gdb_commands(lines, matched)
|
||||
end
|
||||
|
||||
--- Emit the gdb-runtime file (post-link). Pure gdb scripting — addresses come from `mipsel-none-elf-nm -S`, get embedded
|
||||
--- in `<ctx.out_root>/gdb_tape_atoms_runtime.gdb`, and load via `set $var = ...` + `define ... end` blocks at gdb
|
||||
--- source-time.
|
||||
--- in `<ctx.out_root>/gdb_tape_atoms_runtime.gdb`, and load via `set $var = ...` + `define ... end` blocks at gdb source-time.
|
||||
--- @param ctx PassCtx
|
||||
local function emit_gdb_runtime(ctx)
|
||||
if not (ctx.flags and ctx.flags.gdb_runtime) then return end
|
||||
|
||||
Binary file not shown.
+37
-48
@@ -16,7 +16,7 @@
|
||||
|
||||
-- Bootstrap: load `duffle_paths.lua` via this script's own path.
|
||||
-- Use `arg[0]` when this file is the entry script (`arg[0]` ends in "ps1_meta.lua");
|
||||
-- fall back to `debug.getinfo(1, "S").source` when this file is being dofile()'d or require()'d (in which case `arg[0]` is the *caller's* path, not ours).
|
||||
-- fall back to `debug.getinfo(1, "S").source` when this file is being dofile()'d or require()'d (in which case `arg[0]` is the *caller's* path).
|
||||
-- That single statement: (a) sets `package.path` + `package.cpath`, (b) at the bottom returns `require("duffle")`.
|
||||
-- So the dofile's return value is the duffle module.
|
||||
local _is_entry_script = arg and arg[0] and arg[0]:match("ps1_meta%.lua$") ~= nil
|
||||
@@ -54,45 +54,44 @@ local PASS_FLAG_DISPATCH_KEY = "__pass__"
|
||||
-- ════════════════════════════════════════════════════════════════════════════
|
||||
|
||||
--- @class PassDescriptor
|
||||
--- @field module string -- module name passed to require()
|
||||
--- @field module string -- Module name passed to require()
|
||||
--- @field kind string -- "shared" | "header-output" | "validation" | "diagnostic" | "report"
|
||||
--- -- Report severity is independent from process exit policy (see PASS_KIND_STOP_ON_ERROR).
|
||||
--- @field deps string[] -- names of upstream passes
|
||||
--- @field groups string[]? -- OPTIONAL build-phase groups this pass is a root of
|
||||
--- -- (e.g. { "pre-link" }, { "post-link" }); absent ⇒ dependency-only
|
||||
--- @field deps string[] -- Names of upstream passes
|
||||
--- @field groups string[]? -- OPTIONAL build-phase groups this pass is a root of (e.g. { "pre-link" }, { "post-link" }); absent ⇒ dependency-only
|
||||
|
||||
--- @class SourceFile
|
||||
--- @field path string -- absolute path to the source file
|
||||
--- @field text string -- the full source text
|
||||
--- @field dir string -- the directory containing the source
|
||||
--- @field basename string -- filename without extension
|
||||
--- @field path string -- Absolute path to the source file
|
||||
--- @field text string -- Full source text
|
||||
--- @field dir string -- Directory containing the source
|
||||
--- @field basename string -- Filename without extension
|
||||
|
||||
--- @class PassCtx
|
||||
--- @field metadata_path string -- path to word_count.metadata.h
|
||||
--- @field shared table -- cross-pass shared state
|
||||
--- @field shared.corpus table -- canonical authored-source/project projection
|
||||
--- @field out_root string -- output root (e.g. "build/gen")
|
||||
--- @field metadata_path string -- Path to word_count.metadata.h
|
||||
--- @field shared table -- Cross-pass shared state
|
||||
--- @field shared.corpus table -- Authored-source/project projection
|
||||
--- @field out_root string -- Output root (e.g. "build/gen")
|
||||
--- @field project_root string -- PS1 repository root
|
||||
--- @field flags table -- CLI flags + per-pass stash
|
||||
--- @field verbose boolean -- if true, log diagnostic info
|
||||
--- @field verbose boolean -- If true, log diagnostic info
|
||||
|
||||
--- @class Finding
|
||||
--- @field line integer -- source line (or 0 for pass-level)
|
||||
--- @field msg string -- finding message
|
||||
--- @field line integer -- Source line (or 0 for pass-level)
|
||||
--- @field msg string -- Finding message
|
||||
|
||||
--- @class PassResult
|
||||
--- @field outputs PassOutputEntry[] -- emitted file paths
|
||||
--- @field errors Finding[] -- build-stops (per-pass kind policy)
|
||||
--- @field warnings Finding[] -- informational
|
||||
--- @field outputs PassOutputEntry[] -- Emitted file paths
|
||||
--- @field errors Finding[] -- Build-stops (per-pass kind policy)
|
||||
--- @field warnings Finding[] -- Informational
|
||||
|
||||
--- @class ParsedArgs
|
||||
--- @field requested_set string[] -- pass names to run (explicit --all expanded)
|
||||
--- @field sources string[] -- exact --source values, retained in CLI order
|
||||
--- @field requested_set string[] -- Pass names to run (explicit --all expanded)
|
||||
--- @field sources string[] -- Exact --source values, retained in CLI order
|
||||
--- @field unity_root string|nil -- --unity-root value; mutually exclusive with sources
|
||||
--- @field metadata string -- --metadata value
|
||||
--- @field out_root string -- --out-root value (default "build/gen")
|
||||
--- @field project_root string -- PS1 repository root (derived from metadata by default)
|
||||
--- @field verbose boolean -- if true, log diagnostic info
|
||||
--- @field verbose boolean -- If true, log diagnostic info
|
||||
|
||||
-- ════════════════════════════════════════════════════════════════════════════
|
||||
-- PASSES Table
|
||||
@@ -138,7 +137,7 @@ local PASSES = {
|
||||
["static-analysis"] = {
|
||||
module = "passes.static_analysis",
|
||||
-- "diagnostic" — every `error`/`warning` finding is written to the report file;
|
||||
-- the orchestrator does NOT exit non-zero on these findings (see PASS_KIND_STOP_ON_ERROR).
|
||||
-- The orchestrator does NOT exit non-zero on these findings (see PASS_KIND_STOP_ON_ERROR).
|
||||
-- Report severity is independent from process exit policy.
|
||||
kind = "diagnostic",
|
||||
deps = {"scan-source", "word-counts", "components", "emission-model"},
|
||||
@@ -163,12 +162,12 @@ local PASSES = {
|
||||
}
|
||||
|
||||
-- ────────────────────────────────────────────────────────────────────────────
|
||||
-- Phase-root selection: derive the sorted set of roots belonging to a named build-phase group, then append them to `args.requested_set`.
|
||||
-- Phase-root selection: Derive the sorted set of roots belonging to a named build-phase group, then append them to `args.requested_set`.
|
||||
-- topo_sort closes the transitive deps from there; dispatch_passes runs every resolved pass without phase-filtering.
|
||||
-- ────────────────────────────────────────────────────────────────────────────
|
||||
|
||||
--- @param group_name string -- the build-phase group ("pre-link" | "post-link")
|
||||
--- @return string[] -- sorted root pass names belonging to that group
|
||||
--- @param group_name string -- Build-phase group ("pre-link" | "post-link")
|
||||
--- @return string[] -- Sorted root pass names belonging to that group
|
||||
local function roots_for_group(group_name)
|
||||
local names = {}
|
||||
for name, pass in pairs(PASSES) do
|
||||
@@ -206,7 +205,7 @@ end
|
||||
-- Report severity is independent from process exit policy.
|
||||
-- A "diagnostic" pass still writes every `error`/`warning` finding into its report file,
|
||||
-- but `report_validation_errors` returns early for non-stopping kinds, so nothing is printed to stderr and the orchestrator does not exit non-zero.
|
||||
-- Adding a new pass kind requires listing it here explicitly; an unknown kind must not silently fall back to "true".
|
||||
-- Adding a new pass kind requires listing it here explicitly; An unknown kind must not silently fall back to "true".
|
||||
local PASS_KIND_STOP_ON_ERROR = {
|
||||
["shared"] = false,
|
||||
["header-output"] = true,
|
||||
@@ -216,8 +215,7 @@ local PASS_KIND_STOP_ON_ERROR = {
|
||||
}
|
||||
|
||||
-- Closed set of CLI flags -> pass names.
|
||||
-- Per-pass flags (e.g. --word-counts) live here; phase flags (--pre-link, --post-link, --all)
|
||||
-- live in FLAG_HANDLERS because they own side effects or invoke group-derivation logic.
|
||||
-- Per-pass flags (e.g. --word-counts); phase flags (--pre-link, --post-link, --all) are within FLAG_HANDLERS because they own side effects or invoke group-derivation logic.
|
||||
-- dwarf-injection is *also* a per-pass opt-in flag, but its selection + opt-in state are both owned by the explicit FLAG_HANDLERS entry below
|
||||
-- (it sets args.flags.dwarf_injection and appends "dwarf-injection" to requested_set), so it is intentionally absent from this table.
|
||||
local PASS_FLAG_TO_NAME = {
|
||||
@@ -246,7 +244,6 @@ end
|
||||
|
||||
-- Per-flag handlers. Each handler takes (args, argv, arg_idx) and returns the new arg_idx (so multi-arg flags like --source FILE advance it).
|
||||
-- Returning nil + os.exit() handles termination flags (--help).
|
||||
|
||||
local FLAG_HANDLERS = {}
|
||||
|
||||
-- ════════════════════════════════════════════════════════════════════════════
|
||||
@@ -317,8 +314,7 @@ local function require_flag_value(argv, arg_idx, flag)
|
||||
local next_known = type(value) == "string"
|
||||
and (FLAG_HANDLERS[value] ~= nil or PASS_FLAG_TO_NAME[value] ~= nil)
|
||||
if value == nil or next_known then
|
||||
io.stderr:write("ps1_meta: " .. flag .. " requires "
|
||||
.. FLAG_VALUE_NAMES[flag] .. "\n")
|
||||
io.stderr:write("ps1_meta: " .. flag .. " requires " .. FLAG_VALUE_NAMES[flag] .. "\n")
|
||||
os.exit(EXIT_INTERNAL_ERROR)
|
||||
end
|
||||
return value, arg_idx + 1
|
||||
@@ -328,12 +324,10 @@ end
|
||||
-- Termination flags like --help call os.exit() instead.
|
||||
-- Populated AFTER print_help so the --help handler can reference it as an upvalue (Lua resolves locals at closure-call time,
|
||||
-- but if the closure is defined before the local, it falls back to _G).
|
||||
FLAG_HANDLERS["--help"] = function(args)
|
||||
print_help()
|
||||
os.exit(0)
|
||||
end
|
||||
|
||||
FLAG_HANDLERS["--verbose"] = function(args) args.verbose = true end
|
||||
FLAG_HANDLERS["--help"] = function(args) print_help(); os.exit(0) end
|
||||
FLAG_HANDLERS["--verbose"] = function(args) args.verbose = true end
|
||||
|
||||
FLAG_HANDLERS["--source"] = function(args, argv, arg_idx)
|
||||
local value, value_idx = require_flag_value(argv, arg_idx, "--source")
|
||||
args.sources[#args.sources + 1] = value
|
||||
@@ -490,14 +484,13 @@ end
|
||||
--- @param args ParsedArgs
|
||||
--- @return PassCtx
|
||||
local function build_ctx(args)
|
||||
local normalized_project_root = duffle.normalize_path(args.project_root)
|
||||
local project_root = normalized_project_root
|
||||
local normalized_project_root = duffle.normalize_path(args.project_root)
|
||||
local project_root = normalized_project_root
|
||||
local project_root_is_absolute = normalized_project_root:match("^%a:/")
|
||||
or normalized_project_root:sub(1, 2) == "//"
|
||||
or normalized_project_root:sub(1, 1) == "/"
|
||||
if not project_root_is_absolute then
|
||||
-- canonical_path_key validates ordinary relative paths and rejects
|
||||
-- drive-relative paths before the absolute-path rewrite is performed.
|
||||
-- canonical_path_key validates ordinary relative paths and rejects drive-relative paths before the absolute-path rewrite is performed.
|
||||
duffle.canonical_path_key(normalized_project_root)
|
||||
project_root = duffle.normalize_path(duffle.to_absolute_path(normalized_project_root))
|
||||
else
|
||||
@@ -511,8 +504,7 @@ local function build_ctx(args)
|
||||
project_root = project_root,
|
||||
})
|
||||
if not ok_resolve then
|
||||
io.stderr:write("ps1_meta: cannot resolve --unity-root "
|
||||
.. tostring(args.unity_root) .. ": " .. tostring(resolved) .. "\n")
|
||||
io.stderr:write("ps1_meta: cannot resolve --unity-root " .. tostring(args.unity_root) .. ": " .. tostring(resolved) .. "\n")
|
||||
os.exit(EXIT_INTERNAL_ERROR)
|
||||
end
|
||||
resolution = resolved
|
||||
@@ -528,8 +520,7 @@ local function build_ctx(args)
|
||||
local path = duffle.normalize_path(input_path)
|
||||
local key_ok, key_or_error = pcall(duffle.canonical_path_key, path)
|
||||
if not key_ok then
|
||||
error("ps1_meta: invalid --source " .. input_path .. ": "
|
||||
.. tostring(key_or_error), 0)
|
||||
error("ps1_meta: invalid --source " .. input_path .. ": " .. tostring(key_or_error), 0)
|
||||
end
|
||||
local file = io.open(path, "r")
|
||||
if not file then
|
||||
@@ -627,9 +618,7 @@ local function topo_sort(passes, requested_set)
|
||||
changed = false
|
||||
for name, _ in pairs(needed) do
|
||||
local pass = passes[name]
|
||||
if not pass then
|
||||
error("unknown pass '" .. name .. "' requested")
|
||||
end
|
||||
if not pass then error("unknown pass '" .. name .. "' requested") end
|
||||
for _, dep in ipairs(pass.deps) do
|
||||
if not needed[dep] then
|
||||
needed[dep] = true
|
||||
|
||||
Reference in New Issue
Block a user