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
```luau
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. |
| `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. |
| `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), `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)
- 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`, `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; `console.log`/`warn`/`error`; `globalThis`, `NaN`, `Infinity`
### Out of scope by design
- Generators and iterators (`function*`, `yield`)
- `Proxy` and `Reflect`
- Labeled statements and `with`
- Tagged template literals
- Optional chaining (`?.`) and nullish coalescing (`??`)
The full divergence list — where ZeroJS behavior differs from a browser engine — lives in the guide's divergence section.
## Test suites
Seven suites ship with the package, run via `tools.tests.run`:
```luau
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_stdlib" }) -- the standard-library surface
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_smoke" }) -- unmodified real-world JS programs
```