diff --git a/code/duffle/gen/macs.h b/code/duffle/gen/macs.h index 0c7e3e0..77e2c71 100644 --- a/code/duffle/gen/macs.h +++ b/code/duffle/gen/macs.h @@ -37,6 +37,7 @@ // The 'Yield' sequence for Tape Atoms (mac_yield). #define mac_yield(...) \ load_word(R_AtomJmp, R_TapePtr, 0) \ + LdSlot_ \ , add_ui_self( R_TapePtr, S_(MipsCode)) \ , jump_reg( R_AtomJmp) \ , BdSlot_ nop @@ -217,7 +218,7 @@ WORD_COUNT(mac_gte_ld_ir123_v3s4, 3) , mac_gte_mv_from_mac123_v3s4(a) \ GteDelay_ /* Read MAC1/2/3 → a.xyz (overwrites source-A's load targets) */ \ , mac_shift_aright_v3s4_self(a, 12) /* Right-shift MAC by 12 (S12.20 → S12.0 OuterProduct12) */ -WORD_COUNT(mac_gte_op_cross_v3s4, 16) +WORD_COUNT(mac_gte_op_cross_v3s4, 13) /* atom_dbg_skip */ #define mac_gte_store_f3(r_primitive_cursor) \ @@ -291,7 +292,7 @@ WORD_COUNT(mac_gte_sqr_v3s4, 5) , shift_aright_var(r_dx, r_dx, r_shift) \ , shift_aright_var(r_dy, r_dy, r_shift) \ , shift_aright_var(r_dz, r_dz, r_shift) -WORD_COUNT(mac_gte_gpf_scale, 12) +WORD_COUNT(mac_gte_gpf_scale, 13) #define mac_trans_mt3s3s4(r_mtx, r_off, r_t0, r_t1, r_t2) \ load_word( r_t0, r_off, O_(V3_S4,x)) \ diff --git a/code/duffle/gen/offsets.h b/code/duffle/gen/offsets.h index d4c38c1..e9d385c 100644 --- a/code/duffle/gen/offsets.h +++ b/code/duffle/gen/offsets.h @@ -33,7 +33,7 @@ enum { atom_offset_example_atom_proc_skip = _atom_offset_example_atom_proc_skip, }; -// --- atom: build_normalize_v3s4 (65 words) --- +// --- atom: build_normalize_v3s4 (63 words) --- #define _atom_offset_aligned_done_srav_path 3 #define _atom_offset_srav_path_aligned_done 4 diff --git a/code/duffle/gte.atom.c b/code/duffle/gte.atom.c index acb81ee..42551af 100644 --- a/code/duffle/gte.atom.c +++ b/code/duffle/gte.atom.c @@ -272,82 +272,65 @@ typedef Struct_(Binds_NormalizeV3S4) { U2 dst_offset; /* offset of dst V3_S4 within the BIOS scratchpad */ }; typedef Struct_(RegUse_build_normalize_v3s4) { - Reg src_ptr; - Reg dst_ptr; - Reg src_x; - Reg src_z; - Reg recip_est; - Reg norm; - Reg shift; - union { - Reg v_sqr_aligned, dst_offset; - } t3; - union { Reg mac2; } t4; - union { Reg btarget, shift_count, sqrtbl_index, src_offset; } t5; + union { Reg_(V3_S4) res, src; }; + union { Reg t0, src_ptr, mac2; }; + union { Reg t1, dst_ptr; }; + union { Reg t2, dst_offset, mac1, v_sqr_aligned; }; + union { Reg t3, src_offset, btarget, shift_count, sqrtbl_index; }; + union { Reg t4, mac3, v_sqr_sum, half_shift_tmp, inv_len; }; + union { Reg t5, lzcr, half_shift; }; }; /* ─── Full normalize (all 4 stages inline) ─── * Generic 4-stage GTE normalize (SQR → sum+LZCR → align+sqrtbl → GPF+srav). */ internal MipsAtom* build_normalize_v3s4(AtomArena_R aa, RegUse_build_normalize_v3s4 r) MipsAtom_Proc_(aa, { - load_half(r.t5.src_offset, R_TapePtr, O_(Binds_NormalizeV3S4, src_offset)), - load_half(r.t3.dst_offset, R_TapePtr, O_(Binds_NormalizeV3S4, dst_offset)), - LdSlot_ add_u(r.src_ptr, R_ScratchBase, r.t5.src_offset), - LdSlot_ add_u(r.dst_ptr, R_ScratchBase, r.t3.dst_offset), + load_half(r.src_offset, R_TapePtr, O_(Binds_NormalizeV3S4, src_offset)), + load_half(r.dst_offset, R_TapePtr, O_(Binds_NormalizeV3S4, dst_offset)), + LdSlot_ add_u(r.src_ptr, R_ScratchBase, r.src_offset), + LdSlot_ add_u(r.dst_ptr, R_ScratchBase, r.dst_offset), LdSlot_ add_ui_self(R_TapePtr, S_(Binds_NormalizeV3S4)), - /* Load src.x/y/z from r_src_ptr (caller-determined address) into r_src_x / r_recip_est / r_src_z. - * r_src_x holds src.x throughout stages 1-2; r_recip_est (which is src.y during this phase) is reused as MAC2 in stage 4. */ - mac_load_word_v3(r.src_x, r.recip_est, r.src_z, r.src_ptr, 0), + mac_load_v3s4(r.src, r.src_ptr, 0), /* Stage 1: mtc2 src → IR1/2/3, SQR fires. */ - LdSlot_ mac_gte_sqr_v3s4(r.src_x, r.recip_est, r.src_z, LdSlot_ nop), + LdSlot_ mac_gte_sqr_v3s4(r.src.x, r.src.y, r.src.z, LdSlot_ nop), - /* Stage 2: mfc2 MAC1/2/3, sum, mtc2 LZCS. */ - mac_gte_mv_from_data_r_mac123(r.t3.v_sqr_aligned, r.t4.mac2, r.norm), LdSlot_ nop, - add_u_self( r.norm, r.t3.v_sqr_aligned), - add_u_self( r.norm, r.t4.mac2), - gte_mv_to_data_r( r.norm, C2_LZCS), GteDelay_ nop2, - gte_mv_from_data_r(r.shift, C2_LZCR), GteDelay_ nop, + /* Stage 2: mfc2 MAC1/2/3, sum, mtc2 LZCS. src_ptr is dead; reuse as mac2. */ + mac_gte_mv_from_data_r_mac123(r.mac1, r.mac2, r.mac3), LdSlot_ nop, + add_u_self( r.v_sqr_sum, r.mac1), + add_u_self( r.v_sqr_sum, r.mac2), + gte_mv_to_data_r( r.v_sqr_sum, C2_LZCS), GteDelay_ nop2, + gte_mv_from_data_r(r.lzcr, C2_LZCR), GteDelay_ nop, - /* Stage 3: round LZCR to even, compute half-shift, align |v|² to bit 24. - * r_norm holds |v|² sum; r_shift holds the LZCR count from mfc2. - * After the component: r_shift = even(LZCR), r_norm = half-shift, r_t3.v_sqr_aligned = |v|². */ - mac_lzcr_round_even_half_shift(r.shift, r.norm, r.t3.v_sqr_aligned), - add_si( r.t5.btarget, r.shift, -24), - branch_lt_zero(r.t5.btarget, atom_offset(aligned_done, srav_path)), BdSlot_ nop, /* bltz → srav_path (LZCR < 24 path) */ - jump_rel(atom_offset(srav_path, aligned_done)), /* b → aligned_done (LZCR >= 24 path) */ - BdSlot_ shift_lleft_var(r.t3.v_sqr_aligned, r.t3.v_sqr_aligned, r.t5.btarget), + /* Stage 3: even(LZCR), half-shift, align |v|² to bit 24. */ + mac_lzcr_round_even_half_shift(r.lzcr, r.v_sqr_sum, r.v_sqr_aligned), + add_si( r.btarget, r.lzcr, -24), + branch_lt_zero(r.btarget, atom_offset(aligned_done, srav_path)), BdSlot_ nop, /* bltz → srav_path (LZCR < 24 path) */ + jump_rel(atom_offset(srav_path, aligned_done)), /* b → aligned_done (LZCR >= 24 path) */ + BdSlot_ shift_lleft_var(r.v_sqr_aligned, r.v_sqr_aligned, r.btarget), atom_label(srav_path) - li_s( r.t5.shift_count, 24), - sub_s(r.t5.shift_count, r.t5.shift_count, r.shift), - shift_aright_var(r.t3.v_sqr_aligned, r.t3.v_sqr_aligned, r.t5.shift_count), + li_s( r.shift_count, 24), + sub_s(r.shift_count, r.shift_count, r.lzcr), + shift_aright_var(r.v_sqr_aligned, r.v_sqr_aligned, r.shift_count), atom_label(aligned_done) - // Save the shift count to r_shift before the next 5 instructions overwrite r_norm (the sqrtbl lookup loads 1/|v| into r_norm, which becomes IR0 in stage 4). - or_u(r.shift, r.norm, 0), /* r_shift ← shift count (preserved through stage 4) */ - add_si( r.t3.v_sqr_aligned, r.t3.v_sqr_aligned, -64), /* r_t3.v_sqr_aligned holds |v|² aligned (top bit at bit 7). */ - shift_lleft(r.t3.v_sqr_aligned, r.t3.v_sqr_aligned, 1), - mac_load_word_imm(r.t5.sqrtbl_index, & gte_normalize_sqr_tbl), add_u_self(r.t5.sqrtbl_index, r.t3.v_sqr_aligned), - load_half(r.norm, r.t5.sqrtbl_index, 0), /* r_norm = sqrtbl[aligned-64] = 1/|v| (IR0 in stage 4) */ + or_u(r.half_shift, r.half_shift_tmp, 0), + add_si( r.v_sqr_aligned, r.v_sqr_aligned, -64), + shift_lleft(r.v_sqr_aligned, r.v_sqr_aligned, 1), + mac_load_word_imm(r.sqrtbl_index, & gte_normalize_sqr_tbl), add_u_self(r.sqrtbl_index, r.v_sqr_aligned), + load_half(r.inv_len, r.sqrtbl_index, 0), LdSlot_ nop, - /* r.src_z holds src.z from the initial load (r.src_z is a dedicated slot, - * never clobbered between the load at body start and the stage-4 GPF use below). - * Stage 4: GPF + srav finalize (r_shift = shift count, r_norm = 1/|v|). */ - mac_gte_general_purpose_interopolation( - r.norm, - r.src_x, /* IR1 = src.x (preserved in r_tmp — r_mac2_scratch was clobbered to MAC2 in stage 1.5) */ - r.recip_est, - r.src_z, /* IR3 = src.z (reloaded) */ - r.t4.mac2, r.recip_est, r.src_z, - GteDelay_ nop, - GteDelay_ nop + mac_gte_general_purpose_interopolation(r.inv_len, + r.src.x, r.src.y, r.src.z, + r.res.x, r.res.y, r.res.z, + GteDelay_ load_word(R_AtomJmp, R_TapePtr, 0), LdSlot_ // ac_yield: word 1 + GteDelay_ add_ui_self( R_TapePtr, S_(MipsCode)) // ac_yield: word 2 ), - /* sra by r_shift = (31-LZCR)/2 (saved before sqrtbl lookup) */ - mac_shift_aright_var_v3_self(r.t4.mac2, r.recip_est, r.src_z, r.shift), - /* Store result.x/y/z to r_dst_ptr (caller-determined dst address). */ - mac_store_word_v3(r.t4.mac2, r.recip_est, r.src_z, r.dst_ptr, 0), + mac_shift_aright_var_v3s4_self(r.res, r.half_shift), + mac_store_v3s4(r.res, r.dst_ptr, 0), - mac_yield() + jump_reg(R_AtomJmp), BdSlot_ nop // ac_yield: word 3-4 + // mac_yield() }) /* ─── GTE OP cross product (a × b → out) ─── diff --git a/code/duffle/lottes_tape.h b/code/duffle/lottes_tape.h index 9c219d3..ed04eea 100644 --- a/code/duffle/lottes_tape.h +++ b/code/duffle/lottes_tape.h @@ -263,7 +263,7 @@ FI_ void tb_scope_run_end(TapeBuilder* tb) { tb_emit(tb,tape_exit); tape_run(tb_ // The 'Yield' sequence for Tape Atoms (mac_yield). atom_dbg_skip MipsAtomComp_(ac_yield) { - load_word(R_AtomJmp, R_TapePtr, 0), + load_word(R_AtomJmp, R_TapePtr, 0), LdSlot_ add_ui_self( R_TapePtr, S_(MipsCode)), jump_reg( R_AtomJmp), BdSlot_ nop, }; diff --git a/code/hello_camera/hello_camera.c b/code/hello_camera/hello_camera.c index 1fc7491..5ede66f 100644 --- a/code/hello_camera/hello_camera.c +++ b/code/hello_camera/hello_camera.c @@ -151,17 +151,13 @@ internal void compile_init_atoms(void) { smem.normalize_v3s4 = build_normalize_v3s4(& ab, RegUse_(build_normalize_v3s4) { - .src_ptr = ralloc(), - .dst_ptr = ralloc(), - .recip_est = ralloc(), - .norm = ralloc(), - .shift = ralloc(), - .src_x = ralloc(), - .src_z = ralloc(), - // .shift_count = ralloc(), /* dedicated slot for stage-3 → stage-4 shift count */ - .t3 = ralloc(), - .t4 = ralloc(), - .t5 = ralloc(), + .res = ralloc_v3(), + .t0 = ralloc(), + .t1 = ralloc(), + .t2 = ralloc(), + .t3 = ralloc(), + .t4 = ralloc(), + .t5 = ralloc(), }); regfile_reset(& rf); diff --git a/scripts/duffle_emit.lua b/scripts/duffle_emit.lua index c92e800..2048ab3 100644 --- a/scripts/duffle_emit.lua +++ b/scripts/duffle_emit.lua @@ -550,7 +550,14 @@ local function _project_emission_inner(root_body_entry, ctx_table) local tok_line = line_of(body_off + bt.rel) or 0 if M.DELAY_MARKERS[ident] then emit_marker("delay", ident, nil, tok_line) - local rest = M.trim(tok:sub(after or (#tok + 1))) + 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) + end if rest ~= "" then process_token({ tok = rest, rel = bt.rel }) end diff --git a/scripts/passes/components.lua b/scripts/passes/components.lua index 007363c..4897cb6 100644 --- a/scripts/passes/components.lua +++ b/scripts/passes/components.lua @@ -329,6 +329,24 @@ local function strip_mac_prefix(ident) return ident end +--- Strip a leading delay marker (`LdSlot_` / `BdSlot_` / `GteDelay_` / `DmaSlot_`) +--- plus following whitespace and block comments. Returns the remainder, or "" +--- when the token is only the marker. +--- `BdSlot_ nop` becomes `nop`. Bare `LdSlot_` becomes "". +--- @param tok string +--- @return string +local function strip_leading_delay_marker(tok) + 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) + if not close then return "" end + rest = rest:sub(close + 2):match("^%s*(.*)$") or "" + end + return rest +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_`) @@ -347,18 +365,30 @@ local function word_count_rec(name, comp_by_name, wc, cache) for _, t in ipairs(tokens) do local trimmed = t.tok if trimmed ~= "" then - local lookup = strip_mac_prefix(duffle.read_ident(trimmed, 1)) - if lookup == "atom_label" or lookup == "atom_offset" then - -- Pure metaprogram anchors; emit zero words. - elseif lookup and comp_by_name[lookup] then - -- It's a `mac_X(...)` call. Recurse. - n = n + word_count_rec(lookup, comp_by_name, wc, cache) - elseif lookup and wc and wc[lookup] then - -- Encoding macro or pseudo-instruction (e.g. mask_upper = 2, nop2 = 2). - n = n + wc[lookup] - else - -- Unrecognized token. Fall back to 1 word. - n = n + 1 + local work = trimmed + while true do + local marker = duffle.read_ident(work, 1) + if marker and duffle.DELAY_MARKERS[marker] then + work = strip_leading_delay_marker(work) + if work == "" then break end + else + break + end + end + if work ~= "" then + local lookup = strip_mac_prefix(duffle.read_ident(work, 1)) + if lookup == "atom_label" or lookup == "atom_offset" then + -- Pure metaprogram anchors; emit zero words. + elseif lookup and comp_by_name[lookup] then + -- It's a `mac_X(...)` call. Recurse. + n = n + word_count_rec(lookup, comp_by_name, wc, cache) + elseif lookup and wc and wc[lookup] then + -- Encoding macro or pseudo-instruction (e.g. mask_upper = 2, nop2 = 2). + n = n + wc[lookup] + else + -- Unrecognized token. Fall back to 1 word. + n = n + 1 + end end end end diff --git a/scripts/passes/emission_model.lua b/scripts/passes/emission_model.lua index c3a0248..855289d 100644 --- a/scripts/passes/emission_model.lua +++ b/scripts/passes/emission_model.lua @@ -7,7 +7,7 @@ --- 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` preserves the existing build-stopping policy. +--- Pass kind = `validation`. Findings record on the result; the orchestrator does not exit non-zero. --- --- Source-order discipline: --- * `corpus.source_order` sets the source-record order. diff --git a/scripts/passes/offsets.lua b/scripts/passes/offsets.lua index c11ad3c..6c66b09 100644 --- a/scripts/passes/offsets.lua +++ b/scripts/passes/offsets.lua @@ -129,32 +129,38 @@ end --- This preserves backward compatibility for any top-level marker that may exist outside a control-transfer instruction. --- @param labels table --- @param branches table[] +--- @param errors table[] --- @return BranchOffset[] -local function compute_offsets(labels, branches) +local function compute_offsets(labels, branches, errors) 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.branch_word .. ")") + errors[#errors + 1] = { + line = br.line or 0, + msg = "Branch target '" .. br.target .. "' has no atom_label (at word " .. br.branch_word .. ")", + } + else + local consuming = br.consuming_encoder + if 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. + -- 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, + tag = br.tag, + branch_word = br.branch_word, + offset = target - br.branch_word - 1, + consuming_encoder = br.consuming_encoder, + consuming_arg_pos = br.consuming_arg_pos, + } + end end - local consuming = br.consuming_encoder - local offset - if consuming == "jump_reg" or consuming == "call_reg" or consuming == "jump_link" then - -- Register-form jumps have no offset field. `atom_offset` cannot be used here. - error("atom_offset cannot be used with " .. consuming - .. " (register-form jumps have no offset field); at word " .. br.branch_word) - end - -- All other consuming instructions (including `branch_*`, `jump`, `call_addr`, and nil for top-level markers) 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. - offset = target - br.branch_word - 1 - results[#results + 1] = { - target = br.target, - tag = br.tag, - branch_word = br.branch_word, - offset = offset, - consuming_encoder = br.consuming_encoder, - consuming_arg_pos = br.consuming_arg_pos, - } end return results end @@ -237,8 +243,9 @@ local M = {} --- @param ctx PassCtx --- @param dir string -- the absolute source directory --- @param sources SourceFile[] -- sources in this directory +--- @param errors table[] --- @return string|nil -- the offsets_h path -local function process_directory(ctx, dir, sources) +local function process_directory(ctx, dir, sources, errors) local atoms_data = {} local function append_atom(atom) @@ -248,7 +255,7 @@ local function process_directory(ctx, dir, sources) atoms_data[#atoms_data + 1] = { name = atom.raw_name or atom.name, total_words = #(paths.word_events or {}), - offsets = compute_offsets(labels, branches), + offsets = compute_offsets(labels, branches, errors), } end @@ -286,7 +293,7 @@ function M.run(ctx) -- Per-directory aggregation: every source in the same directory contributes to one `gen/offsets.h`. local sources_by_dir = corpus.sources_by_dir or duffle.group_sources_by_dir(corpus.source_order) for dir, sources in pairs(sources_by_dir) do - local out_path = process_directory(ctx, dir, sources) + local out_path = process_directory(ctx, dir, sources, errors) if out_path then outputs[#outputs + 1] = { offsets_h = out_path } end diff --git a/scripts/passes/scan_source.lua b/scripts/passes/scan_source.lua index 46f4815..cf5ad57 100644 --- a/scripts/passes/scan_source.lua +++ b/scripts/passes/scan_source.lua @@ -1674,9 +1674,20 @@ parse_reg_use_schema_body = function(body, type_registry, opts) errors[#errors + 1] = { kind = "reguse_malformed" } return nil, errors end - local members = {} + local views = {} local union_readonly = nil local inner_pos = 1 + + local function note_readonly(flag) + if union_readonly == nil then + union_readonly = flag + elseif union_readonly ~= flag then + errors[#errors + 1] = { kind = "reguse_mixed_const" } + return false + end + return true + end + while inner_pos <= #inner do inner_pos = duffle.skip_ws_and_cmt(inner, inner_pos) if inner_pos > #inner then break end @@ -1689,56 +1700,228 @@ parse_reg_use_schema_body = function(body, type_registry, opts) errors[#errors + 1] = { kind = "reguse_const_reg_spelling" } return nil, errors end - if m_type ~= "Reg" then + + if m_type == "Reg_" then + local after_ty = duffle.skip_ws_and_cmt(inner, m_type_end) + if inner:sub(after_ty, after_ty) ~= "(" then + errors[#errors + 1] = { kind = "reguse_malformed" } + return nil, errors + end + local type_inner, after_paren = duffle.read_parens(inner, after_ty) + if not type_inner then + errors[#errors + 1] = { kind = "reguse_malformed" } + return nil, errors + end + local typed_fields = fields_for_reg_type(duffle.trim(type_inner), type_registry) + after_paren = duffle.skip_ws_and_cmt(inner, after_paren) + local inst_names, new_inner = parse_reg_names(inner, after_paren) + if not inst_names or #inst_names == 0 then + errors[#errors + 1] = { kind = "reguse_malformed" } + return nil, errors + end + if not note_readonly(false) then return nil, errors end + if not typed_fields then + if require_types then + errors[#errors + 1] = { kind = "reguse_unknown_reg_type", type_name = duffle.trim(type_inner) } + return nil, errors + end + pending = true + else + for _, inst in ipairs(inst_names) do + local names = {} + for _, field in ipairs(typed_fields) do + names[#names + 1] = inst .. "." .. field + end + views[#views + 1] = { names = names, lanes = true } + end + end + inner_pos = new_inner + + elseif m_type == "struct" then + local after_struct = duffle.skip_ws_and_cmt(inner, m_type_end) + if inner:sub(after_struct, after_struct) ~= "{" then + local _, tag_end = duffle.read_ident(inner, after_struct) + if not tag_end then + errors[#errors + 1] = { kind = "reguse_malformed" } + return nil, errors + end + after_struct = duffle.skip_ws_and_cmt(inner, tag_end) + end + if inner:sub(after_struct, after_struct) ~= "{" then + errors[#errors + 1] = { kind = "reguse_malformed" } + return nil, errors + end + local struct_inner, after_struct_braces = duffle.read_braces(inner, after_struct) + if not struct_inner then + errors[#errors + 1] = { kind = "reguse_malformed" } + return nil, errors + end + local lane_names = {} + local s_pos = 1 + while s_pos <= #struct_inner do + s_pos = duffle.skip_ws_and_cmt(struct_inner, s_pos) + if s_pos > #struct_inner then break end + local s_ty, s_ty_end = duffle.read_ident(struct_inner, s_pos) + if not s_ty then + s_pos = s_pos + 1 + goto continue_struct + end + if s_ty == "Reg_" then + local after_ty = duffle.skip_ws_and_cmt(struct_inner, s_ty_end) + if struct_inner:sub(after_ty, after_ty) ~= "(" then + errors[#errors + 1] = { kind = "reguse_malformed" } + return nil, errors + end + local type_inner, after_paren = duffle.read_parens(struct_inner, after_ty) + if not type_inner then + errors[#errors + 1] = { kind = "reguse_malformed" } + return nil, errors + end + local typed_fields = fields_for_reg_type(duffle.trim(type_inner), type_registry) + after_paren = duffle.skip_ws_and_cmt(struct_inner, after_paren) + local inst_names, new_s = parse_reg_names(struct_inner, after_paren) + if not inst_names or #inst_names == 0 then + errors[#errors + 1] = { kind = "reguse_malformed" } + return nil, errors + end + if not note_readonly(false) then return nil, errors end + if not typed_fields then + if require_types then + errors[#errors + 1] = { kind = "reguse_unknown_reg_type", type_name = duffle.trim(type_inner) } + return nil, errors + end + pending = true + else + for _, inst in ipairs(inst_names) do + for _, field in ipairs(typed_fields) do + lane_names[#lane_names + 1] = inst .. "." .. field + end + end + end + s_pos = new_s + elseif s_ty == "Reg" then + local s_after = duffle.skip_ws_and_cmt(struct_inner, s_ty_end) + local s_readonly = false + local maybe_const, maybe_end = duffle.read_ident(struct_inner, s_after) + if maybe_const == "const" then + s_readonly = true + s_after = duffle.skip_ws_and_cmt(struct_inner, maybe_end) + end + if not note_readonly(s_readonly) then return nil, errors end + local names, new_s = parse_reg_names(struct_inner, s_after) + if not names or #names == 0 then + errors[#errors + 1] = { kind = "reguse_malformed" } + return nil, errors + end + for _, n in ipairs(names) do lane_names[#lane_names + 1] = n end + s_pos = new_s + else + errors[#errors + 1] = { kind = "reguse_malformed" } + return nil, errors + end + ::continue_struct:: + end + if #lane_names == 0 and not pending then + errors[#errors + 1] = { kind = "reguse_malformed" } + return nil, errors + end + if #lane_names > 0 then + views[#views + 1] = { names = lane_names, lanes = true } + end + inner_pos = duffle.skip_ws_and_cmt(inner, after_struct_braces) + if inner:sub(inner_pos, inner_pos) == ";" then inner_pos = inner_pos + 1 end + + elseif m_type == "Reg" then + local m_after = duffle.skip_ws_and_cmt(inner, m_type_end) + local m_readonly = false + local maybe_const, maybe_end = duffle.read_ident(inner, m_after) + if maybe_const == "const" then + m_readonly = true + m_after = duffle.skip_ws_and_cmt(inner, maybe_end) + end + if not note_readonly(m_readonly) then return nil, errors end + local names, new_inner = parse_reg_names(inner, m_after) + if not names or #names == 0 then + errors[#errors + 1] = { kind = "reguse_malformed" } + return nil, errors + end + views[#views + 1] = { names = names, lanes = false } + inner_pos = new_inner + else errors[#errors + 1] = { kind = "reguse_malformed" } return nil, errors end - local m_after = duffle.skip_ws_and_cmt(inner, m_type_end) - local m_readonly = false - local maybe_const, maybe_end = duffle.read_ident(inner, m_after) - if maybe_const == "const" then - m_readonly = true - m_after = duffle.skip_ws_and_cmt(inner, maybe_end) - end - if union_readonly == nil then - union_readonly = m_readonly - elseif union_readonly ~= m_readonly then - errors[#errors + 1] = { kind = "reguse_mixed_const" } - return nil, errors - end - local names, new_inner = parse_reg_names(inner, m_after) - if not names or #names == 0 then - errors[#errors + 1] = { kind = "reguse_malformed" } - return nil, errors - end - for _, n in ipairs(names) do members[#members + 1] = n end - inner_pos = new_inner ::continue_inner:: end - if #members == 0 then - errors[#errors + 1] = { kind = "reguse_malformed" } - return nil, errors - end + local after_close = duffle.skip_ws_and_cmt(body, after_braces) local inst_name, inst_end = duffle.read_ident(body, after_close) - local aliases = {} - local slot_name - if inst_name then - slot_name = inst_name - for _, m in ipairs(members) do - local path = inst_name .. "." .. m - if not add_alias(path, slot_name) then return nil, errors end - aliases[#aliases + 1] = path + + if #views == 0 then + if not pending then + errors[#errors + 1] = { kind = "reguse_malformed" } + return nil, errors end - after_close = inst_end else - slot_name = members[1] - for _, m in ipairs(members) do - if not add_alias(m, slot_name) then return nil, errors end - aliases[#aliases + 1] = m - end + local has_lanes = false + for _, v in ipairs(views) do + if v.lanes then has_lanes = true end end - if not add_slot(slot_name, aliases, union_readonly) then return nil, errors end + + if has_lanes then + local width = nil + for _, v in ipairs(views) do + if not v.lanes then + errors[#errors + 1] = { kind = "reguse_malformed" } + return nil, errors + end + if width == nil then + width = #v.names + elseif #v.names ~= width then + errors[#errors + 1] = { kind = "reguse_union_width" } + return nil, errors + end + end + if width then + for i = 1, width do + local slot_name = views[1].names[i] + if inst_name then slot_name = inst_name .. "." .. slot_name end + local aliases = {} + for _, v in ipairs(views) do + local n = v.names[i] + if inst_name then n = inst_name .. "." .. n end + if not add_alias(n, slot_name) then return nil, errors end + aliases[#aliases + 1] = n + end + if not add_slot(slot_name, aliases, union_readonly) then return nil, errors end + end + end + else + local members = {} + for _, v in ipairs(views) do + for _, n in ipairs(v.names) do members[#members + 1] = n end + end + local aliases = {} + local slot_name + if inst_name then + slot_name = inst_name + for _, m in ipairs(members) do + local path = inst_name .. "." .. m + if not add_alias(path, slot_name) then return nil, errors end + aliases[#aliases + 1] = path + end + else + slot_name = members[1] + for _, m in ipairs(members) do + if not add_alias(m, slot_name) then return nil, errors end + aliases[#aliases + 1] = m + end + end + if not add_slot(slot_name, aliases, union_readonly) then return nil, errors end + end + end + + if inst_name then after_close = inst_end end after_close = duffle.skip_ws_and_cmt(body, after_close) if body:sub(after_close, after_close) == ";" then after_close = after_close + 1 end pos = after_close @@ -2801,6 +2984,7 @@ local SCHEMA_BODY_ERROR = { reguse_duplicate_slot = true, reguse_const_reg_spelling = true, reguse_mixed_const = true, + reguse_union_width = true, } -- Re-parse every RegUse_* body against the merged type_name_registry. diff --git a/scripts/passes/static_analysis.lua b/scripts/passes/static_analysis.lua index bef05a3..4e478f2 100644 --- a/scripts/passes/static_analysis.lua +++ b/scripts/passes/static_analysis.lua @@ -879,6 +879,14 @@ local function analyze_hardware_relations(atom) end if is_match then local gap = ev_word - prod.word - 1 + local required = prod.required + -- A following COP2 command waits at the transfer boundary. + -- Software nops are not required for that consumer class. + if is_gte_command(ev) + and (semantic == "MTC2" or semantic == "CTC2") + and relation.id ~= "mtc2_irgb_visibility" then + required = 0 + end local unknown_visibility = relation.visibility and relation.visibility.kind == "unknown_consumer" if unknown_visibility then hazards[#hazards + 1] = { @@ -906,7 +914,7 @@ local function analyze_hardware_relations(atom) , (relation.evidence and relation.evidence.confidence or "unknown") ), } - elseif prod.required ~= nil and gap < prod.required then + elseif required ~= nil and gap < required then local payload = { check = "transfer_hazards", kind = prod.violation_kind or "error", @@ -923,7 +931,7 @@ local function analyze_hardware_relations(atom) consumer_word = ev_word, consumer_token = ev_ident, gap = gap, - required = prod.required, + required = required, evidence_confidence = relation.evidence and relation.evidence.confidence or "unknown", evidence_source = relation.evidence and relation.evidence.source or "", msg = string.format("%s at line %d: %s relation %s (producer %s at word %d, %s:%d) violated: consumer at word %d (gap=%d, required=%d) [%s]" @@ -941,7 +949,7 @@ local function analyze_hardware_relations(atom) hazards[#hazards + 1] = payload end local satisfied = nil - if not unknown_visibility then satisfied = gap >= prod.required end + if not unknown_visibility then satisfied = gap >= required end -- Record the relation touch on `paths.relations` even when the gap is satisfied. -- Unknown relations are informational, not numeric pass/fail measurements. relations[#relations + 1] = { @@ -951,7 +959,7 @@ local function analyze_hardware_relations(atom) consumer_word = ev_word, destination = prod.destination, gap = gap, - required = prod.required, + required = required, satisfied = satisfied, } table.remove(pending, pending_idx) diff --git a/scripts/ps1_meta.lua b/scripts/ps1_meta.lua index fa32783..538d305 100644 --- a/scripts/ps1_meta.lua +++ b/scripts/ps1_meta.lua @@ -142,8 +142,7 @@ local PASSES = { }, ["static-analysis"] = { module = "passes.static_analysis", - -- "diagnostic" — every `error`/`warning` finding is written to the report file; - -- The orchestrator does NOT exit non-zero on these findings (see PASS_KIND_STOP_ON_ERROR). + -- "diagnostic" — every `error`/`warning` finding is written to the report file. -- Report severity is independent from process exit policy. kind = "diagnostic", deps = {"scan-source", "word-counts", "components", "emission-model"}, @@ -206,16 +205,13 @@ local function request_roots_for_group(args, group_name) end end --- Pass-kind taxonomy: Which kinds stop the build on errors? --- +-- Pass-kind taxonomy: findings always print. No pass kind stops the build. -- Report severity is independent from process exit policy. --- A "diagnostic" pass still writes every `error`/`warning` finding into its report file, --- but `report_validation_errors` returns early for non-stopping kinds, so nothing is printed to stderr and the orchestrator does not exit non-zero. --- Adding a new pass kind requires listing it here explicitly; An unknown kind must not silently fall back to "true". +-- Adding a new pass kind requires listing it here explicitly; an unknown kind must not silently fall back to "true". local PASS_KIND_STOP_ON_ERROR = { ["shared"] = false, - ["header-output"] = true, - ["validation"] = true, + ["header-output"] = false, + ["validation"] = false, ["diagnostic"] = false, ["report"] = false, } @@ -693,19 +689,19 @@ end -- Main Orchestrator -- ════════════════════════════════════════════════════════════════════════════ ---- (internal) If the pass's kind is in PASS_KIND_STOP_ON_ERROR and it reported errors, write each error to stderr. ---- Returns true if any validation errors were reported. +--- (internal) Write every pass error to stderr. +--- Returns true only when the pass kind still stops the build. --- @param pass_name string --- @param pass PassDescriptor --- @param result PassResult --- @return boolean local function report_validation_errors(pass_name, pass, result) - local has_errors = result.errors and #result.errors > 0 - if not (has_errors and PASS_KIND_STOP_ON_ERROR[pass.kind]) then return false end + local has_errors = result.errors and #result.errors > 0 + if not has_errors then return false end for _, e in ipairs(result.errors) do io.stderr:write(string.format("[%s] line %d: %s\n", pass_name, e.line or 0, e.msg or "")) end - return true + return PASS_KIND_STOP_ON_ERROR[pass.kind] == true end --- (internal) Run each pass in `order` in topological sequence.