Chapter 1

How Bloom Works

A journey from text to pixels on screen

When you write Bloom code and run it, a lot happens behind the scenes. This book explains the entire journey your code takes, from the moment you type it to the moment shapes appear on your canvas. It is written to be read front to back: each chapter builds on the one before, the way a compiler's stages hand work to the next. But every chapter also stands on its own, so you can jump straight to the part you're curious about and follow the cross-references from there.

Source Code Your .bloom file
Lexer Tokenization
Parser AST Generation
Interpreter or VM Execution
Canvas

The first three stages — lexer, parser, and the resulting AST — are shared. After that, Bloom has two different ways to actually run the tree: a tree-walking interpreter and a bytecode virtual machine. The rest of this guide explains both.

Table of Contents

The Big Picture

Programming languages are like translators. They take something humans can read and turn it into something a computer can execute. Bloom does this in four main stages:

1

Lexical Analysis

The Lexer reads your code character by character and groups them into meaningful chunks called tokens. It's like breaking a sentence into words.

circle(100, 50, 30)

becomes

IDENTIFIER "circle" LPAREN NUMBER 100 COMMA NUMBER 50 COMMA NUMBER 30 RPAREN
2

Parsing

The Parser takes those tokens and figures out how they relate to each other, building a tree structure called an Abstract Syntax Tree (AST).

Call
circle
100
50
30
3

Interpretation

The Interpreter walks through the AST and executes each node. When it sees a function call to circle, it draws a circle on the canvas.

Why This Matters

Understanding how a language works helps you:

Two Execution Backends

Bloom can run the very same AST in two completely different ways:

Tree-Walking Interpreter
Bytecode VM
How it runs
Recursively walks the AST
Compiles AST to flat opcodes, then executes them on a stack machine
Speed
Slower (reference)
Roughly 2–6× faster on loops, far more on deep recursion
Closures
Full support (environment chain)
Full support (boxed-cell upvalues)
Modules / imports
Supported
Not supported (falls back to the interpreter)
Role
Reference implementation & module fallback
Default engine for speed

How Bloom chooses

You don't pick a backend by hand. Bloom defaults to the bytecode VM because it is much faster, and the VM now runs almost everything the language offers — including closures, which it captures natively with a boxed-cell upvalue model (Chapter 6 tells that whole story). The one feature the VM still can't handle is modules / imports: it has no module system. When the compiler sees an import, it throws a clear error, and Bloom transparently re-runs the program on the tree-walking interpreter, which links modules.

VM compiles & runs The default. The overwhelming majority of sketches — including closures — stay here and get VM speed.
Import detected → fall back The compiler refuses, Bloom retries on the interpreter, and the module-using sketch runs correctly.

The result: you get VM speed for the overwhelming majority of sketches — automatically, with no flag to set — and the interpreter steps in only for the few programs that pull in modules.

Same front-end, two back-ends Both engines consume the identical AST and share the same value semantics (nil, false, and 0 are falsy; empty strings and arrays are truthy; the same arithmetic and vector rules; the same beginner-friendly error text). That shared front-end is why falling back from one to the other is safe.