Development & testing

Amber has no build system beyond a shell script and no dependencies beyond a C compiler. A full rebuild is about eleven seconds. That is a deliberate property, not an accident of size — it is what makes "change a kernel, rebuild, re-measure" a loop you will actually run.

Build

git clone https://github.com/BonucciAndrea/amber && cd amber
./build.sh          # portable -O3 -flto, +OpenMP if available
./amber             # REPL

build.sh finds cc, gcc or clang in that order and builds with portable flags — no -march=native — so the binary runs on any x86-64 or arm64 host. Set CC to override. Nothing is installed system-wide.

Test suites

./amber test.k        # core language + primitives
./amber test-fin.k    # finance / HFT module
./amber test-ext.k    # extension seam

Each prints a count and a pass/fail line and exits non-zero on failure. test.k also runs the SIMD and parallel self-checks, printing the active backend and confirming that the vectorised and scalar paths agree.

The bar for a core change

Both suites green, and — for anything touching a kernel — evidence that output is unchanged. Fold your change behind the existing dispatcher rather than around it, and diff the results of a representative script against the previous binary before and after. A kernel that is faster and subtly different is a regression.

Differential oracles

Two scripts, tests/test_arith_diff.k and tests/test_fusion_diff.k, print the results and types of a few thousand expressions across every width, null, NaN, negative zero, range, generic list and mask shape. Run each under the previous binary and the new one and diff the outputs: a kernel rewrite is correct when only the hunks you meant to change remain. They are how 2.1.0's fused paths were shown to be bit-identical to 2.0.1 across 1.1 million lines of output.

Benchmarks

./amber bench.k                  # attribute / find speed-ups
./amber bench-fin.k              # the finance module
./amber bench-std.k              # moving windows, sorts
python bench/run_comparative.py  # the ten-engine table

The comparative harness specifies its workloads once in bench/SPEC.md; every engine implements that document and nothing else, and answers are compared bit-exactly against a C reference. See Benchmarks for what it produces and how to read it.

Adding a kernel

Read Engine internals first for the dispatcher shape. The pattern for a new type specialisation is:

  1. Write the kernel next to its siblings in the relevant translation unit — 3.c for a reduce or scan, 2.c for element-wise — following the local macro style.
  2. Widen the dispatcher's type guard to admit the new type, and add the routing line.
  3. Where semantics are subtle (NaN, signed zero, overflow), reuse an existing correct path rather than re-deriving it — the of1/of0 integer fold exists for exactly this.
  4. Rebuild, run both suites, and diff output against the old binary on a script that exercises the edge values.

If your kernel is domain-specific rather than a general primitive, it belongs in ext/ instead — the extension seam lets you add amber_* entry points without touching the core or carrying a fork.

Continuous integration

WorkflowTriggersWhat it does
cievery push, every PRCompiler matrix including a strict -std=c99 build, then both test suites. Fastest signal — a toolchain break is reported against a named compiler version in minutes.
comparative benchmarksPRs, and pushes to mainBuilds every comparison engine, runs the suite, posts results as a sticky PR comment. On main it commits a refreshed docs/BENCHMARKS.md.
A branch push does not run the benchmarks

Because comparative benchmarks triggers on pull_request and on pushes to main only, pushing a branch gets you ci alone. For a performance change that is precisely the signal you want, so open a PR rather than merging locally — the benchmark comment is the review.

Licence

Amber is GNU AGPLv3, because its evaluator, parser and heap derive from ngn/k. That obligation travels with the interpreter core and with anything linked into it. It is not a footnote — if you are planning to build a product on Amber, read the licence and the NOTICE before you design the architecture, not after. Talking to amberd over a socket and linking libamber.so into your binary are very different positions.