Haskell bindings for Verilator — write your hardware testbenches in Haskell, run them at compiled-C++ speed.
Status: v0.1.0 tagged; expect API churn across v0.x releases.
Targets Verilator 5.040+ only. CI runs on every commit against both
GHC 9.10 (Cabal 3.12) and GHC 9.12 (Cabal 3.14).
{-# LANGUAGE OverloadedLabels, DataKinds, DeriveGeneric, DeriveAnyClass #-}
import Clash.Prelude (BitVector)
import Verilambda
data BlinkyPorts f = BlinkyPorts
{ clock_50 :: f Bit
, key0 :: f Bit
, ledr :: f (BitVector 8)
} deriving stock Generic
deriving anyclass (Ports, ClockReset)
main :: IO ()
main = runSim @BlinkyPorts "Blinky" do
assertReset
cycles 4_194_304 -- 2^22 cycles, one LEDR tick at 50 MHz
#ledr `shouldBe` 1Two reasons, in order of importance:
- To build something we wanted to use. Clash + Verilator is a productive combination for FPGA development, and we wanted a Haskell-native testbench library with very specific ergonomic and type-preservation properties. Writing one from scratch was the most direct way to get that.
- To experiment with Claude Opus 4.7's Haskell capabilities. Most of the code, tests, Nix packaging, CI, and documentation in this repo was written through pair-programming with Claude Code running the Opus 4.7 1M-context model in max-effort mode. The library doubles as a real-world data point on how far current AI coding assistants can go on a non-trivial typed-Haskell project with native FFI, Cabal integration, and hardware-facing tooling.
A well-regarded alternative in the same design space is Gergő Érdi's
clashilator
package. Projects that don't need the specific properties listed below
will likely be perfectly happy with it.
verilambda's concrete differentiators are:
- Type-preserving —
BitVector 8stays aBitVector 8all the way to the C++ call. The compiler catches width mismatches before the simulator does. - Ergonomic — a 10-line monadic testbench body for the common case.
Overloaded labels (
#ledr) for port access, hspec-flavoured expectations, first-class property testing. - No Template Haskell — port reflection happens via GHC Generics + the
barbieshigher-kinded-data pattern, not TH splices. This avoids TH's usual trade-offs around cross-compilation and tooling friction. - Modern Verilator — 5.040+ only, no legacy compatibility shims.
Three layers:
- You declare your DUT's ports once as a higher-kinded data record.
- At
cabal buildtime, verilambda reads the Clash manifest, generates a thin C ABI shim, and invokes Verilator to compile it. - At runtime, a
SimMmonad exposes a clean API (cycles,#port .= value,#portshouldBevalue,withTrace) over the compiled model.
No runtime parsing, no Template Haskell, no string-keyed port lookups.
- GHC: 9.10.3 or newer
- Cabal: 3.12+ (
build-type: Custompath). Cabal 3.14+ required for the optionalbuild-type: Hookspath. - Verilator: 5.040 or newer
- Linux: macOS support is planned for v0.2
Not yet on Hackage. For now, pin the repo as a flake input:
inputs.verilambda.url = "github:purefunsolutions/verilambda";Or clone locally and reference via cabal.project:
packages: . /path/to/verilambdaDrop verilambda into an existing Clash project in three steps:
- Let Clash emit Verilog +
clash-manifest.jsonnext to your design. - Pick one of the two integration modes below. Both wrap the same
pure pipeline in
Verilambda.BuildDriver(shim-gen → Verilator →libV<top>.a), injected into your binary viaextra-lib-dirs+extra-libsat link time. - Write your testbench against the
VerilambdaAPI.
Either mode expects verilambda-shim-gen and verilator (≥ 5.040) on
$PATH at cabal build time. The flake's devShell
(nix develop github:purefunsolutions/verilambda) supplies both.
A reference project lives at examples/blinky/ —
the Blinky counter from alterade2-flake
wired end-to-end through verilambda.
my-dut/
├── my-dut.cabal
├── cabal.project
├── Setup.hs # classic Custom path, see below
├── SetupHooks.hs # or: modern Hooks path, see below
├── src/
│ └── MyDut.hs # your Clash design
├── verilog/
│ └── my_dut.v # Clash-emitted: `clash --verilog MyDut`
├── clash-manifest.json # Clash-emitted alongside the Verilog
└── test/
└── Main.hs # your verilambda testbench
This is the integration path shipped in v0.1.0. Tested end-to-end
against GHC 9.10.3 / Cabal 3.12 and GHC 9.12.2 / Cabal 3.14.
Your my-dut.cabal:
cabal-version: 3.0
name: my-dut
version: 0.1.0
build-type: Custom
custom-setup
setup-depends:
, base
, Cabal >= 3.0 && < 4
, directory
, process
, verilambda
test-suite my-dut-test
type: exitcode-stdio-1.0
main-is: Main.hs
hs-source-dirs: test
build-depends:
, base
, clash-prelude
, verilambdaYour Setup.hs (four lines of wiring + three lines of DUT-specific
config):
module Main (main) where
import Verilambda.Setup (BuildConfig (..), defaultBuildConfig, verilambdaMainWithHooks)
main :: IO ()
main = verilambdaMainWithHooks defaultBuildConfig
{ bcManifestPath = "clash-manifest.json"
, bcTopName = "my_dut" -- lower-cased Verilog module name
, bcVerilogFiles = [ "verilog/my_dut.v" ] -- one or more .v files
}What verilambdaMainWithHooks does on your behalf, at cabal build:
- Runs
verilambda-shim-gen --manifest clash-manifest.json --out-dir dist/build/verilambda/cbits/to produce a type-matched C ABI shim for your DUT. - Invokes
verilator --cc --build --trace -CFLAGS -fPICon your Verilog + the generated shim, producinglibVmy_dut.a+libverilated.aunderdist/build/verilambda/obj_dir/. - Injects
extra-lib-dirs=…/obj_dirandextra-libs=Vmy_dut, verilated, stdc++into every component'sBuildInfovia aHookedBuildInforeturn, so GHC's linker picks them up transparently.
The rest of your project stays build-type: Simple-shaped — no
manual configure flags, no --extra-lib-dirs on the command line.
build-type: Hooks lands in verilambda v0.2. The planned API
mirrors the Custom path one-to-one, so code written against
Verilambda.Setup today migrates to Verilambda.Setup.Hooks with a
single import change.
Your my-dut.cabal:
cabal-version: 3.14
name: my-dut
version: 0.1.0
build-type: Hooks
custom-setup
setup-depends:
, base
, Cabal >= 3.14 && < 4
, verilambdaYour SetupHooks.hs (no Setup.hs needed at all):
module SetupHooks (setupHooks) where
import Distribution.Simple.SetupHooks (SetupHooks)
import Verilambda.Setup.Hooks (BuildConfig (..), defaultBuildConfig, verilambdaSetupHooks)
setupHooks :: SetupHooks
setupHooks = verilambdaSetupHooks defaultBuildConfig
{ bcManifestPath = "clash-manifest.json"
, bcTopName = "my_dut"
, bcVerilogFiles = [ "verilog/my_dut.v" ]
}Until v0.2 ships, Cabal 3.14+ users should stay on the Custom path
above — it works unchanged under Cabal 3.14 (CPP-guarded
makeSymbolicPath handles the API difference internally), and CI
tests it on GHC 9.12.2 (blinky-sim-ghc912-builds flake check).
Today, testbenches supply a SimBackend value — a record of
foreign import ccall functions against the generated shim's C ABI.
See examples/blinky/src/Main.hs for
a working end-to-end example (8 FFI declarations, one HKD port record,
a SimM body). v0.2 will fold this boilerplate into shim-gen's
Haskell-emitting pass, at which point the 10-line example at the top
of this README becomes the common case.
PLAN.md— the design document this project is being built from; covers architecture, module layout, and the road to v0.1.0.examples/blinky/— a runnable reference integration.nix run .#blinky-simprints the LEDR transition table for the Blinky design;nix flake checkproves it matches byte-for-byte against the Verilator C++ and GHDL VHDL simulators in alterade2-flake.
Dual-licensed under either of:
at your option.
Note: verilambda links against Verilator's runtime, which is LGPLv3+artwork-2.0. Downstream users should be aware of this dependency when distributing binaries built with verilambda.