eel-conformance EEL2 executable specification

The expression language of MilkDrop presets, as cases you can run.

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.

npm install eel-conformance Browse the JSON schema.json
82 cases in 9 sections; solid bars are pinned cases, hatched are provisional
Generated from the corpus by bun run coverage:svg. Solid: pinned. Hatched: provisional.

The corpus

Every case, read live from the same cases/*.json files the package ships. Search by id, name, program text or note.

Loading cases…

Runner contract

A conforming runner executes one case like this.

  1. Start from zero. Every variable is 0 until assigned. Reading an unknown name is legal and yields 0, never an error. Apply the case's env on top.
  2. Allocate both guest buffers at 1,048,576 f32 slots each, zero-filled, then apply megabuf and gmegabuf from the case. megabuf is per-preset scratch; gmegabuf is shared across presets.
  3. Execute program as one block, in order. It is a single program, not independent lines: state carries from each statement to the next.
  4. Use the fixed RNG. Every rand() / randint() draw returns exactly 0.5. Randomness is not what these cases test, and a fixed draw is what makes them portable.
  5. Compare. For each entry in 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.

pinned

Derived from documented MilkDrop 2.x / ns-eel behaviour. A conforming implementation must produce this value.

provisional

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.

Run it against your engine

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.

Rules that catch new implementations

Open questions

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.

  1. 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. Unary minus versus ^. -2 ^ 2 parses as (-2)^2 = 4. Most languages that spell exponentiation as an operator bind unary minus more loosely.
  3. ^ associativity. 2 ^ 3 ^ 2 parses left-associatively as 64, not the more usual 512.