Log inGet started
World

ZeroJS

README.md

ZeroJS is a JavaScript interpreter for the Zero engine, written in Luau. It parses and executes ES2017-practical JavaScript entirely inside the Luau VM — no native code, no WebAssembly, no external dependency — and marshals values back to Luau transparently.

Importing

local zerojs = require("~zerojs.api")

~zerojs is this package's alias, registered from the name in its package.yaml wherever the package sits. The same spelling therefore works from the world that authors the package and from a world that installed it, at any depth. Every module is reachable the same way — ~zerojs.parser, ~zerojs.stdlib.promise — and the package's own modules reference each other with those ids.

API surface

Module-level

SymbolTypeDescription
zerojs.eval(src)(string) -> anyParse and execute src in a fresh one-shot VM. Returns the marshaled JS completion value. Raises a Luau error on a JS exception.
zerojs.new(opts?)(table?) -> VMCreate a persistent VM. opts.env seeds additional JS globals from a Luau table. opts.engine picks the tier ("auto", "compiled", "interp"). opts.console is a table of writers by level (log, info, debug, warn, error): each receives the formatted text of one console call, untagged, and a level without a writer uses log's. With no opts.console, the console writes [zerojs]-tagged lines to the engine log.
zerojs.nullsentinelThe JS null value (distinct from Luau nil).
zerojs.undefinedsentinelThe JS undefined value, which marshals to Luau nil when returned.
zerojs.versionstringVersion string of this package.
zerojs.lastError(vm)(VM) -> table?Returns the structured payload { name, message, stack } from the most recent JS exception on vm. nil if no exception has occurred on that VM.

VM instance

MethodDescription
vm:run(src, opts?)Execute src in this VM's persistent realm. opts.sourceName names the source for error messages and stack frames. Returns the marshaled completion value. Raises directing to runAsync when the script leaves a timer armed or a coroutine suspended across a frame.
vm:runAsync(src, opts?)Execute src, driving its event loop across engine frames until it settles. Returns a RunHandle immediately. opts.timeoutMs rejects the run if it outlasts that many milliseconds. Only one runAsync may be in flight per VM at a time; a second call while the first is unsettled raises.
vm:get(name)Read a global from the VM's realm.
vm:set(name, value)Write a marshaled Luau value as a JS global in the VM's realm.

RunHandle (returned by vm:runAsync)

FieldDescription
donefalse until the run reaches a terminal state, then true.
okOnce done, true for a resolved run and false for a rejection or timeout.
valueThe marshaled completion value on a resolved run. A run that timed out after its script had already completed with a value (an armed setInterval, say, kept the run open) keeps that value here; every other failure leaves it nil.
errorThe rendered message text on a rejected or timed-out run.
lastErrorThe structured { name, message, stack } payload for a failed run.
threadThe engine task.spawn driver pumping the loop each frame — present only while the run is still pumping, and nil once the run settles on its arming frame (a synchronous or microtask-only completion needs no per-frame driver).
handle:await(maxFrames?)From a yieldable Luau context, poll done frame-by-frame until the handle settles, then return it.

A runAsync stays pending while the loop has work: an armed setInterval keeps it pending exactly as an un-unref'd handle keeps Node alive — clear the interval or bound the run with timeoutMs.

Marshaling

Luau values passed to opts.env or vm:set are converted to JS values. JS values returned from run/eval are converted back to Luau: numbers, strings, and booleans are transparent; arrays become 1-based Luau tables; objects become string-keyed Luau tables; JS functions become callable Luau closures; undefined becomes nil; null becomes zerojs.null.

Step-budget sandbox

Each VM runs with a default execution budget of 1,000,000 interpreter steps. A script that exceeds the budget raises a JS RangeError: step budget exceeded, which surfaces as a Luau error. The budget resets on each run/eval call.

Language scope

  • Variable declarations: var (function-scoped hoisting), let and const with full TDZ enforcement
  • Functions: declarations, expressions, arrow functions, closures, rest parameters, default parameters
  • Classes: declarations and expressions, inheritance via extends, super() in constructors and methods, getter and setter accessors, instance fields, static fields and methods, computed method keys, named class expressions, TDZ before declaration
  • Destructuring: object and array patterns in declarations, assignment, and function parameters; nested patterns; rename, default, rest; computed keys in object patterns; elision in array patterns
  • Spread: array literals, call arguments, new arguments, object literals
  • Template literals (untagged)
  • Optional chaining: a?.b, a?.[k], f?.(x). A null or undefined base ends the whole chain with undefined and skips every later link and argument; parentheses end the chain; the chain is not an assignment, update or new target
  • Nullish coalescing ?? and the logical assignments ??=, ||= and &&=, which evaluate the right side and write only when the current value calls for it. ?? does not mix with || or && without parentheses
  • Full coercion and equality semantics: ==/!= abstract equality, ===/!== strict equality, typeof, instanceof
  • try/catch/finally, throw
  • All loop forms: for, while, do-while, for-in, for-of; switch with fallthrough
  • async/await — real event-loop semantics: an async function returns a promise, runs synchronously up to the first await, and resumes from a microtask when its awaited operand settles
  • Property enumerability: for-in yields only own enumerable keys; object rest excludes prototype methods

Asynchrony and the event loop

  • Promise with the full ES surface: then/catch/finally, resolve/reject, and the combinators all, allSettled, race, and any (with AggregateError when every input to any rejects)
  • queueMicrotask for scheduling a microtask directly
  • Timers: setTimeout/clearTimeout and setInterval/clearInterval, with extra arguments forwarded to the callback
  • vm:runAsync(src, opts?) drives a script's timers, microtasks, and promise settlements across engine frames and returns a RunHandle (see the VM table above)
  • A long synchronous loop under runAsync yields at each step-budget boundary and resumes on a later frame instead of raising RangeError; the same script under vm:run still raises

Standard library

  • Object: callable coercion plus keys, values, entries, assign, freeze, isFrozen, create, getPrototypeOf, defineProperty, fromEntries, hasOwn, and the Object.prototype methods (hasOwnProperty, toString, isPrototypeOf)
  • Function.prototype: call, apply, bind
  • Global conversions and predicates: parseInt, parseFloat, isNaN, isFinite, String, Number, Boolean
  • Array and Array.prototype: construction, isArray, from, of, and the iteration/mutation/query methods (map, filter, reduce, forEach, find, some, every, slice, splice, concat, join, indexOf, includes, sort, reverse, push, pop, shift, unshift, flat, fill, keys, values, entries)
  • String.prototype: slice, substring, substr, split, indexOf, includes, startsWith, endsWith, repeat, padStart, padEnd, trim, trimStart, trimEnd, toUpperCase, toLowerCase, charAt, charCodeAt, codePointAt, at, concat, replace, replaceAll, match, matchAll, search
  • Number: isInteger, isSafeInteger, isFinite, isNaN, parseFloat, parseInt, MAX_SAFE_INTEGER, MIN_SAFE_INTEGER, MAX_VALUE, MIN_VALUE, EPSILON, POSITIVE_INFINITY, NEGATIVE_INFINITY, NaN, and Number.prototype (toFixed, toString, toPrecision)
  • Math: the constants and functions (abs, floor, ceil, round, trunc, sign, min, max, pow, sqrt, cbrt, hypot, exp, log, log2, log10, the trig family, random)
  • Date: construction as new Date() (now) and new Date(ms) (epoch milliseconds), now (the wall clock at the moment of the call, never ahead of it and never moving backwards; whole-second readings of the wall clock bound it until a reading crosses a second boundary), getTime, getFullYear, getMonth, getDate, getHours, getMinutes, getSeconds, getMilliseconds, getDay, toISOString, toJSON
  • JSON: parse and stringify (indent argument and array replacer whitelist)
  • Map and Set: construction from iterables, get/set/has/delete/clear, add, size, forEach, keys, values, entries, insertion-order iteration
  • RegExp: literals and the constructor, test, exec, lastIndex, source, flags, global, ignoreCase, multiline, dotAll, and the String regex methods above
  • Errors: Error, TypeError, RangeError, ReferenceError, SyntaxError constructors and inheritance; Error.prototype.name, message and toString, so String(e), ${e} and "" + e read <name>: <message>
  • console: log, info, debug, warn, error, dir and assert. Values print the way a JavaScript console prints them: strings bare at the top and quoted inside, { a: 1, b: [ 1, 2 ] }, Map(1) { 'a' => 1 }, [Function: f], errors as Error: message, nested values below depth 2 as [Object] and [Array], arrays and collections cut after 100 entries, a self-containing value as <ref *1> { self: [Circular *1] }, and a rendering wider than 80 characters across lines. A leading %s, %d, %i, %f, %j, %o, %O, %c or %% formats the arguments after it
  • performance.now() (milliseconds on a monotonic clock since the VM was created) and performance.timeOrigin
  • globalThis, NaN, Infinity

Out of scope by design

  • Generators and iterators (function*, yield)
  • Proxy and Reflect
  • Labeled statements and with
  • Tagged template literals

The full divergence list — where ZeroJS behavior differs from a browser engine — lives in the guide's divergence section.

Test suites

Eleven suites ship with the package, in its tests/ folder, run via tools.tests.run:

tools.tests.run({ suite = "zerojs_lexer" })        -- tokenization
tools.tests.run({ suite = "zerojs_parser" })       -- AST construction and error recovery
tools.tests.run({ suite = "zerojs_semantics" })    -- runtime semantics end-to-end
tools.tests.run({ suite = "zerojs_language" })     -- ?., ??, logical assignment, error text, console, Date.now, performance.now
tools.tests.run({ suite = "zerojs_stdlib" })       -- the standard-library surface
tools.tests.run({ suite = "zerojs_typed_arrays" }) -- typed arrays, ArrayBuffer, DataView
tools.tests.run({ suite = "zerojs_regex" })        -- the regex engine and RegExp methods
tools.tests.run({ suite = "zerojs_async" })        -- the event loop, promises, and timers
tools.tests.run({ suite = "zerojs_modules" })      -- the module graph
tools.tests.run({ suite = "zerojs_assettype" })    -- the zJs asset type
tools.tests.run({ suite = "zerojs_smoke" })        -- unmodified real-world JS programs