From a37ffe6f58d2e373572d410657fcac99839ebc33 Mon Sep 17 00:00:00 2001 From: Ed_ Date: Fri, 21 Aug 2026 14:02:31 -0400 Subject: [PATCH] Last adjustments for metaprogram, hitting a intermission checkpoint to do docs, writeups, and other research distillations. --- code/duffle/lottes_tape.h | 2 +- code/hello_camera/hello_camera.c | 10 +-- scripts/passes/report.lua | 16 +++++ scripts/passes/scan_source.lua | 105 ++++++++++++++++++++++++++++- scripts/passes/static_analysis.lua | 62 +++++++++++++++++ scripts/ps1_meta.lua | 8 +-- 6 files changed, 192 insertions(+), 11 deletions(-) diff --git a/code/duffle/lottes_tape.h b/code/duffle/lottes_tape.h index 8d12f7b..bcb3b2f 100644 --- a/code/duffle/lottes_tape.h +++ b/code/duffle/lottes_tape.h @@ -248,11 +248,11 @@ FI_ TapeBuilder tb_make(Slice mem) { return (TapeBuilder){ u4_(mem.ptr), mem.len FI_ void tb_emit(TapeBuilder* tb, MipsAtom* 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; } #define tb_emit_(atom) tb_emit(& tb, atom) -#define tb_data_(field, data) tb_data(& tb, u4_(data)) FI_ void tb_bind(TapeBuilder* tb, Slice data) { mem_copy(tb->ptr + tb->used * S_(MipsCode), u4_(data.ptr), data.len); tb->used += data.len / S_(MipsCode); } #define tb_bind_(tb,type,...) tb_bind(tb, (Slice){ (B1*)(& (type){__VA_ARGS__}), S_(type) }); static_assert(S_(type) % S_(MipsCode) == 0) +// NOTE(Ed): Wip still ideating convention. Possibly will never use a composite. #define tb_emit_wbind_(tb,atom,...) tb_emit(tb,atom); tb_bind_(tb,tmpl(Binds,atom),__VA_ARGS__) #define tb_emit_wbind2_(tb,atom,type,...) tb_emit(tb,atom); tb_bind_(tb,type,__VA_ARGS__) diff --git a/code/hello_camera/hello_camera.c b/code/hello_camera/hello_camera.c index 5950daf..29fe8e4 100644 --- a/code/hello_camera/hello_camera.c +++ b/code/hello_camera/hello_camera.c @@ -247,15 +247,15 @@ void update(PrimitiveArena* pa, U4* ordering_buf) tb.used = 0; tb_scope_run(& tb) { // Grab latest state from bios. tb_emit_(pad_bios_snapshot); - tb_data_(raw, & smem.pad_raw[0]); - tb_data_(state, & smem.pad[0]); + tb_data(& tb, u4_(& smem.pad_raw[0])); + tb_data(& tb, u4_(& smem.pad[0])); // tb_emit_(pad_bios_snapshot); // tb_data_(raw, & smem.pad_raw[1]); // tb_data_(state, & smem.pad[1]); tb_emit_(pad_input_cam); - tb_data_(state, & smem.pad[0]); - tb_data_(cam, & smem.cam); + tb_data(& tb, u4_(& smem.pad[0])); + tb_data(& tb, u4_(& smem.cam)); // tb_emit_(pad_input_cube_rotation); // tb_data_(state, & smem.pad[0]); @@ -288,7 +288,7 @@ void update(PrimitiveArena* pa, U4* ordering_buf) if (use_c11_path == false) { tb.used = 0; tb_scope_run(& tb) { - resolve_look_at(& tb, & smem.cam.look_at, & smem.cam.pos, & smem.cube.pos, & v3s4(0, -fp_one, 0)); + camera_look_at(& tb, & smem.cam, & smem.cube.pos, & v3s4(0, -fp_one, 0)); } } diff --git a/scripts/passes/report.lua b/scripts/passes/report.lua index bfa57b3..846c69a 100644 --- a/scripts/passes/report.lua +++ b/scripts/passes/report.lua @@ -573,6 +573,21 @@ local function render_section_atom_bundles(add, view) end end +--- @param add fun(s: string): nil +--- @param view ModuleView +--- @return nil +local function render_section_tape_emits(add, view) + local wrote = false ---@type boolean + for _, src in ipairs(view.sources or {}) do ---@type integer, SourceFile + for _, emit in ipairs((src.scan and src.scan.tape_emits) or {}) do ---@type integer, TapeEmit + wrote = true + add(string.format("- `%s` `%s`", emit.name or "?", emit.binds or "—")) + end + end + if not wrote then add("_(none)_"); add(""); return end + add("") +end + --- @param add fun(s: string): nil --- @param view ModuleView --- @return nil @@ -925,6 +940,7 @@ local SECTION_RENDERERS = { ---@type SectionRenderer[] { header = "## Component annotations", render = render_section_component_annotations }, { header = "## Binds_* structs", render = render_section_binds }, { header = "## Atom bundles", render = render_section_atom_bundles }, + { header = "## Tape emits", render = render_section_tape_emits }, { header = "## Phases / views / ctx", render = render_section_phases }, { header = "## Register aliases", render = render_section_aliases }, { header = "## Auto-reg", render = render_section_autoreg }, diff --git a/scripts/passes/scan_source.lua b/scripts/passes/scan_source.lua index e391d11..0832f51 100644 --- a/scripts/passes/scan_source.lua +++ b/scripts/passes/scan_source.lua @@ -189,6 +189,14 @@ local parse_enum_int_literal ---@type fun(text: string, start: integer): (intege --- @class TapeChain --- @field [integer] string -- ordered atom names in one tb_emit chain +--- @class TapeEmit +--- @field name string +--- @field binds string|nil +--- @field line integer +--- @field path string +--- @field slot string|nil +--- @field data_words integer|nil + --- @class RegUseView --- @field names string[] --- @field lanes boolean @@ -215,6 +223,7 @@ local parse_enum_int_literal ---@type fun(text: string, start: integer): (intege --- @field reg_use_schemas table --- @field reg_use_errors RegUseError[] --- @field tape_chains TapeChain[] +--- @field tape_emits TapeEmit[] --- @field atom_bundles table --- @field _source_file string|nil --- @field _code_macros table|nil -- bag @@ -2900,6 +2909,23 @@ local function parse_addrs_assign(source, pos, ident_end, line_of, out) return rhs end +--- @param out SourceScan +--- @param name string +--- @param last string +--- @param line integer +--- @return nil +local function push_tape_emit(out, name, last, line) + out.tape_emits = out.tape_emits or {} + out.tape_emits[#out.tape_emits + 1] = { ---@type TapeEmit + name = name, + binds = nil, + line = line, + path = out._source_file or "", + slot = last:find("->", 1, true) and name or nil, + data_words = 0, + } +end + --- @param source string --- @param pos integer --- @param ident_end integer @@ -2910,10 +2936,12 @@ local function parse_tb_emit_(source, pos, ident_end, line_of, out) local after = duffle.skip_ws_and_cmt(source, ident_end) ---@type integer if source:sub(after, after) ~= "(" then return ident_end end local inner, after_p = duffle.read_parens(source, after) ---@type string|nil, integer - local name = duffle.trim(inner or ""):match("^([%w_]+)") ---@type string + local last = duffle.trim(inner or "") ---@type string + local name = last:match("^([%w_]+)") ---@type string if name then out._chain = out._chain or {} out._chain[#out._chain + 1] = name + push_tape_emit(out, name, last, line_of(pos)) end return after_p or (after + 1) end @@ -2938,6 +2966,49 @@ local function parse_tb_emit(source, pos, ident_end, line_of, out) if name then out._chain = out._chain or {} out._chain[#out._chain + 1] = name + push_tape_emit(out, name, last, line_of(pos)) + end + return after_p or (after + 1) +end + +--- @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_tb_bind_(source, pos, ident_end, line_of, out) + local after = duffle.skip_ws_and_cmt(source, ident_end) ---@type integer + if source:sub(after, after) ~= "(" then return ident_end end + local inner, after_p = duffle.read_parens(source, after) ---@type string|nil, integer + local args = duffle.split_top_level_commas(inner or "") ---@type string[] + local typ = duffle.trim(args[2] or "") ---@type string + if typ ~= "" then + local emits = out.tape_emits or {} ---@type TapeEmit[] + for i = #emits, 1, -1 do ---@type integer + if emits[i].binds == nil then + emits[i].binds = typ + break + end + end + end + return after_p or (after + 1) +end + +--- @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_tb_data(source, pos, ident_end, line_of, out) + local after = duffle.skip_ws_and_cmt(source, ident_end) ---@type integer + if source:sub(after, after) ~= "(" then return ident_end end + local _, after_p = duffle.read_parens(source, after) ---@type string|nil, integer + local emits = out.tape_emits or {} ---@type TapeEmit[] + local last = emits[#emits] ---@type TapeEmit|nil + if last then + last.data_words = (last.data_words or 0) + 1 end return after_p or (after + 1) end @@ -2945,6 +3016,8 @@ end local C_STMT_PARSERS = { ---@type table tb_emit_ = parse_tb_emit_, tb_emit = parse_tb_emit, + tb_bind_ = parse_tb_bind_, + tb_data = parse_tb_data, addrs = parse_addrs_assign, } @@ -2998,6 +3071,7 @@ local function scan_source(source, source_file, code_macros, code_macro_bodies) raw_atoms = {}, binds = {}, atom_bundles = {}, + tape_emits = {}, atom_infos = {}, component_atom_infos = {}, macros = {}, @@ -3313,6 +3387,7 @@ local function merge_corpus_registries(corpus) corpus.reg_use_errors = corpus.reg_use_errors or {} corpus.tape_chains = corpus.tape_chains or {} corpus.atom_bundles = corpus.atom_bundles or {} + corpus.tape_emits = corpus.tape_emits 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. @@ -3331,6 +3406,7 @@ local function merge_corpus_registries(corpus) "reg_use_errors", "tape_chains", "atom_bundles", + "tape_emits", }) do corpus[key] = {} end @@ -3438,6 +3514,9 @@ local function merge_corpus_registries(corpus) for _, chain in ipairs(scan.tape_chains or {}) do ---@type integer, TapeChain corpus.tape_chains[#corpus.tape_chains + 1] = chain end + for _, emit in ipairs(scan.tape_emits or {}) do ---@type integer, TapeEmit + corpus.tape_emits[#corpus.tape_emits + 1] = emit + end -- atom_bundles: keyed by typedef name. First-wins per name (like components). for name, bundle in pairs(scan.atom_bundles or {}) do ---@type string, AtomBundle @@ -3462,6 +3541,30 @@ local function merge_corpus_registries(corpus) bundle.entries = entries end end + + -- Resolve tb_emit names: atom first, else unique catalog slot → entries[slot]. + -- Ambiguous slot or missing entry: leave the raw ident (no A/B later). + for _, emit in ipairs(corpus.tape_emits) do ---@type integer, TapeEmit + if corpus.atoms_by_name[emit.name] == nil then + local slot = emit.slot or emit.name ---@type string + local hit = nil ---@type AtomBundle|nil + local n = 0 ---@type integer + for _, bundle in pairs(corpus.atom_bundles) do ---@type string, AtomBundle + for _, s in ipairs(bundle.slots or {}) do ---@type integer, string + if s == slot then + n = n + 1 + hit = bundle + break + end + end + end + local ident = hit and hit.entries and hit.entries[slot] ---@type string|nil + if n == 1 and ident then + emit.name = ident + emit.slot = slot + end + end + end end local SCHEMA_BODY_ERROR = { ---@type table -- bag: reguse error kind -> true diff --git a/scripts/passes/static_analysis.lua b/scripts/passes/static_analysis.lua index 621936b..4525c37 100644 --- a/scripts/passes/static_analysis.lua +++ b/scripts/passes/static_analysis.lua @@ -4016,6 +4016,66 @@ end -- Each check is one table row and one `check_*` function. -- This is the plex pattern: the iteration is in ONE place (validate), the variation is in DATA (this table). +--- @param src SourceFile +--- @param pipe_ctx PassScratch +--- @param findings Finding[] +--- @return nil +local function check_tb_bind_type_match(src, pipe_ctx, findings) + local atoms_by_name = pipe_ctx.atoms_by_name or {} ---@type table + local info_by = {} ---@type table + for _, info in ipairs(pipe_ctx.atom_infos_all or {}) do ---@type integer, AtomInfoEntry + info_by[info.atom_name] = info + end + for name, info in pairs(pipe_ctx.info_by_atom or {}) do ---@type string, AtomInfoEntry + info_by[name] = info + end + for _, emit in ipairs((src.scan and src.scan.tape_emits) or {}) do ---@type integer, TapeEmit + if atoms_by_name[emit.name] then + local atom_binds = info_by[emit.name] and info_by[emit.name].binds ---@type string|nil + if emit.binds and atom_binds and emit.binds ~= atom_binds then + findings[#findings + 1] = { + atom = emit.name, + line = emit.line or 0, + check = "tb_bind_type_match", + kind = "error", + msg = string.format("tb_emit '%s' binds %s but atom_bind is %s" + , emit.name, emit.binds, atom_binds), + } + end + end + end +end + +--- @param src SourceFile +--- @param pipe_ctx PassScratch +--- @param findings Finding[] +--- @return nil +local function check_tb_bind_required(src, pipe_ctx, findings) + local atoms_by_name = pipe_ctx.atoms_by_name or {} ---@type table + local info_by = {} ---@type table + for _, info in ipairs(pipe_ctx.atom_infos_all or {}) do ---@type integer, AtomInfoEntry + info_by[info.atom_name] = info + end + for name, info in pairs(pipe_ctx.info_by_atom or {}) do ---@type string, AtomInfoEntry + info_by[name] = info + end + for _, emit in ipairs((src.scan and src.scan.tape_emits) or {}) do ---@type integer, TapeEmit + if atoms_by_name[emit.name] then + local atom_binds = info_by[emit.name] and info_by[emit.name].binds ---@type string|nil + if atom_binds and emit.binds == nil and (emit.data_words or 0) == 0 then + findings[#findings + 1] = { + atom = emit.name, + line = emit.line or 0, + check = "tb_bind_required", + kind = "error", + msg = string.format("tb_emit '%s' has atom_bind(%s) but no tb_bind_ or tb_data" + , emit.name, atom_binds), + } + end + end + end +end + local CHECK_RULES = { ---@type CheckRule[] { name = "transfer_hazards", per_atom = check_transfer_hazards }, { name = "gte_input_latch", per_atom = check_gte_input_latch }, @@ -4040,6 +4100,8 @@ local CHECK_RULES = { ---@type CheckRule[] { name = "binds_no_substruct_deref", per_source = check_binds_no_substruct_deref }, { name = "component_self_consistency", per_source = check_component_self_consistency }, { name = "atom_calls_inferred_traffic", per_atom = check_atom_calls_inferred_traffic }, + { name = "tb_bind_type_match", per_source = check_tb_bind_type_match }, + { name = "tb_bind_required", per_source = check_tb_bind_required }, } -- ════════════════════════════════════════════════════════════════════════════ diff --git a/scripts/ps1_meta.lua b/scripts/ps1_meta.lua index 309d694..088d7aa 100644 --- a/scripts/ps1_meta.lua +++ b/scripts/ps1_meta.lua @@ -78,6 +78,7 @@ local PASS_FLAG_DISPATCH_KEY = "__pass__" ---@type string --- @field word_counts WordCounts --- @field components table --- @field atom_bundles table|nil +--- @field tape_emits TapeEmit[]|nil --- @field collisions CorpusCollision[] --- @field resolver SourceResolver --- @field component_atom_infos AtomInfoEntry[]|nil @@ -374,8 +375,7 @@ COMMON_FLAGS: --help Show this help and exit EXIT CODES: - 0 All requested passes succeeded - 1 Validation errors found + 0 Ran. Findings print on stderr and in the report; they do not fail the process. 2 Metaprogram internal error EXAMPLES: @@ -667,6 +667,7 @@ local function build_ctx(args) word_counts = {}, components = {}, atom_bundles = {}, + tape_emits = {}, collisions = {}, resolver = resolution.resolver, } @@ -818,8 +819,7 @@ local function main(argv) local requested = args.requested_set ---@type string[] local closed = topo_sort(PASSES, requested) ---@type string[] - local had_errors = dispatch_passes(ctx, closed) ---@type boolean - if had_errors then os.exit(EXIT_VALIDATION_ERRORS) end + dispatch_passes(ctx, closed) end) if not ok then