///|
/// Opaque policy and interruption controls for the staged bounded-evaluation
/// facade. Runtime owns their invariants; the root package is the consumer API.
pub using @runtime {
type ExecutionPolicy,
type ExecutionPolicyError,
type InterruptionHandle,
}
///|
pub fn run(
source : String,
annex_b? : Bool = false,
) -> (Array[String], String) raise Error {
let interp = @interpreter.new_interpreter(annex_b~)
let prog = @parser.parse(source)
let result = interp.run(prog.stmts)
// Drain microtask queue after script execution
// This implements the event loop checkpoint per WHATWG spec
interp.run_microtasks()
// Process timer task queue with microtask checkpoints between each
interp.run_timers()
(interp.host.output, result.to_string())
}
///|
/// 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`.
pub fn run_compiled(
source : String,
annex_b? : Bool = false,
) -> (Array[String], String) raise Error {
let interp = @interpreter.new_interpreter(annex_b~)
let prog = @parser.parse(source)
let compiled = @compiler.compile_script_to_closure(prog.stmts)
let result = compiled(interp)
interp.run_microtasks()
interp.run_timers()
(interp.host.output, result.to_string())
}
///|
/// Run JavaScript source with event loop support
/// This version allows the host to control microtask and timer execution timing
pub fn run_with_event_loop(
source : String,
annex_b? : Bool = false,
) -> (@interpreter.Interpreter, @interpreter.Value) raise Error {
let interp = @interpreter.new_interpreter(annex_b~)
let prog = @parser.parse(source)
let result = interp.run(prog.stmts)
// Return interpreter to allow host to manage event loop
(interp, result)
}
///|
/// Check if there are pending microtasks in the queue
pub fn has_pending_microtasks(interp : @interpreter.Interpreter) -> Bool {
interp.host.microtask_queue.length() > 0
}
///|
/// Check if there are pending timers in the queue
pub fn has_pending_timers(interp : @interpreter.Interpreter) -> Bool {
interp.host.timer_queue.length() > 0
}
///|
/// Run a single microtask checkpoint
/// Returns true if there are more microtasks to process
pub fn run_microtask_checkpoint(
interp : @interpreter.Interpreter,
) -> Bool raise Error {
interp.run_microtasks()
interp.host.microtask_queue.length() > 0
}
///|
/// Run all pending timers with microtask draining between each
pub fn run_timer_checkpoint(
interp : @interpreter.Interpreter,
) -> Unit raise Error {
interp.run_timers()
}
///|
/// Stable errors raised by the stateful Engine facade.
pub(all) suberror EngineError {
ParseError(String)
JavaScriptException(String)
MissingGlobal(String)
NotCallable(String)
JsonConversionError(String)
InternalError(String)
} derive(Debug)
///|
pub extend EngineError with @debug.Debug::{to_repr}
///|
pub impl Show for EngineError with fn output(self, logger) {
let (name, message) = match self {
ParseError(message) => ("ParseError", message)
JavaScriptException(message) => ("JavaScriptException", message)
MissingGlobal(message) => ("MissingGlobal", message)
NotCallable(message) => ("NotCallable", message)
JsonConversionError(message) => ("JsonConversionError", message)
InternalError(message) => ("InternalError", message)
}
logger.write_string(name)
logger.write_string(": ")
logger.write_string(message)
}
///|
pub extend EngineError with Show::{output, to_string}
///|
/// A persistent JavaScript realm for repeated evaluation and JSON calls.
pub struct Engine {
interp : @interpreter.Interpreter
}
///|
pub fn Engine::Engine(annex_b? : Bool = false) -> Engine {
{ interp: @interpreter.new_interpreter(annex_b~), }
}
///|
fn Engine::with_console_output_sink(
annex_b : Bool,
console_output_sink : (@runtime.ConsoleOutputKind, String) -> Unit raise Error,
) -> Engine {
{
interp: @interpreter.new_interpreter(
annex_b~,
console_output_sink=Some(console_output_sink),
),
}
}
///|
fn classify_engine_runtime_error(err : Error) -> EngineError {
match @errors.name_message_if_js_error(err) {
Some(("InternalError", message)) => InternalError(message)
Some((name, message)) => JavaScriptException(name + ": " + message)
None => InternalError(err.to_string())
}
}
///|
pub fn Engine::eval(self : Engine, source : String) -> Unit raise EngineError {
let candidate_mode = @engine_candidate_mode.candidate_mode_enabled()
let program = @parser.parse(source) catch {
err => raise ParseError(err.to_string())
}
if candidate_mode {
ignore(@compiler.run_candidate_program(self.interp, program.stmts)) catch {
@runtime.JsException(value) =>
raise JavaScriptException(value.to_string())
err => raise classify_engine_runtime_error(err)
}
} else {
ignore(self.interp.run(program.stmts)) catch {
@runtime.JsException(value) =>
raise JavaScriptException(value.to_string())
err => raise classify_engine_runtime_error(err)
}
}
}
///|
/// Evaluate a script loaded by a host function while preserving JavaScript
/// exceptions for the currently-active execution. Parse failures become
/// JavaScript SyntaxErrors at this boundary.
fn Engine::eval_shell_loaded(self : Engine, source : String) -> Unit raise {
let program = @parser.parse(source) catch {
err => raise @errors.SyntaxError(message=err.to_string())
}
if @engine_candidate_mode.candidate_mode_enabled() {
ignore(@compiler.run_candidate_program(self.interp, program.stmts))
} else {
ignore(self.interp.run(program.stmts))
}
}
///|
fn Engine::get_global_export(
self : Engine,
name : String,
) -> @runtime.Value raise EngineError {
let has_binding = self.interp.global.has(name) catch {
err => raise classify_engine_runtime_error(err)
}
if has_binding {
return self.interp.global.get(name) catch {
err => raise classify_engine_runtime_error(err)
}
}
let own_property = self.interp.get_own_property(
self.interp.global_this,
@runtime.String_(name),
) catch {
@runtime.JsException(value) => raise JavaScriptException(value.to_string())
err => raise classify_engine_runtime_error(err)
}
guard own_property is Some(_) else { raise MissingGlobal(name) }
self.interp.get_property(self.interp.global_this, name, @token.Loc::default()) catch {
@runtime.JsException(value) => raise JavaScriptException(value.to_string())
err => raise classify_engine_runtime_error(err)
}
}
///|
pub fn Engine::call_json(
self : Engine,
name : String,
args : Array[Json],
) -> Json raise EngineError {
let callee = self.get_global_export(name)
guard @runtime.is_callable(callee) else { raise NotCallable(name) }
let runtime_args : Array[@runtime.Value] = []
for arg in args {
let converted = @runtime.json_to_realm_value(self.interp.realm_state, arg) catch {
@runtime.JsonBridgeFailure(message) => raise JsonConversionError(message)
}
runtime_args.push(converted)
}
let result = self.interp.call_value(
callee,
@runtime.Undefined,
runtime_args,
@token.Loc::default(),
) catch {
@runtime.JsException(value) => raise JavaScriptException(value.to_string())
err => raise classify_engine_runtime_error(err)
}
@runtime.realm_value_to_json(self.interp.realm_state, result) catch {
@runtime.JsonBridgeFailure(message) => raise JsonConversionError(message)
}
}
///|
pub fn Engine::take_output(self : Engine) -> Array[String] {
let output = self.interp.host.output.copy()
self.interp.host.output.clear()
output
}
///|
pub fn Engine::has_pending_microtasks(self : Engine) -> Bool {
self.interp.host.microtask_queue.length() > 0
}
///|
pub fn Engine::has_pending_timers(self : Engine) -> Bool {
self.interp.host.timer_queue.length() > 0
}
///|
pub fn Engine::run_microtask_checkpoint(
self : Engine,
) -> Bool raise EngineError {
self.interp.run_microtasks() catch {
@runtime.JsException(value) => raise JavaScriptException(value.to_string())
err => raise classify_engine_runtime_error(err)
}
self.has_pending_microtasks()
}
///|
pub fn Engine::run_timer_checkpoint(self : Engine) -> Unit raise EngineError {
self.interp.run_timers() catch {
@runtime.JsException(value) => raise JavaScriptException(value.to_string())
err => raise classify_engine_runtime_error(err)
}
}
///|
/// Source returned by a shell host after resolving a requested script name.
/// `identity` is used as the referrer for nested `load()` calls.
pub struct LoadedScript {
identity : String
source : String
}
///|
pub fn LoadedScript::LoadedScript(
identity~ : String,
source~ : String,
) -> LoadedScript {
{ identity, source, }
}
///|
fn Engine::install_shell_global(
self : Engine,
name : String,
value : @runtime.Value,
) -> Unit {
self.interp.global.def_builtin(name, value)
self.install_host_global(name, value)
}
///|
fn Engine::make_shell_reader(
self : Engine,
load_source : (String, String?) -> LoadedScript raise,
read_bytes : ((String, String?) -> Bytes raise)?,
current_referrer : () -> String?,
) -> @runtime.Value {
@runtime.make_native_func(
name="read",
length=1,
realm_state=Some(self.interp.realm_state),
fn(args) raise {
guard args.length() > 0 else {
raise @errors.TypeError(message="read() requires a file name")
}
let request = self.interp.to_js_string(args[0])
let binary = args.length() > 1 &&
self.interp.to_js_string(args[1]) == "binary"
if binary {
let reader = match read_bytes {
Some(reader) => reader
None =>
raise @errors.InternalError(
message="binary file reads are not available in this host",
)
}
let bytes = reader(request, current_referrer()) catch {
err =>
raise @errors.InternalError(
message="cannot read '\{request}': \{err}",
)
}
let prototype = self.interp.global.get("[[ArrayBufferPrototype]]")
let buffer = @stdlib.make_arraybuffer_object(
self.interp.realm_state,
bytes.length(),
prototype,
)
let buffer_id = match @stdlib.get_buffer_id(buffer) {
Some(buffer_id) => buffer_id
None =>
raise @errors.InternalError(
message="failed to allocate ArrayBuffer",
)
}
let destination = match
@stdlib.get_arraybuffer_bytes(self.interp.realm_state, buffer_id) {
Some(destination) => destination
None =>
raise @errors.InternalError(message="failed to access ArrayBuffer")
}
for i in 0..
raise @errors.InternalError(
message="cannot read '\{request}': \{err}",
)
}
@runtime.String_(loaded.source)
},
)
}
///|
/// A persistent JavaScript shell realm. The host owns filesystem policy and
/// supplies script loading as a capability; the shell owns evaluation order,
/// the load stack, and JavaScript-visible host functions.
pub struct Shell {
priv engine : Engine
priv load_source : (String, String?) -> LoadedScript raise
priv read_bytes : ((String, String?) -> Bytes raise)?
priv load_stack : Array[String]
}
///|
pub fn Shell::Shell(
load_source~ : (String, String?) -> LoadedScript raise,
read_bytes? : ((String, String?) -> Bytes raise)? = None,
now_millis? : (() -> Double)? = None,
annex_b? : Bool = false,
) -> Shell {
let shell = {
engine: Engine(annex_b~),
load_source,
read_bytes,
load_stack: [],
}
let load = @runtime.make_native_func(
name="load",
length=1,
realm_state=Some(shell.engine.interp.realm_state),
fn(args) raise {
guard args.length() > 0 else {
raise @errors.TypeError(message="load() requires a script name")
}
let request = shell.engine.interp.to_js_string(args[0])
shell.load_runtime(request)
@runtime.Undefined
},
)
shell.engine.install_shell_global("load", load)
let read = shell.engine.make_shell_reader(
shell.load_source,
shell.read_bytes,
() => shell.load_referrer(),
)
shell.engine.install_shell_global("read", read)
shell.engine.install_shell_global("readFile", read)
let run_string = @runtime.make_native_func(
name="runString",
length=1,
realm_state=Some(shell.engine.interp.realm_state),
fn(args) raise {
let source = if args.length() == 0 {
""
} else {
shell.engine.interp.to_js_string(args[0])
}
let child = Engine::with_console_output_sink(annex_b, fn(
kind,
text,
) raise {
shell.engine.interp.host.emit_console_output(kind, text)
})
let child_load_stack : Array[String] = []
let child_referrer = () => {
if child_load_stack.length() == 0 {
shell.load_referrer()
} else {
Some(child_load_stack[child_load_stack.length() - 1])
}
}
let child_read = child.make_shell_reader(
shell.load_source,
shell.read_bytes,
child_referrer,
)
let load_string = @runtime.make_native_func(
name="loadString",
length=1,
realm_state=Some(child.interp.realm_state),
fn(load_args) raise {
guard load_args.length() > 0 else {
raise @errors.TypeError(message="loadString() requires source text")
}
let loaded_source = child.interp.to_js_string(load_args[0])
child.eval_shell_loaded(loaded_source)
@runtime.Undefined
},
)
let child_load = @runtime.make_native_func(
name="load",
length=1,
realm_state=Some(child.interp.realm_state),
fn(load_args) raise {
guard load_args.length() > 0 else {
raise @errors.TypeError(message="load() requires a script name")
}
let request = child.interp.to_js_string(load_args[0])
let referrer = child_referrer()
let loaded = (shell.load_source)(request, referrer) catch {
err =>
raise @errors.InternalError(
message="cannot load '\{request}': \{err}",
)
}
child_load_stack.push(loaded.identity)
let evaluated : Result[Unit, Error] = Ok(
child.eval_shell_loaded(loaded.source),
) catch {
err => Err(err)
}
ignore(child_load_stack.pop())
match evaluated {
Ok(_) => @runtime.Undefined
Err(err) => raise err
}
},
)
child.install_shell_global("loadString", load_string)
child.install_shell_global("load", child_load)
child.install_shell_global("read", child_read)
child.install_shell_global("readFile", child_read)
child.eval_shell_loaded(source)
child.interp.global_this
},
)
shell.engine.install_shell_global("runString", run_string)
match now_millis {
Some(clock) => {
let now = @runtime.make_native_func(
name="now",
realm_state=Some(shell.engine.interp.realm_state),
fn(_args) { @runtime.Number(clock()) },
)
let mark = @runtime.make_native_func(
name="mark",
length=1,
realm_state=Some(shell.engine.interp.realm_state),
fn(_args) { @runtime.Undefined },
)
let measure = @runtime.make_native_func(
name="measure",
length=1,
realm_state=Some(shell.engine.interp.realm_state),
fn(_args) { @runtime.Undefined },
)
let performance = @runtime.make_host_object(
name="Performance",
proto=@runtime.get_obj_proto(
realm_state=Some(shell.engine.interp.realm_state),
),
methods=Map::from_array([
("now", now),
("mark", mark),
("measure", measure),
]),
)
shell.engine.install_shell_global("performance", performance)
}
None => ()
}
shell
}
///|
fn Shell::load_referrer(self : Shell) -> String? {
if self.load_stack.length() == 0 {
None
} else {
Some(self.load_stack[self.load_stack.length() - 1])
}
}
///|
fn Shell::load(self : Shell, request : String) -> Unit raise EngineError {
let referrer = self.load_referrer()
let loaded = (self.load_source)(request, referrer) catch {
err => raise InternalError("cannot load '\{request}': \{err}")
}
self.load_stack.push(loaded.identity)
let evaluated : Result[Unit, EngineError] = Ok(
self.engine.eval(loaded.source),
) catch {
err => Err(err)
}
ignore(self.load_stack.pop())
match evaluated {
Ok(_) => ()
Err(err) => raise err
}
}
///|
/// `load()` runs inside an active JavaScript call. Keep runtime exceptions in
/// their native form so the outer evaluation reports the original JS error.
fn Shell::load_runtime(self : Shell, request : String) -> Unit raise {
let referrer = self.load_referrer()
let loaded = (self.load_source)(request, referrer) catch {
err =>
raise @errors.InternalError(message="cannot load '\{request}': \{err}")
}
self.load_stack.push(loaded.identity)
let evaluated : Result[Unit, Error] = Ok(
self.engine.eval_shell_loaded(loaded.source),
) catch {
err => Err(err)
}
ignore(self.load_stack.pop())
match evaluated {
Ok(_) => ()
Err(err) => raise err
}
}
///|
/// Resolve and execute a script as the shell entry point.
pub fn Shell::run_file(self : Shell, path : String) -> Unit raise EngineError {
self.load(path)
}
///|
/// Evaluate source in the shell's persistent realm without changing the load
/// referrer. This is the implementation of the command-line `-e` mode.
pub fn Shell::eval(self : Shell, source : String) -> Unit raise EngineError {
self.engine.eval(source)
}
///|
/// Evaluate a pre-resolved module graph in this Shell's Realm. Host globals,
/// output, arguments, and pending jobs are shared with script evaluation.
pub fn Shell::run_modules(
self : Shell,
modules : Array[(String, String)],
) -> Unit raise EngineError {
ignore(self.engine.interp.run_modules(modules)) catch {
@runtime.JsException(value) => raise JavaScriptException(value.to_string())
err => raise classify_engine_runtime_error(err)
}
}
///|
/// Install command-line arguments without parsing an extra JavaScript setup
/// program. `arguments` follows d8/JSC and excludes the script name;
/// `scriptArgs` follows QuickJS and includes it as element zero.
pub fn Shell::set_arguments(
self : Shell,
script_name : String,
args : Array[String],
) -> Unit raise EngineError {
let arguments_json = Json::array(args.map(arg => Json::string(arg)))
let script_args : Array[String] = [script_name]
for arg in args {
script_args.push(arg)
}
let script_args_json = Json::array(script_args.map(arg => Json::string(arg)))
let arguments = @runtime.json_to_realm_value(
self.engine.interp.realm_state,
arguments_json,
) catch {
@runtime.JsonBridgeFailure(message) => raise JsonConversionError(message)
}
let script_arguments = @runtime.json_to_realm_value(
self.engine.interp.realm_state,
script_args_json,
) catch {
@runtime.JsonBridgeFailure(message) => raise JsonConversionError(message)
}
self.engine.interp.global.def_builtin("arguments", arguments)
self.engine.interp.global.def_builtin("scriptArgs", script_arguments)
ignore(
self.engine.interp.set_property(
self.engine.interp.global_this,
"arguments",
arguments,
@token.Loc::default(),
),
) catch {
err => raise classify_engine_runtime_error(err)
}
ignore(
self.engine.interp.set_property(
self.engine.interp.global_this,
"scriptArgs",
script_arguments,
@token.Loc::default(),
),
) catch {
err => raise classify_engine_runtime_error(err)
}
}
///|
pub fn Shell::take_output(self : Shell) -> Array[String] {
self.engine.take_output()
}
///|
pub fn Shell::drain_jobs(self : Shell) -> Unit raise EngineError {
while self.engine.has_pending_microtasks() {
ignore(self.engine.run_microtask_checkpoint())
}
self.engine.run_timer_checkpoint()
}
///|
/// Run a JavaScript module source and return its exports
/// The module is executed in strict mode and its exports are collected
pub fn run_module(
source : String,
annex_b? : Bool = false,
) -> (Array[String], Map[String, @interpreter.Value]) raise Error {
let interp = @interpreter.new_interpreter(annex_b~)
let prog = @parser.parse(source)
let exports = interp.run_module(prog.stmts)
interp.run_microtasks()
interp.run_timers()
(interp.host.output, exports)
}
///|
/// 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.
pub fn run_modules(
modules : Array[(String, String)],
annex_b? : Bool = false,
) -> (Array[String], Map[String, @interpreter.Value]) raise Error {
let interp = @interpreter.new_interpreter(annex_b~)
let last_exports = interp.run_modules(modules)
interp.run_microtasks()
interp.run_timers()
(interp.host.output, last_exports)
}