js_engine

Pure MoonBit cross-target embedded JavaScript engine

javascript
interpreter
embedded
scripting
moon add dowdiness/js_engine@0.8.0
Download zip
Author
Version
0.8.0
License
Apache-2.0
Last updated
8 days ago
Downloads
1K

Dependencies

README

#js_engine

A pure MoonBit, cross-target embedded JavaScript engine. It uses a tree-walking interpreter and runs on MoonBit's native, JavaScript, Wasm, and Wasm-GC targets.

  • Conformance on test262: each file is run in strict and non-strict modes and reported per mode. Do not sum the modes. Generate current numbers from CI artifacts with make test262-report; see docs/TEST262.md.
  • Cross-target embedding: the same stateful Engine API is tested on native, JavaScript, Wasm, and Wasm-GC.
  • Benchmark dashboard: https://dowdiness.github.io/js_engine/benchmarks/

#Quick Start

#CLI

moon run cmd/main -- 'console.log(1 + 2)' # 3

moon run cmd/main -- ' function fib(n) { if (n <= 1) { return n; } return fib(n - 1) + fib(n - 2); } console.log(fib(10)); ' # 55

More sample programs live in example/.

#As a Library

///|
test "README stateful rule engine" {
let engine = @js_engine.Engine()
let source =
#|let evaluations = 0;
#|function allow(request) {
#| evaluations += 1;
#| return { allowed: request.role === "admin", evaluations };
#|}
engine.eval(source)
let admin = Json::object({ "role": Json::string("admin") })
let member_request = Json::object({ "role": Json::string("member") })
json_inspect(engine.call_json("allow", [admin]), content={
"allowed": true,
"evaluations": 1,
})
json_inspect(engine.call_json("allow", [member_request]), content={
"allowed": false,
"evaluations": 2,
})
}

Engine keeps one global realm alive across calls. Its strict JSON boundary copies plain data directly: it does not consult a mutable global JSON, call getters or toJSON, or execute Proxy traps. Promise results and non-JSON values are rejected. This API is intended for trusted application scripts, not as a security sandbox. See example/rule_engine/ for the runnable example.

The stable embedding guide defines the JSON boundary, lookup rules, queue checkpoints, retained-state behavior, error reuse limits, and four-target contract.

For one-shot evaluation, the existing facade remains available:

///|
test "README one-shot facade" {
let (output, _) = @js_engine.run("console.log(1 + 2)")
json_inspect(output, content=["3"])
}

The public entry points are defined in js_engine.mbt and classified in the stable guide:

  • Stable embedding: run, Engine, EngineError, and the unbounded persistent Engine methods listed in the guide.
  • Staged Stage 4 availability: Engine::eval_bounded, Engine::call_json_bounded, Engine::run_microtask_checkpoint_bounded, Engine::run_timer_checkpoint_bounded, ExecutionPolicy, ExecutionPolicyError, and InterruptionHandle.
  • Compatibility: run_module / run_modules; their export maps expose raw runtime values.
  • Advanced/internal: run_compiled and the module-level event-loop APIs that expose or accept a raw interpreter.

#Embedding (custom host objects)

For DOM-style globals and native methods, create a wired interpreter and inject bindings — do not reverse-engineer Interpreter::new / setup_builtins unless you need to replace builtin installation itself:

let interp = @interpreter.new_interpreter()
// Build query_selector with realm_state=Some(interp.realm_state) — see guide.
let document = @runtime.make_host_object(
name="Document",
proto=@runtime.get_obj_proto(realm_state=Some(interp.realm_state)),
methods={ "querySelector": query_selector },
)
interp.global.def_builtin("document", document)
// Then parse, interp.run, interp.run_microtasks(), interp.run_timers().

Full advanced cookbook (make_*_func + realm_state, errors, host slots, globalThis, custom setup_builtins): docs/advanced-embedding.md.

#Supported Language

Core ES5 plus selected ES6+ features: let / const / var, arrow functions, closures, classes, for / while / for-in / for-of, try / catch / finally, template literals, destructuring, spread / rest, ES Modules, Promises + microtasks, setTimeout / setInterval, ES6 Proxy (13 traps) + Reflect API (13 methods), TypedArrays (9 types), ArrayBuffer, DataView, RegExp, JSON, Map / Set / WeakMap / WeakSet, generators, Symbols.

For the full per-category breakdown, see docs/supported-features.md.

#Conformance

Test262 conformance by edition — CI run 30346236658, tip 265bbfd, 2026-07-28. P/E = passed ÷ executed (excludes skipped tests). Refresh: make test262-report ARGS="--format=readme".

#strict

EditionDiscoveredSkippedExecutedPassedFailedTimeout/ErrPassed / ExecutedPassed / Discovered
Pre-ES2015 (baseline)13,281013,27713,057220498.3%98.3%
ES201510,30016110,13110,03398899.0%97.4%
ES20161000999901100.0%99.0%
ES201773634439239200100.0%53.3%
ES20184,7257273,9983,822176095.6%80.9%
ES2019128012810622082.8%82.8%
ES20201,7841,5372472443098.8%13.7%
ES202146812834032614095.9%69.7%
ES20225,065345,0312,7652,266055.0%54.6%
ES2023254332212183098.6%85.8%
ES20241,07286620610898052.4%10.1%
ES20251,14877936929673080.2%25.8%
Annex B3654431926554283.1%72.6%
Stage 35,5315,5191266050.0%0.1%
Total44,98610,20134,77031,7373,0331591.3%70.5%

Fully-skipped buckets (no tests executed) folded into Total: Unmapped (29).

#non-strict

EditionDiscoveredSkippedExecutedPassedFailedTimeout/ErrPassed / ExecutedPassed / Discovered
Pre-ES2015 (baseline)13,917013,90913,544365897.4%97.3%
ES201510,78816010,62010,498122898.9%97.3%
ES20161000999901100.0%99.0%
ES201777534443143100100.0%55.6%
ES20184,7817354,0463,870176095.7%80.9%
ES2019127012710522082.7%82.7%
ES20201,9841,6043803773099.2%19.0%
ES202144412831630214095.6%68.0%
ES20225,3612965,0652,7882,277055.0%52.0%
ES2023277562212183098.6%78.7%
ES20241,07787020710998052.7%10.1%
ES20251,18081336729473080.1%24.9%
Annex B1,156441,110909201281.9%78.6%
Stage 35,6965,593103109309.7%0.2%
Total47,69210,67237,00133,5543,4471990.7%70.4%

Fully-skipped buckets (no tests executed) folded into Total: Unmapped (29).

#Package Structure

token/ Token types and source locations errors/ JavaScript error variants and formatting helpers lexer/ Tokenizer ast/ AST node definitions parser/ Recursive descent parser with Pratt precedence static_semantics/ Early-error and declaration-fact analysis compiler/ Opt-in closure-conversion prototype interpreter/ Wiring layer for runtime + standard library interpreter/runtime/ Tree-walking evaluator, value model, host state interpreter/stdlib/ JavaScript built-ins cmd/main/ CLI entry point cmd/test262_runner/ Native test262 runner cmd/report_test262/ CI artifact report generator benchmarks/ Benchmark workloads and runner example/rule_engine/ Canonical stateful JSON rule-engine embedding

#Development

moon check # Type check moon test # Run unit tests moon fmt # Format code moon info # Update .mbti interface files moon build # Build

Run the test262 conformance suite with make test262. See docs/TEST262.md for prerequisites, filtering, and options.

#Documentation

#License

Apache-2.0

#
ExecutionPolicy

Opaque policy and interruption controls for the staged bounded-evaluation facade. Runtime owns their invariants; the root package is the consumer API.

#
ExecutionPolicyError

Opaque policy and interruption controls for the staged bounded-evaluation facade. Runtime owns their invariants; the root package is the consumer API.

#
InterruptionHandle

Opaque policy and interruption controls for the staged bounded-evaluation facade. Runtime owns their invariants; the root package is the consumer API.

#
EngineError

pub(all) suberror EngineError {
ParseError(String)
JavaScriptException(String)
MissingGlobal(String)
NotCallable(String)
JsonConversionError(String)
InternalError(String)
} derive(
Debug
)

Stable errors raised by the stateful Engine facade.
impl Show for EngineError

#
Engine

A persistent JavaScript realm for repeated evaluation and JSON calls.

#
Engine::Engine

fn Engine::Engine(annex_b? : Bool) -> Engine

#
Engine::call_json

fn Engine::call_json(self : Engine, name : String, args : Array[Json]) -> Json raise EngineError

#
Engine::call_json_bounded

fn Engine::call_json_bounded(self : Engine, name : String, args : Array[Json], policy :
ExecutionPolicy
) -> Result[Json, EngineDiagnostic]

Call a JSON-boundary function under one explicitly supplied, operation-scoped execution policy. The same fresh control carrier spans global lookup, direct JSON conversion, target execution, and direct result conversion. The direct bridge itself does not execute JavaScript.

#
Engine::call_json_diagnostic

fn Engine::call_json_diagnostic(self : Engine, name : String, args : Array[Json]) -> Result[Json, EngineDiagnostic]

Call a JSON-boundary function while returning operation-aware failure details.

#
Engine::eval

fn Engine::eval(self : Engine, source : String) -> Unit raise EngineError

#
Engine::eval_bounded

fn Engine::eval_bounded(self : Engine, source : String, policy :
ExecutionPolicy
, source_id? : String) -> Result[Unit, EngineDiagnostic]

Evaluate source under one explicitly supplied, operation-scoped policy. Parsing remains outside the control carrier; all existing unbounded Engine entry points retain their current behavior and signatures.

#
Engine::eval_diagnostic

fn Engine::eval_diagnostic(self : Engine, source : String, source_id? : String) -> Result[Unit, EngineDiagnostic]

Evaluate source while returning operation-aware failure details atomically.

#
Engine::has_pending_microtasks

fn Engine::has_pending_microtasks(self : Engine) -> Bool

#
Engine::has_pending_timers

fn Engine::has_pending_timers(self : Engine) -> Bool

#
Engine::inject_json

fn Engine::inject_json(self : Engine, name : String, value : Json) -> Result[Unit, EngineDiagnostic]

Copy host-owned JSON into this Engine as an immutable global binding and matching immutable own property of globalThis.

#
Engine::run_microtask_checkpoint

fn Engine::run_microtask_checkpoint(self : Engine) -> Bool raise EngineError

#
Engine::run_microtask_checkpoint_bounded

fn Engine::run_microtask_checkpoint_bounded(self : Engine, policy :
ExecutionPolicy
) -> Result[Bool, EngineDiagnostic]

Run one microtask checkpoint under a fresh operation-scoped execution policy. Empty-queue detection is outside the execution-step budget.

#
Engine::run_microtask_checkpoint_diagnostic

fn Engine::run_microtask_checkpoint_diagnostic(self : Engine) -> Result[Bool, EngineDiagnostic]

Run a microtask checkpoint while returning operation-aware failure details.

#
Engine::run_timer_checkpoint

fn Engine::run_timer_checkpoint(self : Engine) -> Unit raise EngineError

#
Engine::run_timer_checkpoint_bounded

fn Engine::run_timer_checkpoint_bounded(self : Engine, policy :
ExecutionPolicy
) -> Result[Unit, EngineDiagnostic]

Run one timer checkpoint under a fresh operation-scoped execution policy. Queue dispatch, callbacks, and timer-following microtasks share this scope.

#
Engine::run_timer_checkpoint_diagnostic

fn Engine::run_timer_checkpoint_diagnostic(self : Engine) -> Result[Unit, EngineDiagnostic]

Run a timer checkpoint while returning operation-aware failure details.

#
Engine::take_output

fn Engine::take_output(self : Engine) -> Array[String]

#
EngineDiagnostic

pub struct EngineDiagnostic {
failure_kind_code_ : String
message_ : String
operation_code_ : String
phase_code_ : String
source_identity_ : String?
source_location_ : SourceLocation?
engine_integrity_ : EngineIntegrity
retained_effects_ : RetainedEffects
pending_jobs_ : PendingJobs
}

Portable, operation-aware details for a failed stable-facade operation.

#
EngineDiagnostic::engine_integrity

fn EngineDiagnostic::engine_integrity(self : EngineDiagnostic) -> EngineIntegrity

#
EngineDiagnostic::failure_kind_code

fn EngineDiagnostic::failure_kind_code(self : EngineDiagnostic) -> String

#
EngineDiagnostic::message

fn EngineDiagnostic::message(self : EngineDiagnostic) -> String

#
EngineDiagnostic::operation_code

fn EngineDiagnostic::operation_code(self : EngineDiagnostic) -> String

#
EngineDiagnostic::pending_jobs

fn EngineDiagnostic::pending_jobs(self : EngineDiagnostic) -> PendingJobs

#
EngineDiagnostic::phase_code

fn EngineDiagnostic::phase_code(self : EngineDiagnostic) -> String

#
EngineDiagnostic::retained_effects

fn EngineDiagnostic::retained_effects(self : EngineDiagnostic) -> RetainedEffects

#
EngineDiagnostic::source_identity

fn EngineDiagnostic::source_identity(self : EngineDiagnostic) -> String?

#
EngineDiagnostic::source_location

fn EngineDiagnostic::source_location(self : EngineDiagnostic) -> SourceLocation?

#
EngineIntegrity

pub(all) enum EngineIntegrity {
Reusable
Discard
Unknown
NotApplicable
}

Whether a persistent Engine remains supported for later operations.

#
PendingJobs

pub(all) enum PendingJobs {
None
Present
Unknown
}

Whether either Engine job queue contains pending work at the failure boundary.

#
RetainedEffects

pub(all) enum RetainedEffects {
None
MayRemain
Unknown
}

Whether observable work from the failed operation may remain committed.

#
SourceLocation

pub struct SourceLocation {
start_ : SourcePosition
end_ : SourcePosition?
}

A half-open source range. The end position is absent when unavailable.

#
SourceLocation::end

#
SourceLocation::start

#
SourcePosition

pub struct SourcePosition {
line_ : Int
column_ : Int
offset_ : Int
}

A position within a source identified by an Engine diagnostic.

#
SourcePosition::column

fn SourcePosition::column(self : SourcePosition) -> Int

#
SourcePosition::line

fn SourcePosition::line(self : SourcePosition) -> Int

#
SourcePosition::offset

fn SourcePosition::offset(self : SourcePosition) -> Int

#
has_pending_microtasks

fn has_pending_microtasks(interp :
Interpreter
) -> Bool

Check if there are pending microtasks in the queue

#
has_pending_timers

Check if there are pending timers in the queue

#
run

fn run(source : String, annex_b? : Bool) -> (Array[String], String) raise

#
run_compiled

fn run_compiled(source : String, annex_b? : Bool) -> (Array[String], String) raise

Run JavaScript source through the opt-in closure-conversion prototype.

The normal run facade remains the default interpreter path. This entry point parses once, compiles the supported script subset to executable closures, then runs it with the same event-loop drain behavior as run.

#
run_diagnostic

fn run_diagnostic(source : String, source_id? : String, annex_b? : Bool) -> Result[(Array[String], String), EngineDiagnostic]

Run a one-shot script while returning operation-aware failure details.

#
run_microtask_checkpoint

fn run_microtask_checkpoint(interp :
Interpreter
) -> Bool raise

Run a single microtask checkpoint Returns true if there are more microtasks to process

#
run_module

fn run_module(source : String, annex_b? : Bool) -> (Array[String], Map[String,
Value
]) raise

Run a JavaScript module source and return its exports The module is executed in strict mode and its exports are collected

#
run_modules

fn run_modules(modules : Array[(String, String)], annex_b? : Bool) -> (Array[String], Map[String,
Value
]) raise

Run multiple modules with dependency resolution. The runtime graph runner pre-registers every module specifier before instantiation/evaluation, so callers do not need to order modules by dependency. Returns the exports of the last module.

#
run_timer_checkpoint

fn run_timer_checkpoint(interp :
Interpreter
) -> Unit raise

Run all pending timers with microtask draining between each

#
run_with_event_loop

Run JavaScript source with event loop support This version allows the host to control microtask and timer execution timing