What djs is for
djs is a small JavaScript and TypeScript engine written in Rust. It exists for one job: running the scripts that a language model writes in pi’s codemode, inside a WebAssembly sandbox, in place of QuickJS. This guide explains how it works and, above all, why it is built the way it is. Almost every unusual decision in djs follows from that one job, so this page starts there.
In short. A codemode script is short, calls slow tools, moves a lot of data, and must be stoppable and resumable at any moment. djs therefore keeps all of its state in one byte array that it can copy, never uses the Rust stack for JavaScript calls, lets the host read values straight out of memory, and compiles source to bytecode in one pass.
A codemode script
Instead of calling tools one at a time, the model writes a small program that calls them, combines their results and returns what it wants to show:
// A stand-in for a tool the host provides; in pi it is `tools.search`.
const search = async (q) => [
{ name: "djs", stars: 120, lang: "Rust" },
{ name: "quickjs", stars: 9000, lang: "C" },
];
const hits = await search("small js engines");
const rust = hits.filter((h) => h.lang === "Rust");
console.log(`${rust.length} of ${hits.length} in Rust:`, rust.map((h) => h.name).join(", "));
return rust;
1 of 2 in Rust: djs
function 0 <anonymous>: params=2 locals=4 stack=6 captures=0
handler [3, 136) -> 136 depth 0
0 async_start loc0 (<state>)
3 fclosure child0
8 set_loc loc1 (search) ; 6:20
11 undefined
12 push_const #0 ("small js engines")
17 call 1 ; 6:26
20 await loc0 (<state>) ; 6:14
23 set_loc loc2 (hits) ; 7:14
26 get_method "filter" ic0 ; 7:18
33 fclosure child1
38 call 1 ; 7:25
41 put_loc loc3 (rust)
44 get_arg arg1 ; 8:1
47 get_method "log" ic1 ; 8:8
54 get_loc loc3 (rust) ; 8:16
57 get_field "length" ic2 ; 8:20
64 to_string ; 8:14
65 push_const #1 (" of ")
70 get_loc loc2 (hits) ; 8:34
73 get_field "length" ic3 ; 8:38
80 to_string ; 8:32
81 push_const #2 (" in Rust:")
86 concat 4
89 get_loc loc3 (rust) ; 8:58
92 get_method "map" ic4 ; 8:62
99 fclosure child2
104 call 1 ; 8:66
107 get_method "join" ic5 ; 8:81
114 push_const #3 (", ")
119 call 1 ; 8:86
122 call 2 ; 8:12
125 drop
126 get_loc loc3 (rust) ; 9:8
129 async_return loc0 (<state>)
132 undefined
133 async_return loc0 (<state>)
136 async_reject loc0 (<state>)
function 0.0 search: params=1 locals=1 stack=3 captures=0
handler [3, 80) -> 80 depth 0
0 async_start loc0 (<state>)
3 object
4 push_const #0 ("djs")
9 define_field "name" ic0
16 push_i8 120
18 define_field "stars" ic1
25 push_const #1 ("Rust")
30 define_field "lang" ic2
37 object
38 push_const #2 ("quickjs")
43 define_field "name" ic3
50 push_i32 9000
55 define_field "stars" ic4
62 push_const #3 ("C")
67 define_field "lang" ic5
74 array_from 2
77 async_return loc0 (<state>)
80 async_reject loc0 (<state>)
function 0.1 <anonymous>: params=1 locals=0 stack=2 captures=0
0 get_arg arg0 ; 7:33
3 get_field "lang" ic0 ; 7:34
10 push_const #0 ("Rust")
15 strict_eq ; 7:40
16 return
function 0.2 <anonymous>: params=1 locals=0 stack=1 captures=0
0 get_arg arg0 ; 8:74
3 get_field "name" ic0 ; 8:75
10 return
Run the example to walk the VM's arena.
Run the example to see it suspend and snapshot.
Such scripts have a particular profile, and each part of it became a design goal:
| What a codemode script does | What djs needs to do | Goal |
|---|---|---|
| Receives tool results: long strings, arrays of similar objects, base64 images | Get data in and out with as little copying and conversion as possible | cheap data passing |
| Runs for milliseconds, once, then is thrown away | Start in microseconds; compile fast; keep the wasm module small | fast startup in wasm |
| Is written fresh every time | Compile in one pass, straight to bytecode | no syntax tree |
| Waits for tools that may take minutes | Save the whole VM while it waits, restore it elsewhere | snapshots |
| Must not hog the host, may loop forever, may be cancelled | Stop between any two instructions and resume later, even inside a map callback or a getter | suspend anywhere |
The goals are in priority order, and the next chapter, what djs optimizes for, explains each of them. Two more constraints shape everything: wasm32 is the target that counts (memory and speed are judged in V8’s wasm tier, D016), and behavior matches QuickJS, down to error messages, because models read those messages and pi’s tests expect them.
The shape of the system
- The compiler turns source text (JavaScript or TypeScript; types are erased while parsing) into bytecode in a single pass, without building a syntax tree.
- The interpreter runs that bytecode. JavaScript calls do not recurse on the Rust stack: every frame lives in the arena, so the interpreter can return to the host after any instruction and pick up exactly where it stopped.
- The arena holds everything: objects, strings, bytecode, the value stack, the call
frames, the promise job queue, the pending tool calls. Values refer to each other by
u32offsets, never by pointers, so the arena can move, be copied and be read from the outside. - The garbage collector compacts the arena in place, which keeps snapshots small and makes the heap image of a program a deterministic function of the program.
- The builtins (
Array.prototype.map,JSON.parse, …) are written in a strict subset of TypeScript and compiled twice: to bytecode that ships in the base snapshot, and, for the hot ones, to Rust state machines that can suspend in the middle. - The host (pi’s codemode, or the
Sandboxclass of the@djs/hostpackage) sends values in through a compact wire format and reads results by walking the arena directly, with a reader generated from the same layout tables as the engine.
What djs is not
djs is always in strict mode, has no ES modules, no Intl and no JIT, among other
things left out on purpose; the non-goals lists them and why.
How to read this guide
The tour goes from the bottom up: the arena and values, cells and objects, the compiler, the interpreter, suspension, the collector, snapshots, the builtins, determinism, and the host interface. Each chapter explains its trade-offs: what the design costs, and the alternatives that were not taken. The chapters explain each decision where it matters; the decision’s ID (D1050) links to its entry in the decision log, the record kept while djs was built, with the full reasoning and the measurements behind it.
Code samples marked live run in djs-wasm in your browser on the docs site. The output shown under each sample was produced by running it in this build of djs when the guide was checked, so it cannot drift from what djs actually does.
Decisions on this page: