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 490d99d8a..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() @@ -2823,7 +2849,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 +3009,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, } } 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