Files
tensor/docs/DEVELOPMENT.md
petrbalvin af4ee19703
Release / gates (push) Successful in 4m38s
Test / test (push) Successful in 5m16s
Release / release (push) Successful in 35s
feat: initial release
Assisted-by: GLM 5.3 Flash
2026-09-03 10:00:00 +02:00

7.9 KiB

Development

How to work on Tensor.

Prerequisites

  • Go 1.27.1, the exact version go.mod declares and the newest stable release at the time of writing. Verify the installed version against the release list rather than memory: go version.
  • just for the recipes. Three of them (test, fmt-check, fuzz-all) are Perl scripts.
  • Perl, for those recipes and for the CI steps that carry logic. Only the interpreter's own builtins are used, so no module installation is needed.
  • A C compiler (gcc) for the race detector, which just race and just gates run; -race requires cgo.

Nothing else: Tensor has zero third-party dependencies.

Setup

git clone https://sourcedock.dev/petrbalvin/tensor.git
cd tensor
just build
just test

Recipes

Every recipe in the project's file, and what it does. Taken from the file itself, so the names and the list match it exactly.

Recipe What it does
just lists the recipes
just build compiles everything, the example programs included
just test the test gate: the full suite with no cache, the coverage profile and the 80 percent floor
just race the same suite under the race detector; the expensive one, run once per task by just gates
just unit ./internal/core/ TestName a fast scoped run for iterating: cached, no race, no coverage
just fuzz FuzzName ./io 60s a time-boxed fuzz of one target in one package; never a gate
just bench benchmarks with -benchmem, five counts; on an idle machine only
just fmt formats all Go sources in place with gofmt
just fmt-check verifies that gofmt produces no diff; prints nothing on success
just vet go vet and go fix -diff
just gates the definition of done in one command: build, fmt-check, vet, test, race, in that order
just clean removes the build artefacts (bin/, coverage.out)
just docs-check runs every Go program in README.md from a temporary module, so the documentation cannot claim what the code no longer does
just fuzz-all 5s fuzzes every target for the budget each; exploration, never a gate

docs-check and fuzz-all are the project extensions; none of them is a gate. The packages value behind test, race, unit and bench names the logic packages and leaves examples/ out: those are main programs with no tests, and the build is what compiles them. Tensor is a library, so the binary recipes (install, run, dev) have no referent here and are absent from the file.

The scripted recipes keep their logic in Perl rather than in the shell, which is the repository rule for every non-product script: the shell starts commands, and anything with a branch or a loop is Perl using the interpreter's own builtins.

gofmt is the single formatting authority: there is no configuration beyond it, just fmt-check is the gate and just fmt the fix.

Running a single test

just unit ./internal/core/ TestQuo

unit is the scoped, cached run for iterating; the second argument is a regular expression matched against test names. Combine with -v for the sub-test names, or call go test directly:

go test -run TestQuo -v -count=1 ./internal/core/

-count=1 defeats the test cache when a result looks stale.

The runnable documentation is part of the suite, so it is exercised the same way. Each package carries its examples beside its tests:

go test ./linalg/ -run Example -count=1 -v

A godoc example that stops compiling, or whose printed output drifts from its // Output: comment, fails the suite rather than the reader. The programs in README.md are checked the same way, though outside the suite, because they are whole main programs:

just docs-check

which extracts every go block into a temporary module against the working tree, runs it, and reports the block that failed.

Coverage

just test
go tool cover -func=coverage.out

The total: line is the number that matters, and just test fails below 80 percent. The sweep names the logic packages, so every library package is measured while the examples stay out of the denominator. For an HTML report:

go tool cover -html=coverage.out -o coverage.html

Two harnesses inside the suite guard properties that coverage percentages do not describe, and both live at the root:

  • TestOracle pins a raw-bit digest of one fixed workload per domain, per platform and per build. A digest that moves is either a deliberate arithmetic change or a regression, and the difference is decided by the person who moved it, not by the test.
  • TestNoResourceLeaks measures the heap across three blocks of ten rounds and fails on a net rise above 256 KiB, which is how a buffer that stops being released is caught before it becomes an outage.

Benchmarks

just bench

One package at a time, with a fixed budget:

go test ./internal/core/ -bench 'BenchmarkMatMul$' -benchtime 2s -run xxx

Benchmark on an idle machine, compare only runs made in one process against each other, and treat a few percent as noise. The packages carry 147 benchmarks, and the weight sits where the time is: 84 in internal/core, 20 in signal, 13 in stats, 11 in integrate, 8 in optim, 7 in linalg, 3 in grad and 1 in internal/engine. The binding measurement method, the report template and the measured reports live in docs/BENCHMARKING.md and docs/benchmarks/.

Debugging the build

go build -gcflags='-m' ./internal/core/   # inlining decisions
go build -gcflags='-S' ./internal/core/   # what the compiler generated

There is exactly one build, and it is the product:

Build Command Assumes
portable go build ./... the toolchain default code generation, no pinned GOAMD64

The portable build pins no GOAMD64 level: the compiler has no auto-vectoriser, so a pinned higher level would buy only scalar FMA contraction, which the bit-pinned kernels suppress by spelling anyway (float64(a*b) + c). A build pinned to a level the oracle has no digest block for skips loudly, so a quiet mismatch cannot happen.

Continuous integration

Gitea Actions workflows live in .gitea/workflows/, are written by hand, and enforce the same gate set as just gates, with scripted steps in Perl and parallelism bounded to the shared runner box:

  • test.yml, on every push and pull request to development: build, format check, vet, the full suite with the coverage floor, and the oracle digests for the platform. Race is absent on purpose: the shared box cannot afford it on every push. The one-iteration benchmark smoke that once rode along is retired outright: the minimum degree battery's 3-D mesh scan alone runs for minutes on one core and allocates terabytes cumulatively, so no form of it fits the shared box, and benchmarking is deliberate work on a developer machine.
  • race.yml, dispatched by hand: the suite under the race detector, with the oracle digests across fedora, alpine and openeuler, which is the glibc against musl check the floating-point kernels need.
  • release.yml, on a v* tag: the gate set minus race once at the tag, then the Gitea release created from the matching CHANGELOG.md section.

A green just gates locally is the fastest way to a green pipeline.

Releases

Releases are cut by merging development into main and tagging vX.Y.Z. The tag pipeline runs the gates at the tag and publishes the release with the CHANGELOG section as its notes: the pipeline reads the section that begins at ## [X.Y.Z] and stops at the next ## [, and refuses a tag whose section is missing or empty. Nothing is injected into the build; the toolchain records the tag because the build simply happens there. Before cutting a tag, run just gates locally: the local gate is the one that races the tree.