Bend 2 vs. Haskell: GHC Lazy STG Thunks & Accelerate vs. Strict Affine Evaluation and 0.38 s Proofs
An architectural comparison of GHC Haskell
(par/pseq, Strategies, Accelerate,
LiquidHaskell) and Bend 2 (BendTT affine dependent types
and the BendRT 128x128 Task Cube).
Executive Summary: Bottom Line Up Front
Haskell and Bend 2 share pure functional DNA (algebraic
datatypes, pattern matching, referential transparency, and an explicit
IO monad). However, Bend 2 discards GHC’s lazy Spineless
Tagless G-machine (STG) and tracing garbage collector in favor of strict
affine evaluation (BendTT) and native CPU/GPU compilation
(BendRT).
- Choose Haskell (GHC 9.10, Cabal/Stack) for complex
compiler toolchains (such as
Pandoc), domain-specific financial contracts, parser combinators (Megaparsec), and lazy infinite stream pipelines backed by Hackage’s three decades of typeclass libraries. - Choose Bend 2 (
bend -o,LAWS.bend,PROOF.bend) when your functional program requires predictable memory footprints (2.0 MB to 2.8 MBRSS with zero space leaks), automatic GPU offloading (f!(x)), or built-in inductive theorem proving (0.38 scheck time). GHC’spar/pseqsparks are confined to CPU threads andAccelerateis restricted to flat array DSLs. Bend 2 compiles recursive algebraic trees directly to 16 CPU cores and Metal/CUDA GPU warps for up to 123.9x GPU speedup (benchmark dataset).
1. Runtime Architecture: GHC Lazy STG Thunks vs. BendRT 64-Bit Affine Words
Why has pure functional programming historically struggled to run on GPUs? The obstacle in GHC Haskell is lazy evaluation and tracing garbage collection:
| Runtime Dimension | GHC Haskell (STG Machine + Generational GC) | Bend 2 (BendTT + BendRT Worklist
Machine) |
|---|---|---|
| Evaluation Strategy | Call-by-need (lazy by default). Unevaluated expressions allocate
heap thunks that mutate in-place
(BLACKHOLE) upon forcing. |
Strict affine evaluation with whole-program Share and Borrow inference (source). |
| Memory Layout & GC | Tracing generational garbage collector; lazy fold accumulation
(foldl) notoriously causes heap space
leaks. |
Zero tracing GC: every term is a 64-bit word
([rfc | tag | aux | loc]), and pattern matching frees
consumed nodes immediately (heap_free) (source). |
| CPU Spark Parallelism | Control.Parallel.Strategies (par,
pseq) schedules sparks on the GHC RTS CPU thread pool;
thunk locking can stall threads. |
Parallel let (a b = f(x) g(y)) deals
tasks into the 128 × 128 Task
Cube (16,384 lock-free rings) with zero work stealing (source). |
| GPU Execution | Requires embedded array DSLs (Data.Array.Accelerate);
cannot compile arbitrary recursive ADTs (data Tree a) to
GPU kernels. |
Compiles recursive ADTs directly to Metal and CUDA
shaders from the same .bend file (source). |
2.
Formal Verification: LiquidHaskell & Type-Level GADTs
vs. LAWS.bend & PROOF.bend
In Haskell, proving properties about functions requires either
encoding invariants via singleton types (DataKinds,
GADTs, TypeFamilies) or using an external SMT
refinement checker like LiquidHaskell:
- The Ergonomic Cost of Singleton Types: Simulating
dependent types in GHC requires duplicating term definitions at the type
level (
SingI,SDecide), producing complex type errors and slow compile times. - Native Dependent Types in Bend 2 (
Type : Typewith Usage Wall): In Bend 2, types and terms share one syntax and one evaluator (bend2/bend.ts). Logical consistency is preserved without universe levels via the Usage Wall: erased proof witnesses carry usage0, while live runtime code carries affine usage1(bendtt.lean). - Sub-Second Constructive Proofs: Instead of waiting
for an external SMT solver,
bend PROOF.bend --verdictchecks 3,200 generic proof instantiations in 0.38 s on an Apple M4 Max (benchmark dataset).
3. Apple M4 Max Benchmarks: Pure Functional Algorithms at C and GPU Speed
Across the 16 functional benchmarks in
/data/bend-ecosystem.json (written using pure algebraic
recursion without mutable loops), Bend 2’s single-core
SEQ-CPU matches -O3 C within
0.70x to 1.50x on 14 workloads while scaling
across 16 CPU cores and GPU warps (benchmark dataset):
| Workload | Category | Native C (-O3) |
Bend SEQ-CPU (RSS) |
Bend PAR-CPU 16c (RSS) |
Bend PAR-GPU Metal |
GPU Speedup vs. SEQ |
|---|---|---|---|---|---|---|
| Conway’s Game of Life | Cellular Automata | 6.776 s |
7.803 s
(2.1 MB) |
0.647 s
(2.7 MB) |
0.063 s |
123.9x (source) |
| N-Body Physics | Particle Gravity | 5.126 s |
5.758 s
(2.2 MB) |
0.516 s
(2.7 MB) |
0.059 s |
97.6x (source) |
| Merkle Tree | Cryptographic Trees | 3.423 s |
4.222 s
(130 MB) |
0.422 s
(131 MB) |
0.071 s |
59.5x (source) |
| 3D Raytracer | Computer Graphics | 3.646 s |
4.616 s
(2.1 MB) |
0.396 s
(2.6 MB) |
0.151 s |
30.6x (source) |
| Tree Bitonic Sort | Sorting Networks | 5.719 s |
6.043 s
(146 MB) |
0.799 s
(156 MB) |
0.450 s |
13.4x (source) |
| Tree Matrix Multiply | Quadtree Algebra | 2.964 s |
2.064 s
(2.8 MB) |
0.226 s
(14.8 MB) |
0.231 s |
8.9x (source) |
| Symbolic Regression | Genetic AST Search | 2.208 s |
3.014 s
(2.1 MB) |
0.266 s
(3.1 MB) |
0.529 s |
5.7x (source) |
For Haskell engineers who love equational reasoning
(reverse (reverse xs) == xs) and algebraic data types, Bend
2 makes those equations executable compiler laws
(LAWS.bend). It then runs the resulting functional trees
across 16,384 GPU lanes with zero space leaks (benchmark dataset).
4.
Side-by-Side Code: GHC par/pseq Sparks
vs. Bend 2 Parallel Let & Equational Proof
In GHC Haskell, parallelizing a recursive tree reduction requires
Control.Parallel (par and pseq)
to force lazy thunks on the CPU runtime, and cannot compile recursive
Tree ADTs to GPU kernels:
-- GHC Haskell: CPU-only spark parallelism with explicit strictness forcing
import Control.Parallel (par, pseq)
data Tree = Leaf ! Word32 | Node !Tree !Tree
sumTree :: Tree -> Word32
sumTree (Leaf x) = x
sumTree (Node l r) = a `par` (b `pseq` (a + b))
where
a = sumTree l
b = sumTree r
In Bend 2, evaluation is strict and affine by default,
a b = sum(lft) sum(rgt) populates the 128 × 128 Task Cube for CPU or GPU execution,
and equational theorems (mirror_involutive) are checked
directly by the compiler in 0.38 s (bendlang/bend):
type Tree:
case @Leaf:
val: U32
case @Node:
lft: Tree
rgt: Tree
def sum(t: Tree) -> U32:
match t:
case @Leaf{val}:
val
case @Node{lft, rgt}:
a b = sum(lft) sum(rgt)
a + b
def mirror(t: Tree) -> Tree:
match t:
case @Leaf{val}:
@Leaf{val}
case @Node{lft, rgt}:
l r = mirror(rgt) mirror(lft)
@Node{l, r}
def mirror_involutive(t: Tree) -> Tree{mirror(mirror(t)) == t}:
match t:
case @Leaf{val}:
finally
case @Node{lft, rgt}:
ih_l = mirror_involutive(lft)
ih_r = mirror_involutive(rgt)
rewrite ih_l
rewrite ih_r
finally