Background
Today, ltl.delay clock handling is inconsistent across layers:
- SVA allows clocked and unclocked
##N delays (clock from @(...) or default clocking context).
- FIRRTL
circt_ltl_delay currently has no explicit clock operand and can rely on surrounding context (e.g., circt_ltl_clock).
- CIRCT lowering/emission currently requires special handling to recover/propagate clock context.
This mismatch leads to implicit clock inference spread across passes and conversions, making behavior hard to reason about and review.
Core Problem
We currently rely on implicit clock inference in multiple places.
The long-term direction is to make delay clock semantics explicit and uniform, while still preserving compatibility during migration.
Candidate Implementation Strategies
-
Aggressive full switch to explicit delay clocks (single large change)
- Pros: direct end-state.
- Cons: large review surface; placeholder-clock semantic issues.
-
Optional clock on ltl.delay (incremental transition)
- Pros: good intermediate compatibility; smaller, reviewable changes.
- Cons: temporary mixed mode (clocked + unclocked delay).
-
Two ops coexisting (delay + clocked_delay)
- Pros: explicit separation.
- Cons: duplicated infrastructure and migration overhead.
Here use Option 3 as the intermediate path, then converge to the explicit-clock end state.
Design
ltl.delay (unclocked)
%result = ltl.delay %input, <delay> [, <length>] : <type>
Pure sequence delay with no clock. Clock is provided externally by an enclosing ltl.clock operation or resolved by the InferLTLClocks pass.
ltl.clocked_delay (explicitly clocked)
%result = ltl.clocked_delay %clk, <edge>, %input, <delay> [, <length>] : <type>
Self-contained clocked delay. The clock and edge parameters are mandatory. Produced by:
InferLTLClocks pass (from unclocked ltl.delay + enclosing ltl.clock context)
ImportVerilog (when assertion clock context is present)
- Direct construction in frontends with explicit clock intent
Clock Lifecycle
ltl.clock and ltl.clocked_delay serve different roles at different pipeline stages:
- Before
InferLTLClocks: ltl.clock broadcasts a clock to a subtree of delays; ltl.clocked_delay carries its own clock. Both may coexist.
InferLTLClocks: Converts ltl.delay under ltl.clock to ltl.clocked_delay, then eliminates the ltl.clock op. Clones subtrees for multi-clock fan-out.
- After: Only
ltl.clocked_delay (explicit clock) and unresolved ltl.delay (warning) remain. No ltl.clock.
- ExportVerilog: Extracts clock from
clocked_delay, hoists to SVA @(edge clk), emits delays as ##N.
InferLTLClocks pass
- Propagates clock/edge from
ltl.clock to unclocked ltl.delay, producing ltl.clocked_delay.
- Removes the
ltl.clock op — clock now lives solely on clocked_delay.
- Clones delay subtrees for multi-clock fan-out.
- Warns on unresolved
ltl.delay (no enclosing clock).
Implementation Plan (3 PRs)
PR 1: LTL IR foundation — ClockedDelayOp + fold infrastructure
- Pure LTL dialect changes. No conversions, no passes.
This is the foundational PR that all others depend on.
PR 2: InferLTLClocks pass
- New standalone pass + registration infrastructure.
PR 3: ExportVerilog + ImportVerilog adaptation
- Update both Verilog conversions to handle dual-op model.
This RFC was discussed by me and @Claude-opus-4.6, and reviewed by myself
Background
Today,
ltl.delayclock handling is inconsistent across layers:##Ndelays (clock from@(...)or default clocking context).circt_ltl_delaycurrently has no explicit clock operand and can rely on surrounding context (e.g.,circt_ltl_clock).This mismatch leads to implicit clock inference spread across passes and conversions, making behavior hard to reason about and review.
Core Problem
We currently rely on implicit clock inference in multiple places.
The long-term direction is to make delay clock semantics explicit and uniform, while still preserving compatibility during migration.
Candidate Implementation Strategies
Aggressive full switch to explicit delay clocks (single large change)
Optional clock on
ltl.delay(incremental transition)Two ops coexisting (
delay+clocked_delay)Here use Option 3 as the intermediate path, then converge to the explicit-clock end state.
Design
ltl.delay(unclocked)Pure sequence delay with no clock. Clock is provided externally by an enclosing
ltl.clockoperation or resolved by theInferLTLClockspass.ltl.clocked_delay(explicitly clocked)Self-contained clocked delay. The
clockandedgeparameters are mandatory. Produced by:InferLTLClockspass (from unclockedltl.delay+ enclosingltl.clockcontext)ImportVerilog(when assertion clock context is present)Clock Lifecycle
ltl.clockandltl.clocked_delayserve different roles at different pipeline stages:InferLTLClocks:ltl.clockbroadcasts a clock to a subtree of delays;ltl.clocked_delaycarries its own clock. Both may coexist.InferLTLClocks: Convertsltl.delayunderltl.clocktoltl.clocked_delay, then eliminates theltl.clockop. Clones subtrees for multi-clock fan-out.ltl.clocked_delay(explicit clock) and unresolvedltl.delay(warning) remain. Noltl.clock.clocked_delay, hoists to SVA@(edge clk), emits delays as##N.InferLTLClockspassltl.clockto unclockedltl.delay, producingltl.clocked_delay.ltl.clockop — clock now lives solely onclocked_delay.ltl.delay(no enclosing clock).Implementation Plan (3 PRs)
PR 1: LTL IR foundation —
ClockedDelayOp+ fold infrastructureThis is the foundational PR that all others depend on.
PR 2:
InferLTLClockspassPR 3: ExportVerilog + ImportVerilog adaptation
This RFC was discussed by me and @Claude-opus-4.6, and reviewed by myself