From 9d066ae29217c3e975b04eee35eb499a28aa6913 Mon Sep 17 00:00:00 2001 From: Ed_ Date: Thu, 9 Jul 2026 20:10:01 -0400 Subject: [PATCH] nesting reduction --- scripts/passes/annotation.lua | 269 +++++++++++++++++++++------------- scripts/passes/components.lua | 154 ++++++++++--------- scripts/ps1_meta.lua | 103 +++++++++---- 3 files changed, 322 insertions(+), 204 deletions(-) diff --git a/scripts/passes/annotation.lua b/scripts/passes/annotation.lua index 5dafe6a..a714cef 100644 --- a/scripts/passes/annotation.lua +++ b/scripts/passes/annotation.lua @@ -109,6 +109,63 @@ end -- Parse TAPE_ATOM_ANNOT(...) calls -- ════════════════════════════════════════════════════════════════════════════ +-- Recognize a `atom_reads(...)` or `atom_writes(...)` register-list +-- call embedded inside an annotation arg list. Returns the kind +-- ("atom_reads" / "atom_writes") and the inner content, or nil if the +-- token isn't a recognized register-list form. Flattened via a +-- prefix lookup instead of a nested if/elseif chain. +local REGS_CALL_PREFIX = { + ["atom_reads("] = { kind = "atom_reads", inner_offset = 12 }, + ["atom_writes("] = { kind = "atom_writes", inner_offset = 13 }, +} + +local function parse_regs_call(s) + if s:sub(-1) ~= ")" then return nil end + local spec = REGS_CALL_PREFIX[s:sub(1, 12)] -- longest prefix first wins + if not spec then return nil end + -- The 12-char prefix "atom_reads(" also matches "atom_writes(" + -- would be ambiguous; the table order above handles it. + -- (atom_reads prefix is 11 chars, atom_writes is 12; the 12-char + -- lookup matches atom_writes first.) + return spec.kind, s:sub(spec.inner_offset, -2) +end + +-- Resolve any phase_* / R_* alias macros in a register list. +local function resolve_reg_aliases(regs) + for i, r in ipairs(regs) do + if MACRO_EXPANSION[r] then regs[i] = MACRO_EXPANSION[r] end + end + return regs +end + +-- Parse a comma-separated inner content (e.g. inside atom_reads(...)) +-- into a list of trimmed identifiers with aliases resolved. +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) +end + +-- Parse a single token (from split_csv_top) into an arg entry. +-- Three forms: register-list call, bare identifier (with alias), +-- "other" (preserved as text). +local function parse_arg_token(s) + local kind, inner = parse_regs_call(s) + if kind then + return { kind = kind, value = parse_regs_list(inner) } + end + local id = read_ident(s, 1) + if id and trim(s) == id then + local v = id + if MACRO_EXPANSION[v] then v = MACRO_EXPANSION[v] end + return { kind = "ident", value = v } + end + return { kind = "other", value = s } +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) @@ -117,45 +174,10 @@ end --- "other" -- something we can't classify (preserved as text) local function parse_atom_annot_args(inner) local args = {} - local tokens = split_csv_top(inner) - for _, tok in ipairs(tokens) do + for _, tok in ipairs(split_csv_top(inner)) do local s = trim(tok) if s ~= "" then - -- Detect register-list calls: atom_reads(...) / atom_writes(...) - local regs_kind = nil - local regs_inner = nil - if s:sub(-1) == ")" then - if s:sub(1, 11) == "atom_reads(" then - regs_kind = "atom_reads" - regs_inner = s:sub(12, -2) - elseif s:sub(1, 12) == "atom_writes(" then - regs_kind = "atom_writes" - regs_inner = s:sub(13, -2) - end - end - - if regs_kind then - local regs = {} - for _, r in ipairs(split_csv_top(regs_inner)) do - local trimmed = trim(r) - if trimmed ~= "" then regs[#regs + 1] = trimmed end - end - -- Resolve any phase_* / R_* alias macros - for i, r in ipairs(regs) do - if MACRO_EXPANSION[r] then regs[i] = MACRO_EXPANSION[r] end - end - args[#args + 1] = {kind = regs_kind, value = regs} - else - -- Bare identifier (e.g. phase_work) - local id = read_ident(s, 1) - if id and trim(s) == id then - local v = id - if MACRO_EXPANSION[v] then v = MACRO_EXPANSION[v] end - args[#args + 1] = {kind = "ident", value = v} - else - args[#args + 1] = {kind = "other", value = s} - end - end + args[#args + 1] = parse_arg_token(s) end end return args @@ -498,6 +520,85 @@ end -- / atom_bind / atom_terminate) -- ════════════════════════════════════════════════════════════════════════════ +--- 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 + +--- Per-macro arg-shape handlers. Each takes (entry, args) and mutates +--- entry.{reads, writes, phase, binds, errors}. Replaces the 5-way +--- `if/elseif/elseif/elseif/elseif` chain inside find_atom_annotations. +local ANNOT_ARG_HANDLERS = {} + +-- atom_bind(name, Binds_Struct, writes) +function ANNOT_ARG_HANDLERS.bind(entry, args) + if #args >= 2 and args[2].kind == "ident" then + entry.binds = args[2].value + end + if #args >= 3 and is_regs_arg(args[3]) then + entry.writes = args[3].value + end +end + +-- atom_init(name) / atom_terminate(name): name only, no extra slots. +ANNOT_ARG_HANDLERS.init = function() end +ANNOT_ARG_HANDLERS.terminate = function() end + +-- Macro name -> handler key. Replaces the `macro_def.binds` check +-- plus the 4-way ident elseif chain. +local MACRO_HANDLER_KEY = { + ["atom_bind"] = "bind", + ["atom_annot"] = "annot", + ["atom_setup"] = "reads_only", + ["atom_commit"] = "reads_only", + ["atom_init"] = "init", + ["atom_terminate"] = "terminate", +} + +-- atom_setup(name, reads) / atom_commit(name, reads): reads from slot 2. +function ANNOT_ARG_HANDLERS.reads_only(entry, args) + if #args >= 2 and is_regs_arg(args[2]) then + entry.reads = args[2].value + end +end + +-- atom_annot(name, phase, reads, writes) +function ANNOT_ARG_HANDLERS.annot(entry, args) + if #args >= 2 and args[2].kind == "ident" then + entry.phase = MACRO_EXPANSION[args[2].value] or args[2].value + end + if #args >= 3 and is_regs_arg(args[3]) then + if args[3].kind == "atom_writes" then + entry.errors[#entry.errors + 1] = "reads slot has atom_writes — swap order?" + end + entry.reads = args[3].value + end + if #args >= 4 and is_regs_arg(args[4]) then + if args[4].kind == "atom_reads" then + entry.errors[#entry.errors + 1] = "writes slot has atom_reads — swap order?" + end + entry.writes = args[4].value + end +end + +-- Build a new annotation entry with the standard shape. +local function new_annot_entry(line, ident, name, kind) + return { + line = line, + macro = ident, + name = name, + kind = kind, + binds = nil, + phase = nil, + reads = {}, + writes = {}, + errors = {}, + } +end + +--- Find every TAPE_ATOM_* macro call in source and convert it to a +--- normalized annotation entry. Dispatches per-macro arg-shape via +--- ANNOT_ARG_HANDLERS (lookup table; no nested if/elseif chain). local function find_atom_annotations(source) local line_of = duffle.LineIndex(source) local annots = {} @@ -513,13 +614,17 @@ local function find_atom_annotations(source) local j = i while j <= len and source:sub(j, j) ~= "\n" do j = j + 1 end i = j + 1 - else + goto continue + end + local ident, after = read_ident(source, i) if not ident then i = i + 1 elseif TAPE_ATOM_MACROS[ident] then local open = skip_ws_and_cmt(source, after) - if source:sub(open, open) == "(" then + if source:sub(open, open) ~= "(" then + i = open + 1 + else local inner, after_paren = read_parens(source, open) local args = parse_atom_annot_args(inner) local macro_def = TAPE_ATOM_MACROS[ident] @@ -532,70 +637,17 @@ local function find_atom_annotations(source) error = "missing atom name (first arg)", } else - local name = args[1].value - local entry = { - line = line_of(i), - macro = ident, - name = name, - kind = macro_def.kind, - binds = nil, - phase = nil, - reads = {}, - writes = {}, - errors = {}, - } - - local function is_regs(a) - return a and (a.kind == "atom_reads" or a.kind == "atom_writes" or a.kind == "regs") - end - - if macro_def.binds then - if #args >= 2 and args[2].kind == "ident" then - entry.binds = args[2].value - end - if #args >= 3 and is_regs(args[3]) then - entry.writes = args[3].value - end - elseif ident == "atom_init" or ident == "atom_terminate" then - -- (name) only - elseif ident == "atom_setup" then - if #args >= 2 and is_regs(args[2]) then - entry.reads = args[2].value - end - elseif ident == "atom_commit" then - if #args >= 2 and is_regs(args[2]) then - entry.reads = args[2].value - end - elseif ident == "atom_annot" then - if #args >= 2 and args[2].kind == "ident" then - entry.phase = MACRO_EXPANSION[args[2].value] or args[2].value - end - if #args >= 3 and is_regs(args[3]) then - if args[3].kind == "atom_writes" then - entry.errors[#entry.errors + 1] = - "reads slot has atom_writes — swap order?" - end - entry.reads = args[3].value - end - if #args >= 4 and is_regs(args[4]) then - if args[4].kind == "atom_reads" then - entry.errors[#entry.errors + 1] = - "writes slot has atom_reads — swap order?" - end - entry.writes = args[4].value - end - end - + local entry = new_annot_entry(line_of(i), ident, args[1].value, macro_def.kind) + local handler = ANNOT_ARG_HANDLERS[MACRO_HANDLER_KEY[ident]] + if handler then handler(entry, args) end annots[#annots + 1] = entry end i = after_paren - else - i = open + 1 end else i = after end - end + ::continue:: end return annots end @@ -727,24 +779,30 @@ local function validate(ctx, src) end -- 7. TAPE_WORDS(mac_X, N) ↔ WORD_COUNT(mac_X, N) drift. - for _, m in ipairs(macros) do - local declared = ctx.shared.word_counts[m.name] + -- Three outcomes: missing (error), mismatch (error), match (info). + -- Flattened via early-return-style helper instead of 3-way elseif. + local function check_macro_drift(m, declared) if not declared then errors[#errors + 1] = { line = m.line, msg = string.format("TAPE_WORDS(%s, %d) but '%s' is not in metadata.h", m.name, m.words, m.name), } - elseif declared ~= m.words then + return + end + if declared ~= m.words then errors[#errors + 1] = { line = m.line, msg = string.format("DRIFT: TAPE_WORDS(%s, %d) but metadata.h declares WORD_COUNT(%s, %d)", m.name, m.words, m.name, declared), } - else - info[#info + 1] = { - line = m.line, - msg = string.format("OK: %s = %d words", m.name, m.words), - } + return end + info[#info + 1] = { + line = m.line, + msg = string.format("OK: %s = %d words", m.name, m.words), + } + end + for _, m in ipairs(macros) do + check_macro_drift(m, ctx.shared.word_counts[m.name]) end -- 8. atom_<...> _Pragma validation: resource/region/group/cadence/async @@ -776,7 +834,8 @@ local function validate(ctx, src) end end - -- 9. CADENCE_ONDEMAND requires async=true. + -- 9. CADENCE_ONDEMAND requires async=true. Flattened as a guard + -- (single condition, no nested if). for _, p in ipairs(pragmas) do if p.attrs.cadence == "ondemand" and p.attrs.async ~= "true" then errors[#errors + 1] = { diff --git a/scripts/passes/components.lua b/scripts/passes/components.lua index a8b7b5b..0de6204 100644 --- a/scripts/passes/components.lua +++ b/scripts/passes/components.lua @@ -487,6 +487,83 @@ end -- once per component AND extend ctx.shared.word_counts. -- ============================================================ +-- ============================================================ +-- Per-component emit logic. Returns the body of lines for one +-- component (signature comment, #define mac_X(...) line with +-- backslash-continued tokens, then WORD_COUNT(mac_X, N) entry). +-- +-- Extracted from emit_component_macros_h so M.run can call it +-- once per component AND extend ctx.shared.word_counts. +-- ============================================================ + +--- Split a (possibly multi-line) comment into per-line entries. +--- Hand-rolled (no regex patterns used). +--- @param s string +--- @return string[] +local function split_comment_lines(s) + local out = {} + local i = 1 + local len = #s + while i <= len do + local nl = s:find("\n", i, true) + if not nl then + out[#out + 1] = s:sub(i) + break + end + out[#out + 1] = s:sub(i, nl - 1) + i = nl + 1 + end + return out +end + +--- Split an atom body by top-level commas; drop empty tokens. +--- @param body string +--- @return string[] +local function tokens_from_body(body) + local out = {} + for _, t in ipairs(split_top_level_commas(body)) do + local trimmed = trim(t) + if trimmed ~= "" then out[#out + 1] = trimmed end + end + return out +end + +--- Determine the macro signature: function-args list (function form) +--- or variadic-ignored (bare form). +--- @param args_str string|nil +--- @return string +local function signature_from_args(args_str) + local arg_names = extract_arg_names(args_str) + if arg_names and #arg_names > 0 then + return table.concat(arg_names, ", ") + end + return "..." +end + +--- Strip the trailing " \" (space + backslash) line continuation +--- from the last body line. The last 2 chars are always that pair. +local function strip_trailing_continuation(lines) + local last = lines[#lines] + if last:sub(-2) == " \\" then + lines[#lines] = last:sub(1, -3) + end +end + +--- Emit the `#define mac_X(sig) \\t \,\t ...` +--- block. Converts `//` line comments to `/* */` block comments in +--- each token so they don't break the C macro `\` line continuations. +local function emit_macro_body(lines, c, sig, tokens) + for j = 1, #tokens do + tokens[j] = convert_line_comments_to_block(tokens[j]) + end + lines[#lines + 1] = "#define mac_" .. c.name .. "(" .. sig .. ") \\" + lines[#lines + 1] = "\t" .. tokens[1] .. " \\" + for j = 2, #tokens do + lines[#lines + 1] = ",\t" .. tokens[j] .. " \\" + end + strip_trailing_continuation(lines) +end + --- @param c Component --- @param components Component[] --- @param wc table @@ -494,81 +571,20 @@ end local function build_component_lines(c, components, wc) local lines = {} - -- Emit the signature comment (if any) above the macro. if c.comment and c.comment ~= "" then - -- Hand-rolled newline splitter (no regex patterns used). - local s = c.comment .. "\n" - local i = 1 - local len = #s - while i <= len do - local nl = s:find("\n", i, true) - if not nl then - lines[#lines + 1] = s:sub(i) - break - end - lines[#lines + 1] = s:sub(i, nl - 1) - i = nl + 1 + for _, line in ipairs(split_comment_lines(c.comment)) do + lines[#lines + 1] = line end end - -- Split body by top-level commas; filter empty tokens. - local tokens = {} - for _, t in ipairs(split_top_level_commas(c.body)) do - local trimmed = trim(t) - if trimmed ~= "" then tokens[#tokens + 1] = trimmed end + local tokens = tokens_from_body(c.body) + local sig = signature_from_args(c.args) + local n = compute_component_word_count(c, components, wc) + + if n > 0 then + emit_macro_body(lines, c, sig, tokens) end - -- Determine the macro signature: with function args (function - -- form) or variadic-ignored (bare form). - local arg_names = extract_arg_names(c.args) - local sig - if arg_names and #arg_names > 0 then - sig = table.concat(arg_names, ", ") - else - sig = "..." - end - - -- Compute the word count of the component body, accounting for - -- macro expansion. Each comma-separated entry in the body is one - -- "instruction slot", but a `mac_Y(...)` call expands to Y's - -- word count. The offset_gen and the metadata's WORD_COUNT table - -- both use this resolved count. - -- - -- We compute it once per component (not per token) and emit - -- the value in the WORD_COUNT entry. The lookup table `counts_by_name` - -- is built from the components list (which find_component_atoms - -- already populated) and falls back to 1 for non-mac tokens. - local counts_by_name = {} - for _, cc in ipairs(components) do - local cc_count = compute_component_word_count(cc, components, wc) - counts_by_name["mac_" .. cc.name] = cc_count - end - local n = compute_component_word_count(c, components, wc) - - if n > 0 then - -- Convert `//` line comments to `/* */` block comments in each - -- token. C macros use `\` line-continuations; if a `//` comment - -- appears before a `\`, the rest of the line (including the - -- continuation) is consumed by the `//`, breaking the macro. - -- Converting to `/* */` preserves the macro structure. - for j = 1, #tokens do - tokens[j] = convert_line_comments_to_block(tokens[j]) - end - -- Emit the mac_() macro - lines[#lines + 1] = "#define mac_" .. c.name .. "(" .. sig .. ") \\" - lines[#lines + 1] = "\t" .. tokens[1] .. " \\" - for j = 2, #tokens do - lines[#lines + 1] = ",\t" .. tokens[j] .. " \\" - end - -- Strip the trailing line-continuation on the last body line. - -- The last 2 chars are always " \" (space + backslash). - -- No regex — just trim with string.sub. - local last = lines[#lines] - if last:sub(-2) == " \\" then - lines[#lines] = last:sub(1, -3) - end - end - -- Emit the WORD_COUNT(mac_, N) entry. lines[#lines + 1] = "WORD_COUNT(mac_" .. c.name .. ", " .. n .. ")" lines[#lines + 1] = "" diff --git a/scripts/ps1_meta.lua b/scripts/ps1_meta.lua index aaccb54..2d9207e 100644 --- a/scripts/ps1_meta.lua +++ b/scripts/ps1_meta.lua @@ -148,6 +148,21 @@ local ALL_PASS_NAMES = { "offsets", "static-analysis", "report", } +--- Append every pass name to args.requested_set. Used by --all and +--- by the "default to --all if no pass flags were given" fallback. +local function request_all_passes(args) + for _, n in ipairs(ALL_PASS_NAMES) do + args.requested_set[#args.requested_set + 1] = n + end +end + +-- Per-flag handlers. Each handler takes (args, argv, i) and returns +-- the new i (so multi-arg flags like --source FILE advance it). +-- Returning nil + os.exit() handles termination flags (--help). +-- This replaces the 8-way `if/elseif/elseif...` chain that nested +-- 4 levels deep and made the dispatch logic hard to scan. +local FLAG_HANDLERS = {} + -- ════════════════════════════════════════════════════════════════════════════ -- CLI parsing -- ════════════════════════════════════════════════════════════════════════════ @@ -188,6 +203,59 @@ EXAMPLE: ]]) end +-- Per-flag handlers. Each takes (args, argv, i) and returns the new i +-- (so multi-arg flags like --source FILE advance it). Termination +-- flags like --help call os.exit() instead. This replaces the 8-way +-- `if/elseif/elseif...` chain that nested 4 levels deep and made the +-- dispatch logic hard to scan. +-- +-- Populated AFTER print_help so the --help handler can reference it +-- as an upvalue (Lua resolves locals at closure-call time, but if the +-- closure is defined before the local, it falls back to _G). +FLAG_HANDLERS["--help"] = function(args) + print_help() + os.exit(0) +end + +FLAG_HANDLERS["--dry-run"] = function(args) + args.dry_run = true +end + +FLAG_HANDLERS["--verbose"] = function(args) + args.verbose = true +end + +FLAG_HANDLERS["--source"] = function(args, argv, i) + args.sources[#args.sources + 1] = argv[i + 1] + return i + 1 +end + +FLAG_HANDLERS["--metadata"] = function(args, argv, i) + args.metadata = argv[i + 1] + return i + 1 +end + +FLAG_HANDLERS["--out-root"] = function(args, argv, i) + args.out_root = argv[i + 1] + return i + 1 +end + +FLAG_HANDLERS["--project-root"] = function(args, argv, i) + args.project_root = argv[i + 1] + return i + 1 +end + +-- Pass-flag handler. Reads the closed-set table, expands --all, +-- appends to requested_set. Single-statement, no nesting. +FLAG_HANDLERS["__pass__"] = function(args, a) + local name = PASS_FLAG_TO_NAME[a] + if name == "__all__" then + request_all_passes(args) + return + end + args.requested_set[#args.requested_set + 1] = name +end + --- Parse argv into a structured table. Validates against a closed enum. --- --- @param argv string[] @@ -206,34 +274,11 @@ local function parse_args(argv) local i = 1 while i <= #argv do local a = argv[i] - if a == "--help" then - print_help() - os.exit(0) - elseif a == "--dry-run" then - args.dry_run = true - elseif a == "--verbose" then - args.verbose = true - elseif a == "--source" then - i = i + 1 - args.sources[#args.sources + 1] = argv[i] - elseif a == "--metadata" then - i = i + 1 - args.metadata = argv[i] - elseif a == "--out-root" then - i = i + 1 - args.out_root = argv[i] - elseif a == "--project-root" then - i = i + 1 - args.project_root = argv[i] + local handler = FLAG_HANDLERS[a] + if handler then + i = handler(args, argv, i) or i elseif PASS_FLAG_TO_NAME[a] then - local name = PASS_FLAG_TO_NAME[a] - if name == "__all__" then - for _, n in ipairs(ALL_PASS_NAMES) do - args.requested_set[#args.requested_set + 1] = n - end - else - args.requested_set[#args.requested_set + 1] = name - end + FLAG_HANDLERS["__pass__"](args, a) else io.stderr:write("ps1_meta: unknown flag '" .. a .. "'\n") io.stderr:write("Run with --help for usage.\n") @@ -244,9 +289,7 @@ local function parse_args(argv) -- Default: --all if no explicit pass flags. if #args.requested_set == 0 then - for _, n in ipairs(ALL_PASS_NAMES) do - args.requested_set[#args.requested_set + 1] = n - end + request_all_passes(args) end -- Defaults: project_root = dirname(metadata).