mirror of
https://github.com/Ed94/pikuma_ps1.git
synced 2026-08-06 15:48:49 +00:00
Compare commits
7
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
590ff1e2ec | ||
|
|
653e18ee28 | ||
|
|
ebb876fe89 | ||
|
|
1b40b16c0e | ||
|
|
9ffd6592bc | ||
|
|
d56adab38f | ||
|
|
08af73d0d2 |
@@ -60,7 +60,7 @@ enum {
|
||||
MipsAtom_(tape_exit) { jump_reg(rret_addr), nop };
|
||||
|
||||
/* Generalized Tape Engine Runner */
|
||||
FI_ void tape_run(Slice_U4 tape) { register U4* tp rgcc(R_TapePtr) = tape.ptr; asm volatile(
|
||||
FI_ void tape_run(Slice_MipsCode tape) { register U4* tp rgcc(R_TapePtr) = u4_r(tape.ptr); asm volatile(
|
||||
asm_words(
|
||||
add_ui( R_SP, R_SP, -MipsStackAlignment) /* Allocate stack space */
|
||||
, store_word( R_RA, R_SP, 0) /* Safely backup $ra to the stack */
|
||||
@@ -91,8 +91,8 @@ FI_ TapeBuilder tb_make(Slice mem) { return (TapeBuilder){ mem.ptr, mem.len, 0 }
|
||||
FI_ void tb_emit(TapeBuilder* tb, MipsCode* atom) { u4_r(tb->ptr)[tb->used] = u4_(atom); ++ tb->used; }
|
||||
FI_ void tb_data(TapeBuilder* tb, U4 data) { u4_r(tb->ptr)[tb->used] = u4_(data); ++ tb->used; }
|
||||
|
||||
FI_ Slice_U4 tb_end (TapeBuilder* tb) { tb_emit(tb,tape_exit); return (Slice_U4){ C_(U4*,tb->ptr), tb->used }; }
|
||||
FI_ Slice_U4 tb_slice(TapeBuilder tb) { return (Slice_U4){ C_(U4*,tb.ptr), tb.used }; }
|
||||
FI_ Slice_MipsCode tb_end (TapeBuilder* tb) { tb_emit(tb,tape_exit); return (Slice_MipsCode){ C_(U4*,tb->ptr), tb->used }; }
|
||||
FI_ Slice_MipsCode tb_slice(TapeBuilder tb) { return (Slice_MipsCode){ C_(U4*,tb.ptr), tb.used }; }
|
||||
#define tb_scope(tb) for(U4 tbs_once=0;tbs_once==0;++tbs_once,tb_emit(tb,tape_exit))
|
||||
|
||||
#pragma endregion Tape Drive
|
||||
@@ -104,6 +104,7 @@ FI_ Slice_U4 tb_slice(TapeBuilder tb) { return (Sli
|
||||
* ---------------------------------------------------------------------------*/
|
||||
|
||||
// The 'Yield' sequence for Tape Atoms (mac_yield).
|
||||
atom_dbg_skip_over()
|
||||
MipsAtomComp_(ac_yield) {
|
||||
load_word(R_AtomJmp, R_TapePtr, 0),
|
||||
add_ui_self( R_TapePtr, S_(MipsCode)),
|
||||
|
||||
@@ -103,7 +103,7 @@ FI_ void farena_init(FArena_R arena, Slice mem) { assert(arena != nullptr);
|
||||
arena->used = 0;
|
||||
}
|
||||
FI_ FArena farena_make(Slice mem) { FArena a; farena_init(& a, mem); return a; }
|
||||
I_ Slice farena_push(FArena_R arena, U4 amount, Opt_farena o) {
|
||||
I_ Slice farena_push(FArena_R arena, U4 amount, Opt_farena o) {
|
||||
if (amount == 0) { return (Slice){}; }
|
||||
U4 desired = amount * (o.type_width == 0 ? 1 : o.type_width);
|
||||
U4 to_commit = align_pow2(desired, o.alignment ? o.alignment : MEM_ALIGNMENT_DEFAULT);
|
||||
|
||||
@@ -15,9 +15,9 @@ enum {
|
||||
atom_offset_bounds_chk_cube_g4_face_exit = _atom_offset_bounds_chk_cube_g4_face_exit,
|
||||
};
|
||||
|
||||
// --- atom: floor_f3_face (66 words) ---
|
||||
// --- atom: floor_f3_face (58 words) ---
|
||||
|
||||
#define _atom_offset_culling_floor_f3_face_exit 29
|
||||
#define _atom_offset_culling_floor_f3_face_exit 25
|
||||
#define _atom_offset_bounds_chk_floor_f3_face_exit 13
|
||||
|
||||
enum {
|
||||
|
||||
+14
-17
@@ -1,6 +1,6 @@
|
||||
#include "stdio.h"
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include "assert.h"
|
||||
#include <assert.h>
|
||||
// #include "libgpu.h"
|
||||
// #include "libetc.h"
|
||||
// #include "libgte.h"
|
||||
@@ -99,8 +99,13 @@ typedef Struct_(Ent_Floor) {
|
||||
A2_V3_S2 faces;
|
||||
};
|
||||
|
||||
enum { scratchpad_size = 1024, };
|
||||
enum {
|
||||
Scratchpad_Len = 1024,
|
||||
MemTape_Len = 512,
|
||||
};
|
||||
typedef Struct_(SMemory) {
|
||||
U4 MemTape[MemTape_Len];
|
||||
|
||||
DoubleBuffer screen_buf;
|
||||
A2_OrderingTable_Buffer ordering_tbl;
|
||||
PrimitiveArena primitives;
|
||||
@@ -207,6 +212,8 @@ void update(PrimitiveArena* pa, U4* ordering_buf)
|
||||
A2_S2 p; //???
|
||||
S4 flag; //????
|
||||
|
||||
TapeBuilder tb = tb_make(slice_ut_arr(smem.MemTape));
|
||||
|
||||
// Draw Cube
|
||||
if (0)
|
||||
{
|
||||
@@ -259,8 +266,7 @@ void update(PrimitiveArena* pa, U4* ordering_buf)
|
||||
U4 prim_base = u4_(pa->buf[smem.active_buf_id]);
|
||||
U4 prim_cursor = prim_base + pa->used;
|
||||
|
||||
LP_ U4 mem_temp_tape[512];
|
||||
TapeBuilder tb = tb_make(slice_ut_arr(mem_temp_tape)); tb_scope(& tb) {
|
||||
tb.used = 0; tb_scope(& tb) {
|
||||
tb_emit(& tb, rbind_cube_g4_face);
|
||||
tb_data(& tb, prim_cursor);
|
||||
tb_data(& tb, u4_(smem.cube.faces));
|
||||
@@ -344,12 +350,11 @@ void update(PrimitiveArena* pa, U4* ordering_buf)
|
||||
U4 prim_base = u4_(pa->buf[smem.active_buf_id]);
|
||||
U4 prim_cursor = prim_base + pa->used;
|
||||
|
||||
// TODO(Ed): We should do a bounds check beforehand to confirm pa can hold all tris.
|
||||
// TODO(Ed): We should do a bounds check beforehand to confirm pa can hold all tris?
|
||||
// The tape atoms in-flight should not need to care.
|
||||
|
||||
// Prepare the tape. (Push protocol to tape)
|
||||
LP_ U4 mem_temp_tape[512];
|
||||
TapeBuilder tb = tb_make(slice_ut_arr(mem_temp_tape)); tb_scope(& tb) {
|
||||
tb.used = 0; tb_scope(& tb) {
|
||||
tb_emit(& tb, set_gte_world);
|
||||
tb_data(& tb, u4_(& smem.tform_world));
|
||||
|
||||
@@ -367,25 +372,18 @@ void update(PrimitiveArena* pa, U4* ordering_buf)
|
||||
tb_data(& tb, u4_(& pa->used));
|
||||
tb_data(& tb, prim_base);
|
||||
}
|
||||
|
||||
tape_run(tb_slice(tb));// Fire off the tape.
|
||||
|
||||
// C-side state (pa->used) has already been updated by the tape!
|
||||
smem.floor.rot.y += 5;
|
||||
}
|
||||
// --- TAPE DIAGNOSTICS ---
|
||||
if (1)
|
||||
if (0)
|
||||
{
|
||||
LP_ U4 mem_temp_tape[512]; FArena tape_arena; farena_init(& tape_arena, slice_ut_arr(mem_temp_tape));
|
||||
TapeBuilder tb = tb_make_old(& tape_arena); tb_scope(& tb) {
|
||||
// Skip set_gte_world atom for diagnostics to isolate the triangle loop
|
||||
for (U4 i = 0; i < Floor_num_faces; i++) {
|
||||
// =======================================================
|
||||
// SWAP EMIT TO TEST DIFFERENT PARTS OF THE PIPELINE:
|
||||
// =======================================================
|
||||
// 1. code_diag_yield -> Tests Tape Engine jump logic
|
||||
// 2. code_diag_color -> Tests OT and Prim Arena memory
|
||||
// 3. code_diag_gte -> Tests Vertex arrays and GTE Math
|
||||
// tb_emit(& tb, code_diag_yield);
|
||||
// tb_emit(& tb, code_diag_color);
|
||||
// tb_emit(& tb, code_diag_gte);
|
||||
@@ -394,7 +392,6 @@ void update(PrimitiveArena* pa, U4* ordering_buf)
|
||||
B1* prim_cursor = (B1*)r_(pa->buf)[smem.active_buf_id] + pa->used;
|
||||
tape_run(tb_slice(tb));
|
||||
pa->used = (U4)prim_cursor - (U4)r_(pa->buf)[smem.active_buf_id];
|
||||
smem.floor.rot.y += 5;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -103,28 +103,27 @@ MipsAtom_(rbind_floor_f3_face) atom_info(atom_bind(Binds_FloorTri), atom_phase(f
|
||||
mac_yield()
|
||||
};
|
||||
|
||||
// atom_dbg_skip_over()
|
||||
internal
|
||||
atom_dbg_skip_over()
|
||||
MipsAtom_(floor_f3_face) atom_info(atom_phase(floor_f3)
|
||||
, atom_reads( R_PrimCursor, R_FaceCursor, R_VertBase, R_OtBase)
|
||||
, atom_writes(R_PrimCursor, R_FaceCursor)
|
||||
, atom_writes(R_PrimCursor, R_FaceCursr)
|
||||
) {
|
||||
mac_load_tri_indices( R_T0, R_T1, R_T2),
|
||||
mac_gte_load_tri_verts(R_T0, R_T1, R_T2),
|
||||
nop2, gte_cmdw_rotate_translate_perspective_triple,
|
||||
nop2, gte_cmdw_nclip,
|
||||
nop2, gte_cmdw_rotate_translate_perspective_triple, // 2 nops retire the final cpu -> gte writes before RTPT
|
||||
gte_cmdw_nclip,
|
||||
|
||||
/* Culling (Branch forward if Backface) */
|
||||
nop2, gte_mv_from_data_r(R_T0, C2_MAC0),
|
||||
nop,
|
||||
branch_le_zero(R_T0, atom_offset(culling, floor_f3_face_exit)), nop,
|
||||
gte_mv_from_data_r(R_T0, C2_MAC0),
|
||||
nop, branch_le_zero(R_T0, atom_offset(culling, floor_f3_face_exit)), nop, // required gte -> cpu load-delay slot.
|
||||
/* Format Primitive */
|
||||
mac_format_f3_color(0xFF, 0xFF, 0xFF), // RGB-form (R=FF, G=FF, B=FF = white)
|
||||
mac_gte_store_f3_post_rtpt(),
|
||||
|
||||
/* Calculate Depth */
|
||||
nop2, gte_avg_sort_z3,
|
||||
nop2, gte_mv_from_data_r(R_T1, C2_OTZ),
|
||||
gte_avg_sort_z3,
|
||||
gte_mv_from_data_r(R_T1, C2_OTZ),
|
||||
/* Bounds Check OTZ < 2048 (Branch forward to skip insertion) */
|
||||
add_ui( R_AT, R_0, OrderingTbl_Len),
|
||||
set_lt_u( R_AT, R_T1, R_AT),
|
||||
|
||||
+62
-58
@@ -81,19 +81,6 @@ $path_psyq = join-path $path_toolchain 'psyq-4_7'
|
||||
$path_psyq_iwyu = join-path $path_toolchain 'psyq_iwyu'
|
||||
$path_psyq_imyu_inc = join-path $path_psyq_iwyu 'include'
|
||||
|
||||
function Get-SourceFiles { param([Parameter(Mandatory=$true)] [string[]]$paths, [Parameter(Mandatory=$true)] [string[]]$extensions)
|
||||
$files = @()
|
||||
foreach ($p in $paths) {
|
||||
if (-not (test-path $p)) { continue }
|
||||
foreach ($ext in $extensions) {
|
||||
Get-ChildItem -Path $p -File -Recurse -Filter "*$ext" -ErrorAction SilentlyContinue | ForEach-Object {
|
||||
$files += $_.FullName
|
||||
}
|
||||
}
|
||||
}
|
||||
return ($files | Sort-Object -Unique)
|
||||
}
|
||||
|
||||
function assemble-unit { param(
|
||||
[string] $unit,
|
||||
[string] $link_module,
|
||||
@@ -243,6 +230,52 @@ function make-binary { param([string]$elf, [string]$exe)
|
||||
if ($LASTEXITCODE -ne 0) { Write-Error "Objcopy failed. Aborting."; exit 1 }
|
||||
}
|
||||
|
||||
function ps1-meta { param(
|
||||
[string]$unity_root,
|
||||
[string[]]$sources,
|
||||
[Parameter(Mandatory=$true)][string]$metadata,
|
||||
[string]$out_root = (join-path $path_build 'gen'),
|
||||
[string[]]$passes = @('--pre-link'),
|
||||
[string[]]$extra_args = @()
|
||||
)
|
||||
# `--unity-root` and `--source` are
|
||||
# mutually exclusive. Exactly one of `$unity_root` / `$sources` must
|
||||
# be supplied; the other must be absent.
|
||||
if ($null -ne $unity_root -and $unity_root -ne '')
|
||||
{
|
||||
if ($null -ne $sources -and $sources.Count -gt 0) {
|
||||
write-error 'ps1-meta: -unity_root and -sources are mutually exclusive'
|
||||
exit 2
|
||||
}
|
||||
}
|
||||
elseif ($null -eq $sources -or $sources.Count -eq 0) {
|
||||
write-error 'ps1-meta: either -unity_root <file> or -sources <file...> is required'
|
||||
exit 2
|
||||
}
|
||||
|
||||
$script = join-path $path_scripts 'ps1_meta.lua'
|
||||
$input_summary = if ($null -ne $unity_root -and $unity_root -ne '') {
|
||||
"unity=$unity_root"
|
||||
}
|
||||
else {
|
||||
"$($sources.Count) source(s)"
|
||||
}
|
||||
write-host "ps1-meta $input_summary, passes=$($passes -join ',')" ` -ForegroundColor Magenta
|
||||
|
||||
$arg_list = @($passes) + @('--metadata', $metadata) + @('--out-root', $out_root) + @($extra_args)
|
||||
if ($null -ne $unity_root -and $unity_root -ne '') {
|
||||
$arg_list += @('--unity-root', $unity_root)
|
||||
}
|
||||
else {
|
||||
foreach ($s in $sources) { $arg_list += @('--source', $s) }
|
||||
}
|
||||
& luajit $script @arg_list
|
||||
if ($LASTEXITCODE -ne 0) {
|
||||
write-error "ps1-meta failed (exit $LASTEXITCODE). Aborting."
|
||||
exit $LASTEXITCODE
|
||||
}
|
||||
}
|
||||
|
||||
function build-hello_psyqo {
|
||||
$includes += @()
|
||||
|
||||
@@ -317,34 +350,16 @@ function build-graphis_hello {
|
||||
}
|
||||
# build-graphis_hello
|
||||
|
||||
function ps1-meta { param(
|
||||
[Parameter(Mandatory=$true)][string[]]$sources,
|
||||
[Parameter(Mandatory=$true)][string]$metadata,
|
||||
[string]$out_root = (join-path $path_build 'gen'),
|
||||
[string[]]$passes = @('--pre-link'),
|
||||
[string[]]$extra_args = @()
|
||||
)
|
||||
$script = join-path $path_scripts 'ps1_meta.lua'
|
||||
write-host "ps1-meta $($sources.Count) source(s), passes=$($passes -join ',')" ` -ForegroundColor Magenta
|
||||
$arg_list = @($passes) + @('--metadata', $metadata) + @('--out-root', $out_root) + @($extra_args)
|
||||
foreach ($s in $sources) { $arg_list += @('--source', $s) }
|
||||
& luajit $script @arg_list
|
||||
if ($LASTEXITCODE -ne 0) {
|
||||
write-error "ps1-meta failed (exit $LASTEXITCODE). Aborting."
|
||||
exit $LASTEXITCODE
|
||||
}
|
||||
}
|
||||
|
||||
function build-gte_hello {
|
||||
$includes += @()
|
||||
|
||||
$path_module = join-path $path_code 'gte_hello'
|
||||
$path_duffle = join-path $path_code 'duffle'
|
||||
$path_atom_metadata = join-path $path_duffle 'word_count.metadata.h'
|
||||
$path_build_gen = join-path $path_build 'gen'
|
||||
|
||||
$source_dirs = @($path_duffle, $path_module)
|
||||
$atom_sources = Get-SourceFiles -paths $source_dirs -extensions @('.h', '.c')
|
||||
ps1-meta -sources $atom_sources -metadata $path_atom_metadata -out_root (join-path $path_build 'gen')
|
||||
$src_c = join-path $path_module 'hello_gte.c'
|
||||
ps1-meta -unity_root $src_c -metadata $path_atom_metadata -out_root $path_build_gen
|
||||
|
||||
$assemble_args = @()
|
||||
$assemble_args += $f_debug
|
||||
@@ -360,7 +375,6 @@ function build-gte_hello {
|
||||
|
||||
# assemble-unit $src_asm $module_asm $includes $assemble_args
|
||||
|
||||
$src_c = join-path $path_module 'hello_gte.c'
|
||||
$module_c = join-path $path_build 'hello_gte_c.o'
|
||||
|
||||
$compile_args = @()
|
||||
@@ -386,27 +400,17 @@ function build-gte_hello {
|
||||
make-binary $elf $exe
|
||||
|
||||
# Post-link: gdb-runtime + dwarf-injection in a single Lua invocation (one luajit cold start).
|
||||
ps1-meta -sources $atom_sources -metadata $path_atom_metadata `
|
||||
-out_root (join-path $path_build 'gen') `
|
||||
-passes @('--post-link') `
|
||||
-extra_args @('--elf', $elf)
|
||||
ps1-meta -unity_root $src_c -metadata $path_atom_metadata -out_root $path_build_gen -passes @('--post-link') ` -extra_args @('--elf', $elf)
|
||||
|
||||
# F' + G' splice: collapse 9 objcopy subprocess invocations into 3.
|
||||
# - 1 call: 3x --update-section for F' (line / aranges / rnglists)
|
||||
# - 1 call: 3x --update-section for G' (info / abbrev / str)
|
||||
# - 1 call: 2x --add-section for G' (loc / loclists — these don't exist in the source ELF)
|
||||
# - 1 call: 1x --set-section-flags (.rodata / .data enable code flag)
|
||||
# = 4 objcopy calls (was 9; saved 5 spawns).
|
||||
$dwarfLineBin = join-path (join-path $path_build 'gen') 'hello_gte.dwarf_line.bin'
|
||||
$dwarfArangesBin = join-path (join-path $path_build 'gen') 'hello_gte.dwarf_aranges.bin'
|
||||
$dwarfRnglistsBin = join-path (join-path $path_build 'gen') 'hello_gte.dwarf_rnglists.bin'
|
||||
$dwarfLineBin = join-path $path_build_gen 'hello_gte.dwarf_line.bin'
|
||||
$dwarfArangesBin = join-path $path_build_gen 'hello_gte.dwarf_aranges.bin'
|
||||
$dwarfRnglistsBin = join-path $path_build_gen 'hello_gte.dwarf_rnglists.bin'
|
||||
$injectElf = join-path $path_build 'hello_gte.dwarf-injected.elf'
|
||||
if ((Test-Path $dwarfLineBin) -and (Test-Path $dwarfArangesBin) -and (Test-Path $dwarfRnglistsBin))
|
||||
{
|
||||
Write-Host "[build] DWARF-injecting $elf -> $injectElf"
|
||||
Copy-Item -LiteralPath $elf -Destination $injectElf -Force
|
||||
|
||||
# Single objcopy call: 3x --update-section for F' (line, aranges, rnglists).
|
||||
# Objcopy call: 3x --update-section for (line, aranges, rnglists).
|
||||
$f_args = @(
|
||||
"--update-section=.debug_line=$dwarfLineBin",
|
||||
"--update-section=.debug_aranges=$dwarfArangesBin",
|
||||
@@ -419,12 +423,11 @@ function build-gte_hello {
|
||||
return;
|
||||
}
|
||||
|
||||
# G' 5-section splice: 3 update-section (info / abbrev / str) + 2 add-section (loc / loclists).
|
||||
$dwarfInfoBin = join-path (join-path $path_build 'gen') 'hello_gte.dwarf_info.bin'
|
||||
$dwarfAbbrevBin = join-path (join-path $path_build 'gen') 'hello_gte.dwarf_abbrev.bin'
|
||||
$dwarfStrBin = join-path (join-path $path_build 'gen') 'hello_gte.dwarf_str.bin'
|
||||
$dwarfLocBin = join-path (join-path $path_build 'gen') 'hello_gte.dwarf_loc.bin'
|
||||
$dwarfLoclistsBin = join-path (join-path $path_build 'gen') 'hello_gte.dwarf_loclists.bin'
|
||||
$dwarfInfoBin = join-path $path_build_gen 'hello_gte.dwarf_info.bin'
|
||||
$dwarfAbbrevBin = join-path $path_build_gen 'hello_gte.dwarf_abbrev.bin'
|
||||
$dwarfStrBin = join-path $path_build_gen 'hello_gte.dwarf_str.bin'
|
||||
$dwarfLocBin = join-path $path_build_gen 'hello_gte.dwarf_loc.bin'
|
||||
$dwarfLoclistsBin = join-path $path_build_gen 'hello_gte.dwarf_loclists.bin'
|
||||
$g_args = @(
|
||||
"--update-section=.debug_info=$dwarfInfoBin",
|
||||
"--update-section=.debug_abbrev=$dwarfAbbrevBin",
|
||||
@@ -441,7 +444,7 @@ function build-gte_hello {
|
||||
|
||||
# Baked atoms execute from RAM but are emitted as C data arrays, so their ELF sections lack SHF_EXECINSTR.
|
||||
# GDB discards line rows for non-code sections. Mark only the debug-copy sections executable.
|
||||
# The shipping ELF and PS-EXE remain byte/flag unchanged.
|
||||
# The original ELF and PS-EXE remain byte/flag unchanged.
|
||||
& $Objcopy `
|
||||
--set-section-flags ".rodata=alloc,load,readonly,code,contents" `
|
||||
--set-section-flags ".data=alloc,load,data,code,contents" `
|
||||
@@ -449,7 +452,8 @@ function build-gte_hello {
|
||||
if ($LASTEXITCODE -ne 0) {
|
||||
Write-Warning "[build] atom-section flag update failed (exit $LASTEXITCODE); removing $injectElf"
|
||||
Remove-Item -LiteralPath $injectElf -ErrorAction SilentlyContinue
|
||||
} else {
|
||||
}
|
||||
else {
|
||||
Write-Host "[build] DWARF-injected ELF: $injectElf"
|
||||
}
|
||||
}
|
||||
|
||||
+1762
-200
File diff suppressed because it is too large
Load Diff
@@ -11,11 +11,10 @@
|
||||
--- ```
|
||||
---
|
||||
--- That small bootstrap: (a) locates this helper via `arg[0]` / `debug.getinfo`,
|
||||
--- (b) loads it (which sets `package.path` + `package.cpath` via cached `git rev-parse`),
|
||||
--- (b) loads it (which sets `package.path` + `package.cpath`),
|
||||
--- (c) at the bottom calls `require("duffle")` (now resolvable since `package.path` was just set) and returns the duffle M.
|
||||
--- Net effect: the caller gets the duffle module in one statement; no separate `dofile(...)` + `require("duffle")` dance.
|
||||
---
|
||||
--- Replaces the prior 2-line (entry) or 4-line (pass) pattern that had the call site do its own path resolution + duplicated setup.
|
||||
|
||||
local M = {}
|
||||
|
||||
@@ -27,9 +26,6 @@ local CACHE_KEY = "__duffle_repo_root__"
|
||||
--- parent of the directory containing this script. We derive it directly from `debug.getinfo(1, "S").source`
|
||||
--- (returns `@<path>` for the currently-running chunk).
|
||||
---
|
||||
--- Replaces the prior `io.popen("git rev-parse --show-toplevel")` approach, which cost ~100-180ms per
|
||||
--- LuaJIT process on Windows due to git's CLI startup. The path-derive approach costs <1ms.
|
||||
---
|
||||
--- If `debug.getinfo` can't parse this script's path (shouldn't happen — dofile always populates source),
|
||||
--- return nil and let `M.setup()` fail loud.
|
||||
--- @return string|nil
|
||||
@@ -61,7 +57,12 @@ end
|
||||
function M.setup()
|
||||
local repo_root = find_repo_root()
|
||||
if not repo_root then
|
||||
io.stderr:write("[duffle_paths] git rev-parse failed -- not in a git repo?\n")
|
||||
-- Unreachable in practice: find_repo_root() derives the repo root from this script's
|
||||
-- own source path via debug.getinfo(1, "S").source (no subprocess, no git CLI, <1ms).
|
||||
-- A nil return means the source path did not match the expected
|
||||
-- <repo>/scripts/duffle_paths.lua layout — a packaging bug, not a "missing git repo"
|
||||
-- condition. os.exit(2) is retained so a real failure surfaces loud rather than
|
||||
-- silently producing an unconfigured module table.
|
||||
os.exit(2)
|
||||
end
|
||||
|
||||
|
||||
+6
-137
@@ -240,7 +240,7 @@ end
|
||||
|
||||
-- Pure-Lua 5.3 LEB128 readers (no `bit` library). `2^shift` arithmetic matches the existing parser.
|
||||
-- Offsets are 0-based; returns (value, next_pos).
|
||||
-- Track A Task 10: promoted from `local function` to M.* exports so passes/dwarf_injection.lua
|
||||
-- Promoted from `local function` to M.* exports so passes/dwarf_injection.lua
|
||||
-- can import them as file-scope locals per the 2nd-caller lift precedent
|
||||
-- (the uleb128 + sleb128 encoders were promoted the same way).
|
||||
function M.read_uleb128_at(buf, pos)
|
||||
@@ -404,10 +404,8 @@ local function read_form_value(buf, str_buf, pos, form)
|
||||
end
|
||||
|
||||
--- Read a `DW_FORM_ref_sig8` value at 0-based offset `pos` from `buf`.
|
||||
--- Returns the low 4 bytes (LE) as `low`, the high 4 bytes (LE) as `high`, and
|
||||
--- the cursor position after the 8-byte value as `next_pos`.
|
||||
--- Callers that need the full type-unit + type-offset pair
|
||||
--- (e.g. to resolve a type identifier embedded as a signature)
|
||||
--- Returns the low 4 bytes (LE) as `low`, the high 4 bytes (LE) as `high`, and the cursor position after the 8-byte value as `next_pos`.
|
||||
--- Callers that need the full type-unit + type-offset pair (e.g. to resolve a type identifier embedded as a signature)
|
||||
--- should use this directly rather than going through `read_form_value`,
|
||||
--- which only exposes the low 4 bytes to preserve its existing (value, next_pos) return shape.
|
||||
--- @param buf string
|
||||
@@ -427,8 +425,7 @@ end
|
||||
--
|
||||
-- Unit header layout (from pos 0):
|
||||
-- unit_length(4) + version(2) + unit_type(1) + address_size(1) + debug_abbrev_offset(4)
|
||||
-- -- followed by type_unit_specific fields:
|
||||
-- type_signature(8) + type_offset(4)
|
||||
-- followed by type_unit_specific fields: type_signature(8) + type_offset(4)
|
||||
-- The type_signature is at byte offset 8 of the body (right after debug_abbrev_offset).
|
||||
-- @param info string -- the .debug_info section bytes
|
||||
-- @param target_sig_lo integer -- low 4 bytes (LE) of the desired signature
|
||||
@@ -619,7 +616,7 @@ end
|
||||
--- - ELF32 symtab entry = 16 bytes (`st_name:4 + st_value:4 + st_size:4 + st_info:1 + st_other:1 + st_shndx:2`); offsets within each entry are zero-based wire offsets.
|
||||
--- - Direct Lua `string.byte`/`string.sub`/`string.find` boundaries receive `+ 1`.
|
||||
--- - We filter on STB_GLOBAL (high nibble of st_info = 1) to match `nm`'s default (external symbols only). STB_WEAK excluded.
|
||||
--- - We strip the `code_` prefix to match the previous `read_nm` output.
|
||||
--- - The `code_` prefix is stripped (MipsAtom_ macros emit bare atom names, no `code_` prefix).
|
||||
--- - `st_size > 0` filter excludes undefined/imported symbols.
|
||||
--- @param elf_path Path
|
||||
--- @return table<string, {integer, integer}>
|
||||
@@ -657,7 +654,7 @@ function M.read_nm(elf_path)
|
||||
-- Extract the name from .strtab (null-terminated C string).
|
||||
local name_end = strtab:find("\0", st_name_off + 1, true) or (st_name_off + 1)
|
||||
local name = strtab:sub(st_name_off + 1, name_end - 1)
|
||||
-- Filter: keep all symbol-table symbols (atoms emit their name as the bare `<name>` since the `code_` prefix was removed from the MipsAtom_ macro).
|
||||
-- Filter: keep all symbol-table symbols (atoms emit their name as the bare `<name>` — MipsAtom_ macros strip the `code_` prefix).
|
||||
-- The atoms_source_map pass already filters out non-atom symbols via the source-map.txt cross-ref.
|
||||
if name and #name > 0 then
|
||||
local st_value = M.read_u32_le(symtab, entry_off + SYM_ST_VALUE)
|
||||
@@ -794,132 +791,4 @@ end
|
||||
-- I/O helpers: atoms source-map + native directory glob
|
||||
-- ════════════════════════════════════════════════════════════════════════════
|
||||
|
||||
--- Parse a FORMAT_VERSION <expected_version> atoms-meta file (sourcemap or provenance).
|
||||
--- Shared by M.parse_source_map_file + M.parse_provenance_file.
|
||||
--- The two callers differ only in how they parse WORD lines; that's `extract_word(line)`.
|
||||
--- Returns the standard `{name -> {total, words}}` shape.
|
||||
--- Returns `{}` on format-version mismatch (and logs to stderr).
|
||||
--- @param path string
|
||||
--- @param expected_version integer
|
||||
--- @param extract_word fun(line: string): table|nil -- caller-supplied per-line parser
|
||||
--- @return table<string, table>
|
||||
function M.parse_atom_records(path, expected_version, extract_word)
|
||||
local out = {}
|
||||
local cur_name, cur_words = nil, {}
|
||||
for raw in io.lines(path) do
|
||||
local line = raw
|
||||
if line:match("^#") then
|
||||
local ver = line:match("^# FORMAT_VERSION%s+(%d+)")
|
||||
if ver and tonumber(ver) ~= expected_version then
|
||||
io.stderr:write(string.format(
|
||||
"[elf_dwarf.parse_atom_records] version mismatch (got %s, expected %d) in %s\n",
|
||||
ver, expected_version, path))
|
||||
return {}
|
||||
end
|
||||
-- skip other comments
|
||||
elseif line:sub(1, 4) == "ATOM" then
|
||||
-- ATOM <name> "<abs-source-path>" <total>
|
||||
local _, _, name = line:find("ATOM%s+(%S+)%s+\"[^\"]*\"%s+(%d+)")
|
||||
if name then
|
||||
cur_name = name
|
||||
cur_words = {}
|
||||
out[name] = { total = 0, words = cur_words }
|
||||
end
|
||||
elseif line == "ENDATOM" then
|
||||
-- Update the recorded total from the entries count
|
||||
-- (matches the `lines[1] = lines[1]:gsub(" 0$", " " .. total)` patch in atoms_source_map.lua:170).
|
||||
if cur_name and out[cur_name] then
|
||||
out[cur_name].total = #cur_words
|
||||
end
|
||||
cur_name, cur_words = nil, {}
|
||||
elseif line:sub(1, 4) == "WORD" and cur_name then
|
||||
local field = extract_word(line)
|
||||
if field then
|
||||
cur_words[#cur_words + 1] = field
|
||||
end
|
||||
end
|
||||
end
|
||||
return out
|
||||
end
|
||||
|
||||
--- Parse a FORMAT_VERSION <expected_version> `*.atoms.sourcemap.txt` file.
|
||||
--- Returns `{name -> {total = N, words = {{pos, line}, ...}}}`.
|
||||
--- Returns `{}` on format-version mismatch (and logs to stderr).
|
||||
---
|
||||
--- **Wire format** (emitted by `passes/atoms_source_map.lua`):
|
||||
--- ```
|
||||
--- # FORMAT_VERSION <n>
|
||||
--- ATOM <name> "<abs-source-path>" <total>
|
||||
--- WORD <n> LINE <line> TEXT <text...>
|
||||
--- ...
|
||||
--- ENDATOM
|
||||
--- ```
|
||||
---
|
||||
--- **Conventions:** the in-memory shape uses `{pos, line, text}`
|
||||
--- (`atoms_source_map.lua:142`); the `.txt` file uses `WORD <n>` so the parser maps `n` → `pos` field name.
|
||||
--- @param sm_path Path
|
||||
--- @param expected_version integer -- expected FORMAT_VERSION line
|
||||
--- @return table<string, table>
|
||||
function M.parse_source_map_file(sm_path, expected_version)
|
||||
return M.parse_atom_records(sm_path, expected_version, function(line)
|
||||
local _, n, _, src_line = line:find("WORD%s+(%d+)%s+LINE%s+(%d+)")
|
||||
if n and src_line then
|
||||
return { pos = tonumber(n), line = tonumber(src_line) }
|
||||
end
|
||||
end)
|
||||
end
|
||||
|
||||
--- Parse a FORMAT_VERSION <expected_version> `*.atoms.provenance.txt` file.
|
||||
--- Returns `{name -> {total = N, words = {{pos, call_file, call_line, comp_name, comp_file, comp_line}, ...}}}`.
|
||||
--- Returns `{}` on format-version mismatch (and logs to stderr).
|
||||
---
|
||||
--- **Wire format** (emitted by `passes/atoms_source_map.lua`):
|
||||
--- ```
|
||||
--- # FORMAT_VERSION <n>
|
||||
--- ATOM <name> "<abs-source-path>" <total>
|
||||
--- WORD <n> CALL <src-file>:<src-line> RAW
|
||||
--- WORD <n> CALL <src-file>:<src-line> MACRO <comp_name> "<comp-file>:<comp-line>"
|
||||
--- ...
|
||||
--- ENDATOM
|
||||
--- ```
|
||||
---
|
||||
--- **Used by** `passes/dwarf_injection.lua` to:
|
||||
--- - group consecutive MACRO rows into component invocations (one `DW_TAG_inlined_subroutine` each)
|
||||
--- - emit abstract `DW_TAG_subprogram` per unique component name
|
||||
--- - extend `.debug_line` so stepping into a `mac_X(...)` lands on the component's source line.
|
||||
--- @param prov_path string -- path to *.atoms.provenance.txt
|
||||
--- @param expected_version integer -- expected FORMAT_VERSION line
|
||||
--- @return table<string, table>
|
||||
function M.parse_provenance_file(prov_path, expected_version)
|
||||
return M.parse_atom_records(prov_path, expected_version, function(line)
|
||||
-- Two accepted shapes:
|
||||
-- WORD <n> CALL <call-file>:<call-line> RAW
|
||||
-- WORD <n> CALL <call-file>:<call-line> MACRO <comp_name> "<comp-file>:<comp-line>"
|
||||
local pos, call_file, call_line, comp_name, comp_file, comp_line =
|
||||
line:match('WORD%s+(%d+)%s+CALL%s+(.-):(%d+)%s+MACRO%s+(%S+)%s+"([^"]*):(%d+)"')
|
||||
if pos then
|
||||
return {
|
||||
pos = tonumber(pos),
|
||||
call_file = call_file,
|
||||
call_line = tonumber(call_line),
|
||||
comp_name = comp_name,
|
||||
comp_file = comp_file,
|
||||
comp_line = tonumber(comp_line),
|
||||
}
|
||||
end
|
||||
-- RAW row.
|
||||
local raw_pos, raw_file, raw_line = line:match('WORD%s+(%d+)%s+CALL%s+(.-):(%d+)%s+RAW')
|
||||
if raw_pos then
|
||||
return {
|
||||
pos = tonumber(raw_pos),
|
||||
call_file = raw_file,
|
||||
call_line = tonumber(raw_line),
|
||||
comp_name = nil,
|
||||
comp_file = nil,
|
||||
comp_line = nil,
|
||||
}
|
||||
end
|
||||
end)
|
||||
end
|
||||
|
||||
return M
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
---
|
||||
--- Writes:
|
||||
--- - `<ctx.out_root>/<dir_basename>.errors.h` — one per module, with `#error` directives on findings (the C compile will surface the error)
|
||||
--- - The annotations.txt report is rendered by `passes/report.lua` from the per-module results stashed in `ctx.flags._annot_results`
|
||||
--- - The annotations.txt report is rendered by `passes/report.lua` from the canonical `corpus.sources_by_dir` projection (re-validating each source via `M.validate()`).
|
||||
---
|
||||
--- **Conventions**: tabs (1/level), EmmyLua annotations, no regex, Lua 5.3 compatible
|
||||
|
||||
@@ -21,7 +21,7 @@ local duffle = dofile(_bootstrap_dir .. "../duffle_paths.lua")
|
||||
local write_file = duffle.write_file
|
||||
local ensure_dir = duffle.ensure_dir
|
||||
|
||||
-- The annotation pass now consults the source-derived registries built by scan_source:
|
||||
-- The annotation pass reads the source-derived registries from scan_source:
|
||||
-- * pipe_ctx.register_alias_registry — for atom_dbg_reg_default(R_X, ...) and atom_reg_types(R_X, ...) member-identity checks
|
||||
-- * pipe_ctx.type_name_registry — for atom_dbg_reg_default(<T>, ...) and atom_reg_types(<T>, ...) type-identity checks
|
||||
|
||||
@@ -45,8 +45,6 @@ local ensure_dir = duffle.ensure_dir
|
||||
--- @field project_root string
|
||||
--- @field upstream table<string, table>
|
||||
--- @field flags table
|
||||
--- @field flags._annot_results table[] -- stashed by annotation pass; consumed by report.lua
|
||||
--- @field dry_run boolean
|
||||
--- @field verbose boolean
|
||||
|
||||
--- @class PassResult
|
||||
@@ -108,12 +106,11 @@ local ensure_dir = duffle.ensure_dir
|
||||
--
|
||||
-- Each check has a uniform `append_to_findings` shape (errors[] / warnings[] / info[]).
|
||||
-- The dispatcher in `validate()` decides which findings list each check writes to — by convention,
|
||||
-- "existence" checks (declaration must exist, struct must exist) write errors[]; "shape" checks
|
||||
-- (writes/reads must be wave-context) write warnings[].
|
||||
-- "existence" checks (declaration must exist, struct must exist) write errors[]; "shape" checks (writes/reads must be wave-context) write warnings[].
|
||||
-- The `macro_word_drift` check writes both errors[] (missing/mismatch) and info[] (match).
|
||||
|
||||
--- Check: every annotated atom must have a matching MipsAtom_(name) declaration.
|
||||
--- @param a AtomAnnotation
|
||||
--- @param a AtomAnnotation
|
||||
--- @param pipe_ctx PipeCtx
|
||||
--- @param findings Findings
|
||||
local function check_atom_decl_exists(a, pipe_ctx, findings)
|
||||
@@ -144,7 +141,7 @@ end
|
||||
--- Emitting a warning here keeps the annotation pass from being stop-on-error for the common test-fixture case,
|
||||
--- while still surfacing the issue in the report.
|
||||
--- The static-analysis report remains the source of truth for build-stopping errors.
|
||||
--- @param a AtomAnnotation
|
||||
--- @param a AtomAnnotation
|
||||
--- @param pipe_ctx PipeCtx
|
||||
--- @param findings Findings
|
||||
local function check_binds_struct_exists(a, pipe_ctx, findings)
|
||||
@@ -160,7 +157,7 @@ end
|
||||
|
||||
--- Check: TAPE_WORDS(mac_X, N) ↔ WORD_COUNT(mac_X, N) drift.
|
||||
--- Three outcomes: missing (error), mismatch (error), match (info).
|
||||
--- @param m MacroEntry
|
||||
--- @param m MacroEntry
|
||||
--- @param wc table<string, integer> -- the shared word-count table (from ctx.shared.word_counts)
|
||||
--- @param findings Findings
|
||||
local function check_macro_word_drift(m, wc, findings)
|
||||
@@ -188,7 +185,7 @@ end
|
||||
--- Check: atom_dbg_reg_default(R_X, <type>) must target a register declared as a debug-visible alias in `pipe_ctx.register_alias_registry`,
|
||||
--- with a type name found in `pipe_ctx.type_name_registry`.
|
||||
--- Pointer depth is still bounded to 0 or 1. Duplicate defaults are still detected.
|
||||
--- @param _src SourceFile -- unused (kept for the per_source shape)
|
||||
--- @param _src SourceFile -- unused (kept for the per_source shape)
|
||||
--- @param pipe_ctx PipeCtx
|
||||
--- @param findings Findings
|
||||
local function check_semantic_reg_defaults(_src, pipe_ctx, findings)
|
||||
@@ -240,7 +237,7 @@ end
|
||||
--- The alias ident `R_<n>` now encodes the GPR identity only for entries that are explicitly opted in via the bare `atom_reg` marker.
|
||||
--- R_T0..R_T3 are intentionally NOT auto-included (per the prototype principle: no auto-include of wave-context; explicit opt-in only).
|
||||
--- The check fires for any R_T0..R_T3 reference that hasn't been opted in via `#define atom_reg`.
|
||||
--- @param _src SourceFile
|
||||
--- @param _src SourceFile
|
||||
--- @param pipe_ctx PipeCtx
|
||||
--- @param findings Findings
|
||||
local function check_atom_reg_types(_src, pipe_ctx, findings)
|
||||
@@ -271,7 +268,7 @@ local function check_atom_reg_types(_src, pipe_ctx, findings)
|
||||
end
|
||||
|
||||
--- Check: atom_view(Binds_X) entries must reference a real Binds_* struct and that struct must declare at least one field.
|
||||
--- @param _src SourceFile
|
||||
--- @param _src SourceFile
|
||||
--- @param pipe_ctx PipeCtx
|
||||
--- @param findings Findings
|
||||
local function check_atom_view_layout(_src, pipe_ctx, findings)
|
||||
@@ -385,15 +382,14 @@ local function check_skip_marker(marker, _pipe_ctx, findings)
|
||||
end
|
||||
end
|
||||
|
||||
--- Migration warning emitted alongside the new registry-membership check.
|
||||
--- Warn when a source references an unregistered alias.
|
||||
---
|
||||
--- R_TapePtr / R_AtomJmp / R_PrimCursor / R_FaceCursor / R_VertBase / R_OtBase
|
||||
--- are the wave-context aliases opted in via `#define atom_reg` in lottes_tape.h (Task 21).
|
||||
--- Any source referencing an R_X that's NOT in the registry will trip the new check; a single pass-level info entry
|
||||
--- 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.
|
||||
--- A source referencing an unregistered R_X emits one pass-level info entry
|
||||
--- (emitted only when at least one such rejection lands in this source) tells users where to look.
|
||||
---
|
||||
--- This check is a stop-gap until users migrate off raw C-ABI register names.
|
||||
--- @param _src SourceFile
|
||||
--- This check directs raw C-ABI register names to explicit alias registration.
|
||||
--- @param _src SourceFile
|
||||
--- @param pipe_ctx PipeCtx
|
||||
--- @param findings Findings
|
||||
local function check_wave_context_migration(_src, pipe_ctx, findings)
|
||||
@@ -446,14 +442,66 @@ local CHECK_RULES = {
|
||||
-- Validation
|
||||
-- ════════════════════════════════════════════════════════════════════════════
|
||||
--
|
||||
-- Pure check: read from src.scan, run validations, emit findings.
|
||||
-- No source walking; no parsing. The scan was done once upstream.
|
||||
-- Pure check: read from src.scan, run validations, emit findings. The scan was done once upstream.
|
||||
|
||||
--- Validate one source against its pre-scanned SourceScan payload.
|
||||
--- Build the corpus-wide pipe_ctx ONCE per pass run.
|
||||
--- Reads the merged `corpus.*` registries (canonical cross-source lookups),
|
||||
--- and the corpus-wide `atom_infos` list (preserving source order + duplicates).
|
||||
--- The corpus is the source of truth; per-source scans retain body / declaration
|
||||
--- ownership via `src.scan` and the per-source `atoms` / `atom_infos` projections.
|
||||
---
|
||||
--- Canonical ownership: a context without `ctx.shared.corpus` is rejected with an explicit canonical-corpus message.
|
||||
--- No per-source fallback synthesis is performed; callers MUST construct a canonical ctx through `build_ctx`.
|
||||
--- @param ctx PassCtx
|
||||
--- @param src SourceFile
|
||||
--- @return PipeCtx
|
||||
local function build_corpus_pipe_ctx(ctx)
|
||||
local corpus = ctx.shared and ctx.shared.corpus
|
||||
if not corpus then
|
||||
error("annotation requires ctx.shared.corpus "
|
||||
.. "(the canonical corpus is the source of truth; "
|
||||
.. "no per-source fallback is supported)", 0)
|
||||
end
|
||||
|
||||
-- Corpus atom_infos preserves source-order + duplicates;
|
||||
-- the per-check `check_unique_annotation` post-rule still flags duplicate annotation
|
||||
-- names within this list. We pre-compute the annot_counts map here so the per_source checks can iterate it without re-walking.
|
||||
local annot_counts = {}
|
||||
for _, info in ipairs(corpus.atom_infos or {}) do
|
||||
if info and info.atom_name then
|
||||
annot_counts[info.atom_name] = (annot_counts[info.atom_name] or 0) + 1
|
||||
end
|
||||
end
|
||||
|
||||
-- The pipe_ctx views REFERENCE the corpus tables directly (no copies).
|
||||
-- Every consumer of these fields observes mutations via the canonical corpus without independently mutable registry construction.
|
||||
return {
|
||||
-- Cross-source lookup tables (canonical corpus projections).
|
||||
register_alias_registry = corpus.register_alias_registry or {},
|
||||
type_name_registry = corpus.type_name_registry or {},
|
||||
atom_views = corpus.atom_views or {},
|
||||
atom_ctxs = corpus.atom_ctxs or {},
|
||||
atom_phases = corpus.atom_phases or {},
|
||||
binds_by_name = corpus.binds_by_name or {},
|
||||
atoms_by_name = corpus.atoms_by_name or {},
|
||||
-- Corpus-wide ordered list of atom_info records (source-order + duplicates).
|
||||
atom_infos_list = corpus.atom_infos or {},
|
||||
-- Corpus-wide annotation count aggregation (post-rule consumes this).
|
||||
annot_counts = annot_counts,
|
||||
-- Corpus-wide collisions (recorded by scan_source.merge_corpus_registries).
|
||||
collisions = corpus.collisions or {},
|
||||
-- wc still consumed by check_macro_word_drift; reads from the canonical
|
||||
-- `corpus.word_counts` table (built by word_count_eval.run).
|
||||
word_counts = corpus.word_counts or {},
|
||||
}
|
||||
end
|
||||
|
||||
--- Validate one source against its pre-scanned SourceScan payload + the corpus-wide pipe_ctx.
|
||||
--- @param ctx PassCtx
|
||||
--- @param src SourceFile
|
||||
--- @param corpus_pipe_ctx PipeCtx|nil -- built once per pass from corpus registries; nil = self-build (canonical projection).
|
||||
--- @return AnnotatedResult
|
||||
local function validate(ctx, src)
|
||||
local function validate(ctx, src, corpus_pipe_ctx)
|
||||
corpus_pipe_ctx = corpus_pipe_ctx or build_corpus_pipe_ctx(ctx)
|
||||
local scan = src.scan
|
||||
|
||||
-- Project the pre-scanned atoms to the AtomEntry shape this pass needs.
|
||||
@@ -479,9 +527,11 @@ local function validate(ctx, src)
|
||||
}
|
||||
end
|
||||
|
||||
-- Build pipe_ctx (Fleury: expose structure). Pre-compute everything the per-check functions need.
|
||||
-- Single source of truth for atom / binds / annotation-count lookups.
|
||||
-- pipe_ctx.types / pipe_ctx.atom_views / pipe_ctx.seen_defaults are projected from the scan payload so per_source check rules can iterate.
|
||||
-- Build the per-source pipe_ctx (Fleury: expose structure).
|
||||
-- Cross-source visibility comes from `corpus_pipe_ctx`;
|
||||
-- per-source declaration / body ownership comes from `src.scan`.
|
||||
-- pipe_ctx.types / pipe_ctx.atom_views / pipe_ctx.seen_defaults / pipe_ctx.type_occurrences
|
||||
-- are projected from the per-source scan so the per_source check rules can iterate the source-local occurrences.
|
||||
local seen_defaults = {}
|
||||
for reg, _ in pairs(scan.types or {}) do
|
||||
seen_defaults[reg] = (seen_defaults[reg] or 0) + 1
|
||||
@@ -494,25 +544,20 @@ local function validate(ctx, src)
|
||||
local pipe_ctx = {
|
||||
atom_index = {},
|
||||
binds_index = {},
|
||||
annot_counts = {},
|
||||
annot_counts = corpus_pipe_ctx.annot_counts,
|
||||
types = scan.types or {},
|
||||
type_occurrences = scan.type_occurrences or {},
|
||||
atom_views = scan.atom_views or {},
|
||||
seen_defaults = seen_defaults,
|
||||
atom_infos_list = atom_infos_list,
|
||||
binds_list = scan.binds or {},
|
||||
-- Project the source-derived registries from the scan payload so per_source checks consult them instead of the deleted
|
||||
-- SEMANTIC_DEFAULT_REGS / KNOWN_REG_DEFAULT_TYPES / etc.
|
||||
register_alias_registry = scan.register_alias_registry or {},
|
||||
type_name_registry = scan.type_name_registry or {},
|
||||
-- Source-derived registries: still populated from the scan payload as a convenience for callers that want source-local visibility.
|
||||
-- The canonical cross-source lookup tables live in corpus_pipe_ctx.
|
||||
register_alias_registry = corpus_pipe_ctx.register_alias_registry,
|
||||
type_name_registry = corpus_pipe_ctx.type_name_registry,
|
||||
}
|
||||
for _, a in ipairs(atoms) do pipe_ctx.atom_index [a.name] = a end
|
||||
for _, b in ipairs(scan.binds) do pipe_ctx.binds_index[b.name] = b end
|
||||
for _, a in ipairs(annots) do
|
||||
if a.name then
|
||||
pipe_ctx.annot_counts[a.name] = (pipe_ctx.annot_counts[a.name] or 0) + 1
|
||||
end
|
||||
end
|
||||
|
||||
-- Findings live in a single struct with three lists (errors / warnings / info).
|
||||
-- Each check writes to the list appropriate for its severity.
|
||||
@@ -544,8 +589,8 @@ local function validate(ctx, src)
|
||||
if rule.post then rule.post(pipe_ctx, findings) end
|
||||
end
|
||||
|
||||
-- Per-skip-marker rules.
|
||||
-- Each raw marker recorded by scan_source (in scan.skip_over.markers) is validated independently;
|
||||
-- Per-skip-marker rules.
|
||||
-- Each raw marker recorded by scan_source (in scan.skip_over.markers) is validated independently;
|
||||
-- the check emits at most one error per marker.
|
||||
-- Valid markers stay attached to scan.skip_over.atoms /.components for dwarf_injection.lua consumer.
|
||||
local skip_markers = scan.skip_over and scan.skip_over.markers or {}
|
||||
@@ -556,7 +601,7 @@ local function validate(ctx, src)
|
||||
end
|
||||
|
||||
-- Per-macro rules (TAPE_WORDS vs WORD_COUNT drift).
|
||||
local wc = ctx.shared.word_counts
|
||||
local wc = corpus_pipe_ctx.word_counts
|
||||
for _, m in ipairs(scan.macros) do
|
||||
for _, rule in ipairs(CHECK_RULES) do
|
||||
if rule.per_macro then rule.per_macro(m, wc, findings) end
|
||||
@@ -572,8 +617,8 @@ local function validate(ctx, src)
|
||||
-- Information summary (always emitted).
|
||||
findings.info[#findings.info + 1] = {
|
||||
line = 0,
|
||||
msg = string.format("scanned: %d atom(s), %d annotation(s), %d macro-word-decl(s), %d binds struct(s)",
|
||||
#atoms, #annots, #scan.macros, #scan.binds),
|
||||
msg = string.format("scanned: %d atom(s), %d annotation(s), %d macro-word-decl(s), %d binds struct(s)"
|
||||
, #atoms, #annots, #scan.macros, #scan.binds),
|
||||
}
|
||||
|
||||
return {
|
||||
@@ -594,7 +639,6 @@ end
|
||||
--- Render `<dir_basename>.errors.h` with `#error` directives for every error found across all sources in the directory.
|
||||
--- Empty directories (no errors, no atoms) produce no file.
|
||||
local function emit_module_errors_h(ctx, dir_basename, atoms_count, errors, sources)
|
||||
if ctx.dry_run then return nil end
|
||||
if atoms_count == 0 and #errors == 0 then
|
||||
return nil
|
||||
end
|
||||
@@ -622,17 +666,6 @@ local function emit_module_errors_h(ctx, dir_basename, atoms_count, errors, sour
|
||||
return out_path
|
||||
end
|
||||
|
||||
--- Stash aggregated per-module results for the report pass to consume.
|
||||
local function emit_module_annotations_stub(ctx, dir, dir_basename, atoms_count)
|
||||
ctx.flags = ctx.flags or {}
|
||||
ctx.flags._annot_results = ctx.flags._annot_results or {}
|
||||
ctx.flags._annot_results[#ctx.flags._annot_results + 1] = {
|
||||
dir = dir,
|
||||
dir_basename = dir_basename,
|
||||
atoms_count = atoms_count,
|
||||
}
|
||||
end
|
||||
|
||||
-- ════════════════════════════════════════════════════════════════════════════
|
||||
-- M.run — orchestrator entry
|
||||
-- ════════════════════════════════════════════════════════════════════════════
|
||||
@@ -651,22 +684,25 @@ function M.run(ctx)
|
||||
local errors = {}
|
||||
local warnings = {}
|
||||
|
||||
-- Per-DIRECTORY (per-module) aggregation. Group sources by `src.dir`, validate every source in the dir, then emit ONE errors.h per dir.
|
||||
-- `ctx.by_dir` is pre-computed in build_ctx (shared across all passes).
|
||||
local by_dir = ctx.by_dir or duffle.group_sources_by_dir(ctx.sources)
|
||||
-- Build the corpus-wide pipe_ctx ONCE per pass run.
|
||||
-- The corpus owns the canonical cross-source registries; per-source scans retain body / declaration ownership.
|
||||
-- The pipe_ctx is shared across every validate() invocation in this M.run so cross-source visibility is constant.
|
||||
local corpus_pipe_ctx = build_corpus_pipe_ctx(ctx)
|
||||
local corpus = ctx.shared.corpus
|
||||
|
||||
-- Per-DIRECTORY (per-module) aggregation.
|
||||
-- Group sources by `src.dir`, validate every source in the dir, then emit ONE errors.h per dir.
|
||||
-- The corpus owns `sources_by_dir`; this pass reads the corpus bucket directly.
|
||||
local by_dir = (corpus and corpus.sources_by_dir) or {}
|
||||
|
||||
for dir, dir_sources in pairs(by_dir) do
|
||||
local dir_basename = dir:match("([^/\\]+)$") or dir
|
||||
local dir_atoms = 0
|
||||
local dir_errors = {}
|
||||
local dir_warnings = {}
|
||||
-- Per-source validate() results, cached for the report pass (it reads from this instead of re-validating each source).
|
||||
ctx.flags = ctx.flags or {}
|
||||
ctx.flags._annot_source_results = ctx.flags._annot_source_results or {}
|
||||
for _, src in ipairs(dir_sources) do
|
||||
local result = validate(ctx, src)
|
||||
local result = validate(ctx, src, corpus_pipe_ctx)
|
||||
result.source = src.path -- tag for downstream rendering
|
||||
ctx.flags._annot_source_results[src.path] = result -- stash so report.lua reads from cache instead of re-running validate()
|
||||
dir_atoms = dir_atoms + #result.atoms
|
||||
for _, e in ipairs(result.errors) do
|
||||
dir_errors[#dir_errors + 1] = { line = e.line, msg = e.msg, source = src.path }
|
||||
@@ -682,8 +718,6 @@ function M.run(ctx)
|
||||
if err_path then
|
||||
table.insert(outputs, { errors_h = err_path })
|
||||
end
|
||||
|
||||
emit_module_annotations_stub(ctx, dir, dir_basename, dir_atoms)
|
||||
end
|
||||
|
||||
return { outputs = outputs, errors = errors, warnings = warnings }
|
||||
|
||||
+134
-342
@@ -1,11 +1,8 @@
|
||||
--- passes/atoms_source_map.lua — Per-.word source-line map emitter for tape atoms.
|
||||
---
|
||||
--- Reads the pre-scanned SourceScan payload (produced once upstream by `duffle.scan_source`)
|
||||
--- for `MipsAtom_(name)` (kind="atom"), `MipsAtomComp_` / `MipsAtomComp_Proc_` (kind="comp_*"),
|
||||
--- and `MipsCode code_<name>` (kind="raw_atom") declarations.
|
||||
--- Walks each atom's pre-tokenized body (`{{tok=string, rel=integer}, ...}` from `duffle.tokenize_body`),
|
||||
--- counts per-token word contributions via `ctx.shared.word_counts`, and emits one
|
||||
--- `WORD N LINE L TEXT T` line per `.word` to `<out_root>/<basename>.atoms.sourcemap.txt`.
|
||||
--- Reads the canonical `atom.paths` projection produced by the upstream `emission_model` pass.
|
||||
--- The ordered `items` stream, dense `word_events`, and `invocations` views are the only semantic inputs to this pass;
|
||||
--- it emits one `WORD N LINE L TEXT T` line per emitted `.word`.
|
||||
---
|
||||
--- **Two output forms** (per the workspace's per-emission-form pattern from
|
||||
--- `guide_metaprogram_ssdl.md`):
|
||||
@@ -34,10 +31,8 @@
|
||||
--- ENDATOM
|
||||
--- ```
|
||||
---
|
||||
--- Marker calls (`atom_label(...)`, `atom_offset(...)`) emit 0 `.word`s.
|
||||
--- They share the same walking convention as `passes/offsets.lua :: scan_atom_body`:
|
||||
--- Markers do NOT advance the word-offset counter, but if a marker is bundled on the same token with a trailing instruction
|
||||
--- (e.g. `atom_label(foo) load_half_u(...)`), the trailing instruction's word count is added. This matches `offsets.lua :: count_marker_rest`.
|
||||
--- Marker records are zero-width in `atom.paths.items`; they do not appear in
|
||||
--- the dense word view and therefore emit no WORD rows.
|
||||
---
|
||||
--- **Conventions:** tabs (1/level), EmmyLua annotations, no regex,
|
||||
--- Lua 5.3 compatible.
|
||||
@@ -50,10 +45,8 @@
|
||||
-- (works both standalone + when require'd). `duffle_paths.lua` sets package.path then returns `require("duffle")`
|
||||
-- at the bottom, so the dofile value IS the duffle module.
|
||||
local _bootstrap_dir = debug.getinfo(1, "S").source:match("^@?(.*[/\\])") or "./"
|
||||
local duffle = dofile(_bootstrap_dir .. "../duffle_paths.lua")
|
||||
local elf_dwarf = require("elf_dwarf")
|
||||
local word_count_eval = require("word_count_eval")
|
||||
local count_token_words = word_count_eval.count_token_words
|
||||
local duffle = dofile(_bootstrap_dir .. "../duffle_paths.lua")
|
||||
local elf_dwarf = require("elf_dwarf")
|
||||
|
||||
-- ════════════════════════════════════════════════════════════════════════════
|
||||
-- Constants
|
||||
@@ -63,199 +56,89 @@ local count_token_words = word_count_eval.count_token_words
|
||||
-- the gdb runtime loader rejects mismatches (E2).
|
||||
local FORMAT_VERSION = 1
|
||||
|
||||
-- Marker-call identifiers (mirrors offsets.lua:33-34).
|
||||
local LABEL_MARKER = "atom_label"
|
||||
local OFFSET_MARKER = "atom_offset"
|
||||
|
||||
-- ════════════════════════════════════════════════════════════════════════════
|
||||
-- Type declarations
|
||||
-- ════════════════════════════════════════════════════════════════════════════
|
||||
|
||||
--- @class AtomSourceMapCtx
|
||||
--- @field sources table[] -- SourceScan payload per source (from `ctx.sources`)
|
||||
--- @field shared table -- `ctx.shared`
|
||||
--- @field shared.word_counts table -- macro name -> word count (populated by word-counts + components passes)
|
||||
--- @field shared.corpus table -- canonical source-order corpus
|
||||
--- @field shared.word_counts table
|
||||
--- @field out_root string -- output root (e.g. "build/gen")
|
||||
--- @field dry_run boolean -- if true, compute but don't write
|
||||
--- @field flags table -- `ctx.flags`; reads `flags.gdb_runtime` + `flags.elf_path`
|
||||
|
||||
-- ════════════════════════════════════════════════════════════════════════════
|
||||
-- Helpers
|
||||
-- Canonical atom-path renderers
|
||||
-- ════════════════════════════════════════════════════════════════════════════
|
||||
|
||||
-- ════════════════════════════════════════════════════════════════════════════
|
||||
-- Provenance emission
|
||||
-- ════════════════════════════════════════════════════════════════════════════
|
||||
|
||||
-- Component-macro invocation prefix (mirrors components.lua's MAC_PREFIX).
|
||||
local MAC_PREFIX = "mac_"
|
||||
local MAC_PREFIX_LEN = 4
|
||||
|
||||
--- Strip the `mac_` prefix from a token's leading identifier.
|
||||
--- Returns nil if the identifier doesn't start with `mac_`
|
||||
--- (so non-component tokens like `load_half_u`, `nop2`, `gte_cmdw_*` fall through cleanly).
|
||||
--- @param tok string
|
||||
--- @return string|nil
|
||||
local function strip_mac_prefix_from_token(tok)
|
||||
local leading = duffle.read_ident(tok, 1)
|
||||
if not leading then return nil end
|
||||
if leading:sub(1, MAC_PREFIX_LEN) == MAC_PREFIX then
|
||||
return leading:sub(MAC_PREFIX_LEN + 1)
|
||||
end
|
||||
return nil
|
||||
end
|
||||
|
||||
--- Fetch the per-word body lines for a `mac_X(...)` invocation.
|
||||
--- Walks the component's pre-tokenized body in lockstep with `count_token_words` and attributes each emitted `.word`
|
||||
--- to a source line via `idx.line_of(...)`.
|
||||
--- Atom labels (`atom_label(...)`) emit 0 `.word`s and are skipped.
|
||||
--- @param bare string|nil -- the bare component name (e.g. `gte_load_tri_verts`)
|
||||
--- @param comp_body_index table
|
||||
--- @param wc table
|
||||
--- @return table|nil -- list of source lines, 1-based by word position
|
||||
local function fetch_body_lines(bare, comp_body_index, wc)
|
||||
if not (bare and comp_body_index) then return nil end
|
||||
local idx = comp_body_index[bare]
|
||||
if not (idx and idx.body_tokens and idx.line_of) then return nil end
|
||||
local lines = {}
|
||||
for _, bt in ipairs(idx.body_tokens) do
|
||||
local bt_tok = duffle.trim(bt.tok or "")
|
||||
if bt_tok ~= "" then
|
||||
local leading = duffle.read_ident(bt_tok, 1)
|
||||
local bt_words
|
||||
if leading == "atom_label" or leading == "atom_offset" then
|
||||
bt_words = 0
|
||||
else
|
||||
bt_words = count_token_words(bt_tok, wc)
|
||||
end
|
||||
if bt_words > 0 then
|
||||
local body_line = idx.line_of(idx.body_off + bt.rel)
|
||||
for _ = 1, bt_words do lines[#lines + 1] = body_line end
|
||||
end
|
||||
end
|
||||
end
|
||||
return lines
|
||||
end
|
||||
|
||||
--- Unified per-word entry walker. `mode` is "sourcemap" (3 fields) or "provenance" (8 fields including component + body-line lookup).
|
||||
--- Returns (entries, total_words). Markers contribute 0 entries.
|
||||
--- Join canonical words to canonical word items. `items` supplies the ordered
|
||||
--- word boundaries, while `word_events` supplies call text and source lines.
|
||||
--- @param atom table
|
||||
--- @param src table
|
||||
--- @param wc table
|
||||
--- @param mode string -- "sourcemap" | "provenance"
|
||||
--- @param comp table|nil -- shared.components map (provenance only)
|
||||
--- @param comp_body_index table|nil -- per-source body index (provenance only)
|
||||
--- @return table[], integer
|
||||
local function compute_word_entries(atom, src, wc, mode, comp, comp_body_index)
|
||||
local entries = {}
|
||||
local pos = 0
|
||||
for _, t in ipairs(atom.body_tokens) do
|
||||
local tok = t.tok
|
||||
local rel = t.rel
|
||||
|
||||
local words
|
||||
if duffle.is_marker_token(tok) then
|
||||
words = duffle.count_marker_rest(tok, wc, count_token_words)
|
||||
else
|
||||
words = count_token_words(tok, wc)
|
||||
end
|
||||
|
||||
-- Provenance-only: resolve component + body_lines (one fetch per token).
|
||||
local comp_name, comp_line, comp_path, comp_kind
|
||||
local body_lines
|
||||
if mode == "provenance" then
|
||||
local bare = strip_mac_prefix_from_token(tok)
|
||||
if bare and comp and comp[bare] then
|
||||
comp_name = bare
|
||||
comp_line = comp[bare].line
|
||||
comp_path = comp[bare].path
|
||||
comp_kind = comp[bare].kind
|
||||
end
|
||||
if comp_name then body_lines = fetch_body_lines(bare, comp_body_index, wc) end
|
||||
end
|
||||
|
||||
if words > 0 then
|
||||
local line = src.scan.line_of(atom.body_off + rel)
|
||||
local text = duffle.trim(tok):gsub("[\t\r\n]+", " ")
|
||||
for i = 1, words do
|
||||
local entry
|
||||
if mode == "provenance" then
|
||||
entry = {
|
||||
pos = pos,
|
||||
line = line,
|
||||
text = text,
|
||||
comp_name = comp_name,
|
||||
comp_line = comp_line,
|
||||
comp_path = comp_path,
|
||||
comp_kind = comp_kind,
|
||||
body_line = body_lines and body_lines[i],
|
||||
}
|
||||
else -- "sourcemap" (default)
|
||||
entry = { pos = pos, line = line, text = text }
|
||||
end
|
||||
entries[#entries + 1] = entry
|
||||
pos = pos + 1
|
||||
end
|
||||
end
|
||||
local function canonical_word_entries(atom)
|
||||
local paths = atom.paths or {}
|
||||
local events = paths.word_events or {}
|
||||
local word_items = {}
|
||||
for _, item in ipairs(paths.items or {}) do
|
||||
if item.kind == "word" then word_items[#word_items + 1] = item end
|
||||
end
|
||||
return entries, pos
|
||||
|
||||
local entries = {}
|
||||
for index, event in ipairs(events) do
|
||||
local item = word_items[index] or {}
|
||||
entries[#entries + 1] = {
|
||||
pos = event.i or (index - 1),
|
||||
line = event.call_line or item.line or 0,
|
||||
text = event.call_text or item.call_text or "",
|
||||
body_line = event.body_line or item.body_line or item.line or 0,
|
||||
invocation = (event.outermost_invocation_id
|
||||
and paths.invocations
|
||||
and paths.invocations[event.outermost_invocation_id]) or nil,
|
||||
}
|
||||
end
|
||||
return entries, #events
|
||||
end
|
||||
|
||||
--- Render one atom's provenance stanza. Format:
|
||||
--- `WORD N CALL <src-path>:<src-line> MACRO <name> "<def-path>:<def-line>" [BODY <line>]` (for component words)
|
||||
--- `WORD N CALL <src-path>:<src-line> RAW` (for direct instructions)
|
||||
--- `BODY <line>` is the source line of THIS specific word within the macro body
|
||||
--- (lottes_tape.h:N where N is the per-word body line).
|
||||
--- Absent for RAW rows and for component rows whose component declaration could not be indexed (older pass combinations / external macros).
|
||||
--- Downstream consumers (dwarf_injection, tests) fall back to DefLine / comp_line when BODY is absent.
|
||||
--- Returns (lines, total_words).
|
||||
--- @param src table
|
||||
--- @param atom table
|
||||
--- @param wc table
|
||||
--- @param comp table -- shared.components map
|
||||
--- @param comp_body_index table -- per-source component body index: bare_name -> {body_off, body_tokens, line_of}
|
||||
--- Render one atom's provenance stanza. Format 1 remains:
|
||||
--- `WORD N CALL <src-path>:<src-line> MACRO <name> "<def-path>:<def-line>" BODY <line>`
|
||||
--- `WORD N CALL <src-path>:<src-line> RAW`
|
||||
--- Component identity comes from the canonical outermost invocation record;
|
||||
--- the count-table lookup is the canonical component declaration witness.
|
||||
--- @param src table
|
||||
--- @param atom table
|
||||
--- @param wc table -- identity alias of corpus.word_counts
|
||||
--- @return string[], integer
|
||||
local function emit_provenance_stanza(src, atom, wc, comp, comp_body_index)
|
||||
local function emit_provenance_stanza(src, atom, wc)
|
||||
local lines = {}
|
||||
local rel_path = src.path:gsub("\\", "/")
|
||||
local entries, total = compute_word_entries(atom, src, wc, "provenance", comp, comp_body_index)
|
||||
local rel_path = src.path:gsub("\\\\", "/")
|
||||
local entries, total = canonical_word_entries(atom)
|
||||
|
||||
-- ATOM header line with placeholder total (patched after we know it).
|
||||
lines[#lines + 1] = string.format('ATOM %s "%s" 0', atom.raw_name or atom.name, rel_path)
|
||||
|
||||
for _, pe in ipairs(entries) do
|
||||
if pe.comp_name then
|
||||
local body_suffix = ""
|
||||
if pe.body_line then
|
||||
body_suffix = " BODY " .. tostring(pe.body_line)
|
||||
end
|
||||
lines[#lines + 1] = string.format('WORD %d CALL %s:%d MACRO %s "%s:%d"%s',
|
||||
pe.pos, rel_path, pe.line, pe.comp_name, pe.comp_path, pe.comp_line, body_suffix)
|
||||
for _, entry in ipairs(entries) do
|
||||
local inv = entry.invocation
|
||||
local macro_count = inv and wc["mac_" .. inv.component_name]
|
||||
if inv and macro_count ~= nil then
|
||||
lines[#lines + 1] = string.format(
|
||||
'WORD %d CALL %s:%d MACRO %s "%s:%d" BODY %d',
|
||||
entry.pos, rel_path, entry.line, inv.component_name,
|
||||
inv.def_path or "", inv.def_line or 0, entry.body_line)
|
||||
else
|
||||
lines[#lines + 1] = string.format("WORD %d CALL %s:%d RAW", pe.pos, rel_path, pe.line)
|
||||
lines[#lines + 1] = string.format(
|
||||
"WORD %d CALL %s:%d RAW", entry.pos, rel_path, entry.line)
|
||||
end
|
||||
end
|
||||
|
||||
-- Patch the placeholder total in the ATOM header line.
|
||||
lines[1] = lines[1]:gsub(" 0$", " " .. tostring(total))
|
||||
lines[#lines + 1] = "ENDATOM"
|
||||
return lines, total
|
||||
end
|
||||
|
||||
--- Build a per-source component body index keyed by the bare component name (e.g. `gte_load_tri_verts`).
|
||||
--- Each entry holds the data we need to map each emitted `.word` to its actual source line within the macro body:
|
||||
--- body_off -- byte offset of the `{` (start of body) in the component's source file.
|
||||
--- body_tokens -- list of {tok, rel} pairs; `rel` is the byte offset within the body.
|
||||
--- line_of -- closure resolving byte offsets in the component's source file to lines.
|
||||
--- Only `comp_bare` + `comp_proc` declarations contribute (a macro invocation can only resolve to one of those).
|
||||
--- First declaration wins (subsequent redeclarations would collide; today's sources declare each component exactly once).
|
||||
--- Render the full provenance file content for one source (one `.atoms.provenance.txt` per source).
|
||||
--- Render the full provenance file content for one source.
|
||||
--- @param src table
|
||||
--- @param wc table
|
||||
--- @param comp table -- shared.components map
|
||||
--- @param comp_body_index table -- cross-source component body index (built once in M.run; may be empty)
|
||||
--- @return string
|
||||
local function render_provenance(src, wc, comp, comp_body_index)
|
||||
local function render_provenance(src, wc)
|
||||
local lines = {}
|
||||
lines[#lines + 1] = "# FORMAT_VERSION 1"
|
||||
lines[#lines + 1] = "# auto-generated by ps1_meta.lua (passes/atoms_source_map.lua) — DO NOT EDIT"
|
||||
@@ -265,16 +148,15 @@ local function render_provenance(src, wc, comp, comp_body_index)
|
||||
lines[#lines + 1] = "# dwarf_injection to synthesize DW_TAG_inlined_subroutine instances + per-word"
|
||||
lines[#lines + 1] = "# line program rows for native source-level step into component bodies."
|
||||
|
||||
-- The cross-source component body index is passed in from M.run (one global lookup shared across every source's provenance file).
|
||||
-- A per-source lookup would miss every component whose declaration is in another source (e.g. `gte_load_tri_verts` is declared in `lottes_tape.h` but invoked from `hello_gte_tape.c`).
|
||||
|
||||
for _, atom in ipairs(src.scan.atoms or {}) do
|
||||
local stanza = emit_provenance_stanza(src, atom, wc, comp, comp_body_index)
|
||||
local function append(atom)
|
||||
local stanza = emit_provenance_stanza(src, atom, wc)
|
||||
for _, line in ipairs(stanza) do lines[#lines + 1] = line end
|
||||
end
|
||||
for _, atom in ipairs(src.scan.atoms or {}) do
|
||||
if atom.paths then append(atom) end
|
||||
end
|
||||
for _, atom in ipairs(src.scan.raw_atoms or {}) do
|
||||
local stanza = emit_provenance_stanza(src, atom, wc, comp, comp_body_index)
|
||||
for _, line in ipairs(stanza) do lines[#lines + 1] = line end
|
||||
if atom.paths then append(atom) end
|
||||
end
|
||||
|
||||
return table.concat(lines, "\n") .. "\n"
|
||||
@@ -286,19 +168,16 @@ end
|
||||
--- @param atom table
|
||||
--- @param wc table
|
||||
--- @return string[], integer
|
||||
local function emit_atom_stanza(src, atom, wc)
|
||||
local lines = {}
|
||||
local rel_path = src.path:gsub("\\", "/")
|
||||
local entries, total = compute_word_entries(atom, src, wc)
|
||||
local function emit_atom_stanza(src, atom)
|
||||
local lines = {}
|
||||
local rel_path = src.path:gsub("\\\\", "/")
|
||||
local entries, total = canonical_word_entries(atom)
|
||||
|
||||
-- ATOM header line with placeholder total (patched after we know it).
|
||||
lines[#lines + 1] = string.format('ATOM %s "%s" 0', atom.raw_name or atom.name, rel_path)
|
||||
for _, we in ipairs(entries) do
|
||||
for _, entry in ipairs(entries) do
|
||||
lines[#lines + 1] = string.format("WORD %d LINE %d TEXT %s",
|
||||
we.pos, we.line, we.text)
|
||||
entry.pos, entry.line, entry.text)
|
||||
end
|
||||
|
||||
-- Patch the placeholder total in the ATOM header line.
|
||||
lines[1] = lines[1]:gsub(" 0$", " " .. tostring(total))
|
||||
lines[#lines + 1] = "ENDATOM"
|
||||
return lines, total
|
||||
@@ -309,18 +188,20 @@ end
|
||||
--- @param src table
|
||||
--- @param wc table
|
||||
--- @return string
|
||||
local function render_source_map(src, wc)
|
||||
local function render_source_map(src)
|
||||
local lines = {}
|
||||
lines[#lines + 1] = "# FORMAT_VERSION " .. FORMAT_VERSION
|
||||
lines[#lines + 1] = "# auto-generated by ps1_meta.lua (passes/atoms_source_map.lua) — DO NOT EDIT"
|
||||
|
||||
for _, atom in ipairs(src.scan.atoms or {}) do
|
||||
local stanza = emit_atom_stanza(src, atom, wc)
|
||||
local function append(atom)
|
||||
local stanza = emit_atom_stanza(src, atom)
|
||||
for _, line in ipairs(stanza) do lines[#lines + 1] = line end
|
||||
end
|
||||
for _, atom in ipairs(src.scan.raw_atoms or {}) do
|
||||
local stanza = emit_atom_stanza(src, atom, wc)
|
||||
for _, line in ipairs(stanza) do lines[#lines + 1] = line end
|
||||
for _, atom in ipairs(src.scan.atoms or {}) do
|
||||
if atom.paths then append(atom) end
|
||||
end
|
||||
for _, atom in ipairs(src.scan.raw_atoms or {}) do
|
||||
if atom.paths then append(atom) end
|
||||
end
|
||||
|
||||
return table.concat(lines, "\n") .. "\n"
|
||||
@@ -343,55 +224,35 @@ end
|
||||
--- @param ctx PassCtx
|
||||
--- @return table[] -- list of {idx, name, src_path, file_base, addr, size_bytes, words, entries}
|
||||
local function build_atom_table(ctx)
|
||||
local wc = (ctx.shared and ctx.shared.word_counts) or {}
|
||||
local addrs = elf_dwarf.read_nm(ctx.flags.elf_path)
|
||||
|
||||
local corpus = ctx.shared and ctx.shared.corpus
|
||||
local matched = {}
|
||||
for _, src in ipairs(ctx.sources) do
|
||||
if src.scan then
|
||||
local file_base = src.path:match("([^/\\]+)$") or src.path
|
||||
for _, atom in ipairs(src.scan.atoms or {}) do
|
||||
if atom.kind == nil or atom.kind == "atom" then
|
||||
local name = atom.raw_name or atom.name
|
||||
local info = addrs[name]
|
||||
if info then
|
||||
local entries, total = compute_word_entries(atom, src, wc)
|
||||
matched[#matched + 1] = {
|
||||
name = name,
|
||||
src_path = src.path,
|
||||
file_base = file_base,
|
||||
addr = info[1],
|
||||
size_bytes = info[2],
|
||||
words = total,
|
||||
entries = entries,
|
||||
}
|
||||
end
|
||||
end
|
||||
end
|
||||
for _, atom in ipairs(src.scan.raw_atoms or {}) do
|
||||
local name = atom.name
|
||||
local info = addrs[name]
|
||||
if info then
|
||||
local entries, total = compute_word_entries(atom, src, wc)
|
||||
matched[#matched + 1] = {
|
||||
name = name,
|
||||
src_path = src.path,
|
||||
file_base = file_base,
|
||||
addr = info[1],
|
||||
size_bytes = info[2],
|
||||
words = total,
|
||||
entries = entries,
|
||||
}
|
||||
end
|
||||
end
|
||||
|
||||
for _, src in ipairs(corpus.source_order or {}) do
|
||||
local file_base = src.path:match("([^/\\\\]+)$") or src.path
|
||||
local function append(atom)
|
||||
if not atom.paths then return end
|
||||
local name = atom.raw_name or atom.name
|
||||
local info = addrs[name]
|
||||
if not info then return end
|
||||
local entries, total = canonical_word_entries(atom)
|
||||
matched[#matched + 1] = {
|
||||
name = name,
|
||||
src_path = src.path,
|
||||
file_base = file_base,
|
||||
addr = info[1],
|
||||
size_bytes = info[2],
|
||||
words = total,
|
||||
entries = entries,
|
||||
}
|
||||
end
|
||||
for _, atom in ipairs((src.scan or {}).atoms or {}) do append(atom) end
|
||||
for _, atom in ipairs((src.scan or {}).raw_atoms or {}) do append(atom) end
|
||||
end
|
||||
|
||||
-- Deterministic order: sort by address (matches `nm` output ordering).
|
||||
table.sort(matched, function(a, b) return a.addr < b.addr end)
|
||||
for i, a in ipairs(matched) do
|
||||
a.idx = i - 1
|
||||
end
|
||||
for i, a in ipairs(matched) do a.idx = i - 1 end
|
||||
return matched
|
||||
end
|
||||
|
||||
@@ -529,31 +390,6 @@ local function append_gdb_commands(lines, matched)
|
||||
lines[#lines + 1] = "end"
|
||||
lines[#lines + 1] = ""
|
||||
|
||||
-- ── show_c2 ──
|
||||
-- GTE data regs (COP2). pcsx-redux's gdb stub doesn't expose COP2 (only 72 regs: 32 GPR + COP0 + FPR).
|
||||
-- curl http://localhost:8080/api/v1/lua/gte
|
||||
-- We keep the command definition as a stub that points the user at the plugin.
|
||||
lines[#lines + 1] = "define show_c2"
|
||||
lines[#lines + 1] = ' echo "[gdb_tape_atoms] show_c2: gdb stub does not expose COP2 in this build."'
|
||||
lines[#lines + 1] = ' echo "[gdb_tape_atoms] Use scripts/pcsx_debug_helper.zip + curl http://localhost:8080/api/v1/lua/gte"'
|
||||
lines[#lines + 1] = ' echo "[gdb_tape_atoms] (or pcsx-redux Debug > Registers window for a native view)"'
|
||||
lines[#lines + 1] = "end"
|
||||
lines[#lines + 1] = "document show_c2"
|
||||
lines[#lines + 1] = " Stub. The gdb stub in this pcsx-redux build does not expose COP2 regs."
|
||||
lines[#lines + 1] = " For GTE data + control state, use the pcsx_debug_helper Lua plugin or the"
|
||||
lines[#lines + 1] = " pcsx-redux Debug > Registers window."
|
||||
lines[#lines + 1] = "end"
|
||||
lines[#lines + 1] = ""
|
||||
|
||||
-- ── show_c2ctl ──
|
||||
lines[#lines + 1] = "define show_c2ctl"
|
||||
lines[#lines + 1] = ' echo "[gdb_tape_atoms] show_c2ctl: see show_c2 for the same workaround."'
|
||||
lines[#lines + 1] = "end"
|
||||
lines[#lines + 1] = "document show_c2ctl"
|
||||
lines[#lines + 1] = " Stub. Same workaround as show_c2."
|
||||
lines[#lines + 1] = "end"
|
||||
lines[#lines + 1] = ""
|
||||
|
||||
-- ── wave_ctx ──
|
||||
lines[#lines + 1] = "define wave_ctx"
|
||||
lines[#lines + 1] = ' printf "$t4 = R_FaceCursor 0x%08x\\n", $t4'
|
||||
@@ -627,10 +463,8 @@ local function emit_gdb_runtime(ctx)
|
||||
lines[#lines + 1] = 'printf "[gdb_tape_atoms] runtime loaded %d atoms from %s\\n", $__atom_count, $__elf_path'
|
||||
|
||||
local out_path = ctx.out_root .. "/gdb_tape_atoms_runtime.gdb"
|
||||
if not ctx.dry_run then
|
||||
duffle.ensure_dir(duffle.dirname(out_path))
|
||||
duffle.write_file_lf(out_path, table.concat(lines, "\n") .. "\n")
|
||||
end
|
||||
duffle.ensure_dir(duffle.dirname(out_path))
|
||||
duffle.write_file_lf(out_path, table.concat(lines, "\n") .. "\n")
|
||||
io.stderr:write(string.format(
|
||||
"[atoms_source_map] wrote %s (%d atoms)\n", out_path, #matched))
|
||||
end
|
||||
@@ -641,42 +475,6 @@ end
|
||||
|
||||
local M = {}
|
||||
|
||||
--- Build the cross-source component body index used by `render_provenance` to attribute each emitted `.word` to its actual line within the macro body.
|
||||
---
|
||||
--- Components are declared in one source (the header that contains `MipsAtomComp_(ac_X)` / `MipsAtomComp_Proc_(ac_X, ...)`)
|
||||
--- but invoked from many source files (every atom body that calls `mac_X(...)`).
|
||||
--- The body_offset + body_tokens + line_of live with the declaration source, so a per-source index would miss invocations from other sources.
|
||||
---
|
||||
--- The cross-source index is keyed by the bare component name (`gte_load_tri_verts`, NOT `ac_gte_load_tri_verts`)
|
||||
--- `strip_mac_prefix_from_token` strips the `mac_` prefix from call-site identifiers and yields that exact bare name;
|
||||
--- matching it here keeps the lookup aligned with the `ctx.shared.components` map's keying convention.
|
||||
--- First declaration wins (subsequent redeclarations would collide; today's sources declare each component exactly once).
|
||||
--- @param ctx PassCtx
|
||||
--- @return table<string, table> -- {[comp_name] = {body_off, body_tokens, line_of}}
|
||||
local function build_cross_source_component_body_index(ctx)
|
||||
local 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 the stripped name is absent (defensive — current scan-source always sets both).
|
||||
local name = atom.name or atom.raw_name
|
||||
if name and not index[name] then
|
||||
index[name] = {
|
||||
body_off = atom.body_off,
|
||||
body_tokens = atom.body_tokens,
|
||||
line_of = line_of,
|
||||
}
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
return index
|
||||
end
|
||||
|
||||
--- Pass entry: emit one `<out_root>/<basename>.atoms.sourcemap.txt` per source file that contains at least one `MipsAtom_(name)` / `MipsCode code_<name>` declaration.
|
||||
--- Also emits `<out_root>/<basename>.atoms.provenance.txt`:
|
||||
--- per-.word provenance with `mac_X(...)` component resolution back to the component's definition file:line + the per-word body line.
|
||||
@@ -688,54 +486,48 @@ function M.run(ctx)
|
||||
local errors = {}
|
||||
local warnings = {}
|
||||
|
||||
-- word-counts + components passes must have populated shared.word_counts.
|
||||
-- If absent, the orchestrator wired the deps wrong — fail loud.
|
||||
local wc = (ctx.shared and ctx.shared.word_counts) or {}
|
||||
if not wc or not next(wc) then
|
||||
local corpus = ctx.shared and ctx.shared.corpus
|
||||
if type(corpus) ~= "table" or type(corpus.source_order) ~= "table" then
|
||||
error("atoms_source_map.run requires ctx.shared.corpus.source_order (canonical corpus).", 0)
|
||||
end
|
||||
|
||||
-- Word counts are owned by `corpus.word_counts`.
|
||||
-- The canonical owner is `corpus.word_counts` (populated by `passes/word_count_eval.lua` + `passes/components.lua`).
|
||||
local wc = corpus.word_counts or {}
|
||||
if not next(wc) then
|
||||
warnings[#warnings + 1] = {
|
||||
line = 0,
|
||||
msg = "atoms_source_map: ctx.shared.word_counts is empty; the word-counts + components passes may not have populated it. Check the PASSES dep edges.",
|
||||
msg = "atoms_source_map: corpus.word_counts is empty; the word-counts + components passes may not have populated it. Check the PASSES dep edges.",
|
||||
}
|
||||
end
|
||||
|
||||
-- shared.components map is populated by `passes/components.lua`.
|
||||
-- Used to attribute each emitted `.word` to either a component macro or the enclosing atom body.
|
||||
-- If absent, all words fall through as RAW (correct behavior — provenance is additive).
|
||||
local comp = (ctx.shared and ctx.shared.components) or {}
|
||||
|
||||
-- Cross-source component body index.
|
||||
-- Built ONCE 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).
|
||||
-- Per-source copies were insufficient — 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.
|
||||
local comp_body_index = build_cross_source_component_body_index(ctx)
|
||||
|
||||
-- Always emit the canonical text form (per-source).
|
||||
for _, src in ipairs(ctx.sources) do
|
||||
if src.scan then
|
||||
local n_atoms = src.scan.atoms and #src.scan.atoms or 0
|
||||
local n_raw_atoms = src.scan.raw_atoms and #src.scan.raw_atoms or 0
|
||||
if n_atoms + n_raw_atoms > 0 then
|
||||
local basename = duffle.basename_no_ext(src.path)
|
||||
|
||||
-- (1) atoms.sourcemap.txt — per-.word line map (unchanged contract).
|
||||
local sourcemap_path = ctx.out_root .. "/" .. basename .. ".atoms.sourcemap.txt"
|
||||
local sourcemap_body = render_source_map(src, wc)
|
||||
|
||||
-- (2) atoms.provenance.txt — per-.word provenance with `mac_X(...)` component resolution back to the component's definition file:line.
|
||||
-- Consumed by `passes/dwarf_injection.lua` to synthesize `DW_TAG_inlined_subroutine` instances for source-level Step Into on component invocations.
|
||||
local prov_path = ctx.out_root .. "/" .. basename .. ".atoms.provenance.txt"
|
||||
local prov_body = render_provenance(src, wc, comp, comp_body_index)
|
||||
|
||||
if not ctx.dry_run then
|
||||
duffle.ensure_dir(duffle.dirname(sourcemap_path))
|
||||
duffle.write_file_lf(sourcemap_path, sourcemap_body)
|
||||
duffle.write_file_lf(prov_path, prov_body)
|
||||
end
|
||||
|
||||
outputs[#outputs + 1] = { kind = "report", path = sourcemap_path }
|
||||
outputs[#outputs + 1] = { kind = "report", path = prov_path }
|
||||
for _, src in ipairs(corpus.source_order) do
|
||||
local has_projection = false
|
||||
for _, atom in ipairs((src.scan or {}).atoms or {}) do
|
||||
if (atom.kind == "atom" or atom.kind == "raw_atom") and atom.paths then
|
||||
has_projection = true; break
|
||||
end
|
||||
end
|
||||
if not has_projection then
|
||||
for _, atom in ipairs((src.scan or {}).raw_atoms or {}) do
|
||||
if atom.paths then has_projection = true; break end
|
||||
end
|
||||
end
|
||||
if has_projection then
|
||||
local basename = duffle.basename_no_ext(src.path)
|
||||
-- (1) atoms.sourcemap.txt — format-1 per-word call-site map.
|
||||
local sourcemap_path = ctx.out_root .. "/" .. basename .. ".atoms.sourcemap.txt"
|
||||
local sourcemap_body = render_source_map(src)
|
||||
-- (2) atoms.provenance.txt — format-1 per-word definition/body map.
|
||||
local prov_path = ctx.out_root .. "/" .. basename .. ".atoms.provenance.txt"
|
||||
local prov_body = render_provenance(src, wc)
|
||||
duffle.ensure_dir(duffle.dirname(sourcemap_path))
|
||||
duffle.write_file_lf(sourcemap_path, sourcemap_body)
|
||||
duffle.write_file_lf(prov_path, prov_body)
|
||||
outputs[#outputs + 1] = { kind = "report", path = sourcemap_path }
|
||||
outputs[#outputs + 1] = { kind = "report", path = prov_path }
|
||||
end
|
||||
end
|
||||
|
||||
-- Optionally emit the gdb-runtime form (post-link, one file per build).
|
||||
|
||||
+156
-120
@@ -11,15 +11,6 @@
|
||||
--- **Conventions**: tabs (1/level), EmmyLua annotations, no regex,
|
||||
--- Lua 5.3 compatible.
|
||||
|
||||
--- @class Component
|
||||
--- @field name string
|
||||
--- @field body string
|
||||
--- @field args string|nil
|
||||
--- @field line integer
|
||||
--- @field comment string|nil
|
||||
|
||||
--- @class M
|
||||
|
||||
-- ════════════════════════════════════════════════════════════════════════════
|
||||
-- Module-scope requires + package.path setup
|
||||
-- ════════════════════════════════════════════════════════════════════════════
|
||||
@@ -31,7 +22,6 @@
|
||||
-- duffle_paths.lua sets package.path then returns `require("duffle")` at the bottom, so the dofile value IS the duffle module.
|
||||
local _bootstrap_dir = debug.getinfo(1, "S").source:match("^@?(.*[/\\])") or "./"
|
||||
local duffle = dofile(_bootstrap_dir .. "../duffle_paths.lua")
|
||||
local word_count_eval = require("word_count_eval")
|
||||
|
||||
-- ════════════════════════════════════════════════════════════════════════════
|
||||
-- Constants
|
||||
@@ -74,7 +64,6 @@ local GEN_SUBDIR = "gen"
|
||||
--- @field project_root string -- project root (e.g. "code/")
|
||||
--- @field upstream table<string, table> -- per-pass upstream outputs
|
||||
--- @field flags table -- CLI flags
|
||||
--- @field dry_run boolean -- if true, compute but don't write
|
||||
--- @field verbose boolean -- log diagnostic info
|
||||
|
||||
--- @class PassResult
|
||||
@@ -108,8 +97,8 @@ local M = {}
|
||||
--- We then verify the preceding context ends with `MipsAtom`
|
||||
--- (the function-decl keyword with possible qualifiers between).
|
||||
---
|
||||
--- @param source string
|
||||
--- @param name string
|
||||
--- @param source string
|
||||
--- @param name string
|
||||
--- @param before_pos integer
|
||||
--- @return string|nil
|
||||
local function find_function_args_for(source, name, before_pos)
|
||||
@@ -149,14 +138,13 @@ end
|
||||
--- Used to copy signature comments from the source declaration (`MipsAtomComp_` / `MipsAtomComp_Proc_` / function decl)
|
||||
--- over to the generated `mac_X` macro, so LSP/IntelliSense displays the args doc.
|
||||
--- @param source string
|
||||
--- @param pos integer
|
||||
--- @param pos integer
|
||||
--- @return string
|
||||
local function preceding_comment_block(source, pos)
|
||||
local scan_pos = pos
|
||||
local pieces = {}
|
||||
while true do
|
||||
-- Skip whitespace (space/tab/newline/CR) backward from `scan_pos`,
|
||||
-- returning the position of the first non-whitespace char.
|
||||
-- skip whitespace backward; land on the next non-ws character.
|
||||
local non_ws = scan_pos - 1
|
||||
while non_ws > 0 do
|
||||
local ch = source:sub(non_ws, non_ws)
|
||||
@@ -167,13 +155,9 @@ local function preceding_comment_block(source, pos)
|
||||
end
|
||||
end
|
||||
if non_ws == 0 then break end
|
||||
|
||||
local is_block_close = non_ws >= 2 and source:sub(non_ws - 1, non_ws) == "*/"
|
||||
local is_line_end = source:sub(non_ws, non_ws) == "\n" or source:sub(non_ws, non_ws) == "\r"
|
||||
|
||||
if is_block_close then
|
||||
-- Find the opening `/*` for a block comment whose `*/` ends at `non_ws`.
|
||||
-- Walk back from `non_ws` over `/*` candidates.
|
||||
if non_ws >= 2 and source:sub(non_ws - 1, non_ws) == "*/" then
|
||||
-- block comment close: find the opening /* by walking back over /* candidates
|
||||
-- in source[1..non_ws-1].
|
||||
local prefix = source:sub(1, non_ws - 1)
|
||||
local open_at = nil
|
||||
for scan = #prefix - 1, 1, -1 do
|
||||
@@ -183,33 +167,28 @@ local function preceding_comment_block(source, pos)
|
||||
end
|
||||
end
|
||||
if not open_at then break end
|
||||
-- Walk back from `open_at` over leading spaces + tabs to include the indentation before the `/*`.
|
||||
-- include the indentation before the /* by walking back over leading spaces + tabs.
|
||||
local block_start = open_at
|
||||
while block_start > 1 do
|
||||
local ch = source:sub(block_start - 1, block_start - 1)
|
||||
if ch == " " or ch == "\t" then
|
||||
block_start = block_start - 1
|
||||
else
|
||||
break
|
||||
end
|
||||
if ch ~= " " and ch ~= "\t" then break end
|
||||
block_start = block_start - 1
|
||||
end
|
||||
table.insert(pieces, 1, source:sub(block_start, non_ws))
|
||||
scan_pos = block_start
|
||||
elseif is_line_end then
|
||||
-- Walk back from `non_ws` to the start of the source line (the most recent `\n` or position 1).
|
||||
else
|
||||
-- line comment path: must end in newline, must start with //.
|
||||
local ch = source:sub(non_ws, non_ws)
|
||||
if ch ~= "\n" and ch ~= "\r" then break end
|
||||
-- walk back from non_ws to the start of the source line (most recent \n or position 1).
|
||||
local line_start = non_ws
|
||||
while line_start > 1 and source:sub(line_start - 1, line_start - 1) ~= "\n" do
|
||||
line_start = line_start - 1
|
||||
end
|
||||
local line = source:sub(line_start, non_ws)
|
||||
if line:sub(1, 2) == "//" then
|
||||
table.insert(pieces, 1, line)
|
||||
scan_pos = line_start - 1
|
||||
else
|
||||
break
|
||||
end
|
||||
else
|
||||
break
|
||||
if line:sub(1, 2) ~= "//" then break end
|
||||
table.insert(pieces, 1, line)
|
||||
scan_pos = line_start - 1
|
||||
end
|
||||
end
|
||||
if #pieces == 0 then return "" end
|
||||
@@ -235,7 +214,6 @@ local function extract_arg_names(args_str)
|
||||
if trimmed ~= "" then
|
||||
-- Find the identifier at the end: walk back over trailers (whitespace + `*` + `[]`),
|
||||
-- then walk back over the identifier chars (alnum + `_`).
|
||||
-- Plex: inlined the 2 single-caller helpers (no 2-caller rule met).
|
||||
local ident_end = #trimmed
|
||||
while ident_end > 0 do
|
||||
local ch = trimmed:sub(ident_end, ident_end)
|
||||
@@ -248,7 +226,7 @@ local function extract_arg_names(args_str)
|
||||
local ident_start = ident_end
|
||||
while ident_start > 0 do
|
||||
local ch = trimmed:sub(ident_start, ident_start)
|
||||
if duffle.is_alnum(ch) or ch == "_" then
|
||||
if duffle.is_alnum_byte(string.byte(ch)) or ch == "_" then
|
||||
ident_start = ident_start - 1
|
||||
else
|
||||
break
|
||||
@@ -267,12 +245,12 @@ end
|
||||
-- Component projection (read from pre-scanned SourceScan)
|
||||
-- ════════════════════════════════════════════════════════════════════════════
|
||||
|
||||
-- Project pre-scanned MipsAtomComp_ / MipsAtomComp_Proc_ entries into Component shape.
|
||||
-- Does per-source backward lookups for args (preceding function decl) and comment (preceding comment block).
|
||||
-- Carries `body_tokens` forward from scan-source so word_count_rec reads from the precomputed table instead of calling duffle.tokenize_body again.
|
||||
-- @param source string -- the full source text (needed for backward lookups)
|
||||
-- @param scan table -- SourceScan from duffle.scan_source
|
||||
-- @return Component[]
|
||||
--- Project pre-scanned MipsAtomComp_ / MipsAtomComp_Proc_ entries into Component shape.
|
||||
--- Does per-source backward lookups for args (preceding function decl) and comment (preceding comment block).
|
||||
--- Carries `body_tokens` forward from scan-source so word_count_rec reads from the precomputed table instead of calling duffle.tokenize_body again.
|
||||
--- @param source string -- the full source text (needed for backward lookups)
|
||||
--- @param scan table -- SourceScan from duffle.scan_source
|
||||
--- @return Component[]
|
||||
local function project_components(source, scan)
|
||||
local out = {}
|
||||
for _, a in ipairs(scan.atoms) do
|
||||
@@ -283,6 +261,7 @@ local function project_components(source, scan)
|
||||
line = a.line,
|
||||
name = a.name,
|
||||
body = a.body,
|
||||
body_off = a.body_off,
|
||||
body_tokens = a.body_tokens,
|
||||
args = args,
|
||||
comment = comment,
|
||||
@@ -340,11 +319,11 @@ end
|
||||
-- Word-count computation (memoized recursive lookup)
|
||||
-- ════════════════════════════════════════════════════════════════════════════
|
||||
|
||||
-- Strip the `mac_` prefix from a component-call ident so we can look it up against the components-by-name table.
|
||||
-- Returns the ident unchanged if it doesn't start with the prefix
|
||||
-- (so a non-component ident like `mask_upper` falls through to the wc-table branch).
|
||||
-- @param ident string|nil
|
||||
-- @return string|nil
|
||||
--- Strip the `mac_` prefix from a component-call ident so we can look it up against the components-by-name table.
|
||||
--- Returns the ident unchanged if it doesn't start with the prefix
|
||||
--- (so a non-component ident like `mask_upper` falls through to the wc-table branch).
|
||||
--- @param ident string|nil
|
||||
--- @return string|nil
|
||||
local function strip_mac_prefix(ident)
|
||||
if not ident then return nil end
|
||||
if ident:sub(1, MAC_PREFIX_LEN) == MAC_PREFIX then
|
||||
@@ -353,13 +332,13 @@ local function strip_mac_prefix(ident)
|
||||
return ident
|
||||
end
|
||||
|
||||
-- (internal) Recursive word-count lookup. `cache` is the memoization table shared across all components
|
||||
-- in a single source's `count_all_components` pass; the in-progress -1 sentinel detects cycles (A -> B -> A).
|
||||
-- @param name string -- the component name (without `mac_`)
|
||||
-- @param comp_by_name table<string, Component>
|
||||
-- @param wc table<string, integer>
|
||||
-- @param cache table<string, integer>
|
||||
-- @return integer
|
||||
--- (internal) Recursive word-count lookup. `cache` is the memoization table shared across all components
|
||||
--- in a single source's `count_all_components` pass; the in-progress -1 sentinel detects cycles (A -> B -> A).
|
||||
--- @param name string -- the component name (without `mac_`)
|
||||
--- @param comp_by_name table<string, Component>
|
||||
--- @param wc table<string, integer>
|
||||
--- @param cache table<string, integer>
|
||||
--- @return integer
|
||||
local function word_count_rec(name, comp_by_name, wc, cache)
|
||||
if cache[name] ~= nil then return cache[name] end
|
||||
cache[name] = -1 -- mark in-progress (cycle detection)
|
||||
@@ -398,7 +377,7 @@ end
|
||||
--- references hit memoized values instead of re-walking the body.
|
||||
--- Cycle detection (A -> B -> A) is preserved via the in-progress `-1` sentinel in `cache`.
|
||||
--- @param components Component[]
|
||||
--- @param wc table<string, integer>
|
||||
--- @param wc table<string, integer>
|
||||
--- @return table<string, integer> -- map of component name (without `mac_`) -> word count
|
||||
local function count_all_components(components, wc)
|
||||
local comp_by_name = {}
|
||||
@@ -471,9 +450,9 @@ end
|
||||
|
||||
--- Build the list of lines for one component
|
||||
--- (signature comment, `#define mac_X(...)` line with backslash-continued tokens, then `WORD_COUNT(mac_X, N)` entry).
|
||||
--- @param c Component
|
||||
--- @param c Component
|
||||
--- @param components Component[]
|
||||
--- @param wc table<string, integer>
|
||||
--- @param wc table<string, integer>
|
||||
--- @return string[] -- list of lines for this component
|
||||
local function build_component_lines(c, counts)
|
||||
local lines = {}
|
||||
@@ -505,10 +484,10 @@ end
|
||||
-- Per-source emit logic
|
||||
-- ════════════════════════════════════════════════════════════════════════════
|
||||
|
||||
-- Build the boilerplate header lines (the `#ifdef INTELLISENSE_DIRECTIVES` block,
|
||||
-- the `// Auto-generated` comment, the `// Source:` line, and the self-contained `WORD_COUNT` macro definition).
|
||||
-- @param src SourceFile
|
||||
-- @return string[]
|
||||
--- Build the boilerplate header lines (the `#ifdef INTELLISENSE_DIRECTIVES` block,
|
||||
--- the `// Auto-generated` comment, the `// Source:` line, and the self-contained `WORD_COUNT` macro definition).
|
||||
--- @param src SourceFile
|
||||
--- @return string[]
|
||||
local function header_boilerplate(src)
|
||||
return {
|
||||
-- #pragma once wrapped in #ifdef INTELLISENSE_DIRECTIVES, matching the convention in lottes_tape.h.
|
||||
@@ -530,13 +509,13 @@ local function header_boilerplate(src)
|
||||
}
|
||||
end
|
||||
|
||||
-- Compute the output path for one source's `.macs.h` file.
|
||||
-- The pre-rework convention uses the *directory* basename
|
||||
-- (not the source file basename) e.g. `code/duffle/lottes_tape.h` produces `code/duffle/gen/duffle.macs.h`.
|
||||
-- This matches what the C codebase #includes.
|
||||
-- @param src SourceFile
|
||||
-- @return string -- the output directory
|
||||
-- @return string -- the full output path
|
||||
--- Compute the output path for one source's `.macs.h` file.
|
||||
--- The pre-rework convention uses the *directory* basename (not the source file basename)
|
||||
--- e.g. `code/duffle/lottes_tape.h` produces `code/duffle/gen/duffle.macs.h`.
|
||||
--- This matches what the C codebase #includes.
|
||||
--- @param src SourceFile
|
||||
--- @return string -- the output directory
|
||||
--- @return string -- the full output path
|
||||
local function compute_macs_h_path(src)
|
||||
local out_dir = src.dir .. "/" .. GEN_SUBDIR
|
||||
local out_path = out_dir .. "/" .. duffle.basename_no_ext(src.dir) .. ".macs.h"
|
||||
@@ -545,11 +524,10 @@ end
|
||||
|
||||
--- Emit a per-source `.macs.h` header with the `mac_X` macros + `WORD_COUNT` entries.
|
||||
--- Writes in BINARY mode so LF line endings are preserved (the git blob is LF; Windows text-mode would emit CRLF and break the byte-identical diff).
|
||||
--- Honors `ctx.dry_run`: prints the intended path but does not write the file.
|
||||
--- @param ctx PassCtx
|
||||
--- @param src SourceFile
|
||||
--- @param ctx PassCtx
|
||||
--- @param src SourceFile
|
||||
--- @param components Component[]
|
||||
--- @param counts table<string, integer> -- precomputed word counts (from count_all_components)
|
||||
--- @param counts table<string, integer> -- precomputed word counts (from count_all_components)
|
||||
--- @return string|nil -- path to the written file (nil if no components)
|
||||
local function emit_component_macros_h(ctx, src, components, counts)
|
||||
if #components == 0 then return nil end
|
||||
@@ -563,11 +541,6 @@ local function emit_component_macros_h(ctx, src, components, counts)
|
||||
end
|
||||
|
||||
local content = table.concat(lines, "\n") .. "\n"
|
||||
if ctx.dry_run then
|
||||
print(string.format(" -> %s (dry-run)", out_path))
|
||||
return out_path
|
||||
end
|
||||
|
||||
duffle.ensure_dir(out_dir)
|
||||
duffle.write_file_lf(out_path, content)
|
||||
print(string.format(" -> %s", out_path))
|
||||
@@ -578,41 +551,86 @@ end
|
||||
-- Pass entry
|
||||
-- ════════════════════════════════════════════════════════════════════════════
|
||||
|
||||
-- (internal) Extend `ctx.shared.word_counts` with this source's component macros so offsets sees them without re-reading the file.
|
||||
-- @param ctx PassCtx
|
||||
-- @param components Component[]
|
||||
-- @param counts table<string, integer> -- precomputed word counts (from count_all_components)
|
||||
local function update_shared_word_counts(ctx, components, counts)
|
||||
local wc = ctx.shared.word_counts
|
||||
--- (internal) Extend the canonical `corpus.word_counts` with this source's component macros so offsets sees them without re-reading the file.
|
||||
--- First declaration wins: a later caller's count is dropped (the existing entry from the first source is preserved).
|
||||
--- @param corpus table -- the canonical corpus
|
||||
--- @param components Component[]
|
||||
--- @param counts table<string, integer> -- precomputed word counts (from count_all_components)
|
||||
local function update_canonical_word_counts(corpus, components, counts)
|
||||
local wc = corpus.word_counts
|
||||
for _, c in ipairs(components) do
|
||||
wc["mac_" .. c.name] = counts[c.name]
|
||||
local key = "mac_" .. c.name
|
||||
if wc[key] == nil then
|
||||
wc[key] = counts[c.name]
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
--- @class ComponentDef
|
||||
--- @field name string -- bare name (without ac_/mac_ prefix)
|
||||
--- @field line integer -- definition source line (line of `MipsAtomComp_(ac_X)` / `MipsAtomComp_Proc_(ac_X, ...)`)
|
||||
--- @field path string -- absolute source path of the definition
|
||||
--- @field kind string -- "comp_bare" | "comp_proc"
|
||||
--- @field name string -- bare name (without ac_/mac_ prefix)
|
||||
--- @field line integer -- definition source line (line of `MipsAtomComp_(ac_X)` / `MipsAtomComp_Proc_(ac_X, ...)`)
|
||||
--- @field path string -- absolute source path of the definition
|
||||
--- @field kind string -- "comp_bare" | "comp_proc"
|
||||
|
||||
--- (internal) Extend `ctx.shared.components` with this source's components-by-name map so downstream passes
|
||||
--- (atoms_source_map, dwarf_injection) can resolve `mac_X(...)` invocations back to their component definition file:line.
|
||||
--- provenance emission uses this to attribute each emitted `.word` to either a component macro or the enclosing atom body.
|
||||
-- @param ctx PassCtx
|
||||
-- @param src SourceFile
|
||||
-- @param components Component[]
|
||||
local function update_shared_components(ctx, src, components)
|
||||
ctx.shared.components = ctx.shared.components or {}
|
||||
--- (internal) Populate the canonical `corpus.components` projection with this source's components-by-name map.
|
||||
--- First declaration wins; later declarations of the same bare name are dropped and recorded as a collision via `corpus.collisions` (kind = "component").
|
||||
--- The pass does NOT write to `ctx.shared.components` (ownership follows the canonical contract).
|
||||
--- @param corpus table -- the canonical corpus
|
||||
--- @param src SourceFile
|
||||
--- @param components Component[]
|
||||
local function update_canonical_components(corpus, src, components)
|
||||
local rel_path = src.path:gsub("\\", "/")
|
||||
for _, c in ipairs(components) do
|
||||
-- Keyed by bare name (e.g. `yield`, `load_tri_indices`).
|
||||
-- The atoms_source_map pass strips the `mac_` prefix from the call site identifier before lookup.
|
||||
ctx.shared.components[c.name] = {
|
||||
name = c.name,
|
||||
line = c.line,
|
||||
path = rel_path,
|
||||
kind = c.kind or "comp_bare",
|
||||
}
|
||||
-- Keyed by bare name (e.g. `yield`, `load_tri_indices`).
|
||||
-- The atoms_source_map pass looks up components by bare name from the canonical corpus;
|
||||
-- `mac_` prefix lives at the call-site identifier and is stripped before lookup.
|
||||
if corpus.components[c.name] == nil then
|
||||
corpus.components[c.name] = {
|
||||
name = c.name,
|
||||
line = c.line,
|
||||
path = rel_path,
|
||||
kind = c.kind or "comp_bare",
|
||||
}
|
||||
else
|
||||
-- A second declaration of the same bare name: record a typed collision so static-analysis + the report can surface it.
|
||||
-- Identical-shape declarations (same path + line) do NOT record a collision (the first-wins entry already covers the case).
|
||||
local existing = corpus.components[c.name]
|
||||
if existing.path ~= rel_path or existing.line ~= c.line then
|
||||
local kind = c.kind or "comp_bare"
|
||||
local first_kind = existing.kind or "comp_bare"
|
||||
corpus.collisions[#corpus.collisions + 1] = {
|
||||
kind = "component",
|
||||
name = c.name,
|
||||
first_site = { path = existing.path, line = existing.line },
|
||||
conflicting_site = { path = rel_path, line = c.line },
|
||||
first_shape = "kind=" .. first_kind,
|
||||
conflicting_shape = "kind=" .. kind,
|
||||
}
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
--- (internal) Populate the canonical `corpus.component_body_index` projection with this source's body index entries.
|
||||
--- First declaration wins; later declarations are dropped (no separate collision record: the components collision is already surfaced by `update_canonical_components`).
|
||||
--- The pass does NOT write to `ctx.shared.component_body_index` (the 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
|
||||
|
||||
@@ -623,24 +641,42 @@ function M.run(ctx)
|
||||
local errors = {}
|
||||
local warnings = {}
|
||||
|
||||
-- Initialize shared component map.
|
||||
-- The atoms_source_map and dwarf_injection passes consume `ctx.shared.components` to resolve `mac_X(...)`
|
||||
-- invocations back to the component's definition file:line.
|
||||
ctx.shared.components = ctx.shared.components or {}
|
||||
-- Canonical-corpus ownership gate.
|
||||
local corpus = ctx.shared and ctx.shared.corpus
|
||||
if type(corpus) ~= "table" then
|
||||
error("components.run requires ctx.shared.corpus (canonical corpus).", 0)
|
||||
end
|
||||
if type(corpus.source_order) ~= "table" then
|
||||
error("components.run requires ctx.shared.corpus.source_order (canonical corpus).", 0)
|
||||
end
|
||||
if type(corpus.word_counts) ~= "table" then
|
||||
error("components.run requires ctx.shared.corpus.word_counts; "
|
||||
.. "word_count_eval.run must run before components.run "
|
||||
.. "(see PASSES deps).", 0)
|
||||
end
|
||||
|
||||
for _, src in ipairs(ctx.sources) do
|
||||
-- Canonical projection ownership:
|
||||
-- * `corpus.word_counts["mac_"..name]` — current component count
|
||||
-- * `corpus.components[name]` — bare-name component definition
|
||||
-- * `corpus.component_body_index[name]` — body / line_of / source index
|
||||
-- The pass does NOT mutate `ctx.shared.components` or `ctx.shared.component_body_index`
|
||||
-- (ownership follows the canonical corpus; consumers read from the corpus directly).
|
||||
|
||||
for _, src in ipairs(corpus.source_order) do
|
||||
-- project_components reads from src.scan + does backward lookups on src.text
|
||||
local components = project_components(src.text, src.scan)
|
||||
if #components > 0 then
|
||||
-- Compute word counts for ALL components once (was: rebuilt per call inside the helpers).
|
||||
local counts = count_all_components(components, ctx.shared.word_counts)
|
||||
-- Compute all component word counts once per source.
|
||||
-- Use `corpus.word_counts` (the canonical count table) so the recursive lookup sees both authored-metadata entries
|
||||
-- (loaded by word_count_eval.run) AND same-source component entries (populated earlier in this loop by `update_canonical_word_counts`).
|
||||
local counts = count_all_components(components, corpus.word_counts)
|
||||
local macs_path = emit_component_macros_h(ctx, src, components, counts)
|
||||
if macs_path then
|
||||
outputs[#outputs + 1] = { macs_h = macs_path }
|
||||
update_shared_word_counts(ctx, components, counts)
|
||||
-- share component definitions with downstream passes.
|
||||
-- `mac_X(...)` invocations in atom bodies resolve back to (path, line) via this map.
|
||||
update_shared_components(ctx, src, components)
|
||||
-- Populate the canonical projections AFTER disk emission (so the byte-identical `.macs.h` contract is preserved before any current-count mutation).
|
||||
update_canonical_word_counts(corpus, components, counts)
|
||||
update_canonical_components(corpus, src, components)
|
||||
update_canonical_component_body_index(corpus, src, components, src.scan)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
+221
-295
@@ -20,8 +20,8 @@
|
||||
--- Splice step runs from PowerShell — no Lua subprocess; no cmd /c parsing issues.
|
||||
--- objcopy's --update-section works fine in PowerShell even though Lua's `os.execute`/`io.popen` would mangle the `=` on Windows.)
|
||||
---
|
||||
--- Result: VSCode's source gutter follows per-stepi inside atom bodies, AND the Variables pane shows the wave-context regs as atom-scoped locals.
|
||||
--- Native VSCode UX (gutter arrow + highlighted line + Run to Cursor + conditional BPs by source line + per-atom locals).
|
||||
--- Result: source stepping follows atom-body lines, and wave-context registers appear as atom-scoped locals.
|
||||
--- Native VSCode stepping, line highlighting, run-to-cursor, conditional breakpoints, and per-atom locals.
|
||||
--- No VSCode plugin, no Python, no pyelftools — pure Lua + objcopy.
|
||||
---
|
||||
--- **Conventions:** tabs (1/level), EmmyLua annotations, Lua 5.3 compatible.
|
||||
@@ -37,23 +37,17 @@ local duffle = dofile(_bootstrap_dir .. "../duffle_paths.lua")
|
||||
|
||||
-- ELF32 / DWARF / atoms-source-map utilities (post-link debug-info injection).
|
||||
-- Sister module to duffle.lua — contains the format-constant tables (ELF32 byte offsets, DWARF opcodes, etc.) and the I/O helpers
|
||||
-- (read_elf_sections, nm, source-map parser, LE byte r/w). `list_dir` lives in duffle.lua as a general I/O primitive (lifted out during F'').
|
||||
-- (read_elf_sections, nm, source-map parser, LE byte r/w). `list_dir` is the general directory primitive in duffle.lua.
|
||||
local elf_dwarf = require("elf_dwarf")
|
||||
|
||||
-- word-counting helper shared with passes/atoms_source_map.lua.
|
||||
-- Used here to walk a component's body_tokens in lockstep with their word-count allocation
|
||||
-- when we propagate per-word body lines into each invocation's `body_lines` array.
|
||||
local word_count_eval = require("word_count_eval")
|
||||
local count_token_words = word_count_eval.count_token_words
|
||||
-- Per-word body lines come from the canonical `atom.paths` projection.
|
||||
|
||||
local lfs = require("lfs")
|
||||
|
||||
-- File-scope aliases to elf_dwarf helpers; the canonical implementations live in scripts/elf_dwarf.lua.
|
||||
-- (2-caller lift: these were duplicated file-locals; the canonical is in elf_dwarf.lua, used by parse_abbrev_table + read_form_value.)
|
||||
local read_uleb128_at = elf_dwarf.read_uleb128_at
|
||||
local read_sleb128_at = elf_dwarf.read_sleb128_at
|
||||
-- ELF decoding helpers come from `elf_dwarf.lua`.
|
||||
local find_abbrev_table_end = elf_dwarf.find_abbrev_table_end
|
||||
-- Note: uleb128 + sleb128 are encoders; read_uleb128_at + read_sleb128_at are decoders. Different functions.
|
||||
-- Local DWARF opcode constants + length-prefixed integers (uleb128 + sleb128 encoders are in elf_dwarf.lua).
|
||||
local uleb128 = elf_dwarf.uleb128
|
||||
local sleb128 = elf_dwarf.sleb128
|
||||
|
||||
@@ -97,7 +91,7 @@ local ATOM_SOURCE_FILE_INDEX = 11
|
||||
-- New abbreviation codes (100+ to avoid collision with gcc's existing 1-60+ codes).
|
||||
local ABBREV_CU = 0x64 -- 100: DW_TAG_compile_unit
|
||||
local ABBREV_SUBPROGRAM = 0x65 -- 101: DW_TAG_subprogram
|
||||
local ABBREV_VARIABLE = 0x66 -- 102: DW_TAG_variable (DW_AT_type = ref4 to U4; was missing pre-2026-07-13 → gdb resolved R_PrimCursor against the C-level enum, not the register)
|
||||
local ABBREV_VARIABLE = 0x66 -- 102: DW_TAG_variable with DW_AT_type = ref4 to U4
|
||||
local ABBREV_STRUCT_TYPE = 0x67 -- 103: DW_TAG_structure_type with children (Binds_X mirror)
|
||||
local ABBREV_MEMBER = 0x68 -- 104: DW_TAG_member no children (DW_AT_type = ref4 to U4 base)
|
||||
local ABBREV_BIND_VAR = 0x69 -- 105: DW_TAG_variable no children + DW_AT_type = ref4 (the bind_args variable)
|
||||
@@ -150,10 +144,10 @@ local DW_AT_language = 0x13
|
||||
local DW_AT_location = 0x02
|
||||
local DW_AT_comp_dir = 0x1B
|
||||
local DW_AT_byte_size = 0x0B
|
||||
local DW_AT_encoding = 0x3E -- DWARF5 §7.7.1: DW_AT_encoding (for DW_ATE_unsigned base type; was 0x13 = DW_AT_language in prior slice - semantically wrong)
|
||||
local DW_AT_encoding = 0x3E -- DWARF5 §7.7.1: DW_AT_encoding for the DW_ATE_unsigned base type
|
||||
local DW_AT_data_member_location = 0x38
|
||||
local DW_AT_type = 0x49
|
||||
local DW_AT_linkage_name = 0x6E -- DWARF5 §7.7.1: DW_AT_linkage_name (standard form; 0x200027 was the GNU extension form - wrong vs DW_FORM_string abbrev)
|
||||
local DW_AT_linkage_name = 0x6E -- DWARF5 §7.7.1: DW_AT_linkage_name with DW_FORM_string
|
||||
local DW_AT_external = 0x3F -- marks a variable/function as externally visible
|
||||
-- Inlined_subroutine + abstract_origin attributes.
|
||||
local DW_AT_abstract_origin = 0x31
|
||||
@@ -211,14 +205,13 @@ local DW_FORM_udata = 0x0F -- ULEB128 (DW_AT_byte_size for struct_typ
|
||||
local DW_FORM_implicit_const = 0x21 -- DWARF5 §7.5.6: abbrev declaration carries a SLEB constant (used by the abbrev-table walker)
|
||||
local DW_FORM_sec_offset = 0x17 -- 4-byte section-relative offset (into .debug_loclists / .debug_rnglists)
|
||||
|
||||
local DW_OP_reg0 = 0x50
|
||||
-- DW_OP_regN = 0x50 + N; the variable's value resides in that register.
|
||||
local DW_OP_piece = 0x93 -- followed by ULEB128 byte count (per DWARF5 §7.7.5)
|
||||
-- DW_OP_reg0 + DW_OP_piece are declared canonically above (lines 114-116) alongside the other DWARF5 §7.7.3 loclist opcodes.
|
||||
|
||||
|
||||
local DW_ATE_unsigned = 0x07 -- DWARF5 §7.8.1: DW_ATE_unsigned (used for U4 base type)
|
||||
|
||||
local DW_LANG_C99 = 0x0C -- = 12 (C99); use as a safe "C-like" placeholder
|
||||
-- (DW_LANG_Mips_Assembler = 0x8001 was used in the, but we want this CU to look like a C TU so VSCode's Variables pane treats it as code.)
|
||||
-- No DW_AT_language attribute is emitted (see build_debug_info_section's abbrev 100).
|
||||
|
||||
-- R_<name> → MIPS GPR lookups go through the merged register_alias_registry
|
||||
-- (collected by collect_per_source_registries from ctx.sources[*].scan.register_alias_registry).
|
||||
@@ -341,7 +334,7 @@ end
|
||||
local DEFAULT_CU_NAME = "tape_atom_locals"
|
||||
local DEFAULT_CU_COMP_DIR = "."
|
||||
|
||||
-- Path templates for the .bin outputs are now in SECTION_WRITERS (see below).
|
||||
-- SECTION_WRITERS owns the .bin output path templates.
|
||||
|
||||
-- Default basename if not provided via ctx.
|
||||
local DEFAULT_BASENAME = "hello_gte"
|
||||
@@ -367,11 +360,12 @@ end
|
||||
--- Consume the per-source scanner associations without naming any atom or component in production.
|
||||
--- Whole atoms remain symbol-keyed; components are file-qualified internally so a source marker associates
|
||||
--- with its exact component definition even though GDB 12 requires function-only skip entries for the resulting synthetic inline frame.
|
||||
--- @param ctx DwarfInjectionCtx
|
||||
--- Iterates `corpus.source_order` (the canonical corpus projection).
|
||||
--- @param corpus table -- the canonical corpus from `ctx.shared.corpus`
|
||||
--- @return table -- {atoms = {[symbol] = association}, components = {[file|name] = association}}
|
||||
local function collect_skip_over(ctx)
|
||||
local function collect_skip_over(corpus)
|
||||
local skip_over = { atoms = {}, components = {} }
|
||||
for _, src in ipairs(ctx.sources or {}) do
|
||||
for _, src in ipairs((corpus and corpus.source_order) or {}) do
|
||||
local scan_skip = src.scan and src.scan.skip_over
|
||||
if scan_skip then
|
||||
for atom_name, association in pairs(scan_skip.atoms or {}) do
|
||||
@@ -396,53 +390,50 @@ local function collect_skip_over(ctx)
|
||||
return skip_over
|
||||
end
|
||||
|
||||
--- Merge the per-source scanner registries (register_alias_registry, type_name_registry, atom_views)
|
||||
--- into a single set of tables that downstream consumers can read from without re-iterating ctx.sources.
|
||||
--- Project the canonical corpus registries into the shape the section builders expect.
|
||||
--- The corpus already owns the merged `register_alias_registry`, `type_name_registry`, `atom_views`, `atom_ctxs`, `atom_phases`, and `atom_infos` projections (populated by `passes.scan_source.lua`).
|
||||
--- This helper just references them so the rest of `dwarf_injection.lua` keeps the same `registries.<key>` access shape it has always used.
|
||||
---
|
||||
--- Every `R_*` lookup and per-atom type override resolution in this file goes through this merged table.
|
||||
--- Aliases without `atom_reg` adjacent are absent; the absence is treated as "not debug-visible" (see build_inserted_children for the precedence chain).
|
||||
---
|
||||
--- When two sources register the same key, the last-writer wins (later sources override earlier).
|
||||
--- Today only one source declares wave-context enums, so collisions are absent.
|
||||
--- @param ctx DwarfInjectionCtx
|
||||
--- @param corpus table -- the canonical corpus from `ctx.shared.corpus`
|
||||
--- @return table -- {
|
||||
--- register_alias_registry = {[R_Name] = AliasEntry},
|
||||
--- type_name_registry = {[T] = TypeEntry},
|
||||
--- atom_views = {[atom_name] = AtomViewEntry},
|
||||
--- }
|
||||
local function collect_per_source_registries(ctx)
|
||||
local merged = {
|
||||
register_alias_registry = {},
|
||||
type_name_registry = {},
|
||||
atom_views = {},
|
||||
local function collect_per_source_registries(corpus)
|
||||
-- The corpus already holds the merged registries; reference them directly.
|
||||
-- No per-source iteration is needed because `passes.scan_source.lua` has already folded every per-source scan into the canonical tables.
|
||||
-- `atom_infos` is preserved byte-for-byte with no filtering; consumers consult `corpus.atoms_by_name`
|
||||
-- themselves when they need to know whether a particular atom_info corresponds to an actual atom record.
|
||||
local atom_infos_list = {}
|
||||
for _, ai in ipairs((corpus and corpus.atom_infos) or {}) do
|
||||
atom_infos_list[#atom_infos_list + 1] = ai
|
||||
end
|
||||
return {
|
||||
register_alias_registry = (corpus and corpus.register_alias_registry) or {},
|
||||
type_name_registry = (corpus and corpus.type_name_registry) or {},
|
||||
atom_views = (corpus and corpus.atom_views) or {},
|
||||
-- Per-atom atom_ctx declarations: atom_name -> {rbind_atom, ...}
|
||||
-- (populated by scan_source from `atom_ctx(<atom_name>)` sub-calls inside `atom_info`)
|
||||
atom_ctxs = {},
|
||||
atom_ctxs = (corpus and corpus.atom_ctxs) or {},
|
||||
-- Per-phase atom groups: phase_label -> {atoms = {atom_name1, ...}}
|
||||
-- (populated by scan_source from `atom_phase(<label>)` sub-calls inside `atom_info`; cross-source merged)
|
||||
atom_phases = {},
|
||||
-- Per-source already-resolved atom_infos (used by the precedence chain's ctx/phase steps)
|
||||
atom_infos = {},
|
||||
atom_phases = (corpus and corpus.atom_phases) or {},
|
||||
-- Corpus-wide atom_infos list, byte-for-byte.
|
||||
atom_infos = atom_infos_list,
|
||||
}
|
||||
for _, src in ipairs(ctx.sources or {}) do
|
||||
local scan = src.scan
|
||||
if scan then
|
||||
for k, v in pairs(scan.register_alias_registry or {}) do merged.register_alias_registry[k] = v end
|
||||
for k, v in pairs(scan.type_name_registry or {}) do merged.type_name_registry[k] = v end
|
||||
for k, v in pairs(scan.atom_views or {}) do merged.atom_views[k] = v end
|
||||
for k, v in pairs(scan.atom_ctxs or {}) do merged.atom_ctxs[k] = v end
|
||||
for k, v in pairs(scan.atom_phases or {}) do merged.atom_phases[k] = v end
|
||||
for _, ai in ipairs(scan.atom_infos or {}) do merged.atom_infos[#merged.atom_infos + 1] = ai end
|
||||
end
|
||||
end
|
||||
return merged
|
||||
end
|
||||
|
||||
--- Render deterministic debugger skip commands. Ordering is stable by category:
|
||||
--- exact atom symbols first (lexicographic), then exact component function names (lexicographic full command).
|
||||
--- Render deterministic debugger skip commands.
|
||||
--- Ordering is stable by category: exact atom symbols first (lexicographic), then exact component function names (lexicographic full command).
|
||||
--- Atom commands come from the matched nm/source-map table so the emitted name is the actual ELF symbol.
|
||||
--- The scanner tables and command set both deduplicate repeated source observations.
|
||||
--- @param skip_over table
|
||||
--- @param skip_over table
|
||||
--- @param atom_table table[] -- nm/source-map cross-reference; names are actual ELF symbols
|
||||
--- @return string
|
||||
local function build_gdbinit(skip_over, atom_table)
|
||||
@@ -474,7 +465,7 @@ end
|
||||
-- LEB128 encoders
|
||||
-- ════════════════════════════════════════════════════════════════════════════
|
||||
--
|
||||
-- Lifted to `elf_dwarf.uleb128` + `elf_dwarf.sleb128` (F'' refactor).
|
||||
-- Uses `elf_dwarf.uleb128` and `elf_dwarf.sleb128`.
|
||||
-- See those helpers for the bit-layout documentation + named constants (LEB_CONT_BIT, LEB_DATA_MASK, SLEB_SIGN_BIT).
|
||||
-- File-scope `local uleb128` + `local sleb128` aliases live near the module top so they're resolvable by every function below.
|
||||
|
||||
@@ -622,13 +613,14 @@ local function build_atom_sequence(atom)
|
||||
-- If entry 1 is the first word of a component invocation, emit the component-definition row at the same PC immediately after.
|
||||
-- The def line is always a statement target unless the inv is explicitly skip-over (atom_dbg_skip_over);
|
||||
-- the whole-atom skip path takes the early-return at line ~497 above so atom_is_stmt is irrelevant here.
|
||||
-- For the body row, prefer `body_lines[1]` (the actual source line of the first body word in the macro's expansion)
|
||||
-- For the body row, prefer `body_lines[1]` (the actual source line of the first body word in the macro's expansion)
|
||||
-- over `comp_line` (the macro signature line).
|
||||
-- When `body_lines` is absent (the component was not indexed by the scan, e.g. an external macro),
|
||||
-- fall back to `comp_line` so the line program remains valid.
|
||||
-- Canonical contract: the emission-model pass populates `body_lines` for every invocation;
|
||||
-- absence now is an error (older emitters / external macros are no longer supported).
|
||||
if inv1 and 1 == inv1.start_pos + 1 then
|
||||
local comp_file_idx_1 = resolve_provenance_file_index(inv1.comp_file)
|
||||
local body_line_1 = (inv1.body_lines and inv1.body_lines[1]) or inv1.comp_line
|
||||
assert(inv1.body_lines, "missing body_lines: emitter did not run emission-model")
|
||||
local body_line_1 = inv1.body_lines[1]
|
||||
emit_row(comp_file_idx_1, body_line_1, not inv1.skip_over)
|
||||
end
|
||||
|
||||
@@ -654,18 +646,20 @@ local function build_atom_sequence(atom)
|
||||
emit_row(call_file_idx, inv.call_line, atom_is_stmt)
|
||||
-- 2) component body row at this PC.
|
||||
-- Prefer `body_lines[1]` over `comp_line` so gdb's `step` lands on the macro's actual body line (not the signature line above `{ ... }`).
|
||||
-- Fallback to `comp_line` when the component isn't indexed.
|
||||
-- Canonical contract: `body_lines` is always populated by the emission-model pass.
|
||||
local body_file_idx = resolve_provenance_file_index(inv.comp_file)
|
||||
local body_line_1 = (inv.body_lines and inv.body_lines[1]) or inv.comp_line
|
||||
assert(inv.body_lines, "missing body_lines: emitter did not run emission-model")
|
||||
local body_line_1 = inv.body_lines[1]
|
||||
emit_row(body_file_idx, body_line_1, not inv.skip_over)
|
||||
elseif inv then
|
||||
-- Subsequent word of an invocation: 1-based offset into body_lines:
|
||||
-- word 1 of the invocation corresponds to body_lines[1], word 2 -> body_lines[2], etc.
|
||||
-- 1-based offset = idx - inv.start_pos (since idx = inv.start_pos + i for the i-th body word).
|
||||
-- When `body_lines` is missing (older emitters / external macros), fall back to comp_line.
|
||||
-- Canonical contract: `body_lines` is always populated by the emission-model pass.
|
||||
local body_file_idx = resolve_provenance_file_index(inv.comp_file)
|
||||
local words_into = idx - inv.start_pos -- 1-based word position in invocation
|
||||
local body_line_i = (inv.body_lines and inv.body_lines[words_into]) or inv.comp_line
|
||||
assert(inv.body_lines, "missing body_lines: emitter did not run emission-model")
|
||||
local body_line_i = inv.body_lines[words_into]
|
||||
emit_row(body_file_idx, body_line_i, not inv.skip_over)
|
||||
else
|
||||
-- RAW word: single call-site row. emit_row restores is_stmt after a selected component range before exposing this adjacent row.
|
||||
@@ -686,173 +680,117 @@ end
|
||||
-- ════════════════════════════════════════════════════════════════════════════
|
||||
|
||||
--- Build the atom table the section builders consume.
|
||||
--- Cross-references nm symbols with source-map.txt entries; sorted by addr.
|
||||
--- Also consumes the provenance file to record per-component invocations.
|
||||
--- Each atom gains an `invocations` field with one entry per `mac_X(...)` call site:
|
||||
--- `{comp_name, call_file, call_line, comp_file, comp_line, start_pos, end_pos, body_lines}`.
|
||||
--- Cross-references nm symbols with `corpus.atoms_by_name` and derives word rows + format-1 outermost invocation rows from `atom.paths`.
|
||||
---
|
||||
--- `body_lines` is the per-word source line within the macro body
|
||||
--- (lottes_tape.h:N where N is the actual line of this `.word` in the macro expansion).
|
||||
--- Without this field, the line program emits `comp_line` for EVERY body word,
|
||||
--- so gdb's `step` from a `mac_X(...)` call lands on the macro signature line and immediately
|
||||
--- returns without traversing the body (since no PC reports a different line).
|
||||
--- The atom table is built entirely from in-memory state — disk source-map and provenance text artifacts are NOT consulted.
|
||||
--- Those artifacts are diagnostic outputs, not semantic inputs; the DWARF injection pass must remain correct regardless of their on-disk content.
|
||||
---
|
||||
--- The data is computed by walking the component's pre-tokenized body in `ctx.sources[i].scan.atoms[j]`
|
||||
--- (the MipsAtomComp_/MipsAtomComp_Proc_ declaration).
|
||||
--- Atom labels (`atom_label(...)`) emit 0 `.word`s and are ignored (matching `passes/atoms_source_map.lua :: is_marker_token` + `count_marker_rest`).
|
||||
--- @param ctx DwarfInjectionCtx
|
||||
--- @param skip_over table -- {atoms = {[symbol] = association}, components = {[file|name] = association}}
|
||||
--- The result shape (one entry per ELF symbol matched against the corpus):
|
||||
--- `{name, addr, size_bytes, words, entries, invocations, skip_over?}`
|
||||
--- where:
|
||||
--- * `entries[i].pos` — 0-based `.word` position (matches the source-map format-1 row layout; downstream DWARF builders compare against this).
|
||||
--- * `entries[i].line` — call-site line for that word.
|
||||
--- * `entries[i].text` — trimmed encoder token text from `atom.paths.word_events`.
|
||||
--- * `invocations[j]` — one entry per format-1 outermost `mac_X(...)` invocation with
|
||||
--- `{comp_name, call_file, call_line, comp_file, comp_line, start_pos, end_pos, body_lines, skip_over}`. `body_lines[k]`
|
||||
--- is the k-th word's source line within the component body.
|
||||
---
|
||||
--- @param corpus table -- the canonical corpus from `ctx.shared.corpus`
|
||||
--- @param addrs table -- ELF symbols keyed by atom name from `elf_dwarf.read_nm`
|
||||
--- @param skip_over table -- {atoms = {[symbol] = association}, components = {[file|name] = association}}
|
||||
--- @return table[] -- list of {name, addr, size_bytes, words, entries, invocations, skip_over?}
|
||||
local function build_atom_table(ctx, skip_over)
|
||||
local basename = ctx.basename or DEFAULT_BASENAME
|
||||
-- Source-map path: convention matches the α MVP's emission location.
|
||||
-- writes `<out_root>/<basename>.atoms.sourcemap.txt` (e.g. `build/gen/hello_gte_tape.atoms.sourcemap.txt`).
|
||||
-- But ctx.out_root is `build/gen` (the per-build output root) and basename defaults to `hello_gte`.
|
||||
-- The actual file emitted today is per-source; we look for any `*.atoms.sourcemap.txt` in out_root.
|
||||
local sm_files = duffle.list_dir(ctx.out_root, "%.atoms.sourcemap%.txt$")
|
||||
if #sm_files == 0 then
|
||||
io.stderr:write(string.format(
|
||||
"[dwarf_injection] no *.atoms.sourcemap.txt in %s; need atoms-source-map pass first\n",
|
||||
ctx.out_root))
|
||||
return {}
|
||||
end
|
||||
local function build_atom_table(corpus, addrs, skip_over)
|
||||
-- Cross-ref: keep only atoms present in BOTH the nm symbol table AND
|
||||
-- the canonical corpus projection. Output is sorted by ascending addr.
|
||||
local atoms_by_name = corpus.atoms_by_name or {}
|
||||
|
||||
-- Read nm + merge all source-map files.
|
||||
local addrs = elf_dwarf.read_nm(ctx.flags.elf_path)
|
||||
local merged = {}
|
||||
for _, sm_path in ipairs(sm_files) do
|
||||
local sm = elf_dwarf.parse_source_map_file(sm_path, 1)
|
||||
for name, sm_data in pairs(sm) do
|
||||
merged[name] = sm_data
|
||||
-- Per-atom ingest. Returns nil if the atom is absent from the corpus
|
||||
-- (caller skips it via the `if atom then ...` guard).
|
||||
local function ingest_atom(name, info)
|
||||
local atom_record = atoms_by_name[name]
|
||||
if not atom_record then return nil end
|
||||
|
||||
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` = the 0-based `.word` position
|
||||
-- `call_line` = 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
|
||||
end
|
||||
local atom = {
|
||||
name = name,
|
||||
addr = info[1],
|
||||
size_bytes = info[2],
|
||||
words = #word_events,
|
||||
entries = entries,
|
||||
skip_over = skip_over.atoms[name] ~= nil,
|
||||
}
|
||||
|
||||
-- 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
|
||||
-- Group consecutive `word_events` rows whose outermost invocation is the SAME
|
||||
-- format-1 invocation into a single `atom.invocations` entry. Rows sharing the
|
||||
-- same comp_name / call_file / call_line / comp_file / comp_line are part of
|
||||
-- the same group. Rows outside any invocation flush cur_inv.
|
||||
if #invocations_proj > 0 then
|
||||
local invocations = {}
|
||||
-- Process one word_event row against the current group state.
|
||||
-- Returns the (possibly updated) cur_inv.
|
||||
local function row(ev, cur_inv)
|
||||
local outer_id = ev.outermost_invocation_id
|
||||
local outer_inv = outer_id and invocations_proj[outer_id] or nil
|
||||
if not (outer_inv and outer_inv.component_name) then
|
||||
-- raw row: flush any pending cur_inv; no new group starts.
|
||||
if cur_inv then invocations[#invocations + 1] = cur_inv end
|
||||
return nil
|
||||
end
|
||||
local inv_key = outer_inv.component_name
|
||||
.. "|" .. (outer_inv.call_path or "")
|
||||
.. "|" .. tostring(outer_inv.call_line or 0)
|
||||
.. "|" .. (outer_inv.def_path or "")
|
||||
.. "|" .. tostring(outer_inv.def_line or 0)
|
||||
local ev_pos = ev.i or 0
|
||||
if cur_inv and cur_inv.key == inv_key then
|
||||
-- same group: extend range + append body line.
|
||||
cur_inv.end_pos = ev_pos
|
||||
cur_inv.body_lines[#cur_inv.body_lines + 1] = ev.body_line or 0
|
||||
return cur_inv
|
||||
end
|
||||
-- key changed (or no current group): flush + start new.
|
||||
if cur_inv then invocations[#invocations + 1] = cur_inv end
|
||||
return {
|
||||
key = inv_key,
|
||||
comp_name = outer_inv.component_name,
|
||||
call_file = outer_inv.call_path or "",
|
||||
call_line = outer_inv.call_line or 0,
|
||||
comp_file = outer_inv.def_path or "",
|
||||
comp_line = outer_inv.def_line or 0,
|
||||
start_pos = ev_pos,
|
||||
end_pos = ev_pos,
|
||||
skip_over = skip_over.components[normalize_debug_path(outer_inv.def_path or ""):lower() .. "\0" .. outer_inv.component_name] ~= nil,
|
||||
body_lines = { ev.body_line or 0 },
|
||||
}
|
||||
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
|
||||
local cur_inv = nil
|
||||
for _, ev in ipairs(word_events) do
|
||||
cur_inv = row(ev, cur_inv)
|
||||
end
|
||||
if cur_inv then invocations[#invocations + 1] = cur_inv end
|
||||
atom.invocations = invocations
|
||||
end
|
||||
return lines
|
||||
return atom
|
||||
end
|
||||
|
||||
-- Cross-ref; keep atoms that exist in both.
|
||||
local out = {}
|
||||
for name, info in pairs(addrs) do
|
||||
local sm = merged[name]
|
||||
if sm then
|
||||
local atom = {
|
||||
name = name,
|
||||
addr = info[1],
|
||||
size_bytes = info[2],
|
||||
words = sm.total,
|
||||
entries = sm.words,
|
||||
skip_over = skip_over.atoms[name] ~= nil,
|
||||
}
|
||||
-- Group consecutive MACRO rows in this atom's provenance into invocations.
|
||||
-- An invocation = one `mac_X(...)` call site spanning N consecutive .word rows.
|
||||
-- Two consecutive rows with the same (comp_name, call_file, call_line, comp_file, comp_line) are part of the same invocation.
|
||||
local prov_data = prov_merged[name]
|
||||
if prov_data and prov_data.words then
|
||||
local invocations = {}
|
||||
local cur_inv = nil
|
||||
for _, w in ipairs(prov_data.words) do
|
||||
if w.comp_name then
|
||||
local inv_key = w.comp_name .. "|" .. w.call_file .. "|" .. w.call_line .. "|" .. w.comp_file .. "|" .. w.comp_line
|
||||
if cur_inv and cur_inv.key == inv_key then
|
||||
-- Same invocation as the previous word — extend its range.
|
||||
cur_inv.end_pos = w.pos
|
||||
else
|
||||
-- New invocation: flush the previous one and start fresh.
|
||||
if cur_inv then invocations[#invocations + 1] = cur_inv end
|
||||
cur_inv = {
|
||||
key = inv_key,
|
||||
comp_name = w.comp_name,
|
||||
call_file = w.call_file,
|
||||
call_line = w.call_line,
|
||||
comp_file = w.comp_file,
|
||||
comp_line = w.comp_line,
|
||||
start_pos = w.pos,
|
||||
end_pos = w.pos,
|
||||
-- component_skip_key: case-insensitive Windows path + "\0" separator + exact component-name.
|
||||
-- (Inlined from `component_skip_key`; the lookup is in skip_over.components keyed by the result.)
|
||||
skip_over = skip_over.components[normalize_debug_path(w.comp_file):lower() .. "\0" .. w.comp_name] ~= nil,
|
||||
}
|
||||
-- Capture the per-word body lines for THIS invocation, indexed by 1-based word position within the invocation.
|
||||
-- body_lines[1] is the line of the first body word (= the line of the first macro-body token, NOT the comp def line).
|
||||
-- Downstream consumers (the line program emitter) fall back to comp_line when this is empty.
|
||||
cur_inv.body_lines = compute_invocation_body_lines(w.comp_name)
|
||||
end
|
||||
else
|
||||
-- RAW row: flush the current invocation.
|
||||
if cur_inv then invocations[#invocations + 1] = cur_inv; cur_inv = nil end
|
||||
end
|
||||
end
|
||||
if cur_inv then invocations[#invocations + 1] = cur_inv end
|
||||
atom.invocations = invocations
|
||||
end
|
||||
out[#out + 1] = atom
|
||||
end
|
||||
local atom = ingest_atom(name, info)
|
||||
if atom then out[#out + 1] = atom end
|
||||
end
|
||||
table.sort(out, function(a, b) return a.addr < b.addr end)
|
||||
return out
|
||||
@@ -889,7 +827,6 @@ end
|
||||
-- We use the SourceScan payload populated by `passes/scan_source.lua` (the dep-closed upstream pass).
|
||||
-- That pass walks each source once and populates `src.scan.atom_infos` (the atom_info sub-call parse) and `src.scan.binds` (the Binds_X struct field parse).
|
||||
--
|
||||
-- The prior local `reparse_binds_body` fallback (and the 2nd source walk inside `parse_rbind_atoms`) is REMOVED.
|
||||
-- parse_rbind_atoms consumes `scan.binds[i].fields` directly, which is populated by the scan-source pass with the typed-field record ({name, type_name, pointer_depth, offset, byte_size}).
|
||||
|
||||
--- Find every `load_word(R_<reg>, R_TapePtr, O_(Binds_X, FieldName))` call in the atom body and return ordered (reg_index, field_name) pairs.
|
||||
@@ -904,10 +841,9 @@ end
|
||||
---
|
||||
--- Pre-tokenized: `body_tokens` is the scan-source pass's pre-split list of top-level
|
||||
--- statements (each entry is a single `load_word(...)` call or other statement).
|
||||
--- Plex: prepass move — use the pre-computed data instead of re-walking the body text.
|
||||
--- @param body_tokens table[] -- the atom's pre-tokenized body statements (from atom.body_tokens)
|
||||
--- @param binds_name string -- expected Binds_X name (skip pairs with mismatching binds)
|
||||
--- @param registries table -- merged registries from collect_per_source_registries
|
||||
--- @param binds_name string -- expected Binds_X name (skip pairs with mismatching binds)
|
||||
--- @param registries table -- merged registries from collect_per_source_registries
|
||||
--- @return table[] -- list of {reg = <MIPS index>, field = <field name>}
|
||||
local function parse_body_load_pairs(body_tokens, binds_name, registries)
|
||||
local pairs = {}
|
||||
@@ -939,9 +875,7 @@ end
|
||||
|
||||
--- Collect every rbind atom + the matching Binds_X struct + (reg, field) pairs.
|
||||
---
|
||||
--- Inputs come from the dep-closed `scan-source` pass:
|
||||
--- ctx.sources[i].scan.atom_infos -- list of {atom_name, binds, reads, writes, info_line}
|
||||
--- ctx.sources[i].scan.binds -- list of {line, name, fields, bytes}
|
||||
--- Inputs come from the dep-closed `scan-source` pass (the per-source `src.scan` payload is preserved on each `corpus.source_order` entry).
|
||||
---
|
||||
--- Returns:
|
||||
--- rbind_atoms = {[atom_name] = {binds, fields, regs, byte_size, info_line}}
|
||||
@@ -950,22 +884,22 @@ end
|
||||
--- The `regs` list per atom is ordered: each entry is the MIPS reg index that holds the matching field in the source-order pop sequence.
|
||||
--- The piece chain uses (DW_OP_regN, DW_OP_piece, ULEB128(field_size)).
|
||||
---
|
||||
--- The 2nd source walk (the body-text `text:find("typedef Struct_(...)")` re-walk) is removed;
|
||||
--- Binds fields come from `scan.binds`; no body-text source walk is needed.
|
||||
--- per-source `scan.binds[i].fields` already carries the typed-field record after the scan-source generalization.
|
||||
--- @param ctx DwarfInjectionCtx
|
||||
--- @param atom_table table[] -- the cross-ref'd atom table from build_atom_table
|
||||
--- @param registries table -- merged registries from collect_per_source_registries
|
||||
--- @return table, table -- (rbind_atoms, rbind_structs)
|
||||
local function parse_rbind_atoms(ctx, atom_table, registries)
|
||||
--- @param corpus table -- the canonical corpus from `ctx.shared.corpus`
|
||||
--- @param atom_table table[] -- the cross-ref'd atom table from build_atom_table
|
||||
--- @param registries table -- merged registries from collect_per_source_registries
|
||||
--- @return table, table -- (rbind_atoms, rbind_structs)
|
||||
local function parse_rbind_atoms(corpus, atom_table, registries)
|
||||
registries = registries or {}
|
||||
local rbind_atoms = {}
|
||||
local rbind_structs = {}
|
||||
|
||||
-- Index binds by struct name; consume `scan.binds[i].fields` directly (no body-text re-walk).
|
||||
-- The scan-source pass emits each Binds_X's fields as {[type_name, pointer_depth, offset, byte_size, ...]}
|
||||
-- The scan-source pass emits each Binds_X's fields as {[type_name, pointer_depth, offset, byte_size, ...]}
|
||||
-- so this pass can build the rbind_structs entry without re-parsing.
|
||||
local binds_by_name = {}
|
||||
for _, src in ipairs(ctx.sources or {}) do
|
||||
for _, src in ipairs((corpus and corpus.source_order) or {}) do
|
||||
local scan = src.scan
|
||||
if scan then
|
||||
for _, b in ipairs(scan.binds or {}) do
|
||||
@@ -986,7 +920,7 @@ local function parse_rbind_atoms(ctx, atom_table, registries)
|
||||
|
||||
-- Walk every atom_info; if `binds` is set, find the atom body_tokens + parse load_word pairs.
|
||||
local body_tokens_by_atom = {}
|
||||
for _, src in ipairs(ctx.sources or {}) do
|
||||
for _, src in ipairs((corpus and corpus.source_order) or {}) do
|
||||
local scan = src.scan
|
||||
if scan then
|
||||
for _, atom in ipairs(scan.atoms or {}) do
|
||||
@@ -996,7 +930,7 @@ local function parse_rbind_atoms(ctx, atom_table, registries)
|
||||
end
|
||||
|
||||
local ai_by_atom = {}
|
||||
for _, src in ipairs(ctx.sources or {}) do
|
||||
for _, src in ipairs((corpus and corpus.source_order) or {}) do
|
||||
local scan = src.scan
|
||||
if scan then
|
||||
for _, ai in ipairs(scan.atom_infos or {}) do
|
||||
@@ -1014,9 +948,9 @@ local function parse_rbind_atoms(ctx, atom_table, registries)
|
||||
if #pairs > 0 then
|
||||
rbind_atoms[atom_name] = {
|
||||
binds = ai.binds,
|
||||
fields = struct.fields, -- {name, offset} from scan.binds
|
||||
fields = struct.fields, -- {name, offset} from scan.binds
|
||||
bytes = struct.bytes,
|
||||
regs = pairs, -- ordered list of {reg, field}
|
||||
regs = pairs, -- ordered list of {reg, field}
|
||||
info_line = ai.info_line,
|
||||
}
|
||||
table.insert(struct.atom_names, atom_name)
|
||||
@@ -1042,12 +976,12 @@ end
|
||||
--- Append per-atom line-program sequences to the existing main .debug_line unit
|
||||
--- (the final unit, referenced by the main CU's DW_AT_stmt_list).
|
||||
---
|
||||
--- The old implementation appended a new Unit 3.
|
||||
--- This builder extends the main compilation unit.
|
||||
--- No compilation unit pointed at it through DW_AT_stmt_list, so gdb ignored it.
|
||||
--- It also encoded byte 13 as the extended-opcode marker; byte 13 is actually the first special opcode.
|
||||
--- The existing final unit already contains hello_gte_tape.c as file index 11 and ends with a valid end_sequence.
|
||||
--- We preserve its bytes, append independent atom sequences, and increase only that unit's DWARF32 unit_length.
|
||||
--- @param existing string -- existing section bytes (verbatim)
|
||||
--- @param existing string -- existing section bytes, byte-for-byte
|
||||
--- @param atom_table table -- list of {name, addr, size_bytes, words, entries}
|
||||
--- @return string
|
||||
local function build_dwarf_line_section(existing, atom_table)
|
||||
@@ -1095,7 +1029,7 @@ end
|
||||
--- segment_size (1 byte) -- = 0
|
||||
--- [entries...] -- address(4) + length(4) per entry
|
||||
--- terminator -- address=0 + length=0 (8 zero bytes)
|
||||
--- @param existing string
|
||||
--- @param existing string
|
||||
--- @param atom_table table
|
||||
--- @return string
|
||||
local function build_dwarf_aranges_section(existing, atom_table)
|
||||
@@ -1133,7 +1067,7 @@ local function build_dwarf_aranges_section(existing, atom_table)
|
||||
return existing
|
||||
end
|
||||
|
||||
local unit_start = i
|
||||
local unit_start = i
|
||||
local unit_end_excl = i + 4 + ul
|
||||
is_last_unit = (unit_end_excl == #existing)
|
||||
|
||||
@@ -1361,7 +1295,7 @@ end
|
||||
--- Abbrev 100 (DW_TAG_compile_unit, with children):
|
||||
--- DW_AT_name (DW_FORM_strp) -- CU name
|
||||
--- DW_AT_comp_dir (DW_FORM_strp) -- compilation dir
|
||||
--- DW_AT_language (DW_FORM_data1) -- DW_LANG_C99
|
||||
--- DW_AT_language (DW_FORM_data1) -- one byte language code
|
||||
--- Abbrev 101 (DW_TAG_subprogram, with children):
|
||||
--- DW_AT_name (DW_FORM_string) -- atom function name
|
||||
--- DW_AT_low_pc (DW_FORM_addr) -- atom.addr
|
||||
@@ -1452,7 +1386,7 @@ local function build_new_abbrev()
|
||||
|
||||
-- Component step-into abstract + inline DIE abbreviations.
|
||||
local DW_INL_declared_inlined = 0x03 -- DWARF5 §3.33.3: "this subroutine was declared inline"
|
||||
-- Abstract subprograms now carry DW_AT_decl_file + DW_AT_decl_line so consumers can resolve the abstract origin back to its definition site
|
||||
-- Abstract subprograms carry DW_AT_decl_file and DW_AT_decl_line for definition-site resolution.
|
||||
-- even when no inlined_subroutine instance currently maps to it.
|
||||
-- DW_FORM_udata is consistent with the call_file/call_line forms on abbrev 108.
|
||||
local abbrev_abstract_subprogram = abbrev(ABBREV_ABSTRACT_SUBPROGRAM, DW_TAG_subprogram, false, -- DW_CHILDREN_no
|
||||
@@ -1540,8 +1474,7 @@ local function build_new_strings(atom_table, registries)
|
||||
-- Register names (one per unique debug-visible R_Name alias from the merged registry,
|
||||
-- filtered to MIPS GPR 0..31 — the same filter that build_inserted_children applies
|
||||
-- for the RR_<name> locals, so .debug_str entries stay in sync with .debug_info).
|
||||
-- Plex: Lua's pairs() is non-deterministic; sort the alias names first so the emitted
|
||||
-- .debug_str bytes are byte-identical across runs.
|
||||
-- Lua's pairs() is non-deterministic; sort the alias names first so the emitted$ .debug_str bytes are byte-identical across runs.
|
||||
local sorted_alias_names = {}
|
||||
for r_name, alias in pairs(registries.register_alias_registry or {}) do
|
||||
if alias.code and alias.code >= 0 and alias.code <= 31 then
|
||||
@@ -1563,8 +1496,8 @@ end
|
||||
--- Build the DWARF DIE bytes to insert into the MAIN CU as children, immediately
|
||||
--- before the main CU's root children-terminator (the final 0 byte of the CU).
|
||||
---
|
||||
--- The same content was emitted as a DETACHED synthetic CU appended after the main CU.
|
||||
--- GDB's PC lookup selects the main CU, so the synthetic CU was out of scope and `RR_PrimCursor` + `bind_args` never appeared in the current frame.
|
||||
--- Insert the DIEs as children of the main compilation unit.
|
||||
--- This keeps `RR_PrimCursor` and `bind_args` in scope for atom PCs.
|
||||
--- Inserting the DIEs as children of the main CU puts them in scope for every PC the main CU owns;
|
||||
--- including every atom PC (since `.debug_aranges` + `.debug_rnglists` already assign atom PCs to it).
|
||||
---
|
||||
@@ -1598,7 +1531,7 @@ end
|
||||
--- DW_AT_type = ref4 → structure_type DIE
|
||||
---
|
||||
--- **DOES NOT** emit the final 0 byte (root terminator).
|
||||
--- build_debug_info_section splices our bytes between the existing DIE bytes and that terminator, which is preserved verbatim.
|
||||
--- build_debug_info_section splices bytes ahead of the root terminator and preserves existing DIE bytes exactly.
|
||||
---
|
||||
--- **ref4 basis**: DW_FORM_ref4 is CU-relative (offset from the first byte of the CU header).
|
||||
--- Our inserted DIEs live in the main CU, so every ref4 = (target section offset) - main_cu_offset.
|
||||
@@ -1648,7 +1581,7 @@ local function build_inserted_children(main_cu_offset, main_cu_end_excl, atom_ta
|
||||
local insertion_start = main_cu_end_excl - 1
|
||||
|
||||
-- Closure state: bytes + next_offset + the typed-view/structure/abstract offset caches.
|
||||
-- All step-emitters mutate this state in place. Plex: Fleury expose structure over interface.
|
||||
-- All step-emitters mutate this state in place.
|
||||
local S = {
|
||||
bytes = {},
|
||||
next_offset = insertion_start, -- 0-based section offset of the NEXT byte to emit
|
||||
@@ -1658,7 +1591,7 @@ local function build_inserted_children(main_cu_offset, main_cu_end_excl, atom_ta
|
||||
member_base_type_offsets = {}, -- {[tn.."|"..byte_size.."|"..encoding] = section_offset}
|
||||
base_type_section_offset = nil, -- set by emit_unsigned_int_base_type
|
||||
}
|
||||
-- Plex helpers.
|
||||
|
||||
local function emit(s)
|
||||
S.bytes[#S.bytes + 1] = s
|
||||
S.next_offset = S.next_offset + #s
|
||||
@@ -1675,7 +1608,7 @@ local function build_inserted_children(main_cu_offset, main_cu_end_excl, atom_ta
|
||||
emit(string.char(4)) -- DW_FORM_data1 (DW_AT_byte_size)
|
||||
emit(string.char(DW_ATE_unsigned)) -- DW_FORM_data1 (DW_AT_encoding)
|
||||
-- (The function body below reads S.next_offset directly via the `next_offset` function;
|
||||
-- the old code used a stale local snapshot that stayed at base_type_section_offset.)
|
||||
-- this keeps offsets synchronized with emitted data.)
|
||||
local function next_offset() return S.next_offset end
|
||||
|
||||
-- Typed local views.
|
||||
@@ -1876,9 +1809,9 @@ local function build_inserted_children(main_cu_offset, main_cu_end_excl, atom_ta
|
||||
-- reusing the pre-emitted base type keeps the wire consistent.
|
||||
-- Once this chain is registered as `type_chain_offsets["U4|1"]`, step (e) of the per-RR_<R_Name> precedence chain will resolve `atom_type(U4 *)`
|
||||
-- declarations on aliases like `R_PrimCursor` and `R_OtBase` to `U4 *` (gdb renders as `(unsigned int *)` with the value displayed in hex).
|
||||
emit(uleb128(ABBREV_TYPED_VIEW_POINTER)) -- DW_TAG_pointer_type (abbrev 110; NOT 9; U4 chain target)
|
||||
emit(uleb128(ABBREV_TYPED_VIEW_POINTER)) -- DW_TAG_pointer_type (abbrev 110; NOT 9; U4 chain target)
|
||||
emit(elf_dwarf.write_u32_le(ref4_of(base_type_section_offset))) -- 4-byte ref4 → "unsigned int" base_type
|
||||
local u4_chain_offset = next_offset() - 5 -- 1 (uleb tag) + 4 (ref4) = 5 bytes; capture the pointer_type's start offset
|
||||
local u4_chain_offset = next_offset() - 5 -- 1 (uleb tag) + 4 (ref4) = 5 bytes; capture the pointer_type's start offset
|
||||
type_chain_offsets["U4|1"] = u4_chain_offset
|
||||
|
||||
-- 2) Emit one DW_TAG_structure_type per unique Binds_X.
|
||||
@@ -1939,7 +1872,7 @@ local function build_inserted_children(main_cu_offset, main_cu_end_excl, atom_ta
|
||||
end
|
||||
|
||||
-- 4) Emit per-atom DW_TAG_subprograms (children of main CU).
|
||||
-- Subprograms are named `<name>` (matching the nm symbol; the `code_` prefix was removed from the MipsAtom_ macro in code/duffle/lottes_tape.h).
|
||||
-- Subprogram names match nm symbols without a `code_` prefix.
|
||||
-- The gcc global `<name>[]` is a DW_TAG_variable without children; our subprogram has the wave-context var children.
|
||||
-- gdb's symbol resolution picks our subprogram (it has low_pc/high_pc + children) over the gcc global for function-context lookups.
|
||||
for _, atom in ipairs(atom_table) do
|
||||
@@ -2023,9 +1956,9 @@ local function build_inserted_children(main_cu_offset, main_cu_end_excl, atom_ta
|
||||
end
|
||||
end
|
||||
|
||||
-- 5-step precedence chain. Plex: data table; 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.
|
||||
-- Adding a step = 1 row in the table + 1 function. The 5-level nested if/else is gone.
|
||||
-- Each precedence rule is one table row and one function.
|
||||
-- Per-atom precomputed state is captured in upvalues: atom_view, reg_to_field_ctx, atom_view_ctx_fields,
|
||||
-- reg_to_field_phase, atom_view_phase_fields, field_type_by_name, reg_to_field, alias, type_chain_offsets.
|
||||
local PRECEDENCE_STEPS = {
|
||||
@@ -2068,7 +2001,7 @@ local function build_inserted_children(main_cu_offset, main_cu_end_excl, atom_ta
|
||||
end,
|
||||
}
|
||||
|
||||
-- Plex: iterate `by_alias` in sorted order (Lua's pairs() is non-deterministic;
|
||||
-- Iterate `by_alias` in sorted order (Lua's pairs() is non-deterministic;
|
||||
-- sorting ensures byte-identical DWARF output across builds).
|
||||
for _, r_name in ipairs(by_alias_order) do
|
||||
local alias = by_alias[r_name]
|
||||
@@ -2078,10 +2011,8 @@ local function build_inserted_children(main_cu_offset, main_cu_end_excl, atom_ta
|
||||
emit(rr_name .. "\0") -- DW_FORM_string (DW_AT_name)
|
||||
-- DW_FORM_exprloc: ULEB byte count + DW_OP_regN byte.
|
||||
-- DW_OP_reg0..reg31 occupy opcodes 0x50..0x6f; DW_OP_reg15 is 0x5f.
|
||||
-- Inlined from `reg_exprloc` (single caller; the function was 3 LOC).
|
||||
-- DW_FORM_exprloc: ULEB byte count + DW_OP_regN byte.
|
||||
-- Inlined from `reg_exprloc` (was at lines 1185-1188, dwarf_injection.lua; the function was 3 LOC and 1 caller).
|
||||
-- The `1` is the length prefix — DW_OP_regN occupies exactly 1 byte (the base opcode is 0x50; regN = 0x50 + N).
|
||||
-- DW_FORM_exprloc: ULEB byte count + DW_OP_regN byte.
|
||||
-- The `1` is the length prefix — DW_OP_regN occupies exactly 1 byte (the base opcode is 0x50; regN = 0x50 + N).
|
||||
-- `alias_code` is the MIPS GPR index (0..31) from the merged register_alias_registry.
|
||||
emit(uleb128(1) .. string.char(DW_OP_reg0 + alias_code)) -- DW_FORM_exprloc (DW_OP_regN from registry code)
|
||||
-- Precedence chain (a..e); step (f) is the void* fallback (initial value).
|
||||
@@ -2096,7 +2027,7 @@ local function build_inserted_children(main_cu_offset, main_cu_end_excl, atom_ta
|
||||
|
||||
-- If rbind, emit bind_args variable with PC-ranged location list.
|
||||
-- The loclist is in .debug_loclists, indexed by `DW_FORM_sec_offset` (4-byte section-relative offset).
|
||||
-- The piece chain is replaced by two PC ranges: [atom.addr, last_load+8) where every field is described as a tape-memory
|
||||
-- The location list uses two PC ranges: [atom.addr, last_load+8) describes every field as tape memory
|
||||
-- (DW_OP_bregN + offset) piece, and [last_load+8, atom.end) where every field is described as a GPR (DW_OP_regN) piece.
|
||||
if atom.rbind then
|
||||
local binds_name = atom.rbind.binds
|
||||
@@ -2110,8 +2041,8 @@ local function build_inserted_children(main_cu_offset, main_cu_end_excl, atom_ta
|
||||
-- Per-component invocation inlined_subroutine instances.
|
||||
-- Each invocation covers a contiguous .word range [start_pos, end_pos] within the atom.
|
||||
-- We compute the corresponding PC range from the atom's start + .word offsets × MIPS_BYTES_PER_WORD.
|
||||
-- call_file now resolves inv.call_file to the line-unit file index (previously hardcoded to ATOM_SOURCE_FILE_INDEX;
|
||||
-- that lost the call-site attribution for any invocation whose call site was NOT the atom's source file).
|
||||
-- Resolve `inv.call_file` to the line-unit file index;
|
||||
-- this preserves call-site attribution across source files.
|
||||
if atom.invocations and not atom.skip_over then
|
||||
for _, inv in ipairs(atom.invocations) do
|
||||
local inv_low = atom.addr + inv.start_pos * MIPS_BYTES_PER_WORD
|
||||
@@ -2128,7 +2059,7 @@ local function build_inserted_children(main_cu_offset, main_cu_end_excl, atom_ta
|
||||
emit(string.char(DIE_CHILDREN_TERMINATOR)) -- end of subprogram's children (DWARF5 §7.5.3)
|
||||
end
|
||||
|
||||
-- DO NOT emit a final 0 here — that's the main CU's root terminator, which build_debug_info_section preserves verbatim.
|
||||
-- Do not emit a final 0 here; build_debug_info_section preserves the root terminator byte.
|
||||
return table.concat(S.bytes)
|
||||
end
|
||||
|
||||
@@ -2150,7 +2081,7 @@ end
|
||||
---
|
||||
--- Fails safely by returning existing sections unchanged if the table walker can't find the table terminator (malformed input).
|
||||
---
|
||||
--- @param existing string -- existing .debug_abbrev bytes (verbatim)
|
||||
--- @param existing string -- existing .debug_abbrev bytes, byte-for-byte
|
||||
--- @param main_abbrev_offset integer -- 0-based offset into `existing` of the main CU's abbrev table
|
||||
--- @return string, integer -- (new_abbrev_bytes, offset_where_duplicate_table_starts = #existing)
|
||||
local function build_debug_abbrev_section(existing, main_abbrev_offset)
|
||||
@@ -2169,7 +2100,7 @@ local function build_debug_abbrev_section(existing, main_abbrev_offset)
|
||||
end
|
||||
|
||||
--- Build the new .debug_str: existing strings + new strings appended.
|
||||
--- @param existing string -- existing .debug_str bytes (verbatim)
|
||||
--- @param existing string -- existing .debug_str bytes, byte-for-byte
|
||||
--- @param atom_table table[]
|
||||
--- @param registries table -- merged registries from collect_per_source_registries
|
||||
--- @return string, integer, table -- (new_str_bytes, new_strings_offset, string_map)
|
||||
@@ -2180,8 +2111,6 @@ end
|
||||
|
||||
--- Build the new .debug_info: SPLICE inserted DIEs into the MAIN CU as children.
|
||||
---
|
||||
--- This function appended a DETACHED synthetic CU to the end of .debug_info.
|
||||
--- That put every atom DIE in a separate CU from the one GDB selected for PC lookup, so `RR_PrimCursor` + `bind_args` never appeared in scope.
|
||||
--- This implementation instead:
|
||||
--- 1. Builds the inserted-children bytes (base_type, struct_types, subprograms with their RR_* + bind_args children) via build_inserted_children.
|
||||
--- 2. Patches the main CU's `unit_length` field to account for the inserted bytes.
|
||||
@@ -2190,7 +2119,7 @@ end
|
||||
--- 5. Replaces the main CU's final byte (the root children-terminator, 0) with: inserted_children_bytes + a single 0 byte (root terminator preserved).
|
||||
---
|
||||
--- The crt CU (everything before main_cu_start) is preserved.
|
||||
--- **No detached synthetic CU is appended.**
|
||||
|
||||
--- @param existing string -- existing .debug_info section bytes
|
||||
--- @param main_cu_start integer -- 0-based offset of the main CU's unit_length field
|
||||
--- @param main_cu_end_excl integer -- 0-based offset of the first byte AFTER the main CU
|
||||
@@ -2215,44 +2144,33 @@ local function build_debug_info_section(existing, main_cu_start, main_cu_end_exc
|
||||
|
||||
-- 4) Splice. All offsets below are 0-based; existing:sub is 1-indexed inclusive.
|
||||
-- Byte ranges (0-based, inclusive):
|
||||
-- [0 .. main_cu_start - 1] crt CU (verbatim)
|
||||
-- [0 .. main_cu_start - 1] crt CU, unchanged
|
||||
-- [main_cu_start + 0 .. + 3] unit_length (PATCHED)
|
||||
-- [main_cu_start + 4 .. + 7] version + unit_type + address_size (verbatim)
|
||||
-- [main_cu_start + 4 .. + 7] version + unit_type + address_size, unchanged
|
||||
-- [main_cu_start + 8 .. + 11] debug_abbrev_offset (PATCHED)
|
||||
-- [main_cu_start + 12 .. main_cu_end_excl - 2] existing DIE bytes (verbatim)
|
||||
-- [main_cu_end_excl - 1] root children-terminator (verbatim 0)
|
||||
-- [main_cu_start + 12 .. main_cu_end_excl - 2] existing DIE bytes, unchanged
|
||||
-- [main_cu_end_excl - 1] root children-terminator, unchanged 0
|
||||
local pre_end = main_cu_end_excl - 2 -- 0-based end of existing DIE bytes (inclusive)
|
||||
local root_terminator = main_cu_end_excl - 1 -- 0-based position of the final 0 byte
|
||||
|
||||
return existing:sub(1, main_cu_start) -- crt CU
|
||||
.. new_unit_length_bytes -- patched unit_length (4 bytes)
|
||||
.. existing:sub(main_cu_start + 5, main_cu_start + 8) -- version(2) + unit_type(1) + address_size(1) verbatim
|
||||
.. existing:sub(main_cu_start + 5, main_cu_start + 8) -- version(2) + unit_type(1) + address_size(1), unchanged
|
||||
.. new_abbrev_offset_bytes -- patched debug_abbrev_offset (4 bytes)
|
||||
.. existing:sub(main_cu_start + 13, pre_end + 1) -- existing DIE bytes verbatim
|
||||
.. existing:sub(main_cu_start + 13, pre_end + 1) -- existing DIE bytes, unchanged
|
||||
.. inserted -- our inserted children
|
||||
.. existing:sub(root_terminator + 1, main_cu_end_excl) -- root children-terminator (verbatim 0)
|
||||
.. existing:sub(root_terminator + 1, main_cu_end_excl) -- root children-terminator, unchanged 0
|
||||
end
|
||||
|
||||
--- Build the .debug_loc: just a terminator.
|
||||
--- Atoms don't have stack frames. The .debug_loc section describes per-instruction location adjustments for call-frame-based variables;
|
||||
--- We use DW_OP_regN which is register-based and doesn't need .debug_loc entries).
|
||||
--- The section itself must not be empty OR gdb may complain; the DW_LLE_end_of_list marker (per DWARF5 §7.7) is a single byte 0x00.
|
||||
--- Single-caller (M.run's SECTION_BUILDERS table below); replaced by a literal at the call site.
|
||||
-- local function build_debug_loc_section() return string.char(0x00) end
|
||||
--
|
||||
-- The actual .debug_loc emission is `string.char(DW_LLE_end_of_list)` inlined
|
||||
-- at the per-section writers below (the dispatch is in M.run, not a table).
|
||||
|
||||
local SECTION_BUILDERS = {
|
||||
debug_line = build_dwarf_line_section,
|
||||
debug_aranges = build_dwarf_aranges_section,
|
||||
debug_rnglists = build_dwarf_rnglists_section,
|
||||
debug_abbrev = build_debug_abbrev_section,
|
||||
debug_info = build_debug_info_section,
|
||||
debug_str = build_debug_str_section,
|
||||
debug_loc = function() return string.char(DW_LLE_end_of_list) end,
|
||||
-- debug_loclists is fed directly in M.run (it needs the merged registries for the R_TapePtr GPR lookup;
|
||||
-- the SECTION_BUILDERS table cannot carry that context, so the dispatch is inlined in the per-run writers loop).
|
||||
}
|
||||
|
||||
-- Per-section output path resolver (mirrors SECTION_BUILDERS).
|
||||
-- Per-section output path resolver.
|
||||
-- Returns the on-disk path for the section's `.bin` blob.
|
||||
local SECTION_WRITERS = {
|
||||
debug_line = function(out_root, basename) return out_root .. "\\" .. basename .. ".dwarf_line.bin" end,
|
||||
@@ -2326,10 +2244,10 @@ function M.run(ctx)
|
||||
|
||||
-- Read the existing DWARF sections directly (no subprocess; lfs + io.open + manual ELF32 section-header walk).
|
||||
-- We need all 8 sections: .debug_line / .debug_aranges / .debug_rnglists get extended
|
||||
-- (additional rows appended to the existing unit), and .debug_info / .debug_abbrev / .debug_str / .debug_loc / .debug_loclists
|
||||
-- (additional rows appended to the existing unit), and .debug_info / .debug_abbrev / .debug_str / .debug_loc / .debug_loclists
|
||||
-- get spliced (the main CU's unit_length is patched;
|
||||
-- no new compile unit is appended; debug_loc/debug_loclists may not exist in the source ELF so we add-section them on splice).
|
||||
-- The dispatch (SECTION_BUILDERS) handles each.
|
||||
-- The per-section dispatch is inlined in the writers loop below.
|
||||
local existing_sections = elf_dwarf.read_elf_sections(elf_path, {
|
||||
".debug_line", ".debug_aranges", ".debug_rnglists",
|
||||
".debug_info", ".debug_abbrev", ".debug_str",
|
||||
@@ -2353,14 +2271,21 @@ function M.run(ctx)
|
||||
-- then build the atom/provenance table with those generic selections.
|
||||
-- Whole atoms remain symbol-keyed; components are file-qualified internally so a source marker associates
|
||||
-- with its exact component definition even though GDB 12 requires function-only skip entries for the resulting synthetic inline frame.
|
||||
local skip_over = collect_skip_over(ctx)
|
||||
local registries = collect_per_source_registries(ctx)
|
||||
local atom_table = build_atom_table(ctx, skip_over)
|
||||
io.stderr:write(string.format("[dwarf_injection] matched %d atoms between nm + source-map\n", #atom_table))
|
||||
-- `corpus` is the sole canonical source projection;
|
||||
-- the sole source of truth; no `ctx.sources` / `ctx.by_dir` aliases).
|
||||
local corpus = (ctx.shared and ctx.shared.corpus) or {}
|
||||
local skip_over = collect_skip_over(corpus)
|
||||
local registries = collect_per_source_registries(corpus)
|
||||
-- Read nm symbols (the ONLY disk-side input to the atom table) and join
|
||||
-- them against `corpus.atoms_by_name` + `atom.paths` for word rows + invocation ancestry.
|
||||
-- Disk source-map/provenance text is NOT consulted (those are diagnostic artifacts; semantic inputs are in memory).
|
||||
local addrs = elf_dwarf.read_nm(ctx.flags.elf_path)
|
||||
local atom_table = build_atom_table(corpus, addrs, skip_over)
|
||||
io.stderr:write(string.format("[dwarf_injection] matched %d atoms between nm + corpus.atoms_by_name\n", #atom_table))
|
||||
|
||||
-- Detect rbind atoms + index Binds_* struct fields (from ctx.sources[i].scan, populated by scan-source pass).
|
||||
-- The merged registries are threaded through so parse_body_load_pairs resolves R_<reg> via register_alias_registry.
|
||||
local _rbind_atoms, rbind_structs = parse_rbind_atoms(ctx, atom_table, registries)
|
||||
local _rbind_atoms, rbind_structs = parse_rbind_atoms(corpus, atom_table, registries)
|
||||
local rbind_count = 0
|
||||
for _ in pairs(_rbind_atoms) do rbind_count = rbind_count + 1 end
|
||||
io.stderr:write(string.format("[dwarf_injection] matched %d rbind atoms across %d Binds_* structs\n",
|
||||
@@ -2369,7 +2294,7 @@ function M.run(ctx)
|
||||
-- Write the .bin files. The build_psyq.ps1 post-link hook splices these into a copy of the ELF via objcopy --update-section.
|
||||
-- Build order:
|
||||
-- 0. Validate .debug_info layout (crT CU + DWARF5 main CU + final 0 root terminator).
|
||||
-- If validation fails, FAIL SAFELY by writing the existing sections verbatim (no malformed output, no synthetic CU append, no header patch).
|
||||
-- If validation fails, write existing sections unchanged and emit no synthetic data.
|
||||
-- 1. Build new .debug_abbrev using the main CU's abbrev offset → returns the offset of the duplicate main table (= #existing_abbrev).
|
||||
-- 2. Build new .debug_info by splicing inserted children into the main CU (patches main CU's unit_length + debug_abbrev_offset;
|
||||
-- preserves all original DIE bytes; does NOT append a synthetic CU).
|
||||
@@ -2378,6 +2303,7 @@ function M.run(ctx)
|
||||
--
|
||||
-- .debug_str now also receives the new RR_<R_Name> entries
|
||||
-- (the merged registry drives the strings table to keep .debug_str and .debug_info in sync).
|
||||
-- (the per-section dispatch is inlined in the writers loop below)
|
||||
local basename = ctx.basename or duffle.basename_no_ext(elf_path) or DEFAULT_BASENAME
|
||||
if ctx.out_root and ctx.out_root ~= "" then
|
||||
duffle.ensure_dir(ctx.out_root)
|
||||
@@ -2447,7 +2373,7 @@ function M.run(ctx)
|
||||
return { outputs = {}, errors = {}, warnings = {} }
|
||||
end
|
||||
|
||||
-- Test-only re-exports: keep the module's main M table lean while letting scratch
|
||||
-- Test-only exports expose the emission and offset paths.
|
||||
-- tests drive the real emission and offset computation paths.
|
||||
M.compute_loclists_offsets_for_test = compute_loclists_offsets
|
||||
M.build_debug_loclists_section_for_test = build_debug_loclists_section
|
||||
|
||||
@@ -0,0 +1,184 @@
|
||||
--- 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, ... }
|
||||
--- @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
|
||||
|
||||
-- Project once, collect errors + warnings for one atom.
|
||||
-- Kind must be one of: atom | raw_atom | comp_bare | comp_proc.
|
||||
local function process_atom(atom, src)
|
||||
if not (atom and atom.body) then return end
|
||||
local kind = atom.kind
|
||||
if kind ~= "atom" and kind ~= "raw_atom" and kind ~= "comp_bare" and kind ~= "comp_proc" then
|
||||
return
|
||||
end
|
||||
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
|
||||
|
||||
-- Walk every source in canonical order; for each source, iterate atoms + raw_atoms.
|
||||
-- Recognized kinds (atom | raw_atom | comp_bare | comp_proc) each receive the atom.paths projection via duffle.project_emission.
|
||||
-- Components are macros inlined into atom bodies; focused tests and isolated component analyses consume atom.paths directly.
|
||||
for _, src in ipairs(corpus.source_order) do
|
||||
local scan = src.scan or {}
|
||||
for _, atom in ipairs(scan.atoms or {}) do
|
||||
process_atom(atom, src)
|
||||
end
|
||||
for _, atom in ipairs(scan.raw_atoms or {}) do
|
||||
process_atom(atom, src)
|
||||
end
|
||||
end
|
||||
|
||||
return {
|
||||
outputs = outputs,
|
||||
errors = errors,
|
||||
warnings = warnings,
|
||||
}
|
||||
end
|
||||
|
||||
return M
|
||||
+88
-197
@@ -21,18 +21,12 @@
|
||||
-- Bootstrap: load `duffle_paths.lua` via `debug.getinfo(1, "S").source` (works both standalone + when require'd).
|
||||
-- duffle_paths.lua sets package.path then returns `require("duffle")` at the bottom, so the dofile value IS the duffle module.
|
||||
local _bootstrap_dir = debug.getinfo(1, "S").source:match("^@?(.*[/\\])") or "./"
|
||||
local duffle = dofile(_bootstrap_dir .. "../duffle_paths.lua")
|
||||
local word_count_eval = require("word_count_eval")
|
||||
local count_token_words = word_count_eval.count_token_words
|
||||
local duffle = dofile(_bootstrap_dir .. "../duffle_paths.lua")
|
||||
|
||||
-- ════════════════════════════════════════════════════════════════════════════
|
||||
-- Constants
|
||||
-- ════════════════════════════════════════════════════════════════════════════
|
||||
|
||||
-- Marker-call identifiers inside atom bodies.
|
||||
local LABEL_MARKER = "atom_label"
|
||||
local OFFSET_MARKER = "atom_offset"
|
||||
|
||||
-- Offset macro/enum naming prefixes (the emitted header uses these).
|
||||
local OFFSET_MACRO_PREFIX = "_atom_offset_"
|
||||
local OFFSET_ENUM_PREFIX = "atom_offset_"
|
||||
@@ -52,16 +46,10 @@ local OFFSET_MACRO_COL = 44
|
||||
--- @field scan table -- pre-scanned SourceScan payload (from duffle.scan_source)
|
||||
|
||||
--- @class PassCtx
|
||||
--- @field sources SourceFile[] -- all source files in the build
|
||||
--- @field metadata_path string -- path to word_count.metadata.h
|
||||
--- @field shared table -- cross-pass shared state
|
||||
--- @field shared.word_counts table -- macro name -> word count
|
||||
--- @field shared.corpus table -- canonical corpus projection
|
||||
--- @field shared.word_counts table
|
||||
--- @field out_root string -- output root (e.g. "build/gen")
|
||||
--- @field project_root string -- project root (e.g. "code/")
|
||||
--- @field upstream table<string, table> -- per-pass upstream outputs
|
||||
--- @field flags table -- CLI flags
|
||||
--- @field dry_run boolean -- if true, compute but don't write
|
||||
--- @field verbose boolean -- log diagnostic info
|
||||
|
||||
--- @class PassResult
|
||||
--- @field outputs table[] -- {kind=, path=} entries describing emit files
|
||||
@@ -69,10 +57,10 @@ local OFFSET_MACRO_COL = 44
|
||||
--- @field warnings table[] -- {line=, msg=} entries; build-succeeds
|
||||
|
||||
--- @class BranchOffset
|
||||
--- @field tag string -- the marker tag (e.g. "F" in `atom_offset(F, T)`)
|
||||
--- @field target string -- the target label name (e.g. "T" in `atom_offset(F, T)`)
|
||||
--- @field pos integer -- the branch's word position within the atom body
|
||||
--- @field offset integer -- computed `target_word - branch_word - 1`
|
||||
--- @field tag string -- the marker tag (e.g. "F" in `atom_offset(F, T)`)
|
||||
--- @field target string -- the target label name (e.g. "T" in `atom_offset(F, T)`)
|
||||
--- @field branch_word integer -- branch word position within the atom body
|
||||
--- @field offset integer -- computed `target_word - branch_word - 1`
|
||||
|
||||
--- @class AtomData
|
||||
--- @field name string -- atom name
|
||||
@@ -80,169 +68,85 @@ local OFFSET_MACRO_COL = 44
|
||||
--- @field offsets BranchOffset[] -- per-branch offset list
|
||||
|
||||
-- ════════════════════════════════════════════════════════════════════════════
|
||||
-- Per-token marker-call helpers (atom_label / atom_offset inside bodies)
|
||||
-- Canonical marker projection
|
||||
-- ════════════════════════════════════════════════════════════════════════════
|
||||
|
||||
-- Extract comma-separated identifier args from a parenthesized group after a function-like macro call.
|
||||
-- Returns (args, after_paren) where `after_paren` is the position just past the closing `)`, or nil if `token` did not start with `(`.
|
||||
-- @param token string
|
||||
-- @param after_ident integer
|
||||
-- @return string[], integer|nil
|
||||
local function extract_ident_args(token, after_ident)
|
||||
local arg_start = duffle.skip_ws_and_cmt(token, after_ident)
|
||||
if token:sub(arg_start, arg_start) ~= "(" then return {}, nil end
|
||||
local inner, after_paren = duffle.read_parens(token, arg_start)
|
||||
-- scan: <marker>(<args>)
|
||||
|
||||
local args = {}
|
||||
local pos = 1
|
||||
local inner_len = #inner
|
||||
while pos <= inner_len do
|
||||
pos = duffle.skip_ws_and_cmt(inner, pos)
|
||||
if pos > inner_len then break end
|
||||
local ident, after = duffle.read_ident(inner, pos)
|
||||
if ident and ident ~= "" then
|
||||
table.insert(args, ident)
|
||||
pos = after
|
||||
else
|
||||
pos = pos + 1
|
||||
end
|
||||
pos = duffle.skip_ws_and_cmt(inner, pos)
|
||||
if pos <= inner_len and inner:sub(pos, pos) == "," then pos = pos + 1 end
|
||||
end
|
||||
|
||||
return args, after_paren
|
||||
end
|
||||
|
||||
-- (internal) Record a `atom_label(name)` marker — `at_pos` is the branch-free word position within the atom body.
|
||||
-- @param labels table<string, integer>
|
||||
-- @param args string[]
|
||||
-- @param at_pos integer
|
||||
local function record_label_marker(labels, args, at_pos)
|
||||
if #args >= 1 then labels[args[1]] = at_pos end
|
||||
end
|
||||
|
||||
-- (internal) Record a `atom_offset(tag, target)` marker.
|
||||
-- @param branches table[] -- list of {pos=, target=, tag=}
|
||||
-- @param args string[]
|
||||
-- @param at_pos integer
|
||||
local function record_offset_marker(branches, args, at_pos)
|
||||
if #args >= 2 then
|
||||
table.insert(branches, { pos = at_pos, target = args[2], tag = args[1] })
|
||||
end
|
||||
end
|
||||
|
||||
-- MARKER_TO_HANDLER — data-driven marker dispatch (the plex pattern).
|
||||
-- Maps the marker ident to its recorder function. Each handler takes (out_table, args, at_pos).
|
||||
-- Adding a new marker type = 1 row + 1 recorder function.
|
||||
local MARKER_TO_HANDLER = {
|
||||
[LABEL_MARKER] = record_label_marker,
|
||||
[OFFSET_MARKER] = record_offset_marker,
|
||||
-- MARKER_PROJECTORS is the marker-kind data table.
|
||||
-- The emission-model pass already records marker word positions;
|
||||
-- this pass only projects those records into the label/branch lookup shape needed by offset computation.
|
||||
local MARKER_PROJECTORS = {
|
||||
label = function(state, marker)
|
||||
state.labels[marker.name] = marker.word_index
|
||||
end,
|
||||
offset = function(state, marker)
|
||||
state.branches[#state.branches + 1] = {
|
||||
tag = marker.name,
|
||||
target = marker.target,
|
||||
branch_word = marker.word_index,
|
||||
}
|
||||
end,
|
||||
}
|
||||
|
||||
--- Scan a single token for atom_label/atom_offset markers, walking through balanced groups transparently (so nested calls are found).
|
||||
--- @param token string
|
||||
--- @param at_pos integer -- the branch-free word position of this token in the body
|
||||
--- @param labels table<string, integer>
|
||||
--- @param branches table[]
|
||||
local function scan_for_atom_markers(token, at_pos, labels, branches)
|
||||
local pos = 1
|
||||
local tok_len = #token
|
||||
while pos <= tok_len do
|
||||
pos = duffle.skip_ws_and_cmt(token, pos)
|
||||
if pos > tok_len then break end
|
||||
local ch = token:sub(pos, pos)
|
||||
if duffle.is_alpha(ch) then
|
||||
local ident, after = duffle.read_ident(token, pos)
|
||||
local handler = MARKER_TO_HANDLER[ident]
|
||||
if handler then
|
||||
local args, after_paren = extract_ident_args(token, after)
|
||||
-- Marker found — dispatch to its recorder. markers share labels and branches as
|
||||
-- out-tables; the recorder picks which one(s) to write to based on its semantics.
|
||||
-- (record_label_marker writes to labels; record_offset_marker writes to branches.)
|
||||
handler(ident == LABEL_MARKER and labels or branches, args, at_pos)
|
||||
pos = after_paren or after
|
||||
else
|
||||
pos = after
|
||||
end
|
||||
else
|
||||
local nx = duffle.skip_str_or_cmt(token, pos)
|
||||
pos = (nx > pos) and nx or (pos + 1)
|
||||
end
|
||||
--- Project canonical marker records into the two lookup tables used by the offset renderer.
|
||||
--- No source text, body text, or body token is inspected.
|
||||
--- @param markers table[] -- atom.paths.markers
|
||||
--- @return table<string, integer>, table[]
|
||||
local function project_markers(markers)
|
||||
local state = { labels = {}, branches = {} }
|
||||
for _, marker in ipairs(markers or {}) do
|
||||
local project = MARKER_PROJECTORS[marker.kind]
|
||||
if project then project(state, marker) end
|
||||
end
|
||||
end
|
||||
|
||||
--- Scan an atom body for labels + branches, count total words.
|
||||
--- Returns (labels, branches, total_words).
|
||||
--- @param body string
|
||||
--- @param word_counts table
|
||||
--- @return table<string, integer>, table[], integer
|
||||
-- scan_atom_body: walk pre-tokenized body for atom_label/atom_offset markers + word counts.
|
||||
-- Uses `atom.body_tokens` from the SourceScan payload (pre-tokenized by scan-source pass).
|
||||
-- @param body_tokens table[] -- {{tok=string, rel=integer}, ...} from duffle.tokenize_body
|
||||
-- @param word_counts table
|
||||
-- @return table, table, integer -- labels, branches, total_words
|
||||
local function scan_atom_body(body_tokens, word_counts)
|
||||
local pos = 0
|
||||
local labels = {}
|
||||
local branches = {}
|
||||
for _, t in ipairs(body_tokens) do
|
||||
local tok = t.tok
|
||||
if duffle.is_marker_token(tok) then
|
||||
-- Marker call: record at the current pos, do NOT advance pos.
|
||||
scan_for_atom_markers(tok, pos, labels, branches)
|
||||
pos = pos + duffle.count_marker_rest(tok, word_counts, count_token_words)
|
||||
else
|
||||
local words = count_token_words(tok, word_counts)
|
||||
scan_for_atom_markers(tok, pos, labels, branches)
|
||||
pos = pos + words
|
||||
end
|
||||
end
|
||||
return labels, branches, pos
|
||||
return state.labels, state.branches
|
||||
end
|
||||
|
||||
-- ════════════════════════════════════════════════════════════════════════════
|
||||
-- Offset computation + header generation
|
||||
-- ════════════════════════════════════════════════════════════════════════════
|
||||
|
||||
-- Compute branch offsets as `target_word - branch_word - 1` (the standard MIPS branch-immediate encoding).
|
||||
-- @param labels table<string, integer>
|
||||
-- @param branches table[]
|
||||
-- @return BranchOffset[]
|
||||
--- Compute branch offsets as `target_word - branch_word - 1` (the standard MIPS branch-immediate encoding).
|
||||
--- @param labels table<string, integer>
|
||||
--- @param branches table[]
|
||||
--- @return BranchOffset[]
|
||||
local function compute_offsets(labels, branches)
|
||||
local results = {}
|
||||
for _, br in ipairs(branches) do
|
||||
local target = labels[br.target]
|
||||
if not target then
|
||||
error("Branch target '" .. br.target .. "' has no atom_label (at word " .. br.pos .. ")")
|
||||
error("Branch target '" .. br.target .. "' has no atom_label (at word " .. br.branch_word .. ")")
|
||||
end
|
||||
results[#results + 1] = { target = br.target, tag = br.tag, offset = target - br.pos - 1 }
|
||||
results[#results + 1] = {
|
||||
target = br.target,
|
||||
tag = br.tag,
|
||||
branch_word = br.branch_word,
|
||||
offset = target - br.branch_word - 1,
|
||||
}
|
||||
end
|
||||
return results
|
||||
end
|
||||
|
||||
-- Right-pad `s` with spaces to width `w`. If `s` is already `w` or wider, no padding is added.
|
||||
-- @param s string
|
||||
-- @param w integer
|
||||
-- @return string
|
||||
--- Right-pad `s` with spaces to width `w`. If `s` is already `w` or wider, no padding is added.
|
||||
--- @param s string
|
||||
--- @param w integer
|
||||
--- @return string
|
||||
local function pad_right(s, w)
|
||||
return s .. string.rep(" ", math.max(0, w - #s))
|
||||
end
|
||||
|
||||
-- (internal) Build a constant-table entry `{macro_name, enum_name, value}` from a BranchOffset.
|
||||
-- @param r BranchOffset
|
||||
-- @return table
|
||||
local function make_offset_const(r)
|
||||
--- (internal) Build a constant-table entry `{macro_name, enum_name, value}` from a BranchOffset.
|
||||
--- @param bo BranchOffset
|
||||
--- @return table
|
||||
local function make_offset_const(bo)
|
||||
return {
|
||||
macro_name = OFFSET_MACRO_PREFIX .. r.tag .. "_" .. r.target,
|
||||
enum_name = OFFSET_ENUM_PREFIX .. r.tag .. "_" .. r.target,
|
||||
value = r.offset,
|
||||
macro_name = OFFSET_MACRO_PREFIX .. bo.tag .. "_" .. bo.target,
|
||||
enum_name = OFFSET_ENUM_PREFIX .. bo.tag .. "_" .. bo.target,
|
||||
value = bo.offset,
|
||||
}
|
||||
end
|
||||
|
||||
-- (internal) Emit one atom's offset constants + enum into the lines buffer.
|
||||
-- @param add fun(s: string)
|
||||
-- @param atom AtomData
|
||||
--- (internal) Emit one atom's offset constants + enum into the lines buffer.
|
||||
--- @param add fun(s: string)
|
||||
--- @param atom AtomData
|
||||
local function emit_atom_offsets(add, atom)
|
||||
if #atom.offsets == 0 then return end
|
||||
add("// --- atom: " .. atom.name .. " (" .. atom.total_words .. " words) ---")
|
||||
@@ -263,10 +167,10 @@ local function emit_atom_offsets(add, atom)
|
||||
add("")
|
||||
end
|
||||
|
||||
-- Generate the per-source .offsets.h header.
|
||||
-- @param source_path string
|
||||
-- @param atoms_data AtomData[]
|
||||
-- @return string
|
||||
--- Generate the per-source .offsets.h header.
|
||||
--- @param source_path string
|
||||
--- @param atoms_data AtomData[]
|
||||
--- @return string
|
||||
local function generate_header(source_path, atoms_data)
|
||||
local basename = duffle.basename_no_ext(source_path)
|
||||
|
||||
@@ -288,59 +192,41 @@ local function generate_header(source_path, atoms_data)
|
||||
return table.concat(lines, "\n") .. "\n"
|
||||
end
|
||||
|
||||
-- ════════════════════════════════════════════════════════════════════════════
|
||||
-- M — module exports
|
||||
-- ════════════════════════════════════════════════════════════════════════════
|
||||
|
||||
local M = {}
|
||||
|
||||
-- Project the pre-scanned SourceScan entries into the {name, body, body_tokens} shape this pass needs.
|
||||
-- MipsAtom_ entries have kind="atom"; MipsCode code_<name> entries have kind="raw_atom".
|
||||
-- `body_tokens` is set by scan-source on every `scan.atoms[i]` / `scan.raw_atoms[i]`; we carry it forward
|
||||
-- so `scan_atom_body` reads from the precomputed table directly (no per-atom tokenize_body fallback).
|
||||
-- @param scan table -- SourceScan from duffle.scan_source
|
||||
-- @return table[] -- list of {name=, body=, body_tokens=}
|
||||
local function project_atoms(scan)
|
||||
local out = {}
|
||||
for _, a in ipairs(scan.atoms) do
|
||||
out[#out + 1] = { name = a.raw_name, body = a.body, body_tokens = a.body_tokens }
|
||||
end
|
||||
for _, a in ipairs(scan.raw_atoms) do
|
||||
out[#out + 1] = { name = a.name, body = a.body, body_tokens = a.body_tokens }
|
||||
end
|
||||
return out
|
||||
end
|
||||
|
||||
-- (internal) Process one source: project atoms from scan, scan bodies, write header.
|
||||
-- Returns the offsets_h path if a header was written, or nil.
|
||||
-- @param ctx PassCtx
|
||||
-- @param src SourceFile
|
||||
-- @return string|nil -- the offsets_h path
|
||||
--- (internal) Process one source: render offsets from canonical atom paths.
|
||||
--- Returns the offsets_h path if a header was written, or nil.
|
||||
--- @param ctx PassCtx
|
||||
--- @param src SourceFile
|
||||
--- @return string|nil -- the offsets_h path
|
||||
local function process_source(ctx, src)
|
||||
local atoms = project_atoms(src.scan)
|
||||
if #atoms == 0 then return nil end
|
||||
|
||||
local atoms_data = {}
|
||||
for _, atom in ipairs(atoms) do
|
||||
local labels, branches, total = scan_atom_body(atom.body_tokens, ctx.shared.word_counts)
|
||||
local scan = src.scan or {}
|
||||
|
||||
local function append_atom(atom)
|
||||
local paths = atom and atom.paths
|
||||
if not paths then return end
|
||||
local labels, branches = project_markers(paths.markers)
|
||||
atoms_data[#atoms_data + 1] = {
|
||||
name = atom.name,
|
||||
total_words = total,
|
||||
name = atom.raw_name or atom.name,
|
||||
total_words = #(paths.word_events or {}),
|
||||
offsets = compute_offsets(labels, branches),
|
||||
}
|
||||
end
|
||||
|
||||
for _, atom in ipairs(scan.atoms or {}) do append_atom(atom) end
|
||||
for _, atom in ipairs(scan.raw_atoms or {}) do append_atom(atom) end
|
||||
if #atoms_data == 0 then return nil end
|
||||
|
||||
local out_path = src.dir .. "/gen/" .. duffle.basename_no_ext(src.dir) .. ".offsets.h"
|
||||
if not ctx.dry_run then
|
||||
duffle.ensure_dir(duffle.dirname(out_path))
|
||||
duffle.write_file(out_path, generate_header(src.path, atoms_data))
|
||||
end
|
||||
duffle.ensure_dir(duffle.dirname(out_path))
|
||||
duffle.write_file(out_path, generate_header(src.path:gsub("/", "\\"), atoms_data))
|
||||
return out_path
|
||||
end
|
||||
|
||||
--- Run the offsets pass.
|
||||
--- For each source, emits a per-module `<dir_basename>.offsets.h` containing `#define _atom_offset_F_T = N` constants
|
||||
--- for every `atom_offset(F, T)` reference in the source's atoms.
|
||||
--- For each canonical source, emits a per-module `<dir_basename>.offsets.h`
|
||||
--- containing constants for every marker recorded in atom.paths.
|
||||
--- @param ctx PassCtx
|
||||
--- @return PassResult
|
||||
function M.run(ctx)
|
||||
@@ -348,7 +234,12 @@ function M.run(ctx)
|
||||
local errors = {}
|
||||
local warnings = {}
|
||||
|
||||
for _, src in ipairs(ctx.sources) do
|
||||
local corpus = ctx.shared and ctx.shared.corpus
|
||||
if type(corpus) ~= "table" or type(corpus.source_order) ~= "table" then
|
||||
error("offsets.run requires ctx.shared.corpus.source_order (canonical corpus).", 0)
|
||||
end
|
||||
|
||||
for _, src in ipairs(corpus.source_order) do
|
||||
local out_path = process_source(ctx, src)
|
||||
if out_path then
|
||||
outputs[#outputs + 1] = { offsets_h = out_path }
|
||||
|
||||
+53
-47
@@ -5,8 +5,10 @@
|
||||
--- - `build/gen/<dir_basename>.annotations.txt` — one per source-directory containing atoms; aggregates across all sources in the directory.
|
||||
--- - `build/gen/annotation_validation.txt` — the project summary.
|
||||
---
|
||||
--- The annotation pass stashes per-MODULE summary entries in `ctx.flags._annot_results` (set by `passes/annotation.lua`).
|
||||
--- This pass re-validates each source via `annotation.validate()` to get the detailed per-source results needed for the report.
|
||||
--- The annotation pass emits `errors.h` files per module and the canonical
|
||||
--- `corpus.sources_by_dir` projection groups sources by directory. This pass
|
||||
--- iterates the canonical dir projection directly and re-validates each source
|
||||
--- via `annotation.validate()` to get the detailed per-source results.
|
||||
---
|
||||
--- **Conventions**: tabs (1/level), EmmyLua annotations, no regex,
|
||||
--- Lua 5.3 compatible.
|
||||
@@ -25,6 +27,13 @@
|
||||
local _bootstrap_dir = debug.getinfo(1, "S").source:match("^@?(.*[/\\])") or "./"
|
||||
local duffle = dofile(_bootstrap_dir .. "../duffle_paths.lua")
|
||||
|
||||
-- Load the annotation pass so we can re-validate each source against the
|
||||
-- canonical corpus projection. The annotation pass exposes `M.validate`,
|
||||
-- which returns the per-source AnnotationResult (atoms / annots / macros /
|
||||
-- binds / errors / warnings) that the report pass renders into the
|
||||
-- per-module `<dir_basename>.annotations.txt` output.
|
||||
local annotation = dofile(_bootstrap_dir .. "annotation.lua")
|
||||
|
||||
-- ════════════════════════════════════════════════════════════════════════════
|
||||
-- Constants
|
||||
-- ════════════════════════════════════════════════════════════════════════════
|
||||
@@ -67,8 +76,6 @@ local PASS_NAME = "report"
|
||||
--- @field project_root string -- project root (e.g. "code/")
|
||||
--- @field upstream table<string, table> -- per-pass upstream outputs
|
||||
--- @field flags table -- CLI flags + per-pass stash
|
||||
--- @field flags._annot_results ModuleEntry[] -- stashed by annotation pass
|
||||
--- @field dry_run boolean -- if true, compute but don't write
|
||||
--- @field verbose boolean -- if true, log diagnostic info
|
||||
|
||||
--- @class PassResult
|
||||
@@ -138,31 +145,31 @@ local PASS_NAME = "report"
|
||||
-- Per-MODULE annotation report (aggregated across all sources in a dir)
|
||||
-- ════════════════════════════════════════════════════════════════════════════
|
||||
|
||||
-- Extract the basename (last path segment) of a forward- or back-slash separated path. Returns the input unchanged if no separator is found.
|
||||
-- @param path string
|
||||
-- @return string
|
||||
--- Extract the basename (last path segment) of a forward- or back-slash separated path. Returns the input unchanged if no separator is found.
|
||||
--- @param path string
|
||||
--- @return string
|
||||
local function source_basename(path)
|
||||
return path:match(BASENAME_PATTERN) or path
|
||||
end
|
||||
|
||||
-- (internal) Format a single annotation entry as one rendered line.
|
||||
-- @param a AnnotEntry
|
||||
-- @param src_name string
|
||||
-- @return string
|
||||
--- (internal) Format a single annotation entry as one rendered line.
|
||||
--- @param a AnnotEntry
|
||||
--- @param src_name string
|
||||
--- @return string
|
||||
local function format_annot_line(a, src_name)
|
||||
if a.error then
|
||||
return string.format(" ✗ line %d %s [ERROR: %s] [%s]", a.line, a.macro or "?", a.error, src_name)
|
||||
end
|
||||
local line = string.format(" ● line %d %s [%s]", a.line, a.name, src_name)
|
||||
if a.binds then line = line .. " binds=" .. a.binds end
|
||||
if #a.reads > 0 then line = line .. " reads={" .. table.concat(a.reads, ",") .. "}" end
|
||||
if #a.writes > 0 then line = line .. " writes={" .. table.concat(a.writes, ",") .. "}" end
|
||||
if a.binds then line = line .. " binds=" .. a.binds end
|
||||
if #a.reads > 0 then line = line .. " reads={" .. table.concat(a.reads, ",") .. "}" end
|
||||
if #a.writes > 0 then line = line .. " writes={" .. table.concat(a.writes, ",") .. "}" end
|
||||
return line
|
||||
end
|
||||
|
||||
-- (internal) Tally totals across all results in a module.
|
||||
-- @param results AnnotationResult[]
|
||||
-- @return integer, integer, integer, integer, integer, integer
|
||||
--- (internal) Tally totals across all results in a module.
|
||||
--- @param results AnnotationResult[]
|
||||
--- @return integer, integer, integer, integer, integer, integer
|
||||
local function tally_module_totals(results)
|
||||
local total_atoms, total_annots, total_binds, total_macros = 0, 0, 0, 0
|
||||
local total_errors, total_warnings = 0, 0
|
||||
@@ -315,9 +322,7 @@ local function render_module_report(dir, sources, results)
|
||||
-- Each renderer writes its header + content via the `add` closure (pre-bound above).
|
||||
-- Adding a new section = 1 row here + 1 render_<thing>_section function.
|
||||
for _, section in ipairs(SECTION_RENDERERS) do
|
||||
add(section.header)
|
||||
section.render(add, results, totals)
|
||||
add("")
|
||||
end
|
||||
|
||||
return table.concat(lines, "\n") .. "\n"
|
||||
@@ -377,21 +382,22 @@ end
|
||||
-- Orchestration helpers
|
||||
-- ════════════════════════════════════════════════════════════════════════════
|
||||
|
||||
-- (internal) Pull per-source validate() results from the annotation pass's stash.
|
||||
-- The annotation pass runs first in the dep chain and caches results in `ctx.flags._annot_source_results`;
|
||||
-- we read from there instead of re-validating each source.
|
||||
-- Returns the list of module results + the flat list of all results (for the project-wide summary).
|
||||
-- @param ctx PassCtx
|
||||
-- @param dir_sources SourceFile[]
|
||||
-- @return AnnotationResult[], AnnotationResult[]
|
||||
--- (internal) Re-validate every source in a directory against the canonical
|
||||
--- corpus projection. Calls `annotation.validate()` per source to produce the
|
||||
--- per-source AnnotationResult (atoms / annots / macros / binds / errors /
|
||||
--- warnings) that the report renderer consumes. This is the canonical path —
|
||||
--- no private stash; each report pass run is reproducible from the corpus.
|
||||
--- Returns the list of module results + the flat list of all results (for the project-wide summary).
|
||||
--- @param ctx PassCtx
|
||||
--- @param dir_sources SourceFile[]
|
||||
--- @return AnnotationResult[], AnnotationResult[]
|
||||
local function lookup_module_results(ctx, dir_sources)
|
||||
local src_cache = (ctx.flags and ctx.flags._annot_source_results) or {}
|
||||
local module_results = {}
|
||||
local all_results = {}
|
||||
for _, src in ipairs(dir_sources) do
|
||||
local result = src_cache[src.path]
|
||||
if result then
|
||||
result.source = src.path -- defensive (annotation tags it too; this guards against cache misses from earlier iterations)
|
||||
if src.scan then
|
||||
local result = annotation.validate(ctx, src, nil)
|
||||
result.source = src.path -- tag for downstream rendering
|
||||
module_results[#module_results + 1] = result
|
||||
all_results[#all_results + 1] = result
|
||||
end
|
||||
@@ -399,9 +405,9 @@ local function lookup_module_results(ctx, dir_sources)
|
||||
return module_results, all_results
|
||||
end
|
||||
|
||||
-- (internal) Does this module's results contain anything worth emitting?
|
||||
-- @param module_results AnnotationResult[]
|
||||
-- @return boolean
|
||||
--- (internal) Does this module's results contain anything worth emitting?
|
||||
--- @param module_results AnnotationResult[]
|
||||
--- @return boolean
|
||||
local function module_has_content(module_results)
|
||||
for _, r in ipairs(module_results) do
|
||||
if #r.atoms > 0 or #r.annots > 0 or #r.binds > 0
|
||||
@@ -412,8 +418,8 @@ local function module_has_content(module_results)
|
||||
return false
|
||||
end
|
||||
|
||||
-- (internal) Log a debug message if `_G[DEBUG_FLAG]` is truthy.
|
||||
-- @param fmt string
|
||||
--- (internal) Log a debug message if `_G[DEBUG_FLAG]` is truthy.
|
||||
--- @param fmt string
|
||||
local function debug_log(fmt, ...)
|
||||
if _G[DEBUG_FLAG] then
|
||||
io.stderr:write(string.format("[%s] " .. fmt, PASS_NAME, ...))
|
||||
@@ -435,27 +441,27 @@ function M.run(ctx)
|
||||
local errors = {}
|
||||
local warnings = {}
|
||||
|
||||
local module_entries = (ctx.flags and ctx.flags._annot_results) or {}
|
||||
local by_dir = ctx.by_dir or duffle.group_sources_by_dir(ctx.sources)
|
||||
-- Module grouping comes from `corpus.sources_by_dir` (the canonical projection).
|
||||
-- Iterate it directly; no private cache, no per-pass stash.
|
||||
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
|
||||
duffle.ensure_dir(ctx.out_root)
|
||||
|
||||
local all_results_for_summary = {}
|
||||
for _, entry in ipairs(module_entries) do
|
||||
debug_log("entry: dir=%s basename=%s atoms_count=%d dir_sources=%d\n", entry.dir, entry.dir_basename, entry.atoms_count, #(by_dir[entry.dir] or {}))
|
||||
for dir, dir_sources in pairs(by_dir) do
|
||||
local dir_basename = dir:match("([^/\\]+)$") or dir
|
||||
debug_log("dir=%s basename=%s sources=%d\n", dir, dir_basename, #dir_sources)
|
||||
|
||||
if entry.atoms_count > 0 or #(by_dir[entry.dir] or {}) > 0 then
|
||||
local dir_sources = by_dir[entry.dir] or {}
|
||||
if #dir_sources > 0 then
|
||||
local module_results, all_results = lookup_module_results(ctx, dir_sources)
|
||||
for _, r in ipairs(all_results) do
|
||||
all_results_for_summary[#all_results_for_summary + 1] = r
|
||||
end
|
||||
|
||||
if module_has_content(module_results) then
|
||||
local out_path = ctx.out_root .. "/" .. entry.dir_basename .. ".annotations.txt"
|
||||
if not ctx.dry_run then
|
||||
duffle.write_file(out_path, render_module_report(entry.dir, dir_sources, module_results))
|
||||
end
|
||||
local out_path = ctx.out_root .. "/" .. dir_basename .. ".annotations.txt"
|
||||
duffle.write_file(out_path, render_module_report(dir, dir_sources, module_results))
|
||||
outputs[#outputs + 1] = { annotations_txt = out_path }
|
||||
else
|
||||
debug_log(" -> no content; skipping\n")
|
||||
@@ -463,7 +469,7 @@ function M.run(ctx)
|
||||
end
|
||||
end
|
||||
|
||||
if not ctx.dry_run and #all_results_for_summary > 0 then
|
||||
if #all_results_for_summary > 0 then
|
||||
local summary_path = ctx.out_root .. "/annotation_validation.txt"
|
||||
duffle.write_file(summary_path, render_project_report(all_results_for_summary))
|
||||
outputs[#outputs + 1] = { summary_txt = summary_path }
|
||||
|
||||
+465
-150
@@ -25,6 +25,13 @@
|
||||
local _bootstrap_dir = debug.getinfo(1, "S").source:match("^@?(.*[/\\])") or "./"
|
||||
local duffle = dofile(_bootstrap_dir .. "../duffle_paths.lua")
|
||||
|
||||
-- Forward declarations for helpers used by earlier parsers (parse_enum_body_fields needs parse_enum_int_literal;
|
||||
-- parse_typedef_binds needs duffle.find_byte).
|
||||
-- Lua local scoping rules require explicit forward declarations because locals are visible only AFTER their declaration site.
|
||||
-- The actual assignments happen later in this file;
|
||||
-- the closures captured by the early parsers resolve the upvalue at call time (Lua 5.3 / LuaJIT upvalue semantics).
|
||||
local parse_enum_int_literal
|
||||
|
||||
-- ════════════════════════════════════════════════════════════════════════════
|
||||
-- Type declarations
|
||||
-- ════════════════════════════════════════════════════════════════════════════
|
||||
@@ -110,7 +117,6 @@ local duffle = dofile(_bootstrap_dir .. "../duffle_paths.lua")
|
||||
--- @field project_root string
|
||||
--- @field upstream table<string, table>
|
||||
--- @field flags table
|
||||
--- @field dry_run boolean
|
||||
--- @field verbose boolean
|
||||
|
||||
--- @class PassResult
|
||||
@@ -286,13 +292,13 @@ local POINTER_BYTE_SIZE = 4
|
||||
-- Maximum chain depth when resolving typedef / TSet_ chains (cycle guard).
|
||||
local TYPE_CHAIN_MAX_DEPTH = 8
|
||||
|
||||
-- Walk a `Struct_` / `Enum_` body, calling `build_field(first, first_end, after_first)` for each entry.
|
||||
-- The builder returns either:
|
||||
-- - (record, new_pos) -- append record to fields; advance body_pos to new_pos
|
||||
-- - (nil, new_pos) -- skip this entry; advance body_pos to new_pos
|
||||
-- After each entry, the walker skips a single trailing `,` or `;`.
|
||||
-- The 2 body-field parsers in this file (struct + enum) share this body-walk loop.
|
||||
-- @param body string
|
||||
--- Walk a `Struct_` / `Enum_` body, calling `build_field(first, first_end, after_first)` for each entry.
|
||||
--- The builder returns either:
|
||||
--- - (record, new_pos) -- append record to fields; advance body_pos to new_pos
|
||||
--- - (nil, new_pos) -- skip this entry; advance body_pos to new_pos
|
||||
--- After each entry, the walker skips a single trailing `,` or `;`.
|
||||
--- The 2 body-field parsers in this file (struct + enum) share this body-walk loop.
|
||||
--- @param body string
|
||||
--- @param build_field fun(first: string, first_end: integer, after_first: integer): (table|nil, integer)
|
||||
--- @return table[]
|
||||
local function walk_body_fields(body, build_field)
|
||||
@@ -328,7 +334,7 @@ local function parse_struct_body_fields(body)
|
||||
-- Parse the trailing `*` chain to derive pointer_depth.
|
||||
local depth, cursor = 0, after_type
|
||||
while cursor <= #body and body:sub(cursor, cursor) == "*" do
|
||||
depth = depth + 1
|
||||
depth = depth + 1
|
||||
cursor = cursor + 1
|
||||
cursor = duffle.skip_ws_and_cmt(body, cursor)
|
||||
end
|
||||
@@ -385,14 +391,12 @@ local function resolve_typedef_byte_size(type_name, type_name_registry, visited,
|
||||
local entry = type_name_registry[type_name]
|
||||
if not entry then return nil end
|
||||
|
||||
-- Confident: this entry was already resolved by the propagation pass
|
||||
-- (e.g., a builtin or a struct whose fields are all resolved).
|
||||
-- Confident: this entry was already resolved by the propagation pass (e.g., a builtin or a struct whose fields are all resolved).
|
||||
if entry.byte_size ~= nil then return entry.byte_size end
|
||||
|
||||
-- Chain-following: typedef / TSet_ aliases follow underlying_type.
|
||||
if entry.underlying_type then
|
||||
return resolve_typedef_byte_size(
|
||||
entry.underlying_type, type_name_registry, visited, depth + 1)
|
||||
return resolve_typedef_byte_size(entry.underlying_type, type_name_registry, visited, depth + 1)
|
||||
end
|
||||
|
||||
-- Struct_ entries with unresolved byte_size can still resolve when their fields are all resolved.
|
||||
@@ -414,7 +418,7 @@ end
|
||||
-- struct byte_size derives from confident fields; void is invalid and skipped; pointers collapse to 4 bytes at parse time.
|
||||
-- Mutates `out.type_name_registry[name].byte_size` AND each struct's fields' `offset` + `byte_size` in place.
|
||||
local function propagate_type_sizes(out)
|
||||
local reg = out.type_name_registry
|
||||
local reg = out.type_name_registry
|
||||
if not reg then return end
|
||||
|
||||
-- Seed builtin primitives (U1/U2/U4/S1/S2/S4 + __UINT*_TYPE__ family).
|
||||
@@ -527,25 +531,22 @@ end
|
||||
-- - a plain register ident: `R_FaceCursor` → reg_name only
|
||||
-- - register + atom_type sub-call: `R_FaceCursor atom_type(V4_S2*)` → reg_name + override entry
|
||||
-- Returns (reg_name, override_entry_or_nil, malformed_flag).
|
||||
-- On malformed `atom_type(...)` (missing close paren, trailing tokens after the close paren, empty type chain, trailing junk inside the
|
||||
-- parens like `V4_S2*()`), the function still returns the leading reg_name but sets `malformed_flag = true` and `override_entry = nil`
|
||||
-- so the caller can silently drop the override while keeping the register in the reads/writes list
|
||||
-- (per the parser-safety contract — "malformed atom_type MUST NOT create any override").
|
||||
-- On malformed `atom_type(...)` (missing close paren, trailing tokens after the close paren, empty type chain, trailing junk inside the parens like `V4_S2*()`),
|
||||
-- the function still returns the leading reg_name but sets `malformed_flag = true` and `override_entry = nil`
|
||||
-- so the caller can silently drop the override while keeping the register in the reads/writes list.
|
||||
local function parse_atom_info_reg_entry(entry)
|
||||
local pos = 1
|
||||
pos = duffle.skip_ws_and_cmt(entry, pos)
|
||||
if pos > #entry then return nil, nil, false end
|
||||
local reg_name, reg_end = duffle.read_ident(entry, pos)
|
||||
local reg_name, reg_end = duffle.read_ident(entry, pos)
|
||||
if not reg_name then return nil, nil, false end
|
||||
pos = duffle.skip_ws_and_cmt(entry, reg_end)
|
||||
-- Plain register, no adjacent atom_type — done.
|
||||
if pos > #entry then return reg_name, nil, false end
|
||||
|
||||
-- Adjacent ident must be a bare `atom_type` (word-bounded both sides).
|
||||
local next_ident, next_end = duffle.read_ident(entry, pos)
|
||||
if not next_ident or next_ident ~= "atom_type" then
|
||||
return reg_name, nil, false
|
||||
end
|
||||
local next_ident, next_end = duffle.read_ident(entry, pos)
|
||||
if not next_ident or next_ident ~= "atom_type" then return reg_name, nil, false end
|
||||
-- Left word-boundary: `_atom_type` should NOT match `atom_type`.
|
||||
if pos > 1 then
|
||||
local prev = entry:byte(pos - 1)
|
||||
@@ -559,27 +560,21 @@ local function parse_atom_info_reg_entry(entry)
|
||||
|
||||
-- Expect `(` immediately after `atom_type` (whitespace tolerated).
|
||||
pos = duffle.skip_ws_and_cmt(entry, next_end)
|
||||
if pos > #entry or entry:sub(pos, pos) ~= "(" then
|
||||
return reg_name, nil, true
|
||||
end
|
||||
if pos > #entry or entry:sub(pos, pos) ~= "(" then return reg_name, nil, true end
|
||||
local sub_inner, sub_after = duffle.read_parens(entry, pos)
|
||||
|
||||
-- Reject any trailing tokens (including `;` / `,`) after the close paren.
|
||||
-- The outer caller already split on top-level commas, so the only legal terminator here is end-of-entry.
|
||||
local after_close = duffle.skip_ws_and_cmt(entry, sub_after)
|
||||
if after_close <= #entry then
|
||||
return reg_name, nil, true
|
||||
end
|
||||
if after_close <= #entry then return reg_name, nil, true end
|
||||
|
||||
-- Parse the type chain inside the parens; require full consumption.
|
||||
-- parse_type_chain returns (ident, depth, end_pos);
|
||||
-- we reject any non-whitespace residue past end_pos (catches `V4_S2*()` etc.).
|
||||
local type_name, depth, after_chain = parse_type_chain(sub_inner, 1)
|
||||
-- We reject any non-whitespace residue past end_pos (catches `V4_S2*()` etc.).
|
||||
local type_name, depth, after_chain = parse_type_chain(sub_inner, 1)
|
||||
if not type_name then return reg_name, nil, true end
|
||||
local end_check = duffle.skip_ws_and_cmt(sub_inner, after_chain)
|
||||
if end_check <= #sub_inner then
|
||||
return reg_name, nil, true
|
||||
end
|
||||
if end_check <= #sub_inner then return reg_name, nil, true end
|
||||
|
||||
return reg_name, { type_name = type_name, pointer_depth = depth }, false
|
||||
end
|
||||
@@ -601,8 +596,8 @@ local function scan_atom_info_subcalls(info_inner, info_line)
|
||||
-- Per-subcall handler table. Each handler takes (sub_inner, info_line) and mutates the outer locals above.
|
||||
-- atom_reads/atom_writes share a handler (same shape; just different output target).
|
||||
local function rw_handler(sub_inner, info_line, kind)
|
||||
-- The reads/writes arrays contain ONLY register idents;
|
||||
-- the `atom_type(...)` sub-entry (when present and well-formed) is recorded as a per-atom reg_type_override.
|
||||
-- reads/writes arrays contain ONLY register idents;
|
||||
-- `atom_type(...)` sub-entry (when present and well-formed) is recorded as a per-atom reg_type_override.
|
||||
local entries = duffle.split_top_level_commas(sub_inner)
|
||||
local regs = {}
|
||||
for _, entry in ipairs(entries) do
|
||||
@@ -625,7 +620,7 @@ local function scan_atom_info_subcalls(info_inner, info_line)
|
||||
end
|
||||
local function ident_handler(sub_inner, info_line, out_name)
|
||||
-- Validates the arg is a single C identifier (no commas / parens / whitespace).
|
||||
-- Silently reject malformed args; the DWARF chain treats the field as un-set.
|
||||
-- Silently reject malformed args; DWARF chain treats the field as un-set.
|
||||
local name = duffle.read_ident(sub_inner, 1)
|
||||
if name then
|
||||
local after_id = duffle.skip_ws_and_cmt(sub_inner, 1 + #name)
|
||||
@@ -706,7 +701,7 @@ local function scan_skip_qualifiers(source, pos)
|
||||
end
|
||||
|
||||
-- ════════════════════════════════════════════════════════════════════════════
|
||||
-- Track A helpers: enum / atom_reg / R_*_Code parsing
|
||||
-- enum / atom_reg / R_*_Code parsing
|
||||
-- ════════════════════════════════════════════════════════════════════════════
|
||||
--
|
||||
-- The parser walks `enum { <body> }` declarations and emits one AliasEntry per R_* entry whose value is followed by a bare `atom_reg` token.
|
||||
@@ -718,6 +713,7 @@ local BYTE_HASH = 0x23 -- '#'
|
||||
local BYTE_NEWLINE = 0x0A -- '\n'
|
||||
local BYTE_DASH = 0x2D -- '-'
|
||||
local BYTE_COMMA = 0x2C -- ','
|
||||
local BYTE_SEMI = 0x3B -- ';'
|
||||
local BYTE_EQUAL = 0x3D -- '='
|
||||
local BYTE_R = 0x52 -- 'R'
|
||||
local BYTE_UNDERSCORE = 0x5F -- '_'
|
||||
@@ -754,13 +750,16 @@ local function hex_digit_value(b)
|
||||
return nil
|
||||
end
|
||||
|
||||
-- Parse a decimal/negative-decimal/hex integer literal starting at byte position `start`.
|
||||
-- Returns (value, end_pos) on success, or (nil, start) on failure / no match.
|
||||
-- Accepts: 12, -1, 0, 0x10, 0X1F, -0x10.
|
||||
-- @param text string
|
||||
-- @param start integer
|
||||
-- @return integer|nil, integer
|
||||
local function parse_enum_int_literal(text, start)
|
||||
--- Parse a decimal/negative-decimal/hex integer literal starting at byte position `start`.
|
||||
--- Returns (value, end_pos) on success, or (nil, start) on failure / no match.
|
||||
--- Accepts: 12, -1, 0, 0x10, 0X1F, -0x10.
|
||||
--- @param text string
|
||||
--- @param start integer
|
||||
--- @return integer|nil, integer
|
||||
--- Implementation note: this is a plain assignment (not `local function`)
|
||||
--- so the forward declaration at the top of the file is the same upvalue the earlier `parse_enum_body_fields` closure captures.
|
||||
--- Lua 5.3 / LuaJIT upvalue semantics resolve the assignment at call time.
|
||||
parse_enum_int_literal = function(text, start)
|
||||
local pos = start
|
||||
local len = #text
|
||||
if pos > len then return nil, start end
|
||||
@@ -804,7 +803,7 @@ local function parse_enum_int_literal(text, start)
|
||||
end
|
||||
|
||||
-- Returns true iff `name` matches the `R_*_Code` ident pattern (R, _, <anything>, _Code).
|
||||
-- Uses byte checks for the fixed bytes per the Track A spec.
|
||||
-- Uses byte checks for the fixed bytes.
|
||||
local function is_r_code_macro(name)
|
||||
if not name or #name < 6 then return false end
|
||||
if name:byte(1) ~= BYTE_R then return false end
|
||||
@@ -847,10 +846,10 @@ local function resolve_code_macro_value(source, pos, code_macros, code_macro_bod
|
||||
|
||||
-- Try integer literal first (decimal / negative / hex).
|
||||
local int_val, int_end = parse_enum_int_literal(source, pos)
|
||||
if int_val ~= nil then return int_val end
|
||||
if int_val ~= nil then return int_val end
|
||||
|
||||
-- Try symbol reference (must be R_*_Code per the spec).
|
||||
local sym = duffle.read_ident(source, pos)
|
||||
local sym = duffle.read_ident(source, pos)
|
||||
if not sym then return nil end
|
||||
if not is_r_code_macro(sym) then return nil end
|
||||
|
||||
@@ -864,7 +863,7 @@ local function resolve_code_macro_value(source, pos, code_macros, code_macro_bod
|
||||
-- Cross-source fallback: walk the body of the missing symbol if any other source collected its RHS during pass 1a.
|
||||
-- The body is parsed as a fresh RHS (it's the raw post-`=` text of the original `#define` line), so the chain continues transparently.
|
||||
local body = code_macro_bodies and code_macro_bodies[sym]
|
||||
if body then
|
||||
if body then
|
||||
return resolve_code_macro_value(body, 1, code_macros, code_macro_bodies, visited, depth + 1)
|
||||
end
|
||||
|
||||
@@ -874,14 +873,14 @@ local function resolve_code_macro_value(source, pos, code_macros, code_macro_bod
|
||||
return nil
|
||||
end
|
||||
|
||||
-- Intercept a `#define R_*_Code <RHS>` preprocessor line.
|
||||
-- Always saves the raw RHS text into `code_macro_bodies` (for cross-source fallback during chain resolution),
|
||||
-- then (if resolvable) stores the resolved integer code into `code_macros` keyed by the macro name.
|
||||
-- `directive_start` points at the `#` byte. The function is silent on non-matching directives, the caller skips the line in any case.
|
||||
-- @param source string
|
||||
-- @param directive_start integer -- byte position of `#`
|
||||
-- @param code_macros table -- out._code_macros / ctx.shared._code_macros
|
||||
-- @param code_macro_bodies table -- out._code_macro_bodies / ctx.shared._code_macro_bodies
|
||||
--- Intercept a `#define R_*_Code <RHS>` preprocessor line.
|
||||
--- Always saves the raw RHS text into `code_macro_bodies` (for cross-source fallback during chain resolution),
|
||||
--- then (if resolvable) stores the resolved integer code into `code_macros` keyed by the macro name.
|
||||
--- `directive_start` points at the `#` byte. The function is silent on non-matching directives, the caller skips the line in any case.
|
||||
--- @param source string
|
||||
--- @param directive_start integer -- byte position of `#`
|
||||
--- @param code_macros table -- out._code_macros / ctx.shared._code_macros
|
||||
--- @param code_macro_bodies table -- out._code_macro_bodies / ctx.shared._code_macro_bodies
|
||||
local function try_extract_code_macro(source, directive_start, code_macros, code_macro_bodies)
|
||||
local rest = duffle.skip_ws_and_cmt(source, directive_start + 1)
|
||||
local kw, kw_end = duffle.read_ident(source, rest)
|
||||
@@ -907,13 +906,13 @@ local function try_extract_code_macro(source, directive_start, code_macros, code
|
||||
if value ~= nil then code_macros[macro_name] = value end
|
||||
end
|
||||
|
||||
-- Quick pre-pass: walk the source looking ONLY for `#define R_*_Code` lines.
|
||||
-- Populates `code_macros` with resolved integer codes AND `code_macro_bodies` with raw RHS text
|
||||
-- (used by the chain walker as cross-source fallback during pass 1b in `M.run`); ignores everything else.
|
||||
-- Used by `M.run` pass 1a to build the cross-source `_code_macros` + `_code_macro_bodies` registries before pass 1b resolves chains.
|
||||
-- @param source string
|
||||
-- @param code_macros table
|
||||
-- @param code_macro_bodies table
|
||||
--- Quick pre-pass: walk the source looking ONLY for `#define R_*_Code` lines.
|
||||
--- Populates `code_macros` with resolved integer codes AND `code_macro_bodies` with raw RHS text
|
||||
--- (used by the chain walker as cross-source fallback during pass 1b in `M.run`); ignores everything else.
|
||||
--- Used by `M.run` pass 1a to build the cross-source `_code_macros` + `_code_macro_bodies` registries before pass 1b resolves chains.
|
||||
--- @param source string
|
||||
--- @param code_macros table
|
||||
--- @param code_macro_bodies table
|
||||
local function scan_source_pre_pass(source, code_macros, code_macro_bodies)
|
||||
local pos = 1
|
||||
local src_len = #source
|
||||
@@ -1012,7 +1011,7 @@ end
|
||||
--
|
||||
-- All parsers read source-as-written via the duffle primitives (skip_ws_and_cmt / read_parens / read_braces / read_balanced).
|
||||
-- No regex per the no_regex constraint; no hand-rolled depth tracking
|
||||
-- (the MipsAtomComp_Proc_ brace matcher now uses duffle.read_braces instead of bespoke byte-dispatch).
|
||||
-- The MipsAtomComp_Proc_ brace matcher uses `duffle.read_braces`.
|
||||
--
|
||||
-- Adding a new construct = 1 row in DECL_PARSERS + 1 parser function. The scan_source() loop never needs editing.
|
||||
|
||||
@@ -1186,7 +1185,7 @@ local function parse_mips_atom_comp_proc(source, pos, ident_end, line_of, out)
|
||||
if not last_brace_pos then return after_paren end
|
||||
|
||||
-- Use duffle.read_braces to find the matching close brace.
|
||||
-- Replaces the pre-refactor hand-rolled depth tracker (~25 LOC of `if c == 123 then depth = depth + 1 ...`).
|
||||
-- Uses `read_balanced` for delimiter-depth tracking.
|
||||
-- If close_pos is past the end of inner, the brace didn't match (malformed input); skip.
|
||||
local body, close_pos = duffle.read_braces(inner, last_brace_pos)
|
||||
if close_pos > #inner + 1 then return after_paren end
|
||||
@@ -1340,10 +1339,9 @@ local function parse_typedef_binds(source, pos, ident_end, line_of, out)
|
||||
register_struct_type(body, name, pos, line_of, out)
|
||||
associate_skip_over_marker(out, name, name, "unrelated", line_of(pos), pos)
|
||||
return after_brace
|
||||
end
|
||||
|
||||
-- ── Shape 2: `typedef Enum_(<underlying>, <name>) { <body> } <alias>;`
|
||||
if id2 == "Enum_" then
|
||||
elseif id2 == "Enum_" then
|
||||
local inner, after_paren, open_paren = read_parens_after(source, id2_end, id2_end)
|
||||
if not inner then return id2_end end
|
||||
-- Split `inner` on the first top-level comma into (<underlying>, <name>).
|
||||
@@ -1359,35 +1357,104 @@ local function parse_typedef_binds(source, pos, ident_end, line_of, out)
|
||||
return after_brace
|
||||
end
|
||||
|
||||
-- ── Shapes 3 + 4: `typedef <type> <alias>;` or
|
||||
-- `typedef <type> TSet_(<name>);`
|
||||
-- Read the <type> ident we already have (id2) and the trailing alias.
|
||||
local after_id2 = duffle.skip_ws_and_cmt(source, id2_end)
|
||||
local id3, id3_end = duffle.read_ident(source, after_id2)
|
||||
if not id3 then return ident_end end
|
||||
-- ── Shapes 3 + 4: `typedef <span> <alias>;` or
|
||||
-- `typedef <span> TSet_(<name>);`
|
||||
--
|
||||
-- The <span> between `typedef` and the alias ident may be MULTI-token (e.g. `unsigned char`, `__UINT8_TYPE__`, `const U4`).
|
||||
-- The alias is the LAST identifier before `;` (Shape 3), OR the argument of `TSet_(...)` when that wrapper is present (Shape 4).
|
||||
--
|
||||
-- Two required exact outcomes:
|
||||
-- typedef unsigned char UTF8; -> name=UTF8, underlying="unsigned char"
|
||||
-- typedef __UINT8_TYPE__ TSet_(U1); -> name=U1, underlying="__UINT8_TYPE__"
|
||||
--
|
||||
-- Algorithm:
|
||||
-- 1. Find the terminating `;` (BYTE_SEMI). If absent, abort cleanly.
|
||||
-- 2. If id2 itself is `TSet_`, capture the parenthesized argument and use it as the alias (the preceding underlying span is empty).
|
||||
-- 3. Otherwise walk idents forward from id2_end to semi_pos:
|
||||
-- - If any ident is `TSet_`, capture its argument as the alias and mark the underlying span as everything from id2 to before TSet_.
|
||||
-- - Otherwise remember the last ident (and its source position) as the alias; the underlying span is everything from id2 to before that ident.
|
||||
-- 4. Trim the underlying span and call register_typedef_alias.
|
||||
--
|
||||
-- Struct_/Enum_ declarations carry their own dedicated parser paths above; this branch only handles non-Struct_/Enum_ typedefs.
|
||||
|
||||
-- Shape 4: `typedef <type> TSet_(<name>);`
|
||||
if id3 == "TSet_" then
|
||||
local tset_inner, tset_after = read_parens_after(source, id3_end, id3_end)
|
||||
if not tset_inner then return id3_end end
|
||||
local tset_name = duffle.trim(tset_inner)
|
||||
register_typedef_alias(id2, tset_name, pos, line_of, out)
|
||||
-- Find the terminating `;` (BYTE_SEMI). If absent, abort cleanly.
|
||||
local semi_pos = duffle.find_byte(source, BYTE_SEMI, id2_end)
|
||||
if not semi_pos then return id2_end end
|
||||
|
||||
-- Shape 4 (TSet_ at id2 position): no preceding underlying span.
|
||||
if id2 == "TSet_" then
|
||||
local inner, after_paren = read_parens_after(source, id2_end, id2_end)
|
||||
if not inner then return id2_end end
|
||||
local tset_name = duffle.trim(inner)
|
||||
-- Empty underlying span is acceptable; the TSet_ wrapper itself
|
||||
-- encodes the alias identity (per the duffle TSet_ convention).
|
||||
register_typedef_alias("", tset_name, pos, line_of, out)
|
||||
associate_skip_over_marker(out, tset_name, tset_name, "unrelated", line_of(pos), pos)
|
||||
return tset_after
|
||||
return after_paren
|
||||
end
|
||||
|
||||
-- Shape 3: `typedef <type> <alias>;`
|
||||
register_typedef_alias(id2, id3, pos, line_of, out)
|
||||
associate_skip_over_marker(out, id3, id3, "unrelated", line_of(pos), pos)
|
||||
return id3_end
|
||||
-- Walk idents forward to find the alias ident (last ident before `;`), or the TSet_(<arg>) form (capture the arg, use it as the alias).
|
||||
local last_ident = nil
|
||||
local last_ident_pos = nil
|
||||
local last_ident_end = nil
|
||||
local tset_arg = nil
|
||||
local tset_arg_end = nil
|
||||
local tset_pos = nil
|
||||
|
||||
local scan = id2_end
|
||||
while scan < semi_pos do
|
||||
scan = duffle.skip_ws_and_cmt(source, scan)
|
||||
if scan >= semi_pos then break end
|
||||
local id, id_end = duffle.read_ident(source, scan)
|
||||
if not id then
|
||||
scan = scan + 1
|
||||
elseif id == "TSet_" then
|
||||
-- Shape 4 (TSet_ at non-id2 position): grab the parenthesized argument.
|
||||
local inner, after_paren = read_parens_after(source, id_end, id_end)
|
||||
if inner then
|
||||
tset_arg = duffle.trim(inner)
|
||||
tset_arg_end = after_paren
|
||||
tset_pos = scan
|
||||
scan = after_paren
|
||||
else
|
||||
scan = id_end
|
||||
end
|
||||
else
|
||||
last_ident = id
|
||||
last_ident_pos = scan
|
||||
last_ident_end = id_end
|
||||
scan = id_end
|
||||
end
|
||||
end
|
||||
|
||||
if tset_arg then
|
||||
-- Shape 4: alias is the TSet_ argument; the underlying span is the trimmed text from the start of id2 up to (but not including) the TSet_ ident.
|
||||
local underlying_span = source:sub(after_typedef, tset_pos - 1)
|
||||
local underlying = duffle.trim(underlying_span)
|
||||
register_typedef_alias(underlying, tset_arg, pos, line_of, out)
|
||||
associate_skip_over_marker(out, tset_arg, tset_arg, "unrelated", line_of(pos), pos)
|
||||
return tset_arg_end or (semi_pos + 1)
|
||||
end
|
||||
|
||||
if last_ident then
|
||||
-- Shape 3: alias is the last ident before `;`; the underlying span is the trimmed text from the start of id2 up to (but not including) the alias ident.
|
||||
local underlying_span = source:sub(after_typedef, last_ident_pos - 1)
|
||||
local underlying = duffle.trim(underlying_span)
|
||||
register_typedef_alias(underlying, last_ident, pos, line_of, out)
|
||||
associate_skip_over_marker(out, last_ident, last_ident, "unrelated", line_of(pos), pos)
|
||||
return last_ident_end
|
||||
end
|
||||
|
||||
-- Malformed: id2 with no following ident before `;`. Skip past the terminating semicolon and let the main loop continue.
|
||||
return semi_pos + 1
|
||||
end
|
||||
|
||||
--- Parse: `_Pragma("mac_X tape_atom words=N")` (operator form).
|
||||
--- @param source string
|
||||
--- @param pos integer
|
||||
--- @param source string
|
||||
--- @param pos integer
|
||||
--- @param ident_end integer
|
||||
--- @param line_of fun(pos: integer): integer
|
||||
--- @param out SourceScan
|
||||
--- @param line_of fun(pos: integer): integer
|
||||
--- @param out SourceScan
|
||||
--- @return integer
|
||||
local function parse_pragma_macro(source, pos, ident_end, line_of, out)
|
||||
local str, str_end = read_parens_after(source, ident_end)
|
||||
@@ -1413,30 +1480,17 @@ local function parse_pragma_macro(source, pos, ident_end, line_of, out)
|
||||
return str_end
|
||||
end
|
||||
|
||||
--- Parse: `pragma` ident (no-op — directive form `#pragma` is handled by `skip_preprocessor_line` upstream).
|
||||
--- If we reach this parser it means the directive skip didn't fire, which can happen for non-#-prefixed pragma.
|
||||
--- Just advance past the ident.
|
||||
--- @param source string
|
||||
--- @param pos integer
|
||||
--- @param ident_end integer
|
||||
--- @param line_of fun(pos: integer): integer
|
||||
--- @param out SourceScan
|
||||
--- @return integer
|
||||
local function parse_pragma_dummy(source, pos, ident_end, line_of, out)
|
||||
return ident_end
|
||||
end
|
||||
|
||||
-- Parse the value side of an enum entry.
|
||||
-- Accepts integer literals (decimal/negative/hex), `R_*_Code` symbol references resolved via `out._code_macros`,
|
||||
-- AND bare `R_*` symbols that map to an `R_*_Code` variant in the registry.
|
||||
-- The bare-`R_*` fallback is needed for the lottes_tape.h wave-context aliases whose enum RHS is the bare register ident (e.g. `R_TapePtr = R_T8 atom_reg`);
|
||||
-- the `R_*_Code` form (e.g. `R_T8_Code`) holds the actual GPR code in mips.h and is now resolvable cross-source via the chain walker.
|
||||
-- The `R_*_Code` form holds the GPR code and resolves across sources through the chain walker.
|
||||
-- Returns (value, end_pos) on success or (nil, pos) if unresolvable.
|
||||
local function parse_enum_value(body, pos, out)
|
||||
local int_val, int_end = parse_enum_int_literal(body, pos)
|
||||
if int_val ~= nil then return int_val, int_end end
|
||||
if int_val ~= nil then return int_val, int_end end
|
||||
|
||||
local sym = duffle.read_ident(body, pos)
|
||||
local sym = duffle.read_ident(body, pos)
|
||||
if not sym then return nil, pos end
|
||||
|
||||
-- Bare `R_*` (no `_Code` suffix) → translate to `R_*_Code` and look that up. Non-`R_*` symbols
|
||||
@@ -1580,8 +1634,7 @@ local DECL_PARSERS = {
|
||||
MipsCode = parse_mips_code,
|
||||
typedef = parse_typedef_binds,
|
||||
_Pragma = parse_pragma_macro,
|
||||
pragma = parse_pragma_dummy,
|
||||
-- Track A: `enum [<tag>] { <body> }` populates `out.register_alias_registry`.
|
||||
-- `enum [<tag>] { <body> }` populates `out.register_alias_registry`.
|
||||
enum = parse_enum,
|
||||
}
|
||||
|
||||
@@ -1612,20 +1665,20 @@ local function scan_source(source, source_file, code_macros, code_macro_bodies)
|
||||
types = {},
|
||||
atom_views = {},
|
||||
line_of = line_of,
|
||||
-- Track A: source-derived register-alias registry (atom_reg opt-in entries).
|
||||
-- Source-derived register-alias registry (atom_reg opt-in entries).
|
||||
-- Keys are full R_* idents (never stripped); see parse_enum / parse_enum_body.
|
||||
register_alias_registry = {},
|
||||
-- Track A: source-derived type-name registry.
|
||||
-- Source-derived type-name registry.
|
||||
-- Populated from `typedef Struct_(...)`, `typedef Enum_(...)`, `typedef <type> <alias>`, and `typedef <type> TSet_(<name>)` declarations.
|
||||
-- The propagation pass at the end of `scan_source()` resolves byte_size via the builtin map,
|
||||
-- typedef chain walking (cycle-guarded, depth <= 8), and struct field sums.
|
||||
-- See `propagate_type_sizes()` below.
|
||||
type_name_registry = {},
|
||||
-- Track A: shared `R_*_Code -> integer code` registry
|
||||
-- Shared `R_*_Code -> integer code` registry
|
||||
-- (passed in from M.run pass 1; same reference so preprocessor intercept writes are visible to the enum-value resolver).
|
||||
-- Stripped from `src.scan` before return.
|
||||
_code_macros = code_macros or {},
|
||||
-- Track A: shared raw RHS body table (passed in from M.run pass 1a;
|
||||
-- Shared raw RHS body table (passed in from M.run pass 1a;
|
||||
-- same reference so preprocessor intercept writes are visible to the cross-source chain walker in resolve_code_macro_value).
|
||||
-- Stripped from `src.scan` before return.
|
||||
_code_macro_bodies = code_macro_bodies or {},
|
||||
@@ -1642,8 +1695,7 @@ local function scan_source(source, source_file, code_macros, code_macro_bodies)
|
||||
-- _Pragma is an operator (not a directive) — it doesn't start with #.
|
||||
local pp_pos = duffle.skip_preprocessor_line(source, pos)
|
||||
if pp_pos then
|
||||
-- Track A intercept: resolve `#define R_*_Code <int-or-symbol>`
|
||||
-- into the shared `_code_macros` registry before skipping the line.
|
||||
-- Resolve `#define R_*_Code <int-or-symbol>` into the shared `_code_macros` registry before skipping the line.
|
||||
try_extract_code_macro(source, pos, out._code_macros, out._code_macro_bodies)
|
||||
pos = pp_pos
|
||||
else
|
||||
@@ -1656,8 +1708,7 @@ local function scan_source(source, source_file, code_macros, code_macro_bodies)
|
||||
if parser then
|
||||
pos = parser(source, pos, ident_end, line_of, out)
|
||||
else
|
||||
-- A component-procedure declaration has an FI_ signature before MipsAtomComp_Proc_;
|
||||
-- keep the marker pending across that prelude.
|
||||
-- A component-procedure declaration has an FI_ signature before MipsAtomComp_Proc_; keep the marker pending across that prelude.
|
||||
-- Any other identifier begins an unrelated declaration/construct and consumes the marker so it cannot drift to a later atom.
|
||||
local markers = out.skip_over.markers
|
||||
local marker = markers[#markers]
|
||||
@@ -1693,6 +1744,252 @@ local function scan_source(source, source_file, code_macros, code_macro_bodies)
|
||||
return out
|
||||
end
|
||||
|
||||
-- ════════════════════════════════════════════════════════════════════════════
|
||||
-- Corpus merge — first-wins lookup identity + typed collisions
|
||||
-- ════════════════════════════════════════════════════════════════════════════
|
||||
-- These helpers run ONCE per `M.run` invocation, after every per-source scan has attached `src.scan`.
|
||||
-- They merge per-source scans into the canonical `ctx.shared.corpus.*` registries.
|
||||
-- The corpus is the source of truth; `src.scan` keeps the source-local projection for the duration of the run but the cross-source visibility lives on `corpus`.
|
||||
|
||||
-- Build a deterministic site record (path + line) from a per-source entry.
|
||||
-- Falls back to the placeholder when an entry lacks a recorded source file or line.
|
||||
local function build_site(path, line)
|
||||
return { path = path or "?", line = line or 0 }
|
||||
end
|
||||
|
||||
-- Compute a deterministic shape signature for an AliasEntry (register_alias_registry).
|
||||
-- Two alias declarations are "identical" iff they resolve to the same shape:
|
||||
-- code (integer) + default_type + default_depth + pointer_depth + has_atom_reg.
|
||||
local function alias_shape(entry)
|
||||
if type(entry) ~= "table" then return "" end
|
||||
return string.format("code=%s;default=%s/%s;depth=%s;atom_reg=%s",
|
||||
tostring(entry.code),
|
||||
tostring(entry.default_type or ""),
|
||||
tostring(entry.default_depth or 0),
|
||||
tostring(entry.pointer_depth or 0),
|
||||
tostring(entry.has_atom_reg and 1 or 0))
|
||||
end
|
||||
|
||||
-- Compute a deterministic shape signature for a type-name registry entry.
|
||||
-- struct: serialized fields (name:type:depth, in declaration order)
|
||||
-- enum: serialized fields (name=value, declaration order)
|
||||
-- typedef: The underlying_type string
|
||||
local function type_shape(entry)
|
||||
if type(entry) ~= "table" then return "" end
|
||||
if entry.kind == "struct" then
|
||||
local fields = entry.fields or {}
|
||||
local parts = {}
|
||||
for _, f in ipairs(fields) do
|
||||
parts[#parts + 1] = string.format("%s:%s*%s",
|
||||
tostring(f.name), tostring(f.type_name), tostring(f.pointer_depth or 0))
|
||||
end
|
||||
return "struct[" .. table.concat(parts, ",") .. "]"
|
||||
elseif entry.kind == "enum" then
|
||||
local fields = entry.fields or {}
|
||||
local parts = {}
|
||||
for _, f in ipairs(fields) do
|
||||
parts[#parts + 1] = string.format("%s=%s", tostring(f.name), tostring(f.value))
|
||||
end
|
||||
return "enum[" .. table.concat(parts, ",") .. "]"
|
||||
elseif entry.kind == "typedef" then
|
||||
return "typedef[" .. tostring(entry.underlying_type or "") .. "]"
|
||||
end
|
||||
return "?"
|
||||
end
|
||||
|
||||
-- Compute a deterministic shape signature for a Binds_* entry (Struct_ projection).
|
||||
-- Binds_* entries come from scan.binds[] (per-source array) with `{line, name, fields, body, bytes}`;
|
||||
-- They share the same `fields` layout as the matching struct entry in `type_name_registry`, so the shape is the struct-field serialization.
|
||||
-- The `kind` field is absent here, so the struct branch of `type_shape` would misfire; we serialize the fields directly.
|
||||
local function bind_shape(entry)
|
||||
if type(entry) ~= "table" then return "" end
|
||||
local fields = entry.fields or {}
|
||||
local parts = {}
|
||||
for _, f in ipairs(fields) do
|
||||
parts[#parts + 1] = string.format("%s:%s*%s",
|
||||
tostring(f.name), tostring(f.type_name), tostring(f.pointer_depth or 0))
|
||||
end
|
||||
return "struct[" .. table.concat(parts, ",") .. "]"
|
||||
end
|
||||
|
||||
-- Compute a deterministic shape signature for an AtomEntry.
|
||||
-- The "kind + body" pair uniquely identifies the same declaration when re-encountered.
|
||||
-- `body` is the brace-delimited body text.
|
||||
local function atom_shape(entry)
|
||||
if type(entry) ~= "table" then return "" end
|
||||
return string.format("kind=%s;body=%s",
|
||||
tostring(entry.kind or ""), tostring(entry.body or ""))
|
||||
end
|
||||
|
||||
-- Compute a deterministic shape signature for an AtomViewEntry.
|
||||
-- Two views are identical iff they bind the same Binds_X with the same reg overrides.
|
||||
local function view_shape(entry)
|
||||
if type(entry) ~= "table" then return "" end
|
||||
local overrides = entry.reg_type_overrides or {}
|
||||
local keys = {}
|
||||
for k in pairs(overrides) do keys[#keys + 1] = k end
|
||||
table.sort(keys, function(a, b) return tostring(a) < tostring(b) end)
|
||||
local parts = {}
|
||||
for _, k in ipairs(keys) do
|
||||
local ov = overrides[k]
|
||||
parts[#parts + 1] = string.format("%s=%s*%s", tostring(k),
|
||||
tostring(ov.type_name), tostring(ov.pointer_depth or 0))
|
||||
end
|
||||
return string.format("binds=%s;overrides=[%s]",
|
||||
tostring(entry.binds_name or ""), table.concat(parts, ","))
|
||||
end
|
||||
|
||||
-- Compute a deterministic shape signature for an AtomCtxEntry.
|
||||
local function ctx_shape(entry)
|
||||
if type(entry) ~= "table" then return "" end
|
||||
return "rbind=" .. tostring(entry.rbind_atom or "")
|
||||
end
|
||||
|
||||
-- Compute a deterministic shape signature for an AtomPhaseGroup.
|
||||
-- The atoms list is compared as a SET (sorted) so that two declarations of `atom_phase(setup)` with atoms = [A] vs atoms = [B]
|
||||
-- are DIFFERENT shapes (first-wins + collision), while atoms = [A,B] vs atoms = [B,A] are considered identical (and coalesce).
|
||||
local function phase_shape(entry)
|
||||
if type(entry) ~= "table" then return "" end
|
||||
local atoms = entry.atoms or {}
|
||||
local sorted = {}
|
||||
for _, a in ipairs(atoms) do sorted[#sorted + 1] = a end
|
||||
table.sort(sorted)
|
||||
return "atoms=[" .. table.concat(sorted, ",") .. "]"
|
||||
end
|
||||
|
||||
-- Merge a new declaration site into a registry following the first-wins discipline.
|
||||
-- * first declaration: Entry becomes the canonical corpus entry (entry.sites initialized).
|
||||
-- * identical subsequent: Append the new site to entry.sites (no collision).
|
||||
-- * conflicting shape: Keep first entry, append ONE typed collision record with shape diff.
|
||||
local function merge_named_with_sites(registry, name, new_entry, site, collisions, kind, shape_fn)
|
||||
if registry[name] == nil then
|
||||
registry[name] = new_entry
|
||||
registry[name].sites = { site }
|
||||
return
|
||||
end
|
||||
local existing = registry[name]
|
||||
local new_shape = shape_fn(new_entry)
|
||||
local old_shape = shape_fn(existing)
|
||||
if new_shape == old_shape and new_shape ~= "" then
|
||||
-- Identical shape: coalesce by appending the site.
|
||||
existing.sites = existing.sites or { build_site(existing.source_file, existing.source_line) }
|
||||
existing.sites[#existing.sites + 1] = site
|
||||
return
|
||||
end
|
||||
-- Conflicting shape: first-wins; record exactly one typed collision.
|
||||
collisions[#collisions + 1] = {
|
||||
kind = kind,
|
||||
name = name,
|
||||
first_site = existing.sites and existing.sites[1]
|
||||
or build_site(existing.source_file, existing.source_line),
|
||||
conflicting_site = site,
|
||||
first_shape = old_shape,
|
||||
conflicting_shape = new_shape,
|
||||
}
|
||||
end
|
||||
|
||||
-- Merge per-source scans into the canonical corpus registries.
|
||||
-- Iterates `corpus.source_order` (not `ctx.sources`) — the corpus is the source of truth.
|
||||
-- Each source owns only its `src.scan`; the corpus owns the cross-source lookup tables.
|
||||
local function merge_corpus_registries(corpus)
|
||||
-- Ensure every expected corpus table exists (the fixture_ctx seeds most of these,
|
||||
-- but a barebones corpus from build_ctx should also be safe).
|
||||
corpus.register_alias_registry = corpus.register_alias_registry or {}
|
||||
corpus.type_name_registry = corpus.type_name_registry or {}
|
||||
corpus.binds_by_name = corpus.binds_by_name or {}
|
||||
corpus.atoms_by_name = corpus.atoms_by_name or {}
|
||||
corpus.atom_views = corpus.atom_views or {}
|
||||
corpus.atom_ctxs = corpus.atom_ctxs or {}
|
||||
corpus.atom_phases = corpus.atom_phases or {}
|
||||
corpus.atom_infos = corpus.atom_infos or {}
|
||||
corpus.collisions = corpus.collisions or {}
|
||||
|
||||
-- Replace the existing corpus collections with empty tables so a re-run on the same corpus produces identical state (deterministic merge).
|
||||
-- This is safe because M.run is the only writer to these tables within a single orchestrator invocation.
|
||||
for _, key in ipairs({
|
||||
"register_alias_registry", "type_name_registry", "binds_by_name",
|
||||
"atoms_by_name", "atom_views", "atom_ctxs", "atom_phases",
|
||||
"atom_infos", "collisions",
|
||||
}) do
|
||||
corpus[key] = {}
|
||||
end
|
||||
|
||||
for _, src in ipairs(corpus.source_order or {}) do
|
||||
local scan = src.scan
|
||||
if scan then
|
||||
local path = src.path
|
||||
-- register_alias_registry: keyed by R_* alias ident.
|
||||
-- Each AliasEntry carries `source_file` (set by scan_source to the path).
|
||||
for name, entry in pairs(scan.register_alias_registry or {}) do
|
||||
local site = build_site(entry.source_file or path, entry.source_line)
|
||||
merge_named_with_sites(
|
||||
corpus.register_alias_registry, name, entry, site,
|
||||
corpus.collisions, "alias", alias_shape)
|
||||
end
|
||||
|
||||
-- type_name_registry: keyed by type ident; covers Struct_/Enum_/typedef.
|
||||
for name, entry in pairs(scan.type_name_registry or {}) do
|
||||
local site = build_site(path, entry.source_line)
|
||||
merge_named_with_sites(
|
||||
corpus.type_name_registry, name, entry, site,
|
||||
corpus.collisions, "type", type_shape)
|
||||
end
|
||||
|
||||
-- binds_by_name: the Binds_* projection of Struct_ types.
|
||||
-- Sources populate scan.binds[] (per-source array) with `{line, name, fields, body}`.
|
||||
-- We merge by name (Binds_X) so cross-source Struct_(X) declarations can be coalesced or collided.
|
||||
for _, bind_entry in ipairs(scan.binds or {}) do
|
||||
local site = build_site(path, bind_entry.line)
|
||||
merge_named_with_sites(
|
||||
corpus.binds_by_name, bind_entry.name, bind_entry, site,
|
||||
corpus.collisions, "binds", bind_shape)
|
||||
end
|
||||
|
||||
-- atoms_by_name: MipsAtom_(name) + MipsAtomComp_(name) + MipsAtomComp_Proc_(name).
|
||||
-- Each atom carries `{line, name, body, body_off, kind, raw_name, ...}`.
|
||||
-- Duplicate atom names across sources are first-wins + collision; see the atom_infos block below for the evidence list.
|
||||
for _, atom_entry in ipairs(scan.atoms or {}) do
|
||||
local site = build_site(path, atom_entry.line)
|
||||
merge_named_with_sites(
|
||||
corpus.atoms_by_name, atom_entry.name, atom_entry, site,
|
||||
corpus.collisions, "atom", atom_shape)
|
||||
end
|
||||
|
||||
-- atom_views: keyed by atom_name; each carries `binds_name` + per-atom overrides.
|
||||
for name, entry in pairs(scan.atom_views or {}) do
|
||||
local site = build_site(path, entry.info_line)
|
||||
merge_named_with_sites(
|
||||
corpus.atom_views, name, entry, site,
|
||||
corpus.collisions, "view", view_shape)
|
||||
end
|
||||
|
||||
-- atom_ctxs: keyed by atom_name; each carries `rbind_atom`.
|
||||
for name, entry in pairs(scan.atom_ctxs or {}) do
|
||||
local site = build_site(path, entry.info_line)
|
||||
merge_named_with_sites(
|
||||
corpus.atom_ctxs, name, entry, site,
|
||||
corpus.collisions, "ctx", ctx_shape)
|
||||
end
|
||||
|
||||
-- atom_phases: keyed by phase label; each carries `atoms = [...]`.
|
||||
-- The phase atoms list is set-shaped for the collision discipline (phase_shape sorts atoms before comparing).
|
||||
for name, entry in pairs(scan.atom_phases or {}) do
|
||||
local site = build_site(path, 0)
|
||||
merge_named_with_sites(
|
||||
corpus.atom_phases, name, entry, site,
|
||||
corpus.collisions, "phase", phase_shape)
|
||||
end
|
||||
|
||||
-- atom_infos: ALWAYS append every record in source/declaration order.
|
||||
-- Duplicates are preserved so the annotation pass can flag them via `check_unique_annotation`;
|
||||
-- The merge is purely order-preserving.
|
||||
for _, info in ipairs(scan.atom_infos or {}) do
|
||||
corpus.atom_infos[#corpus.atom_infos + 1] = info
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
-- ════════════════════════════════════════════════════════════════════════════
|
||||
-- M — module exports
|
||||
-- ════════════════════════════════════════════════════════════════════════════
|
||||
@@ -1704,37 +2001,50 @@ local M = {}
|
||||
--- Walk each source once and attach the fat SourceScan payload to `src.scan`.
|
||||
--- No output files; this is a pure in-memory pre-processing pass.
|
||||
---
|
||||
--- Runs in 3 phases.
|
||||
--- Pass 1a: `scan_source_pre_pass` over every source, populating the cross-source `ctx.shared._code_macros` AND `ctx.shared._code_macro_bodies` registries.
|
||||
--- The bodies table holds the raw post-`=` text of every `#define R_*_Code` line (cross-source) so the chain walker can fall back when a
|
||||
--- sdefining `#define` lives in a different source than the chain call site.
|
||||
--- Pass 1b: Resolve every collected macro's chain using the bodies table as fallback.
|
||||
--- This fills in `code_macros` entries whose defining source was scanned AFTER the call site
|
||||
--- (e.g. lottes_tape.h's `R_TapePtr_Code -> R_T8_Code` chain into mips.h's `R_T8_Code = 24`).
|
||||
--- Pass 2: The full `scan_source(source, source_file, code_macros, code_macro_bodies)` walk, which feeds `out._code_macros = ctx.shared._code_macros`
|
||||
--- (and `out._code_macro_bodies = ctx.shared._code_macro_bodies`)
|
||||
--- so the enum parser can resolve cross-source `R_*_Code` references and bare `R_*` symbols via the `_Code` registry fallback.
|
||||
--- Strip: `src.scan._code_macros` AND `src.scan._code_macro_bodies` are nilled before returning so downstream passes
|
||||
--- (annotation, components, offsets, dwarf_injection, etc.) don't see the private parse state.
|
||||
--- Runs in 5 phases.
|
||||
--- Resolve: Resolve the canonical source order from `ctx.shared.corpus.source_order`.
|
||||
--- The canonical corpus is the SOLE source of truth; no `ctx.sources` alias is consulted and no per-source fallback synthesis is performed.
|
||||
--- Pass 1a: `scan_source_pre_pass` over every source, populating LOCAL `code_macros` AND LOCAL `code_macro_bodies` tables.
|
||||
--- The bodies table holds the raw post-`=` text of every `#define R_*_Code` line (cross-source)
|
||||
--- so the chain walker can fall back when the defining `#define` lives in a different source than the chain call site.
|
||||
--- Pass 1b: Resolve every collected macro's chain using the bodies table as fallback.
|
||||
--- This fills in `code_macros` entries whose defining source was scanned AFTER the call site (e.g. lottes_tape.h's `R_TapePtr_Code -> R_T8_Code` chain into mips.h's `R_T8_Code = 24`).
|
||||
--- Pass 2: The full `scan_source(source, source_file, code_macros, code_macro_bodies)` walk per source. The per-source `src.scan` payload includes the source-local registries
|
||||
--- (register_alias_registry, type_name_registry, atom_views, atom_ctxs, atom_phases, binds, atoms, atom_infos, ...).
|
||||
--- Strip: Strip `src.scan._code_macros`, `src.scan._code_macro_bodies`, and the `_source_file` pointer.
|
||||
--- The LOCAL tables `code_macros` and `code_macro_bodies` go out of scope here; they MUST NOT appear on `ctx.shared`, `ctx.shared.corpus`, or any `src.scan` after this point.
|
||||
--- Merge: Iterate `ctx.shared.corpus.source_order` in declared order. For every source's local registry, first-wins lookup identity (entry from the first declaration site becomes the canonical corpus entry);
|
||||
--- identical shapes coalesce by appending the declaration site; conflicting shapes keep the first lookup entry and append ONE typed collision record with shape diff.
|
||||
--- Populate `register_alias_registry`, `type_name_registry`, `binds_by_name`, `atoms_by_name`, `atom_views`, `atom_ctxs`, `atom_phases`.
|
||||
--- `atom_infos` ALWAYS appends every record (preserving source order + duplicates for annotation evidence).
|
||||
---
|
||||
--- @param ctx PassCtx
|
||||
--- @return PassResult
|
||||
function M.run(ctx)
|
||||
-- Initialize the cross-source code-macro + body registries in ctx.shared.
|
||||
-- (Pass 1a writes into both; pass 1b reads from both; pass 2 reads both.)
|
||||
ctx.shared = ctx.shared or {}
|
||||
ctx.shared._code_macros = ctx.shared._code_macros or {}
|
||||
ctx.shared._code_macro_bodies = ctx.shared._code_macro_bodies or {}
|
||||
local code_macros = ctx.shared._code_macros
|
||||
local code_macro_bodies = ctx.shared._code_macro_bodies
|
||||
-- The cross-source _code_macros / _code_macro_bodies tables are LOCAL to this run.
|
||||
-- They are shared across source scans ONLY long enough to resolve cross-source R_*_Code chains, then DISCARDED.
|
||||
-- They MUST NOT appear on ctx.shared, ctx.shared.corpus, or any src.scan after
|
||||
-- this function returns.
|
||||
local code_macros = {}
|
||||
local code_macro_bodies = {}
|
||||
|
||||
-- Resolve the canonical source list. The corpus owns the authoritative source_order.
|
||||
-- A context without `ctx.shared.corpus` is rejected with an explicit canonical-corpus error.
|
||||
ctx.shared = ctx.shared or {}
|
||||
local corpus = ctx.shared.corpus
|
||||
if not corpus or type(corpus.source_order) ~= "table" then
|
||||
error("scan_source.run requires ctx.shared.corpus.source_order (the canonical corpus is the source of truth; no per-source fallback is supported)", 0)
|
||||
end
|
||||
local sources = corpus.source_order
|
||||
|
||||
-- Pass 1a: collect `_code_macros` + `_code_macro_bodies` across ALL sources.
|
||||
for _, src in ipairs(ctx.sources) do
|
||||
for _, src in ipairs(sources) do
|
||||
scan_source_pre_pass(src.text, code_macros, code_macro_bodies)
|
||||
end
|
||||
|
||||
-- Pass 1b: resolve every collected macro's chain with cross-source fallback.
|
||||
-- The bodies table was populated for every `#define R_*_Code` line in pass 1a;
|
||||
-- this iteration finishes the chain even when the chain hops span sources
|
||||
-- This iteration finishes the chain even when the chain hops span sources
|
||||
-- (e.g. R_TapePtr_Code -> R_T8_Code -> 24 spans lottes_tape.h into mips.h).
|
||||
-- Same `code_macros` table is shared with pass 2 below.
|
||||
for macro_name, _ in pairs(code_macro_bodies) do
|
||||
@@ -1746,28 +2056,33 @@ function M.run(ctx)
|
||||
end
|
||||
end
|
||||
|
||||
-- Pass 2: run the full scan with the shared `_code_macros` + `_code_macro_bodies`
|
||||
-- so the enum parser can resolve cross-source `R_*_Code` references and bare `R_*` symbols via the `_Code` registry fallback.
|
||||
for _, src in ipairs(ctx.sources) do
|
||||
-- Pass 2: run the full scan with the shared `_code_macros` + `_code_macro_bodies` so the enum parser can resolve cross-source
|
||||
-- `R_*_Code` references and bare `R_*` symbols via the `_Code` registry fallback.
|
||||
-- The private `_code_macros` / `_code_macro_bodies` / `_source_file` strip is midway through `source_order`.
|
||||
-- Must not leave earlier sources leaking the private parse state, because a separate post-loop would not run when an exception propagates out of pass 2.
|
||||
for _, src in ipairs(sources) do
|
||||
src.scan = scan_source(src.text, src.path, code_macros, code_macro_bodies)
|
||||
-- Pre-tokenize each atom body once (plex: single source of truth).
|
||||
-- Downstream passes (offsets, word-counts, components, static-analysis) read from
|
||||
-- `atom.body_tokens` instead of calling `split_top_level_commas` / `tokenize_body` independently.
|
||||
-- The tokens are memoized in duffle.lua's cache, so re-access is O(1).
|
||||
for _, atom in ipairs(src.scan.atoms) do atom.body_tokens = duffle.tokenize_body(atom.body) end
|
||||
for _, atom in ipairs(src.scan.raw_atoms or {}) do atom.body_tokens = duffle.tokenize_body(atom.body) end
|
||||
end
|
||||
|
||||
-- Strip `_code_macros` + `_code_macro_bodies` (and the convenience `_source_file` pointer)
|
||||
-- before returning so downstream passes don't see private parse state.
|
||||
for _, src in ipairs(ctx.sources) do
|
||||
-- Strip the three private fields immediately so a later fatal source does not leave this source leaking parse state.
|
||||
-- The shared `code_macros` / `code_macro_bodies` locals remain in the outer scope and keep their contents for subsequent sources.
|
||||
if src.scan then
|
||||
src.scan._code_macros = nil
|
||||
src.scan._code_macro_bodies = nil
|
||||
src.scan._source_file = nil
|
||||
end
|
||||
-- Pre-tokenize each atom body once (plex: single source of truth).
|
||||
-- Downstream passes (offsets, word-counts, components, static-analysis) read from `atom.body_tokens` instead of calling `split_top_level_commas` / `tokenize_body` independently.
|
||||
-- The tokens are memoized in duffle.lua's cache, so re-access is O(1).
|
||||
for _, atom in ipairs(src.scan.atoms) do atom.body_tokens = duffle.tokenize_body(atom.body) end
|
||||
for _, atom in ipairs(src.scan.raw_atoms or {}) do atom.body_tokens = duffle.tokenize_body(atom.body) end
|
||||
end
|
||||
|
||||
-- Merge per-source scans into the canonical corpus registries.
|
||||
-- First-wins lookup identity + collision discipline (see merge_corpus_registries).
|
||||
-- The corpus is always present; no conditional / fallback path.
|
||||
merge_corpus_registries(corpus)
|
||||
|
||||
-- code_macros and code_macro_bodies go out of scope here; their references are not captured on corpus, ctx.shared, or any src.scan.
|
||||
-- The Lua GC reclaims them on M.run return.
|
||||
return { outputs = {}, errors = {}, warnings = {} }
|
||||
end
|
||||
|
||||
|
||||
+1341
-262
File diff suppressed because it is too large
Load Diff
@@ -1,11 +1,17 @@
|
||||
--- word_count_eval.lua — Word-counting logic for the tape-atom metaprogram pipeline.
|
||||
---
|
||||
--- Three responsibilities:
|
||||
--- 1. **Public utilities** (used by `passes/components.lua`, `passes/offsets.lua`, `passes/annotation.lua`):
|
||||
--- - `M.count_token_words(token, wc)` — words emitted by one token
|
||||
--- - `M.scan_dir(dir, suffix)` — glob walk for *.macs.h
|
||||
--- 2. **Pass entry** `M.run(ctx)` — loads metadata.h + *.macs.h into `ctx.shared.word_counts` for downstream passes.
|
||||
--- 3. **Internal helpers** for the body scanner.
|
||||
--- Two responsibilities:
|
||||
--- 1. **Public utility** `M.count_token_words(token, wc)`: Used by `passes/offsets.lua`, `passes/annotation.lua`, and other passes.
|
||||
--- 2. **Pass entry** `M.run(ctx)`: Loads the authored `word_count.metadata.h` into `ctx.shared.corpus.word_counts` for downstream passes.
|
||||
--- The generated `.macs.h` files are OUTPUT artifacts and are NOT inputs to this pass;
|
||||
--- Current component counts are owned by `passes/components.lua` (which populates `corpus.word_counts` and `corpus.component_body_index`
|
||||
--- AFTER computing each current count from the just-built body + `corpus.word_counts`).
|
||||
---
|
||||
--- **Canonical contract**:
|
||||
--- * `ctx.shared.corpus.word_counts` is the canonical count table.
|
||||
--- * `corpus.word_counts` is the sole count table. Consumers read `corpus.word_counts` directly.
|
||||
--- * `ctx.shared.components` and `ctx.shared.component_body_index` are NOT created by this pass (canonical projections only).
|
||||
--- * No `.macs.h` recursive discovery (no `scan_dir`, no scan cache, no `_invalidate_scan_cache`).
|
||||
---
|
||||
--- **Conventions**: tabs (1/level), EmmyLua annotations, no regex,
|
||||
--- Lua 5.3 compatible.
|
||||
@@ -14,23 +20,11 @@
|
||||
-- Module-scope requires + package.path setup
|
||||
-- ════════════════════════════════════════════════════════════════════════════
|
||||
|
||||
-- Resolve `arg[0]` to an absolute-ish script directory so that `require("duffle")` resolves against `scripts/` regardless of CWD.
|
||||
-- Bootstrap: see `ps1_meta.lua` for the rationale.
|
||||
-- Bootstrap: load `scripts/duffle_paths.lua` (sets package.path + package.cpath).
|
||||
-- Uses `debug.getinfo` to find this file's own directory, so it works both standalone and when require'd from the orchestrator.
|
||||
-- Bootstrap: load `duffle_paths.lua` via `debug.getinfo(1, "S").source` (works both standalone + when require'd).
|
||||
-- duffle_paths.lua sets package.path then returns `require("duffle")` at the bottom, so the dofile value IS the duffle module.
|
||||
local _bootstrap_dir = debug.getinfo(1, "S").source:match("^@?(.*[/\\])") or "./"
|
||||
local duffle = dofile(_bootstrap_dir .. "../duffle_paths.lua")
|
||||
|
||||
-- ════════════════════════════════════════════════════════════════════════════
|
||||
-- Constants
|
||||
-- ════════════════════════════════════════════════════════════════════════════
|
||||
|
||||
-- Required native extension: lfs (LuaFileSystem). Built by `update_deps.ps1` to
|
||||
-- `toolchain/lfs/lfs.dll` and wired into package.cpath by `scripts/duffle_paths.lua`.
|
||||
-- If lfs is missing, `require` throws — fail loud per the build-tool convention.
|
||||
local lfs = require("lfs")
|
||||
local duffle = dofile(_bootstrap_dir .. "../duffle_paths.lua")
|
||||
|
||||
-- ════════════════════════════════════════════════════════════════════════════
|
||||
-- Type declarations
|
||||
@@ -49,12 +43,12 @@ local lfs = require("lfs")
|
||||
--- @field sources SourceFile[] -- all source files in the build
|
||||
--- @field metadata_path string -- path to word_count.metadata.h
|
||||
--- @field shared table -- cross-pass shared state
|
||||
--- @field shared.word_counts WordCounts -- populated by this pass
|
||||
--- @field shared.corpus table -- canonical corpus (required)
|
||||
--- @field shared.corpus.word_counts WordCounts -- canonical count table (populated by this pass)
|
||||
--- @field out_root string -- output root (e.g. "build/gen")
|
||||
--- @field project_root string -- project root (e.g. "code/")
|
||||
--- @field upstream table<string, table> -- per-pass upstream outputs
|
||||
--- @field flags table -- CLI flags
|
||||
--- @field dry_run boolean -- if true, compute but don't write
|
||||
--- @field verbose boolean -- if true, log diagnostic info
|
||||
|
||||
--- @class PassResult
|
||||
@@ -76,8 +70,8 @@ local M = {}
|
||||
--- For most tokens (regular MIPS instructions) this returns 1.
|
||||
--- For `mac_X(...)` calls, this returns the resolved word count from `wc` (recursively if needed). For `nop2` etc., returns wc[name].
|
||||
--- For unknown macros, returns 1 and (optionally) warns.
|
||||
--- @param token string -- a single token from split_top_level_commas
|
||||
--- @param wc WordCounts -- the shared word-count table
|
||||
--- @param token string -- a single token from split_top_level_commas
|
||||
--- @param wc WordCounts -- the shared word-count table
|
||||
--- @return integer
|
||||
function M.count_token_words(token, wc)
|
||||
local s = duffle.trim(token)
|
||||
@@ -92,83 +86,42 @@ function M.count_token_words(token, wc)
|
||||
return 1
|
||||
end
|
||||
|
||||
-- ┌────────────────────────────────────────────────────────────────────┐
|
||||
-- │ Shared utility: scan_dir │
|
||||
-- └────────────────────────────────────────────────────────────────────┘
|
||||
|
||||
-- Cache the scan_dir result per (dir, suffix) in package.loaded.
|
||||
-- The cache persists for the lifetime of the Lua process (cleared when ps1_meta.lua exits).
|
||||
-- If a build removes/creates .macs.h files mid-process, the caller can invalidate by calling `M._invalidate_scan_cache()`.
|
||||
local SCAN_CACHE_KEY = "__word_count_eval_scan_cache__"
|
||||
|
||||
--- Scan `code/` for files matching `suffix` (e.g. `*.macs.h`).
|
||||
--- Native directory enumeration via lfs (~2ms). Zero subprocess spawns.
|
||||
--- @param dir string -- project root directory
|
||||
--- @param suffix string -- file pattern, e.g. "*.macs.h"
|
||||
--- @return string[]
|
||||
function M.scan_dir(dir, suffix)
|
||||
local key = dir .. "\0" .. suffix
|
||||
|
||||
local cache = package.loaded[SCAN_CACHE_KEY]
|
||||
if cache and cache[key] then return cache[key] end
|
||||
|
||||
local results = {}
|
||||
local code_dir = dir .. "/code"
|
||||
if lfs.attributes(code_dir, "mode") == "directory" then
|
||||
for mod_name in lfs.dir(code_dir) do
|
||||
if mod_name ~= "." and mod_name ~= ".." then
|
||||
local gen_path = code_dir .. "/" .. mod_name .. "/gen"
|
||||
if lfs.attributes(gen_path, "mode") == "directory" then
|
||||
for fname in lfs.dir(gen_path) do
|
||||
if fname:match("%.macs%.h$") then
|
||||
results[#results + 1] = gen_path .. "/" .. fname
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
-- Cache the result (including empty results).
|
||||
cache = cache or {}
|
||||
cache[key] = results
|
||||
package.loaded[SCAN_CACHE_KEY] = cache
|
||||
|
||||
return results
|
||||
end
|
||||
|
||||
--- Invalidate the scan cache (call after creating new .macs.h files in the same Lua process — usually not needed).
|
||||
function M._invalidate_scan_cache() package.loaded[SCAN_CACHE_KEY] = nil end
|
||||
|
||||
-- ┌────────────────────────────────────────────────────────────────────┐
|
||||
-- │ Pass entry: M.run(ctx) — "word-counts" pass │
|
||||
-- └────────────────────────────────────────────────────────────────────┘
|
||||
|
||||
--- Load metadata.h + scan for existing *.macs.h files into ctx.shared.word_counts.
|
||||
--- Loading the .macs.h files is idempotent: entries from later (current-build) .macs.h files override metadata.h entries of the same name.
|
||||
--- Load the authored `word_count.metadata.h` into `ctx.shared.corpus.word_counts`.
|
||||
--- Generated `.macs.h` files are OUTPUT artifacts and are NOT scanned as inputs.
|
||||
--- Current component counts are computed and inserted by `passes/components.lua`
|
||||
--- after the components pass iterates `corpus.source_order` and writes each source's `<dir_basename>.macs.h` file.
|
||||
---
|
||||
--- Contract:
|
||||
--- * `ctx.shared.corpus` MUST exist (canonical corpus ownership).
|
||||
--- * `ctx.metadata_path` MUST be a readable file path to the authored `word_count.metadata.h`.
|
||||
--- * The pass assigns exactly one table to `corpus.word_counts`.
|
||||
--- Consumers read the corpus-owned table directly.
|
||||
--- Consumers must read `corpus.word_counts` directly.
|
||||
--- @param ctx PassCtx
|
||||
--- @return PassResult
|
||||
function M.run(ctx)
|
||||
local wc = {}
|
||||
|
||||
-- 1. Load metadata.h (the encoding-macro source of truth).
|
||||
local meta_counts = duffle.load_word_counts(ctx.metadata_path)
|
||||
for name, count in pairs(meta_counts) do wc[name] = count end
|
||||
|
||||
-- 2. Scan project_root recursively for *.macs.h files (component-macro source).
|
||||
local macs_files = M.scan_dir(ctx.project_root, "*.macs.h")
|
||||
for _, macs_path in ipairs(macs_files) do
|
||||
local ok, mc = pcall(duffle.load_word_counts, macs_path)
|
||||
if not ok then
|
||||
io.stderr:write(string.format("[word_count_eval] parse error in '%s': %s\n", macs_path, tostring(mc)))
|
||||
elseif type(mc) ~= "table" then
|
||||
io.stderr:write(string.format("[word_count_eval] '%s' did not return a table (got %s)\n", macs_path, type(mc)))
|
||||
else
|
||||
for name, count in pairs(mc) do wc[name] = count end
|
||||
end
|
||||
-- 1. Canonical-corpus ownership gate.
|
||||
local corpus = ctx.shared and ctx.shared.corpus
|
||||
if type(corpus) ~= "table" then
|
||||
error("word_count_eval.run requires ctx.shared.corpus (canonical corpus). The fixture must install the corpus before running this pass.", 0)
|
||||
end
|
||||
|
||||
ctx.shared.word_counts = wc
|
||||
-- 2. metadata_path gate.
|
||||
if type(ctx.metadata_path) ~= "string" or ctx.metadata_path == "" then
|
||||
error("word_count_eval.run requires ctx.metadata_path (path to the authored word_count.metadata.h).", 0)
|
||||
end
|
||||
|
||||
-- 3. Load authored metadata. Generated .macs.h files are NOT scanned
|
||||
-- (the canonical pass computes their counts from the just-built bodies after disk emission; see passes/components.lua).
|
||||
local wc = duffle.load_word_counts(ctx.metadata_path)
|
||||
|
||||
-- 4. Assign the canonical count table. ONE assignment, no copy. The assignment creates no secondary alias.
|
||||
corpus.word_counts = wc
|
||||
|
||||
return { outputs = {}, errors = {}, warnings = {} }
|
||||
end
|
||||
|
||||
|
||||
+283
-467
File diff suppressed because it is too large
Load Diff
+1
-16
@@ -39,13 +39,7 @@ pop-location
|
||||
|
||||
# ════════════════════════════════════════════════════════════════════════════
|
||||
# PCSX-Redux — built via MSBuild (VS2022)
|
||||
#
|
||||
# Requires: Visual Studio 2022 with the C++ desktop workload.
|
||||
# The .vcxproj files target platform toolset v145, but VS2022 ships v143;
|
||||
# we pass /p:PlatformToolset=v143 to retarget at build time (no file edits).
|
||||
# NuGet packages (glfw, luajit.native, libFFmpeg-lite, x64sentry) are
|
||||
# restored automatically by MSBuild on first build.
|
||||
#
|
||||
# Output: toolchain\pcsx-redux\vsprojects\x64\Debug\pcsx-redux.exe
|
||||
# ════════════════════════════════════════════════════════════════════════════
|
||||
|
||||
@@ -65,8 +59,7 @@ $path_pcsx_sln = join-path $path_pcsx_redux 'vsprojects\pcsx-redux.sln'
|
||||
& $msbuild_exe $path_pcsx_sln /p:Configuration=Release /p:Platform=x64 /p:PlatformToolset=v143 /m /v:minimal
|
||||
|
||||
# Locate luajit via scoop. `luajit.exe` is on PATH via scoop's shim;
|
||||
# we use `scoop prefix` to find the install root for the include dir
|
||||
# (needed to compile lpeg against luajit's headers).
|
||||
# we use `scoop prefix` to find the install root for the include dir (needed to compile lpeg against luajit's headers).
|
||||
# If scoop or luajit is missing, fail fast with an actionable message.
|
||||
$luajit_prefix = & scoop prefix luajit 2>$null
|
||||
if (-not $luajit_prefix -or -not (Test-Path (Join-Path $luajit_prefix 'bin/luajit.exe'))) {
|
||||
@@ -87,7 +80,6 @@ if (-not $lua_inc_dir) {
|
||||
# Generate lpeg.dll by compiling the 6 source files directly.
|
||||
# `gcc` is on PATH (scoop's shim puts it there).
|
||||
# The source files: lpcap.c lpcode.c lpcset.c lpprint.c lptree.c lpvm.c
|
||||
# (per the lpeg makefile — no `make.lua` template generator in this version).
|
||||
# Link against luajit's import library (`libluajit-5.1.a`) for the Lua C API symbols (lua_*, luaL_*).
|
||||
$luajit_lib_dir = Join-Path $luajit_prefix 'lib'
|
||||
$lpeg_sources = @('lpcap.c', 'lpcode.c', 'lpcset.c', 'lpprint.c', 'lptree.c', 'lpvm.c')
|
||||
@@ -103,8 +95,6 @@ pop-location
|
||||
|
||||
# ════════════════════════════════════════════════════════════════════════════
|
||||
# lfs (LuaFileSystem) — compiled from pcsx-redux's vendored luafilesystem source.
|
||||
# Used by word_count_eval.lua :: scan_dir for native directory enumeration (~2ms)
|
||||
# instead of spawning `dir /b /s` as a subprocess (~56ms).
|
||||
# Source: toolchain/pcsx-redux/third_party/luafilesystem/src/lfs.c
|
||||
# Output: toolchain/lfs/lfs.dll
|
||||
# ════════════════════════════════════════════════════════════════════════════
|
||||
@@ -118,11 +108,6 @@ $lfs_dll_import = join-path $luajit_lib_dir 'libluajit-5.1.dll.a'
|
||||
|
||||
# ════════════════════════════════════════════════════════════════════════════
|
||||
# OpenBIOS — built from the PCSX-Redux source tree via make + mipsel-none-elf
|
||||
#
|
||||
# OpenBIOS is an open-source PS1 BIOS implementation (no retail BIOS dump needed).
|
||||
# It builds with the MIPS cross-toolchain (`mipsel-none-elf-gcc`, on PATH via the `mips` toolchain installer)
|
||||
# + `make` (on PATH via scoop).
|
||||
#
|
||||
# Output: toolchain\pcsx-redux\src\mips\openbios\openbios.bin
|
||||
# ════════════════════════════════════════════════════════════════════════════
|
||||
|
||||
|
||||
Reference in New Issue
Block a user