Skip to content

Repository files navigation

Beagle

License: MIT OR Apache-2.0

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.

Documentation

Static reference stays thin on purpose — the compiler answers instead: beagle help, beagle langs, beagle sig, beagle fields.

Quickstart

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 beagle

beagle 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.

One canonical source shape

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.

Think high-level, get native

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.

Targets

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.

Real codebases author against Beagle

  • firn — a complete NixOS system, authored in .bnix and schema-typed end to end; builds from flake.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.

How it is held correct

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.

What it isn't

  • 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.

Contributing

Read CLAUDE.md first — its three-statement generative spec is the canonical anchor for any surface question.

License

Dual-licensed under the MIT License or the Apache License, Version 2.0, at your option. See LICENSE for the chooser.

About

a typed programming language that emits to multiple backends

Topics

Resources

Stars

Watchers

Forks

Releases

Packages

Contributors

Languages