"Scheme's soul, a proof assistant's spine, grown from one spore."
Blight is a dependently-typed programming language that is also a proof assistant, written in Scheme-style s-expression syntax. You can write ordinary programs in it (recursion, data types, pattern matching, a standard library) and state and prove theorems about them — and the same small, trusted core checks both. That core is the spore: a tiny kernel that is the only thing allowed to certify that a term is well-typed. Everything else is built on top of it.
The one big idea: trust is bounded, power is not. The kernel — the spore — is the only code
that can mint a Proof, and it is deliberately microscopic. "Trusted" here means implicitly
trusted: relied on with no external check, so a bug in it could silently certify something false.
That is a liability, not a badge — which is exactly why everything else is kept out of it. Data
types, pattern matching, traits, ML-style functors, effect handlers, tactics, the standard library,
the package manager — all of it is tower code: the untrusted, explicitly-checked layer that the
kernel re-verifies. (Tower is a role, not a folder — it spans both the untrusted Rust crates and the
.bl standard library.) So a tower bug at worst fails to produce a Proof — it is caught — and
can never mint a false one. And a second, independently-written re-checker can re-verify any
accepted proof, so soundness rests on "two small checkers agree, or the second honestly declines —
never silently disagree," not "trust one large compiler."
How it compares. If you know other languages: Blight takes its dependent, cubical type theory
from the Cubical Agda family, its quantitative 0/1/ω grading from Idris 2's QTT, and adds
algebraic effects with handlers — all wearing Scheme syntax and organized, like Coq, around a tiny
kernel that re-checks every proof. The kernel itself is a cubical type theory with quantitative
grading and effects.
This repo is the bootstrap implementation: a Rust host that seeds the kernel, elaborates the surface syntax, and compiles checked terms to native (or WebAssembly) code.
- Spec: docs/blight-spec.md — the canonical language design.
- Tutorial: docs/tutorial.md — a hands-on walk from
Natto a tactic proof. - Cookbook: docs/cookbook.md — task-oriented recipes (errors, hash maps, effect sequencing, totality measures, level polymorphism, proofs), every snippet anchored to an acceptance-tested example.
- Performance guide: docs/performance-guide.md — the user-facing "how to stay fast": the Int/Nat two-speed model, the recognizer contract, regions, and diagnosing a slow program.
- Implementation notes: docs/implementation.md — host architecture, the TCB boundary, the milestone map.
- Performance: docs/performance.md — cost model, benchmarks, advantages/disadvantages.
- Benchmarks game: docs/benchmarks-game.md — scaling tables + a measured cross-language table (fib/sum/factorial vs C/Rust/OCaml/Haskell/Python) where Blight-Int lands in the C/Rust/OCaml cluster.
- Roadmap: docs/roadmap.md — can we build games / be fast / do I/O, and what each costs the trusted kernel.
- v0.1 program: docs/roadmap-v0.1.md — the forward roadmap (ergonomics, self-hosting Stage-1, proof track, release), red-commit-first TDD per milestone.
- Research frontier: docs/research-frontier.md — the honest map of the cubical × quantitative × effects fusion: what's machine-checked as working, what's machine-checked as not (with the Lean theorem names), and why Blight stratifies rather than claiming a seamless fusion.
- Examples: examples/ — small runnable programs and a sample package.
Blight is s-expression syntax over a dependently-typed core. Here are five complete, real snippets (each is a checked-in example or stdlib module).
Data + structural recursion. Nat is the unary Peano encoding; plus recurses on its first
argument (from std/nat.bl):
(defdata Nat () (Zero) (Succ (n Nat)))
(define-rec plus (Pi ((a Nat) (b Nat)) Nat)
(lam (a b) (match a
[(Zero) b]
[(Succ n) (Succ (plus n b))])))A buildable program. A main global is what blight build compiles and runs; here it is a
Nat (examples/hello_nat.bl — (2 * 3) + 1):
(load "std/nat.bl")
(define two Nat 2)
(define three Nat 3)
(define main Nat (plus (mult two three) 1))cargo run -p blight-repl --features llvm -- build examples/hello_nat.bl -o hello && ./hello
# 7A length-indexed type. Vec a n tracks its length in the type; vec-length reads the index
back as a Nat (examples/vec_head.bl builds a Vec Nat 3 and prints 3):
(load "std/nat.bl")
(load "std/vec.bl")
(define sample (Vec Nat three)
(vcons two one (vcons one two (vcons Zero three (vnil)))))
(define main Nat (vec-length Nat three sample))A guarantee the type enforces. Because the length lives in the type, you can write a safe-head
whose argument type Vec A (Succ n) demands a non-empty vector. Calling it on an empty vector is
a compile-time type error — the index Succ n cannot match Zero — caught by the trusted kernel
and, independently, by the second re-checker (examples/safe_head.bl):
(define-rec safe-head (Pi ((A (Type 0)) (n Nat) (v (Vec A (Succ n)))) (Maybe A))
(lam (A n v) (match v
[(vnil) nothing] ; unreachable: no caller can supply a Vec A Zero
[(vcons m x xs) (just x)])))
(the (Maybe Nat) (safe-head Nat one sample)) ; ok
(the (Maybe Nat) (safe-head Nat Zero (vnil))) ; rejected: expected index Succ Zero, found ZeroA proof by tactics, re-checked by the kernel. plus n Zero = n is not definitional, so it needs
induction; tactics only propose the term and the spore re-checks it
(plus_zero_tac.bl):
(define-by plus-zero
(Pi ((n Nat)) (Path Nat (plus n Zero) n))
(intro n
(induction n
[(Zero) refl]
[(Succ k) (cong Succ (exact k#ih))])))Text output. A buildable main can be a String, which the runtime prints as text. A string
literal desugars into a cons-list of Nat codepoints — no primitive string type in the kernel
(examples/hello_string.bl prints hello):
(load "std/string.bl")
(define main String "hello")Only blight-kernel is trusted (TCB); its Cargo manifest sets [lints.rust] unsafe_code = "forbid",
so the trusted base contains no unsafe. "Trusted" is meant in the precise sense: implicitly
trusted — believed without any external check. Every other crate is untrusted in the equally
precise sense: explicitly checked — it can only propose terms that the kernel must independently
accept, so its bugs are caught rather than believed.
flowchart TD
source["source .bl"] --> elab
elab["blight-elab (reader + elaborator)"] -->|"core Term"| kernel
repl["blight-repl (the blight CLI)"] --> elab
kernel["blight-kernel (TRUSTED spore)"] -->|"Proof"| recheck
kernel -->|"checked Term"| codegen
recheck["blight-recheck (independent re-checker)"] -.->|"agrees / declines"| kernel
codegen["blight-codegen (LLVM -> native / wasm)"] --> binary["native binary / wasm object"]
subgraph tcb [Trusted computing base]
kernel
end
| Crate | Trust | Purpose |
|---|---|---|
blight-kernel |
Trusted | The spore: terms, normalization-by-evaluation, the typing rules, and the cubical Kan table. The only Proof door; unsafe is lint-forbidden. |
blight-recheck |
Untrusted | An independent, minimal re-checker that re-verifies a Proof's conclusion from scratch (spec §8.3). |
blight-elab |
Untrusted | The s-expression reader, bidirectional elaborator, macros, tactics, and the spores package manager. |
blight-codegen |
Untrusted | The backend: erasure, closure conversion, monomorphization, ANF, and LLVM codegen to a native binary or a WebAssembly object. |
blight-repl |
Untrusted | The blight CLI binary: read, elaborate, check, report, build. |
blight-net |
Untrusted | Data-only distributed transport: serialized values over TCP, using the runtime serializer's wire format. No blight-kernel dependency and no foreign axioms — it only moves bytes, so it does not grow the trusted base. |
The standard library and the self-model live as .bl sources in
crates/blight-prelude — tower code (the untrusted, explicitly-checked
role described above), not a compiled crate.
Blight is pre-1.0 (current version 0.1.0). The always-available path is a from-source build with
a stock Rust toolchain (edition 2021, rust-version = 1.96):
cargo build --release
./target/release/blight --version # blight 0.1.0Tagged v0.1.x releases also publish a prebuilt blight binary for macOS (arm64, x86_64) and
Linux (x86_64) as release artifacts, built by
.github/workflows/release.yml: download the archive for your
platform, put blight on your PATH, and confirm with blight --version. Those prebuilt binaries
are check-only (kernel + re-checker + REPL); native/WASM compilation via blight build needs the
llvm feature and an LLVM 18 toolchain, so build from source for that — see Build below.
The kernel, elaborator, re-checker, and REPL build with a stock Rust toolchain (edition 2021,
rust-version = 1.96):
cargo build
cargo test --workspaceThe native/WASM backend (blight build) needs the llvm feature, which requires LLVM 18 and
clang. On macOS via Homebrew:
brew install llvm@18
export LLVM_SYS_181_PREFIX="$(brew --prefix llvm@18)"
cargo build --features llvm
cargo test --workspace --features blight-codegen/llvm,blight-repl/llvmGetting started: the fastest path from zero to a checked proof is
docs/tutorial.md — install, REPL, your first define, a Nat program, an
interactive Console-effect program, and a tactic proof, all runnable as you go. The rest of this
section is the terse reference version.
cargo run -p blight-replEnter top-level forms — (defdata …), (define …), (define-rec name T body),
(deftotal name T body), (load "path"), or (the T e); multi-line forms are read until the
parentheses balance, and Ctrl-D exits. Each form prints ok for a declaration or the checked
conclusion for a (the …). define-rec allows general (possibly partial, Later-guarded)
recursion, while deftotal requires the definition to compile to a structural eliminator — a
non-structural recursive call is a hard error.
Lines starting with : are REPL commands rather than forms:
:help(:h) — list the commands.:type <expr>(:t) — infer and pretty-print the type of an expression.:load <file>(:l) — load and check a file of forms.:quit(:q) — exit.
The package form (import "pkg/mod") is resolved against a spore.toml manifest (see the
spores package manager below), so it runs inside a package build rather than the
bare REPL.
blight build <file.bl> [-o <bin>] [--recheck] [--target=wasm32] [--opt=<level>]
<file.bl>is elaborated and every form is type-checked; itsmainglobal is compiled, run, and printed.mainis usually aNat(printed as a decimal numeral), but it may also be aString, in which case the runtime prints it as text. Strings are untrusted tower code — aString(std/string.bl) is a cons-list ofNatcodepoints and the reader desugars a quoted literal like"hello"into that chain, so the kernel gains no primitive string type.-o <bin>sets the output path (defaults to the input stem, or<stem>.wasmfor wasm).--recheckre-verifies every kernel-accepted judgement with the independent re-checker before emitting code; a rejection aborts the build as a soundness alarm.--target=wasm32emits a WebAssembly module — a linked.wasm(exportingbl_main) when a wasm-capableclang+wasm-ldare found, else the object only (see caveats). Default isnative.--opt=<level>selects the LLVM IR optimization pipeline:0/none,2/default(the default), or3/aggressive. The pipelines preservemusttailtail calls. Note: with no cross-object LTO against the C runtime, higher levels currently cost a little compile time without measurably moving runtime (see docs/performance.md §2d).
blight build requires a binary built --features llvm. For example, from a checkout:
cargo run -p blight-repl --features llvm -- build examples/hello_nat.bl -o hello
./hello- Standard library (crates/blight-prelude/std) —
std/nat(Nat,plus,mult,pred,sub,min,max,even,odd),std/bool(Bool,not,and,or),std/order(nat-le/nat-eq, theShow/Ordtraits),std/ordering(Ordering,ordering-flip,nat-compare),std/list(List,length,append,map,filter,reverse,foldr,concat),std/maybe(Maybe,maybe-map,maybe-bind,maybe-or),std/either(Either,either,either-map-left,either-bind),std/pair(Pair,pair-fst,pair-snd,pair-swap),std/function(id,compose,const,flip),std/vec(length-indexedVec,vec-length),std/string,std/tree(Tree,RedBlackTree), and thestd/preludeaggregator. - Tactics + proofs —
plus-zero : Π(n:Nat). Path Nat (plus n Zero) nis proved entirely by tactics in plus_zero_tac.bl; the tactic vocabulary (refl,intro,induction,cong, …) is documented in tactics.bl. Tactics only propose terms; the kernel re-checks. - Cubical paths — equality is a
Pathtype, and the kernel's Kan table implementstransp,hcomp, andcomp(comp = hcomp + transp, CCHM) plusGlue/unglue, conformance-tested against Cubical Agda (crates/blight-kernel/src/kan.rs). Function extensionality and univalence (ua) are both provable in the standard library (std/path.bl):funextre-checks, anduais built from a single-faceGlueline whosetranspreduces to the equivalence's forward map (the univalence computation rule, witnessed by a kernel white-box test and the closed ua_compute.bl). The independent re-checker now modelsua/Glue(F1): it re-derives Glue formation, the CCHM boundary reductions, and thetransp-over-uacomputation (transp_glue), souare-checksOkin both checkers — see caveats. - Traits and modules — dictionary-passing
Show/Ordand an ML-styleRedBlackTreefunctor applied to aNatmodule (std/tree.bl). - Effects and grades — quantitative binders (
0/1/ω) drive erasure and region elision; effects are tracked by a! Ejudgement (spec §3, §4). sporespackage manager — aspore.tomlmanifest plus an idempotent, cycle-checked(import "pkg/mod")form (crates/blight-elab/src/spores.rs); see examples/package.- Self-model — spore.bl models the kernel's own core term language in Blight, and spore_meta.bl proves small metatheorems about it (re-checked through the kernel door).
- Concurrency, parallelism, and distribution — the actor/CSP surface
(std/actor.bl) declares
spawn/send/receive/yieldas graded algebraic effects, so resume-once safety is enforced by the kernel's continuation grades (a double-resume of a linearsendis aGradeViolation, not a runtime race; see actor_pingpong.bl). Underneath, the C runtime is share-nothing multicore: each OS-thread worker gets its own thread-local heap/stack, and a native worker pool runs independent computations in parallel — messages cross heaps by structural copy of immutable values. The same serializer feeds a data-only distributed transport (blight-net) over TCP, with an M24 remote-addressing layer (NodeId/Router) that routes the samesend/receiveops to named remote nodes (a two-OS-process ping/pong over loopback). All of this is untrusted tower/runtime: zero TCB growth, no newforeignaxioms. Measured speedup/throughput:bench/multicore.sh. - Independent re-checking —
--recheck, backed byblight-recheck.
The examples/ directory has a mix of buildable programs (a main — a Nat, a
native Int, or a String printed as text — that blight build compiles, runs, and prints) and
load-only programs (typecheck through the REPL / test corpus, e.g. tactic proofs, functors, and
effects). See examples/README.md for the full table with run
commands and expected outputs.
| Example | Kind | What it shows |
|---|---|---|
| hello_nat.bl | buildable → 7 |
smallest program; plus/mult |
| containers.bl | buildable → 2 |
Maybe, two-parameter Either, length-indexed Vec |
| vec_head.bl | buildable → 3 |
recover a Vec's index as a Nat |
| safe_head.bl | buildable → 1 |
length-indexed safe-head: the type makes calling it on an empty vector a compile-time error (caught by the kernel and the independent re-checker) |
| list_sum.bl | buildable → 6 |
List + foldr |
| list_sort.bl | buildable → 1 |
insertion sort over a List Nat |
| minmax.bl | buildable → 7 |
min/max on Nat |
| fib.bl | buildable → 13 |
structural Fibonacci via a pair accumulator |
| factorial.bl | buildable → 24 |
structural factorial (fact 4) |
| gcd.bl | buildable → 4 |
fuel-bounded subtractive Euclidean GCD |
| ackermann.bl | buildable → 9 |
Ackermann via deftotal structural descent |
| collatz_steps.bl | buildable → 8 |
fuel-bounded Collatz step count |
| tree_sum.bl | buildable → 6 |
fold a Tree Nat built with tree-insert |
| either_compute.bl | buildable → 4 |
Either/Maybe computation |
| region_scratch.bl | buildable → 2 |
(region …) arena allocation |
| hello_string.bl | buildable → hello |
smallest text-printing program; main : String |
| string_length.bl | buildable → 5 |
string-length over the codepoint spine |
| string_reverse.bl | buildable → cba |
string-reverse, printed as text |
| palindrome.bl | buildable → 1 |
string-eq word (reverse word) |
| caesar.bl | buildable → bcd |
per-codepoint Caesar shift, printed as text |
| plus_zero_proof.bl | load-only | tactic proof plus n Zero = n |
| mult_one_proof.bl | load-only | tactic proof mult n 1 = n |
| traits.bl / show_dispatch.bl | load-only | Show/Ord dictionary dispatch |
| functor.bl | load-only | an ML-style functor over an ORD module |
| redblacktree.bl | load-only | the RedBlackTree functor |
| effects_demo.bl | load-only | a State effect + handler |
| actor_pingpong.bl | buildable → 5 |
the actor/CSP surface (std/actor.bl): spawn/send/yield/receive as graded effects under an inline cooperative scheduler |
Blight's priorities are soundness (a tiny trusted kernel plus an independent re-checker) and predictable, bounded-stack execution — not peak throughput. The headline trade-offs:
- Advantages — two small checkers agreeing (or the second honestly, countably declining —
never silently disagreeing) is the soundness story; deep recursion runs in bounded
stack via the
Later/Fixtrampoline;(region …)arenas reclaim in O(1) and bypass the GC; the GC is precise (no leaks mid-run) andmusttailcalls are guaranteed. - Disadvantages —
Natis unary, so arithmetic on user-defined numeric functions is O(n) heap allocations; the backend recognizer (M20) rewrites the preludeplus/mult/pred/sub(and the M23 untrustedFloat) to O(1) machine-word ops, and the primitive machineIntis O(1) throughout, but a bespoke recursion overSuccstill pays the unary cost. Every value is boxed (M21 unboxes machine-wordNat/Intand nullary cons into tagged pointers, but compound data stays heap-boxed); LLVM optimization passes (--opt) now pay off via M22 cross-object LTO (the runtime helpers inline into hot Blight code, ~1.15x on allocation-heavy loops, bit-identical output); the trampoline allocates a thunk per step; the wasm runtime is bump-only (no GC, noLater/effects/regions); the native heap starts at 64 MiB and grows (doubling semi-spaces) under pressure, so only a true host-OOM aborts.
Full cost model, measured numbers, and reproduction instructions are in
docs/performance.md. The in-tree harness is criterion benches
(cargo bench -p blight-codegen --bench pipeline, and --features llvm --bench runtime for runtime
- GC/arena counters) plus
bench/run.sh(hyperfine overblight buildand the built binaries) andbench/multicore.sh(worker-pool scaling + serializer throughput for the share-nothing runtime).
All milestones M0-M6 are implemented and green (cargo test --workspace, with and without the
llvm feature), as is the post-M6 work: M7-M14 (capability + soundness hardening), M15-M19
(share-nothing multicore + distributed runtime), and M20-M24 (a max-performance sweep — fast-Nat,
unboxing, cross-object LTO, untrusted Float, distributed-actor addressing — all zero TCB growth).
The v0.1 roadmap (ergonomics, self-hosting, proof track, release) is
complete — v0.1.0 is released. Its headline: self-hosting Stage-1 is declared — the
Blight-written front end checks a real fragment (STLC + Nat/Bool/Sum, plus a dependent-Pi
checker) and the trusted kernel independently re-checks every term it produces
(examples/selfhost_stage1.bl).
| Milestone | Deliverable |
|---|---|
| M0 | Stage-0 kernel (full cubical) + reader/elaborator/REPL |
| M1 | Quantitative grading exploited at the surface |
| M2 | Effects + handlers (! E judgement) |
| M3 | Tower rewritten in Blight + tactics |
| M4 | Native backend (LLVM) |
| M5 | Region elision from grades + GC maturation |
| M6 | Self-hosting model + ecosystem (std tree, spores, WASM, extended re-checker) |
| M7-M14 | Post-M6 hardening: console/foreign/heap/int codegen, re-checker completeness, dependent-match refinement into the kernel, metatheory notes, intrinsic self-host sketch |
| M15-M19 | Share-nothing multicore + distributed runtime: thread-local runtime, std/actor.bl graded actor/CSP API, native worker pool, structural serializer, blight-net TCP transport (zero TCB growth, no new foreign axioms) |
| M20-M24 | Max-performance sweep, all untrusted: backend fast-Nat recognizer (O(n)→O(1) arithmetic, differential-tested), tagged-pointer unboxing, cross-object LTO, untrusted fixed-point Float (no kernel FloatTy), distributed-actor remote addressing (NodeId/Router). Zero TCB growth, no new foreign axioms |
See the milestone map, the M6 status, and the post-M6 roadmap (M7-M24) for detail.
- The combination of cubical + grading + effects in one kernel has no published end-to-end
normalization proof; M0 soundness rests on the component results plus extensive testing (spec §10).
An external, machine-checked (Lean 4) mechanization lives in
mechanization/(see docs/metatheory-mechanized.md), zerosorry: it covers the QTT grade semiring, weakening + substitution for the graded core, the constant-family cubical Kan fragment (transp/hcomp), a strong-normalization + canonicity proof for that fragment (a Tait reducibility-candidates argument; a closedBoolreduces tott/ff), and a separate bona-fide dependent-Πcore with weakening/substitution/progress/preservation. It is a genuine independent proof, not a restatement of the kernel's own checking. What it does not yet cover, honestly: the fully heterogeneous cubical corner — non-constant type lines,PathP,Glue/ua— and dependentΣ(which needs a definitional-conversion relation as a prerequisite); those remain outside the mechanized fragment. - The cubical layer ships
transp/hcomp/compandGlue/unglue. Bothfunextanduaare exported from std/path.bl:funextre-checks, anduais built from a single-faceGlueline. The univalence computation rule (transpoverua ereduces toe's forward map) holds definitionally in the kernel and is witnessed by a white-box test plus the closed ua_compute.bl. What is deliberately deferred is a polymorphicua-computeslemma proved inside Blight: that would force threading De Bruijn levels through the entire open-Kan API (every structural reducer and the equivalence machinery) — trusted-surface growth the kernel-size analysis argues against. The independent re-checker modelsua/Glue(F1): the ground univalence computation re-checksOkin both checkers; only this polymorphic restatement is deferred. See docs/metatheory.md §1.4–§1.5. --target=wasm32emits a WebAssembly object and, when a wasm-capableclang+wasm-ldare available (setBLIGHT_WASM_CC/BLIGHT_WASM_LDor have them onPATH), links a runnable.wasmmodule exportingbl_mainagainst a minimal freestanding wasm ABI; without that toolchain it falls back to emitting the object only.- The re-checker covers the core fragment plus multi-parameter / multi-index inductive eliminators,
the cubical Kan table (transp/hcomp/comp), native
Int, and effects/handlers + partiality at the type level (a genuine second opinion, not a blanket decline). It honestly declines (rather than rejects) only the genuinely out-of-fragment forms: cubical partial elements andforeignpostulates (an FFI axiom you must believe — it genuinely grows the trusted base, since the kernel cannot re-verify a foreign symbol). CubicalGlue/ua(F1) and universe-level variables (T2) are now modeled, not declined. Declines are counted and reported, never silently skipped. - Performance is correctness-first, not throughput-first:
Natis unary (so a bespoke recursion overSuccis O(n) allocations — though the backend recognizer (M20) makes the preludeplus/mult/etc. and the untrustedFloat(M23) O(1) machine-word ops, and the primitive machineIntis O(1); see docs/benchmarks-game.md), values are heap-boxed (M21 unboxes machine-wordNat/Intand nullary cons into tagged pointers), LLVM optimization passes (--opt) now pay off via M22 cross-object LTO (~1.15x on allocation-heavy loops, bit-identical; see docs/performance.md §2f), the trampoline allocates a thunk per step, and the native heap starts at 64 MiB and grows (doubling semi-spaces) under pressure, so only a true host-OOM aborts. The wasm runtime is a bump allocator only and omits the GC,Later/effects, and regions. See docs/performance.md for the full picture.
Licensed under the MIT License.