Skip to content

[MooreToCore] Observe class-backed event waits#10630

Open
AmurG wants to merge 1 commit into
llvm:mainfrom
AmurG:pr-vif-edge-wait-main
Open

[MooreToCore] Observe class-backed event waits#10630
AmurG wants to merge 1 commit into
llvm:mainfrom
AmurG:pr-vif-edge-wait-main

Conversation

@AmurG

@AmurG AmurG commented Jun 11, 2026

Copy link
Copy Markdown
Contributor

When getValuesToObserve finds no out-of-region operands for a moore.wait_event — which happens when the awaited storage is class-backed or reached through a virtual interface handle, invisible to observer analysis — the lowered llhd.wait observes 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.wait that 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

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>
@circt-bot

circt-bot Bot commented Jun 11, 2026

Copy link
Copy Markdown

Results of circt-tests run for afab026 compared to results for 23609cf: no change to test results.

Comment on lines +11 to +66
// 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
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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
endclass

The 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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants