Lockstep, the script

What to say to judges, from the 20-second hook to the hard technical questions. Read it out loud a few times. The bold parts are the ones to memorize word for word.

The hook

20 seconds. Say this first, to everyone.

Every time you open a website, your browser runs code written by strangers. A lot of it runs on WebAssembly, and there is one official rulebook for how it must behave.

My project, Lockstep, reads that rulebook and automatically builds a fast engine that follows it exactly.

The simple version

About 1 minute. For judges with no technical background.

WebAssembly is what lets apps like Figma, Photoshop on the web and games run inside your browser. It’s built into every major browser.

It has an official rulebook that says exactly what every instruction does. Every browser has to follow it exactly, because a mistake can become a security hole.

Think of a board game. The official referee reads the rulebook for every single move. It’s always right, but it’s so slow that one big engine team stopped using the official reference for testing.

Browsers use engines that people wrote by hand. They’re fast, but they’re written separately from the rules, so they can get things wrong.

I built a translator that reads the rulebook and writes the fast engine automatically. I called it Lockstep, because the engine moves in step with the rules: it comes straight from them.

It passes all 55,861 official tests, exactly like the rulebook, and it’s about 45 times faster than running the rulebook directly.

So the rules everyone trusts can now also be fast enough to use.

If they look lost at any point, go back to the board game: referee, hand-made copy, and my machine that builds the fast version from the rulebook.

The demo

Open http://localhost:8090 before the judges arrive (python3 demo/app.py 8090).

1. Run the race

Set fib(18) to fib(20), then click Run the race.

Same program, same rules, two engines. The top one reads the rulebook step by step. The bottom one is my engine, built from the same rulebook.

(When it finishes.) Same answer, about 20 times faster. And the rulebook’s interpreter needed around 30 seconds just to load before it could even start.

2. Run all 259 files

Scroll to the test suite and click Run all 259 files.

These are the official tests, written by the people who make WebAssembly. 259 files. Watch the counter.

(When it reaches 55,861.) 55,861 out of 55,861, in about 8 seconds, on this laptop. The rulebook’s own interpreter gets exactly the same result on every file.

3. From rule to code

Only if they’re curious. Click br_if.

On the left is the rule exactly as the official specification writes it. On the right is the code my compiler generated from it. I didn’t write the right side. That’s why it can’t drift away from the rules.

The fib program

The program both engines run in the race. Know this in case a judge asks “what is it computing?”

Fibonacci numbers are a famous sequence where each number is the sum of the two before it: 0, 1, 1, 2, 3, 5, 8, 13, 21, 34, 55… So fib(10) = 55, and fib(18) = 2,584.

The race computes it the slow, recursive way on purpose. To get fib(18), the program asks for fib(17) and fib(16); each of those asks for two more, and so on, all the way down to fib(1) and fib(0). That adds up to 8,361 function calls for fib(18), and the count grows about 1.6 times with every step up.

That’s why it’s a good test: the answer is easy to check, and the amount of work is big and adjustable, so you can see how fast each engine really is.

The actual program

The exact WebAssembly code both engines run, from bench/progs/kernels.wast.
(func $fib (export "fib") (param $n i32) (result i32)
  (if (result i32) (i32.lt_s (local.get $n) (i32.const 2))
    (then (local.get $n))
    (else (i32.add (call $fib (i32.sub (local.get $n) (i32.const 1)))
                   (call $fib (i32.sub (local.get $n) (i32.const 2)))))))

If n is less than 2, the answer is n itself: fib(0) = 0 and fib(1) = 1.

Otherwise, call fib(n − 1) and fib(n − 2), and add the two answers.

The technical details

fib(n) makes exactly 2 · fib(n + 1) − 1 calls, so the work grows like φⁿ, where φ ≈ 1.618 is the golden ratio.

Each call runs about a dozen instructions (local.get, i32.const, i32.lt_s, if, i32.sub, call, i32.add), so it stresses the core of an engine: function calls, branches, local variables and integer math.

The comparison is fair: both engines run the same .wast file and report the CPU time of the test only, without the 30-second spec load. Small n is dominated by fixed setup, which is why fib(18) to fib(20) shows the gap best (about 20×). My paper goes further and uses the slope method across sizes, which removes setup completely.

The technical version

About 3 minutes. For judges who know programming or compilers.

Context. Since 2025 the WebAssembly specification is written in SpecTec, a domain-specific language. SpecTec turns the rules into 474 step-by-step algorithms in its algorithmic language, AL, and it has a meta-interpreter that runs them. That’s the ground truth, but it takes about 30 seconds to load and then runs 27 to 336 times slower than the hand-written OCaml reference interpreter.

Question. Can the specification be compiled mechanically into an engine that agrees with it and is much faster?

Feasibility first. I started with a one-week kill test. I hand-translated the 16 algorithms behind about 15 instructions. Zero needed logic outside the AL, and the result was about 1,000 times faster per loop iteration. So I went ahead.

The compiler. I wrote a new SpecTec backend, spectec --rust, about 700 lines of OCaml. It compiles every AL algorithm into a Rust function, statement by statement, keeping the meta-interpreter’s semantics. Decisions the interpreter makes at run time, like which function a call refers to or how a pattern matches, are made once, at compile time. The subtle part is the tail rule: when an Execute isn’t the last statement, everything it starts has to finish before the next statement, so the compiler inserts a run-to-depth there, and leaves tail positions to the main loop so loops don’t grow the Rust stack.

The runtime. About 4,900 hand-written lines: values, unbounded integers, the context machine, the numeric built-ins ported from SpecTec, and the test runner. It contains no instruction rules. Tests come in through SpecTec’s own parser and validator, dumped as AL values.

Correctness. The criterion is identical results to the meta-interpreter on every file of the official core test suite: 55,861 of 55,861 commands. Like SpecTec’s own runner, it skips 12 long-running files.

Measuring speed. CPU time, the slope method so setup costs cancel out, medians over interleaved and shuffled runs, and geometric means. Because the server was shared, I also used exact allocation counts from a counting allocator, which don’t depend on load.

Results. The first generated engine was 28 times faster than the meta-interpreter on 8 benchmark programs, and 6.8 times faster on the whole suite.

Optimizations. Then seven meaning-preserving rewrites, each re-verified on the full suite: slice arguments, direct numeric dispatch, shared instruction sequences, single-allocation constructors, reusing pure call results through a purity analysis, tag-based type tests, and compact byte memory. Together: 1.69 times faster, 47 to 83 percent fewer allocations, and one test’s memory went from 2.84 GB to 35 MB. Now it’s about 45 times faster than the meta-interpreter and 3.5 times slower than the hand-written interpreter.

Limits and next steps. The timings are preliminary, from a busy server, on small benchmarks. Next: native machine integers, a separate control stack, re-measuring on a quiet machine, and measuring how many engine lines change when a new feature is added to the spec. That should be zero.

Questions judges ask

Tap a question, answer it out loud, then open it to check.
What is WebAssembly, simply?

A universal, safe instruction language that every browser understands, so programs written in C, C++, Rust and others can run in a web page at near-native speed.

What is a compiler?

A program that translates code from one language into another. Mine translates the WebAssembly rulebook into Rust code.

Why does this matter in real life?

Browser and server engines are tested by running huge numbers of random programs and comparing them with a trusted reference. That reference needs to be correct and fast. Wasmtime’s developers abandoned the official OCaml reference interpreter for this because it was too slow (PLDI 2023). Lockstep is a step toward a reference that’s both exact and fast.

Also: new features become runnable as soon as they’re written in the spec, and compilers that output WebAssembly can be checked against the real rules.

Why not just use the reference interpreter?

It’s written by hand, separately from the spec, so it can disagree with it, and every new feature has to be implemented in it by hand. Mine is generated from the spec itself. Also, in September 2025 a single change made it up to 20 times slower on some programs.

How do you know your engine is correct?

The official test suite: 55,861 commands, identical to the spec’s own interpreter, file by file, and re-checked after every single change I made.

Isn’t passing tests just evidence, not proof?

Yes. A bug that my engine shares with the meta-interpreter wouldn’t show up, because both follow the same AL. I list that as a limitation. Tests plus a safety argument for each rewrite is strong evidence, not a proof.

Why is it still slower than the hand-written interpreter?

It follows the spec’s abstract machine literally: generic values for everything, contexts for every block, labels exited one at a time. Removing that cost without changing the meaning is the research part, and each rewrite closes some of the gap.

Why Rust?

It’s as fast as C but memory-safe, its compiler optimizes the generated code well, and its reference-counted values match how the spec shares values.

What was the hardest part?

Two things. The tail rule, deciding when a step must fully finish before the next one. And proving each speed-up safe: for example, sharing instruction lists is only safe because the spec never edits an instruction list in place, so I checked every in-place update in the generated code.

Explain the purity analysis.

A function is pure if it reads no machine state, changes nothing in place, and only calls other pure functions. Purity depends on the functions it calls, so I repeat the check until nothing changes, which is called a fixpoint. Then, if a pure function is called twice with arguments that can’t have changed, the second call reuses the first result. That’s how the arithmetic instruction stopped doing its math twice.

What is the slope method?

I run each program at several sizes and fit a straight line through time against amount of work. The slope is the cost per unit of work, and the fixed setup cost, like loading the spec, drops out.

Why count allocations?

The server was shared with other jobs, so timings were noisy. Allocation counts are exact and identical on every run, so they show the effect of each change clearly.

What did you build, and what already existed?

Already existed: SpecTec, the WebAssembly specification and the official tests. New in my project: the Rust backend for SpecTec, the test dumper, the runtime, the measurement tools, the seven optimizations and all the experiments.

Did you use AI?

Yes. I used AI coding assistants, Claude Code and Gemini (for porting the numeric functions), and my logbook records how. I set the research question, designed the experiments, made the decisions and verified everything against the official tests, and I can explain every part.

Why does it skip 12 test files?

SpecTec’s own test runner skips them because they’re too slow for it. I kept exactly the same rules so the comparison is fair. Running them is one of my next steps, since my engine is now fast enough to try.

Is anyone using it?

Not yet. It’s a research prototype. A next step is to offer the backend to the SpecTec maintainers.

What would you do with more time?

Native machine integers, a separate control stack, faster branches, bigger real-world benchmarks, re-measuring on a quiet machine, and the feature experiment: add a new feature to the spec, regenerate, and count how many engine lines changed.

Numbers to memorize

55,861official test commands passed, all of them, identical per file to the spec’s interpreter
259files in the official core test suite (12 long ones skipped, like SpecTec does)
474algorithms in the spec, all compiled
22,000lines of Rust generated
700lines of OCaml in the compiler (the new SpecTec backend)
4,900lines of hand-written runtime, with no instruction rules
~45×faster than running the spec directly (was 28× before optimizing)
6.8×faster on the whole test suite
3.5×still slower than the hand-written reference interpreter (was 5.6×)
1.69×gained from 7 meaning-preserving rewrites
47–83%fewer memory allocations
2.84 GB → 35 MBmemory on one test, after compact memory
~1,000×kill-test speedup of the small hand translation
30 stime the spec’s interpreter needs just to load
8 sfor the whole test suite on the laptop

In Arabic

The simple version, for judges who prefer Arabic. Change the wording until it sounds like you.

لما بتفتح Figma أو لعبة في المتصفح، اللي بيشغّلها تكنولوجيا اسمها WebAssembly، وليها كتاب قواعد رسمي، وكل متصفح لازم يمشي عليه بالظبط، لأن أي غلطة ممكن تبقى ثغرة أمنية.

الحَكَم الرسمي اللي بيقرا القواعد خطوة بخطوة دايمًا صح، بس بطيء جدًا، لدرجة إن فريق كبير بطّل يستخدمه في الاختبار.

أنا عملت برنامج بيقرا كتاب القواعد ده ويحوّله تلقائيًا لمحرّك سريع، اسمه لوك ستب، لأنه ماشي خطوة بخطوة مع القواعد. نجح في كل الـ55,861 اختبار الرسميين، وأسرع حوالي 45 مرة من تشغيل القواعد مباشرة.

Never say

  • “It’s faster than Chrome” or “faster than real engines”It isn’t. It’s faster than running the spec directly, and still 3.5× slower than the hand-written interpreter.
  • “It’s proven correct”Say “it passes every official test the spec’s interpreter runs, identically”.
  • “100% of all tests”, to an expertSay “all 55,861 commands SpecTec’s runner executes”. Mention the 12 skipped long files if asked.
  • “These are the final numbers”They’re preliminary: the server was busy. Say so before they ask.
  • “I wrote all of it by hand”You used AI coding tools, and the logbook records it. Honesty here earns trust.