A small, readable scripting language with fast tooling, genuinely good error messages, and extremely questionable sheep-related naming decisions.
Website · Playground · Docs · Tour · Spec · Architecture · Roadmap
const FLOCK = ["Dolly", "Shaun", "Lambchop"]
fn greet(name) {
return "Baa, {name}!"
}
for name in FLOCK {
baa greet(name)
}
baa "That's {len(FLOCK)} sheep accounted for."
$ baa run hello.baa
Baa, Dolly!
Baa, Shaun!
Baa, Lambchop!
That's 3 sheep accounted for.Contents
What Baa is · Install · Your first flock · The language in ninety seconds · Diagnostics · Commands · Standard library · Examples · Editor support · Would you rather this were Rust? · Development · Brand assets · Why sheep? · Licence
A complete, working programming language: a hand-written lexer, a recursive-descent parser with error recovery, a real AST, a semantic analyser, a tree-walking interpreter, a standard library, a formatter, a linter, a test runner, a REPL and a project tool. It runs real programs. It is not a syntax mock-up.
Nothing here is a wrapper around another language. Baa does not compile to JavaScript, transpile to anything, or hand your program to another runtime. The lexer reads characters, the parser builds a tree, the resolver binds every name to its declaration, and the interpreter walks that tree — all of it written for this language and tested against a conformance suite of 63 programs pinned to their exact output.
GitHub's language bar counts the implementation, which is TypeScript for the
frontend and reference runtime, and Rust for the native one. That is the same
thing it says about every young language — Elm's compiler is Haskell, Gleam's
and Roc's are Rust — and it is not a statement about what .baa files are.
Baa itself is absent from that bar for the ordinary reason: GitHub's Linguist
only recognises languages that are already in wide use.
The joke is the name. Everything underneath is built to be used.
| Small core | Fourteen statement forms, one numeric type, no inheritance, no hidden coercions. You can hold the whole language in your head. |
| Diagnostics that help | Every error has a stable BAAnnn code, a source span, an underlined excerpt and, where possible, a suggestion. |
| Fast enough to be practical | Around 1.2 million function calls and 4.5 million loop iterations per second on a laptop. baa run starts in about a tenth of a second. |
| Tooling in the box | fmt, lint, check --watch, test, doc, lsp, repl, init, build, doctor. No plugin hunt on day one. |
| Serious when it needs to be | --no-baa, or CI=true, swaps every sheep joke for neutral wording and keeps the codes identical. |
| Nothing to trust | No third-party packages. Nothing is downloaded, nothing runs implicitly, and no subprocess ever sees a shell. |
| Less than you have | --deny-fs, --deny-env, --deny-process and --allow-fs <dir> run a program with fewer capabilities than the shell that started it. |
| Readable by a program | --format json on check, lint and fmt: the same diagnostics the terminal shows, as one JSON object a CI job can annotate from. |
| Two places to run | The same files serve web pages and build native Windows applications. One language, one set of tests, two targets. |
Verified on every commit, across Windows, Linux and macOS: 750+ tests, a formatter that must be a fixed point, a linter that must be clean, a conformance suite that pins the exact output of 63 programs, and a browser running the playground to prove the interpreter still compiles for the web.
Baa needs Node.js 22.18 or newer, and nothing else at runtime.
npm install -g baa-lang
baa doctor # check the installationOr run it without installing anything:
npx baa-lang run hello.baaFrom a clone, for working on Baa itself
Node runs Baa's TypeScript sources directly, so there is no build step for development:
git clone https://github.com/PatrickJnr/sheep.git
cd sheep
npm install # dev dependencies: typescript, and puppeteer-core for the site checks
npm link # puts `baa` on your PATHEvery command also works as node src/cli/index.ts <command>, with nothing
linked.
Publishing is the one place a build happens. Node refuses to strip types from
files under node_modules, so the npm package ships compiled JavaScript,
produced by npm run build and invoked automatically by prepack.
Why not Rust? The question was not ducked, and a Rust implementation is an open track rather than a maybe. See ARCHITECTURE.md for the trade-offs and the Rust section for the plan.
baa init hill-farm
cd hill-farm
baa run
baa testbaa init writes a baa.toml, a main.baa, a module and a test, so you start
with a project that already has something to run and something to prove.
Or try it with nothing installed at all, in the playground. That runs the genuine interpreter, compiled to JavaScript, not a re-implementation.
// Bindings. `let` can change, `const` cannot, and the compiler checks.
let sheep = 12
const MAX_SHEEP = 100
// Strings interpolate with braces, and take whole expressions.
baa "The flock holds {sheep} of a maximum {MAX_SHEEP}."
// Functions, with defaults and rest parameters.
fn tally(label, ..counts) {
return "{label}: {counts.sum()}"
}
baa tally("this week", 3, 4, 5)
// Arrays and maps, compared by value rather than identity.
const flock = ["Dolly", "Shaun"]
const ages = { Dolly: 6, Shaun: 4 }
baa flock.map(fn(name) { return ages[name] })
// Loops over anything with elements.
for name, age in ages {
baa "{name} is {age}"
}
// `match` is an expression, and patterns compare structurally.
const size = match len(flock) {
0 => "empty",
1 || 2 => "a small flock",
n if n > 50 => "a very large flock",
_ => "a flock",
}
// Errors carry values, and runtime failures are catchable.
try {
baa flock[99]
} catch problem {
baa "{problem.code}: {problem.message}"
}
The full tour is in LANGUAGE.md, about fifteen minutes end to end. The precise rules, including the grammar, are in SPEC.md.
Baa spends real effort on being wrong helpfully.
$ baa check flock.baa
error[BAA102]: `sheap` is not part of the current flock.
┌─ flock.baa:4:19
│
3 │ fn greet() {
4 │ baa "Baa, " + sheap
│ ^^^^^ not found in this pasture
│
= help: Did you mean `sheep`?
1 file checked, 1 errorThree things worth knowing:
- Codes are stable.
BAA102means the same thing in every future version, so grepping a CI log for it is safe. - Professional mode keeps the information.
--no-baaturns that message intoUndefined name \sheap`.with the same code, span and suggestion.CI=true` does it automatically. - Runtime failures carry a real stack, captured where the failure happened
rather than reconstructed afterwards, and they are catchable as a map with
code,message,file,lineandcolumn. - A tool can read them too.
baa check --format jsonwrites one JSON object carrying every diagnostic with its code, both wordings, file and range, so a CI job annotates a pull request without parsing prose: docs/diagnostics-json.md.
All 48 of them are listed in docs/errors.md.
| Command | What it does |
|---|---|
baa run [file] |
Execute a program, or the project entry point |
baa <file> |
The same, without saying run. What a shebang uses |
baa check [paths] |
Parse and analyse without running |
baa test [paths] |
Run test "..." { ... } blocks |
baa fmt [paths] |
Format source files, --check for CI |
baa lint [paths] |
Report warnings, --deny-warnings for CI |
baa repl |
Interactive session |
baa lsp |
Language server, for editors |
baa init [dir] |
Create a new project |
baa build |
Validate the project and write baa.lock, --locked to verify it |
baa add / baa remove |
Manage local dependencies |
baa doc [paths] |
Write a reference from /// comments |
baa doctor |
Diagnose the installation |
baa modules |
List the standard library |
baa serve [dir] |
Serve a directory of .baa pages over HTTP |
baa app <action> |
Native applications: new, build, run, test |
Full reference, including exit codes and the manifest format: docs/cli.md.
Nine modules, sheep-branded on the outside and completely boring on the inside. An API you have to remember at 2am is no place for a joke.
| Module | Contents |
|---|---|
wool |
Text: formatting, casing, wrapping, bytes |
flock |
Collections: grouping, chunking, zipping, building maps |
ram |
Arithmetic: rounding, integer division, statistics, constants |
meadow |
Time and chance: clocks, calendars, seeded randomness |
pasture |
Files and paths |
shepherd |
Arguments, environment, stdin, subprocesses |
lamb |
JSON |
gate |
Web requests and replies, over CGI |
barn |
Native windows: controls, layout and events |
Plus a ten-name prelude that needs no import. Full reference: docs/stdlib.md.
Every file in examples/ is executable, formatted by baa fmt,
clean under baa lint, and run by the test suite. All but one have their exact
output recorded and compared byte for byte; stdlib.baa reads the clock, so it
is executed and checked for errors rather than compared. If one breaks, CI says
so.
| File | Shows |
|---|---|
hello.baa |
The smallest useful program |
variables.baa |
Bindings, types, operators |
functions.baa |
Defaults, rest params, closures |
loops.baa |
Every loop and iteration form |
collections.baa |
Arrays and maps in anger |
modules.baa |
Imports, aliases, local files |
errors.baa |
Throwing, catching, finally |
stdlib.baa |
Every module whose output is the same on every machine |
site/ |
A website: pages that answer HTTP requests over CGI, running here |
fizzbuzz.baa |
match on structural patterns |
large_program.baa |
A ~200-line flock register: parsing, validation, statistics, a report and JSON |
A .baa file is a web page. It is also, when it imports barn instead of
gate, a desktop application:
import barn
const window = barn.window({ title: "Hello", width: 320, height: 140 })
const layout = barn.column(window, { weight: 1 })
const label = barn.label(layout, { text: "Baa", align: "center", size: 20 })
const button = barn.button(layout, { text: "Again" })
fn on_click() {
barn.set_text(label, "Baa baa")
}
barn.on(button, "click", on_click)
barn.show(window)
barn.run()
$ baa app build
Built build/Hello.exe
1 module, using barn
736 KB, windowedOne executable. No Node.js on the machine that runs it, no browser inside it, no unpacking: a real Win32 window with the system's own controls, its own menu bar and its own file dialogs. It carries its own icon and version metadata, written into the executable without a linker, so Explorer and the Properties dialog show what the manifest says.
baa app build analyses the program with exactly the code baa check uses,
writes the resolved tree into an image, and appends that image to a runtime
written in Rust. There is one frontend, so the two cannot disagree about what
your program means, and the runtime passes all 63 conformance programs byte for
byte.
The calculator in
examples/native/calculator/ imports the web
calculator's arithmetic module unchanged — same tokeniser, same
precedence-climbing parser, same tests, two front ends. That is the shape the
platform is for.
Windows today. The window model has no Win32 in it and a second backend is an
addition rather than a rewrite, but until somebody writes one, barn.show on
another platform says so.
Native applications ·
barn reference ·
Building for Windows
baa lsp is a language server. It provides diagnostics as you type, whole-file
formatting, a document outline, hover, go to definition, find references and
rename, and it runs the same analysis as baa check and baa lint, so an
editor cannot disagree with the command line about whether a file is valid.
Neovim, Helix and Emacs can point at it directly. Setup for each, and what the server does not do yet, is in docs/editors.md.
editors/vscode/ is a VS Code extension that starts the
server for you, alongside syntax highlighting, snippets, and bracket and
comment configuration. Search the Extensions panel for Baa, or:
code --install-extension baa-lang.baa-langOpen a .baa file and everything above works with no configuration. It
analyses nothing itself — it runs baa lsp, so the editor sees what
baa check sees.
Every release also attaches a .vsix, for an air-gapped machine or a version
the marketplace does not have yet. Install that with
code --install-extension baa-lang.vsix rather than by double-clicking it,
which hands the file to Visual Studio — a different product, which will read
the package correctly and refuse it.
Definition, references and rename read the resolver's symbol table rather than the text, so renaming a binding that shadows an outer one of the same name rewrites the inner uses and leaves the outer alone.
So would some of us. The reference implementation is TypeScript for the reasons written up in ARCHITECTURE.md, and a second implementation is an explicitly open track.
Everything a port needs already exists and is kept fresh by CI:
SPEC.md: the full language definition, with an EBNF grammartests/conformance/suite.json: 63 programs with their exact output, and 27 with the diagnostic codes they must reporttests/conformance/diagnostics.json: all 48 diagnostics, both wordings, ready to embedrust/README.md: crate layout, order of work, and the design notes worth carrying over
Half of it now exists. rust/crates/baa-native is a working Rust
runtime — values, the tree-walking interpreter, eight standard-library
modules, a window model and a Win32 backend — written for
native applications. It passes all 50
conformance programs byte for byte.
It has no lexer, no parser, no resolver, no formatter, no linter and no CLI,
and gains nothing from having them: the reference implementation hands it a
resolved tree, so there is one frontend and it cannot disagree with itself.
That leaves the interesting half of a second implementation open, and the
milestone unchanged — a conformant baa run from Rust source, passing both
halves of the suite. Whoever takes it on starts with a runtime that already
works and a conformance harness that already runs.
npm test # the full unit and integration suite
npm run typecheck # tsc --noEmit, strict
npm run fmt:check # the formatter must be a fixed point
npm run lint # Baa's own linter, over the examples
npm run test:baa # Baa's test blocks, run by Baa
npm run bench # front-end and runtime benchmarks
npm run ci # all of the aboveRegenerating derived files, all of which are committed and checked in CI:
npm run gen # docs and the conformance suite (plus the site, if present)The pipeline is deliberately separable (lexer, parser, AST, resolver, runtime) so each stage is tested on its own. ARCHITECTURE.md explains the shape and the route towards a bytecode VM.
Contributions are welcome: CONTRIBUTING.md.
The PNGs in assets/images/ are what this README embeds, so
the images resolve from a plain clone with no server involved. PNG rather than
SVG because the wordmark is live text, which would otherwise render in whatever
serif the viewer happens to have installed.
The SVG originals are served from the site, and are the better choice anywhere that can use them.

| Asset | In this repository | Hosted |
|---|---|---|
| Mark | assets/images/icon.png |
icon.svg |
| Lockup | logo.png · dark |
logo.svg |
| Social card | social.png · dark |
social.svg |
The site these are served from is built and deployed separately and is not
part of this repository. Palette: ink #22352C, wool #F7F5EE, pasture
#2F4B3F.
print is a boring word and baa is not. That is genuinely the whole origin
story.
It turned out to be a useful forcing function. A language with a ridiculous name gets no benefit of the doubt, so everything else had to be right: the diagnostics, the formatter's determinism, the test coverage, the specification. Nobody excuses a bad error message because the project is a joke.
The humour is kept where it cannot do harm: module names, error wording and documentation. It never changes what an operator does, and it is one flag away from gone.
MIT. See LICENSE.