mirror of
https://github.com/Ed94/pikuma_ps1.git
synced 2026-08-06 15:48:49 +00:00
Compare commits
5
Commits
987f4dee1e
...
97d2f66c5a
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
97d2f66c5a | ||
|
|
d9406553b3 | ||
|
|
e662d175ab | ||
|
|
5387a07b84 | ||
|
|
65d805e3ba |
@@ -15,3 +15,5 @@ toolchain/PSn00bSDK
|
||||
*.a
|
||||
.sentry-native
|
||||
.vscode/settings.json
|
||||
toolchain/lfs
|
||||
toolchain/lpeg
|
||||
|
||||
+138
-9
@@ -20,6 +20,12 @@
|
||||
|
||||
local M = {}
|
||||
|
||||
-- Optional native extension: lfs (LuaFileSystem). When present, ensure_dir uses
|
||||
-- lfs.attributes + lfs.mkdir instead of spawning `cmd.exe mkdir` — saves ~55ms per
|
||||
-- unique directory on Windows. Built by `update_deps.ps1` to `toolchain/lfs/lfs.dll`
|
||||
-- and wired into package.cpath by `scripts/duffle_paths.lua`.
|
||||
local lfs = pcall(require, "lfs") and require("lfs") or nil
|
||||
|
||||
-- ════════════════════════════════════════════════════════════════════════════
|
||||
-- Cross-file type aliases
|
||||
-- ════════════════════════════════════════════════════════════════════════════
|
||||
@@ -308,21 +314,37 @@ end
|
||||
-- Convert a (possibly relative) path to an absolute path, using CWD if needed.
|
||||
-- Normalizes forward slashes to backslashes on Windows.
|
||||
-- Used for byte-identical emit: the // Source: comment line uses the absolute path.
|
||||
--
|
||||
-- The CWD is memoized (one `io.popen("cd")` per process — ~50ms on Windows).
|
||||
-- Without the cache, calling this per-source in the components pass added ~1.5s to a 30-source build.
|
||||
-- @param path string
|
||||
-- @return string
|
||||
local _absolute_path_cache = {}
|
||||
|
||||
function M.to_absolute_path(path)
|
||||
if _absolute_path_cache[path] then return _absolute_path_cache[path] end
|
||||
if #path >= 2 and path:sub(2, 2) == ":" then
|
||||
-- Already absolute; normalize slashes for consistency.
|
||||
return (path:gsub("/", "\\"))
|
||||
local result = (path:gsub("/", "\\"))
|
||||
_absolute_path_cache[path] = result
|
||||
return result
|
||||
end
|
||||
local p = io.popen("cd")
|
||||
if not p then return path end
|
||||
local cwd = p:read("*l")
|
||||
p:close()
|
||||
if not cwd then return path end
|
||||
-- Native: lfs.currentdir() is ~0ms vs io.popen("cd") at ~50ms per call.
|
||||
local cwd
|
||||
if lfs then
|
||||
cwd = lfs.currentdir()
|
||||
else
|
||||
local p = io.popen("cd")
|
||||
if not p then _absolute_path_cache[path] = path; return path end
|
||||
cwd = p:read("*l")
|
||||
p:close()
|
||||
end
|
||||
if not cwd then _absolute_path_cache[path] = path; return path end
|
||||
cwd = cwd:gsub("/", "\\")
|
||||
local tail = (path:gsub("/", "\\"))
|
||||
return cwd .. "\\" .. tail
|
||||
local result = cwd .. "\\" .. tail
|
||||
_absolute_path_cache[path] = result
|
||||
return result
|
||||
end
|
||||
|
||||
-- Cache of directories already verified to exist in this process.
|
||||
@@ -334,8 +356,15 @@ local _ensured_dirs = {}
|
||||
function M.ensure_dir(path)
|
||||
if _ensured_dirs[path] then return end
|
||||
_ensured_dirs[path] = true
|
||||
local is_win = package.config:sub(1, 1) == "\\"
|
||||
os.execute(is_win and ('if not exist "' .. path .. '" mkdir "' .. path .. '"') or ('mkdir -p "' .. path .. '" 2>/dev/null'))
|
||||
if lfs then
|
||||
-- Native: ~0ms when dir exists (the common case). lfs.mkdir on a new dir is ~2ms (no shell spawn).
|
||||
-- Falls through silently if lfs.mkdir fails (e.g. permission denied); the subsequent write_file will surface the error.
|
||||
if lfs.attributes(path, "mode") ~= "directory" then lfs.mkdir(path) end
|
||||
else
|
||||
-- Fallback: shell mkdir. Slow (~55ms per call on Windows due to cmd.exe spawn) but works without lfs.
|
||||
local is_win = package.config:sub(1, 1) == "\\"
|
||||
os.execute(is_win and ('if not exist "' .. path .. '" mkdir "' .. path .. '"') or ('mkdir -p "' .. path .. '" 2>/dev/null'))
|
||||
end
|
||||
end
|
||||
|
||||
-- Test helper: clear the cache (used by tests + between process runs).
|
||||
@@ -549,6 +578,106 @@ function M.split_top_level_commas(body)
|
||||
return tokens
|
||||
end
|
||||
|
||||
-- ════════════════════════════════════════════════════════════════════════════
|
||||
-- Section 4b: tokenize_body + build_body_line_index (shared, memoized)
|
||||
-- ════════════════════════════════════════════════════════════════════════════
|
||||
|
||||
-- Moved here from passes/static_analysis.lua so all passes can share the memoized
|
||||
-- per-body tokenization. The memoization key is the body string (immutable per pass).
|
||||
|
||||
local _tokenize_body_cache = {}
|
||||
local _body_line_index_cache = {}
|
||||
|
||||
--- Tokenize the body inner-text into a flat list of `{tok, rel}` pairs.
|
||||
--- `tok` is the trimmed token string; `rel` is the byte offset within `body`.
|
||||
--- Memoized on the body string — first call pays O(body_len), subsequent calls return cached.
|
||||
--- @param body string
|
||||
--- @return table[] -- {{tok=string, rel=integer}, ...}
|
||||
function M.tokenize_body(body)
|
||||
if _tokenize_body_cache[body] ~= nil then return _tokenize_body_cache[body] end
|
||||
local out = {}
|
||||
local len = #body
|
||||
local rel = 1
|
||||
while rel <= len do
|
||||
local ws_end = M.skip_ws_and_cmt(body, rel)
|
||||
if ws_end > rel then rel = ws_end end
|
||||
if rel > len then break end
|
||||
|
||||
local scan = rel
|
||||
while scan <= len do
|
||||
local c = body:byte(scan)
|
||||
if c == 44 then break end -- ','
|
||||
if c == 10 then break end -- '\n'
|
||||
if c == 59 then break end -- ';'
|
||||
if c == 40 then local _, a = M.read_parens (body, scan); scan = a -- '('
|
||||
elseif c == 123 then local _, a = M.read_braces (body, scan); scan = a -- '{'
|
||||
elseif c == 91 then local _, a = M.read_brackets (body, scan); scan = a -- '['
|
||||
elseif c == 34 or c == 39 then scan = M.skip_str_or_cmt(body, scan) + 1 -- '"' or '\''
|
||||
else
|
||||
scan = scan + 1
|
||||
end
|
||||
end
|
||||
local tok = M.trim(body:sub(rel, scan - 1))
|
||||
if tok ~= "" then out[#out + 1] = { tok = tok, rel = rel } end
|
||||
if scan <= len then
|
||||
scan = scan + 1
|
||||
local w = M.skip_ws_and_cmt(body, scan)
|
||||
if w > scan then scan = w end
|
||||
end
|
||||
rel = scan
|
||||
end
|
||||
_tokenize_body_cache[body] = out
|
||||
return out
|
||||
end
|
||||
|
||||
--- Tokenize the body into a flat list of trimmed string tokens (preserves comments).
|
||||
--- Uses `split_top_level_commas` (which appends trailing comments to the previous token)
|
||||
--- so the components pass can emit `/* Words: ... */` comments in the .macs.h output.
|
||||
--- @param body string
|
||||
--- @return string[]
|
||||
function M.tokenize_body_simple(body)
|
||||
local tokens = M.split_top_level_commas(body)
|
||||
local out = {}
|
||||
for i = 1, #tokens do out[i] = M.trim(tokens[i]) end
|
||||
return out
|
||||
end
|
||||
|
||||
--- Build a line-index: count `\n` chars from offset 1 up to the offset; that count + 1 is the line number (1-based).
|
||||
--- Memoized on the body string.
|
||||
--- @param body string
|
||||
--- @return table -- index[pos] = line_number
|
||||
function M.build_body_line_index(body)
|
||||
if _body_line_index_cache[body] ~= nil then return _body_line_index_cache[body] end
|
||||
local index = {}
|
||||
local len = #body
|
||||
local newline_count = 0
|
||||
for pos = 1, len do
|
||||
if pos > 1 then
|
||||
index[pos] = newline_count + 1
|
||||
end
|
||||
if body:byte(pos) == 10 then
|
||||
newline_count = newline_count + 1
|
||||
end
|
||||
end
|
||||
index[len + 1] = newline_count + 1
|
||||
_body_line_index_cache[body] = index
|
||||
return index
|
||||
end
|
||||
|
||||
--- Find the end of a marker call (`atom_label(...)` or `atom_offset(...)`).
|
||||
--- Returns the position past the closing `)`, or nil if the token isn't a marker call.
|
||||
--- @param tok string
|
||||
--- @return integer|nil
|
||||
function M.find_marker_call_end(tok)
|
||||
local ident, after = M.read_ident(tok, 1)
|
||||
if not ident then return nil end
|
||||
if ident ~= "atom_label" and ident ~= "atom_offset" then return nil end
|
||||
local paren_pos = M.skip_ws_and_cmt(tok, after)
|
||||
if tok:sub(paren_pos, paren_pos) ~= "(" then return nil end
|
||||
local _, close = M.read_parens(tok, paren_pos)
|
||||
return close
|
||||
end
|
||||
|
||||
-- ════════════════════════════════════════════════════════════════════════════
|
||||
-- Section 5: load_word_counts
|
||||
-- ════════════════════════════════════════════════════════════════════════════
|
||||
|
||||
+51
-14
@@ -19,24 +19,58 @@
|
||||
|
||||
local M = {}
|
||||
|
||||
-- Cache key for the repo root. Stored in `package.loaded` (process-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.
|
||||
-- Cache key for the repo root. Stored in `package.loaded` (process-global) so all 8 entry scripts + passes scripts share one resolution.
|
||||
local CACHE_KEY = "__duffle_repo_root__"
|
||||
|
||||
--- Resolve the repo root via git (cached after first call).
|
||||
--- Returns a normalized path with a trailing forward-slash, or nil if not in a git repo.
|
||||
--- Resolve the repo root from this script's own path. Zero shell spawn.
|
||||
--- `duffle_paths.lua` always lives at `<repo>/scripts/duffle_paths.lua`, so the repo root is the
|
||||
--- parent of the directory containing this script. We derive it directly from `debug.getinfo(1, "S").source`
|
||||
--- (returns `@<path>` for the currently-running chunk).
|
||||
---
|
||||
--- Replaces the prior `io.popen("git rev-parse --show-toplevel")` approach, which cost ~100-180ms per
|
||||
--- LuaJIT process on Windows due to git's CLI startup. The path-derive approach costs <1ms.
|
||||
---
|
||||
--- If this script's path can't be parsed (shouldn't happen — dofile/debug.getinfo always populates source),
|
||||
--- fall back to a defensive walk: starting from this script's directory, walk UP until we find a parent that
|
||||
--- contains a `scripts/` directory. The first match is the repo root.
|
||||
--- @return string|nil
|
||||
local function find_repo_root()
|
||||
if package.loaded[CACHE_KEY] then return package.loaded[CACHE_KEY] end
|
||||
local p = io.popen("git rev-parse --show-toplevel 2>nul")
|
||||
local root
|
||||
if p then root = p:read("*l"); p:close() end
|
||||
if not root or root == "" then return nil end
|
||||
-- Normalize to forward slashes (Windows accepts both, but mixed `\` + `/` confuses LuaJIT's file APIs).
|
||||
root = root:gsub("\\", "/")
|
||||
if not root:match("/$") then root = root .. "/" end
|
||||
package.loaded[CACHE_KEY] = root
|
||||
return root
|
||||
|
||||
local source = debug.getinfo(1, "S").source
|
||||
-- Strip the leading `@` (Lua's dofile marker) and the trailing `/duffle_paths.lua` filename.
|
||||
-- What remains is the directory containing this script, i.e. `<repo>/scripts/` (with trailing slash or not).
|
||||
local scripts_dir = source and source:match("^@?(.*)[/\\]duffle_paths%.lua$")
|
||||
if scripts_dir then
|
||||
-- The repo root is the parent of `scripts/`. Strip the trailing `scripts/` (with or without trailing slash).
|
||||
local root = scripts_dir:gsub("scripts[\\/]?$", "")
|
||||
root = root:gsub("\\", "/")
|
||||
if root == "" then root = "./" end
|
||||
if not root:match("/$") then root = root .. "/" end
|
||||
package.loaded[CACHE_KEY] = root
|
||||
return root
|
||||
end
|
||||
|
||||
-- Defensive fallback: walk UP from this script's directory until we find a parent that contains `scripts/`.
|
||||
-- In practice this branch never fires — debug.getinfo always returns a source for dofile()'d chunks.
|
||||
local lfs = pcall(require, "lfs") and require("lfs") or nil
|
||||
if lfs then
|
||||
local dir = source and source:match("^@?(.*[/\\])") or "./"
|
||||
dir = dir:gsub("\\", "/")
|
||||
while dir and dir ~= "" do
|
||||
local candidate_scripts = dir .. "scripts"
|
||||
if lfs.attributes(candidate_scripts, "mode") == "directory" then
|
||||
dir = dir:gsub("/$", "")
|
||||
package.loaded[CACHE_KEY] = dir .. "/"
|
||||
return dir .. "/"
|
||||
end
|
||||
local parent = dir:match("^(.*)/[^/]+/$")
|
||||
if not parent then break end
|
||||
dir = parent .. "/"
|
||||
end
|
||||
end
|
||||
|
||||
return nil
|
||||
end
|
||||
|
||||
--- Set `package.path` (for `require("duffle")` + `require("passes.X")`) and
|
||||
@@ -62,9 +96,12 @@ function M.setup()
|
||||
.. package.path
|
||||
|
||||
-- lpeg: built by `update_deps.ps1` to `toolchain/lpeg/lpeg.dll`.
|
||||
-- Wire its directory into cpath so `require("lpeg")` resolves.
|
||||
-- lfs: compiled from pcsx-redux's vendored luafilesystem source to `toolchain/lfs/lfs.dll`.
|
||||
-- Wire both directories into cpath so `require("lpeg")` and `require("lfs")` resolve.
|
||||
local lpeg_dir = repo_root .. "toolchain/lpeg/"
|
||||
local lfs_dir = repo_root .. "toolchain/lfs/"
|
||||
package.cpath = lpeg_dir .. "?.dll;"
|
||||
.. lfs_dir .. "?.dll;"
|
||||
.. package.cpath
|
||||
end
|
||||
|
||||
|
||||
@@ -329,7 +329,8 @@ function M.run(ctx)
|
||||
|
||||
-- Per-DIRECTORY (per-module) aggregation. Group sources by `src.dir`,
|
||||
-- validate every source in the dir, then emit ONE errors.h per dir.
|
||||
local by_dir = duffle.group_sources_by_dir(ctx.sources)
|
||||
-- `ctx.by_dir` is pre-computed in build_ctx (shared across all passes).
|
||||
local by_dir = ctx.by_dir or duffle.group_sources_by_dir(ctx.sources)
|
||||
|
||||
for dir, dir_sources in pairs(by_dir) do
|
||||
local dir_basename = dir:match("([^/\\]+)$") or dir
|
||||
|
||||
@@ -350,6 +350,8 @@ end
|
||||
|
||||
-- Project pre-scanned MipsAtomComp_ / MipsAtomComp_Proc_ entries into Component shape.
|
||||
-- Does per-source backward lookups for args (preceding function decl) and comment (preceding comment block).
|
||||
-- Carries `body_tokens` forward from scan-source so word_count_rec reads from the precomputed table
|
||||
-- instead of calling duffle.tokenize_body again.
|
||||
-- @param source string -- the full source text (needed for backward lookups)
|
||||
-- @param scan table -- SourceScan from duffle.scan_source
|
||||
-- @return Component[]
|
||||
@@ -360,11 +362,12 @@ local function project_components(source, scan)
|
||||
local args = find_function_args_for(source, a.raw_name, a.ident_pos)
|
||||
local comment = preceding_comment_block(source, a.ident_pos)
|
||||
out[#out + 1] = {
|
||||
line = a.line,
|
||||
name = a.name,
|
||||
body = a.body,
|
||||
args = args,
|
||||
comment = comment,
|
||||
line = a.line,
|
||||
name = a.name,
|
||||
body = a.body,
|
||||
body_tokens = a.body_tokens,
|
||||
args = args,
|
||||
comment = comment,
|
||||
}
|
||||
end
|
||||
end
|
||||
@@ -445,8 +448,9 @@ local function word_count_rec(name, comp_by_name, wc, cache)
|
||||
local n
|
||||
if cc then
|
||||
n = 0
|
||||
for _, t in ipairs(duffle.split_top_level_commas(cc.body)) do
|
||||
local trimmed = duffle.trim(t)
|
||||
local tokens = cc.body_tokens
|
||||
for _, t in ipairs(tokens) do
|
||||
local trimmed = t.tok
|
||||
if trimmed ~= "" then
|
||||
local lookup = strip_mac_prefix(duffle.read_ident(trimmed, 1))
|
||||
if lookup and comp_by_name[lookup] then
|
||||
@@ -518,12 +522,7 @@ end
|
||||
--- @param body string
|
||||
--- @return string[]
|
||||
local function tokens_from_body(body)
|
||||
local out = {}
|
||||
for _, t in ipairs(duffle.split_top_level_commas(body)) do
|
||||
local trimmed = duffle.trim(t)
|
||||
if trimmed ~= "" then out[#out + 1] = trimmed end
|
||||
end
|
||||
return out
|
||||
return duffle.tokenize_body_simple(body)
|
||||
end
|
||||
|
||||
--- Determine the macro signature: function-args list (function form) or variadic-ignored (bare form).
|
||||
|
||||
+21
-48
@@ -164,44 +164,6 @@ local function scan_for_atom_markers(token, at_pos, labels, branches)
|
||||
end
|
||||
end
|
||||
|
||||
--- Find the end position (just past the closing ')') of the first atom_label/atom_offset call in `tok`. Returns 0 if no such call.
|
||||
--- @param tok string
|
||||
--- @return integer -- 0 if no marker call found; otherwise end-1 (just past ')')
|
||||
local function find_marker_call_end(tok)
|
||||
local pos = 1
|
||||
local tok_len = #tok
|
||||
while pos <= tok_len do
|
||||
pos = duffle.skip_ws_and_cmt(tok, pos)
|
||||
if pos > tok_len then break end
|
||||
local ch = tok:sub(pos, pos)
|
||||
if duffle.is_space(ch) then
|
||||
pos = pos + 1
|
||||
elseif ch == "/" then
|
||||
-- comment — skip past it (delegated to duffle.skip_str_or_cmt)
|
||||
local nx = duffle.skip_str_or_cmt(tok, pos)
|
||||
pos = (nx > pos) and nx or (pos + 1)
|
||||
else
|
||||
local ident, after_ident = duffle.read_ident(tok, pos)
|
||||
-- scan: <ident>
|
||||
if ident == LABEL_MARKER or ident == OFFSET_MARKER then
|
||||
-- scan: atom_label(<name>) OR atom_offset(<tag>, <target>)
|
||||
local open_paren = duffle.skip_ws_and_cmt(tok, after_ident)
|
||||
if tok:sub(open_paren, open_paren) == "(" then
|
||||
local _, end_paren = duffle.read_parens(tok, open_paren)
|
||||
return end_paren - 1
|
||||
end
|
||||
return 0
|
||||
end
|
||||
pos = after_ident or (pos + 1)
|
||||
end
|
||||
end
|
||||
return 0
|
||||
end
|
||||
|
||||
-- ════════════════════════════════════════════════════════════════════════════
|
||||
-- Per-atom body scan (for atom_label / atom_offset markers)
|
||||
-- ════════════════════════════════════════════════════════════════════════════
|
||||
|
||||
-- (internal) Count words emitted by the rest of `tok` after a marker call
|
||||
-- (the marker call itself emits 0 words, but the source pattern may bundle the marker with the next instruction on the same line,
|
||||
-- separated by no top-level comma).
|
||||
@@ -210,9 +172,12 @@ end
|
||||
-- @param word_counts table
|
||||
-- @return integer
|
||||
local function count_marker_rest(tok, word_counts)
|
||||
local marker_end = find_marker_call_end(tok)
|
||||
if marker_end <= 0 or marker_end >= #tok then return 0 end
|
||||
local rest = duffle.trim(tok:sub(marker_end + 1))
|
||||
-- duffle.find_marker_call_end returns the position PAST the closing `)` of the marker call
|
||||
-- (or nil if `tok` isn't a marker call). Canonical impl in duffle.lua is faster than the
|
||||
-- file-local copy that used to live here (byte-indexed, no `tok:sub` per char).
|
||||
local marker_end = duffle.find_marker_call_end(tok)
|
||||
if not marker_end or marker_end >= #tok then return 0 end
|
||||
local rest = duffle.trim(tok:sub(marker_end))
|
||||
if rest == "" then return 0 end
|
||||
return count_token_words(rest, word_counts)
|
||||
end
|
||||
@@ -230,11 +195,17 @@ end
|
||||
--- @param body string
|
||||
--- @param word_counts table
|
||||
--- @return table<string, integer>, table[], integer
|
||||
local function scan_atom_body(body, word_counts)
|
||||
-- scan_atom_body: walk pre-tokenized body for atom_label/atom_offset markers + word counts.
|
||||
-- Uses `atom.body_tokens` from the SourceScan payload (pre-tokenized by scan-source pass).
|
||||
-- @param body_tokens table[] -- {{tok=string, rel=integer}, ...} from duffle.tokenize_body
|
||||
-- @param word_counts table
|
||||
-- @return table, table, integer -- labels, branches, total_words
|
||||
local function scan_atom_body(body_tokens, word_counts)
|
||||
local pos = 0
|
||||
local labels = {}
|
||||
local branches = {}
|
||||
for _, tok in ipairs(duffle.split_top_level_commas(body)) do
|
||||
for _, t in ipairs(body_tokens) do
|
||||
local tok = t.tok
|
||||
if is_marker_token(tok) then
|
||||
-- Marker call: record at the current pos, do NOT advance pos.
|
||||
scan_for_atom_markers(tok, pos, labels, branches)
|
||||
@@ -342,17 +313,19 @@ end
|
||||
|
||||
local M = {}
|
||||
|
||||
-- Project the pre-scanned SourceScan entries into the {name, body} shape this pass needs.
|
||||
-- Project the pre-scanned SourceScan entries into the {name, body, body_tokens} shape this pass needs.
|
||||
-- MipsAtom_ entries have kind="atom"; MipsCode code_<name> entries have kind="raw_atom".
|
||||
-- `body_tokens` is set by scan-source on every `scan.atoms[i]` / `scan.raw_atoms[i]`; we carry it forward
|
||||
-- so `scan_atom_body` reads from the precomputed table directly (no per-atom tokenize_body fallback).
|
||||
-- @param scan table -- SourceScan from duffle.scan_source
|
||||
-- @return table[] -- list of {name=, body=}
|
||||
-- @return table[] -- list of {name=, body=, body_tokens=}
|
||||
local function project_atoms(scan)
|
||||
local out = {}
|
||||
for _, a in ipairs(scan.atoms) do
|
||||
out[#out + 1] = { name = a.raw_name, body = a.body }
|
||||
out[#out + 1] = { name = a.raw_name, body = a.body, body_tokens = a.body_tokens }
|
||||
end
|
||||
for _, a in ipairs(scan.raw_atoms) do
|
||||
out[#out + 1] = { name = a.name, body = a.body }
|
||||
out[#out + 1] = { name = a.name, body = a.body, body_tokens = a.body_tokens }
|
||||
end
|
||||
return out
|
||||
end
|
||||
@@ -368,7 +341,7 @@ local function process_source(ctx, src)
|
||||
|
||||
local atoms_data = {}
|
||||
for _, atom in ipairs(atoms) do
|
||||
local labels, branches, total = scan_atom_body(atom.body, ctx.shared.word_counts)
|
||||
local labels, branches, total = scan_atom_body(atom.body_tokens, ctx.shared.word_counts)
|
||||
atoms_data[#atoms_data + 1] = {
|
||||
name = atom.name,
|
||||
total_words = total,
|
||||
|
||||
@@ -404,7 +404,7 @@ function M.run(ctx)
|
||||
local warnings = {}
|
||||
|
||||
local module_entries = (ctx.flags and ctx.flags._annot_results) or {}
|
||||
local by_dir = duffle.group_sources_by_dir(ctx.sources)
|
||||
local by_dir = ctx.by_dir or duffle.group_sources_by_dir(ctx.sources)
|
||||
|
||||
if not ctx.dry_run then duffle.ensure_dir(ctx.out_root) end
|
||||
|
||||
|
||||
@@ -467,6 +467,16 @@ local M = {}
|
||||
function M.run(ctx)
|
||||
for _, src in ipairs(ctx.sources) do
|
||||
src.scan = scan_source(src.text)
|
||||
-- Pre-tokenize each atom body once (plex: single source of truth).
|
||||
-- Downstream passes (offsets, word-counts, components, static-analysis) read from
|
||||
-- `atom.body_tokens` instead of calling `split_top_level_commas` / `tokenize_body` independently.
|
||||
-- The tokens are memoized in duffle.lua's cache, so re-access is O(1).
|
||||
for _, atom in ipairs(src.scan.atoms) do
|
||||
atom.body_tokens = duffle.tokenize_body(atom.body)
|
||||
end
|
||||
for _, atom in ipairs(src.scan.raw_atoms or {}) do
|
||||
atom.body_tokens = duffle.tokenize_body(atom.body)
|
||||
end
|
||||
end
|
||||
return { outputs = {}, errors = {}, warnings = {} }
|
||||
end
|
||||
|
||||
+195
-260
@@ -129,180 +129,142 @@ local OUTPUT_EXTENSION = ".static_analysis.txt"
|
||||
--- Used by the checks to convert per-token offsets in the body to lina numbers relative to the start of `body`.
|
||||
--- The atom's source-line of the body-start is added by the caller.
|
||||
---
|
||||
-- Memoization cache for build_body_line_index / tokenize_body.
|
||||
-- Atom bodies are immutable for the duration of a validate() pass (they come from src.scan which is set once by scan-source),
|
||||
-- so the same body string is safe to use as a cache key. Each call site (5 check_* functions + analyze_atom_paths)
|
||||
-- was re-running the full O(body_len) scan on the SAME body. After memoization, the first call pays the O(body_len) cost,
|
||||
-- every subsequent call on the same body returns the cached table in O(1).
|
||||
--
|
||||
-- (Future-proofing: if a caller ever mutates the returned tokens / line-index tables, the cache aliasing means those
|
||||
-- mutations would leak across atoms. Current callers all read-only; safe today.)
|
||||
local _body_line_index_cache = {}
|
||||
local _tokenize_body_cache = {}
|
||||
-- NOTE: `tokenize_body` and `build_body_line_index` moved to `duffle.lua` as shared
|
||||
-- memoized utilities (`duffle.tokenize_body`, `duffle.build_body_line_index`).
|
||||
-- The local copies were deleted; all callers now use the duffle versions.
|
||||
|
||||
--- Simple line-counting: count `\n` chars from offset 1 up to the offset; that count + 1 is the line number (1-based).
|
||||
local function build_body_line_index(body)
|
||||
if _body_line_index_cache[body] ~= nil then return _body_line_index_cache[body] end
|
||||
local index = {}
|
||||
local len = #body
|
||||
local newline_count = 0
|
||||
for pos = 1, len do
|
||||
if pos > 1 then
|
||||
index[pos] = newline_count + 1 -- line of `pos` relative to body
|
||||
-- ════════════════════════════════════════════════════════════════════════════
|
||||
-- classify_tokens — per-token classification (the plex's pre-computed data layer)
|
||||
-- ════════════════════════════════════════════════════════════════════════════
|
||||
|
||||
-- ONE forward pass over the token list produces a flat table of per-token classifications.
|
||||
-- Every check + analyze_atom_paths reads from this table instead of re-scanning the token strings.
|
||||
--
|
||||
-- The classification is stored on `atom.paths.tok_class` as an array indexed by token index (1..#tokens).
|
||||
-- Each entry has:
|
||||
-- ident — the leading identifier (e.g. "load_word", "gte_cmdw_rtpt", "nop", "mac_yield")
|
||||
-- nop_words — 0 / 1 / 2 (for "nop" / "nop2" / anything else)
|
||||
-- nop_prefix — consecutive nop words ending just BEFORE this token (forward-pass pre-compute;
|
||||
-- replaces the backward walk in count_preceding_nops — O(N) instead of O(N²))
|
||||
-- is_yield — true if this token is `mac_yield` or `mac_yield(...)`
|
||||
-- is_atom_label — true if this token is `atom_label(name)`; label_name has the name
|
||||
-- is_branch — true if this token is `branch_*(...)`; branch_label has the label or false
|
||||
-- is_load_word — true if this token starts with `load_word(`
|
||||
-- is_store_word — true if this token starts with `store_word(`
|
||||
--
|
||||
-- Checks that need the leading ident use `tok_class.ident` instead of re-matching the token string.
|
||||
-- Checks that need "how many nops before token i" use `tok_class.nop_prefix` instead of walking backwards.
|
||||
|
||||
--- @class TokClass
|
||||
--- @field ident string -- leading identifier
|
||||
--- @field nop_words integer -- 0/1/2
|
||||
--- @field nop_prefix integer -- consecutive nop words before this token
|
||||
--- @field is_yield boolean
|
||||
--- @field is_atom_label boolean
|
||||
--- @field label_name string|nil -- for atom_label(name)
|
||||
--- @field is_branch boolean
|
||||
--- @field branch_label string|false|nil -- for branch_*(..., atom_offset(F, label))
|
||||
--- @field is_load_word boolean
|
||||
--- @field is_store_word boolean
|
||||
|
||||
local function classify_tokens(tokens)
|
||||
local n = #tokens
|
||||
local tc = {}
|
||||
local nop_run = 0 -- running count of consecutive nop words (forward pass)
|
||||
for tok_idx, t in ipairs(tokens) do
|
||||
local tok = t.tok
|
||||
local ident = tok:match("^([%w_]+)") or "?"
|
||||
local nop_words = 0
|
||||
if ident == "nop" then nop_words = 1
|
||||
elseif ident == "nop2" then nop_words = 2 end
|
||||
|
||||
local is_yield = ident == "mac_yield"
|
||||
local is_atom_label = false
|
||||
local label_name = nil
|
||||
local is_branch = false
|
||||
local branch_label = nil
|
||||
local is_load_word = ident == "load_word"
|
||||
local is_store_word = ident == "store_word"
|
||||
|
||||
if ident == "atom_label" then
|
||||
is_atom_label = true
|
||||
label_name = tok:match("^atom_label%s*%(%s*([%w_]+)%s*%)")
|
||||
elseif tok:match("^branch_[%w_]+%s*%(") then
|
||||
is_branch = true
|
||||
branch_label = tok:match("atom_offset%s*%([^,]+,%s*([%w_]+)%s*%)") or false
|
||||
end
|
||||
if body:byte(pos) == 10 then -- '\n'
|
||||
newline_count = newline_count + 1
|
||||
|
||||
tc[tok_idx] = {
|
||||
ident = ident,
|
||||
nop_words = nop_words,
|
||||
nop_prefix = nop_run,
|
||||
is_yield = is_yield,
|
||||
is_atom_label = is_atom_label,
|
||||
label_name = label_name,
|
||||
is_branch = is_branch,
|
||||
branch_label = branch_label,
|
||||
is_load_word = is_load_word,
|
||||
is_store_word = is_store_word,
|
||||
}
|
||||
-- Advance the nop run for the NEXT token.
|
||||
if nop_words > 0 then
|
||||
nop_run = nop_run + nop_words
|
||||
else
|
||||
nop_run = 0
|
||||
end
|
||||
end
|
||||
-- Offsets beyond the body still resolve to the final line
|
||||
index[len + 1] = newline_count + 1
|
||||
_body_line_index_cache[body] = index
|
||||
return index
|
||||
end
|
||||
|
||||
--- Count of COP2-nop words contributed by a single top-level token.
|
||||
-- `nop` -> 1
|
||||
-- `nop2` -> 2 (i.e. `nop, nop` baked into one asm word)
|
||||
-- `nop,` / `nop2,` -> same as above; strip trailing comma defensively
|
||||
-- anything else -> 0
|
||||
--
|
||||
-- (Branch-delay-slot nops like `branch_*(..., nop)` are tokenized separately by split_top_level_commas:
|
||||
-- The branch arg ends before the trailing comma, and `nop` becomes its own token.
|
||||
-- So no special handling is needed here.)
|
||||
local function nop_word_count(token)
|
||||
local s = duffle.trim(token)
|
||||
-- Strip trailing comma(s) (defensive against raw text via, but our tokenize_body already strips them; this is a safety net)
|
||||
s = s:gsub(",$", "")
|
||||
s = duffle.trim(s)
|
||||
if s == "nop" then return 1 end
|
||||
if s == "nop2" then return 2 end
|
||||
return 0
|
||||
end
|
||||
|
||||
--- Tokenize the body inner-text into a flat list of `(token, body_rel_offset)`
|
||||
--- pairs (nested parens/braces/brackets are honored; comments and strings are skipped).
|
||||
--- `body_rel_offset` is the char offset within `body` of the start of the token.
|
||||
--- Callers add it to the atom's `body_off` to get an absolute source position for line tracking.
|
||||
local function tokenize_body(body)
|
||||
if _tokenize_body_cache[body] ~= nil then return _tokenize_body_cache[body] end
|
||||
local out = {}
|
||||
local len = #body
|
||||
local rel = 1
|
||||
while rel <= len do
|
||||
-- Find next non-whitespace, non-comment start
|
||||
local ws_end = duffle.skip_ws_and_cmt(body, rel)
|
||||
if ws_end > rel then
|
||||
rel = ws_end
|
||||
end
|
||||
if rel > len then break end
|
||||
|
||||
-- Find comma/newline/semicolon after this token.
|
||||
-- Read balanced groups so commas inside parens/braces/brackets aren't treated as separators.
|
||||
-- Comments / strings are skipped.
|
||||
local scan = rel
|
||||
while scan <= len do
|
||||
local c = body:byte(scan)
|
||||
if c == 44 then break end -- ','
|
||||
if c == 10 then break end -- '\n'
|
||||
if c == 59 then break end -- ';'
|
||||
if c == 40 then local _, a = duffle.read_parens (body, scan); scan = a -- '('
|
||||
elseif c == 123 then local _, a = duffle.read_braces (body, scan); scan = a -- '{'
|
||||
elseif c == 91 then local _, a = duffle.read_brackets (body, scan); scan = a -- '['
|
||||
elseif c == 34 or c == 39 then scan = duffle.skip_str_or_cmt(body, scan) + 1 -- '"' or '\''
|
||||
else
|
||||
scan = scan + 1
|
||||
end
|
||||
end
|
||||
-- Extract token [rel .. scan-1]
|
||||
local tok = duffle.trim(body:sub(rel, scan - 1))
|
||||
if tok ~= "" then
|
||||
out[#out + 1] = { tok = tok, rel = rel }
|
||||
end
|
||||
-- Move past the separator
|
||||
if scan <= len then
|
||||
scan = scan + 1
|
||||
-- Also skip whitespace before next token
|
||||
local w = duffle.skip_ws_and_cmt(body, scan)
|
||||
if w > scan then scan = w end
|
||||
end
|
||||
rel = scan
|
||||
end
|
||||
_tokenize_body_cache[body] = out
|
||||
return out
|
||||
return tc
|
||||
end
|
||||
|
||||
-- ════════════════════════════════════════════════════════════════════════════
|
||||
-- Check #1: GTE pipeline-fill
|
||||
-- ════════════════════════════════════════════════════════════════════════════
|
||||
|
||||
-- Count consecutive nop words immediately BEFORE token index `ti` in the token list.
|
||||
-- Walks backwards from ti-1, accumulating nop_word_count, stopping at the first non-nop.
|
||||
-- scan: nop, nop, <non-nop> -> have = count of nop words before ti
|
||||
local function count_preceding_nops(tokens, ti)
|
||||
local have = 0
|
||||
local where_ti = ti - 1
|
||||
while where_ti >= 1 do
|
||||
local n = nop_word_count(tokens[where_ti].tok)
|
||||
if n == 0 then break end
|
||||
have = have + n
|
||||
where_ti = where_ti - 1
|
||||
end
|
||||
return have
|
||||
end
|
||||
-- Check a single gte_cmdw_* token for pipeline-fill compliance.
|
||||
-- Uses the pre-computed `tok_class` entry (nop_prefix replaces the backward walk; ident replaces the per-token match).
|
||||
local function check_one_gte_cmdw(atom, tc_entry, ti, line_in_body, findings)
|
||||
local ident = tc_entry.ident
|
||||
if not ident:match("^gte_cmdw_") then return end
|
||||
|
||||
-- Check a single gte_cmdw_* token for pipeline-fill compliance.
|
||||
-- Emits a finding if the preceding nops are insufficient (error) or the macro isn't in the latency table (warning).
|
||||
-- scan: <nop>... <gte_cmdw_X> -> validate nop count vs GTE_PIPELINE_LATENCY[X]
|
||||
local function check_one_gte_cmdw(a, tok, tokens, ti, line_in_body, findings)
|
||||
local cmdw_full = tok:match("^(gte_cmdw_[%w_]+)%s*[,%)]") or tok:match("^(gte_cmdw_[%w_]+)%s*$")
|
||||
if not cmdw_full then return end
|
||||
|
||||
local variant = cmdw_full:match("^gte_cmdw_(.+)$")
|
||||
local need = duffle.GTE_PIPELINE_LATENCY[cmdw_full]
|
||||
local line = a.line + line_in_body[tokens[ti].rel]
|
||||
local variant = ident:match("^gte_cmdw_(.+)$")
|
||||
local need = duffle.GTE_PIPELINE_LATENCY[ident]
|
||||
local line = atom.line + line_in_body[atom.paths.tokens[ti].rel]
|
||||
|
||||
if need == nil then
|
||||
-- alias or new gte_cmdw_<X> not yet in latency table
|
||||
findings[#findings + 1] = {
|
||||
atom = a.name,
|
||||
atom = atom.name,
|
||||
line = line,
|
||||
check = "gte_pipeline_fill",
|
||||
kind = "warning",
|
||||
msg = string.format(
|
||||
"%s at line %d uses `gte_cmdw_%s` but that macro is not in duffle.GTE_PIPELINE_LATENCY -- add a min_nops entry",
|
||||
a.name, line, variant),
|
||||
atom.name, line, variant),
|
||||
}
|
||||
elseif need > 0 then
|
||||
local have = count_preceding_nops(tokens, ti)
|
||||
local have = tc_entry.nop_prefix
|
||||
if have < need then
|
||||
findings[#findings + 1] = {
|
||||
atom = a.name,
|
||||
atom = atom.name,
|
||||
line = line,
|
||||
check = "gte_pipeline_fill",
|
||||
kind = "error",
|
||||
msg = string.format(
|
||||
"%s at line %d needs %d nop word%s immediately BEFORE `gte_cmdw_%s`; only %d found",
|
||||
a.name, line, need, need == 1 and "" or "s", variant, have),
|
||||
atom.name, line, need, need == 1 and "" or "s", variant, have),
|
||||
}
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
--- Walk the token list. Whenever we hit a `gte_cmdw_<X>` token, count consecutive nop words immediately preceding it.
|
||||
--- If count < the minimum declared in `duffle.GTE_PIPELINE_LATENCY[X]`, record a finding.
|
||||
--- Aliases are resolved against the lookup table directly; if a macro name is not in the table, emit a soft warning
|
||||
--- (the user might have added a new gte_cmdw_* but not updated duffle.lua).
|
||||
--- Per-atom: walk this atom's tokens, check every `gte_cmdw_*` for pipeline-fill compliance.
|
||||
--- Signature changed from `(atoms, findings)` to `(atom, findings)` in Stage 1B of the plex move:
|
||||
--- the per-atom iteration now lives in validate()'s single loop; each check is a per-atom predicate.
|
||||
local function check_gte_pipeline_fill(atom, findings)
|
||||
local tokens = tokenize_body(atom.body)
|
||||
local line_in_body = build_body_line_index(atom.body)
|
||||
local tn = #tokens
|
||||
local ti = 1
|
||||
while ti <= tn do
|
||||
check_one_gte_cmdw(atom, tokens[ti].tok, tokens, ti, line_in_body, findings)
|
||||
ti = ti + 1
|
||||
--- Per-atom: check every `gte_cmdw_*` for pipeline-fill compliance.
|
||||
--- Uses the pre-computed `atom.paths.tok_class` — nop_prefix (forward-pass pre-compute)
|
||||
--- replaces the old backward walk; ident replaces the per-token `tok:match` classification.
|
||||
local function check_gte_pipeline_fill(atom, pipe_ctx, findings)
|
||||
local tc = atom.paths.tok_class
|
||||
local line_in_body = atom.paths.line_in_body
|
||||
local tn = #atom.paths.tokens
|
||||
for ti = 1, tn do
|
||||
check_one_gte_cmdw(atom, tc[ti], ti, line_in_body, findings)
|
||||
end
|
||||
end
|
||||
|
||||
@@ -315,7 +277,8 @@ end
|
||||
---
|
||||
--- Empty bodies are not currently flagged — runtime infrastructure atoms like `MipsAtom_(yield) { mac_yield() }`
|
||||
--- and `MipsAtom_(tape_exit) { jump_reg(rret_addr), nop }` are valid as-is; mac_yield at the end is the contract.
|
||||
local function check_mac_yield_uniformity(atom, findings)
|
||||
--- Stage 2: signature uniformized to `(atom, pipe_ctx, findings)` — pipe_ctx is ignored here.
|
||||
local function check_mac_yield_uniformity(atom, pipe_ctx, findings)
|
||||
-- Per-kind semantics:
|
||||
-- MipsAtom_ (baked atom): exactly 1 mac_yield at the end of the body. Control transfer is the atom's job.
|
||||
-- MipsAtomComp_ (bare static-array component): ZERO mac_yield.
|
||||
@@ -324,16 +287,15 @@ local function check_mac_yield_uniformity(atom, findings)
|
||||
-- Same reasoning -- it's a function returning a MipsAtom slice, invoked from a parent atom.
|
||||
--
|
||||
-- The GTE pipeline-fill check applies to all 3 kinds (see check_gte_pipeline_fill). Only the mac_yield rule branches on kind.
|
||||
local tokens = tokenize_body(atom.body)
|
||||
local line_in_body = build_body_line_index(atom.body)
|
||||
local tokens = atom.paths.tokens
|
||||
local line_in_body = atom.paths.line_in_body
|
||||
local tc = atom.paths.tok_class
|
||||
local n = #tokens
|
||||
|
||||
local count = 0
|
||||
local last_idx = 0
|
||||
for tok_idx, t in ipairs(tokens) do
|
||||
local tok = t.tok
|
||||
-- Match `mac_yield(...)` or just `mac_yield`. The bareword
|
||||
-- variant is rare in modern style but tolerated.
|
||||
if tok:match("^mac_yield%s*%(") or tok == "mac_yield" then
|
||||
for tok_idx = 1, n do
|
||||
if tc[tok_idx].is_yield then
|
||||
count = count + 1
|
||||
last_idx = tok_idx
|
||||
end
|
||||
@@ -364,14 +326,13 @@ local function check_mac_yield_uniformity(atom, findings)
|
||||
"%s at line %d has %d `mac_yield()` calls; exactly 1 is allowed",
|
||||
atom.name, line_for(last_idx), count),
|
||||
}
|
||||
elseif last_idx < #tokens then
|
||||
elseif last_idx < n then
|
||||
-- 1 call, but not the last token. We DON'T fail if the post-token is just `nop` or `nop2` or a branch with `, nop` delay slot.
|
||||
-- It's the standard "yield, then BD nop" idiom.
|
||||
local post_non_nop = false
|
||||
for search_idx = last_idx + 1, #tokens do
|
||||
local t = tokens[search_idx].tok
|
||||
if t ~= "" and t ~= "nop" and t ~= "nop2"
|
||||
and not t:match("%,%s*nop%)%s*$") then
|
||||
for search_idx = last_idx + 1, n do
|
||||
if tc[search_idx].nop_words == 0
|
||||
and tokens[search_idx].tok ~= "" then
|
||||
post_non_nop = true
|
||||
break
|
||||
end
|
||||
@@ -406,6 +367,7 @@ local function check_mac_yield_uniformity(atom, findings)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
-- ════════════════════════════════════════════════════════════════════════════
|
||||
-- Check #3: ABI handoff discipline
|
||||
-- ════════════════════════════════════════════════════════════════════════════
|
||||
@@ -439,14 +401,15 @@ local function check_abi_handoff(atom, pipe_ctx, findings)
|
||||
}
|
||||
return
|
||||
end
|
||||
local tokens = tokenize_body(atom.body)
|
||||
local line_in_body = build_body_line_index(atom.body)
|
||||
local tokens = atom.paths.tokens
|
||||
local line_in_body = atom.paths.line_in_body
|
||||
local tc = atom.paths.tok_class
|
||||
local found_field_set = {}
|
||||
local found_advance = false
|
||||
local bind_re = "O_%(" .. binds_name .. ",%s*([%w_]+)%s*%)"
|
||||
for _, t in ipairs(tokens) do
|
||||
for tok_idx, t in ipairs(tokens) do
|
||||
local tok = t.tok
|
||||
if tok:match("^load_word%s*%(") then
|
||||
if tc[tok_idx].is_load_word then
|
||||
if tok:find("R_TapePtr", 1, true) and tok:find("O_(" .. binds_name .. ",", 1, true) then
|
||||
local field = tok:match(bind_re)
|
||||
-- scan: load_word(R_*, R_TapePtr, O_(<Binds_X>, <field>))
|
||||
@@ -505,22 +468,23 @@ end
|
||||
--- - Atoms containing a `mac_<name>(...)` call whose name is not in duffle.GP0_MACRO_CONTRIB emit a "new macro; update duffle.GP0_MACRO_CONTRIB" advisory.
|
||||
---
|
||||
--- Applies only to `kind = "atom"` (baked atoms). Components don't emit full primitives.
|
||||
--- Per-atom: detect which GP0 primitive the atom is emitting, sum the macro contributions,
|
||||
--- compare to the expected packet size. Signature changed in Stage 1B: `(atom, findings)`.
|
||||
local function check_gpu_portstore_shape(atom, findings)
|
||||
--- Stage 2: signature uniformized to `(atom, pipe_ctx, findings)` — pipe_ctx is ignored here.
|
||||
local function check_gpu_portstore_shape(atom, pipe_ctx, findings)
|
||||
if atom.kind ~= "atom" then return end
|
||||
local tokens = tokenize_body(atom.body)
|
||||
local line_in_body = build_body_line_index(atom.body)
|
||||
local tokens = atom.paths.tokens
|
||||
local line_in_body = atom.paths.line_in_body
|
||||
local tc = atom.paths.tok_class
|
||||
local cmd_byte = nil
|
||||
local cmd_line = nil
|
||||
local contrib = 0
|
||||
local saw_format = false
|
||||
local saw_prim_write = false
|
||||
for _, t in ipairs(tokens) do
|
||||
for tok_idx, t in ipairs(tokens) do
|
||||
local tok = t.tok
|
||||
local ident = tc[tok_idx].ident
|
||||
-- Match `mac_format_<shape>_color(...)` and strip `_color`
|
||||
-- to get the bare shape suffix (f3 / g4 / etc).
|
||||
local shape = tok:match("^mac_format_([%w_]+)_color%s*%(") or tok:match("^mac_format_([%w_]+)_color%s*$")
|
||||
local shape = ident:match("^mac_format_([%w_]+)_color$")
|
||||
if shape and duffle.GP0_CMD_BY_SHAPE[shape] then
|
||||
if not cmd_byte then
|
||||
cmd_byte = duffle.GP0_CMD_BY_SHAPE[shape]
|
||||
@@ -531,17 +495,15 @@ local function check_gpu_portstore_shape(atom, findings)
|
||||
local n = duffle.GP0_MACRO_CONTRIB[contrib_key]
|
||||
if n then contrib = contrib + n end
|
||||
end
|
||||
local gte_store = tok:match("^mac_gte_store_[%w_]+")
|
||||
if gte_store then
|
||||
local n = duffle.GP0_MACRO_CONTRIB[gte_store]
|
||||
if ident:match("^mac_gte_store_[%w_]+$") then
|
||||
local n = duffle.GP0_MACRO_CONTRIB[ident]
|
||||
if n then contrib = contrib + n end
|
||||
end
|
||||
local ot_tag = tok:match("^mac_insert_ot_tag_([%w_]+)")
|
||||
if ot_tag then
|
||||
local n = duffle.GP0_MACRO_CONTRIB["mac_insert_ot_tag_" .. ot_tag]
|
||||
if ident:match("^mac_insert_ot_tag_[%w_]+$") then
|
||||
local n = duffle.GP0_MACRO_CONTRIB[ident]
|
||||
if n then contrib = contrib + n end
|
||||
end
|
||||
if tok:match("^store_word%s*%(") and tok:find("R_PrimCursor", 1, true) then
|
||||
if tc[tok_idx].is_store_word and tok:find("R_PrimCursor", 1, true) then
|
||||
saw_prim_write = true
|
||||
end
|
||||
end
|
||||
@@ -571,57 +533,12 @@ local function check_gpu_portstore_shape(atom, findings)
|
||||
end
|
||||
|
||||
-- ════════════════════════════════════════════════════════════════════════════
|
||||
-- Check #5: per-atom cycle budget
|
||||
-- Check #5: per-atom cycle budget (uses analyze_atom_paths's unknown_macros)
|
||||
-- ════════════════════════════════════════════════════════════════════════════
|
||||
|
||||
--- Compute the cycle cost of one token. The token is a string like
|
||||
--- `add_ui(R_T0, R_T1, 4)` or `nop2` or `gte_cmdw_rtpt`. Returns:
|
||||
--- cycles - integer cycle cost (from duffle.INSTRUCTION_LATENCY, or duffle.UNKNOWN_INSTRUCTION_CYCLES if not in the table)
|
||||
--- macro_name - the bare ident (e.g. `add_ui`, `gte_cmdw_rtpt`, `nop2`, `mac_yield`)
|
||||
--- unknown - true iff the macro wasn't in duffle.INSTRUCTION_LATENCY.
|
||||
--- The function strips trailing `()` from function-call style macros so `mac_yield()` and `mac_yield` resolve identically.
|
||||
local function token_cycles(tok)
|
||||
-- Extract the leading ident. Tolerate `(...)` args.
|
||||
local ident = tok:match("^([%w_]+)")
|
||||
if not ident then return duffle.UNKNOWN_INSTRUCTION_CYCLES, "?", true end
|
||||
local cost = duffle.INSTRUCTION_LATENCY[ident]
|
||||
if cost == nil then
|
||||
return duffle.UNKNOWN_INSTRUCTION_CYCLES, ident, true
|
||||
end
|
||||
return cost, ident, false
|
||||
end
|
||||
|
||||
--- Find every `atom_label(name)` token in the token list and return a map `label_name -> token_idx`. Labels are 0-cost markers;
|
||||
--- the path walker uses them as branch targets.
|
||||
local function find_atom_labels(tokens)
|
||||
local labels = {}
|
||||
for tok_idx, t in ipairs(tokens) do
|
||||
local name = t.tok:match("^atom_label%s*%(%s*([%w_]+)%s*%)")
|
||||
if name then labels[name] = tok_idx end
|
||||
end
|
||||
return labels
|
||||
end
|
||||
|
||||
--- Find every `branch_*(...)` token in the token list and return a map `token_idx -> label_name|false`.
|
||||
--- If the branch's args contain an `atom_offset(F, label)` call, the label name is recorded; otherwise the branch's target is unknown
|
||||
--- (likely a literal offset) and we record `false` as a sentinel.
|
||||
--- The CFG walker checks KEY PRESENCE (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.
|
||||
--- (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(tok_idx)` return false for both "not a branch" and "branch with unknown target".)
|
||||
local function find_branch_targets(tokens)
|
||||
local targets = {}
|
||||
for tok_idx, t in ipairs(tokens) do
|
||||
if t.tok:match("^branch_[%w_]+%s*%(") then
|
||||
-- branch_<cond>(rs, atom_offset(F, label)) or
|
||||
-- branch_<cond>(rs, rt, atom_offset(F, label))
|
||||
-- atom_offset's arg list is (flag, name); we want the name.
|
||||
local label = t.tok:match("atom_offset%s*%([^,]+,%s*([%w_]+)%s*%)")
|
||||
targets[tok_idx] = label or false -- `false` = known branch, unknown target
|
||||
end
|
||||
end
|
||||
return targets
|
||||
end
|
||||
-- NOTE: `token_cycles`, `find_atom_labels`, `find_branch_targets` were removed
|
||||
-- when `classify_tokens` (the pre-computed per-token classification) replaced them.
|
||||
-- The classification lives on `atom.paths.tok_class`; analyze_atom_paths reads it.
|
||||
|
||||
--- Walk all paths through an atom body and return per-path cycle sums.
|
||||
--- Builds a tiny CFG: each token has a "next" pointer; branches have two (fall-through + taken).
|
||||
@@ -638,43 +555,44 @@ end
|
||||
--- (warning; loop bodies aren't supported)
|
||||
--- unknown_macros - list of unique macro names not in duffle.INSTRUCTION_LATENCY
|
||||
local function analyze_atom_paths(atom)
|
||||
local tokens = tokenize_body(atom.body)
|
||||
local labels = find_atom_labels(tokens)
|
||||
local branches = find_branch_targets(tokens)
|
||||
local tokens = atom.paths.tokens or duffle.tokenize_body(atom.body)
|
||||
local tc = atom.paths.tok_class or classify_tokens(tokens)
|
||||
local n = #tokens
|
||||
|
||||
-- Pre-compute per-token cycle costs and identify terminators.
|
||||
local n = #tokens
|
||||
local costs = {}
|
||||
local unknown_set = {}
|
||||
for tok_idx, t in ipairs(tokens) do
|
||||
local c, _, unknown = token_cycles(t.tok)
|
||||
costs[tok_idx] = c
|
||||
if unknown then
|
||||
unknown_set[t.tok:match("^([%w_]+)") or "?"] = true
|
||||
-- Build label + branch maps from the pre-computed classification (no re-scan).
|
||||
local labels = {}
|
||||
local branches = {}
|
||||
for tok_idx = 1, n do
|
||||
local c = tc[tok_idx]
|
||||
if c.is_atom_label and c.label_name then
|
||||
labels[c.label_name] = tok_idx
|
||||
end
|
||||
if c.is_branch then
|
||||
branches[tok_idx] = c.branch_label
|
||||
end
|
||||
end
|
||||
|
||||
-- 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 atom's prologue absorbs it).
|
||||
local function is_terminator(tok_idx)
|
||||
local tok = tokens[tok_idx].tok
|
||||
return tok == "mac_yield" or tok:match("^mac_yield%s*%(")
|
||||
-- Pre-compute per-token cycle costs from the pre-computed ident (no re-match).
|
||||
local costs = {}
|
||||
local unknown_set = {}
|
||||
for tok_idx = 1, n do
|
||||
local c = tc[tok_idx]
|
||||
local cost = duffle.INSTRUCTION_LATENCY[c.ident]
|
||||
if cost == nil then
|
||||
cost = duffle.UNKNOWN_INSTRUCTION_CYCLES
|
||||
unknown_set[c.ident] = true
|
||||
end
|
||||
costs[tok_idx] = cost
|
||||
end
|
||||
|
||||
-- CFG successor function. Returns a list of next token indices for the given position.
|
||||
-- Branch tokens produce 2 successors (fall-through + taken); normal tokens produce 1 (next); terminators produce 0.
|
||||
-- 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 own cost instead (so it's counted once).
|
||||
--
|
||||
-- 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 nil target means "literal offset, taken path is unknown").
|
||||
-- We check key-presence via `branches[tok_idx] ~= nil` because `branches[tok_idx]` returns nil for both "absent" AND "present with nil value".
|
||||
-- Distinguishing them requires the key check.
|
||||
-- A token is a terminator if it's `mac_yield`.
|
||||
local function is_terminator(tok_idx)
|
||||
return tc[tok_idx].is_yield
|
||||
end
|
||||
|
||||
-- A token is a "branch" if the classification says so.
|
||||
local function is_branch(tok_idx)
|
||||
local v = branches[tok_idx]
|
||||
if v == nil then return false end
|
||||
-- v is non-nil: either a string (atom_offset target) or false (literal offset, no target). Both indicate a branch.
|
||||
return true
|
||||
return tc[tok_idx].is_branch
|
||||
end
|
||||
local function successors(tok_idx)
|
||||
local tok = tokens[tok_idx].tok
|
||||
@@ -807,6 +725,23 @@ local function check_per_atom_cycle_budget(atom, pipe_ctx, findings)
|
||||
end
|
||||
end
|
||||
|
||||
-- ════════════════════════════════════════════════════════════════════════════
|
||||
-- CHECK_RULES — data-driven check dispatch (Muratori: data over control flow)
|
||||
-- ════════════════════════════════════════════════════════════════════════════
|
||||
|
||||
-- Each rule is a table entry: { name, per_atom }.
|
||||
-- `per_atom(atom, pipe_ctx, findings)` runs once per atom inside validate()'s single loop.
|
||||
-- Adding a new check = 1 row here + 1 check_* function. No validate() edit required.
|
||||
-- This is the plex pattern: the iteration is in ONE place (validate), the variation is in DATA (this table).
|
||||
|
||||
local CHECK_RULES = {
|
||||
{ name = "gte_pipeline_fill", per_atom = check_gte_pipeline_fill },
|
||||
{ name = "mac_yield_uniformity", per_atom = check_mac_yield_uniformity },
|
||||
{ name = "abi_handoff", per_atom = check_abi_handoff },
|
||||
{ name = "gpu_portstore_shape", per_atom = check_gpu_portstore_shape },
|
||||
{ name = "per_atom_cycle_budget", per_atom = check_per_atom_cycle_budget },
|
||||
}
|
||||
|
||||
-- ════════════════════════════════════════════════════════════════════════════
|
||||
-- Per-source validation
|
||||
-- ════════════════════════════════════════════════════════════════════════════
|
||||
@@ -849,18 +784,18 @@ local function validate(ctx, src)
|
||||
local findings = {}
|
||||
for _, a in ipairs(atoms) do
|
||||
a.paths = a.paths or {}
|
||||
a.paths.tokens = tokenize_body(a.body)
|
||||
a.paths.line_in_body = build_body_line_index(a.body)
|
||||
a.paths.tokens = a.body_tokens
|
||||
a.paths.line_in_body = duffle.build_body_line_index(a.body)
|
||||
a.paths.tok_class = classify_tokens(a.paths.tokens)
|
||||
|
||||
-- analyze_atom_paths fills the *cycles / branches / has_loops / unknown_macros* fields of a.paths.
|
||||
analyze_atom_paths(a)
|
||||
|
||||
-- Run all 5 checks on this one atom. Each is now a per-atom predicate.
|
||||
check_gte_pipeline_fill(a, findings)
|
||||
check_mac_yield_uniformity(a, findings)
|
||||
check_abi_handoff(a, pipe_ctx, findings)
|
||||
check_gpu_portstore_shape(a, findings)
|
||||
check_per_atom_cycle_budget(a, pipe_ctx, findings)
|
||||
-- Run all checks on this one atom via the CHECK_RULES data table (Muratori: data over control flow).
|
||||
-- Adding a new check = 1 row in CHECK_RULES; this loop never needs editing.
|
||||
for _, rule in ipairs(CHECK_RULES) do
|
||||
rule.per_atom(a, pipe_ctx, findings)
|
||||
end
|
||||
end
|
||||
|
||||
local errors = {}
|
||||
@@ -1115,7 +1050,7 @@ function M.run(ctx)
|
||||
--
|
||||
-- Group sources by `src.dir`. The first component of `dir` is the module name (e.g. "code/duffle" -> "duffle", "code/gte_hello" -> "gte_hello").
|
||||
-- Output path is `<out_root>/<module_basename>.static_analysis.txt`.
|
||||
local by_dir = duffle.group_sources_by_dir(ctx.sources)
|
||||
local by_dir = ctx.by_dir or duffle.group_sources_by_dir(ctx.sources)
|
||||
|
||||
for dir, dir_sources in pairs(by_dir) do
|
||||
-- Run validate() against every source in this directory; accumulate atoms / findings / errors / warnings.
|
||||
|
||||
@@ -4,7 +4,6 @@
|
||||
--- 1. **Public utilities** (used by `passes/components.lua`, `passes/offsets.lua`, `passes/annotation.lua`):
|
||||
--- - `M.count_token_words(token, wc)` — words emitted by one token
|
||||
--- - `M.scan_dir(dir, suffix)` — glob walk for *.macs.h
|
||||
--- - `M.count_body_words(body, wc)` — words emitted by an atom body
|
||||
--- 2. **Pass entry** `M.run(ctx)` — loads metadata.h + *.macs.h into `ctx.shared.word_counts` for downstream passes.
|
||||
--- 3. **Internal helpers** for the body scanner.
|
||||
---
|
||||
@@ -29,13 +28,17 @@ local duffle = dofile(_bootstrap_dir .. "../duffle_paths.lua")
|
||||
-- Constants
|
||||
-- ════════════════════════════════════════════════════════════════════════════
|
||||
|
||||
-- Windows separator chars — used to convert `dir /b /s` output (which uses `\`) into POSIX paths (which our scripts expect).
|
||||
-- Windows separator chars — used to convert `dir` output (which uses `\`) into POSIX paths (which our scripts expect).
|
||||
local PATH_SEP_BACKSLASH = "\\"
|
||||
local PATH_SEP_FORWARD = "/"
|
||||
|
||||
-- Glob command for Windows directory walk. `dir /b /s` lists all matching files recursively with bare paths (no headers);
|
||||
-- `2>nul` discards the "file not found" stderr when nothing matches.
|
||||
local DIR_GLOB_CMD = 'dir /b /s "%s\\%s" 2>nul'
|
||||
-- Fallback glob command (subprocess). Used when `lfs` (LuaFileSystem) is not available.
|
||||
-- Scoped to `code\` to avoid walking `.git/`, `toolchain/`, `build/`, etc.
|
||||
local DIR_GLOB_CMD = 'dir /b /s "%s\\code\\%s" 2>nul'
|
||||
|
||||
-- Try to load lfs (LuaFileSystem). If available, scan_dir uses native directory enumeration (~2ms)
|
||||
-- instead of spawning `dir /b /s` as a subprocess (~56ms). Built by update_deps.ps1 into toolchain/lfs/lfs.dll.
|
||||
local lfs = pcall(require, "lfs") and require("lfs") or nil
|
||||
|
||||
-- ════════════════════════════════════════════════════════════════════════════
|
||||
-- Type declarations
|
||||
@@ -120,36 +123,50 @@ end
|
||||
-- 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.
|
||||
--- No regex per the no_regex constraint — uses plain byte matching via `dir /b /s` on Windows.
|
||||
--- Scan `code/` for files matching `suffix` (e.g. `*.macs.h`).
|
||||
--- Uses `lfs` (LuaFileSystem) when available — native directory enumeration at ~2ms.
|
||||
--- Falls back to `dir /b /s` subprocess (~56ms) when `lfs` is not compiled.
|
||||
---
|
||||
--- @param dir string -- directory to scan (absolute or relative)
|
||||
--- @param dir string -- project root directory
|
||||
--- @param suffix string -- file pattern, e.g. "*.macs.h"
|
||||
--- @return string[]
|
||||
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 pipe = io.popen(DIR_GLOB_CMD:format(dir, suffix))
|
||||
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
|
||||
local path = raw_line:gsub(PATH_SEP_BACKSLASH, PATH_SEP_FORWARD)
|
||||
results[#results + 1] = path
|
||||
end
|
||||
pipe:close()
|
||||
|
||||
-- Cache the result.
|
||||
if lfs then
|
||||
-- Native walk: list code/<module>/gen/ for matching files. Zero subprocess spawns.
|
||||
local code_dir = dir .. "/code"
|
||||
if lfs.attributes(code_dir, "mode") == "directory" then
|
||||
for mod_name in lfs.dir(code_dir) do
|
||||
if mod_name ~= "." and mod_name ~= ".." then
|
||||
local gen_path = code_dir .. "/" .. mod_name .. "/gen"
|
||||
if lfs.attributes(gen_path, "mode") == "directory" then
|
||||
for fname in lfs.dir(gen_path) do
|
||||
if fname:match("%.macs%.h$") then
|
||||
results[#results + 1] = gen_path .. "/" .. fname
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
else
|
||||
-- Fallback: single `dir /b /s` subprocess scoped to code\.
|
||||
local pipe = io.popen(DIR_GLOB_CMD:format(dir, suffix))
|
||||
if pipe then
|
||||
for raw_line in pipe:lines() do
|
||||
results[#results + 1] = raw_line:gsub(PATH_SEP_BACKSLASH, PATH_SEP_FORWARD)
|
||||
end
|
||||
pipe:close()
|
||||
end
|
||||
end
|
||||
|
||||
-- Cache the result (including empty results).
|
||||
cache = cache or {}
|
||||
cache[key] = results
|
||||
package.loaded[SCAN_CACHE_KEY] = cache
|
||||
@@ -160,88 +177,6 @@ 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 │
|
||||
-- └────────────────────────────────────────────────────────────────────┘
|
||||
|
||||
--- Count words emitted by an entire atom body (a brace-delimited block).
|
||||
--- Splits by top-level commas; for each token, delegates to count_token_words.
|
||||
--- Handles `atom_label(name)` / `atom_offset(tag, name)` markers
|
||||
--- (record at current pos, do NOT advance pos; if the marker call bundles an instruction after it, count that instruction too).
|
||||
---
|
||||
--- @param body string -- brace-delimited atom body (without braces)
|
||||
--- @param wc WordCounts -- the shared word-count table
|
||||
--- @return integer -- total words
|
||||
function M.count_body_words(body, wc)
|
||||
local total = 0
|
||||
for _, tok in ipairs(duffle.split_top_level_commas(body)) do
|
||||
local pos = 1
|
||||
local tok_len = #tok
|
||||
while pos <= tok_len and duffle.is_space(tok:sub(pos, pos)) do
|
||||
pos = pos + 1
|
||||
end
|
||||
local leading_ident = duffle.read_ident(tok, pos)
|
||||
local is_marker = leading_ident == "atom_label" or leading_ident == "atom_offset"
|
||||
if is_marker then
|
||||
-- Marker call: record at current pos, do NOT advance pos.
|
||||
-- But the source pattern may bundle the marker with the next instruction on a new line (no top-level comma between them).
|
||||
-- In that case, the rest of `tok` after the marker call is a real instruction that must still be counted.
|
||||
local marker_end = M.find_marker_call_end(tok)
|
||||
if marker_end > 0 and marker_end < #tok then
|
||||
local rest = duffle.trim(tok:sub(marker_end + 1))
|
||||
if rest ~= "" then
|
||||
total = total + M.count_token_words(rest, wc)
|
||||
end
|
||||
end
|
||||
else
|
||||
total = total + M.count_token_words(tok, wc)
|
||||
end
|
||||
end
|
||||
return total
|
||||
end
|
||||
|
||||
--- Find the end position (just past the closing ')') of the first atom_label/atom_offset call in `tok`. Returns 0 if no such call.
|
||||
--- Internal helper for count_body_words.
|
||||
---
|
||||
--- @param tok string
|
||||
--- @return integer -- 0 if no marker call found
|
||||
function M.find_marker_call_end(tok)
|
||||
local pos = 1
|
||||
local tok_len = #tok
|
||||
while pos <= tok_len do
|
||||
pos = duffle.skip_ws_and_cmt(tok, pos)
|
||||
if pos > tok_len then break end
|
||||
local ch = tok:sub(pos, pos)
|
||||
if duffle.is_space(ch) then
|
||||
pos = pos + 1
|
||||
elseif ch == "/" then
|
||||
-- comment — skip past it (delegated to duffle.skip_str_or_cmt)
|
||||
local nx = duffle.skip_str_or_cmt(tok, pos)
|
||||
pos = (nx > pos) and nx or (pos + 1)
|
||||
else
|
||||
local ident, after_ident = duffle.read_ident(tok, pos)
|
||||
local marker_end = find_marker_end(tok, ident, after_ident)
|
||||
if marker_end > 0 then return marker_end end
|
||||
pos = after_ident or (pos + 1)
|
||||
end
|
||||
end
|
||||
return 0
|
||||
end
|
||||
|
||||
-- (internal) If `ident` is `atom_label`/`atom_offset` followed by `(...)`, return the position just past the closing ')'.
|
||||
-- Otherwise 0.
|
||||
-- @param tok string
|
||||
-- @param ident string|nil
|
||||
-- @param after_ident integer
|
||||
-- @return integer
|
||||
local function find_marker_end(tok, ident, after_ident)
|
||||
if ident ~= "atom_label" and ident ~= "atom_offset" then return 0 end
|
||||
local open_paren = duffle.skip_ws_and_cmt(tok, after_ident)
|
||||
if tok:sub(open_paren, open_paren) ~= "(" then return 0 end
|
||||
local _, end_paren = duffle.read_parens(tok, open_paren)
|
||||
return end_paren - 1
|
||||
end
|
||||
|
||||
-- ┌────────────────────────────────────────────────────────────────────┐
|
||||
-- │ Pass entry: M.run(ctx) — "word-counts" pass │
|
||||
-- └────────────────────────────────────────────────────────────────────┘
|
||||
|
||||
@@ -355,8 +355,14 @@ local function build_ctx(args)
|
||||
}
|
||||
end
|
||||
|
||||
-- Pre-compute the per-directory grouping once (Fleury: expose structure).
|
||||
-- Three passes (annotation, report, static-analysis) call group_sources_by_dir with the same ctx.sources;
|
||||
-- computing it here and stashing on ctx.by_dir eliminates 2 redundant calls.
|
||||
local by_dir = duffle.group_sources_by_dir(sources)
|
||||
|
||||
return {
|
||||
sources = sources,
|
||||
by_dir = by_dir,
|
||||
metadata_path = args.metadata,
|
||||
shared = {},
|
||||
upstream = {},
|
||||
|
||||
@@ -101,6 +101,21 @@ push-location $path_lpeg
|
||||
& gcc @lpeg_compile_args
|
||||
pop-location
|
||||
|
||||
# ════════════════════════════════════════════════════════════════════════════
|
||||
# lfs (LuaFileSystem) — compiled from pcsx-redux's vendored luafilesystem source.
|
||||
# Used by word_count_eval.lua :: scan_dir for native directory enumeration (~2ms)
|
||||
# instead of spawning `dir /b /s` as a subprocess (~56ms).
|
||||
# Source: toolchain/pcsx-redux/third_party/luafilesystem/src/lfs.c
|
||||
# Output: toolchain/lfs/lfs.dll
|
||||
# ════════════════════════════════════════════════════════════════════════════
|
||||
|
||||
$path_lfs = join-path $path_toolchain 'lfs'
|
||||
verify-path $path_lfs
|
||||
$lfs_src = join-path $path_pcsx_redux 'third_party\luafilesystem\src\lfs.c'
|
||||
$lfs_dll = join-path $path_lfs 'lfs.dll'
|
||||
$lfs_dll_import = join-path $luajit_lib_dir 'libluajit-5.1.dll.a'
|
||||
& gcc -O2 -shared "-I$lua_inc_dir" -o $lfs_dll $lfs_src $lfs_dll_import
|
||||
|
||||
# ════════════════════════════════════════════════════════════════════════════
|
||||
# OpenBIOS — built from the PCSX-Redux source tree via make + mipsel-none-elf
|
||||
#
|
||||
|
||||
Reference in New Issue
Block a user