From 43ec15283bdefffc54767342204e23bf9c82a257 Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 23 Aug 2026 12:01:18 +0000 Subject: [PATCH 1/2] trs: unwritten wire value planes init to the undet pattern, not zero MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Parity bug from the matx corpus (MATX-PARITY-HANDOFF.md bug 1, witness SharedRegfileTest): `mkDWire _` read back 00000000 under trs where Bluesim reads aaaaaaaa. The handoff suspected the exporter's instance-arg lowering; the mechanism is elsewhere — there is no exporter bug: aDropUndet (at compile) collapses `if whas then wget else _` into a bare wget ("chosen values", ADropUndet.hs), so the .ba already contains the collapsed read (BIR-dump witness: the def named IF_w_whas_THEN_w_wget_ELSE_DONTCARE is a plain MethCall). Both engines therefore observe the UNSET wire's raw value plane, and Bluesim's MOD_Wire constructor fills it with write_undet's 0xAA pattern (wide_data.cxx) while trs's RWire/BypassWire constructors used Value::zero. The prim-init audit shows these two were the only observable value planes initialized to zero — every other prim class already inits Value::undet. Fix: both constructors init Value::undet. Initial-data-only change: no layout or version bumps (arena_attach copies whatever the prim holds; baked window state changes value, not shape, and stays byte-equal to a Bluesim run at the same instant). Fixture witness (docs fixture MxParityDw): dwire_default=aaaaaaaa, byte-identical to the Bluesim reference. Co-Authored-By: Claude Claude-Session: https://claude.ai/code/session_01CerPH99xuDTaGhaBQ3wwqS --- src/trs/crates/trs-interp/src/prim.rs | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/src/trs/crates/trs-interp/src/prim.rs b/src/trs/crates/trs-interp/src/prim.rs index 490d99d8a..56f88ef35 100644 --- a/src/trs/crates/trs-interp/src/prim.rs +++ b/src/trs/crates/trs-interp/src/prim.rs @@ -2823,7 +2823,11 @@ impl RWire { let width = if zero_width { 0 } else { carg(consts, 0) as u32 }; RWire { width, - value: Value::zero(width.max(1)), + // the unset value plane is OBSERVABLE: aDropUndet collapses + // `if whas then wget else _` to bare wget at compile, so an + // unwritten wire's data must match Bluesim's write_undet + // fill (MOD_Wire ctor), not zero + value: Value::undet(width.max(1)), valid: false, written: false, slot: None, @@ -2979,7 +2983,9 @@ impl BypassWire { let width = if zero_width { 1 } else { carg(consts, 0) as u32 }; BypassWire { width, - value: Value::zero(width.max(1)), + // same contract as RWire: pre-first-write reads must see + // Bluesim's write_undet fill (see RWire::new) + value: Value::undet(width.max(1)), slot: None, } } From 37a5bd5ad31efbdcfad99fad9da2141d75d3c072 Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 23 Aug 2026 13:46:21 +0000 Subject: [PATCH 2/2] trs: crossed reads see post-edge state in the after-edge combo pass MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Parity bug from the matx corpus (MATX-PARITY-HANDOFF.md bug 2, witness ClockGatingTest): with two software-driven MakeClock domains edged coincidently by one testbench action, a clock-gate detector of the toggle/toggleDly shape (matx mkClkGate2Bool: a CrossingReg toggle in the observed domain, crossed-read + delayed sample in the observer domain) reads gate=0 steadily under trs where Bluesim reads gate=1 — the crossed toggle and its delayed sample compare equal every instant. Mechanism: Bluesim's MOD_Reg::METH_crossed backdates a same-instant write (returns prev_value) ONLY outside the combinational schedule — `bk_is_same_time(written_at) && !bk_is_combo_sched()` — so a crossed read from an after-edge clock-crossing rule (e.g. mkNullCrossingWire's export rule, which is how such detectors leave their domain) sees the POST-edge value. trs backdated unconditionally on written_at == now, so the export re-read stayed pre-edge and equaled the delayed sample forever. No other Bluesim prim checks bk_is_combo_sched (SyncVar, DualPortRam, LatchCrossingReg backdate unconditionally); trs already matches those. Fix, mirroring the kernel: - prim.rs: a thread-local IN_COMBO_SCHED flag (bk_is_combo_sched); Reg::crossed exempts backdating while it is set. - lib.rs: the advance loop stamps the flag around the PG_FINAL early-rule pass — the only executor of clock-crossing rules on every tier (compiled edges skip early rules; the central player bails on early comps; CrossingReg* is never arena-backed, so compiled reads bounce to the boxed prim). - lib.rs eval: during the combo pass, DATA defs recompute at post-edge state instead of serving their arena slots — Bluesim's after-edge schedule computes its def cone as fresh locals; edge-time slots hold values whose crossed reads were still backdated. Rule-fire defs (can_fire/will_fire) and names with no def-table entry still serve from their slots. Fixture MakeClkCross joins the regress battery (23 checks), pinning both semantics: interp and artifact byte-identical to Bluesim, with the pre-fix run diverging in exactly the corpus signature. The first-translation fixture (wire-only crossing, no CrossingReg) was parity-OK before and after — mkNullCrossingWire's early rule updates its wire in the final pass, which quantizes coincident-edge sampling either way; reproducing needed the crossed-read shape. Batteries 23/23 in both modes; the corpus reseal stamp lands on the PR. No artifact format changes: old artifacts pick up the fixed semantics on re-run because early rules execute on the runtime tier. Co-Authored-By: Claude Claude-Session: https://claude.ai/code/session_01CerPH99xuDTaGhaBQ3wwqS --- src/trs/crates/trs-interp/src/lib.rs | 45 +++++++++++++++++----- src/trs/crates/trs-interp/src/prim.rs | 28 +++++++++++++- src/trs/tests/regress/MakeClkCross.bsv | 53 ++++++++++++++++++++++++++ src/trs/tests/regress/run.sh | 4 ++ 4 files changed, 120 insertions(+), 10 deletions(-) create mode 100644 src/trs/tests/regress/MakeClkCross.bsv diff --git a/src/trs/crates/trs-interp/src/lib.rs b/src/trs/crates/trs-interp/src/lib.rs index c9c855d86..818f98eca 100644 --- a/src/trs/crates/trs-interp/src/lib.rs +++ b/src/trs/crates/trs-interp/src/lib.rs @@ -1421,18 +1421,39 @@ impl Interp { return v; } // JIT mode: fire signals and schedule-position defs live - // in arena slots kept current by the native scheds + // in arena slots kept current by the native scheds. In + // the combo (PG_FINAL early-rule) pass, DATA defs must + // recompute at post-edge state — Bluesim's after-edge + // schedule computes its def cone as fresh locals — so + // only rule-fire defs (edge-schedule state the combo fn + // reads as ports) still serve from their slots; a data + // slot holds the EDGE-time value, where a crossed read + // was still backdated if !self.jit_arena_ptr.is_null() { if let Some(&(base, w)) = self.jit_eager_slots.get(&(inst, *name)) { - let words = ((w.max(1) as usize) + 63) / 64; - let limbs = unsafe { - std::slice::from_raw_parts( - self.jit_arena_ptr.add(base as usize), - words, - ) + let use_slot = if crate::prim::in_combo_sched() { + let module = self.module_of(inst); + let mir = self.mods[module].ir; + // a name with no def-table entry has no expr + // to recompute — its slot stays authoritative + self.mods[module].defs.get(name).map_or(true, |&di| { + let p = &self.d.modules[mir].defs[di].props; + p.can_fire || p.will_fire + }) + } else { + true + }; + if use_slot { + let words = ((w.max(1) as usize) + 63) / 64; + let limbs = unsafe { + std::slice::from_raw_parts( + self.jit_arena_ptr.add(base as usize), + words, + ) + } + .to_vec(); + return Value::from_limbs64(w.max(1), limbs); } - .to_vec(); - return Value::from_limbs64(w.max(1), limbs); } } let module = self.module_of(inst); @@ -4855,6 +4876,11 @@ impl Interp { if self.vcd.is_active() { self.vcd_event(t); } + // kernel combinational schedule (bk_is_combo_sched): + // crossed reads inside this pass see POST-edge state — + // the same-instant backdate applies to destination- + // domain logic only (MOD_Reg::METH_crossed) + crate::prim::set_combo_sched(true); for rci in std::mem::take(&mut fired_this_slice) { let rc = &rcomps[rci]; if rc.early.is_empty() { @@ -4892,6 +4918,7 @@ impl Interp { } } } + crate::prim::set_combo_sched(false); } // steady state may only begin here (fusion compiles after diff --git a/src/trs/crates/trs-interp/src/prim.rs b/src/trs/crates/trs-interp/src/prim.rs index 56f88ef35..e5c2467af 100644 --- a/src/trs/crates/trs-interp/src/prim.rs +++ b/src/trs/crates/trs-interp/src/prim.rs @@ -23,6 +23,26 @@ pub(crate) fn quiet_engine() -> bool { QUIET_ENGINE.with(|c| c.get()) } +thread_local! { + /// Kernel combinational-schedule flag (bk_is_combo_sched): true + /// while the after-edge PG_FINAL pass runs clock-crossing "early" + /// rules. The owning Interp stamps it around that pass; early + /// rules always execute on the interp tier (compiled edges skip + /// them), so one flag covers every artifact tier. + pub(crate) static IN_COMBO_SCHED: std::cell::Cell = + const { std::cell::Cell::new(false) }; +} + +/// See `IN_COMBO_SCHED`: the advance loop stamps this around the +/// after-edge early-rule pass. +pub(crate) fn set_combo_sched(on: bool) { + IN_COMBO_SCHED.with(|c| c.set(on)); +} + +pub(crate) fn in_combo_sched() -> bool { + IN_COMBO_SCHED.with(|c| c.get()) +} + thread_local! { /// Reference Bluesim reads a RegFile/BRAM load file when the model /// object is constructed, which is run time; a `.mem` is an input to @@ -2382,9 +2402,15 @@ impl Prim for Reg { match method { "read" | "get" | "_read" => self.load(), // crossing read: a same-instant write is not yet visible + // to destination-domain LOGIC — but the after-edge combo + // pass (clock-crossing early rules) reads post-edge state, + // exactly like METH_crossed's !bk_is_combo_sched guard + // (bs_prim_mod_reg.h:92). Without the exemption a gate + // detector's export wire re-reads the pre-edge value and + // its coincident-edge sampler never observes a change. // (crossing regs are never arena-backed) "crossed" => { - if self.written_at == now { + if self.written_at == now && !in_combo_sched() { self.prev.clone() } else { self.value.clone() diff --git a/src/trs/tests/regress/MakeClkCross.bsv b/src/trs/tests/regress/MakeClkCross.bsv new file mode 100644 index 000000000..7a217850e --- /dev/null +++ b/src/trs/tests/regress/MakeClkCross.bsv @@ -0,0 +1,53 @@ +// Coincident MakeClock-domain edges + a CrossingReg sampled across them +// (the matx mkClkGate2Bool shape): a CrossingReg toggle in domain A +// (crossed-side = domain B), a domain-B register sampling .crossed, and +// the gate compare exported to the default domain via mkNullCrossingWire, +// whose write rule is a clock_crossing_rule in domain B's after-edge +// (combo-sched) pass. Pins two semantics: a crossed read backdates to +// the pre-edge value for destination-domain LOGIC only — the combo pass +// reads post-edge (MOD_Reg::METH_crossed's !bk_is_combo_sched arm) — and +// data defs consumed by early rules recompute at post-edge state rather +// than serving edge-time slot values. +import Clocks::*; + +(* synthesize *) +module sysMakeClkCross(Empty); + MakeClockIfc#(Bool) aclkIfc <- mkUngatedClock(False); + Clock aclk = aclkIfc.new_clk; + Reset arst <- mkAsyncResetFromCR(0, aclk); + + MakeClockIfc#(Bool) bclkIfc <- mkUngatedClock(False); + Clock bclk = bclkIfc.new_clk; + Reset brst <- mkAsyncResetFromCR(0, bclk); + + // domain-A toggle in a CrossingReg whose crossed side is domain B + CrossingReg#(Bool) tog <- mkNullCrossingReg(bclk, False, clocked_by aclk, reset_by arst); + rule tog_r; + tog <= !tog; + endrule + + // domain-B delayed sample of the crossed value (starts True, like + // mkClkGate2Bool's toggleDly) + Reg#(Bool) dly <- mkReg(True, clocked_by bclk, reset_by brst); + rule dly_r; + dly <= tog.crossed; + endrule + + // the gate-detector output: a domain-B combinational value + Bool gate = (tog.crossed != dly); + + Clock cc <- exposeCurrentClock; + ReadOnly#(Bool) gateV <- mkNullCrossingWire(cc, gate); + ReadOnly#(Bool) togV <- mkNullCrossingWire(cc, tog.crossed); + ReadOnly#(Bool) dlyV <- mkNullCrossingWire(cc, dly); + + Reg#(UInt#(8)) cyc <- mkReg(0); + rule drive; + cyc <= cyc + 1; + Bool lvl = (pack(cyc)[0] == 1); + aclkIfc.setClockValue(lvl); + bclkIfc.setClockValue(lvl); + $display("cyc %0d lvl=%b tog=%b dly=%b gate=%b", cyc, lvl, togV, dlyV, gateV); + if (cyc == 12) $finish(0); + endrule +endmodule diff --git a/src/trs/tests/regress/run.sh b/src/trs/tests/regress/run.sh index d61e7d1b0..987a8f3ed 100755 --- a/src/trs/tests/regress/run.sh +++ b/src/trs/tests/regress/run.sh @@ -282,6 +282,10 @@ check_dyn() { # name top errtag if ! cmp -s "$SRC/$name.expected" gota.out; then echo "FAIL $name (art stdout)"; diff "$SRC/$name.expected" gota.out | head -3; fail=1; return; fi echo "PASS $name" } +# coincident MakeClock domains + CrossingReg crossed-read semantics: +# destination-domain logic backdates to pre-edge, the after-edge combo +# pass reads post-edge (gate detectors break as steady-0 otherwise) +check MakeClkCross sysMakeClkCross check_dyn DynSched sysDynSched G0100 check_dyn DynSchedBoth sysDynSchedBoth G0101 check_dyn DynSchedSelf sysDynSchedSelf G0096