diff --git a/scripts/duffle.lua b/scripts/duffle.lua index 7e0e4e5..826a8b3 100644 --- a/scripts/duffle.lua +++ b/scripts/duffle.lua @@ -342,6 +342,20 @@ end -- Not normally needed since Lua state is per-process. function M._reset_ensured_dirs() _ensured_dirs = {} end +-- Group a list of `SourceFile`-shaped records by their `dir` field. +-- Used by the annotation / static-analysis / report passes to partition sources into per-DIRECTORY (per-module) buckets +-- before emitting per-module reports. Insertion order preserved within each bucket (matches source order in `ctx.sources`). +-- @param sources table[] -- list of source records (each having a `dir` string field) +-- @return table -- map of `dir` -> sources in that dir +function M.group_sources_by_dir(sources) + local by_dir = {} + for _, src in ipairs(sources) do + by_dir[src.dir] = by_dir[src.dir] or {} + table.insert(by_dir[src.dir], src) + end + return by_dir +end + -- ════════════════════════════════════════════════════════════════════════════ -- Section 4: C-language scanner primitives -- ════════════════════════════════════════════════════════════════════════════ diff --git a/scripts/duffle_paths.lua b/scripts/duffle_paths.lua index cbccdce..e2c9c1a 100644 --- a/scripts/duffle_paths.lua +++ b/scripts/duffle_paths.lua @@ -1,13 +1,21 @@ --- duffle_paths.lua — Single-line bootstrap helper for the tape-atom Lua scripts. --- ---- Each entry script (ps1_meta.lua, word_count_eval.lua, and the 5 passes/*.lua files) starts with: +--- Each entry script (ps1_meta.lua + the 7 passes/*.lua files) starts with one of: --- ```lua +--- -- Entry script (ps1_meta.lua — `arg[0]` is set): --- local duffle = dofile((arg[0]:match("(.*[/\\])") or "./") .. "duffle_paths.lua") +--- +--- -- Pass module (debug.getinfo path resolution; works both standalone and when require'd): +--- local _bootstrap_dir = debug.getinfo(1, "S").source:match("^@?(.*[/\\])") or "./" +--- local duffle = dofile(_bootstrap_dir .. "../duffle_paths.lua") --- ``` ---- That single line: (a) locates this helper via `arg[0]`, ---- (b) loads it (which sets `package.path` + `package.cpath` via `git rev-parse`), ---- (c) returns the `M` table (a wrapper around the setup function). ---- After this line, `require("duffle")` and `require("passes.X")` both resolve normally. +--- +--- That small bootstrap: (a) locates this helper via `arg[0]` / `debug.getinfo`, +--- (b) loads it (which sets `package.path` + `package.cpath` via cached `git rev-parse`), +--- (c) at the bottom calls `require("duffle")` (now resolvable since `package.path` was just set) and returns the duffle M. +--- Net effect: the caller gets the duffle module in one statement; no separate `dofile(...)` + `require("duffle")` dance. +--- +--- Replaces the prior 2-line (entry) or 4-line (pass) pattern that had the call site do its own path resolution + duplicated setup. local M = {} @@ -63,4 +71,6 @@ end -- Run the setup as a side effect. M.setup() -return M +-- Now that package.path includes scripts/, `require("duffle")` resolves. Return the duffle module +-- so callers can do `local duffle = dofile(...duffle_paths.lua)` in one line. +return require("duffle") diff --git a/scripts/passes/annotation.lua b/scripts/passes/annotation.lua index 80af908..2b2c1b9 100644 --- a/scripts/passes/annotation.lua +++ b/scripts/passes/annotation.lua @@ -18,10 +18,10 @@ -- Bootstrap: load `scripts/duffle_paths.lua` (sets package.path + package.cpath). -- Uses `debug.getinfo` to find this file's own directory, so it works -- both standalone and when require'd from the orchestrator. -local _src = debug.getinfo(1, "S").source:sub(2) -local _dir = _src:match("(.*[/\\])") or "./" -dofile(_dir .. "../duffle_paths.lua") -local duffle = require("duffle") +-- Bootstrap: load `duffle_paths.lua` via `debug.getinfo(1, "S").source` (works both standalone + when require'd). +-- duffle_paths.lua sets package.path then returns `require("duffle")` at the bottom, so the dofile value IS the duffle module. +local _bootstrap_dir = debug.getinfo(1, "S").source:match("^@?(.*[/\\])") or "./" +local duffle = dofile(_bootstrap_dir .. "../duffle_paths.lua") local write_file = duffle.write_file local ensure_dir = duffle.ensure_dir @@ -329,11 +329,7 @@ function M.run(ctx) -- Per-DIRECTORY (per-module) aggregation. Group sources by `src.dir`, -- validate every source in the dir, then emit ONE errors.h per dir. - local by_dir = {} - for _, src in ipairs(ctx.sources) do - by_dir[src.dir] = by_dir[src.dir] or {} - table.insert(by_dir[src.dir], src) - end + local by_dir = duffle.group_sources_by_dir(ctx.sources) for dir, dir_sources in pairs(by_dir) do local dir_basename = dir:match("([^/\\]+)$") or dir diff --git a/scripts/passes/components.lua b/scripts/passes/components.lua index dc96844..98b420f 100644 --- a/scripts/passes/components.lua +++ b/scripts/passes/components.lua @@ -28,10 +28,10 @@ -- Bootstrap: load `scripts/duffle_paths.lua` (sets package.path + package.cpath). -- Uses `debug.getinfo` to find this file's own directory, so it works -- both standalone and when require'd from the orchestrator. -local _src = debug.getinfo(1, "S").source:sub(2) -local _dir = _src:match("(.*[/\\])") or "./" -dofile(_dir .. "../duffle_paths.lua") -local duffle = require("duffle") +-- Bootstrap: load `duffle_paths.lua` via `debug.getinfo(1, "S").source` (works both standalone + when require'd). +-- duffle_paths.lua sets package.path then returns `require("duffle")` at the bottom, so the dofile value IS the duffle module. +local _bootstrap_dir = debug.getinfo(1, "S").source:match("^@?(.*[/\\])") or "./" +local duffle = dofile(_bootstrap_dir .. "../duffle_paths.lua") local word_count_eval = require("word_count_eval") -- ════════════════════════════════════════════════════════════════════════════ @@ -431,8 +431,8 @@ local function strip_mac_prefix(ident) return ident end --- (internal) Recursive word-count lookup. `cache` is the memoization table across all calls to `compute_component_word_count`; --- the in-progress -1 sentinel detects cycles (A -> B -> A). +-- (internal) Recursive word-count lookup. `cache` is the memoization table shared across all components +-- in a single source's `count_all_components` pass; the in-progress -1 sentinel detects cycles (A -> B -> A). -- @param name string -- the component name (without `mac_`) -- @param comp_by_name table -- @param wc table @@ -469,25 +469,25 @@ local function word_count_rec(name, comp_by_name, wc, cache) return n end ---- Compute the word count of a component body, accounting for macro expansion. ---- Each comma-separated entry in the body is a "slot" that contributes its own word count. ---- For most entries (regular MIPS instructions) the count is 1. ---- For `mac_Y(...)` calls, the count is the word count of `mac_Y` (recursive lookup through `components`). ---- For encoding macros with a known multi-word count (e.g. `mask_upper` = 2), ---- the count is taken from `word_counts`. +--- Compute word counts for every component in `components` in a single pass. +--- The name-lookup table + memoization cache are built ONCE (per source) instead of per-component, +--- so the cache survives across siblings and a component's recursive `mac_Y(...)` references hit memoized values +--- instead of re-walking the body. Previously each call rebuilt both tables (O(N) tables per call → O(N^2)). --- ---- The lookup is memoized via `word_count_rec` to avoid infinite recursion (e.g. if two components referenced each other). ---- This is the same algorithm as the original `tape_atom_annotation_pass.lua` (commit 7d20a4d). +--- Cycle detection (A -> B -> A) is preserved via the in-progress `-1` sentinel in `cache`. --- ---- @param c Component --- @param components Component[] --- @param wc table ---- @return integer -local function compute_component_word_count(c, components, wc) +--- @return table -- map of component name (without `mac_`) -> word count +local function count_all_components(components, wc) local comp_by_name = {} for _, cc in ipairs(components) do comp_by_name[cc.name] = cc end - local cache = {} - return word_count_rec(c.name, comp_by_name, wc, cache) + local cache = {} + local counts = {} + for _, c in ipairs(components) do + counts[c.name] = word_count_rec(c.name, comp_by_name, wc, cache) + end + return counts end -- ════════════════════════════════════════════════════════════════════════════ @@ -566,7 +566,7 @@ end --- @param components Component[] --- @param wc table --- @return string[] -- list of lines for this component -local function build_component_lines(c, components, wc) +local function build_component_lines(c, counts) local lines = {} if c.comment and c.comment ~= "" then @@ -577,7 +577,8 @@ local function build_component_lines(c, components, wc) local tokens = tokens_from_body(c.body) local sig = signature_from_args(c.args) - local n = compute_component_word_count(c, components, wc) + -- Direct lookup against the per-source precomputed `counts` table (built once by count_all_components). + local n = counts[c.name] if n > 0 then emit_macro_body(lines, c, sig, tokens) @@ -640,16 +641,16 @@ end --- @param ctx PassCtx --- @param src SourceFile --- @param components Component[] +--- @param counts table -- precomputed word counts (from count_all_components) --- @return string|nil -- path to the written file (nil if no components) -local function emit_component_macros_h(ctx, src, components) +local function emit_component_macros_h(ctx, src, components, counts) if #components == 0 then return nil end local out_dir, out_path = compute_macs_h_path(src) local lines = header_boilerplate(src) - local wc = ctx.shared.word_counts for _, c in ipairs(components) do - for _, l in ipairs(build_component_lines(c, components, wc)) do + for _, l in ipairs(build_component_lines(c, counts)) do lines[#lines + 1] = l end end @@ -675,10 +676,11 @@ end -- so offsets sees them without re-reading the file. -- @param ctx PassCtx -- @param components Component[] -local function update_shared_word_counts(ctx, components) +-- @param counts table -- precomputed word counts (from count_all_components) +local function update_shared_word_counts(ctx, components, counts) local wc = ctx.shared.word_counts for _, c in ipairs(components) do - wc["mac_" .. c.name] = compute_component_word_count(c, components, wc) + wc["mac_" .. c.name] = counts[c.name] end end @@ -693,10 +695,12 @@ function M.run(ctx) -- project_components reads from src.scan + does backward lookups on src.text local components = project_components(src.text, src.scan) if #components > 0 then - local macs_path = emit_component_macros_h(ctx, src, components) + -- Compute word counts for ALL components once (was: rebuilt per call inside the helpers). + local counts = count_all_components(components, ctx.shared.word_counts) + local macs_path = emit_component_macros_h(ctx, src, components, counts) if macs_path then outputs[#outputs + 1] = { macs_h = macs_path } - update_shared_word_counts(ctx, components) + update_shared_word_counts(ctx, components, counts) end end end diff --git a/scripts/passes/offsets.lua b/scripts/passes/offsets.lua index 65e89e9..be0d124 100644 --- a/scripts/passes/offsets.lua +++ b/scripts/passes/offsets.lua @@ -18,10 +18,10 @@ -- Bootstrap: load `scripts/duffle_paths.lua` (sets package.path + package.cpath). -- Uses `debug.getinfo` to find this file's own directory, so it works -- both standalone and when require'd from the orchestrator. -local _src = debug.getinfo(1, "S").source:sub(2) -local _dir = _src:match("(.*[/\\])") or "./" -dofile(_dir .. "../duffle_paths.lua") -local duffle = require("duffle") +-- Bootstrap: load `duffle_paths.lua` via `debug.getinfo(1, "S").source` (works both standalone + when require'd). +-- duffle_paths.lua sets package.path then returns `require("duffle")` at the bottom, so the dofile value IS the duffle module. +local _bootstrap_dir = debug.getinfo(1, "S").source:match("^@?(.*[/\\])") or "./" +local duffle = dofile(_bootstrap_dir .. "../duffle_paths.lua") local word_count_eval = require("word_count_eval") local count_token_words = word_count_eval.count_token_words diff --git a/scripts/passes/report.lua b/scripts/passes/report.lua index 1ab0b87..5e0f6dd 100644 --- a/scripts/passes/report.lua +++ b/scripts/passes/report.lua @@ -21,10 +21,10 @@ -- Bootstrap: load `scripts/duffle_paths.lua` (sets package.path + package.cpath). -- Uses `debug.getinfo` to find this file's own directory, so it works -- both standalone and when require'd from the orchestrator. -local _src = debug.getinfo(1, "S").source:sub(2) -local _dir = _src:match("(.*[/\\])") or "./" -dofile(_dir .. "../duffle_paths.lua") -local duffle = require("duffle") +-- Bootstrap: load `duffle_paths.lua` via `debug.getinfo(1, "S").source` (works both standalone + when require'd). +-- duffle_paths.lua sets package.path then returns `require("duffle")` at the bottom, so the dofile value IS the duffle module. +local _bootstrap_dir = debug.getinfo(1, "S").source:match("^@?(.*[/\\])") or "./" +local duffle = dofile(_bootstrap_dir .. "../duffle_paths.lua") -- ════════════════════════════════════════════════════════════════════════════ -- Constants @@ -346,18 +346,6 @@ end -- Orchestration helpers -- ════════════════════════════════════════════════════════════════════════════ --- Group source files by their `dir` field. Used to mirror the per-DIRECTORY partitioning the annotation pass uses. --- @param sources SourceFile[] --- @return table -- map of dir -> sources in that dir -local function group_sources_by_dir(sources) - local by_dir = {} - for _, src in ipairs(sources) do - by_dir[src.dir] = by_dir[src.dir] or {} - table.insert(by_dir[src.dir], src) - end - return by_dir -end - -- (internal) Validate each source in `dir_sources` via the annotation pass, tagging each result with `result.source = src.path` for downstream rendering. -- Returns the list of module results + the flat list of all results (for the project-wide summary). -- @param ctx PassCtx @@ -413,7 +401,7 @@ function M.run(ctx) local warnings = {} local module_entries = (ctx.flags and ctx.flags._annot_results) or {} - local by_dir = group_sources_by_dir(ctx.sources) + local by_dir = duffle.group_sources_by_dir(ctx.sources) if not ctx.dry_run then duffle.ensure_dir(ctx.out_root) end diff --git a/scripts/passes/scan_source.lua b/scripts/passes/scan_source.lua index 029f050..8f255f9 100644 --- a/scripts/passes/scan_source.lua +++ b/scripts/passes/scan_source.lua @@ -21,10 +21,10 @@ -- Bootstrap: load `scripts/duffle_paths.lua` (sets package.path + package.cpath). -- Uses `debug.getinfo` to find this file's own directory, so it works -- both standalone and when require'd from the orchestrator. -local _src = debug.getinfo(1, "S").source:sub(2) -local _dir = _src:match("(.*[/\\])") or "./" -dofile(_dir .. "../duffle_paths.lua") -local duffle = require("duffle") +-- Bootstrap: load `duffle_paths.lua` via `debug.getinfo(1, "S").source` (works both standalone + when require'd). +-- duffle_paths.lua sets package.path then returns `require("duffle")` at the bottom, so the dofile value IS the duffle module. +local _bootstrap_dir = debug.getinfo(1, "S").source:match("^@?(.*[/\\])") or "./" +local duffle = dofile(_bootstrap_dir .. "../duffle_paths.lua") -- ════════════════════════════════════════════════════════════════════════════ -- Type declarations diff --git a/scripts/passes/static_analysis.lua b/scripts/passes/static_analysis.lua index 3ca711d..7fb9c6b 100644 --- a/scripts/passes/static_analysis.lua +++ b/scripts/passes/static_analysis.lua @@ -22,10 +22,10 @@ -- Bootstrap: load `scripts/duffle_paths.lua` (sets package.path + package.cpath). -- Uses `debug.getinfo` to find this file's own directory, so it works both standalone and when require'd from the orchestrator. -local _src = debug.getinfo(1, "S").source:sub(2) -local _dir = _src:match("(.*[/\\])") or "./" -dofile(_dir .. "../duffle_paths.lua") -local duffle = require("duffle") +-- Bootstrap: load `duffle_paths.lua` via `debug.getinfo(1, "S").source` (works both standalone + when require'd). +-- duffle_paths.lua sets package.path then returns `require("duffle")` at the bottom, so the dofile value IS the duffle module. +local _bootstrap_dir = debug.getinfo(1, "S").source:match("^@?(.*[/\\])") or "./" +local duffle = dofile(_bootstrap_dir .. "../duffle_paths.lua") -- ════════════════════════════════════════════════════════════════════════════ -- Constants @@ -392,197 +392,6 @@ local function check_mac_yield_uniformity(atoms, findings) end end end - --- ════════════════════════════════════════════════════════════════════════════ --- Source walkers: Binds_* structs + per-atom atom_info --- ════════════════════════════════════════════════════════════════════════════ - --- Parse the U4 fields from a Binds_X body. Returns (fields, byte_count). -local function parse_binds_fields(body) - local fields = {} - local byte_off = 0 - 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 - body_pos = field_end or (type_end + 1) - else - body_pos = type_end + 1 - end - end - return fields, byte_off -end - ---- Walk source-as-written, return a list of `{line, name, fields, bytes}` for every `typedef Struct_(Binds_X) { ... };` declaration. ---- Only U4 fields are tracked (Binds_* are always word arrays in this codebase pointers stored as U4, indices as U4, etc.). ---- Mirrors annotation.lua :: find_binds_structs but is independent (no shared cross-pass state for static-analysis; each pass re-walks source). -local function find_binds_structs(source_text) - local line_of = duffle.LineIndex(source_text) - local out = {} - 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 - - local pp_pos = duffle.skip_preprocessor_line(source_text, pos) - if pp_pos then pos = pp_pos; goto continue end - - local ident, ident_end = duffle.read_ident(source_text, pos) - -- scan: - if not ident then pos = pos + 1; goto continue end - if ident ~= "typedef" then pos = ident_end; goto continue end - - local after_typedef = duffle.skip_ws_and_cmt(source_text, ident_end) - local id2, id2_end = duffle.read_ident(source_text, after_typedef) - -- scan: typedef - if id2 ~= "Struct_" then pos = id2_end or (after_typedef + 1); goto continue end - - 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; goto continue end - - local inner, after_paren = duffle.read_parens(source_text, open_paren) - -- scan: typedef Struct_() - local name = duffle.trim(inner) - local brace = duffle.scan_to_char(source_text, "{", after_paren) - -- scan: typedef Struct_() { - if not brace then pos = open_paren + 1; goto continue end - - local body, after_brace = duffle.read_braces(source_text, brace) - -- scan: typedef Struct_() { } - local fields, byte_off = parse_binds_fields(body) - if name:sub(1, 6) == "Binds_" then - out[#out + 1] = { line = line_of(pos), name = name, fields = fields, bytes = byte_off } - end - pos = after_brace - - ::continue:: - end - return out -end - --- Parse the register list from inside `atom_reads(...)` or `atom_writes(...)`. -local function parse_reg_list(sub_inner) - local regs = {} - 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 - sub_inner_pos = sub_inner_pos + 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 - return regs -end - --- Parse the sub-calls inside `atom_info(atom_bind(...), atom_reads(...), atom_writes(...))`. --- Returns (binds, reads, writes). -local function parse_atom_info_subcalls(info_inner) - local binds, reads, writes = nil, nil, nil - 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 - sub_pos = sub_pos + 1 - elseif sub_ident == "atom_bind" then - 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) - -- scan: atom_bind() - binds = duffle.trim(sub_inner) - sub_pos = sub_after2 - else - 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_end) - if info_inner:sub(sub_open, sub_open) == "(" then - local sub_inner, sub_after2 = duffle.read_parens(info_inner, sub_open) - -- scan: atom_reads() OR atom_writes() - local regs = parse_reg_list(sub_inner) - if kind == "atom_reads" then reads = regs else writes = regs end - sub_pos = sub_after2 - else - sub_pos = sub_open + 1 - end - else - sub_pos = sub_end - end - end - return binds, reads, writes -end - ---- For every `MipsAtom_(name)` followed by `atom_info(...)`, parse the atom_info's `atom_bind(Binds_X)`, `atom_reads(...)`, and `atom_writes(...)` sub-calls. ---- Returns a list of `{atom_name, binds, reads, writes, info_line}` for use by check_abi_handoff. -local function find_atom_info(source_text) - local line_of = duffle.LineIndex(source_text) - local out = {} - 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 - - local pp_pos = duffle.skip_preprocessor_line(source_text, pos) - if pp_pos then pos = pp_pos; goto continue end - - local ident, ident_end = duffle.read_ident(source_text, pos) - -- scan: - if not ident then pos = pos + 1; goto continue end - if ident ~= "MipsAtom_" then pos = ident_end; goto continue end - - 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; goto continue end - - local inner, after_paren = duffle.read_parens(source_text, open_paren) - -- scan: MipsAtom_() - 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_end = duffle.read_ident(source_text, lookahead) - -- scan: MipsAtom_() - if look_ident ~= "atom_info" then pos = after_paren; goto continue end - - local info_open = duffle.skip_ws_and_cmt(source_text, look_end) - if source_text:sub(info_open, info_open) ~= "(" then pos = info_open + 1; goto continue end - - local info_inner, info_after = duffle.read_parens(source_text, info_open) - -- scan: MipsAtom_() atom_info(, , ) - local binds, reads, writes = parse_atom_info_subcalls(info_inner) - out[#out + 1] = { - atom_name = atom_name, binds = binds, - reads = reads or {}, writes = writes or {}, - info_line = line_of(lookahead), - } - pos = info_after - - ::continue:: - end - return out -end - -- ════════════════════════════════════════════════════════════════════════════ -- Check #3: ABI handoff discipline -- ════════════════════════════════════════════════════════════════════════════ @@ -1275,13 +1084,9 @@ function M.run(ctx) -- Aggregate per-DIRECTORY (per-module). One static_analysis.txt per source-directory, emitted only if the directory contains at least one atom. -- Empty-source directories (e.g. duffle headers with no atoms) produce no report. -- - -- Group sources by `src.dir`. The first component of `dir` is the module name (e.g. "code/duffle" -> "duffle", "code/gte_hello" -> "gte_hello"). + -- Group sources by `src.dir`. The first component of `dir` is the module name (e.g. "code/duffle" -> "duffle", "code/gte_hello" -> "gte_hello"). -- Output path is `/.static_analysis.txt`. - local by_dir = {} - for _, src in ipairs(ctx.sources) do - by_dir[src.dir] = by_dir[src.dir] or {} - table.insert(by_dir[src.dir], src) - end + local by_dir = duffle.group_sources_by_dir(ctx.sources) for dir, dir_sources in pairs(by_dir) do -- Run validate() against every source in this directory; accumulate atoms / findings / errors / warnings. diff --git a/scripts/passes/word_count_eval.lua b/scripts/passes/word_count_eval.lua index baead0c..c40a10e 100644 --- a/scripts/passes/word_count_eval.lua +++ b/scripts/passes/word_count_eval.lua @@ -20,10 +20,10 @@ -- Bootstrap: see `ps1_meta.lua` for the rationale. -- Bootstrap: load `scripts/duffle_paths.lua` (sets package.path + package.cpath). -- Uses `debug.getinfo` to find this file's own directory, so it works both standalone and when require'd from the orchestrator. -local _src = debug.getinfo(1, "S").source:sub(2) -local _dir = _src:match("(.*[/\\])") or "./" -dofile(_dir .. "../duffle_paths.lua") -local duffle = require("duffle") +-- Bootstrap: load `duffle_paths.lua` via `debug.getinfo(1, "S").source` (works both standalone + when require'd). +-- duffle_paths.lua sets package.path then returns `require("duffle")` at the bottom, so the dofile value IS the duffle module. +local _bootstrap_dir = debug.getinfo(1, "S").source:match("^@?(.*[/\\])") or "./" +local duffle = dofile(_bootstrap_dir .. "../duffle_paths.lua") -- ════════════════════════════════════════════════════════════════════════════ -- Constants diff --git a/scripts/ps1_meta.lua b/scripts/ps1_meta.lua index f7befc3..26cefcf 100644 --- a/scripts/ps1_meta.lua +++ b/scripts/ps1_meta.lua @@ -19,13 +19,10 @@ -- 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: load `duffle_paths.lua` (uses `git rev-parse` to find the repo root, then sets package.path + package.cpath). --- After this line, `require("duffle")` and `require("passes.X")` both resolve. -dofile((arg[0]:match("(.*[/\\])") or "./") .. "duffle_paths.lua") -local duffle = require("duffle") +-- Bootstrap: load `duffle_paths.lua` via `arg[0]` (this script's own path). +-- That single statement: (a) sets `package.path` + `package.cpath` (via cached `git rev-parse`), +-- (b) at the bottom returns `require("duffle")`. So the dofile's return value is the duffle module. +local duffle = dofile((arg[0]:match("(.*[/\\])") or "./") .. "duffle_paths.lua") -- ════════════════════════════════════════════════════════════════════════════ -- Constants