Cockos documents EEL2 in prose. This corpus pins the behaviour of its MilkDrop dialect, the one per_frame, per_pixel, per_point and init blocks use, as cases instead: each rule is a program, an input state and a required output state, in plain JSON, so conformance is something you run rather than something you argue about.
bun run coverage:svg. Solid: pinned. Hatched: provisional.Every case, read live from the same cases/*.json files the package ships. Search by id, name, program text or note.
Loading cases…
A conforming runner executes one case like this.
env on top.megabuf and gmegabuf from the case. megabuf is per-preset scratch; gmegabuf is shared across presets.program as one block, in order. It is a single program, not independent lines: state carries from each statement to the next.rand() / randint() draw returns exactly 0.5. Randomness is not what these cases test, and a fixed draw is what makes them portable.expected, the variable must be within 1e-12 + |expected| * 1e-9. Same for expectedMegabuf / expectedGmegabuf against buffer slots. Absent variables read as 0, so {"x": 0} is a real assertion.Buffer size is part of the contract, not an implementation detail. Bounds behaviour is only defined against a declared size, and a runner that allocates a short buffer will read past its end and produce garbage where the specification requires 0.
Derived from documented MilkDrop 2.x / ns-eel behaviour. A conforming implementation must produce this value.
This corpus's observed behaviour, not yet confirmed against ns-eel. A question, not a requirement. Enforcing it still has value, since silent drift is worse than a known value, but check upstream before porting one.
The TypeScript harness does the seeding and comparing; you supply the one thing that is yours. A runner in any other language reads the JSON directly.
import { conforms, formatReport, runConformance } from 'eel-conformance';
import { execute } from './my-eel-engine';
const report = await runConformance(({ program, env, megabuf, gmegabuf, random }) => {
execute(program.join('\n'), { env, megabuf, gmegabuf, rand: random });
});
console.log(formatReport(report));
process.exit(conforms(report) ? 0 : 1);
A thrown error is recorded on that case and the run continues, because the useful output is the whole table. conforms is true when every pinned case passed; provisional failures are reported separately and never count. Variable names in env are lower-cased, matching the language's case-insensitivity.
The eel-conformance CLI lists sections and cases, shows one case as JSON, and validates the shape of a corpus against the schema's rules without a schema library.
1 / 0.0000001 divides normally. A tolerance guard silently zeroes results real presets depend on.% truncates both operands to integers first; float remainder is mod() / fmod().|v| > 1e-5, not v != 0, for if, &&, ||, ! and bnot.== is exact; equal() uses the close factor. Only one of them is tolerant.bor() and band() are logical, not bitwise, despite the names. bor(2,4) is 1, not 6.frac() subtracts the floor, so frac(-0.25) is 0.75.step(edge, v) takes the edge first.pi and e are ordinary prepopulated variables, and assignments to them stick.contVol and contvol are one variable.smoothstep() with equal edges is defined here as v < edge ? 0 : 1; GLSL and WGSL leave it undefined, so a GPU backend must special-case it.The provisional cases are places where the originating implementation had to pick a behaviour and the reference is unverified. Resolving them against ns-eel would be the most useful contribution.
while condition truthiness. A while() condition is tested against exact zero, while every other boolean context uses the close-factor threshold. If the threshold turns out to be correct, a class of presets is currently spinning to the iteration cap every frame.^. -2 ^ 2 parses as (-2)^2 = 4. Most languages that spell exponentiation as an operator bind unary minus more loosely.^ associativity. 2 ^ 3 ^ 2 parses left-associatively as 64, not the more usual 512.