mirror of
https://github.com/Ed94/pikuma_ps1.git
synced 2026-08-04 14:48:48 +00:00
Better static analysis for C0 <-> C2 data race hazards.
This commit is contained in:
@@ -104,6 +104,7 @@ FI_ Slice_MipsCode tb_slice(TapeBuilder tb) { return (Sl
|
|||||||
* ---------------------------------------------------------------------------*/
|
* ---------------------------------------------------------------------------*/
|
||||||
|
|
||||||
// The 'Yield' sequence for Tape Atoms (mac_yield).
|
// The 'Yield' sequence for Tape Atoms (mac_yield).
|
||||||
|
atom_dbg_skip_over()
|
||||||
MipsAtomComp_(ac_yield) {
|
MipsAtomComp_(ac_yield) {
|
||||||
load_word(R_AtomJmp, R_TapePtr, 0),
|
load_word(R_AtomJmp, R_TapePtr, 0),
|
||||||
add_ui_self( R_TapePtr, S_(MipsCode)),
|
add_ui_self( R_TapePtr, S_(MipsCode)),
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
#include "stdio.h"
|
#include <stdio.h>
|
||||||
#include <stdlib.h>
|
#include <stdlib.h>
|
||||||
#include "assert.h"
|
#include <assert.h>
|
||||||
// #include "libgpu.h"
|
// #include "libgpu.h"
|
||||||
// #include "libetc.h"
|
// #include "libetc.h"
|
||||||
// #include "libgte.h"
|
// #include "libgte.h"
|
||||||
|
|||||||
@@ -103,8 +103,8 @@ MipsAtom_(rbind_floor_f3_face) atom_info(atom_bind(Binds_FloorTri), atom_phase(f
|
|||||||
mac_yield()
|
mac_yield()
|
||||||
};
|
};
|
||||||
|
|
||||||
internal
|
|
||||||
// atom_dbg_skip_over()
|
// atom_dbg_skip_over()
|
||||||
|
internal
|
||||||
MipsAtom_(floor_f3_face) atom_info(atom_phase(floor_f3)
|
MipsAtom_(floor_f3_face) atom_info(atom_phase(floor_f3)
|
||||||
, atom_reads( R_PrimCursor, R_FaceCursor, R_VertBase, R_OtBase)
|
, atom_reads( R_PrimCursor, R_FaceCursor, R_VertBase, R_OtBase)
|
||||||
, atom_writes(R_PrimCursor, R_FaceCursr)
|
, atom_writes(R_PrimCursor, R_FaceCursr)
|
||||||
|
|||||||
+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_iwyu = join-path $path_toolchain 'psyq_iwyu'
|
||||||
$path_psyq_imyu_inc = join-path $path_psyq_iwyu 'include'
|
$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(
|
function assemble-unit { param(
|
||||||
[string] $unit,
|
[string] $unit,
|
||||||
[string] $link_module,
|
[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 }
|
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 {
|
function build-hello_psyqo {
|
||||||
$includes += @()
|
$includes += @()
|
||||||
|
|
||||||
@@ -317,34 +350,16 @@ function build-graphis_hello {
|
|||||||
}
|
}
|
||||||
# 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 {
|
function build-gte_hello {
|
||||||
$includes += @()
|
$includes += @()
|
||||||
|
|
||||||
$path_module = join-path $path_code 'gte_hello'
|
$path_module = join-path $path_code 'gte_hello'
|
||||||
$path_duffle = join-path $path_code 'duffle'
|
$path_duffle = join-path $path_code 'duffle'
|
||||||
$path_atom_metadata = join-path $path_duffle 'word_count.metadata.h'
|
$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)
|
$src_c = join-path $path_module 'hello_gte.c'
|
||||||
$atom_sources = Get-SourceFiles -paths $source_dirs -extensions @('.h', '.c')
|
ps1-meta -unity_root $src_c -metadata $path_atom_metadata -out_root $path_build_gen
|
||||||
ps1-meta -sources $atom_sources -metadata $path_atom_metadata -out_root (join-path $path_build 'gen')
|
|
||||||
|
|
||||||
$assemble_args = @()
|
$assemble_args = @()
|
||||||
$assemble_args += $f_debug
|
$assemble_args += $f_debug
|
||||||
@@ -360,7 +375,6 @@ function build-gte_hello {
|
|||||||
|
|
||||||
# assemble-unit $src_asm $module_asm $includes $assemble_args
|
# 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'
|
$module_c = join-path $path_build 'hello_gte_c.o'
|
||||||
|
|
||||||
$compile_args = @()
|
$compile_args = @()
|
||||||
@@ -386,27 +400,17 @@ function build-gte_hello {
|
|||||||
make-binary $elf $exe
|
make-binary $elf $exe
|
||||||
|
|
||||||
# Post-link: gdb-runtime + dwarf-injection in a single Lua invocation (one luajit cold start).
|
# Post-link: gdb-runtime + dwarf-injection in a single Lua invocation (one luajit cold start).
|
||||||
ps1-meta -sources $atom_sources -metadata $path_atom_metadata `
|
ps1-meta -unity_root $src_c -metadata $path_atom_metadata -out_root $path_build_gen -passes @('--post-link') ` -extra_args @('--elf', $elf)
|
||||||
-out_root (join-path $path_build 'gen') `
|
|
||||||
-passes @('--post-link') `
|
|
||||||
-extra_args @('--elf', $elf)
|
|
||||||
|
|
||||||
# F' + G' splice: collapse 9 objcopy subprocess invocations into 3.
|
$dwarfLineBin = join-path $path_build_gen 'hello_gte.dwarf_line.bin'
|
||||||
# - 1 call: 3x --update-section for F' (line / aranges / rnglists)
|
$dwarfArangesBin = join-path $path_build_gen 'hello_gte.dwarf_aranges.bin'
|
||||||
# - 1 call: 3x --update-section for G' (info / abbrev / str)
|
$dwarfRnglistsBin = join-path $path_build_gen 'hello_gte.dwarf_rnglists.bin'
|
||||||
# - 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'
|
|
||||||
$injectElf = join-path $path_build 'hello_gte.dwarf-injected.elf'
|
$injectElf = join-path $path_build 'hello_gte.dwarf-injected.elf'
|
||||||
if ((Test-Path $dwarfLineBin) -and (Test-Path $dwarfArangesBin) -and (Test-Path $dwarfRnglistsBin))
|
if ((Test-Path $dwarfLineBin) -and (Test-Path $dwarfArangesBin) -and (Test-Path $dwarfRnglistsBin))
|
||||||
{
|
{
|
||||||
Write-Host "[build] DWARF-injecting $elf -> $injectElf"
|
Write-Host "[build] DWARF-injecting $elf -> $injectElf"
|
||||||
Copy-Item -LiteralPath $elf -Destination $injectElf -Force
|
Copy-Item -LiteralPath $elf -Destination $injectElf -Force
|
||||||
|
# Objcopy call: 3x --update-section for (line, aranges, rnglists).
|
||||||
# Single objcopy call: 3x --update-section for F' (line, aranges, rnglists).
|
|
||||||
$f_args = @(
|
$f_args = @(
|
||||||
"--update-section=.debug_line=$dwarfLineBin",
|
"--update-section=.debug_line=$dwarfLineBin",
|
||||||
"--update-section=.debug_aranges=$dwarfArangesBin",
|
"--update-section=.debug_aranges=$dwarfArangesBin",
|
||||||
@@ -419,12 +423,11 @@ function build-gte_hello {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
# G' 5-section splice: 3 update-section (info / abbrev / str) + 2 add-section (loc / loclists).
|
$dwarfInfoBin = join-path $path_build_gen 'hello_gte.dwarf_info.bin'
|
||||||
$dwarfInfoBin = join-path (join-path $path_build 'gen') 'hello_gte.dwarf_info.bin'
|
$dwarfAbbrevBin = join-path $path_build_gen 'hello_gte.dwarf_abbrev.bin'
|
||||||
$dwarfAbbrevBin = join-path (join-path $path_build 'gen') 'hello_gte.dwarf_abbrev.bin'
|
$dwarfStrBin = join-path $path_build_gen 'hello_gte.dwarf_str.bin'
|
||||||
$dwarfStrBin = join-path (join-path $path_build 'gen') 'hello_gte.dwarf_str.bin'
|
$dwarfLocBin = join-path $path_build_gen 'hello_gte.dwarf_loc.bin'
|
||||||
$dwarfLocBin = join-path (join-path $path_build 'gen') 'hello_gte.dwarf_loc.bin'
|
$dwarfLoclistsBin = join-path $path_build_gen 'hello_gte.dwarf_loclists.bin'
|
||||||
$dwarfLoclistsBin = join-path (join-path $path_build 'gen') 'hello_gte.dwarf_loclists.bin'
|
|
||||||
$g_args = @(
|
$g_args = @(
|
||||||
"--update-section=.debug_info=$dwarfInfoBin",
|
"--update-section=.debug_info=$dwarfInfoBin",
|
||||||
"--update-section=.debug_abbrev=$dwarfAbbrevBin",
|
"--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.
|
# 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.
|
# 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 `
|
& $Objcopy `
|
||||||
--set-section-flags ".rodata=alloc,load,readonly,code,contents" `
|
--set-section-flags ".rodata=alloc,load,readonly,code,contents" `
|
||||||
--set-section-flags ".data=alloc,load,data,code,contents" `
|
--set-section-flags ".data=alloc,load,data,code,contents" `
|
||||||
@@ -449,7 +452,8 @@ function build-gte_hello {
|
|||||||
if ($LASTEXITCODE -ne 0) {
|
if ($LASTEXITCODE -ne 0) {
|
||||||
Write-Warning "[build] atom-section flag update failed (exit $LASTEXITCODE); removing $injectElf"
|
Write-Warning "[build] atom-section flag update failed (exit $LASTEXITCODE); removing $injectElf"
|
||||||
Remove-Item -LiteralPath $injectElf -ErrorAction SilentlyContinue
|
Remove-Item -LiteralPath $injectElf -ErrorAction SilentlyContinue
|
||||||
} else {
|
}
|
||||||
|
else {
|
||||||
Write-Host "[build] DWARF-injected ELF: $injectElf"
|
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 write_file = duffle.write_file
|
||||||
local ensure_dir = duffle.ensure_dir
|
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.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
|
-- * 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[]).
|
-- 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,
|
-- 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
|
-- "existence" checks (declaration must exist, struct must exist) write errors[]; "shape" checks (writes/reads must be wave-context) write warnings[].
|
||||||
-- (writes/reads must be wave-context) write warnings[].
|
|
||||||
-- The `macro_word_drift` check writes both errors[] (missing/mismatch) and info[] (match).
|
-- The `macro_word_drift` check writes both errors[] (missing/mismatch) and info[] (match).
|
||||||
|
|
||||||
--- Check: every annotated atom must have a matching MipsAtom_(name) declaration.
|
--- Check: every annotated atom must have a matching MipsAtom_(name) declaration.
|
||||||
--- @param a AtomAnnotation
|
--- @param a AtomAnnotation
|
||||||
--- @param pipe_ctx PipeCtx
|
--- @param pipe_ctx PipeCtx
|
||||||
--- @param findings Findings
|
--- @param findings Findings
|
||||||
local function check_atom_decl_exists(a, pipe_ctx, 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,
|
--- 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.
|
--- while still surfacing the issue in the report.
|
||||||
--- The static-analysis report remains the source of truth for build-stopping errors.
|
--- The static-analysis report remains the source of truth for build-stopping errors.
|
||||||
--- @param a AtomAnnotation
|
--- @param a AtomAnnotation
|
||||||
--- @param pipe_ctx PipeCtx
|
--- @param pipe_ctx PipeCtx
|
||||||
--- @param findings Findings
|
--- @param findings Findings
|
||||||
local function check_binds_struct_exists(a, pipe_ctx, 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.
|
--- Check: TAPE_WORDS(mac_X, N) ↔ WORD_COUNT(mac_X, N) drift.
|
||||||
--- Three outcomes: missing (error), mismatch (error), match (info).
|
--- 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 wc table<string, integer> -- the shared word-count table (from ctx.shared.word_counts)
|
||||||
--- @param findings Findings
|
--- @param findings Findings
|
||||||
local function check_macro_word_drift(m, wc, 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`,
|
--- 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`.
|
--- 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.
|
--- 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 pipe_ctx PipeCtx
|
||||||
--- @param findings Findings
|
--- @param findings Findings
|
||||||
local function check_semantic_reg_defaults(_src, pipe_ctx, 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.
|
--- 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).
|
--- 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`.
|
--- 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 pipe_ctx PipeCtx
|
||||||
--- @param findings Findings
|
--- @param findings Findings
|
||||||
local function check_atom_reg_types(_src, pipe_ctx, 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
|
end
|
||||||
|
|
||||||
--- Check: atom_view(Binds_X) entries must reference a real Binds_* struct and that struct must declare at least one field.
|
--- 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 pipe_ctx PipeCtx
|
||||||
--- @param findings Findings
|
--- @param findings Findings
|
||||||
local function check_atom_view_layout(_src, pipe_ctx, 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
|
||||||
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.
|
--- 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.
|
--- (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.
|
--- This check directs raw C-ABI register names to explicit alias registration.
|
||||||
--- @param _src SourceFile
|
--- @param _src SourceFile
|
||||||
--- @param pipe_ctx PipeCtx
|
--- @param pipe_ctx PipeCtx
|
||||||
--- @param findings Findings
|
--- @param findings Findings
|
||||||
local function check_wave_context_migration(_src, pipe_ctx, findings)
|
local function check_wave_context_migration(_src, pipe_ctx, findings)
|
||||||
@@ -445,14 +444,65 @@ local CHECK_RULES = {
|
|||||||
-- Validation
|
-- Validation
|
||||||
-- ════════════════════════════════════════════════════════════════════════════
|
-- ════════════════════════════════════════════════════════════════════════════
|
||||||
--
|
--
|
||||||
-- Pure check: read from src.scan, run validations, emit findings.
|
-- Pure check: read from src.scan, run validations, emit findings. The scan was done once upstream.
|
||||||
-- No source walking; no parsing. 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 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
|
--- @return AnnotatedResult
|
||||||
local function validate(ctx, src)
|
local function validate(ctx, src, corpus_pipe_ctx)
|
||||||
local scan = src.scan
|
local scan = src.scan
|
||||||
|
|
||||||
-- Project the pre-scanned atoms to the AtomEntry shape this pass needs.
|
-- Project the pre-scanned atoms to the AtomEntry shape this pass needs.
|
||||||
@@ -478,9 +528,11 @@ local function validate(ctx, src)
|
|||||||
}
|
}
|
||||||
end
|
end
|
||||||
|
|
||||||
-- Build pipe_ctx (Fleury: expose structure). Pre-compute everything the per-check functions need.
|
-- Build the per-source pipe_ctx (Fleury: expose structure).
|
||||||
-- Single source of truth for atom / binds / annotation-count lookups.
|
-- Cross-source visibility comes from `corpus_pipe_ctx`;
|
||||||
-- pipe_ctx.types / pipe_ctx.atom_views / pipe_ctx.seen_defaults are projected from the scan payload so per_source check rules can iterate.
|
-- 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 = {}
|
local seen_defaults = {}
|
||||||
for reg, _ in pairs(scan.types or {}) do
|
for reg, _ in pairs(scan.types or {}) do
|
||||||
seen_defaults[reg] = (seen_defaults[reg] or 0) + 1
|
seen_defaults[reg] = (seen_defaults[reg] or 0) + 1
|
||||||
@@ -493,25 +545,20 @@ local function validate(ctx, src)
|
|||||||
local pipe_ctx = {
|
local pipe_ctx = {
|
||||||
atom_index = {},
|
atom_index = {},
|
||||||
binds_index = {},
|
binds_index = {},
|
||||||
annot_counts = {},
|
annot_counts = corpus_pipe_ctx.annot_counts,
|
||||||
types = scan.types or {},
|
types = scan.types or {},
|
||||||
type_occurrences = scan.type_occurrences or {},
|
type_occurrences = scan.type_occurrences or {},
|
||||||
atom_views = scan.atom_views or {},
|
atom_views = scan.atom_views or {},
|
||||||
seen_defaults = seen_defaults,
|
seen_defaults = seen_defaults,
|
||||||
atom_infos_list = atom_infos_list,
|
atom_infos_list = atom_infos_list,
|
||||||
binds_list = scan.binds or {},
|
binds_list = scan.binds or {},
|
||||||
-- Project the source-derived registries from the scan payload so per_source checks consult them instead of the deleted
|
-- Source-derived registries: still populated from the scan payload as a convenience for callers that want source-local visibility.
|
||||||
-- SEMANTIC_DEFAULT_REGS / KNOWN_REG_DEFAULT_TYPES / etc.
|
-- The canonical cross-source lookup tables live in corpus_pipe_ctx.
|
||||||
register_alias_registry = scan.register_alias_registry or {},
|
register_alias_registry = corpus_pipe_ctx.register_alias_registry,
|
||||||
type_name_registry = scan.type_name_registry or {},
|
type_name_registry = corpus_pipe_ctx.type_name_registry,
|
||||||
}
|
}
|
||||||
for _, a in ipairs(atoms) do pipe_ctx.atom_index [a.name] = a end
|
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 _, 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).
|
-- Findings live in a single struct with three lists (errors / warnings / info).
|
||||||
-- Each check writes to the list appropriate for its severity.
|
-- 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
|
if rule.post then rule.post(pipe_ctx, findings) end
|
||||||
end
|
end
|
||||||
|
|
||||||
-- Per-skip-marker rules.
|
-- Per-skip-marker rules.
|
||||||
-- Each raw marker recorded by scan_source (in scan.skip_over.markers) is validated independently;
|
-- Each raw marker recorded by scan_source (in scan.skip_over.markers) is validated independently;
|
||||||
-- the check emits at most one error per marker.
|
-- the check emits at most one error per marker.
|
||||||
-- Valid markers stay attached to scan.skip_over.atoms /.components for dwarf_injection.lua consumer.
|
-- 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 {}
|
local skip_markers = scan.skip_over and scan.skip_over.markers or {}
|
||||||
@@ -555,7 +602,7 @@ local function validate(ctx, src)
|
|||||||
end
|
end
|
||||||
|
|
||||||
-- Per-macro rules (TAPE_WORDS vs WORD_COUNT drift).
|
-- 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 _, m in ipairs(scan.macros) do
|
||||||
for _, rule in ipairs(CHECK_RULES) do
|
for _, rule in ipairs(CHECK_RULES) do
|
||||||
if rule.per_macro then rule.per_macro(m, wc, findings) end
|
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).
|
-- Information summary (always emitted).
|
||||||
findings.info[#findings.info + 1] = {
|
findings.info[#findings.info + 1] = {
|
||||||
line = 0,
|
line = 0,
|
||||||
msg = string.format("scanned: %d atom(s), %d annotation(s), %d macro-word-decl(s), %d binds struct(s)",
|
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),
|
, #atoms, #annots, #scan.macros, #scan.binds),
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
@@ -650,9 +697,16 @@ function M.run(ctx)
|
|||||||
local errors = {}
|
local errors = {}
|
||||||
local warnings = {}
|
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.
|
-- Build the corpus-wide pipe_ctx ONCE per pass run.
|
||||||
-- `ctx.by_dir` is pre-computed in build_ctx (shared across all passes).
|
-- The corpus owns the canonical cross-source registries; per-source scans retain body / declaration ownership.
|
||||||
local by_dir = ctx.by_dir or duffle.group_sources_by_dir(ctx.sources)
|
-- 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
|
for dir, dir_sources in pairs(by_dir) do
|
||||||
local dir_basename = dir:match("([^/\\]+)$") or dir
|
local dir_basename = dir:match("([^/\\]+)$") or dir
|
||||||
@@ -663,7 +717,7 @@ function M.run(ctx)
|
|||||||
ctx.flags = ctx.flags or {}
|
ctx.flags = ctx.flags or {}
|
||||||
ctx.flags._annot_source_results = ctx.flags._annot_source_results or {}
|
ctx.flags._annot_source_results = ctx.flags._annot_source_results or {}
|
||||||
for _, src in ipairs(dir_sources) do
|
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
|
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()
|
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
|
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.
|
--- 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`)
|
--- Reads the canonical `atom.paths` projection produced by the upstream `emission_model` pass.
|
||||||
--- for `MipsAtom_(name)` (kind="atom"), `MipsAtomComp_` / `MipsAtomComp_Proc_` (kind="comp_*"),
|
--- The ordered `items` stream, dense `word_events`, and `invocations` views are the only semantic inputs to this pass;
|
||||||
--- and `MipsCode code_<name>` (kind="raw_atom") declarations.
|
--- it emits one `WORD N LINE L TEXT T` line per emitted `.word`.
|
||||||
--- 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`.
|
|
||||||
---
|
---
|
||||||
--- **Two output forms** (per the workspace's per-emission-form pattern from
|
--- **Two output forms** (per the workspace's per-emission-form pattern from
|
||||||
--- `guide_metaprogram_ssdl.md`):
|
--- `guide_metaprogram_ssdl.md`):
|
||||||
@@ -34,10 +31,8 @@
|
|||||||
--- ENDATOM
|
--- ENDATOM
|
||||||
--- ```
|
--- ```
|
||||||
---
|
---
|
||||||
--- Marker calls (`atom_label(...)`, `atom_offset(...)`) emit 0 `.word`s.
|
--- Marker records are zero-width in `atom.paths.items`; they do not appear in
|
||||||
--- They share the same walking convention as `passes/offsets.lua :: scan_atom_body`:
|
--- the dense word view and therefore emit no WORD rows.
|
||||||
--- 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`.
|
|
||||||
---
|
---
|
||||||
--- **Conventions:** tabs (1/level), EmmyLua annotations, no regex,
|
--- **Conventions:** tabs (1/level), EmmyLua annotations, no regex,
|
||||||
--- Lua 5.3 compatible.
|
--- Lua 5.3 compatible.
|
||||||
@@ -50,10 +45,8 @@
|
|||||||
-- (works both standalone + when require'd). `duffle_paths.lua` sets package.path then returns `require("duffle")`
|
-- (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.
|
-- at the bottom, so the dofile value IS the duffle module.
|
||||||
local _bootstrap_dir = debug.getinfo(1, "S").source:match("^@?(.*[/\\])") or "./"
|
local _bootstrap_dir = debug.getinfo(1, "S").source:match("^@?(.*[/\\])") or "./"
|
||||||
local duffle = dofile(_bootstrap_dir .. "../duffle_paths.lua")
|
local duffle = dofile(_bootstrap_dir .. "../duffle_paths.lua")
|
||||||
local elf_dwarf = require("elf_dwarf")
|
local elf_dwarf = require("elf_dwarf")
|
||||||
local word_count_eval = require("word_count_eval")
|
|
||||||
local count_token_words = word_count_eval.count_token_words
|
|
||||||
|
|
||||||
-- ════════════════════════════════════════════════════════════════════════════
|
-- ════════════════════════════════════════════════════════════════════════════
|
||||||
-- Constants
|
-- Constants
|
||||||
@@ -63,199 +56,90 @@ local count_token_words = word_count_eval.count_token_words
|
|||||||
-- the gdb runtime loader rejects mismatches (E2).
|
-- the gdb runtime loader rejects mismatches (E2).
|
||||||
local FORMAT_VERSION = 1
|
local FORMAT_VERSION = 1
|
||||||
|
|
||||||
-- Marker-call identifiers (mirrors offsets.lua:33-34).
|
|
||||||
local LABEL_MARKER = "atom_label"
|
|
||||||
local OFFSET_MARKER = "atom_offset"
|
|
||||||
|
|
||||||
-- ════════════════════════════════════════════════════════════════════════════
|
-- ════════════════════════════════════════════════════════════════════════════
|
||||||
-- Type declarations
|
-- Type declarations
|
||||||
-- ════════════════════════════════════════════════════════════════════════════
|
-- ════════════════════════════════════════════════════════════════════════════
|
||||||
|
|
||||||
--- @class AtomSourceMapCtx
|
--- @class AtomSourceMapCtx
|
||||||
--- @field sources table[] -- SourceScan payload per source (from `ctx.sources`)
|
|
||||||
--- @field shared table -- `ctx.shared`
|
--- @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 out_root string -- output root (e.g. "build/gen")
|
||||||
--- @field dry_run boolean -- if true, compute but don't write
|
--- @field dry_run boolean -- if true, compute but don't write
|
||||||
--- @field flags table -- `ctx.flags`; reads `flags.gdb_runtime` + `flags.elf_path`
|
--- @field flags table -- `ctx.flags`; reads `flags.gdb_runtime` + `flags.elf_path`
|
||||||
|
|
||||||
-- ════════════════════════════════════════════════════════════════════════════
|
-- ════════════════════════════════════════════════════════════════════════════
|
||||||
-- Helpers
|
-- Canonical atom-path renderers
|
||||||
-- ════════════════════════════════════════════════════════════════════════════
|
-- ════════════════════════════════════════════════════════════════════════════
|
||||||
|
|
||||||
-- ════════════════════════════════════════════════════════════════════════════
|
--- Join canonical words to canonical word items. `items` supplies the ordered
|
||||||
-- Provenance emission
|
--- word boundaries, while `word_events` supplies call text and source lines.
|
||||||
-- ════════════════════════════════════════════════════════════════════════════
|
|
||||||
|
|
||||||
-- 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.
|
|
||||||
--- @param atom table
|
--- @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
|
--- @return table[], integer
|
||||||
local function compute_word_entries(atom, src, wc, mode, comp, comp_body_index)
|
local function canonical_word_entries(atom)
|
||||||
local entries = {}
|
local paths = atom.paths or {}
|
||||||
local pos = 0
|
local events = paths.word_events or {}
|
||||||
for _, t in ipairs(atom.body_tokens) do
|
local word_items = {}
|
||||||
local tok = t.tok
|
for _, item in ipairs(paths.items or {}) do
|
||||||
local rel = t.rel
|
if item.kind == "word" then word_items[#word_items + 1] = item end
|
||||||
|
|
||||||
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
|
|
||||||
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
|
end
|
||||||
|
|
||||||
--- Render one atom's provenance stanza. Format:
|
--- Render one atom's provenance stanza. Format 1 remains:
|
||||||
--- `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> MACRO <name> "<def-path>:<def-line>" BODY <line>`
|
||||||
--- `WORD N CALL <src-path>:<src-line> RAW` (for direct instructions)
|
--- `WORD N CALL <src-path>:<src-line> RAW`
|
||||||
--- `BODY <line>` is the source line of THIS specific word within the macro body
|
--- Component identity comes from the canonical outermost invocation record;
|
||||||
--- (lottes_tape.h:N where N is the per-word body line).
|
--- the count-table lookup is the canonical component declaration witness.
|
||||||
--- Absent for RAW rows and for component rows whose component declaration could not be indexed (older pass combinations / external macros).
|
--- @param src table
|
||||||
--- Downstream consumers (dwarf_injection, tests) fall back to DefLine / comp_line when BODY is absent.
|
--- @param atom table
|
||||||
--- Returns (lines, total_words).
|
--- @param wc table -- identity alias of corpus.word_counts
|
||||||
--- @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}
|
|
||||||
--- @return string[], integer
|
--- @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 lines = {}
|
||||||
local rel_path = src.path:gsub("\\", "/")
|
local rel_path = src.path:gsub("\\\\", "/")
|
||||||
local entries, total = compute_word_entries(atom, src, wc, "provenance", comp, comp_body_index)
|
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)
|
lines[#lines + 1] = string.format('ATOM %s "%s" 0', atom.raw_name or atom.name, rel_path)
|
||||||
|
|
||||||
for _, pe in ipairs(entries) do
|
for _, entry in ipairs(entries) do
|
||||||
if pe.comp_name then
|
local inv = entry.invocation
|
||||||
local body_suffix = ""
|
local macro_count = inv and wc["mac_" .. inv.component_name]
|
||||||
if pe.body_line then
|
if inv and macro_count ~= nil then
|
||||||
body_suffix = " BODY " .. tostring(pe.body_line)
|
lines[#lines + 1] = string.format(
|
||||||
end
|
'WORD %d CALL %s:%d MACRO %s "%s:%d" BODY %d',
|
||||||
lines[#lines + 1] = string.format('WORD %d CALL %s:%d MACRO %s "%s:%d"%s',
|
entry.pos, rel_path, entry.line, inv.component_name,
|
||||||
pe.pos, rel_path, pe.line, pe.comp_name, pe.comp_path, pe.comp_line, body_suffix)
|
inv.def_path or "", inv.def_line or 0, entry.body_line)
|
||||||
else
|
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
|
||||||
end
|
end
|
||||||
|
|
||||||
-- Patch the placeholder total in the ATOM header line.
|
|
||||||
lines[1] = lines[1]:gsub(" 0$", " " .. tostring(total))
|
lines[1] = lines[1]:gsub(" 0$", " " .. tostring(total))
|
||||||
lines[#lines + 1] = "ENDATOM"
|
lines[#lines + 1] = "ENDATOM"
|
||||||
return lines, total
|
return lines, total
|
||||||
end
|
end
|
||||||
|
|
||||||
--- Build a per-source component body index keyed by the bare component name (e.g. `gte_load_tri_verts`).
|
--- Render the full provenance file content for one source.
|
||||||
--- 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).
|
|
||||||
--- @param src table
|
--- @param src table
|
||||||
--- @param wc 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
|
--- @return string
|
||||||
local function render_provenance(src, wc, comp, comp_body_index)
|
local function render_provenance(src, wc)
|
||||||
local lines = {}
|
local lines = {}
|
||||||
lines[#lines + 1] = "# FORMAT_VERSION 1"
|
lines[#lines + 1] = "# FORMAT_VERSION 1"
|
||||||
lines[#lines + 1] = "# auto-generated by ps1_meta.lua (passes/atoms_source_map.lua) — DO NOT EDIT"
|
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] = "# 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."
|
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).
|
local function append(atom)
|
||||||
-- 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`).
|
local stanza = emit_provenance_stanza(src, atom, wc)
|
||||||
|
|
||||||
for _, atom in ipairs(src.scan.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
|
for _, line in ipairs(stanza) do lines[#lines + 1] = line end
|
||||||
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
|
for _, atom in ipairs(src.scan.raw_atoms or {}) do
|
||||||
local stanza = emit_provenance_stanza(src, atom, wc, comp, comp_body_index)
|
if atom.paths then append(atom) end
|
||||||
for _, line in ipairs(stanza) do lines[#lines + 1] = line end
|
|
||||||
end
|
end
|
||||||
|
|
||||||
return table.concat(lines, "\n") .. "\n"
|
return table.concat(lines, "\n") .. "\n"
|
||||||
@@ -286,19 +169,16 @@ end
|
|||||||
--- @param atom table
|
--- @param atom table
|
||||||
--- @param wc table
|
--- @param wc table
|
||||||
--- @return string[], integer
|
--- @return string[], integer
|
||||||
local function emit_atom_stanza(src, atom, wc)
|
local function emit_atom_stanza(src, atom)
|
||||||
local lines = {}
|
local lines = {}
|
||||||
local rel_path = src.path:gsub("\\", "/")
|
local rel_path = src.path:gsub("\\\\", "/")
|
||||||
local entries, total = compute_word_entries(atom, src, wc)
|
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)
|
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",
|
lines[#lines + 1] = string.format("WORD %d LINE %d TEXT %s",
|
||||||
we.pos, we.line, we.text)
|
entry.pos, entry.line, entry.text)
|
||||||
end
|
end
|
||||||
|
|
||||||
-- Patch the placeholder total in the ATOM header line.
|
|
||||||
lines[1] = lines[1]:gsub(" 0$", " " .. tostring(total))
|
lines[1] = lines[1]:gsub(" 0$", " " .. tostring(total))
|
||||||
lines[#lines + 1] = "ENDATOM"
|
lines[#lines + 1] = "ENDATOM"
|
||||||
return lines, total
|
return lines, total
|
||||||
@@ -309,18 +189,20 @@ end
|
|||||||
--- @param src table
|
--- @param src table
|
||||||
--- @param wc table
|
--- @param wc table
|
||||||
--- @return string
|
--- @return string
|
||||||
local function render_source_map(src, wc)
|
local function render_source_map(src)
|
||||||
local lines = {}
|
local lines = {}
|
||||||
lines[#lines + 1] = "# FORMAT_VERSION " .. FORMAT_VERSION
|
lines[#lines + 1] = "# FORMAT_VERSION " .. FORMAT_VERSION
|
||||||
lines[#lines + 1] = "# auto-generated by ps1_meta.lua (passes/atoms_source_map.lua) — DO NOT EDIT"
|
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 function append(atom)
|
||||||
local stanza = emit_atom_stanza(src, atom, wc)
|
local stanza = emit_atom_stanza(src, atom)
|
||||||
for _, line in ipairs(stanza) do lines[#lines + 1] = line end
|
for _, line in ipairs(stanza) do lines[#lines + 1] = line end
|
||||||
end
|
end
|
||||||
for _, atom in ipairs(src.scan.raw_atoms or {}) do
|
for _, atom in ipairs(src.scan.atoms or {}) do
|
||||||
local stanza = emit_atom_stanza(src, atom, wc)
|
if atom.paths then append(atom) end
|
||||||
for _, line in ipairs(stanza) do lines[#lines + 1] = line end
|
end
|
||||||
|
for _, atom in ipairs(src.scan.raw_atoms or {}) do
|
||||||
|
if atom.paths then append(atom) end
|
||||||
end
|
end
|
||||||
|
|
||||||
return table.concat(lines, "\n") .. "\n"
|
return table.concat(lines, "\n") .. "\n"
|
||||||
@@ -343,55 +225,35 @@ end
|
|||||||
--- @param ctx PassCtx
|
--- @param ctx PassCtx
|
||||||
--- @return table[] -- list of {idx, name, src_path, file_base, addr, size_bytes, words, entries}
|
--- @return table[] -- list of {idx, name, src_path, file_base, addr, size_bytes, words, entries}
|
||||||
local function build_atom_table(ctx)
|
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 addrs = elf_dwarf.read_nm(ctx.flags.elf_path)
|
||||||
|
local corpus = ctx.shared and ctx.shared.corpus
|
||||||
local matched = {}
|
local matched = {}
|
||||||
for _, src in ipairs(ctx.sources) do
|
|
||||||
if src.scan then
|
for _, src in ipairs(corpus.source_order or {}) do
|
||||||
local file_base = src.path:match("([^/\\]+)$") or src.path
|
local file_base = src.path:match("([^/\\\\]+)$") or src.path
|
||||||
for _, atom in ipairs(src.scan.atoms or {}) do
|
local function append(atom)
|
||||||
if atom.kind == nil or atom.kind == "atom" then
|
if not atom.paths then return end
|
||||||
local name = atom.raw_name or atom.name
|
local name = atom.raw_name or atom.name
|
||||||
local info = addrs[name]
|
local info = addrs[name]
|
||||||
if info then
|
if not info then return end
|
||||||
local entries, total = compute_word_entries(atom, src, wc)
|
local entries, total = canonical_word_entries(atom)
|
||||||
matched[#matched + 1] = {
|
matched[#matched + 1] = {
|
||||||
name = name,
|
name = name,
|
||||||
src_path = src.path,
|
src_path = src.path,
|
||||||
file_base = file_base,
|
file_base = file_base,
|
||||||
addr = info[1],
|
addr = info[1],
|
||||||
size_bytes = info[2],
|
size_bytes = info[2],
|
||||||
words = total,
|
words = total,
|
||||||
entries = entries,
|
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
|
|
||||||
end
|
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
|
end
|
||||||
|
|
||||||
-- Deterministic order: sort by address (matches `nm` output ordering).
|
-- Deterministic order: sort by address (matches `nm` output ordering).
|
||||||
table.sort(matched, function(a, b) return a.addr < b.addr end)
|
table.sort(matched, function(a, b) return a.addr < b.addr end)
|
||||||
for i, a in ipairs(matched) do
|
for i, a in ipairs(matched) do a.idx = i - 1 end
|
||||||
a.idx = i - 1
|
|
||||||
end
|
|
||||||
return matched
|
return matched
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -652,57 +514,52 @@ function M.run(ctx)
|
|||||||
local errors = {}
|
local errors = {}
|
||||||
local warnings = {}
|
local warnings = {}
|
||||||
|
|
||||||
-- word-counts + components passes must have populated shared.word_counts.
|
local corpus = ctx.shared and ctx.shared.corpus
|
||||||
-- If absent, the orchestrator wired the deps wrong — fail loud.
|
if type(corpus) ~= "table" or type(corpus.source_order) ~= "table" then
|
||||||
local wc = (ctx.shared and ctx.shared.word_counts) or {}
|
error("atoms_source_map.run requires ctx.shared.corpus.source_order (canonical corpus).", 0)
|
||||||
if not wc or not next(wc) then
|
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] = {
|
warnings[#warnings + 1] = {
|
||||||
line = 0,
|
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
|
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).
|
-- Always emit the canonical text form (per-source).
|
||||||
for _, src in ipairs(ctx.sources) do
|
for _, src in ipairs(corpus.source_order) do
|
||||||
if src.scan then
|
local has_projection = false
|
||||||
local n_atoms = src.scan.atoms and #src.scan.atoms or 0
|
for _, atom in ipairs((src.scan or {}).atoms or {}) do
|
||||||
local n_raw_atoms = src.scan.raw_atoms and #src.scan.raw_atoms or 0
|
if atom.paths then has_projection = true; break end
|
||||||
if n_atoms + n_raw_atoms > 0 then
|
end
|
||||||
local basename = duffle.basename_no_ext(src.path)
|
if not has_projection then
|
||||||
|
for _, atom in ipairs((src.scan or {}).raw_atoms or {}) do
|
||||||
-- (1) atoms.sourcemap.txt — per-.word line map (unchanged contract).
|
if atom.paths then has_projection = true; break end
|
||||||
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 }
|
|
||||||
end
|
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
|
end
|
||||||
|
|
||||||
-- Optionally emit the gdb-runtime form (post-link, one file per build).
|
-- 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`
|
--- We then verify the preceding context ends with `MipsAtom`
|
||||||
--- (the function-decl keyword with possible qualifiers between).
|
--- (the function-decl keyword with possible qualifiers between).
|
||||||
---
|
---
|
||||||
--- @param source string
|
--- @param source string
|
||||||
--- @param name string
|
--- @param name string
|
||||||
--- @param before_pos integer
|
--- @param before_pos integer
|
||||||
--- @return string|nil
|
--- @return string|nil
|
||||||
local function find_function_args_for(source, name, before_pos)
|
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)
|
--- 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.
|
--- over to the generated `mac_X` macro, so LSP/IntelliSense displays the args doc.
|
||||||
--- @param source string
|
--- @param source string
|
||||||
--- @param pos integer
|
--- @param pos integer
|
||||||
--- @return string
|
--- @return string
|
||||||
local function preceding_comment_block(source, pos)
|
local function preceding_comment_block(source, pos)
|
||||||
local scan_pos = pos
|
local scan_pos = pos
|
||||||
@@ -266,12 +266,12 @@ end
|
|||||||
-- Component projection (read from pre-scanned SourceScan)
|
-- Component projection (read from pre-scanned SourceScan)
|
||||||
-- ════════════════════════════════════════════════════════════════════════════
|
-- ════════════════════════════════════════════════════════════════════════════
|
||||||
|
|
||||||
-- Project pre-scanned MipsAtomComp_ / MipsAtomComp_Proc_ entries into Component shape.
|
--- 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).
|
--- 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.
|
--- 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 source string -- the full source text (needed for backward lookups)
|
||||||
-- @param scan table -- SourceScan from duffle.scan_source
|
--- @param scan table -- SourceScan from duffle.scan_source
|
||||||
-- @return Component[]
|
--- @return Component[]
|
||||||
local function project_components(source, scan)
|
local function project_components(source, scan)
|
||||||
local out = {}
|
local out = {}
|
||||||
for _, a in ipairs(scan.atoms) do
|
for _, a in ipairs(scan.atoms) do
|
||||||
@@ -282,6 +282,7 @@ local function project_components(source, scan)
|
|||||||
line = a.line,
|
line = a.line,
|
||||||
name = a.name,
|
name = a.name,
|
||||||
body = a.body,
|
body = a.body,
|
||||||
|
body_off = a.body_off,
|
||||||
body_tokens = a.body_tokens,
|
body_tokens = a.body_tokens,
|
||||||
args = args,
|
args = args,
|
||||||
comment = comment,
|
comment = comment,
|
||||||
@@ -339,11 +340,11 @@ end
|
|||||||
-- Word-count computation (memoized recursive lookup)
|
-- 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.
|
--- 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
|
--- 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).
|
--- (so a non-component ident like `mask_upper` falls through to the wc-table branch).
|
||||||
-- @param ident string|nil
|
--- @param ident string|nil
|
||||||
-- @return string|nil
|
--- @return string|nil
|
||||||
local function strip_mac_prefix(ident)
|
local function strip_mac_prefix(ident)
|
||||||
if not ident then return nil end
|
if not ident then return nil end
|
||||||
if ident:sub(1, MAC_PREFIX_LEN) == MAC_PREFIX then
|
if ident:sub(1, MAC_PREFIX_LEN) == MAC_PREFIX then
|
||||||
@@ -352,13 +353,13 @@ local function strip_mac_prefix(ident)
|
|||||||
return ident
|
return ident
|
||||||
end
|
end
|
||||||
|
|
||||||
-- (internal) Recursive word-count lookup. `cache` is the memoization table shared across all components
|
--- (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).
|
--- 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 name string -- the component name (without `mac_`)
|
||||||
-- @param comp_by_name table<string, Component>
|
--- @param comp_by_name table<string, Component>
|
||||||
-- @param wc table<string, integer>
|
--- @param wc table<string, integer>
|
||||||
-- @param cache table<string, integer>
|
--- @param cache table<string, integer>
|
||||||
-- @return integer
|
--- @return integer
|
||||||
local function word_count_rec(name, comp_by_name, wc, cache)
|
local function word_count_rec(name, comp_by_name, wc, cache)
|
||||||
if cache[name] ~= nil then return cache[name] end
|
if cache[name] ~= nil then return cache[name] end
|
||||||
cache[name] = -1 -- mark in-progress (cycle detection)
|
cache[name] = -1 -- mark in-progress (cycle detection)
|
||||||
@@ -397,7 +398,7 @@ end
|
|||||||
--- references hit memoized values instead of re-walking the body.
|
--- 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`.
|
--- Cycle detection (A -> B -> A) is preserved via the in-progress `-1` sentinel in `cache`.
|
||||||
--- @param components Component[]
|
--- @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
|
--- @return table<string, integer> -- map of component name (without `mac_`) -> word count
|
||||||
local function count_all_components(components, wc)
|
local function count_all_components(components, wc)
|
||||||
local comp_by_name = {}
|
local comp_by_name = {}
|
||||||
@@ -470,9 +471,9 @@ end
|
|||||||
|
|
||||||
--- Build the list of lines for one component
|
--- 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).
|
--- (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 components Component[]
|
||||||
--- @param wc table<string, integer>
|
--- @param wc table<string, integer>
|
||||||
--- @return string[] -- list of lines for this component
|
--- @return string[] -- list of lines for this component
|
||||||
local function build_component_lines(c, counts)
|
local function build_component_lines(c, counts)
|
||||||
local lines = {}
|
local lines = {}
|
||||||
@@ -504,10 +505,10 @@ end
|
|||||||
-- Per-source emit logic
|
-- Per-source emit logic
|
||||||
-- ════════════════════════════════════════════════════════════════════════════
|
-- ════════════════════════════════════════════════════════════════════════════
|
||||||
|
|
||||||
-- Build the boilerplate header lines (the `#ifdef INTELLISENSE_DIRECTIVES` block,
|
--- 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).
|
--- the `// Auto-generated` comment, the `// Source:` line, and the self-contained `WORD_COUNT` macro definition).
|
||||||
-- @param src SourceFile
|
--- @param src SourceFile
|
||||||
-- @return string[]
|
--- @return string[]
|
||||||
local function header_boilerplate(src)
|
local function header_boilerplate(src)
|
||||||
return {
|
return {
|
||||||
-- #pragma once wrapped in #ifdef INTELLISENSE_DIRECTIVES, matching the convention in lottes_tape.h.
|
-- #pragma once wrapped in #ifdef INTELLISENSE_DIRECTIVES, matching the convention in lottes_tape.h.
|
||||||
@@ -529,13 +530,13 @@ local function header_boilerplate(src)
|
|||||||
}
|
}
|
||||||
end
|
end
|
||||||
|
|
||||||
-- Compute the output path for one source's `.macs.h` file.
|
--- Compute the output path for one source's `.macs.h` file.
|
||||||
-- The pre-rework convention uses the *directory* basename
|
--- The pre-rework convention uses the *directory* basename (not the source file basename)
|
||||||
-- (not the source file basename) e.g. `code/duffle/lottes_tape.h` produces `code/duffle/gen/duffle.macs.h`.
|
--- e.g. `code/duffle/lottes_tape.h` produces `code/duffle/gen/duffle.macs.h`.
|
||||||
-- This matches what the C codebase #includes.
|
--- This matches what the C codebase #includes.
|
||||||
-- @param src SourceFile
|
--- @param src SourceFile
|
||||||
-- @return string -- the output directory
|
--- @return string -- the output directory
|
||||||
-- @return string -- the full output path
|
--- @return string -- the full output path
|
||||||
local function compute_macs_h_path(src)
|
local function compute_macs_h_path(src)
|
||||||
local out_dir = src.dir .. "/" .. GEN_SUBDIR
|
local out_dir = src.dir .. "/" .. GEN_SUBDIR
|
||||||
local out_path = out_dir .. "/" .. duffle.basename_no_ext(src.dir) .. ".macs.h"
|
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.
|
--- 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).
|
--- 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.
|
--- Honors `ctx.dry_run`: prints the intended path but does not write the file.
|
||||||
--- @param ctx PassCtx
|
--- @param ctx PassCtx
|
||||||
--- @param src SourceFile
|
--- @param src SourceFile
|
||||||
--- @param components Component[]
|
--- @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)
|
--- @return string|nil -- path to the written file (nil if no components)
|
||||||
local function emit_component_macros_h(ctx, src, components, counts)
|
local function emit_component_macros_h(ctx, src, components, counts)
|
||||||
if #components == 0 then return nil end
|
if #components == 0 then return nil end
|
||||||
@@ -577,41 +578,86 @@ end
|
|||||||
-- Pass entry
|
-- Pass entry
|
||||||
-- ════════════════════════════════════════════════════════════════════════════
|
-- ════════════════════════════════════════════════════════════════════════════
|
||||||
|
|
||||||
-- (internal) Extend `ctx.shared.word_counts` with this source's component macros so offsets sees them without re-reading the file.
|
--- (internal) Extend the canonical `corpus.word_counts` with this source's component macros so offsets sees them without re-reading the file.
|
||||||
-- @param ctx PassCtx
|
--- First declaration wins: a later caller's count is dropped (the existing entry from the first source is preserved).
|
||||||
-- @param components Component[]
|
--- @param corpus table -- the canonical corpus
|
||||||
-- @param counts table<string, integer> -- precomputed word counts (from count_all_components)
|
--- @param components Component[]
|
||||||
local function update_shared_word_counts(ctx, components, counts)
|
--- @param counts table<string, integer> -- precomputed word counts (from count_all_components)
|
||||||
local wc = ctx.shared.word_counts
|
local function update_canonical_word_counts(corpus, components, counts)
|
||||||
|
local wc = corpus.word_counts
|
||||||
for _, c in ipairs(components) do
|
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
|
||||||
end
|
end
|
||||||
|
|
||||||
--- @class ComponentDef
|
--- @class ComponentDef
|
||||||
--- @field name string -- bare name (without ac_/mac_ prefix)
|
--- @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 line integer -- definition source line (line of `MipsAtomComp_(ac_X)` / `MipsAtomComp_Proc_(ac_X, ...)`)
|
||||||
--- @field path string -- absolute source path of the definition
|
--- @field path string -- absolute source path of the definition
|
||||||
--- @field kind string -- "comp_bare" | "comp_proc"
|
--- @field kind string -- "comp_bare" | "comp_proc"
|
||||||
|
|
||||||
--- (internal) Extend `ctx.shared.components` with this source's components-by-name map so downstream passes
|
--- (internal) Populate the canonical `corpus.components` projection with this source's components-by-name map.
|
||||||
--- (atoms_source_map, dwarf_injection) can resolve `mac_X(...)` invocations back to their component definition file:line.
|
--- First declaration wins; later declarations of the same bare name are dropped and recorded as a collision via `corpus.collisions` (kind = "component").
|
||||||
--- provenance emission uses this to attribute each emitted `.word` to either a component macro or the enclosing atom body.
|
--- The pass does NOT write to `ctx.shared.components` (ownership follows the canonical contract).
|
||||||
-- @param ctx PassCtx
|
--- @param corpus table -- the canonical corpus
|
||||||
-- @param src SourceFile
|
--- @param src SourceFile
|
||||||
-- @param components Component[]
|
--- @param components Component[]
|
||||||
local function update_shared_components(ctx, src, components)
|
local function update_canonical_components(corpus, src, components)
|
||||||
ctx.shared.components = ctx.shared.components or {}
|
|
||||||
local rel_path = src.path:gsub("\\", "/")
|
local rel_path = src.path:gsub("\\", "/")
|
||||||
for _, c in ipairs(components) do
|
for _, c in ipairs(components) do
|
||||||
-- Keyed by bare name (e.g. `yield`, `load_tri_indices`).
|
-- 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.
|
-- The atoms_source_map pass looks up components by bare name from the canonical corpus;
|
||||||
ctx.shared.components[c.name] = {
|
-- `mac_` prefix lives at the call-site identifier and is stripped before lookup.
|
||||||
name = c.name,
|
if corpus.components[c.name] == nil then
|
||||||
line = c.line,
|
corpus.components[c.name] = {
|
||||||
path = rel_path,
|
name = c.name,
|
||||||
kind = c.kind or "comp_bare",
|
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
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -622,24 +668,42 @@ function M.run(ctx)
|
|||||||
local errors = {}
|
local errors = {}
|
||||||
local warnings = {}
|
local warnings = {}
|
||||||
|
|
||||||
-- Initialize shared component map.
|
-- Canonical-corpus ownership gate.
|
||||||
-- The atoms_source_map and dwarf_injection passes consume `ctx.shared.components` to resolve `mac_X(...)`
|
local corpus = ctx.shared and ctx.shared.corpus
|
||||||
-- invocations back to the component's definition file:line.
|
if type(corpus) ~= "table" then
|
||||||
ctx.shared.components = ctx.shared.components or {}
|
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
|
-- project_components reads from src.scan + does backward lookups on src.text
|
||||||
local components = project_components(src.text, src.scan)
|
local components = project_components(src.text, src.scan)
|
||||||
if #components > 0 then
|
if #components > 0 then
|
||||||
-- Compute word counts for ALL components once (was: rebuilt per call inside the helpers).
|
-- Compute all component word counts once per source.
|
||||||
local counts = count_all_components(components, ctx.shared.word_counts)
|
-- 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)
|
local macs_path = emit_component_macros_h(ctx, src, components, counts)
|
||||||
if macs_path then
|
if macs_path then
|
||||||
outputs[#outputs + 1] = { macs_h = macs_path }
|
outputs[#outputs + 1] = { macs_h = macs_path }
|
||||||
update_shared_word_counts(ctx, components, counts)
|
-- Populate the canonical projections AFTER disk emission (so the byte-identical `.macs.h` contract is preserved before any current-count mutation).
|
||||||
-- share component definitions with downstream passes.
|
update_canonical_word_counts(corpus, components, counts)
|
||||||
-- `mac_X(...)` invocations in atom bodies resolve back to (path, line) via this map.
|
update_canonical_components(corpus, src, components)
|
||||||
update_shared_components(ctx, src, components)
|
update_canonical_component_body_index(corpus, src, components, src.scan)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
+164
-237
@@ -20,8 +20,8 @@
|
|||||||
--- Splice step runs from PowerShell — no Lua subprocess; no cmd /c parsing issues.
|
--- 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.)
|
--- 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.
|
--- Result: source stepping follows atom-body lines, and wave-context registers appear as atom-scoped locals.
|
||||||
--- Native VSCode UX (gutter arrow + highlighted line + Run to Cursor + conditional BPs by source line + per-atom 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.
|
--- No VSCode plugin, no Python, no pyelftools — pure Lua + objcopy.
|
||||||
---
|
---
|
||||||
--- **Conventions:** tabs (1/level), EmmyLua annotations, Lua 5.3 compatible.
|
--- **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).
|
-- 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
|
-- 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")
|
local elf_dwarf = require("elf_dwarf")
|
||||||
|
|
||||||
-- word-counting helper shared with passes/atoms_source_map.lua.
|
-- Per-word body lines come from the canonical `atom.paths` projection.
|
||||||
-- 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
|
|
||||||
|
|
||||||
local lfs = require("lfs")
|
local lfs = require("lfs")
|
||||||
|
|
||||||
-- File-scope aliases to elf_dwarf helpers; the canonical implementations live in scripts/elf_dwarf.lua.
|
-- 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_uleb128_at = elf_dwarf.read_uleb128_at
|
||||||
local read_sleb128_at = elf_dwarf.read_sleb128_at
|
local read_sleb128_at = elf_dwarf.read_sleb128_at
|
||||||
local find_abbrev_table_end = elf_dwarf.find_abbrev_table_end
|
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).
|
-- 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_CU = 0x64 -- 100: DW_TAG_compile_unit
|
||||||
local ABBREV_SUBPROGRAM = 0x65 -- 101: DW_TAG_subprogram
|
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_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_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)
|
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_location = 0x02
|
||||||
local DW_AT_comp_dir = 0x1B
|
local DW_AT_comp_dir = 0x1B
|
||||||
local DW_AT_byte_size = 0x0B
|
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_data_member_location = 0x38
|
||||||
local DW_AT_type = 0x49
|
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
|
local DW_AT_external = 0x3F -- marks a variable/function as externally visible
|
||||||
-- Inlined_subroutine + abstract_origin attributes.
|
-- Inlined_subroutine + abstract_origin attributes.
|
||||||
local DW_AT_abstract_origin = 0x31
|
local DW_AT_abstract_origin = 0x31
|
||||||
@@ -341,7 +337,7 @@ end
|
|||||||
local DEFAULT_CU_NAME = "tape_atom_locals"
|
local DEFAULT_CU_NAME = "tape_atom_locals"
|
||||||
local DEFAULT_CU_COMP_DIR = "."
|
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.
|
-- Default basename if not provided via ctx.
|
||||||
local DEFAULT_BASENAME = "hello_gte"
|
local DEFAULT_BASENAME = "hello_gte"
|
||||||
@@ -367,11 +363,12 @@ end
|
|||||||
--- Consume the per-source scanner associations without naming any atom or component in production.
|
--- 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
|
--- 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.
|
--- 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}}
|
--- @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 = {} }
|
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
|
local scan_skip = src.scan and src.scan.skip_over
|
||||||
if scan_skip then
|
if scan_skip then
|
||||||
for atom_name, association in pairs(scan_skip.atoms or {}) do
|
for atom_name, association in pairs(scan_skip.atoms or {}) do
|
||||||
@@ -396,53 +393,50 @@ local function collect_skip_over(ctx)
|
|||||||
return skip_over
|
return skip_over
|
||||||
end
|
end
|
||||||
|
|
||||||
--- Merge the per-source scanner registries (register_alias_registry, type_name_registry, atom_views)
|
--- Project the canonical corpus registries into the shape the section builders expect.
|
||||||
--- into a single set of tables that downstream consumers can read from without re-iterating ctx.sources.
|
--- 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.
|
--- 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).
|
--- 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).
|
--- 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.
|
--- 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 -- {
|
--- @return table -- {
|
||||||
--- register_alias_registry = {[R_Name] = AliasEntry},
|
--- register_alias_registry = {[R_Name] = AliasEntry},
|
||||||
--- type_name_registry = {[T] = TypeEntry},
|
--- type_name_registry = {[T] = TypeEntry},
|
||||||
--- atom_views = {[atom_name] = AtomViewEntry},
|
--- atom_views = {[atom_name] = AtomViewEntry},
|
||||||
--- }
|
--- }
|
||||||
local function collect_per_source_registries(ctx)
|
local function collect_per_source_registries(corpus)
|
||||||
local merged = {
|
-- The corpus already holds the merged registries; reference them directly.
|
||||||
register_alias_registry = {},
|
-- No per-source iteration is needed because `passes.scan_source.lua` has already folded every per-source scan into the canonical tables.
|
||||||
type_name_registry = {},
|
-- `atom_infos` is preserved byte-for-byte with no filtering; consumers consult `corpus.atoms_by_name`
|
||||||
atom_views = {},
|
-- 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, ...}
|
-- Per-atom atom_ctx declarations: atom_name -> {rbind_atom, ...}
|
||||||
-- (populated by scan_source from `atom_ctx(<atom_name>)` sub-calls inside `atom_info`)
|
-- (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, ...}}
|
-- 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)
|
-- (populated by scan_source from `atom_phase(<label>)` sub-calls inside `atom_info`; cross-source merged)
|
||||||
atom_phases = {},
|
atom_phases = (corpus and corpus.atom_phases) or {},
|
||||||
-- Per-source already-resolved atom_infos (used by the precedence chain's ctx/phase steps)
|
-- Corpus-wide atom_infos list, byte-for-byte.
|
||||||
atom_infos = {},
|
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
|
end
|
||||||
|
|
||||||
--- Render deterministic debugger skip commands. Ordering is stable by category:
|
--- Render deterministic debugger skip commands.
|
||||||
--- exact atom symbols first (lexicographic), then exact component function names (lexicographic full command).
|
--- 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.
|
--- 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.
|
--- 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
|
--- @param atom_table table[] -- nm/source-map cross-reference; names are actual ELF symbols
|
||||||
--- @return string
|
--- @return string
|
||||||
local function build_gdbinit(skip_over, atom_table)
|
local function build_gdbinit(skip_over, atom_table)
|
||||||
@@ -474,7 +468,7 @@ end
|
|||||||
-- LEB128 encoders
|
-- 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).
|
-- 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.
|
-- 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.
|
--- Build the atom table the section builders consume.
|
||||||
--- Cross-references nm symbols with source-map.txt entries; sorted by addr.
|
--- Cross-references nm symbols with `corpus.atoms_by_name` and derives word rows + format-1 outermost invocation rows from `atom.paths`.
|
||||||
--- 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}`.
|
|
||||||
---
|
---
|
||||||
--- `body_lines` is the per-word source line within the macro body
|
--- The atom table is built entirely from in-memory state — disk source-map and provenance text artifacts are NOT consulted.
|
||||||
--- (lottes_tape.h:N where N is the actual line of this `.word` in the macro expansion).
|
--- Those artifacts are diagnostic outputs, not semantic inputs; the DWARF injection pass must remain correct regardless of their on-disk content.
|
||||||
--- 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 data is computed by walking the component's pre-tokenized body in `ctx.sources[i].scan.atoms[j]`
|
--- The result shape (one entry per ELF symbol matched against the corpus):
|
||||||
--- (the MipsAtomComp_/MipsAtomComp_Proc_ declaration).
|
--- `{name, addr, size_bytes, words, entries, invocations, skip_over?}`
|
||||||
--- Atom labels (`atom_label(...)`) emit 0 `.word`s and are ignored (matching `passes/atoms_source_map.lua :: is_marker_token` + `count_marker_rest`).
|
--- where:
|
||||||
--- @param ctx DwarfInjectionCtx
|
--- * `entries[i].pos` — 0-based `.word` position (matches the source-map format-1 row layout; downstream DWARF builders compare against this).
|
||||||
--- @param skip_over table -- {atoms = {[symbol] = association}, components = {[file|name] = association}}
|
--- * `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?}
|
--- @return table[] -- list of {name, addr, size_bytes, words, entries, invocations, skip_over?}
|
||||||
local function build_atom_table(ctx, skip_over)
|
local function build_atom_table(corpus, addrs, skip_over)
|
||||||
local basename = ctx.basename or DEFAULT_BASENAME
|
local atoms_by_name = corpus.atoms_by_name or {}
|
||||||
-- 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
|
|
||||||
|
|
||||||
-- Read nm + merge all source-map files.
|
-- Cross-ref: keep only the atoms that exist in BOTH the nm symbol table AND the canonical corpus projection.
|
||||||
local addrs = elf_dwarf.read_nm(ctx.flags.elf_path)
|
-- Address-ascending sort + lexical Stable tie-breaker: declaration order, then symbol address.
|
||||||
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.
|
|
||||||
local out = {}
|
local out = {}
|
||||||
for name, info in pairs(addrs) do
|
for name, info in pairs(addrs) do
|
||||||
local sm = merged[name]
|
local atom_record = atoms_by_name[name]
|
||||||
if sm then
|
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 = {
|
local atom = {
|
||||||
name = name,
|
name = name,
|
||||||
addr = info[1],
|
addr = info[1],
|
||||||
size_bytes = info[2],
|
size_bytes = info[2],
|
||||||
words = sm.total,
|
words = #word_events,
|
||||||
entries = sm.words,
|
entries = entries,
|
||||||
skip_over = skip_over.atoms[name] ~= nil,
|
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.
|
-- Group consecutive `word_events` rows whose outermost invocation
|
||||||
-- Two consecutive rows with the same (comp_name, call_file, call_line, comp_file, comp_line) are part of the same invocation.
|
-- is the SAME format-1 invocation into a single `atom.invocations`
|
||||||
local prov_data = prov_merged[name]
|
-- entry. Keep entries grouped by outermost invocation
|
||||||
if prov_data and prov_data.words then
|
-- (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 invocations = {}
|
||||||
local cur_inv = nil
|
local cur_inv = nil
|
||||||
for _, w in ipairs(prov_data.words) do
|
for _, ev in ipairs(word_events) do
|
||||||
if w.comp_name then
|
local outer_id = ev.outermost_invocation_id
|
||||||
local inv_key = w.comp_name .. "|" .. w.call_file .. "|" .. w.call_line .. "|" .. w.comp_file .. "|" .. w.comp_line
|
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
|
if cur_inv and cur_inv.key == inv_key then
|
||||||
-- Same invocation as the previous word — extend its range.
|
cur_inv.end_pos = ev_pos
|
||||||
cur_inv.end_pos = w.pos
|
cur_inv.body_lines[#cur_inv.body_lines + 1] = ev.body_line or 0
|
||||||
else
|
else
|
||||||
-- New invocation: flush the previous one and start fresh.
|
|
||||||
if cur_inv then invocations[#invocations + 1] = cur_inv end
|
if cur_inv then invocations[#invocations + 1] = cur_inv end
|
||||||
cur_inv = {
|
cur_inv = {
|
||||||
key = inv_key,
|
key = inv_key,
|
||||||
comp_name = w.comp_name,
|
comp_name = outer_inv.component_name,
|
||||||
call_file = w.call_file,
|
call_file = outer_inv.call_path or "",
|
||||||
call_line = w.call_line,
|
call_line = outer_inv.call_line or 0,
|
||||||
comp_file = w.comp_file,
|
comp_file = outer_inv.def_path or "",
|
||||||
comp_line = w.comp_line,
|
comp_line = outer_inv.def_line or 0,
|
||||||
start_pos = w.pos,
|
start_pos = ev_pos,
|
||||||
end_pos = w.pos,
|
end_pos = ev_pos,
|
||||||
-- component_skip_key: case-insensitive Windows path + "\0" separator + exact component-name.
|
skip_over = skip_over.components[normalize_debug_path(outer_inv.def_path or ""):lower()
|
||||||
-- (Inlined from `component_skip_key`; the lookup is in skip_over.components keyed by the result.)
|
.. "\0" .. outer_inv.component_name] ~= nil,
|
||||||
skip_over = skip_over.components[normalize_debug_path(w.comp_file):lower() .. "\0" .. w.comp_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
|
end
|
||||||
else
|
else
|
||||||
-- RAW row: flush the current invocation.
|
-- 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).
|
-- 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).
|
-- 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}).
|
-- 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.
|
--- 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
|
--- 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).
|
--- 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 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 binds_name string -- expected Binds_X name (skip pairs with mismatching binds)
|
||||||
--- @param registries table -- merged registries from collect_per_source_registries
|
--- @param registries table -- merged registries from collect_per_source_registries
|
||||||
--- @return table[] -- list of {reg = <MIPS index>, field = <field name>}
|
--- @return table[] -- list of {reg = <MIPS index>, field = <field name>}
|
||||||
local function parse_body_load_pairs(body_tokens, binds_name, registries)
|
local function parse_body_load_pairs(body_tokens, binds_name, registries)
|
||||||
local pairs = {}
|
local pairs = {}
|
||||||
@@ -938,9 +863,7 @@ end
|
|||||||
|
|
||||||
--- Collect every rbind atom + the matching Binds_X struct + (reg, field) pairs.
|
--- Collect every rbind atom + the matching Binds_X struct + (reg, field) pairs.
|
||||||
---
|
---
|
||||||
--- Inputs come from the dep-closed `scan-source` pass:
|
--- Inputs come from the dep-closed `scan-source` pass (the per-source `src.scan` payload is preserved on each `corpus.source_order` entry).
|
||||||
--- 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}
|
|
||||||
---
|
---
|
||||||
--- Returns:
|
--- Returns:
|
||||||
--- rbind_atoms = {[atom_name] = {binds, fields, regs, byte_size, info_line}}
|
--- 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 `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 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.
|
--- per-source `scan.binds[i].fields` already carries the typed-field record after the scan-source generalization.
|
||||||
--- @param ctx DwarfInjectionCtx
|
--- @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 atom_table table[] -- the cross-ref'd atom table from build_atom_table
|
||||||
--- @param registries table -- merged registries from collect_per_source_registries
|
--- @param registries table -- merged registries from collect_per_source_registries
|
||||||
--- @return table, table -- (rbind_atoms, rbind_structs)
|
--- @return table, table -- (rbind_atoms, rbind_structs)
|
||||||
local function parse_rbind_atoms(ctx, atom_table, registries)
|
local function parse_rbind_atoms(corpus, atom_table, registries)
|
||||||
registries = registries or {}
|
registries = registries or {}
|
||||||
local rbind_atoms = {}
|
local rbind_atoms = {}
|
||||||
local rbind_structs = {}
|
local rbind_structs = {}
|
||||||
|
|
||||||
-- Index binds by struct name; consume `scan.binds[i].fields` directly (no body-text re-walk).
|
-- 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.
|
-- so this pass can build the rbind_structs entry without re-parsing.
|
||||||
local binds_by_name = {}
|
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
|
local scan = src.scan
|
||||||
if scan then
|
if scan then
|
||||||
for _, b in ipairs(scan.binds or {}) do
|
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.
|
-- Walk every atom_info; if `binds` is set, find the atom body_tokens + parse load_word pairs.
|
||||||
local body_tokens_by_atom = {}
|
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
|
local scan = src.scan
|
||||||
if scan then
|
if scan then
|
||||||
for _, atom in ipairs(scan.atoms or {}) do
|
for _, atom in ipairs(scan.atoms or {}) do
|
||||||
@@ -995,7 +918,7 @@ local function parse_rbind_atoms(ctx, atom_table, registries)
|
|||||||
end
|
end
|
||||||
|
|
||||||
local ai_by_atom = {}
|
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
|
local scan = src.scan
|
||||||
if scan then
|
if scan then
|
||||||
for _, ai in ipairs(scan.atom_infos or {}) do
|
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
|
if #pairs > 0 then
|
||||||
rbind_atoms[atom_name] = {
|
rbind_atoms[atom_name] = {
|
||||||
binds = ai.binds,
|
binds = ai.binds,
|
||||||
fields = struct.fields, -- {name, offset} from scan.binds
|
fields = struct.fields, -- {name, offset} from scan.binds
|
||||||
bytes = struct.bytes,
|
bytes = struct.bytes,
|
||||||
regs = pairs, -- ordered list of {reg, field}
|
regs = pairs, -- ordered list of {reg, field}
|
||||||
info_line = ai.info_line,
|
info_line = ai.info_line,
|
||||||
}
|
}
|
||||||
table.insert(struct.atom_names, atom_name)
|
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
|
--- 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 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.
|
--- 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.
|
--- 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.
|
--- 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.
|
--- 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}
|
--- @param atom_table table -- list of {name, addr, size_bytes, words, entries}
|
||||||
--- @return string
|
--- @return string
|
||||||
local function build_dwarf_line_section(existing, atom_table)
|
local function build_dwarf_line_section(existing, atom_table)
|
||||||
@@ -1094,7 +1017,7 @@ end
|
|||||||
--- segment_size (1 byte) -- = 0
|
--- segment_size (1 byte) -- = 0
|
||||||
--- [entries...] -- address(4) + length(4) per entry
|
--- [entries...] -- address(4) + length(4) per entry
|
||||||
--- terminator -- address=0 + length=0 (8 zero bytes)
|
--- terminator -- address=0 + length=0 (8 zero bytes)
|
||||||
--- @param existing string
|
--- @param existing string
|
||||||
--- @param atom_table table
|
--- @param atom_table table
|
||||||
--- @return string
|
--- @return string
|
||||||
local function build_dwarf_aranges_section(existing, atom_table)
|
local function build_dwarf_aranges_section(existing, atom_table)
|
||||||
@@ -1132,7 +1055,7 @@ local function build_dwarf_aranges_section(existing, atom_table)
|
|||||||
return existing
|
return existing
|
||||||
end
|
end
|
||||||
|
|
||||||
local unit_start = i
|
local unit_start = i
|
||||||
local unit_end_excl = i + 4 + ul
|
local unit_end_excl = i + 4 + ul
|
||||||
is_last_unit = (unit_end_excl == #existing)
|
is_last_unit = (unit_end_excl == #existing)
|
||||||
|
|
||||||
@@ -1451,7 +1374,7 @@ local function build_new_abbrev()
|
|||||||
|
|
||||||
-- Component step-into abstract + inline DIE abbreviations.
|
-- Component step-into abstract + inline DIE abbreviations.
|
||||||
local DW_INL_declared_inlined = 0x03 -- DWARF5 §3.33.3: "this subroutine was declared inline"
|
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.
|
-- 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.
|
-- 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
|
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
|
--- 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).
|
--- 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.
|
--- Insert the DIEs as children of the main compilation unit.
|
||||||
--- 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.
|
--- 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;
|
--- 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).
|
--- 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
|
--- DW_AT_type = ref4 → structure_type DIE
|
||||||
---
|
---
|
||||||
--- **DOES NOT** emit the final 0 byte (root terminator).
|
--- **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).
|
--- **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.
|
--- 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(4)) -- DW_FORM_data1 (DW_AT_byte_size)
|
||||||
emit(string.char(DW_ATE_unsigned)) -- DW_FORM_data1 (DW_AT_encoding)
|
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 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
|
local function next_offset() return S.next_offset end
|
||||||
|
|
||||||
-- Typed local views.
|
-- 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.
|
-- 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 *)`
|
-- 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).
|
-- 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
|
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
|
type_chain_offsets["U4|1"] = u4_chain_offset
|
||||||
|
|
||||||
-- 2) Emit one DW_TAG_structure_type per unique Binds_X.
|
-- 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
|
end
|
||||||
|
|
||||||
-- 4) Emit per-atom DW_TAG_subprograms (children of main CU).
|
-- 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.
|
-- 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.
|
-- 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
|
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.
|
-- 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.
|
-- 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,
|
-- 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.
|
-- reg_to_field_phase, atom_view_phase_fields, field_type_by_name, reg_to_field, alias, type_chain_offsets.
|
||||||
local PRECEDENCE_STEPS = {
|
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)
|
emit(rr_name .. "\0") -- DW_FORM_string (DW_AT_name)
|
||||||
-- DW_FORM_exprloc: ULEB byte count + DW_OP_regN byte.
|
-- DW_FORM_exprloc: ULEB byte count + DW_OP_regN byte.
|
||||||
-- DW_OP_reg0..reg31 occupy opcodes 0x50..0x6f; DW_OP_reg15 is 0x5f.
|
-- 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.
|
||||||
-- 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).
|
||||||
-- 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).
|
|
||||||
-- `alias_code` is the MIPS GPR index (0..31) from the merged register_alias_registry.
|
-- `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)
|
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).
|
-- 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.
|
-- 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 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.
|
-- (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
|
if atom.rbind then
|
||||||
local binds_name = atom.rbind.binds
|
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.
|
-- Per-component invocation inlined_subroutine instances.
|
||||||
-- Each invocation covers a contiguous .word range [start_pos, end_pos] within the atom.
|
-- 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.
|
-- 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;
|
-- Resolve `inv.call_file` to the line-unit file index;
|
||||||
-- that lost the call-site attribution for any invocation whose call site was NOT the atom's source file).
|
-- this preserves call-site attribution across source files.
|
||||||
if atom.invocations and not atom.skip_over then
|
if atom.invocations and not atom.skip_over then
|
||||||
for _, inv in ipairs(atom.invocations) do
|
for _, inv in ipairs(atom.invocations) do
|
||||||
local inv_low = atom.addr + inv.start_pos * MIPS_BYTES_PER_WORD
|
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)
|
emit(string.char(DIE_CHILDREN_TERMINATOR)) -- end of subprogram's children (DWARF5 §7.5.3)
|
||||||
end
|
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)
|
return table.concat(S.bytes)
|
||||||
end
|
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).
|
--- 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
|
--- @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)
|
--- @return string, integer -- (new_abbrev_bytes, offset_where_duplicate_table_starts = #existing)
|
||||||
local function build_debug_abbrev_section(existing, main_abbrev_offset)
|
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
|
end
|
||||||
|
|
||||||
--- Build the new .debug_str: existing strings + new strings appended.
|
--- 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 atom_table table[]
|
||||||
--- @param registries table -- merged registries from collect_per_source_registries
|
--- @param registries table -- merged registries from collect_per_source_registries
|
||||||
--- @return string, integer, table -- (new_str_bytes, new_strings_offset, string_map)
|
--- @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.
|
-- 4) Splice. All offsets below are 0-based; existing:sub is 1-indexed inclusive.
|
||||||
-- Byte ranges (0-based, 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 + 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 + 8 .. + 11] debug_abbrev_offset (PATCHED)
|
||||||
-- [main_cu_start + 12 .. main_cu_end_excl - 2] existing DIE bytes (verbatim)
|
-- [main_cu_start + 12 .. main_cu_end_excl - 2] existing DIE bytes, unchanged
|
||||||
-- [main_cu_end_excl - 1] root children-terminator (verbatim 0)
|
-- [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 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
|
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
|
return existing:sub(1, main_cu_start) -- crt CU
|
||||||
.. new_unit_length_bytes -- patched unit_length (4 bytes)
|
.. 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)
|
.. 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
|
.. 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
|
end
|
||||||
|
|
||||||
--- Build the .debug_loc: just a terminator.
|
--- 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;
|
--- 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).
|
--- 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.
|
--- 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 function build_debug_loc_section() return string.char(0x00) end
|
||||||
|
|
||||||
local SECTION_BUILDERS = {
|
local SECTION_BUILDERS = {
|
||||||
@@ -2351,14 +2271,21 @@ function M.run(ctx)
|
|||||||
-- then build the atom/provenance table with those generic selections.
|
-- 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
|
-- 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.
|
-- 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)
|
-- `corpus` is the sole canonical source projection;
|
||||||
local registries = collect_per_source_registries(ctx)
|
-- the sole source of truth; no `ctx.sources` / `ctx.by_dir` aliases).
|
||||||
local atom_table = build_atom_table(ctx, skip_over)
|
local corpus = (ctx.shared and ctx.shared.corpus) or {}
|
||||||
io.stderr:write(string.format("[dwarf_injection] matched %d atoms between nm + source-map\n", #atom_table))
|
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).
|
-- 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.
|
-- 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
|
local rbind_count = 0
|
||||||
for _ in pairs(_rbind_atoms) do rbind_count = rbind_count + 1 end
|
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",
|
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.
|
-- 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:
|
-- Build order:
|
||||||
-- 0. Validate .debug_info layout (crT CU + DWARF5 main CU + final 0 root terminator).
|
-- 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).
|
-- 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;
|
-- 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).
|
-- preserves all original DIE bytes; does NOT append a synthetic CU).
|
||||||
@@ -2445,7 +2372,7 @@ function M.run(ctx)
|
|||||||
return { outputs = {}, errors = {}, warnings = {} }
|
return { outputs = {}, errors = {}, warnings = {} }
|
||||||
end
|
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.
|
-- tests drive the real emission and offset computation paths.
|
||||||
M.compute_loclists_offsets_for_test = compute_loclists_offsets
|
M.compute_loclists_offsets_for_test = compute_loclists_offsets
|
||||||
M.build_debug_loclists_section_for_test = build_debug_loclists_section
|
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).
|
-- 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.
|
-- 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 _bootstrap_dir = debug.getinfo(1, "S").source:match("^@?(.*[/\\])") or "./"
|
||||||
local duffle = dofile(_bootstrap_dir .. "../duffle_paths.lua")
|
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
|
|
||||||
|
|
||||||
-- ════════════════════════════════════════════════════════════════════════════
|
-- ════════════════════════════════════════════════════════════════════════════
|
||||||
-- Constants
|
-- 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).
|
-- Offset macro/enum naming prefixes (the emitted header uses these).
|
||||||
local OFFSET_MACRO_PREFIX = "_atom_offset_"
|
local OFFSET_MACRO_PREFIX = "_atom_offset_"
|
||||||
local OFFSET_ENUM_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)
|
--- @field scan table -- pre-scanned SourceScan payload (from duffle.scan_source)
|
||||||
|
|
||||||
--- @class PassCtx
|
--- @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 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 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 dry_run boolean -- if true, compute but don't write
|
||||||
--- @field verbose boolean -- log diagnostic info
|
|
||||||
|
|
||||||
--- @class PassResult
|
--- @class PassResult
|
||||||
--- @field outputs table[] -- {kind=, path=} entries describing emit files
|
--- @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
|
--- @field warnings table[] -- {line=, msg=} entries; build-succeeds
|
||||||
|
|
||||||
--- @class BranchOffset
|
--- @class BranchOffset
|
||||||
--- @field tag string -- the marker tag (e.g. "F" in `atom_offset(F, T)`)
|
--- @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 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 branch_word integer -- branch word position within the atom body
|
||||||
--- @field offset integer -- computed `target_word - branch_word - 1`
|
--- @field offset integer -- computed `target_word - branch_word - 1`
|
||||||
|
|
||||||
--- @class AtomData
|
--- @class AtomData
|
||||||
--- @field name string -- atom name
|
--- @field name string -- atom name
|
||||||
@@ -80,169 +69,85 @@ local OFFSET_MACRO_COL = 44
|
|||||||
--- @field offsets BranchOffset[] -- per-branch offset list
|
--- @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.
|
-- MARKER_PROJECTORS is the marker-kind data table.
|
||||||
-- Returns (args, after_paren) where `after_paren` is the position just past the closing `)`, or nil if `token` did not start with `(`.
|
-- The emission-model pass already records marker word positions;
|
||||||
-- @param token string
|
-- this pass only projects those records into the label/branch lookup shape needed by offset computation.
|
||||||
-- @param after_ident integer
|
local MARKER_PROJECTORS = {
|
||||||
-- @return string[], integer|nil
|
label = function(state, marker)
|
||||||
local function extract_ident_args(token, after_ident)
|
state.labels[marker.name] = marker.word_index
|
||||||
local arg_start = duffle.skip_ws_and_cmt(token, after_ident)
|
end,
|
||||||
if token:sub(arg_start, arg_start) ~= "(" then return {}, nil end
|
offset = function(state, marker)
|
||||||
local inner, after_paren = duffle.read_parens(token, arg_start)
|
state.branches[#state.branches + 1] = {
|
||||||
-- scan: <marker>(<args>)
|
tag = marker.name,
|
||||||
|
target = marker.target,
|
||||||
local args = {}
|
branch_word = marker.word_index,
|
||||||
local pos = 1
|
}
|
||||||
local inner_len = #inner
|
end,
|
||||||
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,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
--- Scan a single token for atom_label/atom_offset markers, walking through balanced groups transparently (so nested calls are found).
|
--- Project canonical marker records into the two lookup tables used by the offset renderer.
|
||||||
--- @param token string
|
--- No source text, body text, or body token is inspected.
|
||||||
--- @param at_pos integer -- the branch-free word position of this token in the body
|
--- @param markers table[] -- atom.paths.markers
|
||||||
--- @param labels table<string, integer>
|
--- @return table<string, integer>, table[]
|
||||||
--- @param branches table[]
|
local function project_markers(markers)
|
||||||
local function scan_for_atom_markers(token, at_pos, labels, branches)
|
local state = { labels = {}, branches = {} }
|
||||||
local pos = 1
|
for _, marker in ipairs(markers or {}) do
|
||||||
local tok_len = #token
|
local project = MARKER_PROJECTORS[marker.kind]
|
||||||
while pos <= tok_len do
|
if project then project(state, marker) end
|
||||||
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
|
|
||||||
end
|
end
|
||||||
end
|
return state.labels, state.branches
|
||||||
|
|
||||||
--- 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
|
|
||||||
end
|
end
|
||||||
|
|
||||||
-- ════════════════════════════════════════════════════════════════════════════
|
-- ════════════════════════════════════════════════════════════════════════════
|
||||||
-- Offset computation + header generation
|
-- Offset computation + header generation
|
||||||
-- ════════════════════════════════════════════════════════════════════════════
|
-- ════════════════════════════════════════════════════════════════════════════
|
||||||
|
|
||||||
-- Compute branch offsets as `target_word - branch_word - 1` (the standard MIPS branch-immediate encoding).
|
--- Compute branch offsets as `target_word - branch_word - 1` (the standard MIPS branch-immediate encoding).
|
||||||
-- @param labels table<string, integer>
|
--- @param labels table<string, integer>
|
||||||
-- @param branches table[]
|
--- @param branches table[]
|
||||||
-- @return BranchOffset[]
|
--- @return BranchOffset[]
|
||||||
local function compute_offsets(labels, branches)
|
local function compute_offsets(labels, branches)
|
||||||
local results = {}
|
local results = {}
|
||||||
for _, br in ipairs(branches) do
|
for _, br in ipairs(branches) do
|
||||||
local target = labels[br.target]
|
local target = labels[br.target]
|
||||||
if not target then
|
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
|
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
|
end
|
||||||
return results
|
return results
|
||||||
end
|
end
|
||||||
|
|
||||||
-- Right-pad `s` with spaces to width `w`. If `s` is already `w` or wider, no padding is added.
|
--- Right-pad `s` with spaces to width `w`. If `s` is already `w` or wider, no padding is added.
|
||||||
-- @param s string
|
--- @param s string
|
||||||
-- @param w integer
|
--- @param w integer
|
||||||
-- @return string
|
--- @return string
|
||||||
local function pad_right(s, w)
|
local function pad_right(s, w)
|
||||||
return s .. string.rep(" ", math.max(0, w - #s))
|
return s .. string.rep(" ", math.max(0, w - #s))
|
||||||
end
|
end
|
||||||
|
|
||||||
-- (internal) Build a constant-table entry `{macro_name, enum_name, value}` from a BranchOffset.
|
--- (internal) Build a constant-table entry `{macro_name, enum_name, value}` from a BranchOffset.
|
||||||
-- @param r BranchOffset
|
--- @param bo BranchOffset
|
||||||
-- @return table
|
--- @return table
|
||||||
local function make_offset_const(r)
|
local function make_offset_const(bo)
|
||||||
return {
|
return {
|
||||||
macro_name = OFFSET_MACRO_PREFIX .. r.tag .. "_" .. r.target,
|
macro_name = OFFSET_MACRO_PREFIX .. bo.tag .. "_" .. bo.target,
|
||||||
enum_name = OFFSET_ENUM_PREFIX .. r.tag .. "_" .. r.target,
|
enum_name = OFFSET_ENUM_PREFIX .. bo.tag .. "_" .. bo.target,
|
||||||
value = r.offset,
|
value = bo.offset,
|
||||||
}
|
}
|
||||||
end
|
end
|
||||||
|
|
||||||
-- (internal) Emit one atom's offset constants + enum into the lines buffer.
|
--- (internal) Emit one atom's offset constants + enum into the lines buffer.
|
||||||
-- @param add fun(s: string)
|
--- @param add fun(s: string)
|
||||||
-- @param atom AtomData
|
--- @param atom AtomData
|
||||||
local function emit_atom_offsets(add, atom)
|
local function emit_atom_offsets(add, atom)
|
||||||
if #atom.offsets == 0 then return end
|
if #atom.offsets == 0 then return end
|
||||||
add("// --- atom: " .. atom.name .. " (" .. atom.total_words .. " words) ---")
|
add("// --- atom: " .. atom.name .. " (" .. atom.total_words .. " words) ---")
|
||||||
@@ -263,10 +168,10 @@ local function emit_atom_offsets(add, atom)
|
|||||||
add("")
|
add("")
|
||||||
end
|
end
|
||||||
|
|
||||||
-- Generate the per-source .offsets.h header.
|
--- Generate the per-source .offsets.h header.
|
||||||
-- @param source_path string
|
--- @param source_path string
|
||||||
-- @param atoms_data AtomData[]
|
--- @param atoms_data AtomData[]
|
||||||
-- @return string
|
--- @return string
|
||||||
local function generate_header(source_path, atoms_data)
|
local function generate_header(source_path, atoms_data)
|
||||||
local basename = duffle.basename_no_ext(source_path)
|
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"
|
return table.concat(lines, "\n") .. "\n"
|
||||||
end
|
end
|
||||||
|
|
||||||
-- ════════════════════════════════════════════════════════════════════════════
|
|
||||||
-- M — module exports
|
|
||||||
-- ════════════════════════════════════════════════════════════════════════════
|
|
||||||
|
|
||||||
local M = {}
|
local M = {}
|
||||||
|
|
||||||
-- Project the pre-scanned SourceScan entries into the {name, body, body_tokens} shape this pass needs.
|
--- (internal) Process one source: render offsets from canonical atom paths.
|
||||||
-- MipsAtom_ entries have kind="atom"; MipsCode code_<name> entries have kind="raw_atom".
|
--- Returns the offsets_h path if a header was written, or nil.
|
||||||
-- `body_tokens` is set by scan-source on every `scan.atoms[i]` / `scan.raw_atoms[i]`; we carry it forward
|
--- @param ctx PassCtx
|
||||||
-- so `scan_atom_body` reads from the precomputed table directly (no per-atom tokenize_body fallback).
|
--- @param src SourceFile
|
||||||
-- @param scan table -- SourceScan from duffle.scan_source
|
--- @return string|nil -- the offsets_h path
|
||||||
-- @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
|
|
||||||
local function process_source(ctx, src)
|
local function process_source(ctx, src)
|
||||||
local atoms = project_atoms(src.scan)
|
|
||||||
if #atoms == 0 then return nil end
|
|
||||||
|
|
||||||
local atoms_data = {}
|
local atoms_data = {}
|
||||||
for _, atom in ipairs(atoms) do
|
local scan = src.scan or {}
|
||||||
local labels, branches, total = scan_atom_body(atom.body_tokens, ctx.shared.word_counts)
|
|
||||||
|
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] = {
|
atoms_data[#atoms_data + 1] = {
|
||||||
name = atom.name,
|
name = atom.raw_name or atom.name,
|
||||||
total_words = total,
|
total_words = #(paths.word_events or {}),
|
||||||
offsets = compute_offsets(labels, branches),
|
offsets = compute_offsets(labels, branches),
|
||||||
}
|
}
|
||||||
end
|
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"
|
local out_path = src.dir .. "/gen/" .. duffle.basename_no_ext(src.dir) .. ".offsets.h"
|
||||||
if not ctx.dry_run then
|
if not ctx.dry_run then
|
||||||
duffle.ensure_dir(duffle.dirname(out_path))
|
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
|
end
|
||||||
return out_path
|
return out_path
|
||||||
end
|
end
|
||||||
|
|
||||||
--- Run the offsets pass.
|
--- Run the offsets pass.
|
||||||
--- For each source, emits a per-module `<dir_basename>.offsets.h` containing `#define _atom_offset_F_T = N` constants
|
--- For each canonical source, emits a per-module `<dir_basename>.offsets.h`
|
||||||
--- for every `atom_offset(F, T)` reference in the source's atoms.
|
--- containing constants for every marker recorded in atom.paths.
|
||||||
--- @param ctx PassCtx
|
--- @param ctx PassCtx
|
||||||
--- @return PassResult
|
--- @return PassResult
|
||||||
function M.run(ctx)
|
function M.run(ctx)
|
||||||
@@ -348,7 +237,12 @@ function M.run(ctx)
|
|||||||
local errors = {}
|
local errors = {}
|
||||||
local warnings = {}
|
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)
|
local out_path = process_source(ctx, src)
|
||||||
if out_path then
|
if out_path then
|
||||||
outputs[#outputs + 1] = { offsets_h = out_path }
|
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)
|
-- 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.
|
--- 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
|
--- @param path string
|
||||||
-- @return string
|
--- @return string
|
||||||
local function source_basename(path)
|
local function source_basename(path)
|
||||||
return path:match(BASENAME_PATTERN) or path
|
return path:match(BASENAME_PATTERN) or path
|
||||||
end
|
end
|
||||||
|
|
||||||
-- (internal) Format a single annotation entry as one rendered line.
|
--- (internal) Format a single annotation entry as one rendered line.
|
||||||
-- @param a AnnotEntry
|
--- @param a AnnotEntry
|
||||||
-- @param src_name string
|
--- @param src_name string
|
||||||
-- @return string
|
--- @return string
|
||||||
local function format_annot_line(a, src_name)
|
local function format_annot_line(a, src_name)
|
||||||
if a.error then
|
if a.error then
|
||||||
return string.format(" ✗ line %d %s [ERROR: %s] [%s]", a.line, a.macro or "?", a.error, src_name)
|
return string.format(" ✗ line %d %s [ERROR: %s] [%s]", a.line, a.macro or "?", a.error, src_name)
|
||||||
end
|
end
|
||||||
local line = string.format(" ● line %d %s [%s]", a.line, a.name, src_name)
|
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.binds then line = line .. " binds=" .. a.binds end
|
||||||
if #a.reads > 0 then line = line .. " reads={" .. table.concat(a.reads, ",") .. "}" 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.writes > 0 then line = line .. " writes={" .. table.concat(a.writes, ",") .. "}" end
|
||||||
return line
|
return line
|
||||||
end
|
end
|
||||||
|
|
||||||
-- (internal) Tally totals across all results in a module.
|
--- (internal) Tally totals across all results in a module.
|
||||||
-- @param results AnnotationResult[]
|
--- @param results AnnotationResult[]
|
||||||
-- @return integer, integer, integer, integer, integer, integer
|
--- @return integer, integer, integer, integer, integer, integer
|
||||||
local function tally_module_totals(results)
|
local function tally_module_totals(results)
|
||||||
local total_atoms, total_annots, total_binds, total_macros = 0, 0, 0, 0
|
local total_atoms, total_annots, total_binds, total_macros = 0, 0, 0, 0
|
||||||
local total_errors, total_warnings = 0, 0
|
local total_errors, total_warnings = 0, 0
|
||||||
@@ -377,13 +377,13 @@ end
|
|||||||
-- Orchestration helpers
|
-- Orchestration helpers
|
||||||
-- ════════════════════════════════════════════════════════════════════════════
|
-- ════════════════════════════════════════════════════════════════════════════
|
||||||
|
|
||||||
-- (internal) Pull per-source validate() results from the annotation pass's stash.
|
--- (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`;
|
--- 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.
|
--- 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).
|
--- Returns the list of module results + the flat list of all results (for the project-wide summary).
|
||||||
-- @param ctx PassCtx
|
--- @param ctx PassCtx
|
||||||
-- @param dir_sources SourceFile[]
|
--- @param dir_sources SourceFile[]
|
||||||
-- @return AnnotationResult[], AnnotationResult[]
|
--- @return AnnotationResult[], AnnotationResult[]
|
||||||
local function lookup_module_results(ctx, dir_sources)
|
local function lookup_module_results(ctx, dir_sources)
|
||||||
local src_cache = (ctx.flags and ctx.flags._annot_source_results) or {}
|
local src_cache = (ctx.flags and ctx.flags._annot_source_results) or {}
|
||||||
local module_results = {}
|
local module_results = {}
|
||||||
@@ -399,9 +399,9 @@ local function lookup_module_results(ctx, dir_sources)
|
|||||||
return module_results, all_results
|
return module_results, all_results
|
||||||
end
|
end
|
||||||
|
|
||||||
-- (internal) Does this module's results contain anything worth emitting?
|
--- (internal) Does this module's results contain anything worth emitting?
|
||||||
-- @param module_results AnnotationResult[]
|
--- @param module_results AnnotationResult[]
|
||||||
-- @return boolean
|
--- @return boolean
|
||||||
local function module_has_content(module_results)
|
local function module_has_content(module_results)
|
||||||
for _, r in ipairs(module_results) do
|
for _, r in ipairs(module_results) do
|
||||||
if #r.atoms > 0 or #r.annots > 0 or #r.binds > 0
|
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
|
return false
|
||||||
end
|
end
|
||||||
|
|
||||||
-- (internal) Log a debug message if `_G[DEBUG_FLAG]` is truthy.
|
--- (internal) Log a debug message if `_G[DEBUG_FLAG]` is truthy.
|
||||||
-- @param fmt string
|
--- @param fmt string
|
||||||
local function debug_log(fmt, ...)
|
local function debug_log(fmt, ...)
|
||||||
if _G[DEBUG_FLAG] then
|
if _G[DEBUG_FLAG] then
|
||||||
io.stderr:write(string.format("[%s] " .. fmt, PASS_NAME, ...))
|
io.stderr:write(string.format("[%s] " .. fmt, PASS_NAME, ...))
|
||||||
@@ -436,7 +436,10 @@ function M.run(ctx)
|
|||||||
local warnings = {}
|
local warnings = {}
|
||||||
|
|
||||||
local module_entries = (ctx.flags and ctx.flags._annot_results) or {}
|
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
|
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 _bootstrap_dir = debug.getinfo(1, "S").source:match("^@?(.*[/\\])") or "./"
|
||||||
local duffle = dofile(_bootstrap_dir .. "../duffle_paths.lua")
|
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
|
-- Type declarations
|
||||||
-- ════════════════════════════════════════════════════════════════════════════
|
-- ════════════════════════════════════════════════════════════════════════════
|
||||||
@@ -286,13 +293,13 @@ local POINTER_BYTE_SIZE = 4
|
|||||||
-- Maximum chain depth when resolving typedef / TSet_ chains (cycle guard).
|
-- Maximum chain depth when resolving typedef / TSet_ chains (cycle guard).
|
||||||
local TYPE_CHAIN_MAX_DEPTH = 8
|
local TYPE_CHAIN_MAX_DEPTH = 8
|
||||||
|
|
||||||
-- Walk a `Struct_` / `Enum_` body, calling `build_field(first, first_end, after_first)` for each entry.
|
--- Walk a `Struct_` / `Enum_` body, calling `build_field(first, first_end, after_first)` for each entry.
|
||||||
-- The builder returns either:
|
--- The builder returns either:
|
||||||
-- - (record, new_pos) -- append record to fields; advance body_pos to new_pos
|
--- - (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
|
--- - (nil, new_pos) -- skip this entry; advance body_pos to new_pos
|
||||||
-- After each entry, the walker skips a single trailing `,` or `;`.
|
--- 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.
|
--- The 2 body-field parsers in this file (struct + enum) share this body-walk loop.
|
||||||
-- @param body string
|
--- @param body string
|
||||||
--- @param build_field fun(first: string, first_end: integer, after_first: integer): (table|nil, integer)
|
--- @param build_field fun(first: string, first_end: integer, after_first: integer): (table|nil, integer)
|
||||||
--- @return table[]
|
--- @return table[]
|
||||||
local function walk_body_fields(body, build_field)
|
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.
|
-- Parse the trailing `*` chain to derive pointer_depth.
|
||||||
local depth, cursor = 0, after_type
|
local depth, cursor = 0, after_type
|
||||||
while cursor <= #body and body:sub(cursor, cursor) == "*" do
|
while cursor <= #body and body:sub(cursor, cursor) == "*" do
|
||||||
depth = depth + 1
|
depth = depth + 1
|
||||||
cursor = cursor + 1
|
cursor = cursor + 1
|
||||||
cursor = duffle.skip_ws_and_cmt(body, cursor)
|
cursor = duffle.skip_ws_and_cmt(body, cursor)
|
||||||
end
|
end
|
||||||
@@ -385,14 +392,12 @@ local function resolve_typedef_byte_size(type_name, type_name_registry, visited,
|
|||||||
local entry = type_name_registry[type_name]
|
local entry = type_name_registry[type_name]
|
||||||
if not entry then return nil end
|
if not entry then return nil end
|
||||||
|
|
||||||
-- Confident: this entry was already resolved by the propagation pass
|
-- Confident: this entry was already resolved by the propagation pass (e.g., a builtin or a struct whose fields are all resolved).
|
||||||
-- (e.g., a builtin or a struct whose fields are all resolved).
|
|
||||||
if entry.byte_size ~= nil then return entry.byte_size end
|
if entry.byte_size ~= nil then return entry.byte_size end
|
||||||
|
|
||||||
-- Chain-following: typedef / TSet_ aliases follow underlying_type.
|
-- Chain-following: typedef / TSet_ aliases follow underlying_type.
|
||||||
if entry.underlying_type then
|
if entry.underlying_type then
|
||||||
return resolve_typedef_byte_size(
|
return resolve_typedef_byte_size(entry.underlying_type, type_name_registry, visited, depth + 1)
|
||||||
entry.underlying_type, type_name_registry, visited, depth + 1)
|
|
||||||
end
|
end
|
||||||
|
|
||||||
-- Struct_ entries with unresolved byte_size can still resolve when their fields are all resolved.
|
-- 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.
|
-- 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.
|
-- 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 function propagate_type_sizes(out)
|
||||||
local reg = out.type_name_registry
|
local reg = out.type_name_registry
|
||||||
if not reg then return end
|
if not reg then return end
|
||||||
|
|
||||||
-- Seed builtin primitives (U1/U2/U4/S1/S2/S4 + __UINT*_TYPE__ family).
|
-- 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
|
-- - a plain register ident: `R_FaceCursor` → reg_name only
|
||||||
-- - register + atom_type sub-call: `R_FaceCursor atom_type(V4_S2*)` → reg_name + override entry
|
-- - register + atom_type sub-call: `R_FaceCursor atom_type(V4_S2*)` → reg_name + override entry
|
||||||
-- Returns (reg_name, override_entry_or_nil, malformed_flag).
|
-- 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
|
-- On malformed `atom_type(...)` (missing close paren, trailing tokens after the close paren, empty type chain, trailing junk inside the parens like `V4_S2*()`),
|
||||||
-- parens like `V4_S2*()`), the function still returns the leading reg_name but sets `malformed_flag = true` and `override_entry = nil`
|
-- 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
|
-- 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").
|
|
||||||
local function parse_atom_info_reg_entry(entry)
|
local function parse_atom_info_reg_entry(entry)
|
||||||
local pos = 1
|
local pos = 1
|
||||||
pos = duffle.skip_ws_and_cmt(entry, pos)
|
pos = duffle.skip_ws_and_cmt(entry, pos)
|
||||||
if pos > #entry then return nil, nil, false end
|
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
|
if not reg_name then return nil, nil, false end
|
||||||
pos = duffle.skip_ws_and_cmt(entry, reg_end)
|
pos = duffle.skip_ws_and_cmt(entry, reg_end)
|
||||||
-- Plain register, no adjacent atom_type — done.
|
-- Plain register, no adjacent atom_type — done.
|
||||||
if pos > #entry then return reg_name, nil, false end
|
if pos > #entry then return reg_name, nil, false end
|
||||||
|
|
||||||
-- Adjacent ident must be a bare `atom_type` (word-bounded both sides).
|
-- Adjacent ident must be a bare `atom_type` (word-bounded both sides).
|
||||||
local next_ident, next_end = duffle.read_ident(entry, pos)
|
local next_ident, next_end = duffle.read_ident(entry, pos)
|
||||||
if not next_ident or next_ident ~= "atom_type" then
|
if not next_ident or next_ident ~= "atom_type" then return reg_name, nil, false end
|
||||||
return reg_name, nil, false
|
|
||||||
end
|
|
||||||
-- Left word-boundary: `_atom_type` should NOT match `atom_type`.
|
-- Left word-boundary: `_atom_type` should NOT match `atom_type`.
|
||||||
if pos > 1 then
|
if pos > 1 then
|
||||||
local prev = entry:byte(pos - 1)
|
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).
|
-- Expect `(` immediately after `atom_type` (whitespace tolerated).
|
||||||
pos = duffle.skip_ws_and_cmt(entry, next_end)
|
pos = duffle.skip_ws_and_cmt(entry, next_end)
|
||||||
if pos > #entry or entry:sub(pos, pos) ~= "(" then
|
if pos > #entry or entry:sub(pos, pos) ~= "(" then return reg_name, nil, true end
|
||||||
return reg_name, nil, true
|
|
||||||
end
|
|
||||||
local sub_inner, sub_after = duffle.read_parens(entry, pos)
|
local sub_inner, sub_after = duffle.read_parens(entry, pos)
|
||||||
|
|
||||||
-- Reject any trailing tokens (including `;` / `,`) after the close paren.
|
-- 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.
|
-- 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)
|
local after_close = duffle.skip_ws_and_cmt(entry, sub_after)
|
||||||
if after_close <= #entry then
|
if after_close <= #entry then return reg_name, nil, true end
|
||||||
return reg_name, nil, true
|
|
||||||
end
|
|
||||||
|
|
||||||
-- Parse the type chain inside the parens; require full consumption.
|
-- Parse the type chain inside the parens; require full consumption.
|
||||||
-- parse_type_chain returns (ident, depth, end_pos);
|
-- parse_type_chain returns (ident, depth, end_pos);
|
||||||
-- we reject any non-whitespace residue past end_pos (catches `V4_S2*()` etc.).
|
-- 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)
|
local type_name, depth, after_chain = parse_type_chain(sub_inner, 1)
|
||||||
if not type_name then return reg_name, nil, true end
|
if not type_name then return reg_name, nil, true end
|
||||||
local end_check = duffle.skip_ws_and_cmt(sub_inner, after_chain)
|
local end_check = duffle.skip_ws_and_cmt(sub_inner, after_chain)
|
||||||
if end_check <= #sub_inner then
|
if end_check <= #sub_inner then return reg_name, nil, true end
|
||||||
return reg_name, nil, true
|
|
||||||
end
|
|
||||||
|
|
||||||
return reg_name, { type_name = type_name, pointer_depth = depth }, false
|
return reg_name, { type_name = type_name, pointer_depth = depth }, false
|
||||||
end
|
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.
|
-- 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).
|
-- atom_reads/atom_writes share a handler (same shape; just different output target).
|
||||||
local function rw_handler(sub_inner, info_line, kind)
|
local function rw_handler(sub_inner, info_line, kind)
|
||||||
-- The reads/writes arrays contain ONLY register idents;
|
-- 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.
|
-- `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 entries = duffle.split_top_level_commas(sub_inner)
|
||||||
local regs = {}
|
local regs = {}
|
||||||
for _, entry in ipairs(entries) do
|
for _, entry in ipairs(entries) do
|
||||||
@@ -625,7 +621,7 @@ local function scan_atom_info_subcalls(info_inner, info_line)
|
|||||||
end
|
end
|
||||||
local function ident_handler(sub_inner, info_line, out_name)
|
local function ident_handler(sub_inner, info_line, out_name)
|
||||||
-- Validates the arg is a single C identifier (no commas / parens / whitespace).
|
-- 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)
|
local name = duffle.read_ident(sub_inner, 1)
|
||||||
if name then
|
if name then
|
||||||
local after_id = duffle.skip_ws_and_cmt(sub_inner, 1 + #name)
|
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_NEWLINE = 0x0A -- '\n'
|
||||||
local BYTE_DASH = 0x2D -- '-'
|
local BYTE_DASH = 0x2D -- '-'
|
||||||
local BYTE_COMMA = 0x2C -- ','
|
local BYTE_COMMA = 0x2C -- ','
|
||||||
|
local BYTE_SEMI = 0x3B -- ';'
|
||||||
local BYTE_EQUAL = 0x3D -- '='
|
local BYTE_EQUAL = 0x3D -- '='
|
||||||
local BYTE_R = 0x52 -- 'R'
|
local BYTE_R = 0x52 -- 'R'
|
||||||
local BYTE_UNDERSCORE = 0x5F -- '_'
|
local BYTE_UNDERSCORE = 0x5F -- '_'
|
||||||
@@ -754,13 +751,16 @@ local function hex_digit_value(b)
|
|||||||
return nil
|
return nil
|
||||||
end
|
end
|
||||||
|
|
||||||
-- Parse a decimal/negative-decimal/hex integer literal starting at byte position `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.
|
--- Returns (value, end_pos) on success, or (nil, start) on failure / no match.
|
||||||
-- Accepts: 12, -1, 0, 0x10, 0X1F, -0x10.
|
--- Accepts: 12, -1, 0, 0x10, 0X1F, -0x10.
|
||||||
-- @param text string
|
--- @param text string
|
||||||
-- @param start integer
|
--- @param start integer
|
||||||
-- @return integer|nil, integer
|
--- @return integer|nil, integer
|
||||||
local function parse_enum_int_literal(text, start)
|
--- 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 pos = start
|
||||||
local len = #text
|
local len = #text
|
||||||
if pos > len then return nil, start end
|
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).
|
-- Try integer literal first (decimal / negative / hex).
|
||||||
local int_val, int_end = parse_enum_int_literal(source, pos)
|
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).
|
-- 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 sym then return nil end
|
||||||
if not is_r_code_macro(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.
|
-- 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.
|
-- 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]
|
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)
|
return resolve_code_macro_value(body, 1, code_macros, code_macro_bodies, visited, depth + 1)
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -874,14 +874,14 @@ local function resolve_code_macro_value(source, pos, code_macros, code_macro_bod
|
|||||||
return nil
|
return nil
|
||||||
end
|
end
|
||||||
|
|
||||||
-- Intercept a `#define R_*_Code <RHS>` preprocessor line.
|
--- 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),
|
--- 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.
|
--- 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.
|
--- `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 source string
|
||||||
-- @param directive_start integer -- byte position of `#`
|
--- @param directive_start integer -- byte position of `#`
|
||||||
-- @param code_macros table -- out._code_macros / ctx.shared._code_macros
|
--- @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
|
--- @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 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 rest = duffle.skip_ws_and_cmt(source, directive_start + 1)
|
||||||
local kw, kw_end = duffle.read_ident(source, rest)
|
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
|
if value ~= nil then code_macros[macro_name] = value end
|
||||||
end
|
end
|
||||||
|
|
||||||
-- Quick pre-pass: walk the source looking ONLY for `#define R_*_Code` lines.
|
--- 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
|
--- 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 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.
|
--- 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 source string
|
||||||
-- @param code_macros table
|
--- @param code_macros table
|
||||||
-- @param code_macro_bodies table
|
--- @param code_macro_bodies table
|
||||||
local function scan_source_pre_pass(source, code_macros, code_macro_bodies)
|
local function scan_source_pre_pass(source, code_macros, code_macro_bodies)
|
||||||
local pos = 1
|
local pos = 1
|
||||||
local src_len = #source
|
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).
|
-- 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
|
-- 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.
|
-- 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
|
if not last_brace_pos then return after_paren end
|
||||||
|
|
||||||
-- Use duffle.read_braces to find the matching close brace.
|
-- 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.
|
-- 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)
|
local body, close_pos = duffle.read_braces(inner, last_brace_pos)
|
||||||
if close_pos > #inner + 1 then return after_paren end
|
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)
|
register_struct_type(body, name, pos, line_of, out)
|
||||||
associate_skip_over_marker(out, name, name, "unrelated", line_of(pos), pos)
|
associate_skip_over_marker(out, name, name, "unrelated", line_of(pos), pos)
|
||||||
return after_brace
|
return after_brace
|
||||||
end
|
|
||||||
|
|
||||||
-- ── Shape 2: `typedef Enum_(<underlying>, <name>) { <body> } <alias>;`
|
-- ── 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)
|
local inner, after_paren, open_paren = read_parens_after(source, id2_end, id2_end)
|
||||||
if not inner then return id2_end end
|
if not inner then return id2_end end
|
||||||
-- Split `inner` on the first top-level comma into (<underlying>, <name>).
|
-- 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
|
return after_brace
|
||||||
end
|
end
|
||||||
|
|
||||||
-- ── Shapes 3 + 4: `typedef <type> <alias>;` or
|
-- ── Shapes 3 + 4: `typedef <span> <alias>;` or
|
||||||
-- `typedef <type> TSet_(<name>);`
|
-- `typedef <span> 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)
|
-- The <span> between `typedef` and the alias ident may be MULTI-token (e.g. `unsigned char`, `__UINT8_TYPE__`, `const U4`).
|
||||||
local id3, id3_end = duffle.read_ident(source, after_id2)
|
-- The alias is the LAST identifier before `;` (Shape 3), OR the argument of `TSet_(...)` when that wrapper is present (Shape 4).
|
||||||
if not id3 then return ident_end end
|
--
|
||||||
|
-- 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>);`
|
-- Find the terminating `;` (BYTE_SEMI). If absent, abort cleanly.
|
||||||
if id3 == "TSet_" then
|
local semi_pos = duffle.find_byte(source, BYTE_SEMI, id2_end)
|
||||||
local tset_inner, tset_after = read_parens_after(source, id3_end, id3_end)
|
if not semi_pos then return id2_end end
|
||||||
if not tset_inner then return id3_end end
|
|
||||||
local tset_name = duffle.trim(tset_inner)
|
-- Shape 4 (TSet_ at id2 position): no preceding underlying span.
|
||||||
register_typedef_alias(id2, tset_name, pos, line_of, out)
|
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)
|
associate_skip_over_marker(out, tset_name, tset_name, "unrelated", line_of(pos), pos)
|
||||||
return tset_after
|
return after_paren
|
||||||
end
|
end
|
||||||
|
|
||||||
-- Shape 3: `typedef <type> <alias>;`
|
-- Walk idents forward to find the alias ident (last ident before `;`), or the TSet_(<arg>) form (capture the arg, use it as the alias).
|
||||||
register_typedef_alias(id2, id3, pos, line_of, out)
|
local last_ident = nil
|
||||||
associate_skip_over_marker(out, id3, id3, "unrelated", line_of(pos), pos)
|
local last_ident_pos = nil
|
||||||
return id3_end
|
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
|
end
|
||||||
|
|
||||||
--- Parse: `_Pragma("mac_X tape_atom words=N")` (operator form).
|
--- Parse: `_Pragma("mac_X tape_atom words=N")` (operator form).
|
||||||
--- @param source string
|
--- @param source string
|
||||||
--- @param pos integer
|
--- @param pos integer
|
||||||
--- @param ident_end integer
|
--- @param ident_end integer
|
||||||
--- @param line_of fun(pos: integer): integer
|
--- @param line_of fun(pos: integer): integer
|
||||||
--- @param out SourceScan
|
--- @param out SourceScan
|
||||||
--- @return integer
|
--- @return integer
|
||||||
local function parse_pragma_macro(source, pos, ident_end, line_of, out)
|
local function parse_pragma_macro(source, pos, ident_end, line_of, out)
|
||||||
local str, str_end = read_parens_after(source, ident_end)
|
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
|
return str_end
|
||||||
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.
|
-- Parse the value side of an enum entry.
|
||||||
-- Accepts integer literals (decimal/negative/hex), `R_*_Code` symbol references resolved via `out._code_macros`,
|
-- 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.
|
-- 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 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.
|
-- Returns (value, end_pos) on success or (nil, pos) if unresolvable.
|
||||||
local function parse_enum_value(body, pos, out)
|
local function parse_enum_value(body, pos, out)
|
||||||
local int_val, int_end = parse_enum_int_literal(body, pos)
|
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
|
if not sym then return nil, pos end
|
||||||
|
|
||||||
-- Bare `R_*` (no `_Code` suffix) → translate to `R_*_Code` and look that up. Non-`R_*` symbols
|
-- 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,
|
MipsCode = parse_mips_code,
|
||||||
typedef = parse_typedef_binds,
|
typedef = parse_typedef_binds,
|
||||||
_Pragma = parse_pragma_macro,
|
_Pragma = parse_pragma_macro,
|
||||||
pragma = parse_pragma_dummy,
|
|
||||||
-- `enum [<tag>] { <body> }` populates `out.register_alias_registry`.
|
-- `enum [<tag>] { <body> }` populates `out.register_alias_registry`.
|
||||||
enum = parse_enum,
|
enum = parse_enum,
|
||||||
}
|
}
|
||||||
@@ -1655,8 +1709,7 @@ local function scan_source(source, source_file, code_macros, code_macro_bodies)
|
|||||||
if parser then
|
if parser then
|
||||||
pos = parser(source, pos, ident_end, line_of, out)
|
pos = parser(source, pos, ident_end, line_of, out)
|
||||||
else
|
else
|
||||||
-- A component-procedure declaration has an FI_ signature before MipsAtomComp_Proc_;
|
-- A component-procedure declaration has an FI_ signature before MipsAtomComp_Proc_; keep the marker pending across that prelude.
|
||||||
-- 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.
|
-- 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 markers = out.skip_over.markers
|
||||||
local marker = markers[#markers]
|
local marker = markers[#markers]
|
||||||
@@ -1692,6 +1745,252 @@ local function scan_source(source, source_file, code_macros, code_macro_bodies)
|
|||||||
return out
|
return out
|
||||||
end
|
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
|
-- M — module exports
|
||||||
-- ════════════════════════════════════════════════════════════════════════════
|
-- ════════════════════════════════════════════════════════════════════════════
|
||||||
@@ -1703,37 +2002,50 @@ local M = {}
|
|||||||
--- Walk each source once and attach the fat SourceScan payload to `src.scan`.
|
--- 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.
|
--- No output files; this is a pure in-memory pre-processing pass.
|
||||||
---
|
---
|
||||||
--- Runs in 3 phases.
|
--- Runs in 5 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.
|
--- Resolve: Resolve the canonical source order from `ctx.shared.corpus.source_order`.
|
||||||
--- 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
|
--- The canonical corpus is the SOLE source of truth; no `ctx.sources` alias is consulted and no per-source fallback synthesis is performed.
|
||||||
--- sdefining `#define` lives in a different source than the chain call site.
|
--- Pass 1a: `scan_source_pre_pass` over every source, populating LOCAL `code_macros` AND LOCAL `code_macro_bodies` tables.
|
||||||
--- Pass 1b: Resolve every collected macro's chain using the bodies table as fallback.
|
--- The bodies table holds the raw post-`=` text of every `#define R_*_Code` line (cross-source)
|
||||||
--- This fills in `code_macros` entries whose defining source was scanned AFTER the call site
|
--- so the chain walker can fall back when the defining `#define` lives in a different source than the chain call site.
|
||||||
--- (e.g. lottes_tape.h's `R_TapePtr_Code -> R_T8_Code` chain into mips.h's `R_T8_Code = 24`).
|
--- Pass 1b: Resolve every collected macro's chain using the bodies table as fallback.
|
||||||
--- Pass 2: The full `scan_source(source, source_file, code_macros, code_macro_bodies)` walk, which feeds `out._code_macros = ctx.shared._code_macros`
|
--- 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`).
|
||||||
--- (and `out._code_macro_bodies = ctx.shared._code_macro_bodies`)
|
--- 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
|
||||||
--- so the enum parser can resolve cross-source `R_*_Code` references and bare `R_*` symbols via the `_Code` registry fallback.
|
--- (register_alias_registry, type_name_registry, atom_views, atom_ctxs, atom_phases, binds, atoms, atom_infos, ...).
|
||||||
--- Strip: `src.scan._code_macros` AND `src.scan._code_macro_bodies` are nilled before returning so downstream passes
|
--- Strip: Strip `src.scan._code_macros`, `src.scan._code_macro_bodies`, and the `_source_file` pointer.
|
||||||
--- (annotation, components, offsets, dwarf_injection, etc.) don't see the private parse state.
|
--- 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
|
--- @param ctx PassCtx
|
||||||
--- @return PassResult
|
--- @return PassResult
|
||||||
function M.run(ctx)
|
function M.run(ctx)
|
||||||
-- Initialize the cross-source code-macro + body registries in ctx.shared.
|
-- The cross-source _code_macros / _code_macro_bodies tables are LOCAL to this run.
|
||||||
-- (Pass 1a writes into both; pass 1b reads from both; pass 2 reads both.)
|
-- They are shared across source scans ONLY long enough to resolve cross-source R_*_Code chains, then DISCARDED.
|
||||||
ctx.shared = ctx.shared or {}
|
-- They MUST NOT appear on ctx.shared, ctx.shared.corpus, or any src.scan after
|
||||||
ctx.shared._code_macros = ctx.shared._code_macros or {}
|
-- this function returns.
|
||||||
ctx.shared._code_macro_bodies = ctx.shared._code_macro_bodies or {}
|
local code_macros = {}
|
||||||
local code_macros = ctx.shared._code_macros
|
local code_macro_bodies = {}
|
||||||
local code_macro_bodies = ctx.shared._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.
|
-- 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)
|
scan_source_pre_pass(src.text, code_macros, code_macro_bodies)
|
||||||
end
|
end
|
||||||
|
|
||||||
-- Pass 1b: resolve every collected macro's chain with cross-source fallback.
|
-- 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;
|
-- 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).
|
-- (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.
|
-- Same `code_macros` table is shared with pass 2 below.
|
||||||
for macro_name, _ in pairs(code_macro_bodies) do
|
for macro_name, _ in pairs(code_macro_bodies) do
|
||||||
@@ -1745,28 +2057,33 @@ function M.run(ctx)
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
-- Pass 2: run the full scan with the shared `_code_macros` + `_code_macro_bodies`
|
-- Pass 2: run the full scan with the shared `_code_macros` + `_code_macro_bodies` so the enum parser can resolve cross-source
|
||||||
-- so the enum parser can resolve cross-source `R_*_Code` references and bare `R_*` symbols via the `_Code` registry fallback.
|
-- `R_*_Code` references and bare `R_*` symbols via the `_Code` registry fallback.
|
||||||
for _, src in ipairs(ctx.sources) do
|
-- 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)
|
src.scan = scan_source(src.text, src.path, code_macros, code_macro_bodies)
|
||||||
-- Pre-tokenize each atom body once (plex: single source of truth).
|
-- Strip the three private fields immediately so a later fatal source does not leave this source leaking parse state.
|
||||||
-- Downstream passes (offsets, word-counts, components, static-analysis) read from
|
-- The shared `code_macros` / `code_macro_bodies` locals remain in the outer scope and keep their contents for subsequent sources.
|
||||||
-- `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
|
|
||||||
if src.scan then
|
if src.scan then
|
||||||
src.scan._code_macros = nil
|
src.scan._code_macros = nil
|
||||||
src.scan._code_macro_bodies = nil
|
src.scan._code_macro_bodies = nil
|
||||||
src.scan._source_file = nil
|
src.scan._source_file = nil
|
||||||
end
|
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
|
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 = {} }
|
return { outputs = {}, errors = {}, warnings = {} }
|
||||||
end
|
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.
|
--- word_count_eval.lua — Word-counting logic for the tape-atom metaprogram pipeline.
|
||||||
---
|
---
|
||||||
--- Three responsibilities:
|
--- Two responsibilities:
|
||||||
--- 1. **Public utilities** (used by `passes/components.lua`, `passes/offsets.lua`, `passes/annotation.lua`):
|
--- 1. **Public utility** `M.count_token_words(token, wc)`: Used by `passes/offsets.lua`, `passes/annotation.lua`, and other passes.
|
||||||
--- - `M.count_token_words(token, wc)` — words emitted by one token
|
--- 2. **Pass entry** `M.run(ctx)`: Loads the authored `word_count.metadata.h` into `ctx.shared.corpus.word_counts` for downstream passes.
|
||||||
--- - `M.scan_dir(dir, suffix)` — glob walk for *.macs.h
|
--- The generated `.macs.h` files are OUTPUT artifacts and are NOT inputs to this pass;
|
||||||
--- 2. **Pass entry** `M.run(ctx)` — loads metadata.h + *.macs.h into `ctx.shared.word_counts` for downstream passes.
|
--- Current component counts are owned by `passes/components.lua` (which populates `corpus.word_counts` and `corpus.component_body_index`
|
||||||
--- 3. **Internal helpers** for the body scanner.
|
--- 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,
|
--- **Conventions**: tabs (1/level), EmmyLua annotations, no regex,
|
||||||
--- Lua 5.3 compatible.
|
--- Lua 5.3 compatible.
|
||||||
@@ -14,23 +20,11 @@
|
|||||||
-- Module-scope requires + package.path setup
|
-- 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).
|
-- 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.
|
-- 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.
|
-- 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 _bootstrap_dir = debug.getinfo(1, "S").source:match("^@?(.*[/\\])") or "./"
|
||||||
local duffle = dofile(_bootstrap_dir .. "../duffle_paths.lua")
|
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")
|
|
||||||
|
|
||||||
-- ════════════════════════════════════════════════════════════════════════════
|
-- ════════════════════════════════════════════════════════════════════════════
|
||||||
-- Type declarations
|
-- Type declarations
|
||||||
@@ -49,7 +43,8 @@ local lfs = require("lfs")
|
|||||||
--- @field sources SourceFile[] -- all source files in the build
|
--- @field sources SourceFile[] -- all source files in the build
|
||||||
--- @field metadata_path string -- path to word_count.metadata.h
|
--- @field metadata_path string -- path to word_count.metadata.h
|
||||||
--- @field shared table -- cross-pass shared state
|
--- @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 out_root string -- output root (e.g. "build/gen")
|
||||||
--- @field project_root string -- project root (e.g. "code/")
|
--- @field project_root string -- project root (e.g. "code/")
|
||||||
--- @field upstream table<string, table> -- per-pass upstream outputs
|
--- @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 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 `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.
|
--- For unknown macros, returns 1 and (optionally) warns.
|
||||||
--- @param token string -- a single token from split_top_level_commas
|
--- @param token string -- a single token from split_top_level_commas
|
||||||
--- @param wc WordCounts -- the shared word-count table
|
--- @param wc WordCounts -- the shared word-count table
|
||||||
--- @return integer
|
--- @return integer
|
||||||
function M.count_token_words(token, wc)
|
function M.count_token_words(token, wc)
|
||||||
local s = duffle.trim(token)
|
local s = duffle.trim(token)
|
||||||
@@ -92,83 +87,42 @@ function M.count_token_words(token, wc)
|
|||||||
return 1
|
return 1
|
||||||
end
|
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 │
|
-- │ Pass entry: M.run(ctx) — "word-counts" pass │
|
||||||
-- └────────────────────────────────────────────────────────────────────┘
|
-- └────────────────────────────────────────────────────────────────────┘
|
||||||
|
|
||||||
--- Load metadata.h + scan for existing *.macs.h files into ctx.shared.word_counts.
|
--- Load the authored `word_count.metadata.h` into `ctx.shared.corpus.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.
|
--- 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
|
--- @param ctx PassCtx
|
||||||
--- @return PassResult
|
--- @return PassResult
|
||||||
function M.run(ctx)
|
function M.run(ctx)
|
||||||
local wc = {}
|
-- 1. Canonical-corpus ownership gate.
|
||||||
|
local corpus = ctx.shared and ctx.shared.corpus
|
||||||
-- 1. Load metadata.h (the encoding-macro source of truth).
|
if type(corpus) ~= "table" then
|
||||||
local meta_counts = duffle.load_word_counts(ctx.metadata_path)
|
error("word_count_eval.run requires ctx.shared.corpus (canonical corpus). The fixture must install the corpus before running this pass.", 0)
|
||||||
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
|
|
||||||
end
|
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 = {} }
|
return { outputs = {}, errors = {}, warnings = {} }
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|||||||
+254
-151
@@ -4,8 +4,8 @@
|
|||||||
---
|
---
|
||||||
--- **Architecture**:
|
--- **Architecture**:
|
||||||
--- - **PASSES table** — declarative dep graph (data, not code).
|
--- - **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).
|
--- - **FLAG_HANDLERS table** — maps CLI flags to handlers.
|
||||||
--- - **parse_args** → **build_ctx** (just opens + reads source files; no inline scanning) → **topo_sort** → **dispatch_passes**.
|
--- - **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`).
|
--- - 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`.
|
--- 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.
|
--- 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.
|
-- 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
|
-- Use `arg[0]` when this file is the entry script (`arg[0]` ends in "ps1_meta.lua");
|
||||||
-- "ps1_meta.lua"); fall back to `debug.getinfo(1, "S").source` when this
|
-- 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).
|
||||||
-- file is being dofile()'d or require()'d (in which case `arg[0]` is the
|
-- That single statement: (a) sets `package.path` + `package.cpath` (via cached `git rev-parse`), (b) at the bottom returns `require("duffle")`.
|
||||||
-- *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.
|
-- 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 _is_entry_script = arg and arg[0] and arg[0]:match("ps1_meta%.lua$") ~= nil
|
||||||
local _bootstrap_src
|
local _bootstrap_src
|
||||||
@@ -81,12 +77,11 @@ local PASS_FLAG_DISPATCH_KEY = "__pass__"
|
|||||||
--- @field basename string -- filename without extension
|
--- @field basename string -- filename without extension
|
||||||
|
|
||||||
--- @class PassCtx
|
--- @class PassCtx
|
||||||
--- @field sources SourceFile[] -- all source files in the build
|
|
||||||
--- @field metadata_path string -- path to word_count.metadata.h
|
--- @field metadata_path string -- path to word_count.metadata.h
|
||||||
--- @field shared table -- cross-pass shared state
|
--- @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 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 upstream table<string, table> -- per-pass output accumulator
|
||||||
--- @field flags table -- CLI flags + per-pass stash
|
--- @field flags table -- CLI flags + per-pass stash
|
||||||
--- @field dry_run boolean -- if true, compute but don't write
|
--- @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
|
--- @field warnings Finding[] -- informational
|
||||||
|
|
||||||
--- @class ParsedArgs
|
--- @class ParsedArgs
|
||||||
--- @field requested_set string[] -- pass names to run (explicit --all expanded)
|
--- @field requested_set string[] -- pass names to run (explicit --all expanded)
|
||||||
--- @field sources string[] -- --source values
|
--- @field sources string[] -- exact --source values, retained in CLI order
|
||||||
--- @field metadata string -- --metadata value
|
--- @field unity_root string|nil -- --unity-root value; mutually exclusive with sources
|
||||||
--- @field out_root string -- --out-root value (default "build/gen")
|
--- @field metadata string -- --metadata value
|
||||||
--- @field project_root string -- --project-root value (default dirname(metadata))
|
--- @field out_root string -- --out-root value (default "build/gen")
|
||||||
--- @field dry_run boolean -- if true, compute but don't write
|
--- @field project_root string -- PS1 repository root (derived from metadata by default)
|
||||||
--- @field verbose boolean -- if true, log diagnostic info
|
--- @field dry_run boolean -- if true, compute but don't write
|
||||||
|
--- @field verbose boolean -- if true, log diagnostic info
|
||||||
|
|
||||||
-- ════════════════════════════════════════════════════════════════════════════
|
-- ════════════════════════════════════════════════════════════════════════════
|
||||||
-- PASSES Table
|
-- PASSES Table
|
||||||
@@ -145,6 +141,13 @@ local PASSES = {
|
|||||||
desc = "Emit mac_X macros from MipsAtomComp_ declarations",
|
desc = "Emit mac_X macros from MipsAtomComp_ declarations",
|
||||||
out = { { kind = "header", path_template = "<source_dir>/gen/<basename>.macs.h" } },
|
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 = {
|
annotation = {
|
||||||
module = "passes.annotation",
|
module = "passes.annotation",
|
||||||
kind = "validation",
|
kind = "validation",
|
||||||
@@ -158,7 +161,7 @@ local PASSES = {
|
|||||||
offsets = {
|
offsets = {
|
||||||
module = "passes.offsets",
|
module = "passes.offsets",
|
||||||
kind = "header-output",
|
kind = "header-output",
|
||||||
deps = {"scan-source", "word-counts", "components"},
|
deps = {"scan-source", "word-counts", "components", "emission-model"},
|
||||||
groups = { "pre-link" },
|
groups = { "pre-link" },
|
||||||
desc = "Compute branch offsets for atom_label / atom_offset",
|
desc = "Compute branch offsets for atom_label / atom_offset",
|
||||||
out = { { kind = "header", path_template = "<source_dir>/gen/<basename>.offsets.h" } },
|
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).
|
-- the orchestrator does NOT exit non-zero on these findings (see PASS_KIND_STOP_ON_ERROR).
|
||||||
-- Report severity is independent from process exit policy.
|
-- Report severity is independent from process exit policy.
|
||||||
kind = "diagnostic",
|
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",
|
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" } },
|
out = { { kind = "report", path_template = "<out_root>/<basename>.static_analysis.txt" } },
|
||||||
},
|
},
|
||||||
["atoms-source-map"] = {
|
["atoms-source-map"] = {
|
||||||
module = "passes.atoms_source_map",
|
module = "passes.atoms_source_map",
|
||||||
kind = "header-output",
|
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.",
|
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 = {
|
out = {
|
||||||
{ kind = "report", path_template = "<out_root>/<basename>.atoms.sourcemap.txt" },
|
{ 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 function request_roots_for_group(args, group_name)
|
||||||
local roots = roots_for_group(group_name)
|
local roots = roots_for_group(group_name)
|
||||||
if #roots == 0 then
|
if #roots == 0 then
|
||||||
error(string.format(
|
error(string.format("ps1_meta: build-phase group %q has zero roots in PASSES; check PASSES rows for a `groups = { %q }` field"
|
||||||
"ps1_meta: build-phase group %q has zero roots in PASSES; "
|
, group_name, group_name))
|
||||||
.. "check PASSES rows for a `groups = { %q }` field",
|
|
||||||
group_name, group_name))
|
|
||||||
end
|
end
|
||||||
for _, name in ipairs(roots) do
|
for _, name in ipairs(roots) do
|
||||||
args.requested_set[#args.requested_set + 1] = name
|
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,
|
-- 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.
|
-- but `report_validation_errors` returns early for non-stopping kinds, so nothing is printed to stderr and the orchestrator does not exit non-zero.
|
||||||
--
|
-- Adding a new pass kind requires listing it here explicitly; an unknown kind must not silently fall back to "true".
|
||||||
-- Closed-set discipline: 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 = {
|
local PASS_KIND_STOP_ON_ERROR = {
|
||||||
["shared"] = false,
|
["shared"] = false,
|
||||||
["header-output"] = true,
|
["header-output"] = true,
|
||||||
@@ -268,9 +268,8 @@ local PASS_KIND_STOP_ON_ERROR = {
|
|||||||
-- Closed set of CLI flags -> pass names.
|
-- Closed set of CLI flags -> pass names.
|
||||||
-- Per-pass flags (e.g. --word-counts) live here; phase flags (--pre-link, --post-link, --all)
|
-- 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.
|
-- 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
|
-- 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
|
||||||
-- explicit FLAG_HANDLERS entry below (it sets args.flags.dwarf_injection and appends "dwarf-injection" to requested_set),
|
-- (it sets args.flags.dwarf_injection and appends "dwarf-injection" to requested_set), so it is intentionally absent from this table.
|
||||||
-- so it is intentionally absent from this table.
|
|
||||||
local PASS_FLAG_TO_NAME = {
|
local PASS_FLAG_TO_NAME = {
|
||||||
["--word-counts"] = "word-counts",
|
["--word-counts"] = "word-counts",
|
||||||
["--components"] = "components",
|
["--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).
|
-- 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).
|
-- 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 = {}
|
local FLAG_HANDLERS = {}
|
||||||
|
|
||||||
-- ════════════════════════════════════════════════════════════════════════════
|
-- ════════════════════════════════════════════════════════════════════════════
|
||||||
@@ -336,10 +335,14 @@ PASS_FLAGS:
|
|||||||
--report Render per-project summary
|
--report Render per-project summary
|
||||||
|
|
||||||
COMMON_FLAGS:
|
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)
|
--metadata PATH Path to metadata.h (required)
|
||||||
--out-root DIR Output root for reports (default: build/gen)
|
--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)
|
--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)
|
--elf PATH Path to linked .elf (for --gdb-runtime / --dwarf-injection)
|
||||||
--dry-run Print dep order (alphabetical); exit 0 without running
|
--dry-run Print dep order (alphabetical); exit 0 without running
|
||||||
@@ -351,17 +354,36 @@ EXIT CODES:
|
|||||||
1 Validation errors found
|
1 Validation errors found
|
||||||
2 Metaprogram internal error
|
2 Metaprogram internal error
|
||||||
|
|
||||||
EXAMPLE:
|
EXAMPLES:
|
||||||
ps1_meta.lua --pre-link --metadata metadata.h --source code/foo.c --source code/bar.c
|
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 metadata.h --source code/foo.c --source code/bar.c --elf build/hello_gte.elf
|
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
|
ps1_meta.lua --all --metadata metadata.h --source code/foo.c --source code/bar.c
|
||||||
]])
|
]])
|
||||||
end
|
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).
|
-- 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.
|
-- 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,
|
-- 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).
|
-- but if the closure is defined before the local, it falls back to _G).
|
||||||
FLAG_HANDLERS["--help"] = function(args)
|
FLAG_HANDLERS["--help"] = function(args)
|
||||||
@@ -369,17 +391,46 @@ FLAG_HANDLERS["--help"] = function(args)
|
|||||||
os.exit(0)
|
os.exit(0)
|
||||||
end
|
end
|
||||||
|
|
||||||
FLAG_HANDLERS["--dry-run"] = function(args) args.dry_run = true end
|
FLAG_HANDLERS["--dry-run"] = function(args) args.dry_run = true end
|
||||||
FLAG_HANDLERS["--verbose"] = function(args) args.verbose = 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["--source"] = function(args, argv, arg_idx)
|
||||||
FLAG_HANDLERS["--metadata"] = function(args, argv, arg_idx) args.metadata = argv[arg_idx + 1]; return arg_idx + 1 end
|
local value, value_idx = require_flag_value(argv, arg_idx, "--source")
|
||||||
FLAG_HANDLERS["--out-root"] = function(args, argv, arg_idx) args.out_root = argv[arg_idx + 1]; return arg_idx + 1 end
|
args.sources[#args.sources + 1] = value
|
||||||
FLAG_HANDLERS["--project-root"] = function(args, argv, arg_idx) args.project_root = argv[arg_idx + 1]; return arg_idx + 1 end
|
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.
|
-- 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`).
|
-- 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["--gdb-runtime"] = function(args)
|
||||||
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
|
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.
|
-- 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.
|
-- 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)
|
FLAG_HANDLERS["--dwarf-injection"] = function(args)
|
||||||
@@ -402,7 +453,7 @@ FLAG_HANDLERS["--post-link"] = function(args)
|
|||||||
request_roots_for_group(args, "post-link")
|
request_roots_for_group(args, "post-link")
|
||||||
end
|
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.
|
-- Pass-flag handler. Reads the closed-set table, expands --all, appends to requested_set.
|
||||||
FLAG_HANDLERS[PASS_FLAG_DISPATCH_KEY] = function(args, a)
|
FLAG_HANDLERS[PASS_FLAG_DISPATCH_KEY] = function(args, a)
|
||||||
@@ -421,6 +472,7 @@ local function parse_args(argv)
|
|||||||
local args = {
|
local args = {
|
||||||
requested_set = {},
|
requested_set = {},
|
||||||
sources = {},
|
sources = {},
|
||||||
|
unity_root = nil,
|
||||||
metadata = nil,
|
metadata = nil,
|
||||||
out_root = DEFAULT_OUT_ROOT,
|
out_root = DEFAULT_OUT_ROOT,
|
||||||
project_root = nil,
|
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.
|
-- 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
|
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
|
if not args.metadata then
|
||||||
io.stderr:write("ps1_meta: --metadata PATH is required\n")
|
io.stderr:write("ps1_meta: --metadata PATH is required\n")
|
||||||
os.exit(EXIT_INTERNAL_ERROR)
|
os.exit(EXIT_INTERNAL_ERROR)
|
||||||
end
|
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)
|
os.exit(EXIT_INTERNAL_ERROR)
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -485,56 +544,128 @@ end
|
|||||||
-- Build ctx from parsed args
|
-- Build ctx from parsed args
|
||||||
-- ════════════════════════════════════════════════════════════════════════════
|
-- ════════════════════════════════════════════════════════════════════════════
|
||||||
|
|
||||||
--- Build the PassCtx from parsed args. Reads each source file once at startup;
|
--- Build the PassCtx from parsed args. Exact mode opens only the repeated `--source` inputs;
|
||||||
--- passes consume `src.text`, not the path (path is preserved for error reporting).
|
--- unity mode delegates direct-include resolution to duffle.resolve_source_corpus`.
|
||||||
|
--- Scanning remains pass-owned (`src.scan`).
|
||||||
--- @param args ParsedArgs
|
--- @param args ParsedArgs
|
||||||
--- @return PassCtx
|
--- @return PassCtx
|
||||||
local function build_ctx(args)
|
local function build_ctx(args)
|
||||||
local sources = {}
|
local normalized_project_root = duffle.normalize_path(args.project_root)
|
||||||
for _, path in ipairs(args.sources) do
|
local project_root = normalized_project_root
|
||||||
-- lfs handles path metadata and directories, not file-content streams.
|
local project_root_is_absolute = normalized_project_root:match("^%a:/")
|
||||||
-- Keep this io.open local so this entry point preserves its tailored diagnostic and exit path below.
|
or normalized_project_root:sub(1, 2) == "//"
|
||||||
local f = io.open(path, "r")
|
or normalized_project_root:sub(1, 1) == "/"
|
||||||
if not f then
|
if not project_root_is_absolute then
|
||||||
io.stderr:write("ps1_meta: cannot open --source " .. path .. "\n")
|
-- 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)
|
os.exit(EXIT_INTERNAL_ERROR)
|
||||||
end
|
end
|
||||||
local text = f:read("*a")
|
resolution = resolved
|
||||||
f:close()
|
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 source = {
|
||||||
local basename = duffle.basename_no_ext(path)
|
path = path,
|
||||||
if #dir > 0 and (dir:sub(-1) == "/" or dir:sub(-1) == "\\") then
|
text = text,
|
||||||
dir = dir:sub(1, -2)
|
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
|
end
|
||||||
|
resolution = {
|
||||||
-- src.scan is populated by the "scan-source" pass (the first pass in the dep graph).
|
unity_root = nil,
|
||||||
-- build_ctx just opens + reads the files; the scan itself happens in the pass module, not inline in the orchestrator.
|
project_root = project_root,
|
||||||
sources[#sources + 1] = {
|
code_root = duffle.normalize_path(project_root .. "/code"),
|
||||||
path = path,
|
source_order = source_order,
|
||||||
text = text,
|
sources_by_path = sources_by_path,
|
||||||
dir = dir,
|
sources_by_dir = duffle.group_sources_by_dir(source_order),
|
||||||
basename = basename,
|
resolver = resolver,
|
||||||
}
|
}
|
||||||
end
|
end
|
||||||
|
|
||||||
-- Pre-compute the per-directory grouping once (Fleury: expose structure).
|
local corpus = {
|
||||||
-- Three passes (annotation, report, static-analysis) call group_sources_by_dir with the same ctx.sources;
|
unity_root = resolution.unity_root,
|
||||||
-- computing it here and stashing on ctx.by_dir eliminates 2 redundant calls.
|
project_root = resolution.project_root,
|
||||||
local by_dir = duffle.group_sources_by_dir(sources)
|
code_root = resolution.code_root,
|
||||||
|
source_order = resolution.source_order,
|
||||||
return {
|
sources_by_path = resolution.sources_by_path,
|
||||||
sources = sources,
|
sources_by_dir = resolution.sources_by_dir,
|
||||||
by_dir = 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,
|
metadata_path = args.metadata,
|
||||||
shared = {},
|
shared = { corpus = corpus },
|
||||||
upstream = {},
|
upstream = {},
|
||||||
out_root = args.out_root,
|
out_root = args.out_root,
|
||||||
project_root = args.project_root,
|
project_root = corpus.project_root,
|
||||||
flags = args.flags or {},
|
flags = args.flags or {},
|
||||||
dry_run = args.dry_run,
|
dry_run = args.dry_run,
|
||||||
verbose = args.verbose,
|
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
|
end
|
||||||
|
|
||||||
-- ════════════════════════════════════════════════════════════════════════════
|
-- ════════════════════════════════════════════════════════════════════════════
|
||||||
@@ -543,14 +674,14 @@ end
|
|||||||
|
|
||||||
--- Topologically sort the requested pass set, augmented with all transitive deps.
|
--- Topologically sort the requested pass set, augmented with all transitive deps.
|
||||||
--- Detects cycles and errors out with details.
|
--- Detects cycles and errors out with details.
|
||||||
--- @param passes table<string, PassDescriptor>
|
--- @param passes table<string, PassDescriptor>
|
||||||
--- @param requested_set string[]
|
--- @param requested_set string[]
|
||||||
--- @return string[] -- execution order
|
--- @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.
|
--- Dependency closure, in-degree calculation, queue seeding, and sorting are local blocks.
|
||||||
--- Each was a 1-caller helper; the 2-caller rule doesn't apply, so inlining produces a single readable function
|
--- Keeping these blocks local makes the topological sort self-contained.
|
||||||
local function topo_sort(passes, requested_set)
|
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 = {}
|
local needed = {}
|
||||||
for _, name in ipairs(requested_set) do needed[name] = true end
|
for _, name in ipairs(requested_set) do needed[name] = true end
|
||||||
local changed = true
|
local changed = true
|
||||||
@@ -570,7 +701,7 @@ local function topo_sort(passes, requested_set)
|
|||||||
end
|
end
|
||||||
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 = {}
|
local in_degree = {}
|
||||||
for name, _ in pairs(needed) do in_degree[name] = 0 end
|
for name, _ in pairs(needed) do in_degree[name] = 0 end
|
||||||
for name, _ in pairs(needed) do
|
for name, _ in pairs(needed) do
|
||||||
@@ -581,14 +712,14 @@ local function topo_sort(passes, requested_set)
|
|||||||
end
|
end
|
||||||
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 = {}
|
local ready = {}
|
||||||
for name, deg in pairs(in_degree) do
|
for name, deg in pairs(in_degree) do
|
||||||
if deg == 0 then ready[#ready + 1] = name end
|
if deg == 0 then ready[#ready + 1] = name end
|
||||||
end
|
end
|
||||||
table.sort(ready)
|
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).
|
-- Newly-zero-degree passes are inserted back into the ready queue (kept sorted).
|
||||||
local order = {}
|
local order = {}
|
||||||
while #ready > 0 do
|
while #ready > 0 do
|
||||||
@@ -629,29 +760,8 @@ end
|
|||||||
-- ASCII dep graph renderer (Decision 6 in the spec)
|
-- 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).
|
-- 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;
|
-- 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.
|
-- 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
|
end
|
||||||
|
|
||||||
-- ════════════════════════════════════════════════════════════════════════════
|
-- ════════════════════════════════════════════════════════════════════════════
|
||||||
-- Main orchestrator
|
-- Main Orchestrator
|
||||||
-- ════════════════════════════════════════════════════════════════════════════
|
-- ════════════════════════════════════════════════════════════════════════════
|
||||||
|
|
||||||
-- (internal) Push a pass's outputs + warnings into `ctx.upstream[name]` for downstream passes to consume.
|
--- (internal) Push a pass's outputs + warnings into `ctx.upstream[name]` for downstream passes to consume.
|
||||||
-- @param ctx PassCtx
|
--- @param ctx PassCtx
|
||||||
-- @param pass_name string
|
--- @param pass_name string
|
||||||
-- @param result PassResult
|
--- @param result PassResult
|
||||||
local function accumulate_pass_result(ctx, pass_name, result)
|
local function accumulate_pass_result(ctx, pass_name, result)
|
||||||
ctx.upstream[pass_name] = ctx.upstream[pass_name] or {}
|
ctx.upstream[pass_name] = ctx.upstream[pass_name] or {}
|
||||||
for _, out in ipairs(result.outputs or {}) do
|
for _, out in ipairs(result.outputs or {}) do table.insert(ctx.upstream[pass_name], out) end
|
||||||
table.insert(ctx.upstream[pass_name], out)
|
for _, warn in ipairs(result.warnings or {}) do table.insert(ctx.upstream[pass_name], warn) end
|
||||||
end
|
|
||||||
for _, warn in ipairs(result.warnings or {}) do
|
|
||||||
table.insert(ctx.upstream[pass_name], warn)
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
|
|
||||||
-- (internal) If the pass's kind is in PASS_KIND_STOP_ON_ERROR and it reported errors, write each error to stderr.
|
--- (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.
|
--- Returns true if any validation errors were reported.
|
||||||
-- @param pass_name string
|
--- @param pass_name string
|
||||||
-- @param pass PassDescriptor
|
--- @param pass PassDescriptor
|
||||||
-- @param result PassResult
|
--- @param result PassResult
|
||||||
-- @return boolean
|
--- @return boolean
|
||||||
local function report_validation_errors(pass_name, pass, result)
|
local function report_validation_errors(pass_name, pass, result)
|
||||||
local has_errors = result.errors and #result.errors > 0
|
local has_errors = result.errors and #result.errors > 0
|
||||||
if not (has_errors and PASS_KIND_STOP_ON_ERROR[pass.kind]) then
|
if not (has_errors and PASS_KIND_STOP_ON_ERROR[pass.kind]) then return false end
|
||||||
return false
|
|
||||||
end
|
|
||||||
for _, e in ipairs(result.errors) do
|
for _, e in ipairs(result.errors) do
|
||||||
io.stderr:write(string.format("[%s] line %d: %s\n",
|
io.stderr:write(string.format("[%s] line %d: %s\n", pass_name, e.line or 0, e.msg or ""))
|
||||||
pass_name, e.line or 0, e.msg or ""))
|
|
||||||
end
|
end
|
||||||
return true
|
return true
|
||||||
end
|
end
|
||||||
|
|
||||||
-- (internal) Run each pass in `order` in topological sequence.
|
--- (internal) Run each pass in `order` in topological sequence.
|
||||||
-- @param ctx PassCtx
|
--- @param ctx PassCtx
|
||||||
-- @param order string[]
|
--- @param order string[]
|
||||||
-- @return boolean -- true if any validation errors were reported
|
--- @return boolean -- true if any validation errors were reported
|
||||||
local function dispatch_passes(ctx, order)
|
local function dispatch_passes(ctx, order)
|
||||||
ctx.shared = {}
|
ctx.shared = ctx.shared or {}
|
||||||
local had_errors = false
|
local had_errors = false
|
||||||
for _, pass_name in ipairs(order) do
|
for _, pass_name in ipairs(order) do
|
||||||
local pass = PASSES[pass_name]
|
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 mod = require(pass.module)
|
||||||
local result = mod.run(ctx)
|
local result = mod.run(ctx)
|
||||||
|
|
||||||
accumulate_pass_result(ctx, pass_name, result)
|
accumulate_pass_result(ctx, pass_name, result)
|
||||||
if report_validation_errors(pass_name, pass, result) then
|
if report_validation_errors(pass_name, pass, result) then
|
||||||
had_errors = true
|
had_errors = true
|
||||||
@@ -755,14 +857,15 @@ local function main(argv)
|
|||||||
end
|
end
|
||||||
|
|
||||||
-- Module export for in-process consumers (tests that dofile this script).
|
-- 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
|
-- 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.
|
||||||
-- 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");
|
-- 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.
|
-- in dofile() mode (test's arg[0] does not match), main() is skipped and the chunk returns `_M` to the caller.
|
||||||
local _M = {
|
local _M = {
|
||||||
render_dep_order = render_dep_order,
|
render_dep_order = render_dep_order,
|
||||||
PASSES = PASSES,
|
PASSES = PASSES,
|
||||||
PASS_KIND_STOP_ON_ERROR = PASS_KIND_STOP_ON_ERROR,
|
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
|
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)
|
# PCSX-Redux — built via MSBuild (VS2022)
|
||||||
#
|
|
||||||
# Requires: Visual Studio 2022 with the C++ desktop workload.
|
# 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
|
# 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
|
& $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;
|
# 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
|
# we use `scoop prefix` to find the install root for the include dir (needed to compile lpeg against luajit's headers).
|
||||||
# (needed to compile lpeg against luajit's headers).
|
|
||||||
# If scoop or luajit is missing, fail fast with an actionable message.
|
# If scoop or luajit is missing, fail fast with an actionable message.
|
||||||
$luajit_prefix = & scoop prefix luajit 2>$null
|
$luajit_prefix = & scoop prefix luajit 2>$null
|
||||||
if (-not $luajit_prefix -or -not (Test-Path (Join-Path $luajit_prefix 'bin/luajit.exe'))) {
|
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.
|
# Generate lpeg.dll by compiling the 6 source files directly.
|
||||||
# `gcc` is on PATH (scoop's shim puts it there).
|
# `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
|
# 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_*).
|
# 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'
|
$luajit_lib_dir = Join-Path $luajit_prefix 'lib'
|
||||||
$lpeg_sources = @('lpcap.c', 'lpcode.c', 'lpcset.c', 'lpprint.c', 'lptree.c', 'lpvm.c')
|
$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.
|
# 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
|
# Source: toolchain/pcsx-redux/third_party/luafilesystem/src/lfs.c
|
||||||
# Output: toolchain/lfs/lfs.dll
|
# 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 — 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
|
# Output: toolchain\pcsx-redux\src\mips\openbios\openbios.bin
|
||||||
# ════════════════════════════════════════════════════════════════════════════
|
# ════════════════════════════════════════════════════════════════════════════
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user