zerojs
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.
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
| Symbol | Type | Description |
|---|---|---|
zerojs.eval(src) | (string) -> any | Parse 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?) -> VM | Create 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.null | sentinel | The JS null value (distinct from Luau nil). |
zerojs.undefined | sentinel | The JS undefined value, which marshals to Luau nil when returned. |
zerojs.version | string | Version 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
| Method | Description |
|---|---|
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)
| Field | Description |
|---|---|
done | false until the run reaches a terminal state, then true. |
ok | Once done, true for a resolved run and false for a rejection or timeout. |
value | The 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. |
error | The rendered message text on a rejected or timed-out run. |
lastError | The structured { name, message, stack } payload for a failed run. |
thread | The 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),letandconstwith 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,
newarguments, object literals - Template literals (untagged)
- Optional chaining:
a?.b,a?.[k],f?.(x). Anullorundefinedbase ends the whole chain withundefinedand skips every later link and argument; parentheses end the chain; the chain is not an assignment, update ornewtarget - 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;switchwith fallthrough async/await— real event-loop semantics: an async function returns a promise, runs synchronously up to the firstawait, and resumes from a microtask when its awaited operand settles- Property enumerability:
for-inyields only own enumerable keys; object rest excludes prototype methods
Asynchrony and the event loop
Promisewith the full ES surface:then/catch/finally,resolve/reject, and the combinatorsall,allSettled,race, andany(withAggregateErrorwhen every input toanyrejects)queueMicrotaskfor scheduling a microtask directly- Timers:
setTimeout/clearTimeoutandsetInterval/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 aRunHandle(see the VM table above)- A long synchronous loop under
runAsyncyields at each step-budget boundary and resumes on a later frame instead of raisingRangeError; the same script undervm:runstill raises
Standard library
Object: callable coercion pluskeys,values,entries,assign,freeze,isFrozen,create,getPrototypeOf,defineProperty,fromEntries,hasOwn, and theObject.prototypemethods (hasOwnProperty,toString,isPrototypeOf)Function.prototype:call,apply,bind- Global conversions and predicates:
parseInt,parseFloat,isNaN,isFinite,String,Number,Boolean ArrayandArray.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,searchNumber:isInteger,isSafeInteger,isFinite,isNaN,parseFloat,parseInt,MAX_SAFE_INTEGER,MIN_SAFE_INTEGER,MAX_VALUE,MIN_VALUE,EPSILON,POSITIVE_INFINITY,NEGATIVE_INFINITY,NaN, andNumber.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 asnew Date()(now) andnew 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,toJSONJSON:parseandstringify(indent argument and array replacer whitelist)MapandSet: construction from iterables,get/set/has/delete/clear,add,size,forEach,keys,values,entries, insertion-order iterationRegExp: literals and the constructor,test,exec,lastIndex,source,flags,global,ignoreCase,multiline,dotAll, and theStringregex methods above- Errors:
Error,TypeError,RangeError,ReferenceError,SyntaxErrorconstructors and inheritance;Error.prototype.name,messageandtoString, soString(e),${e}and"" + eread<name>: <message> console:log,info,debug,warn,error,dirandassert. 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 asError: 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,%cor%%formats the arguments after itperformance.now()(milliseconds on a monotonic clock since the VM was created) andperformance.timeOriginglobalThis,NaN,Infinity
Out of scope by design
- Generators and iterators (
function*,yield) ProxyandReflect- 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
Scoped to this part · feeds back into the world's score.