Typed Clojure with canonical source and structured diagnostics, compiling to idiomatic Clojure, JavaScript, Nix, Odin, Zig, and TypeScript.
Beagle is a typed Lisp designed for ordinary text editing and a short authoring loop. It compiles one high-level language to six targets — including Zig and Odin, which link to native executables. Each backend renders code that the target language's own programmers would recognize, not a lowest-common-denominator transliteration.
Types exist here for a specific job: making authoring, diagnostics, and automated repair reliable. They check at compile time and erase before emit. The point isn't rejection for its own sake — it's to give an authoring loop exact facts: what kind of mistake happened, where in the source, after which canonicalization, against which target.
docs/CHEATSHEET.md— the language surface, generated from the compiler; every example is parse- and type-checked by the suite.docs/surface.md— what the cheatsheet does not enumerate: canonical layout, macros, threading, reader conditionals, sourcemap fidelity.docs/targets-by-example.md— one source body across three backends, and a NixOS module typed against the option schema.docs/cli.md— the CLI and the authoring loop.docs/architecture.md— pipeline, layout, where to change what.docs/self-hosting.md— how the compiler is held correct.docs/target-policy.md— why targets get removed, not deprecated.docs/INFLUENCES.md— lineage and thesis.
Static reference stays thin on purpose — the compiler answers instead: beagle help, beagle langs, beagle sig, beagle fields.
The flake pins the whole toolchain, including the Zig the native backend links
with. Inside the devshell (direnv allow), the binary is bin/beagle — written
beagle below. Using the compiler requires no database or coordinator:
$ cat src/main.bzig
#lang beagle/zig
(ns main)
(defn main [] -> Nil
(println "hello from beagle"))
$ beagle build --target zig --exe ./hello src/main.bzig
src/main.bzig -> ./hello
$ ./hello
hello from beaglebeagle init --target TARGET DIR scaffolds a project for any of the six;
beagle build FILE OUT writes the target's source instead of linking a binary.
Run beagle doctor --deep before authoring to verify the complete diagnostic
path. beagle check --agent FILE is the fast compiler oracle; beagle init --hooks makes a project invoke it on each edit.
Zero- and one-entry parameter or typed-field vectors stay inline:
(defn zero [] -> Int 0)
(defn increment [x: Int] -> Int (+ x 1))Two or more entries put the vector on the following line. Binding names start
in the same column; : attaches to the name and has exactly one following
space. Names and types are never padded into columns:
(defn add
[long-name: Int
x: Int] -> Int
(+ long-name x))The parser hard-rejects other physical layouts and carries an exact source-range repair when changing the range cannot alter a comment. This gives people, formatters, and agents the same answer instead of a style choice.
A typed Beagle function:
(ns g)
(defn calc
[a: Int
b: Int
c: Int] -> Int
(+ (* a b) (- c a) (quot b 2) (rem c 3) (mod a 5)))becomes Zig that reaches for the language's own operators rather than a runtime shim (module preamble elided):
pub fn calc(a: i64, b: i64, c: i64) i64 {
return ((a * b) + (c - a) + @divTrunc(b, 2) + @rem(c, 3) + @mod(a, 5));
}Both halves are committed goldens in
beagle-test/tests/fixtures/zig-golden
and the suite fails when the emitter drifts from them.
The leverage runs upward too: types can come from the target itself. A NixOS
option carries one in the schema, so services.openssh.enable is known to be
Bool at compile time — assigning a String fails with file:line:col
precision before nixos-rebuild is ever invoked.
One AST, idiomatic output per backend — Clojure eager persistent maps, JavaScript plain objects and ES modules, Nix lazy attrsets, Odin structs and explicit context, Zig explicit allocators and error unions, TypeScript typed function boundaries over JS.
The table is generated from beagle-lib/private/targets.rkt by beagle doc-fill; query it live with beagle langs (--view domains for what each
target is for).
| target | language | source | #lang |
output | status |
|---|---|---|---|---|---|
clj |
Clojure | .bclj |
#lang beagle |
.clj |
live — self-hosted, oracle-certified, fuzz-guarded |
js |
JavaScript | .bjs |
#lang beagle/js |
.js |
live — self-hosted, oracle-certified, fuzz-guarded |
nix |
Nix | .bnix |
#lang beagle/nix |
.nix |
live — self-hosted, oracle-certified, fuzz-guarded |
odin |
Odin | .bodin |
#lang beagle/odin |
.odin |
live — Racket emitter; self-host port pending conformance goldens |
zig |
Zig | .bzig |
#lang beagle/zig |
.zig |
live — Racket emitter with restored structural goldens |
scriptc |
TypeScript | .bsc |
#lang beagle/scriptc |
.ts |
live — Racket emitter over the JS lowering; experimental boundary |
Six language targets. facts is not one of them — it is the compact, lossy projection of the parsed AST into CNF analysis facts, represented as three-slot vectors (bin/beagle-facts): a query surface, not an authoring language. The verbose, program-lossless source↔fact projection is beagle facts-roundtrip, where lossless means reader-datum identity, not byte identity.
That status column is the maturity ordering: the three source-to-source
backends are oracle-certified and fuzz-guarded, the two native backends are held
by structural goldens. Targets are removed rather than deprecated when they stop
earning their place — docs/target-policy.md.
- firn — a complete NixOS system,
authored in
.bnixand schema-typed end to end; builds fromflake.bnix. - gjoa — a Firefox fork tuned for
power users, authored in
.bjs. - wake — an application compiler
(entities, views, routes → direct-DOM JS), itself authored in
.bjs. - fram — a slot-addressable,
typed-triple substrate with stratified Datalog, authored in
.bclj. - north — a work tracker and
agent orchestrator over one triple graph, authored in
.bclj.
The clj-target compiler is written in Beagle and compiles itself to a
byte-level fixpoint, with the original Racket compiler as a conformance oracle
and a nightly differential fuzz campaign holding the two to byte-exact agreement
on an empty exemption list — docs/self-hosting.md.
- Not a schema language, not a validation runtime — types check at compile time, then erase.
- Not a new Lisp in spirit — a strict typed subset of Clojure; divergence from Clojure must serve the type system or a backend, or it dies.
- Not stable. Pre-1.0, the surface still moves, and removals are hard breaks: there is no deprecation path.
- Not benchmarked. The repository gates correctness, not speed, and publishes no performance numbers.
Read CLAUDE.md first — its three-statement generative spec is the
canonical anchor for any surface question.
Dual-licensed under the MIT License or the
Apache License, Version 2.0, at your option. See
LICENSE for the chooser.