The language
djs runs modern JavaScript: the language of the current ECMAScript specification, with its builtins, in strict mode. This page says what a script can rely on: how its code is compiled, what is left out and why, which builtins exist, and the two variations a host can choose, the slim build and frozen builtins.
In short. Code is always strict. A host runs a script as the body of an async function, so
awaitandreturnwork at the top level; Scripts in the specification’s sense exist for the REPL and for tests, and ES modules for native hosts that load them through a loader of their own. What is left out (with, directeval,Intl, timers, I/O, and modules in the wasm modules) is what codemode scripts don’t need and what would cost size or safety; the rest of the standard library is there, matching QuickJS’s behavior and messages.
Always strict
Every piece of code djs compiles is strict mode code, with or without a "use strict"
directive. There is no sloppy mode to fall back to: assigning to an undeclared name is a
ReferenceError, this in a plain function call is undefined, octal literals and
with are syntax errors, and writing to a read-only property throws.
function whoAmI() { return this; }
console.log(typeof whoAmI());
try {
undeclared = 1;
} catch (e) {
console.log(String(e));
}
try {
Object.freeze([1, 2]).push(3);
} catch (e) {
console.log(String(e));
}
undefined ReferenceError: 'undeclared' is not defined TypeError: object is not extensible
function 0 <anonymous>: params=0 locals=4 stack=5 captures=0
handler [29, 37) -> 39 depth 0
handler [66, 96) -> 98 depth 0
handler [3, 129) -> 129 depth 0
0 async_start loc0 (<state>)
3 fclosure child0
8 put_loc1 (whoAmI)
9 get_global "console" ic0 ; 2:1
16 get_method "log" ic1 ; 2:8
23 get_loc1 (whoAmI) ; 2:20
24 undefined
25 call0 ; 2:26
26 typeof
27 call1 ; 2:12
28 drop
29 push1 ; 4:3
30 put_global "undeclared" ic2
37 jump8 -> 66
39 put_loc2 (e)
40 get_global "console" ic3 ; 6:3
47 get_method "log" ic4 ; 6:10
54 get_global "String" ic5 ; 6:15
61 undefined
62 get_loc2 (e) ; 6:22
63 call1 ; 6:21
64 call1 ; 6:14
65 drop
66 get_global "Object" ic6 ; 9:3
73 get_method "freeze" ic7 ; 9:9
80 push1
81 push2
82 array_from 2
85 call1 ; 9:16
86 get_method "push" ic8 ; 9:24
93 push3
94 call1 ; 9:29
95 drop
96 jump8 -> 125
98 put_loc3 (e)
99 get_global "console" ic9 ; 11:3
106 get_method "log" ic10 ; 11:10
113 get_global "String" ic11 ; 11:15
120 undefined
121 get_loc3 (e) ; 11:22
122 call1 ; 11:21
123 call1 ; 11:14
124 drop
125 undefined
126 async_return loc0 (<state>)
129 async_reject loc0 (<state>)
function 0.0 whoAmI: params=0 locals=1 stack=1 captures=0
0 special_object 4
2 put_loc0 (this)
3 get_loc0 (this) ; 1:28
4 return
Run the example to walk the VM's arena.
Run the example to see it suspend.
One exception keeps old code working: a function made by the Function constructor
without a "use strict" directive gets the sloppy binding of this (the global object
when called plainly), and nothing else of sloppy mode.
Trade-off. Strict only, over both modes. Sloppy mode doubles the semantics of scopes,
arguments,thisand assignments, and every one of those paths would need the same care for suspension, dumps and determinism as the strict one. Scripts written today, and every script a model writes, are strict-compatible. The cost: an old snippet that relies on sloppy semantics (an implicit global,arguments.callee) fails with an error instead of running.
Codemode bodies and Scripts
A host runs a script as a codemode body: the body of an async function. That gives
it top-level await and return, and the parameters the host passes (params in the
JavaScript interface) are its parameters. The value the body
returns, or its promise’s value, is the result of the run; an exception it does not
catch is the run’s error.
const sizes = await Promise.all(["a", "bb", "ccc"].map(async (s) => s.length));
if (sizes.length === 0) return "nothing";
const total = sizes.reduce((a, b) => a + b);
console.log(sizes, total);
return total;
[1,2,3] 6
function 0 <anonymous>: params=0 locals=3 stack=5 captures=0
handler [3, 97) -> 97 depth 0
0 async_start loc0 (<state>)
3 get_global "Promise" ic0 ; 1:21
10 get_method "all" ic1 ; 1:28
17 push_const8 #0 ("a")
19 push_const8 #1 ("bb")
21 push_const8 #2 ("ccc")
23 array_from 3
26 get_method "map" ic2 ; 1:51
33 fclosure child0
38 call1 ; 1:55
39 call1 ; 1:32
40 await loc0 (<state>) ; 1:15
43 set_loc1 (sizes) ; 2:5
44 get_field "length" ic3 ; 2:10
51 push0
52 strict_eq ; 2:18
53 jump_if_false8 -> 60
55 push_const8 #3 ("nothing")
57 async_return loc0 (<state>)
60 get_loc1 (sizes) ; 3:15
61 get_method "reduce" ic4 ; 3:20
68 fclosure child1
73 call1 ; 3:27
74 put_loc2 (total)
75 get_global "console" ic5 ; 4:1
82 get_method "log" ic6 ; 4:8
89 get_loc1 (sizes) ; 4:13
90 get_loc2 (total) ; 4:20
91 call2 ; 4:12
92 drop
93 get_loc2 (total) ; 5:8
94 async_return loc0 (<state>)
97 async_reject loc0 (<state>)
function 0.0 <anonymous>: params=1 locals=1 stack=1 captures=0
handler [3, 14) -> 14 depth 0
0 async_start loc0 (<state>)
3 get_arg0 ; 1:69
4 get_field "length" ic0 ; 1:70
11 async_return loc0 (<state>)
14 async_reject loc0 (<state>)
function 0.1 <anonymous>: params=2 locals=0 stack=2 captures=0
0 get_arg0 ; 3:38
1 get_arg1 ; 3:42
2 add ; 3:40
3 return
Run the example to walk the VM's arena.
Run the example to see it suspend.
A body’s declarations are local to it: two runs in the same sandbox share only what the
first one put on globalThis.
The second kind of code is a Script in the specification’s sense: top-level var
and function declarations become properties of the global object, let, const and
class go into a global lexical scope that later Scripts see, and the Script has a
completion value. The REPL runs every input as a Script (the REPL),
the test suites use them, and a continuation dump is one (dumps). A Script
has no top-level await.
Modules
The third kind is module code, for native hosts that embed djs in Rust: a policy
written as a module that imports libraries the host provides. Every form of import and
export works (default, named, namespace and string names, re-exports, export * as,
export default of expressions, functions and classes), and so does import.meta, whose
url is the module’s key, and so does top-level await: a module that awaits at its top
level is evaluated asynchronously, its importers run once it is done, and modules that do
not depend on each other wait at the same time, in the order the specification gives.
this at a module’s top level is undefined. TypeScript modules work too: type-only imports and exports are
erased, and an import that no value refers to is dropped, as tsc does when it cannot see
the types.
The engine does no I/O for modules. The host implements a loader with two calls, one
that turns a specifier into a key and one that returns a key’s source
(crates/djs-core/src/vm/module.rs); keys are
opaque strings, and a key already loaded is not loaded again. Loading a module loads,
compiles and links its whole graph at once, so a missing or ambiguous import is a
SyntaxError before any module runs. Evaluating it is a run like any call: module bodies
are ordinary frames, so they can wait on host calls, run out of fuel and continue, and an
error a module throws stays its result for later evaluations. Imports are live bindings
(an importer sees the exporter’s binding, not a copy), cycles work as the specification
says, namespace objects behave as specified, and the module records live in the VM’s heap,
so a snapshot taken after evaluation carries the evaluated graph: a VM
made from it reads the same exports. The host reads a module’s exports and namespace
directly.
A host can also define synthetic modules: modules without source whose exports are
values the host made, such as host functions and configuration data (an @sdk/net with
allowHosts and secret). Code imports them like any module, and the host can change an
export’s value later, which importers see, since their bindings are live.
import() goes through the same loader when the host turns that on, in modules and
in Scripts alike. The call becomes an asynchronous host call with the specifier and the
referrer, which is the importing module’s key or the Script’s name, known when the code
is compiled. The host loads and links the graph and answers with its key, or with the
error, which rejects the promise; the engine then evaluates the graph in a later step and
fulfills the promise with the namespace. A module that throws rejects every later import
of it with the same error.
JSON modules are the one kind of module besides JavaScript and TypeScript: import data from "./config.json" with { type: "json" } (or import("./config.json", { with: { type: "json" } })) gets the parsed JSON text as the default export. The type attribute
reaches the host’s loader with the specifier, and the loader says which modules are JSON
when it returns their source. An import with type: "json" loads only a JSON module and one
without it never does (a TypeError either way, so a policy that expects data cannot be
handed code), and any attribute other than type is a SyntaxError when the graph loads.
Modules come with the native build for Rust hosts, together with the rest of what only
such hosts use: TextEncoder and TextDecoder are the other part. The wasm modules leave
them out, since codemode scripts are one piece of code that gets its data from host
functions. There import and export declarations are syntax errors, import()
compiles everywhere but returns a rejected promise, as below (as it does in a native VM
whose host does not answer imports), and a use of TextEncoder is a ReferenceError that
names the build.
What is left out, and why
| Left out | Why |
|---|---|
Sloppy mode, with | See above: one set of semantics. with is a syntax error. |
Direct eval | A direct eval can see and change the caller’s local variables, so every function that contains one would need its scopes kept by name. eval(x) is always an indirect eval: it compiles x as a global Script. The Function constructors work. |
| ES modules in codemode | A codemode script is one piece of code: in a body or a Script, import and export declarations are syntax errors, and import() compiles and returns a rejected promise. Hosts give scripts their functions as globals or parameters instead (bindings). Native hosts can load modules. |
Intl | Locale data is megabytes. toLocaleString and friends use a fixed, locale-independent form. |
Temporal | Not needed by the scripts djs runs; Date is there. |
SharedArrayBuffer, Atomics, workers | A sandbox is one thread. |
Timers, fetch, file access, structuredClone | The engine does no I/O and has no event loop of its own: everything outside the script is a host function the host chose to give it. |
| A JIT | wasm and a small module size come first; the interpreter is fast enough for scripts that mostly move data. |
A few host-like globals are there because scripts use them and they need no outside
world: atob, btoa, queueMicrotask, DOMException, and console, which is a host
object whose methods the host provides.
console.log(typeof Intl, typeof setTimeout, typeof atob, typeof queueMicrotask);
try {
await import("./tools.js");
} catch (e) {
console.log(String(e));
}
console.log(eval("typeof globalThis"), (1234.5).toLocaleString());
undefined undefined function function ReferenceError: could not load module './tools.js' object 1234.5
function 0 <anonymous>: params=0 locals=2 stack=6 captures=0
handler [53, 67) -> 69 depth 0
handler [3, 137) -> 137 depth 0
0 async_start loc0 (<state>)
3 get_global "console" ic0 ; 1:1
10 get_method "log" ic1 ; 1:8
17 get_global_typeof "Intl" ic2 ; 1:20
24 typeof
25 get_global_typeof "setTimeout" ic3 ; 1:33
32 typeof
33 get_global_typeof "atob" ic4 ; 1:52
40 typeof
41 get_global_typeof "queueMicrotask" ic5 ; 1:65
48 typeof
49 call 4 ; 1:12
52 drop
53 push_const8 #0 ("./tools.js")
55 undefined
56 push_const8 #1 ("example.js") ; 3:9
58 call_intrinsic %ModuleOp 3
63 await loc0 (<state>) ; 3:3
66 drop
67 jump8 -> 96
69 put_loc1 (e)
70 get_global "console" ic6 ; 5:3
77 get_method "log" ic7 ; 5:10
84 get_global "String" ic8 ; 5:15
91 undefined
92 get_loc1 (e) ; 5:22
93 call1 ; 5:21
94 call1 ; 5:14
95 drop
96 get_global "console" ic9 ; 7:1
103 get_method "log" ic10 ; 7:8
110 get_global "eval" ic11 ; 7:13
117 undefined
118 push_const8 #2 ("typeof globalThis")
120 call1 ; 7:17
121 push_const8 #3 (1234.5)
123 get_method "toLocaleString" ic12 ; 7:48
130 call0 ; 7:63
131 call2 ; 7:12
132 drop
133 undefined
134 async_return loc0 (<state>)
137 async_reject loc0 (<state>)
Run the example to walk the VM's arena.
Run the example to see it suspend.
The builtins
Everything else of the standard library is there:
- the fundamentals:
Object,Function,Symbol,Errorand its subclasses,Proxy,Reflect,globalThis; - numbers and text:
Number,Math,BigInt,Stringwith all its methods,RegExpwith named groups, lookbehind, thed,s,uandvflags and Unicode property escapes; - collections:
Array(with the change-by-copy methods such astoSortedandwith), typed arrays,ArrayBuffer,DataView,Map,Setwith its set operations,WeakMap,WeakSet,WeakRef,FinalizationRegistry,Object.groupByandMap.groupBy; - control:
Promisewith its combinators andwithResolvers, generators, async functions and async generators, iterators with theIteratorhelpers (map,filter,take, …),Array.fromAsync; - data:
JSON,Date,Uint8Array.fromBase64andtoBase64, the URI functions,TextEncoderandTextDecoderin the native build (UTF-8 only: another label is a RangeError; withfatal,ignoreBOMandstream); - resource management:
usingandawait usingdeclarations,DisposableStackandAsyncDisposableStack; - classes with private fields and methods, static blocks, and decorators.
const words = ["fuel", "frames", "arena", "image", "fuel"];
const byLength = Map.groupBy(new Set(words), (w) => w.length);
console.log([...byLength.keys()].toSorted().join(" "));
console.log(/(?<word>a\w+)/v.exec("the arena").groups.word, "image".at(-1));
const it = Iterator.from(words).filter((w) => w.startsWith("f")).map((w) => w.toUpperCase());
console.log(it.toArray().join(" "));
{
using _ = { [Symbol.dispose]() { console.log("disposed at the end of the block"); } };
console.log("in the block");
}
4 5 6 arena e FUEL FRAMES FUEL in the block disposed at the end of the block
function 0 <anonymous>: params=0 locals=8 stack=6 captures=0
handler [244, 295) -> 316 depth 0
handler [3, 335) -> 335 depth 0
0 async_start loc0 (<state>)
3 push_const8 #0 ("fuel")
5 push_const8 #1 ("frames")
7 push_const8 #2 ("arena")
9 push_const8 #3 ("image")
11 push_const8 #0 ("fuel")
13 array_from 5
16 put_loc1 (words)
17 get_global "Map" ic0 ; 2:18
24 get_method "groupBy" ic1 ; 2:21
31 get_global "Set" ic2 ; 2:34
38 get_loc1 (words) ; 2:38
39 construct 1 ; 2:37
42 fclosure child0
47 call2 ; 2:29
48 put_loc2 (byLength)
49 get_global "console" ic3 ; 3:1
56 get_method "log" ic4 ; 3:8
63 array_from 0
66 get_loc2 (byLength) ; 3:17
67 get_method "keys" ic5 ; 3:25
74 call0 ; 3:30
75 array_spread
76 get_method "toSorted" ic6 ; 3:33
83 call0 ; 3:42
84 get_method "join" ic7 ; 3:44
91 push_const8 #4 (" ")
93 call1 ; 3:49
94 call1 ; 3:12
95 drop
96 get_global "console" ic8 ; 4:1
103 get_method "log" ic9 ; 4:8
110 push_const8 #5 (internal@7768)
112 push_const8 #6 ("(?<word>a\\w+)")
114 push_const8 #7 ("v")
116 call_intrinsic %RegExpCreate 3
121 get_method "exec" ic10 ; 4:29
128 push_const8 #8 ("the arena")
130 call1 ; 4:34
131 get_field "groups" ic11 ; 4:47
138 get_field "word" ic12 ; 4:54
145 push_const8 #3 ("image")
147 get_method "at" ic13 ; 4:68
154 push1
155 neg ; 4:72
156 call1 ; 4:71
157 call2 ; 4:12
158 drop
159 get_global "Iterator" ic14 ; 5:12
166 get_method "from" ic15 ; 5:20
173 get_loc1 (words) ; 5:26
174 call1 ; 5:25
175 get_method "filter" ic16 ; 5:32
182 fclosure child1
187 call1 ; 5:39
188 get_method "map" ic17 ; 5:65
195 fclosure child2
200 call1 ; 5:69
201 put_loc3 (it)
202 get_global "console" ic18 ; 6:1
209 get_method "log" ic19 ; 6:8
216 get_loc3 (it) ; 6:13
217 get_method "toArray" ic20 ; 6:15
224 call0 ; 6:23
225 get_method "join" ic21 ; 6:25
232 push_const8 #4 (" ")
234 call1 ; 6:30
235 call1 ; 6:12
236 drop
237 call_intrinsic %UsingCreate 0
242 put_loc8 loc4 (<temp>)
244 object
245 get_global "Symbol" ic22 ; 8:16
252 get_field "dispose" ic23 ; 8:22
259 fclosure child3
264 set_function_name 0
266 define_method_elem
267 get_loc8 loc4 (<temp>) ; 8:9
269 swap
270 call_intrinsic %UsingAdd 2
275 put_loc8 loc7 (_)
277 get_global "console" ic24 ; 9:3
284 get_method "log" ic25 ; 9:10
291 push_const8 #9 ("in the block")
293 call1 ; 9:14
294 drop
295 push0
296 put_loc8 loc5 (<temp>)
298 get_loc8 loc4 (<temp>)
300 call_intrinsic %UsingDispose 1
305 drop
306 get_loc8 loc5 (<temp>)
308 jump_if_false8 -> 331
310 get_loc8 loc6 (<temp>)
312 undefined
313 put_loc8 loc6 (<temp>)
315 throw
316 get_loc8 loc4 (<temp>)
318 swap
319 call_intrinsic %UsingDisposeError 2
324 put_loc8 loc6 (<temp>)
326 push1
327 put_loc8 loc5 (<temp>)
329 jump8 -> 298
331 undefined
332 async_return loc0 (<state>)
335 async_reject loc0 (<state>)
function 0.0 <anonymous>: params=1 locals=0 stack=1 captures=0
0 get_arg0 ; 2:53
1 get_field "length" ic0 ; 2:54
8 return
function 0.1 <anonymous>: params=1 locals=0 stack=3 captures=0
0 get_arg0 ; 5:47
1 get_method "startsWith" ic0 ; 5:48
8 push_const8 #0 ("f")
10 call1 ; 5:59
11 return
function 0.2 <anonymous>: params=1 locals=0 stack=2 captures=0
0 get_arg0 ; 5:77
1 get_method "toUpperCase" ic0 ; 5:78
8 call0 ; 5:90
9 return
function 0.3 <anonymous>: params=0 locals=0 stack=3 captures=0
0 get_global "console" ic0 ; 8:36
7 get_method "log" ic1 ; 8:43
14 push_const8 #0 ("disposed at the end of the block")
16 call1 ; 8:47
17 drop
18 return_undef
Run the example to walk the VM's arena.
Run the example to see it suspend.
What a builtin does, and the text of the errors it throws, follows QuickJS where the specification leaves room, because models read those messages and the tests compare them. Status in numbers gives the conformance figures.
The slim build
djs also comes in a slim build, for hosts that count every kilobyte. It leaves out the
families that codemode scripts don’t use and that cost real size: typed arrays with
ArrayBuffer and DataView, BigInt, the Iterator helpers, WeakRef and
FinalizationRegistry, DisposableStack and the using declarations, and decorators.
A script that reaches for one gets an error that names the build, and feature tests keep
working:
new Uint8Array(4); // ReferenceError: 'Uint8Array' is not available in this build
typeof BigInt; // "undefined"
[].values().map; // undefined: the Iterator helpers are gone
10n; // SyntaxError: BigInt literals are not available in this build
Builtins explains how the build is made.
Frozen builtins
A host can make a sandbox with frozen builtins (frozenBuiltins in the JavaScript
interface). The builtin objects (prototypes,
constructors, Math, JSON, …) then behave as frozen: writing to them fails with a
TypeError, their properties read as non-writable and non-configurable, and
Object.isFrozen(Array.prototype) is true. Assigning over an inherited builtin
property, as in obj.toString = f or this.name = "MyError" in an Error subclass,
defines an own property instead of failing, as after a “lockdown” in hardened
JavaScript. globalThis itself stays ordinary, so a script can still add globals.
Array.prototype.sum = function () {}; // TypeError: the builtins are frozen
class MyError extends Error {
constructor(m) { super(m); this.name = "MyError"; } // fine: an own property
}
Frozen builtins protect the host’s helpers from a script that patches a prototype, and they are what lets djs write continuation dumps: a builtin that cannot change can be written in a dump by naming it. The mode is chosen when the sandbox is created and stays with it.
Where to look
builtins/: the builtins, written in a strict subset of TypeScript (builtins).crates/djs-core/src/compiler/: the compiler, including what it rejects.tests/test262/expected-failures.txt: the test262 tests djs fails, with reasons.