Testing the Engines Against One Reference
A plan for how every execution engine is tested: the IR walker as the one reference, every other engine compared against it in the same process, one corpus and one runner, and the bytecode on by default.
Contents
Where things stand
Surveyed in October 2026.
Two oracles, inconsistently applied. Some suites compare a backend with
the IR walker at run time -- dual_tests (Cranelift AOT), c_dual_tests (C
AOT), std_all_tests (JIT, both AOTs), the chaos dispatcher suite, the cli's
backend_tests. Others compare against checked-in expected files --
interp_tests, module_interp_tests, interp_constlet_tests,
interp_jit_tests, aot_tests, std_tests, and the whole of test-bc. The
bytecode is never compared with the IR walker in one process: under
DATALOVE_INTERP=bc both sides of every differential suite are the bytecode.
Expected files duplicated per backend. The 394 .c.out.expected files are
byte-identical to their .out.expected twins. c_dual_tests runs only under
slow_tests, so its copies go stale whenever lowering changes; sixteen commits
since June have re-blessed them.
Configurations standing in for test structure. test-bc is a CI job to set
one environment variable. test-slow reruns all of datalove-datafun to add
one binary. test-sys-riders is in test-ci and not in CI.
test-sanitizers-all names recipes that do not exist, and test-miri-interp
stops in the harness's crossbeam before reaching the interpreter.
Overlapping suites. std_tests is a subset of std_all_tests;
interp_tests of interp_jit_tests, the two overwriting each other's
.out.actual. The tuned dispatcher suite seldom reaches its thresholds. The
specialization suite fails only because its expected files say PASS.
Corpora tied to one or two engines. interp/ (420 worldfiles), dual/
(394), std_tests/ (165), aot/ (76), module_interp/ (67),
specialize_differential/ (29), interp_constlet/ (15): most programs run on
one engine or two.
Gaps. Worldgen fuzzing never runs in CI. The dynamic inliner has no direct
test. The REPL engine tests and the JIT crate's unit tests are outside
test-bc.
The design
One reference. The IR walker -- with its debug liveness checks, the checked engine -- is the only engine whose results are compared with expected files. An expected file records the reference's behavior and the front end's (typecheck, ownership, IR); never one engine's copy of another's.
Every other engine compared with it, in process. For each program the runner runs the reference, checks it against the expected file, then runs each engine that applies and compares what it observes -- each script unit's output and debug log -- with the reference's. A new engine adds no expected files.
The engines:
| engine | how |
|---|---|
| IR walker | the reference |
| bytecode | Engine::Bytecode on the executor |
| JIT | a JitEngine compiling on first call |
| chaos | an OptimizingDispatcher with a chaos config seeded from the fixture; asserting the JIT or the inliner engaged somewhere in the run |
| Cranelift AOT | a single-fragment program compiled, linked and run |
| C AOT | the same through the C backend |
The engine is chosen by the runner, not by an environment variable, so the reference can never silently be something else. An engine applies to a fixture unless the fixture's shape rules it out (the AOT backends take exactly one script fragment) or the fixture opts out by a header line, for what a backend does not support yet.
One corpus, one runner. The execution corpora merge into one directory, read by one harness. Lowered-IR snapshots belong to the lowering suites, not the execution corpus.
The bytecode by default. IrInterpreter defaults to the bytecode;
DATALOVE_INTERP=ir forces the IR walker, for debugging. The runner sets each
engine explicitly. test-bc goes: the runner always compares the bytecode
with the IR walker.
Four CI configurations.
| job | runs |
|---|---|
| default | every crate, every engine |
| slow | property tests; wasm check; sys riders built; a fixed-seed worldgen run |
| index-64 | default under index-64 |
| parallel | default under DATALOVE_PARALLEL=1 |
Order of work
- The runner over the existing corpora, beside the current suites, with the engine matrix and in-process comparison. Confirm it catches what they catch.
- The bytecode by default, and
test-bcgone. - The corpora merged and the superseded suites deleted, one at a time.
- CI rebalanced to the four jobs, with worldgen in the slow one.
What goes
interp_tests, interp_jit_tests, dual_tests, c_dual_tests, aot_tests,
std_tests, module_interp_tests, interp_constlet_tests, the tuned
dispatcher suite, and the chaos suite as a suite of its own; the 394
.c.out.expected files and the two .disabled expected files; the untracked
interp3/, module_interp3/ and analysis/ directories; the test-bc,
broken sanitizer and broken Miri recipes. The cli's backend_tests stays, for
the command line itself.
As built
October 2026.
engine_tests runs fixtures/engines/: the 420 programs of interp/, and
the 523 of dual/, aot/, module_interp/, interp_constlet/ and
specialize_differential/ that were not byte-for-byte copies of another,
prefixed with where they came from. A module_interp/ program gained a
fragment that requires its main module and debuglogs main(). The engines
are the bytecode, the JIT, the chaos dispatcher (on the bytecode), Cranelift
AOT and C AOT, and two that compile differently -- noconst, const inlining
off, and nospec, specialization off -- held to the reference's outputs and
debug logs only, and only for a program that compiled without error. The run
prints how many programs each engine took: of 972, the AOT backends take
about 700. A fixture opts out of an engine with a // engines: -c comment
before its first section, saying why; one does. Under Miri only the
engines that generate no code run.
A fixture can say what has to have run, in the same place: agreeing with
the reference proves nothing if an engine quietly ran something the
reference's way, as the bytecode does when it leaves an instruction to the IR
walker. // expect-bytecode-ops: ListStoreAt BrCmpF64 requires the bytecode
run to have lowered some body to each op; // expect-bytecode-lowered: RefStore ListSet requires that no body left an instruction of those kinds to
the walker; // expect-osr: entered requires the osr run to have entered a
loop and been refused at none. The bytecode keeps the kinds of op it lowered
to and of instruction it left (BcStats), and the harness reads them, and
the JIT's loop counts, after each run. The corpus-wide counts are split by
engine too: the jit engine compiles every function first, so OSR entering any
of its loops is a failure.
C AOT runs in the default job, not only the slow one: over the whole corpus it costs about as much again as the rest, which is cheap next to a backend that drifts. Its first run found a fault no suite had reached: a table's row descriptor left out of the file that builds one.
std_engine_tests, the old std_all_tests, is the standard library's
runner, with the IR walker named as its reference and the bytecode added.
It stays separate because its programs are scripts against the real sys/
library, built once per worker, rather than worldfiles.
The expected files still carry the IR. The design moves IR snapshots to the lowering suites; they are still in the reference's rendering, so a lowering change re-blesses the engine corpus as it did the old suites.
CI is four jobs, just test-ci the same four: default, index-64,
parallel, and slow, which is the slow_tests property and exhaustive tests,
the wasm build, the suite against sys riders built from source, and
just test-worldgen on a fixed seed, so that a failure is a change rather
than a draw; another seed is a command-line argument away. Its first run,
under leak checking, found const inlining's dead code pass removing the only
instruction that consumed a value, and two faults in the generator.