mirror of
https://github.com/Ed94/pikuma_ps1.git
synced 2026-08-25 02:20:33 +00:00
utilizing trailing type annotations more
This commit is contained in:
+93
-185
@@ -19,10 +19,8 @@
|
||||
-- fall back to `debug.getinfo(1, "S").source` when this file is being dofile()'d or require()'d (in which case `arg[0]` is the *caller's* path).
|
||||
-- That single statement: (a) sets `package.path` + `package.cpath`, (b) at the bottom returns `require("duffle")`.
|
||||
-- So the dofile's return value is the duffle module.
|
||||
--- @type boolean
|
||||
local _is_entry_script = arg and arg[0] and arg[0]:match("ps1_meta%.lua$") ~= nil
|
||||
--- @type string
|
||||
local _bootstrap_src
|
||||
local _is_entry_script = arg and arg[0] and arg[0]:match("ps1_meta%.lua$") ~= nil ---@type boolean
|
||||
local _bootstrap_src ---@type string
|
||||
if _is_entry_script then
|
||||
_bootstrap_src = arg[0]
|
||||
else
|
||||
@@ -30,33 +28,26 @@ else
|
||||
-- strip the leading "@" so the directory match works in both cases.
|
||||
_bootstrap_src = debug.getinfo(1, "S").source:sub(2)
|
||||
end
|
||||
--- @type DuffleExport
|
||||
local duffle = dofile((_bootstrap_src:match("(.*[/\\])") or "./") .. "duffle_paths.lua")
|
||||
local duffle = dofile((_bootstrap_src:match("(.*[/\\])") or "./") .. "duffle_paths.lua") ---@type DuffleExport
|
||||
|
||||
-- ════════════════════════════════════════════════════════════════════════════
|
||||
-- Constants
|
||||
-- ════════════════════════════════════════════════════════════════════════════
|
||||
|
||||
-- Exit codes (per the --help text and the post-build summary convention).
|
||||
--- @type integer
|
||||
local EXIT_OK = 0
|
||||
--- @type integer
|
||||
local EXIT_VALIDATION_ERRORS = 1
|
||||
--- @type integer
|
||||
local EXIT_INTERNAL_ERROR = 2
|
||||
local EXIT_OK = 0 ---@type integer
|
||||
local EXIT_VALIDATION_ERRORS = 1 ---@type integer
|
||||
local EXIT_INTERNAL_ERROR = 2 ---@type integer
|
||||
|
||||
-- Default --out-root value if not provided.
|
||||
--- @type string
|
||||
local DEFAULT_OUT_ROOT = "build/gen"
|
||||
local DEFAULT_OUT_ROOT = "build/gen" ---@type string
|
||||
|
||||
-- Sentinel for "all passes" in `PASS_FLAG_TO_NAME`. Distinguishes `--all` from the per-pass flags (which map to individual pass names).
|
||||
--- @type string
|
||||
local ALL_PASSES_SENTINEL = "__all__"
|
||||
local ALL_PASSES_SENTINEL = "__all__" ---@type string
|
||||
|
||||
-- Sentinel key for the pass-flag dispatcher in `FLAG_HANDLERS`.
|
||||
-- The actual pass names are looked up via `PASS_FLAG_TO_NAME`, not direct dispatch, so this key never matches a real flag.
|
||||
--- @type string
|
||||
local PASS_FLAG_DISPATCH_KEY = "__pass__"
|
||||
local PASS_FLAG_DISPATCH_KEY = "__pass__" ---@type string
|
||||
|
||||
-- ════════════════════════════════════════════════════════════════════════════
|
||||
-- Type declarations
|
||||
@@ -159,8 +150,7 @@ local PASS_FLAG_DISPATCH_KEY = "__pass__"
|
||||
-- A row without a `groups` entry is dependency-only: it runs only when a transitive dep requests it,
|
||||
-- but it remains directly requestable through its explicit CLI flag (e.g. --atoms-source-map, --scan-source).
|
||||
|
||||
--- @type table<string, PassDescriptor>
|
||||
local PASSES = {
|
||||
local PASSES = { ---@type table<string, PassDescriptor>
|
||||
["scan-source"] = {
|
||||
module = "passes.scan_source",
|
||||
kind = "shared", deps = {},
|
||||
@@ -230,13 +220,10 @@ local PASSES = {
|
||||
--- @param group_name string -- Build-phase group ("pre-link" | "post-link")
|
||||
--- @return string[] -- Sorted root pass names belonging to that group
|
||||
local function roots_for_group(group_name)
|
||||
--- @type string[]
|
||||
local names = {}
|
||||
--- @type string, PassDescriptor
|
||||
for name, pass in pairs(PASSES) do
|
||||
local names = {} ---@type string[]
|
||||
for name, pass in pairs(PASSES) do ---@type string, PassDescriptor
|
||||
if pass.groups then
|
||||
--- @type integer, string
|
||||
for _, g in ipairs(pass.groups) do
|
||||
for _, g in ipairs(pass.groups) do ---@type integer, string
|
||||
if g == group_name then
|
||||
names[#names + 1] = name
|
||||
break
|
||||
@@ -251,18 +238,16 @@ end
|
||||
--- Append every root belonging to `group_name` to `args.requested_set`.
|
||||
--- Errors loudly if no PASSES row declares the group, so a typo'd or future-removed group name
|
||||
--- cannot silently fall through to pre-link (or any other default) and dispatch nothing.
|
||||
--- @param args ParsedArgs
|
||||
--- @param args ParsedArgs
|
||||
--- @param group_name string
|
||||
--- @return nil
|
||||
local function request_roots_for_group(args, group_name)
|
||||
--- @type string[]
|
||||
local roots = roots_for_group(group_name)
|
||||
local roots = roots_for_group(group_name) ---@type string[]
|
||||
if #roots == 0 then
|
||||
error(string.format("ps1_meta: build-phase group %q has zero roots in PASSES; check PASSES rows for a `groups = { %q }` field"
|
||||
, group_name, group_name))
|
||||
end
|
||||
--- @type integer, string
|
||||
for _, name in ipairs(roots) do
|
||||
for _, name in ipairs(roots) do ---@type integer, string
|
||||
args.requested_set[#args.requested_set + 1] = name
|
||||
end
|
||||
end
|
||||
@@ -270,8 +255,7 @@ end
|
||||
-- Pass-kind taxonomy: findings always print. No pass kind stops the build.
|
||||
-- Report severity is independent from process exit policy.
|
||||
-- Adding a new pass kind requires listing it here explicitly; an unknown kind must not silently fall back to "true".
|
||||
--- @type table<string, boolean> -- bag: pass kind -> stop-on-error
|
||||
local PASS_KIND_STOP_ON_ERROR = {
|
||||
local PASS_KIND_STOP_ON_ERROR = { ---@type table<string, boolean> -- bag: pass kind -> stop-on-error
|
||||
["shared"] = false,
|
||||
["header-output"] = false,
|
||||
["validation"] = false,
|
||||
@@ -283,8 +267,7 @@ local PASS_KIND_STOP_ON_ERROR = {
|
||||
-- Per-pass flags (e.g. --word-counts); phase flags (--pre-link, --post-link, --all) are within FLAG_HANDLERS because they own side effects or invoke group-derivation logic.
|
||||
-- dwarf-injection is *also* a per-pass opt-in flag, but its selection + opt-in state are both owned by the explicit FLAG_HANDLERS entry below
|
||||
-- (it sets args.flags.dwarf_injection and appends "dwarf-injection" to requested_set), so it is intentionally absent from this table.
|
||||
--- @type table<string, string> -- bag: CLI flag -> pass name or ALL_PASSES_SENTINEL
|
||||
local PASS_FLAG_TO_NAME = {
|
||||
local PASS_FLAG_TO_NAME = { ---@type table<string, string> -- bag: CLI flag -> pass name or ALL_PASSES_SENTINEL
|
||||
["--word-counts"] = "word-counts",
|
||||
["--components"] = "components",
|
||||
["--validate"] = "annotation",
|
||||
@@ -301,21 +284,17 @@ local PASS_FLAG_TO_NAME = {
|
||||
--- @param args ParsedArgs
|
||||
--- @return nil
|
||||
local function request_all_passes(args)
|
||||
--- @type string[]
|
||||
local names = {}
|
||||
--- @type string
|
||||
for name in pairs(PASSES) do names[#names + 1] = name end
|
||||
local names = {} ---@type string[]
|
||||
for name in pairs(PASSES) do names[#names + 1] = name end ---@type string
|
||||
table.sort(names)
|
||||
--- @type integer, string
|
||||
for _, n in ipairs(names) do
|
||||
for _, n in ipairs(names) do ---@type integer, string
|
||||
args.requested_set[#args.requested_set + 1] = n
|
||||
end
|
||||
end
|
||||
|
||||
-- Per-flag handlers. Each handler takes (args, argv, arg_idx) and returns the new arg_idx (so multi-arg flags like --source FILE advance it).
|
||||
-- Returning nil + os.exit() handles termination flags (--help).
|
||||
--- @type table<string, FlagHandler>
|
||||
local FLAG_HANDLERS = {}
|
||||
local FLAG_HANDLERS = {} ---@type table<string, FlagHandler>
|
||||
|
||||
-- ════════════════════════════════════════════════════════════════════════════
|
||||
-- CLI parsing
|
||||
@@ -372,8 +351,7 @@ EXAMPLES:
|
||||
]])
|
||||
end
|
||||
|
||||
--- @type table<string, string> -- bag: flag -> value metavar
|
||||
local FLAG_VALUE_NAMES = {
|
||||
local FLAG_VALUE_NAMES = { ---@type table<string, string> -- bag: flag -> value metavar
|
||||
["--source"] = "FILE",
|
||||
["--unity-root"] = "FILE",
|
||||
["--metadata"] = "PATH",
|
||||
@@ -388,10 +366,8 @@ local FLAG_VALUE_NAMES = {
|
||||
--- @return string
|
||||
--- @return integer
|
||||
local function require_flag_value(argv, arg_idx, flag)
|
||||
--- @type string|nil
|
||||
local value = argv[arg_idx + 1]
|
||||
--- @type boolean
|
||||
local next_known = type(value) == "string"
|
||||
local value = argv[arg_idx + 1] ---@type string|nil
|
||||
local next_known = type(value) == "string" ---@type boolean
|
||||
and (FLAG_HANDLERS[value] ~= nil or PASS_FLAG_TO_NAME[value] ~= nil)
|
||||
if value == nil or next_known then
|
||||
io.stderr:write("ps1_meta: " .. flag .. " requires " .. FLAG_VALUE_NAMES[flag] .. "\n")
|
||||
@@ -417,8 +393,7 @@ FLAG_HANDLERS["--verbose"] = function(args) args.verbose = true end
|
||||
--- @param arg_idx integer
|
||||
--- @return integer
|
||||
FLAG_HANDLERS["--source"] = function(args, argv, arg_idx)
|
||||
--- @type string, integer
|
||||
local value, value_idx = require_flag_value(argv, arg_idx, "--source")
|
||||
local value, value_idx = require_flag_value(argv, arg_idx, "--source") ---@type string, integer
|
||||
args.sources[#args.sources + 1] = value
|
||||
return value_idx
|
||||
end
|
||||
@@ -427,8 +402,7 @@ end
|
||||
--- @param arg_idx integer
|
||||
--- @return integer
|
||||
FLAG_HANDLERS["--unity-root"] = function(args, argv, arg_idx)
|
||||
--- @type string, integer
|
||||
local value, value_idx = require_flag_value(argv, arg_idx, "--unity-root")
|
||||
local value, value_idx = require_flag_value(argv, arg_idx, "--unity-root") ---@type string, integer
|
||||
args.unity_root = value
|
||||
return value_idx
|
||||
end
|
||||
@@ -437,8 +411,7 @@ end
|
||||
--- @param arg_idx integer
|
||||
--- @return integer
|
||||
FLAG_HANDLERS["--metadata"] = function(args, argv, arg_idx)
|
||||
--- @type string, integer
|
||||
local value, value_idx = require_flag_value(argv, arg_idx, "--metadata")
|
||||
local value, value_idx = require_flag_value(argv, arg_idx, "--metadata") ---@type string, integer
|
||||
args.metadata = value
|
||||
return value_idx
|
||||
end
|
||||
@@ -447,8 +420,7 @@ end
|
||||
--- @param arg_idx integer
|
||||
--- @return integer
|
||||
FLAG_HANDLERS["--out-root"] = function(args, argv, arg_idx)
|
||||
--- @type string, integer
|
||||
local value, value_idx = require_flag_value(argv, arg_idx, "--out-root")
|
||||
local value, value_idx = require_flag_value(argv, arg_idx, "--out-root") ---@type string, integer
|
||||
args.out_root = value
|
||||
return value_idx
|
||||
end
|
||||
@@ -457,8 +429,7 @@ end
|
||||
--- @param arg_idx integer
|
||||
--- @return integer
|
||||
FLAG_HANDLERS["--project-root"] = function(args, argv, arg_idx)
|
||||
--- @type string, integer
|
||||
local value, value_idx = require_flag_value(argv, arg_idx, "--project-root")
|
||||
local value, value_idx = require_flag_value(argv, arg_idx, "--project-root") ---@type string, integer
|
||||
args.project_root = value
|
||||
return value_idx
|
||||
end
|
||||
@@ -476,8 +447,7 @@ end
|
||||
--- @param arg_idx integer
|
||||
--- @return integer
|
||||
FLAG_HANDLERS["--elf"] = function(args, argv, arg_idx)
|
||||
--- @type string, integer
|
||||
local value, value_idx = require_flag_value(argv, arg_idx, "--elf")
|
||||
local value, value_idx = require_flag_value(argv, arg_idx, "--elf") ---@type string, integer
|
||||
args.flags = args.flags or {}
|
||||
args.flags.elf_path = value
|
||||
return value_idx
|
||||
@@ -517,8 +487,7 @@ end
|
||||
--- @param a string
|
||||
--- @return nil
|
||||
FLAG_HANDLERS[PASS_FLAG_DISPATCH_KEY] = function(args, a)
|
||||
--- @type string|nil
|
||||
local name = PASS_FLAG_TO_NAME[a]
|
||||
local name = PASS_FLAG_TO_NAME[a] ---@type string|nil
|
||||
if name == ALL_PASSES_SENTINEL then
|
||||
request_all_passes(args)
|
||||
return
|
||||
@@ -530,8 +499,7 @@ end
|
||||
--- @param argv string[]
|
||||
--- @return ParsedArgs
|
||||
local function parse_args(argv)
|
||||
--- @type ParsedArgs
|
||||
local args = {
|
||||
local args = { ---@type ParsedArgs
|
||||
requested_set = {},
|
||||
sources = {},
|
||||
unity_root = nil,
|
||||
@@ -541,13 +509,10 @@ local function parse_args(argv)
|
||||
verbose = false,
|
||||
}
|
||||
|
||||
--- @type integer
|
||||
local pos = 1
|
||||
local pos = 1 ---@type integer
|
||||
while pos <= #argv do
|
||||
--- @type string
|
||||
local a = argv[pos]
|
||||
--- @type FlagHandler|nil
|
||||
local handler = FLAG_HANDLERS[a]
|
||||
local a = argv[pos] ---@type string
|
||||
local handler = FLAG_HANDLERS[a] ---@type FlagHandler|nil
|
||||
if handler then
|
||||
pos = handler(args, argv, pos) or pos
|
||||
elseif PASS_FLAG_TO_NAME[a] then
|
||||
@@ -572,17 +537,14 @@ local function parse_args(argv)
|
||||
-- `<repo>/code/duffle/word_count.metadata.h` is the canonical metadata location.
|
||||
-- `project_root` names `<repo>`; the resolver derives `<project_root>/code` separately.
|
||||
if not args.project_root then
|
||||
--- @type string
|
||||
local metadata_dir = duffle.dirname(duffle.normalize_path(args.metadata))
|
||||
--- @type string
|
||||
local code_root = duffle.dirname(metadata_dir)
|
||||
local metadata_dir = duffle.dirname(duffle.normalize_path(args.metadata)) ---@type string
|
||||
local code_root = duffle.dirname(metadata_dir) ---@type string
|
||||
args.project_root = duffle.dirname(code_root)
|
||||
else
|
||||
args.project_root = duffle.normalize_path(args.project_root)
|
||||
end
|
||||
|
||||
--- @type boolean
|
||||
local has_unity = type(args.unity_root) == "string" and args.unity_root ~= ""
|
||||
local has_unity = type(args.unity_root) == "string" and args.unity_root ~= "" ---@type boolean
|
||||
if has_unity and #args.sources > 0 then
|
||||
io.stderr:write("ps1_meta: --unity-root FILE and --source FILE are mutually exclusive\n")
|
||||
os.exit(EXIT_INTERNAL_ERROR)
|
||||
@@ -595,14 +557,10 @@ local function parse_args(argv)
|
||||
-- Post-link opt-ins (--gdb-runtime, --dwarf-injection) write output that depends on the linked ELF.
|
||||
-- Without --elf the metaprogram can't satisfy those requests, so refuse loud and early.
|
||||
-- This covers the explicit --post-link batch, --dwarf-injection by itself, and --gdb-runtime by itself.
|
||||
--- @type PassFlags
|
||||
local flags = args.flags or {}
|
||||
--- @type string|nil
|
||||
local elf_path = flags.elf_path
|
||||
--- @type boolean
|
||||
local has_elf = type(elf_path) == "string" and #elf_path > 0
|
||||
--- @type boolean
|
||||
local post_links = flags.gdb_runtime or flags.dwarf_injection
|
||||
local flags = args.flags or {} ---@type PassFlags
|
||||
local elf_path = flags.elf_path ---@type string|nil
|
||||
local has_elf = type(elf_path) == "string" and #elf_path > 0 ---@type boolean
|
||||
local post_links = flags.gdb_runtime or flags.dwarf_injection ---@type boolean
|
||||
if post_links and not has_elf then
|
||||
io.stderr:write("ps1_meta: --elf PATH is required for post-link output\n")
|
||||
os.exit(EXIT_INTERNAL_ERROR)
|
||||
@@ -621,12 +579,9 @@ end
|
||||
--- @param args ParsedArgs
|
||||
--- @return PassCtx
|
||||
local function build_ctx(args)
|
||||
--- @type string
|
||||
local normalized_project_root = duffle.normalize_path(args.project_root)
|
||||
--- @type string
|
||||
local project_root = normalized_project_root
|
||||
--- @type boolean
|
||||
local project_root_is_absolute = normalized_project_root:match("^%a:/")
|
||||
local normalized_project_root = duffle.normalize_path(args.project_root) ---@type string
|
||||
local project_root = normalized_project_root ---@type string
|
||||
local project_root_is_absolute = normalized_project_root:match("^%a:/") ---@type boolean
|
||||
or normalized_project_root:sub(1, 2) == "//"
|
||||
or normalized_project_root:sub(1, 1) == "/"
|
||||
if not project_root_is_absolute then
|
||||
@@ -637,11 +592,9 @@ local function build_ctx(args)
|
||||
-- Do not route POSIX/UNC/drive-absolute paths through to_absolute_path.
|
||||
duffle.canonical_path_key(project_root)
|
||||
end
|
||||
--- @type Corpus
|
||||
local resolution
|
||||
local resolution ---@type Corpus
|
||||
if args.unity_root then
|
||||
--- @type boolean, Corpus|string
|
||||
local ok_resolve, resolved = pcall(duffle.resolve_source_corpus, {
|
||||
local ok_resolve, resolved = pcall(duffle.resolve_source_corpus, { ---@type boolean, Corpus|string
|
||||
unity_root = args.unity_root,
|
||||
project_root = project_root,
|
||||
})
|
||||
@@ -651,45 +604,35 @@ local function build_ctx(args)
|
||||
end
|
||||
resolution = resolved
|
||||
else
|
||||
--- @type SourceFile[]
|
||||
local source_order = {}
|
||||
--- @type table<Path, SourceFile>
|
||||
local sources_by_path = {}
|
||||
--- @type SourceResolver
|
||||
local resolver = {
|
||||
local source_order = {} ---@type SourceFile[]
|
||||
local sources_by_path = {} ---@type table<Path, SourceFile>
|
||||
local resolver = { ---@type SourceResolver
|
||||
resolved = {},
|
||||
skipped = {},
|
||||
shadowed = {},
|
||||
}
|
||||
--- @type integer, string
|
||||
for _, input_path in ipairs(args.sources) do
|
||||
--- @type string
|
||||
local path = duffle.normalize_path(input_path)
|
||||
--- @type boolean, string
|
||||
local key_ok, key_or_error = pcall(duffle.canonical_path_key, path)
|
||||
for _, input_path in ipairs(args.sources) do ---@type integer, string
|
||||
local path = duffle.normalize_path(input_path) ---@type string
|
||||
local key_ok, key_or_error = pcall(duffle.canonical_path_key, path) ---@type boolean, string
|
||||
if not key_ok then
|
||||
error("ps1_meta: invalid --source " .. input_path .. ": " .. tostring(key_or_error), 0)
|
||||
end
|
||||
--- @type file*|nil
|
||||
local file = io.open(path, "r")
|
||||
local file = io.open(path, "r") ---@type file*|nil
|
||||
if not file then
|
||||
io.stderr:write("ps1_meta: cannot open --source " .. input_path .. "\n")
|
||||
os.exit(EXIT_INTERNAL_ERROR)
|
||||
end
|
||||
--- @type string
|
||||
local text = file:read("*a")
|
||||
local text = file:read("*a") ---@type string
|
||||
file:close()
|
||||
|
||||
--- @type SourceFile
|
||||
local source = {
|
||||
local source = { ---@type SourceFile
|
||||
path = path,
|
||||
text = text,
|
||||
dir = duffle.dirname(path),
|
||||
basename = duffle.basename_no_ext(path),
|
||||
}
|
||||
source_order[#source_order + 1] = source
|
||||
--- @type string
|
||||
local key = key_or_error
|
||||
local key = key_or_error ---@type string
|
||||
if not sources_by_path[key] then sources_by_path[key] = source end
|
||||
resolver.resolved[#resolver.resolved + 1] = {
|
||||
include_path = path,
|
||||
@@ -713,8 +656,7 @@ local function build_ctx(args)
|
||||
}
|
||||
end
|
||||
|
||||
--- @type Corpus
|
||||
local corpus = {
|
||||
local corpus = { ---@type Corpus
|
||||
unity_root = resolution.unity_root,
|
||||
project_root = resolution.project_root,
|
||||
code_root = resolution.code_root,
|
||||
@@ -735,8 +677,7 @@ local function build_ctx(args)
|
||||
collisions = {},
|
||||
resolver = resolution.resolver,
|
||||
}
|
||||
--- @type PassCtx
|
||||
local ctx = {
|
||||
local ctx = { ---@type PassCtx
|
||||
metadata_path = args.metadata,
|
||||
shared = { corpus = corpus },
|
||||
out_root = args.out_root,
|
||||
@@ -765,21 +706,15 @@ end
|
||||
--- Keeping these blocks local makes the topological sort self-contained.
|
||||
local function topo_sort(passes, requested_set)
|
||||
-- Dependency closure: include every pass transitively required by `requested_set`.
|
||||
--- @type table<string, boolean> -- bag: pass name -> needed
|
||||
local needed = {}
|
||||
--- @type integer, string
|
||||
for _, name in ipairs(requested_set) do needed[name] = true end
|
||||
--- @type boolean
|
||||
local changed = true
|
||||
local needed = {} ---@type table<string, boolean> -- bag: pass name -> needed
|
||||
for _, name in ipairs(requested_set) do needed[name] = true end ---@type integer, string
|
||||
local changed = true ---@type boolean
|
||||
while changed do
|
||||
changed = false
|
||||
--- @type string, boolean
|
||||
for name, _ in pairs(needed) do
|
||||
--- @type PassDescriptor
|
||||
local pass = passes[name]
|
||||
for name, _ in pairs(needed) do ---@type string, boolean
|
||||
local pass = passes[name] ---@type PassDescriptor
|
||||
if not pass then error("unknown pass '" .. name .. "' requested") end
|
||||
--- @type integer, string
|
||||
for _, dep in ipairs(pass.deps) do
|
||||
for _, dep in ipairs(pass.deps) do ---@type integer, string
|
||||
if not needed[dep] then
|
||||
needed[dep] = true
|
||||
changed = true
|
||||
@@ -789,14 +724,10 @@ local function topo_sort(passes, requested_set)
|
||||
end
|
||||
|
||||
-- In-degree calculation: count each needed pass's needed dependencies.
|
||||
--- @type table<string, integer> -- bag: pass name -> in-degree
|
||||
local in_degree = {}
|
||||
--- @type string, boolean
|
||||
for name, _ in pairs(needed) do in_degree[name] = 0 end
|
||||
--- @type string, boolean
|
||||
for name, _ in pairs(needed) do
|
||||
--- @type integer, string
|
||||
for _, dep in ipairs(passes[name].deps) do
|
||||
local in_degree = {} ---@type table<string, integer> -- bag: pass name -> in-degree
|
||||
for name, _ in pairs(needed) do in_degree[name] = 0 end ---@type string, boolean
|
||||
for name, _ in pairs(needed) do ---@type string, boolean
|
||||
for _, dep in ipairs(passes[name].deps) do ---@type integer, string
|
||||
if needed[dep] then
|
||||
in_degree[name] = in_degree[name] + 1
|
||||
end
|
||||
@@ -804,27 +735,21 @@ local function topo_sort(passes, requested_set)
|
||||
end
|
||||
|
||||
-- Ready-queue seeding: add zero-in-degree passes in deterministic order.
|
||||
--- @type string[]
|
||||
local ready = {}
|
||||
--- @type string, integer
|
||||
for name, deg in pairs(in_degree) do
|
||||
local ready = {} ---@type string[]
|
||||
for name, deg in pairs(in_degree) do ---@type string, integer
|
||||
if deg == 0 then ready[#ready + 1] = name end
|
||||
end
|
||||
table.sort(ready)
|
||||
|
||||
-- Ready-queue drain: decrement dependents when each pass is emitted.
|
||||
-- Newly-zero-degree passes are inserted back into the ready queue (kept sorted).
|
||||
--- @type string[]
|
||||
local order = {}
|
||||
local order = {} ---@type string[]
|
||||
while #ready > 0 do
|
||||
--- @type string
|
||||
local just_finished = table.remove(ready, 1)
|
||||
local just_finished = table.remove(ready, 1) ---@type string
|
||||
order[#order + 1] = just_finished
|
||||
--- @type string, boolean
|
||||
for name, _ in pairs(needed) do
|
||||
for name, _ in pairs(needed) do ---@type string, boolean
|
||||
if name ~= just_finished then
|
||||
--- @type integer, string
|
||||
for _, dep in ipairs(passes[name].deps) do
|
||||
for _, dep in ipairs(passes[name].deps) do ---@type integer, string
|
||||
if dep == just_finished then
|
||||
in_degree[name] = in_degree[name] - 1
|
||||
if in_degree[name] == 0 then
|
||||
@@ -840,13 +765,10 @@ local function topo_sort(passes, requested_set)
|
||||
-- Cycle detection: if `order` doesn't include all needed passes, some are stuck with in_degree > 0
|
||||
-- (the cycle closed on itself before Kahn could process them).
|
||||
-- Without this check, a fully-closed cycle (e.g. A -> B -> A) would silently return an empty order list, leaving the orchestrator to dispatch nothing.
|
||||
--- @type integer
|
||||
local needed_count = 0
|
||||
--- @type string
|
||||
for _ in pairs(needed) do needed_count = needed_count + 1 end -- count hash entries; Lua's #t doesn't work
|
||||
local needed_count = 0 ---@type integer
|
||||
for _ in pairs(needed) do needed_count = needed_count + 1 end ---@type string -- count hash entries; Lua's #t doesn't work
|
||||
if #order ~= needed_count then
|
||||
--- @type string, integer
|
||||
for name, deg in pairs(in_degree) do
|
||||
for name, deg in pairs(in_degree) do ---@type string, integer
|
||||
if deg > 0 then
|
||||
error("dependency cycle detected involving pass '" .. name .. "'")
|
||||
end
|
||||
@@ -867,11 +789,9 @@ end
|
||||
--- @param result PassResult
|
||||
--- @return boolean
|
||||
local function report_validation_errors(pass_name, pass, result)
|
||||
--- @type boolean
|
||||
local has_errors = result.errors and #result.errors > 0
|
||||
local has_errors = result.errors and #result.errors > 0 ---@type boolean
|
||||
if not has_errors then return false end
|
||||
--- @type integer, PassFinding
|
||||
for _, e in ipairs(result.errors) do
|
||||
for _, e in ipairs(result.errors) do ---@type integer, PassFinding
|
||||
io.stderr:write(string.format("[%s] line %d: %s\n", pass_name, e.line or 0, e.msg or ""))
|
||||
end
|
||||
return PASS_KIND_STOP_ON_ERROR[pass.kind] == true
|
||||
@@ -882,16 +802,11 @@ end
|
||||
--- @param order string[]
|
||||
--- @return boolean -- true if any validation errors were reported
|
||||
local function dispatch_passes(ctx, order)
|
||||
--- @type boolean
|
||||
local had_errors = false
|
||||
--- @type integer, string
|
||||
for _, pass_name in ipairs(order) do
|
||||
--- @type PassDescriptor
|
||||
local pass = PASSES[pass_name]
|
||||
--- @type PassModule
|
||||
local mod = require(pass.module)
|
||||
--- @type PassResult
|
||||
local result = mod.run(ctx)
|
||||
local had_errors = false ---@type boolean
|
||||
for _, pass_name in ipairs(order) do ---@type integer, string
|
||||
local pass = PASSES[pass_name] ---@type PassDescriptor
|
||||
local mod = require(pass.module) ---@type PassModule
|
||||
local result = mod.run(ctx) ---@type PassResult
|
||||
if report_validation_errors(pass_name, pass, result) then
|
||||
had_errors = true
|
||||
end
|
||||
@@ -903,20 +818,14 @@ end
|
||||
--- @param argv string[]
|
||||
--- @return nil
|
||||
local function main(argv)
|
||||
--- @type boolean, string|nil
|
||||
local ok, err = pcall(function()
|
||||
--- @type ParsedArgs
|
||||
local args = parse_args(argv)
|
||||
--- @type PassCtx
|
||||
local ctx = build_ctx(args)
|
||||
local ok, err = pcall(function() ---@type boolean, string|nil
|
||||
local args = parse_args(argv) ---@type ParsedArgs
|
||||
local ctx = build_ctx(args) ---@type PassCtx
|
||||
|
||||
--- @type string[]
|
||||
local requested = args.requested_set
|
||||
--- @type string[]
|
||||
local closed = topo_sort(PASSES, requested)
|
||||
local requested = args.requested_set ---@type string[]
|
||||
local closed = topo_sort(PASSES, requested) ---@type string[]
|
||||
|
||||
--- @type boolean
|
||||
local had_errors = dispatch_passes(ctx, closed)
|
||||
local had_errors = dispatch_passes(ctx, closed) ---@type boolean
|
||||
if had_errors then os.exit(EXIT_VALIDATION_ERRORS) end
|
||||
end)
|
||||
|
||||
@@ -931,8 +840,7 @@ end
|
||||
-- Module export for in-process consumers (tests that dofile this script).
|
||||
-- The conditional `main(...)` call below only fires when this file is invoked as the entry script (arg[0] ends in "ps1_meta.lua");
|
||||
-- in dofile() mode (test's arg[0] does not match), main() is skipped and the chunk returns `_M` to the caller.
|
||||
--- @type Ps1MetaMod
|
||||
local _M = {
|
||||
local _M = { ---@type Ps1MetaMod
|
||||
PASSES = PASSES,
|
||||
PASS_KIND_STOP_ON_ERROR = PASS_KIND_STOP_ON_ERROR,
|
||||
parse_args = parse_args,
|
||||
|
||||
Reference in New Issue
Block a user