From d026e6ae26f5245caef73cf322a9b5d1d572bc34 Mon Sep 17 00:00:00 2001 From: dotCooCoo Date: Sat, 7 Nov 2026 12:36:48 -0800 Subject: [PATCH] JavaScript: interpreter with a garbage-collected heap and execution limits vectorcraft-js gains a Realm that runs ECMAScript 5.1 scripts. The parsed script compiles to stack-machine bytecode, a verifier checks every jump target, handler, constant index and stack depth before the code runs, and the interpreter keeps JavaScript calls on its own frame stack. Closures, this, arguments, with, direct and indirect eval, new Function, try/catch/finally, labeled break and continue, and for-in in insertion order follow sections 10 to 14 of ECMA-262 5.1. Variable references are resolved before the right-hand side of an assignment runs (11.13). Objects and environments live in an arena with a mark-and-sweep collector and index handles. Built-ins are limited to Object and Object.prototype's basic methods, Function with call and apply, eval and the error constructors. Every run is bounded by fuel, heap bytes, frames, nested host calls and an interrupt flag another thread can set, with smaller defaults in the wasm build. A run that reaches a limit ends with a limit error that a script's try cannot catch. Source passed to eval and Function counts against Options::max_source_units. cargo xtask test262 also runs the suite's ES5 test/language tests that need only these built-ins and prints their counts apart from the parse counts. Nothing in the app calls the interpreter yet. --- crates/js/Cargo.toml | 2 +- crates/js/src/builtins.rs | 328 ++++++ crates/js/src/bytecode.rs | 359 ++++++ crates/js/src/compiler.rs | 1249 ++++++++++++++++++++ crates/js/src/heap.rs | 515 ++++++++ crates/js/src/interp.rs | 2064 +++++++++++++++++++++++++++++++++ crates/js/src/interp_tests.rs | 492 ++++++++ crates/js/src/lib.rs | 21 + crates/js/src/realm.rs | 218 ++++ crates/js/src/tests.rs | 2 +- crates/js/src/value.rs | 381 ++++++ crates/js/tests/test262.rs | 138 ++- docs/target-app-parity.md | 1 + xtask/src/main.rs | 6 +- xtask/src/test262.rs | 3 +- 15 files changed, 5773 insertions(+), 6 deletions(-) create mode 100644 crates/js/src/builtins.rs create mode 100644 crates/js/src/bytecode.rs create mode 100644 crates/js/src/compiler.rs create mode 100644 crates/js/src/heap.rs create mode 100644 crates/js/src/interp.rs create mode 100644 crates/js/src/interp_tests.rs create mode 100644 crates/js/src/realm.rs create mode 100644 crates/js/src/value.rs diff --git a/crates/js/Cargo.toml b/crates/js/Cargo.toml index f39ab33b6..a961fd7dd 100644 --- a/crates/js/Cargo.toml +++ b/crates/js/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "vectorcraft-js" -description = "JavaScript for scripts: an ECMAScript 5.1 lexer and parser to a syntax tree with source positions" +description = "JavaScript for scripts: an ECMAScript 5.1 parser, and an interpreter with a garbage-collected heap and execution limits" version.workspace = true edition.workspace = true license.workspace = true diff --git a/crates/js/src/builtins.rs b/crates/js/src/builtins.rs new file mode 100644 index 000000000..02f2364e9 --- /dev/null +++ b/crates/js/src/builtins.rs @@ -0,0 +1,328 @@ +//! The built-in objects the interpreter itself needs (ECMA-262 5.1 section 15): `Object` and the +//! basic methods of `Object.prototype`, `Function` with `Function.prototype.call`, `apply` and +//! `toString`, `eval`, the error constructors and `Error.prototype.toString`, and the prototypes +//! primitive values look properties up on. The rest of the standard library is not here yet. + +use std::rc::Rc; + +use crate::bytecode::{CodeKind, Source}; +use crate::compiler::{CompileFail, compile_constructed_function}; +use crate::heap::{Class, Func, HIDDEN, NativeFn, NativeKind, ObjId, Object, Prop, key}; +use crate::interp::{Abrupt, ErrorType, Interp, JsResult}; +use crate::value::{Str, Value}; + +fn alloc(interp: &mut Interp, class: Class, proto: Option) -> ObjId { + interp.heap.alloc_object(Object::new(class, proto)) +} + +/// A built-in function object. +fn native(interp: &mut Interp, name: &'static str, f: NativeFn, length: u32, kind: NativeKind, constructor: bool) -> JsResult { + let proto = interp.intrinsics.function_proto; + let o = alloc(interp, Class::Function(Func::Native { f, kind, constructor, name }), Some(proto)); + interp.define(o, key("length"), Prop::Data { value: Value::Number(f64::from(length)), flags: 0 })?; + Ok(o) +} + +fn method(interp: &mut Interp, on: ObjId, name: &'static str, f: NativeFn, length: u32) -> JsResult<()> { + let m = native(interp, name, f, length, NativeKind::Plain, false)?; + interp.define(on, key(name), Prop::Data { value: Value::Object(m), flags: HIDDEN }) +} + +fn data(interp: &mut Interp, on: ObjId, name: &str, value: Value, flags: u8) -> JsResult<()> { + interp.define(on, key(name), Prop::Data { value, flags }) +} + +/// Links a constructor and its prototype and makes the constructor a global. +fn constructor(interp: &mut Interp, name: &'static str, ctor: ObjId, proto: ObjId) -> JsResult<()> { + data(interp, ctor, "prototype", Value::Object(proto), 0)?; + data(interp, proto, "constructor", Value::Object(ctor), HIDDEN)?; + let global = interp.global; + data(interp, global, name, Value::Object(ctor), HIDDEN) +} + +/// Creates the built-ins and the global object's properties. +pub(crate) fn install(interp: &mut Interp) { + if let Err(e) = try_install(interp) { + // Installing touches only objects it has just made, so this does not happen; a realm + // that somehow lacks a built-in reports it when a script uses it. + let _ = e; + } +} + +fn try_install(interp: &mut Interp) -> JsResult<()> { + let object_proto = alloc(interp, Class::Ordinary, None); + interp.intrinsics.object_proto = object_proto; + let function_proto = alloc( + interp, + Class::Function(Func::Native { f: function_proto_call, kind: NativeKind::Plain, constructor: false, name: "" }), + Some(object_proto), + ); + interp.intrinsics.function_proto = function_proto; + data(interp, function_proto, "length", Value::Number(0.0), 0)?; + let global = interp.global; + if let Some(g) = interp.heap.object_mut(global) { + g.proto = Some(object_proto); + } + interp.intrinsics.array_proto = alloc(interp, Class::Array { length: 0, length_writable: true }, Some(object_proto)); + interp.intrinsics.string_proto = alloc(interp, Class::String(Str::from("")), Some(object_proto)); + interp.intrinsics.number_proto = alloc(interp, Class::Number, Some(object_proto)); + interp.intrinsics.boolean_proto = alloc(interp, Class::Boolean, Some(object_proto)); + interp.intrinsics.regexp_proto = alloc(interp, Class::Ordinary, Some(object_proto)); + let thrower = native(interp, "", throw_type_error, 0, NativeKind::Plain, false)?; + if let Some(t) = interp.heap.object_mut(thrower) { + t.extensible = false; + } + interp.intrinsics.throw_type_error = thrower; + + data(interp, global, "NaN", Value::Number(f64::NAN), 0)?; + data(interp, global, "Infinity", Value::Number(f64::INFINITY), 0)?; + data(interp, global, "undefined", Value::Undefined, 0)?; + let eval = native(interp, "eval", eval, 1, NativeKind::Eval, false)?; + interp.intrinsics.eval = eval; + data(interp, global, "eval", Value::Object(eval), HIDDEN)?; + + let object = native(interp, "Object", object_ctor, 1, NativeKind::Plain, true)?; + constructor(interp, "Object", object, object_proto)?; + method(interp, object_proto, "toString", object_to_string, 0)?; + method(interp, object_proto, "toLocaleString", object_to_locale_string, 0)?; + method(interp, object_proto, "valueOf", object_value_of, 0)?; + method(interp, object_proto, "hasOwnProperty", has_own_property, 1)?; + method(interp, object_proto, "isPrototypeOf", is_prototype_of, 1)?; + method(interp, object_proto, "propertyIsEnumerable", property_is_enumerable, 1)?; + + let function = native(interp, "Function", function_ctor, 1, NativeKind::Plain, true)?; + constructor(interp, "Function", function, function_proto)?; + let call = native(interp, "call", unreachable_native, 1, NativeKind::Call, false)?; + data(interp, function_proto, "call", Value::Object(call), HIDDEN)?; + let apply = native(interp, "apply", unreachable_native, 2, NativeKind::Apply, false)?; + data(interp, function_proto, "apply", Value::Object(apply), HIDDEN)?; + method(interp, function_proto, "toString", function_to_string, 0)?; + + install_errors(interp, object_proto) +} + +fn install_errors(interp: &mut Interp, object_proto: ObjId) -> JsResult<()> { + let kinds: [(&'static str, NativeFn); 7] = [ + ("Error", error_ctor), + ("EvalError", eval_error_ctor), + ("RangeError", range_error_ctor), + ("ReferenceError", reference_error_ctor), + ("SyntaxError", syntax_error_ctor), + ("TypeError", type_error_ctor), + ("URIError", uri_error_ctor), + ]; + let mut error_proto = object_proto; + for (i, (name, f)) in kinds.into_iter().enumerate() { + let proto = alloc(interp, Class::Ordinary, Some(if i == 0 { object_proto } else { error_proto })); + if i == 0 { + error_proto = proto; + if let Some(p) = interp.heap.object_mut(proto) { + p.class = Class::Error; + } + method(interp, proto, "toString", error_to_string, 0)?; + } + if let Some(slot) = interp.intrinsics.error_protos.get_mut(i) { + *slot = proto; + } + data(interp, proto, "name", Value::str(name), HIDDEN)?; + data(interp, proto, "message", Value::str(""), HIDDEN)?; + let ctor = native(interp, name, f, 1, NativeKind::Plain, true)?; + constructor(interp, name, ctor, proto)?; + } + Ok(()) +} + +fn arg(args: &[Value], i: usize) -> Value { + args.get(i).cloned().unwrap_or(Value::Undefined) +} + +fn unreachable_native(_: &mut Interp, _: Value, _: &[Value], _: bool) -> JsResult { + Err(Abrupt::Internal("call or apply reached its native body".into())) +} + +fn throw_type_error(interp: &mut Interp, _: Value, _: &[Value], _: bool) -> JsResult { + Err(interp.type_error("'caller', 'callee' and 'arguments' cannot be used on strict mode functions")) +} + +fn function_proto_call(_: &mut Interp, _: Value, _: &[Value], _: bool) -> JsResult { + Ok(Value::Undefined) +} + +// ---- Object ---- + +/// `Object(value)` and `new Object(value)` (sections 15.2.1 and 15.2.2). +fn object_ctor(interp: &mut Interp, _: Value, args: &[Value], _: bool) -> JsResult { + let v = arg(args, 0); + if v.is_nullish() { + return Ok(Value::Object(interp.new_object())); + } + interp.to_object(&v).map(Value::Object) +} + +/// `Object.prototype.toString` (section 15.2.4.2). +fn object_to_string(interp: &mut Interp, this: Value, _: &[Value], _: bool) -> JsResult { + let class = match &this { + Value::Undefined => "Undefined", + Value::Null => "Null", + _ => { + let o = interp.to_object(&this)?; + interp.heap.object(o).map_or("Object", |o| o.class.name()) + } + }; + Ok(Value::str(&format!("[object {class}]"))) +} + +fn object_to_locale_string(interp: &mut Interp, this: Value, _: &[Value], _: bool) -> JsResult { + let o = interp.to_object(&this)?; + let f = interp.get_from(o, &key("toString"), &this)?; + if !interp.is_callable(&f) { + return Err(interp.type_error("toString is not a function")); + } + interp.call(&f, &this, &[]) +} + +fn object_value_of(interp: &mut Interp, this: Value, _: &[Value], _: bool) -> JsResult { + interp.to_object(&this).map(Value::Object) +} + +fn has_own_property(interp: &mut Interp, this: Value, args: &[Value], _: bool) -> JsResult { + let k = interp.to_key(&arg(args, 0))?; + let o = interp.to_object(&this)?; + Ok(Value::Bool(interp.get_own_property(o, &k)?.is_some())) +} + +fn is_prototype_of(interp: &mut Interp, this: Value, args: &[Value], _: bool) -> JsResult { + let Value::Object(mut v) = arg(args, 0) else { return Ok(Value::Bool(false)) }; + let o = interp.to_object(&this)?; + while let Some(p) = interp.heap.object(v).and_then(|v| v.proto) { + if p == o { + return Ok(Value::Bool(true)); + } + v = p; + interp.use_fuel()?; + } + Ok(Value::Bool(false)) +} + +fn property_is_enumerable(interp: &mut Interp, this: Value, args: &[Value], _: bool) -> JsResult { + let k = interp.to_key(&arg(args, 0))?; + let o = interp.to_object(&this)?; + Ok(Value::Bool(interp.get_own_property(o, &k)?.is_some_and(|p| p.enumerable()))) +} + +// ---- Function ---- + +/// `Function(p1, …, body)` and `new Function(…)` (section 15.3.2.1): the source is read as +/// `(function anonymous(p1,…\n) {\nbody\n})` and must be exactly that one function. +fn function_ctor(interp: &mut Interp, _: Value, args: &[Value], _: bool) -> JsResult { + let mut parts = Vec::with_capacity(args.len()); + for a in args { + parts.push(interp.to_str(a)?.to_rust_string()); + } + let body = parts.pop().unwrap_or_default(); + let text = format!("(function anonymous({}\n) {{\n{body}\n}})", parts.join(",")); + let units: Vec = text.encode_utf16().collect(); + interp.source_units = interp.source_units.saturating_add(units.len()); + let remaining = interp.options.max_source_units.saturating_sub(interp.source_units.saturating_sub(units.len())); + let options = crate::Options { strict: false, max_source_units: remaining, ..interp.options.clone() }; + let program = match crate::parse_utf16(&units, &options) { + Ok(p) => p, + Err(e) => return Err(interp.throw(ErrorType::Syntax, &e.message)), + }; + let source = Rc::new(Source { lines: program.lines.clone(), text: units.into() }); + let code = match compile_constructed_function(&program, &source) { + Ok(Some(code)) => code, + Ok(None) => return Err(interp.throw(ErrorType::Syntax, "the parameters or body do not form one function")), + Err(CompileFail::Syntax(e)) => return Err(interp.throw(ErrorType::Syntax, &e.message)), + Err(CompileFail::Internal(m)) => return Err(Abrupt::Internal(m)), + }; + let env = interp.global_env; + interp.make_closure(code, env).map(Value::Object) +} + +/// `Function.prototype.toString` (section 15.3.4.2): a function's source text, or a stand-in +/// for a built-in. +fn function_to_string(interp: &mut Interp, this: Value, _: &[Value], _: bool) -> JsResult { + let func = this.as_object().and_then(|o| interp.heap.object(o)).and_then(|o| match &o.class { + Class::Function(f) => Some(f.clone()), + _ => None, + }); + let text = match func { + Some(Func::Closure { code, .. }) => { + let (start, end) = (code.span.0 as usize, code.span.1 as usize); + String::from_utf16_lossy(code.source.text.get(start..end).unwrap_or_default()) + } + Some(Func::Native { name, .. }) => format!("function {name}() {{ [native code] }}"), + Some(Func::Host { name, .. }) => format!("function {name}() {{ [native code] }}"), + None => return Err(interp.type_error("Function.prototype.toString was called on a value that is not a function")), + }; + Ok(Value::str(&text)) +} + +/// The global `eval` called indirectly (section 15.1.2.1.1): global code in the global +/// environment. +fn eval(interp: &mut Interp, _: Value, args: &[Value], _: bool) -> JsResult { + let x = arg(args, 0); + let Value::String(s) = &x else { return Ok(x) }; + let units = s.flat(); + let code = interp.compile_source(&units, false, CodeKind::Eval).map_err(|e| interp.compile_error_to_abrupt(e))?; + interp.run_global(code, true) +} + +// ---- errors ---- + +fn make_error(interp: &mut Interp, t: ErrorType, args: &[Value]) -> JsResult { + let message = arg(args, 0); + let text = if matches!(message, Value::Undefined) { None } else { Some(interp.to_str(&message)?) }; + let o = interp.error_object(t, ""); + if let Some(text) = text { + interp.define(o, key("message"), Prop::Data { value: Value::String(text), flags: HIDDEN })?; + } + Ok(Value::Object(o)) +} + +fn error_ctor(interp: &mut Interp, _: Value, args: &[Value], _: bool) -> JsResult { + make_error(interp, ErrorType::Error, args) +} + +fn eval_error_ctor(interp: &mut Interp, _: Value, args: &[Value], _: bool) -> JsResult { + make_error(interp, ErrorType::Eval, args) +} + +fn range_error_ctor(interp: &mut Interp, _: Value, args: &[Value], _: bool) -> JsResult { + make_error(interp, ErrorType::Range, args) +} + +fn reference_error_ctor(interp: &mut Interp, _: Value, args: &[Value], _: bool) -> JsResult { + make_error(interp, ErrorType::Reference, args) +} + +fn syntax_error_ctor(interp: &mut Interp, _: Value, args: &[Value], _: bool) -> JsResult { + make_error(interp, ErrorType::Syntax, args) +} + +fn type_error_ctor(interp: &mut Interp, _: Value, args: &[Value], _: bool) -> JsResult { + make_error(interp, ErrorType::Type, args) +} + +fn uri_error_ctor(interp: &mut Interp, _: Value, args: &[Value], _: bool) -> JsResult { + make_error(interp, ErrorType::Uri, args) +} + +/// `Error.prototype.toString` (section 15.11.4.4). +fn error_to_string(interp: &mut Interp, this: Value, _: &[Value], _: bool) -> JsResult { + let Value::Object(o) = this else { return Err(interp.type_error("Error.prototype.toString was called on a value that is not an object")) }; + let name = interp.get_from(o, &key("name"), &this)?; + let name = if matches!(name, Value::Undefined) { Str::from("Error") } else { interp.to_str(&name)? }; + let message = interp.get_from(o, &key("message"), &this)?; + let message = if matches!(message, Value::Undefined) { Str::from("") } else { interp.to_str(&message)? }; + if name.len() == 0 { + return Ok(Value::String(message)); + } + if message.len() == 0 { + return Ok(Value::String(name)); + } + let sep = Str::from(": "); + let head = interp.concat(&name, &sep)?; + interp.concat(&head, &message).map(Value::String) +} diff --git a/crates/js/src/bytecode.rs b/crates/js/src/bytecode.rs new file mode 100644 index 000000000..4ecb582d0 --- /dev/null +++ b/crates/js/src/bytecode.rs @@ -0,0 +1,359 @@ +//! Bytecode: the instructions a function, a script or eval code compiles to, and the verifier +//! that checks them before they run. +//! +//! The interpreter is a stack machine. Each instruction takes its operands from the top of the +//! frame's operand stack and pushes its result there; jumps name an instruction index. The +//! verifier walks every path through the code and checks that each jump target, exception handler +//! and `finally` dispatch target is an instruction, that each constant, name and function index is +//! in its table, that the stack never goes below the frame's base, that every path reaching an +//! instruction arrives with the same stack depth, and that no path runs past the last +//! instruction. Code that fails is reported as an internal error and never runs. + +use std::rc::Rc; + +use crate::LineTable; +use crate::ast::BinaryOp; +use crate::heap::Key; +use crate::value::Str; + +#[derive(Clone, Copy, Debug, PartialEq)] +pub(crate) enum Op { + Undefined, + Null, + True, + False, + /// Pushes `numbers[i]`. + Number(u32), + /// Pushes `strings[i]`. + String(u32), + This, + Pop, + /// Removes the `n` values under the top one. + PopUnder(u32), + Dup, + /// `a b` → `a b a b`. + Dup2, + Swap, + /// `a b c` → `b c a`. + Rot3, + /// Pushes the value of the variable `names[i]`. + GetVar(u32), + /// Pushes the value of the variable and the `this` a call to it gets. + GetVarThis(u32), + /// `value` → `value`, assigned to the variable. + SetVar(u32), + /// Pushes the base of the reference to the variable `names[i]`, resolved before the + /// right-hand side of an assignment runs. + RefVar(u32), + /// `base` → `value`: the variable's value read through the resolved base. + GetRef(u32), + /// `base value` → `value`, assigned to the variable through the resolved base. + PutRef(u32), + TypeofVar(u32), + DeleteVar(u32), + IncVar { + name: u32, + prefix: bool, + dec: bool, + }, + /// `object` → `object.name`. + GetMember(u32), + /// `object` → `object.name object`. + GetMemberThis(u32), + /// `object value` → `value`. + SetMember(u32), + DeleteMember(u32), + IncMember { + name: u32, + prefix: bool, + dec: bool, + }, + /// `object key` → `object name`: checks that the object can have properties and turns the key + /// into a property name, before the right-hand side of an assignment runs. + ToKey, + /// `object` → `object`: throws a `TypeError` when it is `undefined` or `null`, before the + /// right-hand side of an assignment to one of its properties runs. + CheckObject, + /// `object key` → `object[key]`. + GetIndex, + /// `object key` → `object[key] object`. + GetIndexThis, + /// `object key value` → `value`. + SetIndex, + DeleteIndex, + IncIndex { + prefix: bool, + dec: bool, + }, + /// `function this arg…` → result. + Call(u32), + /// Like `Call`, but a direct eval when the function is the global `eval`. + CallEval(u32), + /// `function arg…` → the new object. + New(u32), + Binary(BinaryOp), + Neg, + Plus, + BitNot, + Not, + Typeof, + ToNumber, + Jump(u32), + /// Pops the top value and jumps when it is falsy. + JumpIfFalse(u32), + JumpIfTrue(u32), + /// Jumps, keeping the top value, when it is falsy; otherwise pops it. + JumpIfFalseKeep(u32), + JumpIfTrueKeep(u32), + Return, + /// Returns the value of the last expression statement (script and eval code). + ReturnCompletion, + Throw, + /// Throws a `ReferenceError` with the message `strings[i]` (an assignment to something that + /// cannot be assigned to). + ThrowReferenceError(u32), + /// Ends a `finally` block: `value kind` on the stack say how it was entered. Kind 0 (normal) + /// pops both and goes on, kind 1 throws the value, and kind `2 + k` jumps to entry `k` of + /// `finally_tables[i]` with the value left on the stack. + EndFinally(u32), + NewObject, + NewArray, + /// `array value` → `array`, the value appended. + ArrayPush, + /// `array` → `array`, one longer. + ArrayHole, + /// `object value` → `object`. + InitProp(u32), + /// `object function` → `object`. + InitGetter(u32), + InitSetter(u32), + /// Pushes a new function made from `functions[i]` and the current environment. + Closure(u32), + /// Pushes a new regular expression object for `regexps[i]`. + RegExp(u32), + /// `object` → nothing: enters a `with` statement's environment. + PushWith, + /// `exception` → nothing: enters a catch clause's environment, binding `names[i]`. + PushCatch(u32), + PopEnv, + /// `object` → `iterator`. + ForInStart, + /// `iterator` → `iterator name`, or jumps with `iterator` when the names are used up. + ForInNext(u32), + /// Pops the value of an expression statement into the completion value. + SetCompletion, +} + +/// What code is: a function body, a script, or eval code. +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub(crate) enum CodeKind { + Function, + Global, + Eval, +} + +/// A range of instructions an exception thrown in jumps out of: to `target`, with the operand +/// stack cut back to `depth` values and the environments to `env_depth`, and the exception pushed. +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub(crate) struct Handler { + pub(crate) start: u32, + pub(crate) end: u32, + pub(crate) target: u32, + pub(crate) depth: u32, + pub(crate) env_depth: u32, +} + +/// The source a piece of code came from, for error positions and `Function.prototype.toString`. +#[derive(Debug)] +pub(crate) struct Source { + pub(crate) lines: LineTable, + pub(crate) text: Rc<[u16]>, +} + +#[derive(Debug)] +pub(crate) struct Code { + pub(crate) ops: Vec, + /// The source offset each instruction comes from. + pub(crate) positions: Vec, + pub(crate) numbers: Vec, + pub(crate) strings: Vec, + pub(crate) names: Vec, + pub(crate) functions: Vec>, + pub(crate) regexps: Vec<(Str, Str)>, + pub(crate) handlers: Vec, + pub(crate) finally_tables: Vec>, + /// The most values the code keeps on the operand stack, set by the verifier. + pub(crate) max_stack: u32, + pub(crate) strict: bool, + /// A function expression's own name, bound inside it. + pub(crate) self_name: Option, + pub(crate) params: Vec, + pub(crate) vars: Vec, + /// Function declarations to instantiate on entry: their names and indices in `functions`. + pub(crate) fun_decls: Vec<(Key, u32)>, + /// The body refers to `arguments` or calls `eval`. + pub(crate) uses_arguments: bool, + pub(crate) source: Rc, + /// Where the function's text starts and ends in the source. + pub(crate) span: (u32, u32), +} + +impl Code { + /// The innermost handler around instruction `pc`. + pub(crate) fn handler(&self, pc: usize) -> Option { + self.handlers.iter().copied().find(|h| (h.start as usize) <= pc && pc < h.end as usize) + } + + /// The line and column of instruction `pc`. + pub(crate) fn line_column(&self, pc: usize) -> (u32, u32) { + let offset = self.positions.get(pc).copied().unwrap_or(self.span.0); + self.source.lines.line_column(offset) + } +} + +/// What an instruction does to the stack on each way out of it. +struct Effect { + pops: u32, + /// Values pushed when it goes on to the next instruction; `None` when it never does. + next: Option, + /// Instruction indices it may jump to, each with the values it pushes on that path. + jumps: Vec<(u32, u32)>, +} + +/// The values an instruction pops, and pushes when it goes on to the next instruction (0 for one +/// that never does), for the compiler's count of the stack depth. +pub(crate) fn fallthrough_effect(op: Op) -> (u32, u32) { + match op { + Op::EndFinally(_) => (2, 0), + _ => effect(op, &[]).map_or((0, 0), |fx| (fx.pops, fx.next.unwrap_or(0))), + } +} + +fn effect(op: Op, finally_tables: &[Vec]) -> Result { + let e = |pops, pushes| Effect { pops, next: Some(pushes), jumps: Vec::new() }; + let end = |pops| Effect { pops, next: None, jumps: Vec::new() }; + Ok(match op { + Op::Undefined | Op::Null | Op::True | Op::False | Op::Number(_) | Op::String(_) | Op::This => e(0, 1), + Op::GetVar(_) | Op::TypeofVar(_) | Op::DeleteVar(_) | Op::IncVar { .. } | Op::RefVar(_) => e(0, 1), + Op::GetRef(_) => e(1, 1), + Op::PutRef(_) => e(2, 1), + Op::NewObject | Op::NewArray | Op::Closure(_) | Op::RegExp(_) => e(0, 1), + Op::Pop | Op::PushWith | Op::PushCatch(_) | Op::SetCompletion => e(1, 0), + Op::PopUnder(n) => e(n.checked_add(1).ok_or("operand too large")?, 1), + Op::Dup => e(1, 2), + Op::Dup2 => e(2, 4), + Op::Swap | Op::ToKey => e(2, 2), + Op::Rot3 => e(3, 3), + Op::GetVarThis(_) => e(0, 2), + Op::SetVar(_) | Op::GetMember(_) | Op::DeleteMember(_) | Op::IncMember { .. } => e(1, 1), + Op::Neg | Op::Plus | Op::BitNot | Op::Not | Op::Typeof | Op::ToNumber | Op::ArrayHole | Op::ForInStart | Op::CheckObject => e(1, 1), + Op::GetMemberThis(_) => e(1, 2), + Op::SetMember(_) | Op::GetIndex | Op::DeleteIndex | Op::IncIndex { .. } | Op::Binary(_) => e(2, 1), + Op::ArrayPush | Op::InitProp(_) | Op::InitGetter(_) | Op::InitSetter(_) => e(2, 1), + Op::GetIndexThis => e(2, 2), + Op::SetIndex => e(3, 1), + Op::Call(n) | Op::CallEval(n) => e(n.checked_add(2).ok_or("operand too large")?, 1), + Op::New(n) => e(n.checked_add(1).ok_or("operand too large")?, 1), + Op::PopEnv => e(0, 0), + Op::Jump(t) => Effect { pops: 0, next: None, jumps: vec![(t, 0)] }, + Op::JumpIfFalse(t) | Op::JumpIfTrue(t) => Effect { pops: 1, next: Some(0), jumps: vec![(t, 0)] }, + Op::JumpIfFalseKeep(t) | Op::JumpIfTrueKeep(t) => Effect { pops: 1, next: Some(0), jumps: vec![(t, 1)] }, + Op::Return | Op::Throw => end(1), + Op::ReturnCompletion | Op::ThrowReferenceError(_) => end(0), + Op::EndFinally(t) => { + let table = finally_tables.get(t as usize).ok_or_else(|| format!("finally table {t} out of range"))?; + Effect { pops: 2, next: Some(0), jumps: table.iter().map(|&target| (target, 1)).collect() } + } + Op::ForInNext(t) => Effect { pops: 1, next: Some(2), jumps: vec![(t, 1)] }, + }) +} + +/// Checks the constant, name, function and regular expression indices an instruction uses. +fn check_indices(op: Op, code: &Code) -> Result<(), String> { + let check = |i: u32, len: usize, what: &str| if (i as usize) < len { Ok(()) } else { Err(format!("{what} {i} out of range")) }; + match op { + Op::Number(i) => check(i, code.numbers.len(), "number"), + Op::String(i) | Op::ThrowReferenceError(i) => check(i, code.strings.len(), "string"), + Op::GetVar(i) + | Op::GetVarThis(i) + | Op::SetVar(i) + | Op::RefVar(i) + | Op::GetRef(i) + | Op::PutRef(i) + | Op::TypeofVar(i) + | Op::DeleteVar(i) + | Op::IncVar { name: i, .. } + | Op::GetMember(i) + | Op::GetMemberThis(i) + | Op::SetMember(i) + | Op::DeleteMember(i) + | Op::IncMember { name: i, .. } + | Op::InitProp(i) + | Op::InitGetter(i) + | Op::InitSetter(i) + | Op::PushCatch(i) => check(i, code.names.len(), "name"), + Op::Closure(i) => check(i, code.functions.len(), "function"), + Op::RegExp(i) => check(i, code.regexps.len(), "regular expression"), + _ => Ok(()), + } +} + +/// Verifies `code` (not the functions inside it, which are verified when they are compiled) and +/// returns the most values it keeps on the stack. +pub(crate) fn verify(code: &Code) -> Result { + let len = code.ops.len(); + if len == 0 { + return Err("no instructions".into()); + } + if code.positions.len() != len { + return Err("the position table does not match the instructions".into()); + } + for (name, _) in &code.fun_decls { + if name.is_empty() { + return Err("a function declaration without a name".into()); + } + } + if code.fun_decls.iter().any(|(_, i)| *i as usize >= code.functions.len()) { + return Err("function declaration index out of range".into()); + } + let mut depth: Vec> = vec![None; len]; + let mut work: Vec<(u32, u32)> = vec![(0, 0)]; + for h in &code.handlers { + if h.start > h.end || h.end as usize > len { + return Err(format!("handler range {}..{} out of range", h.start, h.end)); + } + work.push((h.target, h.depth.checked_add(1).ok_or("handler depth too large")?)); + } + let mut max = 0; + while let Some((pc, d)) = work.pop() { + let slot = depth.get_mut(pc as usize).ok_or_else(|| format!("jump to {pc}, past the end"))?; + match *slot { + Some(seen) if seen == d => continue, + Some(seen) => return Err(format!("instruction {pc} reached with stack depths {seen} and {d}")), + None => *slot = Some(d), + } + max = max.max(d); + let op = code.ops.get(pc as usize).copied().ok_or("instruction out of range")?; + check_indices(op, code)?; + let fx = effect(op, &code.finally_tables)?; + let base = d.checked_sub(fx.pops).ok_or_else(|| format!("instruction {pc} ({op:?}) pops below the stack"))?; + if let Some(pushes) = fx.next { + if pc as usize + 1 >= len { + return Err("the code runs past its last instruction".into()); + } + work.push((pc + 1, base + pushes)); + } + for (target, pushes) in fx.jumps { + work.push((target, base + pushes)); + } + } + for h in &code.handlers { + if let Some(Some(d)) = depth.get(h.start as usize) + && *d < h.depth + && h.start < h.end + { + return Err(format!("handler at {} expects {} values but its range starts with {d}", h.target, h.depth)); + } + } + Ok(max) +} diff --git a/crates/js/src/compiler.rs b/crates/js/src/compiler.rs new file mode 100644 index 000000000..75ee257af --- /dev/null +++ b/crates/js/src/compiler.rs @@ -0,0 +1,1249 @@ +//! The compiler: a syntax tree to bytecode, one [`Code`] per function, script or eval code. +//! +//! Variables are looked up by name at run time through the environment chain, as ECMA-262 5.1 +//! section 10 describes, so `with` and direct `eval` need no special analysis. Declarations are +//! hoisted: each code lists its `var` names and function declarations, and the interpreter binds +//! them when the code is entered (section 10.5). +//! +//! The compiler counts the operand stack depth as it emits, so `break`, `continue` and `return` +//! know how many values to drop. A jump out of a `try` with a `finally` block goes through the +//! block: it pushes a completion (a value and a kind number) and jumps to the block, whose +//! `EndFinally` instruction goes on with the jump afterwards. Long left-nested runs of binary +//! operators and member accesses, right-nested assignment chains, prefix operators, `else if` +//! chains and labels are compiled with loops; every other kind of nesting is counted, and code +//! nested deeper than [`MAX_DEPTH`] gives a `SyntaxError` instead of exhausting the stack. + +use std::collections::{HashMap, HashSet}; +use std::rc::Rc; + +use crate::ast::{ + AssignOp, Ast, BinaryOp, Expr, ExprId, ForInTarget, ForInit, FunctionId, FunctionKind, LogicalOp, PropertyValue, Stmt, StmtId, UnaryOp, UpdateOp, + VarDecl, +}; +use crate::bytecode::{Code, CodeKind, Handler, Op, Source, fallthrough_effect, verify}; +use crate::error::{SyntaxError, TOO_DEEP}; +use crate::heap::Key; +use crate::value::Str; + +/// How deeply the compiler may recurse: past the parser's default nesting limit, and shallow +/// enough for a 1 MiB stack. +pub(crate) const MAX_DEPTH: u32 = 600; + +/// Why code could not be compiled. +#[derive(Debug)] +pub(crate) enum CompileFail { + Syntax(SyntaxError), + /// The compiler made code the verifier rejects. + Internal(String), +} + +type CResult = Result; + +fn internal(message: impl Into) -> CResult { + Err(CompileFail::Internal(message.into())) +} + +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +struct Label(usize); + +/// Where `break`, `continue` and `return` go. +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +enum Exit { + /// To the end of the jump context at this index. + Break(usize), + /// To the continue point of the loop at this index. + Continue(usize), + Return, +} + +enum Ctx { + /// A loop, a `switch` or a labeled statement. + Jump { labels: Vec, is_loop: bool, plain_break: bool, brk: Label, cont: Option