Skip to content

Value circuits new#36

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

Value circuits new#36
cchalmers wants to merge 16 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 on lines +15 to +40
A block can span several clock domains: the value-level bindings are
split into groups connected by shared variables and each group is lifted
with its own `fmap`/`bundle`/`unbundle`, so only buses whose values
actually meet must share a domain. Sharing a value across domains is
rejected by the type checker. Lets that don't touch value land stay at
the bus level, so let-bound sub-circuits can be used with `-<`.

The value markers have distinct semantics: `SignalV x` asserts the
bus is a `Signal` (best inference — it works against fully generic
sub-circuits); `FwdV x` samples/drives the forward channel of any
signal-like bus via the new `SignalBus` class (`Signal`s, `Vec`s and
tuples of them, custom buses) but needs the bus type determined by
context; and `DSignalV x` is `SignalV` for delayed signals — the delay
index is part of the bus type, so a logic group's values must all sit at
the same pipeline depth, and its outputs are produced at that depth.
Mixing plain and delayed markers in one group is reported by the plugin.

The value boundary is generated with the new `SigTag`, `FwdTag` and
`DSigTag` pattern synonyms (`Circuit` module); `SigTag`/`DSigTag` pin the
bus type so that type inference survives nested circuits (the `Fwd`
family is not injective) and "too shallow" `SignalV` markers report a
direct `Vec`-vs-`Signal` style mismatch. **Breaking**: `ExternalNames`
gained `signalTagName`, `fwdTagName` and `dSignalTagName` fields, so
custom plugins (e.g. clash-protocols style) need to supply them —
`defExternalNames` is now exported so they can be record updates of the
defaults.

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.

For the changelog specifically, I feel only the first paragraph is necessary. All other info should be in documentation / README.

Comment thread src/Circuit.hs
Comment on lines +261 to +262
instance (SignalBus a, SignalBus b, BusDom a ~ BusDom b) => SignalBus (a, b) where
type BusDom (a, b) = BusDom a

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 guess this should be generated using TH. Up to a 3-tuple seems a bit meager.

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

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

It's now up to 12 for everything.

Comment thread tests/fixtures/CrossDomainError.hs Outdated
Comment on lines +13 to +14
-- unifies the domains via the generated bundle before it checks the slave
-- pattern), so the marker sits on the @circuit@ line.

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.

You mean, this always happens? I would expect it to happen either at the introduction of a or b.

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

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

Yeah, ideally it should. I'll see if I can get it to error there.

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

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

Fixed, good suggestion.

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