Typed Clojure with one canonical source shape, structured diagnostics, and a compiler-backed authoring loop.
Beagle is a typed Lisp designed for ordinary text editing and a short authoring loop. The product is not a high target count: it is one source language whose parser, checker, canonicalizer, and repair tools give people and agents the same answer. A backend stays only when a real consumer makes its semantics testable.
There are two deliberate compilation paths. The source profile chooses between
them: .bgl with bare #lang beagle always targets Native Core and lowers to
an immutable validated Native Core program. The .bgl source is not
target-neutral and does not mean "no target selected"; the resulting frozen
native program is backend-neutral so it can be materialized as native code.
C17 and QBE are the current materializers; Wasm belongs at that same
materializer layer rather than becoming another source profile.
Hosted profiles emit source for a runtime or evaluator that remains part of the
system: .bclj, .bjs, and .bnix select Clojure, JavaScript, and Nix
respectively. The Native Core lowering tool may itself be implemented and run
as hosted .bclj during bootstrapping. That is an implementation detail of the
compiler, not an optional native path from .bclj. Fram stays Beagle source in
both cases.
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 the hosted backends, plus the Core build boundary.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 compiler and its toolchain. Inside the devshell (direnv allow), the binary is bin/beagle — written beagle below. Using the compiler
requires no database or coordinator:
$ cat src/main.bclj
#lang beagle/clj
(ns main)
(defn greet [name: String] -> String
(str "hello " name))
(println (greet "from Beagle"))
$ beagle doctor --deep
Authoring loop: ok
$ beagle check --agent src/main.bclj
0 errors
$ beagle build src/main.bclj build/main.clj
src/main.bclj -> build/main.clj
$ bb build/main.clj
hello from Beaglebeagle init --target TARGET DIR scaffolds a project for any live target;
beagle build FILE OUT writes the target's source instead of linking a binary.
For Core, author .bgl with bare #lang beagle and select the projection
separately: beagle build --materializer c17|qbe --out DIR FILE.bgl. The build
always writes module.native-program and its digest; only the selected C17 or QBE
artifact is projected beside it.
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-, one-, and two-entry parameter or typed-field vectors stay inline when the complete signature fits within 80 columns:
(defn zero [] -> Int 0)
(defn increment [x: Int] -> Int (+ x 1))
(defn add [x: Int y: Int] -> Int (+ x y))Three or more entries always put the vector on the following line. An
over-width zero-, one-, or two-entry signature does too. A vertical vector has
one logical entry per line and is never partially packed. Binding names start
in the same column; a typed entry is flat name: Type. Names and types are
never padded into columns:
(defn clamp
[long-name: Int
minimum: Int
maximum: Int] -> Int
...)Types attach to names, so flat name: Type is the only annotation spelling and
one vector annotates every binding or none (& rest is exempt). The reader
accepts either physical layout; beagle fmt --write . performs the token-aware
mechanical rewrite and beagle fmt --check . gives people, CI, and agents the
same answer without making whitespace part of language validity.
Hosted emission is for domains where the host source and runtime are part of the product:
.bclj / .bjs / .bnix → parse → check → emit → .clj / .js / .nix
The Nix path is exactly this kind of hosted path. A .bnix file becomes a Nix
expression and is evaluated by Nix. It does not enter the native pipeline. This
is useful because Beagle can type a NixOS option against the real option schema:
assigning a String to services.openssh.enable fails before
nixos-rebuild.
Native Core is the .bgl lowering path, not another idiomatic source emitter:
.bgl + #lang beagle → source stage → typed stage → frozen native program
├─→ restricted C reference
├─→ QBE IL → native object
└─→ Wasm (materializer)
The frozen native program owns typed operations, effects, regions, layouts, control flow, capabilities, and ABI facts. Materializers are deliberately replaceable projections of that same frozen program. They are judged by correct binaries and independent agreement, not by whether a human would maintain the generated C or QBE.
Fram's files remain Beagle; they are not rewritten as C or another systems
language. The Core path is beagle build --materializer c17|qbe: it accepts
canonical .bgl, freezes one native program, and materializes only the selected
projection. The generated
fram.fri-replay report
is a concrete vertical slice: real Fram parser, mutation, outcome, and replay
bodies lower into one validated Native Core program and execute through the
reference materializer.
The table below is the compiler's live direct-emitter inventory, not a strategic promise that every current row will remain. Each admitted target must have a real consumer and an executable semantic oracle.
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 |
|---|---|---|---|---|---|
core |
Beagle Native Core | .bgl |
#lang beagle |
frozen native program | live — native pipeline: frozen native program; select C17 or QBE materializer |
clj |
Clojure | .bclj |
#lang beagle/clj |
.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 |
Four source profiles. Core produces the authoritative frozen native program; --materializer c17|qbe selects a projection. 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.
Core is a source profile, not a direct source emitter; its row names the frozen
native program build product while the materializer remains an explicit build
option.
Profiles 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 Native Core
.bgl. - north — a work tracker and
agent orchestrator over one triple graph, authored in
.bcljand built on Fram.
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 a universal idiomatic-native transpiler — hosted emitters exist where generated source is a real interface; native code comes from one frozen native program and replaceable materializers.
- 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.