mirror of
https://github.com/Ed94/pikuma_ps1.git
synced 2026-08-04 14:48:48 +00:00
dealing with this mess still.
This commit is contained in:
@@ -1,25 +1,14 @@
|
||||
--- passes/annotation.lua — Atom-annotation DSL validator.
|
||||
---
|
||||
--- Validates `MipsAtom_(name) atom_info(atom_bind(Binds_X),
|
||||
--- atom_reads(...), atom_writes(...)) { ... }` declarations in source files.
|
||||
--- Also reads:
|
||||
--- - `Binds_*` struct declarations (`typedef Struct_(Binds_X) { ... };`)
|
||||
--- - `TAPE_WORDS(mac_X, N)` pragma directives (`#pragma` + `_Pragma`)
|
||||
--- Validates `MipsAtom_(name) atom_info(atom_bind(Binds_X), atom_reads(...), atom_writes(...)) { ... }` declarations in source files.
|
||||
--- Also reads: `Binds_*` struct declarations (`typedef Struct_(Binds_X) { ... };`)
|
||||
---
|
||||
--- Writes:
|
||||
--- - `<ctx.out_root>/<dir_basename>.errors.h` — one per module, with
|
||||
--- `#error` directives on findings (the C compile will surface the
|
||||
--- error)
|
||||
--- - The annotations.txt report is rendered by `passes/report.lua`
|
||||
--- from the per-module results stashed in `ctx.flags._annot_results`
|
||||
---
|
||||
--- **Ported from** `scripts/tape_atom_annotation_pass.lua:78-545 +
|
||||
--- 1081-1407` (validation only — NOT rendering, which goes to
|
||||
--- `passes/report.lua`).
|
||||
--- - `<ctx.out_root>/<dir_basename>.errors.h` — one per module, with `#error` directives on findings (the C compile will surface the error)
|
||||
--- - The annotations.txt report is rendered by `passes/report.lua` from the per-module results stashed in `ctx.flags._annot_results`
|
||||
---
|
||||
--- **Conventions**: tabs (1/level), EmmyLua annotations, no regex,
|
||||
--- Lua 5.3 compatible. See
|
||||
--- `C:\projects\Pikuma\ps1-ai\conductor\code_styleguides\lua.md`.
|
||||
--- Lua 5.3 compatible
|
||||
|
||||
-- ════════════════════════════════════════════════════════════════════════════
|
||||
-- Module-scope requires + package.path setup
|
||||
@@ -170,7 +159,7 @@ local BYTE_COMMA = 44
|
||||
--- @param s string
|
||||
--- @return string[]
|
||||
local function split_csv_top(s)
|
||||
local tokens = {}
|
||||
local tokens = {}
|
||||
local pos = 1
|
||||
local chunk_a = 1
|
||||
local depth = 0
|
||||
@@ -179,13 +168,13 @@ local function split_csv_top(s)
|
||||
local ch = s:byte(pos)
|
||||
if ch == BYTE_OPEN_PAREN or ch == BYTE_OPEN_BRACE or ch == BYTE_OPEN_BRACK then
|
||||
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
|
||||
depth = depth - 1
|
||||
pos = pos + 1
|
||||
pos = pos + 1
|
||||
elseif ch == BYTE_COMMA and depth == 0 then
|
||||
tokens[#tokens + 1] = s:sub(chunk_a, pos - 1)
|
||||
pos = pos + 1
|
||||
pos = pos + 1
|
||||
chunk_a = pos
|
||||
else
|
||||
pos = pos + 1
|
||||
@@ -259,9 +248,7 @@ end
|
||||
-- Resolve any phase_* / R_* alias macros in a register list.
|
||||
-- (Phase / region / cadence aliases have been dropped. Kept as an
|
||||
-- identity function so callers can stay uniform.)
|
||||
local function resolve_reg_aliases(regs)
|
||||
return regs
|
||||
end
|
||||
local function resolve_reg_aliases(regs) return regs end
|
||||
|
||||
-- Parse a comma-separated inner content (e.g. inside atom_reads(...))
|
||||
-- into a list of trimmed identifiers with aliases resolved.
|
||||
@@ -269,7 +256,7 @@ local function parse_regs_list(inner)
|
||||
local out = {}
|
||||
for _, r in ipairs(split_csv_top(inner)) do
|
||||
local trimmed = trim(r)
|
||||
if trimmed ~= "" then out[#out + 1] = trimmed end
|
||||
if trimmed ~= "" then out[#out + 1] = trimmed end
|
||||
end
|
||||
return resolve_reg_aliases(out)
|
||||
end
|
||||
@@ -387,7 +374,7 @@ local function parse_pragma_directive(source, ident_pos, after_ident)
|
||||
end
|
||||
if entry then
|
||||
local line_of = duffle.LineIndex(source)
|
||||
entry.line = line_of(ident_pos)
|
||||
entry.line = line_of(ident_pos)
|
||||
end
|
||||
return entry, eol
|
||||
end
|
||||
@@ -417,7 +404,7 @@ local function find_macro_word_annotations(source)
|
||||
local entry, new_pos = parse_pragma_operator(source, pos, after_ident)
|
||||
if entry then
|
||||
local line_of = duffle.LineIndex(source)
|
||||
entry.line = line_of(pos)
|
||||
entry.line = line_of(pos)
|
||||
out[#out + 1] = entry
|
||||
end
|
||||
pos = new_pos
|
||||
@@ -479,7 +466,7 @@ end
|
||||
--- @param line_of fun(pos: integer): integer
|
||||
--- @return BindsStruct|nil, integer
|
||||
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)
|
||||
if type_ident ~= STRUCT_TYPE then
|
||||
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
|
||||
|
||||
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)
|
||||
if not brace then return nil, open_paren + 1 end
|
||||
|
||||
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).
|
||||
if name:sub(1, BINDS_PREFIX_LEN) ~= BINDS_PREFIX then
|
||||
@@ -554,10 +541,8 @@ end
|
||||
--- @param pos integer
|
||||
--- @return string|nil, integer
|
||||
local function read_alnum_ident(s, pos)
|
||||
local str_len = #s
|
||||
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 str_len = #s; 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
|
||||
if pos == start then return nil, pos end
|
||||
return s:sub(start, pos - 1), pos
|
||||
end
|
||||
@@ -586,14 +571,14 @@ local function find_atom_names(source)
|
||||
pos = open_paren + 1
|
||||
else
|
||||
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
|
||||
out[#out + 1] = { line = line_of(pos), name = name }
|
||||
end
|
||||
local brace = scan_to_char(source, "{", after_paren)
|
||||
if brace then
|
||||
local _, after_brace = read_braces(source, brace)
|
||||
pos = after_brace
|
||||
pos = after_brace
|
||||
else
|
||||
pos = open_paren + 1
|
||||
end
|
||||
@@ -609,39 +594,24 @@ end
|
||||
-- ════════════════════════════════════════════════════════════════════════════
|
||||
|
||||
--- True iff the parsed arg is a register-list call (any recognized form).
|
||||
local function is_regs_arg(a)
|
||||
return a and (a.kind == "atom_reads" or a.kind == "atom_writes" or a.kind == "regs")
|
||||
end
|
||||
local function is_regs_arg(a) 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-atom_info sub-call dispatch. Each takes (entry, args) and mutates
|
||||
--- entry.{reads, writes, binds, errors}. The new annotation shape is:
|
||||
---
|
||||
--- 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.
|
||||
--- Per-macro arg-shape handlers. Each takes (entry, args) and mutates Per-atom_info sub-call dispatch.
|
||||
--- Each takes (entry, args) and mutates entry.{reads, writes, binds, errors}.
|
||||
--- All sub-calls are order-independent; each is dispatched on its `kind` (atom_bind / atom_reads / atom_writes) when parsed.
|
||||
local ANNOT_ARG_HANDLERS = {}
|
||||
|
||||
-- atom_info(atom_bind(Binds_X), atom_reads(...), atom_writes(...))
|
||||
function ANNOT_ARG_HANDLERS.info(entry, args)
|
||||
for _, arg in ipairs(args) do
|
||||
if arg.kind == "atom_bind" then
|
||||
entry.binds = arg.value
|
||||
elseif arg.kind == "atom_reads" then
|
||||
entry.reads = arg.value
|
||||
elseif arg.kind == "atom_writes" then
|
||||
entry.writes = arg.value
|
||||
elseif arg.kind == "ident" then
|
||||
if arg.kind == "atom_bind" then entry.binds = arg.value
|
||||
elseif arg.kind == "atom_reads" then entry.reads = arg.value
|
||||
elseif arg.kind == "atom_writes" then entry.writes = arg.value
|
||||
elseif arg.kind == "ident" then
|
||||
-- Reserved for future phase tokens. Currently ignored.
|
||||
-- (Could be reintroduced as `phase_*` sub-calls of atom_info.)
|
||||
else
|
||||
entry.errors[#entry.errors + 1] = string.format(
|
||||
"unexpected atom_info arg kind=%s value=%s", arg.kind, tostring(arg.value))
|
||||
entry.errors[#entry.errors + 1] = string.format("unexpected atom_info arg kind=%s value=%s", arg.kind, tostring(arg.value))
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -660,27 +630,25 @@ local function new_annot_entry(line, ident, name, kind)
|
||||
}
|
||||
end
|
||||
|
||||
--- Try to parse an `atom_info(...)` call right after the `MipsAtom_(name)`
|
||||
--- parens. Returns the annotation entry (if present) and the new source
|
||||
--- position past the atom_info call. Returns nil if no atom_info follows.
|
||||
--- Try to parse an `atom_info(...)` call right after the `MipsAtom_(name)` parens.
|
||||
--- Returns the annotation entry (if present) and the new source position past the atom_info call.
|
||||
--- Returns nil if no atom_info follows.
|
||||
--- @param source string
|
||||
--- @param atom_name string
|
||||
--- @param after_mipsatom_paren integer -- position past the MipsAtom_(...) close paren
|
||||
--- @param line_of fun(pos: integer): integer
|
||||
--- @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 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)
|
||||
if look_ident ~= ATOM_INFO then return nil, after_mipsatom_paren end
|
||||
|
||||
local info_open = skip_ws_and_cmt(source, look_after)
|
||||
if source:byte(info_open) ~= BYTE_OPEN_PAREN then
|
||||
return nil, info_open + 1
|
||||
end
|
||||
if source:byte(info_open) ~= BYTE_OPEN_PAREN then return nil, info_open + 1 end
|
||||
|
||||
local info_inner, info_after = read_parens(source, info_open)
|
||||
local args = parse_atom_annot_args(info_inner)
|
||||
local entry = new_annot_entry(line_of(lookahead), ATOM_INFO, atom_name, "info")
|
||||
local args = parse_atom_annot_args(info_inner)
|
||||
local entry = new_annot_entry(line_of(lookahead), ATOM_INFO, atom_name, "info")
|
||||
ANNOT_ARG_HANDLERS.info(entry, args)
|
||||
return entry, info_after
|
||||
end
|
||||
@@ -741,10 +709,10 @@ end
|
||||
local function validate(ctx, src)
|
||||
local source = src.text
|
||||
|
||||
local annots = find_atom_annotations(source)
|
||||
local macros = find_macro_word_annotations(source)
|
||||
local binds = find_binds_structs(source)
|
||||
local atoms = find_atom_names(source)
|
||||
local annots = find_atom_annotations(source)
|
||||
local macros = find_macro_word_annotations(source)
|
||||
local binds = find_binds_structs(source)
|
||||
local atoms = find_atom_names(source)
|
||||
|
||||
local atom_index = {}
|
||||
for _, a in ipairs(atoms) do atom_index[a.name] = a end
|
||||
|
||||
@@ -1,19 +1,12 @@
|
||||
--- passes/components.lua — Component-macro header generator.
|
||||
---
|
||||
--- Walks every source for `MipsAtomComp_(ac_X) { body }` (and the
|
||||
--- function-form `MipsAtomComp_Proc_(ac_X, { body })`) declarations
|
||||
--- and emits a per-directory `<dir_basename>.macs.h` containing one
|
||||
--- `#define mac_X(sig) \` macro per component + `WORD_COUNT(mac_X, N)`
|
||||
--- Walks every source for `MipsAtomComp_(ac_X) { body }`
|
||||
--- (and the function-form `MipsAtomComp_Proc_(ac_X, { body })`) declarations
|
||||
--- and emits a per-directory `<dir_basename>.macs.h` containing one `#define mac_X(sig) \` macro per component + `WORD_COUNT(mac_X, N)`
|
||||
--- 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,
|
||||
--- Lua 5.3 compatible. See
|
||||
--- `C:\projects\Pikuma\ps1-ai\conductor\code_styleguides\lua.md`.
|
||||
--- Lua 5.3 compatible.
|
||||
|
||||
--- @class Component
|
||||
--- @field name string
|
||||
@@ -28,13 +21,9 @@
|
||||
-- Module-scope requires + package.path setup
|
||||
-- ════════════════════════════════════════════════════════════════════════════
|
||||
|
||||
-- Resolve `arg[0]` to an absolute-ish script directory so that
|
||||
-- `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.
|
||||
-- Resolve `arg[0]` to an absolute-ish script directory so that `require("duffle")` resolves against `scripts/` regardless of CWD.
|
||||
dofile((arg[0]:match("(.*[/\\])") or "./") .. "duffle_paths.lua")
|
||||
local duffle = require("duffle")
|
||||
local duffle = require("duffle")
|
||||
local word_count_eval = require("word_count_eval")
|
||||
|
||||
-- ════════════════════════════════════════════════════════════════════════════
|
||||
@@ -97,23 +86,19 @@ local GEN_SUBDIR = "gen"
|
||||
-- Local helpers (file I/O + path normalization)
|
||||
-- ════════════════════════════════════════════════════════════════════════════
|
||||
|
||||
-- Write content to disk in binary mode so LF line endings are preserved on
|
||||
-- Windows (text mode would convert LF -> CRLF, breaking byte-identical diffs
|
||||
-- against git-tracked gen/*.macs.h files which are stored as LF).
|
||||
-- Write content to disk in binary mode so LF line endings are preserved on Windows
|
||||
-- (text mode would convert LF -> CRLF, breaking byte-identical diffs against git-tracked gen/*.macs.h files which are stored as LF).
|
||||
-- @param path string
|
||||
-- @param content string
|
||||
local function write_file_lf(path, content)
|
||||
local f = io.open(path, "wb")
|
||||
if not f then error("Cannot write " .. path) end
|
||||
f:write(content)
|
||||
f:close()
|
||||
f:write(content); f:close()
|
||||
end
|
||||
|
||||
-- Convert a (possibly relative) path to an absolute Windows path. The
|
||||
-- pre-rework output's "// Source:" comment line used the absolute path
|
||||
-- (e.g. "C:\projects\Pikuma\ps1\code\duffle\lottes_tape.h"); if we want
|
||||
-- byte-identical output, we must normalize relative -> absolute before
|
||||
-- emitting that comment.
|
||||
-- Convert a (possibly relative) path to an absolute Windows path.
|
||||
-- The pre-rework output's "// Source:" comment line used the absolute path (e.g. "C:\projects\Pikuma\ps1\code\duffle\lottes_tape.h");
|
||||
-- If we want byte-identical output, we must normalize relative -> absolute before emitting that comment.
|
||||
-- @param path string
|
||||
-- @return string
|
||||
local function to_absolute_path(path)
|
||||
@@ -135,10 +120,6 @@ end
|
||||
|
||||
local M = {}
|
||||
|
||||
-- ════════════════════════════════════════════════════════════════════════════
|
||||
-- Ported helpers (verbatim from tape_atom_annotation_pass.lua:604-1079)
|
||||
-- ════════════════════════════════════════════════════════════════════════════
|
||||
|
||||
-- ════════════════════════════════════════════════════════════════════════════
|
||||
-- Function-args extraction (precedes MipsAtomComp_Proc_ invocations)
|
||||
-- ════════════════════════════════════════════════════════════════════════════
|
||||
|
||||
+201
-201
@@ -162,23 +162,23 @@ local OUTPUT_EXTENSION = ".static_analysis.txt"
|
||||
local function find_atom_bodies(source_text)
|
||||
local line_of = duffle.LineIndex(source_text)
|
||||
local out = {}
|
||||
local len = #source_text
|
||||
local i = 1
|
||||
while i <= len do
|
||||
i = duffle.skip_ws_and_cmt(source_text, i); if i > len then break end
|
||||
local src_len = #source_text
|
||||
local pos = 1
|
||||
while pos <= src_len do
|
||||
pos = duffle.skip_ws_and_cmt(source_text, pos); if pos > src_len then break end
|
||||
-- Skip preprocessor directives (#define / #include / #pragma /
|
||||
-- etc). Otherwise the `#define MipsAtom_(sym) ...` definition
|
||||
-- in lottes_tape.h gets matched as an atom named "sym" and
|
||||
-- its `body` swallows the next real atom declaration via
|
||||
-- duffle.scan_to_char("{", ...).
|
||||
if source_text:sub(i, i) == "#" then
|
||||
local j = i
|
||||
while j <= len and source_text:byte(j) ~= 10 do j = j + 1 end
|
||||
i = j + 1
|
||||
if source_text:sub(pos, pos) == "#" then
|
||||
local eol_pos = pos
|
||||
while eol_pos <= src_len and source_text:byte(eol_pos) ~= 10 do eol_pos = eol_pos + 1 end
|
||||
pos = eol_pos + 1
|
||||
else
|
||||
local ident, after = duffle.read_ident(source_text, i)
|
||||
local ident, ident_end = duffle.read_ident(source_text, pos)
|
||||
if not ident then
|
||||
i = i + 1
|
||||
pos = pos + 1
|
||||
elseif ident == "MipsAtom_"
|
||||
or ident == "MipsAtomComp_"
|
||||
or ident == "MipsAtomComp_Proc_" then
|
||||
@@ -190,69 +190,69 @@ local function find_atom_bodies(source_text)
|
||||
else kind = "comp_proc"
|
||||
end
|
||||
|
||||
local open = duffle.skip_ws_and_cmt(source_text, after)
|
||||
if source_text:sub(open, open) ~= "(" then
|
||||
i = open + 1
|
||||
local open_paren = duffle.skip_ws_and_cmt(source_text, ident_end)
|
||||
if source_text:sub(open_paren, open_paren) ~= "(" then
|
||||
pos = open_paren + 1
|
||||
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
|
||||
-- MipsAtomComp_Proc_(sym, { body })
|
||||
-- The body is inside the LAST `{ ... }` in the args
|
||||
-- (the macro takes 2 args: sym name, then body in {}).
|
||||
-- Find the last `{` in `inner`, then the matching `}`.
|
||||
local last_open
|
||||
for k = #inner, 1, -1 do
|
||||
if inner:sub(k, k) == "{" then last_open = k; break end
|
||||
local last_brace_pos
|
||||
for search_pos = #inner, 1, -1 do
|
||||
if inner:sub(search_pos, search_pos) == "{" then last_brace_pos = search_pos; break end
|
||||
end
|
||||
if not last_open then
|
||||
i = open + 1
|
||||
if not last_brace_pos then
|
||||
pos = open_paren + 1
|
||||
else
|
||||
-- Walk forward to find matching `}` honoring balanced
|
||||
-- ()/[] 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.
|
||||
local depth = 1
|
||||
local j = last_open + 1
|
||||
while j <= #inner and depth > 0 do
|
||||
local c = inner:byte(j)
|
||||
local inner_pos = last_brace_pos + 1
|
||||
while inner_pos <= #inner and depth > 0 do
|
||||
local c = inner:byte(inner_pos)
|
||||
if c == 123 then
|
||||
depth = depth + 1; j = j + 1
|
||||
depth = depth + 1; inner_pos = inner_pos + 1
|
||||
elseif c == 125 then
|
||||
depth = depth - 1
|
||||
if depth == 0 then break end
|
||||
j = j + 1
|
||||
inner_pos = inner_pos + 1
|
||||
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
|
||||
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
|
||||
j = duffle.duffle.skip_str_or_cmt(inner, j) + 1
|
||||
inner_pos = duffle.duffle.skip_str_or_cmt(inner, inner_pos) + 1
|
||||
else
|
||||
j = j + 1
|
||||
inner_pos = inner_pos + 1
|
||||
end
|
||||
end
|
||||
if depth ~= 0 then
|
||||
-- unmatched; bail
|
||||
i = open + 1
|
||||
pos = open_paren + 1
|
||||
else
|
||||
-- First ident in `inner` is the comp name.
|
||||
local name_match = inner:match("^%s*([%w_]+)")
|
||||
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
|
||||
-- LAST `{` in `inner`, which sits at `open+1+last_open`
|
||||
-- (open+1 = just inside the outer paren, +last_open
|
||||
-- = at the `{`).
|
||||
local body_off = open + 1 + last_open
|
||||
-- LAST `{` in `inner`, which sits at `open_paren+1+last_brace_pos`
|
||||
-- (open_paren+1 = just inside the outer paren,
|
||||
-- +last_brace_pos = at the `{`).
|
||||
local body_off = open_paren + 1 + last_brace_pos
|
||||
out[#out + 1] = {
|
||||
line = line_of(i),
|
||||
line = line_of(pos),
|
||||
name = name,
|
||||
body = body,
|
||||
body_off = body_off + 1,
|
||||
kind = kind,
|
||||
}
|
||||
i = after_paren
|
||||
pos = after_paren
|
||||
end
|
||||
end
|
||||
else
|
||||
@@ -260,34 +260,34 @@ local function find_atom_bodies(source_text)
|
||||
-- MipsAtomComp_(sym) { body };
|
||||
-- name is the first arg, body is the FIRST { ... } after
|
||||
-- the paren.
|
||||
local a = 1
|
||||
while a <= #inner and inner:sub(a, a):match("[%s]") do a = a + 1 end
|
||||
local b = a
|
||||
while b <= #inner and inner:sub(b, b):match("[%w_]") do b = b + 1 end
|
||||
local name = inner:sub(a, b - 1)
|
||||
local name_start = 1
|
||||
while name_start <= #inner and inner:sub(name_start, name_start):match("[%s]") do name_start = name_start + 1 end
|
||||
local name_end = name_start
|
||||
while name_end <= #inner and inner:sub(name_end, name_end):match("[%w_]") do name_end = name_end + 1 end
|
||||
local name = inner:sub(name_start, name_end - 1)
|
||||
if name == "" then
|
||||
i = open + 1
|
||||
pos = open_paren + 1
|
||||
else
|
||||
local brace = duffle.scan_to_char(source_text, "{", after_paren)
|
||||
if brace then
|
||||
local body, after_brace = duffle.read_braces(source_text, brace)
|
||||
local body_off = brace + 1
|
||||
out[#out + 1] = {
|
||||
line = line_of(i),
|
||||
line = line_of(pos),
|
||||
name = name,
|
||||
body = body,
|
||||
body_off = body_off,
|
||||
kind = kind,
|
||||
}
|
||||
i = after_brace
|
||||
pos = after_brace
|
||||
else
|
||||
i = open + 1
|
||||
pos = open_paren + 1
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
else
|
||||
i = after
|
||||
pos = ident_end
|
||||
end
|
||||
end -- close the new preprocessor-skip else
|
||||
end
|
||||
@@ -309,11 +309,11 @@ local function build_body_line_index(body)
|
||||
local index = {}
|
||||
local len = #body
|
||||
local newline_count = 0
|
||||
for i = 1, len do
|
||||
if i > 1 then
|
||||
index[i] = newline_count + 1 -- line of `i` relative to body
|
||||
for pos = 1, len do
|
||||
if pos > 1 then
|
||||
index[pos] = newline_count + 1 -- line of `pos` relative to body
|
||||
end
|
||||
if body:byte(i) == 10 then -- '\n'
|
||||
if body:byte(pos) == 10 then -- '\n'
|
||||
newline_count = newline_count + 1
|
||||
end
|
||||
end
|
||||
@@ -324,7 +324,7 @@ 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 arg)
|
||||
-- `nop2` -> 2 (i.e. `nop, nop` baked into one asm word)
|
||||
-- `nop,` / `nop2,` -> same as above; strip trailing comma defensively
|
||||
-- anything else -> 0
|
||||
--
|
||||
@@ -363,37 +363,37 @@ local function tokenize_body(body)
|
||||
-- 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 i = rel
|
||||
while i <= len do
|
||||
local c = body:byte(i)
|
||||
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, i); i = a
|
||||
local _, a = duffle.read_parens(body, scan); scan = a
|
||||
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 -- '['
|
||||
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 '\''
|
||||
i = duffle.duffle.skip_str_or_cmt(body, i) + 1
|
||||
scan = duffle.duffle.skip_str_or_cmt(body, scan) + 1
|
||||
else
|
||||
i = i + 1
|
||||
scan = scan + 1
|
||||
end
|
||||
end
|
||||
-- Extract token [rel .. i-1]
|
||||
local tok = duffle.trim(body:sub(rel, i - 1))
|
||||
-- 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 i <= len then
|
||||
i = i + 1
|
||||
if scan <= len then
|
||||
scan = scan + 1
|
||||
-- Also skip whitespace before next token
|
||||
local w = duffle.skip_ws_and_cmt(body, i)
|
||||
if w > i then i = w end
|
||||
local w = duffle.skip_ws_and_cmt(body, scan)
|
||||
if w > scan then scan = w end
|
||||
end
|
||||
rel = i
|
||||
rel = scan
|
||||
end
|
||||
return out
|
||||
end
|
||||
@@ -514,13 +514,13 @@ local function check_mac_yield_uniformity(atoms, findings)
|
||||
|
||||
local count = 0
|
||||
local last_idx = 0
|
||||
for i, t in ipairs(tokens) do
|
||||
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
|
||||
count = count + 1
|
||||
last_idx = i
|
||||
last_idx = tok_idx
|
||||
end
|
||||
end
|
||||
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`
|
||||
-- delay slot -- it's the standard "yield, then BD nop" idiom.
|
||||
local post_non_nop = false
|
||||
for j = last_idx + 1, #tokens do
|
||||
local t = tokens[j].tok
|
||||
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
|
||||
post_non_nop = true
|
||||
@@ -607,63 +607,63 @@ end
|
||||
local function find_binds_structs(source_text)
|
||||
local line_of = duffle.LineIndex(source_text)
|
||||
local out = {}
|
||||
local len = #source_text
|
||||
local i = 1
|
||||
while i <= len do
|
||||
i = duffle.skip_ws_and_cmt(source_text, i); if i > len then break end
|
||||
if source_text:sub(i, i) == "#" then
|
||||
local j = i
|
||||
while j <= len and source_text:byte(j) ~= 10 do j = j + 1 end
|
||||
i = j + 1
|
||||
local src_len = #source_text
|
||||
local pos = 1
|
||||
while pos <= src_len do
|
||||
pos = duffle.skip_ws_and_cmt(source_text, pos); if pos > src_len then break end
|
||||
if source_text:sub(pos, pos) == "#" then
|
||||
local eol_pos = pos
|
||||
while eol_pos <= src_len and source_text:byte(eol_pos) ~= 10 do eol_pos = eol_pos + 1 end
|
||||
pos = eol_pos + 1
|
||||
else
|
||||
local ident, after = duffle.read_ident(source_text, i)
|
||||
local ident, ident_end = duffle.read_ident(source_text, pos)
|
||||
if not ident then
|
||||
i = i + 1
|
||||
pos = pos + 1
|
||||
elseif ident == "typedef" then
|
||||
local j = duffle.skip_ws_and_cmt(source_text, after)
|
||||
local id2, after2 = duffle.read_ident(source_text, j)
|
||||
local after_typedef = duffle.skip_ws_and_cmt(source_text, ident_end)
|
||||
local id2, id2_end = duffle.read_ident(source_text, after_typedef)
|
||||
if id2 ~= "Struct_" then
|
||||
i = after2 or (j + 1)
|
||||
pos = id2_end or (after_typedef + 1)
|
||||
else
|
||||
local open = duffle.skip_ws_and_cmt(source_text, after2)
|
||||
if source_text:sub(open, open) ~= "(" then
|
||||
i = open + 1
|
||||
local open_paren = duffle.skip_ws_and_cmt(source_text, id2_end)
|
||||
if source_text:sub(open_paren, open_paren) ~= "(" then
|
||||
pos = open_paren + 1
|
||||
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 brace = duffle.scan_to_char(source_text, "{", after_paren)
|
||||
if not brace then
|
||||
i = open + 1
|
||||
pos = open_paren + 1
|
||||
else
|
||||
local body, after_brace = duffle.read_braces(source_text, brace)
|
||||
local fields = {}
|
||||
local byte_off = 0
|
||||
local k = 1
|
||||
while k <= #body do
|
||||
k = duffle.skip_ws_and_cmt(body, k); if k > #body then break end
|
||||
local tid, tafter = duffle.read_ident(body, k)
|
||||
if not tid then
|
||||
k = k + 1
|
||||
elseif tid == "U4" then
|
||||
local fid, fafter = duffle.read_ident(body, duffle.skip_ws_and_cmt(body, tafter))
|
||||
if fid then
|
||||
fields[#fields + 1] = { name = fid, offset = byte_off }
|
||||
local body_pos = 1
|
||||
while body_pos <= #body do
|
||||
body_pos = duffle.skip_ws_and_cmt(body, body_pos); if body_pos > #body then break end
|
||||
local type_ident, type_end = duffle.read_ident(body, body_pos)
|
||||
if not type_ident then
|
||||
body_pos = body_pos + 1
|
||||
elseif type_ident == "U4" then
|
||||
local field_ident, field_end = duffle.read_ident(body, duffle.skip_ws_and_cmt(body, type_end))
|
||||
if field_ident then
|
||||
fields[#fields + 1] = { name = field_ident, offset = byte_off }
|
||||
byte_off = byte_off + 4
|
||||
end
|
||||
k = fafter or (tafter + 1)
|
||||
body_pos = field_end or (type_end + 1)
|
||||
else
|
||||
k = tafter + 1
|
||||
body_pos = type_end + 1
|
||||
end
|
||||
end
|
||||
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
|
||||
i = after_brace
|
||||
pos = after_brace
|
||||
end
|
||||
end
|
||||
end
|
||||
else
|
||||
i = after
|
||||
pos = ident_end
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -678,77 +678,77 @@ end
|
||||
local function find_atom_info(source_text)
|
||||
local line_of = duffle.LineIndex(source_text)
|
||||
local out = {}
|
||||
local len = #source_text
|
||||
local i = 1
|
||||
while i <= len do
|
||||
i = duffle.skip_ws_and_cmt(source_text, i); if i > len then break end
|
||||
if source_text:sub(i, i) == "#" then
|
||||
local j = i
|
||||
while j <= len and source_text:byte(j) ~= 10 do j = j + 1 end
|
||||
i = j + 1
|
||||
local src_len = #source_text
|
||||
local pos = 1
|
||||
while pos <= src_len do
|
||||
pos = duffle.skip_ws_and_cmt(source_text, pos); if pos > src_len then break end
|
||||
if source_text:sub(pos, pos) == "#" then
|
||||
local eol_pos = pos
|
||||
while eol_pos <= src_len and source_text:byte(eol_pos) ~= 10 do eol_pos = eol_pos + 1 end
|
||||
pos = eol_pos + 1
|
||||
else
|
||||
local ident, after = duffle.read_ident(source_text, i)
|
||||
local ident, ident_end = duffle.read_ident(source_text, pos)
|
||||
if not ident then
|
||||
i = i + 1
|
||||
pos = pos + 1
|
||||
elseif ident == "MipsAtom_" then
|
||||
local open = duffle.skip_ws_and_cmt(source_text, after)
|
||||
if source_text:sub(open, open) ~= "(" then
|
||||
i = open + 1
|
||||
local open_paren = duffle.skip_ws_and_cmt(source_text, ident_end)
|
||||
if source_text:sub(open_paren, open_paren) ~= "(" then
|
||||
pos = open_paren + 1
|
||||
else
|
||||
local inner, after_paren = duffle.read_parens(source_text, open)
|
||||
local a = 1
|
||||
while a <= #inner and inner:sub(a, a):match("[%s]") do a = a + 1 end
|
||||
local b = a
|
||||
while b <= #inner and inner:sub(b, b):match("[%w_]") do b = b + 1 end
|
||||
local atom_name = inner:sub(a, b - 1)
|
||||
local inner, after_paren = duffle.read_parens(source_text, open_paren)
|
||||
local name_start = 1
|
||||
while name_start <= #inner and inner:sub(name_start, name_start):match("[%s]") do name_start = name_start + 1 end
|
||||
local name_end = name_start
|
||||
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(name_start, name_end - 1)
|
||||
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
|
||||
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
|
||||
local info_inner, info_after = duffle.read_parens(source_text, info_open)
|
||||
local binds, reads, writes = nil, nil, nil
|
||||
local j = 1
|
||||
while j <= #info_inner do
|
||||
j = duffle.skip_ws_and_cmt(info_inner, j); if j > #info_inner then break end
|
||||
local sub_ident, sub_after = duffle.read_ident(info_inner, j)
|
||||
local sub_pos = 1
|
||||
while sub_pos <= #info_inner do
|
||||
sub_pos = duffle.skip_ws_and_cmt(info_inner, sub_pos); if sub_pos > #info_inner then break end
|
||||
local sub_ident, sub_end = duffle.read_ident(info_inner, sub_pos)
|
||||
if not sub_ident then
|
||||
j = j + 1
|
||||
sub_pos = sub_pos + 1
|
||||
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
|
||||
local sub_inner, sub_after2 = duffle.read_parens(info_inner, sub_open)
|
||||
binds = duffle.trim(sub_inner)
|
||||
j = sub_after2
|
||||
sub_pos = sub_after2
|
||||
else
|
||||
j = sub_open + 1
|
||||
sub_pos = sub_open + 1
|
||||
end
|
||||
elseif sub_ident == "atom_reads" or sub_ident == "atom_writes" then
|
||||
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
|
||||
local sub_inner, sub_after2 = duffle.read_parens(info_inner, sub_open)
|
||||
local regs = {}
|
||||
local p = 1
|
||||
while p <= #sub_inner do
|
||||
p = duffle.skip_ws_and_cmt(sub_inner, p); if p > #sub_inner then break end
|
||||
local pid, pa = duffle.read_ident(sub_inner, p)
|
||||
if pid then
|
||||
regs[#regs + 1] = duffle.trim(pid)
|
||||
p = pa
|
||||
local sub_inner_pos = 1
|
||||
while sub_inner_pos <= #sub_inner do
|
||||
sub_inner_pos = duffle.skip_ws_and_cmt(sub_inner, sub_inner_pos); if sub_inner_pos > #sub_inner then break end
|
||||
local reg_ident, reg_end = duffle.read_ident(sub_inner, sub_inner_pos)
|
||||
if reg_ident then
|
||||
regs[#regs + 1] = duffle.trim(reg_ident)
|
||||
sub_inner_pos = reg_end
|
||||
else
|
||||
p = p + 1
|
||||
sub_inner_pos = sub_inner_pos + 1
|
||||
end
|
||||
if p > #sub_inner then break end
|
||||
if sub_inner:sub(p, p) == "," then p = p + 1 end
|
||||
if sub_inner_pos > #sub_inner then break end
|
||||
if sub_inner:sub(sub_inner_pos, sub_inner_pos) == "," then sub_inner_pos = sub_inner_pos + 1 end
|
||||
end
|
||||
if kind == "atom_reads" then reads = regs else writes = regs end
|
||||
j = sub_after2
|
||||
sub_pos = sub_after2
|
||||
else
|
||||
j = sub_open + 1
|
||||
sub_pos = sub_open + 1
|
||||
end
|
||||
else
|
||||
j = sub_after
|
||||
sub_pos = sub_end
|
||||
end
|
||||
end
|
||||
out[#out + 1] = {
|
||||
@@ -756,16 +756,16 @@ local function find_atom_info(source_text)
|
||||
reads = reads or {}, writes = writes or {},
|
||||
info_line = line_of(lookahead),
|
||||
}
|
||||
i = info_after
|
||||
pos = info_after
|
||||
else
|
||||
i = info_open + 1
|
||||
pos = info_open + 1
|
||||
end
|
||||
else
|
||||
i = after_paren
|
||||
pos = after_paren
|
||||
end
|
||||
end
|
||||
else
|
||||
i = after
|
||||
pos = ident_end
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -852,8 +852,8 @@ local function check_abi_handoff(atoms, atom_infos, binds_index, findings)
|
||||
end
|
||||
|
||||
if #found_field_seq == #expected_field_seq then
|
||||
for k = 1, #expected_field_seq do
|
||||
if found_field_seq[k] ~= expected_field_seq[k] then
|
||||
for field_idx = 1, #expected_field_seq do
|
||||
if found_field_seq[field_idx] ~= expected_field_seq[field_idx] then
|
||||
findings[#findings + 1] = {
|
||||
atom = a.name, line = a.line,
|
||||
check = "abi_handoff", kind = "error",
|
||||
@@ -993,9 +993,9 @@ end
|
||||
--- walker uses them as branch targets.
|
||||
local function find_atom_labels(tokens)
|
||||
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*%)")
|
||||
if name then labels[name] = i end
|
||||
if name then labels[name] = tok_idx end
|
||||
end
|
||||
return labels
|
||||
end
|
||||
@@ -1005,20 +1005,20 @@ end
|
||||
--- `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(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.
|
||||
--- (We can't use `nil` for the unknown-target case because `targets[i] = nil`
|
||||
--- REMOVES the key from the Lua table, which would make `is_branch(i)`
|
||||
--- (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 i, t in ipairs(tokens) do
|
||||
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[i] = label or false -- `false` = known branch, unknown target
|
||||
targets[tok_idx] = label or false -- `false` = known branch, unknown target
|
||||
end
|
||||
end
|
||||
return targets
|
||||
@@ -1052,9 +1052,9 @@ local function analyze_atom_paths(atom)
|
||||
local n = #tokens
|
||||
local costs = {}
|
||||
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)
|
||||
costs[i] = c
|
||||
costs[tok_idx] = c
|
||||
if unknown then
|
||||
unknown_set[t.tok:match("^([%w_]+)") or "?"] = true
|
||||
end
|
||||
@@ -1063,42 +1063,42 @@ local function analyze_atom_paths(atom)
|
||||
-- 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(i)
|
||||
local tok = tokens[i].tok
|
||||
local function is_terminator(tok_idx)
|
||||
local tok = tokens[tok_idx].tok
|
||||
return tok == "mac_yield" or tok:match("^mac_yield%s*%(")
|
||||
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 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
|
||||
-- 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[i] ~= nil` because
|
||||
-- `branches[i]` returns nil for both "absent" AND "present with
|
||||
-- nil value" — distinguishing them requires the key check.
|
||||
local function is_branch(i)
|
||||
local v = branches[i]
|
||||
-- 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.
|
||||
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
|
||||
end
|
||||
local function successors(i)
|
||||
local tok = tokens[i].tok
|
||||
if is_terminator(i) then
|
||||
return {}, i -- empty list; term = i signals "path ends here"
|
||||
local function successors(tok_idx)
|
||||
local tok = tokens[tok_idx].tok
|
||||
if is_terminator(tok_idx) then
|
||||
return {}, tok_idx -- empty list; term = tok_idx signals "path ends here"
|
||||
end
|
||||
if is_branch(i) then
|
||||
local label = branches[i] -- may be false for literal-offset branches
|
||||
if is_branch(tok_idx) then
|
||||
local label = branches[tok_idx] -- may be false for literal-offset branches
|
||||
local succ = {}
|
||||
-- Fall-through: skip the BD slot (i+1). Use i+2.
|
||||
if i + 2 <= n then
|
||||
succ[#succ + 1] = i + 2
|
||||
-- Fall-through: skip the BD slot (tok_idx+1). Use tok_idx+2.
|
||||
if tok_idx + 2 <= n then
|
||||
succ[#succ + 1] = tok_idx + 2
|
||||
end
|
||||
-- Taken: only if the branch has a known atom_offset target.
|
||||
if label then
|
||||
@@ -1114,8 +1114,8 @@ local function analyze_atom_paths(atom)
|
||||
return succ, nil
|
||||
end
|
||||
-- Normal token: just the next one
|
||||
if i + 1 <= n then
|
||||
return { i + 1 }, nil
|
||||
if tok_idx + 1 <= n then
|
||||
return { tok_idx + 1 }, nil
|
||||
end
|
||||
return {}, nil
|
||||
end
|
||||
@@ -1129,16 +1129,16 @@ local function analyze_atom_paths(atom)
|
||||
local cycles_max = -1
|
||||
local path_count = 0
|
||||
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 _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
|
||||
if visited[i] then
|
||||
if visited[tok_idx] then
|
||||
has_loops = true
|
||||
if _G._DEBUG_DFS_LOOP then
|
||||
io.stderr:write(string.format(" -> LOOP at i=%d (tok=%s) acc=%d\n",
|
||||
i, tokens[i].tok, acc))
|
||||
io.stderr:write(string.format(" -> LOOP at tok_idx=%d (tok=%s) acc=%d\n",
|
||||
tok_idx, tokens[tok_idx].tok, acc))
|
||||
end
|
||||
return
|
||||
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
|
||||
-- (and skip the BD slot in the successor list — already done in
|
||||
-- `successors` above for fall-through; for taken path the BD
|
||||
-- slot was at i+1 which is now skipped entirely).
|
||||
local cost = costs[i]
|
||||
if is_branch(i) and i + 1 <= n then
|
||||
cost = cost + costs[i + 1]
|
||||
-- slot was at tok_idx+1 which is now skipped entirely).
|
||||
local cost = costs[tok_idx]
|
||||
if is_branch(tok_idx) and tok_idx + 1 <= n then
|
||||
cost = cost + costs[tok_idx + 1]
|
||||
end
|
||||
local new_acc = acc + cost
|
||||
|
||||
local succ, term = successors(i)
|
||||
local succ, term = successors(tok_idx)
|
||||
if term then
|
||||
-- Terminator: record the path's cycle sum. We do NOT add
|
||||
-- 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
|
||||
return
|
||||
end
|
||||
visited[i] = true
|
||||
for _, next_i in ipairs(succ) do
|
||||
dfs(next_i, new_acc, visited)
|
||||
visited[tok_idx] = true
|
||||
for _, next_tok_idx in ipairs(succ) do
|
||||
dfs(next_tok_idx, new_acc, visited)
|
||||
end
|
||||
visited[i] = nil
|
||||
visited[tok_idx] = nil
|
||||
end
|
||||
if n >= 1 then dfs(1, 0, {}) end
|
||||
|
||||
-- cycles_full: sum of every token's cost (the previous model; useful
|
||||
-- for comparing against the path-aware min/max).
|
||||
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
|
||||
-- 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
|
||||
|
||||
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)
|
||||
|
||||
-- branch_count: number of `branch_*(...)` tokens. (More useful than
|
||||
@@ -1220,7 +1220,7 @@ local function count_atom_cycles(atom)
|
||||
end
|
||||
end
|
||||
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
|
||||
end
|
||||
|
||||
|
||||
@@ -107,6 +107,28 @@ end
|
||||
-- │ 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.
|
||||
--- No regex per the no_regex constraint — uses plain byte matching
|
||||
--- via `dir /b /s` on Windows.
|
||||
@@ -115,17 +137,47 @@ end
|
||||
--- @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 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
|
||||
local path = raw_line:gsub(PATH_SEP_BACKSLASH, PATH_SEP_FORWARD)
|
||||
results[#results + 1] = path
|
||||
end
|
||||
pipe:close()
|
||||
|
||||
-- Cache the result.
|
||||
cache = cache or {}
|
||||
cache[key] = results
|
||||
package.loaded[SCAN_CACHE_KEY] = cache
|
||||
|
||||
return results
|
||||
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 │
|
||||
-- └────────────────────────────────────────────────────────────────────┘
|
||||
|
||||
Reference in New Issue
Block a user