A controller in C, compiled to WebAssembly in the sandbox and checked by its trajectory
The results on this page were measured in real runs through plimsoll's docker tier (container) on 2026-09-24; the inputs the runs were given are named as inputs where they appear. The caller sent two files: controller.c, a controller that swings a hanging pole up and balances it, and controller.js, a Node shim with no arithmetic of its own. The run's first step compiled the C to WebAssembly inside the sandbox, with no network. The later steps handed the result to the runner baked into the image, which pushed a simulated cart with a pole hinged on it (the plant, control engineering's word for the thing being controlled, itself compiled to WebAssembly) through the controller one 10 ms tick at a time and recorded every tick. The SHA-256 of that record is the fingerprint.
What the plant did under the C controller, scenario
The browser only replays the runner's record; no physics runs here. Loops every 20 s.
The verdict
- controller.wasm, compiled in the sandbox
- The same source compiled again, in a second run
- IDENTICAL
- The same scenario, second run
- IDENTICAL
- The JavaScript controller it was ported from
- DIFFERS
- The same C, built against Node's
Math.cos - SAME AS JAVASCRIPT
- Where the C record differs from the JavaScript one
controller.c, compiled inside the run
// A cart-pole swing-up controller in C, compiled to WebAssembly inside the sandbox
// and run against the plant /models/cartpole.wasm.
//
// The law: while the pole is down, drive the cart so the pole's mechanical energy
// climbs toward its upright value (push in the direction that adds energy, with a
// small centring term so the cart stays on the track). Once the pole is near
// upright and slow, hand over to a linear balancing law with the angle wrapped
// into [-pi, pi]. Pole mass 0.1 kg and half-length 0.5 m are the plant's defaults;
// the cart mass is not known and the law does not need it.
//
// The contract with controller.js, the Node shim the runner spawns: the module exports
// one function, control, called once per tick with the state and the tick index,
// returning the force in newtons. State between ticks lives in globals, because a
// module instance lives for the whole episode. The module must import nothing: the
// shim instantiates it with an empty import object.
//
// Every expression keeps the operand order of the JavaScript law it was ported
// from, and the build compiles with -ffp-contract=off, so the only arithmetic that
// can differ from a JavaScript run of the same law is cos itself: wasi-libc's cos
// here, V8's Math.cos there.
#include <math.h>
#define M 0.1
#define G 9.81
#define L 0.5
#define KE 60.0
#define KX 2.0
#define KV 3.0
#define KP 40.0
#define KD 5.0
#define KXB 1.0
#define KVB 1.0
static int catching = 0;
// wrap maps an angle into [-pi, pi). fmod is exact, like JavaScript's %.
static double wrap(double a) {
double w = fmod(a + M_PI, 2 * M_PI);
if (w < 0) w += 2 * M_PI;
return w - M_PI;
}
__attribute__((export_name("control")))
double control(double x, double v, double theta, double omega, int k) {
(void)k; // this law does not need the tick index; a scheduled one would
const double eUp = M * G * L;
double tw = wrap(theta);
double c = cos(theta);
if (!catching && c > 0.9 && fabs(omega) < 3.5) catching = 1;
if (catching && c < 0.3) catching = 0;
double u;
if (catching) {
u = KP * tw + KD * omega + KXB * x + KVB * v;
} else {
double E = (2.0 / 3.0) * M * L * L * omega * omega + M * G * L * c;
double dir = omega * c >= 0 ? 1 : -1;
u = KE * (E - eUp) * dir - KX * x - KV * v;
}
if (u > 20) u = 20;
if (u < -20) u = -20;
return u;
}
reference.js, the JavaScript law it is a port of
// The JavaScript controller that controller.c is a line-for-line port of: the same
// swing-up law, spawned by the runner as a Node process. While the pole is down it drives
// the cart so the pole's mechanical energy climbs toward its upright value (push in
// the direction that adds energy, with a small centring term so the cart stays on
// the track); once the pole is near upright and slow it hands over to a linear
// balancing law with the angle wrapped into [-pi, pi]. Pole mass 0.1 kg and
// half-length 0.5 m are the plant's defaults.
//
// The example runs this file beside the C build so the page can compare their
// fingerprints from one run. Every expression has the operand order of its C twin,
// so the only arithmetic that differs between the two is cos: V8's Math.cos here,
// the cos compiled into controller.wasm there.
import { createInterface } from 'node:readline';
const m = 0.1, g = 9.81, l = 0.5;
const eUp = m * g * l;
const kE = 60, kx = 2.0, kv = 3.0;
const kp = 40, kd = 5, kxb = 1, kvb = 1;
let catching = false;
const wrap = (a) => { let w = (a + Math.PI) % (2 * Math.PI); if (w < 0) w += 2 * Math.PI; return w - Math.PI; };
const rl = createInterface({ input: process.stdin });
rl.on('line', (line) => {
const [x, v, theta, omega] = line.split('|')[0].trim().split(' ').map(Number);
const tw = wrap(theta);
const c = Math.cos(theta);
if (!catching && c > 0.9 && Math.abs(omega) < 3.5) catching = true;
if (catching && c < 0.3) catching = false;
let u;
if (catching) {
u = kp * tw + kd * omega + kxb * x + kvb * v;
} else {
const E = (2 / 3) * m * l * l * omega * omega + m * g * l * c;
const dir = omega * c >= 0 ? 1 : -1;
u = kE * (E - eUp) * dir - kx * x - kv * v;
}
if (u > 20) u = 20;
if (u < -20) u = -20;
process.stdout.write(u + '\n');
});
The build step, as the run executed it: clang --target=wasm32-wasip1 -mexec-model=reactor -O2 -ffp-contract=off -Wall -Werror -o controller.wasm controller.c. -ffp-contract=off keeps the compiler from fusing a multiply and an add into one rounding, so every expression rounds exactly where its JavaScript twin does.
Four scenarios, one fingerprint each
The pole starts hanging (the start angle is measured from upright, so pi rad is straight down) and the cart mass varies. Upright from is when the final unbroken stretch with the pole within ° of vertical began; the score is 1 minus that time over the run's length, and zero if the cart ever leaves the track. Each C fingerprint was the same in both runs.
| Scenario | Start angle | Cart mass | Upright from | Score | Fingerprint of the C build | Against the JavaScript law | C built against Math.cos |
|---|
Where the C and JavaScript records differ, and where they do not
This controller's arithmetic is +, -, *, /, fmod and one cos per tick. The first four and fmod are exactly rounded by IEEE 754 in both languages, and the build keeps every operation in the JavaScript order, so the C and the JavaScript controllers can differ only where their cos differs. The C module's cos is wasi-libc's, compiled into the module; the JavaScript controller's is V8's Math.cos. The two agree to the last bit on nearly every input, and not on all of them. The run tests that this is the whole difference by building the same controller.c a second way, clang --target=wasm32-wasip1 -mexec-model=reactor -O2 -ffp-contract=off -Wall -Werror -Dcos=host_cos -Wl,--allow-undefined -o controller.wasm controller.c, with the shim binding host_cos to Math.cos: that build and the JavaScript controller give the same fingerprint on every scenario in the table.
What the fingerprint proves, and what it does not
- It proves identical replay. The fingerprint is the SHA-256 of every recorded tick's state and force as raw doubles, so the same fingerprint twice means the two runs produced the same record to the last bit, and a different one means some bit somewhere differs. It is sensitive to one bit anywhere in the record, the controller's forces included, as the comparison above shows.
- It does not prove physical accuracy. The simulator uses fixed steps to represent a cart and a pole. A fingerprint says the plant and the controller computed the same thing again; it says nothing about how close that thing is to a real cart on a real rail. Nor does a different fingerprint by itself mean different physics: the record holds the controller's forces, so a last-bit change there changes the hash even when the plant rounds it away.
- It does not say which
cosis right. Both builds are deterministic and both swing the pole up; they are two slightly different programs, and the fingerprint tells them apart. Which to trust is a question about the math library, not about the run. - It does not rank controllers. Two controllers whose gains differ in the last digit balance the same and hash differently. Whether the pole came up, and when, is the separate score in the table, which the example program computes on the host from the runner's record.
- It does not survive a toolchain change. The module's bytes depend on the compiler image and the flags; change either and the recorded hashes change, deliberately.
- It is not isolation. The compiler, the runner and the controller all run inside one project run, behind the tier the daemon reports. The shim gives the module an empty import object, so it cannot call the host, but that is a property of the shim; the sandbox boundary is the tier.
The same plant, with a JavaScript controller written by an agent and the physics oracle's own runner: the physics oracle run report (INTERNAL · run report →). Reproduce this page: make docker-images && go run ./examples/wasm-controller in the plimsoll repository, with docker. The page is written from the run.