Skip to content

Value circuits new#36

Open
cchalmers wants to merge 21 commits into
masterfrom
value-circuits-new
Open

Value circuits new#36
cchalmers wants to merge 21 commits into
masterfrom
value-circuits-new

Conversation

@cchalmers

@cchalmers cchalmers commented Jun 10, 2026

Copy link
Copy Markdown
Owner

The value level circuit syntax I originally started work on 6 years ago! master...value-circuits

This was written with the anthropic's new fable model. I also got it to add significantly more testing.

This gets you pretty close to being able to write everything with circuit syntax! Mealy machines can be done with a registerC:

accum :: Circuit (Signal dom Int) (Signal dom Int)
accum = circuit \(SignalV i) -> do
  SignalV acc <- registerC 0 -< SignalV acc'
  let acc' = acc + i
  idC -< SignalV acc'

It also supports multiple clock domains (although you don't get a great error when you mix them up):

dualCounter :: Circuit (Signal domA Bool, Signal domB Bool) (Signal domA Int, Signal domB Int)
dualCounter = circuit \(SignalV enA, SignalV enB) -> do
  SignalV n <- registerC 0 -< SignalV (if enA then n + 1 else n)
  SignalV m <- registerC 0 -< SignalV (if enB then m + 1 else m)
  idC -< (SignalV n, SignalV m)

and DSignalC support (makes sure all groups have the same delay):

  dpipeC :: Circuit (DSignal dom d Int) (DSignal dom (d + 1) Int)
  dpipeC = circuit \(DSignalV i) -> do
    DSignalV a <- dregisterC 0 -< DSignalV (i + 1)
    idC -< DSignalV (a * 2)

I've tested this enough internally that I'm happy with it. The only issue that came up is the lazyness which has been fixed.

@cchalmers cchalmers force-pushed the value-circuits-new branch from f847f89 to b2f0f98 Compare June 10, 2026 21:55
@martijnbastiaan

Copy link
Copy Markdown
Collaborator

RIP Fable..

I find that I typically mix and match Circuit and non-Circuit constructs, maybe it makes sense to also allow this in lets? Perhaps then split Fwd and Signal (de)construction? E.g., something like:

accum :: Circuit (Signal dom Int) (Signal dom Int)
accum = circuit \(Fwd (Values i)) -> do
  Fwd (Values acc) <- registerC 0 -< Fwd (Values acc')
  let acc' = acc + i
  idC -< Fwd (Values acc')

~

accum :: Circuit (Signal dom Int) (Signal dom Int)
accum = circuit \(Fwd (Values i)) -> do
  let
    Values acc = register 0 (Values acc')
    acc' = acc + i
  idC -< Fwd (Values acc')

Not sure about any of this, just floating an idea.

@cchalmers

cchalmers commented Jun 14, 2026

Copy link
Copy Markdown
Owner Author

I'll think about it but adding support in general Haskell expressions and bindings might open a whole can of worms. Right now, since it's limited to the "arrow land", it's relatively simple without much in the way of edge cases (I hope).

I'm generally in favour of keeping the plugin simple and predictable at the cost of a bit of verbosity. You can always use helpers (something like onCSignal :: (Signal dom a -> Signal dom b) -> Circuit (CSignal dom a) (CSignal dom b)) to promote something to Circuit land, or use idC to bridge in or out of value land.

@martijnbastiaan

Copy link
Copy Markdown
Collaborator

Yeah, makes sense. I'd rather have less magic too.

@cchalmers cchalmers force-pushed the value-circuits-new branch from 83565d7 to cd19ade Compare June 23, 2026 09:21
cchalmers and others added 13 commits June 23, 2026 17:05
Since #14 the error locations often pointed to the end of the circuit
instead of the correct location. Locate each generated binding at its
circuit expression so GHC blames the offending statement. The regression
test is in a separate PR.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Compiles tests/fixtures/BusError.hs (a deliberate type error on a bus) with
the plugin enabled and asserts that GHC reports the error on the offending
line rather than at the end of the circuit block.

Under cabal the plugin is found via the package environment file. Under
`nix build` the package isn't registered in a package db during its own
check phase, so the flake points GHC_PACKAGE_PATH (via the builder's
NIX_GHC_PACKAGE_PATH_FOR_TEST hook) at the in-place db, letting the test run
for real there too.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Value-group logic is lifted to the signal level with `fmap` over a
`bundle` of the group's inputs. Two strictness points there can make
combinational feedback between value groups deadlock simulation:

  * `bundle` lifts its first element with `fmap` / `mapSignal#`, which
    forces that element's spine. Prepend a lazy unit so no real input
    sits in that spine-forcing head slot.

  * The logic function matched its inputs strictly, so a constructor
    pattern at the boundary (destructuring the sampled value) forced its
    input to produce *any* of the group's outputs -- even outputs that do
    not use it. That deadlocks when the input depends, through the
    circuit, on such an output. Match each value input lazily
    (irrefutable), as the bus-level plumbing already does, so an output
    only forces the inputs it actually uses.

Both keep value-group feedback well founded without changing the lifted
logic. The library and error-location test suites still pass.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@cchalmers cchalmers force-pushed the value-circuits-new branch from cd19ade to 10ba3c1 Compare June 23, 2026 17:43
@cchalmers cchalmers marked this pull request as ready for review June 23, 2026 20:30
Comment thread CHANGELOG.md Outdated
Comment thread src/Circuit.hs
Comment thread tests/fixtures/CrossDomainError.hs Outdated
cchalmers and others added 3 commits July 2, 2026 10:36
Keep the summary and the breaking ExternalNames change; the marker
semantics and multi-domain details live in the README and haddocks.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
SignalBus previously stopped at 3-tuples. Extend it (and the Fwd/Bwd,
TrivialBwd and BusTagBundle instances it leans on) to 12-tuples, and
exercise a tuple bus with an FwdV marker in the examples and tests.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Generated tuple patterns and expressions had no source location, so a
type error on a whole bundle -- e.g. a cross-domain mismatch discovered
while checking the slave pattern of a value circuit -- fell back to the
head of the circuit expression. Give them the combined span of the
ports they bundle, so the error points at the pattern that introduces
the offending ports. The CrossDomainError fixture now puts its markers
on a different line than the circuit keyword to pin this down.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

@rowanG077 rowanG077 left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

I like this a lot. It makes circuit notation fit much more naturally with standard Clash code.

Could you add write-ghc-environment-files: always to the cabal.project? This ensures that a standard cabal build + cabal test works.

Comment thread tests/fixtures/ValueExprError.hs Outdated
import Clash.Signal.Internal (Signal ((:-)))

registerC :: a -> Circuit (Signal dom a) (Signal dom a)
registerC a = Circuit $ \(s :-> ()) -> (() :-> (a :- s))

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Why use internals here? This seems to work just fine:

registerC :: HiddenClockResetEnable dom => a -> Circuit (Signal dom a) (Signal dom a)
registerC a = Circuit $ \(s :-> ()) -> (() :-> register a s)

Comment thread tests/fixtures/CrossDomainError.hs Outdated
import Clash.Signal.Internal (Signal ((:-)))

registerC :: a -> Circuit (Signal dom a) (Signal dom a)
registerC a = Circuit $ \(s :-> ()) -> (() :-> (a :- s))

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Same here

Comment thread src/CircuitNotation.hs Outdated
else lifted
outsLoc = noAnnSrcSpan (foldr (combineSrcSpans . getLocA) noSrcSpan outVarPs)
knotBind = L loc $ patBind (tupP outsLoc outVarPs) knotExpr
in if null outs then [] else [logicBind, knotBind]

@rowanG077 rowanG077 Jul 7, 2026

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

This drops value groups with no outputs. This means code like this is accepted:

deadLet :: Circuit (Signal dom Int) (Signal dom Bool)
deadLet = circuit \(SignalV a) -> do
  let bad = P.not a
  idC -< SignalV (0 :: Int)

Where that should be a type error imo.

Comment thread src/CircuitNotation.hs

-- | Variable names bound by a pattern (conservative, syntactic; as-pattern
-- names are not collected).
patVarNames :: LPat GhcPs -> [String]

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

This is maybe a bit of a nit, but it would be nice if this would work:

valueAsPattern :: Circuit (Signal dom (Int, Int)) (Signal dom (Int, Int))
valueAsPattern = circuit \(SignalV p@(a, b)) -> do
  idC -< SignalV p

That can also just be an issue, doesn't need to be fixed now imo.

Comment thread src/CircuitNotation.hs
sigComp (L _ rdr) = case unqualName rdr of
[n] -> Map.lookup n nameComp
_ -> Nothing
splitSigs = concatMap

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Should pragmas/fixities annotation move as well, like:

valueLocalInline :: Circuit (Signal dom Int) (Signal dom Int)
valueLocalInline = circuit \(SignalV a) -> do
  let
    {-# INLINE inc #-}
    inc x = x + 1
  idC -< SignalV (inc a)

cchalmers and others added 5 commits July 11, 2026 10:16
So that a plain `cabal build && cabal test` drops a .ghc.environment file,
letting the error-location test's bare `ghc` find the circuit-notation
plugin without the CI-only --write-ghc-environment-files flag. Addresses
review feedback from @rowanG077 on #36.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The ValueExprError and CrossDomainError fixtures defined a local register
circuit via Clash.Signal.Internal's (:-). Use the public `register`
instead, as suggested by @rowanG077 on #36. The deliberate errors still
land on their marked lines (checked by the error-location test).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Three fixes from @rowanG077's review of #36:

* Type-check value groups with no outputs. Previously a value group that
  produced no output (e.g. an unused `let bad = not a`) was dropped
  entirely, so a broken circuit compiled silently. The group's logic is
  now still bound (to a wildcard) so its expressions are type-checked;
  with nothing downstream to pin the value, the mismatch surfaces where
  the input value enters the circuit. Regression: tests/fixtures/DeadLetError.hs.

* Move INLINE/NOINLINE pragmas and fixity declarations with a value-level
  `let` when it is relocated into a group's generated logic function.
  Previously only type signatures were routed to the group, so a local
  pragma was orphaned in the outer let and GHC rejected it as "lacks an
  accompanying binding". Example: localLetPragmas in ValueCircuits.hs.

* Support as-patterns on value markers: `SignalV p@(a, b)` now binds `p`
  as well as `a` and `b` (patVarNames collects the as-pattern binder).
  Example: valueAsPattern in ValueCircuits.hs.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
AsPat carries a separate `@`-token field on GHC 9.4-9.10 (4 fields) that
9.12 folded into its extension field (3 fields). CPP-guard the pattern so
the as-pattern support builds on all supported GHCs, not just 9.12.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The `@`-token field was dropped from AsPat in GHC 9.10, not 9.12 as the
previous fix assumed (9.10.3 then failed with the 4-field pattern). Guard
on `>= 910`. Verified by building and running the test suites on all four
supported GHCs (9.6.7, 9.8.4, 9.10.3, 9.12.4) via the flake.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
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.

3 participants