Haskell bindings for Stim—Google's high-performance stabilizer circuit simulator.
stim-hs provides safe, idiomatic Haskell bindings to Stim—Google's high-performance stabilizer circuit simulator and analyzer widely used in quantum error-correction (QEC) research.
Quantum error correction is computationally demanding. Stabilizer circuit simulation—the backbone of most QEC research—requires manipulating thousands of qubits, millions of gates, and complex detector error models. Google's Stim has emerged as the de facto standard for this workload, offering a highly optimized C++ core with a Python frontend that feels almost NumPy-native.
Python's ecosystem for numerical and quantum computing is far more mature, with NumPy, SciPy, and Stim's own Python frontend providing a polished, batteries-included experience. Haskell, by contrast, lacks of established libraries for quantum error correction.
stim-hs fills that gap. By providing high-quality bindings to Stim, it gives Haskell developers access to the same high-performance stabilizer simulation that powers Python QEC research. This opens the door for type-safe, composable QEC application development in Haskell—from decoder prototypes to classical control software—without requiring a Python runtime.
This project deliberately mirrors the architectural decisions of stim-rs, a Rust binding to Stim that demonstrates how to wrap a large C++ codebase with rigorous engineering discipline:
- Vendored upstream sources: Stim is pinned as a git submodule for hermetic, reproducible builds.
- Two-layer abstraction: a thin, unsafe bridge layer and a thick, safe wrapper layer.
- Automated parity auditing: Python-based inventory scripts verify API coverage against upstream Python docs.
Where Haskell diverges is in the bridge technology. Rust has the transformative cxx crate for safe, bidirectional C++ interop. Haskell's FFI is fundamentally C-oriented. There is no cxx equivalent. Therefore, stim-hs adopts the time-tested C-shim pattern: a hand-written C compatibility layer that exposes Stim's C++ API through C-callable functions with opaque pointers.
┌─────────────────────────────────────┐
│ stim (Haskell) │
│ ├── Stim.Circuit │
│ ├── Stim.TableauSimulator │
│ ├── Stim.Tableau │
│ ├── Stim.Sampler │
│ └── Stim.Types │
├─────────────────────────────────────┤
│ stim-c (C compatibility layer) │
│ ├── stimhs_circuit_* │
│ ├── stimhs_tableau_sim_* │
│ ├── stimhs_det_sampler_* │
│ └── stimhs_meas_sampler_* │
├─────────────────────────────────────┤
│ vendor/stim (C++ upstream) │
│ ├── stim::Circuit │
│ ├── stim::TableauSimulator<W> │
│ ├── stim::FrameSimulator<W> │
│ └── ... │
└─────────────────────────────────────┘
Stim's C++ objects hold significant native memory. Haskell's tracing garbage collector has non-deterministic finalization. stim-hs therefore provides dual resource management:
ForeignPtrwith finalizers: a GC safety net that eventually frees the C++ object.bracket-style explicit management: deterministicwithCircuit,withTableauSim, etc., for predictable memory usage in tight loops.
import qualified Data.Vector.Storable as VS
import Stim
-- Deterministic cleanup
withCircuit $ \circ -> do
_ <- appendH circ (VS.fromList [0])
_ <- appendCNOT circ (VS.fromList [0, 1])
circuitToString circAll fallible C++ operations are caught at the boundary and translated into Haskell Either StimError a:
data StimError = StimError
{ stimErrorCode :: !Int
, stimErrorMessage :: !Text
} deriving (Eq, Show)
instance Exception StimErrorThe C layer guarantees that no C++ exceptions leak across the FFI boundary. Every entry point is wrapped in a try/catch(...) firewall that writes a UTF-8 error message into a caller-provided buffer.
Shot data and measurement results are returned as strict Data.Vector.Storable.Vector Word8 for zero-copy interoperability with Haskell's numerical ecosystem. Future versions may add bit-packed or massiv integrations.
- GHC 9.6+ and Cabal 3.10+
- C++20-capable compiler (
g++orclang++) makegitwith submodule support
git clone --recurse-submodules https://github.com/overshiki/stim-hs.git
cd stim-hs
# Build the C compatibility layer
make -C c-stim
# Build the Haskell library
cd stim-hs && cabal build
# Run the practical QEC demo
cabal run stim-hs-demoThe custom Setup.hs automatically invokes make -C c-stim during configuration, so in many cases a single cabal build inside stim/ is sufficient.
- Linux: tested with GCC 15 + GHC 9.6.7
- macOS: should work with
clang+++-stdlib=libc++ - Windows: a
CMakeLists.txtis provided inc-stim/for MSVC compatibility; MinGW viamakeis also supported
{-# LANGUAGE OverloadedStrings #-}
import qualified Data.Vector.Storable as VS
import Stim
bellCircuit :: IO (Either StimError Circuit)
bellCircuit = do
circ <- circuitNew
_ <- appendH circ (VS.fromList [0])
_ <- appendCNOT circ (VS.fromList [0, 1])
_ <- appendM circ (VS.fromList [0, 1])
return (Right circ)main :: IO ()
main = do
Right circ <- circuitFromString "H 0\nCNOT 0 1\nM 0 1"
Right s <- circuitToString circ
putStrLn smain :: IO ()
main = withTableauSim 2 $ \sim -> do
Right () <- doH sim 0
Right () <- doCNOT sim 0 1
Right tab <- currentTableau sim
Right s <- tableauToString tab
putStrLn smain :: IO ()
main = do
Right circ <- circuitFromString "H 0\nCNOT 0 1\nM 0 1"
Right sampler <- compileMeasurementSampler circ
Right shots <- sampleMeasurements sampler 100
print (shotDataBytes shots)A runnable demo is included in stim-hs/app/Main.hs. It constructs a distance-3 repetition code, adds depolarizing noise, samples detection events, and prints statistics:
cd stim-hs && cabal run stim-hs-demoSample output:
============================================================
stim-hs: Distance-3 Repetition Code Demo
============================================================
[1] Parsing noiseless repetition code (2 rounds)...
[2] Sampling noiseless circuit (100 shots)...
Average detection events per shot (noiseless): 0.0
[3] Parsing noisy repetition code (2 rounds, 1% depolarizing noise)...
(Extra 0.5% depolarizing noise appended to data qubits)
[4] Sampling noisy circuit (1000 shots)...
Number of detectors: 3
Number of shots: 1000
Average detection events per shot (noisy): 5.7e-2
Every effectful operation returns IO (Either StimError a). You can handle errors explicitly or use the convenience functions that throw:
-- Explicit
result <- circuitFromString "invalid gate"
case result of
Left err -> putStrLn $ "Error: " ++ show err
Right circ -> print circ
-- Throwing variant (would need to be defined in your app)
fromStringOrDie :: String -> IO Circuit
fromStringOrDie s = circuitFromString s >>= either throwIO return| Component | Technology | Responsibility |
|---|---|---|
vendor/stim |
Git submodule (C++20) | Upstream Stim sources, pinned to a stable release |
c-stim/ |
C++ + C headers | Opaque-pointer C API, exception firewalls, buffer marshaling |
c-stim/Makefile |
GNU Make | Compiles libstimhs.a from vendored sources + shims |
stim/Setup.hs |
Custom Cabal setup | Builds C layer, injects absolute lib/include paths |
stim/src/Stim/Internal/ |
Raw FFI | ForeignPtr newtypes, withErrorBuffer, CString helpers |
stim/src/Stim/ |
Public API | Safe, IO-based, Vector.Storable-aware Haskell interface |
- No
cxxequivalent: Haskell lacks acxx-like bridge. The C-shim route is the only production-viable path. - Static
libstdc++linking: vendored C++ is compiled with a modern toolchain;ld-optionsembed the static archive to avoid runtime dependency mismatches. IOfor all mutation: Stim objects are inherently stateful. A pure fiction would destroy performance by copying C++ objects on every gate append.safeFFI calls: CPU-bound Stim operations may take milliseconds to seconds;foreign import ccall safeallows other Haskell threads to run.
Current status (v0.1.0):
- ✅ Core types:
Circuit,TableauSimulator,Tableau - ✅ Basic gate appending:
H,CNOT,M,MX,DETECTOR,OBSERVABLE_INCLUDE - ✅ String round-tripping:
circuitToString/circuitFromString - ✅ Measurement sampling via
CompiledMeasurementSampler - ✅ Detector sampling via
CompiledDetectorSampler - ✅ DetectorErrorModel` (DEM) construction and sampling
Near-term roadmap:
-
PauliStringandFlowtypes - I/O for Stim shot data formats (
b8,r8,dets, etc.) - Hackage release
Licensed under Apache-2.0. The vendored upstream Stim sources under vendor/stim are also distributed under Apache-2.0.
This project is under active development. Issues and pull requests are welcome.