Truth in public

Measured, both ways

Compatibility against GHC's own test suite — including the axis most compilers don't publish.

There are many ways to claim compatibility, and one way to measure it: GHC's own test corpus, run both directions. A compiler must accept the programs GHC accepts — and it must reject the programs GHC rejects, because a compiler that accepts wrong programs is more dangerous than one that refuses right ones.

These numbers are parsed at build time from measurement artifacts committed to the repository — corpus, method, and every failing file listed. This page cannot claim what the repo cannot prove.

should_compile · accepts

858/ 938

~91% (858 of 938)

programs GHC accepts that Haskelujah also accepts

measured 2026-08-02 @ 8df50557

stability · DETERMINISTIC — two independent full passes, byte-identical failing sets

should_fail · rejects

180/ 767

~23% (180 of 767)

programs GHC rejects that Haskelujah also rejects — the honest number, and the current front of the work

measured 2026-08-02 @ 8df50557

stability · DETERMINISTIC — two independent full passes, byte-identical sets (the

Method: each axis is a single timed sweep of the corpus, 15-second per-file timeout, counts as committed in the artifacts above. Percentages are floored to the whole, deliberately — a single sweep earns no third significant figure. Each card quotes its artifact's stability line verbatim rather than translating it. When a sweep is re-run and its artifact updated, this page updates with it.

224,961 lines of Rust
25 focused crates
499 module interfaces
411 Hackage package snapshots in the corpus

Counted by spec-chirho/stats-chirho.sh on 2026-07-27.

λ

Main.hs

module Main where

double :: Int -> Int
double n = n + n

main :: IO ()
main = print (double 21)

I · Lex II · Layout

Tokens, then structure

{ module Main where
  { double :: Int -> Int
  ; double n = n + n
  ; main :: IO ()
  ; main = print (double 21)
  } }

The lexer streams tokens; the layout algorithm reads indentation and writes the braces and semicolons you never typed.

III · Parse IV · Lower

A tree that forgets nothing

Module "Main"
├─ TypeSig  double :: Int -> Int
├─ FunBind  double
│   └─ Match [VarPat n]
│       └─ InfixApp (Var n) (+) (Var n)
└─ FunBind  main
    └─ App (Var print)
           (App (Var double) (Lit 21))

A lossless concrete syntax tree — every comment and space survives — then lowered to abstract syntax.

V · Resolve VI · Kind Infer

Every name finds its home

double ↦ Main.double
(+)    ↦ GHC.Num.+        Num :: Type -> Constraint
print  ↦ System.IO.print  IO  :: Type -> Type
Int    ↦ GHC.Types.Int    Int :: Type

Scopes are resolved, imports are followed, and every type constructor is assigned a kind by unification.

VII · Type Infer VIII · Exhaustiveness

The types agree

double :: Int -> Int   ✓ inferred ≡ declared
main   :: IO ()        ✓
Num Int                ✓ solved by instance
double n               ✓ patterns exhaustive

Hindley–Milner inference meets the declared signatures; class constraints are solved; every match is checked for coverage.

IX · Core X · Simplify

Sugar gone, dictionaries visible

double = λ(n :: Int) → (+) @Int $fNumInt n n
main   = print @Int $fShowInt (double (I# 21#))

-- after simplify: inline, then fold
main   = print @Int $fShowInt (I# 42#)

System FC Core: typeclasses become dictionary arguments. The simplifier inlines double and folds 21 + 21 where it can.

XI · Codegen XII · Link

And then, machine

_main:                      ; arm64
    mov  x0, #42
    bl   _hlj_print_int
    ret

(func $main                 ;; or wasm
  (call $print_i64 (i64.const 42)))

Three backends share one Core — LLVM, WebAssembly, Cranelift. Their maturity is tracked honestly in the ledger below.

One binary

The whole toolchain, under one name

Twelve subcommands — compiler to editor — verified against the CLI source, not a brochure.

  • haskelujah init

    Scaffold a new project

  • haskelujah build

    Compile .cabal projects to native binaries

  • haskelujah run

    Execute — or shebang a script with #!/usr/bin/env haskelujah

  • haskelujah check

    Type-check without codegen

  • haskelujah repl

    Interactive :type, :info, :load, evaluation

  • haskelujah test

    Compile and run test suites

  • haskelujah install

    Fetch from Hackage; resolve dependencies

  • haskelujah fmt

    Format Haskell source

  • haskelujah lsp

    Diagnostics and hover for VS Code, Neovim, Zed, Helix

  • haskelujah mcp

    Model Context Protocol server for AI assistants

  • haskelujah edit

    Built-in editor, GUI or terminal

  • haskelujah clean

    Remove build artifacts

haskelujah · recorded 80×24

Recorded terminal demo: haskelujah init creates a project, check type-checks it, test runs the suite, fmt formats it, and mcp lists AI tools.

Prefer your own terminal? Ten seconds:

cargo install haskelujah && haskelujah repl

Begin

Up and running in a minute

One install, no GHC, no Stack, no LLVM toolchain. Then write Haskell like a script.

1 · install

cargo install haskelujah

or the script:

2 · write

#!/usr/bin/env haskelujah
-- hello.hs — no build step
main :: IO ()
main = putStrLn "Hallelujah."

3 · run

$ chmod +x hello.hs && ./hello.hs
Hallelujah.

The same file also builds to a native binary with haskelujah build, or to WebAssembly — and the execution paths agree on what they print (receipts in the honest ledger below).

Confessio

Where we still fall short

An honest ledger, kept current. Trust is built from what a project admits, not what it advertises.

  • We are too permissive

    GHC rejects 767 programs in the should_fail corpus; today we correctly reject only a fraction of them (the second number above). A soundness-first effort is underway to close this honestly rather than quietly.

  • The type checker is deterministic — fixed 2026-07-27

    This ledger previously confessed that a corpus file flipped between accepted and rejected across runs of the same binary on the same source. Root cause: signature schemes quantified their type variables in hash-map iteration order. Schemes now quantify in first-appearance order, verified by 100-run determinism loops and a re-measure of both corpora on the fixed binary — failing sets byte-identical, stability lines now read deterministic. Said plainly: no number improved. The coin-flip file now fails every time instead of sometimes; we did not gain a passing file, we stopped pretending a coin flip was a result.

    full writeup ↗
  • The interpreter takes the right branch — fixed 2026-07-27

    This ledger previously confessed that a local recursive helper closing over an enclosing pattern-bound argument could take the wrong branch — the canonical primes sieve returned a silently wrong list on the interpreter path, and so did ordinary where-bound helpers. Measured root: captured recursive let groups repatched one shared static placeholder, so later calls redirected earlier calls’ lazy edges. The fix allocates each captured recursive group per invocation (closed groups remain static), verified by focused letrec, STG-lowering, runtime and GC-integrity gates — and the canonical sieve now prints [2,3,5,7,11,13]. Six falsified hypotheses preceded the measured root; the writeup keeps them all.

    full writeup ↗
  • Compiled output prints true values — fixed 2026-07-27, verified three ways

    This ledger confessed that native code printing a structured value could emit a heap address, a raw constructor tag, or an internal name. The fix threads the Show evidence the type checker already solved into native print — a general mechanism, not per-type patches — with evidence-driven renderers for constructor types. This record closed once, wrongly: an independent check caught Maybe and Either still broken, the entry reopened within minutes, and the completed fix then passed the exact failing forms on three independently built binaries, interpreter, LLVM, and Cranelift agreeing on every case. The six-minute overclaim and its reversal are part of this page’s history on purpose.

    full writeup ↗
  • Rank-N inference gaps

    Most rank-2 types work; some deep higher-rank patterns (lens-style LensLike′) still defeat inference.

  • Quantified constraints

    forall a. C a ⇒ D (f a) in superclass positions is not yet supported. Blocks deepseq.

  • Associated type families

    Token s / Tokens s normalisation is incomplete. Blocks the second stage of megaparsec.

  • Template Haskell

    Basic splices and makeLenses work; typed splices, quasi-quoters, and full reify are incomplete.

  • FFI

    Basic libc interop (puts, printf, malloc). Full C-header parsing and foreign exports are not yet implemented.

  1. Soundness first

    Reject what GHC rejects. The should_fail number on this page is the scoreboard.

  2. The compiler, in your browser

    Compile Haskelujah itself to WebAssembly so this page can run the real type checker, live. No fake playgrounds before then — this page shows only what is real.

  3. Backend maturity

    Closures, thunks, and GC wired into compiled code on every backend.

  4. Self-hosting

    Compile a non-trivial Haskell frontend for Haskelujah with Haskelujah.

  5. Template Haskell, complete

    Typed splices, quasi-quoters, reify for every declaration form.

  6. Cross-compilation & profiling

    Target selection from the CLI; cost centres, heap and time profiles.

“For God so loved the world, that he gave his only begotten Son, that whosoever believeth in him should not perish, but have everlasting life.” — John 3:16

Compiler and page alike are written AI-first — a small fleet of AI agents, working in one repository under human direction, measured in public. If you want work built this way, write to us. How this page was made: the website guide.

Haskelujah · soli deo gloria