[MooreToCore] Observe class-backed event waits#10630
Conversation
When all operands of a `moore.wait_event`'s `detect_event` ops are read through storage that is invisible to the observer analysis -- for example class property storage or virtual interface members accessed through a pointer -- `getValuesToObserve` returns an empty set. The resulting `llhd.wait` then has no observed values, so the process suspends and never wakes up again, hanging the simulation at that wait forever. Fix this by observing the values sampled before the wait in that case: materialize the pre-wait sampled detect inputs to their converted HW value types and use them as the observed values of the `llhd.wait`, so the process wakes up and re-checks for events whenever any of them changes. Behavior is unchanged whenever the observer analysis finds at least one value to observe. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
| // All event operands are read out of class-backed storage that is created | ||
| // inside the wait body, so observer analysis cannot see anything to observe | ||
| // outside the region. Without observing the sampled pre-wait values, the | ||
| // resulting `llhd.wait` would have no observed values and hang forever. | ||
| // CHECK-LABEL: hw.module @ClassPropertyWaitEvent | ||
| moore.module @ClassPropertyWaitEvent() { | ||
| moore.procedure initial { | ||
| // CHECK: llhd.process { | ||
| // CHECK: cf.br ^[[WAIT:.+]] | ||
| // CHECK: ^[[WAIT]]: | ||
| // CHECK: func.call @malloc | ||
| // CHECK: [[REF:%.+]] = builtin.unrealized_conversion_cast {{%.+}} : !llvm.ptr to !llhd.ref<i1> | ||
| // CHECK: [[EVENT:%.+]] = llhd.prb [[REF]] : i1 | ||
| // CHECK: llhd.wait ([[EVENT]] : i1), ^[[CHECKBB:.+]] | ||
| // CHECK: ^[[CHECKBB]]: | ||
| // CHECK-NOT: comb.icmp | ||
| // CHECK: cf.br | ||
| moore.wait_event { | ||
| %obj = moore.class.new : <@ClassEventWait> | ||
| %ref = moore.class.property_ref %obj[@changed] : <@ClassEventWait> -> !moore.ref<i1> | ||
| %v = moore.read %ref : <i1> | ||
| moore.detect_event any %v : i1 | ||
| } | ||
| moore.return | ||
| } | ||
| moore.output | ||
| } | ||
|
|
||
| // Same situation for edge-sensitive waits on storage behind a pointer that | ||
| // observer analysis cannot see: the sampled pre-wait value must show up as | ||
| // the observed value of the `llhd.wait`, and the edge detection compares it | ||
| // against the value probed after the wait. | ||
| // CHECK-LABEL: hw.module @DynamicRefPosedgeWait | ||
| moore.module @DynamicRefPosedgeWait() { | ||
| moore.procedure initial { | ||
| // CHECK: llhd.process { | ||
| // CHECK: cf.br ^[[WAIT:.+]] | ||
| // CHECK: ^[[WAIT]]: | ||
| // CHECK: [[BEFORE:%.+]] = llhd.prb {{%.+}} : i1 | ||
| // CHECK: llhd.wait ([[BEFORE]] : i1), ^[[CHECKBB:.+]] | ||
| // CHECK: ^[[CHECKBB]]: | ||
| // CHECK: [[AFTER:%.+]] = llhd.prb {{%.+}} : i1 | ||
| // CHECK: [[TRUE:%.+]] = hw.constant true | ||
| // CHECK: [[TMP1:%.+]] = comb.xor bin [[BEFORE]], [[TRUE]] | ||
| // CHECK: [[TMP2:%.+]] = comb.and bin [[TMP1]], [[AFTER]] | ||
| // CHECK: cf.cond_br [[TMP2]] | ||
| moore.wait_event { | ||
| %arg0 = llvm.mlir.zero : !llvm.ptr | ||
| %ref = builtin.unrealized_conversion_cast %arg0 : !llvm.ptr to !moore.ref<i1> | ||
| %v = moore.read %ref : <i1> | ||
| moore.detect_event posedge %v : i1 | ||
| } | ||
| moore.return | ||
| } | ||
| moore.output | ||
| } |
There was a problem hiding this comment.
Hmmm looking at this, not having any observable values actually seems like the correct behavior: all the class-related things are constructed entirely within the moore.wait_event region, so nothing on the outside can ever change them. Might be an artifact of the test though.
Do you have any SV examples that exhibit this? I remember discussing with @Scheremo in the past that SV has very limited signal-like behavior for classes and class variables. I wonder how much of the wakeup semantics we do need for classes. And if we do, we probably need a mechanism to detect accesses to class-typed SSA values and insert a potentially new wake-up mechanism there. It would be great to have some examples to look at.
There was a problem hiding this comment.
Fair — the test as written is artificial in exactly the way you describe (everything constructed inside the region), and I agree empty-observe is arguably correct for that shape in isolation. The real-world case we keep hitting in sv-tests UVM rows is a virtual interface reached through class storage:
class driver_t;
virtual dut_if vif; // set externally, e.g. via uvm_config_db
task run();
forever begin
@(posedge vif.clk); // the wait reads vif through a class field
drive_one_txn();
end
endtask
endclassThe detect input is a load through the class handle, so observer analysis finds no out-of-region operands, the lowered llhd.wait observes nothing, and the process never wakes — this is what hangs the uvm_*_vif_config_db family in sv-tests at runtime (the handle is written by another process via config_db before run_phase). Sampling the pre-wait values as observed values is admittedly a conservative stopgap: it wakes on changes to the values the event detection actually compares, at the cost of some spurious wakeups.
I agree the right long-term shape is a mechanism that detects accesses to class-typed SSA values and inserts an explicit wake-up — happy to write that up as a proposal (and loop in @Scheremo's earlier discussion). Would you prefer this stopgap land gated to the empty-observe case as-is while that design is worked out, or hold this PR for the mechanism? Meanwhile I can also rework the test to the vif-through-class-field shape above so it documents the real pattern rather than the artificial one.
When
getValuesToObservefinds no out-of-region operands for amoore.wait_event— which happens when the awaited storage is class-backed or reached through a virtual interface handle, invisible to observer analysis — the loweredllhd.waitobserves nothing and the process can never wake up.In that case, materialize the pre-wait sampled detect values to HW value types and use them as the wait's observed values, so the process wakes on changes to the very values the event detection compares. Behavior is unchanged when observer analysis does find values to observe.
Includes a regression test with a class-allocated event source (previously lowered to an observer-less
llhd.waitthat hangs forever) and a dynamic-ref posedge wait.Part of a series upstreaming SystemVerilog/UVM simulation support validated against the sv-tests suite.
🤖 Generated with Claude Code