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 # REPLbuild.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 seamEach 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.
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 tableThe 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:
- Write the kernel next to its siblings in the relevant translation unit —
3.cfor a reduce or scan,2.cfor element-wise — following the local macro style. - Widen the dispatcher's type guard to admit the new type, and add the routing line.
- Where semantics are subtle (NaN, signed zero, overflow), reuse an existing correct path rather
than re-deriving it — the
of1/of0integer fold exists for exactly this. - 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
| Workflow | Triggers | What it does |
|---|---|---|
ci | every push, every PR | Compiler 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 benchmarks | PRs, and pushes to main | Builds every comparison engine, runs the suite, posts results as a sticky PR comment. On main it commits a refreshed docs/BENCHMARKS.md. |
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.