diff --git a/code/duffle/gen/duffle.macs.h b/code/duffle/gen/duffle.macs.h index 8a7e3b5..1df5c30 100644 --- a/code/duffle/gen/duffle.macs.h +++ b/code/duffle/gen/duffle.macs.h @@ -1,7 +1,7 @@ #ifdef INTELLISENSE_DIRECTIVES #pragma once #endif -// Auto-generated by tape_atom_annotation_pass.lua — DO NOT EDIT +// Auto-generated by ps1_meta.lua — DO NOT EDIT // Source: C:\projects\Pikuma\ps1\code\duffle\lottes_tape.h // Component atoms (MipsAtomComp_(ac_*)) -> macro variants (mac_*) diff --git a/code/duffle/gen/duffle.offsets.h b/code/duffle/gen/duffle.offsets.h index eabc7da..12ccce8 100644 --- a/code/duffle/gen/duffle.offsets.h +++ b/code/duffle/gen/duffle.offsets.h @@ -1,5 +1,5 @@ // Auto-generated by ps1_meta.lua (passes/offsets.lua) — DO NOT EDIT -// Source: C:\projects\Pikuma\ps1\code\duffle\lottes_tape.h +// Source: code\duffle\lottes_tape.h #pragma once #pragma region lottes_tape diff --git a/code/gte_hello/gen/gte_hello.offsets.h b/code/gte_hello/gen/gte_hello.offsets.h deleted file mode 100644 index e75de50..0000000 --- a/code/gte_hello/gen/gte_hello.offsets.h +++ /dev/null @@ -1,29 +0,0 @@ -// Auto-generated by ps1_meta.lua (passes/offsets.lua) — DO NOT EDIT -// Source: C:\projects\Pikuma\ps1\code\gte_hello\hello_gte_tape.c -#pragma once - -#pragma region hello_gte_tape - - -// --- atom: cube_g4_face (87 words) --- - -#define _atom_offset_cull_cube_g4_face_exit 48 -#define _atom_offset_bounds_chk_cube_g4_face_exit 12 - -enum { - atom_offset_cull_cube_g4_face_exit = _atom_offset_cull_cube_g4_face_exit, - atom_offset_bounds_chk_cube_g4_face_exit = _atom_offset_bounds_chk_cube_g4_face_exit, -}; - -// --- atom: floor_f3_face (66 words) --- - -#define _atom_offset_culling_floor_f3_face_exit 29 -#define _atom_offset_bounds_chk_floor_f3_face_exit 13 - -enum { - atom_offset_culling_floor_f3_face_exit = _atom_offset_culling_floor_f3_face_exit, - atom_offset_bounds_chk_floor_f3_face_exit = _atom_offset_bounds_chk_floor_f3_face_exit, -}; - -#pragma endregion hello_gte_tape - diff --git a/code/gte_hello/hello_gte_tape.c b/code/gte_hello/hello_gte_tape.c index ee52759..09c1584 100644 --- a/code/gte_hello/hello_gte_tape.c +++ b/code/gte_hello/hello_gte_tape.c @@ -123,7 +123,6 @@ MipsAtom_(floor_f3_face) atom_info( nop, branch_le_zero(R_T0, atom_offset(culling, floor_f3_face_exit)), nop, /* Format Primitive */ - // mac_format_f3_color(0x20FF, 0xFFFF), // works mac_format_f3_color(0xFF, 0xFF, 0xFF), // RGB-form (R=FF, G=FF, B=FF = white) mac_gte_store_f3_post_rtpt(), diff --git a/scripts/audit_lua_nesting.lua b/scripts/audit_lua_nesting.lua index 2b4f0a6..9a6e458 100644 --- a/scripts/audit_lua_nesting.lua +++ b/scripts/audit_lua_nesting.lua @@ -18,12 +18,12 @@ local M = {} local BLOCK_OPEN = { - ["do"] = true, - ["function"] = true, - ["if"] = true, - ["for"] = true, - ["while"] = true, - ["repeat"] = true, + ["do"] = true, + ["function"] = true, + ["if"] = true, + ["for"] = true, + ["while"] = true, + ["repeat"] = true, } local function is_block_close(token) return token == "end" or token == "until" end @@ -31,121 +31,118 @@ local function is_block_close(token) return token == "end" or token == "until" e -- (internal) Walk one source file and return a list of -- {line, depth, token} entries where depth > max_nesting. local function audit_file(path, max_nesting) - local f = io.open(path, "r") - if not f then error("Cannot open " .. path) end - local content = f:read("*a") - f:close() + local f = io.open(path, "r") + if not f then error("Cannot open " .. path) end + local content = f:read("*a") + f:close() - local violations = {} - local depth = 0 - local line = 1 - local pos = 1 - local src_len = #content - local token_idx = 0 + local violations = {} + local depth = 0 + local line = 1 + local pos = 1 + local src_len = #content + local token_idx = 0 - local function read_ident_at(start_pos) - local ident_start = start_pos - if ident_start > src_len then return nil end - local first_ch = content:sub(ident_start, ident_start) - if not (first_ch:match("[%a_]")) then return nil end - local scan = start_pos + 1 - while scan <= src_len do - local ch = content:sub(scan, scan) - if not (ch:match("[%w_]")) then break end - scan = scan + 1 - end - return content:sub(ident_start, scan - 1), scan - end + local function read_ident_at(start_pos) + local ident_start = start_pos + if ident_start > src_len then return nil end + local first_ch = content:sub(ident_start, ident_start) + if not (first_ch:match("[%a_]")) then return nil end + local scan = start_pos + 1 + while scan <= src_len do + local ch = content:sub(scan, scan) + if not (ch:match("[%w_]")) then break end + scan = scan + 1 + end + return content:sub(ident_start, scan - 1), scan + end - -- Skip past a string literal or comment starting at `start_pos`. - -- Returns the position just past the construct, or nil if `start_pos` - -- is not the start of a string/comment. - local function skip_string_or_comment(start_pos) - local ch = content:sub(start_pos, start_pos) - if ch == '"' or ch == "'" then - local scan = start_pos + 1 - while scan <= src_len do - local c = content:sub(scan, scan) - if c == "\\" then - scan = scan + 2 - elseif c == ch then - return scan + 1 - else - scan = scan + 1 - end - end - return src_len + 1 - elseif ch == "-" and content:sub(start_pos + 1, start_pos + 1) == "-" then - local scan = start_pos + 2 - if content:sub(scan, scan + 1) == "[[" and content:sub(scan + 2, scan + 3) == "[" then - -- Long bracket comment [==[ ... ]==] - scan = scan + 2 - local eq = "" - while content:sub(scan, scan) == "=" do - eq = eq .. "=" - scan = scan + 1 - end - local close_marker = "]" .. eq .. "]" - local close_pos = content:find(close_marker, scan, true) - if close_pos then - return close_pos + #close_marker - else - return src_len + 1 - end - else - while scan <= src_len and content:sub(scan, scan) ~= "\n" do scan = scan + 1 end - return scan + 1 - end - elseif ch == "[" and content:sub(start_pos + 1, start_pos + 1) == "[" then - local scan = start_pos + 2 - local eq = "" - while content:sub(scan, scan) == "=" do - eq = eq .. "=" - scan = scan + 1 - end - local close_marker = "]" .. eq .. "]" - local close_pos = content:find(close_marker, scan, true) - if close_pos then - return close_pos + #close_marker - else - return src_len + 1 - end - end - return nil - end + -- Skip past a string literal or comment starting at `start_pos`. + -- Returns the position just past the construct, or nil if `start_pos` + -- is not the start of a string/comment. + local function skip_string_or_comment(start_pos) + local ch = content:sub(start_pos, start_pos) + if ch == '"' or ch == "'" then + local scan = start_pos + 1 + while scan <= src_len do + local c = content:sub(scan, scan) + if c == "\\" then scan = scan + 2 + elseif c == ch then return scan + 1 + else scan = scan + 1 + end + end + return src_len + 1 + elseif ch == "-" and content:sub(start_pos + 1, start_pos + 1) == "-" then + local scan = start_pos + 2 + if content:sub(scan, scan + 1) == "[[" and content:sub(scan + 2, scan + 3) == "[" then + -- Long bracket comment [==[ ... ]==] + scan = scan + 2 + local eq = "" + while content:sub(scan, scan) == "=" do + eq = eq .. "=" + scan = scan + 1 + end + local close_marker = "]" .. eq .. "]" + local close_pos = content:find(close_marker, scan, true) + if close_pos then + return close_pos + #close_marker + else + return src_len + 1 + end + else + while scan <= src_len and content:sub(scan, scan) ~= "\n" do scan = scan + 1 end + return scan + 1 + end + elseif ch == "[" and content:sub(start_pos + 1, start_pos + 1) == "[" then + local scan = start_pos + 2 + local eq = "" + while content:sub(scan, scan) == "=" do + eq = eq .. "=" + scan = scan + 1 + end + local close_marker = "]" .. eq .. "]" + local close_pos = content:find(close_marker, scan, true) + if close_pos then + return close_pos + #close_marker + else + return src_len + 1 + end + end + return nil + end - while pos <= src_len do - local ch = content:sub(pos, pos) - if ch == "\n" then line = line + 1 end + while pos <= src_len do + local ch = content:sub(pos, pos) + if ch == "\n" then line = line + 1 end - local skip_to = skip_string_or_comment(pos) - if skip_to then - for scan = pos, skip_to - 1 do - if content:sub(scan, scan) == "\n" then line = line + 1 end - end - pos = skip_to - elseif ch:match("[%a_]") then - local tok, next_pos = read_ident_at(pos) - token_idx = token_idx + 1 - if BLOCK_OPEN[tok] then - depth = depth + 1 - if depth > max_nesting then - violations[#violations + 1] = { - line = line, - depth = depth, - token = tok, - } - end - elseif is_block_close(tok) then - depth = depth - 1 - end - pos = next_pos - else - pos = pos + 1 - end - end + local skip_to = skip_string_or_comment(pos) + if skip_to then + for scan = pos, skip_to - 1 do + if content:sub(scan, scan) == "\n" then line = line + 1 end + end + pos = skip_to + elseif ch:match("[%a_]") then + local tok, next_pos = read_ident_at(pos) + token_idx = token_idx + 1 + if BLOCK_OPEN[tok] then + depth = depth + 1 + if depth > max_nesting then + violations[#violations + 1] = { + line = line, + depth = depth, + token = tok, + } + end + elseif is_block_close(tok) then + depth = depth - 1 + end + pos = next_pos + else + pos = pos + 1 + end + end - return violations + return violations end --- Audit one file. Returns nil if clean, else a list of violations. @@ -153,78 +150,78 @@ end --- @param max_nesting integer -- default 5 --- @return table|nil function M.audit(path, max_nesting) - local violations = audit_file(path, max_nesting or 5) - if #violations == 0 then return nil end - return violations + local violations = audit_file(path, max_nesting or 5) + if #violations == 0 then return nil end + return violations end -- Module CLI. if arg and arg[1] then - local max_nesting = 5 - local files = {} - for arg_idx = 1, #arg do - if arg[arg_idx] == "--max" and arg[arg_idx + 1] then - max_nesting = tonumber(arg[arg_idx + 1]) or 5 - else - files[#files + 1] = arg[arg_idx] - end - end + local max_nesting = 5 + local files = {} + for arg_idx = 1, #arg do + if arg[arg_idx] == "--max" and arg[arg_idx + 1] then + max_nesting = tonumber(arg[arg_idx + 1]) or 5 + else + files[#files + 1] = arg[arg_idx] + end + end - -- Accept either a directory or a file path. Directory args are - -- expanded via `dir /b *.lua` (Windows) or `ls *.lua` (Unix). - local function is_dir(p) - local f = io.open(p, "r") - if f then f:close() return false end - return true - end - local function list_lua(dir) - local out = {} - local cmd - if package.config:sub(1, 1) == "\\" then - cmd = 'dir /b "' .. dir .. '\\*.lua" 2>nul' - else - cmd = 'ls -1 "' .. dir .. '"/*.lua 2>/dev/null' - end - local p = io.popen(cmd) - if p then - for line in p:lines() do - if line:match("%.lua$") then - out[#out + 1] = dir .. "/" .. line - end - end - p:close() - end - return out - end + -- Accept either a directory or a file path. Directory args are + -- expanded via `dir /b *.lua` (Windows) or `ls *.lua` (Unix). + local function is_dir(p) + local f = io.open(p, "r") + if f then f:close() return false end + return true + end + local function list_lua(dir) + local out = {} + local cmd + if package.config:sub(1, 1) == "\\" then + cmd = 'dir /b "' .. dir .. '\\*.lua" 2>nul' + else + cmd = 'ls -1 "' .. dir .. '"/*.lua 2>/dev/null' + end + local p = io.popen(cmd) + if p then + for line in p:lines() do + if line:match("%.lua$") then + out[#out + 1] = dir .. "/" .. line + end + end + p:close() + end + return out + end - local to_check = {} - for _, f in ipairs(files) do - if is_dir(f) then - for _, sub in ipairs(list_lua(f)) do to_check[#to_check + 1] = sub end - else - to_check[#to_check + 1] = f - end - end + local to_check = {} + for _, f in ipairs(files) do + if is_dir(f) then + for _, sub in ipairs(list_lua(f)) do to_check[#to_check + 1] = sub end + else + to_check[#to_check + 1] = f + end + end - local total_violations = 0 - for _, f in ipairs(to_check) do - local v = M.audit(f, max_nesting) - if v then - io.write(string.format("\n%s\n", f)) - for _, x in ipairs(v) do - io.write(string.format(" line %d: depth %d (after '%s')\n", x.line, x.depth, x.token)) - end - total_violations = total_violations + #v - end - end + local total_violations = 0 + for _, f in ipairs(to_check) do + local v = M.audit(f, max_nesting) + if v then + io.write(string.format("\n%s\n", f)) + for _, x in ipairs(v) do + io.write(string.format(" line %d: depth %d (after '%s')\n", x.line, x.depth, x.token)) + end + total_violations = total_violations + #v + end + end - if total_violations == 0 then - io.write("OK: no files exceed max nesting of " .. max_nesting .. "\n") - os.exit(0) - else - io.write(string.format("\n%d nesting violation(s) found.\n", total_violations)) - os.exit(1) - end + if total_violations == 0 then + io.write("OK: no files exceed max nesting of " .. max_nesting .. "\n") + os.exit(0) + else + io.write(string.format("\n%d nesting violation(s) found.\n", total_violations)) + os.exit(1) + end end return M diff --git a/scripts/duffle.lua b/scripts/duffle.lua index b8b1165..06dada7 100644 --- a/scripts/duffle.lua +++ b/scripts/duffle.lua @@ -15,10 +15,8 @@ --- Lua 5.3 compatible; no ``/``, no `continue`, no --- 5.4 string.dump improvements. LuaJIT 5.1+extensions model is the primary target. --- ---- **No `:match` / `:gmatch` regex use anywhere**; all delimiter- ---- splitting is hand-rolled or via LPeg (the regex-free PEG library). ---- The hot lexer primitives are LPeg-backed where it pays off; ---- hand-rolled variants remain for callers that need a fallback. +--- **No `:match` / `:gmatch` regex use anywhere**; +--- all delimiter-splitting is hand-rolled or via LPeg (the regex-free PEG library). local M = {} @@ -320,21 +318,17 @@ function M._reset_ensured_dirs() _ensured_dirs = {} end -- Skip a string or C-style comment starting at position `pos`. -- Returns the position just past the construct, or `pos` unchanged if no string/comment starts there. LPeg-backed. -function M.skip_str_or_cmt(s, pos) - return lpeg.match(lpeg_str_or_cmt_pat, s, pos) or pos -end +function M.skip_str_or_cmt(s, pos) return lpeg.match(lpeg_str_or_cmt_pat, s, pos) or pos end -- Skip whitespace AND C-style comments starting at position `pos`. -- LPeg-backed; ~5-10x faster than a hand-rolled byte-by-byte walker. -function M.skip_ws_and_cmt(s, pos) - return lpeg.match(lpeg_ws_and_cmt_pat, s, pos) or pos -end +function M.skip_ws_and_cmt(s, pos) return lpeg.match(lpeg_ws_and_cmt_pat, s, pos) or pos end -- Read a C-style identifier (alpha followed by zero+ alnum) starting at position `pos`. -- Returns the identifier string + the position just past it, or nil + pos if no identifier starts here. LPeg-backed. function M.read_ident(s, pos) local result = lpeg.match(lpeg_ident_pat, s, pos) - if result then return result, pos + #result end + if result then return result, pos + #result end return nil, pos end @@ -344,7 +338,9 @@ end function M.read_balanced(s, open_char, close_char, pos) local open_byte = open_char:byte() if s:byte(pos) ~= open_byte then return nil, pos end + -- scan: pos = pos + 1 + -- scan: local len = #s local depth = 1 local a = pos @@ -353,15 +349,23 @@ function M.read_balanced(s, open_char, close_char, pos) if c == open_byte then depth = depth + 1 pos = pos + 1 + -- scan: (depth=depth) elseif c == close_char:byte() then depth = depth - 1 if depth == 0 then break end pos = pos + 1 + -- scan: (depth=depth) else local nx = M.skip_str_or_cmt(s, pos) - if nx > pos then pos = nx else pos = pos + 1 end + if nx > pos then + -- scan: + pos = nx + else + pos = pos + 1 + end end end + -- scan: return s:sub(a, pos - 1), pos + 1 end @@ -379,12 +383,17 @@ function M.scan_to_char(s, target, start) while pos <= #s do local c = s:byte(pos) if c == target_byte then return pos end + -- scan: ... | if c == BYTE_OPEN_PAREN then local _, a = M.read_balanced(s, "(", ")", pos); pos = a + -- scan: ... ( ) ... elseif c == BYTE_OPEN_BRACE then local _, a = M.read_balanced(s, "{", "}", pos); pos = a + -- scan: ... { } ... elseif c == BYTE_OPEN_BRACK then local _, a = M.read_balanced(s, "[", "]", pos); pos = a + -- scan: ... [ ] ... else local nx = M.skip_str_or_cmt(s, pos) pos = (nx > pos) and nx or (pos + 1) + -- scan: ... ... end end return nil @@ -447,28 +456,29 @@ function M.split_top_level_commas(body) end while pos <= body_len do - local c = body:byte(pos) - if c == BYTE_OPEN_PAREN then -- '(' - local _, a = M.read_parens(body, pos); pos = a - elseif c == BYTE_OPEN_BRACE then -- '{' - local _, a = M.read_braces(body, pos); pos = a - elseif c == BYTE_OPEN_BRACK then -- '[' - local _, a = M.read_brackets(body, pos); pos = a - elseif c == BYTE_COMMA then -- ',' + local c = body:byte(pos) + if c == BYTE_OPEN_PAREN then local _, a = M.read_parens(body, pos); pos = a -- scan: ... ( ... + elseif c == BYTE_OPEN_BRACE then local _, a = M.read_braces(body, pos); pos = a -- scan: ... { ... + elseif c == BYTE_OPEN_BRACK then local _, a = M.read_brackets(body, pos); pos = a -- scan: ... ( ... + elseif c == BYTE_COMMA then + -- scan: ... , ... emit(pos - 1) pos = pos + 1 token_start = pos - elseif c == BYTE_SEMI then -- ';' + elseif c == BYTE_SEMI then + -- scan: ... ; ... emit(pos - 1) pos = pos + 1 token_start = pos - elseif c == BYTE_NEWLINE then -- '\n' + elseif c == BYTE_NEWLINE then + -- scan: ... \n ... emit(pos - 1) pos = pos + 1 token_start = pos else local nx = M.skip_str_or_cmt(body, pos) if nx > pos then + -- scan: ... ... -- Skipped a comment or string at top level: emit token break. pos = nx emit(pos - 1) @@ -477,6 +487,7 @@ function M.split_top_level_commas(body) end end end + -- scan: , , ... emit(body_len) return tokens end @@ -495,6 +506,7 @@ function M.load_word_counts(metadata_path) local nl = M.find_byte(content, BYTE_NEWLINE, pos) local line_end = nl or (len + 1) local line = content:sub(pos, line_end - 1) + -- scan: WORD_COUNT(, ) local trimmed = M.trim(line) if trimmed:sub(1, #prefix) == prefix and trimmed:sub(-1) == ")" then local inner = trimmed:sub(#prefix + 1, #trimmed - 1) @@ -527,11 +539,8 @@ function M.LineIndex(source) local lo, hi = 1, n while lo <= hi do local mid = math.floor((lo + hi) / 2) - if positions[mid] <= query_pos then - lo = mid + 1 - else - hi = mid - 1 - end + if positions[mid] <= query_pos then lo = mid + 1 + else hi = mid - 1 end end return hi + 1 end @@ -666,7 +675,7 @@ M.GP0_MACRO_CONTRIB = { ["mac_insert_ot_tag_g4"] = 1, } --- Per-macro cycle cost (best-case, no stalls). Used by the static-analysis `count_atom_cycles` pass (Phase 3) to emit per-atom cycle budgets. +-- Per-macro cycle cost (best-case, no stalls). Used by the static-analysis pass to emit per-atom cycle budgets. -- The counts cover the EXPANDED instruction sequence the macro emits (NOT just the token it appears as in source). -- For example: -- mac_pack_color_word(off, cmd, r, g, b) emits: diff --git a/scripts/passes/annotation.lua b/scripts/passes/annotation.lua index 5829282..23fbd70 100644 --- a/scripts/passes/annotation.lua +++ b/scripts/passes/annotation.lua @@ -149,7 +149,6 @@ local BYTE_COMMA = 44 --- @field errors Finding[] --- @field warnings Finding[] --- @field info Finding[] ---- @field pragmas table -- reserved (currently always nil; legacy compat) --- ════════════════════════════════════════════════════════════════════════════ -- split helpers @@ -239,18 +238,14 @@ local function parse_regs_call(s) return spec.kind, inner 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 - --- Parse a comma-separated inner content (e.g. inside atom_reads(...)) into a list of trimmed identifiers with aliases resolved. +-- Parse a comma-separated inner content (e.g. inside atom_reads(...)) into a list of trimmed identifiers. 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 end - return resolve_reg_aliases(out) + return out end -- Parse a single token (from split_csv_top) into an arg entry. @@ -272,11 +267,11 @@ end --- Extract identifier args from a parenthesized group. --- Returns a list of {kind, value} pairs where kind is one of: ---- "ident" -- a bare identifier (e.g. phase_work) +--- "ident" -- a bare identifier (e.g. a reserved token) --- "atom_reads" -- an atom_reads(...) call: value is the register list --- "atom_writes" -- an atom_writes(...) call: value is the register list --- "other" -- something we can't classify (preserved as text) -local function parse_atom_annot_args(inner) +local function parse_atom_info_args(inner) local args = {} for _, tok in ipairs(split_csv_top(inner)) do local s = trim(tok) @@ -593,8 +588,7 @@ local function find_atom_names(source) end -- ════════════════════════════════════════════════════════════════════════════ --- Find atom annotations (atom_annot / atom_init / atom_setup / atom_commit --- / atom_bind / atom_terminate) +-- Find atom annotations (atom_info + atom_bind / atom_reads / atom_writes) -- ════════════════════════════════════════════════════════════════════════════ --- True iff the parsed arg is a register-list call (any recognized form). @@ -612,8 +606,7 @@ function ANNOT_ARG_HANDLERS.info(entry, args) 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.) + -- Reserved for future use. Currently ignored. else entry.errors[#entry.errors + 1] = string.format("unexpected atom_info arg kind=%s value=%s", arg.kind, tostring(arg.value)) end @@ -653,7 +646,7 @@ local function parse_atom_info_call(source, atom_name, after_mipsatom_paren, lin local info_inner, info_after = read_parens(source, info_open) -- scan: MipsAtom_() atom_info(, , ) - local args = parse_atom_annot_args(info_inner) + local args = parse_atom_info_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 @@ -713,7 +706,7 @@ local function find_atom_annotations(source) end -- ════════════════════════════════════════════════════════════════════════════ --- Validation (ported from tape_atom_annotation_pass.lua:1193-1405) +-- Validation -- ════════════════════════════════════════════════════════════════════════════ local function validate(ctx, src) @@ -865,7 +858,6 @@ local function validate(ctx, src) atoms = atoms, annots = annots, macros = macros, - pragmas = pragmas, binds = binds, errors = errors, warnings = warnings, diff --git a/scripts/passes/components.lua b/scripts/passes/components.lua index 32104fb..c6ac104 100644 --- a/scripts/passes/components.lua +++ b/scripts/passes/components.lua @@ -727,7 +727,7 @@ local function header_boilerplate(src) "#ifdef INTELLISENSE_DIRECTIVES", "#pragma once", "#endif", - "// Auto-generated by tape_atom_annotation_pass.lua — DO NOT EDIT", + "// Auto-generated by ps1_meta.lua — DO NOT EDIT", "// Source: " .. to_absolute_path(src.path), "// Component atoms (MipsAtomComp_(ac_*)) -> macro variants (mac_*)", "", diff --git a/scripts/passes/static_analysis.lua b/scripts/passes/static_analysis.lua index b96c0e4..bfd72ac 100644 --- a/scripts/passes/static_analysis.lua +++ b/scripts/passes/static_analysis.lua @@ -1039,9 +1039,6 @@ end --- has_loops - true iff a path re-entered a token it had visited --- (warning; loop bodies aren't supported) --- unknown_macros - list of unique macro names not in duffle.INSTRUCTION_LATENCY ---- cycles_full - sum of ALL token costs (the previous "best case" ---- value; included for backward-compat; double-counts ---- the BD-slot nop relative to cycles_min/max) local function analyze_atom_paths(atom) local tokens = tokenize_body(atom.body) local labels = find_atom_labels(tokens) @@ -1173,13 +1170,8 @@ local function analyze_atom_paths(atom) end if n >= 1 then dfs(1, 0, {}) end - -- cycles_full: sum of every token's cost (the legacy sum-of-all-tokens - -- value; over-counts BD-slot nops relative to the path-aware min/max). - local cycles_full = 0 - 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. + -- default to 0 (atom costs nothing). if cycles_min == math.huge then cycles_min = 0 end if cycles_max == -1 then cycles_max = 0 end @@ -1187,16 +1179,13 @@ local function analyze_atom_paths(atom) 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 - -- `#branches` which is the size of the targets map and would be equal - -- to #branches anyway, but the rename is clearer.) + -- branch_count: number of `branch_*(...)` tokens. local branch_count = 0 for _ in pairs(branches) do branch_count = branch_count + 1 end return { cycles_min = cycles_min, cycles_max = cycles_max, - cycles_full = cycles_full, branches = branch_count, paths = path_count, has_loops = has_loops, @@ -1204,32 +1193,22 @@ local function analyze_atom_paths(atom) } end ---- Returns total cycle count (the sum-of-all-tokens value, which over-counts ---- BD-slot nops) + unknown macro list. New code should call ---- `analyze_atom_paths(atom)` instead. -local function count_atom_cycles(atom) - local tokens = tokenize_body(atom.body) - local total = 0 - local unknown_set = {} - for _, t in ipairs(tokens) do - local c, _, unknown = token_cycles(t.tok) - total = total + c - if unknown then - unknown_set[t.tok:match("^([%w_]+)") or "?"] = true - end - end - local unknown_list = {} - for macro_name in pairs(unknown_set) do unknown_list[#unknown_list + 1] = macro_name end - return total, unknown_list -end - --- Per-source check that emits one finding per unknown macro seen --- (deduplicated across atoms so the warning section doesn't get --- spammed with N copies of "macro X not in duffle.INSTRUCTION_LATENCY"). local function check_per_atom_cycle_budget(atoms, findings) local unknown_seen = {} for _, a in ipairs(atoms) do - local _, unknown_macros = count_atom_cycles(a) + local tokens = tokenize_body(a.body) + local unknown_set = {} + for _, t in ipairs(tokens) do + local _, _, unknown = token_cycles(t.tok) + if unknown then + unknown_set[t.tok:match("^([%w_]+)") or "?"] = true + end + end + local unknown_macros = {} + for macro_name in pairs(unknown_set) do unknown_macros[#unknown_macros + 1] = macro_name end for _, name in ipairs(unknown_macros) do if not unknown_seen[name] then unknown_seen[name] = a.line @@ -1269,22 +1248,15 @@ local function validate(ctx, src) check_gpu_portstore_shape(atoms, findings) check_per_atom_cycle_budget(atoms, findings) - -- Path-aware cycle-budget output: attach per-path cycle data to each - -- atom. Best-case (no-stall) cycle count with BD-slot absorbed; the - -- `cycles_full` field is the legacy sum-of-all-tokens value (kept - -- for backward compat; over-counts BD-slot nops). - local cycles_by_atom = {} + -- Path-aware cycle-budget output: attach per-path cycle data to each atom. for _, a in ipairs(atoms) do local p = analyze_atom_paths(a) a.paths = p - a.cycles = p.cycles_max -- default for any code that reads .cycles a.cycles_min = p.cycles_min a.cycles_max = p.cycles_max - a.cycles_full = p.cycles_full a.branch_count = p.branches a.unknown_macros = p.unknown_macros a.has_loops = p.has_loops - cycles_by_atom[a.name] = p end local errors = {} @@ -1347,87 +1319,9 @@ local function validate(ctx, src) end -- ════════════════════════════════════════════════════════════════════════════ --- Per-source output: build/gen/.static_analysis.txt +-- Per-directory output: build/gen/.static_analysis.txt -- ════════════════════════════════════════════════════════════════════════════ -local function emit_static_analysis_txt(ctx, src, result) - local out_path = ctx.out_root .. "/" .. src.basename .. ".static_analysis.txt" - if ctx.dry_run then return out_path end - duffle.ensure_dir(ctx.out_root) - - local lines = {} - local function add(s) lines[#lines + 1] = s end - - add("========================================================") - add("STATIC ANALYSIS PASS -- " .. src.path) - add("========================================================") - add("") - -- Tally atoms by kind for the header summary - local n_atoms, n_bare, n_proc = 0, 0, 0 - for _, a in ipairs(result.atoms) do - n_atoms = n_atoms + 1 - if a.kind == "comp_bare" then n_bare = n_bare + 1 - elseif a.kind == "comp_proc" then n_proc = n_proc + 1 - end - end - local header_atoms = string.format("Atoms: %d", n_atoms) - if n_bare > 0 or n_proc > 0 then - header_atoms = header_atoms .. string.format(" (atoms: %d, comp_bare: %d, comp_proc: %d)", - n_atoms - n_bare - n_proc, n_bare, n_proc) - end - add(string.format("%s Findings: %d Errors: %d Warnings: %d", - header_atoms, #result.findings, #result.errors, #result.warnings)) - add("") - - -- Group findings by atom for readability - local by_atom = {} - for _, f in ipairs(result.findings) do - by_atom[f.atom] = by_atom[f.atom] or {} - by_atom[f.atom][#by_atom[f.atom] + 1] = f - end - - if next(by_atom) == nil then - add(" (no findings -- every atom passed all checks)") - else - add("── Findings by atom ─────────────────────────────────────") - for _, a in ipairs(result.atoms) do - local fs = by_atom[a.name] - if fs then - add(string.format(" %s line %d", a.name, a.line)) - for _, f in ipairs(fs) do - add(string.format(" [%s] %s", f.check, f.msg)) - end - end - end - end - - add("") - add("── Errors ──────────────────────────────────────────────") - if #result.errors == 0 then add(" (none)") end - for _, e in ipairs(result.errors) do - add(string.format(" X line %d %s", e.line, e.msg)) - end - - add("") - add("── Warnings ────────────────────────────────────────────") - if #result.warnings == 0 then add(" (none)") end - for _, w in ipairs(result.warnings) do - add(string.format(" ! line %d %s", w.line, w.msg)) - end - - add("") - add("── Info ────────────────────────────────────────────────") - for _, i_ in ipairs(result.info) do - add(string.format(" %s", i_.msg)) - end - - duffle.write_file(out_path, table.concat(lines, "\n") .. "\n") - return out_path -end - --- (Old per-source emit function above; kept for backward compat but no longer called from M.run. --- Replaced by `emit_module_static_analysis_txt` which aggregates by directory.) - --- Per-directory emit. Aggregates atoms + findings across every source --- in `dir_sources` and writes a single report to --- `/.static_analysis.txt`. Called only when diff --git a/scripts/ps1_meta.lua b/scripts/ps1_meta.lua index bf5a4d4..d79f865 100644 --- a/scripts/ps1_meta.lua +++ b/scripts/ps1_meta.lua @@ -573,7 +573,7 @@ end -- @param result PassResult -- @return boolean local function report_validation_errors(pass_name, pass, result) - local has_errors = result.errors and #result.errors > 0 + local has_errors = result.errors and #result.errors > 0 if not (has_errors and PASS_KIND_STOP_ON_ERROR[pass.kind]) then return false end