Version 0.8.0 · MIT · needs only Node
A programming language with a little more Baa.
A modern, readable programming language for people who enjoy clean syntax, fast tools and extremely questionable sheep-related naming decisions.
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."
What is Baa?
A complete language, not a syntax mock-up.
Baa has 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 deterministic formatter, a linter, a test runner, a REPL and a project tool. It runs real programs. The joke is the name; everything underneath is built to be used.
Small on purpose
Nine statement forms, one numeric type, no inheritance, no hidden coercions. You can hold the whole language in your head, and the specification fits in one page you might actually read.
Errors that help
Every diagnostic has a stable code, a source span, an underlined excerpt and a suggestion. There are 44 of them, and none say "unexpected token".
Quick enough
Roughly 1.2 million function calls and 4.5 million loop iterations a second. Not a match for anything compiled: comfortably fast for the scripting it is meant for.
Tools in the box
fmt, lint, check,
test, repl, init,
build, doctor, lsp,
serve, app. No plugin hunt on day one.
Serious when needed
--no-baa: or a CI environment variable:
swaps every sheep joke for neutral wording, keeping the codes and
spans identical. Your build log stays a build log.
Nothing to trust
Zero third-party packages: dependencies is empty, and
everything Baa imports is a Node built-in. Nothing is downloaded,
nothing runs implicitly, and no subprocess ever sees a shell.
The reasoning is written down.
The language
Familiar where it matters, sheep where it doesn't.
If you have written Python, Lua, Go or JavaScript, you can read Baa
already. The only word you have to learn is baa, which
prints.
// `let` can change. `const` cannot: and the compiler checks.
let sheep = 12
const MAX_SHEEP = 100
sheep += 1
// Strings interpolate with braces, and take whole expressions.
baa "The flock holds {sheep} of a maximum {MAX_SHEEP}."
// Defaults, rest parameters, closures, first-class functions.
fn tally(label, ..counts) {
return "{label}: {counts.sum()}"
}
fn make_counter(start) {
let count = start
return fn() {
count += 1
return count
}
}
const next = make_counter(10)
baa tally("this week", 3, 4, 5), next(), next()
import flock
import ram
const HERD = [
{ name: "Dolly", farm: "Hill", weight: 46.5 },
{ name: "Shaun", farm: "Hill", weight: 52.0 },
{ name: "Timmy", farm: "Dale", weight: 21.0 },
]
// Methods where a method reads well.
baa HERD.map(fn(s) { return s.name }).sort().join(", ")
// Modules where it doesn't.
const by_farm = flock.group_by(HERD, fn(s) { return s.farm })
for farm, members in by_farm {
const weights = members.map(fn(s) { return s.weight })
baa "{farm}: {members.length()} sheep, mean {ram.round(ram.mean(weights), 1)}kg"
}
// Arrays and maps compare by value, not by identity.
baa [1, [2]] == [1, [2]], { a: 1 } == { a: 1 }
// `match` is an expression, and patterns compare structurally:
// so a pair of booleans is a perfectly good pattern.
fn fizzbuzz(n) {
return match [n % 3 == 0, n % 5 == 0] {
[true, true] => "FizzBuzz",
[true, false] => "Fizz",
[false, true] => "Buzz",
_ => to_string(n),
}
}
fn describe(flock) {
return match flock.length() {
0 => "empty",
1 => "one lonely sheep",
2 || 3 => "a small flock",
n if n > 50 => "a very large flock ({n})",
_ => "a flock",
}
}
for n in 1..=15 {
baa fizzbuzz(n)
}
// Throw any value you like: errors keep their type.
fn shear(sheep) {
if sheep.shorn {
throw { code: "ALREADY_SHORN", sheep: sheep.name }
}
return "snip"
}
try {
shear({ name: "Dolly", shorn: true })
} catch problem {
baa "{problem.code}: {problem.sheep}"
}
// Runtime failures are catchable too, and carry a stable code.
try {
baa ["Dolly"][99]
} catch problem {
baa problem.code, problem.line // BAA304 18
} finally {
baa "gate closed"
}
// A standard module, a rename, and individual names.
import wool
import meadow as clock
import { round, mean } from ram
// A local file. Paths are relative to the importing file.
import "./pen.baa"
import "./helpers/text.baa" as text
baa wool.title_case("the flock is thriving")
baa clock.format(0, "YYYY-MM-DD")
baa round(mean([1, 2, 3, 4]), 2)
let my_pen = []
pen.admit(my_pen, "Dolly")
baa pen.describe(my_pen)
// Modules are values, so you can pass one around.
fn report(module, subject) {
return module.describe(subject)
}
baa report(pen, my_pen)
Diagnostics
Beautiful errors, because even sheep deserve good diagnostics.
Every message has a stable BAAnnn code, the file, the line,
the column, the offending source line with the exact span underlined,
and, where Baa can work it out, the fix.
Codes never change meaning. You can grep a CI log for
BAA102 and rely on it meaning the same thing in every
future version.
Runtime failures carry a real call stack, captured where the failure
happened rather than reconstructed after the fact. And when you would
rather not be told about sheep, --no-baa gives you the
same information in neutral wording.
error[BAA102]: `sheap` is not part of the current flock.
┌─ examples/hello.baa:4:19
│
3 │ const flock = ["Dolly", "Shaun"]
4 │ baa "Baa, " + sheap
│ ^^^^^ not found in this pasture
│
= help: Did you mean `sheep`?
error[BAA201]: `count_sheep` expects 1 argument(s) but received 2.
┌─ count.baa:9:13
│
8 │ fn count_sheep(flock) {
9 │ baa count_sheep(flock, true)
│ ^^^^^^^^^^^^^ too many arguments
│ declared here → count.baa:8:4
│
= trace:
at count_sheep (count.baa:9:13)
at main (count.baa:14:1)
Tooling
Fast enough to outrun a startled sheep.
One executable, twelve subcommands, no configuration file needed to start. Formatting is deterministic, linting has an escape hatch, and every command has an exit code you can build a pipeline on.
$ baa init hill-farm
Created hill_farm in hill-farm
baa.toml
main.baa
greetings.baa
tests/greetings_test.baa
$ cd hill-farm && baa run
Hello, flock!
Baa, Dolly!
$ baa test
tests/greetings_test.baa
ok greets a sheep by name 0.2ms
ok greets every sheep in a flock 0.3ms
2 passed, 0 failed in 4ms
$ baa fmt --check .
11 files: 0 would change, 11 already tidy
$ baa lint --deny-warnings .
11 files linted, no problems
Deterministic formatting
The same syntax tree always produces the same bytes, and a second run changes nothing. Comments are never lost: there is a test that proves it over every example and a set of deliberately awkward cases.
Tests in the language
test "name" { ... } is a statement, not a framework.
baa run registers tests without running them;
baa test runs them and reports timings.
Reproducible runs
baa run --seed 42 makes everything drawing on
meadow deterministic, so a flaky test is a real bug
rather than bad luck.
Built for CI
--check, --deny-warnings and proper exit
codes. CI=true turns off colour and the jokes on its
own: nothing to configure.
Diagnostics a program can read
--format json on check,
lint and fmt writes one object carrying
every code, span and wording, so a job can annotate a pull request
without parsing prose.
Less than your shell has
--deny-fs, --deny-env,
--deny-process and --allow-fs <dir>
run a program you have not read with fewer capabilities than the
shell that started it.
Standard library
Zero dependencies. Maximum fleece.
9 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, so
the functions are called things like group_by and
round.
wool
Text. Formatting, case conversion, wrapping, centring, bytes.
flock
Collections. Grouping, chunking, zipping, partitioning, building maps from pairs.
ram
Arithmetic. Rounding, integer division, statistics, constants, base conversion.
meadow
Time and chance. Clocks, calendars, and randomness you can seed.
pasture
Files and paths. Reading, writing, listing, and platform-aware path arithmetic.
shepherd
The outside world. Arguments, environment, stdin, and subprocesses that never see a shell.
lamb
Data. JSON in, JSON out, with clear errors for anything that has no JSON form.
prelude
Always in scope: len, type_of, clone, assert_eq and six more.
Two places to run
The same files. A web page, or a Windows application.
A .baa file is a web page when it imports
gate, and a desktop application when it imports
barn. Same language, same tooling, same tests. A module
that imports neither belongs to both, which is how the native
calculator and the web calculator share one arithmetic file.
On the web
A page is a program that writes a reply. It runs under CGI on ordinary shared hosting, one process per request, with no JavaScript required on the page.
A live example, rendered by Baa, right now · Web pages
On the desktop
baa app build produces one Windows executable holding
the runtime and your program. A real window with the system's own
controls, its own menu bar and its own file dialogs — no browser
inside it, and no Node.js on the machine that runs it.
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()
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 rather
than doing nothing.
Examples
Every example in the repository runs.
They are all formatted by baa fmt, clean under
baa lint, and their output is recorded and asserted by the
test suite. If one of them breaks, CI says so.
hello.baa
The smallest program that does something useful.
variables.baa
Bindings, every value type, and all the operators.
functions.baa
Defaults, rest parameters, closures, recursion.
loops.baa
Every loop form, over every iterable.
collections.baa
Arrays and maps used in anger.
errors.baa
Throwing, catching, finally, and structured errors.
stdlib.baa
A tour of six standard modules, in one program.
large_program.baa
A ~200-line flock register: parsing, validation, statistics, a report and JSON.
For AI agents
A reference written for models, generated from the compiler.
Baa is small and new, so a language model has not read much of it.
Rather than hope, the site publishes
/llms.txt and
/llms-full.txt, following the
llms.txt convention: plain Markdown at a known address, meant
to be fetched by an agent before it writes anything.
llms.txt
The index, about two kilobytes. What Baa is, the rule that decides whether a file is a web page or a native application, and where the real documentation lives.
llms-full.txt
Everything needed to write Baa that runs, in one fetch: the syntax, every standard-library signature, which modules exist on which target, and worked examples of a module, a page and an application.
Why generate it
A hand-written cheat sheet goes stale, and a stale signature is worse than none: a model given one writes code that cannot run and has no way to notice. So both files are built from the same source the compiler uses. Every function, every argument count and every diagnostic code in them is read out of the implementation, and the build fails if the published copy no longer matches.
The examples are compiled by the test suite, and the section on mistakes is checked against the language itself: each claim about what goes wrong has a test asserting it still goes wrong that way.
Most of it is about what does not work. Which modules
a native application cannot use, that if is a statement
rather than an expression, and the one construct that compiles and
silently produces the wrong answer — a { inside a
string opens an interpolation, so "{" + x + "}" is one
string rather than three. Those are the things a model gets wrong, and
they are stated first.
Why sheep?
Because a language called Baa has to be twice as good.
print is a boring word. 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, documentation. It never changes what an operator does, and it is one flag away from gone.
$ baa check pen.baa
error[BAA103]: `MAX_SHEEP` was shorn with `const`: its value can't grow back.
$ baa --no-baa check pen.baa
error[BAA103]: Cannot assign to constant `MAX_SHEEP`.
Same code, same span, same suggestion. Only the wording moves.
Community
Finally, a language that understands the importance of a good flock.
Baa is open source under the MIT licence, built in the open, and small enough that a newcomer can read the whole implementation in an evening.
Read the source
Around 14,000 lines of documented TypeScript — lexer, parser, resolver, runtime and tooling — plus 8,000 of Rust for the native runtime.
Contribute
The house rules, the test layout, and what makes a change likely to be merged. Documentation fixes count.
Report a bug
The most useful report is a .baa file that misbehaves.
Specification bugs count double.
Would you rather this were Rust?
So would some of us. There is a full port plan and a conformance suite waiting for whoever wants it.
See the roadmap
What is next, and, just as importantly, what was considered and deliberately left out.
Just try it
The real interpreter, running in your browser. No install, no sign-up, no telemetry.