Contributing to Baa

Thanks for looking. Baa is a small, deliberately coherent language, and the most valuable contributions are the ones that keep it that way.


Getting set up#

You need Node.js 22.18 or newer. There is no build step, Node runs the TypeScript sources directly.

bash
git clone https://github.com/PatrickJnr/sheep.git
cd sheep
npm install
npm run ci        # typecheck, format check, lint, tests

npm link puts baa on your PATH. Without it, every command works as node src/cli/index.ts <command>.

Read ARCHITECTURE.md before your first change. It is short, and it explains why things are where they are.

The commands you will use#

CommandWhat it does
npm testThe full Node test suite
node --test tests/parser.test.tsOne suite
npm run typechecktsc --noEmit, strict
npm run fmt / fmt:checkBaa's formatter over the examples
npm run lintBaa's linter over the examples
npm run test:baaBaa's own test blocks, run by Baa
npm run benchBenchmarks
node tools/gen-docs.tsRegenerate the generated docs
node tools/record-examples.tsRe-record example output

Working on the native runtime#

The Rust runtime in rust/ is optional: everything in src/ builds and tests without it, and the tests that need it skip with a stated reason.

bash
cargo build --release --manifest-path rust/Cargo.toml   # once
cargo test --manifest-path rust/Cargo.toml              # its own units
npm run lint:rust                                       # clippy, with warnings denied
npm run test:native                                     # conformance, then the drift guards
npm run bench:native                                    # before claiming anything is faster

Two rules keep the two implementations from drifting apart, and both are enforced by tests rather than by discipline:

  • The reference implementation is the definition. Where the runtime and

    src/ disagree, the runtime is wrong, and node tools/native-conformance.ts says so. If the specification is unclear, fix the specification first.

  • Anything written twice is compared. The module list, barn's functions

    and their arities, and the image format's version all exist on both sides; tests/native.test.ts compares them. Adding a barn function means adding it in rust/.../stdlib/barn.rs and src/stdlib/barn.ts, and the test will tell you if you forget.

The diagnostic catalogue is generated, not copied: change src/diagnostics/codes.ts and run npm run gen.

What makes a good contribution#

Bug fixes are always welcome. A failing test in the pull request is worth more than a paragraph of description.

New standard-library functions need to answer: would most programs that need this write it badly by hand? flock.group_by earns its place; flock.second_to_last does not.

Language changes need a problem, not a feature. Open an issue first with a real program that is awkward to write today. The bar is high on purpose: every keyword is one more thing a newcomer has to learn, and Baa's pitch is that you can hold it in your head.

Documentation improvements are genuinely valuable, especially where the docs and the implementation disagree: that is always a bug on one side or the other.

House rules#

Keep the humour where it belongs#

Sheep terminology lives in module names, error wording and documentation. It never lives in:

  • operator or keyword semantics;
  • standard-library function names (group_by, not herd_by);
  • anything that changes what a program does.

Every diagnostic needs both a woolly and a plain wording with the same placeholders. A test enforces this. If a joke would make a CI log harder to read, it does not go in.

Every diagnostic gets a code#

Add it to src/diagnostics/codes.ts in the right range (SPEC §8), with a source span, a note that explains what is wrong there, and a help that says what to do. Codes never change meaning; new diagnostics take new numbers.

Then run node tools/gen-docs.ts: docs/errors.md is generated.

Nothing is done without a test#

The suite is layered so a change can be tested at the level it happens:

ChangeTest in
Tokens, spans, triviatests/lexer.test.ts
Grammar, precedence, recoverytests/parser.test.ts
Scope, arity, module rulestests/resolver.test.ts
Semanticstests/runtime.test.ts
Library functionstests/stdlib.test.ts
A new diagnostictests/diagnostics.test.ts
Formattingtests/formatter.test.ts
Lint rulestests/linter.test.ts
Commands and exit codestests/cli.test.ts

If the change is user-facing, add or extend an example in examples/, then run node tools/record-examples.ts and review the diff.

The formatter must stay a fixed point#

format(format(x)) == format(x) for everything, and no comment is ever lost. Both are asserted by the test suite. If a formatting change breaks either, the change is wrong, not the test.

Cross-platform by default#

  • No hard-coded / or \: use node:path.
  • No shelling out.
  • Read files with the existing SourceFile, which normalises line endings so

    spans match on every platform.

  • Anything touching the outside world goes through RuntimeHost, so tests can

    swap it and a future sandbox can restrict it.

Style#

The TypeScript here is plain and boring on purpose: plain data, switch on a kind discriminant, no clever abstractions, no dependency injection framework. Match the surrounding code. Comments explain why, not what, the code already says what.

Type-stripping means no enum, no namespace, no parameter properties, and import type for anything used only as a type. npm run typecheck catches all of it.

Pull requests#

  1. Branch from main.
  2. Make the change, with tests.
  3. npm run ci must pass.
  4. Update the docs the change touches: SPEC.md for semantics, LANGUAGE.md

    for the tour, and CHANGELOG.md.

  5. Write a description that says what problem this solves. A before/after

    snippet is worth three paragraphs.

Small, focused pull requests get reviewed quickly. A change that touches the lexer, the standard library and the roadmap at once will not.

Reporting bugs#

The most useful report is a .baa file that misbehaves, plus what you expected and what happened. baa --version and baa doctor output help.

Specification bugs count: if SPEC.md and the implementation disagree, that is a bug in one of them, and finding it is a real contribution.

Security issues go to SECURITY.md, not to the issue tracker.

Releasing#

Releases are cut from a tag. The workflow verifies on all three platforms first, so a tag that does not build never becomes a release.

  1. Update CHANGELOG.md with a ## [x.y.z], YYYY-MM-DD heading. The release

    notes are extracted from that entry, and the workflow fails if it is missing.

  2. Set the same version in package.json. It is the only place the version

    lives: baa version and the conformance suite both read it from there, and the workflow refuses a tag that disagrees with it.

  3. git tag -a vx.y.z -m "Baa x.y.z" and push the tag.

Publishing to npm#

baa-lang is published with trusted publishing: GitHub Actions authenticates over OIDC with a short-lived token, and there is no npm token stored in the repository. npm is retiring the alternative, tokens that bypass 2FA lost account and package management in July 2026 and lose direct publish in January 2027, so there is nothing here to migrate later.

A trusted publisher is configured on the package's own settings page, which means it cannot be set up before the package exists. The first publish is therefore done by hand, once:

bash
npm login                      # interactive, with real 2FA
npm publish --access public

Then on npmjs.com, under the package's Trusted Publisher section, add GitHub Actions with the organisation PatrickJnr, the repository sheep, and the workflow filename release.yml. Every tagged release publishes itself after that.

Until that is done the publish step reports what is missing and stops without failing, because a GitHub release is a release whether or not npm has a copy.

Provenance attestations are generated by trusted publishing itself, now that the repository is private. npm does not attest a build nobody can inspect, and the workflow therefore does not pass --provenance: trusted publishing adds it by itself when a build qualifies.

Code of conduct#

By taking part you agree to the Code of Conduct. It is short, and it amounts to: be decent, assume good faith, and remember there is a person on the other end.