mirror of
https://github.com/Ed94/pikuma_ps1.git
synced 2026-08-04 22:58:47 +00:00
Better static analysis for C0 <-> C2 data race hazards.
This commit is contained in:
+62
-58
@@ -81,19 +81,6 @@ $path_psyq = join-path $path_toolchain 'psyq-4_7'
|
||||
$path_psyq_iwyu = join-path $path_toolchain 'psyq_iwyu'
|
||||
$path_psyq_imyu_inc = join-path $path_psyq_iwyu 'include'
|
||||
|
||||
function Get-SourceFiles { param([Parameter(Mandatory=$true)] [string[]]$paths, [Parameter(Mandatory=$true)] [string[]]$extensions)
|
||||
$files = @()
|
||||
foreach ($p in $paths) {
|
||||
if (-not (test-path $p)) { continue }
|
||||
foreach ($ext in $extensions) {
|
||||
Get-ChildItem -Path $p -File -Recurse -Filter "*$ext" -ErrorAction SilentlyContinue | ForEach-Object {
|
||||
$files += $_.FullName
|
||||
}
|
||||
}
|
||||
}
|
||||
return ($files | Sort-Object -Unique)
|
||||
}
|
||||
|
||||
function assemble-unit { param(
|
||||
[string] $unit,
|
||||
[string] $link_module,
|
||||
@@ -243,6 +230,52 @@ function make-binary { param([string]$elf, [string]$exe)
|
||||
if ($LASTEXITCODE -ne 0) { Write-Error "Objcopy failed. Aborting."; exit 1 }
|
||||
}
|
||||
|
||||
function ps1-meta { param(
|
||||
[string]$unity_root,
|
||||
[string[]]$sources,
|
||||
[Parameter(Mandatory=$true)][string]$metadata,
|
||||
[string]$out_root = (join-path $path_build 'gen'),
|
||||
[string[]]$passes = @('--pre-link'),
|
||||
[string[]]$extra_args = @()
|
||||
)
|
||||
# `--unity-root` and `--source` are
|
||||
# mutually exclusive. Exactly one of `$unity_root` / `$sources` must
|
||||
# be supplied; the other must be absent.
|
||||
if ($null -ne $unity_root -and $unity_root -ne '')
|
||||
{
|
||||
if ($null -ne $sources -and $sources.Count -gt 0) {
|
||||
write-error 'ps1-meta: -unity_root and -sources are mutually exclusive'
|
||||
exit 2
|
||||
}
|
||||
}
|
||||
elseif ($null -eq $sources -or $sources.Count -eq 0) {
|
||||
write-error 'ps1-meta: either -unity_root <file> or -sources <file...> is required'
|
||||
exit 2
|
||||
}
|
||||
|
||||
$script = join-path $path_scripts 'ps1_meta.lua'
|
||||
$input_summary = if ($null -ne $unity_root -and $unity_root -ne '') {
|
||||
"unity=$unity_root"
|
||||
}
|
||||
else {
|
||||
"$($sources.Count) source(s)"
|
||||
}
|
||||
write-host "ps1-meta $input_summary, passes=$($passes -join ',')" ` -ForegroundColor Magenta
|
||||
|
||||
$arg_list = @($passes) + @('--metadata', $metadata) + @('--out-root', $out_root) + @($extra_args)
|
||||
if ($null -ne $unity_root -and $unity_root -ne '') {
|
||||
$arg_list += @('--unity-root', $unity_root)
|
||||
}
|
||||
else {
|
||||
foreach ($s in $sources) { $arg_list += @('--source', $s) }
|
||||
}
|
||||
& luajit $script @arg_list
|
||||
if ($LASTEXITCODE -ne 0) {
|
||||
write-error "ps1-meta failed (exit $LASTEXITCODE). Aborting."
|
||||
exit $LASTEXITCODE
|
||||
}
|
||||
}
|
||||
|
||||
function build-hello_psyqo {
|
||||
$includes += @()
|
||||
|
||||
@@ -317,34 +350,16 @@ function build-graphis_hello {
|
||||
}
|
||||
# build-graphis_hello
|
||||
|
||||
function ps1-meta { param(
|
||||
[Parameter(Mandatory=$true)][string[]]$sources,
|
||||
[Parameter(Mandatory=$true)][string]$metadata,
|
||||
[string]$out_root = (join-path $path_build 'gen'),
|
||||
[string[]]$passes = @('--pre-link'),
|
||||
[string[]]$extra_args = @()
|
||||
)
|
||||
$script = join-path $path_scripts 'ps1_meta.lua'
|
||||
write-host "ps1-meta $($sources.Count) source(s), passes=$($passes -join ',')" ` -ForegroundColor Magenta
|
||||
$arg_list = @($passes) + @('--metadata', $metadata) + @('--out-root', $out_root) + @($extra_args)
|
||||
foreach ($s in $sources) { $arg_list += @('--source', $s) }
|
||||
& luajit $script @arg_list
|
||||
if ($LASTEXITCODE -ne 0) {
|
||||
write-error "ps1-meta failed (exit $LASTEXITCODE). Aborting."
|
||||
exit $LASTEXITCODE
|
||||
}
|
||||
}
|
||||
|
||||
function build-gte_hello {
|
||||
$includes += @()
|
||||
|
||||
$path_module = join-path $path_code 'gte_hello'
|
||||
$path_duffle = join-path $path_code 'duffle'
|
||||
$path_atom_metadata = join-path $path_duffle 'word_count.metadata.h'
|
||||
$path_build_gen = join-path $path_build 'gen'
|
||||
|
||||
$source_dirs = @($path_duffle, $path_module)
|
||||
$atom_sources = Get-SourceFiles -paths $source_dirs -extensions @('.h', '.c')
|
||||
ps1-meta -sources $atom_sources -metadata $path_atom_metadata -out_root (join-path $path_build 'gen')
|
||||
$src_c = join-path $path_module 'hello_gte.c'
|
||||
ps1-meta -unity_root $src_c -metadata $path_atom_metadata -out_root $path_build_gen
|
||||
|
||||
$assemble_args = @()
|
||||
$assemble_args += $f_debug
|
||||
@@ -360,7 +375,6 @@ function build-gte_hello {
|
||||
|
||||
# assemble-unit $src_asm $module_asm $includes $assemble_args
|
||||
|
||||
$src_c = join-path $path_module 'hello_gte.c'
|
||||
$module_c = join-path $path_build 'hello_gte_c.o'
|
||||
|
||||
$compile_args = @()
|
||||
@@ -386,27 +400,17 @@ function build-gte_hello {
|
||||
make-binary $elf $exe
|
||||
|
||||
# Post-link: gdb-runtime + dwarf-injection in a single Lua invocation (one luajit cold start).
|
||||
ps1-meta -sources $atom_sources -metadata $path_atom_metadata `
|
||||
-out_root (join-path $path_build 'gen') `
|
||||
-passes @('--post-link') `
|
||||
-extra_args @('--elf', $elf)
|
||||
ps1-meta -unity_root $src_c -metadata $path_atom_metadata -out_root $path_build_gen -passes @('--post-link') ` -extra_args @('--elf', $elf)
|
||||
|
||||
# F' + G' splice: collapse 9 objcopy subprocess invocations into 3.
|
||||
# - 1 call: 3x --update-section for F' (line / aranges / rnglists)
|
||||
# - 1 call: 3x --update-section for G' (info / abbrev / str)
|
||||
# - 1 call: 2x --add-section for G' (loc / loclists — these don't exist in the source ELF)
|
||||
# - 1 call: 1x --set-section-flags (.rodata / .data enable code flag)
|
||||
# = 4 objcopy calls (was 9; saved 5 spawns).
|
||||
$dwarfLineBin = join-path (join-path $path_build 'gen') 'hello_gte.dwarf_line.bin'
|
||||
$dwarfArangesBin = join-path (join-path $path_build 'gen') 'hello_gte.dwarf_aranges.bin'
|
||||
$dwarfRnglistsBin = join-path (join-path $path_build 'gen') 'hello_gte.dwarf_rnglists.bin'
|
||||
$dwarfLineBin = join-path $path_build_gen 'hello_gte.dwarf_line.bin'
|
||||
$dwarfArangesBin = join-path $path_build_gen 'hello_gte.dwarf_aranges.bin'
|
||||
$dwarfRnglistsBin = join-path $path_build_gen 'hello_gte.dwarf_rnglists.bin'
|
||||
$injectElf = join-path $path_build 'hello_gte.dwarf-injected.elf'
|
||||
if ((Test-Path $dwarfLineBin) -and (Test-Path $dwarfArangesBin) -and (Test-Path $dwarfRnglistsBin))
|
||||
{
|
||||
Write-Host "[build] DWARF-injecting $elf -> $injectElf"
|
||||
Copy-Item -LiteralPath $elf -Destination $injectElf -Force
|
||||
|
||||
# Single objcopy call: 3x --update-section for F' (line, aranges, rnglists).
|
||||
# Objcopy call: 3x --update-section for (line, aranges, rnglists).
|
||||
$f_args = @(
|
||||
"--update-section=.debug_line=$dwarfLineBin",
|
||||
"--update-section=.debug_aranges=$dwarfArangesBin",
|
||||
@@ -419,12 +423,11 @@ function build-gte_hello {
|
||||
return;
|
||||
}
|
||||
|
||||
# G' 5-section splice: 3 update-section (info / abbrev / str) + 2 add-section (loc / loclists).
|
||||
$dwarfInfoBin = join-path (join-path $path_build 'gen') 'hello_gte.dwarf_info.bin'
|
||||
$dwarfAbbrevBin = join-path (join-path $path_build 'gen') 'hello_gte.dwarf_abbrev.bin'
|
||||
$dwarfStrBin = join-path (join-path $path_build 'gen') 'hello_gte.dwarf_str.bin'
|
||||
$dwarfLocBin = join-path (join-path $path_build 'gen') 'hello_gte.dwarf_loc.bin'
|
||||
$dwarfLoclistsBin = join-path (join-path $path_build 'gen') 'hello_gte.dwarf_loclists.bin'
|
||||
$dwarfInfoBin = join-path $path_build_gen 'hello_gte.dwarf_info.bin'
|
||||
$dwarfAbbrevBin = join-path $path_build_gen 'hello_gte.dwarf_abbrev.bin'
|
||||
$dwarfStrBin = join-path $path_build_gen 'hello_gte.dwarf_str.bin'
|
||||
$dwarfLocBin = join-path $path_build_gen 'hello_gte.dwarf_loc.bin'
|
||||
$dwarfLoclistsBin = join-path $path_build_gen 'hello_gte.dwarf_loclists.bin'
|
||||
$g_args = @(
|
||||
"--update-section=.debug_info=$dwarfInfoBin",
|
||||
"--update-section=.debug_abbrev=$dwarfAbbrevBin",
|
||||
@@ -441,7 +444,7 @@ function build-gte_hello {
|
||||
|
||||
# Baked atoms execute from RAM but are emitted as C data arrays, so their ELF sections lack SHF_EXECINSTR.
|
||||
# GDB discards line rows for non-code sections. Mark only the debug-copy sections executable.
|
||||
# The shipping ELF and PS-EXE remain byte/flag unchanged.
|
||||
# The original ELF and PS-EXE remain byte/flag unchanged.
|
||||
& $Objcopy `
|
||||
--set-section-flags ".rodata=alloc,load,readonly,code,contents" `
|
||||
--set-section-flags ".data=alloc,load,data,code,contents" `
|
||||
@@ -449,7 +452,8 @@ function build-gte_hello {
|
||||
if ($LASTEXITCODE -ne 0) {
|
||||
Write-Warning "[build] atom-section flag update failed (exit $LASTEXITCODE); removing $injectElf"
|
||||
Remove-Item -LiteralPath $injectElf -ErrorAction SilentlyContinue
|
||||
} else {
|
||||
}
|
||||
else {
|
||||
Write-Host "[build] DWARF-injected ELF: $injectElf"
|
||||
}
|
||||
}
|
||||
|
||||
+1429
-145
File diff suppressed because it is too large
Load Diff
@@ -21,7 +21,7 @@ local duffle = dofile(_bootstrap_dir .. "../duffle_paths.lua")
|
||||
local write_file = duffle.write_file
|
||||
local ensure_dir = duffle.ensure_dir
|
||||
|
||||
-- The annotation pass now consults the source-derived registries built by scan_source:
|
||||
-- The annotation pass reads the source-derived registries from scan_source:
|
||||
-- * pipe_ctx.register_alias_registry — for atom_dbg_reg_default(R_X, ...) and atom_reg_types(R_X, ...) member-identity checks
|
||||
-- * pipe_ctx.type_name_registry — for atom_dbg_reg_default(<T>, ...) and atom_reg_types(<T>, ...) type-identity checks
|
||||
|
||||
@@ -108,12 +108,11 @@ local ensure_dir = duffle.ensure_dir
|
||||
--
|
||||
-- Each check has a uniform `append_to_findings` shape (errors[] / warnings[] / info[]).
|
||||
-- The dispatcher in `validate()` decides which findings list each check writes to — by convention,
|
||||
-- "existence" checks (declaration must exist, struct must exist) write errors[]; "shape" checks
|
||||
-- (writes/reads must be wave-context) write warnings[].
|
||||
-- "existence" checks (declaration must exist, struct must exist) write errors[]; "shape" checks (writes/reads must be wave-context) write warnings[].
|
||||
-- The `macro_word_drift` check writes both errors[] (missing/mismatch) and info[] (match).
|
||||
|
||||
--- Check: every annotated atom must have a matching MipsAtom_(name) declaration.
|
||||
--- @param a AtomAnnotation
|
||||
--- @param a AtomAnnotation
|
||||
--- @param pipe_ctx PipeCtx
|
||||
--- @param findings Findings
|
||||
local function check_atom_decl_exists(a, pipe_ctx, findings)
|
||||
@@ -144,7 +143,7 @@ end
|
||||
--- Emitting a warning here keeps the annotation pass from being stop-on-error for the common test-fixture case,
|
||||
--- while still surfacing the issue in the report.
|
||||
--- The static-analysis report remains the source of truth for build-stopping errors.
|
||||
--- @param a AtomAnnotation
|
||||
--- @param a AtomAnnotation
|
||||
--- @param pipe_ctx PipeCtx
|
||||
--- @param findings Findings
|
||||
local function check_binds_struct_exists(a, pipe_ctx, findings)
|
||||
@@ -160,7 +159,7 @@ end
|
||||
|
||||
--- Check: TAPE_WORDS(mac_X, N) ↔ WORD_COUNT(mac_X, N) drift.
|
||||
--- Three outcomes: missing (error), mismatch (error), match (info).
|
||||
--- @param m MacroEntry
|
||||
--- @param m MacroEntry
|
||||
--- @param wc table<string, integer> -- the shared word-count table (from ctx.shared.word_counts)
|
||||
--- @param findings Findings
|
||||
local function check_macro_word_drift(m, wc, findings)
|
||||
@@ -188,7 +187,7 @@ end
|
||||
--- Check: atom_dbg_reg_default(R_X, <type>) must target a register declared as a debug-visible alias in `pipe_ctx.register_alias_registry`,
|
||||
--- with a type name found in `pipe_ctx.type_name_registry`.
|
||||
--- Pointer depth is still bounded to 0 or 1. Duplicate defaults are still detected.
|
||||
--- @param _src SourceFile -- unused (kept for the per_source shape)
|
||||
--- @param _src SourceFile -- unused (kept for the per_source shape)
|
||||
--- @param pipe_ctx PipeCtx
|
||||
--- @param findings Findings
|
||||
local function check_semantic_reg_defaults(_src, pipe_ctx, findings)
|
||||
@@ -240,7 +239,7 @@ end
|
||||
--- The alias ident `R_<n>` now encodes the GPR identity only for entries that are explicitly opted in via the bare `atom_reg` marker.
|
||||
--- R_T0..R_T3 are intentionally NOT auto-included (per the prototype principle: no auto-include of wave-context; explicit opt-in only).
|
||||
--- The check fires for any R_T0..R_T3 reference that hasn't been opted in via `#define atom_reg`.
|
||||
--- @param _src SourceFile
|
||||
--- @param _src SourceFile
|
||||
--- @param pipe_ctx PipeCtx
|
||||
--- @param findings Findings
|
||||
local function check_atom_reg_types(_src, pipe_ctx, findings)
|
||||
@@ -271,7 +270,7 @@ local function check_atom_reg_types(_src, pipe_ctx, findings)
|
||||
end
|
||||
|
||||
--- Check: atom_view(Binds_X) entries must reference a real Binds_* struct and that struct must declare at least one field.
|
||||
--- @param _src SourceFile
|
||||
--- @param _src SourceFile
|
||||
--- @param pipe_ctx PipeCtx
|
||||
--- @param findings Findings
|
||||
local function check_atom_view_layout(_src, pipe_ctx, findings)
|
||||
@@ -385,14 +384,14 @@ local function check_skip_marker(marker, _pipe_ctx, findings)
|
||||
end
|
||||
end
|
||||
|
||||
--- Migration warning emitted alongside the new registry-membership check.
|
||||
--- Warn when a source references an unregistered alias.
|
||||
---
|
||||
--- R_TapePtr / R_AtomJmp / R_PrimCursor / R_FaceCursor / R_VertBase / R_OtBase are the context aliases opted in via `#define atom_reg` in lottes_tape.h.
|
||||
--- Any source referencing an R_X that's NOT in the registry will trip the new check; a single pass-level info entry
|
||||
--- A source referencing an unregistered R_X emits one pass-level info entry
|
||||
--- (emitted only when at least one such rejection lands in this source) tells users where to look.
|
||||
---
|
||||
--- This check is a stop-gap until users migrate off raw C-ABI register names.
|
||||
--- @param _src SourceFile
|
||||
--- This check directs raw C-ABI register names to explicit alias registration.
|
||||
--- @param _src SourceFile
|
||||
--- @param pipe_ctx PipeCtx
|
||||
--- @param findings Findings
|
||||
local function check_wave_context_migration(_src, pipe_ctx, findings)
|
||||
@@ -445,14 +444,65 @@ local CHECK_RULES = {
|
||||
-- Validation
|
||||
-- ════════════════════════════════════════════════════════════════════════════
|
||||
--
|
||||
-- Pure check: read from src.scan, run validations, emit findings.
|
||||
-- No source walking; no parsing. The scan was done once upstream.
|
||||
-- Pure check: read from src.scan, run validations, emit findings. The scan was done once upstream.
|
||||
|
||||
--- Validate one source against its pre-scanned SourceScan payload.
|
||||
--- Build the corpus-wide pipe_ctx ONCE per pass run.
|
||||
--- Reads the merged `corpus.*` registries (canonical cross-source lookups),
|
||||
--- and the corpus-wide `atom_infos` list (preserving source order + duplicates).
|
||||
--- The corpus is the source of truth; per-source scans retain body / declaration
|
||||
--- ownership via `src.scan` and the per-source `atoms` / `atom_infos` projections.
|
||||
---
|
||||
--- Canonical ownership: a context without `ctx.shared.corpus` is rejected with an explicit canonical-corpus message.
|
||||
--- No per-source fallback synthesis is performed; callers MUST construct a canonical ctx through `build_ctx`.
|
||||
--- @param ctx PassCtx
|
||||
--- @param src SourceFile
|
||||
--- @return PipeCtx
|
||||
local function build_corpus_pipe_ctx(ctx)
|
||||
local corpus = ctx.shared and ctx.shared.corpus
|
||||
if not corpus then
|
||||
error("annotation requires ctx.shared.corpus "
|
||||
.. "(the canonical corpus is the source of truth; "
|
||||
.. "no per-source fallback is supported)", 0)
|
||||
end
|
||||
|
||||
-- Corpus atom_infos preserves source-order + duplicates;
|
||||
-- the per-check `check_unique_annotation` post-rule still flags duplicate annotation
|
||||
-- names within this list. We pre-compute the annot_counts map here so the per_source checks can iterate it without re-walking.
|
||||
local annot_counts = {}
|
||||
for _, info in ipairs(corpus.atom_infos or {}) do
|
||||
if info and info.atom_name then
|
||||
annot_counts[info.atom_name] = (annot_counts[info.atom_name] or 0) + 1
|
||||
end
|
||||
end
|
||||
|
||||
-- The pipe_ctx views REFERENCE the corpus tables directly (no copies).
|
||||
-- Every consumer of these fields observes mutations via the canonical corpus without independently mutable registry construction.
|
||||
return {
|
||||
-- Cross-source lookup tables (canonical corpus projections).
|
||||
register_alias_registry = corpus.register_alias_registry or {},
|
||||
type_name_registry = corpus.type_name_registry or {},
|
||||
atom_views = corpus.atom_views or {},
|
||||
atom_ctxs = corpus.atom_ctxs or {},
|
||||
atom_phases = corpus.atom_phases or {},
|
||||
binds_by_name = corpus.binds_by_name or {},
|
||||
atoms_by_name = corpus.atoms_by_name or {},
|
||||
-- Corpus-wide ordered list of atom_info records (source-order + duplicates).
|
||||
atom_infos_list = corpus.atom_infos or {},
|
||||
-- Corpus-wide annotation count aggregation (post-rule consumes this).
|
||||
annot_counts = annot_counts,
|
||||
-- Corpus-wide collisions (recorded by scan_source.merge_corpus_registries).
|
||||
collisions = corpus.collisions or {},
|
||||
-- wc still consumed by check_macro_word_drift; reads from the canonical
|
||||
-- `corpus.word_counts` table (built by word_count_eval.run).
|
||||
word_counts = corpus.word_counts or {},
|
||||
}
|
||||
end
|
||||
|
||||
--- Validate one source against its pre-scanned SourceScan payload + the corpus-wide pipe_ctx.
|
||||
--- @param ctx PassCtx
|
||||
--- @param src SourceFile
|
||||
--- @param corpus_pipe_ctx PipeCtx -- built once per pass from corpus registries
|
||||
--- @return AnnotatedResult
|
||||
local function validate(ctx, src)
|
||||
local function validate(ctx, src, corpus_pipe_ctx)
|
||||
local scan = src.scan
|
||||
|
||||
-- Project the pre-scanned atoms to the AtomEntry shape this pass needs.
|
||||
@@ -478,9 +528,11 @@ local function validate(ctx, src)
|
||||
}
|
||||
end
|
||||
|
||||
-- Build pipe_ctx (Fleury: expose structure). Pre-compute everything the per-check functions need.
|
||||
-- Single source of truth for atom / binds / annotation-count lookups.
|
||||
-- pipe_ctx.types / pipe_ctx.atom_views / pipe_ctx.seen_defaults are projected from the scan payload so per_source check rules can iterate.
|
||||
-- Build the per-source pipe_ctx (Fleury: expose structure).
|
||||
-- Cross-source visibility comes from `corpus_pipe_ctx`;
|
||||
-- per-source declaration / body ownership comes from `src.scan`.
|
||||
-- pipe_ctx.types / pipe_ctx.atom_views / pipe_ctx.seen_defaults / pipe_ctx.type_occurrences
|
||||
-- are projected from the per-source scan so the per_source check rules can iterate the source-local occurrences.
|
||||
local seen_defaults = {}
|
||||
for reg, _ in pairs(scan.types or {}) do
|
||||
seen_defaults[reg] = (seen_defaults[reg] or 0) + 1
|
||||
@@ -493,25 +545,20 @@ local function validate(ctx, src)
|
||||
local pipe_ctx = {
|
||||
atom_index = {},
|
||||
binds_index = {},
|
||||
annot_counts = {},
|
||||
annot_counts = corpus_pipe_ctx.annot_counts,
|
||||
types = scan.types or {},
|
||||
type_occurrences = scan.type_occurrences or {},
|
||||
atom_views = scan.atom_views or {},
|
||||
seen_defaults = seen_defaults,
|
||||
atom_infos_list = atom_infos_list,
|
||||
binds_list = scan.binds or {},
|
||||
-- Project the source-derived registries from the scan payload so per_source checks consult them instead of the deleted
|
||||
-- SEMANTIC_DEFAULT_REGS / KNOWN_REG_DEFAULT_TYPES / etc.
|
||||
register_alias_registry = scan.register_alias_registry or {},
|
||||
type_name_registry = scan.type_name_registry or {},
|
||||
-- Source-derived registries: still populated from the scan payload as a convenience for callers that want source-local visibility.
|
||||
-- The canonical cross-source lookup tables live in corpus_pipe_ctx.
|
||||
register_alias_registry = corpus_pipe_ctx.register_alias_registry,
|
||||
type_name_registry = corpus_pipe_ctx.type_name_registry,
|
||||
}
|
||||
for _, a in ipairs(atoms) do pipe_ctx.atom_index [a.name] = a end
|
||||
for _, b in ipairs(scan.binds) do pipe_ctx.binds_index[b.name] = b end
|
||||
for _, a in ipairs(annots) do
|
||||
if a.name then
|
||||
pipe_ctx.annot_counts[a.name] = (pipe_ctx.annot_counts[a.name] or 0) + 1
|
||||
end
|
||||
end
|
||||
|
||||
-- Findings live in a single struct with three lists (errors / warnings / info).
|
||||
-- Each check writes to the list appropriate for its severity.
|
||||
@@ -543,8 +590,8 @@ local function validate(ctx, src)
|
||||
if rule.post then rule.post(pipe_ctx, findings) end
|
||||
end
|
||||
|
||||
-- Per-skip-marker rules.
|
||||
-- Each raw marker recorded by scan_source (in scan.skip_over.markers) is validated independently;
|
||||
-- Per-skip-marker rules.
|
||||
-- Each raw marker recorded by scan_source (in scan.skip_over.markers) is validated independently;
|
||||
-- the check emits at most one error per marker.
|
||||
-- Valid markers stay attached to scan.skip_over.atoms /.components for dwarf_injection.lua consumer.
|
||||
local skip_markers = scan.skip_over and scan.skip_over.markers or {}
|
||||
@@ -555,7 +602,7 @@ local function validate(ctx, src)
|
||||
end
|
||||
|
||||
-- Per-macro rules (TAPE_WORDS vs WORD_COUNT drift).
|
||||
local wc = ctx.shared.word_counts
|
||||
local wc = corpus_pipe_ctx.word_counts
|
||||
for _, m in ipairs(scan.macros) do
|
||||
for _, rule in ipairs(CHECK_RULES) do
|
||||
if rule.per_macro then rule.per_macro(m, wc, findings) end
|
||||
@@ -571,8 +618,8 @@ local function validate(ctx, src)
|
||||
-- Information summary (always emitted).
|
||||
findings.info[#findings.info + 1] = {
|
||||
line = 0,
|
||||
msg = string.format("scanned: %d atom(s), %d annotation(s), %d macro-word-decl(s), %d binds struct(s)",
|
||||
#atoms, #annots, #scan.macros, #scan.binds),
|
||||
msg = string.format("scanned: %d atom(s), %d annotation(s), %d macro-word-decl(s), %d binds struct(s)"
|
||||
, #atoms, #annots, #scan.macros, #scan.binds),
|
||||
}
|
||||
|
||||
return {
|
||||
@@ -650,9 +697,16 @@ function M.run(ctx)
|
||||
local errors = {}
|
||||
local warnings = {}
|
||||
|
||||
-- Per-DIRECTORY (per-module) aggregation. Group sources by `src.dir`, validate every source in the dir, then emit ONE errors.h per dir.
|
||||
-- `ctx.by_dir` is pre-computed in build_ctx (shared across all passes).
|
||||
local by_dir = ctx.by_dir or duffle.group_sources_by_dir(ctx.sources)
|
||||
-- Build the corpus-wide pipe_ctx ONCE per pass run.
|
||||
-- The corpus owns the canonical cross-source registries; per-source scans retain body / declaration ownership.
|
||||
-- The pipe_ctx is shared across every validate() invocation in this M.run so cross-source visibility is constant.
|
||||
local corpus_pipe_ctx = build_corpus_pipe_ctx(ctx)
|
||||
local corpus = ctx.shared.corpus
|
||||
|
||||
-- Per-DIRECTORY (per-module) aggregation.
|
||||
-- Group sources by `src.dir`, validate every source in the dir, then emit ONE errors.h per dir.
|
||||
-- The corpus owns `sources_by_dir`; this pass reads the corpus bucket directly.
|
||||
local by_dir = (corpus and corpus.sources_by_dir) or {}
|
||||
|
||||
for dir, dir_sources in pairs(by_dir) do
|
||||
local dir_basename = dir:match("([^/\\]+)$") or dir
|
||||
@@ -663,7 +717,7 @@ function M.run(ctx)
|
||||
ctx.flags = ctx.flags or {}
|
||||
ctx.flags._annot_source_results = ctx.flags._annot_source_results or {}
|
||||
for _, src in ipairs(dir_sources) do
|
||||
local result = validate(ctx, src)
|
||||
local result = validate(ctx, src, corpus_pipe_ctx)
|
||||
result.source = src.path -- tag for downstream rendering
|
||||
ctx.flags._annot_source_results[src.path] = result -- stash so report.lua reads from cache instead of re-running validate()
|
||||
dir_atoms = dir_atoms + #result.atoms
|
||||
|
||||
+136
-279
@@ -1,11 +1,8 @@
|
||||
--- passes/atoms_source_map.lua — Per-.word source-line map emitter for tape atoms.
|
||||
---
|
||||
--- Reads the pre-scanned SourceScan payload (produced once upstream by `duffle.scan_source`)
|
||||
--- for `MipsAtom_(name)` (kind="atom"), `MipsAtomComp_` / `MipsAtomComp_Proc_` (kind="comp_*"),
|
||||
--- and `MipsCode code_<name>` (kind="raw_atom") declarations.
|
||||
--- Walks each atom's pre-tokenized body (`{{tok=string, rel=integer}, ...}` from `duffle.tokenize_body`),
|
||||
--- counts per-token word contributions via `ctx.shared.word_counts`, and emits one
|
||||
--- `WORD N LINE L TEXT T` line per `.word` to `<out_root>/<basename>.atoms.sourcemap.txt`.
|
||||
--- Reads the canonical `atom.paths` projection produced by the upstream `emission_model` pass.
|
||||
--- The ordered `items` stream, dense `word_events`, and `invocations` views are the only semantic inputs to this pass;
|
||||
--- it emits one `WORD N LINE L TEXT T` line per emitted `.word`.
|
||||
---
|
||||
--- **Two output forms** (per the workspace's per-emission-form pattern from
|
||||
--- `guide_metaprogram_ssdl.md`):
|
||||
@@ -34,10 +31,8 @@
|
||||
--- ENDATOM
|
||||
--- ```
|
||||
---
|
||||
--- Marker calls (`atom_label(...)`, `atom_offset(...)`) emit 0 `.word`s.
|
||||
--- They share the same walking convention as `passes/offsets.lua :: scan_atom_body`:
|
||||
--- Markers do NOT advance the word-offset counter, but if a marker is bundled on the same token with a trailing instruction
|
||||
--- (e.g. `atom_label(foo) load_half_u(...)`), the trailing instruction's word count is added. This matches `offsets.lua :: count_marker_rest`.
|
||||
--- Marker records are zero-width in `atom.paths.items`; they do not appear in
|
||||
--- the dense word view and therefore emit no WORD rows.
|
||||
---
|
||||
--- **Conventions:** tabs (1/level), EmmyLua annotations, no regex,
|
||||
--- Lua 5.3 compatible.
|
||||
@@ -50,10 +45,8 @@
|
||||
-- (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 elf_dwarf = require("elf_dwarf")
|
||||
local word_count_eval = require("word_count_eval")
|
||||
local count_token_words = word_count_eval.count_token_words
|
||||
local duffle = dofile(_bootstrap_dir .. "../duffle_paths.lua")
|
||||
local elf_dwarf = require("elf_dwarf")
|
||||
|
||||
-- ════════════════════════════════════════════════════════════════════════════
|
||||
-- Constants
|
||||
@@ -63,199 +56,90 @@ local count_token_words = word_count_eval.count_token_words
|
||||
-- the gdb runtime loader rejects mismatches (E2).
|
||||
local FORMAT_VERSION = 1
|
||||
|
||||
-- Marker-call identifiers (mirrors offsets.lua:33-34).
|
||||
local LABEL_MARKER = "atom_label"
|
||||
local OFFSET_MARKER = "atom_offset"
|
||||
|
||||
-- ════════════════════════════════════════════════════════════════════════════
|
||||
-- Type declarations
|
||||
-- ════════════════════════════════════════════════════════════════════════════
|
||||
|
||||
--- @class AtomSourceMapCtx
|
||||
--- @field sources table[] -- SourceScan payload per source (from `ctx.sources`)
|
||||
--- @field shared table -- `ctx.shared`
|
||||
--- @field shared.word_counts table -- macro name -> word count (populated by word-counts + components passes)
|
||||
--- @field shared.corpus table -- canonical source-order corpus
|
||||
--- @field shared.word_counts table -- identity alias of `corpus.word_counts`
|
||||
--- @field out_root string -- output root (e.g. "build/gen")
|
||||
--- @field dry_run boolean -- if true, compute but don't write
|
||||
--- @field flags table -- `ctx.flags`; reads `flags.gdb_runtime` + `flags.elf_path`
|
||||
|
||||
-- ════════════════════════════════════════════════════════════════════════════
|
||||
-- Helpers
|
||||
-- Canonical atom-path renderers
|
||||
-- ════════════════════════════════════════════════════════════════════════════
|
||||
|
||||
-- ════════════════════════════════════════════════════════════════════════════
|
||||
-- Provenance emission
|
||||
-- ════════════════════════════════════════════════════════════════════════════
|
||||
|
||||
-- Component-macro invocation prefix (mirrors components.lua's MAC_PREFIX).
|
||||
local MAC_PREFIX = "mac_"
|
||||
local MAC_PREFIX_LEN = 4
|
||||
|
||||
--- Strip the `mac_` prefix from a token's leading identifier.
|
||||
--- Returns nil if the identifier doesn't start with `mac_`
|
||||
--- (so non-component tokens like `load_half_u`, `nop2`, `gte_cmdw_*` fall through cleanly).
|
||||
--- @param tok string
|
||||
--- @return string|nil
|
||||
local function strip_mac_prefix_from_token(tok)
|
||||
local leading = duffle.read_ident(tok, 1)
|
||||
if not leading then return nil end
|
||||
if leading:sub(1, MAC_PREFIX_LEN) == MAC_PREFIX then
|
||||
return leading:sub(MAC_PREFIX_LEN + 1)
|
||||
end
|
||||
return nil
|
||||
end
|
||||
|
||||
--- Fetch the per-word body lines for a `mac_X(...)` invocation.
|
||||
--- Walks the component's pre-tokenized body in lockstep with `count_token_words` and attributes each emitted `.word`
|
||||
--- to a source line via `idx.line_of(...)`.
|
||||
--- Atom labels (`atom_label(...)`) emit 0 `.word`s and are skipped.
|
||||
--- @param bare string|nil -- the bare component name (e.g. `gte_load_tri_verts`)
|
||||
--- @param comp_body_index table
|
||||
--- @param wc table
|
||||
--- @return table|nil -- list of source lines, 1-based by word position
|
||||
local function fetch_body_lines(bare, comp_body_index, wc)
|
||||
if not (bare and comp_body_index) then return nil end
|
||||
local idx = comp_body_index[bare]
|
||||
if not (idx and idx.body_tokens and idx.line_of) then return nil end
|
||||
local lines = {}
|
||||
for _, bt in ipairs(idx.body_tokens) do
|
||||
local bt_tok = duffle.trim(bt.tok or "")
|
||||
if bt_tok ~= "" then
|
||||
local leading = duffle.read_ident(bt_tok, 1)
|
||||
local bt_words
|
||||
if leading == "atom_label" or leading == "atom_offset" then
|
||||
bt_words = 0
|
||||
else
|
||||
bt_words = count_token_words(bt_tok, wc)
|
||||
end
|
||||
if bt_words > 0 then
|
||||
local body_line = idx.line_of(idx.body_off + bt.rel)
|
||||
for _ = 1, bt_words do lines[#lines + 1] = body_line end
|
||||
end
|
||||
end
|
||||
end
|
||||
return lines
|
||||
end
|
||||
|
||||
--- Unified per-word entry walker. `mode` is "sourcemap" (3 fields) or "provenance" (8 fields including component + body-line lookup).
|
||||
--- Returns (entries, total_words). Markers contribute 0 entries.
|
||||
--- Join canonical words to canonical word items. `items` supplies the ordered
|
||||
--- word boundaries, while `word_events` supplies call text and source lines.
|
||||
--- @param atom table
|
||||
--- @param src table
|
||||
--- @param wc table
|
||||
--- @param mode string -- "sourcemap" | "provenance"
|
||||
--- @param comp table|nil -- shared.components map (provenance only)
|
||||
--- @param comp_body_index table|nil -- per-source body index (provenance only)
|
||||
--- @return table[], integer
|
||||
local function compute_word_entries(atom, src, wc, mode, comp, comp_body_index)
|
||||
local entries = {}
|
||||
local pos = 0
|
||||
for _, t in ipairs(atom.body_tokens) do
|
||||
local tok = t.tok
|
||||
local rel = t.rel
|
||||
|
||||
local words
|
||||
if duffle.is_marker_token(tok) then
|
||||
words = duffle.count_marker_rest(tok, wc, count_token_words)
|
||||
else
|
||||
words = count_token_words(tok, wc)
|
||||
end
|
||||
|
||||
-- Provenance-only: resolve component + body_lines (one fetch per token).
|
||||
local comp_name, comp_line, comp_path, comp_kind
|
||||
local body_lines
|
||||
if mode == "provenance" then
|
||||
local bare = strip_mac_prefix_from_token(tok)
|
||||
if bare and comp and comp[bare] then
|
||||
comp_name = bare
|
||||
comp_line = comp[bare].line
|
||||
comp_path = comp[bare].path
|
||||
comp_kind = comp[bare].kind
|
||||
end
|
||||
if comp_name then body_lines = fetch_body_lines(bare, comp_body_index, wc) end
|
||||
end
|
||||
|
||||
if words > 0 then
|
||||
local line = src.scan.line_of(atom.body_off + rel)
|
||||
local text = duffle.trim(tok):gsub("[\t\r\n]+", " ")
|
||||
for i = 1, words do
|
||||
local entry
|
||||
if mode == "provenance" then
|
||||
entry = {
|
||||
pos = pos,
|
||||
line = line,
|
||||
text = text,
|
||||
comp_name = comp_name,
|
||||
comp_line = comp_line,
|
||||
comp_path = comp_path,
|
||||
comp_kind = comp_kind,
|
||||
body_line = body_lines and body_lines[i],
|
||||
}
|
||||
else -- "sourcemap" (default)
|
||||
entry = { pos = pos, line = line, text = text }
|
||||
end
|
||||
entries[#entries + 1] = entry
|
||||
pos = pos + 1
|
||||
end
|
||||
end
|
||||
local function canonical_word_entries(atom)
|
||||
local paths = atom.paths or {}
|
||||
local events = paths.word_events or {}
|
||||
local word_items = {}
|
||||
for _, item in ipairs(paths.items or {}) do
|
||||
if item.kind == "word" then word_items[#word_items + 1] = item end
|
||||
end
|
||||
return entries, pos
|
||||
|
||||
local entries = {}
|
||||
for index, event in ipairs(events) do
|
||||
local item = word_items[index] or {}
|
||||
entries[#entries + 1] = {
|
||||
pos = event.i or (index - 1),
|
||||
line = event.call_line or item.line or 0,
|
||||
text = event.call_text or item.call_text or "",
|
||||
body_line = event.body_line or item.body_line or item.line or 0,
|
||||
invocation = (event.outermost_invocation_id
|
||||
and paths.invocations
|
||||
and paths.invocations[event.outermost_invocation_id]) or nil,
|
||||
}
|
||||
end
|
||||
return entries, #events
|
||||
end
|
||||
|
||||
--- Render one atom's provenance stanza. Format:
|
||||
--- `WORD N CALL <src-path>:<src-line> MACRO <name> "<def-path>:<def-line>" [BODY <line>]` (for component words)
|
||||
--- `WORD N CALL <src-path>:<src-line> RAW` (for direct instructions)
|
||||
--- `BODY <line>` is the source line of THIS specific word within the macro body
|
||||
--- (lottes_tape.h:N where N is the per-word body line).
|
||||
--- Absent for RAW rows and for component rows whose component declaration could not be indexed (older pass combinations / external macros).
|
||||
--- Downstream consumers (dwarf_injection, tests) fall back to DefLine / comp_line when BODY is absent.
|
||||
--- Returns (lines, total_words).
|
||||
--- @param src table
|
||||
--- @param atom table
|
||||
--- @param wc table
|
||||
--- @param comp table -- shared.components map
|
||||
--- @param comp_body_index table -- per-source component body index: bare_name -> {body_off, body_tokens, line_of}
|
||||
--- Render one atom's provenance stanza. Format 1 remains:
|
||||
--- `WORD N CALL <src-path>:<src-line> MACRO <name> "<def-path>:<def-line>" BODY <line>`
|
||||
--- `WORD N CALL <src-path>:<src-line> RAW`
|
||||
--- Component identity comes from the canonical outermost invocation record;
|
||||
--- the count-table lookup is the canonical component declaration witness.
|
||||
--- @param src table
|
||||
--- @param atom table
|
||||
--- @param wc table -- identity alias of corpus.word_counts
|
||||
--- @return string[], integer
|
||||
local function emit_provenance_stanza(src, atom, wc, comp, comp_body_index)
|
||||
local function emit_provenance_stanza(src, atom, wc)
|
||||
local lines = {}
|
||||
local rel_path = src.path:gsub("\\", "/")
|
||||
local entries, total = compute_word_entries(atom, src, wc, "provenance", comp, comp_body_index)
|
||||
local rel_path = src.path:gsub("\\\\", "/")
|
||||
local entries, total = canonical_word_entries(atom)
|
||||
|
||||
-- ATOM header line with placeholder total (patched after we know it).
|
||||
lines[#lines + 1] = string.format('ATOM %s "%s" 0', atom.raw_name or atom.name, rel_path)
|
||||
|
||||
for _, pe in ipairs(entries) do
|
||||
if pe.comp_name then
|
||||
local body_suffix = ""
|
||||
if pe.body_line then
|
||||
body_suffix = " BODY " .. tostring(pe.body_line)
|
||||
end
|
||||
lines[#lines + 1] = string.format('WORD %d CALL %s:%d MACRO %s "%s:%d"%s',
|
||||
pe.pos, rel_path, pe.line, pe.comp_name, pe.comp_path, pe.comp_line, body_suffix)
|
||||
for _, entry in ipairs(entries) do
|
||||
local inv = entry.invocation
|
||||
local macro_count = inv and wc["mac_" .. inv.component_name]
|
||||
if inv and macro_count ~= nil then
|
||||
lines[#lines + 1] = string.format(
|
||||
'WORD %d CALL %s:%d MACRO %s "%s:%d" BODY %d',
|
||||
entry.pos, rel_path, entry.line, inv.component_name,
|
||||
inv.def_path or "", inv.def_line or 0, entry.body_line)
|
||||
else
|
||||
lines[#lines + 1] = string.format("WORD %d CALL %s:%d RAW", pe.pos, rel_path, pe.line)
|
||||
lines[#lines + 1] = string.format(
|
||||
"WORD %d CALL %s:%d RAW", entry.pos, rel_path, entry.line)
|
||||
end
|
||||
end
|
||||
|
||||
-- Patch the placeholder total in the ATOM header line.
|
||||
lines[1] = lines[1]:gsub(" 0$", " " .. tostring(total))
|
||||
lines[#lines + 1] = "ENDATOM"
|
||||
return lines, total
|
||||
end
|
||||
|
||||
--- Build a per-source component body index keyed by the bare component name (e.g. `gte_load_tri_verts`).
|
||||
--- Each entry holds the data we need to map each emitted `.word` to its actual source line within the macro body:
|
||||
--- body_off -- byte offset of the `{` (start of body) in the component's source file.
|
||||
--- body_tokens -- list of {tok, rel} pairs; `rel` is the byte offset within the body.
|
||||
--- line_of -- closure resolving byte offsets in the component's source file to lines.
|
||||
--- Only `comp_bare` + `comp_proc` declarations contribute (a macro invocation can only resolve to one of those).
|
||||
--- First declaration wins (subsequent redeclarations would collide; today's sources declare each component exactly once).
|
||||
--- Render the full provenance file content for one source (one `.atoms.provenance.txt` per source).
|
||||
--- Render the full provenance file content for one source.
|
||||
--- @param src table
|
||||
--- @param wc table
|
||||
--- @param comp table -- shared.components map
|
||||
--- @param comp_body_index table -- cross-source component body index (built once in M.run; may be empty)
|
||||
--- @return string
|
||||
local function render_provenance(src, wc, comp, comp_body_index)
|
||||
local function render_provenance(src, wc)
|
||||
local lines = {}
|
||||
lines[#lines + 1] = "# FORMAT_VERSION 1"
|
||||
lines[#lines + 1] = "# auto-generated by ps1_meta.lua (passes/atoms_source_map.lua) — DO NOT EDIT"
|
||||
@@ -265,16 +149,15 @@ local function render_provenance(src, wc, comp, comp_body_index)
|
||||
lines[#lines + 1] = "# dwarf_injection to synthesize DW_TAG_inlined_subroutine instances + per-word"
|
||||
lines[#lines + 1] = "# line program rows for native source-level step into component bodies."
|
||||
|
||||
-- The cross-source component body index is passed in from M.run (one global lookup shared across every source's provenance file).
|
||||
-- A per-source lookup would miss every component whose declaration is in another source (e.g. `gte_load_tri_verts` is declared in `lottes_tape.h` but invoked from `hello_gte_tape.c`).
|
||||
|
||||
for _, atom in ipairs(src.scan.atoms or {}) do
|
||||
local stanza = emit_provenance_stanza(src, atom, wc, comp, comp_body_index)
|
||||
local function append(atom)
|
||||
local stanza = emit_provenance_stanza(src, atom, wc)
|
||||
for _, line in ipairs(stanza) do lines[#lines + 1] = line end
|
||||
end
|
||||
for _, atom in ipairs(src.scan.atoms or {}) do
|
||||
if atom.paths then append(atom) end
|
||||
end
|
||||
for _, atom in ipairs(src.scan.raw_atoms or {}) do
|
||||
local stanza = emit_provenance_stanza(src, atom, wc, comp, comp_body_index)
|
||||
for _, line in ipairs(stanza) do lines[#lines + 1] = line end
|
||||
if atom.paths then append(atom) end
|
||||
end
|
||||
|
||||
return table.concat(lines, "\n") .. "\n"
|
||||
@@ -286,19 +169,16 @@ end
|
||||
--- @param atom table
|
||||
--- @param wc table
|
||||
--- @return string[], integer
|
||||
local function emit_atom_stanza(src, atom, wc)
|
||||
local lines = {}
|
||||
local rel_path = src.path:gsub("\\", "/")
|
||||
local entries, total = compute_word_entries(atom, src, wc)
|
||||
local function emit_atom_stanza(src, atom)
|
||||
local lines = {}
|
||||
local rel_path = src.path:gsub("\\\\", "/")
|
||||
local entries, total = canonical_word_entries(atom)
|
||||
|
||||
-- ATOM header line with placeholder total (patched after we know it).
|
||||
lines[#lines + 1] = string.format('ATOM %s "%s" 0', atom.raw_name or atom.name, rel_path)
|
||||
for _, we in ipairs(entries) do
|
||||
for _, entry in ipairs(entries) do
|
||||
lines[#lines + 1] = string.format("WORD %d LINE %d TEXT %s",
|
||||
we.pos, we.line, we.text)
|
||||
entry.pos, entry.line, entry.text)
|
||||
end
|
||||
|
||||
-- Patch the placeholder total in the ATOM header line.
|
||||
lines[1] = lines[1]:gsub(" 0$", " " .. tostring(total))
|
||||
lines[#lines + 1] = "ENDATOM"
|
||||
return lines, total
|
||||
@@ -309,18 +189,20 @@ end
|
||||
--- @param src table
|
||||
--- @param wc table
|
||||
--- @return string
|
||||
local function render_source_map(src, wc)
|
||||
local function render_source_map(src)
|
||||
local lines = {}
|
||||
lines[#lines + 1] = "# FORMAT_VERSION " .. FORMAT_VERSION
|
||||
lines[#lines + 1] = "# auto-generated by ps1_meta.lua (passes/atoms_source_map.lua) — DO NOT EDIT"
|
||||
|
||||
for _, atom in ipairs(src.scan.atoms or {}) do
|
||||
local stanza = emit_atom_stanza(src, atom, wc)
|
||||
local function append(atom)
|
||||
local stanza = emit_atom_stanza(src, atom)
|
||||
for _, line in ipairs(stanza) do lines[#lines + 1] = line end
|
||||
end
|
||||
for _, atom in ipairs(src.scan.raw_atoms or {}) do
|
||||
local stanza = emit_atom_stanza(src, atom, wc)
|
||||
for _, line in ipairs(stanza) do lines[#lines + 1] = line end
|
||||
for _, atom in ipairs(src.scan.atoms or {}) do
|
||||
if atom.paths then append(atom) end
|
||||
end
|
||||
for _, atom in ipairs(src.scan.raw_atoms or {}) do
|
||||
if atom.paths then append(atom) end
|
||||
end
|
||||
|
||||
return table.concat(lines, "\n") .. "\n"
|
||||
@@ -343,55 +225,35 @@ end
|
||||
--- @param ctx PassCtx
|
||||
--- @return table[] -- list of {idx, name, src_path, file_base, addr, size_bytes, words, entries}
|
||||
local function build_atom_table(ctx)
|
||||
local wc = (ctx.shared and ctx.shared.word_counts) or {}
|
||||
local addrs = elf_dwarf.read_nm(ctx.flags.elf_path)
|
||||
|
||||
local corpus = ctx.shared and ctx.shared.corpus
|
||||
local matched = {}
|
||||
for _, src in ipairs(ctx.sources) do
|
||||
if src.scan then
|
||||
local file_base = src.path:match("([^/\\]+)$") or src.path
|
||||
for _, atom in ipairs(src.scan.atoms or {}) do
|
||||
if atom.kind == nil or atom.kind == "atom" then
|
||||
local name = atom.raw_name or atom.name
|
||||
local info = addrs[name]
|
||||
if info then
|
||||
local entries, total = compute_word_entries(atom, src, wc)
|
||||
matched[#matched + 1] = {
|
||||
name = name,
|
||||
src_path = src.path,
|
||||
file_base = file_base,
|
||||
addr = info[1],
|
||||
size_bytes = info[2],
|
||||
words = total,
|
||||
entries = entries,
|
||||
}
|
||||
end
|
||||
end
|
||||
end
|
||||
for _, atom in ipairs(src.scan.raw_atoms or {}) do
|
||||
local name = atom.name
|
||||
local info = addrs[name]
|
||||
if info then
|
||||
local entries, total = compute_word_entries(atom, src, wc)
|
||||
matched[#matched + 1] = {
|
||||
name = name,
|
||||
src_path = src.path,
|
||||
file_base = file_base,
|
||||
addr = info[1],
|
||||
size_bytes = info[2],
|
||||
words = total,
|
||||
entries = entries,
|
||||
}
|
||||
end
|
||||
end
|
||||
|
||||
for _, src in ipairs(corpus.source_order or {}) do
|
||||
local file_base = src.path:match("([^/\\\\]+)$") or src.path
|
||||
local function append(atom)
|
||||
if not atom.paths then return end
|
||||
local name = atom.raw_name or atom.name
|
||||
local info = addrs[name]
|
||||
if not info then return end
|
||||
local entries, total = canonical_word_entries(atom)
|
||||
matched[#matched + 1] = {
|
||||
name = name,
|
||||
src_path = src.path,
|
||||
file_base = file_base,
|
||||
addr = info[1],
|
||||
size_bytes = info[2],
|
||||
words = total,
|
||||
entries = entries,
|
||||
}
|
||||
end
|
||||
for _, atom in ipairs((src.scan or {}).atoms or {}) do append(atom) end
|
||||
for _, atom in ipairs((src.scan or {}).raw_atoms or {}) do append(atom) end
|
||||
end
|
||||
|
||||
-- Deterministic order: sort by address (matches `nm` output ordering).
|
||||
table.sort(matched, function(a, b) return a.addr < b.addr end)
|
||||
for i, a in ipairs(matched) do
|
||||
a.idx = i - 1
|
||||
end
|
||||
for i, a in ipairs(matched) do a.idx = i - 1 end
|
||||
return matched
|
||||
end
|
||||
|
||||
@@ -652,57 +514,52 @@ function M.run(ctx)
|
||||
local errors = {}
|
||||
local warnings = {}
|
||||
|
||||
-- word-counts + components passes must have populated shared.word_counts.
|
||||
-- If absent, the orchestrator wired the deps wrong — fail loud.
|
||||
local wc = (ctx.shared and ctx.shared.word_counts) or {}
|
||||
if not wc or not next(wc) then
|
||||
local corpus = ctx.shared and ctx.shared.corpus
|
||||
if type(corpus) ~= "table" or type(corpus.source_order) ~= "table" then
|
||||
error("atoms_source_map.run requires ctx.shared.corpus.source_order (canonical corpus).", 0)
|
||||
end
|
||||
|
||||
-- Word counts are owned by `corpus.word_counts`.
|
||||
-- The canonical owner is `corpus.word_counts` (populated by `passes/word_count_eval.lua` + `passes/components.lua`).
|
||||
local wc = corpus.word_counts or {}
|
||||
if not next(wc) then
|
||||
warnings[#warnings + 1] = {
|
||||
line = 0,
|
||||
msg = "atoms_source_map: ctx.shared.word_counts is empty; the word-counts + components passes may not have populated it. Check the PASSES dep edges.",
|
||||
msg = "atoms_source_map: corpus.word_counts is empty; the word-counts + components passes may not have populated it. Check the PASSES dep edges.",
|
||||
}
|
||||
end
|
||||
|
||||
-- shared.components map is populated by `passes/components.lua`.
|
||||
-- Used to attribute each emitted `.word` to either a component macro or the enclosing atom body.
|
||||
-- If absent, all words fall through as RAW (correct behavior — provenance is additive).
|
||||
local comp = (ctx.shared and ctx.shared.components) or {}
|
||||
|
||||
-- Cross-source component body index.
|
||||
-- Built ONCE (and memoized at `ctx.shared.component_body_index`) so every source's provenance writer can resolve `mac_X(...)`
|
||||
-- invocations back to the macro's body tokens (regardless of which source declared the component).
|
||||
-- The atom file (`hello_gte_tape.c`) does not contain the `MipsAtomComp_(...)` declarations,
|
||||
-- so the body data would be missing for every component invocation the atom file emitted.
|
||||
-- Superseded by `duffle.get_component_body_index` so the same index is shared with `static_analysis.lua`
|
||||
-- and any future dependency-check pass (sourcemap/provenance byte-identical because the new entry only ADDS fields).
|
||||
local comp_body_index = duffle.get_component_body_index(ctx)
|
||||
|
||||
-- Always emit the canonical text form (per-source).
|
||||
for _, src in ipairs(ctx.sources) do
|
||||
if src.scan then
|
||||
local n_atoms = src.scan.atoms and #src.scan.atoms or 0
|
||||
local n_raw_atoms = src.scan.raw_atoms and #src.scan.raw_atoms or 0
|
||||
if n_atoms + n_raw_atoms > 0 then
|
||||
local basename = duffle.basename_no_ext(src.path)
|
||||
|
||||
-- (1) atoms.sourcemap.txt — per-.word line map (unchanged contract).
|
||||
local sourcemap_path = ctx.out_root .. "/" .. basename .. ".atoms.sourcemap.txt"
|
||||
local sourcemap_body = render_source_map(src, wc)
|
||||
|
||||
-- (2) atoms.provenance.txt — per-.word provenance with `mac_X(...)` component resolution back to the component's definition file:line.
|
||||
-- Consumed by `passes/dwarf_injection.lua` to synthesize `DW_TAG_inlined_subroutine` instances for source-level Step Into on component invocations.
|
||||
local prov_path = ctx.out_root .. "/" .. basename .. ".atoms.provenance.txt"
|
||||
local prov_body = render_provenance(src, wc, comp, comp_body_index)
|
||||
|
||||
if not ctx.dry_run then
|
||||
duffle.ensure_dir(duffle.dirname(sourcemap_path))
|
||||
duffle.write_file_lf(sourcemap_path, sourcemap_body)
|
||||
duffle.write_file_lf(prov_path, prov_body)
|
||||
end
|
||||
|
||||
outputs[#outputs + 1] = { kind = "report", path = sourcemap_path }
|
||||
outputs[#outputs + 1] = { kind = "report", path = prov_path }
|
||||
for _, src in ipairs(corpus.source_order) do
|
||||
local has_projection = false
|
||||
for _, atom in ipairs((src.scan or {}).atoms or {}) do
|
||||
if atom.paths then has_projection = true; break end
|
||||
end
|
||||
if not has_projection then
|
||||
for _, atom in ipairs((src.scan or {}).raw_atoms or {}) do
|
||||
if atom.paths then has_projection = true; break end
|
||||
end
|
||||
end
|
||||
if has_projection then
|
||||
local basename = duffle.basename_no_ext(src.path)
|
||||
|
||||
-- (1) atoms.sourcemap.txt — format-1 per-word call-site map.
|
||||
local sourcemap_path = ctx.out_root .. "/" .. basename .. ".atoms.sourcemap.txt"
|
||||
local sourcemap_body = render_source_map(src)
|
||||
|
||||
-- (2) atoms.provenance.txt — format-1 per-word definition/body map.
|
||||
local prov_path = ctx.out_root .. "/" .. basename .. ".atoms.provenance.txt"
|
||||
local prov_body = render_provenance(src, wc)
|
||||
|
||||
if not ctx.dry_run then
|
||||
duffle.ensure_dir(duffle.dirname(sourcemap_path))
|
||||
duffle.write_file_lf(sourcemap_path, sourcemap_body)
|
||||
duffle.write_file_lf(prov_path, prov_body)
|
||||
end
|
||||
|
||||
outputs[#outputs + 1] = { kind = "report", path = sourcemap_path }
|
||||
outputs[#outputs + 1] = { kind = "report", path = prov_path }
|
||||
end
|
||||
end
|
||||
|
||||
-- Optionally emit the gdb-runtime form (post-link, one file per build).
|
||||
|
||||
+140
-76
@@ -108,8 +108,8 @@ local M = {}
|
||||
--- We then verify the preceding context ends with `MipsAtom`
|
||||
--- (the function-decl keyword with possible qualifiers between).
|
||||
---
|
||||
--- @param source string
|
||||
--- @param name string
|
||||
--- @param source string
|
||||
--- @param name string
|
||||
--- @param before_pos integer
|
||||
--- @return string|nil
|
||||
local function find_function_args_for(source, name, before_pos)
|
||||
@@ -149,7 +149,7 @@ end
|
||||
--- Used to copy signature comments from the source declaration (`MipsAtomComp_` / `MipsAtomComp_Proc_` / function decl)
|
||||
--- over to the generated `mac_X` macro, so LSP/IntelliSense displays the args doc.
|
||||
--- @param source string
|
||||
--- @param pos integer
|
||||
--- @param pos integer
|
||||
--- @return string
|
||||
local function preceding_comment_block(source, pos)
|
||||
local scan_pos = pos
|
||||
@@ -266,12 +266,12 @@ end
|
||||
-- Component projection (read from pre-scanned SourceScan)
|
||||
-- ════════════════════════════════════════════════════════════════════════════
|
||||
|
||||
-- Project pre-scanned MipsAtomComp_ / MipsAtomComp_Proc_ entries into Component shape.
|
||||
-- Does per-source backward lookups for args (preceding function decl) and comment (preceding comment block).
|
||||
-- Carries `body_tokens` forward from scan-source so word_count_rec reads from the precomputed table instead of calling duffle.tokenize_body again.
|
||||
-- @param source string -- the full source text (needed for backward lookups)
|
||||
-- @param scan table -- SourceScan from duffle.scan_source
|
||||
-- @return Component[]
|
||||
--- Project pre-scanned MipsAtomComp_ / MipsAtomComp_Proc_ entries into Component shape.
|
||||
--- Does per-source backward lookups for args (preceding function decl) and comment (preceding comment block).
|
||||
--- Carries `body_tokens` forward from scan-source so word_count_rec reads from the precomputed table instead of calling duffle.tokenize_body again.
|
||||
--- @param source string -- the full source text (needed for backward lookups)
|
||||
--- @param scan table -- SourceScan from duffle.scan_source
|
||||
--- @return Component[]
|
||||
local function project_components(source, scan)
|
||||
local out = {}
|
||||
for _, a in ipairs(scan.atoms) do
|
||||
@@ -282,6 +282,7 @@ local function project_components(source, scan)
|
||||
line = a.line,
|
||||
name = a.name,
|
||||
body = a.body,
|
||||
body_off = a.body_off,
|
||||
body_tokens = a.body_tokens,
|
||||
args = args,
|
||||
comment = comment,
|
||||
@@ -339,11 +340,11 @@ end
|
||||
-- Word-count computation (memoized recursive lookup)
|
||||
-- ════════════════════════════════════════════════════════════════════════════
|
||||
|
||||
-- Strip the `mac_` prefix from a component-call ident so we can look it up against the components-by-name table.
|
||||
-- Returns the ident unchanged if it doesn't start with the prefix
|
||||
-- (so a non-component ident like `mask_upper` falls through to the wc-table branch).
|
||||
-- @param ident string|nil
|
||||
-- @return string|nil
|
||||
--- Strip the `mac_` prefix from a component-call ident so we can look it up against the components-by-name table.
|
||||
--- Returns the ident unchanged if it doesn't start with the prefix
|
||||
--- (so a non-component ident like `mask_upper` falls through to the wc-table branch).
|
||||
--- @param ident string|nil
|
||||
--- @return string|nil
|
||||
local function strip_mac_prefix(ident)
|
||||
if not ident then return nil end
|
||||
if ident:sub(1, MAC_PREFIX_LEN) == MAC_PREFIX then
|
||||
@@ -352,13 +353,13 @@ local function strip_mac_prefix(ident)
|
||||
return ident
|
||||
end
|
||||
|
||||
-- (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<string, Component>
|
||||
-- @param wc table<string, integer>
|
||||
-- @param cache table<string, integer>
|
||||
-- @return integer
|
||||
--- (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<string, Component>
|
||||
--- @param wc table<string, integer>
|
||||
--- @param cache table<string, integer>
|
||||
--- @return integer
|
||||
local function word_count_rec(name, comp_by_name, wc, cache)
|
||||
if cache[name] ~= nil then return cache[name] end
|
||||
cache[name] = -1 -- mark in-progress (cycle detection)
|
||||
@@ -397,7 +398,7 @@ end
|
||||
--- references hit memoized values instead of re-walking the body.
|
||||
--- Cycle detection (A -> B -> A) is preserved via the in-progress `-1` sentinel in `cache`.
|
||||
--- @param components Component[]
|
||||
--- @param wc table<string, integer>
|
||||
--- @param wc table<string, integer>
|
||||
--- @return table<string, integer> -- map of component name (without `mac_`) -> word count
|
||||
local function count_all_components(components, wc)
|
||||
local comp_by_name = {}
|
||||
@@ -470,9 +471,9 @@ end
|
||||
|
||||
--- Build the list of lines for one component
|
||||
--- (signature comment, `#define mac_X(...)` line with backslash-continued tokens, then `WORD_COUNT(mac_X, N)` entry).
|
||||
--- @param c Component
|
||||
--- @param c Component
|
||||
--- @param components Component[]
|
||||
--- @param wc table<string, integer>
|
||||
--- @param wc table<string, integer>
|
||||
--- @return string[] -- list of lines for this component
|
||||
local function build_component_lines(c, counts)
|
||||
local lines = {}
|
||||
@@ -504,10 +505,10 @@ end
|
||||
-- Per-source emit logic
|
||||
-- ════════════════════════════════════════════════════════════════════════════
|
||||
|
||||
-- Build the boilerplate header lines (the `#ifdef INTELLISENSE_DIRECTIVES` block,
|
||||
-- the `// Auto-generated` comment, the `// Source:` line, and the self-contained `WORD_COUNT` macro definition).
|
||||
-- @param src SourceFile
|
||||
-- @return string[]
|
||||
--- Build the boilerplate header lines (the `#ifdef INTELLISENSE_DIRECTIVES` block,
|
||||
--- the `// Auto-generated` comment, the `// Source:` line, and the self-contained `WORD_COUNT` macro definition).
|
||||
--- @param src SourceFile
|
||||
--- @return string[]
|
||||
local function header_boilerplate(src)
|
||||
return {
|
||||
-- #pragma once wrapped in #ifdef INTELLISENSE_DIRECTIVES, matching the convention in lottes_tape.h.
|
||||
@@ -529,13 +530,13 @@ local function header_boilerplate(src)
|
||||
}
|
||||
end
|
||||
|
||||
-- Compute the output path for one source's `.macs.h` file.
|
||||
-- The pre-rework convention uses the *directory* basename
|
||||
-- (not the source file basename) e.g. `code/duffle/lottes_tape.h` produces `code/duffle/gen/duffle.macs.h`.
|
||||
-- This matches what the C codebase #includes.
|
||||
-- @param src SourceFile
|
||||
-- @return string -- the output directory
|
||||
-- @return string -- the full output path
|
||||
--- Compute the output path for one source's `.macs.h` file.
|
||||
--- The pre-rework convention uses the *directory* basename (not the source file basename)
|
||||
--- e.g. `code/duffle/lottes_tape.h` produces `code/duffle/gen/duffle.macs.h`.
|
||||
--- This matches what the C codebase #includes.
|
||||
--- @param src SourceFile
|
||||
--- @return string -- the output directory
|
||||
--- @return string -- the full output path
|
||||
local function compute_macs_h_path(src)
|
||||
local out_dir = src.dir .. "/" .. GEN_SUBDIR
|
||||
local out_path = out_dir .. "/" .. duffle.basename_no_ext(src.dir) .. ".macs.h"
|
||||
@@ -545,10 +546,10 @@ end
|
||||
--- Emit a per-source `.macs.h` header with the `mac_X` macros + `WORD_COUNT` entries.
|
||||
--- Writes in BINARY mode so LF line endings are preserved (the git blob is LF; Windows text-mode would emit CRLF and break the byte-identical diff).
|
||||
--- Honors `ctx.dry_run`: prints the intended path but does not write the file.
|
||||
--- @param ctx PassCtx
|
||||
--- @param src SourceFile
|
||||
--- @param ctx PassCtx
|
||||
--- @param src SourceFile
|
||||
--- @param components Component[]
|
||||
--- @param counts table<string, integer> -- precomputed word counts (from count_all_components)
|
||||
--- @param counts table<string, integer> -- 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, counts)
|
||||
if #components == 0 then return nil end
|
||||
@@ -577,41 +578,86 @@ end
|
||||
-- Pass entry
|
||||
-- ════════════════════════════════════════════════════════════════════════════
|
||||
|
||||
-- (internal) Extend `ctx.shared.word_counts` with this source's component macros so offsets sees them without re-reading the file.
|
||||
-- @param ctx PassCtx
|
||||
-- @param components Component[]
|
||||
-- @param counts table<string, integer> -- precomputed word counts (from count_all_components)
|
||||
local function update_shared_word_counts(ctx, components, counts)
|
||||
local wc = ctx.shared.word_counts
|
||||
--- (internal) Extend the canonical `corpus.word_counts` with this source's component macros so offsets sees them without re-reading the file.
|
||||
--- First declaration wins: a later caller's count is dropped (the existing entry from the first source is preserved).
|
||||
--- @param corpus table -- the canonical corpus
|
||||
--- @param components Component[]
|
||||
--- @param counts table<string, integer> -- precomputed word counts (from count_all_components)
|
||||
local function update_canonical_word_counts(corpus, components, counts)
|
||||
local wc = corpus.word_counts
|
||||
for _, c in ipairs(components) do
|
||||
wc["mac_" .. c.name] = counts[c.name]
|
||||
local key = "mac_" .. c.name
|
||||
if wc[key] == nil then
|
||||
wc[key] = counts[c.name]
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
--- @class ComponentDef
|
||||
--- @field name string -- bare name (without ac_/mac_ prefix)
|
||||
--- @field line integer -- definition source line (line of `MipsAtomComp_(ac_X)` / `MipsAtomComp_Proc_(ac_X, ...)`)
|
||||
--- @field path string -- absolute source path of the definition
|
||||
--- @field kind string -- "comp_bare" | "comp_proc"
|
||||
--- @field name string -- bare name (without ac_/mac_ prefix)
|
||||
--- @field line integer -- definition source line (line of `MipsAtomComp_(ac_X)` / `MipsAtomComp_Proc_(ac_X, ...)`)
|
||||
--- @field path string -- absolute source path of the definition
|
||||
--- @field kind string -- "comp_bare" | "comp_proc"
|
||||
|
||||
--- (internal) Extend `ctx.shared.components` with this source's components-by-name map so downstream passes
|
||||
--- (atoms_source_map, dwarf_injection) can resolve `mac_X(...)` invocations back to their component definition file:line.
|
||||
--- provenance emission uses this to attribute each emitted `.word` to either a component macro or the enclosing atom body.
|
||||
-- @param ctx PassCtx
|
||||
-- @param src SourceFile
|
||||
-- @param components Component[]
|
||||
local function update_shared_components(ctx, src, components)
|
||||
ctx.shared.components = ctx.shared.components or {}
|
||||
--- (internal) Populate the canonical `corpus.components` projection with this source's components-by-name map.
|
||||
--- First declaration wins; later declarations of the same bare name are dropped and recorded as a collision via `corpus.collisions` (kind = "component").
|
||||
--- The pass does NOT write to `ctx.shared.components` (ownership follows the canonical contract).
|
||||
--- @param corpus table -- the canonical corpus
|
||||
--- @param src SourceFile
|
||||
--- @param components Component[]
|
||||
local function update_canonical_components(corpus, src, components)
|
||||
local rel_path = src.path:gsub("\\", "/")
|
||||
for _, c in ipairs(components) do
|
||||
-- Keyed by bare name (e.g. `yield`, `load_tri_indices`).
|
||||
-- The atoms_source_map pass strips the `mac_` prefix from the call site identifier before lookup.
|
||||
ctx.shared.components[c.name] = {
|
||||
name = c.name,
|
||||
line = c.line,
|
||||
path = rel_path,
|
||||
kind = c.kind or "comp_bare",
|
||||
}
|
||||
-- Keyed by bare name (e.g. `yield`, `load_tri_indices`).
|
||||
-- The atoms_source_map pass looks up components by bare name from the canonical corpus;
|
||||
-- `mac_` prefix lives at the call-site identifier and is stripped before lookup.
|
||||
if corpus.components[c.name] == nil then
|
||||
corpus.components[c.name] = {
|
||||
name = c.name,
|
||||
line = c.line,
|
||||
path = rel_path,
|
||||
kind = c.kind or "comp_bare",
|
||||
}
|
||||
else
|
||||
-- A second declaration of the same bare name: record a typed collision so static-analysis + the report can surface it.
|
||||
-- Identical-shape declarations (same path + line) do NOT record a collision (the first-wins entry already covers the case).
|
||||
local existing = corpus.components[c.name]
|
||||
if existing.path ~= rel_path or existing.line ~= c.line then
|
||||
local kind = c.kind or "comp_bare"
|
||||
local first_kind = existing.kind or "comp_bare"
|
||||
corpus.collisions[#corpus.collisions + 1] = {
|
||||
kind = "component",
|
||||
name = c.name,
|
||||
first_site = { path = existing.path, line = existing.line },
|
||||
conflicting_site = { path = rel_path, line = c.line },
|
||||
first_shape = "kind=" .. first_kind,
|
||||
conflicting_shape = "kind=" .. kind,
|
||||
}
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
--- (internal) Populate the canonical `corpus.component_body_index` projection with this source's body index entries.
|
||||
--- First declaration wins; later declarations are dropped (no separate collision record: the components collision is already surfaced by `update_canonical_components`).
|
||||
--- The pass does NOT write to `ctx.shared.component_body_index` (the legacy corpus owns this projection).
|
||||
--- @param corpus table -- the canonical corpus
|
||||
--- @param src SourceFile
|
||||
--- @param components Component[]
|
||||
--- @param scan table -- the SourceScan payload (for line_of)
|
||||
local function update_canonical_component_body_index(corpus, src, components, scan)
|
||||
local line_of = scan and scan.line_of
|
||||
for _, c in ipairs(components) do
|
||||
if corpus.component_body_index[c.name] == nil then
|
||||
corpus.component_body_index[c.name] = {
|
||||
body_tokens = c.body_tokens,
|
||||
body_off = c.body_off,
|
||||
line_of = line_of,
|
||||
source = src.path,
|
||||
declaration = c.line,
|
||||
kind = c.kind,
|
||||
}
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@@ -622,24 +668,42 @@ function M.run(ctx)
|
||||
local errors = {}
|
||||
local warnings = {}
|
||||
|
||||
-- Initialize shared component map.
|
||||
-- The atoms_source_map and dwarf_injection passes consume `ctx.shared.components` to resolve `mac_X(...)`
|
||||
-- invocations back to the component's definition file:line.
|
||||
ctx.shared.components = ctx.shared.components or {}
|
||||
-- Canonical-corpus ownership gate.
|
||||
local corpus = ctx.shared and ctx.shared.corpus
|
||||
if type(corpus) ~= "table" then
|
||||
error("components.run requires ctx.shared.corpus (canonical corpus).", 0)
|
||||
end
|
||||
if type(corpus.source_order) ~= "table" then
|
||||
error("components.run requires ctx.shared.corpus.source_order (canonical corpus).", 0)
|
||||
end
|
||||
if type(corpus.word_counts) ~= "table" then
|
||||
error("components.run requires ctx.shared.corpus.word_counts; "
|
||||
.. "word_count_eval.run must run before components.run "
|
||||
.. "(see PASSES deps).", 0)
|
||||
end
|
||||
|
||||
for _, src in ipairs(ctx.sources) do
|
||||
-- Canonical projection ownership:
|
||||
-- * `corpus.word_counts["mac_"..name]` — current component count
|
||||
-- * `corpus.components[name]` — bare-name component definition
|
||||
-- * `corpus.component_body_index[name]` — body / line_of / source index
|
||||
-- The pass does NOT mutate `ctx.shared.components` or `ctx.shared.component_body_index`
|
||||
-- (ownership follows the canonical corpus; consumers read from the corpus directly).
|
||||
|
||||
for _, src in ipairs(corpus.source_order) do
|
||||
-- project_components reads from src.scan + does backward lookups on src.text
|
||||
local components = project_components(src.text, src.scan)
|
||||
if #components > 0 then
|
||||
-- Compute word counts for ALL components once (was: rebuilt per call inside the helpers).
|
||||
local counts = count_all_components(components, ctx.shared.word_counts)
|
||||
-- Compute all component word counts once per source.
|
||||
-- Use `corpus.word_counts` (the canonical count table) so the recursive lookup sees both authored-metadata entries
|
||||
-- (loaded by word_count_eval.run) AND same-source component entries (populated earlier in this loop by `update_canonical_word_counts`).
|
||||
local counts = count_all_components(components, corpus.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, counts)
|
||||
-- share component definitions with downstream passes.
|
||||
-- `mac_X(...)` invocations in atom bodies resolve back to (path, line) via this map.
|
||||
update_shared_components(ctx, src, components)
|
||||
-- Populate the canonical projections AFTER disk emission (so the byte-identical `.macs.h` contract is preserved before any current-count mutation).
|
||||
update_canonical_word_counts(corpus, components, counts)
|
||||
update_canonical_components(corpus, src, components)
|
||||
update_canonical_component_body_index(corpus, src, components, src.scan)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
+164
-237
@@ -20,8 +20,8 @@
|
||||
--- Splice step runs from PowerShell — no Lua subprocess; no cmd /c parsing issues.
|
||||
--- objcopy's --update-section works fine in PowerShell even though Lua's `os.execute`/`io.popen` would mangle the `=` on Windows.)
|
||||
---
|
||||
--- Result: VSCode's source gutter follows per-stepi inside atom bodies, AND the Variables pane shows the wave-context regs as atom-scoped locals.
|
||||
--- Native VSCode UX (gutter arrow + highlighted line + Run to Cursor + conditional BPs by source line + per-atom locals).
|
||||
--- Result: source stepping follows atom-body lines, and wave-context registers appear as atom-scoped locals.
|
||||
--- Native VSCode stepping, line highlighting, run-to-cursor, conditional breakpoints, and per-atom locals.
|
||||
--- No VSCode plugin, no Python, no pyelftools — pure Lua + objcopy.
|
||||
---
|
||||
--- **Conventions:** tabs (1/level), EmmyLua annotations, Lua 5.3 compatible.
|
||||
@@ -37,19 +37,15 @@ local duffle = dofile(_bootstrap_dir .. "../duffle_paths.lua")
|
||||
|
||||
-- ELF32 / DWARF / atoms-source-map utilities (post-link debug-info injection).
|
||||
-- Sister module to duffle.lua — contains the format-constant tables (ELF32 byte offsets, DWARF opcodes, etc.) and the I/O helpers
|
||||
-- (read_elf_sections, nm, source-map parser, LE byte r/w). `list_dir` lives in duffle.lua as a general I/O primitive (lifted out during F'').
|
||||
-- (read_elf_sections, nm, source-map parser, LE byte r/w). `list_dir` is the general directory primitive in duffle.lua.
|
||||
local elf_dwarf = require("elf_dwarf")
|
||||
|
||||
-- word-counting helper shared with passes/atoms_source_map.lua.
|
||||
-- Used here to walk a component's body_tokens in lockstep with their word-count allocation
|
||||
-- when we propagate per-word body lines into each invocation's `body_lines` array.
|
||||
local word_count_eval = require("word_count_eval")
|
||||
local count_token_words = word_count_eval.count_token_words
|
||||
-- Per-word body lines come from the canonical `atom.paths` projection.
|
||||
|
||||
local lfs = require("lfs")
|
||||
|
||||
-- File-scope aliases to elf_dwarf helpers; the canonical implementations live in scripts/elf_dwarf.lua.
|
||||
-- (2-caller lift: these were duplicated file-locals; the canonical is in elf_dwarf.lua, used by parse_abbrev_table + read_form_value.)
|
||||
-- ELF decoding helpers come from `elf_dwarf.lua`.
|
||||
local read_uleb128_at = elf_dwarf.read_uleb128_at
|
||||
local read_sleb128_at = elf_dwarf.read_sleb128_at
|
||||
local find_abbrev_table_end = elf_dwarf.find_abbrev_table_end
|
||||
@@ -97,7 +93,7 @@ local ATOM_SOURCE_FILE_INDEX = 11
|
||||
-- New abbreviation codes (100+ to avoid collision with gcc's existing 1-60+ codes).
|
||||
local ABBREV_CU = 0x64 -- 100: DW_TAG_compile_unit
|
||||
local ABBREV_SUBPROGRAM = 0x65 -- 101: DW_TAG_subprogram
|
||||
local ABBREV_VARIABLE = 0x66 -- 102: DW_TAG_variable (DW_AT_type = ref4 to U4; was missing pre-2026-07-13 → gdb resolved R_PrimCursor against the C-level enum, not the register)
|
||||
local ABBREV_VARIABLE = 0x66 -- 102: DW_TAG_variable with DW_AT_type = ref4 to U4
|
||||
local ABBREV_STRUCT_TYPE = 0x67 -- 103: DW_TAG_structure_type with children (Binds_X mirror)
|
||||
local ABBREV_MEMBER = 0x68 -- 104: DW_TAG_member no children (DW_AT_type = ref4 to U4 base)
|
||||
local ABBREV_BIND_VAR = 0x69 -- 105: DW_TAG_variable no children + DW_AT_type = ref4 (the bind_args variable)
|
||||
@@ -150,10 +146,10 @@ local DW_AT_language = 0x13
|
||||
local DW_AT_location = 0x02
|
||||
local DW_AT_comp_dir = 0x1B
|
||||
local DW_AT_byte_size = 0x0B
|
||||
local DW_AT_encoding = 0x3E -- DWARF5 §7.7.1: DW_AT_encoding (for DW_ATE_unsigned base type; was 0x13 = DW_AT_language in prior slice - semantically wrong)
|
||||
local DW_AT_encoding = 0x3E -- DWARF5 §7.7.1: DW_AT_encoding for the DW_ATE_unsigned base type
|
||||
local DW_AT_data_member_location = 0x38
|
||||
local DW_AT_type = 0x49
|
||||
local DW_AT_linkage_name = 0x6E -- DWARF5 §7.7.1: DW_AT_linkage_name (standard form; 0x200027 was the GNU extension form - wrong vs DW_FORM_string abbrev)
|
||||
local DW_AT_linkage_name = 0x6E -- DWARF5 §7.7.1: DW_AT_linkage_name with DW_FORM_string
|
||||
local DW_AT_external = 0x3F -- marks a variable/function as externally visible
|
||||
-- Inlined_subroutine + abstract_origin attributes.
|
||||
local DW_AT_abstract_origin = 0x31
|
||||
@@ -341,7 +337,7 @@ end
|
||||
local DEFAULT_CU_NAME = "tape_atom_locals"
|
||||
local DEFAULT_CU_COMP_DIR = "."
|
||||
|
||||
-- Path templates for the .bin outputs are now in SECTION_WRITERS (see below).
|
||||
-- SECTION_WRITERS owns the .bin output path templates.
|
||||
|
||||
-- Default basename if not provided via ctx.
|
||||
local DEFAULT_BASENAME = "hello_gte"
|
||||
@@ -367,11 +363,12 @@ end
|
||||
--- Consume the per-source scanner associations without naming any atom or component in production.
|
||||
--- Whole atoms remain symbol-keyed; components are file-qualified internally so a source marker associates
|
||||
--- with its exact component definition even though GDB 12 requires function-only skip entries for the resulting synthetic inline frame.
|
||||
--- @param ctx DwarfInjectionCtx
|
||||
--- Iterates `corpus.source_order` (the canonical corpus projection).
|
||||
--- @param corpus table -- the canonical corpus from `ctx.shared.corpus`
|
||||
--- @return table -- {atoms = {[symbol] = association}, components = {[file|name] = association}}
|
||||
local function collect_skip_over(ctx)
|
||||
local function collect_skip_over(corpus)
|
||||
local skip_over = { atoms = {}, components = {} }
|
||||
for _, src in ipairs(ctx.sources or {}) do
|
||||
for _, src in ipairs((corpus and corpus.source_order) or {}) do
|
||||
local scan_skip = src.scan and src.scan.skip_over
|
||||
if scan_skip then
|
||||
for atom_name, association in pairs(scan_skip.atoms or {}) do
|
||||
@@ -396,53 +393,50 @@ local function collect_skip_over(ctx)
|
||||
return skip_over
|
||||
end
|
||||
|
||||
--- Merge the per-source scanner registries (register_alias_registry, type_name_registry, atom_views)
|
||||
--- into a single set of tables that downstream consumers can read from without re-iterating ctx.sources.
|
||||
--- Project the canonical corpus registries into the shape the section builders expect.
|
||||
--- The corpus already owns the merged `register_alias_registry`, `type_name_registry`, `atom_views`, `atom_ctxs`, `atom_phases`, and `atom_infos` projections (populated by `passes.scan_source.lua`).
|
||||
--- This helper just references them so the rest of `dwarf_injection.lua` keeps the same `registries.<key>` access shape it has always used.
|
||||
---
|
||||
--- Every `R_*` lookup and per-atom type override resolution in this file goes through this merged table.
|
||||
--- Aliases without `atom_reg` adjacent are absent; the absence is treated as "not debug-visible" (see build_inserted_children for the precedence chain).
|
||||
---
|
||||
--- When two sources register the same key, the last-writer wins (later sources override earlier).
|
||||
--- Today only one source declares wave-context enums, so collisions are absent.
|
||||
--- @param ctx DwarfInjectionCtx
|
||||
--- @param corpus table -- the canonical corpus from `ctx.shared.corpus`
|
||||
--- @return table -- {
|
||||
--- register_alias_registry = {[R_Name] = AliasEntry},
|
||||
--- type_name_registry = {[T] = TypeEntry},
|
||||
--- atom_views = {[atom_name] = AtomViewEntry},
|
||||
--- }
|
||||
local function collect_per_source_registries(ctx)
|
||||
local merged = {
|
||||
register_alias_registry = {},
|
||||
type_name_registry = {},
|
||||
atom_views = {},
|
||||
local function collect_per_source_registries(corpus)
|
||||
-- The corpus already holds the merged registries; reference them directly.
|
||||
-- No per-source iteration is needed because `passes.scan_source.lua` has already folded every per-source scan into the canonical tables.
|
||||
-- `atom_infos` is preserved byte-for-byte with no filtering; consumers consult `corpus.atoms_by_name`
|
||||
-- themselves when they need to know whether a particular atom_info corresponds to an actual atom record.
|
||||
local atom_infos_list = {}
|
||||
for _, ai in ipairs((corpus and corpus.atom_infos) or {}) do
|
||||
atom_infos_list[#atom_infos_list + 1] = ai
|
||||
end
|
||||
return {
|
||||
register_alias_registry = (corpus and corpus.register_alias_registry) or {},
|
||||
type_name_registry = (corpus and corpus.type_name_registry) or {},
|
||||
atom_views = (corpus and corpus.atom_views) or {},
|
||||
-- Per-atom atom_ctx declarations: atom_name -> {rbind_atom, ...}
|
||||
-- (populated by scan_source from `atom_ctx(<atom_name>)` sub-calls inside `atom_info`)
|
||||
atom_ctxs = {},
|
||||
atom_ctxs = (corpus and corpus.atom_ctxs) or {},
|
||||
-- Per-phase atom groups: phase_label -> {atoms = {atom_name1, ...}}
|
||||
-- (populated by scan_source from `atom_phase(<label>)` sub-calls inside `atom_info`; cross-source merged)
|
||||
atom_phases = {},
|
||||
-- Per-source already-resolved atom_infos (used by the precedence chain's ctx/phase steps)
|
||||
atom_infos = {},
|
||||
atom_phases = (corpus and corpus.atom_phases) or {},
|
||||
-- Corpus-wide atom_infos list, byte-for-byte.
|
||||
atom_infos = atom_infos_list,
|
||||
}
|
||||
for _, src in ipairs(ctx.sources or {}) do
|
||||
local scan = src.scan
|
||||
if scan then
|
||||
for k, v in pairs(scan.register_alias_registry or {}) do merged.register_alias_registry[k] = v end
|
||||
for k, v in pairs(scan.type_name_registry or {}) do merged.type_name_registry[k] = v end
|
||||
for k, v in pairs(scan.atom_views or {}) do merged.atom_views[k] = v end
|
||||
for k, v in pairs(scan.atom_ctxs or {}) do merged.atom_ctxs[k] = v end
|
||||
for k, v in pairs(scan.atom_phases or {}) do merged.atom_phases[k] = v end
|
||||
for _, ai in ipairs(scan.atom_infos or {}) do merged.atom_infos[#merged.atom_infos + 1] = ai end
|
||||
end
|
||||
end
|
||||
return merged
|
||||
end
|
||||
|
||||
--- Render deterministic debugger skip commands. Ordering is stable by category:
|
||||
--- exact atom symbols first (lexicographic), then exact component function names (lexicographic full command).
|
||||
--- Render deterministic debugger skip commands.
|
||||
--- Ordering is stable by category: exact atom symbols first (lexicographic), then exact component function names (lexicographic full command).
|
||||
--- Atom commands come from the matched nm/source-map table so the emitted name is the actual ELF symbol.
|
||||
--- The scanner tables and command set both deduplicate repeated source observations.
|
||||
--- @param skip_over table
|
||||
--- @param skip_over table
|
||||
--- @param atom_table table[] -- nm/source-map cross-reference; names are actual ELF symbols
|
||||
--- @return string
|
||||
local function build_gdbinit(skip_over, atom_table)
|
||||
@@ -474,7 +468,7 @@ end
|
||||
-- LEB128 encoders
|
||||
-- ════════════════════════════════════════════════════════════════════════════
|
||||
--
|
||||
-- Lifted to `elf_dwarf.uleb128` + `elf_dwarf.sleb128` (F'' refactor).
|
||||
-- Uses `elf_dwarf.uleb128` and `elf_dwarf.sleb128`.
|
||||
-- See those helpers for the bit-layout documentation + named constants (LEB_CONT_BIT, LEB_DATA_MASK, SLEB_SIGN_BIT).
|
||||
-- File-scope `local uleb128` + `local sleb128` aliases live near the module top so they're resolvable by every function below.
|
||||
|
||||
@@ -686,162 +680,94 @@ end
|
||||
-- ════════════════════════════════════════════════════════════════════════════
|
||||
|
||||
--- Build the atom table the section builders consume.
|
||||
--- Cross-references nm symbols with source-map.txt entries; sorted by addr.
|
||||
--- Also consumes the provenance file to record per-component invocations.
|
||||
--- Each atom gains an `invocations` field with one entry per `mac_X(...)` call site:
|
||||
--- `{comp_name, call_file, call_line, comp_file, comp_line, start_pos, end_pos, body_lines}`.
|
||||
--- Cross-references nm symbols with `corpus.atoms_by_name` and derives word rows + format-1 outermost invocation rows from `atom.paths`.
|
||||
---
|
||||
--- `body_lines` is the per-word source line within the macro body
|
||||
--- (lottes_tape.h:N where N is the actual line of this `.word` in the macro expansion).
|
||||
--- Without this field, the line program emits `comp_line` for EVERY body word,
|
||||
--- so gdb's `step` from a `mac_X(...)` call lands on the macro signature line and immediately
|
||||
--- returns without traversing the body (since no PC reports a different line).
|
||||
--- The atom table is built entirely from in-memory state — disk source-map and provenance text artifacts are NOT consulted.
|
||||
--- Those artifacts are diagnostic outputs, not semantic inputs; the DWARF injection pass must remain correct regardless of their on-disk content.
|
||||
---
|
||||
--- The data is computed by walking the component's pre-tokenized body in `ctx.sources[i].scan.atoms[j]`
|
||||
--- (the MipsAtomComp_/MipsAtomComp_Proc_ declaration).
|
||||
--- Atom labels (`atom_label(...)`) emit 0 `.word`s and are ignored (matching `passes/atoms_source_map.lua :: is_marker_token` + `count_marker_rest`).
|
||||
--- @param ctx DwarfInjectionCtx
|
||||
--- @param skip_over table -- {atoms = {[symbol] = association}, components = {[file|name] = association}}
|
||||
--- The result shape (one entry per ELF symbol matched against the corpus):
|
||||
--- `{name, addr, size_bytes, words, entries, invocations, skip_over?}`
|
||||
--- where:
|
||||
--- * `entries[i].pos` — 0-based `.word` position (matches the source-map format-1 row layout; downstream DWARF builders compare against this).
|
||||
--- * `entries[i].line` — call-site line for that word.
|
||||
--- * `entries[i].text` — trimmed encoder token text from `atom.paths.word_events`.
|
||||
--- * `invocations[j]` — one entry per format-1 outermost `mac_X(...)` invocation with
|
||||
--- `{comp_name, call_file, call_line, comp_file, comp_line, start_pos, end_pos, body_lines, skip_over}`. `body_lines[k]`
|
||||
--- is the k-th word's source line within the component body.
|
||||
---
|
||||
--- @param corpus table -- the canonical corpus from `ctx.shared.corpus`
|
||||
--- @param addrs table -- ELF symbols keyed by atom name from `elf_dwarf.read_nm`
|
||||
--- @param skip_over table -- {atoms = {[symbol] = association}, components = {[file|name] = association}}
|
||||
--- @return table[] -- list of {name, addr, size_bytes, words, entries, invocations, skip_over?}
|
||||
local function build_atom_table(ctx, skip_over)
|
||||
local basename = ctx.basename or DEFAULT_BASENAME
|
||||
-- Source-map path: convention matches the α MVP's emission location.
|
||||
-- writes `<out_root>/<basename>.atoms.sourcemap.txt` (e.g. `build/gen/hello_gte_tape.atoms.sourcemap.txt`).
|
||||
-- But ctx.out_root is `build/gen` (the per-build output root) and basename defaults to `hello_gte`.
|
||||
-- The actual file emitted today is per-source; we look for any `*.atoms.sourcemap.txt` in out_root.
|
||||
local sm_files = duffle.list_dir(ctx.out_root, "%.atoms.sourcemap%.txt$")
|
||||
if #sm_files == 0 then
|
||||
io.stderr:write(string.format(
|
||||
"[dwarf_injection] no *.atoms.sourcemap.txt in %s; need atoms-source-map pass first\n",
|
||||
ctx.out_root))
|
||||
return {}
|
||||
end
|
||||
local function build_atom_table(corpus, addrs, skip_over)
|
||||
local atoms_by_name = corpus.atoms_by_name or {}
|
||||
|
||||
-- Read nm + merge all source-map files.
|
||||
local addrs = elf_dwarf.read_nm(ctx.flags.elf_path)
|
||||
local merged = {}
|
||||
for _, sm_path in ipairs(sm_files) do
|
||||
local sm = elf_dwarf.parse_source_map_file(sm_path, 1)
|
||||
for name, sm_data in pairs(sm) do
|
||||
merged[name] = sm_data
|
||||
end
|
||||
end
|
||||
|
||||
-- Also read *.atoms.provenance.txt to extract per-component invocations.
|
||||
-- Files are merged by atom name; entries carry the original {pos, call_file, call_line, comp_name, comp_file, comp_line} shape.
|
||||
local prov_files = duffle.list_dir(ctx.out_root, "%.atoms.provenance%.txt$")
|
||||
local prov_merged = {}
|
||||
for _, prov_path in ipairs(prov_files) do
|
||||
local prov = elf_dwarf.parse_provenance_file(prov_path, 1)
|
||||
for name, prov_data in pairs(prov) do
|
||||
prov_merged[name] = prov_data
|
||||
end
|
||||
end
|
||||
|
||||
-- Build a per-source component index keyed by the bare component name (e.g. `gte_load_tri_verts`, NOT `ac_gte_load_tri_verts`).
|
||||
-- The bare name matches the provenance row's `comp_name` field (which is `strip_mac_prefix_from_token(tok)` — strips `mac_`, leaves the rest).
|
||||
-- Each entry holds the data we need to walk the component body's tokens in lockstep with their word counts:
|
||||
-- body_off -- byte offset of the `{` (start of body) in the component's source file.
|
||||
-- body_tokens -- list of {tok, rel} pairs; `rel` is the byte offset within the body.
|
||||
-- line_of -- closure resolving byte offsets in the component's source file to lines.
|
||||
-- The data is consumed by `compute_invocation_body_lines` per invocation.
|
||||
local component_index = {}
|
||||
for _, src in ipairs(ctx.sources or {}) do
|
||||
if src.scan and src.scan.atoms then
|
||||
local line_of = src.scan.line_of
|
||||
for _, atom in ipairs(src.scan.atoms) do
|
||||
if atom.kind == "comp_bare" or atom.kind == "comp_proc" then
|
||||
-- Prefer `atom.name` (stripped of `ac_` prefix); fall back to `raw_name`
|
||||
-- only if `name` is missing (defensive; scan-source always sets both).
|
||||
local name = atom.name or atom.raw_name
|
||||
if name and not component_index[name] then
|
||||
component_index[name] = {
|
||||
body_off = atom.body_off,
|
||||
body_tokens = atom.body_tokens,
|
||||
line_of = line_of,
|
||||
source_path = src.path,
|
||||
}
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
-- Per-word line lookup for a component body: walk body_tokens, count each token's emitted .words via count_token_words, attribute that count the same source line.
|
||||
-- Atom labels (atom_label/atom_offset) emit 0 .words; their lines are skipped to stay aligned with `passes/atoms_source_map.lua :: count_marker_rest`.
|
||||
-- @param comp_name string -- the bare name (e.g. `gte_load_tri_verts`)
|
||||
-- @return table -- list of source lines, 1-based by word position; empty if no data
|
||||
local wc = (ctx.shared and ctx.shared.word_counts) or {}
|
||||
local function compute_invocation_body_lines(comp_name)
|
||||
local comp_idx = component_index[comp_name]
|
||||
if not (comp_idx and comp_idx.body_tokens and comp_idx.line_of) then return {} end
|
||||
local lines = {}
|
||||
for _, bt in ipairs(comp_idx.body_tokens) do
|
||||
local tok = duffle.trim(bt.tok or "")
|
||||
if tok ~= "" then
|
||||
-- Match atoms_source_map.lua's marker check (no public export; duplicated for independence).
|
||||
local leading = duffle.read_ident(tok, 1)
|
||||
local words
|
||||
if leading == "atom_label" or leading == "atom_offset" then
|
||||
words = 0 -- markers emit 0 .words; do not advance the body line counter.
|
||||
else
|
||||
words = count_token_words(tok, wc)
|
||||
end
|
||||
if words > 0 then
|
||||
local body_line = comp_idx.line_of(comp_idx.body_off + bt.rel)
|
||||
for _ = 1, words do lines[#lines + 1] = body_line end
|
||||
end
|
||||
end
|
||||
end
|
||||
return lines
|
||||
end
|
||||
|
||||
-- Cross-ref; keep atoms that exist in both.
|
||||
-- Cross-ref: keep only the atoms that exist in BOTH the nm symbol table AND the canonical corpus projection.
|
||||
-- Address-ascending sort + lexical Stable tie-breaker: declaration order, then symbol address.
|
||||
local out = {}
|
||||
for name, info in pairs(addrs) do
|
||||
local sm = merged[name]
|
||||
if sm then
|
||||
local atom_record = atoms_by_name[name]
|
||||
if atom_record then
|
||||
local paths = atom_record.paths or {}
|
||||
local word_events = paths.word_events or {}
|
||||
local invocations_proj = paths.invocations or {}
|
||||
|
||||
-- Build the dense entries list from `word_events`. `word_events[i].i` is the 0-based `.word` position;
|
||||
-- `call_line` is the root atom's physical source line for that word (stamped by emission_model).
|
||||
local entries = {}
|
||||
for idx, ev in ipairs(word_events) do
|
||||
entries[#entries + 1] = {
|
||||
pos = ev.i or (idx - 1),
|
||||
line = ev.call_line or 0,
|
||||
text = ev.call_text or "",
|
||||
}
|
||||
end
|
||||
|
||||
local atom = {
|
||||
name = name,
|
||||
addr = info[1],
|
||||
size_bytes = info[2],
|
||||
words = sm.total,
|
||||
entries = sm.words,
|
||||
skip_over = skip_over.atoms[name] ~= nil,
|
||||
words = #word_events,
|
||||
entries = entries,
|
||||
skip_over = skip_over.atoms[name] ~= nil,
|
||||
}
|
||||
-- Group consecutive MACRO rows in this atom's provenance into invocations.
|
||||
-- An invocation = one `mac_X(...)` call site spanning N consecutive .word rows.
|
||||
-- Two consecutive rows with the same (comp_name, call_file, call_line, comp_file, comp_line) are part of the same invocation.
|
||||
local prov_data = prov_merged[name]
|
||||
if prov_data and prov_data.words then
|
||||
|
||||
-- Group consecutive `word_events` rows whose outermost invocation
|
||||
-- is the SAME format-1 invocation into a single `atom.invocations`
|
||||
-- entry. Keep entries grouped by outermost invocation
|
||||
-- (two consecutive rows with the same comp_name/call_file/call_line/
|
||||
-- comp_file/comp_line are part of the same invocation).
|
||||
if #invocations_proj > 0 then
|
||||
local invocations = {}
|
||||
local cur_inv = nil
|
||||
for _, w in ipairs(prov_data.words) do
|
||||
if w.comp_name then
|
||||
local inv_key = w.comp_name .. "|" .. w.call_file .. "|" .. w.call_line .. "|" .. w.comp_file .. "|" .. w.comp_line
|
||||
for _, ev in ipairs(word_events) do
|
||||
local outer_id = ev.outermost_invocation_id
|
||||
local outer_inv = outer_id and invocations_proj[outer_id] or nil
|
||||
if outer_inv and outer_inv.component_name then
|
||||
local inv_key = outer_inv.component_name
|
||||
.. "|" .. (outer_inv.call_path or "")
|
||||
.. "|" .. tostring(outer_inv.call_line or 0)
|
||||
.. "|" .. (outer_inv.def_path or "")
|
||||
.. "|" .. tostring(outer_inv.def_line or 0)
|
||||
local ev_pos = ev.i or 0
|
||||
if cur_inv and cur_inv.key == inv_key then
|
||||
-- Same invocation as the previous word — extend its range.
|
||||
cur_inv.end_pos = w.pos
|
||||
cur_inv.end_pos = ev_pos
|
||||
cur_inv.body_lines[#cur_inv.body_lines + 1] = ev.body_line or 0
|
||||
else
|
||||
-- New invocation: flush the previous one and start fresh.
|
||||
if cur_inv then invocations[#invocations + 1] = cur_inv end
|
||||
cur_inv = {
|
||||
key = inv_key,
|
||||
comp_name = w.comp_name,
|
||||
call_file = w.call_file,
|
||||
call_line = w.call_line,
|
||||
comp_file = w.comp_file,
|
||||
comp_line = w.comp_line,
|
||||
start_pos = w.pos,
|
||||
end_pos = w.pos,
|
||||
-- component_skip_key: case-insensitive Windows path + "\0" separator + exact component-name.
|
||||
-- (Inlined from `component_skip_key`; the lookup is in skip_over.components keyed by the result.)
|
||||
skip_over = skip_over.components[normalize_debug_path(w.comp_file):lower() .. "\0" .. w.comp_name] ~= nil,
|
||||
comp_name = outer_inv.component_name,
|
||||
call_file = outer_inv.call_path or "",
|
||||
call_line = outer_inv.call_line or 0,
|
||||
comp_file = outer_inv.def_path or "",
|
||||
comp_line = outer_inv.def_line or 0,
|
||||
start_pos = ev_pos,
|
||||
end_pos = ev_pos,
|
||||
skip_over = skip_over.components[normalize_debug_path(outer_inv.def_path or ""):lower()
|
||||
.. "\0" .. outer_inv.component_name] ~= nil,
|
||||
body_lines = { ev.body_line or 0 },
|
||||
}
|
||||
-- Capture the per-word body lines for THIS invocation, indexed by 1-based word position within the invocation.
|
||||
-- body_lines[1] is the line of the first body word (= the line of the first macro-body token, NOT the comp def line).
|
||||
-- Downstream consumers (the line program emitter) fall back to comp_line when this is empty.
|
||||
cur_inv.body_lines = compute_invocation_body_lines(w.comp_name)
|
||||
end
|
||||
else
|
||||
-- RAW row: flush the current invocation.
|
||||
@@ -889,7 +815,6 @@ end
|
||||
-- We use the SourceScan payload populated by `passes/scan_source.lua` (the dep-closed upstream pass).
|
||||
-- That pass walks each source once and populates `src.scan.atom_infos` (the atom_info sub-call parse) and `src.scan.binds` (the Binds_X struct field parse).
|
||||
--
|
||||
-- The prior local `reparse_binds_body` fallback (and the 2nd source walk inside `parse_rbind_atoms`) is REMOVED.
|
||||
-- parse_rbind_atoms consumes `scan.binds[i].fields` directly, which is populated by the scan-source pass with the typed-field record ({name, type_name, pointer_depth, offset, byte_size}).
|
||||
|
||||
--- Find every `load_word(R_<reg>, R_TapePtr, O_(Binds_X, FieldName))` call in the atom body and return ordered (reg_index, field_name) pairs.
|
||||
@@ -905,8 +830,8 @@ end
|
||||
--- Pre-tokenized: `body_tokens` is the scan-source pass's pre-split list of top-level
|
||||
--- statements (each entry is a single `load_word(...)` call or other statement).
|
||||
--- @param body_tokens table[] -- the atom's pre-tokenized body statements (from atom.body_tokens)
|
||||
--- @param binds_name string -- expected Binds_X name (skip pairs with mismatching binds)
|
||||
--- @param registries table -- merged registries from collect_per_source_registries
|
||||
--- @param binds_name string -- expected Binds_X name (skip pairs with mismatching binds)
|
||||
--- @param registries table -- merged registries from collect_per_source_registries
|
||||
--- @return table[] -- list of {reg = <MIPS index>, field = <field name>}
|
||||
local function parse_body_load_pairs(body_tokens, binds_name, registries)
|
||||
local pairs = {}
|
||||
@@ -938,9 +863,7 @@ end
|
||||
|
||||
--- Collect every rbind atom + the matching Binds_X struct + (reg, field) pairs.
|
||||
---
|
||||
--- Inputs come from the dep-closed `scan-source` pass:
|
||||
--- ctx.sources[i].scan.atom_infos -- list of {atom_name, binds, reads, writes, info_line}
|
||||
--- ctx.sources[i].scan.binds -- list of {line, name, fields, bytes}
|
||||
--- Inputs come from the dep-closed `scan-source` pass (the per-source `src.scan` payload is preserved on each `corpus.source_order` entry).
|
||||
---
|
||||
--- Returns:
|
||||
--- rbind_atoms = {[atom_name] = {binds, fields, regs, byte_size, info_line}}
|
||||
@@ -949,22 +872,22 @@ end
|
||||
--- The `regs` list per atom is ordered: each entry is the MIPS reg index that holds the matching field in the source-order pop sequence.
|
||||
--- The piece chain uses (DW_OP_regN, DW_OP_piece, ULEB128(field_size)).
|
||||
---
|
||||
--- The 2nd source walk (the body-text `text:find("typedef Struct_(...)")` re-walk) is removed;
|
||||
--- Binds fields come from `scan.binds`; no body-text source walk is needed.
|
||||
--- per-source `scan.binds[i].fields` already carries the typed-field record after the scan-source generalization.
|
||||
--- @param ctx DwarfInjectionCtx
|
||||
--- @param atom_table table[] -- the cross-ref'd atom table from build_atom_table
|
||||
--- @param registries table -- merged registries from collect_per_source_registries
|
||||
--- @return table, table -- (rbind_atoms, rbind_structs)
|
||||
local function parse_rbind_atoms(ctx, atom_table, registries)
|
||||
--- @param corpus table -- the canonical corpus from `ctx.shared.corpus`
|
||||
--- @param atom_table table[] -- the cross-ref'd atom table from build_atom_table
|
||||
--- @param registries table -- merged registries from collect_per_source_registries
|
||||
--- @return table, table -- (rbind_atoms, rbind_structs)
|
||||
local function parse_rbind_atoms(corpus, atom_table, registries)
|
||||
registries = registries or {}
|
||||
local rbind_atoms = {}
|
||||
local rbind_structs = {}
|
||||
|
||||
-- Index binds by struct name; consume `scan.binds[i].fields` directly (no body-text re-walk).
|
||||
-- The scan-source pass emits each Binds_X's fields as {[type_name, pointer_depth, offset, byte_size, ...]}
|
||||
-- The scan-source pass emits each Binds_X's fields as {[type_name, pointer_depth, offset, byte_size, ...]}
|
||||
-- so this pass can build the rbind_structs entry without re-parsing.
|
||||
local binds_by_name = {}
|
||||
for _, src in ipairs(ctx.sources or {}) do
|
||||
for _, src in ipairs((corpus and corpus.source_order) or {}) do
|
||||
local scan = src.scan
|
||||
if scan then
|
||||
for _, b in ipairs(scan.binds or {}) do
|
||||
@@ -985,7 +908,7 @@ local function parse_rbind_atoms(ctx, atom_table, registries)
|
||||
|
||||
-- Walk every atom_info; if `binds` is set, find the atom body_tokens + parse load_word pairs.
|
||||
local body_tokens_by_atom = {}
|
||||
for _, src in ipairs(ctx.sources or {}) do
|
||||
for _, src in ipairs((corpus and corpus.source_order) or {}) do
|
||||
local scan = src.scan
|
||||
if scan then
|
||||
for _, atom in ipairs(scan.atoms or {}) do
|
||||
@@ -995,7 +918,7 @@ local function parse_rbind_atoms(ctx, atom_table, registries)
|
||||
end
|
||||
|
||||
local ai_by_atom = {}
|
||||
for _, src in ipairs(ctx.sources or {}) do
|
||||
for _, src in ipairs((corpus and corpus.source_order) or {}) do
|
||||
local scan = src.scan
|
||||
if scan then
|
||||
for _, ai in ipairs(scan.atom_infos or {}) do
|
||||
@@ -1013,9 +936,9 @@ local function parse_rbind_atoms(ctx, atom_table, registries)
|
||||
if #pairs > 0 then
|
||||
rbind_atoms[atom_name] = {
|
||||
binds = ai.binds,
|
||||
fields = struct.fields, -- {name, offset} from scan.binds
|
||||
fields = struct.fields, -- {name, offset} from scan.binds
|
||||
bytes = struct.bytes,
|
||||
regs = pairs, -- ordered list of {reg, field}
|
||||
regs = pairs, -- ordered list of {reg, field}
|
||||
info_line = ai.info_line,
|
||||
}
|
||||
table.insert(struct.atom_names, atom_name)
|
||||
@@ -1041,12 +964,12 @@ end
|
||||
--- Append per-atom line-program sequences to the existing main .debug_line unit
|
||||
--- (the final unit, referenced by the main CU's DW_AT_stmt_list).
|
||||
---
|
||||
--- The old implementation appended a new Unit 3.
|
||||
--- This builder extends the main compilation unit.
|
||||
--- No compilation unit pointed at it through DW_AT_stmt_list, so gdb ignored it.
|
||||
--- It also encoded byte 13 as the extended-opcode marker; byte 13 is actually the first special opcode.
|
||||
--- The existing final unit already contains hello_gte_tape.c as file index 11 and ends with a valid end_sequence.
|
||||
--- We preserve its bytes, append independent atom sequences, and increase only that unit's DWARF32 unit_length.
|
||||
--- @param existing string -- existing section bytes (verbatim)
|
||||
--- @param existing string -- existing section bytes, byte-for-byte
|
||||
--- @param atom_table table -- list of {name, addr, size_bytes, words, entries}
|
||||
--- @return string
|
||||
local function build_dwarf_line_section(existing, atom_table)
|
||||
@@ -1094,7 +1017,7 @@ end
|
||||
--- segment_size (1 byte) -- = 0
|
||||
--- [entries...] -- address(4) + length(4) per entry
|
||||
--- terminator -- address=0 + length=0 (8 zero bytes)
|
||||
--- @param existing string
|
||||
--- @param existing string
|
||||
--- @param atom_table table
|
||||
--- @return string
|
||||
local function build_dwarf_aranges_section(existing, atom_table)
|
||||
@@ -1132,7 +1055,7 @@ local function build_dwarf_aranges_section(existing, atom_table)
|
||||
return existing
|
||||
end
|
||||
|
||||
local unit_start = i
|
||||
local unit_start = i
|
||||
local unit_end_excl = i + 4 + ul
|
||||
is_last_unit = (unit_end_excl == #existing)
|
||||
|
||||
@@ -1451,7 +1374,7 @@ local function build_new_abbrev()
|
||||
|
||||
-- Component step-into abstract + inline DIE abbreviations.
|
||||
local DW_INL_declared_inlined = 0x03 -- DWARF5 §3.33.3: "this subroutine was declared inline"
|
||||
-- Abstract subprograms now carry DW_AT_decl_file + DW_AT_decl_line so consumers can resolve the abstract origin back to its definition site
|
||||
-- Abstract subprograms carry DW_AT_decl_file and DW_AT_decl_line for definition-site resolution.
|
||||
-- even when no inlined_subroutine instance currently maps to it.
|
||||
-- DW_FORM_udata is consistent with the call_file/call_line forms on abbrev 108.
|
||||
local abbrev_abstract_subprogram = abbrev(ABBREV_ABSTRACT_SUBPROGRAM, DW_TAG_subprogram, false, -- DW_CHILDREN_no
|
||||
@@ -1561,8 +1484,8 @@ end
|
||||
--- Build the DWARF DIE bytes to insert into the MAIN CU as children, immediately
|
||||
--- before the main CU's root children-terminator (the final 0 byte of the CU).
|
||||
---
|
||||
--- The same content was emitted as a DETACHED synthetic CU appended after the main CU.
|
||||
--- GDB's PC lookup selects the main CU, so the synthetic CU was out of scope and `RR_PrimCursor` + `bind_args` never appeared in the current frame.
|
||||
--- Insert the DIEs as children of the main compilation unit.
|
||||
--- This keeps `RR_PrimCursor` and `bind_args` in scope for atom PCs.
|
||||
--- Inserting the DIEs as children of the main CU puts them in scope for every PC the main CU owns;
|
||||
--- including every atom PC (since `.debug_aranges` + `.debug_rnglists` already assign atom PCs to it).
|
||||
---
|
||||
@@ -1596,7 +1519,7 @@ end
|
||||
--- DW_AT_type = ref4 → structure_type DIE
|
||||
---
|
||||
--- **DOES NOT** emit the final 0 byte (root terminator).
|
||||
--- build_debug_info_section splices our bytes between the existing DIE bytes and that terminator, which is preserved verbatim.
|
||||
--- build_debug_info_section splices bytes ahead of the root terminator and preserves existing DIE bytes exactly.
|
||||
---
|
||||
--- **ref4 basis**: DW_FORM_ref4 is CU-relative (offset from the first byte of the CU header).
|
||||
--- Our inserted DIEs live in the main CU, so every ref4 = (target section offset) - main_cu_offset.
|
||||
@@ -1673,7 +1596,7 @@ local function build_inserted_children(main_cu_offset, main_cu_end_excl, atom_ta
|
||||
emit(string.char(4)) -- DW_FORM_data1 (DW_AT_byte_size)
|
||||
emit(string.char(DW_ATE_unsigned)) -- DW_FORM_data1 (DW_AT_encoding)
|
||||
-- (The function body below reads S.next_offset directly via the `next_offset` function;
|
||||
-- the old code used a stale local snapshot that stayed at base_type_section_offset.)
|
||||
-- this keeps offsets synchronized with emitted data.)
|
||||
local function next_offset() return S.next_offset end
|
||||
|
||||
-- Typed local views.
|
||||
@@ -1874,9 +1797,9 @@ local function build_inserted_children(main_cu_offset, main_cu_end_excl, atom_ta
|
||||
-- reusing the pre-emitted base type keeps the wire consistent.
|
||||
-- Once this chain is registered as `type_chain_offsets["U4|1"]`, step (e) of the per-RR_<R_Name> precedence chain will resolve `atom_type(U4 *)`
|
||||
-- declarations on aliases like `R_PrimCursor` and `R_OtBase` to `U4 *` (gdb renders as `(unsigned int *)` with the value displayed in hex).
|
||||
emit(uleb128(ABBREV_TYPED_VIEW_POINTER)) -- DW_TAG_pointer_type (abbrev 110; NOT 9; U4 chain target)
|
||||
emit(uleb128(ABBREV_TYPED_VIEW_POINTER)) -- DW_TAG_pointer_type (abbrev 110; NOT 9; U4 chain target)
|
||||
emit(elf_dwarf.write_u32_le(ref4_of(base_type_section_offset))) -- 4-byte ref4 → "unsigned int" base_type
|
||||
local u4_chain_offset = next_offset() - 5 -- 1 (uleb tag) + 4 (ref4) = 5 bytes; capture the pointer_type's start offset
|
||||
local u4_chain_offset = next_offset() - 5 -- 1 (uleb tag) + 4 (ref4) = 5 bytes; capture the pointer_type's start offset
|
||||
type_chain_offsets["U4|1"] = u4_chain_offset
|
||||
|
||||
-- 2) Emit one DW_TAG_structure_type per unique Binds_X.
|
||||
@@ -1937,7 +1860,7 @@ local function build_inserted_children(main_cu_offset, main_cu_end_excl, atom_ta
|
||||
end
|
||||
|
||||
-- 4) Emit per-atom DW_TAG_subprograms (children of main CU).
|
||||
-- Subprograms are named `<name>` (matching the nm symbol; the `code_` prefix was removed from the MipsAtom_ macro in code/duffle/lottes_tape.h).
|
||||
-- Subprogram names match nm symbols without a `code_` prefix.
|
||||
-- The gcc global `<name>[]` is a DW_TAG_variable without children; our subprogram has the wave-context var children.
|
||||
-- gdb's symbol resolution picks our subprogram (it has low_pc/high_pc + children) over the gcc global for function-context lookups.
|
||||
for _, atom in ipairs(atom_table) do
|
||||
@@ -2023,7 +1946,7 @@ local function build_inserted_children(main_cu_offset, main_cu_end_excl, atom_ta
|
||||
|
||||
-- 5-step precedence chain. The dispatch loop runs the first step that yields a non-nil offset.
|
||||
-- Each step returns the type's section offset or nil if it missed.
|
||||
-- Adding a step = 1 row in the table + 1 function. The 5-level nested if/else is gone.
|
||||
-- Each precedence rule is one table row and one function.
|
||||
-- Per-atom precomputed state is captured in upvalues: atom_view, reg_to_field_ctx, atom_view_ctx_fields,
|
||||
-- reg_to_field_phase, atom_view_phase_fields, field_type_by_name, reg_to_field, alias, type_chain_offsets.
|
||||
local PRECEDENCE_STEPS = {
|
||||
@@ -2076,10 +1999,8 @@ local function build_inserted_children(main_cu_offset, main_cu_end_excl, atom_ta
|
||||
emit(rr_name .. "\0") -- DW_FORM_string (DW_AT_name)
|
||||
-- DW_FORM_exprloc: ULEB byte count + DW_OP_regN byte.
|
||||
-- DW_OP_reg0..reg31 occupy opcodes 0x50..0x6f; DW_OP_reg15 is 0x5f.
|
||||
-- Inlined from `reg_exprloc` (single caller; the function was 3 LOC).
|
||||
-- DW_FORM_exprloc: ULEB byte count + DW_OP_regN byte.
|
||||
-- Inlined from `reg_exprloc` (was at lines 1185-1188, dwarf_injection.lua; the function was 3 LOC and 1 caller).
|
||||
-- The `1` is the length prefix — DW_OP_regN occupies exactly 1 byte (the base opcode is 0x50; regN = 0x50 + N).
|
||||
-- DW_FORM_exprloc: ULEB byte count + DW_OP_regN byte.
|
||||
-- The `1` is the length prefix — DW_OP_regN occupies exactly 1 byte (the base opcode is 0x50; regN = 0x50 + N).
|
||||
-- `alias_code` is the MIPS GPR index (0..31) from the merged register_alias_registry.
|
||||
emit(uleb128(1) .. string.char(DW_OP_reg0 + alias_code)) -- DW_FORM_exprloc (DW_OP_regN from registry code)
|
||||
-- Precedence chain (a..e); step (f) is the void* fallback (initial value).
|
||||
@@ -2094,7 +2015,7 @@ local function build_inserted_children(main_cu_offset, main_cu_end_excl, atom_ta
|
||||
|
||||
-- If rbind, emit bind_args variable with PC-ranged location list.
|
||||
-- The loclist is in .debug_loclists, indexed by `DW_FORM_sec_offset` (4-byte section-relative offset).
|
||||
-- The piece chain is replaced by two PC ranges: [atom.addr, last_load+8) where every field is described as a tape-memory
|
||||
-- The location list uses two PC ranges: [atom.addr, last_load+8) describes every field as tape memory
|
||||
-- (DW_OP_bregN + offset) piece, and [last_load+8, atom.end) where every field is described as a GPR (DW_OP_regN) piece.
|
||||
if atom.rbind then
|
||||
local binds_name = atom.rbind.binds
|
||||
@@ -2108,8 +2029,8 @@ local function build_inserted_children(main_cu_offset, main_cu_end_excl, atom_ta
|
||||
-- Per-component invocation inlined_subroutine instances.
|
||||
-- Each invocation covers a contiguous .word range [start_pos, end_pos] within the atom.
|
||||
-- We compute the corresponding PC range from the atom's start + .word offsets × MIPS_BYTES_PER_WORD.
|
||||
-- call_file now resolves inv.call_file to the line-unit file index (previously hardcoded to ATOM_SOURCE_FILE_INDEX;
|
||||
-- that lost the call-site attribution for any invocation whose call site was NOT the atom's source file).
|
||||
-- Resolve `inv.call_file` to the line-unit file index;
|
||||
-- this preserves call-site attribution across source files.
|
||||
if atom.invocations and not atom.skip_over then
|
||||
for _, inv in ipairs(atom.invocations) do
|
||||
local inv_low = atom.addr + inv.start_pos * MIPS_BYTES_PER_WORD
|
||||
@@ -2126,7 +2047,7 @@ local function build_inserted_children(main_cu_offset, main_cu_end_excl, atom_ta
|
||||
emit(string.char(DIE_CHILDREN_TERMINATOR)) -- end of subprogram's children (DWARF5 §7.5.3)
|
||||
end
|
||||
|
||||
-- DO NOT emit a final 0 here — that's the main CU's root terminator, which build_debug_info_section preserves verbatim.
|
||||
-- Do not emit a final 0 here; build_debug_info_section preserves the root terminator byte.
|
||||
return table.concat(S.bytes)
|
||||
end
|
||||
|
||||
@@ -2148,7 +2069,7 @@ end
|
||||
---
|
||||
--- Fails safely by returning existing sections unchanged if the table walker can't find the table terminator (malformed input).
|
||||
---
|
||||
--- @param existing string -- existing .debug_abbrev bytes (verbatim)
|
||||
--- @param existing string -- existing .debug_abbrev bytes, byte-for-byte
|
||||
--- @param main_abbrev_offset integer -- 0-based offset into `existing` of the main CU's abbrev table
|
||||
--- @return string, integer -- (new_abbrev_bytes, offset_where_duplicate_table_starts = #existing)
|
||||
local function build_debug_abbrev_section(existing, main_abbrev_offset)
|
||||
@@ -2167,7 +2088,7 @@ local function build_debug_abbrev_section(existing, main_abbrev_offset)
|
||||
end
|
||||
|
||||
--- Build the new .debug_str: existing strings + new strings appended.
|
||||
--- @param existing string -- existing .debug_str bytes (verbatim)
|
||||
--- @param existing string -- existing .debug_str bytes, byte-for-byte
|
||||
--- @param atom_table table[]
|
||||
--- @param registries table -- merged registries from collect_per_source_registries
|
||||
--- @return string, integer, table -- (new_str_bytes, new_strings_offset, string_map)
|
||||
@@ -2213,29 +2134,28 @@ local function build_debug_info_section(existing, main_cu_start, main_cu_end_exc
|
||||
|
||||
-- 4) Splice. All offsets below are 0-based; existing:sub is 1-indexed inclusive.
|
||||
-- Byte ranges (0-based, inclusive):
|
||||
-- [0 .. main_cu_start - 1] crt CU (verbatim)
|
||||
-- [0 .. main_cu_start - 1] crt CU, unchanged
|
||||
-- [main_cu_start + 0 .. + 3] unit_length (PATCHED)
|
||||
-- [main_cu_start + 4 .. + 7] version + unit_type + address_size (verbatim)
|
||||
-- [main_cu_start + 4 .. + 7] version + unit_type + address_size, unchanged
|
||||
-- [main_cu_start + 8 .. + 11] debug_abbrev_offset (PATCHED)
|
||||
-- [main_cu_start + 12 .. main_cu_end_excl - 2] existing DIE bytes (verbatim)
|
||||
-- [main_cu_end_excl - 1] root children-terminator (verbatim 0)
|
||||
-- [main_cu_start + 12 .. main_cu_end_excl - 2] existing DIE bytes, unchanged
|
||||
-- [main_cu_end_excl - 1] root children-terminator, unchanged 0
|
||||
local pre_end = main_cu_end_excl - 2 -- 0-based end of existing DIE bytes (inclusive)
|
||||
local root_terminator = main_cu_end_excl - 1 -- 0-based position of the final 0 byte
|
||||
|
||||
return existing:sub(1, main_cu_start) -- crt CU
|
||||
.. new_unit_length_bytes -- patched unit_length (4 bytes)
|
||||
.. existing:sub(main_cu_start + 5, main_cu_start + 8) -- version(2) + unit_type(1) + address_size(1) verbatim
|
||||
.. existing:sub(main_cu_start + 5, main_cu_start + 8) -- version(2) + unit_type(1) + address_size(1), unchanged
|
||||
.. new_abbrev_offset_bytes -- patched debug_abbrev_offset (4 bytes)
|
||||
.. existing:sub(main_cu_start + 13, pre_end + 1) -- existing DIE bytes verbatim
|
||||
.. existing:sub(main_cu_start + 13, pre_end + 1) -- existing DIE bytes, unchanged
|
||||
.. inserted -- our inserted children
|
||||
.. existing:sub(root_terminator + 1, main_cu_end_excl) -- root children-terminator (verbatim 0)
|
||||
.. existing:sub(root_terminator + 1, main_cu_end_excl) -- root children-terminator, unchanged 0
|
||||
end
|
||||
|
||||
--- Build the .debug_loc: just a terminator.
|
||||
--- Atoms don't have stack frames. The .debug_loc section describes per-instruction location adjustments for call-frame-based variables;
|
||||
--- We use DW_OP_regN which is register-based and doesn't need .debug_loc entries).
|
||||
--- The section itself must not be empty OR gdb may complain; the DW_LLE_end_of_list marker (per DWARF5 §7.7) is a single byte 0x00.
|
||||
--- Single-caller (M.run's SECTION_BUILDERS table below); replaced by a literal at the call site.
|
||||
-- local function build_debug_loc_section() return string.char(0x00) end
|
||||
|
||||
local SECTION_BUILDERS = {
|
||||
@@ -2351,14 +2271,21 @@ function M.run(ctx)
|
||||
-- then build the atom/provenance table with those generic selections.
|
||||
-- Whole atoms remain symbol-keyed; components are file-qualified internally so a source marker associates
|
||||
-- with its exact component definition even though GDB 12 requires function-only skip entries for the resulting synthetic inline frame.
|
||||
local skip_over = collect_skip_over(ctx)
|
||||
local registries = collect_per_source_registries(ctx)
|
||||
local atom_table = build_atom_table(ctx, skip_over)
|
||||
io.stderr:write(string.format("[dwarf_injection] matched %d atoms between nm + source-map\n", #atom_table))
|
||||
-- `corpus` is the sole canonical source projection;
|
||||
-- the sole source of truth; no `ctx.sources` / `ctx.by_dir` aliases).
|
||||
local corpus = (ctx.shared and ctx.shared.corpus) or {}
|
||||
local skip_over = collect_skip_over(corpus)
|
||||
local registries = collect_per_source_registries(corpus)
|
||||
-- Read nm symbols (the ONLY disk-side input to the atom table) and join
|
||||
-- them against `corpus.atoms_by_name` + `atom.paths` for word rows + invocation ancestry.
|
||||
-- Disk source-map/provenance text is NOT consulted (those are diagnostic artifacts; semantic inputs are in memory).
|
||||
local addrs = elf_dwarf.read_nm(ctx.flags.elf_path)
|
||||
local atom_table = build_atom_table(corpus, addrs, skip_over)
|
||||
io.stderr:write(string.format("[dwarf_injection] matched %d atoms between nm + corpus.atoms_by_name\n", #atom_table))
|
||||
|
||||
-- Detect rbind atoms + index Binds_* struct fields (from ctx.sources[i].scan, populated by scan-source pass).
|
||||
-- The merged registries are threaded through so parse_body_load_pairs resolves R_<reg> via register_alias_registry.
|
||||
local _rbind_atoms, rbind_structs = parse_rbind_atoms(ctx, atom_table, registries)
|
||||
local _rbind_atoms, rbind_structs = parse_rbind_atoms(corpus, atom_table, registries)
|
||||
local rbind_count = 0
|
||||
for _ in pairs(_rbind_atoms) do rbind_count = rbind_count + 1 end
|
||||
io.stderr:write(string.format("[dwarf_injection] matched %d rbind atoms across %d Binds_* structs\n",
|
||||
@@ -2367,7 +2294,7 @@ function M.run(ctx)
|
||||
-- Write the .bin files. The build_psyq.ps1 post-link hook splices these into a copy of the ELF via objcopy --update-section.
|
||||
-- Build order:
|
||||
-- 0. Validate .debug_info layout (crT CU + DWARF5 main CU + final 0 root terminator).
|
||||
-- If validation fails, FAIL SAFELY by writing the existing sections verbatim (no malformed output, no synthetic CU append, no header patch).
|
||||
-- If validation fails, write existing sections unchanged and emit no synthetic data.
|
||||
-- 1. Build new .debug_abbrev using the main CU's abbrev offset → returns the offset of the duplicate main table (= #existing_abbrev).
|
||||
-- 2. Build new .debug_info by splicing inserted children into the main CU (patches main CU's unit_length + debug_abbrev_offset;
|
||||
-- preserves all original DIE bytes; does NOT append a synthetic CU).
|
||||
@@ -2445,7 +2372,7 @@ function M.run(ctx)
|
||||
return { outputs = {}, errors = {}, warnings = {} }
|
||||
end
|
||||
|
||||
-- Test-only re-exports: keep the module's main M table lean while letting scratch
|
||||
-- Test-only exports expose the emission and offset paths.
|
||||
-- tests drive the real emission and offset computation paths.
|
||||
M.compute_loclists_offsets_for_test = compute_loclists_offsets
|
||||
M.build_debug_loclists_section_for_test = build_debug_loclists_section
|
||||
|
||||
@@ -0,0 +1,194 @@
|
||||
--- passes/emission_model.lua: Per-atom emission projection.
|
||||
---
|
||||
--- The `emission-model` pass owns `atom.paths` (the canonical per-atom mutable surface)
|
||||
--- for every atom-with-body and every raw atom-with-body declared in `ctx.shared.corpus.source_order`.
|
||||
--- For each such atom, the pass invokes `duffle.project_emission(body_text, component_index, word_counts)`
|
||||
--- and stores the ordered `items` stream plus the dense `word_events` / `markers` / `invocations` views on `atom.paths`.
|
||||
---
|
||||
--- Public boundary:
|
||||
--- * `M.run(ctx)` is the only entry point.
|
||||
--- * The pass returns `{outputs = {}, errors = ..., warnings = ...}`.
|
||||
--- Pass kind = `validation` → `PASS_KIND_STOP_ON_ERROR.validation` keeps build-stopping semantics (no policy change in this task).
|
||||
---
|
||||
--- Source-order discipline:
|
||||
--- * `corpus.source_order` is the canonical ordering of source records.
|
||||
--- * For each source, the pass iterates `src.scan.atoms` and `src.scan.raw_atoms` IN SOURCE ORDER, preserving declaration order.
|
||||
---
|
||||
--- Per-atom projection fields on `atom.paths`:
|
||||
--- `tokens`, `line_in_body`, `items`, `word_events`, `markers`, `invocations`, `errors`, `warnings`.
|
||||
--- The dense views are built from `items` only; the pass never re-walks source text or tokens.
|
||||
---
|
||||
--- Component expansion and construction validation:
|
||||
--- * known `mac_X(...)` calls recursively expand component bodies;
|
||||
--- * invocation records retain monotonic IDs, parent IDs, immediate call text, and the immutable outermost root call text;
|
||||
--- * component cycles retain balanced invocation boundaries and emit a `cycle` construction error without recursing indefinitely;
|
||||
--- * declared-vs-measured component word counts emit `count_mismatch` construction errors; opaque uncounted macros emit warnings.
|
||||
---
|
||||
--- The pass does NOT consult `_code_macros` / `_code_macro_bodies`. Those private tables are owned by `passes.scan_source` and stripped before this pass runs.
|
||||
|
||||
local M = {}
|
||||
|
||||
-- ─────────────────────────────────────────────────────────────────────────
|
||||
-- Bootstrap: load `duffle_paths.lua` via debug.getinfo so the module works standalone (run as `luajit passes/emission_model.lua`) and when require'd from the orchestrator.
|
||||
-- ─────────────────────────────────────────────────────────────────────────
|
||||
local _bootstrap_dir = debug.getinfo(1, "S").source:match("^@?(.*[/\\])") or "./"
|
||||
local duffle = dofile(_bootstrap_dir .. "../duffle_paths.lua")
|
||||
|
||||
-- ─────────────────────────────────────────────────────────────────────────
|
||||
-- Helpers
|
||||
-- ─────────────────────────────────────────────────────────────────────────
|
||||
|
||||
-- Convert the recursive walk's body-relative line numbers into physical source lines once.
|
||||
-- Consumers read these canonical fields rather than rebuilding line state or tokenizing source again.
|
||||
local function stamp_root_provenance(projection, atom_record, src, corpus)
|
||||
local root_line_of = src.scan and src.scan.line_of
|
||||
local root_body_line = root_line_of and root_line_of((atom_record.body_off or 1) - 1)
|
||||
or atom_record.line or 0
|
||||
local component_index = corpus.component_body_index or {}
|
||||
local word_items = {}
|
||||
|
||||
for _, item in ipairs(projection.items) do
|
||||
if item.kind == "word" then word_items[#word_items + 1] = item end
|
||||
end
|
||||
|
||||
local function body_line_for(event, item)
|
||||
local body_line_of = root_line_of
|
||||
local body_off = atom_record.body_off or 0
|
||||
local ids = event.invocation_ids or {}
|
||||
local inner_id = ids[#ids]
|
||||
local inner_inv = inner_id and projection.invocations[inner_id]
|
||||
if inner_inv then
|
||||
local component = component_index[inner_inv.component_name]
|
||||
if component and component.line_of then
|
||||
-- Component-body walkers already receive the declaration source's full line index, so their item.line is physical.
|
||||
return item.line or 0
|
||||
end
|
||||
end
|
||||
local first_line = body_line_of and body_line_of(math.max(1, body_off - 1)) or root_body_line
|
||||
return (first_line or 0) + (item.line or 1) - 1
|
||||
end
|
||||
|
||||
-- Stamp root-source path onto invocation records whose `call_path` was left empty by the walker.
|
||||
-- The walker passes `body_entry.source` to `emit_invoke_begin` as the call_path argument; for the root body_entry created by `M.project_emission` that source is ""
|
||||
-- (the caller passes only the body text).
|
||||
-- After this stamp every invocation record has a physical call_path that matches what `passes/atoms_source_map.lua` matches the in-memory provenance projection.
|
||||
local root_path = src.path or ""
|
||||
for _, inv in ipairs(projection.invocations) do
|
||||
if inv.call_path == nil or inv.call_path == "" then
|
||||
inv.call_path = root_path
|
||||
end
|
||||
end
|
||||
|
||||
for index, we in ipairs(projection.word_events) do
|
||||
local item = word_items[index] or {}
|
||||
local body_line = body_line_for(we, item)
|
||||
item.line = body_line
|
||||
we.body_line = body_line
|
||||
|
||||
local call_line = body_line
|
||||
local outer_id = we.outermost_invocation_id or 0
|
||||
local outer_inv = projection.invocations[outer_id]
|
||||
if outer_inv then call_line = (root_body_line or 0) + (outer_inv.call_line or 1) - 1 end
|
||||
we.call_line = call_line
|
||||
|
||||
if we.def_path == nil or we.def_path == "" then we.def_path = src.path or "" end
|
||||
if we.def_line == nil or we.def_line == 0 then we.def_line = atom_record.line or 0 end
|
||||
if we.call_path == nil or we.call_path == "" then we.call_path = src.path or "" end
|
||||
end
|
||||
end
|
||||
|
||||
-- Project one atom record into `atom.paths`.
|
||||
-- Mutates the atom record in-place and returns the projection (for pass-level error/warning accumulation).
|
||||
local function project_atom(atom_record, src, corpus)
|
||||
local body = atom_record.body or ""
|
||||
local wc = corpus.word_counts or {}
|
||||
local cbi = corpus.component_body_index or {}
|
||||
local proj = duffle.project_emission(body, cbi, wc)
|
||||
local paths = {
|
||||
tokens = atom_record.body_tokens or {},
|
||||
line_in_body = duffle.build_body_line_index(body),
|
||||
items = proj.items,
|
||||
word_events = proj.word_events,
|
||||
markers = proj.markers,
|
||||
invocations = proj.invocations,
|
||||
errors = proj.errors,
|
||||
warnings = proj.warnings,
|
||||
}
|
||||
stamp_root_provenance(proj, atom_record, src, corpus)
|
||||
atom_record.paths = paths
|
||||
return proj
|
||||
end
|
||||
|
||||
-- ─────────────────────────────────────────────────────────────────────────
|
||||
-- Run the emission-model pass.
|
||||
-- ─────────────────────────────────────────────────────────────────────────
|
||||
|
||||
--- @param ctx PassCtx -- { shared = { corpus = ... }, out_root, dry_run, ... }
|
||||
--- @return PassResult
|
||||
function M.run(ctx)
|
||||
local outputs = {}
|
||||
local errors = {}
|
||||
local warnings = {}
|
||||
|
||||
local corpus = ctx and ctx.shared and ctx.shared.corpus
|
||||
if type(corpus) ~= "table" then error("emission_model: ctx.shared.corpus is required (canonical projection)", 0) end
|
||||
if type(corpus.source_order) ~= "table" then error("emission_model: ctx.shared.corpus.source_order is required", 0) end
|
||||
|
||||
-- Walk every source in canonical source order; for each source, iterate atoms.
|
||||
-- Atom declarations (`kind == "atom"` / `"raw_atom"`) receive the canonical `atom.paths` projection; component declarations
|
||||
-- (`comp_bare` / `comp_proc`) are recursively expanded by atom projections and do not get an independent projection themselves.
|
||||
-- Test-only fixtures that need per-component word events may still consume `duffle.expand_word_events`
|
||||
-- (which remains available; emission-model owns the canonical per-atom projection).
|
||||
for _, src in ipairs(corpus.source_order) do
|
||||
local scan = src.scan or {}
|
||||
for _, atom in ipairs(scan.atoms or {}) do
|
||||
if atom and atom.body and (atom.kind == "atom" or atom.kind == "raw_atom") then
|
||||
local proj = project_atom(atom, src, corpus)
|
||||
for _, e in ipairs(proj.errors) do
|
||||
-- Preserve `kind` (cycle / count_mismatch / unbalanced) so readers can dispatch on the diagnostic class without re-parsing the message string.
|
||||
errors[#errors + 1] = {
|
||||
kind = e.kind,
|
||||
line = e.line,
|
||||
msg = e.msg,
|
||||
source = e.source or src.path,
|
||||
}
|
||||
end
|
||||
for _, w in ipairs(proj.warnings) do
|
||||
warnings[#warnings + 1] = {
|
||||
kind = w.kind,
|
||||
line = w.line,
|
||||
msg = w.msg,
|
||||
}
|
||||
end
|
||||
end
|
||||
end
|
||||
for _, atom in ipairs(scan.raw_atoms or {}) do
|
||||
if atom and atom.body then
|
||||
local proj = project_atom(atom, src, corpus)
|
||||
for _, e in ipairs(proj.errors) do
|
||||
errors[#errors + 1] = {
|
||||
kind = e.kind,
|
||||
line = e.line,
|
||||
msg = e.msg,
|
||||
source = e.source or src.path,
|
||||
}
|
||||
end
|
||||
for _, w in ipairs(proj.warnings) do
|
||||
warnings[#warnings + 1] = {
|
||||
kind = w.kind,
|
||||
line = w.line,
|
||||
msg = w.msg,
|
||||
}
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
return {
|
||||
outputs = outputs,
|
||||
errors = errors,
|
||||
warnings = warnings,
|
||||
}
|
||||
end
|
||||
|
||||
return M
|
||||
+87
-193
@@ -21,18 +21,12 @@
|
||||
-- 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
|
||||
local duffle = dofile(_bootstrap_dir .. "../duffle_paths.lua")
|
||||
|
||||
-- ════════════════════════════════════════════════════════════════════════════
|
||||
-- Constants
|
||||
-- ════════════════════════════════════════════════════════════════════════════
|
||||
|
||||
-- Marker-call identifiers inside atom bodies.
|
||||
local LABEL_MARKER = "atom_label"
|
||||
local OFFSET_MARKER = "atom_offset"
|
||||
|
||||
-- Offset macro/enum naming prefixes (the emitted header uses these).
|
||||
local OFFSET_MACRO_PREFIX = "_atom_offset_"
|
||||
local OFFSET_ENUM_PREFIX = "atom_offset_"
|
||||
@@ -52,16 +46,11 @@ local OFFSET_MACRO_COL = 44
|
||||
--- @field scan table -- pre-scanned SourceScan payload (from duffle.scan_source)
|
||||
|
||||
--- @class PassCtx
|
||||
--- @field sources SourceFile[] -- all source files in the build
|
||||
--- @field metadata_path string -- path to word_count.metadata.h
|
||||
--- @field shared table -- cross-pass shared state
|
||||
--- @field shared.word_counts table -- macro name -> word count
|
||||
--- @field shared.corpus table -- canonical corpus projection
|
||||
--- @field shared.word_counts table -- compatibility alias to corpus.word_counts
|
||||
--- @field out_root string -- output root (e.g. "build/gen")
|
||||
--- @field project_root string -- project root (e.g. "code/")
|
||||
--- @field upstream table<string, table> -- per-pass upstream outputs
|
||||
--- @field flags table -- CLI flags
|
||||
--- @field dry_run boolean -- if true, compute but don't write
|
||||
--- @field verbose boolean -- log diagnostic info
|
||||
|
||||
--- @class PassResult
|
||||
--- @field outputs table[] -- {kind=, path=} entries describing emit files
|
||||
@@ -69,10 +58,10 @@ local OFFSET_MACRO_COL = 44
|
||||
--- @field warnings table[] -- {line=, msg=} entries; build-succeeds
|
||||
|
||||
--- @class BranchOffset
|
||||
--- @field tag string -- the marker tag (e.g. "F" in `atom_offset(F, T)`)
|
||||
--- @field target string -- the target label name (e.g. "T" in `atom_offset(F, T)`)
|
||||
--- @field pos integer -- the branch's word position within the atom body
|
||||
--- @field offset integer -- computed `target_word - branch_word - 1`
|
||||
--- @field tag string -- the marker tag (e.g. "F" in `atom_offset(F, T)`)
|
||||
--- @field target string -- the target label name (e.g. "T" in `atom_offset(F, T)`)
|
||||
--- @field branch_word integer -- branch word position within the atom body
|
||||
--- @field offset integer -- computed `target_word - branch_word - 1`
|
||||
|
||||
--- @class AtomData
|
||||
--- @field name string -- atom name
|
||||
@@ -80,169 +69,85 @@ local OFFSET_MACRO_COL = 44
|
||||
--- @field offsets BranchOffset[] -- per-branch offset list
|
||||
|
||||
-- ════════════════════════════════════════════════════════════════════════════
|
||||
-- Per-token marker-call helpers (atom_label / atom_offset inside bodies)
|
||||
-- Canonical marker projection
|
||||
-- ════════════════════════════════════════════════════════════════════════════
|
||||
|
||||
-- Extract comma-separated identifier args from a parenthesized group after a function-like macro call.
|
||||
-- Returns (args, after_paren) where `after_paren` is the position just past the closing `)`, or nil if `token` did not start with `(`.
|
||||
-- @param token string
|
||||
-- @param after_ident integer
|
||||
-- @return string[], integer|nil
|
||||
local function extract_ident_args(token, after_ident)
|
||||
local arg_start = duffle.skip_ws_and_cmt(token, after_ident)
|
||||
if token:sub(arg_start, arg_start) ~= "(" then return {}, nil end
|
||||
local inner, after_paren = duffle.read_parens(token, arg_start)
|
||||
-- scan: <marker>(<args>)
|
||||
|
||||
local args = {}
|
||||
local pos = 1
|
||||
local inner_len = #inner
|
||||
while pos <= inner_len do
|
||||
pos = duffle.skip_ws_and_cmt(inner, pos)
|
||||
if pos > inner_len then break end
|
||||
local ident, after = duffle.read_ident(inner, pos)
|
||||
if ident and ident ~= "" then
|
||||
table.insert(args, ident)
|
||||
pos = after
|
||||
else
|
||||
pos = pos + 1
|
||||
end
|
||||
pos = duffle.skip_ws_and_cmt(inner, pos)
|
||||
if pos <= inner_len and inner:sub(pos, pos) == "," then pos = pos + 1 end
|
||||
end
|
||||
|
||||
return args, after_paren
|
||||
end
|
||||
|
||||
-- (internal) Record a `atom_label(name)` marker — `at_pos` is the branch-free word position within the atom body.
|
||||
-- @param labels table<string, integer>
|
||||
-- @param args string[]
|
||||
-- @param at_pos integer
|
||||
local function record_label_marker(labels, args, at_pos)
|
||||
if #args >= 1 then labels[args[1]] = at_pos end
|
||||
end
|
||||
|
||||
-- (internal) Record a `atom_offset(tag, target)` marker.
|
||||
-- @param branches table[] -- list of {pos=, target=, tag=}
|
||||
-- @param args string[]
|
||||
-- @param at_pos integer
|
||||
local function record_offset_marker(branches, args, at_pos)
|
||||
if #args >= 2 then
|
||||
table.insert(branches, { pos = at_pos, target = args[2], tag = args[1] })
|
||||
end
|
||||
end
|
||||
|
||||
-- MARKER_TO_HANDLER — data-driven marker dispatch (the plex pattern).
|
||||
-- Maps the marker ident to its recorder function. Each handler takes (out_table, args, at_pos).
|
||||
-- Adding a new marker type = 1 row + 1 recorder function.
|
||||
local MARKER_TO_HANDLER = {
|
||||
[LABEL_MARKER] = record_label_marker,
|
||||
[OFFSET_MARKER] = record_offset_marker,
|
||||
-- MARKER_PROJECTORS is the marker-kind data table.
|
||||
-- The emission-model pass already records marker word positions;
|
||||
-- this pass only projects those records into the label/branch lookup shape needed by offset computation.
|
||||
local MARKER_PROJECTORS = {
|
||||
label = function(state, marker)
|
||||
state.labels[marker.name] = marker.word_index
|
||||
end,
|
||||
offset = function(state, marker)
|
||||
state.branches[#state.branches + 1] = {
|
||||
tag = marker.name,
|
||||
target = marker.target,
|
||||
branch_word = marker.word_index,
|
||||
}
|
||||
end,
|
||||
}
|
||||
|
||||
--- Scan a single token for atom_label/atom_offset markers, walking through balanced groups transparently (so nested calls are found).
|
||||
--- @param token string
|
||||
--- @param at_pos integer -- the branch-free word position of this token in the body
|
||||
--- @param labels table<string, integer>
|
||||
--- @param branches table[]
|
||||
local function scan_for_atom_markers(token, at_pos, labels, branches)
|
||||
local pos = 1
|
||||
local tok_len = #token
|
||||
while pos <= tok_len do
|
||||
pos = duffle.skip_ws_and_cmt(token, pos)
|
||||
if pos > tok_len then break end
|
||||
local ch = token:sub(pos, pos)
|
||||
if duffle.is_alpha(ch) then
|
||||
local ident, after = duffle.read_ident(token, pos)
|
||||
local handler = MARKER_TO_HANDLER[ident]
|
||||
if handler then
|
||||
local args, after_paren = extract_ident_args(token, after)
|
||||
-- Marker found — dispatch to its recorder. markers share labels and branches as
|
||||
-- out-tables; the recorder picks which one(s) to write to based on its semantics.
|
||||
-- (record_label_marker writes to labels; record_offset_marker writes to branches.)
|
||||
handler(ident == LABEL_MARKER and labels or branches, args, at_pos)
|
||||
pos = after_paren or after
|
||||
else
|
||||
pos = after
|
||||
end
|
||||
else
|
||||
local nx = duffle.skip_str_or_cmt(token, pos)
|
||||
pos = (nx > pos) and nx or (pos + 1)
|
||||
end
|
||||
--- Project canonical marker records into the two lookup tables used by the offset renderer.
|
||||
--- No source text, body text, or body token is inspected.
|
||||
--- @param markers table[] -- atom.paths.markers
|
||||
--- @return table<string, integer>, table[]
|
||||
local function project_markers(markers)
|
||||
local state = { labels = {}, branches = {} }
|
||||
for _, marker in ipairs(markers or {}) do
|
||||
local project = MARKER_PROJECTORS[marker.kind]
|
||||
if project then project(state, marker) end
|
||||
end
|
||||
end
|
||||
|
||||
--- Scan an atom body for labels + branches, count total words.
|
||||
--- Returns (labels, branches, total_words).
|
||||
--- @param body string
|
||||
--- @param word_counts table
|
||||
--- @return table<string, integer>, table[], integer
|
||||
-- scan_atom_body: walk pre-tokenized body for atom_label/atom_offset markers + word counts.
|
||||
-- Uses `atom.body_tokens` from the SourceScan payload (pre-tokenized by scan-source pass).
|
||||
-- @param body_tokens table[] -- {{tok=string, rel=integer}, ...} from duffle.tokenize_body
|
||||
-- @param word_counts table
|
||||
-- @return table, table, integer -- labels, branches, total_words
|
||||
local function scan_atom_body(body_tokens, word_counts)
|
||||
local pos = 0
|
||||
local labels = {}
|
||||
local branches = {}
|
||||
for _, t in ipairs(body_tokens) do
|
||||
local tok = t.tok
|
||||
if duffle.is_marker_token(tok) then
|
||||
-- Marker call: record at the current pos, do NOT advance pos.
|
||||
scan_for_atom_markers(tok, pos, labels, branches)
|
||||
pos = pos + duffle.count_marker_rest(tok, word_counts, count_token_words)
|
||||
else
|
||||
local words = count_token_words(tok, word_counts)
|
||||
scan_for_atom_markers(tok, pos, labels, branches)
|
||||
pos = pos + words
|
||||
end
|
||||
end
|
||||
return labels, branches, pos
|
||||
return state.labels, state.branches
|
||||
end
|
||||
|
||||
-- ════════════════════════════════════════════════════════════════════════════
|
||||
-- Offset computation + header generation
|
||||
-- ════════════════════════════════════════════════════════════════════════════
|
||||
|
||||
-- Compute branch offsets as `target_word - branch_word - 1` (the standard MIPS branch-immediate encoding).
|
||||
-- @param labels table<string, integer>
|
||||
-- @param branches table[]
|
||||
-- @return BranchOffset[]
|
||||
--- Compute branch offsets as `target_word - branch_word - 1` (the standard MIPS branch-immediate encoding).
|
||||
--- @param labels table<string, integer>
|
||||
--- @param branches table[]
|
||||
--- @return BranchOffset[]
|
||||
local function compute_offsets(labels, branches)
|
||||
local results = {}
|
||||
for _, br in ipairs(branches) do
|
||||
local target = labels[br.target]
|
||||
if not target then
|
||||
error("Branch target '" .. br.target .. "' has no atom_label (at word " .. br.pos .. ")")
|
||||
error("Branch target '" .. br.target .. "' has no atom_label (at word " .. br.branch_word .. ")")
|
||||
end
|
||||
results[#results + 1] = { target = br.target, tag = br.tag, offset = target - br.pos - 1 }
|
||||
results[#results + 1] = {
|
||||
target = br.target,
|
||||
tag = br.tag,
|
||||
branch_word = br.branch_word,
|
||||
offset = target - br.branch_word - 1,
|
||||
}
|
||||
end
|
||||
return results
|
||||
end
|
||||
|
||||
-- Right-pad `s` with spaces to width `w`. If `s` is already `w` or wider, no padding is added.
|
||||
-- @param s string
|
||||
-- @param w integer
|
||||
-- @return string
|
||||
--- Right-pad `s` with spaces to width `w`. If `s` is already `w` or wider, no padding is added.
|
||||
--- @param s string
|
||||
--- @param w integer
|
||||
--- @return string
|
||||
local function pad_right(s, w)
|
||||
return s .. string.rep(" ", math.max(0, w - #s))
|
||||
end
|
||||
|
||||
-- (internal) Build a constant-table entry `{macro_name, enum_name, value}` from a BranchOffset.
|
||||
-- @param r BranchOffset
|
||||
-- @return table
|
||||
local function make_offset_const(r)
|
||||
--- (internal) Build a constant-table entry `{macro_name, enum_name, value}` from a BranchOffset.
|
||||
--- @param bo BranchOffset
|
||||
--- @return table
|
||||
local function make_offset_const(bo)
|
||||
return {
|
||||
macro_name = OFFSET_MACRO_PREFIX .. r.tag .. "_" .. r.target,
|
||||
enum_name = OFFSET_ENUM_PREFIX .. r.tag .. "_" .. r.target,
|
||||
value = r.offset,
|
||||
macro_name = OFFSET_MACRO_PREFIX .. bo.tag .. "_" .. bo.target,
|
||||
enum_name = OFFSET_ENUM_PREFIX .. bo.tag .. "_" .. bo.target,
|
||||
value = bo.offset,
|
||||
}
|
||||
end
|
||||
|
||||
-- (internal) Emit one atom's offset constants + enum into the lines buffer.
|
||||
-- @param add fun(s: string)
|
||||
-- @param atom AtomData
|
||||
--- (internal) Emit one atom's offset constants + enum into the lines buffer.
|
||||
--- @param add fun(s: string)
|
||||
--- @param atom AtomData
|
||||
local function emit_atom_offsets(add, atom)
|
||||
if #atom.offsets == 0 then return end
|
||||
add("// --- atom: " .. atom.name .. " (" .. atom.total_words .. " words) ---")
|
||||
@@ -263,10 +168,10 @@ local function emit_atom_offsets(add, atom)
|
||||
add("")
|
||||
end
|
||||
|
||||
-- Generate the per-source .offsets.h header.
|
||||
-- @param source_path string
|
||||
-- @param atoms_data AtomData[]
|
||||
-- @return string
|
||||
--- Generate the per-source .offsets.h header.
|
||||
--- @param source_path string
|
||||
--- @param atoms_data AtomData[]
|
||||
--- @return string
|
||||
local function generate_header(source_path, atoms_data)
|
||||
local basename = duffle.basename_no_ext(source_path)
|
||||
|
||||
@@ -288,59 +193,43 @@ local function generate_header(source_path, atoms_data)
|
||||
return table.concat(lines, "\n") .. "\n"
|
||||
end
|
||||
|
||||
-- ════════════════════════════════════════════════════════════════════════════
|
||||
-- M — module exports
|
||||
-- ════════════════════════════════════════════════════════════════════════════
|
||||
|
||||
local M = {}
|
||||
|
||||
-- Project the pre-scanned SourceScan entries into the {name, body, body_tokens} shape this pass needs.
|
||||
-- MipsAtom_ entries have kind="atom"; MipsCode code_<name> entries have kind="raw_atom".
|
||||
-- `body_tokens` is set by scan-source on every `scan.atoms[i]` / `scan.raw_atoms[i]`; we carry it forward
|
||||
-- so `scan_atom_body` reads from the precomputed table directly (no per-atom tokenize_body fallback).
|
||||
-- @param scan table -- SourceScan from duffle.scan_source
|
||||
-- @return table[] -- list of {name=, body=, body_tokens=}
|
||||
local function project_atoms(scan)
|
||||
local out = {}
|
||||
for _, a in ipairs(scan.atoms) do
|
||||
out[#out + 1] = { name = a.raw_name, body = a.body, body_tokens = a.body_tokens }
|
||||
end
|
||||
for _, a in ipairs(scan.raw_atoms) do
|
||||
out[#out + 1] = { name = a.name, body = a.body, body_tokens = a.body_tokens }
|
||||
end
|
||||
return out
|
||||
end
|
||||
|
||||
-- (internal) Process one source: project atoms from scan, scan bodies, write header.
|
||||
-- Returns the offsets_h path if a header was written, or nil.
|
||||
-- @param ctx PassCtx
|
||||
-- @param src SourceFile
|
||||
-- @return string|nil -- the offsets_h path
|
||||
--- (internal) Process one source: render offsets from canonical atom paths.
|
||||
--- Returns the offsets_h path if a header was written, or nil.
|
||||
--- @param ctx PassCtx
|
||||
--- @param src SourceFile
|
||||
--- @return string|nil -- the offsets_h path
|
||||
local function process_source(ctx, src)
|
||||
local atoms = project_atoms(src.scan)
|
||||
if #atoms == 0 then return nil end
|
||||
|
||||
local atoms_data = {}
|
||||
for _, atom in ipairs(atoms) do
|
||||
local labels, branches, total = scan_atom_body(atom.body_tokens, ctx.shared.word_counts)
|
||||
local scan = src.scan or {}
|
||||
|
||||
local function append_atom(atom)
|
||||
local paths = atom and atom.paths
|
||||
if not paths then return end
|
||||
local labels, branches = project_markers(paths.markers)
|
||||
atoms_data[#atoms_data + 1] = {
|
||||
name = atom.name,
|
||||
total_words = total,
|
||||
name = atom.raw_name or atom.name,
|
||||
total_words = #(paths.word_events or {}),
|
||||
offsets = compute_offsets(labels, branches),
|
||||
}
|
||||
end
|
||||
|
||||
for _, atom in ipairs(scan.atoms or {}) do append_atom(atom) end
|
||||
for _, atom in ipairs(scan.raw_atoms or {}) do append_atom(atom) end
|
||||
if #atoms_data == 0 then return nil end
|
||||
|
||||
local out_path = src.dir .. "/gen/" .. duffle.basename_no_ext(src.dir) .. ".offsets.h"
|
||||
if not ctx.dry_run then
|
||||
duffle.ensure_dir(duffle.dirname(out_path))
|
||||
duffle.write_file(out_path, generate_header(src.path, atoms_data))
|
||||
duffle.write_file(out_path, generate_header(src.path:gsub("/", "\\"), atoms_data))
|
||||
end
|
||||
return out_path
|
||||
end
|
||||
|
||||
--- Run the offsets pass.
|
||||
--- For each source, emits a per-module `<dir_basename>.offsets.h` containing `#define _atom_offset_F_T = N` constants
|
||||
--- for every `atom_offset(F, T)` reference in the source's atoms.
|
||||
--- For each canonical source, emits a per-module `<dir_basename>.offsets.h`
|
||||
--- containing constants for every marker recorded in atom.paths.
|
||||
--- @param ctx PassCtx
|
||||
--- @return PassResult
|
||||
function M.run(ctx)
|
||||
@@ -348,7 +237,12 @@ function M.run(ctx)
|
||||
local errors = {}
|
||||
local warnings = {}
|
||||
|
||||
for _, src in ipairs(ctx.sources) do
|
||||
local corpus = ctx.shared and ctx.shared.corpus
|
||||
if type(corpus) ~= "table" or type(corpus.source_order) ~= "table" then
|
||||
error("offsets.run requires ctx.shared.corpus.source_order (canonical corpus).", 0)
|
||||
end
|
||||
|
||||
for _, src in ipairs(corpus.source_order) do
|
||||
local out_path = process_source(ctx, src)
|
||||
if out_path then
|
||||
outputs[#outputs + 1] = { offsets_h = out_path }
|
||||
|
||||
+29
-26
@@ -138,31 +138,31 @@ local PASS_NAME = "report"
|
||||
-- Per-MODULE annotation report (aggregated across all sources in a dir)
|
||||
-- ════════════════════════════════════════════════════════════════════════════
|
||||
|
||||
-- Extract the basename (last path segment) of a forward- or back-slash separated path. Returns the input unchanged if no separator is found.
|
||||
-- @param path string
|
||||
-- @return string
|
||||
--- Extract the basename (last path segment) of a forward- or back-slash separated path. Returns the input unchanged if no separator is found.
|
||||
--- @param path string
|
||||
--- @return string
|
||||
local function source_basename(path)
|
||||
return path:match(BASENAME_PATTERN) or path
|
||||
end
|
||||
|
||||
-- (internal) Format a single annotation entry as one rendered line.
|
||||
-- @param a AnnotEntry
|
||||
-- @param src_name string
|
||||
-- @return string
|
||||
--- (internal) Format a single annotation entry as one rendered line.
|
||||
--- @param a AnnotEntry
|
||||
--- @param src_name string
|
||||
--- @return string
|
||||
local function format_annot_line(a, src_name)
|
||||
if a.error then
|
||||
return string.format(" ✗ line %d %s [ERROR: %s] [%s]", a.line, a.macro or "?", a.error, src_name)
|
||||
end
|
||||
local line = string.format(" ● line %d %s [%s]", a.line, a.name, src_name)
|
||||
if a.binds then line = line .. " binds=" .. a.binds end
|
||||
if #a.reads > 0 then line = line .. " reads={" .. table.concat(a.reads, ",") .. "}" end
|
||||
if #a.writes > 0 then line = line .. " writes={" .. table.concat(a.writes, ",") .. "}" end
|
||||
if a.binds then line = line .. " binds=" .. a.binds end
|
||||
if #a.reads > 0 then line = line .. " reads={" .. table.concat(a.reads, ",") .. "}" end
|
||||
if #a.writes > 0 then line = line .. " writes={" .. table.concat(a.writes, ",") .. "}" end
|
||||
return line
|
||||
end
|
||||
|
||||
-- (internal) Tally totals across all results in a module.
|
||||
-- @param results AnnotationResult[]
|
||||
-- @return integer, integer, integer, integer, integer, integer
|
||||
--- (internal) Tally totals across all results in a module.
|
||||
--- @param results AnnotationResult[]
|
||||
--- @return integer, integer, integer, integer, integer, integer
|
||||
local function tally_module_totals(results)
|
||||
local total_atoms, total_annots, total_binds, total_macros = 0, 0, 0, 0
|
||||
local total_errors, total_warnings = 0, 0
|
||||
@@ -377,13 +377,13 @@ end
|
||||
-- Orchestration helpers
|
||||
-- ════════════════════════════════════════════════════════════════════════════
|
||||
|
||||
-- (internal) Pull per-source validate() results from the annotation pass's stash.
|
||||
-- The annotation pass runs first in the dep chain and caches results in `ctx.flags._annot_source_results`;
|
||||
-- we read from there instead of re-validating each source.
|
||||
-- Returns the list of module results + the flat list of all results (for the project-wide summary).
|
||||
-- @param ctx PassCtx
|
||||
-- @param dir_sources SourceFile[]
|
||||
-- @return AnnotationResult[], AnnotationResult[]
|
||||
--- (internal) Pull per-source validate() results from the annotation pass's stash.
|
||||
--- The annotation pass runs first in the dep chain and caches results in `ctx.flags._annot_source_results`;
|
||||
--- we read from there instead of re-validating each source.
|
||||
--- Returns the list of module results + the flat list of all results (for the project-wide summary).
|
||||
--- @param ctx PassCtx
|
||||
--- @param dir_sources SourceFile[]
|
||||
--- @return AnnotationResult[], AnnotationResult[]
|
||||
local function lookup_module_results(ctx, dir_sources)
|
||||
local src_cache = (ctx.flags and ctx.flags._annot_source_results) or {}
|
||||
local module_results = {}
|
||||
@@ -399,9 +399,9 @@ local function lookup_module_results(ctx, dir_sources)
|
||||
return module_results, all_results
|
||||
end
|
||||
|
||||
-- (internal) Does this module's results contain anything worth emitting?
|
||||
-- @param module_results AnnotationResult[]
|
||||
-- @return boolean
|
||||
--- (internal) Does this module's results contain anything worth emitting?
|
||||
--- @param module_results AnnotationResult[]
|
||||
--- @return boolean
|
||||
local function module_has_content(module_results)
|
||||
for _, r in ipairs(module_results) do
|
||||
if #r.atoms > 0 or #r.annots > 0 or #r.binds > 0
|
||||
@@ -412,8 +412,8 @@ local function module_has_content(module_results)
|
||||
return false
|
||||
end
|
||||
|
||||
-- (internal) Log a debug message if `_G[DEBUG_FLAG]` is truthy.
|
||||
-- @param fmt string
|
||||
--- (internal) Log a debug message if `_G[DEBUG_FLAG]` is truthy.
|
||||
--- @param fmt string
|
||||
local function debug_log(fmt, ...)
|
||||
if _G[DEBUG_FLAG] then
|
||||
io.stderr:write(string.format("[%s] " .. fmt, PASS_NAME, ...))
|
||||
@@ -436,7 +436,10 @@ function M.run(ctx)
|
||||
local warnings = {}
|
||||
|
||||
local module_entries = (ctx.flags and ctx.flags._annot_results) or {}
|
||||
local by_dir = ctx.by_dir or duffle.group_sources_by_dir(ctx.sources)
|
||||
-- Read module grouping from `corpus.sources_by_dir` (the canonical projection).
|
||||
-- Module grouping comes from `corpus.sources_by_dir`.
|
||||
local corpus = ctx.shared and ctx.shared.corpus
|
||||
local by_dir = (corpus and corpus.sources_by_dir) or {}
|
||||
|
||||
if not ctx.dry_run then duffle.ensure_dir(ctx.out_root) end
|
||||
|
||||
|
||||
+457
-140
@@ -25,6 +25,13 @@
|
||||
local _bootstrap_dir = debug.getinfo(1, "S").source:match("^@?(.*[/\\])") or "./"
|
||||
local duffle = dofile(_bootstrap_dir .. "../duffle_paths.lua")
|
||||
|
||||
-- Forward declarations for helpers used by earlier parsers (parse_enum_body_fields needs parse_enum_int_literal;
|
||||
-- parse_typedef_binds needs duffle.find_byte).
|
||||
-- Lua local scoping rules require explicit forward declarations because locals are visible only AFTER their declaration site.
|
||||
-- The actual assignments happen later in this file;
|
||||
-- the closures captured by the early parsers resolve the upvalue at call time (Lua 5.3 / LuaJIT upvalue semantics).
|
||||
local parse_enum_int_literal
|
||||
|
||||
-- ════════════════════════════════════════════════════════════════════════════
|
||||
-- Type declarations
|
||||
-- ════════════════════════════════════════════════════════════════════════════
|
||||
@@ -286,13 +293,13 @@ local POINTER_BYTE_SIZE = 4
|
||||
-- Maximum chain depth when resolving typedef / TSet_ chains (cycle guard).
|
||||
local TYPE_CHAIN_MAX_DEPTH = 8
|
||||
|
||||
-- Walk a `Struct_` / `Enum_` body, calling `build_field(first, first_end, after_first)` for each entry.
|
||||
-- The builder returns either:
|
||||
-- - (record, new_pos) -- append record to fields; advance body_pos to new_pos
|
||||
-- - (nil, new_pos) -- skip this entry; advance body_pos to new_pos
|
||||
-- After each entry, the walker skips a single trailing `,` or `;`.
|
||||
-- The 2 body-field parsers in this file (struct + enum) share this body-walk loop.
|
||||
-- @param body string
|
||||
--- Walk a `Struct_` / `Enum_` body, calling `build_field(first, first_end, after_first)` for each entry.
|
||||
--- The builder returns either:
|
||||
--- - (record, new_pos) -- append record to fields; advance body_pos to new_pos
|
||||
--- - (nil, new_pos) -- skip this entry; advance body_pos to new_pos
|
||||
--- After each entry, the walker skips a single trailing `,` or `;`.
|
||||
--- The 2 body-field parsers in this file (struct + enum) share this body-walk loop.
|
||||
--- @param body string
|
||||
--- @param build_field fun(first: string, first_end: integer, after_first: integer): (table|nil, integer)
|
||||
--- @return table[]
|
||||
local function walk_body_fields(body, build_field)
|
||||
@@ -328,7 +335,7 @@ local function parse_struct_body_fields(body)
|
||||
-- Parse the trailing `*` chain to derive pointer_depth.
|
||||
local depth, cursor = 0, after_type
|
||||
while cursor <= #body and body:sub(cursor, cursor) == "*" do
|
||||
depth = depth + 1
|
||||
depth = depth + 1
|
||||
cursor = cursor + 1
|
||||
cursor = duffle.skip_ws_and_cmt(body, cursor)
|
||||
end
|
||||
@@ -385,14 +392,12 @@ local function resolve_typedef_byte_size(type_name, type_name_registry, visited,
|
||||
local entry = type_name_registry[type_name]
|
||||
if not entry then return nil end
|
||||
|
||||
-- Confident: this entry was already resolved by the propagation pass
|
||||
-- (e.g., a builtin or a struct whose fields are all resolved).
|
||||
-- Confident: this entry was already resolved by the propagation pass (e.g., a builtin or a struct whose fields are all resolved).
|
||||
if entry.byte_size ~= nil then return entry.byte_size end
|
||||
|
||||
-- Chain-following: typedef / TSet_ aliases follow underlying_type.
|
||||
if entry.underlying_type then
|
||||
return resolve_typedef_byte_size(
|
||||
entry.underlying_type, type_name_registry, visited, depth + 1)
|
||||
return resolve_typedef_byte_size(entry.underlying_type, type_name_registry, visited, depth + 1)
|
||||
end
|
||||
|
||||
-- Struct_ entries with unresolved byte_size can still resolve when their fields are all resolved.
|
||||
@@ -414,7 +419,7 @@ end
|
||||
-- struct byte_size derives from confident fields; void is invalid and skipped; pointers collapse to 4 bytes at parse time.
|
||||
-- Mutates `out.type_name_registry[name].byte_size` AND each struct's fields' `offset` + `byte_size` in place.
|
||||
local function propagate_type_sizes(out)
|
||||
local reg = out.type_name_registry
|
||||
local reg = out.type_name_registry
|
||||
if not reg then return end
|
||||
|
||||
-- Seed builtin primitives (U1/U2/U4/S1/S2/S4 + __UINT*_TYPE__ family).
|
||||
@@ -527,25 +532,22 @@ end
|
||||
-- - a plain register ident: `R_FaceCursor` → reg_name only
|
||||
-- - register + atom_type sub-call: `R_FaceCursor atom_type(V4_S2*)` → reg_name + override entry
|
||||
-- Returns (reg_name, override_entry_or_nil, malformed_flag).
|
||||
-- On malformed `atom_type(...)` (missing close paren, trailing tokens after the close paren, empty type chain, trailing junk inside the
|
||||
-- parens like `V4_S2*()`), the function still returns the leading reg_name but sets `malformed_flag = true` and `override_entry = nil`
|
||||
-- so the caller can silently drop the override while keeping the register in the reads/writes list
|
||||
-- (per the parser-safety contract — "malformed atom_type MUST NOT create any override").
|
||||
-- On malformed `atom_type(...)` (missing close paren, trailing tokens after the close paren, empty type chain, trailing junk inside the parens like `V4_S2*()`),
|
||||
-- the function still returns the leading reg_name but sets `malformed_flag = true` and `override_entry = nil`
|
||||
-- so the caller can silently drop the override while keeping the register in the reads/writes list.
|
||||
local function parse_atom_info_reg_entry(entry)
|
||||
local pos = 1
|
||||
pos = duffle.skip_ws_and_cmt(entry, pos)
|
||||
if pos > #entry then return nil, nil, false end
|
||||
local reg_name, reg_end = duffle.read_ident(entry, pos)
|
||||
local reg_name, reg_end = duffle.read_ident(entry, pos)
|
||||
if not reg_name then return nil, nil, false end
|
||||
pos = duffle.skip_ws_and_cmt(entry, reg_end)
|
||||
-- Plain register, no adjacent atom_type — done.
|
||||
if pos > #entry then return reg_name, nil, false end
|
||||
|
||||
-- Adjacent ident must be a bare `atom_type` (word-bounded both sides).
|
||||
local next_ident, next_end = duffle.read_ident(entry, pos)
|
||||
if not next_ident or next_ident ~= "atom_type" then
|
||||
return reg_name, nil, false
|
||||
end
|
||||
local next_ident, next_end = duffle.read_ident(entry, pos)
|
||||
if not next_ident or next_ident ~= "atom_type" then return reg_name, nil, false end
|
||||
-- Left word-boundary: `_atom_type` should NOT match `atom_type`.
|
||||
if pos > 1 then
|
||||
local prev = entry:byte(pos - 1)
|
||||
@@ -559,27 +561,21 @@ local function parse_atom_info_reg_entry(entry)
|
||||
|
||||
-- Expect `(` immediately after `atom_type` (whitespace tolerated).
|
||||
pos = duffle.skip_ws_and_cmt(entry, next_end)
|
||||
if pos > #entry or entry:sub(pos, pos) ~= "(" then
|
||||
return reg_name, nil, true
|
||||
end
|
||||
if pos > #entry or entry:sub(pos, pos) ~= "(" then return reg_name, nil, true end
|
||||
local sub_inner, sub_after = duffle.read_parens(entry, pos)
|
||||
|
||||
-- Reject any trailing tokens (including `;` / `,`) after the close paren.
|
||||
-- The outer caller already split on top-level commas, so the only legal terminator here is end-of-entry.
|
||||
local after_close = duffle.skip_ws_and_cmt(entry, sub_after)
|
||||
if after_close <= #entry then
|
||||
return reg_name, nil, true
|
||||
end
|
||||
if after_close <= #entry then return reg_name, nil, true end
|
||||
|
||||
-- Parse the type chain inside the parens; require full consumption.
|
||||
-- parse_type_chain returns (ident, depth, end_pos);
|
||||
-- we reject any non-whitespace residue past end_pos (catches `V4_S2*()` etc.).
|
||||
local type_name, depth, after_chain = parse_type_chain(sub_inner, 1)
|
||||
-- We reject any non-whitespace residue past end_pos (catches `V4_S2*()` etc.).
|
||||
local type_name, depth, after_chain = parse_type_chain(sub_inner, 1)
|
||||
if not type_name then return reg_name, nil, true end
|
||||
local end_check = duffle.skip_ws_and_cmt(sub_inner, after_chain)
|
||||
if end_check <= #sub_inner then
|
||||
return reg_name, nil, true
|
||||
end
|
||||
if end_check <= #sub_inner then return reg_name, nil, true end
|
||||
|
||||
return reg_name, { type_name = type_name, pointer_depth = depth }, false
|
||||
end
|
||||
@@ -601,8 +597,8 @@ local function scan_atom_info_subcalls(info_inner, info_line)
|
||||
-- Per-subcall handler table. Each handler takes (sub_inner, info_line) and mutates the outer locals above.
|
||||
-- atom_reads/atom_writes share a handler (same shape; just different output target).
|
||||
local function rw_handler(sub_inner, info_line, kind)
|
||||
-- The reads/writes arrays contain ONLY register idents;
|
||||
-- the `atom_type(...)` sub-entry (when present and well-formed) is recorded as a per-atom reg_type_override.
|
||||
-- reads/writes arrays contain ONLY register idents;
|
||||
-- `atom_type(...)` sub-entry (when present and well-formed) is recorded as a per-atom reg_type_override.
|
||||
local entries = duffle.split_top_level_commas(sub_inner)
|
||||
local regs = {}
|
||||
for _, entry in ipairs(entries) do
|
||||
@@ -625,7 +621,7 @@ local function scan_atom_info_subcalls(info_inner, info_line)
|
||||
end
|
||||
local function ident_handler(sub_inner, info_line, out_name)
|
||||
-- Validates the arg is a single C identifier (no commas / parens / whitespace).
|
||||
-- Silently reject malformed args; the DWARF chain treats the field as un-set.
|
||||
-- Silently reject malformed args; DWARF chain treats the field as un-set.
|
||||
local name = duffle.read_ident(sub_inner, 1)
|
||||
if name then
|
||||
local after_id = duffle.skip_ws_and_cmt(sub_inner, 1 + #name)
|
||||
@@ -718,6 +714,7 @@ local BYTE_HASH = 0x23 -- '#'
|
||||
local BYTE_NEWLINE = 0x0A -- '\n'
|
||||
local BYTE_DASH = 0x2D -- '-'
|
||||
local BYTE_COMMA = 0x2C -- ','
|
||||
local BYTE_SEMI = 0x3B -- ';'
|
||||
local BYTE_EQUAL = 0x3D -- '='
|
||||
local BYTE_R = 0x52 -- 'R'
|
||||
local BYTE_UNDERSCORE = 0x5F -- '_'
|
||||
@@ -754,13 +751,16 @@ local function hex_digit_value(b)
|
||||
return nil
|
||||
end
|
||||
|
||||
-- Parse a decimal/negative-decimal/hex integer literal starting at byte position `start`.
|
||||
-- Returns (value, end_pos) on success, or (nil, start) on failure / no match.
|
||||
-- Accepts: 12, -1, 0, 0x10, 0X1F, -0x10.
|
||||
-- @param text string
|
||||
-- @param start integer
|
||||
-- @return integer|nil, integer
|
||||
local function parse_enum_int_literal(text, start)
|
||||
--- Parse a decimal/negative-decimal/hex integer literal starting at byte position `start`.
|
||||
--- Returns (value, end_pos) on success, or (nil, start) on failure / no match.
|
||||
--- Accepts: 12, -1, 0, 0x10, 0X1F, -0x10.
|
||||
--- @param text string
|
||||
--- @param start integer
|
||||
--- @return integer|nil, integer
|
||||
--- Implementation note: this is a plain assignment (not `local function`)
|
||||
--- so the forward declaration at the top of the file is the same upvalue the earlier `parse_enum_body_fields` closure captures.
|
||||
--- Lua 5.3 / LuaJIT upvalue semantics resolve the assignment at call time.
|
||||
parse_enum_int_literal = function(text, start)
|
||||
local pos = start
|
||||
local len = #text
|
||||
if pos > len then return nil, start end
|
||||
@@ -847,10 +847,10 @@ local function resolve_code_macro_value(source, pos, code_macros, code_macro_bod
|
||||
|
||||
-- Try integer literal first (decimal / negative / hex).
|
||||
local int_val, int_end = parse_enum_int_literal(source, pos)
|
||||
if int_val ~= nil then return int_val end
|
||||
if int_val ~= nil then return int_val end
|
||||
|
||||
-- Try symbol reference (must be R_*_Code per the spec).
|
||||
local sym = duffle.read_ident(source, pos)
|
||||
local sym = duffle.read_ident(source, pos)
|
||||
if not sym then return nil end
|
||||
if not is_r_code_macro(sym) then return nil end
|
||||
|
||||
@@ -864,7 +864,7 @@ local function resolve_code_macro_value(source, pos, code_macros, code_macro_bod
|
||||
-- Cross-source fallback: walk the body of the missing symbol if any other source collected its RHS during pass 1a.
|
||||
-- The body is parsed as a fresh RHS (it's the raw post-`=` text of the original `#define` line), so the chain continues transparently.
|
||||
local body = code_macro_bodies and code_macro_bodies[sym]
|
||||
if body then
|
||||
if body then
|
||||
return resolve_code_macro_value(body, 1, code_macros, code_macro_bodies, visited, depth + 1)
|
||||
end
|
||||
|
||||
@@ -874,14 +874,14 @@ local function resolve_code_macro_value(source, pos, code_macros, code_macro_bod
|
||||
return nil
|
||||
end
|
||||
|
||||
-- Intercept a `#define R_*_Code <RHS>` preprocessor line.
|
||||
-- Always saves the raw RHS text into `code_macro_bodies` (for cross-source fallback during chain resolution),
|
||||
-- then (if resolvable) stores the resolved integer code into `code_macros` keyed by the macro name.
|
||||
-- `directive_start` points at the `#` byte. The function is silent on non-matching directives, the caller skips the line in any case.
|
||||
-- @param source string
|
||||
-- @param directive_start integer -- byte position of `#`
|
||||
-- @param code_macros table -- out._code_macros / ctx.shared._code_macros
|
||||
-- @param code_macro_bodies table -- out._code_macro_bodies / ctx.shared._code_macro_bodies
|
||||
--- Intercept a `#define R_*_Code <RHS>` preprocessor line.
|
||||
--- Always saves the raw RHS text into `code_macro_bodies` (for cross-source fallback during chain resolution),
|
||||
--- then (if resolvable) stores the resolved integer code into `code_macros` keyed by the macro name.
|
||||
--- `directive_start` points at the `#` byte. The function is silent on non-matching directives, the caller skips the line in any case.
|
||||
--- @param source string
|
||||
--- @param directive_start integer -- byte position of `#`
|
||||
--- @param code_macros table -- out._code_macros / ctx.shared._code_macros
|
||||
--- @param code_macro_bodies table -- out._code_macro_bodies / ctx.shared._code_macro_bodies
|
||||
local function try_extract_code_macro(source, directive_start, code_macros, code_macro_bodies)
|
||||
local rest = duffle.skip_ws_and_cmt(source, directive_start + 1)
|
||||
local kw, kw_end = duffle.read_ident(source, rest)
|
||||
@@ -907,13 +907,13 @@ local function try_extract_code_macro(source, directive_start, code_macros, code
|
||||
if value ~= nil then code_macros[macro_name] = value end
|
||||
end
|
||||
|
||||
-- Quick pre-pass: walk the source looking ONLY for `#define R_*_Code` lines.
|
||||
-- Populates `code_macros` with resolved integer codes AND `code_macro_bodies` with raw RHS text
|
||||
-- (used by the chain walker as cross-source fallback during pass 1b in `M.run`); ignores everything else.
|
||||
-- Used by `M.run` pass 1a to build the cross-source `_code_macros` + `_code_macro_bodies` registries before pass 1b resolves chains.
|
||||
-- @param source string
|
||||
-- @param code_macros table
|
||||
-- @param code_macro_bodies table
|
||||
--- Quick pre-pass: walk the source looking ONLY for `#define R_*_Code` lines.
|
||||
--- Populates `code_macros` with resolved integer codes AND `code_macro_bodies` with raw RHS text
|
||||
--- (used by the chain walker as cross-source fallback during pass 1b in `M.run`); ignores everything else.
|
||||
--- Used by `M.run` pass 1a to build the cross-source `_code_macros` + `_code_macro_bodies` registries before pass 1b resolves chains.
|
||||
--- @param source string
|
||||
--- @param code_macros table
|
||||
--- @param code_macro_bodies table
|
||||
local function scan_source_pre_pass(source, code_macros, code_macro_bodies)
|
||||
local pos = 1
|
||||
local src_len = #source
|
||||
@@ -1012,7 +1012,7 @@ end
|
||||
--
|
||||
-- All parsers read source-as-written via the duffle primitives (skip_ws_and_cmt / read_parens / read_braces / read_balanced).
|
||||
-- No regex per the no_regex constraint; no hand-rolled depth tracking
|
||||
-- (the MipsAtomComp_Proc_ brace matcher now uses duffle.read_braces instead of bespoke byte-dispatch).
|
||||
-- The MipsAtomComp_Proc_ brace matcher uses `duffle.read_braces`.
|
||||
--
|
||||
-- Adding a new construct = 1 row in DECL_PARSERS + 1 parser function. The scan_source() loop never needs editing.
|
||||
|
||||
@@ -1186,7 +1186,7 @@ local function parse_mips_atom_comp_proc(source, pos, ident_end, line_of, out)
|
||||
if not last_brace_pos then return after_paren end
|
||||
|
||||
-- Use duffle.read_braces to find the matching close brace.
|
||||
-- Replaces the pre-refactor hand-rolled depth tracker (~25 LOC of `if c == 123 then depth = depth + 1 ...`).
|
||||
-- Uses `read_balanced` for delimiter-depth tracking.
|
||||
-- If close_pos is past the end of inner, the brace didn't match (malformed input); skip.
|
||||
local body, close_pos = duffle.read_braces(inner, last_brace_pos)
|
||||
if close_pos > #inner + 1 then return after_paren end
|
||||
@@ -1340,10 +1340,9 @@ local function parse_typedef_binds(source, pos, ident_end, line_of, out)
|
||||
register_struct_type(body, name, pos, line_of, out)
|
||||
associate_skip_over_marker(out, name, name, "unrelated", line_of(pos), pos)
|
||||
return after_brace
|
||||
end
|
||||
|
||||
-- ── Shape 2: `typedef Enum_(<underlying>, <name>) { <body> } <alias>;`
|
||||
if id2 == "Enum_" then
|
||||
elseif id2 == "Enum_" then
|
||||
local inner, after_paren, open_paren = read_parens_after(source, id2_end, id2_end)
|
||||
if not inner then return id2_end end
|
||||
-- Split `inner` on the first top-level comma into (<underlying>, <name>).
|
||||
@@ -1359,35 +1358,104 @@ local function parse_typedef_binds(source, pos, ident_end, line_of, out)
|
||||
return after_brace
|
||||
end
|
||||
|
||||
-- ── Shapes 3 + 4: `typedef <type> <alias>;` or
|
||||
-- `typedef <type> TSet_(<name>);`
|
||||
-- Read the <type> ident we already have (id2) and the trailing alias.
|
||||
local after_id2 = duffle.skip_ws_and_cmt(source, id2_end)
|
||||
local id3, id3_end = duffle.read_ident(source, after_id2)
|
||||
if not id3 then return ident_end end
|
||||
-- ── Shapes 3 + 4: `typedef <span> <alias>;` or
|
||||
-- `typedef <span> TSet_(<name>);`
|
||||
--
|
||||
-- The <span> between `typedef` and the alias ident may be MULTI-token (e.g. `unsigned char`, `__UINT8_TYPE__`, `const U4`).
|
||||
-- The alias is the LAST identifier before `;` (Shape 3), OR the argument of `TSet_(...)` when that wrapper is present (Shape 4).
|
||||
--
|
||||
-- Two required exact outcomes:
|
||||
-- typedef unsigned char UTF8; -> name=UTF8, underlying="unsigned char"
|
||||
-- typedef __UINT8_TYPE__ TSet_(U1); -> name=U1, underlying="__UINT8_TYPE__"
|
||||
--
|
||||
-- Algorithm:
|
||||
-- 1. Find the terminating `;` (BYTE_SEMI). If absent, abort cleanly.
|
||||
-- 2. If id2 itself is `TSet_`, capture the parenthesized argument and use it as the alias (the preceding underlying span is empty).
|
||||
-- 3. Otherwise walk idents forward from id2_end to semi_pos:
|
||||
-- - If any ident is `TSet_`, capture its argument as the alias and mark the underlying span as everything from id2 to before TSet_.
|
||||
-- - Otherwise remember the last ident (and its source position) as the alias; the underlying span is everything from id2 to before that ident.
|
||||
-- 4. Trim the underlying span and call register_typedef_alias.
|
||||
--
|
||||
-- Struct_/Enum_ declarations carry their own dedicated parser paths above; this branch only handles non-Struct_/Enum_ typedefs.
|
||||
|
||||
-- Shape 4: `typedef <type> TSet_(<name>);`
|
||||
if id3 == "TSet_" then
|
||||
local tset_inner, tset_after = read_parens_after(source, id3_end, id3_end)
|
||||
if not tset_inner then return id3_end end
|
||||
local tset_name = duffle.trim(tset_inner)
|
||||
register_typedef_alias(id2, tset_name, pos, line_of, out)
|
||||
-- Find the terminating `;` (BYTE_SEMI). If absent, abort cleanly.
|
||||
local semi_pos = duffle.find_byte(source, BYTE_SEMI, id2_end)
|
||||
if not semi_pos then return id2_end end
|
||||
|
||||
-- Shape 4 (TSet_ at id2 position): no preceding underlying span.
|
||||
if id2 == "TSet_" then
|
||||
local inner, after_paren = read_parens_after(source, id2_end, id2_end)
|
||||
if not inner then return id2_end end
|
||||
local tset_name = duffle.trim(inner)
|
||||
-- Empty underlying span is acceptable; the TSet_ wrapper itself
|
||||
-- encodes the alias identity (per the duffle TSet_ convention).
|
||||
register_typedef_alias("", tset_name, pos, line_of, out)
|
||||
associate_skip_over_marker(out, tset_name, tset_name, "unrelated", line_of(pos), pos)
|
||||
return tset_after
|
||||
return after_paren
|
||||
end
|
||||
|
||||
-- Shape 3: `typedef <type> <alias>;`
|
||||
register_typedef_alias(id2, id3, pos, line_of, out)
|
||||
associate_skip_over_marker(out, id3, id3, "unrelated", line_of(pos), pos)
|
||||
return id3_end
|
||||
-- Walk idents forward to find the alias ident (last ident before `;`), or the TSet_(<arg>) form (capture the arg, use it as the alias).
|
||||
local last_ident = nil
|
||||
local last_ident_pos = nil
|
||||
local last_ident_end = nil
|
||||
local tset_arg = nil
|
||||
local tset_arg_end = nil
|
||||
local tset_pos = nil
|
||||
|
||||
local scan = id2_end
|
||||
while scan < semi_pos do
|
||||
scan = duffle.skip_ws_and_cmt(source, scan)
|
||||
if scan >= semi_pos then break end
|
||||
local id, id_end = duffle.read_ident(source, scan)
|
||||
if not id then
|
||||
scan = scan + 1
|
||||
elseif id == "TSet_" then
|
||||
-- Shape 4 (TSet_ at non-id2 position): grab the parenthesized argument.
|
||||
local inner, after_paren = read_parens_after(source, id_end, id_end)
|
||||
if inner then
|
||||
tset_arg = duffle.trim(inner)
|
||||
tset_arg_end = after_paren
|
||||
tset_pos = scan
|
||||
scan = after_paren
|
||||
else
|
||||
scan = id_end
|
||||
end
|
||||
else
|
||||
last_ident = id
|
||||
last_ident_pos = scan
|
||||
last_ident_end = id_end
|
||||
scan = id_end
|
||||
end
|
||||
end
|
||||
|
||||
if tset_arg then
|
||||
-- Shape 4: alias is the TSet_ argument; the underlying span is the trimmed text from the start of id2 up to (but not including) the TSet_ ident.
|
||||
local underlying_span = source:sub(after_typedef, tset_pos - 1)
|
||||
local underlying = duffle.trim(underlying_span)
|
||||
register_typedef_alias(underlying, tset_arg, pos, line_of, out)
|
||||
associate_skip_over_marker(out, tset_arg, tset_arg, "unrelated", line_of(pos), pos)
|
||||
return tset_arg_end or (semi_pos + 1)
|
||||
end
|
||||
|
||||
if last_ident then
|
||||
-- Shape 3: alias is the last ident before `;`; the underlying span is the trimmed text from the start of id2 up to (but not including) the alias ident.
|
||||
local underlying_span = source:sub(after_typedef, last_ident_pos - 1)
|
||||
local underlying = duffle.trim(underlying_span)
|
||||
register_typedef_alias(underlying, last_ident, pos, line_of, out)
|
||||
associate_skip_over_marker(out, last_ident, last_ident, "unrelated", line_of(pos), pos)
|
||||
return last_ident_end
|
||||
end
|
||||
|
||||
-- Malformed: id2 with no following ident before `;`. Skip past the terminating semicolon and let the main loop continue.
|
||||
return semi_pos + 1
|
||||
end
|
||||
|
||||
--- Parse: `_Pragma("mac_X tape_atom words=N")` (operator form).
|
||||
--- @param source string
|
||||
--- @param pos integer
|
||||
--- @param source string
|
||||
--- @param pos integer
|
||||
--- @param ident_end integer
|
||||
--- @param line_of fun(pos: integer): integer
|
||||
--- @param out SourceScan
|
||||
--- @param line_of fun(pos: integer): integer
|
||||
--- @param out SourceScan
|
||||
--- @return integer
|
||||
local function parse_pragma_macro(source, pos, ident_end, line_of, out)
|
||||
local str, str_end = read_parens_after(source, ident_end)
|
||||
@@ -1413,30 +1481,17 @@ local function parse_pragma_macro(source, pos, ident_end, line_of, out)
|
||||
return str_end
|
||||
end
|
||||
|
||||
--- Parse: `pragma` ident (no-op — directive form `#pragma` is handled by `skip_preprocessor_line` upstream).
|
||||
--- If we reach this parser it means the directive skip didn't fire, which can happen for non-#-prefixed pragma.
|
||||
--- Just advance past the ident.
|
||||
--- @param source string
|
||||
--- @param pos integer
|
||||
--- @param ident_end integer
|
||||
--- @param line_of fun(pos: integer): integer
|
||||
--- @param out SourceScan
|
||||
--- @return integer
|
||||
local function parse_pragma_dummy(source, pos, ident_end, line_of, out)
|
||||
return ident_end
|
||||
end
|
||||
|
||||
-- Parse the value side of an enum entry.
|
||||
-- Accepts integer literals (decimal/negative/hex), `R_*_Code` symbol references resolved via `out._code_macros`,
|
||||
-- AND bare `R_*` symbols that map to an `R_*_Code` variant in the registry.
|
||||
-- The bare-`R_*` fallback is needed for the lottes_tape.h wave-context aliases whose enum RHS is the bare register ident (e.g. `R_TapePtr = R_T8 atom_reg`);
|
||||
-- the `R_*_Code` form (e.g. `R_T8_Code`) holds the actual GPR code in mips.h and is now resolvable cross-source via the chain walker.
|
||||
-- The `R_*_Code` form holds the GPR code and resolves across sources through the chain walker.
|
||||
-- Returns (value, end_pos) on success or (nil, pos) if unresolvable.
|
||||
local function parse_enum_value(body, pos, out)
|
||||
local int_val, int_end = parse_enum_int_literal(body, pos)
|
||||
if int_val ~= nil then return int_val, int_end end
|
||||
if int_val ~= nil then return int_val, int_end end
|
||||
|
||||
local sym = duffle.read_ident(body, pos)
|
||||
local sym = duffle.read_ident(body, pos)
|
||||
if not sym then return nil, pos end
|
||||
|
||||
-- Bare `R_*` (no `_Code` suffix) → translate to `R_*_Code` and look that up. Non-`R_*` symbols
|
||||
@@ -1580,7 +1635,6 @@ local DECL_PARSERS = {
|
||||
MipsCode = parse_mips_code,
|
||||
typedef = parse_typedef_binds,
|
||||
_Pragma = parse_pragma_macro,
|
||||
pragma = parse_pragma_dummy,
|
||||
-- `enum [<tag>] { <body> }` populates `out.register_alias_registry`.
|
||||
enum = parse_enum,
|
||||
}
|
||||
@@ -1655,8 +1709,7 @@ local function scan_source(source, source_file, code_macros, code_macro_bodies)
|
||||
if parser then
|
||||
pos = parser(source, pos, ident_end, line_of, out)
|
||||
else
|
||||
-- A component-procedure declaration has an FI_ signature before MipsAtomComp_Proc_;
|
||||
-- keep the marker pending across that prelude.
|
||||
-- A component-procedure declaration has an FI_ signature before MipsAtomComp_Proc_; keep the marker pending across that prelude.
|
||||
-- Any other identifier begins an unrelated declaration/construct and consumes the marker so it cannot drift to a later atom.
|
||||
local markers = out.skip_over.markers
|
||||
local marker = markers[#markers]
|
||||
@@ -1692,6 +1745,252 @@ local function scan_source(source, source_file, code_macros, code_macro_bodies)
|
||||
return out
|
||||
end
|
||||
|
||||
-- ════════════════════════════════════════════════════════════════════════════
|
||||
-- Corpus merge — first-wins lookup identity + typed collisions
|
||||
-- ════════════════════════════════════════════════════════════════════════════
|
||||
-- These helpers run ONCE per `M.run` invocation, after every per-source scan has attached `src.scan`.
|
||||
-- They merge per-source scans into the canonical `ctx.shared.corpus.*` registries.
|
||||
-- The corpus is the source of truth; `src.scan` keeps the source-local projection for the duration of the run but the cross-source visibility lives on `corpus`.
|
||||
|
||||
-- Build a deterministic site record (path + line) from a per-source entry.
|
||||
-- Falls back to the placeholder when an entry lacks a recorded source file or line.
|
||||
local function build_site(path, line)
|
||||
return { path = path or "?", line = line or 0 }
|
||||
end
|
||||
|
||||
-- Compute a deterministic shape signature for an AliasEntry (register_alias_registry).
|
||||
-- Two alias declarations are "identical" iff they resolve to the same shape:
|
||||
-- code (integer) + default_type + default_depth + pointer_depth + has_atom_reg.
|
||||
local function alias_shape(entry)
|
||||
if type(entry) ~= "table" then return "" end
|
||||
return string.format("code=%s;default=%s/%s;depth=%s;atom_reg=%s",
|
||||
tostring(entry.code),
|
||||
tostring(entry.default_type or ""),
|
||||
tostring(entry.default_depth or 0),
|
||||
tostring(entry.pointer_depth or 0),
|
||||
tostring(entry.has_atom_reg and 1 or 0))
|
||||
end
|
||||
|
||||
-- Compute a deterministic shape signature for a type-name registry entry.
|
||||
-- struct: serialized fields (name:type:depth, in declaration order)
|
||||
-- enum: serialized fields (name=value, declaration order)
|
||||
-- typedef: The underlying_type string
|
||||
local function type_shape(entry)
|
||||
if type(entry) ~= "table" then return "" end
|
||||
if entry.kind == "struct" then
|
||||
local fields = entry.fields or {}
|
||||
local parts = {}
|
||||
for _, f in ipairs(fields) do
|
||||
parts[#parts + 1] = string.format("%s:%s*%s",
|
||||
tostring(f.name), tostring(f.type_name), tostring(f.pointer_depth or 0))
|
||||
end
|
||||
return "struct[" .. table.concat(parts, ",") .. "]"
|
||||
elseif entry.kind == "enum" then
|
||||
local fields = entry.fields or {}
|
||||
local parts = {}
|
||||
for _, f in ipairs(fields) do
|
||||
parts[#parts + 1] = string.format("%s=%s", tostring(f.name), tostring(f.value))
|
||||
end
|
||||
return "enum[" .. table.concat(parts, ",") .. "]"
|
||||
elseif entry.kind == "typedef" then
|
||||
return "typedef[" .. tostring(entry.underlying_type or "") .. "]"
|
||||
end
|
||||
return "?"
|
||||
end
|
||||
|
||||
-- Compute a deterministic shape signature for a Binds_* entry (Struct_ projection).
|
||||
-- Binds_* entries come from scan.binds[] (per-source array) with `{line, name, fields, body, bytes}`;
|
||||
-- They share the same `fields` layout as the matching struct entry in `type_name_registry`, so the shape is the struct-field serialization.
|
||||
-- The `kind` field is absent here, so the struct branch of `type_shape` would misfire; we serialize the fields directly.
|
||||
local function bind_shape(entry)
|
||||
if type(entry) ~= "table" then return "" end
|
||||
local fields = entry.fields or {}
|
||||
local parts = {}
|
||||
for _, f in ipairs(fields) do
|
||||
parts[#parts + 1] = string.format("%s:%s*%s",
|
||||
tostring(f.name), tostring(f.type_name), tostring(f.pointer_depth or 0))
|
||||
end
|
||||
return "struct[" .. table.concat(parts, ",") .. "]"
|
||||
end
|
||||
|
||||
-- Compute a deterministic shape signature for an AtomEntry.
|
||||
-- The "kind + body" pair uniquely identifies the same declaration when re-encountered.
|
||||
-- `body` is the brace-delimited body text.
|
||||
local function atom_shape(entry)
|
||||
if type(entry) ~= "table" then return "" end
|
||||
return string.format("kind=%s;body=%s",
|
||||
tostring(entry.kind or ""), tostring(entry.body or ""))
|
||||
end
|
||||
|
||||
-- Compute a deterministic shape signature for an AtomViewEntry.
|
||||
-- Two views are identical iff they bind the same Binds_X with the same reg overrides.
|
||||
local function view_shape(entry)
|
||||
if type(entry) ~= "table" then return "" end
|
||||
local overrides = entry.reg_type_overrides or {}
|
||||
local keys = {}
|
||||
for k in pairs(overrides) do keys[#keys + 1] = k end
|
||||
table.sort(keys, function(a, b) return tostring(a) < tostring(b) end)
|
||||
local parts = {}
|
||||
for _, k in ipairs(keys) do
|
||||
local ov = overrides[k]
|
||||
parts[#parts + 1] = string.format("%s=%s*%s", tostring(k),
|
||||
tostring(ov.type_name), tostring(ov.pointer_depth or 0))
|
||||
end
|
||||
return string.format("binds=%s;overrides=[%s]",
|
||||
tostring(entry.binds_name or ""), table.concat(parts, ","))
|
||||
end
|
||||
|
||||
-- Compute a deterministic shape signature for an AtomCtxEntry.
|
||||
local function ctx_shape(entry)
|
||||
if type(entry) ~= "table" then return "" end
|
||||
return "rbind=" .. tostring(entry.rbind_atom or "")
|
||||
end
|
||||
|
||||
-- Compute a deterministic shape signature for an AtomPhaseGroup.
|
||||
-- The atoms list is compared as a SET (sorted) so that two declarations of `atom_phase(setup)` with atoms = [A] vs atoms = [B]
|
||||
-- are DIFFERENT shapes (first-wins + collision), while atoms = [A,B] vs atoms = [B,A] are considered identical (and coalesce).
|
||||
local function phase_shape(entry)
|
||||
if type(entry) ~= "table" then return "" end
|
||||
local atoms = entry.atoms or {}
|
||||
local sorted = {}
|
||||
for _, a in ipairs(atoms) do sorted[#sorted + 1] = a end
|
||||
table.sort(sorted)
|
||||
return "atoms=[" .. table.concat(sorted, ",") .. "]"
|
||||
end
|
||||
|
||||
-- Merge a new declaration site into a registry following the first-wins discipline.
|
||||
-- * first declaration: Entry becomes the canonical corpus entry (entry.sites initialized).
|
||||
-- * identical subsequent: Append the new site to entry.sites (no collision).
|
||||
-- * conflicting shape: Keep first entry, append ONE typed collision record with shape diff.
|
||||
local function merge_named_with_sites(registry, name, new_entry, site, collisions, kind, shape_fn)
|
||||
if registry[name] == nil then
|
||||
registry[name] = new_entry
|
||||
registry[name].sites = { site }
|
||||
return
|
||||
end
|
||||
local existing = registry[name]
|
||||
local new_shape = shape_fn(new_entry)
|
||||
local old_shape = shape_fn(existing)
|
||||
if new_shape == old_shape and new_shape ~= "" then
|
||||
-- Identical shape: coalesce by appending the site.
|
||||
existing.sites = existing.sites or { build_site(existing.source_file, existing.source_line) }
|
||||
existing.sites[#existing.sites + 1] = site
|
||||
return
|
||||
end
|
||||
-- Conflicting shape: first-wins; record exactly one typed collision.
|
||||
collisions[#collisions + 1] = {
|
||||
kind = kind,
|
||||
name = name,
|
||||
first_site = existing.sites and existing.sites[1]
|
||||
or build_site(existing.source_file, existing.source_line),
|
||||
conflicting_site = site,
|
||||
first_shape = old_shape,
|
||||
conflicting_shape = new_shape,
|
||||
}
|
||||
end
|
||||
|
||||
-- Merge per-source scans into the canonical corpus registries.
|
||||
-- Iterates `corpus.source_order` (not `ctx.sources`) — the corpus is the source of truth.
|
||||
-- Each source owns only its `src.scan`; the corpus owns the cross-source lookup tables.
|
||||
local function merge_corpus_registries(corpus)
|
||||
-- Ensure every expected corpus table exists (the fixture_ctx seeds most of these,
|
||||
-- but a barebones corpus from build_ctx should also be safe).
|
||||
corpus.register_alias_registry = corpus.register_alias_registry or {}
|
||||
corpus.type_name_registry = corpus.type_name_registry or {}
|
||||
corpus.binds_by_name = corpus.binds_by_name or {}
|
||||
corpus.atoms_by_name = corpus.atoms_by_name or {}
|
||||
corpus.atom_views = corpus.atom_views or {}
|
||||
corpus.atom_ctxs = corpus.atom_ctxs or {}
|
||||
corpus.atom_phases = corpus.atom_phases or {}
|
||||
corpus.atom_infos = corpus.atom_infos or {}
|
||||
corpus.collisions = corpus.collisions or {}
|
||||
|
||||
-- Replace the existing corpus collections with empty tables so a re-run on the same corpus produces identical state (deterministic merge).
|
||||
-- This is safe because M.run is the only writer to these tables within a single orchestrator invocation.
|
||||
for _, key in ipairs({
|
||||
"register_alias_registry", "type_name_registry", "binds_by_name",
|
||||
"atoms_by_name", "atom_views", "atom_ctxs", "atom_phases",
|
||||
"atom_infos", "collisions",
|
||||
}) do
|
||||
corpus[key] = {}
|
||||
end
|
||||
|
||||
for _, src in ipairs(corpus.source_order or {}) do
|
||||
local scan = src.scan
|
||||
if scan then
|
||||
local path = src.path
|
||||
-- register_alias_registry: keyed by R_* alias ident.
|
||||
-- Each AliasEntry carries `source_file` (set by scan_source to the path).
|
||||
for name, entry in pairs(scan.register_alias_registry or {}) do
|
||||
local site = build_site(entry.source_file or path, entry.source_line)
|
||||
merge_named_with_sites(
|
||||
corpus.register_alias_registry, name, entry, site,
|
||||
corpus.collisions, "alias", alias_shape)
|
||||
end
|
||||
|
||||
-- type_name_registry: keyed by type ident; covers Struct_/Enum_/typedef.
|
||||
for name, entry in pairs(scan.type_name_registry or {}) do
|
||||
local site = build_site(path, entry.source_line)
|
||||
merge_named_with_sites(
|
||||
corpus.type_name_registry, name, entry, site,
|
||||
corpus.collisions, "type", type_shape)
|
||||
end
|
||||
|
||||
-- binds_by_name: the Binds_* projection of Struct_ types.
|
||||
-- Sources populate scan.binds[] (per-source array) with `{line, name, fields, body}`.
|
||||
-- We merge by name (Binds_X) so cross-source Struct_(X) declarations can be coalesced or collided.
|
||||
for _, bind_entry in ipairs(scan.binds or {}) do
|
||||
local site = build_site(path, bind_entry.line)
|
||||
merge_named_with_sites(
|
||||
corpus.binds_by_name, bind_entry.name, bind_entry, site,
|
||||
corpus.collisions, "binds", bind_shape)
|
||||
end
|
||||
|
||||
-- atoms_by_name: MipsAtom_(name) + MipsAtomComp_(name) + MipsAtomComp_Proc_(name).
|
||||
-- Each atom carries `{line, name, body, body_off, kind, raw_name, ...}`.
|
||||
-- Duplicate atom names across sources are first-wins + collision; see the atom_infos block below for the evidence list.
|
||||
for _, atom_entry in ipairs(scan.atoms or {}) do
|
||||
local site = build_site(path, atom_entry.line)
|
||||
merge_named_with_sites(
|
||||
corpus.atoms_by_name, atom_entry.name, atom_entry, site,
|
||||
corpus.collisions, "atom", atom_shape)
|
||||
end
|
||||
|
||||
-- atom_views: keyed by atom_name; each carries `binds_name` + per-atom overrides.
|
||||
for name, entry in pairs(scan.atom_views or {}) do
|
||||
local site = build_site(path, entry.info_line)
|
||||
merge_named_with_sites(
|
||||
corpus.atom_views, name, entry, site,
|
||||
corpus.collisions, "view", view_shape)
|
||||
end
|
||||
|
||||
-- atom_ctxs: keyed by atom_name; each carries `rbind_atom`.
|
||||
for name, entry in pairs(scan.atom_ctxs or {}) do
|
||||
local site = build_site(path, entry.info_line)
|
||||
merge_named_with_sites(
|
||||
corpus.atom_ctxs, name, entry, site,
|
||||
corpus.collisions, "ctx", ctx_shape)
|
||||
end
|
||||
|
||||
-- atom_phases: keyed by phase label; each carries `atoms = [...]`.
|
||||
-- The phase atoms list is set-shaped for the collision discipline (phase_shape sorts atoms before comparing).
|
||||
for name, entry in pairs(scan.atom_phases or {}) do
|
||||
local site = build_site(path, 0)
|
||||
merge_named_with_sites(
|
||||
corpus.atom_phases, name, entry, site,
|
||||
corpus.collisions, "phase", phase_shape)
|
||||
end
|
||||
|
||||
-- atom_infos: ALWAYS append every record in source/declaration order.
|
||||
-- Duplicates are preserved so the annotation pass can flag them via `check_unique_annotation`;
|
||||
-- The merge is purely order-preserving.
|
||||
for _, info in ipairs(scan.atom_infos or {}) do
|
||||
corpus.atom_infos[#corpus.atom_infos + 1] = info
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
-- ════════════════════════════════════════════════════════════════════════════
|
||||
-- M — module exports
|
||||
-- ════════════════════════════════════════════════════════════════════════════
|
||||
@@ -1703,37 +2002,50 @@ local M = {}
|
||||
--- Walk each source once and attach the fat SourceScan payload to `src.scan`.
|
||||
--- No output files; this is a pure in-memory pre-processing pass.
|
||||
---
|
||||
--- Runs in 3 phases.
|
||||
--- Pass 1a: `scan_source_pre_pass` over every source, populating the cross-source `ctx.shared._code_macros` AND `ctx.shared._code_macro_bodies` registries.
|
||||
--- The bodies table holds the raw post-`=` text of every `#define R_*_Code` line (cross-source) so the chain walker can fall back when a
|
||||
--- sdefining `#define` lives in a different source than the chain call site.
|
||||
--- Pass 1b: Resolve every collected macro's chain using the bodies table as fallback.
|
||||
--- This fills in `code_macros` entries whose defining source was scanned AFTER the call site
|
||||
--- (e.g. lottes_tape.h's `R_TapePtr_Code -> R_T8_Code` chain into mips.h's `R_T8_Code = 24`).
|
||||
--- Pass 2: The full `scan_source(source, source_file, code_macros, code_macro_bodies)` walk, which feeds `out._code_macros = ctx.shared._code_macros`
|
||||
--- (and `out._code_macro_bodies = ctx.shared._code_macro_bodies`)
|
||||
--- so the enum parser can resolve cross-source `R_*_Code` references and bare `R_*` symbols via the `_Code` registry fallback.
|
||||
--- Strip: `src.scan._code_macros` AND `src.scan._code_macro_bodies` are nilled before returning so downstream passes
|
||||
--- (annotation, components, offsets, dwarf_injection, etc.) don't see the private parse state.
|
||||
--- Runs in 5 phases.
|
||||
--- Resolve: Resolve the canonical source order from `ctx.shared.corpus.source_order`.
|
||||
--- The canonical corpus is the SOLE source of truth; no `ctx.sources` alias is consulted and no per-source fallback synthesis is performed.
|
||||
--- Pass 1a: `scan_source_pre_pass` over every source, populating LOCAL `code_macros` AND LOCAL `code_macro_bodies` tables.
|
||||
--- The bodies table holds the raw post-`=` text of every `#define R_*_Code` line (cross-source)
|
||||
--- so the chain walker can fall back when the defining `#define` lives in a different source than the chain call site.
|
||||
--- Pass 1b: Resolve every collected macro's chain using the bodies table as fallback.
|
||||
--- This fills in `code_macros` entries whose defining source was scanned AFTER the call site (e.g. lottes_tape.h's `R_TapePtr_Code -> R_T8_Code` chain into mips.h's `R_T8_Code = 24`).
|
||||
--- Pass 2: The full `scan_source(source, source_file, code_macros, code_macro_bodies)` walk per source. The per-source `src.scan` payload includes the source-local registries
|
||||
--- (register_alias_registry, type_name_registry, atom_views, atom_ctxs, atom_phases, binds, atoms, atom_infos, ...).
|
||||
--- Strip: Strip `src.scan._code_macros`, `src.scan._code_macro_bodies`, and the `_source_file` pointer.
|
||||
--- The LOCAL tables `code_macros` and `code_macro_bodies` go out of scope here; they MUST NOT appear on `ctx.shared`, `ctx.shared.corpus`, or any `src.scan` after this point.
|
||||
--- Merge: Iterate `ctx.shared.corpus.source_order` in declared order. For every source's local registry, first-wins lookup identity (entry from the first declaration site becomes the canonical corpus entry);
|
||||
--- identical shapes coalesce by appending the declaration site; conflicting shapes keep the first lookup entry and append ONE typed collision record with shape diff.
|
||||
--- Populate `register_alias_registry`, `type_name_registry`, `binds_by_name`, `atoms_by_name`, `atom_views`, `atom_ctxs`, `atom_phases`.
|
||||
--- `atom_infos` ALWAYS appends every record (preserving source order + duplicates for annotation evidence).
|
||||
---
|
||||
--- @param ctx PassCtx
|
||||
--- @return PassResult
|
||||
function M.run(ctx)
|
||||
-- Initialize the cross-source code-macro + body registries in ctx.shared.
|
||||
-- (Pass 1a writes into both; pass 1b reads from both; pass 2 reads both.)
|
||||
ctx.shared = ctx.shared or {}
|
||||
ctx.shared._code_macros = ctx.shared._code_macros or {}
|
||||
ctx.shared._code_macro_bodies = ctx.shared._code_macro_bodies or {}
|
||||
local code_macros = ctx.shared._code_macros
|
||||
local code_macro_bodies = ctx.shared._code_macro_bodies
|
||||
-- The cross-source _code_macros / _code_macro_bodies tables are LOCAL to this run.
|
||||
-- They are shared across source scans ONLY long enough to resolve cross-source R_*_Code chains, then DISCARDED.
|
||||
-- They MUST NOT appear on ctx.shared, ctx.shared.corpus, or any src.scan after
|
||||
-- this function returns.
|
||||
local code_macros = {}
|
||||
local code_macro_bodies = {}
|
||||
|
||||
-- Resolve the canonical source list. The corpus owns the authoritative source_order; no legacy alias is consulted and no per-source fallback synthesis is performed.
|
||||
-- A context without `ctx.shared.corpus` is rejected with an explicit canonical-corpus message so callers migrate to the canonical context (no production compatibility layer).
|
||||
ctx.shared = ctx.shared or {}
|
||||
local corpus = ctx.shared.corpus
|
||||
if not corpus or type(corpus.source_order) ~= "table" then
|
||||
error("scan_source.run requires ctx.shared.corpus.source_order (the canonical corpus is the source of truth; no per-source fallback is supported)", 0)
|
||||
end
|
||||
local sources = corpus.source_order
|
||||
|
||||
-- Pass 1a: collect `_code_macros` + `_code_macro_bodies` across ALL sources.
|
||||
for _, src in ipairs(ctx.sources) do
|
||||
for _, src in ipairs(sources) do
|
||||
scan_source_pre_pass(src.text, code_macros, code_macro_bodies)
|
||||
end
|
||||
|
||||
-- Pass 1b: resolve every collected macro's chain with cross-source fallback.
|
||||
-- The bodies table was populated for every `#define R_*_Code` line in pass 1a;
|
||||
-- this iteration finishes the chain even when the chain hops span sources
|
||||
-- This iteration finishes the chain even when the chain hops span sources
|
||||
-- (e.g. R_TapePtr_Code -> R_T8_Code -> 24 spans lottes_tape.h into mips.h).
|
||||
-- Same `code_macros` table is shared with pass 2 below.
|
||||
for macro_name, _ in pairs(code_macro_bodies) do
|
||||
@@ -1745,28 +2057,33 @@ function M.run(ctx)
|
||||
end
|
||||
end
|
||||
|
||||
-- Pass 2: run the full scan with the shared `_code_macros` + `_code_macro_bodies`
|
||||
-- so the enum parser can resolve cross-source `R_*_Code` references and bare `R_*` symbols via the `_Code` registry fallback.
|
||||
for _, src in ipairs(ctx.sources) do
|
||||
-- Pass 2: run the full scan with the shared `_code_macros` + `_code_macro_bodies` so the enum parser can resolve cross-source
|
||||
-- `R_*_Code` references and bare `R_*` symbols via the `_Code` registry fallback.
|
||||
-- The private `_code_macros` / `_code_macro_bodies` / `_source_file` strip is midway through `source_order`.
|
||||
-- Must not leave earlier sources leaking the private parse state, because a separate post-loop would not run when an exception propagates out of pass 2.
|
||||
for _, src in ipairs(sources) do
|
||||
src.scan = scan_source(src.text, src.path, code_macros, code_macro_bodies)
|
||||
-- Pre-tokenize each atom body once (plex: single source of truth).
|
||||
-- Downstream passes (offsets, word-counts, components, static-analysis) read from
|
||||
-- `atom.body_tokens` instead of calling `split_top_level_commas` / `tokenize_body` independently.
|
||||
-- The tokens are memoized in duffle.lua's cache, so re-access is O(1).
|
||||
for _, atom in ipairs(src.scan.atoms) do atom.body_tokens = duffle.tokenize_body(atom.body) end
|
||||
for _, atom in ipairs(src.scan.raw_atoms or {}) do atom.body_tokens = duffle.tokenize_body(atom.body) end
|
||||
end
|
||||
|
||||
-- Strip `_code_macros` + `_code_macro_bodies` (and the convenience `_source_file` pointer)
|
||||
-- before returning so downstream passes don't see private parse state.
|
||||
for _, src in ipairs(ctx.sources) do
|
||||
-- Strip the three private fields immediately so a later fatal source does not leave this source leaking parse state.
|
||||
-- The shared `code_macros` / `code_macro_bodies` locals remain in the outer scope and keep their contents for subsequent sources.
|
||||
if src.scan then
|
||||
src.scan._code_macros = nil
|
||||
src.scan._code_macro_bodies = nil
|
||||
src.scan._source_file = nil
|
||||
end
|
||||
-- Pre-tokenize each atom body once (plex: single source of truth).
|
||||
-- Downstream passes (offsets, word-counts, components, static-analysis) read from `atom.body_tokens` instead of calling `split_top_level_commas` / `tokenize_body` independently.
|
||||
-- The tokens are memoized in duffle.lua's cache, so re-access is O(1).
|
||||
for _, atom in ipairs(src.scan.atoms) do atom.body_tokens = duffle.tokenize_body(atom.body) end
|
||||
for _, atom in ipairs(src.scan.raw_atoms or {}) do atom.body_tokens = duffle.tokenize_body(atom.body) end
|
||||
end
|
||||
|
||||
-- Merge per-source scans into the canonical corpus registries.
|
||||
-- First-wins lookup identity + collision discipline (see merge_corpus_registries).
|
||||
-- The corpus is always present; no conditional / fallback path.
|
||||
merge_corpus_registries(corpus)
|
||||
|
||||
-- code_macros and code_macro_bodies go out of scope here; their references are not captured on corpus, ctx.shared, or any src.scan.
|
||||
-- The Lua GC reclaims them on M.run return.
|
||||
return { outputs = {}, errors = {}, warnings = {} }
|
||||
end
|
||||
|
||||
|
||||
+1169
-366
File diff suppressed because it is too large
Load Diff
@@ -1,11 +1,17 @@
|
||||
--- word_count_eval.lua — Word-counting logic for the tape-atom metaprogram pipeline.
|
||||
---
|
||||
--- Three responsibilities:
|
||||
--- 1. **Public utilities** (used by `passes/components.lua`, `passes/offsets.lua`, `passes/annotation.lua`):
|
||||
--- - `M.count_token_words(token, wc)` — words emitted by one token
|
||||
--- - `M.scan_dir(dir, suffix)` — glob walk for *.macs.h
|
||||
--- 2. **Pass entry** `M.run(ctx)` — loads metadata.h + *.macs.h into `ctx.shared.word_counts` for downstream passes.
|
||||
--- 3. **Internal helpers** for the body scanner.
|
||||
--- Two responsibilities:
|
||||
--- 1. **Public utility** `M.count_token_words(token, wc)`: Used by `passes/offsets.lua`, `passes/annotation.lua`, and other passes.
|
||||
--- 2. **Pass entry** `M.run(ctx)`: Loads the authored `word_count.metadata.h` into `ctx.shared.corpus.word_counts` for downstream passes.
|
||||
--- The generated `.macs.h` files are OUTPUT artifacts and are NOT inputs to this pass;
|
||||
--- Current component counts are owned by `passes/components.lua` (which populates `corpus.word_counts` and `corpus.component_body_index`
|
||||
--- AFTER computing each current count from the just-built body + `corpus.word_counts`).
|
||||
---
|
||||
--- **Canonical contract**:
|
||||
--- * `ctx.shared.corpus.word_counts` is the canonical count table.
|
||||
--- * `corpus.word_counts` is the sole count table. Consumers read `corpus.word_counts` directly.
|
||||
--- * `ctx.shared.components` and `ctx.shared.component_body_index` are NOT created by this pass (canonical projections only).
|
||||
--- * No `.macs.h` recursive discovery (no `scan_dir`, no scan cache, no `_invalidate_scan_cache`).
|
||||
---
|
||||
--- **Conventions**: tabs (1/level), EmmyLua annotations, no regex,
|
||||
--- Lua 5.3 compatible.
|
||||
@@ -14,23 +20,11 @@
|
||||
-- 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.
|
||||
-- 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.
|
||||
-- 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
|
||||
-- ════════════════════════════════════════════════════════════════════════════
|
||||
|
||||
-- Required native extension: lfs (LuaFileSystem). Built by `update_deps.ps1` to
|
||||
-- `toolchain/lfs/lfs.dll` and wired into package.cpath by `scripts/duffle_paths.lua`.
|
||||
-- If lfs is missing, `require` throws — fail loud per the build-tool convention.
|
||||
local lfs = require("lfs")
|
||||
local duffle = dofile(_bootstrap_dir .. "../duffle_paths.lua")
|
||||
|
||||
-- ════════════════════════════════════════════════════════════════════════════
|
||||
-- Type declarations
|
||||
@@ -49,7 +43,8 @@ local lfs = require("lfs")
|
||||
--- @field sources SourceFile[] -- all source files in the build
|
||||
--- @field metadata_path string -- path to word_count.metadata.h
|
||||
--- @field shared table -- cross-pass shared state
|
||||
--- @field shared.word_counts WordCounts -- populated by this pass
|
||||
--- @field shared.corpus table -- canonical corpus (required)
|
||||
--- @field shared.corpus.word_counts WordCounts -- canonical count table (populated by this pass)
|
||||
--- @field out_root string -- output root (e.g. "build/gen")
|
||||
--- @field project_root string -- project root (e.g. "code/")
|
||||
--- @field upstream table<string, table> -- per-pass upstream outputs
|
||||
@@ -76,8 +71,8 @@ local M = {}
|
||||
--- For most tokens (regular MIPS instructions) this returns 1.
|
||||
--- For `mac_X(...)` calls, this returns the resolved word count from `wc` (recursively if needed). For `nop2` etc., returns wc[name].
|
||||
--- For unknown macros, returns 1 and (optionally) warns.
|
||||
--- @param token string -- a single token from split_top_level_commas
|
||||
--- @param wc WordCounts -- the shared word-count table
|
||||
--- @param token string -- a single token from split_top_level_commas
|
||||
--- @param wc WordCounts -- the shared word-count table
|
||||
--- @return integer
|
||||
function M.count_token_words(token, wc)
|
||||
local s = duffle.trim(token)
|
||||
@@ -92,83 +87,42 @@ function M.count_token_words(token, wc)
|
||||
return 1
|
||||
end
|
||||
|
||||
-- ┌────────────────────────────────────────────────────────────────────┐
|
||||
-- │ Shared utility: scan_dir │
|
||||
-- └────────────────────────────────────────────────────────────────────┘
|
||||
|
||||
-- Cache the scan_dir result per (dir, suffix) in package.loaded.
|
||||
-- The cache persists for the lifetime of the Lua process (cleared when ps1_meta.lua exits).
|
||||
-- If a build removes/creates .macs.h files mid-process, the caller can invalidate by calling `M._invalidate_scan_cache()`.
|
||||
local SCAN_CACHE_KEY = "__word_count_eval_scan_cache__"
|
||||
|
||||
--- Scan `code/` for files matching `suffix` (e.g. `*.macs.h`).
|
||||
--- Native directory enumeration via lfs (~2ms). Zero subprocess spawns.
|
||||
--- @param dir string -- project root directory
|
||||
--- @param suffix string -- file pattern, e.g. "*.macs.h"
|
||||
--- @return string[]
|
||||
function M.scan_dir(dir, suffix)
|
||||
local key = dir .. "\0" .. suffix
|
||||
|
||||
local cache = package.loaded[SCAN_CACHE_KEY]
|
||||
if cache and cache[key] then return cache[key] end
|
||||
|
||||
local results = {}
|
||||
local code_dir = dir .. "/code"
|
||||
if lfs.attributes(code_dir, "mode") == "directory" then
|
||||
for mod_name in lfs.dir(code_dir) do
|
||||
if mod_name ~= "." and mod_name ~= ".." then
|
||||
local gen_path = code_dir .. "/" .. mod_name .. "/gen"
|
||||
if lfs.attributes(gen_path, "mode") == "directory" then
|
||||
for fname in lfs.dir(gen_path) do
|
||||
if fname:match("%.macs%.h$") then
|
||||
results[#results + 1] = gen_path .. "/" .. fname
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
-- Cache the result (including empty results).
|
||||
cache = cache or {}
|
||||
cache[key] = results
|
||||
package.loaded[SCAN_CACHE_KEY] = cache
|
||||
|
||||
return results
|
||||
end
|
||||
|
||||
--- Invalidate the scan cache (call after creating new .macs.h files in the same Lua process — usually not needed).
|
||||
function M._invalidate_scan_cache() package.loaded[SCAN_CACHE_KEY] = nil end
|
||||
|
||||
-- ┌────────────────────────────────────────────────────────────────────┐
|
||||
-- │ Pass entry: M.run(ctx) — "word-counts" pass │
|
||||
-- └────────────────────────────────────────────────────────────────────┘
|
||||
|
||||
--- Load metadata.h + scan for existing *.macs.h files into ctx.shared.word_counts.
|
||||
--- Loading the .macs.h files is idempotent: entries from later (current-build) .macs.h files override metadata.h entries of the same name.
|
||||
--- Load the authored `word_count.metadata.h` into `ctx.shared.corpus.word_counts`.
|
||||
--- Generated `.macs.h` files are OUTPUT artifacts and are NOT scanned as inputs.
|
||||
--- Current component counts are computed and inserted by `passes/components.lua`
|
||||
--- after the components pass iterates `corpus.source_order` and writes each source's `<dir_basename>.macs.h` file.
|
||||
---
|
||||
--- Contract:
|
||||
--- * `ctx.shared.corpus` MUST exist (canonical corpus ownership).
|
||||
--- * `ctx.metadata_path` MUST be a readable file path to the authored `word_count.metadata.h`.
|
||||
--- * The pass assigns exactly one table to `corpus.word_counts`.
|
||||
--- Consumers read the corpus-owned table directly.
|
||||
--- Consumers must read `corpus.word_counts` directly.
|
||||
--- @param ctx PassCtx
|
||||
--- @return PassResult
|
||||
function M.run(ctx)
|
||||
local wc = {}
|
||||
|
||||
-- 1. Load metadata.h (the encoding-macro source of truth).
|
||||
local meta_counts = duffle.load_word_counts(ctx.metadata_path)
|
||||
for name, count in pairs(meta_counts) do wc[name] = count end
|
||||
|
||||
-- 2. Scan project_root recursively for *.macs.h files (component-macro source).
|
||||
local macs_files = M.scan_dir(ctx.project_root, "*.macs.h")
|
||||
for _, macs_path in ipairs(macs_files) do
|
||||
local ok, mc = pcall(duffle.load_word_counts, macs_path)
|
||||
if not ok then
|
||||
io.stderr:write(string.format("[word_count_eval] parse error in '%s': %s\n", macs_path, tostring(mc)))
|
||||
elseif type(mc) ~= "table" then
|
||||
io.stderr:write(string.format("[word_count_eval] '%s' did not return a table (got %s)\n", macs_path, type(mc)))
|
||||
else
|
||||
for name, count in pairs(mc) do wc[name] = count end
|
||||
end
|
||||
-- 1. Canonical-corpus ownership gate.
|
||||
local corpus = ctx.shared and ctx.shared.corpus
|
||||
if type(corpus) ~= "table" then
|
||||
error("word_count_eval.run requires ctx.shared.corpus (canonical corpus). The fixture must install the corpus before running this pass.", 0)
|
||||
end
|
||||
|
||||
ctx.shared.word_counts = wc
|
||||
-- 2. metadata_path gate.
|
||||
if type(ctx.metadata_path) ~= "string" or ctx.metadata_path == "" then
|
||||
error("word_count_eval.run requires ctx.metadata_path (path to the authored word_count.metadata.h).", 0)
|
||||
end
|
||||
|
||||
-- 3. Load authored metadata. Generated .macs.h files are NOT scanned
|
||||
-- (the canonical pass computes their counts from the just-built bodies after disk emission; see passes/components.lua).
|
||||
local wc = duffle.load_word_counts(ctx.metadata_path)
|
||||
|
||||
-- 4. Assign the canonical count table. ONE assignment, no copy. The assignment creates no secondary alias.
|
||||
corpus.word_counts = wc
|
||||
|
||||
return { outputs = {}, errors = {}, warnings = {} }
|
||||
end
|
||||
|
||||
|
||||
+254
-151
@@ -4,8 +4,8 @@
|
||||
---
|
||||
--- **Architecture**:
|
||||
--- - **PASSES table** — declarative dep graph (data, not code).
|
||||
--- - **FLAG_HANDLERS table** — per-flag CLI dispatchers (handler-map pattern; replaces an 8-way if/elseif chain).
|
||||
--- - **parse_args** → **build_ctx** (just opens + reads source files; no inline scanning) → **topo_sort** → **dispatch_passes**.
|
||||
--- - **FLAG_HANDLERS table** — maps CLI flags to handlers.
|
||||
--- - **parse_args** → **build_ctx** (resolves unity/direct includes or exact sources; no semantic scanning) → **topo_sort** → **dispatch_passes**.
|
||||
--- - The first pass in the dep graph is `scan-source` (see `passes/scan_source.lua`).
|
||||
--- It calls `duffle.scan_source` once per source to produce the fat `SourceScan` payload, which is attached to each `src.scan`.
|
||||
--- Every other pass that reads source structure depends on `scan-source` and consumes `src.scan` as a read-only.
|
||||
@@ -18,13 +18,9 @@
|
||||
-- ════════════════════════════════════════════════════════════════════════════
|
||||
|
||||
-- Bootstrap: load `duffle_paths.lua` via this script's own path.
|
||||
-- Use `arg[0]` when this file is the entry script (`arg[0]` ends in
|
||||
-- "ps1_meta.lua"); 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, not ours).
|
||||
--
|
||||
-- That single statement: (a) sets `package.path` + `package.cpath`
|
||||
-- (via cached `git rev-parse`), (b) at the bottom returns `require("duffle")`.
|
||||
-- Use `arg[0]` when this file is the entry script (`arg[0]` ends in "ps1_meta.lua");
|
||||
-- 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, not ours).
|
||||
-- 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 _is_entry_script = arg and arg[0] and arg[0]:match("ps1_meta%.lua$") ~= nil
|
||||
local _bootstrap_src
|
||||
@@ -81,12 +77,11 @@ local PASS_FLAG_DISPATCH_KEY = "__pass__"
|
||||
--- @field basename string -- filename without extension
|
||||
|
||||
--- @class PassCtx
|
||||
--- @field sources SourceFile[] -- all source files in the build
|
||||
--- @field metadata_path string -- path to word_count.metadata.h
|
||||
--- @field shared table -- cross-pass shared state
|
||||
--- @field shared.word_counts table<string, integer> -- populated by word-counts pass
|
||||
--- @field shared.corpus table -- canonical authored-source/project projection
|
||||
--- @field out_root string -- output root (e.g. "build/gen")
|
||||
--- @field project_root string -- project root (e.g. "code/")
|
||||
--- @field project_root string -- PS1 repository root
|
||||
--- @field upstream table<string, table> -- per-pass output accumulator
|
||||
--- @field flags table -- CLI flags + per-pass stash
|
||||
--- @field dry_run boolean -- if true, compute but don't write
|
||||
@@ -106,13 +101,14 @@ local PASS_FLAG_DISPATCH_KEY = "__pass__"
|
||||
--- @field warnings Finding[] -- informational
|
||||
|
||||
--- @class ParsedArgs
|
||||
--- @field requested_set string[] -- pass names to run (explicit --all expanded)
|
||||
--- @field sources string[] -- --source values
|
||||
--- @field metadata string -- --metadata value
|
||||
--- @field out_root string -- --out-root value (default "build/gen")
|
||||
--- @field project_root string -- --project-root value (default dirname(metadata))
|
||||
--- @field dry_run boolean -- if true, compute but don't write
|
||||
--- @field verbose boolean -- if true, log diagnostic info
|
||||
--- @field requested_set string[] -- pass names to run (explicit --all expanded)
|
||||
--- @field sources string[] -- exact --source values, retained in CLI order
|
||||
--- @field unity_root string|nil -- --unity-root value; mutually exclusive with sources
|
||||
--- @field metadata string -- --metadata value
|
||||
--- @field out_root string -- --out-root value (default "build/gen")
|
||||
--- @field project_root string -- PS1 repository root (derived from metadata by default)
|
||||
--- @field dry_run boolean -- if true, compute but don't write
|
||||
--- @field verbose boolean -- if true, log diagnostic info
|
||||
|
||||
-- ════════════════════════════════════════════════════════════════════════════
|
||||
-- PASSES Table
|
||||
@@ -145,6 +141,13 @@ local PASSES = {
|
||||
desc = "Emit mac_X macros from MipsAtomComp_ declarations",
|
||||
out = { { kind = "header", path_template = "<source_dir>/gen/<basename>.macs.h" } },
|
||||
},
|
||||
["emission-model"] = {
|
||||
module = "passes.emission_model",
|
||||
kind = "validation",
|
||||
deps = {"components"},
|
||||
desc = "Build canonical per-atom words, markers, and invocation ancestry",
|
||||
out = {},
|
||||
},
|
||||
annotation = {
|
||||
module = "passes.annotation",
|
||||
kind = "validation",
|
||||
@@ -158,7 +161,7 @@ local PASSES = {
|
||||
offsets = {
|
||||
module = "passes.offsets",
|
||||
kind = "header-output",
|
||||
deps = {"scan-source", "word-counts", "components"},
|
||||
deps = {"scan-source", "word-counts", "components", "emission-model"},
|
||||
groups = { "pre-link" },
|
||||
desc = "Compute branch offsets for atom_label / atom_offset",
|
||||
out = { { kind = "header", path_template = "<source_dir>/gen/<basename>.offsets.h" } },
|
||||
@@ -169,14 +172,14 @@ local PASSES = {
|
||||
-- the orchestrator does NOT exit non-zero on these findings (see PASS_KIND_STOP_ON_ERROR).
|
||||
-- Report severity is independent from process exit policy.
|
||||
kind = "diagnostic",
|
||||
deps = {"scan-source", "word-counts", "components"},
|
||||
deps = {"scan-source", "word-counts", "components", "emission-model"},
|
||||
desc = "Static analysis: GTE pipeline-fill, mac_yield uniformity, ABI handoff, GPU port-store shape, per-atom cycle budget, type consistency",
|
||||
out = { { kind = "report", path_template = "<out_root>/<basename>.static_analysis.txt" } },
|
||||
},
|
||||
["atoms-source-map"] = {
|
||||
module = "passes.atoms_source_map",
|
||||
kind = "header-output",
|
||||
deps = {"word-counts", "components"},
|
||||
deps = {"word-counts", "components", "emission-model"},
|
||||
desc = "Emit gen/<basename>.atoms.sourcemap.txt (per-.word C source line map for gdb debugging) AND gen/<basename>.atoms.provenance.txt (per-.word provenance; each word tagged with its call-site file:line and, when emitted by a mac_X(...) component invocation, the component's definition file:line). Consumed by passes/dwarf_injection.lua to synthesize DW_TAG_inlined_subroutine instances for source-level Step Into on component invocations.",
|
||||
out = {
|
||||
{ kind = "report", path_template = "<out_root>/<basename>.atoms.sourcemap.txt" },
|
||||
@@ -241,10 +244,8 @@ end
|
||||
local function request_roots_for_group(args, group_name)
|
||||
local roots = roots_for_group(group_name)
|
||||
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))
|
||||
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
|
||||
for _, name in ipairs(roots) do
|
||||
args.requested_set[#args.requested_set + 1] = name
|
||||
@@ -255,8 +256,7 @@ end
|
||||
--
|
||||
-- Report severity is independent from process exit policy. A "diagnostic" pass still writes every `error`/`warning` finding into its report file,
|
||||
-- but `report_validation_errors` returns early for non-stopping kinds, so nothing is printed to stderr and the orchestrator does not exit non-zero.
|
||||
--
|
||||
-- Closed-set discipline: adding a new pass kind requires listing it here explicitly; an unknown kind must not silently fall back to "true".
|
||||
-- Adding a new pass kind requires listing it here explicitly; an unknown kind must not silently fall back to "true".
|
||||
local PASS_KIND_STOP_ON_ERROR = {
|
||||
["shared"] = false,
|
||||
["header-output"] = true,
|
||||
@@ -268,9 +268,8 @@ local PASS_KIND_STOP_ON_ERROR = {
|
||||
-- Closed set of CLI flags -> pass names.
|
||||
-- Per-pass flags (e.g. --word-counts) live here; phase flags (--pre-link, --post-link, --all)
|
||||
-- live in 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.
|
||||
-- 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.
|
||||
local PASS_FLAG_TO_NAME = {
|
||||
["--word-counts"] = "word-counts",
|
||||
["--components"] = "components",
|
||||
@@ -297,7 +296,7 @@ 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).
|
||||
-- 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 = {}
|
||||
|
||||
-- ════════════════════════════════════════════════════════════════════════════
|
||||
@@ -336,10 +335,14 @@ PASS_FLAGS:
|
||||
--report Render per-project summary
|
||||
|
||||
COMMON_FLAGS:
|
||||
--source FILE Source file to process (repeatable)
|
||||
--unity-root FILE Unity source root: load root + direct quoted authored
|
||||
includes only. Mutually exclusive with --source.
|
||||
--source FILE Exact source file to process (repeatable, never expands
|
||||
includes). Mutually exclusive with --unity-root.
|
||||
--metadata PATH Path to metadata.h (required)
|
||||
--out-root DIR Output root for reports (default: build/gen)
|
||||
--project-root DIR Project root for .macs.h scan (default: dirname(metadata))
|
||||
--project-root DIR PS1 repository root (default: derived from
|
||||
<repo>/code/duffle/word_count.metadata.h)
|
||||
--gdb-runtime Also emit <out_root>/gdb_tape_atoms_runtime.gdb (post-link, requires --elf)
|
||||
--elf PATH Path to linked .elf (for --gdb-runtime / --dwarf-injection)
|
||||
--dry-run Print dep order (alphabetical); exit 0 without running
|
||||
@@ -351,17 +354,36 @@ EXIT CODES:
|
||||
1 Validation errors found
|
||||
2 Metaprogram internal error
|
||||
|
||||
EXAMPLE:
|
||||
ps1_meta.lua --pre-link --metadata metadata.h --source code/foo.c --source code/bar.c
|
||||
ps1_meta.lua --post-link --metadata metadata.h --source code/foo.c --source code/bar.c --elf build/hello_gte.elf
|
||||
EXAMPLES:
|
||||
ps1_meta.lua --pre-link --metadata code/duffle/word_count.metadata.h --unity-root code/gte_hello/hello_gte.c
|
||||
ps1_meta.lua --post-link --metadata code/duffle/word_count.metadata.h --unity-root code/gte_hello/hello_gte.c --elf build/hello_gte.elf
|
||||
ps1_meta.lua --all --metadata metadata.h --source code/foo.c --source code/bar.c
|
||||
]])
|
||||
end
|
||||
|
||||
local FLAG_VALUE_NAMES = {
|
||||
["--source"] = "FILE",
|
||||
["--unity-root"] = "FILE",
|
||||
["--metadata"] = "PATH",
|
||||
["--out-root"] = "DIR",
|
||||
["--project-root"] = "DIR",
|
||||
["--elf"] = "PATH",
|
||||
}
|
||||
|
||||
local function require_flag_value(argv, arg_idx, flag)
|
||||
local value = argv[arg_idx + 1]
|
||||
local next_known = type(value) == "string"
|
||||
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")
|
||||
os.exit(EXIT_INTERNAL_ERROR)
|
||||
end
|
||||
return value, arg_idx + 1
|
||||
end
|
||||
|
||||
-- Per-flag handlers. Each takes (args, argv, arg_idx) and returns the new arg_idx (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)
|
||||
@@ -369,17 +391,46 @@ FLAG_HANDLERS["--help"] = function(args)
|
||||
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, arg_idx) args.sources[#args.sources + 1] = argv[arg_idx + 1]; return arg_idx + 1 end
|
||||
FLAG_HANDLERS["--metadata"] = function(args, argv, arg_idx) args.metadata = argv[arg_idx + 1]; return arg_idx + 1 end
|
||||
FLAG_HANDLERS["--out-root"] = function(args, argv, arg_idx) args.out_root = argv[arg_idx + 1]; return arg_idx + 1 end
|
||||
FLAG_HANDLERS["--project-root"] = function(args, argv, arg_idx) args.project_root = argv[arg_idx + 1]; return arg_idx + 1 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, arg_idx)
|
||||
local value, value_idx = require_flag_value(argv, arg_idx, "--source")
|
||||
args.sources[#args.sources + 1] = value
|
||||
return value_idx
|
||||
end
|
||||
FLAG_HANDLERS["--unity-root"] = function(args, argv, arg_idx)
|
||||
local value, value_idx = require_flag_value(argv, arg_idx, "--unity-root")
|
||||
args.unity_root = value
|
||||
return value_idx
|
||||
end
|
||||
FLAG_HANDLERS["--metadata"] = function(args, argv, arg_idx)
|
||||
local value, value_idx = require_flag_value(argv, arg_idx, "--metadata")
|
||||
args.metadata = value
|
||||
return value_idx
|
||||
end
|
||||
FLAG_HANDLERS["--out-root"] = function(args, argv, arg_idx)
|
||||
local value, value_idx = require_flag_value(argv, arg_idx, "--out-root")
|
||||
args.out_root = value
|
||||
return value_idx
|
||||
end
|
||||
FLAG_HANDLERS["--project-root"] = function(args, argv, arg_idx)
|
||||
local value, value_idx = require_flag_value(argv, arg_idx, "--project-root")
|
||||
args.project_root = value
|
||||
return value_idx
|
||||
end
|
||||
|
||||
-- Per-pass stash flags. Read by `passes/atoms_source_map.lua` to opt into the post-link gdb-runtime emission.
|
||||
-- Same shape as the existing per-flag handlers. mutates `args.flags` (which propagates into `ctx.flags`).
|
||||
FLAG_HANDLERS["--gdb-runtime"] = function(args) args.flags = args.flags or {}; args.flags.gdb_runtime = true end
|
||||
FLAG_HANDLERS["--elf"] = function(args, argv, arg_idx) args.flags = args.flags or {}; args.flags.elf_path = argv[arg_idx + 1]; return arg_idx + 1 end
|
||||
FLAG_HANDLERS["--gdb-runtime"] = function(args)
|
||||
args.flags = args.flags or {}
|
||||
args.flags.gdb_runtime = true
|
||||
end
|
||||
FLAG_HANDLERS["--elf"] = function(args, argv, arg_idx)
|
||||
local value, value_idx = require_flag_value(argv, arg_idx, "--elf")
|
||||
args.flags = args.flags or {}
|
||||
args.flags.elf_path = value
|
||||
return value_idx
|
||||
end
|
||||
-- Enable DWARF injection (default OFF). Opts in to the post-link pass and sets the flag in one shot.
|
||||
-- The explicit handler below owns both selection and opt-in state, so --dwarf-injection is intentionally absent from PASS_FLAG_TO_NAME.
|
||||
FLAG_HANDLERS["--dwarf-injection"] = function(args)
|
||||
@@ -402,7 +453,7 @@ FLAG_HANDLERS["--post-link"] = function(args)
|
||||
request_roots_for_group(args, "post-link")
|
||||
end
|
||||
|
||||
-- (atom locals) is now consolidated into --dwarf-injection; no separate flag.
|
||||
-- `--dwarf-injection` also emits atom-local debug data.
|
||||
|
||||
-- Pass-flag handler. Reads the closed-set table, expands --all, appends to requested_set.
|
||||
FLAG_HANDLERS[PASS_FLAG_DISPATCH_KEY] = function(args, a)
|
||||
@@ -421,6 +472,7 @@ local function parse_args(argv)
|
||||
local args = {
|
||||
requested_set = {},
|
||||
sources = {},
|
||||
unity_root = nil,
|
||||
metadata = nil,
|
||||
out_root = DEFAULT_OUT_ROOT,
|
||||
project_root = nil,
|
||||
@@ -448,21 +500,28 @@ local function parse_args(argv)
|
||||
-- The first invocation of a build is always pre-link, so this avoids silently also invoking post-link work in builds without an ELF artifact.
|
||||
if #args.requested_set == 0 then request_roots_for_group(args, "pre-link") end
|
||||
|
||||
-- Defaults: project_root = dirname(metadata).
|
||||
if args.metadata and not args.project_root then
|
||||
local d = duffle.dirname(args.metadata)
|
||||
if #d > 0 and (d:sub(-1) == "/" or d:sub(-1) == "\\") then
|
||||
d = d:sub(1, -2)
|
||||
end
|
||||
args.project_root = duffle.dirname(d)
|
||||
end
|
||||
|
||||
if not args.metadata then
|
||||
io.stderr:write("ps1_meta: --metadata PATH is required\n")
|
||||
os.exit(EXIT_INTERNAL_ERROR)
|
||||
end
|
||||
if #args.sources == 0 then
|
||||
io.stderr:write("ps1_meta: at least one --source FILE is required\n")
|
||||
|
||||
-- `<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
|
||||
local metadata_dir = duffle.dirname(duffle.normalize_path(args.metadata))
|
||||
local code_root = duffle.dirname(metadata_dir)
|
||||
args.project_root = duffle.dirname(code_root)
|
||||
else
|
||||
args.project_root = duffle.normalize_path(args.project_root)
|
||||
end
|
||||
|
||||
local has_unity = type(args.unity_root) == "string" and args.unity_root ~= ""
|
||||
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)
|
||||
end
|
||||
if not has_unity and #args.sources == 0 then
|
||||
io.stderr:write("ps1_meta: either --unity-root FILE or at least one --source FILE is required\n")
|
||||
os.exit(EXIT_INTERNAL_ERROR)
|
||||
end
|
||||
|
||||
@@ -485,56 +544,128 @@ end
|
||||
-- Build ctx from parsed args
|
||||
-- ════════════════════════════════════════════════════════════════════════════
|
||||
|
||||
--- Build the PassCtx from parsed args. Reads each source file once at startup;
|
||||
--- passes consume `src.text`, not the path (path is preserved for error reporting).
|
||||
--- Build the PassCtx from parsed args. Exact mode opens only the repeated `--source` inputs;
|
||||
--- unity mode delegates direct-include resolution to duffle.resolve_source_corpus`.
|
||||
--- Scanning remains pass-owned (`src.scan`).
|
||||
--- @param args ParsedArgs
|
||||
--- @return PassCtx
|
||||
local function build_ctx(args)
|
||||
local sources = {}
|
||||
for _, path in ipairs(args.sources) do
|
||||
-- lfs handles path metadata and directories, not file-content streams.
|
||||
-- Keep this io.open local so this entry point preserves its tailored diagnostic and exit path below.
|
||||
local f = io.open(path, "r")
|
||||
if not f then
|
||||
io.stderr:write("ps1_meta: cannot open --source " .. path .. "\n")
|
||||
local normalized_project_root = duffle.normalize_path(args.project_root)
|
||||
local project_root = normalized_project_root
|
||||
local project_root_is_absolute = normalized_project_root:match("^%a:/")
|
||||
or normalized_project_root:sub(1, 2) == "//"
|
||||
or normalized_project_root:sub(1, 1) == "/"
|
||||
if not project_root_is_absolute then
|
||||
-- canonical_path_key validates ordinary relative paths and rejects
|
||||
-- drive-relative paths before the legacy display-path helper is used.
|
||||
duffle.canonical_path_key(normalized_project_root)
|
||||
project_root = duffle.normalize_path(duffle.to_absolute_path(normalized_project_root))
|
||||
else
|
||||
-- Do not route POSIX/UNC/drive-absolute paths through to_absolute_path.
|
||||
duffle.canonical_path_key(project_root)
|
||||
end
|
||||
local resolution
|
||||
if args.unity_root then
|
||||
local ok_resolve, resolved = pcall(duffle.resolve_source_corpus, {
|
||||
unity_root = args.unity_root,
|
||||
project_root = project_root,
|
||||
})
|
||||
if not ok_resolve then
|
||||
io.stderr:write("ps1_meta: cannot resolve --unity-root "
|
||||
.. tostring(args.unity_root) .. ": " .. tostring(resolved) .. "\n")
|
||||
os.exit(EXIT_INTERNAL_ERROR)
|
||||
end
|
||||
local text = f:read("*a")
|
||||
f:close()
|
||||
resolution = resolved
|
||||
else
|
||||
local source_order = {}
|
||||
local sources_by_path = {}
|
||||
local resolver = {
|
||||
resolved = {},
|
||||
skipped = {},
|
||||
shadowed = {},
|
||||
}
|
||||
for _, input_path in ipairs(args.sources) do
|
||||
local path = duffle.normalize_path(input_path)
|
||||
local key_ok, key_or_error = pcall(duffle.canonical_path_key, path)
|
||||
if not key_ok then
|
||||
error("ps1_meta: invalid --source " .. input_path .. ": "
|
||||
.. tostring(key_or_error), 0)
|
||||
end
|
||||
local file = io.open(path, "r")
|
||||
if not file then
|
||||
io.stderr:write("ps1_meta: cannot open --source " .. input_path .. "\n")
|
||||
os.exit(EXIT_INTERNAL_ERROR)
|
||||
end
|
||||
local text = file:read("*a")
|
||||
file:close()
|
||||
|
||||
local dir = duffle.dirname(path)
|
||||
local basename = duffle.basename_no_ext(path)
|
||||
if #dir > 0 and (dir:sub(-1) == "/" or dir:sub(-1) == "\\") then
|
||||
dir = dir:sub(1, -2)
|
||||
local source = {
|
||||
path = path,
|
||||
text = text,
|
||||
dir = duffle.dirname(path),
|
||||
basename = duffle.basename_no_ext(path),
|
||||
}
|
||||
source_order[#source_order + 1] = source
|
||||
local key = key_or_error
|
||||
if not sources_by_path[key] then sources_by_path[key] = source end
|
||||
resolver.resolved[#resolver.resolved + 1] = {
|
||||
include_path = path,
|
||||
include_text = nil,
|
||||
root_source = nil,
|
||||
root_line = nil,
|
||||
candidate_a = path,
|
||||
candidate_b = nil,
|
||||
selected_path = path,
|
||||
disposition = "exact",
|
||||
}
|
||||
end
|
||||
|
||||
-- src.scan is populated by the "scan-source" pass (the first pass in the dep graph).
|
||||
-- build_ctx just opens + reads the files; the scan itself happens in the pass module, not inline in the orchestrator.
|
||||
sources[#sources + 1] = {
|
||||
path = path,
|
||||
text = text,
|
||||
dir = dir,
|
||||
basename = basename,
|
||||
resolution = {
|
||||
unity_root = nil,
|
||||
project_root = project_root,
|
||||
code_root = duffle.normalize_path(project_root .. "/code"),
|
||||
source_order = source_order,
|
||||
sources_by_path = sources_by_path,
|
||||
sources_by_dir = duffle.group_sources_by_dir(source_order),
|
||||
resolver = resolver,
|
||||
}
|
||||
end
|
||||
|
||||
-- Pre-compute the per-directory grouping once (Fleury: expose structure).
|
||||
-- Three passes (annotation, report, static-analysis) call group_sources_by_dir with the same ctx.sources;
|
||||
-- computing it here and stashing on ctx.by_dir eliminates 2 redundant calls.
|
||||
local by_dir = duffle.group_sources_by_dir(sources)
|
||||
|
||||
return {
|
||||
sources = sources,
|
||||
by_dir = by_dir,
|
||||
local corpus = {
|
||||
unity_root = resolution.unity_root,
|
||||
project_root = resolution.project_root,
|
||||
code_root = resolution.code_root,
|
||||
source_order = resolution.source_order,
|
||||
sources_by_path = resolution.sources_by_path,
|
||||
sources_by_dir = resolution.sources_by_dir,
|
||||
atoms_by_name = {},
|
||||
binds_by_name = {},
|
||||
atom_infos = {},
|
||||
register_alias_registry = {},
|
||||
type_name_registry = {},
|
||||
atom_views = {},
|
||||
atom_ctxs = {},
|
||||
atom_phases = {},
|
||||
word_counts = {},
|
||||
components = {},
|
||||
component_body_index = {},
|
||||
collisions = {},
|
||||
resolver = resolution.resolver,
|
||||
}
|
||||
local ctx = {
|
||||
metadata_path = args.metadata,
|
||||
shared = {},
|
||||
shared = { corpus = corpus },
|
||||
upstream = {},
|
||||
out_root = args.out_root,
|
||||
project_root = args.project_root,
|
||||
project_root = corpus.project_root,
|
||||
flags = args.flags or {},
|
||||
dry_run = args.dry_run,
|
||||
verbose = args.verbose,
|
||||
}
|
||||
|
||||
-- Source records and directory buckets are owned by the corpus.
|
||||
-- Consumers read `corpus.source_order` and `corpus.sources_by_dir` directly.
|
||||
-- The corpus is the sole source of truth for source records and module grouping; `ctx` only holds per-pass execution state.
|
||||
return ctx
|
||||
end
|
||||
|
||||
-- ════════════════════════════════════════════════════════════════════════════
|
||||
@@ -543,14 +674,14 @@ end
|
||||
|
||||
--- Topologically sort the requested pass set, augmented with all transitive deps.
|
||||
--- Detects cycles and errors out with details.
|
||||
--- @param passes table<string, PassDescriptor>
|
||||
--- @param passes table<string, PassDescriptor>
|
||||
--- @param requested_set string[]
|
||||
--- @return string[] -- execution order
|
||||
---
|
||||
--- Implementation note: the 4 algorithm phases (dep-closure, in-degree, ready-queue, sort) are inlined as 3 small blocks within this function.
|
||||
--- Each was a 1-caller helper; the 2-caller rule doesn't apply, so inlining produces a single readable function
|
||||
--- Dependency closure, in-degree calculation, queue seeding, and sorting are local blocks.
|
||||
--- Keeping these blocks local makes the topological sort self-contained.
|
||||
local function topo_sort(passes, requested_set)
|
||||
-- Phase 1: dep-closure. Include every pass name transitively required by `requested_set`.
|
||||
-- Dependency closure: include every pass transitively required by `requested_set`.
|
||||
local needed = {}
|
||||
for _, name in ipairs(requested_set) do needed[name] = true end
|
||||
local changed = true
|
||||
@@ -570,7 +701,7 @@ local function topo_sort(passes, requested_set)
|
||||
end
|
||||
end
|
||||
|
||||
-- Phase 2: in-degrees for Kahn's algorithm. For each pass in `needed`, the number of its deps that are also in `needed`.
|
||||
-- In-degree calculation: count each needed pass's needed dependencies.
|
||||
local in_degree = {}
|
||||
for name, _ in pairs(needed) do in_degree[name] = 0 end
|
||||
for name, _ in pairs(needed) do
|
||||
@@ -581,14 +712,14 @@ local function topo_sort(passes, requested_set)
|
||||
end
|
||||
end
|
||||
|
||||
-- Phase 3: seed the ready queue with passes whose in-degree is 0, sorted alphabetically for deterministic order.
|
||||
-- Ready-queue seeding: add zero-in-degree passes in deterministic order.
|
||||
local ready = {}
|
||||
for name, deg in pairs(in_degree) do
|
||||
if deg == 0 then ready[#ready + 1] = name end
|
||||
end
|
||||
table.sort(ready)
|
||||
|
||||
-- Phase 4: drain the ready queue. For each popped pass, decrement the in-degree of every remaining pass that depended on it.
|
||||
-- Ready-queue drain: decrement dependents when each pass is emitted.
|
||||
-- Newly-zero-degree passes are inserted back into the ready queue (kept sorted).
|
||||
local order = {}
|
||||
while #ready > 0 do
|
||||
@@ -629,29 +760,8 @@ end
|
||||
-- ASCII dep graph renderer (Decision 6 in the spec)
|
||||
-- ════════════════════════════════════════════════════════════════════════════
|
||||
|
||||
--- Topological dep-order printer (used by --dry-run).
|
||||
---
|
||||
--- ASCII art graph rendering was removed from this file.
|
||||
--- Re-render the PASSES graph manually in `docs/guide_metaprogram_ssdl.md` if you need an updated visual;
|
||||
--- the canonical ASCII view there is regenerated by hand whenever PASSES rows change.
|
||||
-- ════════════════════════════════════════════════════════════════════════════
|
||||
local function render_dep_order(passes, closed)
|
||||
local lines = {}
|
||||
lines[#lines + 1] = "[ps1_meta] Resolved dependency order (closed under deps):"
|
||||
for pass_idx, name in ipairs(closed) do
|
||||
local p = passes[name]
|
||||
local deps_str = (#p.deps == 0) and "(no deps)" or
|
||||
"(deps: " .. table.concat(p.deps, ", ") .. ")"
|
||||
lines[#lines + 1] = string.format(" %d. %-22s %-45s [%s]",
|
||||
pass_idx, name, deps_str, p.kind)
|
||||
end
|
||||
return table.concat(lines, "\n") .. "\n"
|
||||
end
|
||||
|
||||
-- ════════════════════════════════════════════════════════════════════════════
|
||||
-- Topological dep-order printer (used by --dry-run).
|
||||
--
|
||||
-- ASCII art graph rendering was removed from this file.
|
||||
-- Re-render the PASSES graph manually in `docs/guide_metaprogram_ssdl.md` if you need an updated visual;
|
||||
-- The canonical ASCII view there is regenerated by hand whenever PASSES rows change.
|
||||
-- ════════════════════════════════════════════════════════════════════════════
|
||||
@@ -669,54 +779,46 @@ local function render_dep_order(passes, closed)
|
||||
end
|
||||
|
||||
-- ════════════════════════════════════════════════════════════════════════════
|
||||
-- Main orchestrator
|
||||
-- Main Orchestrator
|
||||
-- ════════════════════════════════════════════════════════════════════════════
|
||||
|
||||
-- (internal) Push a pass's outputs + warnings into `ctx.upstream[name]` for downstream passes to consume.
|
||||
-- @param ctx PassCtx
|
||||
-- @param pass_name string
|
||||
-- @param result PassResult
|
||||
--- (internal) Push a pass's outputs + warnings into `ctx.upstream[name]` for downstream passes to consume.
|
||||
--- @param ctx PassCtx
|
||||
--- @param pass_name string
|
||||
--- @param result PassResult
|
||||
local function accumulate_pass_result(ctx, pass_name, result)
|
||||
ctx.upstream[pass_name] = ctx.upstream[pass_name] or {}
|
||||
for _, out in ipairs(result.outputs or {}) do
|
||||
table.insert(ctx.upstream[pass_name], out)
|
||||
end
|
||||
for _, warn in ipairs(result.warnings or {}) do
|
||||
table.insert(ctx.upstream[pass_name], warn)
|
||||
end
|
||||
for _, out in ipairs(result.outputs or {}) do table.insert(ctx.upstream[pass_name], out) end
|
||||
for _, warn in ipairs(result.warnings or {}) do table.insert(ctx.upstream[pass_name], warn) end
|
||||
end
|
||||
|
||||
-- (internal) If the pass's kind is in PASS_KIND_STOP_ON_ERROR and it reported errors, write each error to stderr.
|
||||
-- Returns true if any validation errors were reported.
|
||||
-- @param pass_name string
|
||||
-- @param pass PassDescriptor
|
||||
-- @param result PassResult
|
||||
-- @return boolean
|
||||
--- (internal) If the pass's kind is in PASS_KIND_STOP_ON_ERROR and it reported errors, write each error to stderr.
|
||||
--- Returns true if any validation errors were reported.
|
||||
--- @param pass_name string
|
||||
--- @param pass PassDescriptor
|
||||
--- @param result PassResult
|
||||
--- @return boolean
|
||||
local function report_validation_errors(pass_name, pass, result)
|
||||
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
|
||||
if not (has_errors and PASS_KIND_STOP_ON_ERROR[pass.kind]) then return false end
|
||||
for _, e in ipairs(result.errors) do
|
||||
io.stderr:write(string.format("[%s] line %d: %s\n",
|
||||
pass_name, e.line or 0, e.msg or ""))
|
||||
io.stderr:write(string.format("[%s] line %d: %s\n", pass_name, e.line or 0, e.msg or ""))
|
||||
end
|
||||
return true
|
||||
end
|
||||
|
||||
-- (internal) Run each pass in `order` in topological sequence.
|
||||
-- @param ctx PassCtx
|
||||
-- @param order string[]
|
||||
-- @return boolean -- true if any validation errors were reported
|
||||
--- (internal) Run each pass in `order` in topological sequence.
|
||||
--- @param ctx PassCtx
|
||||
--- @param order string[]
|
||||
--- @return boolean -- true if any validation errors were reported
|
||||
local function dispatch_passes(ctx, order)
|
||||
ctx.shared = {}
|
||||
ctx.shared = ctx.shared or {}
|
||||
local had_errors = false
|
||||
for _, pass_name in ipairs(order) do
|
||||
local pass = PASSES[pass_name]
|
||||
io.stderr:write(string.format("[ps1_meta] %-22s running\n", pass_name))
|
||||
-- io.stderr:write(string.format("[ps1_meta] %-22s running\n", pass_name))
|
||||
local mod = require(pass.module)
|
||||
local result = mod.run(ctx)
|
||||
|
||||
accumulate_pass_result(ctx, pass_name, result)
|
||||
if report_validation_errors(pass_name, pass, result) then
|
||||
had_errors = true
|
||||
@@ -755,14 +857,15 @@ local function main(argv)
|
||||
end
|
||||
|
||||
-- Module export for in-process consumers (tests that dofile this script).
|
||||
-- The closed dep-order printer + the canonical `PASSES` table are exposed so a test
|
||||
-- can observe the resolved dep order for synthetic PASSES tables without spawning a subprocess.
|
||||
-- The closed dep-order printer + the `PASSES` table are exposed so a test can observe the resolved dep order for synthetic PASSES tables without spawning a subprocess.
|
||||
-- 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.
|
||||
local _M = {
|
||||
render_dep_order = render_dep_order,
|
||||
PASSES = PASSES,
|
||||
PASS_KIND_STOP_ON_ERROR = PASS_KIND_STOP_ON_ERROR,
|
||||
parse_args = parse_args,
|
||||
build_ctx = build_ctx,
|
||||
}
|
||||
|
||||
if arg and arg[0] and arg[0]:match("ps1_meta%.lua$") then
|
||||
|
||||
+1
-16
@@ -39,13 +39,7 @@ pop-location
|
||||
|
||||
# ════════════════════════════════════════════════════════════════════════════
|
||||
# PCSX-Redux — built via MSBuild (VS2022)
|
||||
#
|
||||
# Requires: Visual Studio 2022 with the C++ desktop workload.
|
||||
# The .vcxproj files target platform toolset v145, but VS2022 ships v143;
|
||||
# we pass /p:PlatformToolset=v143 to retarget at build time (no file edits).
|
||||
# NuGet packages (glfw, luajit.native, libFFmpeg-lite, x64sentry) are
|
||||
# restored automatically by MSBuild on first build.
|
||||
#
|
||||
# Output: toolchain\pcsx-redux\vsprojects\x64\Debug\pcsx-redux.exe
|
||||
# ════════════════════════════════════════════════════════════════════════════
|
||||
|
||||
@@ -65,8 +59,7 @@ $path_pcsx_sln = join-path $path_pcsx_redux 'vsprojects\pcsx-redux.sln'
|
||||
& $msbuild_exe $path_pcsx_sln /p:Configuration=Release /p:Platform=x64 /p:PlatformToolset=v143 /m /v:minimal
|
||||
|
||||
# Locate luajit via scoop. `luajit.exe` is on PATH via scoop's shim;
|
||||
# we use `scoop prefix` to find the install root for the include dir
|
||||
# (needed to compile lpeg against luajit's headers).
|
||||
# we use `scoop prefix` to find the install root for the include dir (needed to compile lpeg against luajit's headers).
|
||||
# If scoop or luajit is missing, fail fast with an actionable message.
|
||||
$luajit_prefix = & scoop prefix luajit 2>$null
|
||||
if (-not $luajit_prefix -or -not (Test-Path (Join-Path $luajit_prefix 'bin/luajit.exe'))) {
|
||||
@@ -87,7 +80,6 @@ if (-not $lua_inc_dir) {
|
||||
# Generate lpeg.dll by compiling the 6 source files directly.
|
||||
# `gcc` is on PATH (scoop's shim puts it there).
|
||||
# The source files: lpcap.c lpcode.c lpcset.c lpprint.c lptree.c lpvm.c
|
||||
# (per the lpeg makefile — no `make.lua` template generator in this version).
|
||||
# Link against luajit's import library (`libluajit-5.1.a`) for the Lua C API symbols (lua_*, luaL_*).
|
||||
$luajit_lib_dir = Join-Path $luajit_prefix 'lib'
|
||||
$lpeg_sources = @('lpcap.c', 'lpcode.c', 'lpcset.c', 'lpprint.c', 'lptree.c', 'lpvm.c')
|
||||
@@ -103,8 +95,6 @@ pop-location
|
||||
|
||||
# ════════════════════════════════════════════════════════════════════════════
|
||||
# lfs (LuaFileSystem) — compiled from pcsx-redux's vendored luafilesystem source.
|
||||
# Used by word_count_eval.lua :: scan_dir for native directory enumeration (~2ms)
|
||||
# instead of spawning `dir /b /s` as a subprocess (~56ms).
|
||||
# Source: toolchain/pcsx-redux/third_party/luafilesystem/src/lfs.c
|
||||
# Output: toolchain/lfs/lfs.dll
|
||||
# ════════════════════════════════════════════════════════════════════════════
|
||||
@@ -118,11 +108,6 @@ $lfs_dll_import = join-path $luajit_lib_dir 'libluajit-5.1.dll.a'
|
||||
|
||||
# ════════════════════════════════════════════════════════════════════════════
|
||||
# OpenBIOS — built from the PCSX-Redux source tree via make + mipsel-none-elf
|
||||
#
|
||||
# OpenBIOS is an open-source PS1 BIOS implementation (no retail BIOS dump needed).
|
||||
# It builds with the MIPS cross-toolchain (`mipsel-none-elf-gcc`, on PATH via the `mips` toolchain installer)
|
||||
# + `make` (on PATH via scoop).
|
||||
#
|
||||
# Output: toolchain\pcsx-redux\src\mips\openbios\openbios.bin
|
||||
# ════════════════════════════════════════════════════════════════════════════
|
||||
|
||||
|
||||
Reference in New Issue
Block a user