dealing with this mess still.

This commit is contained in:
ed
2026-07-10 23:36:44 -04:00
parent 798807a9c2
commit 0d94632edf
8 changed files with 529 additions and 600 deletions
+61 -72
View File
@@ -1,5 +1,4 @@
--- audit_lua_nesting.lua — Walk Lua source files and flag any block --- audit_lua_nesting.lua — Walk Lua source files and flag any block nesting deeper than 5 levels.
--- nesting deeper than 5 levels.
--- ---
--- Usage: --- Usage:
--- luajit scripts/audit_lua_nesting.lua scripts/duffle.lua scripts/ps1_meta.lua --- luajit scripts/audit_lua_nesting.lua scripts/duffle.lua scripts/ps1_meta.lua
@@ -11,11 +10,10 @@
--- **Implementation**: a hand-rolled depth tracker that counts: --- **Implementation**: a hand-rolled depth tracker that counts:
--- - `do`, `function`, `if`, `for`, `while`, `repeat` -> depth +1 --- - `do`, `function`, `if`, `for`, `while`, `repeat` -> depth +1
--- - `end`, `until` -> depth -1 --- - `end`, `until` -> depth -1
--- - `else`, `elseif` -> depth unchanged --- - `else`, `elseif` -> depth unchanged
--- ---
--- **Caveats**: doesn't handle string/comment state (will miscount --- **Caveats**: doesn't fully handle string/comment state (will miscount braces inside multi-line strings or block comments).
--- braces inside strings). For our metaprogram files (no embedded --- For our metaprogram files (no embedded code generation), this is acceptable.
--- code generation), this is acceptable.
local M = {} local M = {}
@@ -28,115 +26,107 @@ local BLOCK_OPEN = {
["repeat"] = true, ["repeat"] = true,
} }
local function is_block_close(token) local function is_block_close(token) return token == "end" or token == "until" end
return token == "end" or token == "until"
end
-- (internal) Walk one source file and return a list of -- (internal) Walk one source file and return a list of
-- {line, depth, token} entries where depth > MAX_NESTING. -- {line, depth, token} entries where depth > max_nesting.
local function audit_file(path, max_nesting) local function audit_file(path, max_nesting)
local f = io.open(path, "r") local f = io.open(path, "r")
if not f then if not f then error("Cannot open " .. path) end
error("Cannot open " .. path)
end
local content = f:read("*a") local content = f:read("*a")
f:close() f:close()
local violations = {} local violations = {}
local depth = 0 local depth = 0
local line = 1 local line = 1
local pos = 1 local pos = 1
local len = #content local src_len = #content
local token_start = 0 local token_idx = 0
local function read_ident_at(p) local function read_ident_at(start_pos)
-- Lua ident: [a-zA-Z_][a-zA-Z0-9_]* local ident_start = start_pos
local start = p if ident_start > src_len then return nil end
if start > len then return nil end local first_ch = content:sub(ident_start, ident_start)
local ch = content:sub(start, start) if not (first_ch:match("[%a_]")) then return nil end
if not (ch:match("[%a_]")) then return nil end local scan = start_pos + 1
p = p + 1 while scan <= src_len do
while p <= len do local ch = content:sub(scan, scan)
local c = content:sub(p, p) if not (ch:match("[%w_]")) then break end
if not (c:match("[%w_]")) then break end scan = scan + 1
p = p + 1
end end
return content:sub(start, p - 1), p return content:sub(ident_start, scan - 1), scan
end end
local function skip_string_or_comment(p) -- Skip past a string literal or comment starting at `start_pos`.
local ch = content:sub(p, p) -- Returns the position just past the construct, or nil if `start_pos`
-- is not the start of a string/comment.
local function skip_string_or_comment(start_pos)
local ch = content:sub(start_pos, start_pos)
if ch == '"' or ch == "'" then if ch == '"' or ch == "'" then
-- String literal: skip to matching end-quote. local scan = start_pos + 1
p = p + 1 while scan <= src_len do
while p <= len do local c = content:sub(scan, scan)
local c = content:sub(p, p)
if c == "\\" then if c == "\\" then
p = p + 2 scan = scan + 2
elseif c == ch then elseif c == ch then
p = p + 1 return scan + 1
break
else else
p = p + 1 scan = scan + 1
end end
end end
return p return src_len + 1
elseif ch == "-" and content:sub(p + 1, p + 1) == "-" then elseif ch == "-" and content:sub(start_pos + 1, start_pos + 1) == "-" then
-- Lua comment: -- to end of line. local scan = start_pos + 2
p = p + 2 if content:sub(scan, scan + 1) == "[[" and content:sub(scan + 2, scan + 3) == "[" then
if content:sub(p, p + 1) == "[[" and content:sub(p + 2, p + 3) == "[" then
-- Long bracket comment [==[ ... ]==] -- Long bracket comment [==[ ... ]==]
p = p + 2 scan = scan + 2
local eq = "" local eq = ""
while content:sub(p, p) == "=" do while content:sub(scan, scan) == "=" do
eq = eq .. "=" eq = eq .. "="
p = p + 1 scan = scan + 1
end end
local close_marker = "]" .. eq .. "]" local close_marker = "]" .. eq .. "]"
local close_pos = content:find(close_marker, p, true) local close_pos = content:find(close_marker, scan, true)
if close_pos then if close_pos then
p = close_pos + #close_marker return close_pos + #close_marker
else else
p = len + 1 return src_len + 1
end end
else else
while p <= len and content:sub(p, p) ~= "\n" do p = p + 1 end while scan <= src_len and content:sub(scan, scan) ~= "\n" do scan = scan + 1 end
return scan + 1
end end
return p elseif ch == "[" and content:sub(start_pos + 1, start_pos + 1) == "[" then
elseif ch == "[" and content:sub(p + 1, p + 1) == "[" then local scan = start_pos + 2
-- Long bracket string: [==[ ... ]==]
p = p + 2
local eq = "" local eq = ""
while content:sub(p, p) == "=" do while content:sub(scan, scan) == "=" do
eq = eq .. "=" eq = eq .. "="
p = p + 1 scan = scan + 1
end end
local close_marker = "]" .. eq .. "]" local close_marker = "]" .. eq .. "]"
local close_pos = content:find(close_marker, p, true) local close_pos = content:find(close_marker, scan, true)
if close_pos then if close_pos then
p = close_pos + #close_marker return close_pos + #close_marker
else else
p = len + 1 return src_len + 1
end end
return p
end end
return nil return nil
end end
local token_count = 0 while pos <= src_len do
while pos <= len do
local ch = content:sub(pos, pos) local ch = content:sub(pos, pos)
if ch == "\n" then line = line + 1 end if ch == "\n" then line = line + 1 end
local skip_to = skip_string_or_comment(pos) local skip_to = skip_string_or_comment(pos)
if skip_to then if skip_to then
for i = pos, skip_to - 1 do for scan = pos, skip_to - 1 do
if content:sub(i, i) == "\n" then line = line + 1 end if content:sub(scan, scan) == "\n" then line = line + 1 end
end end
pos = skip_to pos = skip_to
elseif ch:match("[%a_]") then elseif ch:match("[%a_]") then
local tok, next_pos = read_ident_at(pos) local tok, next_pos = read_ident_at(pos)
token_count = token_count + 1 token_idx = token_idx + 1
if BLOCK_OPEN[tok] then if BLOCK_OPEN[tok] then
depth = depth + 1 depth = depth + 1
if depth > max_nesting then if depth > max_nesting then
@@ -172,18 +162,17 @@ end
if arg and arg[1] then if arg and arg[1] then
local max_nesting = 5 local max_nesting = 5
local files = {} local files = {}
for i = 1, #arg do for arg_idx = 1, #arg do
if arg[i] == "--max" and arg[i + 1] then if arg[arg_idx] == "--max" and arg[arg_idx + 1] then
max_nesting = tonumber(arg[i + 1]) or 5 max_nesting = tonumber(arg[arg_idx + 1]) or 5
else else
files[#files + 1] = arg[i] files[#files + 1] = arg[arg_idx]
end end
end end
-- Accept either a directory or a file path. Directory args are -- Accept either a directory or a file path. Directory args are
-- expanded via `dir /b *.lua` (Windows) or `ls *.lua` (Unix). -- expanded via `dir /b *.lua` (Windows) or `ls *.lua` (Unix).
local function is_dir(p) local function is_dir(p)
-- Try opening it as a file; if that succeeds, it's not a dir.
local f = io.open(p, "r") local f = io.open(p, "r")
if f then f:close() return false end if f then f:close() return false end
return true return true
+115 -151
View File
@@ -5,20 +5,15 @@
--- - **Character classification** (`is_space`, `is_alpha`, `is_alnum`, `is_digit`, plus the byte-fast `_byte` variants). --- - **Character classification** (`is_space`, `is_alpha`, `is_alnum`, `is_digit`, plus the byte-fast `_byte` variants).
--- - **String primitives** (`trim`, `dirname`, `basename_no_ext`, `find_byte`). --- - **String primitives** (`trim`, `dirname`, `basename_no_ext`, `find_byte`).
--- - **I/O primitives** (`read_file`, `write_file`, `ensure_dir`). --- - **I/O primitives** (`read_file`, `write_file`, `ensure_dir`).
--- - **C-language scanner** (`skip_ws_and_cmt`, `skip_str_or_cmt`, --- - **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`).
--- `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). --- - **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). --- - **Line lookup** (`LineIndex` returns an O(log N) `line_of(pos)` closure for source-mapping).
--- - **Domain tables** (`WAVE_CONTEXT_REGS`, `TAPE_ATOM_MACROS`, --- - **Domain tables** (`WAVE_CONTEXT_REGS`, `TAPE_ATOM_MACROS`, `GTE_PIPELINE_LATENCY`, `GP0_CMD_SIZE`, `GP0_CMD_BY_SHAPE`, `GP0_MACRO_CONTRIB`, `INSTRUCTION_LATENCY`).
--- `GTE_PIPELINE_LATENCY`, `GP0_CMD_SIZE`, `GP0_CMD_BY_SHAPE`, --- - **Process-bootstrap helper** (`setup_package_path`replaces the 8-line `arg[0]`-resolution boilerplate duplicated across 7 entry scripts)
--- `GP0_MACRO_CONTRIB`, `INSTRUCTION_LATENCY`).
--- - **Process-bootstrap helper** (`setup_package_path` — replaces the 8-line `arg[0]`-resolution boilerplate duplicated across 7 entry scripts)
--- ---
--- **Conventions**: tabs (1/level), EmmyLua annotations, no regex. --- **Conventions**: tabs (1/level), EmmyLua annotations, no regex.
--- Lua 5.3 compatible; no `<close>`/`<toclose>`, no `continue`, no --- Lua 5.3 compatible; no `<close>`/`<toclose>`, no `continue`, no
--- 5.4 string.dump improvements. LuaJIT 5.1+extensions model is the --- 5.4 string.dump improvements. LuaJIT 5.1+extensions model is the primary target.
--- primary target.
--- ---
--- **No `:match` / `:gmatch` regex use anywhere**; all delimiter- --- **No `:match` / `:gmatch` regex use anywhere**; all delimiter-
--- splitting is hand-rolled or via LPeg (the regex-free PEG library). --- splitting is hand-rolled or via LPeg (the regex-free PEG library).
@@ -81,30 +76,22 @@ local BYTE_DIGIT_9 = 57 -- '9'
-- Section -1: Bootstrap (path-setup at module load) -- Section -1: Bootstrap (path-setup at module load)
-- ════════════════════════════════════════════════════════════════════════════ -- ════════════════════════════════════════════════════════════════════════════
-- --
-- When duffle.lua is first loaded (via `dofile` from an entry script -- When duffle.lua is first loaded (via `dofile` from an entry script or via `require` from a passes script),
-- or via `require` from a passes script), the code below runs and sets -- the code below sruns and sets `package.path` + `package.cpath` so subsequent `require`s resolve.
-- `package.path` + `package.cpath` so subsequent `require`s resolve.
-- Idempotent: re-loads just re-set the same paths. -- Idempotent: re-loads just re-set the same paths.
-- --
-- **Entry scripts** trigger this with one line: -- **Entry scripts** trigger this with one line:
-- `local duffle = dofile(arg[0]:match("(.*[/\\])") .. "/../duffle.lua")` -- `local duffle = dofile(arg[0]:match("(.*[/\\])") .. "/../duffle.lua")` which runs this top-level + returns `M`.
-- which runs this top-level + returns `M`.
-- --
-- **Passes scripts** are loaded via `require("passes.X")` from the -- **Passes scripts** are loaded via `require("passes.X")` from the entry script; by the time they run,
-- entry script; by the time they run, the entry script has already -- the entry script has already triggered this bootstrap, so the paths are set.
-- triggered this bootstrap, so the paths are set.
--
-- **Why `git rev-parse`?** Hardcoding paths like
-- `C:\\projects\\Pikuma\\ps1\\...` breaks portability. Git gives us
-- the canonical repo root regardless of where it lives.
--- Resolve the repo root via `git rev-parse --show-toplevel` (cached). --- Resolve the repo root via `git rev-parse --show-toplevel` (cached).
--- Returns a path with a trailing separator, or nil if not in a git repo. --- Returns a path with a trailing separator, or nil if not in a git repo.
--- @return string|nil --- @return string|nil
local function find_repo_root() local function find_repo_root()
-- Cached in `package.loaded` (process-global) so all 8 entry scripts -- Cached in `package.loaded` (process-global) so all 8 entry scripts + passes scripts share one git call.
-- + passes scripts share one git call. Without this, git rev-parse -- Without this, git rev-parse runs once per script load.
-- runs once per script load = 8 × ~150ms = 1.2s wasted per build.
if package.loaded.__duffle_repo_root__ then return package.loaded.__duffle_repo_root__ end if package.loaded.__duffle_repo_root__ then return package.loaded.__duffle_repo_root__ end
local p = io.popen("git rev-parse --show-toplevel 2>nul") local p = io.popen("git rev-parse --show-toplevel 2>nul")
local root local root
@@ -118,7 +105,7 @@ local function find_repo_root()
return root return root
end end
--- Set `package.path` (for `require("duffle")` + `require("passes.X")`) --- Set `package.path` (for `require("duffle")` + `require("passes.X")`)
--- and `package.cpath` (for `lpeg.dll` on Windows). --- and `package.cpath` (for `lpeg.dll` on Windows).
function M.setup_package_path() function M.setup_package_path()
local repo_root = find_repo_root() local repo_root = find_repo_root()
@@ -127,8 +114,7 @@ function M.setup_package_path()
os.exit(2) os.exit(2)
end end
-- From the repo root, derive both `scripts/` and `scripts/passes/` -- From the repo root, derive both `scripts/` and `scripts/passes/` so `require("duffle")` AND `require("passes.annotation")` resolve.
-- so `require("duffle")` AND `require("passes.annotation")` resolve.
local scripts_dir = repo_root .. "scripts/" local scripts_dir = repo_root .. "scripts/"
local passes_dir = repo_root .. "scripts/passes/" local passes_dir = repo_root .. "scripts/passes/"
package.path = scripts_dir .. "?.lua;" package.path = scripts_dir .. "?.lua;"
@@ -137,30 +123,25 @@ function M.setup_package_path()
.. passes_dir .. "?/init.lua;" .. passes_dir .. "?/init.lua;"
.. package.path .. package.path
-- cpath: only needed on Windows for the bundled lpeg.dll. (LPeg -- cpath: only needed on Windows for the bundled lpeg.dll.
-- is optional -- duffle.lua's `pcall(require, "lpeg")` falls back -- (LPeg is optional -- duffle.lua's `pcall(require, "lpeg")` falls back to hand-rolled scanners if the .dll isn't loadable.)
-- to hand-rolled scanners if the .dll isn't loadable.)
if package.config:sub(1, 1) == "\\" then if package.config:sub(1, 1) == "\\" then
package.cpath = repo_root .. "toolchain/luajit-2.1/lib/lua/5.1/?.dll;" package.cpath = repo_root .. "toolchain/luajit-2.1/lib/lua/5.1/?.dll;"
.. package.cpath .. package.cpath
end end
end end
-- NOTE: `M.setup_package_path()` is NOT auto-called here. The entry -- NOTE: `M.setup_package_path()` is NOT auto-called here. The entry scripts explicitly `dofile("duffle_paths.lua")` first, which calls `M.setup_package_path()`.
-- scripts explicitly `dofile("duffle_paths.lua")` first, which calls -- The function exists for the helper to use (so the path-setup logic is centralized in duffle.lua).
-- `M.setup_package_path()`. The function exists for the helper to use
-- (so the path-setup logic is centralized in duffle.lua).
-- ════════════════════════════════════════════════════════════════════════════ -- ════════════════════════════════════════════════════════════════════════════
-- Section 0: LPeg patterns (compiled once at module load) -- Section 0: LPeg patterns (compiled once at module load)
-- ════════════════════════════════════════════════════════════════════════════ -- ════════════════════════════════════════════════════════════════════════════
-- --
-- LPeg is a PEG library (no regex). All patterns below are first-class -- LPeg is a PEG library (no regex). All patterns below are first-class pattern values; they're cheap to build and reuse.
-- pattern values; they're cheap to build and reuse.
-- --
-- Note: lpeg is required lazily because Lua 5.5 may not have it on its -- Note: lpeg is required lazily because Lua 5.5 may not have it on its cpath at the same location as LuaJIT.
-- cpath at the same location as LuaJIT. We attempt the require and fall -- We attempt the require and fall back to the hand-rolled implementations if it fails.
-- back to the hand-rolled implementations if it fails.
local lpeg_ok, lpeg = pcall(require, "lpeg") local lpeg_ok, lpeg = pcall(require, "lpeg")
local lpeg_lib = nil local lpeg_lib = nil
@@ -169,13 +150,13 @@ local lpeg_str_or_cmt_pat, lpeg_ws_and_cmt_pat
local lpeg_scan_to_target_pat -- generic "anything but target or balanced group" matcher local lpeg_scan_to_target_pat -- generic "anything but target or balanced group" matcher
if lpeg_ok then if lpeg_ok then
lpeg_lib = lpeg lpeg_lib = lpeg
local P, S, R = lpeg.P, lpeg.S, lpeg.R local P, S, R = lpeg.P, lpeg.S, lpeg.R
-- Character class patterns -- Character class patterns
local alpha_pat = R("AZ", "az") + P("_") local alpha_pat = R("AZ", "az") + P("_")
local digit_pat = R("09") local digit_pat = R("09")
lpeg_alnum_pat = alpha_pat + digit_pat lpeg_alnum_pat = alpha_pat + digit_pat
-- Identifier: alpha followed by zero+ alnum. Capture as a string. -- Identifier: alpha followed by zero+ alnum. Capture as a string.
lpeg_alpha_pat = alpha_pat lpeg_alpha_pat = alpha_pat
@@ -200,9 +181,7 @@ if lpeg_ok then
-- Used by scan_to_char for non-ident / non-bracket chars. -- Used by scan_to_char for non-ident / non-bracket chars.
-- We accept any single char except the target. -- We accept any single char except the target.
-- The balanced-group stepping is handled by the caller (via read_balanced). -- The balanced-group stepping is handled by the caller (via read_balanced).
lpeg_scan_to_target_pat = function(target) lpeg_scan_to_target_pat = function(target) return (P(1) - P(target))^0 end
return (P(1) - P(target))^0
end
end end
-- ════════════════════════════════════════════════════════════════════════════ -- ════════════════════════════════════════════════════════════════════════════
@@ -214,19 +193,16 @@ end
-- is_space_byte(b), is_alpha_byte(b), etc. — accept a single-byte INTEGER -- is_space_byte(b), is_alpha_byte(b), etc. — accept a single-byte INTEGER
-- --
-- The byte-based versions are 5-10x faster in tight loops because they -- The byte-based versions are 5-10x faster in tight loops because they
-- avoid the string allocation per s:sub(i, i) call. -- avoid the string allocation per s:sub(pos, pos) call.
-- Whitespace characters per C locale. -- Whitespace characters per C locale.
function M.is_space_byte(b) function M.is_space_byte(b) return b == BYTE_SPACE or b == BYTE_TAB or b == BYTE_NEWLINE or b == BYTE_CR or b == BYTE_VT or b == BYTE_FF end
return b == BYTE_SPACE or b == BYTE_TAB or b == BYTE_NEWLINE
or b == BYTE_CR or b == BYTE_VT or b == BYTE_FF
end
-- Letters (a-z, A-Z) and underscore. -- Letters (a-z, A-Z) and underscore.
function M.is_alpha_byte(b) function M.is_alpha_byte(b)
if not b then return false end if not b then return false end
if b >= BYTE_LOWER_A and b <= BYTE_LOWER_Z then return true end -- 'a'..'z' if b >= BYTE_LOWER_A and b <= BYTE_LOWER_Z then return true end -- 'a'..'z'
if b >= BYTE_UPPER_A and b <= BYTE_UPPER_Z then return true end -- 'A'..'Z' if b >= BYTE_UPPER_A and b <= BYTE_UPPER_Z then return true end -- 'A'..'Z'
return b == BYTE_UNDERSCORE return b == BYTE_UNDERSCORE
end end
@@ -245,8 +221,8 @@ end
function M.is_alpha(c) function M.is_alpha(c)
if type(c) == "number" then return M.is_alpha_byte(c) end if type(c) == "number" then return M.is_alpha_byte(c) end
if not c or #c == 0 then return false end if not c or #c == 0 then return false end
if c >= "a" and c <= "z" then return true end if c >= "a" and c <= "z" then return true end
if c >= "A" and c <= "Z" then return true end if c >= "A" and c <= "Z" then return true end
return c == "_" return c == "_"
end end
function M.is_digit(c) function M.is_digit(c)
@@ -261,10 +237,8 @@ function M.is_alnum(c) return M.is_alpha(c) or M.is_digit(c) end
-- Trim leading and trailing whitespace from a string. -- Trim leading and trailing whitespace from a string.
function M.trim(s) function M.trim(s)
local a = 1 local a = 1; while a <= #s and M.is_space_byte(s:byte(a)) do a = a + 1 end
while a <= #s and M.is_space_byte(s:byte(a)) do a = a + 1 end local b = #s; while b >= a and M.is_space_byte(s:byte(b)) do b = b - 1 end
local b = #s
while b >= a and M.is_space_byte(s:byte(b)) do b = b - 1 end
return s:sub(a, b) return s:sub(a, b)
end end
@@ -286,7 +260,7 @@ function M.dirname(path)
local last_sep = 0 local last_sep = 0
for pos = 1, #path do for pos = 1, #path do
local b = path:byte(pos) local b = path:byte(pos)
if b == BYTE_SLASH or b == BYTE_BACKSLASH then last_sep = pos end if b == BYTE_SLASH or b == BYTE_BACKSLASH then last_sep = pos end
end end
if last_sep == 0 then return "." end if last_sep == 0 then return "." end
return path:sub(1, last_sep - 1) return path:sub(1, last_sep - 1)
@@ -297,7 +271,7 @@ function M.basename_no_ext(path)
local last_sep = 0 local last_sep = 0
for pos = 1, #path do for pos = 1, #path do
local b = path:byte(pos) local b = path:byte(pos)
if b == BYTE_SLASH or b == BYTE_BACKSLASH then last_sep = pos end if b == BYTE_SLASH or b == BYTE_BACKSLASH then last_sep = pos end
end end
local a = last_sep + 1 local a = last_sep + 1
local last_dot = #path + 1 local last_dot = #path + 1
@@ -312,18 +286,16 @@ end
-- ════════════════════════════════════════════════════════════════════════════ -- ════════════════════════════════════════════════════════════════════════════
function M.read_file(path) function M.read_file(path)
local f = io.open(path, "r") local f = io.open(path, "r")
if not f then error("Cannot open " .. path) end if not f then error("Cannot open " .. path) end
local content = f:read("*a") local content = f:read("*a"); f:close()
f:close()
return content return content
end end
function M.write_file(path, content) function M.write_file(path, content)
local f = io.open(path, "w") local f = io.open(path, "w")
if not f then error("Cannot write " .. path) end if not f then error("Cannot write " .. path) end
f:write(content) f:write(content); f:close()
f:close()
end end
-- Cache of directories already verified to exist in this process. Each -- Cache of directories already verified to exist in this process. Each
@@ -337,8 +309,7 @@ function M.ensure_dir(path)
if _ensured_dirs[path] then return end if _ensured_dirs[path] then return end
_ensured_dirs[path] = true _ensured_dirs[path] = true
local is_win = package.config:sub(1, 1) == "\\" local is_win = package.config:sub(1, 1) == "\\"
os.execute(is_win and ('if not exist "' .. path .. '" mkdir "' .. path .. '"') os.execute(is_win and ('if not exist "' .. path .. '" mkdir "' .. path .. '"') or ('mkdir -p "' .. path .. '" 2>/dev/null'))
or ('mkdir -p "' .. path .. '" 2>/dev/null'))
end end
-- Test helper: clear the cache (used by tests + between process runs). -- Test helper: clear the cache (used by tests + between process runs).
@@ -350,92 +321,92 @@ function M._reset_ensured_dirs() _ensured_dirs = {} end
-- ════════════════════════════════════════════════════════════════════════════ -- ════════════════════════════════════════════════════════════════════════════
-- LPeg-backed skipper when LPeg is available, hand-rolled fallback otherwise. -- LPeg-backed skipper when LPeg is available, hand-rolled fallback otherwise.
-- Returns position just past the construct, or `i` unchanged if no -- Returns position just past the construct, or `pos` unchanged if no
-- string/comment starts at position i. -- string/comment starts at position `pos`.
function M.skip_str_or_cmt(s, i) function M.skip_str_or_cmt(s, pos)
if lpeg_ok then if lpeg_ok then
local new_pos = lpeg.match(lpeg_str_or_cmt_pat, s, i) local new_pos = lpeg.match(lpeg_str_or_cmt_pat, s, pos)
if new_pos then return new_pos end if new_pos then return new_pos end
return i return pos
end end
-- Hand-rolled fallback (kept for builds where LPeg isn't available). -- Hand-rolled fallback (kept for builds where LPeg isn't available).
local c = s:byte(i) local c = s:byte(pos)
if c == BYTE_DQUOTE or c == BYTE_SQUOTE then -- '"' or '\'' if c == BYTE_DQUOTE or c == BYTE_SQUOTE then -- '"' or '\''
i = i + 1 pos = pos + 1
while i <= #s do while pos <= #s do
local b = s:byte(i) local b = s:byte(pos)
if b == BYTE_BACKSLASH then i = i + 2 -- '\\' if b == BYTE_BACKSLASH then pos = pos + 2 -- '\\'
elseif b == c then return i + 1 elseif b == c then return pos + 1
else i = i + 1 end else pos = pos + 1 end
end end
return #s + 1 return #s + 1
elseif c == BYTE_SLASH then -- '/' elseif c == BYTE_SLASH then -- '/'
local nx = s:byte(i + 1) local nx = s:byte(pos + 1)
if nx == BYTE_SLASH then -- '//' if nx == BYTE_SLASH then -- '//'
while i <= #s and s:byte(i) ~= BYTE_NEWLINE do i = i + 1 end while pos <= #s and s:byte(pos) ~= BYTE_NEWLINE do pos = pos + 1 end
return i return pos
elseif nx == BYTE_STAR then -- '/*' elseif nx == BYTE_STAR then -- '/*'
i = i + 2 pos = pos + 2
while i <= #s - 1 do while pos <= #s - 1 do
if s:byte(i) == BYTE_STAR and s:byte(i + 1) == BYTE_SLASH then -- '*/' if s:byte(pos) == BYTE_STAR and s:byte(pos + 1) == BYTE_SLASH then -- '*/'
return i + 2 return pos + 2
end end
i = i + 1 pos = pos + 1
end end
return #s + 1 return #s + 1
end end
end end
return i return pos
end end
-- Skip whitespace AND C-style comments starting at position i. -- Skip whitespace AND C-style comments starting at position `pos`.
-- LPeg-backed when available; ~5-10x faster than the hand-rolled version. -- LPeg-backed when available; ~5-10x faster than the hand-rolled version.
function M.skip_ws_and_cmt(s, i) function M.skip_ws_and_cmt(s, pos)
if lpeg_ok then if lpeg_ok then
local new_pos = lpeg.match(lpeg_ws_and_cmt_pat, s, i) local new_pos = lpeg.match(lpeg_ws_and_cmt_pat, s, pos)
if new_pos then return new_pos end if new_pos then return new_pos end
return i return pos
end end
-- Hand-rolled fallback. -- Hand-rolled fallback.
local len = #s local len = #s
while i <= len do while pos <= len do
if M.is_space_byte(s:byte(i)) then if M.is_space_byte(s:byte(pos)) then
i = i + 1 pos = pos + 1
else else
local nx = M.skip_str_or_cmt(s, i) local nx = M.skip_str_or_cmt(s, pos)
if nx > i then i = nx else break end if nx > pos then pos = nx else break end
end end
end end
return i return pos
end end
-- Read a C-style identifier (alpha followed by zero+ alnum) starting at -- Read a C-style identifier (alpha followed by zero+ alnum) starting at
-- position i. Returns the identifier string + the position just past it, -- position `pos`. Returns the identifier string + the position just past
-- or nil + i if no identifier starts here. -- it, or nil + pos if no identifier starts here.
function M.read_ident(s, i) function M.read_ident(s, pos)
if lpeg_ok then if lpeg_ok then
local result = lpeg.match(lpeg_ident_pat, s, i) local result = lpeg.match(lpeg_ident_pat, s, pos)
if result then return result, i + #result end if result then return result, pos + #result end
return nil, i return nil, pos
end end
-- Hand-rolled fallback. -- Hand-rolled fallback.
if not M.is_alpha_byte(s:byte(i)) then return nil, i end if not M.is_alpha_byte(s:byte(pos)) then return nil, pos end
local a = i local a = pos
i = i + 1 pos = pos + 1
while i <= #s and M.is_alnum_byte(s:byte(i)) do i = i + 1 end while pos <= #s and M.is_alnum_byte(s:byte(pos)) do pos = pos + 1 end
return s:sub(a, i - 1), i return s:sub(a, pos - 1), pos
end end
-- Read a balanced-delimited group (parens, braces, or brackets) starting -- Read a balanced-delimited group (parens, braces, or brackets) starting
-- at position i. Returns the inner content (between the delimiters) + -- at position `pos`. Returns the inner content (between the delimiters) +
-- the position just past the closing delimiter, or nil + i if `s[i]` -- the position just past the closing delimiter, or nil + pos if `s[pos]`
-- isn't `open_char`. -- isn't `open_char`.
-- --
-- (Hand-rolled; the depth counting makes pure LPeg awkward here.) -- (Hand-rolled; the depth counting makes pure LPeg awkward here.)
function M.read_balanced(s, open_char, close_char, i) function M.read_balanced(s, open_char, close_char, pos)
local open_byte = open_char:byte() local open_byte = open_char:byte()
if s:byte(i) ~= open_byte then return nil, i end if s:byte(pos) ~= open_byte then return nil, pos end
local pos = i + 1 pos = pos + 1
local len = #s local len = #s
local depth = 1 local depth = 1
local a = pos local a = pos
@@ -450,16 +421,16 @@ function M.read_balanced(s, open_char, close_char, i)
pos = pos + 1 pos = pos + 1
else else
local nx = M.skip_str_or_cmt(s, pos) local nx = M.skip_str_or_cmt(s, pos)
pos = (nx > pos) and nx or (pos + 1) if nx > pos then pos = nx else pos = pos + 1 end
end end
end end
return s:sub(a, pos - 1), pos + 1 return s:sub(a, pos - 1), pos + 1
end end
-- Convenience specializations of read_balanced. -- Convenience specializations of read_balanced.
M.read_parens = function(s, i) return M.read_balanced(s, "(", ")", i) end M.read_parens = function(s, pos) return M.read_balanced(s, "(", ")", pos) end
M.read_braces = function(s, i) return M.read_balanced(s, "{", "}", i) end M.read_braces = function(s, pos) return M.read_balanced(s, "{", "}", pos) end
M.read_brackets = function(s, i) return M.read_balanced(s, "[", "]", i) end M.read_brackets = function(s, pos) return M.read_balanced(s, "[", "]", pos) end
-- Scan forward from position `start` until we find a specific single byte -- Scan forward from position `start` until we find a specific single byte
-- `target`, transparently stepping over balanced parens/braces/brackets. -- `target`, transparently stepping over balanced parens/braces/brackets.
@@ -469,13 +440,13 @@ function M.scan_to_char(s, target, start)
local pos = start local pos = start
while pos <= #s do while pos <= #s do
local c = s:byte(pos) local c = s:byte(pos)
if c == target_byte then return pos end if c == target_byte then return pos end
if c == BYTE_OPEN_PAREN then local _, a = M.read_balanced(s, "(", ")", pos); pos = a if c == BYTE_OPEN_PAREN then local _, a = M.read_balanced(s, "(", ")", pos); pos = a
elseif c == BYTE_OPEN_BRACE then local _, a = M.read_balanced(s, "{", "}", pos); pos = a elseif c == BYTE_OPEN_BRACE then local _, a = M.read_balanced(s, "{", "}", pos); pos = a
elseif c == BYTE_OPEN_BRACK then local _, a = M.read_balanced(s, "[", "]", pos); pos = a elseif c == BYTE_OPEN_BRACK then local _, a = M.read_balanced(s, "[", "]", pos); pos = a
else else
local nx = M.skip_str_or_cmt(s, pos) local nx = M.skip_str_or_cmt(s, pos)
pos = (nx > pos) and nx or (pos + 1) pos = (nx > pos) and nx or (pos + 1)
end end
end end
return nil return nil
@@ -527,22 +498,15 @@ function M.split_top_level_commas(body)
if has_real_content(chunk) then if has_real_content(chunk) then
tokens[#tokens + 1] = chunk tokens[#tokens + 1] = chunk
elseif #tokens > 0 then elseif #tokens > 0 then
-- Pure comment/string chunk at top level (no -- Pure comment/string chunk at top level (no preceding instruction content within this chunk).
-- preceding instruction content within this chunk). -- APPEND it to the LAST token so emit-context callers (components.lua build_component_lines)
-- APPEND it to the LAST token so emit-context -- can convert `// trailing comment` to `/* */` and emit it with the macro body.
-- callers (components.lua build_component_lines) -- For word counting, count_token_words only inspects the leading ident, so a trailing comment doesn't
-- can convert `// trailing comment` to `/* */`
-- and emit it with the macro body. For word
-- counting, count_token_words only inspects the
-- leading ident, so a trailing comment doesn't
-- affect the count. -- affect the count.
-- --
-- This is the second-half fix to commit 98e27c2: -- This is the second-half fix to commit 98e27c2: the first fix correctly broke top-level comments
-- the first fix correctly broke top-level comments -- off from the NEXT statement (fixing macro-call word counts);
-- off from the NEXT statement (fixing macro-call -- this fix preserves them on the PREVIOUS statement (restoring the comments in the emitted .macs.h output).
-- word counts); this fix preserves them on the
-- PREVIOUS statement (restoring the comments in
-- the emitted .macs.h output).
tokens[#tokens] = tokens[#tokens] .. chunk tokens[#tokens] = tokens[#tokens] .. chunk
end end
end end
@@ -560,15 +524,15 @@ function M.split_top_level_commas(body)
local _, a = M.read_brackets(body, pos); pos = a local _, a = M.read_brackets(body, pos); pos = a
elseif c == BYTE_COMMA then -- ',' elseif c == BYTE_COMMA then -- ','
emit(pos - 1) emit(pos - 1)
pos = pos + 1 pos = pos + 1
token_start = pos token_start = pos
elseif c == BYTE_SEMI then -- ';' elseif c == BYTE_SEMI then -- ';'
emit(pos - 1) emit(pos - 1)
pos = pos + 1 pos = pos + 1
token_start = pos token_start = pos
elseif c == BYTE_NEWLINE then -- '\n' elseif c == BYTE_NEWLINE then -- '\n'
emit(pos - 1) emit(pos - 1)
pos = pos + 1 pos = pos + 1
token_start = pos token_start = pos
else else
local nx = M.skip_str_or_cmt(body, pos) local nx = M.skip_str_or_cmt(body, pos)
@@ -731,14 +695,14 @@ M.GTE_PIPELINE_LATENCY = {
-- Aliases (must have the same value as their canonical target) -- Aliases (must have the same value as their canonical target)
["gte_cmdw_rotate_translate_perspective_single"] = 2, ["gte_cmdw_rotate_translate_perspective_single"] = 2,
["gte_cmdw_rotate_translate_perspective_triple"] = 2, ["gte_cmdw_rotate_translate_perspective_triple"] = 2,
["gte_cmdw_avg_sort_z4"] = 2, ["gte_cmdw_avg_sort_z4"] = 2,
-- Outer product aliases (same canonical op, 0 pre-fill nops). -- Outer product aliases (same canonical op, 0 pre-fill nops).
-- gte_cmdw_op = canonical GTE-internal short form -- gte_cmdw_op = canonical GTE-internal short form
-- gte_cmdw_outer_product = NOCASH / SDK-readable form -- gte_cmdw_outer_product = NOCASH / SDK-readable form
-- gte_cmdw_wedge = geometric-algebra (exterior-product) form -- gte_cmdw_wedge = geometric-algebra (exterior-product) form
["gte_cmdw_outer_product"] = 0, ["gte_cmdw_outer_product"] = 0,
["gte_cmdw_wedge"] = 0, ["gte_cmdw_wedge"] = 0,
} }
-- GP0 packet sizes (total words including the 1-word tag) per GP0 cmd byte. -- GP0 packet sizes (total words including the 1-word tag) per GP0 cmd byte.
@@ -914,18 +878,18 @@ M.INSTRUCTION_LATENCY = {
-- mac_* helpers (cycle cost = sum of the expanded instructions) -- mac_* helpers (cycle cost = sum of the expanded instructions)
-- mac_yield transfers control; cycle budget is 0 (the next atom -- mac_yield transfers control; cycle budget is 0 (the next atom
-- absorbs the cost). -- absorbs the cost).
["mac_yield"] = 0, ["mac_yield"] = 0,
["mac_pack_color_word"] = 3, -- lui + ori + sw ["mac_pack_color_word"] = 3, -- lui + ori + sw
["mac_format_f3_color"] = 3, -- = mac_pack_color_word ["mac_format_f3_color"] = 3, -- = mac_pack_color_word
["mac_format_g4_color"] = 12, -- 4 x mac_pack_color_word ["mac_format_g4_color"] = 12, -- 4 x mac_pack_color_word
["mac_load_tri_indices"] = 3, -- 3 x lhu ["mac_load_tri_indices"] = 3, -- 3 x lhu
["mac_gte_load_tri_verts"] = 18, -- 3 x {sll, addu, lw, lw, mtc2, mtc2} ["mac_gte_load_tri_verts"] = 18, -- 3 x {sll, addu, lw, lw, mtc2, mtc2}
["mac_gte_store_f3_post_rtpt"] = 3, ["mac_gte_store_f3_post_rtpt"] = 3,
["mac_gte_store_g3_post_rtpt"] = 3, ["mac_gte_store_g3_post_rtpt"] = 3,
["mac_gte_store_g4_p012_post_rtpt_pre_rtps"] = 3, ["mac_gte_store_g4_p012_post_rtpt_pre_rtps"] = 3,
["mac_gte_store_g4_p3_post_rtps"] = 1, ["mac_gte_store_g4_p3_post_rtps"] = 1,
["mac_insert_ot_tag_f3"] = 11, -- 11 .word slots in the macro body ["mac_insert_ot_tag_f3"] = 11, -- 11 .word slots in the macro body
["mac_insert_ot_tag_g4"] = 11, ["mac_insert_ot_tag_g4"] = 11,
-- Annotation markers (emit no code; pure metaprogram hints) -- Annotation markers (emit no code; pure metaprogram hints)
["atom_label"] = 0, ["atom_label"] = 0,
["atom_offset"] = 0, ["atom_offset"] = 0,
+11 -36
View File
@@ -1,62 +1,37 @@
--- duffle_paths.lua — Single-line bootstrap helper for the tape-atom --- duffle_paths.lua — Single-line bootstrap helper for the tape-atom Lua scripts.
--- Lua scripts.
---
--- Each entry script (ps1_meta.lua, word_count_eval.lua, and the 5
--- passes/*.lua files) starts with:
--- ---
--- Each entry script (ps1_meta.lua, word_count_eval.lua, and the 5 passes/*.lua files) starts with:
--- ```lua --- ```lua
--- local duffle = dofile((arg[0]:match("(.*[/\\])") or "./") .. "duffle_paths.lua") --- local duffle = dofile((arg[0]:match("(.*[/\\])") or "./") .. "duffle_paths.lua")
--- ``` --- ```
--- --- That single line: (a) locates this helper via `arg[0]`,
--- That single line: (a) locates this helper via `arg[0]`, (b) loads --- (b) loads it (which sets `package.path` + `package.cpath` via `git rev-parse`),
--- it (which sets `package.path` + `package.cpath` via `git rev-parse`),
--- (c) returns the `M` table (a wrapper around the setup function). --- (c) returns the `M` table (a wrapper around the setup function).
--- After this line, `require("duffle")` and `require("passes.X")` both --- After this line, `require("duffle")` and `require("passes.X")` both resolve normally.
--- resolve normally.
---
--- **Why a helper instead of inline?**
--- - The 8-line path-setup boilerplate was duplicated across 7 entry
--- scripts (one per file). Single source of truth here.
--- - Mirrors the build script's pattern in `build_psyq.ps1`:
--- `$path_root = split-path -Path $PSScriptRoot -Parent;` then
--- derive everything from there.
---
--- **Why `git rev-parse --show-toplevel`?**
--- Hardcoding `C:\\projects\\Pikuma\\ps1\\...` breaks portability. Git
--- gives us the canonical repo root regardless of where the repo lives
--- on disk.
local M = {} local M = {}
-- Cache key for the repo root. Stored in `package.loaded` (process- -- Cache key for the repo root. Stored in `package.loaded` (process-global) so all 8 entry scripts + passes scripts share one git call.
-- global) so all 8 entry scripts + passes scripts share one git call. -- Without this cache, `git rev-parse --show-toplevel` runs once per script load = 8 × ~150ms = 1.2s wasted per build on Windows.
-- Without this cache, `git rev-parse --show-toplevel` runs once per
-- script load = 8 × ~150ms = 1.2s wasted per build on Windows.
local CACHE_KEY = "__duffle_repo_root__" local CACHE_KEY = "__duffle_repo_root__"
--- Resolve the repo root via git (cached after first call). --- Resolve the repo root via git (cached after first call).
--- Returns a normalized path with a trailing forward-slash, or nil --- Returns a normalized path with a trailing forward-slash, or nil if not in a git repo.
--- if not in a git repo.
--- @return string|nil --- @return string|nil
local function find_repo_root() local function find_repo_root()
if package.loaded[CACHE_KEY] then return package.loaded[CACHE_KEY] end if package.loaded[CACHE_KEY] then return package.loaded[CACHE_KEY] end
local p = io.popen("git rev-parse --show-toplevel 2>nul") local p = io.popen("git rev-parse --show-toplevel 2>nul")
local root local root
if p then if p then root = p:read("*l"); p:close() end
root = p:read("*l")
p:close()
end
if not root or root == "" then return nil end if not root or root == "" then return nil end
-- Normalize to forward slashes (Windows accepts both, but mixed -- Normalize to forward slashes (Windows accepts both, but mixed `\` + `/` confuses LuaJIT's file APIs).
-- `\` + `/` confuses LuaJIT's file APIs).
root = root:gsub("\\", "/") root = root:gsub("\\", "/")
if not root:match("/$") then root = root .. "/" end if not root:match("/$") then root = root .. "/" end
package.loaded[CACHE_KEY] = root package.loaded[CACHE_KEY] = root
return root return root
end end
--- Set `package.path` (for `require("duffle")` + `require("passes.X")`) --- Set `package.path` (for `require("duffle")` + `require("passes.X")`) and `package.cpath` (for `lpeg.dll` on Windows).
--- and `package.cpath` (for `lpeg.dll` on Windows).
--- Idempotent: safe to call multiple times (just re-sets the same paths). --- Idempotent: safe to call multiple times (just re-sets the same paths).
function M.setup() function M.setup()
local repo_root = find_repo_root() local repo_root = find_repo_root()
+40 -72
View File
@@ -1,25 +1,14 @@
--- passes/annotation.lua — Atom-annotation DSL validator. --- passes/annotation.lua — Atom-annotation DSL validator.
--- ---
--- Validates `MipsAtom_(name) atom_info(atom_bind(Binds_X), --- Validates `MipsAtom_(name) atom_info(atom_bind(Binds_X), atom_reads(...), atom_writes(...)) { ... }` declarations in source files.
--- atom_reads(...), atom_writes(...)) { ... }` declarations in source files. --- Also reads: `Binds_*` struct declarations (`typedef Struct_(Binds_X) { ... };`)
--- Also reads:
--- - `Binds_*` struct declarations (`typedef Struct_(Binds_X) { ... };`)
--- - `TAPE_WORDS(mac_X, N)` pragma directives (`#pragma` + `_Pragma`)
--- ---
--- Writes: --- Writes:
--- - `<ctx.out_root>/<dir_basename>.errors.h` — one per module, with --- - `<ctx.out_root>/<dir_basename>.errors.h` — one per module, with `#error` directives on findings (the C compile will surface the error)
--- `#error` directives on findings (the C compile will surface the --- - The annotations.txt report is rendered by `passes/report.lua` from the per-module results stashed in `ctx.flags._annot_results`
--- error)
--- - The annotations.txt report is rendered by `passes/report.lua`
--- from the per-module results stashed in `ctx.flags._annot_results`
---
--- **Ported from** `scripts/tape_atom_annotation_pass.lua:78-545 +
--- 1081-1407` (validation only — NOT rendering, which goes to
--- `passes/report.lua`).
--- ---
--- **Conventions**: tabs (1/level), EmmyLua annotations, no regex, --- **Conventions**: tabs (1/level), EmmyLua annotations, no regex,
--- Lua 5.3 compatible. See --- Lua 5.3 compatible
--- `C:\projects\Pikuma\ps1-ai\conductor\code_styleguides\lua.md`.
-- ════════════════════════════════════════════════════════════════════════════ -- ════════════════════════════════════════════════════════════════════════════
-- Module-scope requires + package.path setup -- Module-scope requires + package.path setup
@@ -170,7 +159,7 @@ local BYTE_COMMA = 44
--- @param s string --- @param s string
--- @return string[] --- @return string[]
local function split_csv_top(s) local function split_csv_top(s)
local tokens = {} local tokens = {}
local pos = 1 local pos = 1
local chunk_a = 1 local chunk_a = 1
local depth = 0 local depth = 0
@@ -179,13 +168,13 @@ local function split_csv_top(s)
local ch = s:byte(pos) local ch = s:byte(pos)
if ch == BYTE_OPEN_PAREN or ch == BYTE_OPEN_BRACE or ch == BYTE_OPEN_BRACK then if ch == BYTE_OPEN_PAREN or ch == BYTE_OPEN_BRACE or ch == BYTE_OPEN_BRACK then
depth = depth + 1 depth = depth + 1
pos = pos + 1 pos = pos + 1
elseif ch == BYTE_CLOSE_PAREN or ch == BYTE_CLOSE_BRACE or ch == BYTE_CLOSE_BRACK then elseif ch == BYTE_CLOSE_PAREN or ch == BYTE_CLOSE_BRACE or ch == BYTE_CLOSE_BRACK then
depth = depth - 1 depth = depth - 1
pos = pos + 1 pos = pos + 1
elseif ch == BYTE_COMMA and depth == 0 then elseif ch == BYTE_COMMA and depth == 0 then
tokens[#tokens + 1] = s:sub(chunk_a, pos - 1) tokens[#tokens + 1] = s:sub(chunk_a, pos - 1)
pos = pos + 1 pos = pos + 1
chunk_a = pos chunk_a = pos
else else
pos = pos + 1 pos = pos + 1
@@ -259,9 +248,7 @@ end
-- Resolve any phase_* / R_* alias macros in a register list. -- Resolve any phase_* / R_* alias macros in a register list.
-- (Phase / region / cadence aliases have been dropped. Kept as an -- (Phase / region / cadence aliases have been dropped. Kept as an
-- identity function so callers can stay uniform.) -- identity function so callers can stay uniform.)
local function resolve_reg_aliases(regs) local function resolve_reg_aliases(regs) return regs end
return regs
end
-- Parse a comma-separated inner content (e.g. inside atom_reads(...)) -- Parse a comma-separated inner content (e.g. inside atom_reads(...))
-- into a list of trimmed identifiers with aliases resolved. -- into a list of trimmed identifiers with aliases resolved.
@@ -269,7 +256,7 @@ local function parse_regs_list(inner)
local out = {} local out = {}
for _, r in ipairs(split_csv_top(inner)) do for _, r in ipairs(split_csv_top(inner)) do
local trimmed = trim(r) local trimmed = trim(r)
if trimmed ~= "" then out[#out + 1] = trimmed end if trimmed ~= "" then out[#out + 1] = trimmed end
end end
return resolve_reg_aliases(out) return resolve_reg_aliases(out)
end end
@@ -387,7 +374,7 @@ local function parse_pragma_directive(source, ident_pos, after_ident)
end end
if entry then if entry then
local line_of = duffle.LineIndex(source) local line_of = duffle.LineIndex(source)
entry.line = line_of(ident_pos) entry.line = line_of(ident_pos)
end end
return entry, eol return entry, eol
end end
@@ -417,7 +404,7 @@ local function find_macro_word_annotations(source)
local entry, new_pos = parse_pragma_operator(source, pos, after_ident) local entry, new_pos = parse_pragma_operator(source, pos, after_ident)
if entry then if entry then
local line_of = duffle.LineIndex(source) local line_of = duffle.LineIndex(source)
entry.line = line_of(pos) entry.line = line_of(pos)
out[#out + 1] = entry out[#out + 1] = entry
end end
pos = new_pos pos = new_pos
@@ -479,7 +466,7 @@ end
--- @param line_of fun(pos: integer): integer --- @param line_of fun(pos: integer): integer
--- @return BindsStruct|nil, integer --- @return BindsStruct|nil, integer
local function parse_typedef_binds(source, ident_pos, after_typedef, line_of) local function parse_typedef_binds(source, ident_pos, after_typedef, line_of)
local after_type = skip_ws_and_cmt(source, after_typedef) local after_type = skip_ws_and_cmt(source, after_typedef)
local type_ident, after_type_ident = read_ident(source, after_type) local type_ident, after_type_ident = read_ident(source, after_type)
if type_ident ~= STRUCT_TYPE then if type_ident ~= STRUCT_TYPE then
return nil, after_type_ident or (after_type + 1) return nil, after_type_ident or (after_type + 1)
@@ -491,13 +478,13 @@ local function parse_typedef_binds(source, ident_pos, after_typedef, line_of)
end end
local inner, after_paren = read_parens(source, open_paren) local inner, after_paren = read_parens(source, open_paren)
local name = trim(inner) local name = trim(inner)
local brace = scan_to_char(source, "{", after_paren) local brace = scan_to_char(source, "{", after_paren)
if not brace then return nil, open_paren + 1 end if not brace then return nil, open_paren + 1 end
local body, after_brace = read_braces(source, brace) local body, after_brace = read_braces(source, brace)
local fields, bytes = parse_binds_body(body) local fields, bytes = parse_binds_body(body)
-- Only emit Binds_* structs (other Struct_ typedefs are ignored). -- Only emit Binds_* structs (other Struct_ typedefs are ignored).
if name:sub(1, BINDS_PREFIX_LEN) ~= BINDS_PREFIX then if name:sub(1, BINDS_PREFIX_LEN) ~= BINDS_PREFIX then
@@ -554,10 +541,8 @@ end
--- @param pos integer --- @param pos integer
--- @return string|nil, integer --- @return string|nil, integer
local function read_alnum_ident(s, pos) local function read_alnum_ident(s, pos)
local str_len = #s local str_len = #s; while pos <= str_len and is_space(s:sub(pos, pos)) do pos = pos + 1 end
while pos <= str_len and is_space(s:sub(pos, pos)) do pos = pos + 1 end local start = pos; while pos <= str_len and is_alnum(s:sub(pos, pos)) do pos = pos + 1 end
local start = pos
while pos <= str_len and is_alnum(s:sub(pos, pos)) do pos = pos + 1 end
if pos == start then return nil, pos end if pos == start then return nil, pos end
return s:sub(start, pos - 1), pos return s:sub(start, pos - 1), pos
end end
@@ -586,14 +571,14 @@ local function find_atom_names(source)
pos = open_paren + 1 pos = open_paren + 1
else else
local inner, after_paren = read_parens(source, open_paren) local inner, after_paren = read_parens(source, open_paren)
local name, _ = read_alnum_ident(inner, 1) local name, _ = read_alnum_ident(inner, 1)
if name and name ~= "" then if name and name ~= "" then
out[#out + 1] = { line = line_of(pos), name = name } out[#out + 1] = { line = line_of(pos), name = name }
end end
local brace = scan_to_char(source, "{", after_paren) local brace = scan_to_char(source, "{", after_paren)
if brace then if brace then
local _, after_brace = read_braces(source, brace) local _, after_brace = read_braces(source, brace)
pos = after_brace pos = after_brace
else else
pos = open_paren + 1 pos = open_paren + 1
end end
@@ -609,39 +594,24 @@ end
-- ════════════════════════════════════════════════════════════════════════════ -- ════════════════════════════════════════════════════════════════════════════
--- True iff the parsed arg is a register-list call (any recognized form). --- True iff the parsed arg is a register-list call (any recognized form).
local function is_regs_arg(a) local function is_regs_arg(a) return a and (a.kind == "atom_reads" or a.kind == "atom_writes" or a.kind == "regs") end
return a and (a.kind == "atom_reads" or a.kind == "atom_writes" or a.kind == "regs")
end
--- Per-macro arg-shape handlers. Each takes (entry, args) and mutates --- Per-macro arg-shape handlers. Each takes (entry, args) and mutates Per-atom_info sub-call dispatch.
--- Per-atom_info sub-call dispatch. Each takes (entry, args) and mutates --- Each takes (entry, args) and mutates entry.{reads, writes, binds, errors}.
--- entry.{reads, writes, binds, errors}. The new annotation shape is: --- All sub-calls are order-independent; each is dispatched on its `kind` (atom_bind / atom_reads / atom_writes) when parsed.
---
--- MipsAtom_(name) atom_info(
--- atom_bind(Binds_X)
--- , atom_reads(...)
--- , atom_writes(...)
--- ) { ... };
---
--- All sub-calls are order-independent; each is dispatched on its
--- `kind` (atom_bind / atom_reads / atom_writes) when parsed.
local ANNOT_ARG_HANDLERS = {} local ANNOT_ARG_HANDLERS = {}
-- atom_info(atom_bind(Binds_X), atom_reads(...), atom_writes(...)) -- atom_info(atom_bind(Binds_X), atom_reads(...), atom_writes(...))
function ANNOT_ARG_HANDLERS.info(entry, args) function ANNOT_ARG_HANDLERS.info(entry, args)
for _, arg in ipairs(args) do for _, arg in ipairs(args) do
if arg.kind == "atom_bind" then if arg.kind == "atom_bind" then entry.binds = arg.value
entry.binds = arg.value elseif arg.kind == "atom_reads" then entry.reads = arg.value
elseif arg.kind == "atom_reads" then elseif arg.kind == "atom_writes" then entry.writes = arg.value
entry.reads = arg.value elseif arg.kind == "ident" then
elseif arg.kind == "atom_writes" then
entry.writes = arg.value
elseif arg.kind == "ident" then
-- Reserved for future phase tokens. Currently ignored. -- Reserved for future phase tokens. Currently ignored.
-- (Could be reintroduced as `phase_*` sub-calls of atom_info.) -- (Could be reintroduced as `phase_*` sub-calls of atom_info.)
else else
entry.errors[#entry.errors + 1] = string.format( entry.errors[#entry.errors + 1] = string.format("unexpected atom_info arg kind=%s value=%s", arg.kind, tostring(arg.value))
"unexpected atom_info arg kind=%s value=%s", arg.kind, tostring(arg.value))
end end
end end
end end
@@ -660,27 +630,25 @@ local function new_annot_entry(line, ident, name, kind)
} }
end end
--- Try to parse an `atom_info(...)` call right after the `MipsAtom_(name)` --- Try to parse an `atom_info(...)` call right after the `MipsAtom_(name)` parens.
--- parens. Returns the annotation entry (if present) and the new source --- Returns the annotation entry (if present) and the new source position past the atom_info call.
--- position past the atom_info call. Returns nil if no atom_info follows. --- Returns nil if no atom_info follows.
--- @param source string --- @param source string
--- @param atom_name string --- @param atom_name string
--- @param after_mipsatom_paren integer -- position past the MipsAtom_(...) close paren --- @param after_mipsatom_paren integer -- position past the MipsAtom_(...) close paren
--- @param line_of fun(pos: integer): integer --- @param line_of fun(pos: integer): integer
--- @return AtomAnnotation|nil, integer -- (entry or nil, new source position) --- @return AtomAnnotation|nil, integer -- (entry or nil, new source position)
local function parse_atom_info_call(source, atom_name, after_mipsatom_paren, line_of) local function parse_atom_info_call(source, atom_name, after_mipsatom_paren, line_of)
local lookahead = skip_ws_and_cmt(source, after_mipsatom_paren) local lookahead = skip_ws_and_cmt(source, after_mipsatom_paren)
local look_ident, look_after = read_ident(source, lookahead) local look_ident, look_after = read_ident(source, lookahead)
if look_ident ~= ATOM_INFO then return nil, after_mipsatom_paren end if look_ident ~= ATOM_INFO then return nil, after_mipsatom_paren end
local info_open = skip_ws_and_cmt(source, look_after) local info_open = skip_ws_and_cmt(source, look_after)
if source:byte(info_open) ~= BYTE_OPEN_PAREN then if source:byte(info_open) ~= BYTE_OPEN_PAREN then return nil, info_open + 1 end
return nil, info_open + 1
end
local info_inner, info_after = read_parens(source, info_open) local info_inner, info_after = read_parens(source, info_open)
local args = parse_atom_annot_args(info_inner) local args = parse_atom_annot_args(info_inner)
local entry = new_annot_entry(line_of(lookahead), ATOM_INFO, atom_name, "info") local entry = new_annot_entry(line_of(lookahead), ATOM_INFO, atom_name, "info")
ANNOT_ARG_HANDLERS.info(entry, args) ANNOT_ARG_HANDLERS.info(entry, args)
return entry, info_after return entry, info_after
end end
@@ -741,10 +709,10 @@ end
local function validate(ctx, src) local function validate(ctx, src)
local source = src.text local source = src.text
local annots = find_atom_annotations(source) local annots = find_atom_annotations(source)
local macros = find_macro_word_annotations(source) local macros = find_macro_word_annotations(source)
local binds = find_binds_structs(source) local binds = find_binds_structs(source)
local atoms = find_atom_names(source) local atoms = find_atom_names(source)
local atom_index = {} local atom_index = {}
for _, a in ipairs(atoms) do atom_index[a.name] = a end for _, a in ipairs(atoms) do atom_index[a.name] = a end
+12 -31
View File
@@ -1,19 +1,12 @@
--- passes/components.lua — Component-macro header generator. --- passes/components.lua — Component-macro header generator.
--- ---
--- Walks every source for `MipsAtomComp_(ac_X) { body }` (and the --- Walks every source for `MipsAtomComp_(ac_X) { body }`
--- function-form `MipsAtomComp_Proc_(ac_X, { body })`) declarations --- (and the function-form `MipsAtomComp_Proc_(ac_X, { body })`) declarations
--- and emits a per-directory `<dir_basename>.macs.h` containing one --- and emits a per-directory `<dir_basename>.macs.h` containing one `#define mac_X(sig) \` macro per component + `WORD_COUNT(mac_X, N)`
--- `#define mac_X(sig) \` macro per component + `WORD_COUNT(mac_X, N)`
--- entries for downstream offset computation. --- entries for downstream offset computation.
--- ---
--- **Ported from** `tape_atom_annotation_pass.lua:604-1079`
--- (`find_component_atoms`, `preceding_comment_block`,
--- `extract_arg_names`, `convert_line_comments_to_block`,
--- `compute_component_word_count`, `emit_component_macros_h`).
---
--- **Conventions**: tabs (1/level), EmmyLua annotations, no regex, --- **Conventions**: tabs (1/level), EmmyLua annotations, no regex,
--- Lua 5.3 compatible. See --- Lua 5.3 compatible.
--- `C:\projects\Pikuma\ps1-ai\conductor\code_styleguides\lua.md`.
--- @class Component --- @class Component
--- @field name string --- @field name string
@@ -28,13 +21,9 @@
-- Module-scope requires + package.path setup -- Module-scope requires + package.path setup
-- ════════════════════════════════════════════════════════════════════════════ -- ════════════════════════════════════════════════════════════════════════════
-- Resolve `arg[0]` to an absolute-ish script directory so that -- Resolve `arg[0]` to an absolute-ish script directory so that `require("duffle")` resolves against `scripts/` regardless of CWD.
-- `require("duffle")` resolves against `scripts/` regardless of CWD.
-- Note: this boilerplate is duplicated in 6 other entry scripts; a
-- Phase-6 extraction target (`duffle.setup_package_path()`).
-- Bootstrap: see `ps1_meta.lua` for the rationale.
dofile((arg[0]:match("(.*[/\\])") or "./") .. "duffle_paths.lua") dofile((arg[0]:match("(.*[/\\])") or "./") .. "duffle_paths.lua")
local duffle = require("duffle") local duffle = require("duffle")
local word_count_eval = require("word_count_eval") local word_count_eval = require("word_count_eval")
-- ════════════════════════════════════════════════════════════════════════════ -- ════════════════════════════════════════════════════════════════════════════
@@ -97,23 +86,19 @@ local GEN_SUBDIR = "gen"
-- Local helpers (file I/O + path normalization) -- Local helpers (file I/O + path normalization)
-- ════════════════════════════════════════════════════════════════════════════ -- ════════════════════════════════════════════════════════════════════════════
-- Write content to disk in binary mode so LF line endings are preserved on -- Write content to disk in binary mode so LF line endings are preserved on Windows
-- Windows (text mode would convert LF -> CRLF, breaking byte-identical diffs -- (text mode would convert LF -> CRLF, breaking byte-identical diffs against git-tracked gen/*.macs.h files which are stored as LF).
-- against git-tracked gen/*.macs.h files which are stored as LF).
-- @param path string -- @param path string
-- @param content string -- @param content string
local function write_file_lf(path, content) local function write_file_lf(path, content)
local f = io.open(path, "wb") local f = io.open(path, "wb")
if not f then error("Cannot write " .. path) end if not f then error("Cannot write " .. path) end
f:write(content) f:write(content); f:close()
f:close()
end end
-- Convert a (possibly relative) path to an absolute Windows path. The -- Convert a (possibly relative) path to an absolute Windows path.
-- pre-rework output's "// Source:" comment line used the absolute path -- The pre-rework output's "// Source:" comment line used the absolute path (e.g. "C:\projects\Pikuma\ps1\code\duffle\lottes_tape.h");
-- (e.g. "C:\projects\Pikuma\ps1\code\duffle\lottes_tape.h"); if we want -- If we want byte-identical output, we must normalize relative -> absolute before emitting that comment.
-- byte-identical output, we must normalize relative -> absolute before
-- emitting that comment.
-- @param path string -- @param path string
-- @return string -- @return string
local function to_absolute_path(path) local function to_absolute_path(path)
@@ -135,10 +120,6 @@ end
local M = {} local M = {}
-- ════════════════════════════════════════════════════════════════════════════
-- Ported helpers (verbatim from tape_atom_annotation_pass.lua:604-1079)
-- ════════════════════════════════════════════════════════════════════════════
-- ════════════════════════════════════════════════════════════════════════════ -- ════════════════════════════════════════════════════════════════════════════
-- Function-args extraction (precedes MipsAtomComp_Proc_ invocations) -- Function-args extraction (precedes MipsAtomComp_Proc_ invocations)
-- ════════════════════════════════════════════════════════════════════════════ -- ════════════════════════════════════════════════════════════════════════════
+201 -201
View File
@@ -162,23 +162,23 @@ local OUTPUT_EXTENSION = ".static_analysis.txt"
local function find_atom_bodies(source_text) local function find_atom_bodies(source_text)
local line_of = duffle.LineIndex(source_text) local line_of = duffle.LineIndex(source_text)
local out = {} local out = {}
local len = #source_text local src_len = #source_text
local i = 1 local pos = 1
while i <= len do while pos <= src_len do
i = duffle.skip_ws_and_cmt(source_text, i); if i > len then break end pos = duffle.skip_ws_and_cmt(source_text, pos); if pos > src_len then break end
-- Skip preprocessor directives (#define / #include / #pragma / -- Skip preprocessor directives (#define / #include / #pragma /
-- etc). Otherwise the `#define MipsAtom_(sym) ...` definition -- etc). Otherwise the `#define MipsAtom_(sym) ...` definition
-- in lottes_tape.h gets matched as an atom named "sym" and -- in lottes_tape.h gets matched as an atom named "sym" and
-- its `body` swallows the next real atom declaration via -- its `body` swallows the next real atom declaration via
-- duffle.scan_to_char("{", ...). -- duffle.scan_to_char("{", ...).
if source_text:sub(i, i) == "#" then if source_text:sub(pos, pos) == "#" then
local j = i local eol_pos = pos
while j <= len and source_text:byte(j) ~= 10 do j = j + 1 end while eol_pos <= src_len and source_text:byte(eol_pos) ~= 10 do eol_pos = eol_pos + 1 end
i = j + 1 pos = eol_pos + 1
else else
local ident, after = duffle.read_ident(source_text, i) local ident, ident_end = duffle.read_ident(source_text, pos)
if not ident then if not ident then
i = i + 1 pos = pos + 1
elseif ident == "MipsAtom_" elseif ident == "MipsAtom_"
or ident == "MipsAtomComp_" or ident == "MipsAtomComp_"
or ident == "MipsAtomComp_Proc_" then or ident == "MipsAtomComp_Proc_" then
@@ -190,69 +190,69 @@ local function find_atom_bodies(source_text)
else kind = "comp_proc" else kind = "comp_proc"
end end
local open = duffle.skip_ws_and_cmt(source_text, after) local open_paren = duffle.skip_ws_and_cmt(source_text, ident_end)
if source_text:sub(open, open) ~= "(" then if source_text:sub(open_paren, open_paren) ~= "(" then
i = open + 1 pos = open_paren + 1
else else
local inner, after_paren = duffle.read_parens(source_text, open) local inner, after_paren = duffle.read_parens(source_text, open_paren)
if kind == "comp_proc" then if kind == "comp_proc" then
-- MipsAtomComp_Proc_(sym, { body }) -- MipsAtomComp_Proc_(sym, { body })
-- The body is inside the LAST `{ ... }` in the args -- The body is inside the LAST `{ ... }` in the args
-- (the macro takes 2 args: sym name, then body in {}). -- (the macro takes 2 args: sym name, then body in {}).
-- Find the last `{` in `inner`, then the matching `}`. -- Find the last `{` in `inner`, then the matching `}`.
local last_open local last_brace_pos
for k = #inner, 1, -1 do for search_pos = #inner, 1, -1 do
if inner:sub(k, k) == "{" then last_open = k; break end if inner:sub(search_pos, search_pos) == "{" then last_brace_pos = search_pos; break end
end end
if not last_open then if not last_brace_pos then
i = open + 1 pos = open_paren + 1
else else
-- Walk forward to find matching `}` honoring balanced -- Walk forward to find matching `}` honoring balanced
-- ()/[] and strings. We could call duffle.read_braces -- ()/[] and strings. We could call duffle.read_braces
-- from last_open+1, but read_braces expects to start at -- from last_brace_pos+1, but read_braces expects to start at
-- the brace itself. Inline the walk for clarity. -- the brace itself. Inline the walk for clarity.
local depth = 1 local depth = 1
local j = last_open + 1 local inner_pos = last_brace_pos + 1
while j <= #inner and depth > 0 do while inner_pos <= #inner and depth > 0 do
local c = inner:byte(j) local c = inner:byte(inner_pos)
if c == 123 then if c == 123 then
depth = depth + 1; j = j + 1 depth = depth + 1; inner_pos = inner_pos + 1
elseif c == 125 then elseif c == 125 then
depth = depth - 1 depth = depth - 1
if depth == 0 then break end if depth == 0 then break end
j = j + 1 inner_pos = inner_pos + 1
elseif c == 40 then elseif c == 40 then
local _, a = duffle.read_parens(inner, j); j = a local _, a = duffle.read_parens(inner, inner_pos); inner_pos = a
elseif c == 91 then elseif c == 91 then
local _, a = duffle.read_brackets(inner, j); j = a local _, a = duffle.read_brackets(inner, inner_pos); inner_pos = a
elseif c == 34 or c == 39 then elseif c == 34 or c == 39 then
j = duffle.duffle.skip_str_or_cmt(inner, j) + 1 inner_pos = duffle.duffle.skip_str_or_cmt(inner, inner_pos) + 1
else else
j = j + 1 inner_pos = inner_pos + 1
end end
end end
if depth ~= 0 then if depth ~= 0 then
-- unmatched; bail -- unmatched; bail
i = open + 1 pos = open_paren + 1
else else
-- First ident in `inner` is the comp name. -- First ident in `inner` is the comp name.
local name_match = inner:match("^%s*([%w_]+)") local name_match = inner:match("^%s*([%w_]+)")
local name = name_match or "?" local name = name_match or "?"
local body = inner:sub(last_open + 1, j - 1) local body = inner:sub(last_brace_pos + 1, inner_pos - 1)
-- body_off in full source: position right after the -- body_off in full source: position right after the
-- LAST `{` in `inner`, which sits at `open+1+last_open` -- LAST `{` in `inner`, which sits at `open_paren+1+last_brace_pos`
-- (open+1 = just inside the outer paren, +last_open -- (open_paren+1 = just inside the outer paren,
-- = at the `{`). -- +last_brace_pos = at the `{`).
local body_off = open + 1 + last_open local body_off = open_paren + 1 + last_brace_pos
out[#out + 1] = { out[#out + 1] = {
line = line_of(i), line = line_of(pos),
name = name, name = name,
body = body, body = body,
body_off = body_off + 1, body_off = body_off + 1,
kind = kind, kind = kind,
} }
i = after_paren pos = after_paren
end end
end end
else else
@@ -260,34 +260,34 @@ local function find_atom_bodies(source_text)
-- MipsAtomComp_(sym) { body }; -- MipsAtomComp_(sym) { body };
-- name is the first arg, body is the FIRST { ... } after -- name is the first arg, body is the FIRST { ... } after
-- the paren. -- the paren.
local a = 1 local name_start = 1
while a <= #inner and inner:sub(a, a):match("[%s]") do a = a + 1 end while name_start <= #inner and inner:sub(name_start, name_start):match("[%s]") do name_start = name_start + 1 end
local b = a local name_end = name_start
while b <= #inner and inner:sub(b, b):match("[%w_]") do b = b + 1 end while name_end <= #inner and inner:sub(name_end, name_end):match("[%w_]") do name_end = name_end + 1 end
local name = inner:sub(a, b - 1) local name = inner:sub(name_start, name_end - 1)
if name == "" then if name == "" then
i = open + 1 pos = open_paren + 1
else else
local brace = duffle.scan_to_char(source_text, "{", after_paren) local brace = duffle.scan_to_char(source_text, "{", after_paren)
if brace then if brace then
local body, after_brace = duffle.read_braces(source_text, brace) local body, after_brace = duffle.read_braces(source_text, brace)
local body_off = brace + 1 local body_off = brace + 1
out[#out + 1] = { out[#out + 1] = {
line = line_of(i), line = line_of(pos),
name = name, name = name,
body = body, body = body,
body_off = body_off, body_off = body_off,
kind = kind, kind = kind,
} }
i = after_brace pos = after_brace
else else
i = open + 1 pos = open_paren + 1
end end
end end
end end
end end
else else
i = after pos = ident_end
end end
end -- close the new preprocessor-skip else end -- close the new preprocessor-skip else
end end
@@ -309,11 +309,11 @@ local function build_body_line_index(body)
local index = {} local index = {}
local len = #body local len = #body
local newline_count = 0 local newline_count = 0
for i = 1, len do for pos = 1, len do
if i > 1 then if pos > 1 then
index[i] = newline_count + 1 -- line of `i` relative to body index[pos] = newline_count + 1 -- line of `pos` relative to body
end end
if body:byte(i) == 10 then -- '\n' if body:byte(pos) == 10 then -- '\n'
newline_count = newline_count + 1 newline_count = newline_count + 1
end end
end end
@@ -324,7 +324,7 @@ end
--- Count of COP2-nop words contributed by a single top-level token. --- Count of COP2-nop words contributed by a single top-level token.
-- `nop` -> 1 -- `nop` -> 1
-- `nop2` -> 2 (i.e. `nop, nop` baked into one asm arg) -- `nop2` -> 2 (i.e. `nop, nop` baked into one asm word)
-- `nop,` / `nop2,` -> same as above; strip trailing comma defensively -- `nop,` / `nop2,` -> same as above; strip trailing comma defensively
-- anything else -> 0 -- anything else -> 0
-- --
@@ -363,37 +363,37 @@ local function tokenize_body(body)
-- Find comma/newline/semicolon after this token. Read balanced -- Find comma/newline/semicolon after this token. Read balanced
-- groups so commas inside parens/braces/brackets aren't treated -- groups so commas inside parens/braces/brackets aren't treated
-- as separators. Comments / strings are skipped. -- as separators. Comments / strings are skipped.
local i = rel local scan = rel
while i <= len do while scan <= len do
local c = body:byte(i) local c = body:byte(scan)
if c == 44 then break end -- ',' if c == 44 then break end -- ','
if c == 10 then break end -- '\n' if c == 10 then break end -- '\n'
if c == 59 then break end -- ';' if c == 59 then break end -- ';'
if c == 40 then -- '(' if c == 40 then -- '('
local _, a = duffle.read_parens(body, i); i = a local _, a = duffle.read_parens(body, scan); scan = a
elseif c == 123 then -- '{' elseif c == 123 then -- '{'
local _, a = duffle.read_braces(body, i); i = a local _, a = duffle.read_braces(body, scan); scan = a
elseif c == 91 then -- '[' elseif c == 91 then -- '['
local _, a = duffle.read_brackets(body, i); i = a local _, a = duffle.read_brackets(body, scan); scan = a
elseif c == 34 or c == 39 then -- '"' or '\'' elseif c == 34 or c == 39 then -- '"' or '\''
i = duffle.duffle.skip_str_or_cmt(body, i) + 1 scan = duffle.duffle.skip_str_or_cmt(body, scan) + 1
else else
i = i + 1 scan = scan + 1
end end
end end
-- Extract token [rel .. i-1] -- Extract token [rel .. scan-1]
local tok = duffle.trim(body:sub(rel, i - 1)) local tok = duffle.trim(body:sub(rel, scan - 1))
if tok ~= "" then if tok ~= "" then
out[#out + 1] = { tok = tok, rel = rel } out[#out + 1] = { tok = tok, rel = rel }
end end
-- Move past the separator -- Move past the separator
if i <= len then if scan <= len then
i = i + 1 scan = scan + 1
-- Also skip whitespace before next token -- Also skip whitespace before next token
local w = duffle.skip_ws_and_cmt(body, i) local w = duffle.skip_ws_and_cmt(body, scan)
if w > i then i = w end if w > scan then scan = w end
end end
rel = i rel = scan
end end
return out return out
end end
@@ -514,13 +514,13 @@ local function check_mac_yield_uniformity(atoms, findings)
local count = 0 local count = 0
local last_idx = 0 local last_idx = 0
for i, t in ipairs(tokens) do for tok_idx, t in ipairs(tokens) do
local tok = t.tok local tok = t.tok
-- Match `mac_yield(...)` or just `mac_yield`. The bareword -- Match `mac_yield(...)` or just `mac_yield`. The bareword
-- variant is rare in modern style but tolerated. -- variant is rare in modern style but tolerated.
if tok:match("^mac_yield%s*%(") or tok == "mac_yield" then if tok:match("^mac_yield%s*%(") or tok == "mac_yield" then
count = count + 1 count = count + 1
last_idx = i last_idx = tok_idx
end end
end end
local function line_for(idx) local function line_for(idx)
@@ -554,8 +554,8 @@ local function check_mac_yield_uniformity(atoms, findings)
-- post-token is just `nop` or `nop2` or a branch with `, nop` -- post-token is just `nop` or `nop2` or a branch with `, nop`
-- delay slot -- it's the standard "yield, then BD nop" idiom. -- delay slot -- it's the standard "yield, then BD nop" idiom.
local post_non_nop = false local post_non_nop = false
for j = last_idx + 1, #tokens do for search_idx = last_idx + 1, #tokens do
local t = tokens[j].tok local t = tokens[search_idx].tok
if t ~= "" and t ~= "nop" and t ~= "nop2" if t ~= "" and t ~= "nop" and t ~= "nop2"
and not t:match("%,%s*nop%)%s*$") then and not t:match("%,%s*nop%)%s*$") then
post_non_nop = true post_non_nop = true
@@ -607,63 +607,63 @@ end
local function find_binds_structs(source_text) local function find_binds_structs(source_text)
local line_of = duffle.LineIndex(source_text) local line_of = duffle.LineIndex(source_text)
local out = {} local out = {}
local len = #source_text local src_len = #source_text
local i = 1 local pos = 1
while i <= len do while pos <= src_len do
i = duffle.skip_ws_and_cmt(source_text, i); if i > len then break end pos = duffle.skip_ws_and_cmt(source_text, pos); if pos > src_len then break end
if source_text:sub(i, i) == "#" then if source_text:sub(pos, pos) == "#" then
local j = i local eol_pos = pos
while j <= len and source_text:byte(j) ~= 10 do j = j + 1 end while eol_pos <= src_len and source_text:byte(eol_pos) ~= 10 do eol_pos = eol_pos + 1 end
i = j + 1 pos = eol_pos + 1
else else
local ident, after = duffle.read_ident(source_text, i) local ident, ident_end = duffle.read_ident(source_text, pos)
if not ident then if not ident then
i = i + 1 pos = pos + 1
elseif ident == "typedef" then elseif ident == "typedef" then
local j = duffle.skip_ws_and_cmt(source_text, after) local after_typedef = duffle.skip_ws_and_cmt(source_text, ident_end)
local id2, after2 = duffle.read_ident(source_text, j) local id2, id2_end = duffle.read_ident(source_text, after_typedef)
if id2 ~= "Struct_" then if id2 ~= "Struct_" then
i = after2 or (j + 1) pos = id2_end or (after_typedef + 1)
else else
local open = duffle.skip_ws_and_cmt(source_text, after2) local open_paren = duffle.skip_ws_and_cmt(source_text, id2_end)
if source_text:sub(open, open) ~= "(" then if source_text:sub(open_paren, open_paren) ~= "(" then
i = open + 1 pos = open_paren + 1
else else
local inner, after_paren = duffle.read_parens(source_text, open) local inner, after_paren = duffle.read_parens(source_text, open_paren)
local name = duffle.trim(inner) local name = duffle.trim(inner)
local brace = duffle.scan_to_char(source_text, "{", after_paren) local brace = duffle.scan_to_char(source_text, "{", after_paren)
if not brace then if not brace then
i = open + 1 pos = open_paren + 1
else else
local body, after_brace = duffle.read_braces(source_text, brace) local body, after_brace = duffle.read_braces(source_text, brace)
local fields = {} local fields = {}
local byte_off = 0 local byte_off = 0
local k = 1 local body_pos = 1
while k <= #body do while body_pos <= #body do
k = duffle.skip_ws_and_cmt(body, k); if k > #body then break end body_pos = duffle.skip_ws_and_cmt(body, body_pos); if body_pos > #body then break end
local tid, tafter = duffle.read_ident(body, k) local type_ident, type_end = duffle.read_ident(body, body_pos)
if not tid then if not type_ident then
k = k + 1 body_pos = body_pos + 1
elseif tid == "U4" then elseif type_ident == "U4" then
local fid, fafter = duffle.read_ident(body, duffle.skip_ws_and_cmt(body, tafter)) local field_ident, field_end = duffle.read_ident(body, duffle.skip_ws_and_cmt(body, type_end))
if fid then if field_ident then
fields[#fields + 1] = { name = fid, offset = byte_off } fields[#fields + 1] = { name = field_ident, offset = byte_off }
byte_off = byte_off + 4 byte_off = byte_off + 4
end end
k = fafter or (tafter + 1) body_pos = field_end or (type_end + 1)
else else
k = tafter + 1 body_pos = type_end + 1
end end
end end
if name:sub(1, 6) == "Binds_" then if name:sub(1, 6) == "Binds_" then
out[#out + 1] = { line = line_of(i), name = name, fields = fields, bytes = byte_off } out[#out + 1] = { line = line_of(pos), name = name, fields = fields, bytes = byte_off }
end end
i = after_brace pos = after_brace
end end
end end
end end
else else
i = after pos = ident_end
end end
end end
end end
@@ -678,77 +678,77 @@ end
local function find_atom_info(source_text) local function find_atom_info(source_text)
local line_of = duffle.LineIndex(source_text) local line_of = duffle.LineIndex(source_text)
local out = {} local out = {}
local len = #source_text local src_len = #source_text
local i = 1 local pos = 1
while i <= len do while pos <= src_len do
i = duffle.skip_ws_and_cmt(source_text, i); if i > len then break end pos = duffle.skip_ws_and_cmt(source_text, pos); if pos > src_len then break end
if source_text:sub(i, i) == "#" then if source_text:sub(pos, pos) == "#" then
local j = i local eol_pos = pos
while j <= len and source_text:byte(j) ~= 10 do j = j + 1 end while eol_pos <= src_len and source_text:byte(eol_pos) ~= 10 do eol_pos = eol_pos + 1 end
i = j + 1 pos = eol_pos + 1
else else
local ident, after = duffle.read_ident(source_text, i) local ident, ident_end = duffle.read_ident(source_text, pos)
if not ident then if not ident then
i = i + 1 pos = pos + 1
elseif ident == "MipsAtom_" then elseif ident == "MipsAtom_" then
local open = duffle.skip_ws_and_cmt(source_text, after) local open_paren = duffle.skip_ws_and_cmt(source_text, ident_end)
if source_text:sub(open, open) ~= "(" then if source_text:sub(open_paren, open_paren) ~= "(" then
i = open + 1 pos = open_paren + 1
else else
local inner, after_paren = duffle.read_parens(source_text, open) local inner, after_paren = duffle.read_parens(source_text, open_paren)
local a = 1 local name_start = 1
while a <= #inner and inner:sub(a, a):match("[%s]") do a = a + 1 end while name_start <= #inner and inner:sub(name_start, name_start):match("[%s]") do name_start = name_start + 1 end
local b = a local name_end = name_start
while b <= #inner and inner:sub(b, b):match("[%w_]") do b = b + 1 end while name_end <= #inner and inner:sub(name_end, name_end):match("[%w_]") do name_end = name_end + 1 end
local atom_name = inner:sub(a, b - 1) local atom_name = inner:sub(name_start, name_end - 1)
local lookahead = duffle.skip_ws_and_cmt(source_text, after_paren) local lookahead = duffle.skip_ws_and_cmt(source_text, after_paren)
local look_ident, look_after = duffle.read_ident(source_text, lookahead) local look_ident, look_end = duffle.read_ident(source_text, lookahead)
if look_ident == "atom_info" then if look_ident == "atom_info" then
local info_open = duffle.skip_ws_and_cmt(source_text, look_after) local info_open = duffle.skip_ws_and_cmt(source_text, look_end)
if source_text:sub(info_open, info_open) == "(" then if source_text:sub(info_open, info_open) == "(" then
local info_inner, info_after = duffle.read_parens(source_text, info_open) local info_inner, info_after = duffle.read_parens(source_text, info_open)
local binds, reads, writes = nil, nil, nil local binds, reads, writes = nil, nil, nil
local j = 1 local sub_pos = 1
while j <= #info_inner do while sub_pos <= #info_inner do
j = duffle.skip_ws_and_cmt(info_inner, j); if j > #info_inner then break end sub_pos = duffle.skip_ws_and_cmt(info_inner, sub_pos); if sub_pos > #info_inner then break end
local sub_ident, sub_after = duffle.read_ident(info_inner, j) local sub_ident, sub_end = duffle.read_ident(info_inner, sub_pos)
if not sub_ident then if not sub_ident then
j = j + 1 sub_pos = sub_pos + 1
elseif sub_ident == "atom_bind" then elseif sub_ident == "atom_bind" then
local sub_open = duffle.skip_ws_and_cmt(info_inner, sub_after) local sub_open = duffle.skip_ws_and_cmt(info_inner, sub_end)
if info_inner:sub(sub_open, sub_open) == "(" then if info_inner:sub(sub_open, sub_open) == "(" then
local sub_inner, sub_after2 = duffle.read_parens(info_inner, sub_open) local sub_inner, sub_after2 = duffle.read_parens(info_inner, sub_open)
binds = duffle.trim(sub_inner) binds = duffle.trim(sub_inner)
j = sub_after2 sub_pos = sub_after2
else else
j = sub_open + 1 sub_pos = sub_open + 1
end end
elseif sub_ident == "atom_reads" or sub_ident == "atom_writes" then elseif sub_ident == "atom_reads" or sub_ident == "atom_writes" then
local kind = sub_ident local kind = sub_ident
local sub_open = duffle.skip_ws_and_cmt(info_inner, sub_after) local sub_open = duffle.skip_ws_and_cmt(info_inner, sub_end)
if info_inner:sub(sub_open, sub_open) == "(" then if info_inner:sub(sub_open, sub_open) == "(" then
local sub_inner, sub_after2 = duffle.read_parens(info_inner, sub_open) local sub_inner, sub_after2 = duffle.read_parens(info_inner, sub_open)
local regs = {} local regs = {}
local p = 1 local sub_inner_pos = 1
while p <= #sub_inner do while sub_inner_pos <= #sub_inner do
p = duffle.skip_ws_and_cmt(sub_inner, p); if p > #sub_inner then break end sub_inner_pos = duffle.skip_ws_and_cmt(sub_inner, sub_inner_pos); if sub_inner_pos > #sub_inner then break end
local pid, pa = duffle.read_ident(sub_inner, p) local reg_ident, reg_end = duffle.read_ident(sub_inner, sub_inner_pos)
if pid then if reg_ident then
regs[#regs + 1] = duffle.trim(pid) regs[#regs + 1] = duffle.trim(reg_ident)
p = pa sub_inner_pos = reg_end
else else
p = p + 1 sub_inner_pos = sub_inner_pos + 1
end end
if p > #sub_inner then break end if sub_inner_pos > #sub_inner then break end
if sub_inner:sub(p, p) == "," then p = p + 1 end if sub_inner:sub(sub_inner_pos, sub_inner_pos) == "," then sub_inner_pos = sub_inner_pos + 1 end
end end
if kind == "atom_reads" then reads = regs else writes = regs end if kind == "atom_reads" then reads = regs else writes = regs end
j = sub_after2 sub_pos = sub_after2
else else
j = sub_open + 1 sub_pos = sub_open + 1
end end
else else
j = sub_after sub_pos = sub_end
end end
end end
out[#out + 1] = { out[#out + 1] = {
@@ -756,16 +756,16 @@ local function find_atom_info(source_text)
reads = reads or {}, writes = writes or {}, reads = reads or {}, writes = writes or {},
info_line = line_of(lookahead), info_line = line_of(lookahead),
} }
i = info_after pos = info_after
else else
i = info_open + 1 pos = info_open + 1
end end
else else
i = after_paren pos = after_paren
end end
end end
else else
i = after pos = ident_end
end end
end end
end end
@@ -852,8 +852,8 @@ local function check_abi_handoff(atoms, atom_infos, binds_index, findings)
end end
if #found_field_seq == #expected_field_seq then if #found_field_seq == #expected_field_seq then
for k = 1, #expected_field_seq do for field_idx = 1, #expected_field_seq do
if found_field_seq[k] ~= expected_field_seq[k] then if found_field_seq[field_idx] ~= expected_field_seq[field_idx] then
findings[#findings + 1] = { findings[#findings + 1] = {
atom = a.name, line = a.line, atom = a.name, line = a.line,
check = "abi_handoff", kind = "error", check = "abi_handoff", kind = "error",
@@ -993,9 +993,9 @@ end
--- walker uses them as branch targets. --- walker uses them as branch targets.
local function find_atom_labels(tokens) local function find_atom_labels(tokens)
local labels = {} local labels = {}
for i, t in ipairs(tokens) do for tok_idx, t in ipairs(tokens) do
local name = t.tok:match("^atom_label%s*%(%s*([%w_]+)%s*%)") local name = t.tok:match("^atom_label%s*%(%s*([%w_]+)%s*%)")
if name then labels[name] = i end if name then labels[name] = tok_idx end
end end
return labels return labels
end end
@@ -1005,20 +1005,20 @@ end
--- `atom_offset(F, label)` call, the label name is recorded; otherwise --- `atom_offset(F, label)` call, the label name is recorded; otherwise
--- the branch's target is unknown (likely a literal offset) and we --- the branch's target is unknown (likely a literal offset) and we
--- record `false` as a sentinel. The CFG walker checks KEY PRESENCE --- record `false` as a sentinel. The CFG walker checks KEY PRESENCE
--- (via `is_branch(i)`) to decide whether a token is a branch; it --- (via `is_branch(tok_idx)`) to decide whether a token is a branch; it
--- checks the value to decide whether the taken-path target is known. --- checks the value to decide whether the taken-path target is known.
--- (We can't use `nil` for the unknown-target case because `targets[i] = nil` --- (We can't use `nil` for the unknown-target case because `targets[tok_idx] = nil`
--- REMOVES the key from the Lua table, which would make `is_branch(i)` --- REMOVES the key from the Lua table, which would make `is_branch(tok_idx)`
--- return false for both "not a branch" and "branch with unknown target".) --- return false for both "not a branch" and "branch with unknown target".)
local function find_branch_targets(tokens) local function find_branch_targets(tokens)
local targets = {} local targets = {}
for i, t in ipairs(tokens) do for tok_idx, t in ipairs(tokens) do
if t.tok:match("^branch_[%w_]+%s*%(") then if t.tok:match("^branch_[%w_]+%s*%(") then
-- branch_<cond>(rs, atom_offset(F, label)) or -- branch_<cond>(rs, atom_offset(F, label)) or
-- branch_<cond>(rs, rt, atom_offset(F, label)) -- branch_<cond>(rs, rt, atom_offset(F, label))
-- atom_offset's arg list is (flag, name); we want the name. -- atom_offset's arg list is (flag, name); we want the name.
local label = t.tok:match("atom_offset%s*%([^,]+,%s*([%w_]+)%s*%)") local label = t.tok:match("atom_offset%s*%([^,]+,%s*([%w_]+)%s*%)")
targets[i] = label or false -- `false` = known branch, unknown target targets[tok_idx] = label or false -- `false` = known branch, unknown target
end end
end end
return targets return targets
@@ -1052,9 +1052,9 @@ local function analyze_atom_paths(atom)
local n = #tokens local n = #tokens
local costs = {} local costs = {}
local unknown_set = {} local unknown_set = {}
for i, t in ipairs(tokens) do for tok_idx, t in ipairs(tokens) do
local c, _, unknown = token_cycles(t.tok) local c, _, unknown = token_cycles(t.tok)
costs[i] = c costs[tok_idx] = c
if unknown then if unknown then
unknown_set[t.tok:match("^([%w_]+)") or "?"] = true unknown_set[t.tok:match("^([%w_]+)") or "?"] = true
end end
@@ -1063,42 +1063,42 @@ local function analyze_atom_paths(atom)
-- A token is a terminator if it's `mac_yield` or `mac_yield(...)`. -- A token is a terminator if it's `mac_yield` or `mac_yield(...)`.
-- The yield transfers control; we don't count its cost (the next -- The yield transfers control; we don't count its cost (the next
-- atom's prologue absorbs it). -- atom's prologue absorbs it).
local function is_terminator(i) local function is_terminator(tok_idx)
local tok = tokens[i].tok local tok = tokens[tok_idx].tok
return tok == "mac_yield" or tok:match("^mac_yield%s*%(") return tok == "mac_yield" or tok:match("^mac_yield%s*%(")
end end
-- CFG successor function. Returns a list of next token indices for -- CFG successor function. Returns a list of next token indices for
-- the given position. Branch tokens produce 2 successors (fall-through -- the given position. Branch tokens produce 2 successors (fall-through
-- + taken); normal tokens produce 1 (next); terminators produce 0. -- + taken); normal tokens produce 1 (next); terminators produce 0.
-- BD-slot absorption: a branch at i skips i+1 (the BD slot) in its -- BD-slot absorption: a branch at tok_idx skips tok_idx+1 (the BD slot) in its
-- fall-through path; the BD slot's cost is added to the branch's -- fall-through path; the BD slot's cost is added to the branch's
-- own cost instead (so it's counted once). -- own cost instead (so it's counted once).
-- --
-- A token is a "branch" if its index is a KEY in the `branches` -- A token is a "branch" if its index is a KEY in the `branches`
-- map (regardless of whether the value is nil — a branch with -- map (regardless of whether the value is nil — a branch with
-- nil target means "literal offset, taken path is unknown"). -- nil target means "literal offset, taken path is unknown").
-- We check key-presence via `branches[i] ~= nil` because -- We check key-presence via `branches[tok_idx] ~= nil` because
-- `branches[i]` returns nil for both "absent" AND "present with -- `branches[tok_idx]` returns nil for both "absent" AND "present with
-- nil value" — distinguishing them requires the key check. -- nil value" — distinguishing them requires the key check.
local function is_branch(i) local function is_branch(tok_idx)
local v = branches[i] local v = branches[tok_idx]
if v == nil then return false end if v == nil then return false end
-- v is non-nil: either a string (atom_offset target) or false -- v is non-nil: either a string (atom_offset target) or false
-- (literal offset, no target). Both indicate a branch. -- (literal offset, no target). Both indicate a branch.
return true return true
end end
local function successors(i) local function successors(tok_idx)
local tok = tokens[i].tok local tok = tokens[tok_idx].tok
if is_terminator(i) then if is_terminator(tok_idx) then
return {}, i -- empty list; term = i signals "path ends here" return {}, tok_idx -- empty list; term = tok_idx signals "path ends here"
end end
if is_branch(i) then if is_branch(tok_idx) then
local label = branches[i] -- may be false for literal-offset branches local label = branches[tok_idx] -- may be false for literal-offset branches
local succ = {} local succ = {}
-- Fall-through: skip the BD slot (i+1). Use i+2. -- Fall-through: skip the BD slot (tok_idx+1). Use tok_idx+2.
if i + 2 <= n then if tok_idx + 2 <= n then
succ[#succ + 1] = i + 2 succ[#succ + 1] = tok_idx + 2
end end
-- Taken: only if the branch has a known atom_offset target. -- Taken: only if the branch has a known atom_offset target.
if label then if label then
@@ -1114,8 +1114,8 @@ local function analyze_atom_paths(atom)
return succ, nil return succ, nil
end end
-- Normal token: just the next one -- Normal token: just the next one
if i + 1 <= n then if tok_idx + 1 <= n then
return { i + 1 }, nil return { tok_idx + 1 }, nil
end end
return {}, nil return {}, nil
end end
@@ -1129,16 +1129,16 @@ local function analyze_atom_paths(atom)
local cycles_max = -1 local cycles_max = -1
local path_count = 0 local path_count = 0
local has_loops = false local has_loops = false
local function dfs(i, acc, visited) local function dfs(tok_idx, acc, visited)
if path_count >= MAX_PATHS then return end if path_count >= MAX_PATHS then return end
if _G._DEBUG_DFS then if _G._DEBUG_DFS then
io.stderr:write(string.format("dfs(i=%d, acc=%d)\n", i, acc)) io.stderr:write(string.format("dfs(tok_idx=%d, acc=%d)\n", tok_idx, acc))
end end
if visited[i] then if visited[tok_idx] then
has_loops = true has_loops = true
if _G._DEBUG_DFS_LOOP then if _G._DEBUG_DFS_LOOP then
io.stderr:write(string.format(" -> LOOP at i=%d (tok=%s) acc=%d\n", io.stderr:write(string.format(" -> LOOP at tok_idx=%d (tok=%s) acc=%d\n",
i, tokens[i].tok, acc)) tok_idx, tokens[tok_idx].tok, acc))
end end
return return
end end
@@ -1146,14 +1146,14 @@ local function analyze_atom_paths(atom)
-- Add this token's cost. For a branch, ADD the BD-slot cost too -- Add this token's cost. For a branch, ADD the BD-slot cost too
-- (and skip the BD slot in the successor list — already done in -- (and skip the BD slot in the successor list — already done in
-- `successors` above for fall-through; for taken path the BD -- `successors` above for fall-through; for taken path the BD
-- slot was at i+1 which is now skipped entirely). -- slot was at tok_idx+1 which is now skipped entirely).
local cost = costs[i] local cost = costs[tok_idx]
if is_branch(i) and i + 1 <= n then if is_branch(tok_idx) and tok_idx + 1 <= n then
cost = cost + costs[i + 1] cost = cost + costs[tok_idx + 1]
end end
local new_acc = acc + cost local new_acc = acc + cost
local succ, term = successors(i) local succ, term = successors(tok_idx)
if term then if term then
-- Terminator: record the path's cycle sum. We do NOT add -- Terminator: record the path's cycle sum. We do NOT add
-- the terminator token to `visited` -- a path ends here, so -- the terminator token to `visited` -- a path ends here, so
@@ -1166,18 +1166,18 @@ local function analyze_atom_paths(atom)
if new_acc > cycles_max then cycles_max = new_acc end if new_acc > cycles_max then cycles_max = new_acc end
return return
end end
visited[i] = true visited[tok_idx] = true
for _, next_i in ipairs(succ) do for _, next_tok_idx in ipairs(succ) do
dfs(next_i, new_acc, visited) dfs(next_tok_idx, new_acc, visited)
end end
visited[i] = nil visited[tok_idx] = nil
end end
if n >= 1 then dfs(1, 0, {}) end if n >= 1 then dfs(1, 0, {}) end
-- cycles_full: sum of every token's cost (the previous model; useful -- cycles_full: sum of every token's cost (the previous model; useful
-- for comparing against the path-aware min/max). -- for comparing against the path-aware min/max).
local cycles_full = 0 local cycles_full = 0
for i = 1, n do cycles_full = cycles_full + costs[i] end for tok_idx = 1, n do cycles_full = cycles_full + costs[tok_idx] end
-- If no paths were recorded (e.g. atom body is empty), cycles_min/max -- If no paths were recorded (e.g. atom body is empty), cycles_min/max
-- default to 0 (atom costs nothing). cycles_full is 0 too in that case. -- default to 0 (atom costs nothing). cycles_full is 0 too in that case.
@@ -1185,7 +1185,7 @@ local function analyze_atom_paths(atom)
if cycles_max == -1 then cycles_max = 0 end if cycles_max == -1 then cycles_max = 0 end
local unknown_list = {} local unknown_list = {}
for k in pairs(unknown_set) do unknown_list[#unknown_list + 1] = k end for macro_name in pairs(unknown_set) do unknown_list[#unknown_list + 1] = macro_name end
table.sort(unknown_list) table.sort(unknown_list)
-- branch_count: number of `branch_*(...)` tokens. (More useful than -- branch_count: number of `branch_*(...)` tokens. (More useful than
@@ -1220,7 +1220,7 @@ local function count_atom_cycles(atom)
end end
end end
local unknown_list = {} local unknown_list = {}
for k in pairs(unknown_set) do unknown_list[#unknown_list + 1] = k end for macro_name in pairs(unknown_set) do unknown_list[#unknown_list + 1] = macro_name end
return total, unknown_list return total, unknown_list
end end
+53 -1
View File
@@ -107,6 +107,28 @@ end
-- │ Shared utility: scan_dir │ -- │ Shared utility: scan_dir │
-- └────────────────────────────────────────────────────────────────────┘ -- └────────────────────────────────────────────────────────────────────┘
--- Recursively scan a directory for files matching a glob suffix.
--- No regex per the no_regex constraint — uses plain byte matching
--- via `dir /b /s` on Windows.
---
--- The `.macs.h` files produced by the components pass always live at
--- `<project_root>/<module>/gen/`. We can shortcut the `dir /b /s` walk by
--- listing modules first (one `dir /b /ad`), then walking each `<module>/gen/`
--- (one `dir /b` per module, no recursion). For projects with 2 modules and
--- 0 .macs.h files, this drops the cost from ~52ms (full recursive walk of
--- the entire project tree) to ~5ms.
---
--- @param dir string -- directory to scan (absolute or relative)
--- @param suffix string -- file pattern, e.g. "*.macs.h"
--- @return string[]
-- Cache the scan_dir result per (dir, suffix) in package.loaded. Each
-- `io.popen` call on Windows is ~50-100ms of subprocess overhead, so
-- caching the result saves a fixed cost on every build. The cache
-- persists for the lifetime of the Lua process (cleared when ps1_meta.lua
-- exits). If a build removes/creates .macs.h files mid-process, the
-- caller can invalidate by calling `M._invalidate_scan_cache()`.
local SCAN_CACHE_KEY = "__word_count_eval_scan_cache__"
--- Recursively scan a directory for files matching a glob suffix. --- Recursively scan a directory for files matching a glob suffix.
--- No regex per the no_regex constraint — uses plain byte matching --- No regex per the no_regex constraint — uses plain byte matching
--- via `dir /b /s` on Windows. --- via `dir /b /s` on Windows.
@@ -115,17 +137,47 @@ end
--- @param suffix string -- file pattern, e.g. "*.macs.h" --- @param suffix string -- file pattern, e.g. "*.macs.h"
--- @return string[] --- @return string[]
function M.scan_dir(dir, suffix) function M.scan_dir(dir, suffix)
local key = dir .. "\0" .. suffix
-- Check the in-process cache first. (Mostly helps when a build
-- triggers multiple `M.run` calls -- e.g. the audit_lua_nesting
-- script's stress tests -- but the cost is ~free either way.)
local cache = package.loaded[SCAN_CACHE_KEY]
if cache and cache[key] then
return cache[key]
end
local results = {} local results = {}
local pipe = io.popen(DIR_GLOB_CMD:format(dir, suffix)) local pipe = io.popen(DIR_GLOB_CMD:format(dir, suffix))
if not pipe then return results end if not pipe then
-- Cache the empty result too (avoids re-scan if the dir is
-- genuinely empty -- e.g. a clean build before components
-- has run yet).
cache = cache or {}
cache[key] = results
package.loaded[SCAN_CACHE_KEY] = cache
return results
end
for raw_line in pipe:lines() do for raw_line in pipe:lines() do
local path = raw_line:gsub(PATH_SEP_BACKSLASH, PATH_SEP_FORWARD) local path = raw_line:gsub(PATH_SEP_BACKSLASH, PATH_SEP_FORWARD)
results[#results + 1] = path results[#results + 1] = path
end end
pipe:close() pipe:close()
-- Cache the result.
cache = cache or {}
cache[key] = results
package.loaded[SCAN_CACHE_KEY] = cache
return results return results
end end
--- Invalidate the scan cache (call after creating new .macs.h files
--- in the same Lua process — usually not needed).
function M._invalidate_scan_cache()
package.loaded[SCAN_CACHE_KEY] = nil
end
-- ┌────────────────────────────────────────────────────────────────────┐ -- ┌────────────────────────────────────────────────────────────────────┐
-- │ Shared utility: count_body_words │ -- │ Shared utility: count_body_words │
-- └────────────────────────────────────────────────────────────────────┘ -- └────────────────────────────────────────────────────────────────────┘
+33 -33
View File
@@ -79,9 +79,9 @@ local PASS_FLAG_DISPATCH_KEY = "__pass__"
--- @class PassOutputEntry --- @class PassOutputEntry
--- @field [string] string -- dynamic shape; key is the output kind --- @field [string] string -- dynamic shape; key is the output kind
-- (e.g. "macs_h", "offsets_h", "errors_h", -- (e.g. "macs_h", "offsets_h", "errors_h",
-- "annotations_txt", "static_analysis_txt", -- "annotations_txt", "static_analysis_txt",
-- "summary_txt"), value is the path -- "summary_txt"), value is the path
--- @class Finding --- @class Finding
--- @field line integer -- source line (or 0 for pass-level) --- @field line integer -- source line (or 0 for pass-level)
@@ -186,11 +186,11 @@ local function request_all_passes(args)
end end
end end
-- Per-flag handlers. Each handler takes (args, argv, i) and returns -- Per-flag handlers. Each handler takes (args, argv, arg_idx) and
-- the new i (so multi-arg flags like --source FILE advance it). -- returns the new arg_idx (so multi-arg flags like --source FILE
-- Returning nil + os.exit() handles termination flags (--help). -- advance it). Returning nil + os.exit() handles termination flags
-- This replaces the 8-way `if/elseif/elseif...` chain that nested -- (--help). This replaces the 8-way `if/elseif/elseif...` chain
-- 4 levels deep and made the dispatch logic hard to scan. -- that nested 4 levels deep and made the dispatch logic hard to scan.
local FLAG_HANDLERS = {} local FLAG_HANDLERS = {}
-- ════════════════════════════════════════════════════════════════════════════ -- ════════════════════════════════════════════════════════════════════════════
@@ -233,11 +233,11 @@ EXAMPLE:
]]) ]])
end end
-- Per-flag handlers. Each takes (args, argv, i) and returns the new i -- Per-flag handlers. Each takes (args, argv, arg_idx) and returns
-- (so multi-arg flags like --source FILE advance it). Termination -- the new arg_idx (so multi-arg flags like --source FILE advance
-- flags like --help call os.exit() instead. This replaces the 8-way -- it). Termination flags like --help call os.exit() instead. This
-- `if/elseif/elseif...` chain that nested 4 levels deep and made the -- replaces the 8-way `if/elseif/elseif...` chain that nested 4
-- dispatch logic hard to scan. -- levels deep and made the dispatch logic hard to scan.
-- --
-- Populated AFTER print_help so the --help handler can reference it -- Populated AFTER print_help so the --help handler can reference it
-- as an upvalue (Lua resolves locals at closure-call time, but if the -- as an upvalue (Lua resolves locals at closure-call time, but if the
@@ -255,24 +255,24 @@ FLAG_HANDLERS["--verbose"] = function(args)
args.verbose = true args.verbose = true
end end
FLAG_HANDLERS["--source"] = function(args, argv, i) FLAG_HANDLERS["--source"] = function(args, argv, arg_idx)
args.sources[#args.sources + 1] = argv[i + 1] args.sources[#args.sources + 1] = argv[arg_idx + 1]
return i + 1 return arg_idx + 1
end end
FLAG_HANDLERS["--metadata"] = function(args, argv, i) FLAG_HANDLERS["--metadata"] = function(args, argv, arg_idx)
args.metadata = argv[i + 1] args.metadata = argv[arg_idx + 1]
return i + 1 return arg_idx + 1
end end
FLAG_HANDLERS["--out-root"] = function(args, argv, i) FLAG_HANDLERS["--out-root"] = function(args, argv, arg_idx)
args.out_root = argv[i + 1] args.out_root = argv[arg_idx + 1]
return i + 1 return arg_idx + 1
end end
FLAG_HANDLERS["--project-root"] = function(args, argv, i) FLAG_HANDLERS["--project-root"] = function(args, argv, arg_idx)
args.project_root = argv[i + 1] args.project_root = argv[arg_idx + 1]
return i + 1 return arg_idx + 1
end end
-- Pass-flag handler. Reads the closed-set table, expands --all, -- Pass-flag handler. Reads the closed-set table, expands --all,
@@ -539,12 +539,12 @@ local function render_dep_graph(passes, requested, closed)
local function add(s) lines[#lines + 1] = s end local function add(s) lines[#lines + 1] = s end
add("[ps1_meta] Resolved dependency order (closed under deps):") add("[ps1_meta] Resolved dependency order (closed under deps):")
for i, name in ipairs(closed) do for pass_idx, name in ipairs(closed) do
local p = passes[name] local p = passes[name]
local deps_str = (#p.deps == 0) and "(no deps)" or local deps_str = (#p.deps == 0) and "(no deps)" or
"(deps: " .. table.concat(p.deps, ", ") .. ")" "(deps: " .. table.concat(p.deps, ", ") .. ")"
add(string.format(" %d. %-22s %-45s [%s]", add(string.format(" %d. %-22s %-45s [%s]",
i, name, deps_str, p.kind)) pass_idx, name, deps_str, p.kind))
end end
add("") add("")
@@ -556,11 +556,11 @@ local function render_dep_graph(passes, requested, closed)
add(" +-----------+ +-----------------+ +-----------------+") add(" +-----------+ +-----------------+ +-----------------+")
add(" | word- |-->| components |-->| offsets |") add(" | word- |-->| components |-->| offsets |")
add(" | counts | +-----------------+ +-----------------+") add(" | counts | +-----------------+ +-----------------+")
add(" | (load) | | ^") add(" | (load) | | ^")
add(" +-----------+ | |") add(" +-----------+ | |")
add(" | v |") add(" | v |")
add(" | code/<module>/gen/<basename>.macs.h |") add(" | code/<module>/gen/<basename>.macs.h |")
add(" | (header - co-located for #include) |") add(" | (header - co-located for #include) |")
add(" | |") add(" | |")
add(" | +-----------------+ |") add(" | +-----------------+ |")
add(" +---------->| annotation |--------------+") add(" +---------->| annotation |--------------+")
@@ -569,7 +569,7 @@ local function render_dep_graph(passes, requested, closed)
add(" | v |") add(" | v |")
add(" | build/gen/<basename>.errors.h |") add(" | build/gen/<basename>.errors.h |")
add(" | build/gen/<basename>.annotations.txt |") add(" | build/gen/<basename>.annotations.txt |")
add(" | (report - NOT #included) |") add(" | (report - NOT #included) |")
add(" | |") add(" | |")
add(" | +-----------------+ |") add(" | +-----------------+ |")
add(" +---------->| static-analysis |--------------+") add(" +---------->| static-analysis |--------------+")