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.

git clone https://github.com/PatrickJnr/sheep.git && cd sheep && npm install && npm link
hello.baa
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.

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.

750+automated tests
48diagnostic codes
9standard modules
0third-party packages

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.

basics.baa
// `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()

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.

Browse the full catalogue →

$ baa check flock.baa
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`?
$ baa --no-baa run count.baa
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.

a working day
$ 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.

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.

Native applications · The barn module

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.

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.

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.

More reasonable questions →

The rule. Sheep terminology lives in module names, error wording and documentation. Never in operator semantics, never in standard-library function names, and never anywhere it makes a program harder to read at 2am.
the same error, both ways
$ 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.