Conversation
- implement `unary_-` for Bits: implicit conversion to SInt[W+1] via `.uint.signed` before negation, matching the existing UInt behavior; covered in DFDecimalSpec (assignment codegen + width-overflow error) - type-system reference: document that bitwise NOT `~` also applies to Bits/UInt vectors and preserves the argument type (incl. Verilog/VHDL mapping notes), and add a dedicated unary negation subsection with the per-type result rules (SInt/Int/Double preserved, UInt[W]/Bits[W] to SInt[W+1]) - from-verilog guide: new admonition contrasting Verilog's context-width wrap-around negation with DFHDL's exact SInt[W+1] result, including the same-width wrap-around form `(-x.sint).bits` and the code-smell note about sign-bit preservation Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- `lsbitsAt(baseIdx, selWidth)` selects selWidth bits with the LSB anchored at baseIdx (Verilog `[baseIdx +: selWidth]`); `msbitsAt` anchors the MSB at baseIdx (Verilog `[baseIdx -: selWidth]`); both generalize msbits/lsbits and desugar to the existing ApplyRange - available on Bits (Bits result) and on UInt/SInt (UInt result, matching the unsigned-slice rule); assignability is preserved so a part-select can be an assignment target, like a `(hi, lo)` slice - bounds are checked at compile time for literal arguments and at elaboration for parametric ones (Arg.Width on the width, BitIndex on both computed endpoints) - DFBitsSpec coverage: all three types, parametric base (folded print), assignment target, out-of-range and zero-width errors - docs: part-select bullet + examples in the type-system reference and a direct `-:`/`+:` mapping in the from-verilog guide Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…ardware generation Rework the FullAdderN docExample to use classic hardware-generation style (a `for`-loop ripple of FullAdder1 instances with a sibling carry chain) instead of a `List`, and update the generated Verilog/VHDL references accordingly. This surfaced two DFacsimile simulation gaps, now fixed: - The scope walk read connection sources eagerly, so a `<>` whose driver appears later in source order failed with "reading a value before it is driven". Connections are continuous (order-free), so forward-read connection sinks are now stood in with MOV placeholders patched at scope finalize. Whole-driven sinks get one whole-value placeholder; partially-driven sinks (bit/range connections, e.g. a ripple carry) get independent per-bit placeholders. A single whole placeholder for a partial sink coalesces independent bits into one node and turns a bit-acyclic ripple into a false combinational cycle. - FullAdderNSimSpec dropped the dead internal-port assertions and now drives and observes only the public ports. Adds OrderFreeConnectSimSpec (hierarchy-free: whole + partial forward references and a multi-hop forward chain) covering the resolution on both simulation tiers. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Two pre-existing gaps left FALL_THROUGH a no-op for some loop forms: - SimplifyRTOps dropped the loop's tags when rewriting a for loop into a while loop, so a FALL_THROUGH for loop lost its marker. Carry the for block's tags onto the generated while block. - DropRTWaits only emitted the `fallThrough` sub-step for while loops with a non-empty body; an empty-bodied FALL_THROUGH while loop silently ignored the tag. Emit the same `fallThrough` sub-step in the empty-body branch. With both fixes a FALL_THROUGH loop of any form lowers to the fall-through cascade that skips it with zero cycles when its guard is false on entry. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Implements the RT-process step hooks and fall-through loops directly in the DFacsimile simulator (M3), verified cycle-exact against the dropRTProcess FSM-lowering oracle on both tiers: - onEntry / onExit: run at a step's non-self transition edges (skipped on a self-transition), matching the FSM lowering's static source-vs-target gate. - fallThrough: a same-cycle conditional cascade along declaration order, with the circular-fall-through guard, running each traversed step's onEntry. - FALL_THROUGH while loops (empty and non-empty body): a zero-cycle skip to the following park when the guard is false on entry. Hook step blocks (onEntry/onExit/fallThrough) are no longer time constructs and are skipped by the ordered body walk; they are emitted at the transition edges. Two cases are cleanly rejected rather than mis-simulated: - FALL_THROUGH for loops, whose FSM lowering reads the pre-reset (stale) iterator in the fall-through guard (hardware skips the loop every other run). - onEntry on the first step, which is process-prologue content that folds into the reset state and is not yet integrated with the boot machinery. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
A reference for continuing to improve DFacsimile: the architecture (Netlist / WideOps / two tiers / ProcLowering FSM model), the fidelity contract (match the FSM-lowering stages bug-for-bug, verified by the dropRTProcess lockstep oracle), the testing + divergence-debugging methodology, and the gotchas banked this milestone. Registered in CLAUDE.md's Claude Instructions. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The process prologue is the leading statements together with the first step's onEntry, so DFacsimile now vets both in the bootstrap gate (mirroring DropRTWaits Rule 6) and folds both into the time-zero state, leading statements first. The onEntry folds only when its step is the process's first state: with a leading wait or loop the FSM lowering keys the generated initial block on that construct instead and leaves the hook on its transition edge. Matching the lowering required moving hook emission from the goto site to the transition's landing point. The lowering plants a goto's hooks at the end of the flattened state body, i.e. after FlattenStepBlocks relocated the inter-step trailing statements and cloned the prologue there, so onExit is now emitted once per execution path right before the state write and onEntry inside the state landing itself. As a consequence a sequential entry into a nested step fires the parent's onExit and the child's onEntry, which is what the flattened form does. Two further fidelity gaps surfaced and are fixed here: - A hook-carrying step, or one whose dispatch's first time-consuming action is hook-carrying, is never a FirstStepFusion candidate; it is now forced to keep a state of its own, so its hooks have a real FSM edge to land on. - The fall-through cascade stops after re-entering the step the transition left, not before it (the lowering's guard sits inside handleNextStep, after that step's onEntry and state write). It now also emits only the next state's onEntry and state write, skipping the traversed state's own body, and re-runs the prologue when it passes the last state. A cascade past the last state of a process with a fused entry state is unsupported rather than approximated. FALL_THROUGH for loops remain the one deferred M3 case (the stale-iterator lowering bug). Verified with eight new lockstep DUTs against the dropRTProcess oracle on both tiers, each validated by temporarily breaking the code it targets; the SERV, protocol-engine and SHA-farm benchmarks produce bit-identical architectural signatures before and after. The user-guide correction: the prologue's "does not run on an explicit jump to the first step" holds for the leading statements but not for the onEntry, which fires on every non-self entry into the first step. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
A `fallThrough` cascade advanced to the declaration-order successor in the flat
state list (`nextBlocks`), which is not where the skipped step goes. A loop step
whose body consumes cycles is followed in that list by its own body's first
state, so skipping the loop entered the body of the very loop being skipped:
case State.S_0 =>
state.din := State.S_1
if (!go) state.din := State.S_1_0 // the loop body's wait, not the exit
DropRTProcess now cascades to the step's default exit -- the target of the last
Goto on its dispatch path, hook bodies excluded -- which is where control lands
when the step neither stays put nor takes an earlier branch. This is what Rule 4
always claimed ("the one S itself eventually transitions to via its own Goto");
declaration order was a proxy that only coincides when the step holds no states
of its own and its exit is the next one declared.
Two consequences:
- Rule 5 needs a second stop condition. Declaration order is a single ring, so a
cascade always came back to its origin; exit gotos form an arbitrary graph, so
a chain can close on itself without reaching it. It now also stops on a step
the same chain already passed through. This is load-bearing, not defensive:
the circular fall-through case recurses forever without it.
- The wrap-around test is tighter. The prologue re-ran whenever the cascade
skipped the last state; it now also requires the cascade to land on the first,
since the last state's exit is no longer necessarily the first.
DFacsimile mirrors the rule in `cascadeFrom` (fidelity, locked decision 11),
resolving only gotos that name their target -- `NextStep`/`ThisStep` keep
falling back to the sequential park walk, which is how FlattenStepBlocks
resolves them.
The circular fall-through golden changes for the better: those steps end in
explicit S0/S1/S2 gotos, and the cascade used to divert through the generated
bootstrap state instead of following them. It now matches the plain transitions.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
`FirstStep` resolved to whichever step came first in the flat state list, which
is the synthesized bootstrap when the prologue is not initial-convertible. So it
re-ran the prologue and paid the bootstrap's cycle, while naming that same step
did neither -- even though the docs equate the two forms, and the convertible
case already behaved that way. The meaning of `FirstStep` was decided by whether
constant-folding happened to apply:
z.din := z + 1 z.din := 7
... ...
if (x) state.din := State.S_0 if (x) state.din := State.Accum
else state.din := State.Accum else z.din := d"8'7"
state.din := State.Accum
`FirstStep` now targets the process's own first step, whatever construct yielded
it -- a `def`, a wait, a loop, named or not. The sequential wrap-around still
goes through the bootstrap, which is what re-runs the prologue.
The obvious implementation -- tag the synthesized step, skip tagged steps --
breaks the printability invariant: a stage's printed output must be a complete
contract, so that printing it, re-elaborating it and continuing the pipeline
matches passing the IR along. CombinationalTag and FallThroughTag are safe
because they print (COMB_LOOP:/FALL_THROUGH:); a "this step was synthesized" tag
has no printed form and nothing regenerates it, and DropRTWaits still emits
relative gotos, so the loss is observable. It is not recoverable structurally
either: the printed bootstrap is byte-identical to a user-written first step
followed by an inter-step statement.
So bootstrap synthesis moves out of DropRTWaits into FlattenStepBlocks, where
every goto becomes explicit in the same run and both targets are materialized in
the IR. FlattenStepBlocks gains a pre-phase that synthesizes the step and carries
its identity by value across its own phases, as Phase 4's fusion candidates
already do. DropRTWaits Rule 6 now only documents that the prologue is left in
place, and why the decision cannot live there.
The step is named S_boot: DropRTWaits owns the S_<n> counter and has already
handed out S_0. The forkJoin HDL goldens shift accordingly, and their state enums
no longer deduplicate across the three processes -- previously a coincidence of
identical generated names, not a property. Verified rename-only by normalizing
both outputs through the rename map and diffing: every transition, reset target
and condition is unchanged.
Behaviour is unchanged throughout: every lockstep test passed on the first run
after the move, including ShareGateProc, which pins the trailing-share case where
the bootstrap must stay on the wrap path.
The printability invariant is now the third required stage property in the
stage-authoring guide.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
FirstStepFusion refused any hook-carrying step, which made FALL_THROUGH on a
loop whose body consumes cycles a pessimization rather than a no-op. The marker
kept the loop head as a state -- 4 states, 2 cycles per iteration -- where the
same loop without it fused away to 3 states and 1 cycle, and already skipped
with zero cycles when the guard was false on entry:
case State.S_0 => case State.S_0 =>
state.din := State.S_1 if (go)
if (!go) state.din := State.S_2 y.din := y + d"8'1"
case State.S_1 => state.din := State.S_1_0
if (go) else state.din := State.S_2
y.din := y + d"8'1"
state.din := State.S_1_0
else state.din := State.S_2
A fallThrough holding nothing but its condition no longer blocks fusion. A
fused step consumes no cycle at all, which is strictly more than the
conditional zero-cycle skip the hook asks for. onEntry/onExit still block, and
so does a fallThrough carrying statements of its own: those must land on a real
FSM edge, and a fused step has none.
The condition is materialized at every site as the inlined dispatch's first
decision, `if (cond) <default exit> else <dispatch>`, and being part of the
dispatch it is forwarded like the step's own guards. When it is the negation of
the dispatch's own leading guard over an else path that is nothing but the
default exit, it is dropped outright -- so a FALL_THROUGH loop lowers to output
byte-identical to the unmarked loop, which is what the right-hand column above
is. That is exactly the shape DropRTWaits gives such a loop; the guard tree is
compared with =~ because the hook holds a clone of it, so identity would only
match a bare port.
The process's first step is excluded on both sides: it survives as the reset
bootstrap state, where the hook would have no edge left to run on.
DFacsimile had no such restriction for loops, so this was a live
simulator/hardware divergence and not only an inefficiency -- the new
FallThroughWaitLoopProc lockstep fails against the pre-change lowering. It now
mirrors the rule in hookBlocked, enterStep's fused path, and fusedFallThroughExit,
which resolves the nested-form equivalent of the flat default exit (a trailing
NextStep in a step that owns nested steps enters the first of them, as
FlattenStepBlocks Rule 4 makes of it). Only the user-written-step case needed
new simulator code: a fallThrough over a nested step used to keep a state.
DropRTProcessSpec's cascade-target regression test needed a new DUT, since its
old one now fuses. Its guard register is assigned conditionally on the loop-back
edge, which keeps the loop step a real state -- and that is what leaves the
fallThrough an edge hook, which is what the test is there to pin.
Found while testing and left alone, being pre-existing and unchanged here: a
fallThrough cascade that performs the forever wrap-around re-runs the prologue
only when its step is the declaration-order last state, which a FALL_THROUGH
loop ending a process never is. The materialized hook is faithful to the
unfused lowering, bug included. Fixing it is not local -- the rotation places
the prologue clone inside the step body, which a fall-through is defined to
skip.
Each piece verified by reverting it and watching the tests fail: the stage rule
takes down 5 test instances, the subsumption check the byte-identical golden,
and the simulator change FallThroughFusedStepProc on both tiers.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
A fallThrough cascade that performs the forever wrap-around re-ran the prologue only when its step was the declaration-order last state -- the same declaration-order-vs-actual-exit mistake as 140bb17. A FALL_THROUGH loop ending a process is never that state, since its own body's state follows it, so skipping the loop dropped the wrap-around re-initialization: def S_0: Step = if (!go) S_0 // skip: no cnt.din := 0 else if (go) { cnt.din := cnt + d"8'1"; S_1_0 } else { cnt.din := d"8'0"; S_0 } The fix is not to teach the cascade to recognize the wrap. Look at the shape: `!go` and `go` are complements of one value in one cycle, so the guard-false path can never be taken -- it is dead code. And that path is exactly where FlattenStepBlocks Rule 3 relocates whatever follows the construct: trailing statements, and here the rotation's prologue clone. Materializing a hook whose condition is the negation of the dispatch's leading guard does not merely skip that work, it makes it unreachable. So dropping the hook is the only reading under which the guard-false path means anything, and it is what the loop docs already promise -- "falls through without consuming any cycles ..., continuing at whatever follows the loop". fallThroughSubsumed now allows statements on that path; it still refuses a further goto or step there, which would mean the path is not the single edge the hook names. DFacsimile needs no mirror: enterLoop's fused skip is an ordinary emitCont, so it was the side that was already right, and the new FallThroughWrapLoopProc lockstep is what caught this. The gap survives only where the loop keeps a state -- a park body, or a fusion fallback -- which is the case gotcha 3 of the dfacsimile skill documents. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
`r.din` is now readable, yielding the register's pending next-cycle value: the latest value assigned to it so far in the current cycle body, or the register output when nothing has been assigned yet. Frontend: `REG_DIN` holds the register value and builds an `Alias.RegDIN` read marker lazily, so `r.din := x` still emits the same IR and constructs no member at all. Reads reach the marker through two `ExactOp2Aux` givens (one per operand position) and one `DFVal.TC` given, so no per-operator or per-DFType plumbing is needed. A named `.din` is rejected, since such a binding reads as a snapshot but behaves as a live view. IR: `DFVal.Alias.RegDIN` is an `Alias.Consumer`, so it can never reach an assignment LHS and every generic alias walker treats it as a plain read of the register on its way to `ToED`. Lowering: `ToED` already built the `<reg>_din` shadow, its default and the register update, so reads add four hooks rather than a new stage. Two corrections were needed for read-modify-write: - a statement containing a DIN read is no longer promoted to a concurrent connection, which had moved the read out of the process and could close a combinational loop; - under VHDL the shadow of a DIN-read register is now a process variable published to the design-level signal, since signal assignment evaluates every right-hand side against the pre-process value and would increment only once. Docs: the user guide gains a register model section and a `.din` read section with the generated VHDL and Verilog, whose designs are pinned by the printer specs. Implementation notes in devdocs/reg-din.md. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
A `fallThrough` condition is decided on the transition *into* its step, in the very cycle in which entering that step already assigns registers: an entered step runs its `onEntry`, a `FALL_THROUGH` for-loop resets the iterator the `for`-to-`while` rewrite gave it, and a forever-process wrap-around re-runs the prologue. Reading the registered values there decided the skip on the values the entering state was about to replace, a cycle behind what the condition names. A `FALL_THROUGH` for-loop therefore skipped itself on every other pass. The new `ExplicitFallThroughDIN` stage rewrites every register read in a `fallThrough` block into a `.din` read. It sits between `DropRTWaits` and `FlattenStepBlocks`, the only window where a user-written condition and a loop-generated one exist in one shape: before it, `DropRTWaits` has not yet synthesized the loop's negated guard; after it, `FirstStepFusion` has already consumed the condition. Keeping it out of `DropRTWaits` also keeps that stage's printout honest, the same reasoning that moved the process bootstrap decision into `FlattenStepBlocks`. The block is rebuilt member by member rather than edited in place. A register read shared by two `fallThrough` blocks needs a distinct `.din` read per block, which a reference redirect keyed on the register cannot express, and redirecting an existing member's references from inside a `MetaDesign` corrupts the reference table. Rebuilding also reaches a named intermediate inside the block, so the whole condition reads one way. Two invariants it respects: the `Ident` marker stays last, since `DropRTProcess` and `FirstStepFusion` both read the condition positionally; and each `.din` read is emitted once per reading reference, since an anonymous value may only be read once. `FirstStepFusion` gains `substDin`, which resolves a `.din` read to the pending value within the expansion (one step ahead of `substDcl`, which crosses only the boundary), and `sameAtFusedEntry`, which lets the subsumption check see past the `.din` reads: fusion inlines the dispatch into the hook's own cycle with nothing in between, so both name one value. Without the latter a register-guarded loop would lose the prologue clone on its guard-false path. `DFacsimile` gains `.din` read support (`buildRegDIN`): the alias chain resolves to its declaration with the same `assignTarget` walk a partial write uses, and the slice comes off `env.getOrElse(dcl, readWV(dcl))`, which is the pending value the design already closes with. Position sensitivity is free, since `env` is walked in body order and re-seeded per site program. Because the simulator reads the elaborated IR, where the hook is still a plain read, it mirrors the stage's rewrite with `dinGuardMode` rather than seeing it. That retires the `FALL_THROUGH` for-loop `unsupported` guard, whose only reason was the behavior this removes: the simulator had been right all along. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…range
The `FALL_THROUGH:` block wrapper tagged every loop elaborated under it,
recursively, with no way to shake the tag off once inside. A nested loop
inherited a property that means nothing for it, and the printer's
consolidation hid the second tag, so a multi-iterator comprehension marked
two loops while printing one.
The recursion is right for `COMB_LOOP` and wrong for `FALL_THROUGH`, and the
difference is what the two annotations are. `COMB_LOOP` marks a REGION: a loop
nested in a combinational loop cannot consume cycles either, and dropping the
tag there would let `SimplifyRTOps` rewrite the inner `for` into a step block
inside a combinational loop. `FALL_THROUGH` marks ONE LOOP: skip me for free if
my guard is false on entry. Nothing about it is regional.
So it moves out of the block form onto the loop it marks:
while (FALL_THROUGH(go))
for (i <- FALL_THROUGH(0 until n))
which removes the scoping question rather than answering it: the marker binds
to exactly one loop syntactically, and a nested loop opts in again by saying so.
Per-generator marking in a comprehension becomes expressible for the first time
(`for (i <- 0 until rows; j <- FALL_THROUGH(0 until cols))`); previously it was
unreachable at any cost, since even splitting the comprehension left the wrapper
tagging everything below it.
`LoopFSMPhase` CONSUMES the marker at the two legal positions, unwrapping it and
passing a flag to the loop under construction, so no value or range is ever
tagged and `FALL_THROUGH` is a plugin-replaced stub in the style of
`DFRange.foreach`. Enforcement falls out of the same code path: a legal marker
has been unwrapped by the time the unit is transformed, so whatever still stands
is one the user wrote where no loop can bind it, and `transformUnit` reports it.
That rejects a comprehension guard (a body predicate, whose free skipping would
need an unbounded number of iterator increments in one cycle), part of a
compound condition, and a marker reaching its loop through a `val`.
The IR contract is unchanged: `FallThroughTag` still sits on the loop block, so
`SimplifyRTOps`, `DropRTWaits`, `FlattenStepBlocks`, `FirstStepFusion`,
`DropRTProcess`, `ExplicitFallThroughDIN` and `DFacsimile` are untouched. The
printer moves the marker into the loop header and keeps the enclosing-loop
suppression for `COMB_LOOP` only, where it is still correct; for fall-through it
would now hide a nested opt-in.
Two constraints found while implementing:
- The condition overload must carry a precise DFHDL type. With a generic `T`,
the `while`'s expected `Boolean` propagates into the argument and
`BooleanHack` fires inside the marker, which the plugin rejects.
- The marker is matched by symbol NAME. `dfhdl.hdl` re-exports `LoopOps`, so
call sites resolve to an export forwarder and an owner check never matches.
`HackedGuard` already handles `BooleanHack` this way.
Tests: four position tests via `assertPluginError`; three `SimplifyRTOpsSpec`
goldens for outer-only, inner-only and guard-keeping marking; and
`PrintCodeStringSpec`'s round trip rewritten as a three-deep comprehension with
the outer and innermost marked, pinning that a nested opt-in prints its own
marker. Full suite green (core 173, compiler_stages 830, lib 168, ips 14),
including the 126 DFacsimile lockstep tests over the migrated DUTs.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
`waitUntil`/`waitWhile` cost their one-cycle minimum even when their
condition already holds on entry, and there was no way to ask for the
zero-cycle skip that `FALL_THROUGH` gives a loop. The mark now applies to
them, written on the wait's own condition, the same way it is written on a
loop's condition or range:
waitUntil(FALL_THROUGH(ready))
waitWhile(FALL_THROUGH(busy))
This is the same property, not a new one. `SimplifyRTOps` already lowers a
conditional wait to a loop on the negated condition, so the wait's tags now
carry over to that loop and `waitUntil(FALL_THROUGH(i))` becomes
`while (FALL_THROUGH(!i))`. Everything downstream is untouched: the
`def fallThrough` sub-step `DropRTWaits` synthesizes, `FlattenStepBlocks`,
and `DropRTProcess` all see the loop shape they already handled.
The plugin rewrites `waitUntil(FALL_THROUGH(c))(using dfc, waitScope)` into
`Wait.plugin(c, isUntil, fallThrough)(using dfc)`, so the tag lands on the
`ir.Wait` at construction and the condition value itself is never touched.
The public `waitUntil`/`waitWhile` signatures are unchanged, which keeps the
tag out of reach of hand-written code that could put it on an ED wait, where
it has no printed form. Enforcement falls out of consumption as it does for
the loops: any `FALL_THROUGH` still standing at `transformUnit` is one no
construct could bind, and the error message now names all three legal
positions. An unconditional wait (`1.cy.wait`, `100.ms.wait`) has no
condition that could already hold, and its argument types cannot accept the
mark.
The printer wraps the wait's condition in `FALL_THROUGH(...)`, so the printed
form re-elaborates to the same IR and each wait states for itself whether it
may be skipped.
DFacsimile has to model this or it would silently spend a cycle the hardware
does not. `enterWait` is now the mirror of `enterLoop`'s fall-through path:
same `crossBoundary` forwarding, branches swapped, since here the trigger
being *true* on entry is the skip. Its exit continuation comes from a new
`waitExitConts` map, because the entry edge sits outside the wait's own park
program, and the `walkSeq` fusion-fallback walk lets a marked wait continue
past.
Tests: three plugin-position tests (both legal wait forms, the mark on part
of a compound condition, the mark reaching its wait through a `val`);
goldens through `simplifyRTOps`, the code-string printer, and `dropRTProcess`
(the full FSM, showing the entry into the wait's state jumping straight on
when the condition already holds); and two lockstep DUTs against the real
FSM lowering on both simulator tiers, one of which pins that the skip
decision reads the register value the same cycle just wrote.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Referencing a DFHDL value before its declaration in a design body is a Scala forward reference: legal in a class body, silently yielding `null`. The `null` then reached `asIR` and surfaced as a raw NullPointerException with no position, hierarchy, or hint. `asIR` is the single front-end to IR crossing, so both the DFMember and the DFType overloads now detect the `null` there and raise a regular elaboration error, which carries the operation's position and hierarchy and lets elaboration continue so all forward references surface at once. Rendering such an error inside a method body crashed in turn: `getName` read the design block's instance cache unconditionally, and a block whose elaboration aborted never got one. It now falls back to the declared meta name via the existing safe probe. Documents the rule under Type System, including the `def` exemption that lets methods and process steps be forward referenced, and cross-references it from the connectivity statement-order note. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The compilation log level was reachable only from source, as a
`given options.CompilerOptions.LogLevel`, so raising it to watch the stages
run meant editing the design and recompiling it. It is now a command-line
option alongside `--cache`, taking any of the seven level names and
defaulting to whatever the givens resolved to:
<your program> --log info commit
The parsed level feeds both `ElaborationOptions.logLevel` and
`CompilerOptions.logLevel`, the latter being what `StageRunner` reads for its
per-stage messages, its post-stage sanity check at `debug`, and its
code-string dumps at `trace`. Scallop's `choice` validates the name against
`LogLevel.values`, so the level always resolves.
Two supporting changes:
The app's own progress logger follows `--log` only when a level was actually
supplied, since it is deliberately louder than the compiler's `warn` default
and a run without the option must keep reporting its progress. For that to be
observable, the welcome banner and the "no command-line given" notice move
into the successful-parse branch, so `--log off` silences the run from its
first line; a CLI parse error now prints the scallop error alone instead of
the banner first.
`Logger` is a process-global singleton keyed by name, so a level set by one
run leaked into the next in the same JVM (reproduced under sbt's unforked
`runMain`, where `--log off` silenced every later run). `DFApp` now pins its
logger to `INFO` at construction, which is what the wvlet default gave it
before.
Tests assert against `ParsedCommandLine` directly rather than captured
output: the levels only ever surface through the wvlet logger, and that
writes to the `System.err` captured when wvlet first initialized, not
necessarily the stream the spec's harness installed.
Documents the whole command line, not just this option, in a new
"Command-Line Interface" user-guide page: the argument ordering rule, the
mode pipeline and how the default mode is picked, the design arguments and
the literal each type takes, the app and per-mode options, how the command
line relates to the `given options.*` defaults, and the `Werror` exit codes.
Linked from Hello Hardware World, which now shows `help` and a few one-line
overrides.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…ches A named value declared inside a branch of a conditional *expression* has to be driven somehow, and `ExplicitNamedVars` picks that drive with `isInEDDomain && !isInProcess`: a connection in an ED domain body, an assignment everywhere else. Only the connection has nowhere to live. `ExplicitCondExprAssign` later wraps the whole expression into a `process(all)`, carrying the connection in with it, and the backend faithfully prints it as an `assign` inside an `always_comb`, which is not valid Verilog (issue #426): always_comb begin if (sign1) data_out1 = neg1[DATA_WIDTH - 1:0]; else begin assign anon_0 = {data_in1}; // <-- rejected by yosys data_out1 = anon_0[DATA_WIDTH - 1:0]; end end `DB.condExprNamedValCheck` rejects that shape at elaboration. It belongs on the elaboration path rather than in `SanityCheck` because a user can write it by hand, which is the deciding question for where a rule goes; the fact that the reported issue reaches it through a compiler stage does not change that. The check is deliberately scoped to the concurrent case only. Compiling the same shape in every scope shows four of the five lower correctly: an ED `process`, an RT domain body and a DF domain body all emit a plain blocking assignment, and a conditional *statement* branch is a scope in its own right regardless. Only the ED domain body miscompiles. So the check reuses `ExplicitNamedVars`' own predicate, and the comment on both says they must stay in agreement. Covers the rejected shape plus three accepted ones (ED process, RT body, conditional statement) in `ElaborationChecksSpec`, and records in the `new-stage` skill where a rule belongs (elaboration vs sanity), how to localize illegal generated HDL when nothing throws, and the current `--log trace` command line. This is the check only; the stages that produce the shape are not fixed yet. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…not live in `NamedAliases` names an anonymous value wherever it happened to be built. When Verilog cannot part-select an arbitrary expression, `NamedVerilogSelection` names the operand at its point of use, and that point can be a conditional expression branch directly inside an ED domain body. There the branch is a concurrent position and not a block, so `ExplicitNamedVars` drove the name by connection, `ExplicitCondExprAssign` then wrapped the whole expression into a `process(all)`, and the connection travelled inside it. Verilog printed an `assign` within an `always_comb`, which yosys rejects (issue #426), and VHDL printed a signal assignment whose value the next statement read a delta cycle too early, which nothing rejects at all. The naming and the placement now happen in one patch. `ChangeRefAndRemove` is what makes that possible: it drops the original from its illegal position and redirects its readers, while the move inserts the *named* instance before the conditional. `FullReplacement` cannot be used, because `Patch.Move` emits a `Remove` per moved member and `Replace` + `Remove` on one member is not a mergeable combination. Nor could that combination be added: `patchMembers` inserts a move's member instances verbatim, so merging the two would relocate the unnamed instance and drop the renamed one. Relocating is total rather than best-effort. A conditional expression reached through the cone travels as its header, its chain blocks and their contents, since a header cannot leave the branch without the blocks it owns. Two naming groups can then want to move the same sub-tree (`(if (d) p else q) + 1` names both the header and the addition), which would duplicate it in the member list, so each pass acts only on the innermost group and leaves the outer one anonymous for the next pass to pick up. `transformSubDB` drives that to a fix point; it terminates because a named value no longer meets any criteria. `SanityCheck` now runs `condExprNamedValCheck` too. The rule was reachable only from `DB.check`, which `Design.onCreateEnd` invokes once on the elaborated user design and nothing invokes again, so it bound the user and left every stage free to produce the shape silently. Wiring it into the pipeline is what showed that a follow-up cleanup stage could never be the fix, since the DB in between would already be invalid. Also exempts a named conditional header from the check. `ExplicitNamedVars` drives one through `patchChains`, an assignment per branch and never a connection, so it nests correctly; rejecting it turned working designs into elaboration errors. Adds a `bugfix` skill for the diagnosis methodology this needed: localizing an illegal shape when nothing throws, deciding which check path a rule belongs to (and that both may apply), scoping an invariant by compiling the shape in every scope, and testing without copying an unlicensed reporter's design. Moves the overlapping material out of `new-stage`, which keeps the stage-authoring parts. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
`VHDLPrinter.csAssignment` hard-coded `:=`, so every `DFNet.Op.Assignment` printed as a variable assignment. VHDL picks the operator from the target's object class, not from the blocking/non-blocking distinction DFHDL picks it from, so a blocking write to a signal (a design-level `VAR`, or an output port assigned inside a `process`) emitted illegal VHDL that GHDL rejects. The classification lives in `DFVal.Dcl.isHDLVariable`, keyed on where the declaration sits and what modifier it carries: process-local, `VAR.SHARED`, HDL-method local/`<> OUT` formal -> `:=` design-level `VAR`, any port, `<> OUT.NB` formal -> `<=` `csDFValDclWithoutInit` was already answering this exact question to pick the declaration keyword, so both sites now derive from the one predicate instead of deciding it independently and drifting into declaring a `signal` that is then written with `:=`. Its owner match is rewritten to consult `isHDLVariable` while keeping every branch, including the unreachable ones (a `VAR.SHARED` inside a process or an HDL method), byte-identical. To get the target to the backend, `csAssignment` now takes the LHS declaration instead of a `shared: Boolean`. That bit existed only for Verilog's `BLKSEQ` lint pragma and left VHDL no way to ask its own question; passing the declaration lets each backend derive what it needs, and the next such distinction costs no signature change. The declaration is the one the printer already dealiased one line down for the `.din` suffix, so this adds no traversal. Verilog and the DFHDL printer are behavior-identical. Nothing under `lib/src/test/resources/ref/` changed, and that is the point: the only `:=` in an architecture anywhere in the reference outputs was TrueDPR's shared variable, the single form that was already right. The rest of the branch had no coverage, which is how this survived. The new `PrintVHDLCodeSpec` test pins all four object classes in one design. Residual gap, unchanged in kind but now visible: a blocking write to a design-level signal followed by a read of it in the same process yields the pre-write value. In a `process(all)` the signal is in the sensitivity list, so the process re-triggers and settles; with a narrower sensitivity list it does not. That shape previously emitted illegal VHDL, so this is not a regression, but legal output is not yet faithful output there. Making it faithful means promoting such declarations to shared variables under VHDL. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
An ED method returning a conditional expression crashed the backend with
`Unsupported member for this VerilogPrinter` (and its VHDL equivalent),
because the conditional was never lowered into a statement.
`NamedAnonCondExpr` is the gate that decides which lowering path an
anonymous conditional takes. It exempts one that is "referenced by an
ident", meaning it is the result of an enclosing conditional expression,
which lowers it along with itself. But anonymous idents come from three
unrelated places, not one: a conditional branch result, a fall-through
step block, and a method's return wiring. The method return hit the
exemption meant for the first, so the conditional was never named,
`ExplicitNamedVars` never turned it into a variable, and the backend was
handed an expression it has no form for.
The exemption now applies only to an ident that trails a scope, not one
that trails a method body. It is narrowed in the direction that leaves
every ident kind not enumerated here on its current behaviour, rather
than an allow-list of owners that would also change cases not yet
thought of.
Two consequences of naming it:
`ExplicitCondExprAssign` rule 2 wraps a conditional statement in a
`process(all)` because an ED design body is concurrent and holds no
statements. An HDL method body is procedural, so a statement needs no
wrapping there, and a process inside a method is illegal (SanityCheck
rejects it). Rule 2 now skips an HDL method body.
`ExplicitNamedVars.patchChains` had drifted from its twin in
`ExplicitCondExprAssign`: recursing into a branch whose value is itself
a conditional expression left the ident placeholder behind and never
retyped that nested header to `DFUnit`. This is a pre-existing bug of
its own, and not method-specific: any named value with a nested
conditional in a branch was broken, crashing the Verilog printer in an
ED design and failing an ownership check in a DF design. Verified
against all three stage files reverted.
Verified across ED functions, ED procedures with `<> OUT`, match
expressions, nested conditionals and static `CONSTRET` functions, in
both backends, all passing ghdl/verilator lint. No reference output
changed.
Each stage gets a test in its own spec, and each was verified to fail
with its own fix reverted:
NamedAnonCondExpr NamedAnonCondExprSpec (new file, plus the
`namedAnonCondExpr` entry point it needed)
ExplicitCondExprAssign "ED method body takes a conditional statement
without a process wrap"
ExplicitNamedVars only visible at the backend: two different
IRs print identically in DFHDL, so the nested
lowering is pinned in PrintVerilog/PrintVHDL
That last point is why an existing `ExplicitNamedVarsSpec` test had been
covering the nested shape for as long as the bug existed, passing the
whole time. Both skills gained the rule: every stage a change touches
gets a test in its own spec, and every new test is verified to fail with
its fix reverted.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…der check `SanityCheck.transformSubDB` ran one hand-picked check out of `DB.subDBCheck` (`condExprNamedValCheck`) and had `orderCheck()` commented out. It now runs the whole per-design set that elaboration runs — `nameCheck`, the connectivity checks behind `connectionTable`, `directRefCheck`, `initialCheck`, and `condExprNamedValCheck` — so a stage's output has to satisfy every rule a user design satisfies, not just the structural ones, and a check added to `subDBCheck` is picked up on both paths for free. `rootDBCheck` stays elaboration-only: those cross-design checks assume a shape the pipeline deliberately rewrites. Widening `subDBCheck` cost nothing. `orderCheck` failed 7 tests, all one root cause. `FirstStepFusion.substValue` cloned an expression tree and THEN walked the clone rewiring declaration reads to their forwarded values. But emitting a forward creates members, so the replacement was appended below the value that reads it: the reset-site fold of `y.din := i` came out as `AsIs(SInt16)`, `Const(0)`, `net`, with the alias pointing forward at a constant defined under it. `cloneAnonValueAndDepsHere` builds each dependency before the value that reads it, which is the only order the flat member list accepts, so the substitution belongs inside the clone rather than after it. It takes a `substDep` hook applied to every dependency as the clone is built, and `substValue` loses its `rewire` walk. Note the no-arg overload's default has to be the recursive clone, not `identity` — `identity` silently stops cloning the cone, which fails a different 7 tests in the `ApplyRange` / parameterized-selection paths. This bug is invisible in every printout: an anonymous value prints inline at its use, so the generated HDL was already correct and no code-string test could have caught it. That is exactly what the order check is for. `SanityCheck` had no spec. The new one elaborates a valid design and then breaks its sub-DB the way a faulty stage would — moving an assignment above everything it reads, and anonymizing a declaration — since its inputs are DBs no design can express. Both tests were verified to pass with the checks reverted and fail with them in place; the fusion fix is pinned by the 7 existing tests. Two skill statements this falsifies are corrected: /bugfix said `SanityCheck` "never calls `DB.check`" and that a forward reference is silent because `orderCheck()` is commented out. /new-stage gains mistake 27 (substituting into a cloned tree after cloning it inverts the member order) and a section listing what `SanityCheck` enforces after a stage. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
`getConstData` memoized one answer per member while taking a `ConstData.CachePolicy` that decides whether a design parameter resolves to its applied data or stays an opaque `UnknownConst`. Two policies, two different answers, one cache slot: whichever consumption came first poisoned the slot for the other, so the same expression folded or stayed parametric depending only on what had already read it. The existing half-measure (invalidate when `this.isDesignParam`) caught the collision on a parameter itself and missed that it propagates to every expression built over it. The cache now holds the `Always` answer only. A design parameter is the sole member whose answer depends on the policy, and it diverges in one direction: `Always` leaves it `UnknownConst` exactly where the resolving policies fold. So a non-`UnknownConst` `Always` answer reached no parameter and is provably policy-independent, while a resolved answer may have come through one and must never reach the shared slot. That asymmetry is what keeps the cache instead of disabling it for every non-default policy: the resolving path consults the shared cache first and re-walks only on `UnknownConst`, so recomputation is confined to the parameter-dependent spine while every parameter-free subtree still answers from cache. `.toScalaInt` and `getConstDataOrDefault` are on paths user code hits constantly. The regression pins both consumption orders in one design, since either alone passes on the broken cache.
The `toScala*` family was mentioned exactly once across the whole site, in passing, in the `generate for` admonition of the Verilog transition guide. Issue #430 was filed against that gap: nothing said which accessor belongs to which type, that a value derived from constants reads just like the constants it came from, or what reading one costs. Add a "Reading a Constant into Scala" section to the type system guide with the accessor table, the constant requirement, the compile-time rejection message, and a warning on the design specialization that reading a parameter implies, alongside the plain Scala parameter alternative. Link it from the two pages a user chasing this lands on first. The example deliberately keeps `.toScalaInt` out of the range bound and marks that line as the implicit conversion, so it does not teach the redundant call it is documenting the alternative to. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
An elaboration option changed on the command line never reached the
elaboration. A design's context is built by `MetaContextPlacerPhase` from the
options given where the design is DECLARED, which is compiled code and cannot
know about an argv, while `DFApp` applied the command line to its own copy.
Nothing joined the two, so the two options the elaboration itself reads back
were quietly ignored: `--nocache` gated the app's step cache while the
sub-design cache kept serving cached bodies, and `--Werror` was inert
altogether, parsing and appearing in `help` while no one ever read the field
it set.
The plugin already has the rule that fixes this, and it is the ordinary one:
a design takes the DFHDL context in scope where it is instantiated, and only
falls back to the declaration-site options when there is none. The generated
`main` now puts a context in scope at that instantiation site by hosting it
in a local method, and the app supplies it:
def mkDsn(dfc: DFC): Foo = new Foo(app.getDsnArg("width"))
app.setDsn(mkDsn(app.getDsnDFC))
`setDsn` still takes its argument by name, so `getDsnDFC` runs at elaboration
time, once `run` has had the command line. Its signature is unchanged, so the
manual path (`dsn.simulate` and friends, through `ManualDFApp`) keeps building
its design in the user's own scope, untouched.
Because `setInitials` seeds the app's elaboration options from the same `@top`
annotation the fallback would have read, this is a no-op on a run with no
command line, and carries every override on a run with one. It also covers the
whole option set rather than the two fields that happen to be reachable today.
`DFApp`'s design-argument context moves onto the same footing. It was
`DFCG()`, whose options fall back to the library defaults resolved at
`DFC.scala`'s own compile site, so a user's `OnError` was ignored when a
command-line argument failed to parse (`exitWithError` reads it, and an
ownerless context reaches that directly from `trydf`). Both contexts now come
from one factory: same options, deliberately separate instances, since a
context owns a `MutableDB` and rebuilding an argument's constant must not
allocate members into the design's own DB.
`getElaborationOptions` is dropped. Its comment claimed the plugin used it for
exactly this, and nothing referenced it; `getDsnDFC` now does the job.
Tests: a fixture design that reports the `cacheEnable` and `Werror` its own
context saw, driven through the generated entry point, since the values are
invisible from outside. Its `tag` argument defeats the app's elaborate-step
cache, which would otherwise skip the body and record nothing.
Documents `DFApp` itself in a new devdoc: what the plugin generates around it,
the five members it resolves by name, where a run's options come from and
which of the two readers each one has, the two contexts, the step pipeline and
its cache keys, and the gotchas. The user-facing `--cache` section now says
that the flag covers the sub-design cache too.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Two lessons from issue #430. The diagnosis genus: when the same code works or fails depending on what ran before it, minimize by deleting the EARLIER statements, not the failing one. A failure that disappears when an unrelated line above it goes away is not in the failing operation at all — something is memoized under a key that does not capture everything the answer depends on. #430 read like "`.toScalaInt` cannot fold parameter arithmetic" but only failed once the same expression had already been consumed as a parametric width. Includes why the fix keeps the cache rather than disabling it off-policy, and that the asymmetry it exploits runs in one direction only (the mirrored rule is unsound). The verification trap: a stashed `compiler_ir` / `core` file can leave zinc serving the fix you just removed, so the test passes and the honest reading sends you looking for a different reproducer. The tell is a `MatchError` out of `compileIncremental` on an unrelated subproject; `clean` before trusting a stashed run. The second one amends the "prove the test fails without the fix" rule added earlier, which assumed stashing is enough.
The sub-design elaboration cache lives BESIDE each project's build output (`SubDesignDiskCache` resolves `dfhdl-cache` as a sibling of the class directory), so nothing removed it short of an sbt `clean` of that project. `clearSandbox` never reached it, which made "clear the sandbox and re-run" a weaker reset than it reads as. `clearElabCache` clears every one of those directories, and `clearDFHDL` clears those plus the sandbox. `clearSandbox` keeps its exact behavior: it is the one used routinely (FullCompileSpec leaves stale output that misleads), while the elaboration cache is content-keyed on the code digest, so a stale entry can never be served and dropping it on every sandbox clear would only cost elaboration time on the next run. The cache directories are derived from each project's `classDirectory` rather than globbed, so they follow the Scala version and any project layout, which is exactly how the runtime resolves them. The recursive delete becomes a shared helper, so `clearSandbox` picks up the two changes that came with the extraction: failures go to the sbt logger rather than `System.err`, and each removed directory logs one line. Without that, `clearElabCache` would be entirely silent about what it did. Documents the commands and the cache directory in CLAUDE.md, records in elaboration-caching.md that `clearSandbox` does not reach this tier, and updates the three skills where this changes what the reader does: the trace caveats in new-stage and bugfix (`--nocache` covers BOTH caches, so `clearSandbox` was never the equivalent alternative they offered), and a new section in compile-perf on these caches skewing the runtime half of a measurement. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The plugin installs a printer so the compiler's own diagnostics name DFHDL types the way a DFHDL user writes them. It covered a handful of shapes and placed everything inline in PreTyperPhase; it now lives in its own file and reaches parity with `ShowType`, which does the same job for the messages DFHDL itself authors. Coverage now spans every named dataflow type (bit/boolean, bit vectors, the integer and fixed-point decimals, host scalars, physical quantities, enums, opaques, structs and vectors) plus the CONST/VAR/VAL modifiers. The frontend type is decomposed ONCE, and the IR type it wraps then selects the rendering, rather than every per-type extractor re-matching the same shape. Width parameters get their own extractor: a literal prints as its number, a reference to a width parameter prints that parameter's name, and anything else prints as an unbounded `Int` (an `IntP.Sig` arithmetic signature could later render as `WIDTH + 1`). Symbol lookups are cached per run, on the phase instance rather than a global, since sbt compiles subprojects concurrently in one JVM. Text is now built the way the compiler builds its own: `controlled` for the recursion and size limiter, `argsText` for bracketed lists, and real precedence handling mirroring `RefinedPrinter.toTextInfixType`, so a vector under `<>` is parenthesized (`(Bit X 4) <> VAR`) while a left-associative `X` chain is not. A type the printer cannot name is left to the standard printer, which keeps DFHDL's own generic signatures readable instead of collapsing them into ShowType's catch-all `DFType`. Testing this needs `assertPluginError`, not `assertCompileError`: `typeCheckErrors` packs its diagnostics through `Message.message`, which renders under a context whose printer the compiler pins to its own `Message.Printer`, so an installed printer is never consulted. The real run reaches the printer only because `CustomReporter` re-renders through `Message.toString`, so the snippet harness now collects the same way, which is both more faithful and the only way to assert on printed types. One expected-message update follows from the wider coverage: a match-pattern error now reads `variable type Bit` where it read `dfhdl.core.DFBit`. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
No description provided.