diff --git a/crates/js/Cargo.toml b/crates/js/Cargo.toml index 0108b50d9..4cb1e62c2 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..3eda7b6a6 --- /dev/null +++ b/crates/js/src/builtins.rs @@ -0,0 +1,352 @@ +//! 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, string_bytes}; +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() { + // Error.prototype and each NativeError prototype are Error objects (sections 15.11.4 and + // 15.11.7.7). + let proto = alloc(interp, Class::Error, Some(if i == 0 { object_proto } else { error_proto })); + if i == 0 { + error_proto = proto; + 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 { + const HEAD: &str = "(function anonymous("; + const MID: &str = "\n) {\n"; + const TAIL: &str = "\n})"; + let mut parts = Vec::with_capacity(args.len()); + for a in args { + parts.push(interp.to_str(a)?); + } + let body = parts.pop().unwrap_or_else(|| Str::from("")); + // The length is checked against the source limit before any argument is flattened or copied. + let total = parts + .iter() + .map(Str::len) + .chain([parts.len().saturating_sub(1), HEAD.len() + MID.len() + TAIL.len(), body.len()]) + .fold(0usize, usize::saturating_add); + interp.check_source_units(total).map_err(|e| interp.compile_error_to_abrupt(e))?; + let mut units = Vec::with_capacity(total); + units.extend(HEAD.encode_utf16()); + for (i, p) in parts.iter().enumerate() { + if i > 0 { + units.push(u16::from(b',')); + } + units.extend_from_slice(&p.flat()); + } + units.extend(MID.encode_utf16()); + units.extend_from_slice(&body.flat()); + units.extend(TAIL.encode_utf16()); + 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)), + }; + interp.charge_code(&code, 0)?; + 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); + let units = code.source.text.get(start..end).unwrap_or_default(); + let bytes = string_bytes(units.len()); + interp.reserve_heap(bytes)?; + interp.heap.bytes = interp.heap.bytes.saturating_add(bytes); + return Ok(Value::String(Str::from_units(units.to_vec()))); + } + 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) }; + interp.check_source_units(s.len()).map_err(|e| interp.compile_error_to_abrupt(e))?; + 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..b23d9a525 --- /dev/null +++ b/crates/js/src/bytecode.rs @@ -0,0 +1,362 @@ +//! 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, + /// Pushes the completion value, which a `finally` block in script and eval code keeps and + /// sets back with `SetCompletion` when it ends normally. + GetCompletion, +} + +/// 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(_) | Op::GetCompletion => 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/chars.rs b/crates/js/src/chars.rs index 75c0b92aa..eb61fe763 100644 --- a/crates/js/src/chars.rs +++ b/crates/js/src/chars.rs @@ -58,6 +58,39 @@ pub fn hex_value(c: u32) -> Option { } } +/// The value of the digit values `digits` in `radix` (8 or 16), rounded once to the nearest number +/// with ties to even (sections 7.8.3 and 9.3.1), or `None` when there are no digits. Digits are +/// kept exactly in a `u128` while they fit; past that, each later digit counts only for its +/// position and for whether any of the dropped bits is set. +pub fn radix_value(digits: impl IntoIterator, radix: u32) -> Option { + let bits = radix.trailing_zeros(); + let mut kept: u128 = 0; + let mut dropped_bits: u32 = 0; + let mut sticky = false; + let mut any = false; + for d in digits { + any = true; + if kept.leading_zeros() >= bits { + kept = kept << bits | u128::from(d); + } else { + dropped_bits = dropped_bits.saturating_add(bits); + sticky |= d != 0; + } + } + if !any { + return None; + } + // `kept` holds at least 125 bits once a digit is dropped, so its lowest bit lies below the + // rounding bit and stands in for every dropped bit. + let mut value = (kept | u128::from(sticky)) as f64; + while dropped_bits > 0 && value.is_finite() { + let step = dropped_bits.min(64); + value *= (1_u128 << step) as f64; + dropped_bits -= step; + } + Some(value) +} + pub fn is_decimal_digit(c: u32) -> bool { (0x30..=0x39).contains(&c) } diff --git a/crates/js/src/compiler.rs b/crates/js/src/compiler.rs new file mode 100644 index 000000000..ae470be44 --- /dev/null +++ b/crates/js/src/compiler.rs @@ -0,0 +1,1260 @@ +//! 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