mirror of
https://github.com/storytold/vectorcraft.git
synced 2026-11-12 17:06:53 +00:00
480 lines
20 KiB
Rust
480 lines
20 KiB
Rust
//! The embedding API: a [`Realm`] holds a global object and its built-ins, runs scripts in them,
|
|
//! and enforces [`Limits`].
|
|
|
|
use std::sync::Arc;
|
|
use std::sync::atomic::{AtomicBool, Ordering};
|
|
|
|
use crate::bytecode::CodeKind;
|
|
use crate::heap::{Func, HIDDEN, Object, Prop, key};
|
|
use crate::interp::{Abrupt, CompileError, Interp};
|
|
use crate::value::Value;
|
|
use crate::{Options, SyntaxError};
|
|
|
|
/// The limits a run is held to. Reaching one ends the run with [`RunErrorKind::Limit`]; scripts
|
|
/// cannot catch it, and their `finally` blocks do not run.
|
|
#[derive(Clone, Debug, PartialEq, Eq)]
|
|
pub struct Limits {
|
|
/// Instructions a run may execute.
|
|
pub fuel: u64,
|
|
/// Estimated bytes the heap (objects, environments and the strings they hold) may reach
|
|
/// after a collection. A string longer than a quarter of this many code units cannot be
|
|
/// made.
|
|
pub heap_bytes: usize,
|
|
/// Call frames that may be active at once.
|
|
pub frames: usize,
|
|
/// How deeply the interpreter may run again inside an operation (getters and setters,
|
|
/// conversions that call `valueOf` or `toString`, indirect `eval`).
|
|
pub reentry: usize,
|
|
}
|
|
|
|
#[cfg(not(target_arch = "wasm32"))]
|
|
impl Default for Limits {
|
|
fn default() -> Self {
|
|
Self { fuel: 1_000_000_000, heap_bytes: 512 << 20, frames: 10_000, reentry: 32 }
|
|
}
|
|
}
|
|
|
|
/// A wasm build has less memory and a smaller stack.
|
|
#[cfg(target_arch = "wasm32")]
|
|
impl Default for Limits {
|
|
fn default() -> Self {
|
|
Self { fuel: 250_000_000, heap_bytes: 128 << 20, frames: 2_000, reentry: 16 }
|
|
}
|
|
}
|
|
|
|
/// Which limit a run reached.
|
|
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
|
pub enum LimitKind {
|
|
Fuel,
|
|
Heap,
|
|
Frames,
|
|
Reentry,
|
|
/// The host set the interrupt flag.
|
|
Interrupted,
|
|
/// A getter, a callback of a built-in or another nested run spent [`MAX_UNBROKEN_FUEL`]
|
|
/// without returning to the point where a resumable run can pause.
|
|
Unbroken,
|
|
/// A host function ended the run ([`crate::HostError::Abort`]).
|
|
Host,
|
|
}
|
|
|
|
/// The tag [`Realm::error_source`] gives for a source that is neither the realm's script nor an
|
|
/// evaluator's: code that `eval` or `Function` compiled, or an error with no place.
|
|
pub const UNKNOWN_SOURCE: u32 = u32::MAX;
|
|
|
|
/// The fuel [`Realm::advance`] lets a slice spend, past the slice it was asked for, in a nested run
|
|
/// it cannot pause.
|
|
pub const MAX_UNBROKEN_FUEL: u64 = 200_000_000;
|
|
|
|
/// How far a resumable run got.
|
|
#[derive(Clone, Debug, PartialEq)]
|
|
pub enum Progress {
|
|
/// The slice ended; call [`Realm::advance`] again.
|
|
Paused,
|
|
/// The script ended, with its completion value.
|
|
Done(JsValue),
|
|
}
|
|
|
|
/// A flag the host sets, from any thread, to stop the running script at its next instruction.
|
|
/// It stays set until [`Interrupt::clear`].
|
|
#[derive(Clone, Debug, Default)]
|
|
pub struct Interrupt(Arc<AtomicBool>);
|
|
|
|
impl Interrupt {
|
|
pub fn new() -> Self {
|
|
Self::default()
|
|
}
|
|
|
|
pub fn set(&self) {
|
|
self.0.store(true, Ordering::Relaxed);
|
|
}
|
|
|
|
pub fn clear(&self) {
|
|
self.0.store(false, Ordering::Relaxed);
|
|
}
|
|
|
|
pub fn is_set(&self) -> bool {
|
|
self.0.load(Ordering::Relaxed)
|
|
}
|
|
}
|
|
|
|
/// A value passed between a script and the host. An object is given by its class name.
|
|
#[derive(Clone, Debug, PartialEq)]
|
|
pub enum JsValue {
|
|
Undefined,
|
|
Null,
|
|
Bool(bool),
|
|
Number(f64),
|
|
String(String),
|
|
Object(String),
|
|
}
|
|
|
|
#[derive(Clone, Debug, PartialEq, Eq)]
|
|
pub enum RunErrorKind {
|
|
/// The source does not parse.
|
|
Syntax,
|
|
/// The script threw an exception it did not catch.
|
|
Thrown,
|
|
Limit(LimitKind),
|
|
/// The interpreter reached a state it never makes.
|
|
Internal,
|
|
}
|
|
|
|
/// Why a run did not complete. `line` and `column` count from 1 (the column in UTF-16 code units)
|
|
/// and give where the error was raised; they are 0 when that is not known.
|
|
#[derive(Clone, Debug, PartialEq, Eq)]
|
|
pub struct RunError {
|
|
pub kind: RunErrorKind,
|
|
pub message: String,
|
|
pub line: u32,
|
|
pub column: u32,
|
|
}
|
|
|
|
impl std::fmt::Display for RunError {
|
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
|
write!(f, "{} (line {}, column {})", self.message, self.line, self.column)
|
|
}
|
|
}
|
|
|
|
impl std::error::Error for RunError {}
|
|
|
|
impl From<SyntaxError> for RunError {
|
|
fn from(e: SyntaxError) -> Self {
|
|
RunError { kind: RunErrorKind::Syntax, message: e.to_string(), line: e.line, column: e.column }
|
|
}
|
|
}
|
|
|
|
/// A global environment with the built-ins, in which scripts run one after another.
|
|
pub struct Realm {
|
|
interp: Interp,
|
|
unbroken_fuel: u64,
|
|
}
|
|
|
|
impl Default for Realm {
|
|
fn default() -> Self {
|
|
Self::new()
|
|
}
|
|
}
|
|
|
|
impl Realm {
|
|
pub fn new() -> Self {
|
|
Self::with_options(Options::default(), Limits::default())
|
|
}
|
|
|
|
/// A realm whose scripts (and the source given to `eval` and `Function`) are read with
|
|
/// `options`; [`Options::max_source_units`] caps the source parsed in one run, all of it
|
|
/// counted together.
|
|
pub fn with_options(options: Options, limits: Limits) -> Self {
|
|
Self { interp: Interp::new(options, limits), unbroken_fuel: MAX_UNBROKEN_FUEL }
|
|
}
|
|
|
|
/// Sets the fuel [`Realm::advance`] lets a nested run spend past its slice (by default
|
|
/// [`MAX_UNBROKEN_FUEL`]).
|
|
pub fn set_unbroken_fuel(&mut self, fuel: u64) {
|
|
self.unbroken_fuel = fuel;
|
|
}
|
|
|
|
/// With `on`, the scripts run next (and the code they define and evaluate) throw a
|
|
/// `TypeError` when they assign to a read-only or getter-only property or add a property to
|
|
/// an object that is not extensible, as strict mode code does, without the rest of strict
|
|
/// mode. Off by default.
|
|
pub fn set_strict_writes(&mut self, on: bool) {
|
|
self.interp.strict_writes = on;
|
|
}
|
|
|
|
pub fn limits(&self) -> &Limits {
|
|
&self.interp.limits
|
|
}
|
|
|
|
/// Sets the limits for the next run. A heap that is already larger than the new
|
|
/// [`Limits::heap_bytes`] ends the next run with the heap limit unless a collection frees
|
|
/// enough of it.
|
|
pub fn set_limits(&mut self, limits: Limits) {
|
|
self.interp.limits = limits;
|
|
self.interp.reset_gc_threshold();
|
|
}
|
|
|
|
/// The realm's interrupt flag. Setting it stops a running script, or the next one to start.
|
|
pub fn interrupt(&self) -> Interrupt {
|
|
Interrupt(self.interp.interrupt.clone())
|
|
}
|
|
|
|
/// Defines a global function that calls `f` with its arguments. An `Err` from `f` is thrown
|
|
/// to the script as an `Error` with that message.
|
|
pub fn define_function(&mut self, name: &str, f: impl Fn(&[JsValue]) -> Result<JsValue, String> + 'static) {
|
|
let interp = &mut self.interp;
|
|
let proto = interp.intrinsics.function_proto;
|
|
let func = Func::Host { f: std::rc::Rc::new(f), name: name.into() };
|
|
let o = interp.heap.alloc_object(Object::new(crate::heap::Class::Function(func), Some(proto)));
|
|
let global = interp.global;
|
|
let _ = interp.define(global, key(name), Prop::Data { value: Value::Object(o), flags: HIDDEN });
|
|
}
|
|
|
|
/// Runs `source` as a script in the global environment and returns its completion value (the
|
|
/// value of the last expression statement that ran). A realm whose built-ins could not be
|
|
/// installed runs nothing and gives an internal error.
|
|
pub fn run(&mut self, source: &str) -> Result<JsValue, RunError> {
|
|
let code = self.compile(source)?;
|
|
let result = self.interp.run_global(code, false);
|
|
self.outcome(result)
|
|
}
|
|
|
|
/// Compiles `source` and readies the realm for a run.
|
|
fn compile(&mut self, source: &str) -> Result<std::rc::Rc<crate::bytecode::Code>, RunError> {
|
|
let interp = &mut self.interp;
|
|
if let Some(message) = &interp.install_error {
|
|
return Err(RunError { kind: RunErrorKind::Internal, message: message.clone(), line: 0, column: 0 });
|
|
}
|
|
interp.fuel = interp.limits.fuel;
|
|
interp.source_units = 0;
|
|
interp.throw_site = None;
|
|
interp.last_thrown = Value::Undefined;
|
|
interp.last_error = Value::Null;
|
|
interp.host_abort = None;
|
|
interp.waiting = false;
|
|
interp.error_tag = UNKNOWN_SOURCE;
|
|
interp.thrown_mark = None;
|
|
interp.finally_marks.clear();
|
|
interp.finally_sites.clear();
|
|
interp.error_mark = None;
|
|
let strict = interp.options.strict;
|
|
interp.source_tag = 0;
|
|
let code = interp.encode_source(source).and_then(|units| interp.compile_source(&units, strict, CodeKind::Global));
|
|
interp.source_tag = UNKNOWN_SOURCE;
|
|
match code {
|
|
Ok(code) => Ok(code),
|
|
Err(CompileError::Syntax(e)) => {
|
|
interp.error_tag = 0;
|
|
Err(e.into())
|
|
}
|
|
Err(CompileError::Internal(m)) => Err(RunError { kind: RunErrorKind::Internal, message: m, line: 0, column: 0 }),
|
|
}
|
|
}
|
|
|
|
/// The end of a run: its completion value or error, with the interpreter's stacks emptied.
|
|
fn outcome(&mut self, result: Result<Value, Abrupt>) -> Result<JsValue, RunError> {
|
|
let interp = &mut self.interp;
|
|
let site = interp.throw_site.take();
|
|
interp.error_tag = site.as_ref().map_or(UNKNOWN_SOURCE, |(code, _)| code.source.tag);
|
|
let (line, column) = site.map_or((0, 0), |(code, pc)| code.line_column(pc));
|
|
interp.error_mark = interp.thrown_mark.take().filter(|_| matches!(result, Err(Abrupt::Throw(_) | Abrupt::Limit(LimitKind::Host))));
|
|
interp.finally_marks.clear();
|
|
interp.finally_sites.clear();
|
|
interp.stack.clear();
|
|
interp.frames.clear();
|
|
interp.temp_roots.clear();
|
|
interp.join_stack.clear();
|
|
interp.waiting = false;
|
|
interp.passing = Value::Undefined;
|
|
match result {
|
|
Ok(v) => Ok(interp.to_js_value(&v)),
|
|
Err(Abrupt::Throw(v)) => {
|
|
let message = interp.describe(&v);
|
|
interp.last_thrown = v;
|
|
Err(RunError { kind: RunErrorKind::Thrown, message, line, column })
|
|
}
|
|
Err(Abrupt::Limit(LimitKind::Host)) => {
|
|
let message = interp.host_abort.take().unwrap_or_else(|| format!("limit reached: {:?}", LimitKind::Host));
|
|
Err(RunError { kind: RunErrorKind::Limit(LimitKind::Host), message, line, column })
|
|
}
|
|
Err(Abrupt::Limit(kind)) => Err(RunError { kind: RunErrorKind::Limit(kind), message: format!("limit reached: {kind:?}"), line, column }),
|
|
Err(Abrupt::Internal(message)) => Err(RunError { kind: RunErrorKind::Internal, message, line, column }),
|
|
}
|
|
}
|
|
|
|
/// Begins a run that [`Realm::advance`] carries on: compiles `source` and enters it.
|
|
pub fn start(&mut self, source: &str) -> Result<(), RunError> {
|
|
let code = self.compile(source)?;
|
|
match self.interp.enter_global(code, false) {
|
|
Ok(()) => Ok(()),
|
|
Err(e) => self.outcome(Err(e)).map(drop),
|
|
}
|
|
}
|
|
|
|
/// Runs the script begun with [`Realm::start`] for about `slice` units of fuel (at least one
|
|
/// instruction), and pauses where it can resume: between two instructions of the script's own
|
|
/// code. A getter, a built-in's callback or another nested run has native frames under it, so
|
|
/// it runs on to its end, for at most [`Realm::set_unbroken_fuel`] more. Once this has returned
|
|
/// [`Progress::Done`] or an error, the run is over. While a host function's call waits for its
|
|
/// result ([`Realm::waits`]), the run does not advance: this gives an internal error.
|
|
pub fn advance(&mut self, slice: u64) -> Result<Progress, RunError> {
|
|
let interp = &mut self.interp;
|
|
if interp.frames.is_empty() {
|
|
return Err(RunError { kind: RunErrorKind::Internal, message: "no run to advance".into(), line: 0, column: 0 });
|
|
}
|
|
if interp.waiting {
|
|
return Err(RunError { kind: RunErrorKind::Internal, message: "the run waits for a host function's result".into(), line: 0, column: 0 });
|
|
}
|
|
let total = interp.fuel;
|
|
let cap = slice.saturating_add(self.unbroken_fuel).min(total);
|
|
interp.fuel = cap;
|
|
interp.in_slice = true;
|
|
let r = interp.execute_slice(cap - slice.min(cap));
|
|
interp.in_slice = false;
|
|
let used = cap - interp.fuel;
|
|
interp.fuel = total - used;
|
|
match r {
|
|
Ok(None) => Ok(Progress::Paused),
|
|
Ok(Some(v)) => self.outcome(Ok(v)).map(Progress::Done),
|
|
// The run's own fuel is not used up, so it was the allowance of this slice.
|
|
Err(Abrupt::Limit(LimitKind::Fuel)) if total > cap => self.outcome(Err(Abrupt::Limit(LimitKind::Unbroken))).map(Progress::Done),
|
|
Err(e) => self.outcome(Err(e)).map(Progress::Done),
|
|
}
|
|
}
|
|
|
|
/// Is a run begun with [`Realm::start`] still going?
|
|
pub fn is_running(&self) -> bool {
|
|
!self.interp.frames.is_empty()
|
|
}
|
|
|
|
/// The flag a host function sets to end the current slice at the next point it can pause.
|
|
pub fn pause_flag(&self) -> std::rc::Rc<std::cell::Cell<bool>> {
|
|
self.interp.pause.clone()
|
|
}
|
|
|
|
/// The flag that is set while a host function runs where it may return
|
|
/// [`crate::HostError::Wait`]: called by the script's own code in a run [`Realm::advance`]
|
|
/// carries on, and not from a getter, a conversion or a built-in's callback.
|
|
pub fn wait_flag(&self) -> std::rc::Rc<std::cell::Cell<bool>> {
|
|
self.interp.can_wait.clone()
|
|
}
|
|
|
|
/// Does a host function's call wait for the result [`Realm::answer`] gives it?
|
|
pub fn waits(&self) -> bool {
|
|
self.interp.waiting
|
|
}
|
|
|
|
/// Gives the call that waits ([`crate::HostError::Wait`]) its result `d`; the next
|
|
/// [`Realm::advance`] goes on from there. A result past the heap limit ends the run with that
|
|
/// limit, and with no call waiting this gives an internal error.
|
|
pub fn answer(&mut self, d: crate::Data) -> Result<(), RunError> {
|
|
let interp = &mut self.interp;
|
|
if !interp.waiting {
|
|
return Err(RunError { kind: RunErrorKind::Internal, message: "no host function waits for a result".into(), line: 0, column: 0 });
|
|
}
|
|
interp.waiting = false;
|
|
// The value on top of the stack stood in for the result.
|
|
let r = interp.write_data(d, 0, false).and_then(|v| {
|
|
interp.stack.pop();
|
|
interp.push(v)
|
|
});
|
|
match r {
|
|
Ok(()) => Ok(()),
|
|
Err(e) => self.outcome(Err(e)).map(drop),
|
|
}
|
|
}
|
|
|
|
/// Has this realm stop when `interrupt` is set, in place of its own flag.
|
|
pub fn share_interrupt(&mut self, interrupt: &Interrupt) {
|
|
self.interp.interrupt = interrupt.0.clone();
|
|
}
|
|
|
|
/// Which source the last error's `line` and `column` are in: 0 for the script given to
|
|
/// [`Realm::run`] or [`Realm::start`], `n` for the `n`th source a
|
|
/// [`crate::HostItem::Evaluator`] gave in this realm, and [`UNKNOWN_SOURCE`] otherwise.
|
|
pub fn error_source(&self) -> u32 {
|
|
self.interp.error_tag
|
|
}
|
|
|
|
/// The mark of the host error ([`crate::HostError::Marked`]) the last run ended with, when no
|
|
/// `catch` clause took that error's exception on its way out; `None` otherwise.
|
|
pub fn error_mark(&self) -> Option<u32> {
|
|
self.interp.error_mark
|
|
}
|
|
|
|
/// Fuel the run has left.
|
|
pub fn fuel_left(&self) -> u64 {
|
|
self.interp.fuel
|
|
}
|
|
|
|
/// Sets how much a value passed to or from a host function may carry.
|
|
pub fn set_data_limits(&mut self, limits: crate::DataLimits) {
|
|
self.interp.data_limits = limits;
|
|
}
|
|
|
|
/// Adds the frozen object `name`, with `items`, to the global environment. Scripts cannot add
|
|
/// to it, change it or replace it. A heap too small for it ends with the error the realm's
|
|
/// next run would give.
|
|
pub fn define_host_object(&mut self, name: &str, items: Vec<(String, crate::HostItem)>) -> Result<(), RunError> {
|
|
match self.interp.install_host_object(name, items) {
|
|
Ok(()) => Ok(()),
|
|
Err(e) => self.outcome(Err(e)).map(drop),
|
|
}
|
|
}
|
|
|
|
/// Adds the global constructors `File` and `Folder`, whose file system work the host functions
|
|
/// in `items` do: `parse`, `relative`, `tempName`, `stat`, `open`, `close`, `read`, `readln`,
|
|
/// `readch`, `seek`, `tell`, `eof`, `encoding`, `setEncoding`, `list`, `resolve`, `execute`,
|
|
/// `encode`, `decode`, `encodingAvailable`, `current` and `locations`, with the values `system`
|
|
/// and `lineFeed` and the messages `text.notOpen`, `text.noWrites`, `text.noPicker` and
|
|
/// `text.badMode`. Each object keeps its path, its `error` text, its open file's id, its
|
|
/// encoding and its line feed in properties that are not enumerable.
|
|
pub fn define_file_classes(&mut self, items: Vec<(String, crate::HostItem)>) -> Result<(), RunError> {
|
|
match crate::file_classes::install(&mut self.interp, items) {
|
|
Ok(()) => Ok(()),
|
|
Err(e) => self.outcome(Err(e)).map(drop),
|
|
}
|
|
}
|
|
|
|
/// Adds the script extensions: the global `localize(object, ...values)`, which gives a
|
|
/// localization object's text for the locale of [`Realm::localization`], the global
|
|
/// `toSource(value)` and `Object.prototype.toSource()`, which write plain data as source text,
|
|
/// and the localized `Object.prototype.toString` while [`crate::Localization::enabled`].
|
|
pub fn add_script_extensions(&mut self) -> Result<(), RunError> {
|
|
match crate::builtins::extensions::install(&mut self.interp) {
|
|
Ok(()) => Ok(()),
|
|
Err(e) => self.outcome(Err(e)).map(drop),
|
|
}
|
|
}
|
|
|
|
/// Defines the global function `name`, whose arguments arrive converted as `args` says
|
|
/// ([`crate::HostItem::Converted`]). Unlike a host object, a script may replace or delete it, as
|
|
/// it may a built-in function.
|
|
pub fn define_host_function(&mut self, name: &str, args: Vec<crate::Arg>, f: crate::HostFunction) -> Result<(), RunError> {
|
|
match self.interp.install_host_function(name, args, f) {
|
|
Ok(()) => Ok(()),
|
|
Err(e) => self.outcome(Err(e)).map(drop),
|
|
}
|
|
}
|
|
|
|
/// The locale `localize` reads, which the host's functions may change during a run.
|
|
pub fn localization(&self) -> std::rc::Rc<crate::Localization> {
|
|
self.interp.localization.clone()
|
|
}
|
|
|
|
/// Defines the global variable `name` holding the exception the last run did not catch
|
|
/// (`undefined` if there was none).
|
|
pub fn bind_last_thrown(&mut self, name: &str) {
|
|
let interp = &mut self.interp;
|
|
let v = interp.last_thrown.clone();
|
|
let global = interp.global;
|
|
let _ = interp.define(global, key(name), Prop::Data { value: v, flags: HIDDEN });
|
|
}
|
|
|
|
/// Sets the clock `Date.now()` and `new Date()` read: the current time in milliseconds since
|
|
/// 1970-01-01 UTC. By default it is the system clock; a wasm build has none, and its default
|
|
/// gives NaN.
|
|
pub fn set_clock(&mut self, clock: impl Fn() -> f64 + 'static) {
|
|
self.interp.date_host.clock = std::rc::Rc::new(clock);
|
|
}
|
|
|
|
/// Sets the time zone `Date` uses for local time: `offset` gives local time's offset from UTC
|
|
/// in milliseconds at a time value (milliseconds since 1970-01-01 UTC), so it can follow
|
|
/// daylight saving time. An offset that is not finite or is a day or more counts as 0. By
|
|
/// default local time is UTC.
|
|
pub fn set_time_zone(&mut self, offset: impl Fn(f64) -> f64 + 'static) {
|
|
self.interp.date_host.offset = std::rc::Rc::new(offset);
|
|
}
|
|
|
|
/// Collects garbage now and returns the heap's estimated size in bytes afterwards.
|
|
pub fn collect_garbage(&mut self) -> usize {
|
|
self.interp.collect_garbage();
|
|
self.interp.heap.bytes
|
|
}
|
|
|
|
#[cfg(test)]
|
|
pub(crate) fn interp(&mut self) -> &mut Interp {
|
|
&mut self.interp
|
|
}
|
|
}
|