Last post the actors were alive. We had a tree-walking interpreter, an LLVM compiler that produced native binaries, a REPL with a state sidebar, and a diff viewer that watched the work happen. That was two days ago.
Since then we got closures, pattern matching, loops, an async scheduler, a tagged value runtime with Perceus reference counting, compiled all of it to native code via LLVM, put the entire interpreter in a browser as a 100KB WASM module, built a canvas that paints your actors as generative art, and embedded the whole thing in this blog.
There's a REPL at the bottom of this page. Go play with it.
Speaking of which. blimp_send used to process messages inline, which meant no fairness and no real concurrency. Now it enqueues the message in the target's mailbox and pumps a round-robin scheduler until the reply comes back.
What that means: when actor A sends to actor B, and B's handler sends to actor C, all three get scheduled fairly. The scheduler does one message per actor per pass, round-robin. If two actors deadlock (A waits on B, B waits on A), it detects it and aborts with a clear error instead of hanging.
This is single-threaded for now. The WASM target will always be single-threaded (that's how browsers work). The native target will get real threads later, but the scheduler architecture is ready for it.
Under the hood, every value in compiled code is now a BlimpVal*, a heap-allocated tagged union. Integers, floats, strings, atoms, booleans, lists, maps, closures, actor refs, all the same pointer type.
And every BlimpVal has a reference count. When you bind a value to a variable, the count goes up. When a scope exits, the count goes down. When it hits zero, the value and all its children get freed recursively. Lists dec their elements, maps dec their values, closures dec their captured environment.
The Perceus insight: if the refcount is 1 (you're the only owner), you can mutate in place instead of allocating. become count: count + 1 doesn't need to allocate a new integer if the old one has refcount 1, it just overwrites it. We have blimp_rc_reuse in the runtime for this but we're not using it aggressively in codegen yet. That's an optimization pass for later.
The interpreter compiles to a 100KB WASM module via Zig's native wasm32-freestanding target. No Emscripten, no WASI, no polyfills. Just the lexer, parser, evaluator, and actor runtime, compiled to WASM.
A tiny JS loader (blimp.js, 80 lines) handles loading the module, passing strings through linear memory, and wiring up print callbacks. The API is three functions: init, eval, getState.
We embedded it in this blog. There's a REPL at the bottom of every page. The playground at /blog/playground.html has the full setup: REPL, state panel, canvas, and a cheat sheet with copy-paste examples of every feature.
We still don't have:
def for named functions inside actors, just anonymous fn for now
- String operations as builtins (concat, split, contains)
- Supervision (Phase 5) so bubbles actually propagate and restart actors
- Send after for timers and heartbeats
- A UI DSL that wraps something like Tailwind so you can build interfaces from Blimp without writing HTML
That last one is the one I keep thinking about. If actors are the runtime and the browser is the target, then the language should be able to describe interfaces. Not with JSX, not with templates. With actors. A button is an actor. A form is an actor. The page is a supervision tree. Clicks are messages.
We'll see. For now, go try the REPL.