diff --git a/code/duffle/gte.atom.c b/code/duffle/gte.atom.c index 42551af..10e3887 100644 --- a/code/duffle/gte.atom.c +++ b/code/duffle/gte.atom.c @@ -330,9 +330,9 @@ MipsAtom_Proc_(aa, { mac_store_v3s4(r.res, r.dst_ptr, 0), jump_reg(R_AtomJmp), BdSlot_ nop // ac_yield: word 3-4 - // mac_yield() }) + /* ─── GTE OP cross product (a × b → out) ─── * Generalized V3_S4 cross product via GTE OP (OuterProduct12 libpsyx convention). * The >> 12 shift converts S12.20 → S12.0 OuterProduct12. */ diff --git a/code/hello_camera/gen/offsets.h b/code/hello_camera/gen/offsets.h index 5d9736d..7ef0afd 100644 --- a/code/hello_camera/gen/offsets.h +++ b/code/hello_camera/gen/offsets.h @@ -8,7 +8,7 @@ #pragma region hello_camera -// --- atom: pad_input_cube_rotation (61 words) --- +// --- atom: pad_input_cube_rotation (60 words) --- #define _atom_offset_dpad_left_exit_dpad_left 6 #define _atom_offset_dpad_right_exit_dpad_right 6 diff --git a/scripts/duffle_emit.lua b/scripts/duffle_emit.lua index b9faa80..5d558b4 100644 --- a/scripts/duffle_emit.lua +++ b/scripts/duffle_emit.lua @@ -210,7 +210,7 @@ local function _project_emission_inner(root_body_entry, ctx_table) local prefix = reg_use_param .. "." if operand:sub(1, #prefix) ~= prefix then return nil end local member_path = operand:sub(#prefix + 1) - local slot = reg_use_schema.alias_to_slot[member_path] + local slot = reg_use_schema.alias_to_slot[member_path] if not slot then return nil, member_path end return "reguse:" .. atom_name .. ":" .. slot, nil, slot end @@ -223,9 +223,7 @@ local function _project_emission_inner(root_body_entry, ctx_table) return ids end - local function emit_word(encoder, args, line, word_call_text, - def_source_now, def_line_now, - immediate_call_text, root_call_text_w, sub_map) + local function emit_word(encoder, args, line, word_call_text, def_source_now, def_line_now, immediate_call_text, root_call_text_w, sub_map) local inv_ids = open_invocation_ids_snapshot() local outermost = inv_ids[1] or 0 -- For words emitted at the root atom body, `immediate_call_text` is nil and the walker's `word_call_text` (the word's own token, e.g. "nop") becomes the effective call_text. @@ -233,7 +231,7 @@ local function _project_emission_inner(root_body_entry, ctx_table) -- The call that triggered the body expansion we're currently walking. local eff_call_text = immediate_call_text or word_call_text local eff_root_call_text = root_call_text_w - local gpr_keys = nil + local gpr_keys = nil if reg_use_schema or sub_map then gpr_keys = {} for pos, arg in ipairs(args or {}) do @@ -268,17 +266,17 @@ local function _project_emission_inner(root_body_entry, ctx_table) if not reg_use_schema then gpr_keys = nil end - local isa = M.instr(encoder) - local isa_kind = isa and isa.kind or "unknown" + local isa = M.instr(encoder) + local isa_kind = isa and isa.kind or "unknown" local nop_words = (encoder == "nop" and 1) or (encoder == "nop2" and 2) or 0 - local is_yield = (encoder == "mac_yield" or encoder == "mac_yield_tail") + local is_yield = (encoder == "mac_yield" or encoder == "mac_yield_tail") local gp0_shape = type(encoder) == "string" and encoder:match("^mac_format_([%w_]+)_color$") or nil - local is_load = (isa_kind == "load") - local is_branch = (isa_kind == "branch") + local is_load = (isa_kind == "load") + local is_branch = (isa_kind == "branch") local is_unconditional_jump = (encoder == "jump" or encoder == "call_addr") - local is_terminal_jump = (encoder == "jump_reg" or encoder == "call_reg" or encoder == "jump_link") + local is_terminal_jump = (encoder == "jump_reg" or encoder == "call_reg" or encoder == "jump_link") items[#items + 1] = { kind = "word", encoder = encoder, @@ -335,14 +333,18 @@ local function _project_emission_inner(root_body_entry, ctx_table) -- `consuming_encoder` + `consuming_arg_pos` carry the surrounding control-transfer instruction context -- (e.g. `branch_le_zero` consuming its 3rd argument, or `jump` / `call_addr` consuming their only argument). -- `passes/offsets.lua` reads these to dispatch per-consuming-instruction offset encoding. - -- nil for top-level markers (where the marker is the entire token — no surrounding consuming instruction). + -- Offset markers require a real consuming encoder. A lone top-level `atom_offset` is not emitted. + -- Label and delay markers may have a nil encoder (they are not consumed as immediates). + if kind == "offset" and (consuming_encoder == nil or consuming_encoder == "") then + return + end local it = { - kind = kind, - name = name, - line = line, - word_index = word_idx, - invocation_ids = inv_ids, - outermost_invocation_id = outermost, + kind = kind, + name = name, + line = line, + word_index = word_idx, + invocation_ids = inv_ids, + outermost_invocation_id = outermost, } if target ~= nil then it.target = target end if consuming_encoder then it.consuming_encoder = consuming_encoder end @@ -450,15 +452,14 @@ local function _project_emission_inner(root_body_entry, ctx_table) -- Commit: label takes 1 arg, offset takes 2. -- For embedded markers, propagate the consuming_encoder + the marker's arg position -- (1-based) so `passes/offsets.lua` can dispatch per-consuming-instruction offset encoding. - -- Top-level markers (no consuming_encoder) get nil for both — the offsets pass treats - -- them as branch-equivalent for backward compatibility. + -- Offset markers are emitted only when a consuming encoder is present. local arg_pos = nil if consuming_encoder and consuming_paren then arg_pos = count_top_level_commas(tok, consuming_paren + 1, pos) + 1 end local args = split_call_args(inner) if ident == "atom_label" then emit_marker("label", args[1] or "", nil, tok_line, nil, nil, consuming_encoder, arg_pos) - else emit_marker("offset", args[1] or "", args[2] or "", tok_line, nil, nil, consuming_encoder, arg_pos) + elseif consuming_encoder then emit_marker("offset", args[1] or "", args[2] or "", tok_line, nil, nil, consuming_encoder, arg_pos) end pos = after_paren ::continue_loop:: @@ -545,7 +546,7 @@ local function _project_emission_inner(root_body_entry, ctx_table) local wc = ctx_table.word_counts if wc and wc[ident] then return wc[ident] end local canon = M.gte_canon(ident) - if canon ~= ident and wc and wc[canon] then return wc[canon] end + if canon ~= ident and wc and wc[canon] then return wc[canon] end warnings[#warnings + 1] = { kind = "uncounted", line = tok_line, @@ -572,6 +573,18 @@ local function _project_emission_inner(root_body_entry, ctx_table) -- "opaque word" emit handles direct encoders + mac_X-without-component. local function process_token(bt) local tok = M.trim(bt.tok or "") + -- Substituted MipsCode args can carry // comments from the call site. + while tok ~= "" do + if tok:sub(1, 2) == "//" then + local nl = tok:find("\n") + tok = M.trim(nl and tok:sub(nl + 1) or "") + elseif tok:sub(1, 2) == "/*" then + local close = tok:find("*/", 3, true) + tok = M.trim(close and tok:sub(close + 2) or "") + else + break + end + end if tok == "" then return end local ident, after = M.read_ident(tok, 1) if not ident then ident = "?" end @@ -582,10 +595,16 @@ local function _project_emission_inner(root_body_entry, ctx_table) local rest = tok:sub(after or (#tok + 1)) while true do rest = M.trim(rest) - if rest:sub(1, 2) ~= "/*" then break end - local close = rest:find("*/", 3, true) - if not close then rest = ""; break end - rest = rest:sub(close + 2) + if rest:sub(1, 2) == "//" then + local nl = rest:find("\n") + rest = nl and rest:sub(nl + 1) or "" + elseif rest:sub(1, 2) == "/*" then + local close = rest:find("*/", 3, true) + if not close then rest = ""; break end + rest = rest:sub(close + 2) + else + break + end end if rest ~= "" then process_token({ tok = rest, rel = bt.rel }) @@ -601,12 +620,19 @@ local function _project_emission_inner(root_body_entry, ctx_table) emit_embedded_markers(tok, tok_line, consuming_encoder_for_markers) end -- atom_label / atom_offset: terminal markers, no further descent. - -- Top-level markers (the marker IS the entire token) have no consuming instruction; - -- nil for both `consuming_encoder` and `consuming_arg_pos`. - -- The offsets pass treats these as branch-equivalent for backward compatibility. - -- TODO(Ed): Review this don't want legacy cruft here.. - if ident == "atom_label" then emit_marker("label", args[1] or "", nil, tok_line); return - elseif ident == "atom_offset" then emit_marker("offset", args[1] or "", args[2] or "", tok_line); return + -- A lone top-level atom_label is an anchor and is emitted with no consuming encoder. + -- A lone top-level atom_offset has no consuming encoder and is not emitted. + if ident == "atom_label" then emit_marker("label", args[1] or "", nil, tok_line); return + elseif ident == "atom_offset" then return + end + -- MipsCode formals (nop_slot1, …): the ident is a sub_map key. + -- Re-process the replacement token so load_word(...) becomes a real encoder. + if sub_map and type(sub_map[ident]) == "string" and sub_map[ident] ~= ident then + local repl = M.trim(sub_map[ident]) + if repl ~= "" then + process_token({ tok = repl, rel = bt.rel }) + return + end end if ident:sub(1, 4) == "mac_" then local bare = ident:sub(5) @@ -738,7 +764,8 @@ end --- * Metadata-backed N-word tokens (`nop2`, `mask_upper`, ...): N `word` items, all sharing the same encoder + word_count = 1. --- `nop2` is normalized to encoder `nop` (per the spec). --- * `atom_label(F)` markers: one `label` item with `name = "F"`, `word_index = current word_idx`; zero-width (does NOT advance word_idx). ---- * `atom_offset(B, T)` markers: one `offset` item with `name = "B"`, `target = "T"`, `word_index = current word_idx`; zero-width. +--- * `atom_offset(B, T)` nested in a consuming instruction: one `offset` item with `name = "B"`, `target = "T"`, `word_index = current word_idx`; zero-width. +--- A lone top-level `atom_offset` is not emitted. --- * Delay markers (`GteDelay_` / `LdSlot_` / `BdSlot_` / `DmaSlot_`): one `delay` item; zero-width. The following encoder is the next token. --- * `mac_X(...)` calls: emit `invoke_begin` (zero-width), recurse into the component body, emit `invoke_end` (zero-width). --- The component body's words land between the begin/end pair; one invocation record is allocated per call (monotonic ID per atom). diff --git a/scripts/elf_dwarf.lua b/scripts/elf_dwarf.lua index c17cc24..beeb260 100644 --- a/scripts/elf_dwarf.lua +++ b/scripts/elf_dwarf.lua @@ -13,7 +13,7 @@ local lfs = require("lfs") -- scripts/elf32.lua contains format-constant tables + the byte-level walker. -- The this file re-exports `read_u32_le` / `read_u16_le` (and the DWARF32 terminator). --- TODO(Ed): Remove re-export. +-- read_u32_le is this module's reader; implementation in elf32.lua. local E = require("elf32") local M = {} @@ -113,7 +113,6 @@ M.MIPS_BYTES_PER_WORD = 0x04 --- spec: DWARF4 spec §7.4 — 32-bit DWARF initial-length terminator M.dw_dwarf32_terminator = E.dw_dwarf32_terminator --- TODO(Ed): Remove re-export. -- ---------------------------------------------------------------------------- -- DWARF4 .debug_aranges (per DWARF5 spec §7.4 — Address Range Table) @@ -372,48 +371,70 @@ end -- For DW_FORM_strp we return the inline string resolved from `str_buf`. -- For DW_FORM_ref4 we return the absolute CU-relative offset. -- The caller decides whether to interpret that as a section offset. -local function read_form_value(buf, str_buf, pos, form) - if form == M.DW_FORM.addr then +local FORM_READERS = { + [M.DW_FORM.addr] = function(buf, _, pos) return M.read_u32_le(buf, pos), pos + 4 - elseif form == M.DW_FORM.string then + end, + [M.DW_FORM.string] = function(buf, _, pos) local s = read_c_string_at(buf, pos) return s, pos + #s + 1 - elseif form == M.DW_FORM.strp then + end, + [M.DW_FORM.strp] = function(buf, str_buf, pos) -- DW_FORM_strp: 4-byte offset into .debug_str. local strp_off = M.read_u32_le(buf, pos) return read_c_string_at(str_buf, strp_off), pos + 4 - elseif form == M.DW_FORM.udata then return M.read_uleb128_at(buf, pos) - elseif form == M.DW_FORM.data1 then return buf:byte(pos + 1), pos + 1 - elseif form == M.DW_FORM.data2 then return M.read_u16_le(buf, pos), pos + 2 - elseif form == M.DW_FORM.data4 then return M.read_u32_le(buf, pos), pos + 4 - elseif form == M.DW_FORM.ref4 then return M.read_u32_le(buf, pos), pos + 4 - elseif form == M.DW_FORM.sec_offset then + end, + [M.DW_FORM.udata] = function(buf, _, pos) + return M.read_uleb128_at(buf, pos) + end, + [M.DW_FORM.data1] = function(buf, _, pos) + return buf:byte(pos + 1), pos + 1 + end, + [M.DW_FORM.data2] = function(buf, _, pos) + return M.read_u16_le(buf, pos), pos + 2 + end, + [M.DW_FORM.data4] = function(buf, _, pos) + return M.read_u32_le(buf, pos), pos + 4 + end, + [M.DW_FORM.ref4] = function(buf, _, pos) + return M.read_u32_le(buf, pos), pos + 4 + end, + [M.DW_FORM.sec_offset] = function(buf, _, pos) -- DW_FORM_sec_offset: 4-byte offset (size depends on DWARF version; -- on DWARF5 32-bit it's always 4 bytes). return M.read_u32_le(buf, pos), pos + 4 - elseif form == M.DW_FORM.flag_present then + end, + [M.DW_FORM.flag_present] = function(_, _, pos) return 1, pos - elseif form == M.DW_FORM.exprloc then + end, + [M.DW_FORM.exprloc] = function(buf, _, pos) -- DW_FORM_exprloc: ULEB byte count + that many bytes of DW_OP_*. - local len, ne = M.read_uleb128_at(buf, pos) + local len, ne = M.read_uleb128_at(buf, pos) if not len then return nil, pos end return nil, ne + len - elseif form == DW_FORM_implicit_const then + end, + [DW_FORM_implicit_const] = function(_, _, pos) -- The constant is declared in the abbrev; no value bytes in the DIE. return nil, pos - elseif form == M.DW_FORM.ref_sig8 then + end, + [M.DW_FORM.ref_sig8] = function(buf, _, pos) -- DW_FORM_ref_sig8 (DWARF5 §7.4.2): An 8-byte value identifying a type by signature. -- The low 4 bytes (LE) are the type signature (content hash); -- The high 4 bytes (LE) are a CU-relative offset into the matching type unit. - -- Consumers use the low 4 to look up the type unit (see M.find_type_unit_by_signature) - -- then the high 4 to resolve the specific type within it. + -- Consumers use the low 4 to look up the type unit (see M.find_type_unit_by_signature) + -- then the high 4 to resolve the specific type within it. -- Return the low 4 as the primary value to preserve the (value, next_pos) shape; - -- the high 4 is exposed via M.read_ref_sig8 (which returns both halves). + -- the high 4 is exposed via M.read_ref_sig8 (which returns both halves). local _, _, next_pos = M.read_ref_sig8(buf, pos) return M.read_u32_le(buf, pos), next_pos - else + end, +} +local function read_form_value(buf, str_buf, pos, form) + local r = FORM_READERS[form] + if not r then return nil, pos end + return r(buf, str_buf, pos) end --- Read a `DW_FORM_ref_sig8` value at 0-based offset `pos` from `buf`. @@ -426,9 +447,7 @@ end --- @return integer -- low 4 bytes (LE), the type signature --- @return integer -- high 4 bytes (LE), the offset within the matching type unit --- @return integer -- cursor after the 8-byte value -function M.read_ref_sig8(buf, pos) - return M.read_u32_le(buf, pos), M.read_u32_le(buf, pos + 4), pos + 8 -end +function M.read_ref_sig8(buf, pos) return M.read_u32_le(buf, pos), M.read_u32_le(buf, pos + 4), pos + 8 end --- DWARF5 §7.5.6 (Type Entries). --- Walk all units in `info` and return the 0-based offset of the first unit whose `DW_AT_type_signature` @@ -459,23 +478,23 @@ function M.find_type_unit_by_signature(info, target_sig_lo, target_sig_hi) return nil, nil -- malformed end -- Per DWARF5 §7.5.6, the type_unit (DW_UT_type = 0x02) body layout is: - -- 0: version (2) - -- 2: unit_type (1) -- DW_UT_type = 0x02 - -- 3: address_size (1) + -- 0: version (2) + -- 2: unit_type (1) -- DW_UT_type = 0x02 + -- 3: address_size (1) -- 4: debug_abbrev_offset (4) - -- 8: type_signature (8) - -- 16: type_offset (4) + -- 8: type_signature (8) + -- 16: type_offset (4) -- 20: if body_end - body_start >= 20 then -- read_ref_sig8 / write_u32_le / etc. are 1-indexed (string:byte); -- pos / body_start / body_end are 0-based wire offsets, so the 1-indexed byte at 0-based wire offset X is string:byte(X + 1). -- Per DWARF5 §7.5.6, the type_unit body is laid out as: - -- byte 0-1: version (2) - -- byte 2: unit_type (1) -- DW_UT_type = 0x02 - -- byte 3: address_size (1) + -- byte 0-1: version (2) + -- byte 2: unit_type (1) -- DW_UT_type = 0x02 + -- byte 3: address_size (1) -- byte 4-7: debug_abbrev_offset (4) - -- byte 8-15: type_signature (8) - -- byte 16-19: type_offset (4) + -- byte 8-15: type_signature (8) + -- byte 16-19: type_offset (4) local unit_type = info:byte(body_start + 2 + 1) -- 0-based +2 = unit_type in 1-indexed if unit_type == 0x02 then -- DW_UT_type local sig_lo, sig_hi, _ = M.read_ref_sig8(info, body_start + 8) -- 0-based +8 = type_signature in 1-indexed @@ -626,14 +645,10 @@ function M.read_nm(elf_path) local addrs = {} -- Existence check first; an empty or missing ELF returns an empty map. - if lfs.attributes(elf_path, "mode") ~= "file" then - return addrs - end + if lfs.attributes(elf_path, "mode") ~= "file" then return addrs end local f = io.open(elf_path, "rb") - if not f then - return addrs - end + if not f then return addrs end -- Build the file adapter for E.*. local file_size @@ -775,7 +790,7 @@ end --- @return string function M.sleb128(n) local bytes = {} - local more = true + local more = true while more do local b = n % (LEB_DATA_MASK + 1) -- extract low 7 bits n = (n - b) / (LEB_DATA_MASK + 1) -- arithmetic shift right by 7 diff --git a/scripts/passes/annotation.lua b/scripts/passes/annotation.lua index 73b4396..8f2bba9 100644 --- a/scripts/passes/annotation.lua +++ b/scripts/passes/annotation.lua @@ -45,14 +45,12 @@ local duffle = dofile(_bootstrap_dir .. "../duffle_paths.lua") --- @field warnings table[] --- @class AtomAnnotation ---- @field line integer -- Source line of the atom_info call ---- @field macro string -- Macro name (always "atom_info" in the new shape) ---- @field name string -- Atom name ---- @field kind string -- Always "info" ---- @field binds string|nil -- Binds_X name if any ---- @field reads string[] -- R_* names (read targets) ---- @field writes string[] -- R_* names (write targets) ---- @field errors string[]|nil -- Parse-time errors from scan_source (atom_info body malformed) +--- @field atom_name string -- Atom name (scan.atom_infos row) +--- @field info_line integer -- Source line of the atom_info call +--- @field binds string|nil -- Binds_X name if any +--- @field reads string[] -- R_* names (read targets) +--- @field writes string[] -- R_* names (write targets) +--- @field errors string[]|nil -- Parse-time errors from scan_source (atom_info body malformed) --- @class DebugSkipMarker -- Sub-shape of scan_source.lua's @class DebugSkipMarker --- @field marker_kind string -- Exact marker ident read from source. Only "atom_dbg_skip" (bare) is positive. @@ -74,7 +72,7 @@ local duffle = dofile(_bootstrap_dir .. "../duffle_paths.lua") --- @field info Finding[] --- @class PipeCtx ---- @field atom_index table -- Name -> AtomAnnotation (only kind=="atom") +--- @field atom_index table -- raw_name or name -> scan.atoms row (kind atom/atom_proc) --- @field binds_index table -- Name -> BindsStruct --- @field annot_counts table -- Name -> annotation count (for unique_annotation check) --- @field types table -- From scan_source @@ -99,14 +97,14 @@ local duffle = dofile(_bootstrap_dir .. "../duffle_paths.lua") --- `macro_word_drift` writes errors[] for missing or mismatched metadata and info[] for a match. --- Check: Every annotated atom must have a matching MipsAtom_(name) declaration. ---- @param a AtomAnnotation +--- @param info AtomAnnotation --- @param pipe_ctx PipeCtx --- @param findings Findings -local function check_atom_decl_exists(a, pipe_ctx, findings) - if not pipe_ctx.atom_index[a.name] then +local function check_atom_decl_exists(info, pipe_ctx, findings) + if not pipe_ctx.atom_index[info.atom_name] then findings.errors[#findings.errors + 1] = { - line = a.line, - msg = string.format("annotation for '%s' has no matching MipsAtom_(%s) { ... }", a.name, a.name), + line = info.info_line, + msg = string.format("annotation for '%s' has no matching MipsAtom_(%s) { ... }", info.atom_name, info.atom_name), } end end @@ -128,17 +126,17 @@ end --- Check: BIND atoms must reference a real Binds_* struct. --- I keep this as a warning so the annotation pass can report the common test-fixture case; `check_abi_handoff` in static analysis supplies the build-stopping error. ---- @param a AtomAnnotation +--- @param info AtomAnnotation --- @param pipe_ctx PipeCtx --- @param findings Findings -local function check_binds_struct_exists(a, pipe_ctx, findings) - if not a.binds then return end - if pipe_ctx.binds_index[a.binds] then return end +local function check_binds_struct_exists(info, pipe_ctx, findings) + if not info.binds then return end + if pipe_ctx.binds_index[info.binds] then return end findings.warnings[#findings.warnings + 1] = { - line = a.line, + line = info.info_line, msg = string.format("'%s' binds '%s' but no Struct_(%s) { ... } " .. "declaration found (also flagged as an error by check_abi_handoff in the static-analysis pass)" - , a.name, a.binds, a.binds), + , info.atom_name, info.binds, info.binds), } end @@ -315,7 +313,7 @@ end --- 6. unsupported target_kind -> marker precedes an unrelated declaration --- Valid markers stamp `debug_skip` on whole-atom, bare-component, and proc-component declaration records in scan_source.lua. --- @param marker DebugSkipMarker ---- @param _pipe_ctx PipeCtx -- Unused; kept for consistency with per_annot // TODO(Ed): Remove? +--- @param _pipe_ctx PipeCtx -- Unused; kept for consistency with per_annot --- @param findings Findings local function check_skip_marker(marker, _pipe_ctx, findings) local kind = marker.marker_kind @@ -400,7 +398,7 @@ end -- ════════════════════════════════════════════════════════════════════════════ -- -- Each rule entry picks one of four "shapes" of dispatch: --- per_annot(annot, pipe_ctx, findings) -- runs once per AtomAnnotation +-- per_annot(info, pipe_ctx, findings) -- runs once per scan.atom_infos row -- post(pipe_ctx, findings) -- runs once after all per_annot calls complete (full-corpus aggregation) -- per_macro(macro, wc, findings) -- runs once per TAPE_WORDS / _Pragma macro declaration -- per_skip_marker(marker, pipe_ctx, findings) -- runs once per src.scan.debug_skip_markers entry @@ -430,7 +428,7 @@ local CHECK_RULES = { --- @param ctx PassCtx --- @return PipeCtx local function build_corpus_pipe_ctx(ctx) - local view = duffle.corpus_view(ctx) + local view = duffle.corpus_view(ctx) local annot_counts = {} for _, info in ipairs(view.atom_infos) do if info and info.atom_name then @@ -452,29 +450,6 @@ 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. - local atoms = {} - for _, a in ipairs(scan.atoms) do - if a.kind == "atom" or a.kind == "atom_proc" then - atoms[#atoms + 1] = { line = a.line, name = a.raw_name or a.name } - end - end - - -- Project the pre-scanned atom_infos to AtomAnnotation shape. - local annots = {} - for _, info in ipairs(scan.atom_infos) do - annots[#annots + 1] = { - line = info.info_line, - macro = "atom_info", - name = info.atom_name, - kind = "info", - binds = info.binds, - reads = info.reads or {}, - writes = info.writes or {}, - errors = info.errors, - } - end - -- Build a per-source pipe_ctx: shared lookups come from `corpus_pipe_ctx`, while declarations, bodies, types, views, defaults, and occurrences come from `src.scan`. local seen_defaults = {}; for reg, _ in pairs (scan.types or {}) do seen_defaults[reg] = (seen_defaults[reg] or 0) + 1 end local atom_infos_list = {}; for _, ai in ipairs(scan.atom_infos or {}) do atom_infos_list[#atom_infos_list + 1] = ai end @@ -493,7 +468,13 @@ local function validate(ctx, src, 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 + local atoms = {} + for _, a in ipairs(scan.atoms) do + if a.kind == "atom" or a.kind == "atom_proc" then + atoms[#atoms + 1] = a + pipe_ctx.atom_index[a.raw_name or a.name] = a + end + end for _, b in ipairs(scan.binds) do pipe_ctx.binds_index[b.name] = b end -- Findings live in a single struct with three lists (errors / warnings / info). @@ -501,20 +482,20 @@ local function validate(ctx, src, corpus_pipe_ctx) local findings = { errors = {}, warnings = {}, info = {} } -- Lift parse-time errors already recorded in scan_source's atom_info payload into this pass's findings list. - for _, a in ipairs(annots) do - if a.errors then - for _, msg in ipairs(a.errors) do + for _, info in ipairs(scan.atom_infos) do + if info.errors then + for _, msg in ipairs(info.errors) do findings.errors[#findings.errors + 1] = { - line = a.line, - msg = string.format("'%s': %s", a.name, msg), + line = info.info_line, + msg = string.format("'%s': %s", info.atom_name, msg), } end end end -- THE per-annotation pipeline. ONE loop. CHECK_RULES dispatches per_annot rules. - for _, a in ipairs(annots) do - duffle.run_check_rules(CHECK_RULES, "per_annot", a, pipe_ctx, findings) + for _, info in ipairs(scan.atom_infos) do + duffle.run_check_rules(CHECK_RULES, "per_annot", info, pipe_ctx, findings) end -- Post-loop rules (one-shot checks that need full-corpus aggregation in pipe_ctx). @@ -541,12 +522,12 @@ local function validate(ctx, src, corpus_pipe_ctx) 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), + , #atoms, #scan.atom_infos, #scan.macros, #scan.binds), } return { atoms = atoms, - annots = annots, + annots = scan.atom_infos, macros = scan.macros, binds = scan.binds, errors = findings.errors, @@ -576,7 +557,7 @@ function M.run(ctx) -- Build the shared pipe_ctx once for this run; every validate() call sees the same cross-source registries. -- The corpus owns the canonical cross-source registries; per-source scans retain body / declaration ownership. local corpus_pipe_ctx = build_corpus_pipe_ctx(ctx) - local corpus = ctx.shared.corpus + local corpus = ctx.shared.corpus -- Group `corpus.sources_by_dir` by module, validate every source in each bucket, and emit one errors.h per directory. local by_dir = (corpus and corpus.sources_by_dir) or {} diff --git a/scripts/passes/atoms_source_map.lua b/scripts/passes/atoms_source_map.lua index 92f2d8b..d1246a4 100644 --- a/scripts/passes/atoms_source_map.lua +++ b/scripts/passes/atoms_source_map.lua @@ -526,13 +526,11 @@ function M.render_atom_provenance(atom, wc, rel_path) local lines = {} lines[#lines + 1] = string.format("ATOM %s %d", (atom.raw_name or atom.name), total) for _, entry in ipairs(entries) do - local inv = entry.invocation + local inv = entry.invocation local macro_count = inv and wc 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) + 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", entry.pos, rel_path, entry.line) diff --git a/scripts/passes/auto_reg.lua b/scripts/passes/auto_reg.lua index b1fb482..6ac304d 100644 --- a/scripts/passes/auto_reg.lua +++ b/scripts/passes/auto_reg.lua @@ -9,8 +9,7 @@ --- These GPRs are unavailable to EVERY atom's source pool. --- Carriers are preserved across atoms by context discipline and must never be reallocated. --- Per-atom body parsing also catches alias references (R_) and hardcoded R_Tn references, ---- so the user can write either `R_T4` or `R_ResolveScratch` in an atom body and the pass will ---- exclude R_T4 from that atom's pool. +--- so the user can write either `R_T4` or `R_ResolveScratch` in an atom body and the pass will exclude R_T4 from that atom's pool. --- --- Conflict detection: If the user hardcodes `R_Tn` in an atom body that shares a phase with an auto-reg that picked `R_Tn`, --- emit `phase_register_clash` as an info finding (no build stop). @@ -27,30 +26,26 @@ local _bootstrap_dir = debug.getinfo(1, "S").source:match("^@?(.*[/\\])") or "./" local duffle = dofile(_bootstrap_dir .. "../duffle_paths.lua") --- ════════════════════════════════════════════════════════════════════════════ --- THE GPR ALLOCATION POOL — what's allocatable, and (more importantly) WHY --- ════════════════════════════════════════════════════════════════════════════ --- --- The auto-reg pass picks physical GPRs for `atom_auto_reg(...)` / `phase_auto_reg(...)` markers. --- The 24-register pool covers R2-R25 (the user/atom allocatable surface): --- R_T0..R_T7, R_V0..R_V1, R_A0..A3, R_S0..S7, R_T8..T9. --- Excluded (and never added to the pool): --- R_0 (code 0) — hardwired zero. Cannot be written. --- R_AT (code 1) — assembler temporary. Reserved by the MIPS O32 ABI. --- R_A0..A3 — explicitly omitted above even though their integer codes --- map to POOL entries; the pool-construction loop below --- only references the POOL string literals, never the --- integer codes, so they are NOT auto-allocated by default. --- (A0-A3 become available when the user adds them to --- POOL or hardcodes an R_A0 reference in the atom body.) --- R_K0/K1 (codes 26-27) — kernel / interrupt handler reserves. Never touched by user code. --- R_GP/SP/FP/RA (codes 28-31) — R_SP/R_FP/R_RA are tape-runtime carriers between --- tape_enter and tape_exit; R_GP stays the host global pointer. +--- ════════════════════════════════════════════════════════════════════════════ +--- THE GPR ALLOCATION POOL — what's allocatable, and (more importantly) WHY +--- ════════════════════════════════════════════════════════════════════════════ +--- +--- The auto-reg pass picks physical GPRs for `atom_auto_reg(...)` / `phase_auto_reg(...)` markers. +--- The 24-register pool covers R2-R25 (the user/atom allocatable surface): +--- R_T0..R_T7, R_V0..R_V1, R_A0..A3, R_S0..S7, R_T8..T9. +--- Excluded (and never added to the pool): +--- R_0 (code 0) — Hardwired zero. Cannot be written. +--- R_AT (code 1) — Assembler temporary. Reserved by the MIPS O32 ABI. +--- R_A0..A3 — Explicitly omitted above even though their integer codes map to POOL entries; +--- the pool-construction loop below only references the POOL string literals, never the integer codes, so they are NOT auto-allocated by default. +--- (A0-A3 become available when the user adds them to POOL or hardcodes an R_A0 reference in the atom body.) +--- R_K0/K1 (codes 26-27) — Kernel / interrupt handler reserves. Never touched by user code. +--- R_GP/SP/FP/RA (codes 28-31) — R_SP/R_FP/R_RA are tape-runtime carriers between tape_enter and tape_exit; R_GP stays the host global pointer. --- local POOL = { + "R_V0", "R_V1", "R_T0", "R_T1", "R_T2", "R_T3", "R_T4", "R_T5", "R_T6", "R_T7", - "R_V0", "R_V1", "R_A0", "R_A1", "R_A2", "R_A3", "R_S0", "R_S1", "R_S2", "R_S3", "R_S4", "R_S5", "R_S6", "R_S7", @@ -131,13 +126,13 @@ local function build_user_pins(corpus) return user_pinned, alias_to_gpr end --- Find every physical GPR referenced in the atom body, via EITHER: --- (a) A hardcoded physical GPR ident (R_T\d+|R_V\d+|R_A\d+|R_S\d+) — the existing regex; --- (b) An alias ident (R_) resolved via alias_to_gpr back to its physical GPR ident. --- Returns { [physical_gpr_ident] = count }. Clash-detection and source-pool-exclusion logic --- only needs the presence of each GPR (boolean test), but keeping count preserves the --- original find_hardcoded_rn shape so callers can switch without churn. --- The alias pattern is sorted lexicographically to keep the regex deterministic. +--- Find every physical GPR referenced in the atom body, via EITHER: +--- (a) A hardcoded physical GPR ident (R_T\d+|R_V\d+|R_A\d+|R_S\d+) — the existing regex; +--- (b) An alias ident (R_) resolved via alias_to_gpr back to its physical GPR ident. +--- Returns { [physical_gpr_ident] = count }. +--- Clash-detection and source-pool-exclusion logic only needs the presence of each GPR (boolean test), +--- but keeping count preserves the original find_hardcoded_rn shape so callers can switch without churn. +--- The alias pattern is sorted lexicographically to keep the regex deterministic. local function find_used_gprs(body_text, alias_to_gpr) local found = {} -- (a) Hardcoded physical GPRs (R_T0..R_T7, R_V0..R_V1, R_A0..R_A3, R_S0..R_S7). @@ -265,8 +260,7 @@ function M.run(ctx) end local source_pool = {} for _, gpr in ipairs(POOL) do - -- Exclude (a) prior commitments, (b) USER-PINNED GPRs (wave-context carriers - -- declared via atom_reg + _Code defs, preserved across atoms globally). + -- Exclude (a) prior commitments, (b) USER-PINNED GPRs (wave-context carriers declared via atom_reg + _Code defs, preserved across atoms globally). if not used[gpr] and not user_pinned[gpr] then source_pool[#source_pool + 1] = gpr end @@ -297,7 +291,7 @@ function M.run(ctx) -- For each resolved (scope, sym) -> R_Tn mapping, scan the atom body source for used GPRs. for atom_scope, decls in pairs(atom_allocations) do local atom = corpus.atoms_by_name and corpus.atoms_by_name[atom_scope] - if atom and atom.body then + if atom and atom.body then local used_in_body = find_used_gprs(atom.body, alias_to_gpr) for sym, allocated_gpr in pairs(decls) do if used_in_body[allocated_gpr] and used_in_body[allocated_gpr] > 0 then diff --git a/scripts/passes/components.lua b/scripts/passes/components.lua index 7730138..3c0cd4d 100644 --- a/scripts/passes/components.lua +++ b/scripts/passes/components.lua @@ -100,9 +100,8 @@ local M = {} --- Returns the args string (e.g., `"U4 off, U4 code, U1 r, U1 g, U1 b"`) or nil if no function declaration is found. --- --- After the `sym` arg was dropped from MipsAtomComp_Proc_, the component name ---- and the args both come from the preceding `FI_ Slice_MipsCode ac_X(args)` ---- declaration. The shared `duffle.find_function_decl_for` helper does the ---- backward walk; this function returns just the args. +--- and the args both come from the preceding `FI_ Slice_MipsCode ac_X(args)` declaration. +--- The shared `duffle.find_function_decl_for` helper does the backward walk; this function returns just the args. --- --- @param source string --- @param name string (retained for signature stability; unused — the walk derives the name) @@ -336,11 +335,11 @@ end --- @param tok string --- @return string local function strip_leading_delay_marker(tok) - local ident = duffle.read_ident(tok, 1) + local ident = duffle.read_ident(tok, 1) if not ident or not duffle.DELAY_MARKERS[ident] then return tok end local rest = tok:sub(#ident + 1):match("^%s*(.*)$") or "" while rest:sub(1, 2) == "/*" do - local close = rest:find("*/", 3, true) + local close = rest:find("*/", 3, true) if not close then return "" end rest = rest:sub(close + 2):match("^%s*(.*)$") or "" end @@ -428,115 +427,87 @@ end -- Always walk the original `MipsAtomComp_` body via `cc.body_tokens`. -- ═══════════════════════════════════════════ ---- (internal) Recursive cycle-cost derivation. Sum `latency[ident]` per emitted instruction in the component body, ---- recursing through nested `mac_*` calls (so `mac_format_g4_color`'s cost = 4 × `mac_pack_color_word`'s cost). ---- Special rule: `mac_yield`'s cost = 0 (per `lottes_tape.h:125-130` "the runtime cost lands in the next atom's prologue"). +--- (internal) One walk of a component body that fills both `cycle_cost` and `gp0_contrib`. +--- Cycle: sum `isa.cycles` / `gte.cycles` / `latency[ident]` / 1 per leaf, recurse `mac_*`. +--- `mac_yield` cycle_cost is 0 (runtime cost lands in the next atom's prologue); its gp0 still comes from the token walk. +--- GP0: count `gte_sw` and `store_word` / `store_half` / `store_byte` that target `R_PrimCursor` / `O_(Poly_` / `r_prim_cursor` / `r_primitive_cursor` / `r_base`. +--- `insert_ot_tag*` gp0_contrib is 0; cycle still comes from the body walk. +--- Missing component: cycle 1, gp0 0. --- @param name string -- component bare name (e.g. "yield", "pack_color_word") --- @param comp_by_name table --- @param latency table ---- @param cache table -- shared memoization; `-1` sentinel detects cycles ---- @return integer -local function cycle_cost_rec(name, comp_by_name, latency, cache) +--- @param cache table +--- @return {cycle_cost=integer, gp0_contrib=integer} +local function component_meta_rec(name, comp_by_name, latency, cache) if cache[name] ~= nil then return cache[name] end - cache[name] = -1 + cache[name] = { cycle_cost = -1, gp0_contrib = -1 } local cc = comp_by_name[name] - local n + local cycle_cost + local gp0_contrib if cc then - if name == "yield" then - -- mac_yield's cost is 0 by convention (the runtime cost lands in the next atom's prologue). - n = 0 - else - n = 0 + local skip_cycle = (name == "yield") + local skip_gp0 = name:match("^insert_ot_tag") ~= nil + cycle_cost = 0 + gp0_contrib = 0 + if not skip_cycle or not skip_gp0 then local tokens = cc.body_tokens for _, t in ipairs(tokens) do local trimmed = t.tok if trimmed ~= "" then local ident = duffle.read_ident(trimmed, 1) - if ident and ident:sub(1, MAC_PREFIX_LEN) == MAC_PREFIX then - -- Nested `mac_X(...)` call: recurse. + if ident and ident:sub(1, MAC_PREFIX_LEN) == MAC_PREFIX then local nested = ident:sub(MAC_PREFIX_LEN + 1) - n = n + cycle_cost_rec(nested, comp_by_name, latency, cache) + local nested_meta = component_meta_rec(nested, comp_by_name, latency, cache) + if not skip_cycle then + cycle_cost = cycle_cost + nested_meta.cycle_cost + end + if not skip_gp0 then + gp0_contrib = gp0_contrib + nested_meta.gp0_contrib + end else - -- Leaf instruction or pseudo-macro. - local isa = duffle.instr(ident) - local gte = duffle.gte(ident) - n = n + ((isa and isa.cycles) or (gte and gte.cycles) or latency[ident] or 1) + if not skip_cycle then + local isa = duffle.instr(ident) + local gte = duffle.gte(ident) + cycle_cost = cycle_cost + ((isa and isa.cycles) or (gte and gte.cycles) or latency[ident] or 1) + end + if not skip_gp0 then + if ident == "gte_sw" then + gp0_contrib = gp0_contrib + 1 + elseif ident == "store_word" or ident == "store_half" or ident == "store_byte" then + if trimmed:find("R_PrimCursor", 1, true) + or trimmed:find("O_(Poly_", 1, true) + or trimmed:find("r_prim_cursor", 1, true) + or trimmed:find("r_primitive_cursor", 1, true) + or trimmed:find("r_base", 1, true) + then + gp0_contrib = gp0_contrib + 1 + end + end + end end end end end else - n = 1 + cycle_cost = 1 + gp0_contrib = 0 end - cache[name] = n - return n -end - ---- (internal) Recursive GP0 prim-buffer contribution. Count `store_word` / `store_half` / `store_byte` ---- calls in the component body that target `R_PrimCursor` (these are the RAM-side prim-buffer words the macro contributes), recursing through nested `mac_*` calls. ---- Only `R_PrimCursor`-targeting stores count. Stores targeting other registers (e.g. `R_OtBase`, heap pointers) are not prim-buffer contributions. ---- @param name string ---- @param comp_by_name table ---- @param cache table ---- @return integer -local function gp0_contrib_rec(name, comp_by_name, cache) - if name:match("^insert_ot_tag") then - cache[name] = 0 - return 0 - end - if cache[name] ~= nil then return cache[name] end - cache[name] = -1 - local cc = comp_by_name[name] - local n - if cc then - n = 0 - local tokens = cc.body_tokens - for _, t in ipairs(tokens) do - local trimmed = t.tok - if trimmed ~= "" then - local ident = duffle.read_ident(trimmed, 1) - if ident and ident:sub(1, MAC_PREFIX_LEN) == MAC_PREFIX then - -- Nested `mac_X(...)` call: recurse. - local nested = ident:sub(MAC_PREFIX_LEN + 1) - n = n + gp0_contrib_rec(nested, comp_by_name, cache) - elseif ident == "gte_sw" then - n = n + 1 - elseif ident == "store_word" or ident == "store_half" or ident == "store_byte" then - if trimmed:find("R_PrimCursor", 1, true) - or trimmed:find("O_(Poly_", 1, true) - or trimmed:find("r_prim_cursor", 1, true) - or trimmed:find("r_primitive_cursor", 1, true) - or trimmed:find("r_base", 1, true) - then - n = n + 1 - end - end - end - end - else - n = 0 - end - cache[name] = n - return n + cache[name] = { cycle_cost = cycle_cost, gp0_contrib = gp0_contrib } + return cache[name] end --- Compute `cycle_cost` + `gp0_contrib` for every component in `components` in a single pass. ---- Memoization cache is built ONCE (per source) and shared across both helpers so that ---- a nested `mac_Y` reference inside a `mac_X` body computes its values once. +--- One memoization cache; a nested `mac_Y` inside a `mac_X` body computes both fields once. --- @param components Component[] --- @param latency table --- @return table local function compute_components_metadata(components, latency) local comp_by_name = {} for _, cc in ipairs(components) do comp_by_name[cc.name] = cc end - local cc_cache = {} - local gc_cache = {} - local out = {} + local cache = {} + local out = {} for _, c in ipairs(components) do - out[c.name] = { - cycle_cost = cycle_cost_rec(c.name, comp_by_name, latency, cc_cache), - gp0_contrib = gp0_contrib_rec(c.name, comp_by_name, gc_cache), - } + out[c.name] = component_meta_rec(c.name, comp_by_name, latency, cache) end return out end @@ -589,29 +560,23 @@ local function strip_trailing_continuation(lines) end end ---- Classify a token as a "pure delay marker token" (a delay-marker identifier ---- with no following instruction — only whitespace and/or block comments). +--- Classify a token as a "pure delay marker token" (a delay-marker identifier with no following instruction — only whitespace and/or block comments). --- Examples that match: --- * `GteDelay_` → marker alone --- * `GteDelay_ /* RT diagonal: D1 = a.x... */` → marker + block comment --- * `GteDelay_ /* RT diagonal: ... */\n\t` → marker + comment + trailing whitespace ---- Examples that DO NOT match (these contain a real instruction after the marker ---- and must be preserved verbatim so the instruction still gets emitted): +--- Examples that DO NOT match (these contain a real instruction after the marker and must be preserved verbatim so the instruction still gets emitted): --- * `GteDelay_ nop2` --- * `GteDelay_ add_si(r.dst_ptr, r.scratch, dst_offset)` --- ---- Why this classification matters: the metaprogram emits tokens separated by `,` ---- and joins them with `\` line continuations. After C preprocessor +--- Why this classification matters: the metaprogram emits tokens separated by `,` and joins them with `\` line continuations. After C preprocessor --- phase 2 (line splicing), the macro body collapses to a single logical line. ---- Each delay-marker identifier expands to empty (its definition ---- `#define GteDelay_ // ...` consumes the `//` line comment during preprocessing ---- of the definition itself, leaving an empty replacement list). When a token ---- is purely a delay marker with only a trailing comment, the `,` the metaprogram ---- normally adds before each token-after-the-first brackets empty content and ---- produces the syntax error `,,` (`expected expression before ',' token`) at ---- C compile. The metaprogram therefore emits such tokens WITHOUT the leading ---- `,` (see `token_skips_leading_comma`) — but the marker + trailing comment ---- are still emitted verbatim so the annotation is preserved in `gen/macs.h`. +--- Each delay-marker identifier expands to empty (its definition `#define GteDelay_ // ...` consumes the `//` line comment during preprocessing +--- of the definition itself, leaving an empty replacement list). +--- When a token is purely a delay marker with only a trailing comment, the `,` the metaprogram normally adds before +--- each token-after-the-first brackets empty content and produces the syntax error `,,` (`expected expression before ',' token`) at C compile. +--- The metaprogram therefore emits such tokens WITHOUT the leading `,` (see `token_skips_leading_comma`) — +--- but the marker + trailing comment are still emitted verbatim so the annotation is preserved in `gen/macs.h`. --- @param tok string -- a single token from split_top_level_commas (already trimmed at the start, may contain trailing whitespace + block comment) --- @return boolean local function is_pure_delay_marker_token(tok) @@ -655,17 +620,14 @@ end --- followed by whitespace + optional block comment and NOTHING ELSE) expand --- to empty at C preprocessor time. Emitting them WITHOUT the leading `,` --- separator that the metaprogram normally adds before each token after the ---- first keeps exactly one `,` between the surrounding real expressions in ---- the spliced macro body: ---- +--- first keeps exactly one `,` between the surrounding real expressions in the spliced macro body: --- * before this rule: ` ,\t ,\t` → after expansion --- ` , /* comment */ , ` → `,,` syntax error. --- * after this rule: ` \t ,\t` → after expansion --- ` /* comment */ , ` → `, ` — valid. --- ---- Tokens like `GteDelay_ nop2` keep the leading `,` (the marker is followed ---- by a real instruction, so the marker + instruction together need the ---- separator on the LEFT to land between two real expressions). +--- Tokens like `GteDelay_ nop2` keep the leading `,` +--- (the marker is followed by a real instruction, so the marker + instruction together need the separator on the LEFT to land between two real expressions). --- @param tok string --- @return boolean -- true if the token needs NO leading `,` separator. local function token_skips_leading_comma(tok) @@ -675,7 +637,10 @@ end --- Emit the `#define mac_X(sig) \\t \,\t ...` block. --- Converts `//` line comments to `/* */` block comments in each token so they don't break the C macro `\` line continuations. --- ---- Pure delay-marker tokens (`GteDelay_` / `LdSlot_` / `BdSlot_` / `DmaSlot_` with only a trailing block comment, no real instruction) are emitted WITHOUT a leading `,` separator; the annotation IS preserved in the generated header (so the comment + marker remain visible to anyone reading `gen/macs.h`), but the C preprocessor expands the marker to empty, so leaving the `,` separator out is what stops the `,,` syntax error. See `token_skips_leading_comma` for the contract. +--- Pure delay-marker tokens (`GteDelay_` / `LdSlot_` / `BdSlot_` / `DmaSlot_` with only a trailing block comment, no real instruction) are emitted WITHOUT a leading `,` separator; +--- the annotation IS preserved in the generated header +--- (so the comment + marker remain visible to anyone reading `gen/macs.h`), but the C preprocessor expands the marker to empty, so leaving the `,` +--- separator out is what stops the `,,` syntax error. See `token_skips_leading_comma` for the contract. local function emit_macro_body(lines, c, sig, tokens) for tok_idx = 1, #tokens do tokens[tok_idx] = convert_line_comments_to_block(tokens[tok_idx]) diff --git a/scripts/passes/dwarf_injection.lua b/scripts/passes/dwarf_injection.lua index bbca81f..c68bde6 100644 --- a/scripts/passes/dwarf_injection.lua +++ b/scripts/passes/dwarf_injection.lua @@ -1661,24 +1661,24 @@ local function build_inserted_children(main_cu_offset, main_cu_end_excl, atom_ta S.bytes[#S.bytes + 1] = s S.next_offset = S.next_offset + #s end + local FORM_WRITERS = { + string = function(emit, v) emit(v .. "\0") end, + data1 = function(emit, v) emit(string.char(v)) end, + udata = function(emit, v) emit(uleb128(v)) end, + addr = function(emit, v) emit(elf_dwarf.write_u32_le(v)) end, + ref4 = function(emit, v) emit(elf_dwarf.write_u32_le(v)) end, + sec_offset = function(emit, v) emit(elf_dwarf.write_u32_le(v)) end, + data2 = function(emit, v) emit(elf_dwarf.write_u16_le(v)) end, + exprloc = function(emit, v) emit(v) end, + } local function emit_die(schema_name, values) local row = DIE_SCHEMA[schema_name] emit(uleb128(row.abbrev)) for _, attr in ipairs(row.attrs) do local v = values[attr.key] - if attr.form == "string" then - emit(v .. "\0") - elseif attr.form == "data1" then - emit(string.char(v)) - elseif attr.form == "udata" then - emit(uleb128(v)) - elseif attr.form == "addr" or attr.form == "ref4" or attr.form == "sec_offset" then - emit(elf_dwarf.write_u32_le(v)) - elseif attr.form == "data2" then - emit(elf_dwarf.write_u16_le(v)) - elseif attr.form == "exprloc" then - emit(v) - end + local w = FORM_WRITERS[attr.form] + if not w then error("emit_die: unknown form " .. tostring(attr.form)) end + w(emit, v) end end local function ref4_of(section_offset) @@ -1737,55 +1737,35 @@ local function build_inserted_children(main_cu_offset, main_cu_end_excl, atom_ta -- then a single DW_TAG_pointer_type pointing at the structure_type. gdb's -- `print *` then expands the struct and lists its members (x, y, z, w for V4_S2; etc.). -- - -- Hardcoded member-info table for the 6 typed views the prototype uses (V2_S2 / V3_S2 / V4_S2 / V2_S4 / V3_S4 / V4_S4). - -- Each row is {byte_size, members}, where each member is {name, offset, byte_size, type_name}. - -- `type_name` is the base type name from `type_name_registry`; "S2" / "S4" need a signed base_type emit too. - -- - -- The table is small + explicit — the prototype principle treats the typed-view struct layout as data, not derived state. - local STRUCT_MEMBER_TABLE = { - -- TODO(Ed): This hardcoding is brittle... - -- TODO(Ed): Better to just have a table for the fundamental types in duffle/dsl.h, we can derive the rest via typedef parsing... - -- 2-element signed short vector (rare; placeholder for future use). - V2_S2 = { byte_size = 4, members = { - { name = "x", offset = 0, byte_size = 2 }, - { name = "y", offset = 2, byte_size = 2 }, - }}, - -- 4-element signed short vector (the dominant face-cursor type). - V4_S2 = { byte_size = 8, members = { - { name = "x", offset = 0, byte_size = 2 }, - { name = "y", offset = 2, byte_size = 2 }, - { name = "z", offset = 4, byte_size = 2 }, - { name = "w", offset = 6, byte_size = 2 }, - }}, - -- 3-element signed short vector (the floor face-cursor type; includes explicit pad byte to match math.h's S2 pad member). - V3_S2 = { byte_size = 8, members = { - { name = "x", offset = 0, byte_size = 2 }, - { name = "y", offset = 2, byte_size = 2 }, - { name = "z", offset = 4, byte_size = 2 }, - { name = "pad", offset = 6, byte_size = 2 }, - }}, - -- 2-element signed int vector. - V2_S4 = { byte_size = 8, members = { - { name = "x", offset = 0, byte_size = 4 }, - { name = "y", offset = 4, byte_size = 4 }, - }}, - -- 3-element signed int vector. - V3_S4 = { byte_size = 16, members = { - { name = "x", offset = 0, byte_size = 4 }, - { name = "y", offset = 4, byte_size = 4 }, - { name = "z", offset = 8, byte_size = 4 }, - { name = "pad", offset = 12, byte_size = 4 }, - }}, - -- 4-element signed int vector. - V4_S4 = { byte_size = 16, members = { - { name = "x", offset = 0, byte_size = 4 }, - { name = "y", offset = 4, byte_size = 4 }, - { name = "z", offset = 8, byte_size = 4 }, - { name = "w", offset = 12, byte_size = 4 }, - }}, - } - local S2_TYPE_BYTE_SIZE = 2 -- S2 = signed 16-bit (see dsl.h) - local S4_TYPE_BYTE_SIZE = 4 -- S4 = signed 32-bit (see dsl.h) + -- Layout for V* / Rect_* / Reg_* / Slice / … comes from corpus.type_name_registry. + -- scan_source parses Struct_() in math.h, math.atom.h, memory.h and fills field offset + byte_size. + -- U1–U4 / S1–S4 / B1–B4 stay authored fundamentals (BUILTIN_BYTE_SIZES + base_type DIEs below). + local type_reg = registries.type_name_registry or {} + local function layout_from_registry(tn) + local entry = type_reg[tn] + if not entry or entry.kind ~= "struct" or not entry.fields or #entry.fields == 0 then + return nil + end + if entry.byte_size == nil then return nil end + local members = {} + for _, f in ipairs(entry.fields) do + if f.offset == nil or f.byte_size == nil then return nil end + members[#members + 1] = { + name = f.name, + offset = f.offset, + byte_size = f.byte_size, + type_name = f.type_name, + pointer_depth = f.pointer_depth or 0, + } + end + return { byte_size = entry.byte_size, members = members } + end + local function encoding_for_type(tn) + if type(tn) == "string" and tn:match("^S[124]$") then return 5 end + return 7 + end + local S2_TYPE_BYTE_SIZE = 2 + local S4_TYPE_BYTE_SIZE = 4 -- Pre-emit the signed base types (S2, S4) once if any typed view's member needs them. -- We emit per-typed-view lazily below; build a member-base-type offset cache (idempotent). @@ -1804,17 +1784,54 @@ local function build_inserted_children(main_cu_offset, main_cu_end_excl, atom_ta end -- Always emit S2 + S4 (the v*_S2 / v*_S4 family base types) once before any typed-view, -- even if no atom currently declares a v*_S2 field, so future atoms pick them up without rewiring. - ensure_member_base_type("S2", S2_TYPE_BYTE_SIZE, 5) -- DW_ATE_signed = 5 + ensure_member_base_type("S2", S2_TYPE_BYTE_SIZE, 5) ensure_member_base_type("S4", S4_TYPE_BYTE_SIZE, 5) local type_chain_offsets = {} + local struct_die_offsets = {} + local function emit_struct_layout(tn) + if struct_die_offsets[tn] then return struct_die_offsets[tn] end + local type_info = layout_from_registry(tn) + if not type_info then return nil end + for _, m in ipairs(type_info.members) do + if (m.pointer_depth or 0) == 0 and layout_from_registry(m.type_name) then + emit_struct_layout(m.type_name) + end + end + local struct_offset = next_offset() + struct_die_offsets[tn] = struct_offset + emit_die("structure_type", { + name = tn, + byte_size = type_info.byte_size, + }) + for _, m in ipairs(type_info.members) do + local member_type_off + if (m.pointer_depth or 0) > 0 then + member_type_off = type_chain_offsets[m.type_name .. "|" .. m.pointer_depth] + elseif layout_from_registry(m.type_name) then + member_type_off = struct_die_offsets[m.type_name] + else + member_type_off = ensure_member_base_type( + m.type_name, m.byte_size, encoding_for_type(m.type_name)) + end + if not member_type_off then + member_type_off = ensure_member_base_type( + m.type_name or "U4", m.byte_size or U4_BYTE_SIZE, 7) + end + emit_die("member", { + name = m.name, + data_member_location = m.offset, + type = ref4_of(member_type_off), + }) + end + emit(string.char(DIE_CHILDREN_TERMINATOR)) + return struct_offset + end for _, tn in ipairs(sorted_typed_types) do if tn ~= "U4" then local depth = used_typed_views[tn] - local type_info = STRUCT_MEMBER_TABLE[tn] - if not type_info then - -- Unknown typed view: fall back to a generic 4-byte unsigned base_type to keep the wire valid. - -- gdb renders as the typename but `print *ptr` only sees the first 4 bytes. + local struct_offset = emit_struct_layout(tn) + if not struct_offset then local innermost_offset = next_offset() emit_die("base_type", { name = tn, @@ -1824,40 +1841,12 @@ local function build_inserted_children(main_cu_offset, main_cu_end_excl, atom_ta local outermost_offset = next_offset() emit_die("pointer_type", { type = ref4_of(innermost_offset) }) type_chain_offsets[tn .. "|" .. depth] = outermost_offset + elseif depth == 1 then + local outermost_offset = next_offset() + emit_die("pointer_type", { type = ref4_of(struct_offset) }) + type_chain_offsets[tn .. "|" .. depth] = outermost_offset else - -- Emit a proper structure_type DIE for this typed view. - local struct_offset = next_offset() - emit_die("structure_type", { - name = tn, - byte_size = type_info.byte_size, - }) - for _, m in ipairs(type_info.members) do - local member_type_name, member_type_encoding - if m.byte_size == 2 then - member_type_name = "S2" - member_type_encoding = 5 - elseif m.byte_size == 4 then - member_type_name = "S4" - member_type_encoding = 5 - else - member_type_name = "U4" - member_type_encoding = 7 - end - local member_base_off = ensure_member_base_type(member_type_name, m.byte_size, member_type_encoding) - emit_die("member", { - name = m.name, - data_member_location = m.offset, - type = ref4_of(member_base_off), - }) - end - emit(string.char(DIE_CHILDREN_TERMINATOR)) - if depth == 1 then - local outermost_offset = next_offset() - emit_die("pointer_type", { type = ref4_of(struct_offset) }) - type_chain_offsets[tn .. "|" .. depth] = outermost_offset - else - error("typed-view: pointer_depth > 1 is not yet supported in this emission path") - end + error("typed-view: pointer_depth > 1 is not yet supported in this emission path") end end end @@ -1991,9 +1980,6 @@ local function build_inserted_children(main_cu_offset, main_cu_end_excl, atom_ta end end local atom_view = (registries.atom_views or {})[atom.name] - -- Build the atom-name lookup table once (cheap; O(atom_table)) so step (b) and step (d) can resolve rbind_atom names. - -- TODO(Ed): Bad assignment? - local atom_by_name = atom_by_name or (function() local m = {}; for _, a in ipairs(atom_table) do if a.name then m[a.name] = a end end; return m end)() -- step (b) inputs: this atom's `atom_ctx()` (resolved from the registries' atom_ctxs) local this_ctx = registries.atom_ctxs and registries.atom_ctxs[atom.name] if this_ctx and this_ctx.rbind_atom then @@ -2073,9 +2059,9 @@ local function build_inserted_children(main_cu_offset, main_cu_end_excl, atom_ta end, -- (e) enum-site atom_type() default on the registry entry. function(r_name, alias_code) - -- TODO(Ed): Bad definition? - if alias and alias.default_type and alias.default_depth and alias.default_depth > 0 then - return type_chain_offsets[alias.default_type .. "|" .. alias.default_depth] + local a = by_alias[r_name] + if a and a.default_type and a.default_depth and a.default_depth > 0 then + return type_chain_offsets[a.default_type .. "|" .. a.default_depth] end end, } diff --git a/scripts/passes/offsets.lua b/scripts/passes/offsets.lua index 6c66b09..5cc8fb7 100644 --- a/scripts/passes/offsets.lua +++ b/scripts/passes/offsets.lua @@ -9,13 +9,6 @@ --- Per-directory aggregation: every source in the same directory contributes to the same `gen/offsets.h`. --- The directory itself is the namespace; the filename does not repeat the module name. --- ---- (Task 12.16 note: atom-namespaced enum names — e.g., `atom_offset__normalize_v3s4__srav_path__aligned_done` — ---- were considered to prevent cross-atom label collisions, but the C-side `atom_offset(F, T)` macro in ---- `code/duffle/dsl.atom.h` doesn't know the current atom_name at expansion time, so any namespacing ---- on the metaprogram side breaks the C build. Reverted. The C-side would need a per-atom ---- `CURRENT_ATOM` #define (set by `MipsAtom_`/`MipsAtom_Proc_` macros) plus an updated `atom_offset` ---- macro that uses it. That's a coordinated refactor — deferred to a future track.) ---- --- The offset is `target_word - branch_word - 1` (the standard MIPS branch-immediate encoding: branch_offset = relative_pc_in_words - 1). -- ════════════════════════════════════════════════════════════════════════════ @@ -124,9 +117,7 @@ end --- For cross-module `j`/`jal` (atom body in one module, target in another), the linker emits a `R_MIPS_26` relocation against the lower 26 bits; the upper 4 bits come from the PC of the delay slot following the `j`. --- The metaprogram doesn't know either at compile time, so the emitted value is the relative word offset that the duffle `enc_i` macro places in the immediate field; the toolchain handles the rest. --- `jump_reg` / `call_reg` / `jump_link` -> ERROR. Register-form jumps have no offset field; `atom_offset` is invalid. ---- ---- Top-level `atom_offset(F, T)` markers (where the marker is the entire token — `consuming_encoder` == nil) default to `branch_*` behavior (relative offset). ---- This preserves backward compatibility for any top-level marker that may exist outside a control-transfer instruction. +--- missing `consuming_encoder` -> ERROR. A lone top-level `atom_offset` is not a branch. --- @param labels table --- @param branches table[] --- @param errors table[] @@ -142,14 +133,19 @@ local function compute_offsets(labels, branches, errors) } else local consuming = br.consuming_encoder - if consuming == "jump_reg" or consuming == "call_reg" or consuming == "jump_link" then + if consuming == nil or consuming == "" then + errors[#errors + 1] = { + line = br.line or 0, + msg = "atom_offset requires a consuming encoder (branch_*, jump, call_addr); top-level atom_offset is invalid; at word " .. br.branch_word, + } + elseif consuming == "jump_reg" or consuming == "call_reg" or consuming == "jump_link" then errors[#errors + 1] = { line = br.line or 0, msg = "atom_offset cannot be used with " .. consuming .. " (register-form jumps have no offset field); at word " .. br.branch_word, } else - -- All other consuming instructions (including `branch_*`, `jump`, `call_addr`, and nil for top-level markers) use the same relative offset value. + -- Consuming instructions with an offset field (`branch_*`, `jump`, `call_addr`) use the same relative offset value. -- The MIPS encoding differs per opcode but the duffle `enc_i` macro handles the truncation to the immediate-field width. results[#results + 1] = { target = br.target, diff --git a/scripts/passes/report.lua b/scripts/passes/report.lua index deff700..0c39093 100644 --- a/scripts/passes/report.lua +++ b/scripts/passes/report.lua @@ -1,8 +1,9 @@ --- passes/report.lua — Per-MODULE annotation report renderer + project-wide summary writer. --- ---- Two output files per build: ---- - `build/gen/.annotations.txt` — one per source-directory containing atoms; aggregates across all sources in the directory. ---- - `build/gen/annotation_validation.txt` — the project summary. +--- Per-module markdown plus one project summary: +--- - `build/.atom_meta_report.md` — one per source-directory containing atoms +--- - `build/.atoms.md` — verbose source map +--- - `build/atom_meta_report.summary.md` — the project summary --- --- The canonical `corpus.sources_by_dir` projection groups sources by directory. --- This pass builds one ModuleView per directory and walks SECTION_RENDERERS. @@ -305,7 +306,6 @@ local function build_module_view(dir, dir_sources, corpus) decls = decls, schemas = schemas, findings = sa.findings or {}, - sa = sa, corpus = corpus, } end diff --git a/scripts/passes/scan_source.lua b/scripts/passes/scan_source.lua index 00234aa..82d8f45 100644 --- a/scripts/passes/scan_source.lua +++ b/scripts/passes/scan_source.lua @@ -179,7 +179,7 @@ end local function read_parens_after(source, ident_end, fallback) local open_paren = duffle.skip_ws_and_cmt(source, ident_end) if source:sub(open_paren, open_paren) ~= "(" then return nil, fallback or open_paren + 1 end - local inner, after_paren = duffle.read_parens(source, open_paren) + local inner, after_paren = duffle.read_parens(source, open_paren) return inner, after_paren, open_paren end @@ -189,7 +189,7 @@ end local function find_body_braces(source, after_paren, fallback) local brace = duffle.scan_to_char(source, "{", after_paren) if not brace then return nil, fallback or (after_paren + 1) end - local body, after_brace = duffle.read_braces(source, brace) + local body, after_brace = duffle.read_braces(source, brace) return body, after_brace, brace + 1 end @@ -249,7 +249,7 @@ local function preceding_comment_walk_backward(source, start_pos) line_start = line_start - 1 end local line = source:sub(line_start, non_ws) - if line:sub(1, 2) ~= "//" then break end + if line:sub(1, 2) ~= "//" then break end table.insert(pieces, 1, line) scan_pos = line_start - 1 end @@ -303,8 +303,8 @@ local function register_atom(out, kind, declaration_line, name, body, body_off, -- Capture the pending marker BEFORE attaching so the walker can anchor the backward comment walk on the marker's marker_pos -- (which is the correct anchor even when an `FI_ MipsAtom ac_X(args)` proc-prelude separates the marker from the declaration). local pending_marker = nil - local markers = out.debug_skip_markers - local m = markers[#markers] + local markers = out.debug_skip_markers + local m = markers[#markers] if m and m.pending then pending_marker = m end local positive = attach_debug_skip_marker(out, kind) @@ -314,7 +314,7 @@ local function register_atom(out, kind, declaration_line, name, body, body_off, -- The walker does not need to detect marker shape. -- A pending_marker record (or the declaration ident_pos fallback) supplies the anchor position. local start_pos = comment_walk_start(pending_marker, pos) - comment = preceding_comment_walk_backward(source, start_pos) + comment = preceding_comment_walk_backward(source, start_pos) end out.atoms[#out.atoms + 1] = { line = declaration_line, @@ -335,7 +335,7 @@ end local function register_raw_atom(out, declaration_line, name, body, body_off, raw_name, pos) out.raw_atoms[#out.raw_atoms + 1] = { line = declaration_line, name = name, body = body, body_off = body_off, - kind = "raw_atom", raw_name = raw_name, + kind = "raw_atom", raw_name = raw_name, } end @@ -344,7 +344,7 @@ end local function parse_type_chain(text, pos) if pos > #text then return nil end -- Skip leading whitespace before the type ident. - local start = duffle.skip_ws_and_cmt(text, pos) + local start = duffle.skip_ws_and_cmt(text, pos) local ident, after = duffle.read_ident(text, start) if not ident then return nil end local depth = 0 @@ -367,6 +367,9 @@ local BUILTIN_BYTE_SIZES = { ["S1"] = 1, ["S2"] = 2, ["S4"] = 4, + ["B1"] = 1, + ["B2"] = 2, + ["B4"] = 4, -- GCC __UINT*/__INT*_TYPE__ family (used by the duffle TSet_ convention in dsl.h). -- MIPS32 has no 64-bit types; __UINT64_TYPE__/__INT64_TYPE__ are excluded. ["__UINT8_TYPE__"] = 1, @@ -422,7 +425,7 @@ end -- Returns the raw fields array with `{name, type_name, pointer_depth}` only (NO offset / byte_size). -- The propagation pass `resolve_struct_field_sizes` walks each struct's fields AFTER type resolution and populates offset + byte_size in place. local function parse_struct_body_fields(body) - local fields = {} + local fields = {} local body_pos = 1 local body_len = #body while body_pos <= body_len do @@ -563,6 +566,16 @@ local function propagate_type_sizes(out) for _ = 1, TYPE_CHAIN_MAX_DEPTH do local any_change = false for _, entry in pairs(reg) do + if entry.kind == "array" and entry.byte_size == nil and entry.counts then + local elem_size = BUILTIN_BYTE_SIZES[entry.elem] + or (reg[entry.elem] and reg[entry.elem].byte_size) + if elem_size then + local n = 1 + for _, c in ipairs(entry.counts) do n = n * c end + entry.byte_size = elem_size * n + any_change = true + end + end if entry.kind == "struct" and entry.fields then local byte_off = 0 local gap_seen = false @@ -710,7 +723,7 @@ local function scan_atom_info_subcalls(info_inner, info_line) -- 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 = {} + local regs = {} for _, entry in ipairs(entries) do local reg_name, override, malformed = parse_atom_info_reg_entry(entry) if reg_name then @@ -742,9 +755,9 @@ local function scan_atom_info_subcalls(info_inner, info_line) end end local function reg_types_handler(sub_inner, info_line) - local args = duffle.split_top_level_commas(sub_inner) + local args = duffle.split_top_level_commas(sub_inner) if not args[1] then return end - reg_overrides = reg_overrides or {} + reg_overrides = reg_overrides or {} local reg_name = duffle.trim(args[1]) local type_name, depth = nil, 0 if args[2] then @@ -763,13 +776,13 @@ local function scan_atom_info_subcalls(info_inner, info_line) } end local SUBCALL_HANDLERS = { - atom_bind = function(sub_inner) binds = duffle.trim(sub_inner) end, -- scan: atom_bind() - atom_reads = function(sub_inner, info_line) rw_handler(sub_inner, info_line, "atom_reads") end, -- scan: atom_reads()], ...>) - atom_writes = function(sub_inner, info_line) rw_handler(sub_inner, info_line, "atom_writes") end, -- scan: atom_writes()], ...>) - atom_view = function(sub_inner) view_binds = duffle.trim(sub_inner) end, -- scan: atom_view() - atom_reg_types = reg_types_handler, -- scan: atom_reg_types(, ) - atom_ctx = function(sub_inner, info_line) ident_handler(sub_inner, info_line, "ctx_atom_name") end, -- scan: atom_ctx() - atom_phase = function(sub_inner, info_line) ident_handler(sub_inner, info_line, "phase_label") end, -- scan: atom_phase(