Chapter 10
The Runtime
Where the pieces become a running sketch
The lexer, parser, interpreter, VM, and WASM compiler are all components. Something has to glue them together, decide which backend to use, hook up the mouse and keyboard, and run draw() sixty times a second. That something is createRuntime() in src/lang/index.ts.
The Entry Point
Every sketch starts the same way. The playground hands your source to createRuntime() with a canvas and some callbacks, then calls .start():
const runtime = createRuntime(source, {
canvas,
onPrint: (msg) => console.log(msg),
onError: (err) => showError(err),
onWarning: (msg, hint) => showHint(msg, hint),
});
runtime.start();
Whatever backend ends up running, you get back the same BloomRuntime shape — start, stop, pause, resume, step, plus getters for the current frame, user variables, and performance metrics. The caller never has to know which engine is underneath.
Choosing a Backend
This is the heart of the runtime. Bloom defaults to the bytecode VM for speed, but transparently falls back to the interpreter for the one thing the VM can't handle — a program that uses import:
if (options.useBytecode !== false) {
try {
const bytecodeRuntime = createBytecodeRuntimeFromSource(source, { ... });
return wrapBytecodeRuntime(bytecodeRuntime);
} catch (error) {
if (!(error instanceof ModulesNotSupportedError)) {
throw error; // Real parse/compile/setup error — do not swallow.
}
// import detected — fall through to the interpreter path.
}
}
// ... interpreter setup below ...
The logic is deliberately narrow. Just one exception triggers a fallback:
import. The VM has no module system, so the interpreter — which resolves and links modules — takes over.Closures used to be the other fallback case, but they no longer are: the VM captures them natively with boxed-cell upvalues, so a closure-using sketch stays on the fast path (the full story is in Chapter 6). Any other error — a real syntax mistake, an undefined variable in setup() — is re-thrown, not swallowed. That matters: a fallback should never hide a genuine bug. Both paths funnel checkCommonMistakes() warnings through onWarning, so the friendly nudges from the error system appear no matter which engine runs.
Wiring Up Input
When a canvas is supplied, the runtime attaches the event listeners that make a sketch interactive. Mouse, touch, and keyboard all feed plain fields on the interpreter (or VM):
mousemove/touchmoveupdatemouseXandmouseYrelative to the canvas rect.mousedown/touchstartsetmousePressed = trueand fire the user'smouseClicked()handler once per press (an edge, not while held).keydown/keyuptrackkey,keyPressed, and akeysPressedset so multiple keys can be held at once.keyPressedonly goes false when the last key is released.
Touch events call preventDefault() so dragging on a sketch doesn't scroll the page, and they also populate a touches array for multi-touch sketches. The Mouse & Keyboard chapter shows these variables in action.
The Draw Loop
If the program defines a draw() function, the runtime starts an animation loop with requestAnimationFrame. The loop is frame-rate aware — it only runs a frame once enough time has elapsed for the target FPS (60 by default), so a fast monitor doesn't run your sketch too quickly:
function loop(currentTime) {
if (!running || paused) return;
const elapsed = currentTime - lastFrameTime;
if (elapsed >= frameDelay) { // frameDelay = 1000 / targetFps
lastFrameTime = currentTime - (elapsed % frameDelay);
if (!executeFrame()) return; // runs draw(), catches errors
// ... record frameTime, update rolling FPS average ...
}
animationId = requestAnimationFrame(loop);
}
executeFrame() calls draw() inside a try/catch. If your code throws, the runtime stops the loop and routes the error to onError rather than spamming the console every frame. It also records the frame time into a 60-sample rolling history to compute the average frame time and FPS you see in the playground's metrics.
Lifecycle Controls
The returned object exposes the controls the playground's run/pause/step buttons use:
start()setup(), then begins the loop if draw() existspause() / resume()step()stop()interpreter.cleanup() to release camera/audio resourcesThe Full Journey, End to End
In the Source Code
createRuntime(source, options)— backend selection + interpreter loopcreateBytecodeRuntimeFromSource(source, options)— the VM loopcompile(source)— convenience that returns{ tokens, ast }BloomRuntime/BloomOptions— the public contract
Want to watch every stage interactively? The How Bloom Works explorer runs this exact pipeline and lets you step through it.