diff --git a/.vscode/launch.json b/.vscode/launch.json index a86cf58f86..c1f754200d 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -4,7 +4,6 @@ // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 "version": "0.2.0", "configurations": [ - { "type": "lldb", "request": "launch", @@ -37,16 +36,16 @@ "symbolSearchPath": "https://msdl.microsoft.com/download/symbols" }, { - "name": "(Windows) Run Test Debugger", - "type": "cppvsdbg", - "request": "launch", - "program": "${workspaceFolder}/target/debug/boa-ea5ed1ef3ee0cbe1.exe", - "args": [], - "stopAtEntry": false, - "cwd": "${workspaceFolder}", - "environment": [], - "externalConsole": true, - "preLaunchTask": "Cargo Test Build", -} + "name": "(Windows) Run Test Debugger", + "type": "cppvsdbg", + "request": "launch", + "program": "${workspaceFolder}/target/debug/boa-ea5ed1ef3ee0cbe1.exe", + "args": [], + "stopAtEntry": false, + "cwd": "${workspaceFolder}", + "environment": [], + "externalConsole": true, + "preLaunchTask": "Cargo Test Build", + } ] } \ No newline at end of file diff --git a/Cargo.lock b/Cargo.lock index 85ddc68591..3bc70dc154 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4,6 +4,7 @@ name = "Boa" version = "0.8.0" dependencies = [ + "bitflags", "criterion", "gc", "jemallocator", @@ -253,18 +254,18 @@ checksum = "5f2a4a2034423744d2cc7ca2068453168dcdb82c438419e639a26bd87839c674" [[package]] name = "gc" -version = "0.3.4" +version = "0.3.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f4917b7233397091baf9136eec3c669c8551b097d69ca2b00a2606e5f07641d1" +checksum = "1a56d49bc3e3be03d3432ba45e0503aa13f7e0ad2fc2e6d8ab50b33725b3d767" dependencies = [ "gc_derive", ] [[package]] name = "gc_derive" -version = "0.3.4" +version = "0.3.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6a5b968c8044a119af2671a52de57689cbf502d6686847abd9e6252ee4c39313" +checksum = "034ffcc744b2fc6ec8d23695a4562428093b6f37c74746d9ee9ce606bfc869b8" dependencies = [ "proc-macro2", "quote", @@ -481,9 +482,9 @@ dependencies = [ [[package]] name = "proc-macro2" -version = "1.0.13" +version = "1.0.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "53f5ffe53a6b28e37c9c1ce74893477864d64f74778a93a4beb43c8fa167f639" +checksum = "1502d12e458c49a4c9cbff560d0fe0060c252bc29799ed94ca2ed4bb665a0101" dependencies = [ "unicode-xid", ] @@ -789,9 +790,9 @@ checksum = "f1bddf1187be692e79c5ffeab891132dfb0f236ed36a43c7ed39f1165ee20191" [[package]] name = "version_check" -version = "0.9.1" +version = "0.9.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "078775d0255232fb988e6fccf26ddc9d1ac274299aaedcedce21c6f72cc533ce" +checksum = "b5a972e5669d67ba988ce3dc826706fb0a8b01471c088cb0b6110b805cc36aed" [[package]] name = "walkdir" diff --git a/boa/Cargo.toml b/boa/Cargo.toml index 6932b3e812..0c4aafc184 100644 --- a/boa/Cargo.toml +++ b/boa/Cargo.toml @@ -11,16 +11,17 @@ exclude = ["../.vscode/*", "../Dockerfile", "../Makefile", "../.editorConfig"] edition = "2018" [dependencies] -gc = { version = "0.3.4", features = ["derive"] } +gc = { version = "0.3.5", features = ["derive"] } serde_json = "1.0.53" rand = "0.7.3" num-traits = "0.2.11" regex = "1.3.7" rustc-hash = "1.1.0" -num-bigint = {version = "0.2.6", features = ["serde"]} +num-bigint = { version = "0.2.6", features = ["serde"] } # Optional Dependencies serde = { version = "1.0.110", features = ["derive"], optional = true } +bitflags = "1.2.1" [dev-dependencies] criterion = "0.3.2" diff --git a/boa/src/builtins/array/mod.rs b/boa/src/builtins/array/mod.rs index 4b2b725595..e41ce0c03f 100644 --- a/boa/src/builtins/array/mod.rs +++ b/boa/src/builtins/array/mod.rs @@ -12,982 +12,1036 @@ #[cfg(test)] mod tests; -use super::function::make_constructor_fn; +use super::function::{make_builtin_fn, make_constructor_fn}; use crate::{ builtins::{ + error::RangeError, object::{ObjectKind, INSTANCE_PROTOTYPE, PROTOTYPE}, property::Property, value::{same_value_zero, ResultValue, Value, ValueData}, }, exec::Interpreter, }; -use std::borrow::Borrow; -use std::cmp::{max, min}; -use std::ops::Deref; - -/// Creates a new `Array` instance. -pub(crate) fn new_array(interpreter: &Interpreter) -> ResultValue { - let array = Value::new_object(Some( - &interpreter - .get_realm() - .environment - .get_global_object() - .expect("Could not get global object"), - )); - array.set_kind(ObjectKind::Array); - array.borrow().set_internal_slot( - INSTANCE_PROTOTYPE, - interpreter - .get_realm() - .environment - .get_binding_value("Array") - .borrow() - .get_field_slice(PROTOTYPE), - ); - array.borrow().set_field_slice("length", Value::from(0)); - Ok(array) -} +use std::{ + borrow::Borrow, + cmp::{max, min}, + ops::Deref, +}; -/// Utility function for creating array objects. -/// -/// `array_obj` can be any array with prototype already set (it will be wiped and -/// recreated from `array_contents`) -pub fn construct_array(array_obj: &Value, array_contents: &[Value]) -> ResultValue { - let array_obj_ptr = array_obj.clone(); - - // Wipe existing contents of the array object - let orig_length = i32::from(&array_obj.get_field_slice("length")); - for n in 0..orig_length { - array_obj_ptr.remove_property(&n.to_string()); +/// JavaScript `Array` built-in implementation. +#[derive(Debug, Clone, Copy)] +pub(crate) struct Array; + +impl Array { + /// Creates a new `Array` instance. + pub(crate) fn new_array(interpreter: &Interpreter) -> ResultValue { + let array = Value::new_object(Some( + &interpreter + .realm() + .environment + .get_global_object() + .expect("Could not get global object"), + )); + array.set_kind(ObjectKind::Array); + array.borrow().set_internal_slot( + INSTANCE_PROTOTYPE, + interpreter + .realm() + .environment + .get_binding_value("Array") + .borrow() + .get_field(PROTOTYPE), + ); + array.borrow().set_field("length", Value::from(0)); + Ok(array) } - // Create length - let length = Property::new() - .value(Value::from(array_contents.len() as i32)) - .writable(true) - .configurable(false) - .enumerable(false); + /// Utility function for creating array objects. + /// + /// `array_obj` can be any array with prototype already set (it will be wiped and + /// recreated from `array_contents`) + pub(crate) fn construct_array(array_obj: &Value, array_contents: &[Value]) -> ResultValue { + let array_obj_ptr = array_obj.clone(); + + // Wipe existing contents of the array object + let orig_length = i32::from(&array_obj.get_field("length")); + for n in 0..orig_length { + array_obj_ptr.remove_property(&n.to_string()); + } + + // Create length + let length = Property::new() + .value(Value::from(array_contents.len() as i32)) + .writable(true) + .configurable(false) + .enumerable(false); - array_obj_ptr.set_property("length".to_string(), length); + array_obj_ptr.set_property("length".to_string(), length); - for (n, value) in array_contents.iter().enumerate() { - array_obj_ptr.set_field_slice(&n.to_string(), value.clone()); + for (n, value) in array_contents.iter().enumerate() { + array_obj_ptr.set_field(n.to_string(), value); + } + Ok(array_obj_ptr) } - Ok(array_obj_ptr) -} -/// Utility function which takes an existing array object and puts additional -/// values on the end, correctly rewriting the length -pub(crate) fn add_to_array_object(array_ptr: &Value, add_values: &[Value]) -> ResultValue { - let orig_length = i32::from(&array_ptr.get_field_slice("length")); + /// Utility function which takes an existing array object and puts additional + /// values on the end, correctly rewriting the length + pub(crate) fn add_to_array_object(array_ptr: &Value, add_values: &[Value]) -> ResultValue { + let orig_length = i32::from(&array_ptr.get_field("length")); - for (n, value) in add_values.iter().enumerate() { - let new_index = orig_length.wrapping_add(n as i32); - array_ptr.set_field_slice(&new_index.to_string(), value.clone()); - } + for (n, value) in add_values.iter().enumerate() { + let new_index = orig_length.wrapping_add(n as i32); + array_ptr.set_field(new_index.to_string(), value); + } - array_ptr.set_field_slice( - "length", - Value::from(orig_length.wrapping_add(add_values.len() as i32)), - ); + array_ptr.set_field( + "length", + Value::from(orig_length.wrapping_add(add_values.len() as i32)), + ); - Ok(array_ptr.clone()) -} + Ok(array_ptr.clone()) + } -/// Create a new array -pub fn make_array(this: &mut Value, args: &[Value], ctx: &mut Interpreter) -> ResultValue { - // Make a new Object which will internally represent the Array (mapping - // between indices and values): this creates an Object with no prototype - - // Set Prototype - let prototype = ctx - .realm - .global_obj - .get_field_slice("Array") - .get_field_slice(PROTOTYPE); - - this.set_internal_slot(INSTANCE_PROTOTYPE, prototype); - // This value is used by console.log and other routines to match Object type - // to its Javascript Identifier (global constructor method name) - this.set_kind(ObjectKind::Array); - - // add our arguments in - let mut length = args.len() as i32; - match args.len() { - 1 if args[0].is_integer() => { - length = i32::from(&args[0]); - // TODO: It should not create an array of undefineds, but an empty array ("holy" array in V8) with length `n`. - for n in 0..length { - this.set_field_slice(&n.to_string(), Value::undefined()); + /// Create a new array + pub(crate) fn make_array( + this: &mut Value, + args: &[Value], + ctx: &mut Interpreter, + ) -> ResultValue { + // Make a new Object which will internally represent the Array (mapping + // between indices and values): this creates an Object with no prototype + + // Set Prototype + let prototype = ctx.realm.global_obj.get_field("Array").get_field(PROTOTYPE); + + this.set_internal_slot(INSTANCE_PROTOTYPE, prototype); + // This value is used by console.log and other routines to match Object type + // to its Javascript Identifier (global constructor method name) + this.set_kind(ObjectKind::Array); + + // add our arguments in + let mut length = args.len() as i32; + match args.len() { + 1 if args[0].is_integer() => { + length = i32::from(&args[0]); + // TODO: It should not create an array of undefineds, but an empty array ("holy" array in V8) with length `n`. + for n in 0..length { + this.set_field(n.to_string(), Value::undefined()); + } } - } - 1 if args[0].is_double() => { - // TODO: throw `RangeError`. - panic!("RangeError: invalid array length"); - } - _ => { - for (n, value) in args.iter().enumerate() { - this.set_field_slice(&n.to_string(), value.clone()); + 1 if args[0].is_double() => { + return Err(RangeError::run_new("invalid array length", ctx)?); + } + _ => { + for (n, value) in args.iter().enumerate() { + this.set_field(n.to_string(), value.clone()); + } } } - } - // finally create length property - let length = Property::new() - .value(Value::from(length)) - .writable(true) - .configurable(false) - .enumerable(false); + // finally create length property + let length = Property::new() + .value(Value::from(length)) + .writable(true) + .configurable(false) + .enumerable(false); - this.set_property("length".to_string(), length); + this.set_property("length".to_string(), length); - Ok(this.clone()) -} + Ok(this.clone()) + } -/// `Array.isArray( arg )` -/// -/// The isArray function takes one argument arg, and returns the Boolean value true -/// if the argument is an object whose class internal property is "Array"; otherwise it returns false. -/// -/// More information: -/// - [ECMAScript reference][spec] -/// - [MDN documentation][mdn] -/// -/// [spec]: https://tc39.es/ecma262/#sec-array.isarray -/// [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/isArray -pub fn is_array(_this: &mut Value, args: &[Value], _interpreter: &mut Interpreter) -> ResultValue { - let value_true = Value::boolean(true); - let value_false = Value::boolean(false); - - match args.get(0) { - Some(arg) => { - match arg.data() { - // 1. - ValueData::Object(ref obj) => { - // 2. - if (*obj).deref().borrow().kind == ObjectKind::Array { - return Ok(value_true); + /// `Array.isArray( arg )` + /// + /// The isArray function takes one argument arg, and returns the Boolean value true + /// if the argument is an object whose class internal property is "Array"; otherwise it returns false. + /// + /// More information: + /// - [ECMAScript reference][spec] + /// - [MDN documentation][mdn] + /// + /// [spec]: https://tc39.es/ecma262/#sec-array.isarray + /// [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/isArray + pub(crate) fn is_array( + _this: &mut Value, + args: &[Value], + _interpreter: &mut Interpreter, + ) -> ResultValue { + let value_true = Value::boolean(true); + let value_false = Value::boolean(false); + + match args.get(0) { + Some(arg) => { + match arg.data() { + // 1. + ValueData::Object(ref obj) => { + // 2. + if (*obj).deref().borrow().kind == ObjectKind::Array { + return Ok(value_true); + } + Ok(value_false) } - Ok(value_false) + // 3. + _ => Ok(value_false), } - // 3. - _ => Ok(value_false), } + None => Ok(value_false), } - None => Ok(value_false), } -} -/// `Array.prototype.concat(...arguments)` -/// -/// When the concat method is called with zero or more arguments, it returns an -/// array containing the array elements of the object followed by the array -/// elements of each argument in order. -/// -/// More information: -/// - [ECMAScript reference][spec] -/// - [MDN documentation][mdn] -/// -/// [spec]: https://tc39.es/ecma262/#sec-array.prototype.concat -/// [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/concat -pub fn concat(this: &mut Value, args: &[Value], _: &mut Interpreter) -> ResultValue { - if args.is_empty() { - // If concat is called with no arguments, it returns the original array - return Ok(this.clone()); - } + /// `Array.prototype.concat(...arguments)` + /// + /// When the concat method is called with zero or more arguments, it returns an + /// array containing the array elements of the object followed by the array + /// elements of each argument in order. + /// + /// More information: + /// - [ECMAScript reference][spec] + /// - [MDN documentation][mdn] + /// + /// [spec]: https://tc39.es/ecma262/#sec-array.prototype.concat + /// [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/concat + pub(crate) fn concat(this: &mut Value, args: &[Value], _: &mut Interpreter) -> ResultValue { + if args.is_empty() { + // If concat is called with no arguments, it returns the original array + return Ok(this.clone()); + } - // Make a new array (using this object as the prototype basis for the new - // one) - let mut new_values: Vec = Vec::new(); + // Make a new array (using this object as the prototype basis for the new + // one) + let mut new_values: Vec = Vec::new(); - let this_length = i32::from(&this.get_field_slice("length")); - for n in 0..this_length { - new_values.push(this.get_field_slice(&n.to_string())); - } - - for concat_array in args { - let concat_length = i32::from(&concat_array.get_field_slice("length")); - for n in 0..concat_length { - new_values.push(concat_array.get_field_slice(&n.to_string())); + let this_length = i32::from(&this.get_field("length")); + for n in 0..this_length { + new_values.push(this.get_field(n.to_string())); } - } - construct_array(this, &new_values) -} + for concat_array in args { + let concat_length = i32::from(&concat_array.get_field("length")); + for n in 0..concat_length { + new_values.push(concat_array.get_field(n.to_string())); + } + } -/// `Array.prototype.push( ...items )` -/// -/// The arguments are appended to the end of the array, in the order in which -/// they appear. The new length of the array is returned as the result of the -/// call. -/// -/// More information: -/// - [ECMAScript reference][spec] -/// - [MDN documentation][mdn] -/// -/// [spec]: https://tc39.es/ecma262/#sec-array.prototype.push -/// [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/push -pub fn push(this: &mut Value, args: &[Value], _: &mut Interpreter) -> ResultValue { - let new_array = add_to_array_object(this, args)?; - Ok(new_array.get_field_slice("length")) -} + Self::construct_array(this, &new_values) + } -/// `Array.prototype.pop()` -/// -/// The last element of the array is removed from the array and returned. -/// -/// More information: -/// - [ECMAScript reference][spec] -/// - [MDN documentation][mdn] -/// -/// [spec]: https://tc39.es/ecma262/#sec-array.prototype.pop -/// [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/pop -pub fn pop(this: &mut Value, _: &[Value], _: &mut Interpreter) -> ResultValue { - let curr_length = i32::from(&this.get_field_slice("length")); - if curr_length < 1 { - return Ok(Value::undefined()); + /// `Array.prototype.push( ...items )` + /// + /// The arguments are appended to the end of the array, in the order in which + /// they appear. The new length of the array is returned as the result of the + /// call. + /// + /// More information: + /// - [ECMAScript reference][spec] + /// - [MDN documentation][mdn] + /// + /// [spec]: https://tc39.es/ecma262/#sec-array.prototype.push + /// [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/push + pub(crate) fn push(this: &mut Value, args: &[Value], _: &mut Interpreter) -> ResultValue { + let new_array = Self::add_to_array_object(this, args)?; + Ok(new_array.get_field("length")) } - let pop_index = curr_length.wrapping_sub(1); - let pop_value: Value = this.get_field_slice(&pop_index.to_string()); - this.remove_property(&pop_index.to_string()); - this.set_field_slice("length", Value::from(pop_index)); - Ok(pop_value) -} -/// `Array.prototype.forEach( callbackFn [ , thisArg ] )` -/// -/// This method executes the provided callback function for each element in the array. -/// -/// More information: -/// - [ECMAScript reference][spec] -/// - [MDN documentation][mdn] -/// -/// [spec]: https://tc39.es/ecma262/#sec-array.prototype.foreach -/// [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/forEach -pub fn for_each(this: &mut Value, args: &[Value], interpreter: &mut Interpreter) -> ResultValue { - if args.is_empty() { - return Err(Value::from("Missing argument for Array.prototype.forEach")); + /// `Array.prototype.pop()` + /// + /// The last element of the array is removed from the array and returned. + /// + /// More information: + /// - [ECMAScript reference][spec] + /// - [MDN documentation][mdn] + /// + /// [spec]: https://tc39.es/ecma262/#sec-array.prototype.pop + /// [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/pop + pub(crate) fn pop(this: &mut Value, _: &[Value], _: &mut Interpreter) -> ResultValue { + let curr_length = i32::from(&this.get_field("length")); + if curr_length < 1 { + return Ok(Value::undefined()); + } + let pop_index = curr_length.wrapping_sub(1); + let pop_value: Value = this.get_field(pop_index.to_string()); + this.remove_property(&pop_index.to_string()); + this.set_field("length", Value::from(pop_index)); + Ok(pop_value) } - let callback_arg = args.get(0).expect("Could not get `callbackFn` argument."); - let mut this_arg = args.get(1).cloned().unwrap_or_else(Value::undefined); + /// `Array.prototype.forEach( callbackFn [ , thisArg ] )` + /// + /// This method executes the provided callback function for each element in the array. + /// + /// More information: + /// - [ECMAScript reference][spec] + /// - [MDN documentation][mdn] + /// + /// [spec]: https://tc39.es/ecma262/#sec-array.prototype.foreach + /// [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/forEach + pub(crate) fn for_each( + this: &mut Value, + args: &[Value], + interpreter: &mut Interpreter, + ) -> ResultValue { + if args.is_empty() { + return Err(Value::from("Missing argument for Array.prototype.forEach")); + } - let length = i32::from(&this.get_field_slice("length")); + let callback_arg = args.get(0).expect("Could not get `callbackFn` argument."); + let mut this_arg = args.get(1).cloned().unwrap_or_else(Value::undefined); - for i in 0..length { - let element = this.get_field_slice(&i.to_string()); - let arguments = [element, Value::from(i), this.clone()]; + let length = i32::from(&this.get_field("length")); - interpreter.call(callback_arg, &mut this_arg, &arguments)?; - } + for i in 0..length { + let element = this.get_field(i.to_string()); + let arguments = [element, Value::from(i), this.clone()]; - Ok(Value::undefined()) -} + interpreter.call(callback_arg, &mut this_arg, &arguments)?; + } -/// `Array.prototype.join( separator )` -/// -/// The elements of the array are converted to Strings, and these Strings are -/// then concatenated, separated by occurrences of the separator. If no -/// separator is provided, a single comma is used as the separator. -/// -/// More information: -/// - [ECMAScript reference][spec] -/// - [MDN documentation][mdn] -/// -/// [spec]: https://tc39.es/ecma262/#sec-array.prototype.join -/// [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/join -pub fn join(this: &mut Value, args: &[Value], _: &mut Interpreter) -> ResultValue { - let separator = if args.is_empty() { - String::from(",") - } else { - args.get(0).expect("Could not get argument").to_string() - }; - - let mut elem_strs: Vec = Vec::new(); - let length = i32::from(&this.get_field_slice("length")); - for n in 0..length { - let elem_str: String = this.get_field_slice(&n.to_string()).to_string(); - elem_strs.push(elem_str); + Ok(Value::undefined()) } - Ok(Value::from(elem_strs.join(&separator))) -} + /// `Array.prototype.join( separator )` + /// + /// The elements of the array are converted to Strings, and these Strings are + /// then concatenated, separated by occurrences of the separator. If no + /// separator is provided, a single comma is used as the separator. + /// + /// More information: + /// - [ECMAScript reference][spec] + /// - [MDN documentation][mdn] + /// + /// [spec]: https://tc39.es/ecma262/#sec-array.prototype.join + /// [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/join + pub(crate) fn join(this: &mut Value, args: &[Value], _: &mut Interpreter) -> ResultValue { + let separator = if args.is_empty() { + String::from(",") + } else { + args.get(0).expect("Could not get argument").to_string() + }; + + let mut elem_strs: Vec = Vec::new(); + let length = i32::from(&this.get_field("length")); + for n in 0..length { + let elem_str: String = this.get_field(n.to_string()).to_string(); + elem_strs.push(elem_str); + } -/// `Array.prototype.toString( separator )` -/// -/// The toString function is intentionally generic; it does not require that -/// its this value be an Array object. Therefore it can be transferred to -/// other kinds of objects for use as a method. -/// -/// More information: -/// - [ECMAScript reference][spec] -/// - [MDN documentation][mdn] -/// -/// [spec]: https://tc39.es/ecma262/#sec-array.prototype.tostring -/// [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/toString -pub fn to_string(this: &mut Value, _args: &[Value], _ctx: &mut Interpreter) -> ResultValue { - let method_name = "join"; - let mut arguments = vec![Value::from(",")]; - // 2. - let mut method = this.get_field_slice(method_name); - // 3. - if !method.is_function() { - method = _ctx - .realm - .global_obj - .get_field_slice("Object") - .get_field_slice(PROTOTYPE) - .get_field_slice("toString"); - - arguments = Vec::new(); + Ok(Value::from(elem_strs.join(&separator))) } - // 4. - let join_result = _ctx.call(&method, this, &arguments); - let match_string = match join_result { - Ok(v) => match *v { - ValueData::String(ref s) => (*s).clone(), - _ => "".to_string(), - }, - Err(v) => format!("error: {}", v), - }; - Ok(Value::from(match_string)) -} -/// `Array.prototype.reverse()` -/// -/// The elements of the array are rearranged so as to reverse their order. -/// The object is returned as the result of the call. -/// -/// More information: -/// - [ECMAScript reference][spec] -/// - [MDN documentation][mdn] -/// -/// [spec]: https://tc39.es/ecma262/#sec-array.prototype.reverse -/// [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/reverse -#[allow(clippy::else_if_without_else)] -pub fn reverse(this: &mut Value, _: &[Value], _: &mut Interpreter) -> ResultValue { - let len = i32::from(&this.get_field_slice("length")); - let middle: i32 = len.wrapping_div(2); - - for lower in 0..middle { - let upper = len.wrapping_sub(lower).wrapping_sub(1); - - let upper_exists = this.has_field(&upper.to_string()); - let lower_exists = this.has_field(&lower.to_string()); - - let upper_value = this.get_field_slice(&upper.to_string()); - let lower_value = this.get_field_slice(&lower.to_string()); - - if upper_exists && lower_exists { - this.set_field_slice(&upper.to_string(), lower_value); - this.set_field_slice(&lower.to_string(), upper_value); - } else if upper_exists { - this.set_field_slice(&lower.to_string(), upper_value); - this.remove_property(&upper.to_string()); - } else if lower_exists { - this.set_field_slice(&upper.to_string(), lower_value); - this.remove_property(&lower.to_string()); + /// `Array.prototype.toString( separator )` + /// + /// The toString function is intentionally generic; it does not require that + /// its this value be an Array object. Therefore it can be transferred to + /// other kinds of objects for use as a method. + /// + /// More information: + /// - [ECMAScript reference][spec] + /// - [MDN documentation][mdn] + /// + /// [spec]: https://tc39.es/ecma262/#sec-array.prototype.tostring + /// [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/toString + #[allow(clippy::wrong_self_convention)] + pub(crate) fn to_string( + this: &mut Value, + _args: &[Value], + _ctx: &mut Interpreter, + ) -> ResultValue { + let method_name = "join"; + let mut arguments = vec![Value::from(",")]; + // 2. + let mut method = this.get_field(method_name); + // 3. + if !method.is_function() { + method = _ctx + .realm + .global_obj + .get_field("Object") + .get_field(PROTOTYPE) + .get_field("toString"); + + arguments = Vec::new(); } + // 4. + let join_result = _ctx.call(&method, this, &arguments); + let match_string = match join_result { + Ok(v) => match *v { + ValueData::String(ref s) => (*s).clone(), + _ => "".to_string(), + }, + Err(v) => format!("error: {}", v), + }; + Ok(Value::from(match_string)) } - Ok(this.clone()) -} + /// `Array.prototype.reverse()` + /// + /// The elements of the array are rearranged so as to reverse their order. + /// The object is returned as the result of the call. + /// + /// More information: + /// - [ECMAScript reference][spec] + /// - [MDN documentation][mdn] + /// + /// [spec]: https://tc39.es/ecma262/#sec-array.prototype.reverse + /// [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/reverse + #[allow(clippy::else_if_without_else)] + pub(crate) fn reverse(this: &mut Value, _: &[Value], _: &mut Interpreter) -> ResultValue { + let len = i32::from(&this.get_field("length")); + let middle: i32 = len.wrapping_div(2); + + for lower in 0..middle { + let upper = len.wrapping_sub(lower).wrapping_sub(1); + + let upper_exists = this.has_field(&upper.to_string()); + let lower_exists = this.has_field(&lower.to_string()); + + let upper_value = this.get_field(upper.to_string()); + let lower_value = this.get_field(lower.to_string()); + + if upper_exists && lower_exists { + this.set_field(upper.to_string(), lower_value); + this.set_field(lower.to_string(), upper_value); + } else if upper_exists { + this.set_field(lower.to_string(), upper_value); + this.remove_property(&upper.to_string()); + } else if lower_exists { + this.set_field(upper.to_string(), lower_value); + this.remove_property(&lower.to_string()); + } + } -/// `Array.prototype.shift()` -/// -/// The first element of the array is removed from the array and returned. -/// -/// More information: -/// - [ECMAScript reference][spec] -/// - [MDN documentation][mdn] -/// -/// [spec]: https://tc39.es/ecma262/#sec-array.prototype.shift -/// [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/shift -pub fn shift(this: &mut Value, _: &[Value], _: &mut Interpreter) -> ResultValue { - let len = i32::from(&this.get_field_slice("length")); - - if len == 0 { - this.set_field_slice("length", Value::from(0)); - // Since length is 0, this will be an Undefined value - return Ok(this.get_field_slice(&0.to_string())); + Ok(this.clone()) } - let first: Value = this.get_field_slice(&0.to_string()); - - for k in 1..len { - let from = k.to_string(); - let to = (k.wrapping_sub(1)).to_string(); - - let from_value = this.get_field_slice(&from); - if from_value.is_undefined() { - this.remove_property(&to); - } else { - this.set_field_slice(&to, from_value); + /// `Array.prototype.shift()` + /// + /// The first element of the array is removed from the array and returned. + /// + /// More information: + /// - [ECMAScript reference][spec] + /// - [MDN documentation][mdn] + /// + /// [spec]: https://tc39.es/ecma262/#sec-array.prototype.shift + /// [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/shift + pub(crate) fn shift(this: &mut Value, _: &[Value], _: &mut Interpreter) -> ResultValue { + let len = i32::from(&this.get_field("length")); + + if len == 0 { + this.set_field("length", Value::from(0)); + // Since length is 0, this will be an Undefined value + return Ok(this.get_field(0.to_string())); } - } - let final_index = len.wrapping_sub(1); - this.remove_property(&(final_index).to_string()); - this.set_field_slice("length", Value::from(final_index)); + let first: Value = this.get_field(0.to_string()); - Ok(first) -} + for k in 1..len { + let from = k.to_string(); + let to = (k.wrapping_sub(1)).to_string(); -/// `Array.prototype.unshift( ...items )` -/// -/// The arguments are prepended to the start of the array, such that their order -/// within the array is the same as the order in which they appear in the -/// argument list. -/// -/// More information: -/// - [ECMAScript reference][spec] -/// - [MDN documentation][mdn] -/// -/// [spec]: https://tc39.es/ecma262/#sec-array.prototype.unshift -/// [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/unshift -pub fn unshift(this: &mut Value, args: &[Value], _: &mut Interpreter) -> ResultValue { - let len = i32::from(&this.get_field_slice("length")); - let arg_c: i32 = args.len() as i32; - - if arg_c > 0 { - for k in (1..=len).rev() { - let from = (k.wrapping_sub(1)).to_string(); - let to = (k.wrapping_add(arg_c).wrapping_sub(1)).to_string(); - - let from_value = this.get_field_slice(&from); + let from_value = this.get_field(from); if from_value.is_undefined() { this.remove_property(&to); } else { - this.set_field_slice(&to, from_value); + this.set_field(to, from_value); } } - for j in 0..arg_c { - this.set_field_slice( - &j.to_string(), - args.get(j as usize) - .expect("Could not get argument") - .clone(), - ); - } - } - let temp = len.wrapping_add(arg_c); - this.set_field_slice("length", Value::from(temp)); - Ok(Value::from(temp)) -} + let final_index = len.wrapping_sub(1); + this.remove_property(&(final_index).to_string()); + this.set_field("length", Value::from(final_index)); -/// `Array.prototype.every( callback, [ thisArg ] )` -/// -/// The every method executes the provided callback function once for each -/// element present in the array until it finds the one where callback returns -/// a falsy value. It returns `false` if it finds such element, otherwise it -/// returns `true`. -/// -/// More information: -/// - [ECMAScript reference][spec] -/// - [MDN documentation][mdn] -/// -/// [spec]: https://tc39.es/ecma262/#sec-array.prototype.every -/// [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/every -pub fn every(this: &mut Value, args: &[Value], interpreter: &mut Interpreter) -> ResultValue { - if args.is_empty() { - return Err(Value::from( - "missing callback when calling function Array.prototype.every", - )); + Ok(first) } - let callback = &args[0]; - let mut this_arg = if args.len() > 1 { - args[1].clone() - } else { - Value::undefined() - }; - let mut i = 0; - let max_len = i32::from(&this.get_field_slice("length")); - let mut len = max_len; - while i < len { - let element = this.get_field_slice(&i.to_string()); - let arguments = [element, Value::from(i), this.clone()]; - let result = interpreter - .call(callback, &mut this_arg, &arguments)? - .is_true(); - if !result { - return Ok(Value::from(false)); - } - len = min(max_len, i32::from(&this.get_field_slice("length"))); - i += 1; + + /// `Array.prototype.unshift( ...items )` + /// + /// The arguments are prepended to the start of the array, such that their order + /// within the array is the same as the order in which they appear in the + /// argument list. + /// + /// More information: + /// - [ECMAScript reference][spec] + /// - [MDN documentation][mdn] + /// + /// [spec]: https://tc39.es/ecma262/#sec-array.prototype.unshift + /// [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/unshift + pub(crate) fn unshift(this: &mut Value, args: &[Value], _: &mut Interpreter) -> ResultValue { + let len = i32::from(&this.get_field("length")); + let arg_c: i32 = args.len() as i32; + + if arg_c > 0 { + for k in (1..=len).rev() { + let from = (k.wrapping_sub(1)).to_string(); + let to = (k.wrapping_add(arg_c).wrapping_sub(1)).to_string(); + + let from_value = this.get_field(from); + if from_value.is_undefined() { + this.remove_property(&to); + } else { + this.set_field(to, from_value); + } + } + for j in 0..arg_c { + this.set_field( + j.to_string(), + args.get(j as usize) + .expect("Could not get argument") + .clone(), + ); + } + } + + let temp = len.wrapping_add(arg_c); + this.set_field("length", Value::from(temp)); + Ok(Value::from(temp)) } - Ok(Value::from(true)) -} -/// `Array.prototype.map( callback, [ thisArg ] )` -/// -/// For each element in the array the callback function is called, and a new -/// array is constructed from the return values of these calls. -/// -/// More information: -/// - [ECMAScript reference][spec] -/// - [MDN documentation][mdn] -/// -/// [spec]: https://tc39.es/ecma262/#sec-array.prototype.map -/// [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/map -pub fn map(this: &mut Value, args: &[Value], interpreter: &mut Interpreter) -> ResultValue { - if args.is_empty() { - return Err(Value::from( - "missing argument 0 when calling function Array.prototype.map", - )); + /// `Array.prototype.every( callback, [ thisArg ] )` + /// + /// The every method executes the provided callback function once for each + /// element present in the array until it finds the one where callback returns + /// a falsy value. It returns `false` if it finds such element, otherwise it + /// returns `true`. + /// + /// More information: + /// - [ECMAScript reference][spec] + /// - [MDN documentation][mdn] + /// + /// [spec]: https://tc39.es/ecma262/#sec-array.prototype.every + /// [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/every + pub(crate) fn every( + this: &mut Value, + args: &[Value], + interpreter: &mut Interpreter, + ) -> ResultValue { + if args.is_empty() { + return Err(Value::from( + "missing callback when calling function Array.prototype.every", + )); + } + let callback = &args[0]; + let mut this_arg = if args.len() > 1 { + args[1].clone() + } else { + Value::undefined() + }; + let mut i = 0; + let max_len = i32::from(&this.get_field("length")); + let mut len = max_len; + while i < len { + let element = this.get_field(i.to_string()); + let arguments = [element, Value::from(i), this.clone()]; + let result = interpreter + .call(callback, &mut this_arg, &arguments)? + .is_true(); + if !result { + return Ok(Value::from(false)); + } + len = min(max_len, i32::from(&this.get_field("length"))); + i += 1; + } + Ok(Value::from(true)) } - let callback = args.get(0).cloned().unwrap_or_else(Value::undefined); - let mut this_val = args.get(1).cloned().unwrap_or_else(Value::undefined); + /// `Array.prototype.map( callback, [ thisArg ] )` + /// + /// For each element in the array the callback function is called, and a new + /// array is constructed from the return values of these calls. + /// + /// More information: + /// - [ECMAScript reference][spec] + /// - [MDN documentation][mdn] + /// + /// [spec]: https://tc39.es/ecma262/#sec-array.prototype.map + /// [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/map + pub(crate) fn map( + this: &mut Value, + args: &[Value], + interpreter: &mut Interpreter, + ) -> ResultValue { + if args.is_empty() { + return Err(Value::from( + "missing argument 0 when calling function Array.prototype.map", + )); + } - let length = i32::from(&this.get_field_slice("length")); + let callback = args.get(0).cloned().unwrap_or_else(Value::undefined); + let mut this_val = args.get(1).cloned().unwrap_or_else(Value::undefined); - let new = new_array(&interpreter)?; + let length = i32::from(&this.get_field("length")); - let values: Vec = (0..length) - .map(|idx| { - let element = this.get_field_slice(&idx.to_string()); - let args = [element, Value::from(idx), new.clone()]; + let new = Self::new_array(interpreter)?; - interpreter - .call(&callback, &mut this_val, &args) - .unwrap_or_else(|_| Value::undefined()) - }) - .collect(); + let values: Vec = (0..length) + .map(|idx| { + let element = this.get_field(idx.to_string()); + let args = [element, Value::from(idx), new.clone()]; - construct_array(&new, &values) -} + interpreter + .call(&callback, &mut this_val, &args) + .unwrap_or_else(|_| Value::undefined()) + }) + .collect(); -/// `Array.prototype.indexOf( searchElement[, fromIndex ] )` -/// -/// -/// indexOf compares searchElement to the elements of the array, in ascending order, -/// using the Strict Equality Comparison algorithm, and if found at one or more indices, -/// returns the smallest such index; otherwise, -1 is returned. -/// -/// The optional second argument fromIndex defaults to 0 (i.e. the whole array is searched). -/// If it is greater than or equal to the length of the array, -1 is returned, -/// i.e. the array will not be searched. If it is negative, it is used as the offset -/// from the end of the array to compute fromIndex. If the computed index is less than 0, -/// the whole array will be searched. -/// -/// More information: -/// - [ECMAScript reference][spec] -/// - [MDN documentation][mdn] -/// -/// [spec]: https://tc39.es/ecma262/#sec-array.prototype.indexof -/// [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/indexOf -pub fn index_of(this: &mut Value, args: &[Value], _: &mut Interpreter) -> ResultValue { - // If no arguments, return -1. Not described in spec, but is what chrome does. - if args.is_empty() { - return Ok(Value::from(-1)); + Self::construct_array(&new, &values) } - let search_element = args[0].clone(); - let len = i32::from(&this.get_field_slice("length")); + /// `Array.prototype.indexOf( searchElement[, fromIndex ] )` + /// + /// + /// indexOf compares searchElement to the elements of the array, in ascending order, + /// using the Strict Equality Comparison algorithm, and if found at one or more indices, + /// returns the smallest such index; otherwise, -1 is returned. + /// + /// The optional second argument fromIndex defaults to 0 (i.e. the whole array is searched). + /// If it is greater than or equal to the length of the array, -1 is returned, + /// i.e. the array will not be searched. If it is negative, it is used as the offset + /// from the end of the array to compute fromIndex. If the computed index is less than 0, + /// the whole array will be searched. + /// + /// More information: + /// - [ECMAScript reference][spec] + /// - [MDN documentation][mdn] + /// + /// [spec]: https://tc39.es/ecma262/#sec-array.prototype.indexof + /// [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/indexOf + pub(crate) fn index_of(this: &mut Value, args: &[Value], _: &mut Interpreter) -> ResultValue { + // If no arguments, return -1. Not described in spec, but is what chrome does. + if args.is_empty() { + return Ok(Value::from(-1)); + } - let mut idx = match args.get(1) { - Some(from_idx_ptr) => { - let from_idx = i32::from(from_idx_ptr); + let search_element = args[0].clone(); + let len = i32::from(&this.get_field("length")); - if from_idx < 0 { - len + from_idx - } else { - from_idx + let mut idx = match args.get(1) { + Some(from_idx_ptr) => { + let from_idx = i32::from(from_idx_ptr); + + if from_idx < 0 { + len + from_idx + } else { + from_idx + } } - } - None => 0, - }; + None => 0, + }; - while idx < len { - let check_element = this.get_field_slice(&idx.to_string()).clone(); + while idx < len { + let check_element = this.get_field(idx.to_string()).clone(); - if check_element.strict_equals(&search_element) { - return Ok(Value::from(idx)); + if check_element.strict_equals(&search_element) { + return Ok(Value::from(idx)); + } + + idx += 1; } - idx += 1; + Ok(Value::from(-1)) } - Ok(Value::from(-1)) -} - -/// `Array.prototype.lastIndexOf( searchElement[, fromIndex ] )` -/// -/// -/// lastIndexOf compares searchElement to the elements of the array in descending order -/// using the Strict Equality Comparison algorithm, and if found at one or more indices, -/// returns the largest such index; otherwise, -1 is returned. -/// -/// The optional second argument fromIndex defaults to the array's length minus one -/// (i.e. the whole array is searched). If it is greater than or equal to the length of the array, -/// the whole array will be searched. If it is negative, it is used as the offset from the end -/// of the array to compute fromIndex. If the computed index is less than 0, -1 is returned. -/// -/// More information: -/// - [ECMAScript reference][spec] -/// - [MDN documentation][mdn] -/// -/// [spec]: https://tc39.es/ecma262/#sec-array.prototype.lastindexof -/// [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/lastIndexOf -pub fn last_index_of(this: &mut Value, args: &[Value], _: &mut Interpreter) -> ResultValue { - // If no arguments, return -1. Not described in spec, but is what chrome does. - if args.is_empty() { - return Ok(Value::from(-1)); - } + /// `Array.prototype.lastIndexOf( searchElement[, fromIndex ] )` + /// + /// + /// lastIndexOf compares searchElement to the elements of the array in descending order + /// using the Strict Equality Comparison algorithm, and if found at one or more indices, + /// returns the largest such index; otherwise, -1 is returned. + /// + /// The optional second argument fromIndex defaults to the array's length minus one + /// (i.e. the whole array is searched). If it is greater than or equal to the length of the array, + /// the whole array will be searched. If it is negative, it is used as the offset from the end + /// of the array to compute fromIndex. If the computed index is less than 0, -1 is returned. + /// + /// More information: + /// - [ECMAScript reference][spec] + /// - [MDN documentation][mdn] + /// + /// [spec]: https://tc39.es/ecma262/#sec-array.prototype.lastindexof + /// [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/lastIndexOf + pub(crate) fn last_index_of( + this: &mut Value, + args: &[Value], + _: &mut Interpreter, + ) -> ResultValue { + // If no arguments, return -1. Not described in spec, but is what chrome does. + if args.is_empty() { + return Ok(Value::from(-1)); + } - let search_element = args[0].clone(); - let len = i32::from(&this.get_field_slice("length")); + let search_element = args[0].clone(); + let len = i32::from(&this.get_field("length")); - let mut idx = match args.get(1) { - Some(from_idx_ptr) => { - let from_idx = i32::from(from_idx_ptr); + let mut idx = match args.get(1) { + Some(from_idx_ptr) => { + let from_idx = i32::from(from_idx_ptr); - if from_idx >= 0 { - min(from_idx, len - 1) - } else { - len + from_idx + if from_idx >= 0 { + min(from_idx, len - 1) + } else { + len + from_idx + } } - } - None => len - 1, - }; + None => len - 1, + }; - while idx >= 0 { - let check_element = this.get_field_slice(&idx.to_string()).clone(); + while idx >= 0 { + let check_element = this.get_field(idx.to_string()).clone(); - if check_element.strict_equals(&search_element) { - return Ok(Value::from(idx)); + if check_element.strict_equals(&search_element) { + return Ok(Value::from(idx)); + } + + idx -= 1; } - idx -= 1; + Ok(Value::from(-1)) } - Ok(Value::from(-1)) -} - -/// `Array.prototype.find( callback, [thisArg] )` -/// -/// The find method executes the callback function once for each index of the array -/// until the callback returns a truthy value. If so, find immediately returns the value -/// of that element. Otherwise, find returns undefined. -/// -/// More information: -/// - [ECMAScript reference][spec] -/// - [MDN documentation][mdn] -/// -/// [spec]: https://tc39.es/ecma262/#sec-array.prototype.find -/// [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/find -pub fn find(this: &mut Value, args: &[Value], interpreter: &mut Interpreter) -> ResultValue { - if args.is_empty() { - return Err(Value::from( - "missing callback when calling function Array.prototype.find", - )); - } - let callback = &args[0]; - let mut this_arg = if args.len() > 1 { - args[1].clone() - } else { - Value::undefined() - }; - let len = i32::from(&this.get_field_slice("length")); - for i in 0..len { - let element = this.get_field_slice(&i.to_string()); - let arguments = [element.clone(), Value::from(i), this.clone()]; - let result = interpreter.call(callback, &mut this_arg, &arguments)?; - if result.is_true() { - return Ok(element); + /// `Array.prototype.find( callback, [thisArg] )` + /// + /// The find method executes the callback function once for each index of the array + /// until the callback returns a truthy value. If so, find immediately returns the value + /// of that element. Otherwise, find returns undefined. + /// + /// More information: + /// - [ECMAScript reference][spec] + /// - [MDN documentation][mdn] + /// + /// [spec]: https://tc39.es/ecma262/#sec-array.prototype.find + /// [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/find + pub(crate) fn find( + this: &mut Value, + args: &[Value], + interpreter: &mut Interpreter, + ) -> ResultValue { + if args.is_empty() { + return Err(Value::from( + "missing callback when calling function Array.prototype.find", + )); + } + let callback = &args[0]; + let mut this_arg = if args.len() > 1 { + args[1].clone() + } else { + Value::undefined() + }; + let len = i32::from(&this.get_field("length")); + for i in 0..len { + let element = this.get_field(i.to_string()); + let arguments = [element.clone(), Value::from(i), this.clone()]; + let result = interpreter.call(callback, &mut this_arg, &arguments)?; + if result.is_true() { + return Ok(element); + } } + Ok(Value::undefined()) } - Ok(Value::undefined()) -} -/// `Array.prototype.findIndex( predicate [ , thisArg ] )` -/// -/// This method executes the provided predicate function for each element of the array. -/// If the predicate function returns `true` for an element, this method returns the index of the element. -/// If all elements return `false`, the value `-1` is returned. -/// -/// More information: -/// - [ECMAScript reference][spec] -/// - [MDN documentation][mdn] -/// -/// [spec]: https://tc39.es/ecma262/#sec-array.prototype.findindex -/// [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/findIndex -pub fn find_index(this: &mut Value, args: &[Value], interpreter: &mut Interpreter) -> ResultValue { - if args.is_empty() { - return Err(Value::from( - "Missing argument for Array.prototype.findIndex", - )); - } + /// `Array.prototype.findIndex( predicate [ , thisArg ] )` + /// + /// This method executes the provided predicate function for each element of the array. + /// If the predicate function returns `true` for an element, this method returns the index of the element. + /// If all elements return `false`, the value `-1` is returned. + /// + /// More information: + /// - [ECMAScript reference][spec] + /// - [MDN documentation][mdn] + /// + /// [spec]: https://tc39.es/ecma262/#sec-array.prototype.findindex + /// [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/findIndex + pub(crate) fn find_index( + this: &mut Value, + args: &[Value], + interpreter: &mut Interpreter, + ) -> ResultValue { + if args.is_empty() { + return Err(Value::from( + "Missing argument for Array.prototype.findIndex", + )); + } - let predicate_arg = args.get(0).expect("Could not get `predicate` argument."); + let predicate_arg = args.get(0).expect("Could not get `predicate` argument."); - let mut this_arg = args.get(1).cloned().unwrap_or_else(Value::undefined); + let mut this_arg = args.get(1).cloned().unwrap_or_else(Value::undefined); - let length = i32::from(&this.get_field_slice("length")); + let length = i32::from(&this.get_field("length")); - for i in 0..length { - let element = this.get_field_slice(&i.to_string()); - let arguments = [element, Value::from(i), this.clone()]; + for i in 0..length { + let element = this.get_field(i.to_string()); + let arguments = [element, Value::from(i), this.clone()]; - let result = interpreter.call(predicate_arg, &mut this_arg, &arguments)?; + let result = interpreter.call(predicate_arg, &mut this_arg, &arguments)?; - if result.is_true() { - return Ok(Value::rational(f64::from(i))); + if result.is_true() { + return Ok(Value::rational(f64::from(i))); + } } - } - - Ok(Value::rational(-1_f64)) -} -/// `Array.prototype.fill( value[, start[, end]] )` -/// -/// The method fills (modifies) all the elements of an array from start index (default 0) -/// to an end index (default array length) with a static value. It returns the modified array. -/// -/// More information: -/// - [ECMAScript reference][spec] -/// - [MDN documentation][mdn] -/// -/// [spec]: https://tc39.es/ecma262/#sec-array.prototype.fill -/// [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/fill -pub fn fill(this: &mut Value, args: &[Value], _: &mut Interpreter) -> ResultValue { - let len: i32 = i32::from(&this.get_field_slice("length")); - let default_value = Value::undefined(); - let value = args.get(0).unwrap_or(&default_value); - let relative_start = args.get(1).unwrap_or(&default_value).to_number() as i32; - let relative_end_val = args.get(2).unwrap_or(&default_value); - let relative_end = if relative_end_val.is_undefined() { - len - } else { - relative_end_val.to_number() as i32 - }; - let start = if relative_start < 0 { - max(len + relative_start, 0) - } else { - min(relative_start, len) - }; - let fin = if relative_end < 0 { - max(len + relative_end, 0) - } else { - min(relative_end, len) - }; - - for i in start..fin { - this.set_field_slice(&i.to_string(), value.clone()); + Ok(Value::rational(-1_f64)) } - Ok(this.clone()) -} - -/// `Array.prototype.includes( valueToFind [, fromIndex] )` -/// -/// Determines whether an array includes a certain value among its entries, returning `true` or `false` as appropriate. -/// -/// More information: -/// - [ECMAScript reference][spec] -/// - [MDN documentation][mdn] -/// -/// [spec]: https://tc39.es/ecma262/#sec-array.prototype.includes -/// [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/includes -pub fn includes_value(this: &mut Value, args: &[Value], _: &mut Interpreter) -> ResultValue { - let search_element = args.get(0).cloned().unwrap_or_else(Value::undefined); - - let length = i32::from(&this.get_field_slice("length")); - - for idx in 0..length { - let check_element = this.get_field_slice(&idx.to_string()).clone(); + /// `Array.prototype.fill( value[, start[, end]] )` + /// + /// The method fills (modifies) all the elements of an array from start index (default 0) + /// to an end index (default array length) with a static value. It returns the modified array. + /// + /// More information: + /// - [ECMAScript reference][spec] + /// - [MDN documentation][mdn] + /// + /// [spec]: https://tc39.es/ecma262/#sec-array.prototype.fill + /// [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/fill + pub(crate) fn fill(this: &mut Value, args: &[Value], _: &mut Interpreter) -> ResultValue { + let len: i32 = i32::from(&this.get_field("length")); + let default_value = Value::undefined(); + let value = args.get(0).unwrap_or(&default_value); + let relative_start = args.get(1).unwrap_or(&default_value).to_number() as i32; + let relative_end_val = args.get(2).unwrap_or(&default_value); + let relative_end = if relative_end_val.is_undefined() { + len + } else { + relative_end_val.to_number() as i32 + }; + let start = if relative_start < 0 { + max(len + relative_start, 0) + } else { + min(relative_start, len) + }; + let fin = if relative_end < 0 { + max(len + relative_end, 0) + } else { + min(relative_end, len) + }; - if same_value_zero(&check_element, &search_element) { - return Ok(Value::from(true)); + for i in start..fin { + this.set_field(i.to_string(), value.clone()); } + + Ok(this.clone()) } - Ok(Value::from(false)) -} + /// `Array.prototype.includes( valueToFind [, fromIndex] )` + /// + /// Determines whether an array includes a certain value among its entries, returning `true` or `false` as appropriate. + /// + /// More information: + /// - [ECMAScript reference][spec] + /// - [MDN documentation][mdn] + /// + /// [spec]: https://tc39.es/ecma262/#sec-array.prototype.includes + /// [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/includes + pub(crate) fn includes_value( + this: &mut Value, + args: &[Value], + _: &mut Interpreter, + ) -> ResultValue { + let search_element = args.get(0).cloned().unwrap_or_else(Value::undefined); + + let length = i32::from(&this.get_field("length")); + + for idx in 0..length { + let check_element = this.get_field(idx.to_string()).clone(); + + if same_value_zero(&check_element, &search_element) { + return Ok(Value::from(true)); + } + } -/// `Array.prototype.slice( [begin[, end]] )` -/// -/// The slice method takes two arguments, start and end, and returns an array containing the -/// elements of the array from element start up to, but not including, element end (or through the -/// end of the array if end is undefined). If start is negative, it is treated as length + start -/// where length is the length of the array. If end is negative, it is treated as length + end where -/// length is the length of the array. -/// -/// More information: -/// - [ECMAScript reference][spec] -/// - [MDN documentation][mdn] -/// -/// [spec]: https://tc39.es/ecma262/#sec-array.prototype.slice -/// [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/slice -pub fn slice(this: &mut Value, args: &[Value], interpreter: &mut Interpreter) -> ResultValue { - let new_array = new_array(interpreter)?; - let len = i32::from(&this.get_field_slice("length")); - - let start = match args.get(0) { - Some(v) => i32::from(v), - None => 0, - }; - let end = match args.get(1) { - Some(v) => i32::from(v), - None => len, - }; - - let from = if start < 0 { - max(len.wrapping_add(start), 0) - } else { - min(start, len) - }; - let to = if end < 0 { - max(len.wrapping_add(end), 0) - } else { - min(end, len) - }; - - let span = max(to.wrapping_sub(from), 0); - let mut new_array_len: i32 = 0; - for i in from..from.wrapping_add(span) { - new_array.set_field_slice( - &new_array_len.to_string(), - this.get_field_slice(&i.to_string()), - ); - new_array_len = new_array_len.wrapping_add(1); + Ok(Value::from(false)) } - new_array.set_field_slice("length", Value::from(new_array_len)); - Ok(new_array) -} -/// `Array.prototype.filter( callback, [ thisArg ] )` -/// -/// For each element in the array the callback function is called, and a new -/// array is constructed for every value whose callback returned a truthy value. -/// -/// More information: -/// - [ECMAScript reference][spec] -/// - [MDN documentation][mdn] -/// -/// [spec]: https://tc39.es/ecma262/#sec-array.prototype.filter -/// [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/filter -pub fn filter(this: &mut Value, args: &[Value], interpreter: &mut Interpreter) -> ResultValue { - if args.is_empty() { - return Err(Value::from( - "missing argument 0 when calling function Array.prototype.filter", - )); + /// `Array.prototype.slice( [begin[, end]] )` + /// + /// The slice method takes two arguments, start and end, and returns an array containing the + /// elements of the array from element start up to, but not including, element end (or through the + /// end of the array if end is undefined). If start is negative, it is treated as length + start + /// where length is the length of the array. If end is negative, it is treated as length + end where + /// length is the length of the array. + /// + /// More information: + /// - [ECMAScript reference][spec] + /// - [MDN documentation][mdn] + /// + /// [spec]: https://tc39.es/ecma262/#sec-array.prototype.slice + /// [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/slice + pub(crate) fn slice( + this: &mut Value, + args: &[Value], + interpreter: &mut Interpreter, + ) -> ResultValue { + let new_array = Self::new_array(interpreter)?; + let len = i32::from(&this.get_field("length")); + + let start = match args.get(0) { + Some(v) => i32::from(v), + None => 0, + }; + let end = match args.get(1) { + Some(v) => i32::from(v), + None => len, + }; + + let from = if start < 0 { + max(len.wrapping_add(start), 0) + } else { + min(start, len) + }; + let to = if end < 0 { + max(len.wrapping_add(end), 0) + } else { + min(end, len) + }; + + let span = max(to.wrapping_sub(from), 0); + let mut new_array_len: i32 = 0; + for i in from..from.wrapping_add(span) { + new_array.set_field(new_array_len.to_string(), this.get_field(i.to_string())); + new_array_len = new_array_len.wrapping_add(1); + } + new_array.set_field("length", Value::from(new_array_len)); + Ok(new_array) } - let callback = args.get(0).cloned().unwrap_or_else(Value::undefined); - let mut this_val = args.get(1).cloned().unwrap_or_else(Value::undefined); + /// `Array.prototype.filter( callback, [ thisArg ] )` + /// + /// For each element in the array the callback function is called, and a new + /// array is constructed for every value whose callback returned a truthy value. + /// + /// More information: + /// - [ECMAScript reference][spec] + /// - [MDN documentation][mdn] + /// + /// [spec]: https://tc39.es/ecma262/#sec-array.prototype.filter + /// [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/filter + pub(crate) fn filter( + this: &mut Value, + args: &[Value], + interpreter: &mut Interpreter, + ) -> ResultValue { + if args.is_empty() { + return Err(Value::from( + "missing argument 0 when calling function Array.prototype.filter", + )); + } - let length = i32::from(&this.get_field_slice("length")); + let callback = args.get(0).cloned().unwrap_or_else(Value::undefined); + let mut this_val = args.get(1).cloned().unwrap_or_else(Value::undefined); - let new = new_array(&interpreter)?; + let length = i32::from(&this.get_field("length")); - let values = (0..length) - .filter_map(|idx| { - let element = this.get_field_slice(&idx.to_string()); + let new = Self::new_array(interpreter)?; - let args = [element.clone(), Value::from(idx), new.clone()]; + let values = (0..length) + .filter_map(|idx| { + let element = this.get_field(idx.to_string()); - let callback_result = interpreter - .call(&callback, &mut this_val, &args) - .unwrap_or_else(|_| Value::undefined()); + let args = [element.clone(), Value::from(idx), new.clone()]; - if callback_result.is_true() { - Some(element) - } else { - None - } - }) - .collect::>(); + let callback_result = interpreter + .call(&callback, &mut this_val, &args) + .unwrap_or_else(|_| Value::undefined()); - construct_array(&new, &values) -} + if callback_result.is_true() { + Some(element) + } else { + None + } + }) + .collect::>(); -/// Array.prototype.some ( callbackfn [ , thisArg ] ) -/// -/// The some method tests whether at least one element in the array passes -/// the test implemented by the provided callback function. It returns a Boolean value, -/// true if the callback function returns a truthy value for at least one element -/// in the array. Otherwise, false. -/// -/// Caution: Calling this method on an empty array returns false for any condition! -/// -/// More information: -/// - [ECMAScript reference][spec] -/// - [MDN documentation][mdn] -/// -/// [spec]: https://tc39.es/ecma262/#sec-array.prototype.some -/// [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/some -pub fn some(this: &mut Value, args: &[Value], interpreter: &mut Interpreter) -> ResultValue { - if args.is_empty() { - return Err(Value::from( - "missing callback when calling function Array.prototype.some", - )); + Self::construct_array(&new, &values) } - let callback = &args[0]; - let mut this_arg = if args.len() > 1 { - args[1].clone() - } else { - Value::undefined() - }; - let mut i = 0; - let max_len = i32::from(&this.get_field_slice("length")); - let mut len = max_len; - while i < len { - let element = this.get_field_slice(&i.to_string()); - let arguments = [element, Value::from(i), this.clone()]; - let result = interpreter - .call(callback, &mut this_arg, &arguments)? - .is_true(); - if result { - return Ok(Value::from(true)); - } - // the length of the array must be updated because the callback can mutate it. - len = min(max_len, i32::from(&this.get_field_slice("length"))); - i += 1; + + /// Array.prototype.some ( callbackfn [ , thisArg ] ) + /// + /// The some method tests whether at least one element in the array passes + /// the test implemented by the provided callback function. It returns a Boolean value, + /// true if the callback function returns a truthy value for at least one element + /// in the array. Otherwise, false. + /// + /// Caution: Calling this method on an empty array returns false for any condition! + /// + /// More information: + /// - [ECMAScript reference][spec] + /// - [MDN documentation][mdn] + /// + /// [spec]: https://tc39.es/ecma262/#sec-array.prototype.some + /// [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/some + pub(crate) fn some( + this: &mut Value, + args: &[Value], + interpreter: &mut Interpreter, + ) -> ResultValue { + if args.is_empty() { + return Err(Value::from( + "missing callback when calling function Array.prototype.some", + )); + } + let callback = &args[0]; + let mut this_arg = if args.len() > 1 { + args[1].clone() + } else { + Value::undefined() + }; + let mut i = 0; + let max_len = i32::from(&this.get_field("length")); + let mut len = max_len; + while i < len { + let element = this.get_field(i.to_string()); + let arguments = [element, Value::from(i), this.clone()]; + let result = interpreter + .call(callback, &mut this_arg, &arguments)? + .is_true(); + if result { + return Ok(Value::from(true)); + } + // the length of the array must be updated because the callback can mutate it. + len = min(max_len, i32::from(&this.get_field("length"))); + i += 1; + } + Ok(Value::from(false)) } - Ok(Value::from(false)) -} -/// Create a new `Array` object. -pub fn create(global: &Value) -> Value { - // Create prototype - let prototype = Value::new_object(None); - let length = Property::default().value(Value::from(0)); - - prototype.set_property_slice("length", length); - - make_builtin_fn!(concat, named "concat", with length 1, of prototype); - make_builtin_fn!(push, named "push", with length 1, of prototype); - make_builtin_fn!(index_of, named "indexOf", with length 1, of prototype); - make_builtin_fn!(last_index_of, named "lastIndexOf", with length 1, of prototype); - make_builtin_fn!(includes_value, named "includes", with length 1, of prototype); - make_builtin_fn!(map, named "map", with length 1, of prototype); - make_builtin_fn!(fill, named "fill", with length 1, of prototype); - make_builtin_fn!(for_each, named "forEach", with length 1, of prototype); - make_builtin_fn!(filter, named "filter", with length 1, of prototype); - make_builtin_fn!(pop, named "pop", of prototype); - make_builtin_fn!(join, named "join", with length 1, of prototype); - make_builtin_fn!(to_string, named "toString", of prototype); - make_builtin_fn!(reverse, named "reverse", of prototype); - make_builtin_fn!(shift, named "shift", of prototype); - make_builtin_fn!(unshift, named "unshift", with length 1, of prototype); - make_builtin_fn!(every, named "every", with length 1, of prototype); - make_builtin_fn!(find, named "find", with length 1, of prototype); - make_builtin_fn!(find_index, named "findIndex", with length 1, of prototype); - make_builtin_fn!(slice, named "slice", with length 2, of prototype); - make_builtin_fn!(some, named "some", with length 2, of prototype); - - let array = make_constructor_fn(make_array, global, prototype); - - // Static Methods - make_builtin_fn!(is_array, named "isArray", with length 1, of array); - - array -} + /// Create a new `Array` object. + pub(crate) fn create(global: &Value) -> Value { + // Create prototype + let prototype = Value::new_object(None); + let length = Property::default().value(Value::from(0)); + + prototype.set_property_slice("length", length); + + make_builtin_fn(Self::concat, "concat", &prototype, 1); + make_builtin_fn(Self::push, "push", &prototype, 1); + make_builtin_fn(Self::index_of, "indexOf", &prototype, 1); + make_builtin_fn(Self::last_index_of, "lastIndexOf", &prototype, 1); + make_builtin_fn(Self::includes_value, "includes", &prototype, 1); + make_builtin_fn(Self::map, "map", &prototype, 1); + make_builtin_fn(Self::fill, "fill", &prototype, 1); + make_builtin_fn(Self::for_each, "forEach", &prototype, 1); + make_builtin_fn(Self::filter, "filter", &prototype, 1); + make_builtin_fn(Self::pop, "pop", &prototype, 0); + make_builtin_fn(Self::join, "join", &prototype, 1); + make_builtin_fn(Self::to_string, "toString", &prototype, 0); + make_builtin_fn(Self::reverse, "reverse", &prototype, 0); + make_builtin_fn(Self::shift, "shift", &prototype, 0); + make_builtin_fn(Self::unshift, "unshift", &prototype, 1); + make_builtin_fn(Self::every, "every", &prototype, 1); + make_builtin_fn(Self::find, "find", &prototype, 1); + make_builtin_fn(Self::find_index, "findIndex", &prototype, 1); + make_builtin_fn(Self::slice, "slice", &prototype, 2); + make_builtin_fn(Self::some, "some", &prototype, 2); + + let array = make_constructor_fn(Self::make_array, global, prototype); + + // Static Methods + make_builtin_fn(Self::is_array, "isArray", &array, 1); + + array + } -/// Initialise the `Array` object on the global object. -#[inline] -pub fn init(global: &Value) { - global.set_field_slice("Array", create(global)); + /// Initialise the `Array` object on the global object. + #[inline] + pub(crate) fn init(global: &Value) { + global.set_field("Array", Self::create(global)); + } } diff --git a/boa/src/builtins/array/tests.rs b/boa/src/builtins/array/tests.rs index 3319aa94fa..3794361817 100644 --- a/boa/src/builtins/array/tests.rs +++ b/boa/src/builtins/array/tests.rs @@ -1,11 +1,9 @@ -use crate::exec::Executor; -use crate::forward; -use crate::realm::Realm; +use crate::{exec::Interpreter, forward, realm::Realm}; #[test] fn is_array() { let realm = Realm::create(); - let mut engine = Executor::new(realm); + let mut engine = Interpreter::new(realm); let init = r#" var empty = []; var new_arr = new Array(); @@ -46,7 +44,7 @@ fn is_array() { fn concat() { //TODO: array display formatter // let realm = Realm::create(); - // let mut engine = Executor::new(realm); + // let mut engine = Interpreter::new(realm); // let init = r#" // var empty = new Array(); // var one = new Array(1); @@ -69,7 +67,7 @@ fn concat() { #[test] fn join() { let realm = Realm::create(); - let mut engine = Executor::new(realm); + let mut engine = Interpreter::new(realm); let init = r#" var empty = [ ]; var one = ["a"]; @@ -90,7 +88,7 @@ fn join() { #[test] fn to_string() { let realm = Realm::create(); - let mut engine = Executor::new(realm); + let mut engine = Interpreter::new(realm); let init = r#" var empty = [ ]; var one = ["a"]; @@ -111,7 +109,7 @@ fn to_string() { #[test] fn every() { let realm = Realm::create(); - let mut engine = Executor::new(realm); + let mut engine = Interpreter::new(realm); // taken from https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/every let init = r#" var empty = []; @@ -156,7 +154,7 @@ fn every() { #[test] fn find() { let realm = Realm::create(); - let mut engine = Executor::new(realm); + let mut engine = Interpreter::new(realm); let init = r#" function comp(a) { return a == "a"; @@ -171,7 +169,7 @@ fn find() { #[test] fn find_index() { let realm = Realm::create(); - let mut engine = Executor::new(realm); + let mut engine = Interpreter::new(realm); let code = r#" function comp(item) { @@ -197,7 +195,7 @@ fn find_index() { #[test] fn push() { let realm = Realm::create(); - let mut engine = Executor::new(realm); + let mut engine = Interpreter::new(realm); let init = r#" var arr = [1, 2]; "#; @@ -212,7 +210,7 @@ fn push() { #[test] fn pop() { let realm = Realm::create(); - let mut engine = Executor::new(realm); + let mut engine = Interpreter::new(realm); let init = r#" var empty = [ ]; var one = [1]; @@ -234,7 +232,7 @@ fn pop() { #[test] fn shift() { let realm = Realm::create(); - let mut engine = Executor::new(realm); + let mut engine = Interpreter::new(realm); let init = r#" var empty = [ ]; var one = [1]; @@ -256,7 +254,7 @@ fn shift() { #[test] fn unshift() { let realm = Realm::create(); - let mut engine = Executor::new(realm); + let mut engine = Interpreter::new(realm); let init = r#" var arr = [3, 4]; "#; @@ -271,7 +269,7 @@ fn unshift() { #[test] fn reverse() { let realm = Realm::create(); - let mut engine = Executor::new(realm); + let mut engine = Interpreter::new(realm); let init = r#" var arr = [1, 2]; var reversed = arr.reverse(); @@ -286,7 +284,7 @@ fn reverse() { #[test] fn index_of() { let realm = Realm::create(); - let mut engine = Executor::new(realm); + let mut engine = Interpreter::new(realm); let init = r#" var empty = [ ]; var one = ["a"]; @@ -350,7 +348,7 @@ fn index_of() { #[test] fn last_index_of() { let realm = Realm::create(); - let mut engine = Executor::new(realm); + let mut engine = Interpreter::new(realm); let init = r#" var empty = [ ]; var one = ["a"]; @@ -414,7 +412,7 @@ fn last_index_of() { #[test] fn fill() { let realm = Realm::create(); - let mut engine = Executor::new(realm); + let mut engine = Interpreter::new(realm); forward(&mut engine, "var a = [1, 2, 3];"); assert_eq!( @@ -511,7 +509,7 @@ fn fill() { #[test] fn includes_value() { let realm = Realm::create(); - let mut engine = Executor::new(realm); + let mut engine = Interpreter::new(realm); let init = r#" var empty = [ ]; var one = ["a"]; @@ -550,7 +548,7 @@ fn includes_value() { #[test] fn map() { let realm = Realm::create(); - let mut engine = Executor::new(realm); + let mut engine = Interpreter::new(realm); let js = r#" var empty = []; @@ -611,7 +609,7 @@ fn map() { #[test] fn slice() { let realm = Realm::create(); - let mut engine = Executor::new(realm); + let mut engine = Interpreter::new(realm); let init = r#" var empty = [ ].slice(); var one = ["a"].slice(); @@ -635,7 +633,7 @@ fn slice() { #[test] fn for_each() { let realm = Realm::create(); - let mut engine = Executor::new(realm); + let mut engine = Interpreter::new(realm); let init = r#" var a = [2, 3, 4, 5]; var sum = 0; @@ -658,7 +656,7 @@ fn for_each() { #[test] fn for_each_push_value() { let realm = Realm::create(); - let mut engine = Executor::new(realm); + let mut engine = Interpreter::new(realm); let init = r#" var a = [1, 2, 3, 4]; function callingCallback(item, index, list) { @@ -679,7 +677,7 @@ fn for_each_push_value() { #[test] fn filter() { let realm = Realm::create(); - let mut engine = Executor::new(realm); + let mut engine = Interpreter::new(realm); let js = r#" var empty = []; @@ -746,7 +744,7 @@ fn filter() { #[test] fn some() { let realm = Realm::create(); - let mut engine = Executor::new(realm); + let mut engine = Interpreter::new(realm); let init = r#" var empty = []; @@ -795,7 +793,7 @@ fn some() { #[test] fn call_array_constructor_with_one_argument() { let realm = Realm::create(); - let mut engine = Executor::new(realm); + let mut engine = Interpreter::new(realm); let init = r#" var empty = new Array(0); diff --git a/boa/src/builtins/bigint/mod.rs b/boa/src/builtins/bigint/mod.rs index 403aae28d9..7ff326915c 100644 --- a/boa/src/builtins/bigint/mod.rs +++ b/boa/src/builtins/bigint/mod.rs @@ -14,93 +14,122 @@ use crate::{ builtins::{ - function::make_constructor_fn, + function::{make_builtin_fn, make_constructor_fn}, value::{ResultValue, Value}, + RangeError, }, exec::Interpreter, - syntax::ast::bigint::BigInt, + syntax::ast::bigint::BigInt as AstBigInt, }; #[cfg(test)] mod tests; -/// `BigInt()` -/// -/// The `BigInt()` constructor is used to create BigInt objects. -/// -/// More information: -/// - [ECMAScript reference][spec] -/// - [MDN documentation][mdn] -/// -/// [spec]: https://tc39.es/ecma262/#sec-bigint-objects -/// [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/BigInt/BigInt -pub fn make_bigint(_this: &mut Value, args: &[Value], _ctx: &mut Interpreter) -> ResultValue { - let data = match args.get(0) { - Some(ref value) => { - if let Some(bigint) = value.to_bigint() { - Value::from(bigint) - } else { - panic!("RangeError: The value cannot be converted to a BigInt because it is not an integer"); +/// `BigInt` implementation. +#[derive(Debug, Clone, Copy)] +pub(crate) struct BigInt; + +impl BigInt { + /// `BigInt()` + /// + /// The `BigInt()` constructor is used to create BigInt objects. + /// + /// More information: + /// - [ECMAScript reference][spec] + /// - [MDN documentation][mdn] + /// + /// [spec]: https://tc39.es/ecma262/#sec-bigint-objects + /// [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/BigInt/BigInt + pub(crate) fn make_bigint( + _this: &mut Value, + args: &[Value], + ctx: &mut Interpreter, + ) -> ResultValue { + let data = match args.get(0) { + Some(ref value) => { + if let Some(bigint) = value.to_bigint() { + Value::from(bigint) + } else { + return Err(RangeError::run_new( + format!( + "{} can't be converted to BigInt because it isn't an integer", + value + ), + ctx, + )?); + } } - } - None => Value::from(BigInt::from(0)), - }; - Ok(data) -} + None => Value::from(AstBigInt::from(0)), + }; + Ok(data) + } -/// `BigInt.prototype.toString( [radix] )` -/// -/// The `toString()` method returns a string representing the specified BigInt object. -/// -/// More information: -/// - [ECMAScript reference][spec] -/// - [MDN documentation][mdn] -/// -/// [spec]: https://tc39.es/ecma262/#sec-bigint.prototype.tostring -/// [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/BigInt/toString -pub fn to_string(this: &mut Value, args: &[Value], _ctx: &mut Interpreter) -> ResultValue { - let radix = if !args.is_empty() { - args[0].to_integer() - } else { - 10 - }; - if radix < 2 && radix > 36 { - panic!("RangeError: toString() radix argument must be between 2 and 36"); + /// `BigInt.prototype.toString( [radix] )` + /// + /// The `toString()` method returns a string representing the specified BigInt object. + /// + /// More information: + /// - [ECMAScript reference][spec] + /// - [MDN documentation][mdn] + /// + /// [spec]: https://tc39.es/ecma262/#sec-bigint.prototype.tostring + /// [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/BigInt/toString + #[allow(clippy::wrong_self_convention)] + pub(crate) fn to_string( + this: &mut Value, + args: &[Value], + ctx: &mut Interpreter, + ) -> ResultValue { + let radix = if !args.is_empty() { + args[0].to_integer() + } else { + 10 + }; + if radix < 2 && radix > 36 { + return Err(RangeError::run_new( + "radix must be an integer at least 2 and no greater than 36", + ctx, + )?); + } + Ok(Value::from( + this.to_bigint().unwrap().to_str_radix(radix as u32), + )) } - Ok(Value::from( - this.to_bigint().unwrap().to_str_radix(radix as u32), - )) -} -// /// `BigInt.prototype.valueOf()` -// /// -// /// The `valueOf()` method returns the wrapped primitive value of a Number object. -// /// -// /// More information: -// /// - [ECMAScript reference][spec] -// /// - [MDN documentation][mdn] -// /// -/// [spec]: https://tc39.es/ecma262/#sec-bigint.prototype.valueof -/// [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/BigInt/valueOf -pub fn value_of(this: &mut Value, _args: &[Value], _ctx: &mut Interpreter) -> ResultValue { - Ok(Value::from( - this.to_bigint().expect("BigInt.prototype.valueOf"), - )) -} + // /// `BigInt.prototype.valueOf()` + // /// + // /// The `valueOf()` method returns the wrapped primitive value of a Number object. + // /// + // /// More information: + // /// - [ECMAScript reference][spec] + // /// - [MDN documentation][mdn] + // /// + /// [spec]: https://tc39.es/ecma262/#sec-bigint.prototype.valueof + /// [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/BigInt/valueOf + pub(crate) fn value_of( + this: &mut Value, + _args: &[Value], + _ctx: &mut Interpreter, + ) -> ResultValue { + Ok(Value::from( + this.to_bigint().expect("BigInt.prototype.valueOf"), + )) + } -/// Create a new `Number` object -pub fn create(global: &Value) -> Value { - let prototype = Value::new_object(Some(global)); - prototype.set_internal_slot("BigIntData", Value::from(BigInt::from(0))); + /// Create a new `Number` object + pub(crate) fn create(global: &Value) -> Value { + let prototype = Value::new_object(Some(global)); + prototype.set_internal_slot("BigIntData", Value::from(AstBigInt::from(0))); - make_builtin_fn!(to_string, named "toString", with length 1, of prototype); - make_builtin_fn!(value_of, named "valueOf", of prototype); + make_builtin_fn(Self::to_string, "toString", &prototype, 1); + make_builtin_fn(Self::value_of, "valueOf", &prototype, 0); - make_constructor_fn(make_bigint, global, prototype) -} + make_constructor_fn(Self::make_bigint, global, prototype) + } -/// Initialise the `BigInt` object on the global object. -#[inline] -pub fn init(global: &Value) { - global.set_field_slice("BigInt", create(global)); + /// Initialise the `BigInt` object on the global object. + #[inline] + pub(crate) fn init(global: &Value) { + global.set_field("BigInt", Self::create(global)); + } } diff --git a/boa/src/builtins/bigint/tests.rs b/boa/src/builtins/bigint/tests.rs index 8bcfa7df52..e8f1b84d1d 100644 --- a/boa/src/builtins/bigint/tests.rs +++ b/boa/src/builtins/bigint/tests.rs @@ -1,9 +1,9 @@ -use crate::{forward, Executor, Realm}; +use crate::{forward, Interpreter, Realm}; #[test] fn equality() { let realm = Realm::create(); - let mut engine = Executor::new(realm); + let mut engine = Interpreter::new(realm); assert_eq!(forward(&mut engine, "0n == 0n"), "true"); assert_eq!(forward(&mut engine, "1n == 0n"), "false"); @@ -57,7 +57,7 @@ fn equality() { #[test] fn bigint_function_conversion() { let realm = Realm::create(); - let mut engine = Executor::new(realm); + let mut engine = Interpreter::new(realm); assert_eq!(forward(&mut engine, "BigInt(1000)"), "1000n"); assert_eq!( @@ -73,7 +73,7 @@ fn bigint_function_conversion() { #[test] fn add() { let realm = Realm::create(); - let mut engine = Executor::new(realm); + let mut engine = Interpreter::new(realm); assert_eq!(forward(&mut engine, "10000n + 1000n"), "11000n"); } @@ -81,7 +81,7 @@ fn add() { #[test] fn sub() { let realm = Realm::create(); - let mut engine = Executor::new(realm); + let mut engine = Interpreter::new(realm); assert_eq!(forward(&mut engine, "10000n - 1000n"), "9000n"); } @@ -89,7 +89,7 @@ fn sub() { #[test] fn mul() { let realm = Realm::create(); - let mut engine = Executor::new(realm); + let mut engine = Interpreter::new(realm); assert_eq!( forward(&mut engine, "123456789n * 102030n"), @@ -100,7 +100,7 @@ fn mul() { #[test] fn div() { let realm = Realm::create(); - let mut engine = Executor::new(realm); + let mut engine = Interpreter::new(realm); assert_eq!(forward(&mut engine, "15000n / 50n"), "300n"); } @@ -108,7 +108,7 @@ fn div() { #[test] fn div_with_truncation() { let realm = Realm::create(); - let mut engine = Executor::new(realm); + let mut engine = Interpreter::new(realm); assert_eq!(forward(&mut engine, "15001n / 50n"), "300n"); } @@ -116,7 +116,7 @@ fn div_with_truncation() { #[test] fn r#mod() { let realm = Realm::create(); - let mut engine = Executor::new(realm); + let mut engine = Interpreter::new(realm); assert_eq!(forward(&mut engine, "15007n % 10n"), "7n"); } @@ -124,7 +124,7 @@ fn r#mod() { #[test] fn pow() { let realm = Realm::create(); - let mut engine = Executor::new(realm); + let mut engine = Interpreter::new(realm); assert_eq!( forward(&mut engine, "100n ** 10n"), @@ -135,7 +135,7 @@ fn pow() { #[test] fn to_string() { let realm = Realm::create(); - let mut engine = Executor::new(realm); + let mut engine = Interpreter::new(realm); assert_eq!(forward(&mut engine, "1000n.toString()"), "1000"); diff --git a/boa/src/builtins/boolean/mod.rs b/boa/src/builtins/boolean/mod.rs index 58956cacda..f70ae4b742 100644 --- a/boa/src/builtins/boolean/mod.rs +++ b/boa/src/builtins/boolean/mod.rs @@ -12,7 +12,7 @@ #[cfg(test)] mod tests; -use super::function::make_constructor_fn; +use super::function::{make_builtin_fn, make_constructor_fn}; use crate::{ builtins::{ object::{internal_methods_trait::ObjectInternalMethods, ObjectKind}, @@ -22,93 +22,105 @@ use crate::{ }; use std::{borrow::Borrow, ops::Deref}; -/// `[[Construct]]` Create a new boolean object -/// -/// `[[Call]]` Creates a new boolean primitive -pub fn construct_boolean(this: &mut Value, args: &[Value], _: &mut Interpreter) -> ResultValue { - this.set_kind(ObjectKind::Boolean); +/// Boolean implementation. +#[derive(Debug, Clone, Copy)] +pub(crate) struct Boolean; - // Get the argument, if any - if let Some(ref value) = args.get(0) { - this.set_internal_slot("BooleanData", to_boolean(value)); - } else { - this.set_internal_slot("BooleanData", to_boolean(&Value::from(false))); - } +impl Boolean { + /// `[[Construct]]` Create a new boolean object + /// + /// `[[Call]]` Creates a new boolean primitive + pub(crate) fn construct_boolean( + this: &mut Value, + args: &[Value], + _: &mut Interpreter, + ) -> ResultValue { + this.set_kind(ObjectKind::Boolean); + + // Get the argument, if any + if let Some(ref value) = args.get(0) { + this.set_internal_slot("BooleanData", Self::to_boolean(value)); + } else { + this.set_internal_slot("BooleanData", Self::to_boolean(&Value::from(false))); + } - match args.get(0) { - Some(ref value) => Ok(to_boolean(value)), - None => Ok(to_boolean(&Value::from(false))), + match args.get(0) { + Some(ref value) => Ok(Self::to_boolean(value)), + None => Ok(Self::to_boolean(&Value::from(false))), + } } -} -/// The `toString()` method returns a string representing the specified `Boolean` object. -/// -/// More information: -/// - [ECMAScript reference][spec] -/// - [MDN documentation][mdn] -/// -/// [spec]: https://tc39.es/ecma262/#sec-boolean-object -/// [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Boolean/toString -pub fn to_string(this: &mut Value, _: &[Value], _: &mut Interpreter) -> ResultValue { - let b = this_boolean_value(this); - Ok(Value::from(b.to_string())) -} + /// The `toString()` method returns a string representing the specified `Boolean` object. + /// + /// More information: + /// - [ECMAScript reference][spec] + /// - [MDN documentation][mdn] + /// + /// [spec]: https://tc39.es/ecma262/#sec-boolean-object + /// [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Boolean/toString + #[allow(clippy::wrong_self_convention)] + pub(crate) fn to_string(this: &mut Value, _: &[Value], _: &mut Interpreter) -> ResultValue { + let b = Self::this_boolean_value(this); + Ok(Value::from(b.to_string())) + } -/// The valueOf() method returns the primitive value of a `Boolean` object. -/// -/// More information: -/// - [ECMAScript reference][spec] -/// - [MDN documentation][mdn] -/// -/// [spec]: https://tc39.es/ecma262/#sec-boolean.prototype.valueof -/// [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Boolean/valueOf -pub fn value_of(this: &mut Value, _: &[Value], _: &mut Interpreter) -> ResultValue { - Ok(this_boolean_value(this)) -} + /// The valueOf() method returns the primitive value of a `Boolean` object. + /// + /// More information: + /// - [ECMAScript reference][spec] + /// - [MDN documentation][mdn] + /// + /// [spec]: https://tc39.es/ecma262/#sec-boolean.prototype.valueof + /// [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Boolean/valueOf + pub(crate) fn value_of(this: &mut Value, _: &[Value], _: &mut Interpreter) -> ResultValue { + Ok(Self::this_boolean_value(this)) + } -// === Utility Functions === -/// [toBoolean](https://tc39.es/ecma262/#sec-toboolean) -/// Creates a new boolean value from the input -pub fn to_boolean(value: &Value) -> Value { - match *value.deref().borrow() { - ValueData::Object(_) => Value::from(true), - ValueData::String(ref s) if !s.is_empty() => Value::from(true), - ValueData::Rational(n) if n != 0.0 && !n.is_nan() => Value::from(true), - ValueData::Integer(n) if n != 0 => Value::from(true), - ValueData::Boolean(v) => Value::from(v), - _ => Value::from(false), + // === Utility Functions === + /// [toBoolean](https://tc39.es/ecma262/#sec-toboolean) + /// Creates a new boolean value from the input + #[allow(clippy::wrong_self_convention)] + pub(crate) fn to_boolean(value: &Value) -> Value { + match *value.deref().borrow() { + ValueData::Object(_) => Value::from(true), + ValueData::String(ref s) if !s.is_empty() => Value::from(true), + ValueData::Rational(n) if n != 0.0 && !n.is_nan() => Value::from(true), + ValueData::Integer(n) if n != 0 => Value::from(true), + ValueData::Boolean(v) => Value::from(v), + _ => Value::from(false), + } } -} -/// An Utility function used to get the internal BooleanData. -/// -/// More information: -/// - [ECMAScript reference][spec] -/// -/// [spec]: https://tc39.es/ecma262/#sec-thisbooleanvalue -pub fn this_boolean_value(value: &Value) -> Value { - match *value.deref().borrow() { - ValueData::Boolean(v) => Value::from(v), - ValueData::Object(ref v) => (v).deref().borrow().get_internal_slot("BooleanData"), - _ => Value::from(false), + /// An Utility function used to get the internal BooleanData. + /// + /// More information: + /// - [ECMAScript reference][spec] + /// + /// [spec]: https://tc39.es/ecma262/#sec-thisbooleanvalue + pub(crate) fn this_boolean_value(value: &Value) -> Value { + match *value.deref().borrow() { + ValueData::Boolean(v) => Value::from(v), + ValueData::Object(ref v) => (v).deref().borrow().get_internal_slot("BooleanData"), + _ => Value::from(false), + } } -} -/// Create a new `Boolean` object. -pub fn create(global: &Value) -> Value { - // Create Prototype - // https://tc39.es/ecma262/#sec-properties-of-the-boolean-prototype-object - let prototype = Value::new_object(Some(global)); - prototype.set_internal_slot("BooleanData", to_boolean(&Value::from(false))); + /// Create a new `Boolean` object. + pub(crate) fn create(global: &Value) -> Value { + // Create Prototype + // https://tc39.es/ecma262/#sec-properties-of-the-boolean-prototype-object + let prototype = Value::new_object(Some(global)); + prototype.set_internal_slot("BooleanData", Self::to_boolean(&Value::from(false))); - make_builtin_fn!(to_string, named "toString", of prototype); - make_builtin_fn!(value_of, named "valueOf", of prototype); + make_builtin_fn(Self::to_string, "toString", &prototype, 0); + make_builtin_fn(Self::value_of, "valueOf", &prototype, 0); - make_constructor_fn(construct_boolean, global, prototype) -} + make_constructor_fn(Self::construct_boolean, global, prototype) + } -/// Initialise the `Boolean` object on the global object. -#[inline] -pub fn init(global: &Value) { - global.set_field_slice("Boolean", create(global)); + /// Initialise the `Boolean` object on the global object. + #[inline] + pub(crate) fn init(global: &Value) { + global.set_field("Boolean", Self::create(global)); + } } diff --git a/boa/src/builtins/boolean/tests.rs b/boa/src/builtins/boolean/tests.rs index 69c3599a5b..200e3eaa37 100644 --- a/boa/src/builtins/boolean/tests.rs +++ b/boa/src/builtins/boolean/tests.rs @@ -1,12 +1,10 @@ use super::*; -use crate::exec::Executor; -use crate::realm::Realm; -use crate::{builtins::value::same_value, forward, forward_val}; +use crate::{builtins::value::same_value, exec::Interpreter, forward, forward_val, realm::Realm}; #[test] fn check_boolean_constructor_is_function() { let global = Value::new_object(None); - let boolean_constructor = create(&global); + let boolean_constructor = Boolean::create(&global); assert_eq!(boolean_constructor.is_function(), true); } @@ -15,7 +13,7 @@ fn check_boolean_constructor_is_function() { #[test] fn construct_and_call() { let realm = Realm::create(); - let mut engine = Executor::new(realm); + let mut engine = Interpreter::new(realm); let init = r#" var one = new Boolean(1); var zero = Boolean(0); @@ -31,7 +29,7 @@ fn construct_and_call() { #[test] fn constructor_gives_true_instance() { let realm = Realm::create(); - let mut engine = Executor::new(realm); + let mut engine = Interpreter::new(realm); let init = r#" var trueVal = new Boolean(true); var trueNum = new Boolean(1); @@ -61,7 +59,7 @@ fn constructor_gives_true_instance() { #[test] fn instances_have_correct_proto_set() { let realm = Realm::create(); - let mut engine = Executor::new(realm); + let mut engine = Interpreter::new(realm); let init = r#" var boolInstance = new Boolean(true); var boolProto = Boolean.prototype; diff --git a/boa/src/builtins/console/mod.rs b/boa/src/builtins/console/mod.rs index bd5cf26cea..b181e21745 100644 --- a/boa/src/builtins/console/mod.rs +++ b/boa/src/builtins/console/mod.rs @@ -18,6 +18,7 @@ mod tests; use crate::{ builtins::{ + function::make_builtin_fn, object::InternalState, value::{display_obj, ResultValue, Value}, }, @@ -491,25 +492,25 @@ pub fn dir(this: &mut Value, args: &[Value], _: &mut Interpreter) -> ResultValue pub fn create(global: &Value) -> Value { let console = Value::new_object(Some(global)); - make_builtin_fn!(assert, named "assert", of console); - make_builtin_fn!(clear, named "clear", of console); - make_builtin_fn!(debug, named "debug", of console); - make_builtin_fn!(error, named "error", of console); - make_builtin_fn!(info, named "info", of console); - make_builtin_fn!(log, named "log", of console); - make_builtin_fn!(trace, named "trace", of console); - make_builtin_fn!(warn, named "warn", of console); - make_builtin_fn!(error, named "exception", of console); - make_builtin_fn!(count, named "count", of console); - make_builtin_fn!(count_reset, named "countReset", of console); - make_builtin_fn!(group, named "group", of console); - make_builtin_fn!(group, named "groupCollapsed", of console); - make_builtin_fn!(group_end , named "groupEnd", of console); - make_builtin_fn!(time, named "time", of console); - make_builtin_fn!(time_log, named "timeLog", of console); - make_builtin_fn!(time_end, named "timeEnd", of console); - make_builtin_fn!(dir, named "dir", of console); - make_builtin_fn!(dir, named "dirxml", of console); + make_builtin_fn(assert, "assert", &console, 0); + make_builtin_fn(clear, "clear", &console, 0); + make_builtin_fn(debug, "debug", &console, 0); + make_builtin_fn(error, "error", &console, 0); + make_builtin_fn(info, "info", &console, 0); + make_builtin_fn(log, "log", &console, 0); + make_builtin_fn(trace, "trace", &console, 0); + make_builtin_fn(warn, "warn", &console, 0); + make_builtin_fn(error, "exception", &console, 0); + make_builtin_fn(count, "count", &console, 0); + make_builtin_fn(count_reset, "countReset", &console, 0); + make_builtin_fn(group, "group", &console, 0); + make_builtin_fn(group, "groupCollapsed", &console, 0); + make_builtin_fn(group_end, "groupEnd", &console, 0); + make_builtin_fn(time, "time", &console, 0); + make_builtin_fn(time_log, "timeLog", &console, 0); + make_builtin_fn(time_end, "timeEnd", &console, 0); + make_builtin_fn(dir, "dir", &console, 0); + make_builtin_fn(dir, "dirxml", &console, 0); console.set_internal_state(ConsoleState::default()); @@ -519,5 +520,5 @@ pub fn create(global: &Value) -> Value { /// Initialise the `console` object on the global object. #[inline] pub fn init(global: &Value) { - global.set_field_slice("console", create(global)); + global.set_field("console", create(global)); } diff --git a/boa/src/builtins/error.rs b/boa/src/builtins/error.rs deleted file mode 100644 index 35f4e91c44..0000000000 --- a/boa/src/builtins/error.rs +++ /dev/null @@ -1,68 +0,0 @@ -//! This module implements the global `Error` object. -//! -//! Error objects are thrown when runtime errors occur. -//! The Error object can also be used as a base object for user-defined exceptions. -//! -//! More information: -//! - [MDN documentation][mdn] -//! - [ECMAScript reference][spec] -//! -//! [spec]: https://tc39.es/ecma262/#sec-error-objects -//! [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Error - -use super::function::make_constructor_fn; -use crate::{ - builtins::{ - object::ObjectKind, - value::{ResultValue, Value}, - }, - exec::Interpreter, -}; - -/// Create a new error object. -pub fn make_error(this: &mut Value, args: &[Value], _: &mut Interpreter) -> ResultValue { - if !args.is_empty() { - this.set_field_slice( - "message", - Value::from( - args.get(0) - .expect("failed getting error message") - .to_string(), - ), - ); - } - // This value is used by console.log and other routines to match Object type - // to its Javascript Identifier (global constructor method name) - this.set_kind(ObjectKind::Error); - Ok(Value::undefined()) -} - -/// `Error.prototype.toString()` -/// -/// The toString() method returns a string representing the specified Error object. -/// -/// More information: -/// - [MDN documentation][mdn] -/// - [ECMAScript reference][spec] -/// -/// [spec]: https://tc39.es/ecma262/#sec-error.prototype.tostring -/// [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Error/toString -pub fn to_string(this: &mut Value, _: &[Value], _: &mut Interpreter) -> ResultValue { - let name = this.get_field_slice("name"); - let message = this.get_field_slice("message"); - Ok(Value::from(format!("{}: {}", name, message))) -} - -/// Create a new `Error` object. -pub fn create(global: &Value) -> Value { - let prototype = Value::new_object(Some(global)); - prototype.set_field_slice("message", Value::from("")); - prototype.set_field_slice("name", Value::from("Error")); - make_builtin_fn!(to_string, named "toString", of prototype); - make_constructor_fn(make_error, global, prototype) -} - -/// Initialise the global object with the `Error` object. -pub fn init(global: &Value) { - global.set_field_slice("Error", create(global)); -} diff --git a/boa/src/builtins/error/mod.rs b/boa/src/builtins/error/mod.rs new file mode 100644 index 0000000000..d430330fc3 --- /dev/null +++ b/boa/src/builtins/error/mod.rs @@ -0,0 +1,84 @@ +//! This module implements the global `Error` object. +//! +//! Error objects are thrown when runtime errors occur. +//! The Error object can also be used as a base object for user-defined exceptions. +//! +//! More information: +//! - [MDN documentation][mdn] +//! - [ECMAScript reference][spec] +//! +//! [spec]: https://tc39.es/ecma262/#sec-error-objects +//! [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Error + +use crate::{ + builtins::{ + function::{make_builtin_fn, make_constructor_fn}, + object::ObjectKind, + value::{ResultValue, Value}, + }, + exec::Interpreter, +}; + +// mod eval; +pub(crate) mod range; +// mod reference; +// mod syntax; +// mod type_err; +// mod uri; + +pub(crate) use self::range::RangeError; + +/// Built-in `Error` object. +#[derive(Debug, Clone, Copy)] +pub(crate) struct Error; + +impl Error { + /// Create a new error object. + pub(crate) fn make_error(this: &mut Value, args: &[Value], _: &mut Interpreter) -> ResultValue { + if !args.is_empty() { + this.set_field( + "message", + Value::from( + args.get(0) + .expect("failed getting error message") + .to_string(), + ), + ); + } + // This value is used by console.log and other routines to match Object type + // to its Javascript Identifier (global constructor method name) + this.set_kind(ObjectKind::Error); + Ok(Value::undefined()) + } + + /// `Error.prototype.toString()` + /// + /// The toString() method returns a string representing the specified Error object. + /// + /// More information: + /// - [MDN documentation][mdn] + /// - [ECMAScript reference][spec] + /// + /// [spec]: https://tc39.es/ecma262/#sec-error.prototype.tostring + /// [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Error/toString + #[allow(clippy::wrong_self_convention)] + pub(crate) fn to_string(this: &mut Value, _: &[Value], _: &mut Interpreter) -> ResultValue { + let name = this.get_field("name"); + let message = this.get_field("message"); + Ok(Value::from(format!("{}: {}", name, message))) + } + + /// Create a new `Error` object. + pub(crate) fn create(global: &Value) -> Value { + let prototype = Value::new_object(Some(global)); + prototype.set_field("message", Value::from("")); + prototype.set_field("name", Value::from("Error")); + make_builtin_fn(Self::to_string, "toString", &prototype, 0); + make_constructor_fn(Self::make_error, global, prototype) + } + + /// Initialise the global object with the `Error` object. + pub(crate) fn init(global: &Value) { + global.set_field("Error", Self::create(global)); + } +} diff --git a/boa/src/builtins/error/range.rs b/boa/src/builtins/error/range.rs new file mode 100644 index 0000000000..fb71ead366 --- /dev/null +++ b/boa/src/builtins/error/range.rs @@ -0,0 +1,95 @@ +//! This module implements the global `RangeError` object. +//! +//! Indicates a value that is not in the set or range of allowable values. +//! +//! More information: +//! - [MDN documentation][mdn] +//! - [ECMAScript reference][spec] +//! +//! [spec]: https://tc39.es/ecma262/#sec-native-error-types-used-in-this-standard-rangeerror +//! [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/RangeError + +use crate::{ + builtins::{ + function::make_builtin_fn, + function::make_constructor_fn, + object::ObjectKind, + value::{ResultValue, Value}, + }, + exec::Interpreter, +}; + +/// JavaScript `RangeError` impleentation. +#[derive(Debug, Clone, Copy)] +pub(crate) struct RangeError; + +impl RangeError { + /// Create a new error object. + pub(crate) fn make_error(this: &mut Value, args: &[Value], _: &mut Interpreter) -> ResultValue { + if !args.is_empty() { + this.set_field( + "message", + Value::from( + args.get(0) + .expect("failed getting error message") + .to_string(), + ), + ); + } + // This value is used by console.log and other routines to match Object type + // to its Javascript Identifier (global constructor method name) + this.set_kind(ObjectKind::Error); + Ok(Value::undefined()) + } + + /// `Error.prototype.toString()` + /// + /// The toString() method returns a string representing the specified Error object. + /// + /// More information: + /// - [MDN documentation][mdn] + /// - [ECMAScript reference][spec] + /// + /// [spec]: https://tc39.es/ecma262/#sec-error.prototype.tostring + /// [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Error/toString + #[allow(clippy::wrong_self_convention)] + pub(crate) fn to_string(this: &mut Value, _: &[Value], _: &mut Interpreter) -> ResultValue { + let name = this.get_field("name"); + let message = this.get_field("message"); + Ok(Value::from(format!("{}: {}", name, message))) + } + + /// Create a new `RangeError` object. + pub(crate) fn create(global: &Value) -> Value { + let prototype = Value::new_object(Some(global)); + prototype.set_field("message", Value::from("")); + prototype.set_field("name", Value::from("RangeError")); + make_builtin_fn(Self::to_string, "toString", &prototype, 0); + make_constructor_fn(Self::make_error, global, prototype) + } + + /// Runs a `new RangeError(message)`. + pub(crate) fn run_new(message: M, interpreter: &mut Interpreter) -> ResultValue + where + M: Into, + { + use crate::{ + exec::Executable, + syntax::ast::{ + node::{Call, Identifier, New}, + Const, + }, + }; + + New::from(Call::new( + Identifier::from("RangeError"), + vec![Const::from(message.into()).into()], + )) + .run(interpreter) + } + + /// Initialise the global object with the `RangeError` object. + pub(crate) fn init(global: &Value) { + global.set_field("RangeError", Self::create(global)); + } +} diff --git a/boa/src/builtins/function/mod.rs b/boa/src/builtins/function/mod.rs index 75cf71322f..92bd850e34 100644 --- a/boa/src/builtins/function/mod.rs +++ b/boa/src/builtins/function/mod.rs @@ -13,18 +13,15 @@ use crate::{ builtins::{ - array, + array::Array, object::{Object, ObjectInternalMethods, ObjectKind, PROTOTYPE}, property::Property, value::{ResultValue, Value}, }, - environment::{ - function_environment_record::BindingStatus, - lexical_environment::{new_function_environment, Environment}, - }, - exec::Executor, - syntax::ast::node::{FormalParameter, Node}, - Interpreter, + environment::function_environment_record::BindingStatus, + environment::lexical_environment::{new_function_environment, Environment}, + exec::{Executable, Interpreter}, + syntax::ast::node::{FormalParameter, StatementList}, }; use gc::{unsafe_empty_trace, Finalize, Trace}; use std::fmt::{self, Debug}; @@ -52,14 +49,14 @@ pub enum ThisMode { #[derive(Clone, Finalize)] pub enum FunctionBody { BuiltIn(NativeFunctionData), - Ordinary(Node), + Ordinary(StatementList), } impl Debug for FunctionBody { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { match self { Self::BuiltIn(_) => write!(f, "native code"), - Self::Ordinary(node) => write!(f, "{}", node), + Self::Ordinary(statements) => write!(f, "{:?}", statements), } } } @@ -174,7 +171,7 @@ impl Function { for i in 0..self.params.len() { let param = self.params.get(i).expect("Could not get param"); // Rest Parameters - if param.is_rest_param { + if param.is_rest_param() { self.add_rest_param(param, i, args_list, interpreter, &local_env); break; } @@ -196,7 +193,7 @@ impl Function { // Call body should be set before reaching here let result = match &self.body { - FunctionBody::Ordinary(ref body) => interpreter.run(body), + FunctionBody::Ordinary(ref body) => body.run(interpreter), _ => panic!("Ordinary function should not have BuiltIn Function body"), }; @@ -238,7 +235,7 @@ impl Function { // Add argument bindings to the function environment for (i, param) in self.params.iter().enumerate() { // Rest Parameters - if param.is_rest_param { + if param.is_rest_param() { self.add_rest_param(param, i, args_list, interpreter, &local_env); break; } @@ -260,7 +257,7 @@ impl Function { // Call body should be set before reaching here let _ = match &self.body { - FunctionBody::Ordinary(ref body) => interpreter.run(body), + FunctionBody::Ordinary(ref body) => body.run(interpreter), _ => panic!("Ordinary function should not have BuiltIn Function body"), }; @@ -281,18 +278,18 @@ impl Function { local_env: &Environment, ) { // Create array of values - let array = array::new_array(interpreter).unwrap(); - array::add_to_array_object(&array, &args_list[index..]).unwrap(); + let array = Array::new_array(interpreter).unwrap(); + Array::add_to_array_object(&array, &args_list[index..]).unwrap(); // Create binding local_env .borrow_mut() - .create_mutable_binding(param.name.clone(), false); + .create_mutable_binding(param.name().to_owned(), false); // Set Binding to value local_env .borrow_mut() - .initialize_binding(¶m.name, array); + .initialize_binding(param.name(), array); } // Adds an argument to the environment @@ -305,12 +302,12 @@ impl Function { // Create binding local_env .borrow_mut() - .create_mutable_binding(param.name.clone(), false); + .create_mutable_binding(param.name().to_owned(), false); // Set Binding to value local_env .borrow_mut() - .initialize_binding(¶m.name, value); + .initialize_binding(param.name(), value); } } @@ -387,9 +384,7 @@ pub fn make_constructor_fn(body: NativeFunctionData, global: &Value, proto: Valu ); // Get reference to Function.prototype - let func_prototype = global - .get_field_slice("Function") - .get_field_slice(PROTOTYPE); + let func_prototype = global.get_field("Function").get_field(PROTOTYPE); // Create the function object and point its instance prototype to Function.prototype let mut constructor_obj = Object::function(); @@ -399,14 +394,32 @@ pub fn make_constructor_fn(body: NativeFunctionData, global: &Value, proto: Valu let constructor_val = Value::from(constructor_obj); // Set proto.constructor -> constructor_obj - proto.set_field_slice("constructor", constructor_val.clone()); - constructor_val.set_field_slice(PROTOTYPE, proto); + proto.set_field("constructor", constructor_val.clone()); + constructor_val.set_field(PROTOTYPE, proto); constructor_val } +/// Macro to create a new member function of a prototype. +/// +/// If no length is provided, the length will be set to 0. +pub fn make_builtin_fn(function: NativeFunctionData, name: N, parent: &Value, length: i32) +where + N: Into, +{ + let func = Function::create_builtin(vec![], FunctionBody::BuiltIn(function)); + + let mut new_func = Object::function(); + new_func.set_func(func); + + let new_func_obj = Value::from(new_func); + new_func_obj.set_field("length", length); + + parent.set_field(name.into(), new_func_obj); +} + /// Initialise the `Function` object on the global object. #[inline] pub fn init(global: &Value) { - global.set_field_slice("Function", create(global)); + global.set_field("Function", create(global)); } diff --git a/boa/src/builtins/json/mod.rs b/boa/src/builtins/json/mod.rs index 592a58fad5..7a5e80458c 100644 --- a/boa/src/builtins/json/mod.rs +++ b/boa/src/builtins/json/mod.rs @@ -13,7 +13,10 @@ //! [json]: https://www.json.org/json-en.html //! [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/JSON -use crate::builtins::value::{ResultValue, Value}; +use crate::builtins::{ + function::make_builtin_fn, + value::{ResultValue, Value}, +}; use crate::exec::Interpreter; use serde_json::{self, Value as JSONValue}; @@ -72,8 +75,8 @@ pub fn stringify(_: &mut Value, args: &[Value], _: &mut Interpreter) -> ResultVa pub fn create(global: &Value) -> Value { let json = Value::new_object(Some(global)); - make_builtin_fn!(parse, named "parse", with length 2, of json); - make_builtin_fn!(stringify, named "stringify", with length 3, of json); + make_builtin_fn(parse, "parse", &json, 2); + make_builtin_fn(stringify, "stringify", &json, 3); json } @@ -81,5 +84,5 @@ pub fn create(global: &Value) -> Value { /// Initialise the `JSON` object on the global object. #[inline] pub fn init(global: &Value) { - global.set_field_slice("JSON", create(global)); + global.set_field("JSON", create(global)); } diff --git a/boa/src/builtins/json/tests.rs b/boa/src/builtins/json/tests.rs index 5143da8cc8..ba8535884e 100644 --- a/boa/src/builtins/json/tests.rs +++ b/boa/src/builtins/json/tests.rs @@ -1,9 +1,9 @@ -use crate::{exec::Executor, forward, realm::Realm}; +use crate::{exec::Interpreter, forward, realm::Realm}; #[test] fn json_sanity() { let realm = Realm::create(); - let mut engine = Executor::new(realm); + let mut engine = Interpreter::new(realm); assert_eq!( forward(&mut engine, r#"JSON.parse('{"aaa":"bbb"}').aaa == 'bbb'"#), "true" diff --git a/boa/src/builtins/math/mod.rs b/boa/src/builtins/math/mod.rs index 2419bcfcf9..04591cad30 100644 --- a/boa/src/builtins/math/mod.rs +++ b/boa/src/builtins/math/mod.rs @@ -12,7 +12,10 @@ //! [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Math use crate::{ - builtins::value::{ResultValue, Value}, + builtins::{ + function::make_builtin_fn, + value::{ResultValue, Value}, + }, exec::Interpreter, }; use rand::random; @@ -506,43 +509,43 @@ pub fn trunc(_: &mut Value, args: &[Value], _: &mut Interpreter) -> ResultValue pub fn create(global: &Value) -> Value { let math = Value::new_object(Some(global)); - math.set_field_slice("E", Value::from(f64::consts::E)); - math.set_field_slice("LN2", Value::from(f64::consts::LN_2)); - math.set_field_slice("LN10", Value::from(f64::consts::LN_10)); - math.set_field_slice("LOG2E", Value::from(f64::consts::LOG2_E)); - math.set_field_slice("LOG10E", Value::from(f64::consts::LOG10_E)); - math.set_field_slice("SQRT1_2", Value::from(0.5_f64.sqrt())); - math.set_field_slice("SQRT2", Value::from(f64::consts::SQRT_2)); - math.set_field_slice("PI", Value::from(f64::consts::PI)); - make_builtin_fn!(abs, named "abs", with length 1, of math); - make_builtin_fn!(acos, named "acos", with length 1, of math); - make_builtin_fn!(acosh, named "acosh", with length 1, of math); - make_builtin_fn!(asin, named "asin", with length 1, of math); - make_builtin_fn!(asinh, named "asinh", with length 1, of math); - make_builtin_fn!(atan, named "atan", with length 1, of math); - make_builtin_fn!(atanh, named "atanh", with length 1, of math); - make_builtin_fn!(atan2, named "atan2", with length 2, of math); - make_builtin_fn!(cbrt, named "cbrt", with length 1, of math); - make_builtin_fn!(ceil, named "ceil", with length 1, of math); - make_builtin_fn!(cos, named "cos", with length 1, of math); - make_builtin_fn!(cosh, named "cosh", with length 1, of math); - make_builtin_fn!(exp, named "exp", with length 1, of math); - make_builtin_fn!(floor, named "floor", with length 1, of math); - make_builtin_fn!(log, named "log", with length 1, of math); - make_builtin_fn!(log10, named "log10", with length 1, of math); - make_builtin_fn!(log2, named "log2", with length 1, of math); - make_builtin_fn!(max, named "max", with length 2, of math); - make_builtin_fn!(min, named "min", with length 2, of math); - make_builtin_fn!(pow, named "pow", with length 2, of math); - make_builtin_fn!(_random, named "random", of math); - make_builtin_fn!(round, named "round", with length 1, of math); - make_builtin_fn!(sign, named "sign", with length 1, of math); - make_builtin_fn!(sin, named "sin", with length 1, of math); - make_builtin_fn!(sinh, named "sinh", with length 1, of math); - make_builtin_fn!(sqrt, named "sqrt", with length 1, of math); - make_builtin_fn!(tan, named "tan", with length 1, of math); - make_builtin_fn!(tanh, named "tanh", with length 1, of math); - make_builtin_fn!(trunc, named "trunc", with length 1, of math); + math.set_field("E", Value::from(f64::consts::E)); + math.set_field("LN2", Value::from(f64::consts::LN_2)); + math.set_field("LN10", Value::from(f64::consts::LN_10)); + math.set_field("LOG2E", Value::from(f64::consts::LOG2_E)); + math.set_field("LOG10E", Value::from(f64::consts::LOG10_E)); + math.set_field("SQRT1_2", Value::from(0.5_f64.sqrt())); + math.set_field("SQRT2", Value::from(f64::consts::SQRT_2)); + math.set_field("PI", Value::from(f64::consts::PI)); + make_builtin_fn(abs, "abs", &math, 1); + make_builtin_fn(acos, "acos", &math, 1); + make_builtin_fn(acosh, "acosh", &math, 1); + make_builtin_fn(asin, "asin", &math, 1); + make_builtin_fn(asinh, "asinh", &math, 1); + make_builtin_fn(atan, "atan", &math, 1); + make_builtin_fn(atanh, "atanh", &math, 1); + make_builtin_fn(atan2, "atan2", &math, 2); + make_builtin_fn(cbrt, "cbrt", &math, 1); + make_builtin_fn(ceil, "ceil", &math, 1); + make_builtin_fn(cos, "cos", &math, 1); + make_builtin_fn(cosh, "cosh", &math, 1); + make_builtin_fn(exp, "exp", &math, 1); + make_builtin_fn(floor, "floor", &math, 1); + make_builtin_fn(log, "log", &math, 1); + make_builtin_fn(log10, "log10", &math, 1); + make_builtin_fn(log2, "log2", &math, 1); + make_builtin_fn(max, "max", &math, 2); + make_builtin_fn(min, "min", &math, 2); + make_builtin_fn(pow, "pow", &math, 2); + make_builtin_fn(_random, "random", &math, 0); + make_builtin_fn(round, "round", &math, 1); + make_builtin_fn(sign, "sign", &math, 1); + make_builtin_fn(sin, "sin", &math, 1); + make_builtin_fn(sinh, "sinh", &math, 1); + make_builtin_fn(sqrt, "sqrt", &math, 1); + make_builtin_fn(tan, "tan", &math, 1); + make_builtin_fn(tanh, "tanh", &math, 1); + make_builtin_fn(trunc, "trunc", &math, 1); math } @@ -550,5 +553,5 @@ pub fn create(global: &Value) -> Value { /// Initialise the `Math` object on the global object. #[inline] pub fn init(global: &Value) { - global.set_field_slice("Math", create(global)); + global.set_field("Math", create(global)); } diff --git a/boa/src/builtins/math/tests.rs b/boa/src/builtins/math/tests.rs index 45ad0b13b1..a733946d69 100644 --- a/boa/src/builtins/math/tests.rs +++ b/boa/src/builtins/math/tests.rs @@ -1,12 +1,12 @@ #![allow(clippy::float_cmp)] -use crate::{exec::Executor, forward, forward_val, realm::Realm}; +use crate::{exec::Interpreter, forward, forward_val, realm::Realm}; use std::f64; #[test] fn abs() { let realm = Realm::create(); - let mut engine = Executor::new(realm); + let mut engine = Interpreter::new(realm); let init = r#" var a = Math.abs(3 - 5); var b = Math.abs(1.23456 - 7.89012); @@ -24,7 +24,7 @@ fn abs() { #[test] fn acos() { let realm = Realm::create(); - let mut engine = Executor::new(realm); + let mut engine = Interpreter::new(realm); let init = r#" var a = Math.acos(8 / 10); var b = Math.acos(5 / 3); @@ -48,7 +48,7 @@ fn acos() { #[test] fn acosh() { let realm = Realm::create(); - let mut engine = Executor::new(realm); + let mut engine = Interpreter::new(realm); let init = r#" var a = Math.acosh(2); var b = Math.acosh(-1); @@ -69,7 +69,7 @@ fn acosh() { #[test] fn asin() { let realm = Realm::create(); - let mut engine = Executor::new(realm); + let mut engine = Interpreter::new(realm); let init = r#" var a = Math.asin(6 / 10); var b = Math.asin(5 / 3); @@ -87,7 +87,7 @@ fn asin() { #[test] fn asinh() { let realm = Realm::create(); - let mut engine = Executor::new(realm); + let mut engine = Interpreter::new(realm); let init = r#" var a = Math.asinh(1); var b = Math.asinh(0); @@ -105,7 +105,7 @@ fn asinh() { #[test] fn atan() { let realm = Realm::create(); - let mut engine = Executor::new(realm); + let mut engine = Interpreter::new(realm); let init = r#" var a = Math.atan(1); var b = Math.atan(0); @@ -126,7 +126,7 @@ fn atan() { #[test] fn atan2() { let realm = Realm::create(); - let mut engine = Executor::new(realm); + let mut engine = Interpreter::new(realm); let init = r#" var a = Math.atan2(90, 15); var b = Math.atan2(15, 90); @@ -144,7 +144,7 @@ fn atan2() { #[test] fn cbrt() { let realm = Realm::create(); - let mut engine = Executor::new(realm); + let mut engine = Interpreter::new(realm); let init = r#" var a = Math.cbrt(64); var b = Math.cbrt(-1); @@ -165,7 +165,7 @@ fn cbrt() { #[test] fn ceil() { let realm = Realm::create(); - let mut engine = Executor::new(realm); + let mut engine = Interpreter::new(realm); let init = r#" var a = Math.ceil(1.95); var b = Math.ceil(4); @@ -186,7 +186,7 @@ fn ceil() { #[test] fn cos() { let realm = Realm::create(); - let mut engine = Executor::new(realm); + let mut engine = Interpreter::new(realm); let init = r#" var a = Math.cos(0); var b = Math.cos(1); @@ -204,7 +204,7 @@ fn cos() { #[test] fn cosh() { let realm = Realm::create(); - let mut engine = Executor::new(realm); + let mut engine = Interpreter::new(realm); let init = r#" var a = Math.cosh(0); var b = Math.cosh(1); @@ -225,7 +225,7 @@ fn cosh() { #[test] fn exp() { let realm = Realm::create(); - let mut engine = Executor::new(realm); + let mut engine = Interpreter::new(realm); let init = r#" var a = Math.exp(0); var b = Math.exp(-1); @@ -246,7 +246,7 @@ fn exp() { #[test] fn floor() { let realm = Realm::create(); - let mut engine = Executor::new(realm); + let mut engine = Interpreter::new(realm); let init = r#" var a = Math.floor(1.95); var b = Math.floor(-3.01); @@ -267,7 +267,7 @@ fn floor() { #[test] fn log() { let realm = Realm::create(); - let mut engine = Executor::new(realm); + let mut engine = Interpreter::new(realm); let init = r#" var a = Math.log(1); var b = Math.log(10); @@ -288,7 +288,7 @@ fn log() { #[test] fn log10() { let realm = Realm::create(); - let mut engine = Executor::new(realm); + let mut engine = Interpreter::new(realm); let init = r#" var a = Math.log10(2); var b = Math.log10(1); @@ -309,7 +309,7 @@ fn log10() { #[test] fn log2() { let realm = Realm::create(); - let mut engine = Executor::new(realm); + let mut engine = Interpreter::new(realm); let init = r#" var a = Math.log2(3); var b = Math.log2(1); @@ -330,7 +330,7 @@ fn log2() { #[test] fn max() { let realm = Realm::create(); - let mut engine = Executor::new(realm); + let mut engine = Interpreter::new(realm); let init = r#" var a = Math.max(10, 20); var b = Math.max(-10, -20); @@ -351,7 +351,7 @@ fn max() { #[test] fn min() { let realm = Realm::create(); - let mut engine = Executor::new(realm); + let mut engine = Interpreter::new(realm); let init = r#" var a = Math.min(10, 20); var b = Math.min(-10, -20); @@ -372,7 +372,7 @@ fn min() { #[test] fn pow() { let realm = Realm::create(); - let mut engine = Executor::new(realm); + let mut engine = Interpreter::new(realm); let init = r#" var a = Math.pow(2, 10); var b = Math.pow(-7, 2); @@ -396,7 +396,7 @@ fn pow() { #[test] fn round() { let realm = Realm::create(); - let mut engine = Executor::new(realm); + let mut engine = Interpreter::new(realm); let init = r#" var a = Math.round(20.5); var b = Math.round(-20.3); @@ -414,7 +414,7 @@ fn round() { #[test] fn sign() { let realm = Realm::create(); - let mut engine = Executor::new(realm); + let mut engine = Interpreter::new(realm); let init = r#" var a = Math.sign(3); var b = Math.sign(-3); @@ -435,7 +435,7 @@ fn sign() { #[test] fn sin() { let realm = Realm::create(); - let mut engine = Executor::new(realm); + let mut engine = Interpreter::new(realm); let init = r#" var a = Math.sin(0); var b = Math.sin(1); @@ -453,7 +453,7 @@ fn sin() { #[test] fn sinh() { let realm = Realm::create(); - let mut engine = Executor::new(realm); + let mut engine = Interpreter::new(realm); let init = r#" var a = Math.sinh(0); var b = Math.sinh(1); @@ -471,7 +471,7 @@ fn sinh() { #[test] fn sqrt() { let realm = Realm::create(); - let mut engine = Executor::new(realm); + let mut engine = Interpreter::new(realm); let init = r#" var a = Math.sqrt(0); var b = Math.sqrt(2); @@ -494,7 +494,7 @@ fn sqrt() { // #[test] // fn tan() { // let realm = Realm::create(); -// let mut engine = Executor::new(realm); +// let mut engine = Interpreter::new(realm); // let init = r#" // var a = Math.tan(1.1); // "#; @@ -509,7 +509,7 @@ fn sqrt() { #[test] fn tanh() { let realm = Realm::create(); - let mut engine = Executor::new(realm); + let mut engine = Interpreter::new(realm); let init = r#" var a = Math.tanh(1); var b = Math.tanh(0); @@ -527,7 +527,7 @@ fn tanh() { #[test] fn trunc() { let realm = Realm::create(); - let mut engine = Executor::new(realm); + let mut engine = Interpreter::new(realm); let init = r#" var a = Math.trunc(13.37); var b = Math.trunc(0.123); diff --git a/boa/src/builtins/mod.rs b/boa/src/builtins/mod.rs index 912c5000e7..c8a0b64d20 100644 --- a/boa/src/builtins/mod.rs +++ b/boa/src/builtins/mod.rs @@ -1,26 +1,5 @@ //! Builtins live here, such as Object, String, Math etc -/// Macro to create a new member function of a prototype. -/// -/// If no length is provided, the length will be set to 0. -macro_rules! make_builtin_fn { - ($fn:ident, named $name:expr, with length $l:tt, of $p:ident) => { - let func = crate::builtins::function::Function::create_builtin( - vec![], - crate::builtins::function::FunctionBody::BuiltIn($fn), - ); - - let mut new_func = crate::builtins::object::Object::function(); - new_func.set_func(func); - let new_func_obj = Value::from(new_func); - new_func_obj.set_field_slice("length", Value::from($l)); - $p.set_field_slice($name, new_func_obj); - }; - ($fn:ident, named $name:expr, of $p:ident) => { - make_builtin_fn!($fn, named $name, with length 0, of $p); - }; -} - pub mod array; pub mod bigint; pub mod boolean; @@ -37,21 +16,32 @@ pub mod string; pub mod symbol; pub mod value; -use value::Value; +pub(crate) use self::{ + array::Array, + bigint::BigInt, + boolean::Boolean, + error::{Error, RangeError}, + number::Number, + regexp::RegExp, + string::String, + value::{ResultValue, Value}, +}; /// Initializes builtin objects and functions #[inline] pub fn init(global: &Value) { - array::init(global); - bigint::init(global); - boolean::init(global); + Array::init(global); + BigInt::init(global); + Boolean::init(global); json::init(global); math::init(global); - number::init(global); + Number::init(global); object::init(global); function::init(global); - regexp::init(global); - string::init(global); + RegExp::init(global); + String::init(global); symbol::init(global); console::init(global); + Error::init(global); + RangeError::init(global); } diff --git a/boa/src/builtins/number/mod.rs b/boa/src/builtins/number/mod.rs index 0da8860f5a..6cf2f2ee77 100644 --- a/boa/src/builtins/number/mod.rs +++ b/boa/src/builtins/number/mod.rs @@ -16,405 +16,457 @@ #[cfg(test)] mod tests; -use super::{function::make_constructor_fn, object::ObjectKind}; +use super::{ + function::{make_builtin_fn, make_constructor_fn}, + object::ObjectKind, +}; use crate::{ builtins::{ object::internal_methods_trait::ObjectInternalMethods, value::{ResultValue, Value, ValueData}, + RangeError, }, exec::Interpreter, }; use num_traits::float::FloatCore; use std::{borrow::Borrow, f64, ops::Deref}; -/// Helper function that converts a Value to a Number. -fn to_number(value: &Value) -> Value { - match *value.deref().borrow() { - ValueData::Boolean(b) => { - if b { - Value::from(1) - } else { - Value::from(0) +const BUF_SIZE: usize = 2200; + +/// `Number` implementation. +#[derive(Debug, Clone, Copy)] +pub(crate) struct Number; + +impl Number { + /// Helper function that converts a Value to a Number. + #[allow(clippy::wrong_self_convention)] + fn to_number(value: &Value) -> Value { + match *value.deref().borrow() { + ValueData::Boolean(b) => { + if b { + Value::from(1) + } else { + Value::from(0) + } } + ValueData::Symbol(_) | ValueData::Undefined => Value::from(f64::NAN), + ValueData::Integer(i) => Value::from(f64::from(i)), + ValueData::Object(ref o) => (o).deref().borrow().get_internal_slot("NumberData"), + ValueData::Null => Value::from(0), + ValueData::Rational(n) => Value::from(n), + ValueData::BigInt(ref bigint) => Value::from(bigint.to_f64()), + ValueData::String(ref s) => match s.parse::() { + Ok(n) => Value::from(n), + Err(_) => Value::from(f64::NAN), + }, } - ValueData::Symbol(_) | ValueData::Undefined => Value::from(f64::NAN), - ValueData::Integer(i) => Value::from(f64::from(i)), - ValueData::Object(ref o) => (o).deref().borrow().get_internal_slot("NumberData"), - ValueData::Null => Value::from(0), - ValueData::Rational(n) => Value::from(n), - ValueData::BigInt(ref bigint) => Value::from(bigint.to_f64()), - ValueData::String(ref s) => match s.parse::() { - Ok(n) => Value::from(n), - Err(_) => Value::from(f64::NAN), - }, } -} -/// Helper function that formats a float as a ES6-style exponential number string. -fn num_to_exponential(n: f64) -> String { - match n.abs() { - x if x > 1.0 => format!("{:e}", n).replace("e", "e+"), - x if x == 0.0 => format!("{:e}", n).replace("e", "e+"), - _ => format!("{:e}", n), + /// Helper function that formats a float as a ES6-style exponential number string. + fn num_to_exponential(n: f64) -> String { + match n.abs() { + x if x > 1.0 => format!("{:e}", n).replace("e", "e+"), + x if x == 0.0 => format!("{:e}", n).replace("e", "e+"), + _ => format!("{:e}", n), + } } -} - -/// `[[Construct]]` - Creates a Number instance -/// -/// `[[Call]]` - Creates a number primitive -pub fn make_number(this: &mut Value, args: &[Value], _ctx: &mut Interpreter) -> ResultValue { - let data = match args.get(0) { - Some(ref value) => to_number(value), - None => to_number(&Value::from(0)), - }; - this.set_kind(ObjectKind::Number); - this.set_internal_slot("NumberData", data.clone()); - - Ok(data) -} -/// `Number()` function. -/// -/// More Information https://tc39.es/ecma262/#sec-number-constructor-number-value -pub fn call_number(_this: &mut Value, args: &[Value], _ctx: &mut Interpreter) -> ResultValue { - let data = match args.get(0) { - Some(ref value) => to_number(value), - None => to_number(&Value::from(0)), - }; - Ok(data) -} + /// `[[Construct]]` - Creates a Number instance + /// + /// `[[Call]]` - Creates a number primitive + pub(crate) fn make_number( + this: &mut Value, + args: &[Value], + _ctx: &mut Interpreter, + ) -> ResultValue { + let data = match args.get(0) { + Some(ref value) => Self::to_number(value), + None => Self::to_number(&Value::from(0)), + }; + this.set_kind(ObjectKind::Number); + this.set_internal_slot("NumberData", data.clone()); -/// `Number.prototype.toExponential( [fractionDigits] )` -/// -/// The `toExponential()` method returns a string representing the Number object in exponential notation. -/// -/// More information: -/// - [ECMAScript reference][spec] -/// - [MDN documentation][mdn] -/// -/// [spec]: https://tc39.es/ecma262/#sec-number.prototype.toexponential -/// [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Number/toExponential -pub fn to_exponential(this: &mut Value, _args: &[Value], _ctx: &mut Interpreter) -> ResultValue { - let this_num = to_number(this).to_number(); - let this_str_num = num_to_exponential(this_num); - Ok(Value::from(this_str_num)) -} + Ok(data) + } -/// `Number.prototype.toFixed( [digits] )` -/// -/// The `toFixed()` method formats a number using fixed-point notation -/// -/// More information: -/// - [ECMAScript reference][spec] -/// - [MDN documentation][mdn] -/// -/// [spec]: https://tc39.es/ecma262/#sec-number.prototype.tofixed -/// [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Number/toFixed -pub fn to_fixed(this: &mut Value, args: &[Value], _ctx: &mut Interpreter) -> ResultValue { - let this_num = to_number(this).to_number(); - let precision = match args.get(0) { - Some(n) => match n.to_integer() { - x if x > 0 => n.to_integer() as usize, - _ => 0, - }, - None => 0, - }; - let this_fixed_num = format!("{:.*}", precision, this_num); - Ok(Value::from(this_fixed_num)) -} + /// `Number()` function. + /// + /// More Information https://tc39.es/ecma262/#sec-number-constructor-number-value + pub(crate) fn call_number( + _this: &mut Value, + args: &[Value], + _ctx: &mut Interpreter, + ) -> ResultValue { + let data = match args.get(0) { + Some(ref value) => Self::to_number(value), + None => Self::to_number(&Value::from(0)), + }; + Ok(data) + } -/// `Number.prototype.toLocaleString( [locales [, options]] )` -/// -/// The `toLocaleString()` method returns a string with a language-sensitive representation of this number. -/// -/// Note that while this technically conforms to the Ecma standard, it does no actual -/// internationalization logic. -/// -/// More information: -/// - [ECMAScript reference][spec] -/// - [MDN documentation][mdn] -/// -/// [spec]: https://tc39.es/ecma262/#sec-number.prototype.tolocalestring -/// [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Number/toLocaleString -pub fn to_locale_string(this: &mut Value, _args: &[Value], _ctx: &mut Interpreter) -> ResultValue { - let this_num = to_number(this).to_number(); - let this_str_num = format!("{}", this_num); - Ok(Value::from(this_str_num)) -} + /// `Number.prototype.toExponential( [fractionDigits] )` + /// + /// The `toExponential()` method returns a string representing the Number object in exponential notation. + /// + /// More information: + /// - [ECMAScript reference][spec] + /// - [MDN documentation][mdn] + /// + /// [spec]: https://tc39.es/ecma262/#sec-number.prototype.toexponential + /// [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Number/toExponential + #[allow(clippy::wrong_self_convention)] + pub(crate) fn to_exponential( + this: &mut Value, + _args: &[Value], + _ctx: &mut Interpreter, + ) -> ResultValue { + let this_num = Self::to_number(this).to_number(); + let this_str_num = Self::num_to_exponential(this_num); + Ok(Value::from(this_str_num)) + } -/// `Number.prototype.toPrecision( [precision] )` -/// -/// The `toPrecision()` method returns a string representing the Number object to the specified precision. -/// -/// More information: -/// - [ECMAScript reference][spec] -/// - [MDN documentation][mdn] -/// -/// [spec]: https://tc39.es/ecma262/#sec-number.prototype.toexponential -/// [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Number/toPrecision -pub fn to_precision(this: &mut Value, args: &[Value], _ctx: &mut Interpreter) -> ResultValue { - let this_num = to_number(this); - let _num_str_len = format!("{}", this_num.to_number()).len(); - let _precision = match args.get(0) { - Some(n) => match n.to_integer() { - x if x > 0 => n.to_integer() as usize, - _ => 0, - }, - None => 0, - }; - // TODO: Implement toPrecision - unimplemented!("TODO: Implement toPrecision"); -} + /// `Number.prototype.toFixed( [digits] )` + /// + /// The `toFixed()` method formats a number using fixed-point notation + /// + /// More information: + /// - [ECMAScript reference][spec] + /// - [MDN documentation][mdn] + /// + /// [spec]: https://tc39.es/ecma262/#sec-number.prototype.tofixed + /// [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Number/toFixed + #[allow(clippy::wrong_self_convention)] + pub(crate) fn to_fixed( + this: &mut Value, + args: &[Value], + _ctx: &mut Interpreter, + ) -> ResultValue { + let this_num = Self::to_number(this).to_number(); + let precision = match args.get(0) { + Some(n) => match n.to_integer() { + x if x > 0 => n.to_integer() as usize, + _ => 0, + }, + None => 0, + }; + let this_fixed_num = format!("{:.*}", precision, this_num); + Ok(Value::from(this_fixed_num)) + } -const BUF_SIZE: usize = 2200; + /// `Number.prototype.toLocaleString( [locales [, options]] )` + /// + /// The `toLocaleString()` method returns a string with a language-sensitive representation of this number. + /// + /// Note that while this technically conforms to the Ecma standard, it does no actual + /// internationalization logic. + /// + /// More information: + /// - [ECMAScript reference][spec] + /// - [MDN documentation][mdn] + /// + /// [spec]: https://tc39.es/ecma262/#sec-number.prototype.tolocalestring + /// [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Number/toLocaleString + #[allow(clippy::wrong_self_convention)] + pub(crate) fn to_locale_string( + this: &mut Value, + _args: &[Value], + _ctx: &mut Interpreter, + ) -> ResultValue { + let this_num = Self::to_number(this).to_number(); + let this_str_num = format!("{}", this_num); + Ok(Value::from(this_str_num)) + } -// https://golang.org/src/math/nextafter.go -#[inline] -fn next_after(x: f64, y: f64) -> f64 { - if x.is_nan() || y.is_nan() { - f64::NAN - } else if (x - y) == 0. { - x - } else if x == 0.0 { - f64::from_bits(1).copysign(y) - } else if y > x || x > 0.0 { - f64::from_bits(x.to_bits() + 1) - } else { - f64::from_bits(x.to_bits() - 1) + /// `Number.prototype.toPrecision( [precision] )` + /// + /// The `toPrecision()` method returns a string representing the Number object to the specified precision. + /// + /// More information: + /// - [ECMAScript reference][spec] + /// - [MDN documentation][mdn] + /// + /// [spec]: https://tc39.es/ecma262/#sec-number.prototype.toexponential + /// [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Number/toPrecision + #[allow(clippy::wrong_self_convention)] + pub(crate) fn to_precision( + this: &mut Value, + args: &[Value], + _ctx: &mut Interpreter, + ) -> ResultValue { + let this_num = Self::to_number(this); + let _num_str_len = format!("{}", this_num.to_number()).len(); + let _precision = match args.get(0) { + Some(n) => match n.to_integer() { + x if x > 0 => n.to_integer() as usize, + _ => 0, + }, + None => 0, + }; + // TODO: Implement toPrecision + unimplemented!("TODO: Implement toPrecision"); } -} -// https://chromium.googlesource.com/v8/v8/+/refs/heads/master/src/numbers/conversions.cc#1230 -pub fn num_to_string(mut value: f64, radix: u8) -> String { - assert!(radix >= 2); - assert!(radix <= 36); - assert!(value.is_finite()); - // assert_ne!(0.0, value); - - // Character array used for conversion. - // Temporary buffer for the result. We start with the decimal point in the - // middle and write to the left for the integer part and to the right for the - // fractional part. 1024 characters for the exponent and 52 for the mantissa - // either way, with additional space for sign, decimal point and string - // termination should be sufficient. - let mut buffer: [u8; BUF_SIZE] = [0; BUF_SIZE]; - let (int_buf, frac_buf) = buffer.split_at_mut(BUF_SIZE / 2); - let mut fraction_cursor = 0; - let negative = value.is_sign_negative(); - if negative { - value = -value + // https://golang.org/src/math/nextafter.go + #[inline] + fn next_after(x: f64, y: f64) -> f64 { + if x.is_nan() || y.is_nan() { + f64::NAN + } else if (x - y) == 0. { + x + } else if x == 0.0 { + f64::from_bits(1).copysign(y) + } else if y > x || x > 0.0 { + f64::from_bits(x.to_bits() + 1) + } else { + f64::from_bits(x.to_bits() - 1) + } } - // Split the value into an integer part and a fractional part. - // let mut integer = value.trunc(); - // let mut fraction = value.fract(); - let mut integer = value.floor(); - let mut fraction = value - integer; - - // We only compute fractional digits up to the input double's precision. - let mut delta = 0.5 * (next_after(value, f64::MAX) - value); - delta = next_after(0.0, f64::MAX).max(delta); - assert!(delta > 0.0); - if fraction >= delta { - // Insert decimal point. - frac_buf[fraction_cursor] = b'.'; - fraction_cursor += 1; - loop { - // Shift up by one digit. - fraction *= radix as f64; - delta *= radix as f64; - // Write digit. - let digit = fraction as u32; - frac_buf[fraction_cursor] = std::char::from_digit(digit, radix as u32).unwrap() as u8; + + // https://chromium.googlesource.com/v8/v8/+/refs/heads/master/src/numbers/conversions.cc#1230 + pub(crate) fn num_to_string(mut value: f64, radix: u8) -> String { + assert!(radix >= 2); + assert!(radix <= 36); + assert!(value.is_finite()); + // assert_ne!(0.0, value); + + // Character array used for conversion. + // Temporary buffer for the result. We start with the decimal point in the + // middle and write to the left for the integer part and to the right for the + // fractional part. 1024 characters for the exponent and 52 for the mantissa + // either way, with additional space for sign, decimal point and string + // termination should be sufficient. + let mut buffer: [u8; BUF_SIZE] = [0; BUF_SIZE]; + let (int_buf, frac_buf) = buffer.split_at_mut(BUF_SIZE / 2); + let mut fraction_cursor = 0; + let negative = value.is_sign_negative(); + if negative { + value = -value + } + // Split the value into an integer part and a fractional part. + // let mut integer = value.trunc(); + // let mut fraction = value.fract(); + let mut integer = value.floor(); + let mut fraction = value - integer; + + // We only compute fractional digits up to the input double's precision. + let mut delta = 0.5 * (Self::next_after(value, f64::MAX) - value); + delta = Self::next_after(0.0, f64::MAX).max(delta); + assert!(delta > 0.0); + if fraction >= delta { + // Insert decimal point. + frac_buf[fraction_cursor] = b'.'; fraction_cursor += 1; - // Calculate remainder. - fraction -= digit as f64; - // Round to even. - if fraction + delta > 1.0 - && (fraction > 0.5 || (fraction - 0.5) < f64::EPSILON && digit & 1 != 0) - { - loop { - // We need to back trace already written digits in case of carry-over. - fraction_cursor -= 1; - if fraction_cursor == 0 { - // CHECK_EQ('.', buffer[fraction_cursor]); - // Carry over to the integer part. - integer += 1.; - break; - } else { - let c: u8 = frac_buf[fraction_cursor]; - // Reconstruct digit. - let digit_0 = (c as char).to_digit(10).unwrap(); - if digit_0 + 1 >= radix as u32 { - continue; + loop { + // Shift up by one digit. + fraction *= radix as f64; + delta *= radix as f64; + // Write digit. + let digit = fraction as u32; + frac_buf[fraction_cursor] = + std::char::from_digit(digit, radix as u32).unwrap() as u8; + fraction_cursor += 1; + // Calculate remainder. + fraction -= digit as f64; + // Round to even. + if fraction + delta > 1.0 + && (fraction > 0.5 || (fraction - 0.5) < f64::EPSILON && digit & 1 != 0) + { + loop { + // We need to back trace already written digits in case of carry-over. + fraction_cursor -= 1; + if fraction_cursor == 0 { + // CHECK_EQ('.', buffer[fraction_cursor]); + // Carry over to the integer part. + integer += 1.; + break; + } else { + let c: u8 = frac_buf[fraction_cursor]; + // Reconstruct digit. + let digit_0 = (c as char).to_digit(10).unwrap(); + if digit_0 + 1 >= radix as u32 { + continue; + } + frac_buf[fraction_cursor] = + std::char::from_digit(digit_0 + 1, radix as u32).unwrap() as u8; + fraction_cursor += 1; + break; } - frac_buf[fraction_cursor] = - std::char::from_digit(digit_0 + 1, radix as u32).unwrap() as u8; - fraction_cursor += 1; - break; } + break; + } + if fraction < delta { + break; } - break; } - if fraction < delta { + } + + // Compute integer digits. Fill unrepresented digits with zero. + let mut int_iter = int_buf.iter_mut().enumerate().rev(); //.rev(); + while FloatCore::integer_decode(integer / f64::from(radix)).1 > 0 { + integer /= radix as f64; + *int_iter.next().unwrap().1 = b'0'; + } + + loop { + let remainder = integer % (radix as f64); + *int_iter.next().unwrap().1 = + std::char::from_digit(remainder as u32, radix as u32).unwrap() as u8; + integer = (integer - remainder) / radix as f64; + if integer <= 0f64 { break; } } - } + // Add sign and terminate string. + if negative { + *int_iter.next().unwrap().1 = b'-'; + } + assert!(fraction_cursor < BUF_SIZE); - // Compute integer digits. Fill unrepresented digits with zero. - let mut int_iter = int_buf.iter_mut().enumerate().rev(); //.rev(); - while FloatCore::integer_decode(integer / f64::from(radix)).1 > 0 { - integer /= radix as f64; - *int_iter.next().unwrap().1 = b'0'; + let integer_cursor = int_iter.next().unwrap().0 + 1; + let fraction_cursor = fraction_cursor + BUF_SIZE / 2; + // dbg!("Number: {}, Radix: {}, Cursors: {}, {}", value, radix, integer_cursor, fraction_cursor); + String::from_utf8_lossy(&buffer[integer_cursor..fraction_cursor]).into() } - loop { - let remainder = integer % (radix as f64); - *int_iter.next().unwrap().1 = - std::char::from_digit(remainder as u32, radix as u32).unwrap() as u8; - integer = (integer - remainder) / radix as f64; - if integer <= 0f64 { - break; + /// `Number.prototype.toString( [radix] )` + /// + /// The `toString()` method returns a string representing the specified Number object. + /// + /// More information: + /// - [ECMAScript reference][spec] + /// - [MDN documentation][mdn] + /// + /// [spec]: https://tc39.es/ecma262/#sec-number.prototype.tostring + /// [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Number/toString + #[allow(clippy::wrong_self_convention)] + pub(crate) fn to_string( + this: &mut Value, + args: &[Value], + ctx: &mut Interpreter, + ) -> ResultValue { + // 1. Let x be ? thisNumberValue(this value). + let x = Self::to_number(this).to_number(); + // 2. If radix is undefined, let radixNumber be 10. + // 3. Else, let radixNumber be ? ToInteger(radix). + let radix_number = args.get(0).map_or(10, |arg| arg.to_integer()) as u8; + + if x == -0. { + return Ok(Value::from("0")); + } else if x.is_nan() { + return Ok(Value::from("NaN")); + } else if x.is_infinite() && x.is_sign_positive() { + return Ok(Value::from("Infinity")); + } else if x.is_infinite() && x.is_sign_negative() { + return Ok(Value::from("-Infinity")); } - } - // Add sign and terminate string. - if negative { - *int_iter.next().unwrap().1 = b'-'; - } - assert!(fraction_cursor < BUF_SIZE); - let integer_cursor = int_iter.next().unwrap().0 + 1; - let fraction_cursor = fraction_cursor + BUF_SIZE / 2; - // dbg!("Number: {}, Radix: {}, Cursors: {}, {}", value, radix, integer_cursor, fraction_cursor); - String::from_utf8_lossy(&buffer[integer_cursor..fraction_cursor]).into() -} + // 4. If radixNumber < 2 or radixNumber > 36, throw a RangeError exception. + if radix_number < 2 || radix_number > 36 { + return Err(RangeError::run_new( + "radix must be an integer at least 2 and no greater than 36", + ctx, + )?); + } -/// `Number.prototype.toString( [radix] )` -/// -/// The `toString()` method returns a string representing the specified Number object. -/// -/// More information: -/// - [ECMAScript reference][spec] -/// - [MDN documentation][mdn] -/// -/// [spec]: https://tc39.es/ecma262/#sec-number.prototype.tostring -/// [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Number/toString -pub fn to_string(this: &mut Value, args: &[Value], _ctx: &mut Interpreter) -> ResultValue { - // 1. Let x be ? thisNumberValue(this value). - let x = to_number(this).to_number(); - // 2. If radix is undefined, let radixNumber be 10. - // 3. Else, let radixNumber be ? ToInteger(radix). - let radix_number = args.get(0).map_or(10, |arg| arg.to_integer()) as u8; - - if x == -0. { - return Ok(Value::from("0")); - } else if x.is_nan() { - return Ok(Value::from("NaN")); - } else if x.is_infinite() && x.is_sign_positive() { - return Ok(Value::from("Infinity")); - } else if x.is_infinite() && x.is_sign_negative() { - return Ok(Value::from("-Infinity")); - } + // 5. If radixNumber = 10, return ! ToString(x). + // This part should use exponential notations for long integer numbers commented tests + if radix_number == 10 { + // return Ok(to_value(format!("{}", Self::to_number(this).to_num()))); + return Ok(Value::from(format!("{}", x))); + } - // 4. If radixNumber < 2 or radixNumber > 36, throw a RangeError exception. - if radix_number < 2 || radix_number > 36 { - panic!("Radix must be between 2 and 36"); - } + // This is a Optimization from the v8 source code to print values that can fit in a single character + // Since the actual num_to_string allocates a 2200 bytes buffer for actual conversion + // I am not sure if this part is effective as the v8 equivalent https://chromium.googlesource.com/v8/v8/+/refs/heads/master/src/builtins/number.tq#53 + // // Fast case where the result is a one character string. + // if x.is_sign_positive() && x.fract() == 0.0 && x < radix_number as f64 { + // return Ok(to_value(format!("{}", std::char::from_digit(x as u32, radix_number as u32).unwrap()))) + // } - // 5. If radixNumber = 10, return ! ToString(x). - // This part should use exponential notations for long integer numbers commented tests - if radix_number == 10 { - // return Ok(to_value(format!("{}", to_number(this).to_num()))); - return Ok(Value::from(format!("{}", x))); + // 6. Return the String representation of this Number value using the radix specified by radixNumber. + Ok(Value::from(Self::num_to_string(x, radix_number))) } - // This is a Optimization from the v8 source code to print values that can fit in a single character - // Since the actual num_to_string allocates a 2200 bytes buffer for actual conversion - // I am not sure if this part is effective as the v8 equivalent https://chromium.googlesource.com/v8/v8/+/refs/heads/master/src/builtins/number.tq#53 - // // Fast case where the result is a one character string. - // if x.is_sign_positive() && x.fract() == 0.0 && x < radix_number as f64 { - // return Ok(to_value(format!("{}", std::char::from_digit(x as u32, radix_number as u32).unwrap()))) - // } - - // 6. Return the String representation of this Number value using the radix specified by radixNumber. - Ok(Value::from(num_to_string(x, radix_number))) -} - -/// `Number.prototype.toString()` -/// -/// The `valueOf()` method returns the wrapped primitive value of a Number object. -/// -/// More information: -/// - [ECMAScript reference][spec] -/// - [MDN documentation][mdn] -/// -/// [spec]: https://tc39.es/ecma262/#sec-number.prototype.valueof -/// [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Number/valueOf -pub fn value_of(this: &mut Value, _args: &[Value], _ctx: &mut Interpreter) -> ResultValue { - Ok(to_number(this)) -} - -/// Create a new `Number` object -pub fn create(global: &Value) -> Value { - let prototype = Value::new_object(Some(global)); - prototype.set_internal_slot("NumberData", Value::from(0)); - - make_builtin_fn!(to_exponential, named "toExponential", with length 1, of prototype); - make_builtin_fn!(to_fixed, named "toFixed", with length 1, of prototype); - make_builtin_fn!(to_locale_string, named "toLocaleString", of prototype); - make_builtin_fn!(to_precision, named "toPrecision", with length 1, of prototype); - make_builtin_fn!(to_string, named "toString", with length 1, of prototype); - make_builtin_fn!(value_of, named "valueOf", of prototype); + /// `Number.prototype.toString()` + /// + /// The `valueOf()` method returns the wrapped primitive value of a Number object. + /// + /// More information: + /// - [ECMAScript reference][spec] + /// - [MDN documentation][mdn] + /// + /// [spec]: https://tc39.es/ecma262/#sec-number.prototype.valueof + /// [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Number/valueOf + pub(crate) fn value_of( + this: &mut Value, + _args: &[Value], + _ctx: &mut Interpreter, + ) -> ResultValue { + Ok(Self::to_number(this)) + } - make_constructor_fn(make_number, global, prototype) -} + /// Create a new `Number` object + pub(crate) fn create(global: &Value) -> Value { + let prototype = Value::new_object(Some(global)); + prototype.set_internal_slot("NumberData", Value::from(0)); -/// Initialise the `Number` object on the global object. -#[inline] -pub fn init(global: &Value) { - global.set_field_slice("Number", create(global)); -} + make_builtin_fn(Self::to_exponential, "toExponential", &prototype, 1); + make_builtin_fn(Self::to_fixed, "toFixed", &prototype, 1); + make_builtin_fn(Self::to_locale_string, "toLocaleString", &prototype, 0); + make_builtin_fn(Self::to_precision, "toPrecision", &prototype, 1); + make_builtin_fn(Self::to_string, "toString", &prototype, 1); + make_builtin_fn(Self::value_of, "valueOf", &prototype, 0); -/// The abstract operation Number::equal takes arguments -/// x (a Number) and y (a Number). It performs the following steps when called: -/// -/// https://tc39.es/ecma262/#sec-numeric-types-number-equal -#[allow(clippy::float_cmp)] -pub fn equals(a: f64, b: f64) -> bool { - a == b -} + make_constructor_fn(Self::make_number, global, prototype) + } -/// The abstract operation Number::sameValue takes arguments -/// x (a Number) and y (a Number). It performs the following steps when called: -/// -/// https://tc39.es/ecma262/#sec-numeric-types-number-sameValue -#[allow(clippy::float_cmp)] -pub fn same_value(a: f64, b: f64) -> bool { - if a.is_nan() && b.is_nan() { - return true; + /// Initialise the `Number` object on the global object. + #[inline] + pub(crate) fn init(global: &Value) { + global.set_field("Number", Self::create(global)); } - if a == 0.0 && b == 0.0 { - if (a.is_sign_negative() && b.is_sign_positive()) - || (a.is_sign_positive() && b.is_sign_negative()) - { - return false; - }; - true - } else { + /// The abstract operation Number::equal takes arguments + /// x (a Number) and y (a Number). It performs the following steps when called: + /// + /// https://tc39.es/ecma262/#sec-numeric-types-number-equal + #[allow(clippy::float_cmp)] + pub(crate) fn equals(a: f64, b: f64) -> bool { a == b } -} -/// The abstract operation Number::sameValueZero takes arguments -/// x (a Number) and y (a Number). It performs the following steps when called: -/// -/// https://tc39.es/ecma262/#sec-numeric-types-number-sameValueZero -#[allow(clippy::float_cmp)] -pub fn same_value_zero(a: f64, b: f64) -> bool { - if a.is_nan() && b.is_nan() { - return true; + /// The abstract operation Number::sameValue takes arguments + /// x (a Number) and y (a Number). It performs the following steps when called: + /// + /// https://tc39.es/ecma262/#sec-numeric-types-number-sameValue + #[allow(clippy::float_cmp)] + pub(crate) fn same_value(a: f64, b: f64) -> bool { + if a.is_nan() && b.is_nan() { + return true; + } + + if a == 0.0 && b == 0.0 { + if (a.is_sign_negative() && b.is_sign_positive()) + || (a.is_sign_positive() && b.is_sign_negative()) + { + return false; + }; + true + } else { + a == b + } } - a == b + /// The abstract operation Number::sameValueZero takes arguments + /// x (a Number) and y (a Number). It performs the following steps when called: + /// + /// https://tc39.es/ecma262/#sec-numeric-types-number-sameValueZero + #[allow(clippy::float_cmp)] + pub(crate) fn same_value_zero(a: f64, b: f64) -> bool { + if a.is_nan() && b.is_nan() { + return true; + } + + a == b + } } diff --git a/boa/src/builtins/number/tests.rs b/boa/src/builtins/number/tests.rs index e99ff55079..4e01da96c8 100644 --- a/boa/src/builtins/number/tests.rs +++ b/boa/src/builtins/number/tests.rs @@ -1,18 +1,23 @@ #![allow(clippy::float_cmp)] -use crate::{builtins::value::Value, exec::Executor, forward, forward_val, realm::Realm}; +use crate::{ + builtins::{Number, Value}, + exec::Interpreter, + forward, forward_val, + realm::Realm, +}; #[test] fn check_number_constructor_is_function() { let global = Value::new_object(None); - let number_constructor = super::create(&global); + let number_constructor = Number::create(&global); assert_eq!(number_constructor.is_function(), true); } #[test] fn call_number() { let realm = Realm::create(); - let mut engine = Executor::new(realm); + let mut engine = Interpreter::new(realm); let init = r#" var default_zero = Number(); var int_one = Number(1); @@ -47,7 +52,7 @@ fn call_number() { #[test] fn to_exponential() { let realm = Realm::create(); - let mut engine = Executor::new(realm); + let mut engine = Interpreter::new(realm); let init = r#" var default_exp = Number().toExponential(); var int_exp = Number(5).toExponential(); @@ -76,7 +81,7 @@ fn to_exponential() { #[test] fn to_fixed() { let realm = Realm::create(); - let mut engine = Executor::new(realm); + let mut engine = Interpreter::new(realm); let init = r#" var default_fixed = Number().toFixed(); var pos_fixed = Number("3.456e+4").toFixed(); @@ -102,7 +107,7 @@ fn to_fixed() { #[test] fn to_locale_string() { let realm = Realm::create(); - let mut engine = Executor::new(realm); + let mut engine = Interpreter::new(realm); let init = r#" var default_locale = Number().toLocaleString(); var small_locale = Number(5).toLocaleString(); @@ -129,7 +134,7 @@ fn to_locale_string() { #[ignore] fn to_precision() { let realm = Realm::create(); - let mut engine = Executor::new(realm); + let mut engine = Interpreter::new(realm); let init = r#" var default_precision = Number().toPrecision(); var low_precision = Number(123456789).toPrecision(1); @@ -161,7 +166,7 @@ fn to_precision() { #[test] fn to_string() { let realm = Realm::create(); - let mut engine = Executor::new(realm); + let mut engine = Interpreter::new(realm); assert_eq!("NaN", &forward(&mut engine, "Number(NaN).toString()")); assert_eq!("Infinity", &forward(&mut engine, "Number(1/0).toString()")); @@ -328,7 +333,7 @@ fn to_string() { // https://github.com/jasonwilliams/boa/pull/381#discussion_r422458544 fn num_to_string_exponential() { let realm = Realm::create(); - let mut engine = Executor::new(realm); + let mut engine = Interpreter::new(realm); assert_eq!( String::from("111111111111111110000"), @@ -371,7 +376,7 @@ fn num_to_string_exponential() { #[test] fn value_of() { let realm = Realm::create(); - let mut engine = Executor::new(realm); + let mut engine = Interpreter::new(realm); // TODO: In addition to parsing numbers from strings, parse them bare As of October 2019 // the parser does not understand scientific e.g., Xe+Y or -Xe-Y notation. let init = r#" @@ -398,39 +403,39 @@ fn value_of() { #[test] fn equal() { - assert_eq!(super::equals(0.0, 0.0), true); - assert_eq!(super::equals(-0.0, 0.0), true); - assert_eq!(super::equals(0.0, -0.0), true); - assert_eq!(super::equals(f64::NAN, -0.0), false); - assert_eq!(super::equals(0.0, f64::NAN), false); + assert_eq!(Number::equals(0.0, 0.0), true); + assert_eq!(Number::equals(-0.0, 0.0), true); + assert_eq!(Number::equals(0.0, -0.0), true); + assert_eq!(Number::equals(f64::NAN, -0.0), false); + assert_eq!(Number::equals(0.0, f64::NAN), false); - assert_eq!(super::equals(1.0, 1.0), true); + assert_eq!(Number::equals(1.0, 1.0), true); } #[test] fn same_value() { - assert_eq!(super::same_value(0.0, 0.0), true); - assert_eq!(super::same_value(-0.0, 0.0), false); - assert_eq!(super::same_value(0.0, -0.0), false); - assert_eq!(super::same_value(f64::NAN, -0.0), false); - assert_eq!(super::same_value(0.0, f64::NAN), false); - assert_eq!(super::equals(1.0, 1.0), true); + assert_eq!(Number::same_value(0.0, 0.0), true); + assert_eq!(Number::same_value(-0.0, 0.0), false); + assert_eq!(Number::same_value(0.0, -0.0), false); + assert_eq!(Number::same_value(f64::NAN, -0.0), false); + assert_eq!(Number::same_value(0.0, f64::NAN), false); + assert_eq!(Number::equals(1.0, 1.0), true); } #[test] fn same_value_zero() { - assert_eq!(super::same_value_zero(0.0, 0.0), true); - assert_eq!(super::same_value_zero(-0.0, 0.0), true); - assert_eq!(super::same_value_zero(0.0, -0.0), true); - assert_eq!(super::same_value_zero(f64::NAN, -0.0), false); - assert_eq!(super::same_value_zero(0.0, f64::NAN), false); - assert_eq!(super::equals(1.0, 1.0), true); + assert_eq!(Number::same_value_zero(0.0, 0.0), true); + assert_eq!(Number::same_value_zero(-0.0, 0.0), true); + assert_eq!(Number::same_value_zero(0.0, -0.0), true); + assert_eq!(Number::same_value_zero(f64::NAN, -0.0), false); + assert_eq!(Number::same_value_zero(0.0, f64::NAN), false); + assert_eq!(Number::equals(1.0, 1.0), true); } #[test] fn from_bigint() { let realm = Realm::create(); - let mut engine = Executor::new(realm); + let mut engine = Interpreter::new(realm); assert_eq!(&forward(&mut engine, "Number(0n)"), "0",); assert_eq!(&forward(&mut engine, "Number(100000n)"), "100000",); diff --git a/boa/src/builtins/object/mod.rs b/boa/src/builtins/object/mod.rs index 88afd80c24..c2ec7df9ff 100644 --- a/boa/src/builtins/object/mod.rs +++ b/boa/src/builtins/object/mod.rs @@ -29,7 +29,7 @@ use std::{ ops::Deref, }; -use super::function::make_constructor_fn; +use super::function::{make_builtin_fn, make_constructor_fn}; pub use internal_methods_trait::ObjectInternalMethods; pub use internal_state::{InternalState, InternalStateCell}; @@ -548,7 +548,7 @@ pub fn make_object(_: &mut Value, args: &[Value], ctx: &mut Interpreter) -> Resu /// Get the `prototype` of an object. pub fn get_prototype_of(_: &mut Value, args: &[Value], _: &mut Interpreter) -> ResultValue { let obj = args.get(0).expect("Cannot get object"); - Ok(obj.get_field_slice(INSTANCE_PROTOTYPE)) + Ok(obj.get_field(INSTANCE_PROTOTYPE)) } /// Set the `prototype` of an object. @@ -611,15 +611,15 @@ pub fn has_own_property(this: &mut Value, args: &[Value], _: &mut Interpreter) - pub fn create(global: &Value) -> Value { let prototype = Value::new_object(None); - make_builtin_fn!(has_own_property, named "hasOwnProperty", of prototype); - make_builtin_fn!(to_string, named "toString", of prototype); + make_builtin_fn(has_own_property, "hasOwnProperty", &prototype, 0); + make_builtin_fn(to_string, "toString", &prototype, 0); let object = make_constructor_fn(make_object, global, prototype); - object.set_field_slice("length", Value::from(1)); - make_builtin_fn!(set_prototype_of, named "setPrototypeOf", with length 2, of object); - make_builtin_fn!(get_prototype_of, named "getPrototypeOf", with length 1, of object); - make_builtin_fn!(define_property, named "defineProperty", with length 3, of object); + object.set_field("length", Value::from(1)); + make_builtin_fn(set_prototype_of, "setPrototypeOf", &object, 2); + make_builtin_fn(get_prototype_of, "getPrototypeOf", &object, 1); + make_builtin_fn(define_property, "defineProperty", &object, 3); object } @@ -627,5 +627,5 @@ pub fn create(global: &Value) -> Value { /// Initialise the `Object` object on the global object. #[inline] pub fn init(global: &Value) { - global.set_field_slice("Object", create(global)); + global.set_field("Object", create(global)); } diff --git a/boa/src/builtins/property/mod.rs b/boa/src/builtins/property/mod.rs index 2153642b2f..5496e6256c 100644 --- a/boa/src/builtins/property/mod.rs +++ b/boa/src/builtins/property/mod.rs @@ -173,12 +173,12 @@ impl Default for Property { impl From<&Property> for Value { fn from(value: &Property) -> Value { let property = Value::new_object(None); - property.set_field_slice("configurable", Value::from(value.configurable)); - property.set_field_slice("enumerable", Value::from(value.enumerable)); - property.set_field_slice("writable", Value::from(value.writable)); - property.set_field_slice("value", value.value.clone().unwrap_or_else(Value::null)); - property.set_field_slice("get", value.get.clone().unwrap_or_else(Value::null)); - property.set_field_slice("set", value.set.clone().unwrap_or_else(Value::null)); + property.set_field("configurable", Value::from(value.configurable)); + property.set_field("enumerable", Value::from(value.enumerable)); + property.set_field("writable", Value::from(value.writable)); + property.set_field("value", value.value.clone().unwrap_or_else(Value::null)); + property.set_field("get", value.get.clone().unwrap_or_else(Value::null)); + property.set_field("set", value.set.clone().unwrap_or_else(Value::null)); property } } @@ -188,12 +188,12 @@ impl<'a> From<&'a Value> for Property { /// if they're not there default to false fn from(value: &Value) -> Self { Self { - configurable: { Some(bool::from(&value.get_field_slice("configurable"))) }, - enumerable: { Some(bool::from(&value.get_field_slice("enumerable"))) }, - writable: { Some(bool::from(&value.get_field_slice("writable"))) }, - value: Some(value.get_field_slice("value")), - get: Some(value.get_field_slice("get")), - set: Some(value.get_field_slice("set")), + configurable: { Some(bool::from(&value.get_field("configurable"))) }, + enumerable: { Some(bool::from(&value.get_field("enumerable"))) }, + writable: { Some(bool::from(&value.get_field("writable"))) }, + value: Some(value.get_field("value")), + get: Some(value.get_field("get")), + set: Some(value.get_field("set")), } } } diff --git a/boa/src/builtins/regexp/mod.rs b/boa/src/builtins/regexp/mod.rs index 521ebf8e51..53de4cf4dd 100644 --- a/boa/src/builtins/regexp/mod.rs +++ b/boa/src/builtins/regexp/mod.rs @@ -13,7 +13,7 @@ use std::ops::Deref; use regex::Regex; -use super::function::make_constructor_fn; +use super::function::{make_builtin_fn, make_constructor_fn}; use crate::{ builtins::{ object::{InternalState, ObjectKind}, @@ -28,7 +28,7 @@ mod tests; /// The internal representation on a `RegExp` object. #[derive(Debug)] -struct RegExp { +pub(crate) struct RegExp { /// Regex matcher. matcher: Regex, @@ -59,428 +59,438 @@ struct RegExp { impl InternalState for RegExp {} -/// Create a new `RegExp` -pub fn make_regexp(this: &mut Value, args: &[Value], _: &mut Interpreter) -> ResultValue { - if args.is_empty() { - return Err(Value::undefined()); - } - let mut regex_body = String::new(); - let mut regex_flags = String::new(); - #[allow(clippy::indexing_slicing)] // length has been checked - match args[0].deref() { - ValueData::String(ref body) => { - // first argument is a string -> use it as regex pattern - regex_body = body.into(); +impl RegExp { + /// Create a new `RegExp` + pub(crate) fn make_regexp( + this: &mut Value, + args: &[Value], + _: &mut Interpreter, + ) -> ResultValue { + if args.is_empty() { + return Err(Value::undefined()); } - ValueData::Object(ref obj) => { - let slots = &obj.borrow().internal_slots; - if slots.get("RegExpMatcher").is_some() { - // first argument is another `RegExp` object, so copy its pattern and flags - if let Some(body) = slots.get("OriginalSource") { - regex_body = String::from(body); - } - if let Some(flags) = slots.get("OriginalFlags") { - regex_flags = String::from(flags); + let mut regex_body = String::new(); + let mut regex_flags = String::new(); + #[allow(clippy::indexing_slicing)] // length has been checked + match args[0].deref() { + ValueData::String(ref body) => { + // first argument is a string -> use it as regex pattern + regex_body = body.into(); + } + ValueData::Object(ref obj) => { + let slots = &obj.borrow().internal_slots; + if slots.get("RegExpMatcher").is_some() { + // first argument is another `RegExp` object, so copy its pattern and flags + if let Some(body) = slots.get("OriginalSource") { + regex_body = String::from(body); + } + if let Some(flags) = slots.get("OriginalFlags") { + regex_flags = String::from(flags); + } } } + _ => return Err(Value::undefined()), } - _ => return Err(Value::undefined()), - } - // if a second argument is given and it's a string, use it as flags - match args.get(1) { - None => {} - Some(flags) => { - if let ValueData::String(flags) = flags.deref() { - regex_flags = flags.into(); + // if a second argument is given and it's a string, use it as flags + match args.get(1) { + None => {} + Some(flags) => { + if let ValueData::String(flags) = flags.deref() { + regex_flags = flags.into(); + } } } - } - // parse flags - let mut sorted_flags = String::new(); - let mut pattern = String::new(); - let mut dot_all = false; - let mut global = false; - let mut ignore_case = false; - let mut multiline = false; - let mut sticky = false; - let mut unicode = false; - if regex_flags.contains('g') { - global = true; - sorted_flags.push('g'); - } - if regex_flags.contains('i') { - ignore_case = true; - sorted_flags.push('i'); - pattern.push('i'); - } - if regex_flags.contains('m') { - multiline = true; - sorted_flags.push('m'); - pattern.push('m'); - } - if regex_flags.contains('s') { - dot_all = true; - sorted_flags.push('s'); - pattern.push('s'); - } - if regex_flags.contains('u') { - unicode = true; - sorted_flags.push('u'); - //pattern.push('s'); // rust uses utf-8 anyway + // parse flags + let mut sorted_flags = String::new(); + let mut pattern = String::new(); + let mut dot_all = false; + let mut global = false; + let mut ignore_case = false; + let mut multiline = false; + let mut sticky = false; + let mut unicode = false; + if regex_flags.contains('g') { + global = true; + sorted_flags.push('g'); + } + if regex_flags.contains('i') { + ignore_case = true; + sorted_flags.push('i'); + pattern.push('i'); + } + if regex_flags.contains('m') { + multiline = true; + sorted_flags.push('m'); + pattern.push('m'); + } + if regex_flags.contains('s') { + dot_all = true; + sorted_flags.push('s'); + pattern.push('s'); + } + if regex_flags.contains('u') { + unicode = true; + sorted_flags.push('u'); + //pattern.push('s'); // rust uses utf-8 anyway + } + if regex_flags.contains('y') { + sticky = true; + sorted_flags.push('y'); + } + // the `regex` crate uses '(?{flags})` inside the pattern to enable flags + if !pattern.is_empty() { + pattern = format!("(?{})", pattern); + } + pattern.push_str(regex_body.as_str()); + + let matcher = Regex::new(pattern.as_str()).expect("failed to create matcher"); + let regexp = RegExp { + matcher, + use_last_index: global || sticky, + flags: sorted_flags, + dot_all, + global, + ignore_case, + multiline, + sticky, + unicode, + }; + + // This value is used by console.log and other routines to match Object type + // to its Javascript Identifier (global constructor method name) + this.set_kind(ObjectKind::Ordinary); + this.set_internal_slot("RegExpMatcher", Value::undefined()); + this.set_internal_slot("OriginalSource", Value::from(regex_body)); + this.set_internal_slot("OriginalFlags", Value::from(regex_flags)); + + this.set_internal_state(regexp); + Ok(this.clone()) } - if regex_flags.contains('y') { - sticky = true; - sorted_flags.push('y'); + + /// `RegExp.prototype.dotAll` + /// + /// The `dotAll` property indicates whether or not the "`s`" flag is used with the regular expression. + /// + /// More information: + /// - [ECMAScript reference][spec] + /// - [MDN documentation][mdn] + /// + /// [spec]: https://tc39.es/ecma262/#sec-get-regexp.prototype.dotAll + /// [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/RegExp/dotAll + fn get_dot_all(this: &mut Value, _: &[Value], _: &mut Interpreter) -> ResultValue { + this.with_internal_state_ref(|regex: &RegExp| Ok(Value::from(regex.dot_all))) } - // the `regex` crate uses '(?{flags})` inside the pattern to enable flags - if !pattern.is_empty() { - pattern = format!("(?{})", pattern); + + /// `RegExp.prototype.flags` + /// + /// The `flags` property returns a string consisting of the [`flags`][flags] of the current regular expression object. + /// + /// More information: + /// - [ECMAScript reference][spec] + /// - [MDN documentation][mdn] + /// + /// [spec]: https://tc39.es/ecma262/#sec-get-regexp.prototype.flags + /// [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/RegExp/flags + /// [flags]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Regular_Expressions#Advanced_searching_with_flags_2 + fn get_flags(this: &mut Value, _: &[Value], _: &mut Interpreter) -> ResultValue { + this.with_internal_state_ref(|regex: &RegExp| Ok(Value::from(regex.flags.clone()))) } - pattern.push_str(regex_body.as_str()); - - let matcher = Regex::new(pattern.as_str()).expect("failed to create matcher"); - let regexp = RegExp { - matcher, - use_last_index: global || sticky, - flags: sorted_flags, - dot_all, - global, - ignore_case, - multiline, - sticky, - unicode, - }; - - // This value is used by console.log and other routines to match Object type - // to its Javascript Identifier (global constructor method name) - this.set_kind(ObjectKind::Ordinary); - this.set_internal_slot("RegExpMatcher", Value::undefined()); - this.set_internal_slot("OriginalSource", Value::from(regex_body)); - this.set_internal_slot("OriginalFlags", Value::from(regex_flags)); - - this.set_internal_state(regexp); - Ok(this.clone()) -} -/// `RegExp.prototype.dotAll` -/// -/// The `dotAll` property indicates whether or not the "`s`" flag is used with the regular expression. -/// -/// More information: -/// - [ECMAScript reference][spec] -/// - [MDN documentation][mdn] -/// -/// [spec]: https://tc39.es/ecma262/#sec-get-regexp.prototype.dotAll -/// [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/RegExp/dotAll -fn get_dot_all(this: &mut Value, _: &[Value], _: &mut Interpreter) -> ResultValue { - this.with_internal_state_ref(|regex: &RegExp| Ok(Value::from(regex.dot_all))) -} + /// `RegExp.prototype.global` + /// + /// The `global` property indicates whether or not the "`g`" flag is used with the regular expression. + /// + /// More information: + /// - [ECMAScript reference][spec] + /// - [MDN documentation][mdn] + /// + /// [spec]: https://tc39.es/ecma262/#sec-get-regexp.prototype.global + /// [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/RegExp/global + fn get_global(this: &mut Value, _: &[Value], _: &mut Interpreter) -> ResultValue { + this.with_internal_state_ref(|regex: &RegExp| Ok(Value::from(regex.global))) + } -/// `RegExp.prototype.flags` -/// -/// The `flags` property returns a string consisting of the [`flags`][flags] of the current regular expression object. -/// -/// More information: -/// - [ECMAScript reference][spec] -/// - [MDN documentation][mdn] -/// -/// [spec]: https://tc39.es/ecma262/#sec-get-regexp.prototype.flags -/// [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/RegExp/flags -/// [flags]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Regular_Expressions#Advanced_searching_with_flags_2 -fn get_flags(this: &mut Value, _: &[Value], _: &mut Interpreter) -> ResultValue { - this.with_internal_state_ref(|regex: &RegExp| Ok(Value::from(regex.flags.clone()))) -} + /// `RegExp.prototype.ignoreCase` + /// + /// The `ignoreCase` property indicates whether or not the "`i`" flag is used with the regular expression. + /// + /// More information: + /// - [ECMAScript reference][spec] + /// - [MDN documentation][mdn] + /// + /// [spec]: https://tc39.es/ecma262/#sec-get-regexp.prototype.ignorecase + /// [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/RegExp/ignoreCase + fn get_ignore_case(this: &mut Value, _: &[Value], _: &mut Interpreter) -> ResultValue { + this.with_internal_state_ref(|regex: &RegExp| Ok(Value::from(regex.ignore_case))) + } -/// `RegExp.prototype.global` -/// -/// The `global` property indicates whether or not the "`g`" flag is used with the regular expression. -/// -/// More information: -/// - [ECMAScript reference][spec] -/// - [MDN documentation][mdn] -/// -/// [spec]: https://tc39.es/ecma262/#sec-get-regexp.prototype.global -/// [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/RegExp/global -fn get_global(this: &mut Value, _: &[Value], _: &mut Interpreter) -> ResultValue { - this.with_internal_state_ref(|regex: &RegExp| Ok(Value::from(regex.global))) -} + /// `RegExp.prototype.multiline` + /// + /// The multiline property indicates whether or not the "m" flag is used with the regular expression. + /// + /// More information: + /// - [ECMAScript reference][spec] + /// - [MDN documentation][mdn] + /// + /// [spec]: https://tc39.es/ecma262/#sec-get-regexp.prototype.multiline + /// [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/RegExp/multiline + fn get_multiline(this: &mut Value, _: &[Value], _: &mut Interpreter) -> ResultValue { + this.with_internal_state_ref(|regex: &RegExp| Ok(Value::from(regex.multiline))) + } -/// `RegExp.prototype.ignoreCase` -/// -/// The `ignoreCase` property indicates whether or not the "`i`" flag is used with the regular expression. -/// -/// More information: -/// - [ECMAScript reference][spec] -/// - [MDN documentation][mdn] -/// -/// [spec]: https://tc39.es/ecma262/#sec-get-regexp.prototype.ignorecase -/// [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/RegExp/ignoreCase -fn get_ignore_case(this: &mut Value, _: &[Value], _: &mut Interpreter) -> ResultValue { - this.with_internal_state_ref(|regex: &RegExp| Ok(Value::from(regex.ignore_case))) -} + /// `RegExp.prototype.source` + /// + /// The `source` property returns a `String` containing the source text of the regexp object, + /// and it doesn't contain the two forward slashes on both sides and any flags. + /// + /// More information: + /// - [ECMAScript reference][spec] + /// - [MDN documentation][mdn] + /// + /// [spec]: https://tc39.es/ecma262/#sec-get-regexp.prototype.source + /// [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/RegExp/source + fn get_source(this: &mut Value, _: &[Value], _: &mut Interpreter) -> ResultValue { + Ok(this.get_internal_slot("OriginalSource")) + } -/// `RegExp.prototype.multiline` -/// -/// The multiline property indicates whether or not the "m" flag is used with the regular expression. -/// -/// More information: -/// - [ECMAScript reference][spec] -/// - [MDN documentation][mdn] -/// -/// [spec]: https://tc39.es/ecma262/#sec-get-regexp.prototype.multiline -/// [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/RegExp/multiline -fn get_multiline(this: &mut Value, _: &[Value], _: &mut Interpreter) -> ResultValue { - this.with_internal_state_ref(|regex: &RegExp| Ok(Value::from(regex.multiline))) -} + /// `RegExp.prototype.sticky` + /// + /// The `flags` property returns a string consisting of the [`flags`][flags] of the current regular expression object. + /// + /// More information: + /// - [ECMAScript reference][spec] + /// - [MDN documentation][mdn] + /// + /// [spec]: https://tc39.es/ecma262/#sec-get-regexp.prototype.sticky + /// [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/RegExp/sticky + fn get_sticky(this: &mut Value, _: &[Value], _: &mut Interpreter) -> ResultValue { + this.with_internal_state_ref(|regex: &RegExp| Ok(Value::from(regex.sticky))) + } -/// `RegExp.prototype.source` -/// -/// The `source` property returns a `String` containing the source text of the regexp object, -/// and it doesn't contain the two forward slashes on both sides and any flags. -/// -/// More information: -/// - [ECMAScript reference][spec] -/// - [MDN documentation][mdn] -/// -/// [spec]: https://tc39.es/ecma262/#sec-get-regexp.prototype.source -/// [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/RegExp/source -fn get_source(this: &mut Value, _: &[Value], _: &mut Interpreter) -> ResultValue { - Ok(this.get_internal_slot("OriginalSource")) -} + /// `RegExp.prototype.unicode` + /// + /// The unicode property indicates whether or not the "`u`" flag is used with a regular expression. + /// unicode is a read-only property of an individual regular expression instance. + /// + /// More information: + /// - [ECMAScript reference][spec] + /// - [MDN documentation][mdn] + /// + /// [spec]: https://tc39.es/ecma262/#sec-get-regexp.prototype.unicode + /// [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/RegExp/unicode + fn get_unicode(this: &mut Value, _: &[Value], _: &mut Interpreter) -> ResultValue { + this.with_internal_state_ref(|regex: &RegExp| Ok(Value::from(regex.unicode))) + } -/// `RegExp.prototype.sticky` -/// -/// The `flags` property returns a string consisting of the [`flags`][flags] of the current regular expression object. -/// -/// More information: -/// - [ECMAScript reference][spec] -/// - [MDN documentation][mdn] -/// -/// [spec]: https://tc39.es/ecma262/#sec-get-regexp.prototype.sticky -/// [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/RegExp/sticky -fn get_sticky(this: &mut Value, _: &[Value], _: &mut Interpreter) -> ResultValue { - this.with_internal_state_ref(|regex: &RegExp| Ok(Value::from(regex.sticky))) -} + /// `RegExp.prototype.test( string )` + /// + /// The `test()` method executes a search for a match between a regular expression and a specified string. + /// + /// Returns `true` or `false`. + /// + /// More information: + /// - [ECMAScript reference][spec] + /// - [MDN documentation][mdn] + /// + /// [spec]: https://tc39.es/ecma262/#sec-regexp.prototype.test + /// [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/RegExp/test + pub(crate) fn test(this: &mut Value, args: &[Value], _: &mut Interpreter) -> ResultValue { + let arg_str = String::from(args.get(0).expect("could not get argument")); + let mut last_index = usize::from(&this.get_field("lastIndex")); + let result = this.with_internal_state_ref(|regex: &RegExp| { + let result = if let Some(m) = regex.matcher.find_at(arg_str.as_str(), last_index) { + if regex.use_last_index { + last_index = m.end(); + } + true + } else { + if regex.use_last_index { + last_index = 0; + } + false + }; + Ok(Value::boolean(result)) + }); + this.set_field("lastIndex", Value::from(last_index)); + result + } -/// `RegExp.prototype.unicode` -/// -/// The unicode property indicates whether or not the "`u`" flag is used with a regular expression. -/// unicode is a read-only property of an individual regular expression instance. -/// -/// More information: -/// - [ECMAScript reference][spec] -/// - [MDN documentation][mdn] -/// -/// [spec]: https://tc39.es/ecma262/#sec-get-regexp.prototype.unicode -/// [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/RegExp/unicode -fn get_unicode(this: &mut Value, _: &[Value], _: &mut Interpreter) -> ResultValue { - this.with_internal_state_ref(|regex: &RegExp| Ok(Value::from(regex.unicode))) -} + /// `RegExp.prototype.exec( string )` + /// + /// The exec() method executes a search for a match in a specified string. + /// + /// Returns a result array, or `null`. + /// + /// More information: + /// - [ECMAScript reference][spec] + /// - [MDN documentation][mdn] + /// + /// [spec]: https://tc39.es/ecma262/#sec-regexp.prototype.exec + /// [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/RegExp/exec + pub(crate) fn exec(this: &mut Value, args: &[Value], _: &mut Interpreter) -> ResultValue { + let arg_str = String::from(args.get(0).expect("could not get argument")); + let mut last_index = usize::from(&this.get_field("lastIndex")); + let result = this.with_internal_state_ref(|regex: &RegExp| { + let mut locations = regex.matcher.capture_locations(); + let result = if let Some(m) = + regex + .matcher + .captures_read_at(&mut locations, arg_str.as_str(), last_index) + { + if regex.use_last_index { + last_index = m.end(); + } + let mut result = Vec::with_capacity(locations.len()); + for i in 0..locations.len() { + if let Some((start, end)) = locations.get(i) { + result.push(Value::from( + arg_str.get(start..end).expect("Could not get slice"), + )); + } else { + result.push(Value::undefined()); + } + } -/// `RegExp.prototype.test( string )` -/// -/// The `test()` method executes a search for a match between a regular expression and a specified string. -/// -/// Returns `true` or `false`. -/// -/// More information: -/// - [ECMAScript reference][spec] -/// - [MDN documentation][mdn] -/// -/// [spec]: https://tc39.es/ecma262/#sec-regexp.prototype.test -/// [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/RegExp/test -pub fn test(this: &mut Value, args: &[Value], _: &mut Interpreter) -> ResultValue { - let arg_str = String::from(args.get(0).expect("could not get argument")); - let mut last_index = usize::from(&this.get_field_slice("lastIndex")); - let result = this.with_internal_state_ref(|regex: &RegExp| { - let result = if let Some(m) = regex.matcher.find_at(arg_str.as_str(), last_index) { - if regex.use_last_index { - last_index = m.end(); - } - true - } else { - if regex.use_last_index { - last_index = 0; - } - false - }; - Ok(Value::boolean(result)) - }); - this.set_field_slice("lastIndex", Value::from(last_index)); - result -} + let result = Value::from(result); + result + .set_property_slice("index", Property::default().value(Value::from(m.start()))); + result.set_property_slice("input", Property::default().value(Value::from(arg_str))); + result + } else { + if regex.use_last_index { + last_index = 0; + } + Value::null() + }; + Ok(result) + }); + this.set_field("lastIndex", Value::from(last_index)); + result + } -/// `RegExp.prototype.exec( string )` -/// -/// The exec() method executes a search for a match in a specified string. -/// -/// Returns a result array, or `null`. -/// -/// More information: -/// - [ECMAScript reference][spec] -/// - [MDN documentation][mdn] -/// -/// [spec]: https://tc39.es/ecma262/#sec-regexp.prototype.exec -/// [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/RegExp/exec -pub fn exec(this: &mut Value, args: &[Value], _: &mut Interpreter) -> ResultValue { - let arg_str = String::from(args.get(0).expect("could not get argument")); - let mut last_index = usize::from(&this.get_field_slice("lastIndex")); - let result = this.with_internal_state_ref(|regex: &RegExp| { - let mut locations = regex.matcher.capture_locations(); - let result = if let Some(m) = - regex - .matcher - .captures_read_at(&mut locations, arg_str.as_str(), last_index) - { - if regex.use_last_index { - last_index = m.end(); + /// `RegExp.prototype[ @@match ]( string )` + /// + /// This method retrieves the matches when matching a string against a regular expression. + /// + /// More information: + /// - [ECMAScript reference][spec] + /// - [MDN documentation][mdn] + /// + /// [spec]: https://tc39.es/ecma262/#sec-regexp.prototype-@@match + /// [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/RegExp/@@match + pub(crate) fn r#match(this: &mut Value, arg: String, ctx: &mut Interpreter) -> ResultValue { + let (matcher, flags) = this + .with_internal_state_ref(|regex: &RegExp| (regex.matcher.clone(), regex.flags.clone())); + if flags.contains('g') { + let mut matches = Vec::new(); + for mat in matcher.find_iter(&arg) { + matches.push(Value::from(mat.as_str())); } - let mut result = Vec::with_capacity(locations.len()); - for i in 0..locations.len() { - if let Some((start, end)) = locations.get(i) { - result.push(Value::from( - arg_str.get(start..end).expect("Could not get slice"), - )); - } else { - result.push(Value::undefined()); - } + if matches.is_empty() { + return Ok(Value::null()); } - - let result = Value::from(result); - result.set_property_slice("index", Property::default().value(Value::from(m.start()))); - result.set_property_slice("input", Property::default().value(Value::from(arg_str))); - result + Ok(Value::from(matches)) } else { - if regex.use_last_index { - last_index = 0; - } - Value::null() - }; - Ok(result) - }); - this.set_field_slice("lastIndex", Value::from(last_index)); - result -} - -/// `RegExp.prototype[ @@match ]( string )` -/// -/// This method retrieves the matches when matching a string against a regular expression. -/// -/// More information: -/// - [ECMAScript reference][spec] -/// - [MDN documentation][mdn] -/// -/// [spec]: https://tc39.es/ecma262/#sec-regexp.prototype-@@match -/// [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/RegExp/@@match -pub fn r#match(this: &mut Value, arg: String, ctx: &mut Interpreter) -> ResultValue { - let (matcher, flags) = - this.with_internal_state_ref(|regex: &RegExp| (regex.matcher.clone(), regex.flags.clone())); - if flags.contains('g') { - let mut matches = Vec::new(); - for mat in matcher.find_iter(&arg) { - matches.push(Value::from(mat.as_str())); + Self::exec(this, &[Value::from(arg)], ctx) } - if matches.is_empty() { - return Ok(Value::null()); - } - Ok(Value::from(matches)) - } else { - exec(this, &[Value::from(arg)], ctx) } -} -/// `RegExp.prototype.toString()` -/// -/// Return a string representing the regular expression. -/// -/// More information: -/// - [ECMAScript reference][spec] -/// - [MDN documentation][mdn] -/// -/// [spec]: https://tc39.es/ecma262/#sec-regexp.prototype.tostring -/// [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/RegExp/toString -pub fn to_string(this: &mut Value, _: &[Value], _: &mut Interpreter) -> ResultValue { - let body = String::from(&this.get_internal_slot("OriginalSource")); - let flags = this.with_internal_state_ref(|regex: &RegExp| regex.flags.clone()); - Ok(Value::from(format!("/{}/{}", body, flags))) -} + /// `RegExp.prototype.toString()` + /// + /// Return a string representing the regular expression. + /// + /// More information: + /// - [ECMAScript reference][spec] + /// - [MDN documentation][mdn] + /// + /// [spec]: https://tc39.es/ecma262/#sec-regexp.prototype.tostring + /// [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/RegExp/toString + #[allow(clippy::wrong_self_convention)] + pub(crate) fn to_string(this: &mut Value, _: &[Value], _: &mut Interpreter) -> ResultValue { + let body = String::from(&this.get_internal_slot("OriginalSource")); + let flags = this.with_internal_state_ref(|regex: &RegExp| regex.flags.clone()); + Ok(Value::from(format!("/{}/{}", body, flags))) + } -/// `RegExp.prototype[ @@matchAll ]( string )` -/// -/// The `[@@matchAll]` method returns all matches of the regular expression against a string. -/// -/// More information: -/// - [ECMAScript reference][spec] -/// - [MDN documentation][mdn] -/// -/// [spec]: https://tc39.es/ecma262/#sec-regexp-prototype-matchall -/// [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/RegExp/@@matchAll -// TODO: it's returning an array, it should return an iterator -pub fn match_all(this: &mut Value, arg_str: String) -> ResultValue { - let matches: Vec = this.with_internal_state_ref(|regex: &RegExp| { - let mut matches = Vec::new(); - - for m in regex.matcher.find_iter(&arg_str) { - if let Some(caps) = regex.matcher.captures(&m.as_str()) { - let match_vec = caps - .iter() - .map(|group| match group { - Some(g) => Value::from(g.as_str()), - None => Value::undefined(), - }) - .collect::>(); - - let match_val = Value::from(match_vec); - - match_val - .set_property_slice("index", Property::default().value(Value::from(m.start()))); - match_val.set_property_slice( - "input", - Property::default().value(Value::from(arg_str.clone())), - ); - matches.push(match_val); - - if !regex.flags.contains('g') { - break; + /// `RegExp.prototype[ @@matchAll ]( string )` + /// + /// The `[@@matchAll]` method returns all matches of the regular expression against a string. + /// + /// More information: + /// - [ECMAScript reference][spec] + /// - [MDN documentation][mdn] + /// + /// [spec]: https://tc39.es/ecma262/#sec-regexp-prototype-matchall + /// [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/RegExp/@@matchAll + // TODO: it's returning an array, it should return an iterator + pub(crate) fn match_all(this: &mut Value, arg_str: String) -> ResultValue { + let matches: Vec = this.with_internal_state_ref(|regex: &RegExp| { + let mut matches = Vec::new(); + + for m in regex.matcher.find_iter(&arg_str) { + if let Some(caps) = regex.matcher.captures(&m.as_str()) { + let match_vec = caps + .iter() + .map(|group| match group { + Some(g) => Value::from(g.as_str()), + None => Value::undefined(), + }) + .collect::>(); + + let match_val = Value::from(match_vec); + + match_val.set_property_slice( + "index", + Property::default().value(Value::from(m.start())), + ); + match_val.set_property_slice( + "input", + Property::default().value(Value::from(arg_str.clone())), + ); + matches.push(match_val); + + if !regex.flags.contains('g') { + break; + } } } - } - matches - }); + matches + }); - let length = matches.len(); - let result = Value::from(matches); - result.set_field_slice("length", Value::from(length)); - result.set_kind(ObjectKind::Array); + let length = matches.len(); + let result = Value::from(matches); + result.set_field("length", Value::from(length)); + result.set_kind(ObjectKind::Array); - Ok(result) -} + Ok(result) + } -/// Create a new `RegExp` object. -pub fn create(global: &Value) -> Value { - // Create prototype - let prototype = Value::new_object(Some(global)); - prototype.set_field_slice("lastIndex", Value::from(0)); - - make_builtin_fn!(test, named "test", with length 1, of prototype); - make_builtin_fn!(exec, named "exec", with length 1, of prototype); - make_builtin_fn!(to_string, named "toString", of prototype); - make_builtin_fn!(get_dot_all, named "dotAll", of prototype); - make_builtin_fn!(get_flags, named "flags", of prototype); - make_builtin_fn!(get_global, named "global", of prototype); - make_builtin_fn!(get_ignore_case, named "ignoreCase", of prototype); - make_builtin_fn!(get_multiline, named "multiline", of prototype); - make_builtin_fn!(get_source, named "source", of prototype); - make_builtin_fn!(get_sticky, named "sticky", of prototype); - make_builtin_fn!(get_unicode, named "unicode", of prototype); - - make_constructor_fn(make_regexp, global, prototype) -} + /// Create a new `RegExp` object. + pub(crate) fn create(global: &Value) -> Value { + // Create prototype + let prototype = Value::new_object(Some(global)); + prototype.set_field("lastIndex", Value::from(0)); + + make_builtin_fn(Self::test, "test", &prototype, 1); + make_builtin_fn(Self::exec, "exec", &prototype, 1); + make_builtin_fn(Self::to_string, "toString", &prototype, 0); + make_builtin_fn(Self::get_dot_all, "dotAll", &prototype, 0); + make_builtin_fn(Self::get_flags, "flags", &prototype, 0); + make_builtin_fn(Self::get_global, "global", &prototype, 0); + make_builtin_fn(Self::get_ignore_case, "ignoreCase", &prototype, 0); + make_builtin_fn(Self::get_multiline, "multiline", &prototype, 0); + make_builtin_fn(Self::get_source, "source", &prototype, 0); + make_builtin_fn(Self::get_sticky, "sticky", &prototype, 0); + make_builtin_fn(Self::get_unicode, "unicode", &prototype, 0); + + make_constructor_fn(Self::make_regexp, global, prototype) + } -/// Initialise the `RegExp` object on the global object. -#[inline] -pub fn init(global: &Value) { - global.set_field_slice("RegExp", create(global)); + /// Initialise the `RegExp` object on the global object. + #[inline] + pub(crate) fn init(global: &Value) { + global.set_field("RegExp", Self::create(global)); + } } diff --git a/boa/src/builtins/regexp/tests.rs b/boa/src/builtins/regexp/tests.rs index dd2fbfc0cb..984a392a23 100644 --- a/boa/src/builtins/regexp/tests.rs +++ b/boa/src/builtins/regexp/tests.rs @@ -1,12 +1,10 @@ use super::*; -use crate::exec::Executor; -use crate::forward; -use crate::realm::Realm; +use crate::{exec::Interpreter, forward, realm::Realm}; #[test] fn constructors() { let realm = Realm::create(); - let mut engine = Executor::new(realm); + let mut engine = Interpreter::new(realm); let init = r#" var constructed = new RegExp("[0-9]+(\\.[0-9]+)?"); var literal = /[0-9]+(\.[0-9]+)?/; @@ -22,7 +20,7 @@ fn constructors() { #[test] fn check_regexp_constructor_is_function() { let global = Value::new_object(None); - let regexp_constructor = create(&global); + let regexp_constructor = RegExp::create(&global); assert_eq!(regexp_constructor.is_function(), true); } @@ -30,7 +28,7 @@ fn check_regexp_constructor_is_function() { // #[test] // fn flags() { -// let mut engine = Executor::new(); +// let mut engine = Interpreter::new(); // let init = r#" // var re_gi = /test/gi; // var re_sm = /test/sm; @@ -57,7 +55,7 @@ fn check_regexp_constructor_is_function() { #[test] fn last_index() { let realm = Realm::create(); - let mut engine = Executor::new(realm); + let mut engine = Interpreter::new(realm); let init = r#" var regex = /[0-9]+(\.[0-9]+)?/g; "#; @@ -73,7 +71,7 @@ fn last_index() { #[test] fn exec() { let realm = Realm::create(); - let mut engine = Executor::new(realm); + let mut engine = Interpreter::new(realm); let init = r#" var re = /quick\s(brown).+?(jumps)/ig; var result = re.exec('The Quick Brown Fox Jumps Over The Lazy Dog'); @@ -93,7 +91,7 @@ fn exec() { #[test] fn to_string() { let realm = Realm::create(); - let mut engine = Executor::new(realm); + let mut engine = Interpreter::new(realm); assert_eq!( forward(&mut engine, "(new RegExp('a+b+c')).toString()"), diff --git a/boa/src/builtins/string/mod.rs b/boa/src/builtins/string/mod.rs index e3c1a665ca..dc4da87832 100644 --- a/boa/src/builtins/string/mod.rs +++ b/boa/src/builtins/string/mod.rs @@ -12,716 +12,748 @@ #[cfg(test)] mod tests; -use super::function::make_constructor_fn; +use super::function::{make_builtin_fn, make_constructor_fn}; use crate::{ builtins::{ object::{Object, ObjectKind}, property::Property, - regexp::{make_regexp, match_all as regexp_match_all, r#match as regexp_match}, value::{ResultValue, Value, ValueData}, + RegExp, }, exec::Interpreter, }; use regex::Regex; +use std::string::String as StdString; use std::{ cmp::{max, min}, f64::NAN, ops::Deref, }; -/// [[Construct]] - Creates a new instance `this` -/// -/// [[Call]] - Returns a new native `string` -/// -pub fn make_string(this: &mut Value, args: &[Value], _: &mut Interpreter) -> ResultValue { - // This value is used by console.log and other routines to match Obexpecty"failed to parse argument for String method"pe - // to its Javascript Identifier (global constructor method name) - let s = args.get(0).unwrap_or(&Value::string("")).clone(); - let length_str = s.to_string().chars().count(); +/// JavaScript `String` implementation. +#[derive(Debug, Clone, Copy)] +pub(crate) struct String; + +impl String { + /// [[Construct]] - Creates a new instance `this` + /// + /// [[Call]] - Returns a new native `string` + /// + pub(crate) fn make_string( + this: &mut Value, + args: &[Value], + _: &mut Interpreter, + ) -> ResultValue { + // This value is used by console.log and other routines to match Obexpecty"failed to parse argument for String method"pe + // to its Javascript Identifier (global constructor method name) + let s = args.get(0).unwrap_or(&Value::string("")).clone(); + let length_str = s.to_string().chars().count(); + + this.set_field("length", Value::from(length_str as i32)); + + this.set_kind(ObjectKind::String); + this.set_internal_slot("StringData", s); + + let arg = match args.get(0) { + Some(v) => v.clone(), + None => Value::undefined(), + }; + + if arg.is_undefined() { + return Ok("".into()); + } + + Ok(Value::from(arg.to_string())) + } + + /// Get the string value to a primitive string + #[allow(clippy::wrong_self_convention)] + pub(crate) fn to_string(this: &mut Value, _: &[Value], _: &mut Interpreter) -> ResultValue { + // Get String from String Object and send it back as a new value + let primitive_val = this.get_internal_slot("StringData"); + Ok(Value::from(format!("{}", primitive_val))) + } - this.set_field_slice("length", Value::from(length_str as i32)); + /// `String.prototype.charAt( index )` + /// + /// The `String` object's `charAt()` method returns a new string consisting of the single UTF-16 code unit located at the specified offset into the string. + /// + /// Characters in a string are indexed from left to right. The index of the first character is `0`, + /// and the index of the last character—in a string called `stringName`—is `stringName.length - 1`. + /// If the `index` you supply is out of this range, JavaScript returns an empty string. + /// + /// If no index is provided to `charAt()`, the default is `0`. + /// + /// More information: + /// - [ECMAScript reference][spec] + /// - [MDN documentation][mdn] + /// + /// [spec]: https://tc39.es/ecma262/#sec-string.prototype.charat + /// [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String/charAt + pub(crate) fn char_at(this: &mut Value, args: &[Value], ctx: &mut Interpreter) -> ResultValue { + // First we get it the actual string a private field stored on the object only the engine has access to. + // Then we convert it into a Rust String by wrapping it in from_value + let primitive_val = ctx.value_to_rust_string(this); + let pos = i32::from( + args.get(0) + .expect("failed to get argument for String method"), + ); - this.set_kind(ObjectKind::String); - this.set_internal_slot("StringData", s); + // Calling .len() on a string would give the wrong result, as they are bytes not the number of + // unicode code points + // Note that this is an O(N) operation (because UTF-8 is complex) while getting the number of + // bytes is an O(1) operation. + let length = primitive_val.chars().count(); - let arg = match args.get(0) { - Some(v) => v.clone(), - None => Value::undefined(), - }; + // We should return an empty string is pos is out of range + if pos >= length as i32 || pos < 0 { + return Ok("".into()); + } - if arg.is_undefined() { - return Ok(Value::from(String::new())); + Ok(Value::from( + primitive_val + .chars() + .nth(pos as usize) + .expect("failed to get value"), + )) } - Ok(Value::from(arg.to_string())) -} + /// `String.prototype.charCodeAt( index )` + /// + /// The `charCodeAt()` method returns an integer between `0` and `65535` representing the UTF-16 code unit at the given index. + /// + /// Unicode code points range from `0` to `1114111` (`0x10FFFF`). The first 128 Unicode code points are a direct match of the ASCII character encoding. + /// + /// `charCodeAt()` returns `NaN` if the given index is less than `0`, or if it is equal to or greater than the `length` of the string. + /// + /// More information: + /// - [ECMAScript reference][spec] + /// - [MDN documentation][mdn] + /// + /// [spec]: https://tc39.es/ecma262/#sec-string.prototype.charcodeat + /// [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String/charCodeAt + pub(crate) fn char_code_at( + this: &mut Value, + args: &[Value], + ctx: &mut Interpreter, + ) -> ResultValue { + // First we get it the actual string a private field stored on the object only the engine has access to. + // Then we convert it into a Rust String by wrapping it in from_value + let primitive_val = ctx.value_to_rust_string(this); + + // Calling .len() on a string would give the wrong result, as they are bytes not the number of unicode code points + // Note that this is an O(N) operation (because UTF-8 is complex) while getting the number of bytes is an O(1) operation. + let length = primitive_val.chars().count(); + let pos = i32::from( + args.get(0) + .expect("failed to get argument for String method"), + ); -/// Get the string value to a primitive string -pub fn to_string(this: &mut Value, _: &[Value], _: &mut Interpreter) -> ResultValue { - // Get String from String Object and send it back as a new value - let primitive_val = this.get_internal_slot("StringData"); - Ok(Value::from(format!("{}", primitive_val))) -} + if pos >= length as i32 || pos < 0 { + return Ok(Value::from(NAN)); + } -/// `String.prototype.charAt( index )` -/// -/// The `String` object's `charAt()` method returns a new string consisting of the single UTF-16 code unit located at the specified offset into the string. -/// -/// Characters in a string are indexed from left to right. The index of the first character is `0`, -/// and the index of the last character—in a string called `stringName`—is `stringName.length - 1`. -/// If the `index` you supply is out of this range, JavaScript returns an empty string. -/// -/// If no index is provided to `charAt()`, the default is `0`. -/// -/// More information: -/// - [ECMAScript reference][spec] -/// - [MDN documentation][mdn] -/// -/// [spec]: https://tc39.es/ecma262/#sec-string.prototype.charat -/// [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String/charAt -pub fn char_at(this: &mut Value, args: &[Value], ctx: &mut Interpreter) -> ResultValue { - // First we get it the actual string a private field stored on the object only the engine has access to. - // Then we convert it into a Rust String by wrapping it in from_value - let primitive_val = ctx.value_to_rust_string(this); - let pos = i32::from( - args.get(0) - .expect("failed to get argument for String method"), - ); - - // Calling .len() on a string would give the wrong result, as they are bytes not the number of - // unicode code points - // Note that this is an O(N) operation (because UTF-8 is complex) while getting the number of - // bytes is an O(1) operation. - let length = primitive_val.chars().count(); - - // We should return an empty string is pos is out of range - if pos >= length as i32 || pos < 0 { - return Ok(Value::from(String::new())); + let utf16_val = primitive_val + .encode_utf16() + .nth(pos as usize) + .expect("failed to get utf16 value"); + // If there is no element at that index, the result is NaN + // TODO: We currently don't have NaN + Ok(Value::from(f64::from(utf16_val))) } - Ok(Value::from( - primitive_val - .chars() - .nth(pos as usize) - .expect("failed to get value"), - )) -} + /// `String.prototype.concat( str1[, ...strN] )` + /// + /// The `concat()` method concatenates the string arguments to the calling string and returns a new string. + /// + /// Changes to the original string or the returned string don't affect the other. + /// + /// If the arguments are not of the type string, they are converted to string values before concatenating. + /// + /// More information: + /// - [ECMAScript reference][spec] + /// - [MDN documentation][mdn] + /// + /// [spec]: https://tc39.es/ecma262/#sec-string.prototype.concat + /// [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String/concat + pub(crate) fn concat(this: &mut Value, args: &[Value], ctx: &mut Interpreter) -> ResultValue { + // First we get it the actual string a private field stored on the object only the engine has access to. + // Then we convert it into a Rust String by wrapping it in from_value + let mut new_str = ctx.value_to_rust_string(this); + + for arg in args { + let concat_str = arg.to_string(); + new_str.push_str(&concat_str); + } -/// `String.prototype.charCodeAt( index )` -/// -/// The `charCodeAt()` method returns an integer between `0` and `65535` representing the UTF-16 code unit at the given index. -/// -/// Unicode code points range from `0` to `1114111` (`0x10FFFF`). The first 128 Unicode code points are a direct match of the ASCII character encoding. -/// -/// `charCodeAt()` returns `NaN` if the given index is less than `0`, or if it is equal to or greater than the `length` of the string. -/// -/// More information: -/// - [ECMAScript reference][spec] -/// - [MDN documentation][mdn] -/// -/// [spec]: https://tc39.es/ecma262/#sec-string.prototype.charcodeat -/// [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String/charCodeAt -pub fn char_code_at(this: &mut Value, args: &[Value], ctx: &mut Interpreter) -> ResultValue { - // First we get it the actual string a private field stored on the object only the engine has access to. - // Then we convert it into a Rust String by wrapping it in from_value - let primitive_val: String = ctx.value_to_rust_string(this); - - // Calling .len() on a string would give the wrong result, as they are bytes not the number of unicode code points - // Note that this is an O(N) operation (because UTF-8 is complex) while getting the number of bytes is an O(1) operation. - let length = primitive_val.chars().count(); - let pos = i32::from( - args.get(0) - .expect("failed to get argument for String method"), - ); - - if pos >= length as i32 || pos < 0 { - return Ok(Value::from(NAN)); + Ok(Value::from(new_str)) } - let utf16_val = primitive_val - .encode_utf16() - .nth(pos as usize) - .expect("failed to get utf16 value"); - // If there is no element at that index, the result is NaN - // TODO: We currently don't have NaN - Ok(Value::from(f64::from(utf16_val))) -} + /// `String.prototype.repeat( count )` + /// + /// The `repeat()` method constructs and returns a new string which contains the specified number of + /// copies of the string on which it was called, concatenated together. + /// + /// More information: + /// - [ECMAScript reference][spec] + /// - [MDN documentation][mdn] + /// + /// [spec]: https://tc39.es/ecma262/#sec-string.prototype.repeat + /// [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String/repeat + pub(crate) fn repeat(this: &mut Value, args: &[Value], ctx: &mut Interpreter) -> ResultValue { + // First we get it the actual string a private field stored on the object only the engine has access to. + // Then we convert it into a Rust String by wrapping it in from_value + let primitive_val = ctx.value_to_rust_string(this); + + let repeat_times = usize::from( + args.get(0) + .expect("failed to get argument for String method"), + ); -/// `String.prototype.concat( str1[, ...strN] )` -/// -/// The `concat()` method concatenates the string arguments to the calling string and returns a new string. -/// -/// Changes to the original string or the returned string don't affect the other. -/// -/// If the arguments are not of the type string, they are converted to string values before concatenating. -/// -/// More information: -/// - [ECMAScript reference][spec] -/// - [MDN documentation][mdn] -/// -/// [spec]: https://tc39.es/ecma262/#sec-string.prototype.concat -/// [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String/concat -pub fn concat(this: &mut Value, args: &[Value], ctx: &mut Interpreter) -> ResultValue { - // First we get it the actual string a private field stored on the object only the engine has access to. - // Then we convert it into a Rust String by wrapping it in from_value - let mut new_str = ctx.value_to_rust_string(this); - - for arg in args { - let concat_str = String::from(arg); - new_str.push_str(&concat_str); + Ok(Value::from(primitive_val.repeat(repeat_times))) } - Ok(Value::from(new_str)) -} + /// `String.prototype.slice( beginIndex [, endIndex] )` + /// + /// The `slice()` method extracts a section of a string and returns it as a new string, without modifying the original string. + /// + /// More information: + /// - [ECMAScript reference][spec] + /// - [MDN documentation][mdn] + /// + /// [spec]: https://tc39.es/ecma262/#sec-string.prototype.slice + /// [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String/slice + pub(crate) fn slice(this: &mut Value, args: &[Value], ctx: &mut Interpreter) -> ResultValue { + // First we get it the actual string a private field stored on the object only the engine has access to. + // Then we convert it into a Rust String by wrapping it in from_value + let primitive_val = ctx.value_to_rust_string(this); + + let start = i32::from( + args.get(0) + .expect("failed to get argument for String method"), + ); -/// `String.prototype.repeat( count )` -/// -/// The `repeat()` method constructs and returns a new string which contains the specified number of -/// copies of the string on which it was called, concatenated together. -/// -/// More information: -/// - [ECMAScript reference][spec] -/// - [MDN documentation][mdn] -/// -/// [spec]: https://tc39.es/ecma262/#sec-string.prototype.repeat -/// [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String/repeat -pub fn repeat(this: &mut Value, args: &[Value], ctx: &mut Interpreter) -> ResultValue { - // First we get it the actual string a private field stored on the object only the engine has access to. - // Then we convert it into a Rust String by wrapping it in from_value - let primitive_val: String = ctx.value_to_rust_string(this); - - let repeat_times = usize::from( - args.get(0) - .expect("failed to get argument for String method"), - ); - - Ok(Value::from(primitive_val.repeat(repeat_times))) -} + let end = i32::from(args.get(1).expect("failed to get argument in slice")); -/// `String.prototype.slice( beginIndex [, endIndex] )` -/// -/// The `slice()` method extracts a section of a string and returns it as a new string, without modifying the original string. -/// -/// More information: -/// - [ECMAScript reference][spec] -/// - [MDN documentation][mdn] -/// -/// [spec]: https://tc39.es/ecma262/#sec-string.prototype.slice -/// [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String/slice -pub fn slice(this: &mut Value, args: &[Value], ctx: &mut Interpreter) -> ResultValue { - // First we get it the actual string a private field stored on the object only the engine has access to. - // Then we convert it into a Rust String by wrapping it in from_value - let primitive_val: String = ctx.value_to_rust_string(this); - - let start = i32::from( - args.get(0) - .expect("failed to get argument for String method"), - ); - - let end = i32::from(args.get(1).expect("failed to get argument in slice")); - - // Calling .len() on a string would give the wrong result, as they are bytes not the number of unicode code points - // Note that this is an O(N) operation (because UTF-8 is complex) while getting the number of bytes is an O(1) operation. - let length: i32 = primitive_val.chars().count() as i32; - - let from: i32 = if start < 0 { - max(length.wrapping_add(start), 0) - } else { - min(start, length) - }; - let to: i32 = if end < 0 { - max(length.wrapping_add(end), 0) - } else { - min(end, length) - }; - - let span = max(to.wrapping_sub(from), 0); - - let mut new_str = String::new(); - for i in from..from.wrapping_add(span) { - new_str.push( - primitive_val - .chars() - .nth(i as usize) - .expect("Could not get nth char"), - ); - } - Ok(Value::from(new_str)) -} + // Calling .len() on a string would give the wrong result, as they are bytes not the number of unicode code points + // Note that this is an O(N) operation (because UTF-8 is complex) while getting the number of bytes is an O(1) operation. + let length = primitive_val.chars().count() as i32; -/// `String.prototype.startWith( searchString[, position] )` -/// -/// The `startsWith()` method determines whether a string begins with the characters of a specified string, returning `true` or `false` as appropriate. -/// -/// More information: -/// - [ECMAScript reference][spec] -/// - [MDN documentation][mdn] -/// -/// [spec]: https://tc39.es/ecma262/#sec-string.prototype.startswith -/// [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String/startsWith -pub fn starts_with(this: &mut Value, args: &[Value], ctx: &mut Interpreter) -> ResultValue { - // First we get it the actual string a private field stored on the object only the engine has access to. - // Then we convert it into a Rust String by wrapping it in from_value - let primitive_val: String = ctx.value_to_rust_string(this); - - // TODO: Should throw TypeError if pattern is regular expression - let search_string = String::from( - args.get(0) - .expect("failed to get argument for String method"), - ); - - let length: i32 = primitive_val.chars().count() as i32; - let search_length: i32 = search_string.chars().count() as i32; - - // If less than 2 args specified, position is 'undefined', defaults to 0 - let position: i32 = if args.len() < 2 { - 0 - } else { - i32::from(args.get(1).expect("failed to get arg")) - }; - - let start = min(max(position, 0), length); - let end = start.wrapping_add(search_length); - - if end > length { - Ok(Value::from(false)) - } else { - // Only use the part of the string from "start" - let this_string: String = primitive_val.chars().skip(start as usize).collect(); - Ok(Value::from(this_string.starts_with(&search_string))) - } -} + let from = if start < 0 { + max(length.wrapping_add(start), 0) + } else { + min(start, length) + }; + let to = if end < 0 { + max(length.wrapping_add(end), 0) + } else { + min(end, length) + }; + + let span = max(to.wrapping_sub(from), 0); -/// `String.prototype.endsWith( searchString[, length] )` -/// -/// The `endsWith()` method determines whether a string ends with the characters of a specified string, returning `true` or `false` as appropriate. -/// -/// More information: -/// - [ECMAScript reference][spec] -/// - [MDN documentation][mdn] -/// -/// [spec]: https://tc39.es/ecma262/#sec-string.prototype.endswith -/// [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String/endsWith -pub fn ends_with(this: &mut Value, args: &[Value], ctx: &mut Interpreter) -> ResultValue { - // First we get it the actual string a private field stored on the object only the engine has access to. - // Then we convert it into a Rust String by wrapping it in from_value - let primitive_val: String = ctx.value_to_rust_string(this); - - // TODO: Should throw TypeError if search_string is regular expression - let search_string = String::from( - args.get(0) - .expect("failed to get argument for String method"), - ); - - let length: i32 = primitive_val.chars().count() as i32; - let search_length: i32 = search_string.chars().count() as i32; - - // If less than 2 args specified, end_position is 'undefined', defaults to - // length of this - let end_position: i32 = if args.len() < 2 { - length - } else { - i32::from(args.get(1).expect("Could not get argumetn")) - }; - - let end = min(max(end_position, 0), length); - let start = end.wrapping_sub(search_length); - - if start < 0 { - Ok(Value::from(false)) - } else { - // Only use the part of the string up to "end" - let this_string: String = primitive_val.chars().take(end as usize).collect(); - Ok(Value::from(this_string.ends_with(&search_string))) + let new_str: StdString = primitive_val + .chars() + .skip(from as usize) + .take(span as usize) + .collect(); + Ok(Value::from(new_str)) } -} -/// `String.prototype.includes( searchString[, position] )` -/// -/// The `includes()` method determines whether one string may be found within another string, returning `true` or `false` as appropriate. -/// -/// More information: -/// - [ECMAScript reference][spec] -/// - [MDN documentation][mdn] -/// -/// [spec]: https://tc39.es/ecma262/#sec-string.prototype.includes -/// [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String/includes -pub fn includes(this: &mut Value, args: &[Value], ctx: &mut Interpreter) -> ResultValue { - // First we get it the actual string a private field stored on the object only the engine has access to. - // Then we convert it into a Rust String by wrapping it in from_value - let primitive_val: String = ctx.value_to_rust_string(this); - - // TODO: Should throw TypeError if search_string is regular expression - let search_string = String::from( - args.get(0) - .expect("failed to get argument for String method"), - ); - - let length: i32 = primitive_val.chars().count() as i32; - - // If less than 2 args specified, position is 'undefined', defaults to 0 - let position: i32 = if args.len() < 2 { - 0 - } else { - i32::from(args.get(1).expect("Could not get argument")) - }; - - let start = min(max(position, 0), length); - - // Take the string from "this" and use only the part of it after "start" - let this_string: String = primitive_val.chars().skip(start as usize).collect(); - - Ok(Value::from(this_string.contains(&search_string))) -} + /// `String.prototype.startWith( searchString[, position] )` + /// + /// The `startsWith()` method determines whether a string begins with the characters of a specified string, returning `true` or `false` as appropriate. + /// + /// More information: + /// - [ECMAScript reference][spec] + /// - [MDN documentation][mdn] + /// + /// [spec]: https://tc39.es/ecma262/#sec-string.prototype.startswith + /// [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String/startsWith + pub(crate) fn starts_with( + this: &mut Value, + args: &[Value], + ctx: &mut Interpreter, + ) -> ResultValue { + // First we get it the actual string a private field stored on the object only the engine has access to. + // Then we convert it into a Rust String by wrapping it in from_value + let primitive_val = ctx.value_to_rust_string(this); + + // TODO: Should throw TypeError if pattern is regular expression + let search_string = StdString::from( + args.get(0) + .expect("failed to get argument for String method"), + ); -/// Return either the string itself or the string of the regex equivalent -fn get_regex_string(value: &Value) -> String { - match value.deref() { - ValueData::String(ref body) => body.into(), - ValueData::Object(ref obj) => { - let slots = &obj.borrow().internal_slots; - if slots.get("RegExpMatcher").is_some() { - // first argument is another `RegExp` object, so copy its pattern and flags - if let Some(body) = slots.get("OriginalSource") { - return String::from(r#body); - } - } - "undefined".to_string() + let length = primitive_val.chars().count() as i32; + let search_length = search_string.chars().count() as i32; + + // If less than 2 args specified, position is 'undefined', defaults to 0 + let position = if args.len() < 2 { + 0 + } else { + i32::from(args.get(1).expect("failed to get arg")) + }; + + let start = min(max(position, 0), length); + let end = start.wrapping_add(search_length); + + if end > length { + Ok(Value::from(false)) + } else { + // Only use the part of the string from "start" + let this_string: StdString = primitive_val.chars().skip(start as usize).collect(); + Ok(Value::from(this_string.starts_with(&search_string))) } - _ => "undefined".to_string(), } -} -/// `String.prototype.replace( regexp|substr, newSubstr|function )` -/// -/// The `replace()` method returns a new string with some or all matches of a `pattern` replaced by a `replacement`. -/// -/// The `pattern` can be a string or a `RegExp`, and the `replacement` can be a string or a function to be called for each match. -/// If `pattern` is a string, only the first occurrence will be replaced. -/// -/// The original string is left unchanged. -/// -/// More information: -/// - [ECMAScript reference][spec] -/// - [MDN documentation][mdn] -/// -/// [spec]: https://tc39.es/ecma262/#sec-string.prototype.replace -/// [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String/replace -pub fn replace(this: &mut Value, args: &[Value], ctx: &mut Interpreter) -> ResultValue { - // TODO: Support Symbol replacer - let primitive_val: String = ctx.value_to_rust_string(this); - if args.is_empty() { - return Ok(Value::from(primitive_val)); + /// `String.prototype.endsWith( searchString[, length] )` + /// + /// The `endsWith()` method determines whether a string ends with the characters of a specified string, returning `true` or `false` as appropriate. + /// + /// More information: + /// - [ECMAScript reference][spec] + /// - [MDN documentation][mdn] + /// + /// [spec]: https://tc39.es/ecma262/#sec-string.prototype.endswith + /// [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String/endsWith + pub(crate) fn ends_with( + this: &mut Value, + args: &[Value], + ctx: &mut Interpreter, + ) -> ResultValue { + // First we get it the actual string a private field stored on the object only the engine has access to. + // Then we convert it into a Rust String by wrapping it in from_value + let primitive_val = ctx.value_to_rust_string(this); + + // TODO: Should throw TypeError if search_string is regular expression + let search_string = StdString::from( + args.get(0) + .expect("failed to get argument for String method"), + ); + + let length = primitive_val.chars().count() as i32; + let search_length = search_string.chars().count() as i32; + + // If less than 2 args specified, end_position is 'undefined', defaults to + // length of this + let end_position = if args.len() < 2 { + length + } else { + i32::from(args.get(1).expect("Could not get argumetn")) + }; + + let end = min(max(end_position, 0), length); + let start = end.wrapping_sub(search_length); + + if start < 0 { + Ok(Value::from(false)) + } else { + // Only use the part of the string up to "end" + let this_string: StdString = primitive_val.chars().take(end as usize).collect(); + Ok(Value::from(this_string.ends_with(&search_string))) + } } - let regex_body = get_regex_string(args.get(0).expect("Value needed")); - let re = Regex::new(®ex_body).expect("unable to convert regex to regex object"); - let mat = re.find(&primitive_val).expect("unable to find value"); - let caps = re - .captures(&primitive_val) - .expect("unable to get capture groups from text"); - - let replace_value = if args.len() > 1 { - // replace_object could be a string or function or not exist at all - let replace_object: &Value = args.get(1).expect("second argument expected"); - match replace_object.deref() { - ValueData::String(val) => { - // https://tc39.es/ecma262/#table-45 - let mut result: String = val.to_string(); - let re = Regex::new(r"\$(\d)").unwrap(); - - if val.find("$$").is_some() { - result = val.replace("$$", "$") - } + /// `String.prototype.includes( searchString[, position] )` + /// + /// The `includes()` method determines whether one string may be found within another string, returning `true` or `false` as appropriate. + /// + /// More information: + /// - [ECMAScript reference][spec] + /// - [MDN documentation][mdn] + /// + /// [spec]: https://tc39.es/ecma262/#sec-string.prototype.includes + /// [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String/includes + pub(crate) fn includes(this: &mut Value, args: &[Value], ctx: &mut Interpreter) -> ResultValue { + // First we get it the actual string a private field stored on the object only the engine has access to. + // Then we convert it into a Rust String by wrapping it in from_value + let primitive_val = ctx.value_to_rust_string(this); + + // TODO: Should throw TypeError if search_string is regular expression + let search_string = StdString::from( + args.get(0) + .expect("failed to get argument for String method"), + ); - if val.find("$`").is_some() { - let start_of_match = mat.start(); - let slice = &primitive_val[..start_of_match]; - result = val.replace("$`", slice); - } + let length = primitive_val.chars().count() as i32; - if val.find("$'").is_some() { - let end_of_match = mat.end(); - let slice = &primitive_val[end_of_match..]; - result = val.replace("$'", slice); - } + // If less than 2 args specified, position is 'undefined', defaults to 0 + let position = if args.len() < 2 { + 0 + } else { + i32::from(args.get(1).expect("Could not get argument")) + }; - if val.find("$&").is_some() { - // get matched value - let matched = caps.get(0).expect("cannot get matched value"); - result = val.replace("$&", matched.as_str()); - } + let start = min(max(position, 0), length); - // Capture $1, $2, $3 etc - if re.is_match(&result) { - let mat_caps = re.captures(&result).unwrap(); - let group_str = mat_caps.get(1).unwrap().as_str(); - let group_int = group_str.parse::().unwrap(); - result = re - .replace(result.as_str(), caps.get(group_int).unwrap().as_str()) - .to_string() - } + // Take the string from "this" and use only the part of it after "start" + let this_string: StdString = primitive_val.chars().skip(start as usize).collect(); - result - } - ValueData::Object(_) => { - // This will return the matched substring first, then captured parenthesized groups later - let mut results: Vec = caps - .iter() - .map(|capture| Value::from(capture.unwrap().as_str())) - .collect(); - - // Returns the starting byte offset of the match - let start = caps - .get(0) - .expect("Unable to get Byte offset from string for match") - .start(); - results.push(Value::from(start)); - // Push the whole string being examined - results.push(Value::from(primitive_val.to_string())); - - let result = ctx.call(&replace_object, this, &results).unwrap(); - - ctx.value_to_rust_string(&result) + Ok(Value::from(this_string.contains(&search_string))) + } + + /// Return either the string itself or the string of the regex equivalent + fn get_regex_string(value: &Value) -> StdString { + match value.deref() { + ValueData::String(ref body) => body.into(), + ValueData::Object(ref obj) => { + let slots = &obj.borrow().internal_slots; + if slots.get("RegExpMatcher").is_some() { + // first argument is another `RegExp` object, so copy its pattern and flags + if let Some(body) = slots.get("OriginalSource") { + return body.to_string(); + } + } + "undefined".to_string() } _ => "undefined".to_string(), } - } else { - "undefined".to_string() - }; - - Ok(Value::from(primitive_val.replacen( - &mat.as_str(), - &replace_value, - 1, - ))) -} + } -/// `String.prototype.indexOf( searchValue[, fromIndex] )` -/// -/// The `indexOf()` method returns the index within the calling `String` object of the first occurrence of the specified value, starting the search at `fromIndex`. -/// -/// Returns -1 if the value is not found. -/// -/// More information: -/// - [ECMAScript reference][spec] -/// - [MDN documentation][mdn] -/// -/// [spec]: https://tc39.es/ecma262/#sec-string.prototype.indexof -/// [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String/indexOf -pub fn index_of(this: &mut Value, args: &[Value], ctx: &mut Interpreter) -> ResultValue { - // First we get it the actual string a private field stored on the object only the engine has access to. - // Then we convert it into a Rust String by wrapping it in from_value - let primitive_val: String = ctx.value_to_rust_string(this); - - // TODO: Should throw TypeError if search_string is regular expression - let search_string = String::from( - args.get(0) - .expect("failed to get argument for String method"), - ); - - let length: i32 = primitive_val.chars().count() as i32; - - // If less than 2 args specified, position is 'undefined', defaults to 0 - let position: i32 = if args.len() < 2 { - 0 - } else { - i32::from(args.get(1).expect("Could not get argument")) - }; - - let start = min(max(position, 0), length); - - // Here cannot use the &str method "find", because this returns the byte - // index: we need to return the char index in the JS String - // Instead, iterate over the part we're checking until the slice we're - // checking "starts with" the search string - for index in start..length { - let this_string: String = primitive_val.chars().skip(index as usize).collect(); - if this_string.starts_with(&search_string) { - // Explicitly return early with the index value - return Ok(Value::from(index)); + /// `String.prototype.replace( regexp|substr, newSubstr|function )` + /// + /// The `replace()` method returns a new string with some or all matches of a `pattern` replaced by a `replacement`. + /// + /// The `pattern` can be a string or a `RegExp`, and the `replacement` can be a string or a function to be called for each match. + /// If `pattern` is a string, only the first occurrence will be replaced. + /// + /// The original string is left unchanged. + /// + /// More information: + /// - [ECMAScript reference][spec] + /// - [MDN documentation][mdn] + /// + /// [spec]: https://tc39.es/ecma262/#sec-string.prototype.replace + /// [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String/replace + pub(crate) fn replace(this: &mut Value, args: &[Value], ctx: &mut Interpreter) -> ResultValue { + // TODO: Support Symbol replacer + let primitive_val = ctx.value_to_rust_string(this); + if args.is_empty() { + return Ok(Value::from(primitive_val)); } + + let regex_body = Self::get_regex_string(args.get(0).expect("Value needed")); + let re = Regex::new(®ex_body).expect("unable to convert regex to regex object"); + let mat = re.find(&primitive_val).expect("unable to find value"); + let caps = re + .captures(&primitive_val) + .expect("unable to get capture groups from text"); + + let replace_value = if args.len() > 1 { + // replace_object could be a string or function or not exist at all + let replace_object: &Value = args.get(1).expect("second argument expected"); + match replace_object.deref() { + ValueData::String(val) => { + // https://tc39.es/ecma262/#table-45 + let mut result = val.to_string(); + let re = Regex::new(r"\$(\d)").unwrap(); + + if val.find("$$").is_some() { + result = val.replace("$$", "$") + } + + if val.find("$`").is_some() { + let start_of_match = mat.start(); + let slice = &primitive_val[..start_of_match]; + result = val.replace("$`", slice); + } + + if val.find("$'").is_some() { + let end_of_match = mat.end(); + let slice = &primitive_val[end_of_match..]; + result = val.replace("$'", slice); + } + + if val.find("$&").is_some() { + // get matched value + let matched = caps.get(0).expect("cannot get matched value"); + result = val.replace("$&", matched.as_str()); + } + + // Capture $1, $2, $3 etc + if re.is_match(&result) { + let mat_caps = re.captures(&result).unwrap(); + let group_str = mat_caps.get(1).unwrap().as_str(); + let group_int = group_str.parse::().unwrap(); + result = re + .replace(result.as_str(), caps.get(group_int).unwrap().as_str()) + .to_string() + } + + result + } + ValueData::Object(_) => { + // This will return the matched substring first, then captured parenthesized groups later + let mut results: Vec = caps + .iter() + .map(|capture| Value::from(capture.unwrap().as_str())) + .collect(); + + // Returns the starting byte offset of the match + let start = caps + .get(0) + .expect("Unable to get Byte offset from string for match") + .start(); + results.push(Value::from(start)); + // Push the whole string being examined + results.push(Value::from(primitive_val.to_string())); + + let result = ctx.call(&replace_object, this, &results).unwrap(); + + ctx.value_to_rust_string(&result) + } + _ => "undefined".to_string(), + } + } else { + "undefined".to_string() + }; + + Ok(Value::from(primitive_val.replacen( + &mat.as_str(), + &replace_value, + 1, + ))) } - // Didn't find a match, so return -1 - Ok(Value::from(-1)) -} -/// `String.prototype.lastIndexOf( searchValue[, fromIndex] )` -/// -/// The `lastIndexOf()` method returns the index within the calling `String` object of the last occurrence of the specified value, searching backwards from `fromIndex`. -/// -/// Returns -1 if the value is not found. -/// -/// More information: -/// - [ECMAScript reference][spec] -/// - [MDN documentation][mdn] -/// -/// [spec]: https://tc39.es/ecma262/#sec-string.prototype.lastindexof -/// [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String/lastIndexOf -pub fn last_index_of(this: &mut Value, args: &[Value], ctx: &mut Interpreter) -> ResultValue { - // First we get it the actual string a private field stored on the object only the engine has access to. - // Then we convert it into a Rust String by wrapping it in from_value - let primitive_val: String = ctx.value_to_rust_string(this); - - // TODO: Should throw TypeError if search_string is regular expression - let search_string = String::from( - args.get(0) - .expect("failed to get argument for String method"), - ); - - let length: i32 = primitive_val.chars().count() as i32; - - // If less than 2 args specified, position is 'undefined', defaults to 0 - let position: i32 = if args.len() < 2 { - 0 - } else { - i32::from(args.get(1).expect("Could not get argument")) - }; - - let start = min(max(position, 0), length); - - // Here cannot use the &str method "rfind", because this returns the last - // byte index: we need to return the last char index in the JS String - // Instead, iterate over the part we're checking keeping track of the higher - // index we found that "starts with" the search string - let mut highest_index: i32 = -1; - for index in start..length { - let this_string: String = primitive_val.chars().skip(index as usize).collect(); - if this_string.starts_with(&search_string) { - highest_index = index; + /// `String.prototype.indexOf( searchValue[, fromIndex] )` + /// + /// The `indexOf()` method returns the index within the calling `String` object of the first occurrence of the specified value, starting the search at `fromIndex`. + /// + /// Returns -1 if the value is not found. + /// + /// More information: + /// - [ECMAScript reference][spec] + /// - [MDN documentation][mdn] + /// + /// [spec]: https://tc39.es/ecma262/#sec-string.prototype.indexof + /// [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String/indexOf + pub(crate) fn index_of(this: &mut Value, args: &[Value], ctx: &mut Interpreter) -> ResultValue { + // First we get it the actual string a private field stored on the object only the engine has access to. + // Then we convert it into a Rust String by wrapping it in from_value + let primitive_val = ctx.value_to_rust_string(this); + + // TODO: Should throw TypeError if search_string is regular expression + let search_string = StdString::from( + args.get(0) + .expect("failed to get argument for String method"), + ); + + let length = primitive_val.chars().count() as i32; + + // If less than 2 args specified, position is 'undefined', defaults to 0 + let position = if args.len() < 2 { + 0 + } else { + i32::from(args.get(1).expect("Could not get argument")) + }; + + let start = min(max(position, 0), length); + + // Here cannot use the &str method "find", because this returns the byte + // index: we need to return the char index in the JS String + // Instead, iterate over the part we're checking until the slice we're + // checking "starts with" the search string + for index in start..length { + let this_string: StdString = primitive_val.chars().skip(index as usize).collect(); + if this_string.starts_with(&search_string) { + // Explicitly return early with the index value + return Ok(Value::from(index)); + } } + // Didn't find a match, so return -1 + Ok(Value::from(-1)) } - // This will still be -1 if no matches were found, else with be >= 0 - Ok(Value::from(highest_index)) -} + /// `String.prototype.lastIndexOf( searchValue[, fromIndex] )` + /// + /// The `lastIndexOf()` method returns the index within the calling `String` object of the last occurrence of the specified value, searching backwards from `fromIndex`. + /// + /// Returns -1 if the value is not found. + /// + /// More information: + /// - [ECMAScript reference][spec] + /// - [MDN documentation][mdn] + /// + /// [spec]: https://tc39.es/ecma262/#sec-string.prototype.lastindexof + /// [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String/lastIndexOf + pub(crate) fn last_index_of( + this: &mut Value, + args: &[Value], + ctx: &mut Interpreter, + ) -> ResultValue { + // First we get it the actual string a private field stored on the object only the engine has access to. + // Then we convert it into a Rust String by wrapping it in from_value + let primitive_val = ctx.value_to_rust_string(this); + + // TODO: Should throw TypeError if search_string is regular expression + let search_string = StdString::from( + args.get(0) + .expect("failed to get argument for String method"), + ); -/// `String.prototype.match( regexp )` -/// -/// The `match()` method retrieves the result of matching a **string** against a [`regular expression`][regex]. -/// -/// More information: -/// - [ECMAScript reference][spec] -/// - [MDN documentation][mdn] -/// -/// [spec]: https://tc39.es/ecma262/#sec-string.prototype.match -/// [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String/match -/// [regex]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Regular_Expressions -pub fn r#match(this: &mut Value, args: &[Value], ctx: &mut Interpreter) -> ResultValue { - let mut re = make_regexp(&mut Value::from(Object::default()), &[args[0].clone()], ctx)?; - regexp_match(&mut re, ctx.value_to_rust_string(this), ctx) -} + let length = primitive_val.chars().count() as i32; + + // If less than 2 args specified, position is 'undefined', defaults to 0 + let position = if args.len() < 2 { + 0 + } else { + i32::from(args.get(1).expect("Could not get argument")) + }; + + let start = min(max(position, 0), length); + + // Here cannot use the &str method "rfind", because this returns the last + // byte index: we need to return the last char index in the JS String + // Instead, iterate over the part we're checking keeping track of the higher + // index we found that "starts with" the search string + let mut highest_index = -1; + for index in start..length { + let this_string: StdString = primitive_val.chars().skip(index as usize).collect(); + if this_string.starts_with(&search_string) { + highest_index = index; + } + } -/// Abstract method `StringPad`. -/// -/// Performs the actual string padding for padStart/End. -/// -fn string_pad( - primitive: String, - max_length: i32, - fill_string: Option, - at_start: bool, -) -> ResultValue { - let primitive_length = primitive.len() as i32; - - if max_length <= primitive_length { - return Ok(Value::from(primitive)); + // This will still be -1 if no matches were found, else with be >= 0 + Ok(Value::from(highest_index)) } - let filler = match fill_string { - Some(filler) => filler, - None => String::from(" "), - }; - - if filler == "" { - return Ok(Value::from(primitive)); + /// `String.prototype.match( regexp )` + /// + /// The `match()` method retrieves the result of matching a **string** against a [`regular expression`][regex]. + /// + /// More information: + /// - [ECMAScript reference][spec] + /// - [MDN documentation][mdn] + /// + /// [spec]: https://tc39.es/ecma262/#sec-string.prototype.match + /// [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String/match + /// [regex]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Regular_Expressions + pub(crate) fn r#match(this: &mut Value, args: &[Value], ctx: &mut Interpreter) -> ResultValue { + let mut re = + RegExp::make_regexp(&mut Value::from(Object::default()), &[args[0].clone()], ctx)?; + RegExp::r#match(&mut re, ctx.value_to_rust_string(this), ctx) } - let fill_len = max_length.wrapping_sub(primitive_length); - let mut fill_str = String::new(); + /// Abstract method `StringPad`. + /// + /// Performs the actual string padding for padStart/End. + /// + fn string_pad( + primitive: StdString, + max_length: i32, + fill_string: Option, + at_start: bool, + ) -> ResultValue { + let primitive_length = primitive.len() as i32; + + if max_length <= primitive_length { + return Ok(Value::from(primitive)); + } - while fill_str.len() < fill_len as usize { - fill_str.push_str(&filler); - } - // Cut to size max_length - let concat_fill_str: String = fill_str.chars().take(fill_len as usize).collect(); + let filler = match fill_string { + Some(filler) => filler, + None => " ".to_owned(), + }; - if at_start { - Ok(Value::from(format!("{}{}", concat_fill_str, &primitive))) - } else { - Ok(Value::from(format!("{}{}", primitive, &concat_fill_str))) - } -} + if filler == "" { + return Ok(Value::from(primitive)); + } + + let fill_len = max_length.wrapping_sub(primitive_length); + let mut fill_str = StdString::new(); + + while fill_str.len() < fill_len as usize { + fill_str.push_str(&filler); + } + // Cut to size max_length + let concat_fill_str: StdString = fill_str.chars().take(fill_len as usize).collect(); -/// `String.prototype.padEnd( targetLength[, padString] )` -/// -/// The `padEnd()` method pads the current string with a given string (repeated, if needed) so that the resulting string reaches a given length. -/// -/// The padding is applied from the end of the current string. -/// -/// More information: -/// - [ECMAScript reference][spec] -/// - [MDN documentation][mdn] -/// -/// [spec]: https://tc39.es/ecma262/#sec-string.prototype.padend -/// [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String/padEnd -pub fn pad_end(this: &mut Value, args: &[Value], ctx: &mut Interpreter) -> ResultValue { - let primitive_val: String = ctx.value_to_rust_string(this); - if args.is_empty() { - return Err(Value::from("padEnd requires maxLength argument")); + if at_start { + Ok(Value::from(format!("{}{}", concat_fill_str, &primitive))) + } else { + Ok(Value::from(format!("{}{}", primitive, &concat_fill_str))) + } } - let max_length = i32::from( - args.get(0) - .expect("failed to get argument for String method"), - ); - let fill_string: Option = match args.len() { - 1 => None, - _ => Some(String::from(args.get(1).expect("Could not get argument"))), - }; + /// `String.prototype.padEnd( targetLength[, padString] )` + /// + /// The `padEnd()` method pads the current string with a given string (repeated, if needed) so that the resulting string reaches a given length. + /// + /// The padding is applied from the end of the current string. + /// + /// More information: + /// - [ECMAScript reference][spec] + /// - [MDN documentation][mdn] + /// + /// [spec]: https://tc39.es/ecma262/#sec-string.prototype.padend + /// [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String/padEnd + pub(crate) fn pad_end(this: &mut Value, args: &[Value], ctx: &mut Interpreter) -> ResultValue { + let primitive_val = ctx.value_to_rust_string(this); + if args.is_empty() { + return Err(Value::from("padEnd requires maxLength argument")); + } + let max_length = i32::from( + args.get(0) + .expect("failed to get argument for String method"), + ); - string_pad(primitive_val, max_length, fill_string, false) -} + let fill_string = match args.len() { + 1 => None, + _ => Some(StdString::from( + args.get(1).expect("Could not get argument"), + )), + }; -/// `String.prototype.padStart( targetLength [, padString] )` -/// -/// The `padStart()` method pads the current string with another string (multiple times, if needed) until the resulting string reaches the given length. -/// -/// The padding is applied from the start of the current string. -/// -/// More information: -/// - [ECMAScript reference][spec] -/// - [MDN documentation][mdn] -/// -/// [spec]: https://tc39.es/ecma262/#sec-string.prototype.padstart -/// [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String/padStart -pub fn pad_start(this: &mut Value, args: &[Value], ctx: &mut Interpreter) -> ResultValue { - let primitive_val: String = ctx.value_to_rust_string(this); - if args.is_empty() { - return Err(Value::from("padStart requires maxLength argument")); + Self::string_pad(primitive_val, max_length, fill_string, false) } - let max_length = i32::from( - args.get(0) - .expect("failed to get argument for String method"), - ); - let fill_string: Option = match args.len() { - 1 => None, - _ => Some(String::from(args.get(1).expect("Could not get argument"))), - }; + /// `String.prototype.padStart( targetLength [, padString] )` + /// + /// The `padStart()` method pads the current string with another string (multiple times, if needed) until the resulting string reaches the given length. + /// + /// The padding is applied from the start of the current string. + /// + /// More information: + /// - [ECMAScript reference][spec] + /// - [MDN documentation][mdn] + /// + /// [spec]: https://tc39.es/ecma262/#sec-string.prototype.padstart + /// [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String/padStart + pub(crate) fn pad_start( + this: &mut Value, + args: &[Value], + ctx: &mut Interpreter, + ) -> ResultValue { + let primitive_val = ctx.value_to_rust_string(this); + if args.is_empty() { + return Err(Value::from("padStart requires maxLength argument")); + } + let max_length = i32::from( + args.get(0) + .expect("failed to get argument for String method"), + ); - string_pad(primitive_val, max_length, fill_string, true) -} + let fill_string = match args.len() { + 1 => None, + _ => Some(StdString::from( + args.get(1).expect("Could not get argument"), + )), + }; + + Self::string_pad(primitive_val, max_length, fill_string, true) + } -/// Helper function to check if a `char` is trimmable. -fn is_trimmable_whitespace(c: char) -> bool { - // The rust implementation of `trim` does not regard the same characters whitespace as ecma standard does - // - // Rust uses \p{White_Space} by default, which also includes: - // `\u{0085}' (next line) - // And does not include: - // '\u{FEFF}' (zero width non-breaking space) - match c { + /// Helper function to check if a `char` is trimmable. + fn is_trimmable_whitespace(c: char) -> bool { + // The rust implementation of `trim` does not regard the same characters whitespace as ecma standard does + // + // Rust uses \p{White_Space} by default, which also includes: + // `\u{0085}' (next line) + // And does not include: + // '\u{FEFF}' (zero width non-breaking space) + match c { // Explicit whitespace: https://tc39.es/ecma262/#sec-white-space '\u{0009}' | '\u{000B}' | '\u{000C}' | '\u{0020}' | '\u{00A0}' | '\u{FEFF}' | // Unicode Space_Seperator category @@ -730,302 +762,321 @@ fn is_trimmable_whitespace(c: char) -> bool { '\u{000A}' | '\u{000D}' | '\u{2028}' | '\u{2029}' => true, _ => false, } -} - -/// String.prototype.trim() -/// -/// The `trim()` method removes whitespace from both ends of a string. -/// -/// Whitespace in this context is all the whitespace characters (space, tab, no-break space, etc.) and all the line terminator characters (LF, CR, etc.). -/// -/// More information: -/// - [ECMAScript reference][spec] -/// - [MDN documentation][mdn] -/// -/// [spec]: https://tc39.es/ecma262/#sec-string.prototype.trim -/// [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String/trim -pub fn trim(this: &mut Value, _: &[Value], ctx: &mut Interpreter) -> ResultValue { - let this_str: String = ctx.value_to_rust_string(this); - Ok(Value::from(this_str.trim_matches(is_trimmable_whitespace))) -} - -/// `String.prototype.trimStart()` -/// -/// The `trimStart()` method removes whitespace from the beginning of a string. -/// -/// Whitespace in this context is all the whitespace characters (space, tab, no-break space, etc.) and all the line terminator characters (LF, CR, etc.). -/// -/// More information: -/// - [ECMAScript reference][spec] -/// - [MDN documentation][mdn] -/// -/// [spec]: https://tc39.es/ecma262/#sec-string.prototype.trimstart -/// [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String/trimStart -pub fn trim_start(this: &mut Value, _: &[Value], ctx: &mut Interpreter) -> ResultValue { - let this_str: String = ctx.value_to_rust_string(this); - Ok(Value::from( - this_str.trim_start_matches(is_trimmable_whitespace), - )) -} + } -/// String.prototype.trimEnd() -/// -/// The `trimEnd()` method removes whitespace from the end of a string. -/// -/// Whitespace in this context is all the whitespace characters (space, tab, no-break space, etc.) and all the line terminator characters (LF, CR, etc.). -/// -/// More information: -/// - [ECMAScript reference][spec] -/// - [MDN documentation][mdn] -/// -/// [spec]: https://tc39.es/ecma262/#sec-string.prototype.trimend -/// [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String/trimEnd -pub fn trim_end(this: &mut Value, _: &[Value], ctx: &mut Interpreter) -> ResultValue { - let this_str: String = ctx.value_to_rust_string(this); - Ok(Value::from( - this_str.trim_end_matches(is_trimmable_whitespace), - )) -} + /// String.prototype.trim() + /// + /// The `trim()` method removes whitespace from both ends of a string. + /// + /// Whitespace in this context is all the whitespace characters (space, tab, no-break space, etc.) and all the line terminator characters (LF, CR, etc.). + /// + /// More information: + /// - [ECMAScript reference][spec] + /// - [MDN documentation][mdn] + /// + /// [spec]: https://tc39.es/ecma262/#sec-string.prototype.trim + /// [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String/trim + pub(crate) fn trim(this: &mut Value, _: &[Value], ctx: &mut Interpreter) -> ResultValue { + let this_str = ctx.value_to_rust_string(this); + Ok(Value::from( + this_str.trim_matches(Self::is_trimmable_whitespace), + )) + } -/// `String.prototype.toLowerCase()` -/// -/// The `toLowerCase()` method returns the calling string value converted to lower case. -/// -/// More information: -/// - [ECMAScript reference][spec] -/// - [MDN documentation][mdn] -/// -/// [spec]: https://tc39.es/ecma262/#sec-string.prototype.tolowercase -/// [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String/toLowerCase -pub fn to_lowercase(this: &mut Value, _: &[Value], ctx: &mut Interpreter) -> ResultValue { - // First we get it the actual string a private field stored on the object only the engine has access to. - // Then we convert it into a Rust String by wrapping it in from_value - let this_str: String = ctx.value_to_rust_string(this); - // The Rust String is mapped to uppercase using the builtin .to_lowercase(). - // There might be corner cases where it does not behave exactly like Javascript expects - Ok(Value::from(this_str.to_lowercase())) -} + /// `String.prototype.trimStart()` + /// + /// The `trimStart()` method removes whitespace from the beginning of a string. + /// + /// Whitespace in this context is all the whitespace characters (space, tab, no-break space, etc.) and all the line terminator characters (LF, CR, etc.). + /// + /// More information: + /// - [ECMAScript reference][spec] + /// - [MDN documentation][mdn] + /// + /// [spec]: https://tc39.es/ecma262/#sec-string.prototype.trimstart + /// [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String/trimStart + pub(crate) fn trim_start(this: &mut Value, _: &[Value], ctx: &mut Interpreter) -> ResultValue { + let this_str = ctx.value_to_rust_string(this); + Ok(Value::from( + this_str.trim_start_matches(Self::is_trimmable_whitespace), + )) + } -/// `String.prototype.toUpperCase()` -/// -/// The `toUpperCase()` method returns the calling string value converted to uppercase. -/// -/// The value will be **converted** to a string if it isn't one -/// -/// More information: -/// - [ECMAScript reference][spec] -/// - [MDN documentation][mdn] -/// -/// [spec]: https://tc39.es/ecma262/#sec-string.prototype.toUppercase -/// [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String/toUpperCase -pub fn to_uppercase(this: &mut Value, _: &[Value], ctx: &mut Interpreter) -> ResultValue { - // First we get it the actual string a private field stored on the object only the engine has access to. - // Then we convert it into a Rust String by wrapping it in from_value - let this_str: String = ctx.value_to_rust_string(this); - // The Rust String is mapped to uppercase using the builtin .to_uppercase(). - // There might be corner cases where it does not behave exactly like Javascript expects - Ok(Value::from(this_str.to_uppercase())) -} + /// String.prototype.trimEnd() + /// + /// The `trimEnd()` method removes whitespace from the end of a string. + /// + /// Whitespace in this context is all the whitespace characters (space, tab, no-break space, etc.) and all the line terminator characters (LF, CR, etc.). + /// + /// More information: + /// - [ECMAScript reference][spec] + /// - [MDN documentation][mdn] + /// + /// [spec]: https://tc39.es/ecma262/#sec-string.prototype.trimend + /// [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String/trimEnd + pub(crate) fn trim_end(this: &mut Value, _: &[Value], ctx: &mut Interpreter) -> ResultValue { + let this_str = ctx.value_to_rust_string(this); + Ok(Value::from( + this_str.trim_end_matches(Self::is_trimmable_whitespace), + )) + } -/// `String.prototype.substring( indexStart[, indexEnd] )` -/// -/// The `substring()` method returns the part of the `string` between the start and end indexes, or to the end of the string. -/// -/// More information: -/// - [ECMAScript reference][spec] -/// - [MDN documentation][mdn] -/// -/// [spec]: https://tc39.es/ecma262/#sec-string.prototype.substring -/// [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String/substring -pub fn substring(this: &mut Value, args: &[Value], ctx: &mut Interpreter) -> ResultValue { - // First we get it the actual string a private field stored on the object only the engine has access to. - // Then we convert it into a Rust String by wrapping it in from_value - let primitive_val: String = ctx.value_to_rust_string(this); - // If no args are specified, start is 'undefined', defaults to 0 - let start = if args.is_empty() { - 0 - } else { - i32::from( - args.get(0) - .expect("failed to get argument for String method"), - ) - }; - let length: i32 = primitive_val.chars().count() as i32; - // If less than 2 args specified, end is the length of the this object converted to a String - let end = if args.len() < 2 { - length - } else { - i32::from(args.get(1).expect("Could not get argument")) - }; - // Both start and end args replaced by 0 if they were negative - // or by the length of the String if they were greater - let final_start = min(max(start, 0), length); - let final_end = min(max(end, 0), length); - // Start and end are swapped if start is greater than end - let from = min(final_start, final_end) as usize; - let to = max(final_start, final_end) as usize; - // Extract the part of the string contained between the start index and the end index - // where start is guaranteed to be smaller or equals to end - let extracted_string: String = primitive_val - .chars() - .skip(from) - .take(to.wrapping_sub(from)) - .collect(); - Ok(Value::from(extracted_string)) -} + /// `String.prototype.toLowerCase()` + /// + /// The `toLowerCase()` method returns the calling string value converted to lower case. + /// + /// More information: + /// - [ECMAScript reference][spec] + /// - [MDN documentation][mdn] + /// + /// [spec]: https://tc39.es/ecma262/#sec-string.prototype.tolowercase + /// [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String/toLowerCase + #[allow(clippy::wrong_self_convention)] + pub(crate) fn to_lowercase( + this: &mut Value, + _: &[Value], + ctx: &mut Interpreter, + ) -> ResultValue { + // First we get it the actual string a private field stored on the object only the engine has access to. + // Then we convert it into a Rust String by wrapping it in from_value + let this_str = ctx.value_to_rust_string(this); + // The Rust String is mapped to uppercase using the builtin .to_lowercase(). + // There might be corner cases where it does not behave exactly like Javascript expects + Ok(Value::from(this_str.to_lowercase())) + } -/// `String.prototype.substr( start[, length] )` -/// -/// The `substr()` method returns a portion of the string, starting at the specified index and extending for a given number of characters afterward. -/// -/// More information: -/// - [ECMAScript reference][spec] -/// - [MDN documentation][mdn] -/// -/// [spec]: https://tc39.es/ecma262/#sec-string.prototype.substr -/// [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String/substr -/// -pub fn substr(this: &mut Value, args: &[Value], ctx: &mut Interpreter) -> ResultValue { - // First we get it the actual string a private field stored on the object only the engine has access to. - // Then we convert it into a Rust String by wrapping it in from_value - let primitive_val: String = ctx.value_to_rust_string(this); - // If no args are specified, start is 'undefined', defaults to 0 - let mut start = if args.is_empty() { - 0 - } else { - i32::from( - args.get(0) - .expect("failed to get argument for String method"), - ) - }; - let length: i32 = primitive_val.chars().count() as i32; - // If less than 2 args specified, end is +infinity, the maximum number value. - // Using i32::max_value() should be safe because the final length used is at most - // the number of code units from start to the end of the string, - // which should always be smaller or equals to both +infinity and i32::max_value - let end = if args.len() < 2 { - i32::max_value() - } else { - i32::from(args.get(1).expect("Could not get argument")) - }; - // If start is negative it become the number of code units from the end of the string - if start < 0 { - start = max(length.wrapping_add(start), 0); + /// `String.prototype.toUpperCase()` + /// + /// The `toUpperCase()` method returns the calling string value converted to uppercase. + /// + /// The value will be **converted** to a string if it isn't one + /// + /// More information: + /// - [ECMAScript reference][spec] + /// - [MDN documentation][mdn] + /// + /// [spec]: https://tc39.es/ecma262/#sec-string.prototype.toUppercase + /// [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String/toUpperCase + #[allow(clippy::wrong_self_convention)] + pub(crate) fn to_uppercase( + this: &mut Value, + _: &[Value], + ctx: &mut Interpreter, + ) -> ResultValue { + // First we get it the actual string a private field stored on the object only the engine has access to. + // Then we convert it into a Rust String by wrapping it in from_value + let this_str = ctx.value_to_rust_string(this); + // The Rust String is mapped to uppercase using the builtin .to_uppercase(). + // There might be corner cases where it does not behave exactly like Javascript expects + Ok(Value::from(this_str.to_uppercase())) } - // length replaced by 0 if it was negative - // or by the number of code units from start to the end of the string if it was greater - let result_length = min(max(end, 0), length.wrapping_sub(start)); - // If length is negative we return an empty string - // otherwise we extract the part of the string from start and is length code units long - if result_length <= 0 { - Ok(Value::from("".to_string())) - } else { - let extracted_string: String = primitive_val + + /// `String.prototype.substring( indexStart[, indexEnd] )` + /// + /// The `substring()` method returns the part of the `string` between the start and end indexes, or to the end of the string. + /// + /// More information: + /// - [ECMAScript reference][spec] + /// - [MDN documentation][mdn] + /// + /// [spec]: https://tc39.es/ecma262/#sec-string.prototype.substring + /// [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String/substring + pub(crate) fn substring( + this: &mut Value, + args: &[Value], + ctx: &mut Interpreter, + ) -> ResultValue { + // First we get it the actual string a private field stored on the object only the engine has access to. + // Then we convert it into a Rust String by wrapping it in from_value + let primitive_val = ctx.value_to_rust_string(this); + // If no args are specified, start is 'undefined', defaults to 0 + let start = if args.is_empty() { + 0 + } else { + i32::from( + args.get(0) + .expect("failed to get argument for String method"), + ) + }; + let length = primitive_val.chars().count() as i32; + // If less than 2 args specified, end is the length of the this object converted to a String + let end = if args.len() < 2 { + length + } else { + i32::from(args.get(1).expect("Could not get argument")) + }; + // Both start and end args replaced by 0 if they were negative + // or by the length of the String if they were greater + let final_start = min(max(start, 0), length); + let final_end = min(max(end, 0), length); + // Start and end are swapped if start is greater than end + let from = min(final_start, final_end) as usize; + let to = max(final_start, final_end) as usize; + // Extract the part of the string contained between the start index and the end index + // where start is guaranteed to be smaller or equals to end + let extracted_string: StdString = primitive_val .chars() - .skip(start as usize) - .take(result_length as usize) + .skip(from) + .take(to.wrapping_sub(from)) .collect(); - Ok(Value::from(extracted_string)) } -} -/// String.prototype.valueOf() -/// -/// The `valueOf()` method returns the primitive value of a `String` object. -/// -/// More information: -/// - [ECMAScript reference][spec] -/// - [MDN documentation][mdn] -/// -/// [spec]: https://tc39.es/ecma262/#sec-string.prototype.value_of -/// [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String/valueOf -pub fn value_of(this: &mut Value, args: &[Value], ctx: &mut Interpreter) -> ResultValue { - // Use the to_string method because it is specified to do the same thing in this case - to_string(this, args, ctx) -} + /// `String.prototype.substr( start[, length] )` + /// + /// The `substr()` method returns a portion of the string, starting at the specified index and extending for a given number of characters afterward. + /// + /// More information: + /// - [ECMAScript reference][spec] + /// - [MDN documentation][mdn] + /// + /// [spec]: https://tc39.es/ecma262/#sec-string.prototype.substr + /// [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String/substr + /// + pub(crate) fn substr(this: &mut Value, args: &[Value], ctx: &mut Interpreter) -> ResultValue { + // First we get it the actual string a private field stored on the object only the engine has access to. + // Then we convert it into a Rust String by wrapping it in from_value + let primitive_val = ctx.value_to_rust_string(this); + // If no args are specified, start is 'undefined', defaults to 0 + let mut start = if args.is_empty() { + 0 + } else { + i32::from( + args.get(0) + .expect("failed to get argument for String method"), + ) + }; + let length = primitive_val.chars().count() as i32; + // If less than 2 args specified, end is +infinity, the maximum number value. + // Using i32::max_value() should be safe because the final length used is at most + // the number of code units from start to the end of the string, + // which should always be smaller or equals to both +infinity and i32::max_value + let end = if args.len() < 2 { + i32::max_value() + } else { + i32::from(args.get(1).expect("Could not get argument")) + }; + // If start is negative it become the number of code units from the end of the string + if start < 0 { + start = max(length.wrapping_add(start), 0); + } + // length replaced by 0 if it was negative + // or by the number of code units from start to the end of the string if it was greater + let result_length = min(max(end, 0), length.wrapping_sub(start)); + // If length is negative we return an empty string + // otherwise we extract the part of the string from start and is length code units long + if result_length <= 0 { + Ok(Value::from("")) + } else { + let extracted_string: StdString = primitive_val + .chars() + .skip(start as usize) + .take(result_length as usize) + .collect(); -/// `String.prototype.matchAll( regexp )` -/// -/// The `matchAll()` method returns an iterator of all results matching a string against a [`regular expression`][regex], including [capturing groups][cg]. -/// -/// More information: -/// - [ECMAScript reference][spec] -/// - [MDN documentation][mdn] -/// -/// [spec]: https://tc39.es/ecma262/#sec-string.prototype.matchall -/// [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String/matchAll -/// [regex]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Regular_Expressions -/// [cg]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Regular_Expressions/Groups_and_Ranges -// TODO: update this method to return iterator -pub fn match_all(this: &mut Value, args: &[Value], ctx: &mut Interpreter) -> ResultValue { - let mut re: Value = match args.get(0) { - Some(arg) => { - if arg.is_null() { - make_regexp( - &mut Value::from(Object::default()), - &[ - Value::from(ctx.value_to_rust_string(arg)), - Value::from(String::from("g")), - ], - ctx, - ) - } else if arg.is_undefined() { - make_regexp( - &mut Value::from(Object::default()), - &[Value::undefined(), Value::from(String::from("g"))], - ctx, - ) - } else { - Ok(arg.clone()) - } + Ok(Value::from(extracted_string)) } - None => make_regexp( - &mut Value::from(Object::default()), - &[Value::from(String::new()), Value::from("g")], - ctx, - ), - }?; - - regexp_match_all(&mut re, ctx.value_to_rust_string(this)) -} + } -/// Create a new `String` object. -pub fn create(global: &Value) -> Value { - // Create prototype - let prototype = Value::new_object(Some(global)); - let length = Property::default().value(Value::from(0)); - - prototype.set_property_slice("length", length); - make_builtin_fn!(char_at, named "charAt", with length 1, of prototype); - make_builtin_fn!(char_code_at, named "charCodeAt", with length 1, of prototype); - make_builtin_fn!(to_string, named "toString", of prototype); - make_builtin_fn!(concat, named "concat", with length 1, of prototype); - make_builtin_fn!(repeat, named "repeat", with length 1, of prototype); - make_builtin_fn!(slice, named "slice", with length 2, of prototype); - make_builtin_fn!(starts_with, named "startsWith", with length 1, of prototype); - make_builtin_fn!(ends_with, named "endsWith", with length 1, of prototype); - make_builtin_fn!(includes, named "includes", with length 1, of prototype); - make_builtin_fn!(index_of, named "indexOf", with length 1, of prototype); - make_builtin_fn!(last_index_of, named "lastIndexOf", with length 1, of prototype); - make_builtin_fn!(r#match, named "match", with length 1, of prototype); - make_builtin_fn!(pad_end, named "padEnd", with length 1, of prototype); - make_builtin_fn!(pad_start, named "padStart", with length 1, of prototype); - make_builtin_fn!(trim, named "trim", of prototype); - make_builtin_fn!(trim_start, named "trimStart", of prototype); - make_builtin_fn!(to_lowercase, named "toLowerCase", of prototype); - make_builtin_fn!(to_uppercase, named "toUpperCase", of prototype); - make_builtin_fn!(substring, named "substring", with length 2, of prototype); - make_builtin_fn!(substr, named "substr", with length 2, of prototype); - make_builtin_fn!(value_of, named "valueOf", of prototype); - make_builtin_fn!(match_all, named "matchAll", with length 1, of prototype); - make_builtin_fn!(replace, named "replace", with length 2, of prototype); - - make_constructor_fn(make_string, global, prototype) -} + /// String.prototype.valueOf() + /// + /// The `valueOf()` method returns the primitive value of a `String` object. + /// + /// More information: + /// - [ECMAScript reference][spec] + /// - [MDN documentation][mdn] + /// + /// [spec]: https://tc39.es/ecma262/#sec-string.prototype.value_of + /// [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String/valueOf + pub(crate) fn value_of(this: &mut Value, args: &[Value], ctx: &mut Interpreter) -> ResultValue { + // Use the to_string method because it is specified to do the same thing in this case + Self::to_string(this, args, ctx) + } -/// Initialise the `String` object on the global object. -#[inline] -pub fn init(global: &Value) { - global.set_field_slice("String", create(global)); + /// `String.prototype.matchAll( regexp )` + /// + /// The `matchAll()` method returns an iterator of all results matching a string against a [`regular expression`][regex], including [capturing groups][cg]. + /// + /// More information: + /// - [ECMAScript reference][spec] + /// - [MDN documentation][mdn] + /// + /// [spec]: https://tc39.es/ecma262/#sec-string.prototype.matchall + /// [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String/matchAll + /// [regex]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Regular_Expressions + /// [cg]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Regular_Expressions/Groups_and_Ranges + // TODO: update this method to return iterator + pub(crate) fn match_all( + this: &mut Value, + args: &[Value], + ctx: &mut Interpreter, + ) -> ResultValue { + let mut re: Value = match args.get(0) { + Some(arg) => { + if arg.is_null() { + RegExp::make_regexp( + &mut Value::from(Object::default()), + &[Value::from(ctx.value_to_rust_string(arg)), Value::from("g")], + ctx, + ) + } else if arg.is_undefined() { + RegExp::make_regexp( + &mut Value::from(Object::default()), + &[Value::undefined(), Value::from("g")], + ctx, + ) + } else { + Ok(arg.clone()) + } + } + None => RegExp::make_regexp( + &mut Value::from(Object::default()), + &[Value::from(""), Value::from("g")], + ctx, + ), + }?; + + RegExp::match_all(&mut re, ctx.value_to_rust_string(this)) + } + + /// Create a new `String` object. + pub(crate) fn create(global: &Value) -> Value { + // Create prototype + let prototype = Value::new_object(Some(global)); + let length = Property::default().value(Value::from(0)); + + prototype.set_property_slice("length", length); + make_builtin_fn(Self::char_at, "charAt", &prototype, 1); + make_builtin_fn(Self::char_code_at, "charCodeAt", &prototype, 1); + make_builtin_fn(Self::to_string, "toString", &prototype, 0); + make_builtin_fn(Self::concat, "concat", &prototype, 1); + make_builtin_fn(Self::repeat, "repeat", &prototype, 1); + make_builtin_fn(Self::slice, "slice", &prototype, 2); + make_builtin_fn(Self::starts_with, "startsWith", &prototype, 1); + make_builtin_fn(Self::ends_with, "endsWith", &prototype, 1); + make_builtin_fn(Self::includes, "includes", &prototype, 1); + make_builtin_fn(Self::index_of, "indexOf", &prototype, 1); + make_builtin_fn(Self::last_index_of, "lastIndexOf", &prototype, 1); + make_builtin_fn(Self::r#match, "match", &prototype, 1); + make_builtin_fn(Self::pad_end, "padEnd", &prototype, 1); + make_builtin_fn(Self::pad_start, "padStart", &prototype, 1); + make_builtin_fn(Self::trim, "trim", &prototype, 0); + make_builtin_fn(Self::trim_start, "trimStart", &prototype, 0); + make_builtin_fn(Self::trim_end, "trimEnd", &prototype, 0); + make_builtin_fn(Self::to_lowercase, "toLowerCase", &prototype, 0); + make_builtin_fn(Self::to_uppercase, "toUpperCase", &prototype, 0); + make_builtin_fn(Self::substring, "substring", &prototype, 2); + make_builtin_fn(Self::substr, "substr", &prototype, 2); + make_builtin_fn(Self::value_of, "valueOf", &prototype, 0); + make_builtin_fn(Self::match_all, "matchAll", &prototype, 1); + make_builtin_fn(Self::replace, "replace", &prototype, 2); + + make_constructor_fn(Self::make_string, global, prototype) + } + + /// Initialise the `String` object on the global object. + #[inline] + pub(crate) fn init(global: &Value) { + global.set_field("String", Self::create(global)); + } } diff --git a/boa/src/builtins/string/tests.rs b/boa/src/builtins/string/tests.rs index 52bdba3efa..31229bdb84 100644 --- a/boa/src/builtins/string/tests.rs +++ b/boa/src/builtins/string/tests.rs @@ -1,12 +1,10 @@ use super::*; -use crate::exec::Executor; -use crate::realm::Realm; -use crate::{forward, forward_val}; +use crate::{exec::Interpreter, forward, forward_val, realm::Realm}; #[test] fn check_string_constructor_is_function() { let global = Value::new_object(None); - let string_constructor = create(&global); + let string_constructor = String::create(&global); assert_eq!(string_constructor.is_function(), true); } @@ -14,7 +12,7 @@ fn check_string_constructor_is_function() { // TODO: re-enable when getProperty() is finished; // fn length() { // //TEST262: https://github.com/tc39/test262/blob/master/test/built-ins/String/length.js -// let mut engine = Executor::new(); +// let mut engine = Interpreter::new(); // let init = r#" // const a = new String(' '); // const b = new String('\ud834\udf06'); @@ -39,7 +37,7 @@ fn check_string_constructor_is_function() { #[test] fn new_string_has_length() { let realm = Realm::create(); - let mut engine = Executor::new(realm); + let mut engine = Interpreter::new(realm); let init = r#" let a = new String("1234"); a @@ -52,7 +50,7 @@ fn new_string_has_length() { #[test] fn new_utf8_string_has_length() { let realm = Realm::create(); - let mut engine = Executor::new(realm); + let mut engine = Interpreter::new(realm); let init = r#" let a = new String("中文"); a @@ -65,7 +63,7 @@ fn new_utf8_string_has_length() { #[test] fn concat() { let realm = Realm::create(); - let mut engine = Executor::new(realm); + let mut engine = Interpreter::new(realm); let init = r#" var hello = new String('Hello, '); var world = new String('world! '); @@ -85,12 +83,14 @@ fn concat() { /// Test the correct type is returned from call and construct fn construct_and_call() { let realm = Realm::create(); - let mut engine = Executor::new(realm); + let mut engine = Interpreter::new(realm); let init = r#" var hello = new String('Hello'); var world = String('world'); "#; - eprintln!("{}", forward(&mut engine, init)); + + forward(&mut engine, init); + let hello = forward_val(&mut engine, "hello").unwrap(); let world = forward_val(&mut engine, "world").unwrap(); @@ -101,50 +101,44 @@ fn construct_and_call() { #[test] fn repeat() { let realm = Realm::create(); - let mut engine = Executor::new(realm); + let mut engine = Interpreter::new(realm); let init = r#" var empty = new String(''); var en = new String('english'); var zh = new String('中文'); "#; - eprintln!("{}", forward(&mut engine, init)); - let empty = String::from(""); - assert_eq!(forward(&mut engine, "empty.repeat(0)"), empty); - assert_eq!(forward(&mut engine, "empty.repeat(1)"), empty); + forward(&mut engine, init); - assert_eq!(forward(&mut engine, "en.repeat(0)"), empty); - assert_eq!(forward(&mut engine, "zh.repeat(0)"), empty); + assert_eq!(forward(&mut engine, "empty.repeat(0)"), ""); + assert_eq!(forward(&mut engine, "empty.repeat(1)"), ""); - assert_eq!( - forward(&mut engine, "en.repeat(1)"), - String::from("english") - ); - assert_eq!( - forward(&mut engine, "zh.repeat(2)"), - String::from("中文中文") - ); + assert_eq!(forward(&mut engine, "en.repeat(0)"), ""); + assert_eq!(forward(&mut engine, "zh.repeat(0)"), ""); + + assert_eq!(forward(&mut engine, "en.repeat(1)"), "english"); + assert_eq!(forward(&mut engine, "zh.repeat(2)"), "中文中文"); } #[test] fn replace() { let realm = Realm::create(); - let mut engine = Executor::new(realm); + let mut engine = Interpreter::new(realm); let init = r#" var a = "abc"; a = a.replace("a", "2"); a "#; - eprintln!("{}", forward(&mut engine, init)); - let empty = String::from("2bc"); - assert_eq!(forward(&mut engine, "a"), empty); + forward(&mut engine, init); + + assert_eq!(forward(&mut engine, "a"), "2bc"); } #[test] fn replace_with_function() { let realm = Realm::create(); - let mut engine = Executor::new(realm); + let mut engine = Interpreter::new(realm); let init = r#" var a = "ecmascript is cool"; var p1, p2, p3; @@ -158,21 +152,20 @@ fn replace_with_function() { a = a.replace(/c(o)(o)(l)/, replacer); a; "#; - eprintln!("{}", forward(&mut engine, init)); - assert_eq!( - forward(&mut engine, "a"), - String::from("ecmascript is awesome!") - ); - assert_eq!(forward(&mut engine, "p1"), String::from("o")); - assert_eq!(forward(&mut engine, "p2"), String::from("o")); - assert_eq!(forward(&mut engine, "p3"), String::from("l")); + forward(&mut engine, init); + + assert_eq!(forward(&mut engine, "a"), "ecmascript is awesome!"); + + assert_eq!(forward(&mut engine, "p1"), "o"); + assert_eq!(forward(&mut engine, "p2"), "o"); + assert_eq!(forward(&mut engine, "p3"), "l"); } #[test] fn starts_with() { let realm = Realm::create(); - let mut engine = Executor::new(realm); + let mut engine = Interpreter::new(realm); let init = r#" var empty = new String(''); var en = new String('english'); @@ -182,21 +175,22 @@ fn starts_with() { var enLiteral = 'english'; var zhLiteral = '中文'; "#; - eprintln!("{}", forward(&mut engine, init)); - let pass = String::from("true"); - assert_eq!(forward(&mut engine, "empty.startsWith('')"), pass); - assert_eq!(forward(&mut engine, "en.startsWith('e')"), pass); - assert_eq!(forward(&mut engine, "zh.startsWith('中')"), pass); - - assert_eq!(forward(&mut engine, "emptyLiteral.startsWith('')"), pass); - assert_eq!(forward(&mut engine, "enLiteral.startsWith('e')"), pass); - assert_eq!(forward(&mut engine, "zhLiteral.startsWith('中')"), pass); + + forward(&mut engine, init); + + assert_eq!(forward(&mut engine, "empty.startsWith('')"), "true"); + assert_eq!(forward(&mut engine, "en.startsWith('e')"), "true"); + assert_eq!(forward(&mut engine, "zh.startsWith('中')"), "true"); + + assert_eq!(forward(&mut engine, "emptyLiteral.startsWith('')"), "true"); + assert_eq!(forward(&mut engine, "enLiteral.startsWith('e')"), "true"); + assert_eq!(forward(&mut engine, "zhLiteral.startsWith('中')"), "true"); } #[test] fn ends_with() { let realm = Realm::create(); - let mut engine = Executor::new(realm); + let mut engine = Interpreter::new(realm); let init = r#" var empty = new String(''); var en = new String('english'); @@ -206,70 +200,45 @@ fn ends_with() { var enLiteral = 'english'; var zhLiteral = '中文'; "#; - eprintln!("{}", forward(&mut engine, init)); - let pass = String::from("true"); - assert_eq!(forward(&mut engine, "empty.endsWith('')"), pass); - assert_eq!(forward(&mut engine, "en.endsWith('h')"), pass); - assert_eq!(forward(&mut engine, "zh.endsWith('文')"), pass); - - assert_eq!(forward(&mut engine, "emptyLiteral.endsWith('')"), pass); - assert_eq!(forward(&mut engine, "enLiteral.endsWith('h')"), pass); - assert_eq!(forward(&mut engine, "zhLiteral.endsWith('文')"), pass); + + forward(&mut engine, init); + + assert_eq!(forward(&mut engine, "empty.endsWith('')"), "true"); + assert_eq!(forward(&mut engine, "en.endsWith('h')"), "true"); + assert_eq!(forward(&mut engine, "zh.endsWith('文')"), "true"); + + assert_eq!(forward(&mut engine, "emptyLiteral.endsWith('')"), "true"); + assert_eq!(forward(&mut engine, "enLiteral.endsWith('h')"), "true"); + assert_eq!(forward(&mut engine, "zhLiteral.endsWith('文')"), "true"); } #[test] fn match_all() { let realm = Realm::create(); - let mut engine = Executor::new(realm); + let mut engine = Interpreter::new(realm); - assert_eq!( - forward(&mut engine, "'aa'.matchAll(null).length"), - String::from("0") - ); - assert_eq!( - forward(&mut engine, "'aa'.matchAll(/b/).length"), - String::from("0") - ); - assert_eq!( - forward(&mut engine, "'aa'.matchAll(/a/).length"), - String::from("1") - ); - assert_eq!( - forward(&mut engine, "'aa'.matchAll(/a/g).length"), - String::from("2") - ); + assert_eq!(forward(&mut engine, "'aa'.matchAll(null).length"), "0"); + assert_eq!(forward(&mut engine, "'aa'.matchAll(/b/).length"), "0"); + assert_eq!(forward(&mut engine, "'aa'.matchAll(/a/).length"), "1"); + assert_eq!(forward(&mut engine, "'aa'.matchAll(/a/g).length"), "2"); forward( &mut engine, "var groupMatches = 'test1test2'.matchAll(/t(e)(st(\\d?))/g)", ); - assert_eq!( - forward(&mut engine, "groupMatches.length"), - String::from("2") - ); - assert_eq!( - forward(&mut engine, "groupMatches[0][1]"), - String::from("e") - ); - assert_eq!( - forward(&mut engine, "groupMatches[0][2]"), - String::from("st1") - ); - assert_eq!( - forward(&mut engine, "groupMatches[0][3]"), - String::from("1") - ); - assert_eq!( - forward(&mut engine, "groupMatches[1][3]"), - String::from("2") - ); + + assert_eq!(forward(&mut engine, "groupMatches.length"), "2"); + assert_eq!(forward(&mut engine, "groupMatches[0][1]"), "e"); + assert_eq!(forward(&mut engine, "groupMatches[0][2]"), "st1"); + assert_eq!(forward(&mut engine, "groupMatches[0][3]"), "1"); + assert_eq!(forward(&mut engine, "groupMatches[1][3]"), "2"); assert_eq!( forward( &mut engine, "'test1test2'.matchAll(/t(e)(st(\\d?))/).length" ), - String::from("1") + "1" ); let init = r#" @@ -277,23 +246,19 @@ fn match_all() { var str = 'table football, foosball'; var matches = str.matchAll(regexp); "#; - eprintln!("{}", forward(&mut engine, init)); - assert_eq!( - forward(&mut engine, "matches[0][0]"), - String::from("football") - ); - assert_eq!(forward(&mut engine, "matches[0].index"), String::from("6")); - assert_eq!( - forward(&mut engine, "matches[1][0]"), - String::from("foosball") - ); - assert_eq!(forward(&mut engine, "matches[1].index"), String::from("16")); + + forward(&mut engine, init); + + assert_eq!(forward(&mut engine, "matches[0][0]"), "football"); + assert_eq!(forward(&mut engine, "matches[0].index"), "6"); + assert_eq!(forward(&mut engine, "matches[1][0]"), "foosball"); + assert_eq!(forward(&mut engine, "matches[1].index"), "16"); } #[test] fn test_match() { let realm = Realm::create(); - let mut engine = Executor::new(realm); + let mut engine = Interpreter::new(realm); let init = r#" var str = new String('The Quick Brown Fox Jumps Over The Lazy Dog'); var result1 = str.match(/quick\s(brown).+?(jumps)/i); @@ -302,7 +267,8 @@ fn test_match() { var result4 = str.match(RegExp("B", 'g')); "#; - eprintln!("{}", forward(&mut engine, init)); + forward(&mut engine, init); + assert_eq!(forward(&mut engine, "result1[0]"), "Quick Brown Fox Jumps"); assert_eq!(forward(&mut engine, "result1[1]"), "Brown"); assert_eq!(forward(&mut engine, "result1[2]"), "Jumps"); diff --git a/boa/src/builtins/symbol/mod.rs b/boa/src/builtins/symbol/mod.rs index 16cd4c0e17..7e9f5f1ed1 100644 --- a/boa/src/builtins/symbol/mod.rs +++ b/boa/src/builtins/symbol/mod.rs @@ -18,7 +18,7 @@ #[cfg(test)] mod tests; -use super::function::make_constructor_fn; +use super::function::{make_builtin_fn, make_constructor_fn}; use crate::{ builtins::{ object::{ @@ -63,8 +63,8 @@ pub fn call_symbol(_: &mut Value, args: &[Value], ctx: &mut Interpreter) -> Resu let proto = ctx .realm .global_obj - .get_field_slice("Symbol") - .get_field_slice(PROTOTYPE); + .get_field("Symbol") + .get_field(PROTOTYPE); sym_instance.set_internal_slot(INSTANCE_PROTOTYPE, proto); Ok(Value(Gc::new(ValueData::Symbol(Box::new(GcCell::new( @@ -92,12 +92,12 @@ pub fn to_string(this: &mut Value, _: &[Value], _: &mut Interpreter) -> ResultVa pub fn create(global: &Value) -> Value { // Create prototype object let prototype = Value::new_object(Some(global)); - make_builtin_fn!(to_string, named "toString", of prototype); + make_builtin_fn(to_string, "toString", &prototype, 0); make_constructor_fn(call_symbol, global, prototype) } /// Initialise the `Symbol` object on the global object. #[inline] pub fn init(global: &Value) { - global.set_field_slice("Symbol", create(global)); + global.set_field("Symbol", create(global)); } diff --git a/boa/src/builtins/symbol/tests.rs b/boa/src/builtins/symbol/tests.rs index 31b78dc16c..ca72edb1f5 100644 --- a/boa/src/builtins/symbol/tests.rs +++ b/boa/src/builtins/symbol/tests.rs @@ -1,7 +1,5 @@ use super::*; -use crate::exec::Executor; -use crate::realm::Realm; -use crate::{forward, forward_val}; +use crate::{exec::Interpreter, forward, forward_val, realm::Realm}; #[test] fn check_symbol_constructor_is_function() { @@ -13,7 +11,7 @@ fn check_symbol_constructor_is_function() { #[test] fn call_symbol_and_check_return_type() { let realm = Realm::create(); - let mut engine = Executor::new(realm); + let mut engine = Interpreter::new(realm); let init = r#" var sym = Symbol(); "#; @@ -25,7 +23,7 @@ fn call_symbol_and_check_return_type() { #[test] fn print_symbol_expect_description() { let realm = Realm::create(); - let mut engine = Executor::new(realm); + let mut engine = Interpreter::new(realm); let init = r#" var sym = Symbol("Hello"); "#; diff --git a/boa/src/builtins/value/conversions.rs b/boa/src/builtins/value/conversions.rs index b86094a6e5..b2459cf40f 100644 --- a/boa/src/builtins/value/conversions.rs +++ b/boa/src/builtins/value/conversions.rs @@ -13,6 +13,12 @@ impl From for Value { } } +impl From> for Value { + fn from(value: Box) -> Self { + Self::string(value) + } +} + impl From<&Value> for String { fn from(value: &Value) -> Self { value.to_string() @@ -25,6 +31,12 @@ impl From<&str> for Value { } } +impl From<&Box> for Value { + fn from(value: &Box) -> Self { + Self::string(value.as_ref()) + } +} + impl From for Value { fn from(value: char) -> Self { Value::string(value.to_string()) diff --git a/boa/src/builtins/value/mod.rs b/boa/src/builtins/value/mod.rs index 77de6df642..2bdb186037 100644 --- a/boa/src/builtins/value/mod.rs +++ b/boa/src/builtins/value/mod.rs @@ -123,7 +123,7 @@ impl Value { /// Returns a new empty object pub fn new_object(global: Option<&Value>) -> Self { if let Some(global) = global { - let object_prototype = global.get_field_slice("Object").get_field_slice(PROTOTYPE); + let object_prototype = global.get_field("Object").get_field(PROTOTYPE); let object = Object::create(object_prototype); Self::object(object) @@ -484,8 +484,11 @@ impl ValueData { /// Resolve the property in the object and get its value, or undefined if this is not an object or the field doesn't exist /// get_field recieves a Property from get_prop(). It should then return the [[Get]] result value if that's set, otherwise fall back to [[Value]] /// TODO: this function should use the get Value if its set - pub fn get_field(&self, field: Value) -> Value { - match *field { + pub fn get_field(&self, field: F) -> Value + where + F: Into, + { + match *field.into() { // Our field will either be a String or a Symbol Self::String(ref s) => { match self.get_property(s) { @@ -586,24 +589,23 @@ impl ValueData { self.get_property(field).is_some() } - /// Resolve the property in the object and get its value, or undefined if this is not an object or the field doesn't exist - pub fn get_field_slice(&self, field: &str) -> Value { - // get_field used to accept strings, but now Symbols accept it needs to accept a value - // So this function will now need to Box strings back into values (at least for now) - let f = Value::string(field.to_string()); - self.get_field(f) - } - /// Set the field in the value /// Field could be a Symbol, so we need to accept a Value (not a string) - pub fn set_field(&self, field: Value, val: Value) -> Value { + pub fn set_field(&self, field: F, val: V) -> Value + where + F: Into, + V: Into, + { + let field = field.into(); + let val = val.into(); + if let Self::Object(ref obj) = *self { if obj.borrow().kind == ObjectKind::Array { if let Ok(num) = field.to_string().parse::() { if num > 0 { - let len = i32::from(&self.get_field_slice("length")); + let len = i32::from(&self.get_field("length")); if len < (num + 1) as i32 { - self.set_field_slice("length", Value::from(num + 1)); + self.set_field("length", Value::from(num + 1)); } } } @@ -621,14 +623,6 @@ impl ValueData { val } - /// Set the field in the value - pub fn set_field_slice(&self, field: &str, val: Value) -> Value { - // set_field used to accept strings, but now Symbols accept it needs to accept a value - // So this function will now need to Box strings back into values (at least for now) - let f = Value::string(field.to_string()); - self.set_field(f, val) - } - /// Set the private field in the value pub fn set_internal_slot(&self, field: &str, val: Value) -> Value { if let Self::Object(ref obj) = *self { @@ -679,7 +673,7 @@ impl ValueData { // Wrap Object in GC'd Value let new_func_val = Value::from(new_func); // Set length to parameters - new_func_val.set_field_slice("length", Value::from(length)); + new_func_val.set_field("length", Value::from(length)); new_func_val } @@ -730,7 +724,7 @@ impl ValueData { .borrow() .properties .iter() - .map(|(k, _)| (k.clone(), self.get_field_slice(k).to_json())) + .map(|(k, _)| (k.clone(), self.get_field(k.as_str()).to_json())) .collect::>(); JSONValue::Object(new_obj) } diff --git a/boa/src/builtins/value/operations.rs b/boa/src/builtins/value/operations.rs index 1a733542b0..e54459ea84 100644 --- a/boa/src/builtins/value/operations.rs +++ b/boa/src/builtins/value/operations.rs @@ -1,6 +1,5 @@ use super::*; -use crate::builtins::number; -use crate::Interpreter; +use crate::{builtins::Number, Interpreter}; use std::borrow::Borrow; use std::convert::TryFrom; @@ -16,7 +15,7 @@ impl Value { } if self.is_number() { - return number::equals(f64::from(self), f64::from(other)); + return Number::equals(f64::from(self), f64::from(other)); } //Null has to be handled specially because "typeof null" returns object and if we managed @@ -59,7 +58,7 @@ impl Value { | (ValueData::Integer(_), ValueData::Boolean(_)) => { let a: &Value = self.borrow(); let b: &Value = other.borrow(); - number::equals(f64::from(a), f64::from(b)) + Number::equals(f64::from(a), f64::from(b)) } // 6. If Type(x) is BigInt and Type(y) is String, then @@ -150,7 +149,7 @@ pub fn same_value(x: &Value, y: &Value, strict: bool) -> bool { // TODO: check BigInt // https://github.com/jasonwilliams/boa/pull/358 if x.is_number() { - return number::same_value(f64::from(x), f64::from(y)); + return Number::same_value(f64::from(x), f64::from(y)); } same_value_non_number(x, y) @@ -330,7 +329,7 @@ pub fn same_value_zero(x: &Value, y: &Value) -> bool { } if x.is_number() { - return number::same_value_zero(f64::from(x), f64::from(y)); + return Number::same_value_zero(f64::from(x), f64::from(y)); } same_value_non_number(x, y) diff --git a/boa/src/builtins/value/tests.rs b/boa/src/builtins/value/tests.rs index de8aa42584..193efee084 100644 --- a/boa/src/builtins/value/tests.rs +++ b/boa/src/builtins/value/tests.rs @@ -1,5 +1,5 @@ use super::*; -use crate::{forward, Executor, Realm}; +use crate::{forward, Interpreter, Realm}; #[test] fn check_is_object() { @@ -27,8 +27,8 @@ fn check_get_set_field() { let obj = Value::new_object(None); // Create string and convert it to a Value let s = Value::from("bar"); - obj.set_field_slice("foo", s); - assert_eq!(obj.get_field_slice("foo").to_string(), "bar"); + obj.set_field("foo", s); + assert_eq!(obj.get_field("foo").to_string(), "bar"); } #[test] @@ -52,7 +52,7 @@ fn check_number_is_true() { #[test] fn abstract_equality_comparison() { let realm = Realm::create(); - let mut engine = Executor::new(realm); + let mut engine = Interpreter::new(realm); assert_eq!(forward(&mut engine, "undefined == undefined"), "true"); assert_eq!(forward(&mut engine, "null == null"), "true"); diff --git a/boa/src/environment/lexical_environment.rs b/boa/src/environment/lexical_environment.rs index 3c7d15faea..65919b446e 100644 --- a/boa/src/environment/lexical_environment.rs +++ b/boa/src/environment/lexical_environment.rs @@ -99,10 +99,8 @@ impl LexicalEnvironment { self.environment_stack.pop_back() } - pub fn environments(&self) -> impl Iterator { - std::iter::successors(Some(self.get_current_environment_ref().clone()), |env| { - env.borrow().get_outer_environment() - }) + pub fn environments(&self) -> impl Iterator { + self.environment_stack.iter().rev() } pub fn get_global_object(&self) -> Option { @@ -114,8 +112,10 @@ impl LexicalEnvironment { } pub fn get_this_binding(&self) -> Value { - let env = self.environment_stack.back().expect("").borrow(); - env.get_this_binding() + self.environments() + .find(|env| env.borrow().has_this_binding()) + .map(|env| env.borrow().get_this_binding()) + .unwrap_or_else(Value::undefined) } pub fn create_mutable_binding(&mut self, name: String, deletion: bool, scope: VariableScope) { @@ -192,10 +192,8 @@ impl LexicalEnvironment { /// get_current_environment_ref is used when you only need to borrow the environment /// (you only need to add a new variable binding, or you want to fetch a value) pub fn get_current_environment_ref(&self) -> &Environment { - let index = self.environment_stack.len().wrapping_sub(1); - &self - .environment_stack - .get(index) + self.environment_stack + .back() .expect("Could not get current environment") } diff --git a/boa/src/environment/object_environment_record.rs b/boa/src/environment/object_environment_record.rs index add288fb75..aada300d7a 100644 --- a/boa/src/environment/object_environment_record.rs +++ b/boa/src/environment/object_environment_record.rs @@ -68,7 +68,7 @@ impl EnvironmentRecordTrait for ObjectEnvironmentRecord { fn get_binding_value(&self, name: &str, strict: bool) -> Value { if self.bindings.has_field(name) { - self.bindings.get_field_slice(name) + self.bindings.get_field(name) } else { if strict { // TODO: throw error here diff --git a/boa/src/exec/array/mod.rs b/boa/src/exec/array/mod.rs new file mode 100644 index 0000000000..548acd461f --- /dev/null +++ b/boa/src/exec/array/mod.rs @@ -0,0 +1,26 @@ +//! Array declaration execution. + +use super::{Executable, Interpreter}; +use crate::{ + builtins::{Array, ResultValue}, + syntax::ast::node::{ArrayDecl, Node}, +}; + +impl Executable for ArrayDecl { + fn run(&self, interpreter: &mut Interpreter) -> ResultValue { + let array = Array::new_array(interpreter)?; + let mut elements = Vec::new(); + for elem in self.as_ref() { + if let Node::Spread(ref x) = elem { + let val = x.run(interpreter)?; + let mut vals = interpreter.extract_array_properties(&val).unwrap(); + elements.append(&mut vals); + continue; // Don't push array after spread + } + elements.push(elem.run(interpreter)?); + } + Array::add_to_array_object(&array, &elements)?; + + Ok(array) + } +} diff --git a/boa/src/exec/block/mod.rs b/boa/src/exec/block/mod.rs new file mode 100644 index 0000000000..c592a05ab9 --- /dev/null +++ b/boa/src/exec/block/mod.rs @@ -0,0 +1,34 @@ +//! Block statement execution. + +use super::{Executable, Interpreter}; +use crate::{ + builtins::value::{ResultValue, Value}, + environment::lexical_environment::new_declarative_environment, + syntax::ast::node::Block, +}; + +impl Executable for Block { + fn run(&self, interpreter: &mut Interpreter) -> ResultValue { + { + let env = &mut interpreter.realm_mut().environment; + env.push(new_declarative_environment(Some( + env.get_current_environment_ref().clone(), + ))); + } + + let mut obj = Value::null(); + for statement in self.statements() { + obj = statement.run(interpreter)?; + + // early return + if interpreter.is_return { + break; + } + } + + // pop the block env + let _ = interpreter.realm_mut().environment.pop(); + + Ok(obj) + } +} diff --git a/boa/src/exec/declaration/mod.rs b/boa/src/exec/declaration/mod.rs new file mode 100644 index 0000000000..3a2af605df --- /dev/null +++ b/boa/src/exec/declaration/mod.rs @@ -0,0 +1,130 @@ +//! Declaration execution. + +use super::{Executable, Interpreter}; +use crate::{ + builtins::{ + function::ThisMode, + value::{ResultValue, Value}, + }, + environment::lexical_environment::VariableScope, + syntax::ast::node::{ + ArrowFunctionDecl, ConstDeclList, FunctionDecl, FunctionExpr, LetDeclList, VarDeclList, + }, +}; + +impl Executable for FunctionDecl { + fn run(&self, interpreter: &mut Interpreter) -> ResultValue { + let val = interpreter.create_function( + self.parameters().to_vec(), + self.body().to_vec(), + ThisMode::NonLexical, + ); + + // Set the name and assign it in the current environment + val.set_field("name", self.name()); + interpreter.realm_mut().environment.create_mutable_binding( + self.name().to_owned(), + false, + VariableScope::Function, + ); + + interpreter + .realm_mut() + .environment + .initialize_binding(self.name(), val.clone()); + + Ok(val) + } +} + +impl Executable for FunctionExpr { + fn run(&self, interpreter: &mut Interpreter) -> ResultValue { + let val = interpreter.create_function( + self.parameters().to_vec(), + self.body().to_vec(), + ThisMode::NonLexical, + ); + + if let Some(name) = self.name() { + val.set_field("name", Value::from(name)); + } + + Ok(val) + } +} + +impl Executable for VarDeclList { + fn run(&self, interpreter: &mut Interpreter) -> ResultValue { + for var in self.as_ref() { + let val = match var.init() { + Some(v) => v.run(interpreter)?, + None => Value::undefined(), + }; + let environment = &mut interpreter.realm_mut().environment; + + if environment.has_binding(var.name()) { + if var.init().is_some() { + environment.set_mutable_binding(var.name(), val, true); + } + } else { + environment.create_mutable_binding( + var.name().to_owned(), + false, + VariableScope::Function, + ); + environment.initialize_binding(var.name(), val); + } + } + Ok(Value::undefined()) + } +} + +impl Executable for ConstDeclList { + fn run(&self, interpreter: &mut Interpreter) -> ResultValue { + for decl in self.as_ref() { + let val = decl.init().run(interpreter)?; + + interpreter + .realm_mut() + .environment + .create_immutable_binding(decl.name().to_owned(), false, VariableScope::Block); + + interpreter + .realm_mut() + .environment + .initialize_binding(decl.name(), val); + } + Ok(Value::undefined()) + } +} + +impl Executable for LetDeclList { + fn run(&self, interpreter: &mut Interpreter) -> ResultValue { + for var in self.as_ref() { + let val = match var.init() { + Some(v) => v.run(interpreter)?, + None => Value::undefined(), + }; + interpreter.realm_mut().environment.create_mutable_binding( + var.name().to_owned(), + false, + VariableScope::Block, + ); + interpreter + .realm_mut() + .environment + .initialize_binding(var.name(), val); + } + Ok(Value::undefined()) + } +} + +impl Executable for ArrowFunctionDecl { + fn run(&self, interpreter: &mut Interpreter) -> ResultValue { + Ok(interpreter.create_function( + self.params().to_vec(), + self.body().to_vec(), + ThisMode::Lexical, + )) + } +} diff --git a/boa/src/exec/expression/mod.rs b/boa/src/exec/expression/mod.rs new file mode 100644 index 0000000000..57fc661ca7 --- /dev/null +++ b/boa/src/exec/expression/mod.rs @@ -0,0 +1,81 @@ +//! Expression execution. + +use super::{Executable, Interpreter}; +use crate::{ + builtins::{ + object::{INSTANCE_PROTOTYPE, PROTOTYPE}, + value::{ResultValue, Value, ValueData}, + }, + syntax::ast::node::{Call, New, Node}, +}; + +impl Executable for Call { + fn run(&self, interpreter: &mut Interpreter) -> ResultValue { + let (mut this, func) = match self.expr() { + Node::GetConstField(ref obj, ref field) => { + let mut obj = obj.run(interpreter)?; + if obj.get_type() != "object" || obj.get_type() != "symbol" { + obj = interpreter + .to_object(&obj) + .expect("failed to convert to object"); + } + (obj.clone(), obj.get_field(field)) + } + Node::GetField(ref obj, ref field) => { + let obj = obj.run(interpreter)?; + let field = field.run(interpreter)?; + (obj.clone(), obj.get_field(field.to_string())) + } + _ => ( + interpreter.realm().global_obj.clone(), + self.expr().run(interpreter)?, + ), // 'this' binding should come from the function's self-contained environment + }; + let mut v_args = Vec::with_capacity(self.args().len()); + for arg in self.args() { + if let Node::Spread(ref x) = arg { + let val = x.run(interpreter)?; + let mut vals = interpreter.extract_array_properties(&val).unwrap(); + v_args.append(&mut vals); + break; // after spread we don't accept any new arguments + } + v_args.push(arg.run(interpreter)?); + } + + // execute the function call itself + let fnct_result = interpreter.call(&func, &mut this, &v_args); + + // unset the early return flag + interpreter.is_return = false; + + fnct_result + } +} + +impl Executable for New { + fn run(&self, interpreter: &mut Interpreter) -> ResultValue { + // let (callee, args) = match call.as_ref() { + // Node::Call(callee, args) => (callee, args), + // _ => unreachable!("Node::New(ref call): 'call' must only be Node::Call type."), + // }; + + let func_object = self.expr().run(interpreter)?; + let mut v_args = Vec::with_capacity(self.args().len()); + for arg in self.args() { + v_args.push(arg.run(interpreter)?); + } + let mut this = Value::new_object(None); + // Create a blank object, then set its __proto__ property to the [Constructor].prototype + this.set_internal_slot(INSTANCE_PROTOTYPE, func_object.get_field(PROTOTYPE)); + + match func_object.data() { + ValueData::Object(ref o) => o.clone().borrow_mut().func.as_ref().unwrap().construct( + &mut func_object.clone(), + &v_args, + interpreter, + &mut this, + ), + _ => Ok(Value::undefined()), + } + } +} diff --git a/boa/src/exec/iteration/mod.rs b/boa/src/exec/iteration/mod.rs new file mode 100644 index 0000000000..ab1f8308d2 --- /dev/null +++ b/boa/src/exec/iteration/mod.rs @@ -0,0 +1,42 @@ +//! Iteration node execution. + +use super::{Executable, Interpreter}; +use crate::{ + builtins::value::{ResultValue, Value}, + environment::lexical_environment::new_declarative_environment, + syntax::ast::node::ForLoop, +}; + +impl Executable for ForLoop { + fn run(&self, interpreter: &mut Interpreter) -> ResultValue { + // Create the block environment. + { + let env = &mut interpreter.realm_mut().environment; + env.push(new_declarative_environment(Some( + env.get_current_environment_ref().clone(), + ))); + } + + if let Some(init) = self.init() { + init.run(interpreter)?; + } + + while self + .condition() + .map(|cond| cond.run(interpreter).map(|v| v.is_true())) + .transpose()? + .unwrap_or(true) + { + self.body().run(interpreter)?; + + if let Some(final_expr) = self.final_expr() { + final_expr.run(interpreter)?; + } + } + + // pop the block env + let _ = interpreter.realm_mut().environment.pop(); + + Ok(Value::undefined()) + } +} diff --git a/boa/src/exec/mod.rs b/boa/src/exec/mod.rs index ebc7fba8f8..f7db20501c 100644 --- a/boa/src/exec/mod.rs +++ b/boa/src/exec/mod.rs @@ -1,11 +1,18 @@ //! Execution of the AST, this is where the interpreter actually runs +mod array; +mod block; +mod declaration; +mod expression; +mod iteration; +mod operator; +mod statement_list; #[cfg(test)] mod tests; +mod try_node; use crate::{ builtins::{ - array, function::{Function as FunctionObject, FunctionBody, ThisMode}, object::{ internal_methods_trait::ObjectInternalMethods, Object, ObjectKind, INSTANCE_PROTOTYPE, @@ -14,589 +21,55 @@ use crate::{ property::Property, value::{ResultValue, Value, ValueData}, }, - environment::lexical_environment::{new_declarative_environment, VariableScope}, realm::Realm, syntax::ast::{ constant::Const, - node::{FormalParameter, MethodDefinitionKind, Node, PropertyDefinition}, - op::{AssignOp, BinOp, BitOp, CompOp, LogOp, NumOp, UnaryOp}, + node::{FormalParameter, MethodDefinitionKind, Node, PropertyDefinition, StatementList}, }, }; -use std::{ - borrow::{Borrow, BorrowMut}, - ops::Deref, -}; +use std::{borrow::Borrow, ops::Deref}; -/// An execution engine -pub trait Executor { - /// Make a new execution engine - fn new(realm: Realm) -> Self; - /// Run an expression - fn run(&mut self, expr: &Node) -> ResultValue; +pub trait Executable { + /// Runs this executable in the given executor. + fn run(&self, interpreter: &mut Interpreter) -> ResultValue; } /// A Javascript intepreter #[derive(Debug)] pub struct Interpreter { + /// Wether it's running a return statement. is_return: bool, /// realm holds both the global object and the environment pub realm: Realm, } -fn exec_assign_op(op: &AssignOp, v_a: Value, v_b: Value) -> Value { - match *op { - AssignOp::Add => v_a + v_b, - AssignOp::Sub => v_a - v_b, - AssignOp::Mul => v_a * v_b, - AssignOp::Exp => v_a.as_num_to_power(v_b), - AssignOp::Div => v_a / v_b, - AssignOp::Mod => v_a % v_b, - AssignOp::And => v_a & v_b, - AssignOp::Or => v_a | v_b, - AssignOp::Xor => v_a ^ v_b, - AssignOp::Shl => v_a << v_b, - AssignOp::Shr => v_a << v_b, - } -} - -impl Executor for Interpreter { - fn new(realm: Realm) -> Self { +impl Interpreter { + /// Creates a new interpreter. + pub fn new(realm: Realm) -> Self { Self { realm, is_return: false, } } - #[allow(clippy::match_same_arms)] - fn run(&mut self, node: &Node) -> ResultValue { - match *node { - Node::Const(Const::Null) => Ok(Value::null()), - Node::Const(Const::Undefined) => Ok(Value::undefined()), - Node::Const(Const::Num(num)) => Ok(Value::rational(num)), - Node::Const(Const::Int(num)) => Ok(Value::integer(num)), - Node::Const(Const::BigInt(ref num)) => Ok(Value::from(num.clone())), - // we can't move String from Const into value, because const is a garbage collected value - // Which means Drop() get's called on Const, but str will be gone at that point. - // Do Const values need to be garbage collected? We no longer need them once we've generated Values - Node::Const(Const::String(ref value)) => Ok(Value::string(value.to_string())), - Node::Const(Const::Bool(value)) => Ok(Value::boolean(value)), - Node::Block(ref es) => { - { - let env = &mut self.realm.environment; - env.push(new_declarative_environment(Some( - env.get_current_environment_ref().clone(), - ))); - } - - let mut obj = Value::null(); - for e in es.iter() { - let val = self.run(e)?; - // early return - if self.is_return { - obj = val; - break; - } - if e == es.last().expect("unable to get last value") { - obj = val; - } - } - - // pop the block env - let _ = self.realm.environment.pop(); - - Ok(obj) - } - Node::Local(ref name) => { - let val = self.realm.environment.get_binding_value(name); - Ok(val) - } - Node::GetConstField(ref obj, ref field) => { - let val_obj = self.run(obj)?; - Ok(val_obj.borrow().get_field_slice(field)) - } - Node::GetField(ref obj, ref field) => { - let val_obj = self.run(obj)?; - let val_field = self.run(field)?; - Ok(val_obj - .borrow() - .get_field_slice(&val_field.borrow().to_string())) - } - Node::Call(ref callee, ref args) => { - let (mut this, func) = match callee.deref() { - Node::GetConstField(ref obj, ref field) => { - let mut obj = self.run(obj)?; - if obj.get_type() != "object" || obj.get_type() != "symbol" { - obj = self.to_object(&obj).expect("failed to convert to object"); - } - (obj.clone(), obj.borrow().get_field_slice(field)) - } - Node::GetField(ref obj, ref field) => { - let obj = self.run(obj)?; - let field = self.run(field)?; - ( - obj.clone(), - obj.borrow().get_field_slice(&field.borrow().to_string()), - ) - } - _ => (self.realm.global_obj.clone(), self.run(&callee.clone())?), // 'this' binding should come from the function's self-contained environment - }; - let mut v_args = Vec::with_capacity(args.len()); - for arg in args.iter() { - if let Node::Spread(ref x) = arg.deref() { - let val = self.run(x)?; - let mut vals = self.extract_array_properties(&val).unwrap(); - v_args.append(&mut vals); - break; // after spread we don't accept any new arguments - } - v_args.push(self.run(arg)?); - } - - // execute the function call itself - let fnct_result = self.call(&func, &mut this, &v_args); - - // unset the early return flag - self.is_return = false; - - fnct_result - } - Node::WhileLoop(ref cond, ref expr) => { - let mut result = Value::undefined(); - while self.run(cond)?.borrow().is_true() { - result = self.run(expr)?; - } - Ok(result) - } - Node::DoWhileLoop(ref body, ref cond) => { - let mut result = self.run(body)?; - while self.run(cond)?.borrow().is_true() { - result = self.run(body)?; - } - Ok(result) - } - Node::ForLoop(ref init, ref cond, ref step, ref body) => { - if let Some(init) = init { - self.run(init)?; - } - - while match cond { - Some(cond) => self.run(cond)?.borrow().is_true(), - None => true, - } { - self.run(body)?; - - if let Some(step) = step { - self.run(step)?; - } - } - - Ok(Value::undefined()) - } - Node::If(ref cond, ref expr, None) => Ok(if self.run(cond)?.borrow().is_true() { - self.run(expr)? - } else { - Value::undefined() - }), - Node::If(ref cond, ref expr, Some(ref else_e)) => { - Ok(if self.run(cond)?.borrow().is_true() { - self.run(expr)? - } else { - self.run(else_e)? - }) - } - Node::Switch(ref val_e, ref vals, ref default) => { - let val = self.run(val_e)?; - let mut result = Value::null(); - let mut matched = false; - for tup in vals.iter() { - let cond = &tup.0; - let block = &tup.1; - if val.strict_equals(&self.run(cond)?) { - matched = true; - let last_expr = block.last().expect("Block has no expressions"); - for expr in block.iter() { - let e_result = self.run(expr)?; - if expr == last_expr { - result = e_result; - } - } - } - } - if !matched && default.is_some() { - result = self.run( - default - .as_ref() - .expect("Could not get default as reference"), - )?; - } - Ok(result) - } - Node::Object(ref properties) => { - let global_val = &self - .realm - .environment - .get_global_object() - .expect("Could not get the global object"); - let obj = Value::new_object(Some(global_val)); - - // TODO: Implement the rest of the property types. - for property in properties.iter() { - match property { - PropertyDefinition::Property(key, value) => { - obj.borrow().set_field_slice(&key.clone(), self.run(value)?); - } - PropertyDefinition::MethodDefinition(kind, name, func) => { - if let MethodDefinitionKind::Ordinary = kind { - obj.borrow().set_field_slice(&name.clone(), self.run(func)?); - } else { - // TODO: Implement other types of MethodDefinitionKinds. - unimplemented!("other types of property method definitions."); - } - } - i => unimplemented!("{:?} type of property", i), - } - } - - Ok(obj) - } - Node::ArrayDecl(ref arr) => { - let array = array::new_array(self)?; - let mut elements = Vec::new(); - for elem in arr.iter() { - if let Node::Spread(ref x) = elem.deref() { - let val = self.run(x)?; - let mut vals = self.extract_array_properties(&val).unwrap(); - elements.append(&mut vals); - continue; // Don't push array after spread - } - elements.push(self.run(elem)?); - } - array::add_to_array_object(&array, &elements)?; - Ok(array) - } - // - Node::FunctionDecl(ref name, ref args, ref expr) => { - let val = self.create_function(args.clone(), expr, ThisMode::NonLexical); - // Set the name and assign it in the current environment - - val.set_field_slice("name", Value::from(name.clone())); - self.realm.environment.create_mutable_binding( - name.clone(), - false, - VariableScope::Function, - ); - - self.realm.environment.initialize_binding(name, val.clone()); - - Ok(val) - } - // - Node::FunctionExpr(ref name, ref args, ref expr) => { - let val = self.create_function(args.clone(), expr, ThisMode::NonLexical); - - if let Some(name) = name { - val.set_field_slice("name", Value::from(name.clone())); - } - - Ok(val) - } - Node::ArrowFunctionDecl(ref args, ref expr) => { - Ok(self.create_function(args.clone(), expr, ThisMode::Lexical)) - } - Node::BinOp(BinOp::Num(ref op), ref a, ref b) => { - let v_a = self.run(a)?; - let v_b = self.run(b)?; - Ok(match *op { - NumOp::Add => v_a + v_b, - NumOp::Sub => v_a - v_b, - NumOp::Mul => v_a * v_b, - NumOp::Exp => v_a.as_num_to_power(v_b), - NumOp::Div => v_a / v_b, - NumOp::Mod => v_a % v_b, - }) - } - Node::UnaryOp(ref op, ref a) => { - let v_a = self.run(a)?; - Ok(match *op { - UnaryOp::Minus => -v_a, - UnaryOp::Plus => Value::from(v_a.to_number()), - UnaryOp::IncrementPost => { - let ret = v_a.clone(); - self.set_value(a, Value::from(v_a.to_number() + 1.0))?; - ret - } - UnaryOp::IncrementPre => { - self.set_value(a, Value::from(v_a.to_number() + 1.0))? - } - UnaryOp::DecrementPost => { - let ret = v_a.clone(); - self.set_value(a, Value::from(v_a.to_number() - 1.0))?; - ret - } - UnaryOp::DecrementPre => { - self.set_value(a, Value::from(v_a.to_number() - 1.0))? - } - UnaryOp::Not => !v_a, - UnaryOp::Tilde => { - let num_v_a = v_a.to_number(); - // NOTE: possible UB: https://github.com/rust-lang/rust/issues/10184 - Value::from(if num_v_a.is_nan() { - -1 - } else { - !(num_v_a as i32) - }) - } - UnaryOp::Void => Value::undefined(), - UnaryOp::Delete => match a.deref() { - Node::GetConstField(ref obj, ref field) => { - Value::boolean(self.run(obj)?.remove_property(field)) - } - Node::GetField(ref obj, ref field) => Value::boolean( - self.run(obj)? - .remove_property(&self.run(field)?.to_string()), - ), - Node::Local(_) => Value::boolean(false), - Node::ArrayDecl(_) - | Node::Block(_) - | Node::Const(_) - | Node::FunctionDecl(_, _, _) - | Node::FunctionExpr(_, _, _) - | Node::New(_) - | Node::Object(_) - | Node::UnaryOp(_, _) => Value::boolean(true), - _ => panic!("SyntaxError: wrong delete argument {}", node), - }, - UnaryOp::TypeOf => Value::from(v_a.get_type()), - }) - } - Node::BinOp(BinOp::Bit(ref op), ref a, ref b) => { - let v_a = self.run(a)?; - let v_b = self.run(b)?; - Ok(match *op { - BitOp::And => v_a & v_b, - BitOp::Or => v_a | v_b, - BitOp::Xor => v_a ^ v_b, - BitOp::Shl => v_a << v_b, - BitOp::Shr => v_a >> v_b, - // TODO Fix - BitOp::UShr => v_a >> v_b, - }) - } - Node::BinOp(BinOp::Comp(ref op), ref a, ref b) => { - let mut v_r_a = self.run(a)?; - let mut v_r_b = self.run(b)?; - let mut v_a = v_r_a.borrow_mut(); - let mut v_b = v_r_b.borrow_mut(); - Ok(Value::from(match *op { - CompOp::Equal => v_r_a.equals(v_b, self), - CompOp::NotEqual => !v_r_a.equals(v_b, self), - CompOp::StrictEqual => v_r_a.strict_equals(v_b), - CompOp::StrictNotEqual => !v_r_a.strict_equals(v_b), - CompOp::GreaterThan => v_a.to_number() > v_b.to_number(), - CompOp::GreaterThanOrEqual => v_a.to_number() >= v_b.to_number(), - CompOp::LessThan => v_a.to_number() < v_b.to_number(), - CompOp::LessThanOrEqual => v_a.to_number() <= v_b.to_number(), - CompOp::In => { - if !v_b.is_object() { - panic!("TypeError: {} is not an Object.", v_b); - } - let key = self.to_property_key(&mut v_a); - self.has_property(&mut v_b, &key) - } - })) - } - Node::BinOp(BinOp::Log(ref op), ref a, ref b) => { - // turn a `Value` into a `bool` - let to_bool = |value| bool::from(&value); - Ok(match *op { - LogOp::And => Value::from(to_bool(self.run(a)?) && to_bool(self.run(b)?)), - LogOp::Or => Value::from(to_bool(self.run(a)?) || to_bool(self.run(b)?)), - }) - } - Node::BinOp(BinOp::Assign(ref op), ref a, ref b) => match a.deref() { - Node::Local(ref name) => { - let v_a = self.realm.environment.get_binding_value(&name); - let v_b = self.run(b)?; - let value = exec_assign_op(op, v_a, v_b); - self.realm - .environment - .set_mutable_binding(&name, value.clone(), true); - Ok(value) - } - Node::GetConstField(ref obj, ref field) => { - let v_r_a = self.run(obj)?; - let v_a = v_r_a.get_field_slice(field); - let v_b = self.run(b)?; - let value = exec_assign_op(op, v_a, v_b); - v_r_a - .borrow() - .set_field_slice(&field.clone(), value.clone()); - Ok(value) - } - _ => Ok(Value::undefined()), - }, - Node::New(ref call) => { - let (callee, args) = match call.as_ref() { - Node::Call(callee, args) => (callee, args), - _ => unreachable!("Node::New(ref call): 'call' must only be Node::Call type."), - }; - - let func_object = self.run(callee)?; - let mut v_args = Vec::with_capacity(args.len()); - for arg in args.iter() { - v_args.push(self.run(arg)?); - } - let mut this = Value::new_object(None); - // Create a blank object, then set its __proto__ property to the [Constructor].prototype - this.borrow().set_internal_slot( - INSTANCE_PROTOTYPE, - func_object.borrow().get_field_slice(PROTOTYPE), - ); - - match *(func_object.borrow()).deref() { - ValueData::Object(ref o) => (*o.deref().clone().borrow_mut()) - .func - .as_ref() - .unwrap() - .construct(&mut func_object.clone(), &v_args, self, &mut this), - _ => Ok(Value::undefined()), - } - } - Node::Return(ref ret) => { - let result = match *ret { - Some(ref v) => self.run(v), - None => Ok(Value::undefined()), - }; - // Set flag for return - self.is_return = true; - result - } - Node::Throw(ref ex) => Err(self.run(ex)?), - Node::Assign(ref ref_e, ref val_e) => { - let val = self.run(val_e)?; - match ref_e.deref() { - Node::Local(ref name) => { - if self.realm.environment.has_binding(name) { - // Binding already exists - self.realm - .environment - .set_mutable_binding(&name, val.clone(), true); - } else { - self.realm.environment.create_mutable_binding( - name.clone(), - true, - VariableScope::Function, - ); - self.realm.environment.initialize_binding(name, val.clone()); - } - } - Node::GetConstField(ref obj, ref field) => { - let val_obj = self.run(obj)?; - val_obj - .borrow() - .set_field_slice(&field.clone(), val.clone()); - } - Node::GetField(ref obj, ref field) => { - let val_obj = self.run(obj)?; - let val_field = self.run(field)?; - val_obj.borrow().set_field(val_field, val.clone()); - } - _ => (), - } - Ok(val) - } - Node::VarDecl(ref vars) => { - for var in vars.iter() { - let (name, value) = var.clone(); - let val = match value { - Some(v) => self.run(&v)?, - None => Value::undefined(), - }; - self.realm.environment.create_mutable_binding( - name.clone(), - false, - VariableScope::Function, - ); - self.realm.environment.initialize_binding(&name, val); - } - Ok(Value::undefined()) - } - Node::LetDecl(ref vars) => { - for var in vars.iter() { - let (name, value) = var.clone(); - let val = match value { - Some(v) => self.run(&v)?, - None => Value::undefined(), - }; - self.realm.environment.create_mutable_binding( - name.clone(), - false, - VariableScope::Block, - ); - self.realm.environment.initialize_binding(&name, val); - } - Ok(Value::undefined()) - } - Node::ConstDecl(ref vars) => { - for (name, value) in vars.iter() { - self.realm.environment.create_immutable_binding( - name.clone(), - false, - VariableScope::Block, - ); - let val = self.run(&value)?; - self.realm.environment.initialize_binding(&name, val); - } - Ok(Value::undefined()) - } - Node::StatementList(ref list) => { - let mut obj = Value::null(); - for (i, item) in list.iter().enumerate() { - let val = self.run(item)?; - // early return - if self.is_return { - obj = val; - break; - } - if i + 1 == list.len() { - obj = val; - } - } - - Ok(obj) - } - Node::Spread(ref node) => { - // TODO: for now we can do nothing but return the value as-is - self.run(node) - } - Node::This => { - // Will either return `this` binding or undefined - Ok(self.realm.environment.get_this_binding()) - } - ref i => unimplemented!("{}", i), - } - } -} - -impl Interpreter { - /// Get the Interpreter's realm - pub(crate) fn get_realm(&self) -> &Realm { + /// Retrieves the `Realm` of this executor. + pub(crate) fn realm(&self) -> &Realm { &self.realm } /// Utility to create a function Value for Function Declarations, Arrow Functions or Function Expressions - pub(crate) fn create_function( - &mut self, - args: Box<[FormalParameter]>, - expr: &Node, - this_mode: ThisMode, - ) -> Value { + pub(crate) fn create_function(&mut self, params: P, body: B, this_mode: ThisMode) -> Value + where + P: Into>, + B: Into, + { let function_prototype = &self .realm .environment .get_global_object() .expect("Could not get the global object") - .get_field_slice("Function") - .get_field_slice("Prototype"); + .get_field("Function") + .get_field("Prototype"); // Every new function has a prototype property pre-made let global_val = &self @@ -606,10 +79,12 @@ impl Interpreter { .expect("Could not get the global object"); let proto = Value::new_object(Some(global_val)); + let params = params.into(); + let params_len = params.len(); let func = FunctionObject::create_ordinary( - args.clone(), + params, self.realm.environment.get_current_environment().clone(), - FunctionBody::Ordinary(expr.clone()), + FunctionBody::Ordinary(body.into()), this_mode, ); @@ -617,32 +92,72 @@ impl Interpreter { new_func.set_func(func); let val = Value::from(new_func); val.set_internal_slot(INSTANCE_PROTOTYPE, function_prototype.clone()); - val.set_field_slice(PROTOTYPE, proto); - val.set_field_slice("length", Value::from(args.len())); + val.set_field(PROTOTYPE, proto); + val.set_field("length", Value::from(params_len)); val } - /// https://tc39.es/ecma262/#sec-call + /// Retrieves the `Realm` of this executor as a mutable reference. + pub(crate) fn realm_mut(&mut self) -> &mut Realm { + &mut self.realm + } + + /// pub(crate) fn call( &mut self, f: &Value, this: &mut Value, arguments_list: &[Value], ) -> ResultValue { - // All functions should be objects, and eventually will be. - // During this transition call will support both native functions and function objects - match (*f).deref() { - ValueData::Object(ref obj) => match (*obj).deref().borrow().func { - Some(ref func) => func.call(&mut f.clone(), arguments_list, self, this), - None => panic!("Expected function"), - }, + match *f.data() { + ValueData::Object(ref obj) => { + let obj = (**obj).borrow(); + let func = obj.func.as_ref().expect("Expected function"); + func.call(&mut f.clone(), arguments_list, self, this) + } _ => Err(Value::undefined()), } } - /// https://tc39.es/ecma262/#sec-ordinarytoprimitive - fn ordinary_to_primitive(&mut self, o: &mut Value, hint: &str) -> Value { + /// Converts a value into a rust heap allocated string. + pub(crate) fn value_to_rust_string(&mut self, value: &Value) -> String { + match *value.deref().borrow() { + ValueData::Null => String::from("null"), + ValueData::Boolean(ref boolean) => boolean.to_string(), + ValueData::Rational(ref num) => num.to_string(), + ValueData::Integer(ref num) => num.to_string(), + ValueData::String(ref string) => string.clone(), + ValueData::Object(_) => { + let prim_value = self.to_primitive(&mut (value.clone()), Some("string")); + self.to_string(&prim_value).to_string() + } + _ => String::from("undefined"), + } + } + + /// Converts an array object into a rust vector of values. + /// + /// This is useful for the spread operator, for any other object an `Err` is returned + pub(crate) fn extract_array_properties(&mut self, value: &Value) -> Result, ()> { + if let ValueData::Object(ref x) = *value.deref().borrow() { + // Check if object is array + if x.deref().borrow().kind == ObjectKind::Array { + let length: i32 = self.value_to_rust_number(&value.get_field("length")) as i32; + let values: Vec = (0..length) + .map(|idx| value.get_field(idx.to_string())) + .collect(); + return Ok(values); + } + + return Err(()); + } + + Err(()) + } + + /// + pub(crate) fn ordinary_to_primitive(&mut self, o: &mut Value, hint: &str) -> Value { debug_assert!(o.get_type() == "object"); debug_assert!(hint == "string" || hint == "number"); let method_names: Vec<&str> = if hint == "string" { @@ -651,7 +166,7 @@ impl Interpreter { vec!["valueOf", "toString"] }; for name in method_names.iter() { - let method: Value = o.get_field_slice(name); + let method: Value = o.get_field(*name); if method.is_function() { let result = self.call(&method, o, &[]); match result { @@ -672,9 +187,13 @@ impl Interpreter { } /// The abstract operation ToPrimitive takes an input argument and an optional argument PreferredType. - /// https://tc39.es/ecma262/#sec-toprimitive + /// #[allow(clippy::wrong_self_convention)] - pub fn to_primitive(&mut self, input: &mut Value, preferred_type: Option<&str>) -> Value { + pub(crate) fn to_primitive( + &mut self, + input: &mut Value, + preferred_type: Option<&str>, + ) -> Value { let mut hint: &str; match (*input).deref() { ValueData::Object(_) => { @@ -703,7 +222,7 @@ impl Interpreter { /// /// https://tc39.es/ecma262/#sec-tostring #[allow(clippy::wrong_self_convention)] - pub fn to_string(&mut self, value: &Value) -> Value { + pub(crate) fn to_string(&mut self, value: &Value) -> Value { match *value.deref().borrow() { ValueData::Undefined => Value::from("undefined"), ValueData::Null => Value::from("null"), @@ -723,7 +242,7 @@ impl Interpreter { /// The abstract operation ToPropertyKey takes argument argument. It converts argument to a value that can be used as a property key. /// https://tc39.es/ecma262/#sec-topropertykey #[allow(clippy::wrong_self_convention)] - pub fn to_property_key(&mut self, value: &mut Value) -> Value { + pub(crate) fn to_property_key(&mut self, value: &mut Value) -> Value { let key = self.to_primitive(value, Some("string")); if key.is_symbol() { key @@ -733,7 +252,7 @@ impl Interpreter { } /// https://tc39.es/ecma262/#sec-hasproperty - pub fn has_property(&self, obj: &mut Value, key: &Value) -> bool { + pub(crate) fn has_property(&self, obj: &mut Value, key: &Value) -> bool { if let Some(obj) = obj.as_object() { if !Property::is_property_key(key) { false @@ -748,7 +267,7 @@ impl Interpreter { /// The abstract operation ToObject converts argument to a value of type Object /// https://tc39.es/ecma262/#sec-toobject #[allow(clippy::wrong_self_convention)] - pub fn to_object(&mut self, value: &Value) -> ResultValue { + pub(crate) fn to_object(&mut self, value: &Value) -> ResultValue { match *value.deref().borrow() { ValueData::Undefined | ValueData::Integer(_) | ValueData::Null => { Err(Value::undefined()) @@ -758,7 +277,7 @@ impl Interpreter { .realm .environment .get_binding_value("Boolean") - .get_field_slice(PROTOTYPE); + .get_field(PROTOTYPE); let bool_obj = Value::new_object_from_prototype(proto, ObjectKind::Boolean); bool_obj.set_internal_slot("BooleanData", value.clone()); @@ -769,7 +288,7 @@ impl Interpreter { .realm .environment .get_binding_value("Number") - .get_field_slice(PROTOTYPE); + .get_field(PROTOTYPE); let number_obj = Value::new_object_from_prototype(proto, ObjectKind::Number); number_obj.set_internal_slot("NumberData", value.clone()); Ok(number_obj) @@ -779,7 +298,7 @@ impl Interpreter { .realm .environment .get_binding_value("String") - .get_field_slice(PROTOTYPE); + .get_field(PROTOTYPE); let string_obj = Value::new_object_from_prototype(proto, ObjectKind::String); string_obj.set_internal_slot("StringData", value.clone()); Ok(string_obj) @@ -790,7 +309,7 @@ impl Interpreter { .realm .environment .get_binding_value("BigInt") - .get_field_slice(PROTOTYPE); + .get_field(PROTOTYPE); let bigint_obj = Value::new_object_from_prototype(proto, ObjectKind::BigInt); bigint_obj.set_internal_slot("BigIntData", value.clone()); Ok(bigint_obj) @@ -798,24 +317,7 @@ impl Interpreter { } } - /// value_to_rust_string() converts a value into a rust heap allocated string - pub fn value_to_rust_string(&mut self, value: &Value) -> String { - match *value.deref().borrow() { - ValueData::Null => String::from("null"), - ValueData::Boolean(ref boolean) => boolean.to_string(), - ValueData::Rational(ref num) => num.to_string(), - ValueData::Integer(ref num) => num.to_string(), - ValueData::String(ref string) => string.clone(), - ValueData::BigInt(ref bigint) => bigint.to_string(), - ValueData::Object(_) => { - let prim_value = self.to_primitive(&mut (value.clone()), Some("string")); - self.to_string(&prim_value).to_string() - } - _ => String::from("undefined"), - } - } - - pub fn value_to_rust_number(&mut self, value: &Value) -> f64 { + pub(crate) fn value_to_rust_number(&mut self, value: &Value) -> f64 { match *value.deref().borrow() { ValueData::Null => f64::from(0), ValueData::Boolean(boolean) => { @@ -843,41 +345,171 @@ impl Interpreter { } } - /// `extract_array_properties` converts an array object into a rust vector of Values. - /// This is useful for the spread operator, for any other object an `Err` is returned - fn extract_array_properties(&mut self, value: &Value) -> Result, ()> { - if let ValueData::Object(ref x) = *value.deref().borrow() { - // Check if object is array - if x.deref().borrow().kind == ObjectKind::Array { - let length: i32 = - self.value_to_rust_number(&value.get_field_slice("length")) as i32; - let values: Vec = (0..length) - .map(|idx| value.get_field_slice(&idx.to_string())) - .collect(); - return Ok(values); - } - - return Err(()); - } - - Err(()) - } - fn set_value(&mut self, node: &Node, value: Value) -> ResultValue { match node { - Node::Local(ref name) => { + Node::Identifier(ref name) => { self.realm .environment - .set_mutable_binding(name, value.clone(), true); + .set_mutable_binding(name.as_ref(), value.clone(), true); Ok(value) } + Node::GetConstField(ref obj, ref field) => Ok(obj.run(self)?.set_field(field, value)), + Node::GetField(ref obj, ref field) => { + Ok(obj.run(self)?.set_field(field.run(self)?, value)) + } + _ => panic!("TypeError: invalid assignment to {}", node), + } + } +} + +impl Executable for Node { + fn run(&self, interpreter: &mut Interpreter) -> ResultValue { + match *self { + Node::Const(Const::Null) => Ok(Value::null()), + Node::Const(Const::Undefined) => Ok(Value::undefined()), + Node::Const(Const::Num(num)) => Ok(Value::rational(num)), + Node::Const(Const::Int(num)) => Ok(Value::integer(num)), + Node::Const(Const::BigInt(ref num)) => Ok(Value::from(num.clone())), + // we can't move String from Const into value, because const is a garbage collected value + // Which means Drop() get's called on Const, but str will be gone at that point. + // Do Const values need to be garbage collected? We no longer need them once we've generated Values + Node::Const(Const::String(ref value)) => Ok(Value::string(value.to_string())), + Node::Const(Const::Bool(value)) => Ok(Value::boolean(value)), + Node::Block(ref block) => block.run(interpreter), + Node::Identifier(ref name) => { + let val = interpreter + .realm() + .environment + .get_binding_value(name.as_ref()); + Ok(val) + } Node::GetConstField(ref obj, ref field) => { - Ok(self.run(obj)?.set_field_slice(field, value)) + let val_obj = obj.run(interpreter)?; + Ok(val_obj.borrow().get_field(field)) } Node::GetField(ref obj, ref field) => { - Ok(self.run(obj)?.set_field(self.run(field)?, value)) + let val_obj = obj.run(interpreter)?; + let val_field = field.run(interpreter)?; + Ok(val_obj.borrow().get_field(val_field.borrow().to_string())) } - _ => panic!("TypeError: invalid assignment to {}", node), + Node::Call(ref expr) => expr.run(interpreter), + Node::WhileLoop(ref cond, ref expr) => { + let mut result = Value::undefined(); + while cond.run(interpreter)?.borrow().is_true() { + result = expr.run(interpreter)?; + } + Ok(result) + } + Node::DoWhileLoop(ref body, ref cond) => { + let mut result = body.run(interpreter)?; + while cond.run(interpreter)?.borrow().is_true() { + result = body.run(interpreter)?; + } + Ok(result) + } + Node::ForLoop(ref for_loop) => for_loop.run(interpreter), + Node::If(ref cond, ref expr, None) => { + Ok(if cond.run(interpreter)?.borrow().is_true() { + expr.run(interpreter)? + } else { + Value::undefined() + }) + } + Node::If(ref cond, ref expr, Some(ref else_e)) => { + Ok(if cond.run(interpreter)?.borrow().is_true() { + expr.run(interpreter)? + } else { + else_e.run(interpreter)? + }) + } + Node::Switch(ref val_e, ref vals, ref default) => { + let val = val_e.run(interpreter)?; + let mut result = Value::null(); + let mut matched = false; + for tup in vals.iter() { + let cond = &tup.0; + let block = &tup.1; + if val.strict_equals(&cond.run(interpreter)?) { + matched = true; + let last_expr = block.last().expect("Block has no expressions"); + for expr in block.iter() { + let e_result = expr.run(interpreter)?; + if expr == last_expr { + result = e_result; + } + } + } + } + if !matched { + if let Some(default) = default { + result = default.run(interpreter)?; + } + } + Ok(result) + } + Node::Object(ref properties) => { + let global_val = &interpreter + .realm() + .environment + .get_global_object() + .expect("Could not get the global object"); + let obj = Value::new_object(Some(global_val)); + + // TODO: Implement the rest of the property types. + for property in properties.iter() { + match property { + PropertyDefinition::Property(key, value) => { + obj.borrow() + .set_field(&key.clone(), value.run(interpreter)?); + } + PropertyDefinition::MethodDefinition(kind, name, func) => { + if let MethodDefinitionKind::Ordinary = kind { + obj.borrow() + .set_field(&name.clone(), func.run(interpreter)?); + } else { + // TODO: Implement other types of MethodDefinitionKinds. + unimplemented!("other types of property method definitions."); + } + } + i => unimplemented!("{:?} type of property", i), + } + } + + Ok(obj) + } + Node::ArrayDecl(ref arr) => arr.run(interpreter), + // + Node::FunctionDecl(ref decl) => decl.run(interpreter), + // + Node::FunctionExpr(ref expr) => expr.run(interpreter), + Node::ArrowFunctionDecl(ref decl) => decl.run(interpreter), + Node::BinOp(ref op) => op.run(interpreter), + Node::UnaryOp(ref op) => op.run(interpreter), + Node::New(ref call) => call.run(interpreter), + Node::Return(ref ret) => { + let result = match *ret { + Some(ref v) => v.run(interpreter), + None => Ok(Value::undefined()), + }; + // Set flag for return + interpreter.is_return = true; + result + } + Node::Throw(ref ex) => Err(ex.run(interpreter)?), + Node::Assign(ref op) => op.run(interpreter), + Node::VarDeclList(ref decl) => decl.run(interpreter), + Node::LetDeclList(ref decl) => decl.run(interpreter), + Node::ConstDeclList(ref decl) => decl.run(interpreter), + Node::Spread(ref node) => { + // TODO: for now we can do nothing but return the value as-is + node.run(interpreter) + } + Node::This => { + // Will either return `this` binding or undefined + Ok(interpreter.realm().environment.get_this_binding()) + } + Node::Try(ref try_node) => try_node.run(interpreter), + ref i => unimplemented!("{:?}", i), } } } diff --git a/boa/src/exec/operator/mod.rs b/boa/src/exec/operator/mod.rs new file mode 100644 index 0000000000..d4cb97495c --- /dev/null +++ b/boa/src/exec/operator/mod.rs @@ -0,0 +1,215 @@ +//! Operator execution. + +use super::{Executable, Interpreter}; +use crate::{ + builtins::value::{ResultValue, Value}, + environment::lexical_environment::VariableScope, + syntax::ast::{ + node::{Assign, BinOp, Node, UnaryOp}, + op::{self, AssignOp, BitOp, CompOp, LogOp, NumOp}, + }, +}; +use std::borrow::BorrowMut; + +impl Executable for Assign { + fn run(&self, interpreter: &mut Interpreter) -> ResultValue { + let val = self.rhs().run(interpreter)?; + match self.lhs() { + Node::Identifier(ref name) => { + let environment = &mut interpreter.realm_mut().environment; + + if environment.has_binding(name.as_ref()) { + // Binding already exists + environment.set_mutable_binding(name.as_ref(), val.clone(), true); + } else { + environment.create_mutable_binding( + name.as_ref().to_owned(), + true, + VariableScope::Function, + ); + environment.initialize_binding(name.as_ref(), val.clone()); + } + } + Node::GetConstField(ref obj, ref field) => { + let val_obj = obj.run(interpreter)?; + val_obj.set_field(field, val.clone()); + } + Node::GetField(ref obj, ref field) => { + let val_obj = obj.run(interpreter)?; + let val_field = field.run(interpreter)?; + val_obj.set_field(val_field, val.clone()); + } + _ => (), + } + Ok(val) + } +} + +impl Executable for BinOp { + fn run(&self, interpreter: &mut Interpreter) -> ResultValue { + match self.op() { + op::BinOp::Num(op) => { + let v_a = self.lhs().run(interpreter)?; + let v_b = self.rhs().run(interpreter)?; + Ok(match op { + NumOp::Add => v_a + v_b, + NumOp::Sub => v_a - v_b, + NumOp::Mul => v_a * v_b, + NumOp::Exp => v_a.as_num_to_power(v_b), + NumOp::Div => v_a / v_b, + NumOp::Mod => v_a % v_b, + }) + } + op::BinOp::Bit(op) => { + let v_a = self.lhs().run(interpreter)?; + let v_b = self.rhs().run(interpreter)?; + Ok(match op { + BitOp::And => v_a & v_b, + BitOp::Or => v_a | v_b, + BitOp::Xor => v_a ^ v_b, + BitOp::Shl => v_a << v_b, + BitOp::Shr => v_a >> v_b, + // TODO Fix + BitOp::UShr => v_a >> v_b, + }) + } + op::BinOp::Comp(op) => { + let mut v_a = self.lhs().run(interpreter)?; + let mut v_b = self.rhs().run(interpreter)?; + Ok(Value::from(match op { + CompOp::Equal => v_a.equals(v_b.borrow_mut(), interpreter), + CompOp::NotEqual => !v_a.equals(v_b.borrow_mut(), interpreter), + CompOp::StrictEqual => v_a.strict_equals(&v_b), + CompOp::StrictNotEqual => !v_a.strict_equals(&v_b), + CompOp::GreaterThan => v_a.to_number() > v_b.to_number(), + CompOp::GreaterThanOrEqual => v_a.to_number() >= v_b.to_number(), + CompOp::LessThan => v_a.to_number() < v_b.to_number(), + CompOp::LessThanOrEqual => v_a.to_number() <= v_b.to_number(), + CompOp::In => { + if !v_b.is_object() { + panic!("TypeError: {} is not an Object.", v_b); + } + let key = interpreter.to_property_key(&mut v_a); + interpreter.has_property(&mut v_b, &key) + } + })) + } + op::BinOp::Log(op) => { + // turn a `Value` into a `bool` + let to_bool = |value| bool::from(&value); + Ok(match op { + LogOp::And => Value::from( + to_bool(self.lhs().run(interpreter)?) + && to_bool(self.rhs().run(interpreter)?), + ), + LogOp::Or => Value::from( + to_bool(self.lhs().run(interpreter)?) + || to_bool(self.rhs().run(interpreter)?), + ), + }) + } + op::BinOp::Assign(op) => match self.lhs() { + Node::Identifier(ref name) => { + let v_a = interpreter + .realm() + .environment + .get_binding_value(name.as_ref()); + let v_b = self.rhs().run(interpreter)?; + let value = Self::run_assign(op, v_a, v_b); + interpreter.realm.environment.set_mutable_binding( + name.as_ref(), + value.clone(), + true, + ); + Ok(value) + } + Node::GetConstField(ref obj, ref field) => { + let v_r_a = obj.run(interpreter)?; + let v_a = v_r_a.get_field(field); + let v_b = self.rhs().run(interpreter)?; + let value = Self::run_assign(op, v_a, v_b); + v_r_a.set_field(&field.clone(), value.clone()); + Ok(value) + } + _ => Ok(Value::undefined()), + }, + } + } +} + +impl BinOp { + /// Runs the assignment operators. + fn run_assign(op: AssignOp, v_a: Value, v_b: Value) -> Value { + match op { + AssignOp::Add => v_a + v_b, + AssignOp::Sub => v_a - v_b, + AssignOp::Mul => v_a * v_b, + AssignOp::Exp => v_a.as_num_to_power(v_b), + AssignOp::Div => v_a / v_b, + AssignOp::Mod => v_a % v_b, + AssignOp::And => v_a & v_b, + AssignOp::Or => v_a | v_b, + AssignOp::Xor => v_a ^ v_b, + AssignOp::Shl => v_a << v_b, + AssignOp::Shr => v_a << v_b, + } + } +} + +impl Executable for UnaryOp { + fn run(&self, interpreter: &mut Interpreter) -> ResultValue { + let v_a = self.target().run(interpreter)?; + + Ok(match self.op() { + op::UnaryOp::Minus => -v_a, + op::UnaryOp::Plus => Value::from(v_a.to_number()), + op::UnaryOp::IncrementPost => { + let ret = v_a.clone(); + interpreter.set_value(self.target(), Value::from(v_a.to_number() + 1.0))?; + ret + } + op::UnaryOp::IncrementPre => { + interpreter.set_value(self.target(), Value::from(v_a.to_number() + 1.0))? + } + op::UnaryOp::DecrementPost => { + let ret = v_a.clone(); + interpreter.set_value(self.target(), Value::from(v_a.to_number() - 1.0))?; + ret + } + op::UnaryOp::DecrementPre => { + interpreter.set_value(self.target(), Value::from(v_a.to_number() - 1.0))? + } + op::UnaryOp::Not => !v_a, + op::UnaryOp::Tilde => { + let num_v_a = v_a.to_number(); + // NOTE: possible UB: https://github.com/rust-lang/rust/issues/10184 + Value::from(if num_v_a.is_nan() { + -1 + } else { + !(num_v_a as i32) + }) + } + op::UnaryOp::Void => Value::undefined(), + op::UnaryOp::Delete => match *self.target() { + Node::GetConstField(ref obj, ref field) => { + Value::boolean(obj.run(interpreter)?.remove_property(field)) + } + Node::GetField(ref obj, ref field) => Value::boolean( + obj.run(interpreter)? + .remove_property(&field.run(interpreter)?.to_string()), + ), + Node::Identifier(_) => Value::boolean(false), + Node::ArrayDecl(_) + | Node::Block(_) + | Node::Const(_) + | Node::FunctionDecl(_) + | Node::FunctionExpr(_) + | Node::New(_) + | Node::Object(_) + | Node::UnaryOp(_) => Value::boolean(true), + _ => panic!("SyntaxError: wrong delete argument {}", self), + }, + op::UnaryOp::TypeOf => Value::from(v_a.get_type()), + }) + } +} diff --git a/boa/src/exec/statement_list.rs b/boa/src/exec/statement_list.rs new file mode 100644 index 0000000000..31558e383f --- /dev/null +++ b/boa/src/exec/statement_list.rs @@ -0,0 +1,26 @@ +//! Statement list execution. + +use super::{Executable, Interpreter}; +use crate::{ + builtins::value::{ResultValue, Value}, + syntax::ast::node::StatementList, +}; + +impl Executable for StatementList { + fn run(&self, interpreter: &mut Interpreter) -> ResultValue { + let mut obj = Value::null(); + for (i, item) in self.statements().iter().enumerate() { + let val = item.run(interpreter)?; + // early return + if interpreter.is_return { + obj = val; + break; + } + if i + 1 == self.statements().len() { + obj = val; + } + } + + Ok(obj) + } +} diff --git a/boa/src/exec/tests.rs b/boa/src/exec/tests.rs index 1bee7bb4b5..105e34b7f2 100644 --- a/boa/src/exec/tests.rs +++ b/boa/src/exec/tests.rs @@ -1,7 +1,4 @@ -use crate::exec; -use crate::exec::Executor; -use crate::forward; -use crate::realm::Realm; +use crate::{exec, exec::Interpreter, forward, realm::Realm}; #[test] fn empty_let_decl_undefined() { @@ -47,7 +44,7 @@ fn object_field_set() { #[test] fn spread_with_arguments() { let realm = Realm::create(); - let mut engine = Executor::new(realm); + let mut engine = Interpreter::new(realm); let scenario = r#" const a = [1, "test", 3, 4]; @@ -74,7 +71,7 @@ fn spread_with_arguments() { #[test] fn array_rest_with_arguments() { let realm = Realm::create(); - let mut engine = Executor::new(realm); + let mut engine = Interpreter::new(realm); let scenario = r#" var b = [4, 5, 6] @@ -329,10 +326,7 @@ fn test_for_loop() { a "#; - assert_eq!( - exec(body_should_not_execute_on_false_condition), - String::from("0") - ); + assert_eq!(&exec(body_should_not_execute_on_false_condition), "0"); let inner_scope = r#" for (let i = 0;false;) {} @@ -626,7 +620,7 @@ mod in_operator { #[test] fn should_set_this_value() { let realm = Realm::create(); - let mut engine = Executor::new(realm); + let mut engine = Interpreter::new(realm); let scenario = r#" function Foo() { @@ -645,7 +639,7 @@ mod in_operator { fn new_instance_should_point_to_prototype() { // A new instance should point to a prototype object created with the constructor function let realm = Realm::create(); - let mut engine = Executor::new(realm); + let mut engine = Interpreter::new(realm); let scenario = r#" function Foo() {} @@ -656,3 +650,97 @@ mod in_operator { assert!(a.get_internal_slot(INSTANCE_PROTOTYPE).is_object(), true); } } + +#[test] +fn var_decl_hoisting() { + let scenario = r#" + x = 5; + + var x; + x; + "#; + assert_eq!(&exec(scenario), "5"); + + let scenario = r#" + x = 5; + + var x = 10; + x; + "#; + assert_eq!(&exec(scenario), "10"); + + let scenario = r#" + x = y; + + var x = 10; + var y = 5; + + x; + "#; + assert_eq!(&exec(scenario), "10"); + + let scenario = r#" + var x = y; + + var y = 5; + x; + "#; + assert_eq!(&exec(scenario), "undefined"); + + let scenario = r#" + let y = x; + x = 5; + + var x = 10; + y; + "#; + assert_eq!(&exec(scenario), "undefined"); +} + +#[test] +fn function_decl_hoisting() { + let scenario = r#" + let a = hello(); + function hello() { return 5 } + + a; + "#; + assert_eq!(&exec(scenario), "5"); + + let scenario = r#" + x = hello(); + + function hello() {return 5} + var x; + x; + "#; + assert_eq!(&exec(scenario), "5"); + + let scenario = r#" + hello = function() { return 5 } + x = hello(); + + x; + "#; + assert_eq!(&exec(scenario), "5"); + + let scenario = r#" + let x = b(); + + function a() {return 5} + function b() {return a()} + + x; + "#; + assert_eq!(&exec(scenario), "5"); + + let scenario = r#" + let x = b(); + + function b() {return a()} + function a() {return 5} + + x; + "#; + assert_eq!(&exec(scenario), "5"); +} diff --git a/boa/src/exec/try_node/mod.rs b/boa/src/exec/try_node/mod.rs new file mode 100644 index 0000000000..8b45e46e1a --- /dev/null +++ b/boa/src/exec/try_node/mod.rs @@ -0,0 +1,54 @@ +//! Try..catch node execution. + +use super::{Executable, Interpreter}; +use crate::{ + builtins::value::ResultValue, + environment::lexical_environment::{new_declarative_environment, VariableScope}, + syntax::ast::node::Try, +}; + +#[cfg(test)] +mod tests; + +impl Executable for Try { + fn run(&self, interpreter: &mut Interpreter) -> ResultValue { + let res = self.block().run(interpreter).map_or_else( + |err| { + if let Some(catch) = self.catch() { + { + let env = &mut interpreter.realm_mut().environment; + env.push(new_declarative_environment(Some( + env.get_current_environment_ref().clone(), + ))); + + if let Some(param) = catch.parameter() { + env.create_mutable_binding( + param.to_owned(), + false, + VariableScope::Block, + ); + + env.initialize_binding(param, err); + } + } + + let res = catch.block().run(interpreter); + + // pop the block env + let _ = interpreter.realm_mut().environment.pop(); + + res + } else { + Err(err) + } + }, + Ok, + ); + + if let Some(finally) = self.finally() { + finally.run(interpreter)?; + } + + res + } +} diff --git a/boa/src/exec/try_node/tests.rs b/boa/src/exec/try_node/tests.rs new file mode 100644 index 0000000000..e5d2af85d3 --- /dev/null +++ b/boa/src/exec/try_node/tests.rs @@ -0,0 +1,95 @@ +use crate::exec; + +#[test] +fn simple_try() { + let scenario = r#" + let a = 10; + try { + a = 20; + } catch { + a = 30; + } + + a; + "#; + assert_eq!(&exec(scenario), "20"); +} + +#[test] +fn finally() { + let scenario = r#" + let a = 10; + try { + a = 20; + } finally { + a = 30; + } + + a; + "#; + assert_eq!(&exec(scenario), "30"); +} + +#[test] +fn catch_finally() { + let scenario = r#" + let a = 10; + try { + a = 20; + } catch { + a = 40; + } finally { + a = 30; + } + + a; + "#; + assert_eq!(&exec(scenario), "30"); +} + +#[test] +fn catch() { + let scenario = r#" + let a = 10; + try { + throw "error"; + } catch { + a = 20; + } + + a; + "#; + assert_eq!(&exec(scenario), "20"); +} + +#[test] +fn catch_binding() { + let scenario = r#" + let a = 10; + try { + throw 20; + } catch(err) { + a = err; + } + + a; + "#; + assert_eq!(&exec(scenario), "20"); +} + +#[test] +fn catch_binding_finally() { + let scenario = r#" + let a = 10; + try { + throw 20; + } catch(err) { + a = err; + } finally { + a = 30; + } + + a; + "#; + assert_eq!(&exec(scenario), "30"); +} diff --git a/boa/src/lib.rs b/boa/src/lib.rs index 8f9bfda549..ed75621262 100644 --- a/boa/src/lib.rs +++ b/boa/src/lib.rs @@ -38,20 +38,21 @@ pub mod environment; pub mod exec; pub mod realm; pub mod syntax; -use crate::{ - builtins::value::ResultValue, - exec::{Executor, Interpreter}, + +use crate::{builtins::value::ResultValue, syntax::ast::node::StatementList}; +pub use crate::{ + exec::{Executable, Interpreter}, realm::Realm, - syntax::{ast::node::Node, lexer::Lexer, parser::Parser}, + syntax::{lexer::Lexer, parser::Parser}, }; -fn parser_expr(src: &str) -> Result { +fn parser_expr(src: &str) -> Result { let mut lexer = Lexer::new(src); - lexer.lex().map_err(|e| format!("SyntaxError: {}", e))?; + lexer.lex().map_err(|e| format!("Syntax Error: {}", e))?; let tokens = lexer.tokens; Parser::new(&tokens) .parse_all() - .map_err(|e| format!("ParsingError: {}", e)) + .map_err(|e| format!("Parsing Error: {}", e)) } /// Execute the code using an existing Interpreter @@ -59,16 +60,11 @@ fn parser_expr(src: &str) -> Result { pub fn forward(engine: &mut Interpreter, src: &str) -> String { // Setup executor let expr = match parser_expr(src) { - Ok(v) => v, - Err(error_string) => { - return error_string; - } + Ok(res) => res, + Err(e) => return e, }; - let result = engine.run(&expr); - match result { - Ok(v) => v.to_string(), - Err(v) => format!("{}: {}", "Error", v.to_string()), - } + expr.run(engine) + .map_or_else(|e| format!("Error: {}", e), |v| v.to_string()) } /// Execute the code using an existing Interpreter. @@ -78,7 +74,7 @@ pub fn forward(engine: &mut Interpreter, src: &str) -> String { pub fn forward_val(engine: &mut Interpreter, src: &str) -> ResultValue { // Setup executor match parser_expr(src) { - Ok(expr) => engine.run(&expr), + Ok(expr) => expr.run(engine), Err(e) => { eprintln!("{}", e); std::process::exit(1); @@ -90,6 +86,6 @@ pub fn forward_val(engine: &mut Interpreter, src: &str) -> ResultValue { pub fn exec(src: &str) -> String { // Create new Realm let realm = Realm::create(); - let mut engine: Interpreter = Executor::new(realm); + let mut engine = Interpreter::new(realm); forward(&mut engine, src) } diff --git a/boa/src/syntax/ast/constant.rs b/boa/src/syntax/ast/constant.rs index 9f117825d4..eb05b1bee5 100644 --- a/boa/src/syntax/ast/constant.rs +++ b/boa/src/syntax/ast/constant.rs @@ -40,7 +40,7 @@ pub enum Const { /// /// [spec]: https://tc39.es/ecma262/#sec-terms-and-definitions-string-value /// [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Grammar_and_types#String_literals - String(String), + String(Box), /// A floating-point number literal. /// @@ -115,19 +115,25 @@ pub enum Const { impl From<&str> for Const { fn from(s: &str) -> Self { - Self::String(s.into()) + Self::String(s.to_owned().into_boxed_str()) } } impl From<&String> for Const { fn from(s: &String) -> Self { - Self::String(s.clone()) + Self::String(s.clone().into_boxed_str()) + } +} + +impl From> for Const { + fn from(s: Box) -> Self { + Self::String(s) } } impl From for Const { fn from(s: String) -> Self { - Self::String(s) + Self::String(s.into_boxed_str()) } } diff --git a/boa/src/syntax/ast/keyword.rs b/boa/src/syntax/ast/keyword.rs index a76195e5a8..69bfba4fb2 100644 --- a/boa/src/syntax/ast/keyword.rs +++ b/boa/src/syntax/ast/keyword.rs @@ -8,12 +8,7 @@ //! [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Lexical_grammar#Keywords use crate::syntax::ast::op::{BinOp, CompOp}; -use std::{ - convert::TryInto, - error, - fmt::{Display, Error, Formatter}, - str::FromStr, -}; +use std::{convert::TryInto, error, fmt, str::FromStr}; #[cfg(feature = "serde")] use serde::{Deserialize, Serialize}; @@ -436,12 +431,55 @@ pub enum Keyword { } impl Keyword { + /// Gets the keyword as a binary operation, if this keyword is the `in` keyword. pub fn as_binop(self) -> Option { match self { Keyword::In => Some(BinOp::Comp(CompOp::In)), _ => None, } } + + /// Gets the keyword as a string. + pub fn as_str(self) -> &'static str { + match self { + Self::Await => "await", + Self::Break => "break", + Self::Case => "case", + Self::Catch => "catch", + Self::Class => "class", + Self::Continue => "continue", + Self::Const => "const", + Self::Debugger => "debugger", + Self::Default => "default", + Self::Delete => "delete", + Self::Do => "do", + Self::Else => "else", + Self::Enum => "enum", + Self::Extends => "extends", + Self::Export => "export", + Self::Finally => "finally", + Self::For => "for", + Self::Function => "function", + Self::If => "if", + Self::In => "in", + Self::InstanceOf => "instanceof", + Self::Import => "import", + Self::Let => "let", + Self::New => "new", + Self::Return => "return", + Self::Super => "super", + Self::Switch => "switch", + Self::This => "this", + Self::Throw => "throw", + Self::Try => "try", + Self::TypeOf => "typeof", + Self::Var => "var", + Self::Void => "void", + Self::While => "while", + Self::With => "with", + Self::Yield => "yield", + } + } } impl TryInto for Keyword { @@ -454,8 +492,8 @@ impl TryInto for Keyword { #[derive(Debug, Clone, Copy)] pub struct KeywordError; -impl Display for KeywordError { - fn fmt(&self, f: &mut Formatter<'_>) -> Result<(), Error> { +impl fmt::Display for KeywordError { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { write!(f, "invalid token") } } @@ -515,49 +553,9 @@ impl FromStr for Keyword { } } } -impl Display for Keyword { - fn fmt(&self, f: &mut Formatter<'_>) -> Result<(), Error> { - write!( - f, - "{}", - match *self { - Self::Await => "await", - Self::Break => "break", - Self::Case => "case", - Self::Catch => "catch", - Self::Class => "class", - Self::Continue => "continue", - Self::Const => "const", - Self::Debugger => "debugger", - Self::Default => "default", - Self::Delete => "delete", - Self::Do => "do", - Self::Else => "else", - Self::Enum => "enum", - Self::Extends => "extends", - Self::Export => "export", - Self::Finally => "finally", - Self::For => "for", - Self::Function => "function", - Self::If => "if", - Self::In => "in", - Self::InstanceOf => "instanceof", - Self::Import => "import", - Self::Let => "let", - Self::New => "new", - Self::Return => "return", - Self::Super => "super", - Self::Switch => "switch", - Self::This => "this", - Self::Throw => "throw", - Self::Try => "try", - Self::TypeOf => "typeof", - Self::Var => "var", - Self::Void => "void", - Self::While => "while", - Self::With => "with", - Self::Yield => "yield", - } - ) + +impl fmt::Display for Keyword { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + fmt::Display::fmt(self.as_str(), f) } } diff --git a/boa/src/syntax/ast/mod.rs b/boa/src/syntax/ast/mod.rs index 21669bcdbb..98c116b656 100644 --- a/boa/src/syntax/ast/mod.rs +++ b/boa/src/syntax/ast/mod.rs @@ -5,6 +5,15 @@ pub mod constant; pub mod keyword; pub mod node; pub mod op; -pub mod pos; -pub mod punc; +pub mod position; +pub mod punctuator; pub mod token; + +pub use self::{ + constant::Const, + keyword::Keyword, + node::Node, + position::{Position, Span}, + punctuator::Punctuator, + token::{Token, TokenKind}, +}; diff --git a/boa/src/syntax/ast/node/array.rs b/boa/src/syntax/ast/node/array.rs new file mode 100644 index 0000000000..c1f2479e59 --- /dev/null +++ b/boa/src/syntax/ast/node/array.rs @@ -0,0 +1,60 @@ +//! Array declaration node. + +use super::{join_nodes, Node}; +use gc::{Finalize, Trace}; +use std::fmt; + +#[cfg(feature = "serde")] +use serde::{Deserialize, Serialize}; + +/// An array is an ordered collection of data (either primitive or object depending upon the +/// language). +/// +/// Arrays are used to store multiple values in a single variable. +/// This is compared to a variable that can store only one value. +/// +/// Each item in an array has a number attached to it, called a numeric index, that allows you +/// to access it. In JavaScript, arrays start at index zero and can be manipulated with various +/// methods. +/// +/// More information: +/// - [ECMAScript reference][spec] +/// - [MDN documentation][mdn] +/// +/// [spec]: https://tc39.es/ecma262/#prod-ArrayLiteral +/// [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +#[derive(Clone, Debug, Trace, Finalize, PartialEq)] +pub struct ArrayDecl { + #[cfg_attr(feature = "serde", serde(flatten))] + arr: Box<[Node]>, +} + +impl AsRef<[Node]> for ArrayDecl { + fn as_ref(&self) -> &[Node] { + &self.arr + } +} + +impl From for ArrayDecl +where + T: Into>, +{ + fn from(decl: T) -> Self { + Self { arr: decl.into() } + } +} + +impl fmt::Display for ArrayDecl { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.write_str("[")?; + join_nodes(f, &self.arr)?; + f.write_str("]") + } +} + +impl From for Node { + fn from(arr: ArrayDecl) -> Self { + Self::ArrayDecl(arr) + } +} diff --git a/boa/src/syntax/ast/node/block.rs b/boa/src/syntax/ast/node/block.rs new file mode 100644 index 0000000000..68a152b14f --- /dev/null +++ b/boa/src/syntax/ast/node/block.rs @@ -0,0 +1,68 @@ +//! Block AST node. + +use super::{Node, StatementList}; +use gc::{Finalize, Trace}; +use std::fmt; + +#[cfg(feature = "serde")] +use serde::{Deserialize, Serialize}; + +/// A `block` statement (or compound statement in other languages) is used to group zero or +/// more statements. +/// +/// The block statement is often called compound statement in other languages. +/// It allows you to use multiple statements where JavaScript expects only one statement. +/// Combining statements into blocks is a common practice in JavaScript. The opposite behavior +/// is possible using an empty statement, where you provide no statement, although one is +/// required. +/// +/// More information: +/// - [ECMAScript reference][spec] +/// - [MDN documentation][mdn] +/// +/// [spec]: https://tc39.es/ecma262/#prod-BlockStatement +/// [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/block +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +#[cfg_attr(feature = "serde", serde(transparent))] +#[derive(Clone, Debug, Trace, Finalize, PartialEq)] +pub struct Block { + #[cfg_attr(feature = "serde", serde(flatten))] + statements: StatementList, +} + +impl Block { + /// Gets the list of statements in this block. + pub(crate) fn statements(&self) -> &[Node] { + self.statements.statements() + } + + /// Implements the display formatting with indentation. + pub(super) fn display(&self, f: &mut fmt::Formatter<'_>, indentation: usize) -> fmt::Result { + writeln!(f, "{{")?; + self.statements.display(f, indentation + 1)?; + write!(f, "{}}}", " ".repeat(indentation)) + } +} + +impl From for Block +where + T: Into, +{ + fn from(list: T) -> Self { + Self { + statements: list.into(), + } + } +} + +impl fmt::Display for Block { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + self.display(f, 0) + } +} + +impl From for Node { + fn from(block: Block) -> Self { + Self::Block(block) + } +} diff --git a/boa/src/syntax/ast/node/declaration.rs b/boa/src/syntax/ast/node/declaration.rs new file mode 100644 index 0000000000..ea73685bd0 --- /dev/null +++ b/boa/src/syntax/ast/node/declaration.rs @@ -0,0 +1,550 @@ +//! Declaration nodes. + +use super::{join_nodes, FormalParameter, Identifier, Node, StatementList}; +use gc::{Finalize, Trace}; +use std::fmt; + +#[cfg(feature = "serde")] +use serde::{Deserialize, Serialize}; + +/// The `var` statement declares a variable, optionally initializing it to a value. +/// +/// var declarations, wherever they occur, are processed before any code is executed. This is +/// called hoisting, and is discussed further below. +/// +/// The scope of a variable declared with var is its current execution context, which is either +/// the enclosing function or, for variables declared outside any function, global. If you +/// re-declare a JavaScript variable, it will not lose its value. +/// +/// Assigning a value to an undeclared variable implicitly creates it as a global variable (it +/// becomes a property of the global object) when the assignment is executed. +/// +/// More information: +/// - [ECMAScript reference][spec] +/// - [MDN documentation][mdn] +/// +/// [spec]: https://tc39.es/ecma262/#prod-VariableStatement +/// [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/var +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +#[derive(Clone, Debug, Trace, Finalize, PartialEq)] +pub struct VarDeclList { + #[cfg_attr(feature = "serde", serde(flatten))] + vars: Box<[VarDecl]>, +} + +impl From for VarDeclList +where + T: Into>, +{ + fn from(list: T) -> Self { + Self { vars: list.into() } + } +} + +impl From for VarDeclList { + fn from(decl: VarDecl) -> Self { + Self { + vars: Box::new([decl]), + } + } +} + +impl AsRef<[VarDecl]> for VarDeclList { + fn as_ref(&self) -> &[VarDecl] { + &self.vars + } +} + +impl fmt::Display for VarDeclList { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + if !self.vars.is_empty() { + write!(f, "var ")?; + join_nodes(f, &self.vars) + } else { + Ok(()) + } + } +} + +impl From for Node { + fn from(list: VarDeclList) -> Self { + Self::VarDeclList(list) + } +} + +/// Individual variable declaration. +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +#[derive(Clone, Debug, Trace, Finalize, PartialEq)] +pub struct VarDecl { + name: Identifier, + init: Option, +} + +impl fmt::Display for VarDecl { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + fmt::Display::fmt(&self.name, f)?; + if let Some(ref init) = self.init { + write!(f, " = {}", init)?; + } + Ok(()) + } +} + +impl VarDecl { + /// Creates a new variable declaration. + pub(in crate::syntax) fn new(name: N, init: I) -> Self + where + N: Into, + I: Into>, + { + Self { + name: name.into(), + init: init.into(), + } + } + + /// Gets the name of the variable. + pub fn name(&self) -> &str { + self.name.as_ref() + } + + /// Gets the initialization node for the variable, if any. + pub fn init(&self) -> Option<&Node> { + self.init.as_ref() + } +} + +/// The `function` expression defines a function with the specified parameters. +/// +/// A function created with a function expression is a `Function` object and has all the +/// properties, methods and behavior of `Function`. +/// +/// A function can also be created using a declaration (see function expression). +/// +/// By default, functions return `undefined`. To return any other value, the function must have +/// a return statement that specifies the value to return. +/// +/// More information: +/// - [ECMAScript reference][spec] +/// - [MDN documentation][mdn] +/// +/// [spec]: https://tc39.es/ecma262/#sec-terms-and-definitions-function +/// [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/function +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +#[derive(Clone, Debug, Trace, Finalize, PartialEq)] +pub struct FunctionExpr { + name: Option>, + parameters: Box<[FormalParameter]>, + body: StatementList, +} + +impl FunctionExpr { + /// Creates a new function expression + pub(in crate::syntax) fn new(name: N, parameters: P, body: B) -> Self + where + N: Into>>, + P: Into>, + B: Into, + { + Self { + name: name.into(), + parameters: parameters.into(), + body: body.into(), + } + } + + /// Gets the name of the function declaration. + pub fn name(&self) -> Option<&str> { + self.name.as_ref().map(Box::as_ref) + } + + /// Gets the list of parameters of the function declaration. + pub fn parameters(&self) -> &[FormalParameter] { + &self.parameters + } + + /// Gets the body of the function declaration. + pub fn body(&self) -> &[Node] { + self.body.statements() + } + + /// Implements the display formatting with indentation. + pub(super) fn display(&self, f: &mut fmt::Formatter<'_>, indentation: usize) -> fmt::Result { + f.write_str("function")?; + if let Some(ref name) = self.name { + write!(f, " {}", name)?; + } + f.write_str("(")?; + join_nodes(f, &self.parameters)?; + f.write_str(") {{")?; + + self.body.display(f, indentation + 1)?; + + writeln!(f, "}}") + } +} + +impl fmt::Display for FunctionExpr { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + self.display(f, 0) + } +} + +impl From for Node { + fn from(expr: FunctionExpr) -> Self { + Self::FunctionExpr(expr) + } +} + +/// The `function` declaration (function statement) defines a function with the specified +/// parameters. +/// +/// A function created with a function declaration is a `Function` object and has all the +/// properties, methods and behavior of `Function`. +/// +/// A function can also be created using an expression (see [function expression][func_expr]). +/// +/// By default, functions return `undefined`. To return any other value, the function must have +/// a return statement that specifies the value to return. +/// +/// More information: +/// - [ECMAScript reference][spec] +/// - [MDN documentation][mdn] +/// +/// [spec]: https://tc39.es/ecma262/#sec-terms-and-definitions-function +/// [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/function +/// [func_expr]: ../enum.Node.html#variant.FunctionExpr +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +#[derive(Clone, Debug, Trace, Finalize, PartialEq)] +pub struct FunctionDecl { + name: Box, + parameters: Box<[FormalParameter]>, + body: StatementList, +} + +impl FunctionDecl { + /// Creates a new function declaration. + pub(in crate::syntax) fn new(name: N, parameters: P, body: B) -> Self + where + N: Into>, + P: Into>, + B: Into, + { + Self { + name: name.into(), + parameters: parameters.into(), + body: body.into(), + } + } + + /// Gets the name of the function declaration. + pub fn name(&self) -> &str { + &self.name + } + + /// Gets the list of parameters of the function declaration. + pub fn parameters(&self) -> &[FormalParameter] { + &self.parameters + } + + /// Gets the body of the function declaration. + pub fn body(&self) -> &[Node] { + self.body.statements() + } + + /// Implements the display formatting with indentation. + pub(super) fn display(&self, f: &mut fmt::Formatter<'_>, indentation: usize) -> fmt::Result { + write!(f, "function {}(", self.name)?; + join_nodes(f, &self.parameters)?; + f.write_str(") {{")?; + + self.body.display(f, indentation + 1)?; + + writeln!(f, "}}") + } +} + +impl From for Node { + fn from(decl: FunctionDecl) -> Self { + Self::FunctionDecl(decl) + } +} + +impl fmt::Display for FunctionDecl { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + self.display(f, 0) + } +} + +/// An arrow function expression is a syntactically compact alternative to a regular function +/// expression. +/// +/// Arrow function expressions are ill suited as methods, and they cannot be used as +/// constructors. Arrow functions cannot be used as constructors and will throw an error when +/// used with new. +/// +/// More information: +/// - [ECMAScript reference][spec] +/// - [MDN documentation][mdn] +/// +/// [spec]: https://tc39.es/ecma262/#prod-ArrowFunction +/// [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Functions/Arrow_functions +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +#[derive(Clone, Debug, Trace, Finalize, PartialEq)] +pub struct ArrowFunctionDecl { + params: Box<[FormalParameter]>, + body: StatementList, +} + +impl ArrowFunctionDecl { + /// Creates a new `ArrowFunctionDecl` AST node. + pub(in crate::syntax) fn new(params: P, body: B) -> Self + where + P: Into>, + B: Into, + { + Self { + params: params.into(), + body: body.into(), + } + } + + /// Gets the list of parameters of the arrow function. + pub(crate) fn params(&self) -> &[FormalParameter] { + &self.params + } + + /// Gets the body of the arrow function. + pub(crate) fn body(&self) -> &[Node] { + &self.body.statements() + } + + /// Implements the display formatting with indentation. + pub(super) fn display(&self, f: &mut fmt::Formatter<'_>, indentation: usize) -> fmt::Result { + write!(f, "(")?; + join_nodes(f, &self.params)?; + f.write_str(") => ")?; + self.body.display(f, indentation) + } +} + +impl fmt::Display for ArrowFunctionDecl { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + self.display(f, 0) + } +} + +impl From for Node { + fn from(decl: ArrowFunctionDecl) -> Self { + Self::ArrowFunctionDecl(decl) + } +} + +/// The `const` statements are block-scoped, much like variables defined using the `let` +/// keyword. +/// +/// This declaration creates a constant whose scope can be either global or local to the block +/// in which it is declared. Global constants do not become properties of the window object, +/// unlike var variables. +/// +/// An initializer for a constant is required. You must specify its value in the same statement +/// in which it's declared. (This makes sense, given that it can't be changed later.) +/// +/// More information: +/// - [ECMAScript reference][spec] +/// - [MDN documentation][mdn] +/// +/// [spec]: https://tc39.es/ecma262/#sec-let-and-const-declarations +/// [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/const +/// [identifier]: https://developer.mozilla.org/en-US/docs/Glossary/identifier +/// [expression]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Expressions_and_Operators#Expressions +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +#[derive(Clone, Debug, Trace, Finalize, PartialEq)] +pub struct ConstDeclList { + #[cfg_attr(feature = "serde", serde(flatten))] + list: Box<[ConstDecl]>, +} + +impl From for ConstDeclList +where + T: Into>, +{ + fn from(list: T) -> Self { + Self { list: list.into() } + } +} + +impl From for ConstDeclList { + fn from(decl: ConstDecl) -> Self { + Self { + list: Box::new([decl]), + } + } +} + +impl AsRef<[ConstDecl]> for ConstDeclList { + fn as_ref(&self) -> &[ConstDecl] { + &self.list + } +} + +impl fmt::Display for ConstDeclList { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + if !self.list.is_empty() { + write!(f, "const ")?; + join_nodes(f, &self.list) + } else { + Ok(()) + } + } +} + +impl From for Node { + fn from(list: ConstDeclList) -> Self { + Self::ConstDeclList(list) + } +} + +/// Individual constant declaration. +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +#[derive(Clone, Debug, Trace, Finalize, PartialEq)] +pub struct ConstDecl { + name: Identifier, + init: Node, +} + +impl fmt::Display for ConstDecl { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "{} = {}", self.name, self.init) + } +} + +impl ConstDecl { + /// Creates a new variable declaration. + pub(in crate::syntax) fn new(name: N, init: I) -> Self + where + N: Into, + I: Into, + { + Self { + name: name.into(), + init: init.into(), + } + } + + /// Gets the name of the variable. + pub fn name(&self) -> &str { + self.name.as_ref() + } + + /// Gets the initialization node for the variable, if any. + pub fn init(&self) -> &Node { + &self.init + } +} + +/// The `let` statement declares a block scope local variable, optionally initializing it to a +/// value. +/// +/// +/// `let` allows you to declare variables that are limited to a scope of a block statement, or +/// expression on which it is used, unlike the `var` keyword, which defines a variable +/// globally, or locally to an entire function regardless of block scope. +/// +/// Just like const the `let` does not create properties of the window object when declared +/// globally (in the top-most scope). +/// +/// More information: +/// - [ECMAScript reference][spec] +/// - [MDN documentation][mdn] +/// +/// [spec]: https://tc39.es/ecma262/#sec-let-and-const-declarations +/// [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/let +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +#[derive(Clone, Debug, Trace, Finalize, PartialEq)] +pub struct LetDeclList { + #[cfg_attr(feature = "serde", serde(flatten))] + list: Box<[LetDecl]>, +} + +impl From for LetDeclList +where + T: Into>, +{ + fn from(list: T) -> Self { + Self { list: list.into() } + } +} + +impl From for LetDeclList { + fn from(decl: LetDecl) -> Self { + Self { + list: Box::new([decl]), + } + } +} + +impl AsRef<[LetDecl]> for LetDeclList { + fn as_ref(&self) -> &[LetDecl] { + &self.list + } +} + +impl fmt::Display for LetDeclList { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + if !self.list.is_empty() { + write!(f, "let ")?; + join_nodes(f, &self.list) + } else { + Ok(()) + } + } +} + +impl From for Node { + fn from(list: LetDeclList) -> Self { + Self::LetDeclList(list) + } +} + +/// Individual constant declaration. +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +#[derive(Clone, Debug, Trace, Finalize, PartialEq)] +pub struct LetDecl { + name: Identifier, + init: Option, +} + +impl fmt::Display for LetDecl { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + fmt::Display::fmt(&self.name, f)?; + if let Some(ref init) = self.init { + write!(f, " = {}", init)?; + } + Ok(()) + } +} + +impl LetDecl { + /// Creates a new variable declaration. + pub(in crate::syntax) fn new(name: N, init: I) -> Self + where + N: Into, + I: Into>, + { + Self { + name: name.into(), + init: init.into(), + } + } + + /// Gets the name of the variable. + pub fn name(&self) -> &str { + self.name.as_ref() + } + + /// Gets the initialization node for the variable, if any. + pub fn init(&self) -> Option<&Node> { + self.init.as_ref() + } +} diff --git a/boa/src/syntax/ast/node/expression.rs b/boa/src/syntax/ast/node/expression.rs new file mode 100644 index 0000000000..5c2b8ae12f --- /dev/null +++ b/boa/src/syntax/ast/node/expression.rs @@ -0,0 +1,118 @@ +//! Expression nodes. + +use super::{join_nodes, Node}; +use gc::{Finalize, Trace}; +use std::fmt; + +#[cfg(feature = "serde")] +use serde::{Deserialize, Serialize}; + +/// Calling the function actually performs the specified actions with the indicated parameters. +/// +/// Defining a function does not execute it. Defining it simply names the function and +/// specifies what to do when the function is called. Functions must be in scope when they are +/// called, but the function declaration can be hoisted. The scope of a function is the +/// function in which it is declared (or the entire program, if it is declared at the top +/// level). +/// +/// More information: +/// - [ECMAScript reference][spec] +/// - [MDN documentation][mdn] +/// +/// [spec]: https://tc39.es/ecma262/#prod-CallExpression +/// [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Functions#Calling_functions +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +#[derive(Clone, Debug, Trace, Finalize, PartialEq)] +pub struct Call { + expr: Box, + args: Box<[Node]>, +} + +impl Call { + /// Creates a new `Call` AST node. + pub fn new(expr: E, args: A) -> Self + where + E: Into, + A: Into>, + { + Self { + expr: Box::new(expr.into()), + args: args.into(), + } + } + + /// Gets the name of the function call. + pub fn expr(&self) -> &Node { + &self.expr + } + + /// Retrieves the arguments passed to the function. + pub fn args(&self) -> &[Node] { + &self.args + } +} + +impl fmt::Display for Call { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "{}(", self.expr)?; + join_nodes(f, &self.args)?; + f.write_str(")") + } +} + +impl From for Node { + fn from(call: Call) -> Self { + Self::Call(call) + } +} + +/// The `new` operator lets developers create an instance of a user-defined object type or of +/// one of the built-in object types that has a constructor function. +/// +/// The new keyword does the following things: +/// - Creates a blank, plain JavaScript object; +/// - Links (sets the constructor of) this object to another object; +/// - Passes the newly created object from Step 1 as the this context; +/// - Returns this if the function doesn't return its own object. +/// +/// More information: +/// - [ECMAScript reference][spec] +/// - [MDN documentation][mdn] +/// +/// [spec]: https://tc39.es/ecma262/#prod-NewExpression +/// [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/new +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +#[derive(Clone, Debug, Trace, Finalize, PartialEq)] +pub struct New { + call: Call, +} + +impl New { + /// Gets the name of the function call. + pub fn expr(&self) -> &Node { + &self.call.expr() + } + + /// Retrieves the arguments passed to the function. + pub fn args(&self) -> &[Node] { + &self.call.args() + } +} + +impl From for New { + fn from(call: Call) -> Self { + Self { call } + } +} + +impl fmt::Display for New { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "new {}", self.call) + } +} + +impl From for Node { + fn from(new: New) -> Self { + Self::New(new) + } +} diff --git a/boa/src/syntax/ast/node/identifier.rs b/boa/src/syntax/ast/node/identifier.rs new file mode 100644 index 0000000000..80c8cfeb7d --- /dev/null +++ b/boa/src/syntax/ast/node/identifier.rs @@ -0,0 +1,58 @@ +//! Local identifier node. + +use super::Node; +use gc::{Finalize, Trace}; +use std::fmt; + +#[cfg(feature = "serde")] +use serde::{Deserialize, Serialize}; + +/// An `identifier` is a sequence of characters in the code that identifies a variable, +/// function, or property. +/// +/// In JavaScript, identifiers are case-sensitive and can contain Unicode letters, $, _, and +/// digits (0-9), but may not start with a digit. +/// +/// An identifier differs from a string in that a string is data, while an identifier is part +/// of the code. In JavaScript, there is no way to convert identifiers to strings, but +/// sometimes it is possible to parse strings into identifiers. +/// +/// More information: +/// - [ECMAScript reference][spec] +/// - [MDN documentation][mdn] +/// +/// [spec]: https://tc39.es/ecma262/#prod-Identifier +/// [mdn]: https://developer.mozilla.org/en-US/docs/Glossary/Identifier +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +#[cfg_attr(feature = "serde", serde(transparent))] +#[derive(Clone, Debug, Trace, Finalize, PartialEq)] +pub struct Identifier { + ident: Box, +} + +impl fmt::Display for Identifier { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + fmt::Display::fmt(&self.ident, f) + } +} + +impl AsRef for Identifier { + fn as_ref(&self) -> &str { + &self.ident + } +} + +impl From for Identifier +where + T: Into>, +{ + fn from(stm: T) -> Self { + Self { ident: stm.into() } + } +} + +impl From for Node { + fn from(local: Identifier) -> Self { + Self::Identifier(local) + } +} diff --git a/boa/src/syntax/ast/node/iteration.rs b/boa/src/syntax/ast/node/iteration.rs new file mode 100644 index 0000000000..c1ec55c60a --- /dev/null +++ b/boa/src/syntax/ast/node/iteration.rs @@ -0,0 +1,139 @@ +use super::Node; +use gc::{Finalize, Trace}; +use std::fmt; + +#[cfg(feature = "serde")] +use serde::{Deserialize, Serialize}; + +/// The `for` statement creates a loop that consists of three optional expressions. +/// +/// A `for` loop repeats until a specified condition evaluates to `false`. +/// The JavaScript for loop is similar to the Java and C for loop. +/// +/// More information: +/// - [ECMAScript reference][spec] +/// - [MDN documentation][mdn] +/// +/// [spec]: https://tc39.es/ecma262/#prod-ForDeclaration +/// [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/for +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +#[derive(Clone, Debug, Trace, Finalize, PartialEq)] +pub struct ForLoop { + #[cfg_attr(feature = "serde", serde(flatten))] + inner: Box, +} + +impl ForLoop { + /// Creates a new for loop AST node. + pub(in crate::syntax) fn new(init: I, condition: C, final_expr: E, body: B) -> Self + where + I: Into>, + C: Into>, + E: Into>, + B: Into, + { + Self { + inner: Box::new(InnerForLoop::new(init, condition, final_expr, body)), + } + } + + /// Gets the initialization node. + pub fn init(&self) -> Option<&Node> { + self.inner.init() + } + + /// Gets the loop condition node. + pub fn condition(&self) -> Option<&Node> { + self.inner.condition() + } + + /// Gets the final expression node. + pub fn final_expr(&self) -> Option<&Node> { + self.inner.final_expr() + } + + /// Gets the body of the for loop. + pub fn body(&self) -> &Node { + self.inner.body() + } + + pub(super) fn display(&self, f: &mut fmt::Formatter<'_>, indentation: usize) -> fmt::Result { + f.write_str("for (")?; + if let Some(init) = self.init() { + fmt::Display::fmt(init, f)?; + } + f.write_str(";")?; + if let Some(condition) = self.condition() { + fmt::Display::fmt(condition, f)?; + } + f.write_str(";")?; + if let Some(final_expr) = self.final_expr() { + fmt::Display::fmt(final_expr, f)?; + } + writeln!(f, ") {{")?; + + self.inner.body().display(f, indentation + 1)?; + + write!(f, "}}") + } +} + +impl fmt::Display for ForLoop { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + self.display(f, 0) + } +} + +impl From for Node { + fn from(for_loop: ForLoop) -> Self { + Self::ForLoop(for_loop) + } +} + +/// Inner structure to avoid multiple indirections in the heap. +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +#[derive(Clone, Debug, Trace, Finalize, PartialEq)] +struct InnerForLoop { + init: Option, + condition: Option, + final_expr: Option, + body: Node, +} + +impl InnerForLoop { + /// Creates a new inner for loop. + fn new(init: I, condition: C, final_expr: E, body: B) -> Self + where + I: Into>, + C: Into>, + E: Into>, + B: Into, + { + Self { + init: init.into(), + condition: condition.into(), + final_expr: final_expr.into(), + body: body.into(), + } + } + + /// Gets the initialization node. + fn init(&self) -> Option<&Node> { + self.init.as_ref() + } + + /// Gets the loop condition node. + fn condition(&self) -> Option<&Node> { + self.condition.as_ref() + } + + /// Gets the final expression node. + fn final_expr(&self) -> Option<&Node> { + self.final_expr.as_ref() + } + + /// Gets the body of the for loop. + fn body(&self) -> &Node { + &self.body + } +} diff --git a/boa/src/syntax/ast/node.rs b/boa/src/syntax/ast/node/mod.rs similarity index 52% rename from boa/src/syntax/ast/node.rs rename to boa/src/syntax/ast/node/mod.rs index dc27bde520..536685ddf1 100644 --- a/boa/src/syntax/ast/node.rs +++ b/boa/src/syntax/ast/node/mod.rs @@ -1,89 +1,56 @@ //! This module implements the `Node` structure, which composes the AST. -use crate::syntax::ast::{ - constant::Const, - op::{BinOp, Operator, UnaryOp}, +pub mod array; +pub mod block; +pub mod declaration; +pub mod expression; +pub mod identifier; +pub mod iteration; +pub mod operator; +pub mod statement_list; +pub mod try_node; + +pub use self::{ + array::ArrayDecl, + block::Block, + declaration::{ + ArrowFunctionDecl, ConstDecl, ConstDeclList, FunctionDecl, FunctionExpr, LetDecl, + LetDeclList, VarDecl, VarDeclList, + }, + expression::{Call, New}, + identifier::Identifier, + iteration::ForLoop, + operator::{Assign, BinOp, UnaryOp}, + statement_list::StatementList, + try_node::{Catch, Finally, Try}, }; +use super::Const; use gc::{Finalize, Trace}; -use std::fmt; +use std::{ + cmp::Ordering, + fmt::{self, Display}, +}; #[cfg(feature = "serde")] use serde::{Deserialize, Serialize}; -/// A Javascript AST Node. #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] #[derive(Clone, Debug, Trace, Finalize, PartialEq)] pub enum Node { - /// An array is an ordered collection of data (either primitive or object depending upon the - /// language). - /// - /// Arrays are used to store multiple values in a single variable. - /// This is compared to a variable that can store only one value. - /// - /// Each item in an array has a number attached to it, called a numeric index, that allows you - /// to access it. In JavaScript, arrays start at index zero and can be manipulated with various - /// methods. - /// - /// More information: - /// - [ECMAScript reference][spec] - /// - [MDN documentation][mdn] - /// - /// [spec]: https://tc39.es/ecma262/#prod-ArrayLiteral - /// [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array - ArrayDecl(Box<[Node]>), + /// Array declaration node. [More information](./array/struct.ArrayDecl.html). + ArrayDecl(ArrayDecl), - /// An arrow function expression is a syntactically compact alternative to a regular function - /// expression. - /// - /// Arrow function expressions are ill suited as methods, and they cannot be used as - /// constructors. Arrow functions cannot be used as constructors and will throw an error when - /// used with new. - /// - /// More information: - /// - [ECMAScript reference][spec] - /// - [MDN documentation][mdn] - /// - /// [spec]: https://tc39.es/ecma262/#prod-ArrowFunction - /// [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Functions/Arrow_functions - ArrowFunctionDecl(Box<[FormalParameter]>, Box), + /// An arrow function expression node. [More information](./arrow_function/struct.ArrowFunctionDecl.html). + ArrowFunctionDecl(ArrowFunctionDecl), - /// An assignment operator assigns a value to its left operand based on the value of its right - /// operand. - /// - /// Assignment operator (`=`), assigns the value of its right operand to its left operand. - /// - /// More information: - /// - [ECMAScript reference][spec] - /// - [MDN documentation][mdn] - /// - /// [spec]: https://tc39.es/ecma262/#prod-AssignmentExpression - /// [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/Assignment_Operators - Assign(Box, Box), + /// An assignment operator node. [More information](./operator/struct.Assign.html). + Assign(Assign), - /// Binary operators requires two operands, one before the operator and one after the operator. - /// - /// More information: - /// - [MDN documentation][mdn] - /// - /// [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Expressions_and_Operators#Operators - BinOp(BinOp, Box, Box), + /// A binary operator node. [More information](./operator/struct.BinOp.html). + BinOp(BinOp), - /// A `block` statement (or compound statement in other languages) is used to group zero or - /// more statements. - /// - /// The block statement is often called compound statement in other languages. - /// It allows you to use multiple statements where JavaScript expects only one statement. - /// Combining statements into blocks is a common practice in JavaScript. The opposite behavior - /// is possible using an empty statement, where you provide no statement, although one is - /// required. - /// - /// More information: - /// - [ECMAScript reference][spec] - /// - [MDN documentation][mdn] - /// - /// [spec]: https://tc39.es/ecma262/#prod-BlockStatement - /// [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/block - Block(Box<[Node]>), + /// A Block node. [More information](./block/struct.Block.html). + Block(Block), /// The `break` statement terminates the current loop, switch, or label statement and transfers /// program control to the statement following the terminated statement. @@ -99,23 +66,10 @@ pub enum Node { /// /// [spec]: https://tc39.es/ecma262/#prod-BreakStatement /// [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/break - Break(Option), + Break(Option>), - /// Calling the function actually performs the specified actions with the indicated parameters. - /// - /// Defining a function does not execute it. Defining it simply names the function and - /// specifies what to do when the function is called. Functions must be in scope when they are - /// called, but the function declaration can be hoisted. The scope of a function is the - /// function in which it is declared (or the entire program, if it is declared at the top - /// level). - /// - /// More information: - /// - [ECMAScript reference][spec] - /// - [MDN documentation][mdn] - /// - /// [spec]: https://tc39.es/ecma262/#prod-CallExpression - /// [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Functions#Calling_functions - Call(Box, Box<[Node]>), + /// A function call. [More information](./expression/struct.Call.html). + Call(Call), /// The `conditional` (ternary) operator is the only JavaScript operator that takes three /// operands. @@ -145,25 +99,8 @@ pub enum Node { /// [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Grammar_and_types#Literals Const(Const), - /// The `const` statements are block-scoped, much like variables defined using the `let` - /// keyword. - /// - /// This declaration creates a constant whose scope can be either global or local to the block - /// in which it is declared. Global constants do not become properties of the window object, - /// unlike var variables. - /// - /// An initializer for a constant is required. You must specify its value in the same statement - /// in which it's declared. (This makes sense, given that it can't be changed later.) - /// - /// More information: - /// - [ECMAScript reference][spec] - /// - [MDN documentation][mdn] - /// - /// [spec]: https://tc39.es/ecma262/#sec-let-and-const-declarations - /// [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/const - /// [identifier]: https://developer.mozilla.org/en-US/docs/Glossary/identifier - /// [expression]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Expressions_and_Operators#Expressions - ConstDecl(Box<[(String, Node)]>), + /// A constant declaration list. [More information](./declaration/struct.ConstDeclList.html). + ConstDeclList(ConstDeclList), /// The `continue` statement terminates execution of the statements in the current iteration of /// the current or labeled loop, and continues execution of the loop with the next iteration. @@ -178,7 +115,7 @@ pub enum Node { /// /// [spec]: https://tc39.es/ecma262/#prod-ContinueStatement /// [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/continue - Continue(Option), + Continue(Option>), /// The `do...while` statement creates a loop that executes a specified statement until the /// test condition evaluates to false. @@ -194,42 +131,11 @@ pub enum Node { /// [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/do...while DoWhileLoop(Box, Box), - /// The `function` declaration (function statement) defines a function with the specified - /// parameters. - /// - /// A function created with a function declaration is a `Function` object and has all the - /// properties, methods and behavior of `Function`. - /// - /// A function can also be created using an expression (see function expression). - /// - /// By default, functions return `undefined`. To return any other value, the function must have - /// a return statement that specifies the value to return. - /// - /// More information: - /// - [ECMAScript reference][spec] - /// - [MDN documentation][mdn] - /// - /// [spec]: https://tc39.es/ecma262/#sec-terms-and-definitions-function - /// [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/function - FunctionDecl(String, Box<[FormalParameter]>, Box), + /// A function declaration node. [More information](./declaration/struct.FunctionDecl.html). + FunctionDecl(FunctionDecl), - /// The `function` expression defines a function with the specified parameters. - /// - /// A function created with a function expression is a `Function` object and has all the - /// properties, methods and behavior of `Function`. - /// - /// A function can also be created using a declaration (see function expression). - /// - /// By default, functions return `undefined`. To return any other value, the function must have - /// a return statement that specifies the value to return. - /// - /// More information: - /// - [ECMAScript reference][spec] - /// - [MDN documentation][mdn] - /// - /// [spec]: https://tc39.es/ecma262/#sec-terms-and-definitions-function - /// [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/function - FunctionExpr(Option, Box<[FormalParameter]>, Box), + /// A function expressino node. [More information](./declaration/struct.FunctionExpr.html) + FunctionExpr(FunctionExpr), /// This property accessor provides access to an object's properties by using the /// [dot notation][mdn]. @@ -252,7 +158,7 @@ pub enum Node { /// /// [spec]: https://tc39.es/ecma262/#sec-property-accessors /// [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/Property_accessors#Dot_notation - GetConstField(Box, String), + GetConstField(Box, Box), /// This property accessor provides access to an object's properties by using the /// [bracket notation][mdn]. @@ -278,23 +184,8 @@ pub enum Node { /// [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/Property_accessors#Bracket_notation GetField(Box, Box), - /// The `for` statement creates a loop that consists of three optional expressions. - /// - /// A `for` loop repeats until a specified condition evaluates to `false`. - /// The JavaScript for loop is similar to the Java and C for loop. - /// - /// More information: - /// - [ECMAScript reference][spec] - /// - [MDN documentation][mdn] - /// - /// [spec]: https://tc39.es/ecma262/#prod-ForDeclaration - /// [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/for - ForLoop( - Option>, - Option>, - Option>, - Box, - ), + /// A `for` statement. [More information](./iteration.struct.ForLoop.html). + ForLoop(ForLoop), /// The `if` statement executes a statement if a specified condition is [`truthy`][truthy]. If /// the condition is [`falsy`][falsy], another statement can be executed. @@ -314,59 +205,14 @@ pub enum Node { /// [expression]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Expressions_and_Operators#Expressions If(Box, Box, Option>), - /// The `let` statement declares a block scope local variable, optionally initializing it to a - /// value. - /// - /// - /// `let` allows you to declare variables that are limited to a scope of a block statement, or - /// expression on which it is used, unlike the `var` keyword, which defines a variable - /// globally, or locally to an entire function regardless of block scope. - /// - /// Just like const the `let` does not create properties of the window object when declared - /// globally (in the top-most scope). - /// - /// More information: - /// - [ECMAScript reference][spec] - /// - [MDN documentation][mdn] - /// - /// [spec]: https://tc39.es/ecma262/#sec-let-and-const-declarations - /// [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/let - LetDecl(Box<[(String, Option)]>), + /// A `let` declaration list. [More information](./declaration/struct.LetDeclList.html). + LetDeclList(LetDeclList), - /// An `identifier` is a sequence of characters in the code that identifies a variable, - /// function, or property. - /// - /// In JavaScript, identifiers are case-sensitive and can contain Unicode letters, $, _, and - /// digits (0-9), but may not start with a digit. - /// - /// An identifier differs from a string in that a string is data, while an identifier is part - /// of the code. In JavaScript, there is no way to convert identifiers to strings, but - /// sometimes it is possible to parse strings into identifiers. - /// - /// More information: - /// - [ECMAScript reference][spec] - /// - [MDN documentation][mdn] - /// - /// [spec]: https://tc39.es/ecma262/#prod-Identifier - /// [mdn]: https://developer.mozilla.org/en-US/docs/Glossary/Identifier - Local(String), + /// A local identifier node. [More information](./identifier/struct.Identifier.html). + Identifier(Identifier), - /// The `new` operator lets developers create an instance of a user-defined object type or of - /// one of the built-in object types that has a constructor function. - /// - /// The new keyword does the following things: - /// - Creates a blank, plain JavaScript object; - /// - Links (sets the constructor of) this object to another object; - /// - Passes the newly created object from Step 1 as the this context; - /// - Returns this if the function doesn't return its own object. - /// - /// More information: - /// - [ECMAScript reference][spec] - /// - [MDN documentation][mdn] - /// - /// [spec]: https://tc39.es/ecma262/#prod-NewExpression - /// [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/new - New(Box), + /// A `new` expression. [More information](./expression/struct.New.html). + New(New), /// Objects in JavaScript may be defined as an unordered collection of related data, of /// primitive or reference types, in the form of “key: value” pairs. @@ -445,14 +291,6 @@ pub enum Node { /// [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/Spread_syntax Spread(Box), - /// Similar to `Node::Block` but without the braces - /// - /// More information: - /// - [ECMAScript reference][spec] - /// - /// [spec]: https://tc39.es/ecma262/#prod-StatementList - StatementList(Box<[Node]>), - /// The `throw` statement throws a user-defined exception. /// /// Syntax: `throw expression;` @@ -469,25 +307,8 @@ pub enum Node { /// [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/throw Throw(Box), - /// The `try...catch` statement marks a block of statements to try and specifies a response - /// should an exception be thrown. - /// - /// The `try` statement consists of a `try`-block, which contains one or more statements. `{}` - /// must always be used, even for single statements. At least one `catch`-block, or a - /// `finally`-block, must be present. - /// - /// More information: - /// - [ECMAScript reference][spec] - /// - [MDN documentation][mdn] - /// - /// [spec]: https://tc39.es/ecma262/#prod-TryStatement - /// [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/try...catch - Try( - Box, - Option>, - Option>, - Option>, - ), + /// A `try...catch` node. [More information](./try_node/struct.Try.htl). + Try(Try), /// The JavaScript `this` keyword refers to the object it belongs to. /// @@ -503,35 +324,11 @@ pub enum Node { /// [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/this This, - /// A unary operation is an operation with only one operand. - /// - /// More information: - /// - [ECMAScript reference][spec] - /// - [MDN documentation][mdn] - /// - /// [spec]: https://tc39.es/ecma262/#prod-UnaryExpression - /// [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Expressions_and_Operators#Unary_operators - UnaryOp(UnaryOp, Box), + /// Unary operation node. [More information](./operator/struct.UnaryOp.html) + UnaryOp(UnaryOp), - /// The `var` statement declares a variable, optionally initializing it to a value. - /// - /// var declarations, wherever they occur, are processed before any code is executed. This is - /// called hoisting, and is discussed further below. - /// - /// The scope of a variable declared with var is its current execution context, which is either - /// the enclosing function or, for variables declared outside any function, global. If you - /// re-declare a JavaScript variable, it will not lose its value. - /// - /// Assigning a value to an undeclared variable implicitly creates it as a global variable (it - /// becomes a property of the global object) when the assignment is executed. - /// - /// More information: - /// - [ECMAScript reference][spec] - /// - [MDN documentation][mdn] - /// - /// [spec]: https://tc39.es/ecma262/#prod-VariableStatement - /// [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/var - VarDecl(Box<[(String, Option)]>), + /// Array declaration node. [More information](./declaration/struct.VarDeclList.html). + VarDeclList(VarDeclList), /// The `while` statement creates a loop that executes a specified statement as long as the /// test condition evaluates to `true`. @@ -547,134 +344,57 @@ pub enum Node { WhileLoop(Box, Box), } -impl Operator for Node { - fn get_assoc(&self) -> bool { - match *self { - Self::UnaryOp(_, _) | Self::If(_, _, _) | Self::Assign(_, _) => false, - _ => true, - } - } - - fn get_precedence(&self) -> u64 { - match self { - Self::GetField(_, _) | Self::GetConstField(_, _) => 1, - Self::Call(_, _) => 2, - Self::UnaryOp(UnaryOp::IncrementPost, _) - | Self::UnaryOp(UnaryOp::IncrementPre, _) - | Self::UnaryOp(UnaryOp::DecrementPost, _) - | Self::UnaryOp(UnaryOp::DecrementPre, _) => 3, - Self::UnaryOp(UnaryOp::Not, _) - | Self::UnaryOp(UnaryOp::Tilde, _) - | Self::UnaryOp(UnaryOp::Minus, _) - | Self::UnaryOp(UnaryOp::TypeOf, _) => 4, - Self::BinOp(op, _, _) => op.get_precedence(), - Self::If(_, _, _) => 15, - // 16 should be yield - Self::Assign(_, _) => 17, - _ => 19, - } - } -} - -impl fmt::Display for Node { +impl Display for Node { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { self.display(f, 0) } } -impl Node { - /// Creates an `ArrayDecl` AST node. - pub fn array_decl(nodes: N) -> Self - where - N: Into>, - { - Self::ArrayDecl(nodes.into()) - } - - /// Creates an `ArraowFunctionDecl` AST node. - pub fn arrow_function_decl(params: P, body: B) -> Self - where - P: Into>, - B: Into>, - { - Self::ArrowFunctionDecl(params.into(), body.into()) - } - - /// Creates an `Assign` AST node. - pub fn assign(lhs: L, rhs: R) -> Self - where - L: Into>, - R: Into>, - { - Self::Assign(lhs.into(), rhs.into()) - } - - /// Creates a `BinOp` AST node. - pub fn bin_op(op: O, lhs: L, rhs: R) -> Self - where - O: Into, - L: Into>, - R: Into>, - { - Self::BinOp(op.into(), lhs.into(), rhs.into()) +impl From for Node { + fn from(c: Const) -> Self { + Self::Const(c) } +} - /// Creates a `Block` AST node. - pub fn block(nodes: N) -> Self - where - N: Into>, - { - Self::Block(nodes.into()) +impl Node { + /// Returns a node ordering based on the hoistability of each node. + pub(crate) fn hoistable_order(a: &Node, b: &Node) -> Ordering { + match (a, b) { + (Node::FunctionDecl(_), Node::FunctionDecl(_)) => Ordering::Equal, + (_, Node::FunctionDecl(_)) => Ordering::Greater, + (Node::FunctionDecl(_), _) => Ordering::Less, + + (_, _) => Ordering::Equal, + } } /// Creates a `Break` AST node. pub fn break_node(label: OL) -> Self where - L: Into, + L: Into>, OL: Into>, { Self::Break(label.into().map(L::into)) } - /// Creates a `Call` AST node. - pub fn call(function: F, params: P) -> Self - where - F: Into>, - P: Into>, - { - Self::Call(function.into(), params.into()) - } - /// Creates a `ConditionalOp` AST node. pub fn conditional_op(condition: C, if_true: T, if_false: F) -> Self where - C: Into>, - T: Into>, - F: Into>, + C: Into, + T: Into, + F: Into, { - Self::ConditionalOp(condition.into(), if_true.into(), if_false.into()) - } - - /// Creates a `Const` AST node. - pub fn const_node(node: C) -> Self - where - C: Into, - { - Self::Const(node.into()) - } - - /// Creates a `ConstDecl` AST node. - pub fn const_decl(decl: D) -> Self - where - D: Into>, - { - Self::ConstDecl(decl.into()) + Self::ConditionalOp( + Box::new(condition.into()), + Box::new(if_true.into()), + Box::new(if_false.into()), + ) } /// Creates a `Continue` AST node. pub fn continue_node(label: OL) -> Self where - L: Into, + L: Into>, OL: Into>, { Self::Continue(label.into().map(L::into)) @@ -683,103 +403,43 @@ impl Node { /// Creates a `DoWhileLoop` AST node. pub fn do_while_loop(body: B, condition: C) -> Self where - B: Into>, - C: Into>, - { - Self::DoWhileLoop(body.into(), condition.into()) - } - - /// Creates a `FunctionDecl` AST node. - pub fn function_decl(name: N, params: P, body: B) -> Self - where - N: Into, - P: Into>, - B: Into>, - { - Self::FunctionDecl(name.into(), params.into(), body.into()) - } - - /// Creates a `FunctionDecl` AST node. - pub fn function_expr(name: ON, params: P, body: B) -> Self - where - N: Into, - ON: Into>, - P: Into>, - B: Into>, + B: Into, + C: Into, { - Self::FunctionExpr(name.into().map(N::into), params.into(), body.into()) + Self::DoWhileLoop(Box::new(body.into()), Box::new(condition.into())) } /// Creates a `GetConstField` AST node. pub fn get_const_field(value: V, label: L) -> Self where - V: Into>, - L: Into, + V: Into, + L: Into>, { - Self::GetConstField(value.into(), label.into()) + Self::GetConstField(Box::new(value.into()), label.into()) } /// Creates a `GetField` AST node. pub fn get_field(value: V, field: F) -> Self where - V: Into>, - F: Into>, - { - Self::GetField(value.into(), field.into()) - } - - /// Creates a `ForLoop` AST node. - pub fn for_loop(init: OI, condition: OC, step: OS, body: B) -> Self - where - OI: Into>, - OC: Into>, - OS: Into>, - I: Into>, - C: Into>, - S: Into>, - B: Into>, + V: Into, + F: Into, { - Self::ForLoop( - init.into().map(I::into), - condition.into().map(C::into), - step.into().map(S::into), - body.into(), - ) + Self::GetField(Box::new(value.into()), Box::new(field.into())) } /// Creates an `If` AST node. pub fn if_node(condition: C, body: B, else_node: OE) -> Self where - C: Into>, - B: Into>, - E: Into>, + C: Into, + B: Into, + E: Into, OE: Into>, { - Self::If(condition.into(), body.into(), else_node.into().map(E::into)) - } - - /// Creates a `LetDecl` AST node. - pub fn let_decl(init: I) -> Self - where - I: Into)]>>, - { - Self::LetDecl(init.into()) - } - - /// Creates a `Local` AST node. - pub fn local(name: N) -> Self - where - N: Into, - { - Self::Local(name.into()) - } - - /// Creates a `New` AST node. - pub fn new(node: N) -> Self - where - N: Into>, - { - Self::New(node.into()) + Self::If( + Box::new(condition.into()), + Box::new(body.into()), + else_node.into().map(E::into).map(Box::new), + ) } /// Creates an `Object` AST node. @@ -793,67 +453,41 @@ impl Node { /// Creates a `Return` AST node. pub fn return_node(expr: OE) -> Self where - E: Into>, + E: Into, OE: Into>, { - Self::Return(expr.into().map(E::into)) + Self::Return(expr.into().map(E::into).map(Box::new)) } /// Creates a `Switch` AST node. pub fn switch(val: V, cases: C, default: OD) -> Self where - V: Into>, + V: Into, C: Into)]>>, OD: Into>, - D: Into>, + D: Into, { - Self::Switch(val.into(), cases.into(), default.into().map(D::into)) + Self::Switch( + Box::new(val.into()), + cases.into(), + default.into().map(D::into).map(Box::new), + ) } /// Creates a `Spread` AST node. pub fn spread(val: V) -> Self where - V: Into>, + V: Into, { - Self::Spread(val.into()) - } - - /// Creates a `StatementList` AST node. - pub fn statement_list(list: L) -> Self - where - L: Into>, - { - Self::StatementList(list.into()) + Self::Spread(Box::new(val.into())) } /// Creates a `Throw` AST node. pub fn throw(val: V) -> Self where - V: Into>, - { - Self::Throw(val.into()) - } - - /// Creates a `Try` AST node. - pub fn try_node(try_node: T, catch: OC, param: OP, finally: OF) -> Self - where - T: Into>, - OC: Into>, - OP: Into>, - OF: Into>, - C: Into>, - P: Into>, - F: Into>, + V: Into, { - let catch = catch.into().map(C::into); - let finally = finally.into().map(F::into); - - debug_assert!( - catch.is_some() || finally.is_some(), - "try/catch must have a catch or a finally block" - ); - - Self::Try(try_node.into(), catch, param.into().map(P::into), finally) + Self::Throw(Box::new(val.into())) } /// Creates a `This` AST node. @@ -861,29 +495,13 @@ impl Node { Self::This } - /// Creates a `UnaryOp` AST node. - pub fn unary_op(op: UnaryOp, val: V) -> Self - where - V: Into>, - { - Self::UnaryOp(op, val.into()) - } - - /// Creates a `VarDecl` AST node. - pub fn var_decl(init: I) -> Self - where - I: Into)]>>, - { - Self::VarDecl(init.into()) - } - /// Creates a `WhileLoop` AST node. pub fn while_loop(condition: C, body: B) -> Self where - C: Into>, - B: Into>, + C: Into, + B: Into, { - Self::WhileLoop(condition.into(), body.into()) + Self::WhileLoop(Box::new(condition.into()), Box::new(body.into())) } /// Implements the display formatting with indentation. @@ -899,9 +517,9 @@ impl Node { Self::ConditionalOp(ref cond, ref if_true, ref if_false) => { write!(f, "{} ? {} : {}", cond, if_true, if_false) } - Self::ForLoop(_, _, _, _) => write!(f, "for loop"), // TODO + Self::ForLoop(ref for_loop) => for_loop.display(f, indentation), Self::This => write!(f, "this"), - Self::Try(_, _, _, _) => write!(f, "try/catch/finally"), // TODO + Self::Try(ref try_catch) => try_catch.display(f, indentation), Self::Break(ref l) => write!( f, "break{}", @@ -921,67 +539,12 @@ impl Node { } ), Self::Spread(ref node) => write!(f, "...{}", node), - Self::Block(ref block) => { - writeln!(f, "{{")?; - for node in block.iter() { - node.display(f, indentation + 1)?; - - match node { - Self::Block(_) - | Self::If(_, _, _) - | Self::Switch(_, _, _) - | Self::FunctionDecl(_, _, _) - | Self::WhileLoop(_, _) - | Self::StatementList(_) => {} - _ => write!(f, ";")?, - } - writeln!(f)?; - } - write!(f, "{}}}", indent) - } - Self::StatementList(ref list) => { - for node in list.iter() { - node.display(f, indentation + 1)?; - - match node { - Self::Block(_) - | Self::If(_, _, _) - | Self::Switch(_, _, _) - | Self::FunctionDecl(_, _, _) - | Self::WhileLoop(_, _) - | Self::StatementList(_) => {} - _ => write!(f, ";")?, - } - writeln!(f)?; - } - Ok(()) - } - Self::Local(ref s) => write!(f, "{}", s), + Self::Block(ref block) => block.display(f, indentation), + Self::Identifier(ref s) => Display::fmt(s, f), Self::GetConstField(ref ex, ref field) => write!(f, "{}.{}", ex, field), Self::GetField(ref ex, ref field) => write!(f, "{}[{}]", ex, field), - Self::Call(ref ex, ref args) => { - write!(f, "{}(", ex)?; - let arg_strs: Box<[String]> = args.iter().map(ToString::to_string).collect(); - write!(f, "{})", arg_strs.join(", ")) - } - Self::New(ref call) => { - let (func, args) = match call.as_ref() { - Self::Call(func, args) => (func, args), - _ => unreachable!("Node::New(ref call): 'call' must only be Node::Call type."), - }; - - write!(f, "new {}", func)?; - f.write_str("(")?; - let mut first = true; - for e in args.iter() { - if !first { - f.write_str(", ")?; - } - first = false; - write!(f, "{}", e)?; - } - f.write_str(")") - } + Self::Call(ref expr) => Display::fmt(expr, f), + Self::New(ref expr) => Display::fmt(expr, f), Self::WhileLoop(ref cond, ref node) => { write!(f, "while ({}) ", cond)?; node.display(f, indentation) @@ -1040,60 +603,19 @@ impl Node { } f.write_str("}") } - Self::ArrayDecl(ref arr) => { - f.write_str("[")?; - join_nodes(f, arr)?; - f.write_str("]") - } - Self::FunctionDecl(ref name, ref _args, ref node) => { - write!(f, "function {} {{", name)?; - //join_nodes(f, args)?; TODO: port - f.write_str("} ")?; - node.display(f, indentation + 1) - } - Self::FunctionExpr(ref name, ref args, ref node) => { - write!(f, "function ")?; - if let Some(func_name) = name { - write!(f, "{}", func_name)?; - } - write!(f, "{{")?; - join_nodes(f, args)?; - f.write_str("} ")?; - node.display(f, indentation + 1) - } - Self::ArrowFunctionDecl(ref args, ref node) => { - write!(f, "(")?; - join_nodes(f, args)?; - f.write_str(") => ")?; - node.display(f, indentation) - } - Self::BinOp(ref op, ref a, ref b) => write!(f, "{} {} {}", a, op, b), - Self::UnaryOp(ref op, ref a) => write!(f, "{}{}", op, a), + Self::ArrayDecl(ref arr) => Display::fmt(arr, f), + Self::VarDeclList(ref list) => Display::fmt(list, f), + Self::FunctionDecl(ref decl) => decl.display(f, indentation), + Self::FunctionExpr(ref expr) => expr.display(f, indentation), + Self::ArrowFunctionDecl(ref decl) => decl.display(f, indentation), + Self::BinOp(ref op) => Display::fmt(op, f), + Self::UnaryOp(ref op) => Display::fmt(op, f), Self::Return(Some(ref ex)) => write!(f, "return {}", ex), Self::Return(None) => write!(f, "return"), Self::Throw(ref ex) => write!(f, "throw {}", ex), - Self::Assign(ref ref_e, ref val) => write!(f, "{} = {}", ref_e, val), - Self::VarDecl(ref vars) | Self::LetDecl(ref vars) => { - if let Self::VarDecl(_) = *self { - f.write_str("var ")?; - } else { - f.write_str("let ")?; - } - for (key, val) in vars.iter() { - match val { - Some(x) => write!(f, "{} = {}", key, x)?, - None => write!(f, "{}", key)?, - } - } - Ok(()) - } - Self::ConstDecl(ref vars) => { - f.write_str("const ")?; - for (key, val) in vars.iter() { - write!(f, "{} = {}", key, val)? - } - Ok(()) - } + Self::Assign(ref op) => Display::fmt(op, f), + Self::LetDeclList(ref decl) => Display::fmt(decl, f), + Self::ConstDeclList(ref decl) => Display::fmt(decl, f), } } } @@ -1101,7 +623,7 @@ impl Node { /// Utility to join multiple Nodes into a single string. fn join_nodes(f: &mut fmt::Formatter<'_>, nodes: &[N]) -> fmt::Result where - N: fmt::Display, + N: Display, { let mut first = true; for e in nodes { @@ -1109,7 +631,7 @@ where f.write_str(", ")?; } first = false; - write!(f, "{}", e)?; + Display::fmt(e, f)?; } Ok(()) } @@ -1119,7 +641,7 @@ where /// In the declaration of a function, the parameters must be identifiers, /// not any value like numbers, strings, or objects. ///```text -///function foo(formalParametar1, formalParametar2) { +///function foo(formalParameter1, formalParameter2) { ///} ///``` /// @@ -1132,15 +654,16 @@ where #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] #[derive(Clone, Debug, PartialEq, Trace, Finalize)] pub struct FormalParameter { - pub name: String, - pub init: Option>, - pub is_rest_param: bool, + name: Box, + init: Option, + is_rest_param: bool, } impl FormalParameter { - pub fn new(name: N, init: Option>, is_rest_param: bool) -> Self + /// Creates a new formal parameter. + pub(in crate::syntax) fn new(name: N, init: Option, is_rest_param: bool) -> Self where - N: Into, + N: Into>, { Self { name: name.into(), @@ -1148,9 +671,24 @@ impl FormalParameter { is_rest_param, } } + + /// Gets the name of the formal parameter. + pub fn name(&self) -> &str { + &self.name + } + + /// Gets the initialization node of the formal parameter, if any. + pub fn init(&self) -> Option<&Node> { + self.init.as_ref() + } + + /// Gets wether the parameter is a rest parameter. + pub fn is_rest_param(&self) -> bool { + self.is_rest_param + } } -impl fmt::Display for FormalParameter { +impl Display for FormalParameter { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { if self.is_rest_param { write!(f, "...")?; @@ -1187,7 +725,7 @@ pub enum PropertyDefinition { /// /// [spec]: https://tc39.es/ecma262/#prod-IdentifierReference /// [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/Object_initializer#Property_definitions - IdentifierReference(String), + IdentifierReference(Box), /// Binds a property name to a JavaScript value. /// @@ -1197,7 +735,7 @@ pub enum PropertyDefinition { /// /// [spec]: https://tc39.es/ecma262/#prod-PropertyDefinition /// [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/Object_initializer#Property_definitions - Property(String, Node), + Property(Box, Node), /// A property of an object can also refer to a function or a getter or setter method. /// @@ -1207,7 +745,7 @@ pub enum PropertyDefinition { /// /// [spec]: https://tc39.es/ecma262/#prod-MethodDefinition /// [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/Object_initializer#Method_definitions - MethodDefinition(MethodDefinitionKind, String, Node), + MethodDefinition(MethodDefinitionKind, Box, FunctionExpr), /// The Rest/Spread Properties for ECMAScript proposal (stage 4) adds spread properties to object literals. /// It copies own enumerable properties from a provided object onto a new object. @@ -1227,7 +765,7 @@ impl PropertyDefinition { /// Creates an `IdentifierReference` property definition. pub fn identifier_reference(ident: I) -> Self where - I: Into, + I: Into>, { Self::IdentifierReference(ident.into()) } @@ -1235,19 +773,18 @@ impl PropertyDefinition { /// Creates a `Property` definition. pub fn property(name: N, value: V) -> Self where - N: Into, + N: Into>, V: Into, { Self::Property(name.into(), value.into()) } /// Creates a `MethodDefinition`. - pub fn method_definition(kind: MethodDefinitionKind, name: N, body: B) -> Self + pub fn method_definition(kind: MethodDefinitionKind, name: N, body: FunctionExpr) -> Self where - N: Into, - B: Into, + N: Into>, { - Self::MethodDefinition(kind, name.into(), body.into()) + Self::MethodDefinition(kind, name.into(), body) } /// Creates a `SpreadObject`. diff --git a/boa/src/syntax/ast/node/operator.rs b/boa/src/syntax/ast/node/operator.rs new file mode 100644 index 0000000000..8725b82ba2 --- /dev/null +++ b/boa/src/syntax/ast/node/operator.rs @@ -0,0 +1,168 @@ +use super::Node; +use crate::syntax::ast::op; +use gc::{Finalize, Trace}; +use std::fmt; + +#[cfg(feature = "serde")] +use serde::{Deserialize, Serialize}; + +/// An assignment operator assigns a value to its left operand based on the value of its right +/// operand. +/// +/// Assignment operator (`=`), assigns the value of its right operand to its left operand. +/// +/// More information: +/// - [ECMAScript reference][spec] +/// - [MDN documentation][mdn] +/// +/// [spec]: https://tc39.es/ecma262/#prod-AssignmentExpression +/// [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/Assignment_Operators +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +#[derive(Clone, Debug, Trace, Finalize, PartialEq)] +pub struct Assign { + lhs: Box, + rhs: Box, +} + +impl Assign { + /// Creates an `Assign` AST node. + pub(in crate::syntax) fn new(lhs: L, rhs: R) -> Self + where + L: Into, + R: Into, + { + Self { + lhs: Box::new(lhs.into()), + rhs: Box::new(rhs.into()), + } + } + + /// Gets the left hand side of the assignment operation. + pub fn lhs(&self) -> &Node { + &self.lhs + } + + /// Gets the right hand side of the assignment operation. + pub fn rhs(&self) -> &Node { + &self.rhs + } +} + +impl fmt::Display for Assign { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "{} = {}", self.lhs, self.rhs) + } +} + +impl From for Node { + fn from(op: Assign) -> Self { + Self::Assign(op) + } +} + +/// Binary operators requires two operands, one before the operator and one after the operator. +/// +/// More information: +/// - [MDN documentation][mdn] +/// +/// [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Expressions_and_Operators#Operators +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +#[derive(Clone, Debug, Trace, Finalize, PartialEq)] +pub struct BinOp { + op: op::BinOp, + lhs: Box, + rhs: Box, +} + +impl BinOp { + /// Creates a `BinOp` AST node. + pub(in crate::syntax) fn new(op: O, lhs: L, rhs: R) -> Self + where + O: Into, + L: Into, + R: Into, + { + Self { + op: op.into(), + lhs: Box::new(lhs.into()), + rhs: Box::new(rhs.into()), + } + } + + /// Gets the binary operation of the node. + pub fn op(&self) -> op::BinOp { + self.op + } + + /// Gets the left hand side of the binary operation. + pub fn lhs(&self) -> &Node { + &self.lhs + } + + /// Gets the right hand side of the binary operation. + pub fn rhs(&self) -> &Node { + &self.rhs + } +} + +impl fmt::Display for BinOp { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "{} {} {}", self.lhs, self.op, self.rhs) + } +} + +impl From for Node { + fn from(op: BinOp) -> Self { + Self::BinOp(op) + } +} + +/// A unary operation is an operation with only one operand. +/// +/// More information: +/// - [ECMAScript reference][spec] +/// - [MDN documentation][mdn] +/// +/// [spec]: https://tc39.es/ecma262/#prod-UnaryExpression +/// [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Expressions_and_Operators#Unary_operators +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +#[derive(Clone, Debug, Trace, Finalize, PartialEq)] +pub struct UnaryOp { + op: op::UnaryOp, + target: Box, +} + +impl UnaryOp { + /// Creates a new `UnaryOp` AST node. + pub(in crate::syntax) fn new(op: op::UnaryOp, target: V) -> Self + where + V: Into, + { + Self { + op, + target: Box::new(target.into()), + } + } + + /// Gets the unary operation of the node. + pub fn op(&self) -> op::UnaryOp { + self.op + } + + /// Gets the target of this unary operator. + pub fn target(&self) -> &Node { + self.target.as_ref() + } +} + +impl fmt::Display for UnaryOp { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "{}{}", self.op, self.target) + } +} + +impl From for Node { + fn from(op: UnaryOp) -> Self { + Self::UnaryOp(op) + } +} diff --git a/boa/src/syntax/ast/node/statement_list.rs b/boa/src/syntax/ast/node/statement_list.rs new file mode 100644 index 0000000000..49404a01ca --- /dev/null +++ b/boa/src/syntax/ast/node/statement_list.rs @@ -0,0 +1,67 @@ +//! Statement list node. + +use super::Node; +use gc::{Finalize, Trace}; +use std::fmt; + +#[cfg(feature = "serde")] +use serde::{Deserialize, Serialize}; + +/// List of statements. +/// +/// Similar to `Node::Block` but without the braces. +/// +/// More information: +/// - [ECMAScript reference][spec] +/// +/// [spec]: https://tc39.es/ecma262/#prod-StatementList +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +#[derive(Clone, Debug, Trace, Finalize, PartialEq)] +pub struct StatementList { + #[cfg_attr(feature = "serde", serde(flatten))] + statements: Box<[Node]>, +} + +impl StatementList { + /// Gets the list of statements. + pub fn statements(&self) -> &[Node] { + &self.statements + } + + /// Implements the display formatting with indentation. + pub(super) fn display(&self, f: &mut fmt::Formatter<'_>, indentation: usize) -> fmt::Result { + let indent = " ".repeat(indentation); + // Print statements + for node in self.statements.iter() { + f.write_str(&indent)?; + node.display(f, indentation + 1)?; + + match node { + Node::Block(_) + | Node::If(_, _, _) + | Node::Switch(_, _, _) + | Node::WhileLoop(_, _) => {} + _ => write!(f, ";")?, + } + writeln!(f)?; + } + Ok(()) + } +} + +impl From for StatementList +where + T: Into>, +{ + fn from(stm: T) -> Self { + Self { + statements: stm.into(), + } + } +} + +impl fmt::Display for StatementList { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + self.display(f, 0) + } +} diff --git a/boa/src/syntax/ast/node/try_node.rs b/boa/src/syntax/ast/node/try_node.rs new file mode 100644 index 0000000000..125d9fe348 --- /dev/null +++ b/boa/src/syntax/ast/node/try_node.rs @@ -0,0 +1,172 @@ +use super::{Block, Identifier, Node}; +use gc::{Finalize, Trace}; +use std::fmt; + +#[cfg(feature = "serde")] +use serde::{Deserialize, Serialize}; + +/// The `try...catch` statement marks a block of statements to try and specifies a response +/// should an exception be thrown. +/// +/// The `try` statement consists of a `try`-block, which contains one or more statements. `{}` +/// must always be used, even for single statements. At least one `catch`-block, or a +/// `finally`-block, must be present. +/// +/// More information: +/// - [ECMAScript reference][spec] +/// - [MDN documentation][mdn] +/// +/// [spec]: https://tc39.es/ecma262/#prod-TryStatement +/// [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/try...catch +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +#[derive(Clone, Debug, Trace, Finalize, PartialEq)] +pub struct Try { + block: Block, + catch: Option, + finally: Option, +} + +impl Try { + /// Creates a new `Try` AST node. + pub(in crate::syntax) fn new( + block: B, + catch: Option, + finally: Option, + ) -> Self + where + B: Into, + { + assert!( + catch.is_some() || finally.is_some(), + "one of catch or finally must be pressent" + ); + + Self { + block: block.into(), + catch, + finally, + } + } + + /// Gets the `try` block. + pub fn block(&self) -> &Block { + &self.block + } + + /// Gets the `catch` block, if any. + pub fn catch(&self) -> Option<&Catch> { + self.catch.as_ref() + } + + /// Gets the `finally` block, if any. + pub fn finally(&self) -> Option<&Block> { + self.finally.as_ref().map(Finally::block) + } + + /// Implements the display formatting with indentation. + pub(super) fn display(&self, f: &mut fmt::Formatter<'_>, indentation: usize) -> fmt::Result { + write!(f, "{}try ", " ".repeat(indentation))?; + self.block.display(f, indentation)?; + + if let Some(ref catch) = self.catch { + catch.display(f, indentation)?; + } + + if let Some(ref finally) = self.finally { + finally.display(f, indentation)?; + } + Ok(()) + } +} + +impl fmt::Display for Try { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + self.display(f, 0) + } +} + +impl From for Node { + fn from(try_catch: Try) -> Self { + Self::Try(try_catch) + } +} + +/// Catch block. +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +#[derive(Clone, Debug, Trace, Finalize, PartialEq)] +pub struct Catch { + parameter: Option, + block: Block, +} + +impl Catch { + /// Creates a new catch block. + pub(in crate::syntax) fn new(parameter: OI, block: B) -> Self + where + OI: Into>, + I: Into, + B: Into, + { + Self { + parameter: parameter.into().map(I::into), + block: block.into(), + } + } + + /// Gets the parameter of the catch block. + pub fn parameter(&self) -> Option<&str> { + self.parameter.as_ref().map(Identifier::as_ref) + } + + /// Retrieves the catch execution block. + pub fn block(&self) -> &Block { + &self.block + } + + /// Implements the display formatting with indentation. + pub(super) fn display(&self, f: &mut fmt::Formatter<'_>, indentation: usize) -> fmt::Result { + f.write_str(" catch")?; + if let Some(ref param) = self.parameter { + write!(f, "({})", param)?; + } + f.write_str(" ")?; + self.block.display(f, indentation) + } +} + +impl fmt::Display for Catch { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + self.display(f, 0) + } +} + +/// Finally block. +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +#[derive(Clone, Debug, Trace, Finalize, PartialEq)] +pub struct Finally { + block: Block, +} + +impl Finally { + /// Gets the finally block. + pub fn block(&self) -> &Block { + &self.block + } + + /// Implements the display formatting with indentation. + pub(super) fn display(&self, f: &mut fmt::Formatter<'_>, indentation: usize) -> fmt::Result { + f.write_str(" finally ")?; + self.block.display(f, indentation) + } +} + +impl From for Finally +where + T: Into, +{ + fn from(block: T) -> Self { + Self { + block: block.into(), + } + } +} diff --git a/boa/src/syntax/ast/op.rs b/boa/src/syntax/ast/op.rs index baac3949df..69329c59fd 100644 --- a/boa/src/syntax/ast/op.rs +++ b/boa/src/syntax/ast/op.rs @@ -1,23 +1,11 @@ //! This module implements various structure for logic handling. -use gc::{Finalize, Trace}; +use gc::{unsafe_empty_trace, Finalize, Trace}; use std::fmt::{Display, Formatter, Result}; #[cfg(feature = "serde")] use serde::{Deserialize, Serialize}; -/// Represents an operator -pub trait Operator { - /// Get the associativity as a boolean that is true if it goes rightwards - fn get_assoc(&self) -> bool; - /// Get the precedence as an unsigned integer, where the lower it is, the more precedence/priority it has - fn get_precedence(&self) -> u64; - /// Get the precedence and associativity of this operator - fn get_precedence_and_assoc(&self) -> (u64, bool) { - (self.get_precedence(), self.get_assoc()) - } -} - /// Arithmetic operators take numerical values (either literals or variables) /// as their operands and return a single numerical value. /// @@ -26,7 +14,7 @@ pub trait Operator { /// /// [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Expressions_and_Operators#Arithmetic #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] -#[derive(Clone, Debug, Trace, Finalize, PartialEq)] +#[derive(Clone, Copy, Debug, Finalize, PartialEq)] pub enum NumOp { /// The addition operator produces the sum of numeric operands or string concatenation. /// @@ -124,6 +112,10 @@ impl Display for NumOp { } } +unsafe impl Trace for NumOp { + unsafe_empty_trace!(); +} + /// A unary operator is one that takes a single operand/argument and performs an operation. /// /// A unary operation is an operation with only one operand. This operand comes either @@ -137,7 +129,7 @@ impl Display for NumOp { /// [spec]: https://tc39.es/ecma262/#prod-UnaryExpression /// [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Expressions_and_Operators#Unary #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] -#[derive(Clone, Debug, Trace, Finalize, PartialEq)] +#[derive(Clone, Copy, Debug, Finalize, PartialEq)] pub enum UnaryOp { /// The increment operator increments (adds one to) its operand and returns a value. /// @@ -336,6 +328,10 @@ impl Display for UnaryOp { } } +unsafe impl Trace for UnaryOp { + unsafe_empty_trace!(); +} + /// A bitwise operator is an operator used to perform bitwise operations /// on bit patterns or binary numerals that involve the manipulation of individual bits. /// @@ -344,7 +340,7 @@ impl Display for UnaryOp { /// /// [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Expressions_and_Operators#Bitwise #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] -#[derive(Clone, Debug, Trace, Finalize, PartialEq)] +#[derive(Clone, Copy, Debug, Finalize, PartialEq)] pub enum BitOp { /// Performs the AND operation on each pair of bits. a AND b yields 1 only if both a and b are 1. /// @@ -447,6 +443,10 @@ impl Display for BitOp { } } +unsafe impl Trace for BitOp { + unsafe_empty_trace!(); +} + /// A comparison operator compares its operands and returns a logical value based on whether the comparison is true. /// /// The operands can be numerical, string, logical, or object values. Strings are compared based on standard @@ -463,7 +463,7 @@ impl Display for BitOp { /// [spec]: tc39.es/ecma262/#sec-testing-and-comparison-operations /// [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Expressions_and_Operators#Comparison #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] -#[derive(Clone, Debug, Trace, Finalize, PartialEq)] +#[derive(Clone, Copy, Debug, Finalize, PartialEq)] pub enum CompOp { /// The equality operator converts the operands if they are not of the same type, then applies strict comparison. /// @@ -579,6 +579,7 @@ pub enum CompOp { /// [spec]: https://tc39.es/ecma262/#prod-RelationalExpression /// [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/Comparison_Operators#Less_than_or_equal_operator LessThanOrEqual, + /// The `in` operator returns true if the specified property is in the specified object or its prototype chain. /// /// Syntax: `prop in object` @@ -614,6 +615,10 @@ impl Display for CompOp { } } +unsafe impl Trace for CompOp { + unsafe_empty_trace!(); +} + /// Logical operators are typically used with Boolean (logical) values; when they are, they return a Boolean value. /// /// However, the `&&` and `||` operators actually return the value of one of the specified operands, @@ -626,7 +631,7 @@ impl Display for CompOp { /// [spec]: https://tc39.es/ecma262/#sec-binary-logical-operators /// [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Expressions_and_Operators#Logical #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] -#[derive(Clone, Debug, Trace, Finalize, PartialEq)] +#[derive(Clone, Copy, Debug, Finalize, PartialEq)] pub enum LogOp { /// The logical AND operator returns the value of the first operand if it can be coerced into `false`; /// otherwise, it returns the second operand. @@ -668,9 +673,13 @@ impl Display for LogOp { } } +unsafe impl Trace for LogOp { + unsafe_empty_trace!(); +} + /// This represents a binary operation between two values. #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] -#[derive(Clone, Debug, Trace, Finalize, PartialEq)] +#[derive(Clone, Copy, Debug, Finalize, PartialEq)] pub enum BinOp { /// Numeric operation. /// @@ -728,35 +737,6 @@ impl From for BinOp { } } -impl Operator for BinOp { - fn get_assoc(&self) -> bool { - true - } - fn get_precedence(&self) -> u64 { - match *self { - Self::Num(NumOp::Exp) => 4, - Self::Num(NumOp::Mul) | Self::Num(NumOp::Div) | Self::Num(NumOp::Mod) => 5, - Self::Num(NumOp::Add) | Self::Num(NumOp::Sub) => 6, - Self::Bit(BitOp::Shl) | Self::Bit(BitOp::Shr) | Self::Bit(BitOp::UShr) => 7, - Self::Comp(CompOp::LessThan) - | Self::Comp(CompOp::LessThanOrEqual) - | Self::Comp(CompOp::GreaterThan) - | Self::Comp(CompOp::GreaterThanOrEqual) - | Self::Comp(CompOp::In) => 8, - Self::Comp(CompOp::Equal) - | Self::Comp(CompOp::NotEqual) - | Self::Comp(CompOp::StrictEqual) - | Self::Comp(CompOp::StrictNotEqual) => 9, - Self::Bit(BitOp::And) => 10, - Self::Bit(BitOp::Xor) => 11, - Self::Bit(BitOp::Or) => 12, - Self::Log(LogOp::And) => 13, - Self::Log(LogOp::Or) => 14, - Self::Assign(_) => 15, - } - } -} - impl Display for BinOp { fn fmt(&self, f: &mut Formatter<'_>) -> Result { write!( @@ -773,6 +753,10 @@ impl Display for BinOp { } } +unsafe impl Trace for BinOp { + unsafe_empty_trace!(); +} + /// An assignment operator assigns a value to its left operand based on the value of its right operand. /// /// The simple assignment operator is equal (`=`), which assigns the value of its right operand to its @@ -787,7 +771,7 @@ impl Display for BinOp { /// [spec]: https://tc39.es/ecma262/#prod-AssignmentOperator /// [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Expressions_and_Operators#Assignment #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] -#[derive(Clone, Debug, Trace, Finalize, PartialEq)] +#[derive(Clone, Copy, Debug, Finalize, PartialEq)] pub enum AssignOp { /// The addition assignment operator adds the value of the right operand to a variable and assigns the result to the variable. /// @@ -928,6 +912,10 @@ pub enum AssignOp { // TODO: Add UShl (unsigned shift left). } +unsafe impl Trace for AssignOp { + unsafe_empty_trace!(); +} + impl Display for AssignOp { fn fmt(&self, f: &mut Formatter<'_>) -> Result { write!( diff --git a/boa/src/syntax/ast/pos.rs b/boa/src/syntax/ast/pos.rs deleted file mode 100644 index a77d50e8e8..0000000000 --- a/boa/src/syntax/ast/pos.rs +++ /dev/null @@ -1,36 +0,0 @@ -//! This module implements the `Pos` structure, which represents a position in the source code. - -#[cfg(feature = "serde")] -use serde::{Deserialize, Serialize}; - -/// A position in the Javascript source code. -/// -/// Stores both the column number and the line number -/// -/// ## Similar Implementations -/// [V8: Location](https://cs.chromium.org/chromium/src/v8/src/parsing/scanner.h?type=cs&q=isValid+Location&g=0&l=216) -#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] -#[derive(Clone, Copy, PartialEq, Debug)] -pub struct Position { - // Column number - pub column_number: u64, - // Line number - pub line_number: u64, -} - -impl Position { - /// Creates a new `Position`. - /// - /// Positions are usually created by a [`Token`](struct.token/Token.html). - /// - /// # Arguments - /// - /// * `line_number` - The line number the token starts at - /// * `column_number` - The column number the token starts at - pub fn new(line_number: u64, column_number: u64) -> Self { - Self { - line_number, - column_number, - } - } -} diff --git a/boa/src/syntax/ast/position.rs b/boa/src/syntax/ast/position.rs new file mode 100644 index 0000000000..15bef03840 --- /dev/null +++ b/boa/src/syntax/ast/position.rs @@ -0,0 +1,291 @@ +//! This module implements the `Pos` structure, which represents a position in the source code. + +use std::{cmp::Ordering, fmt, num::NonZeroU32}; + +#[cfg(feature = "serde")] +use serde::{Deserialize, Serialize}; + +/// A position in the JavaScript source code. +/// +/// Stores both the column number and the line number +/// +/// ## Similar Implementations +/// [V8: Location](https://cs.chromium.org/chromium/src/v8/src/parsing/scanner.h?type=cs&q=isValid+Location&g=0&l=216) +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)] +pub struct Position { + /// Line number. + line_number: NonZeroU32, + /// Column number. + column_number: NonZeroU32, +} + +impl Position { + /// Creates a new `Position`. + #[inline] + pub fn new(line_number: u32, column_number: u32) -> Self { + Self { + line_number: NonZeroU32::new(line_number).expect("line number cannot be 0"), + column_number: NonZeroU32::new(column_number).expect("column number cannot be 0"), + } + } + + /// Gets the line number of the position. + #[inline] + pub fn line_number(self) -> u32 { + self.line_number.get() + } + + /// Gets the column number of the position. + #[inline] + pub fn column_number(self) -> u32 { + self.column_number.get() + } +} + +impl fmt::Display for Position { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "{}:{}", self.line_number, self.column_number) + } +} + +/// A span in the JavaScript source code. +/// +/// Stores a start position and an end position. +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub struct Span { + start: Position, + end: Position, +} + +impl Span { + /// Creates a new `Span`. + #[inline] + pub fn new(start: Position, end: Position) -> Self { + assert!(start <= end, "a span cannot start after its end"); + + Self { start, end } + } + + /// Gets the starting position of the span. + #[inline] + pub fn start(self) -> Position { + self.start + } + + /// Gets the final position of the span. + #[inline] + pub fn end(self) -> Position { + self.end + } + + /// Checks if this span inclusively contains another span or position. + #[inline] + pub fn contains(self, other: S) -> bool + where + S: Into, + { + let other = other.into(); + self.start <= other.start && self.end >= other.end + } +} + +impl From for Span { + fn from(pos: Position) -> Self { + Self { + start: pos, + end: pos, + } + } +} + +impl PartialOrd for Span { + fn partial_cmp(&self, other: &Self) -> Option { + if self == other { + Some(Ordering::Equal) + } else if self.end < other.start { + Some(Ordering::Less) + } else if self.start > other.end { + Some(Ordering::Greater) + } else { + None + } + } +} + +impl fmt::Display for Span { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "[{}..{}]", self.start, self.end) + } +} + +#[cfg(test)] +mod tests { + use super::{Position, Span}; + + /// Checks that we cannot create a position with 0 as the column. + #[test] + #[should_panic] + fn invalid_position_column() { + Position::new(10, 0); + } + + /// Checks that we cannot create a position with 0 as the line. + #[test] + #[should_panic] + fn invalid_position_line() { + Position::new(0, 10); + } + + /// Checks that the `PartialEq` implementation of `Position` is consistent. + #[test] + fn position_equality() { + assert_eq!(Position::new(10, 50), Position::new(10, 50)); + assert_ne!(Position::new(10, 50), Position::new(10, 51)); + assert_ne!(Position::new(10, 50), Position::new(11, 50)); + assert_ne!(Position::new(10, 50), Position::new(11, 51)); + } + + /// Checks that the `PartialOrd` implementation of `Position` is consistent. + #[test] + fn position_order() { + assert!(Position::new(10, 50) < Position::new(10, 51)); + assert!(Position::new(9, 50) < Position::new(10, 50)); + assert!(Position::new(10, 50) < Position::new(11, 51)); + assert!(Position::new(10, 50) < Position::new(11, 49)); + + assert!(Position::new(10, 51) > Position::new(10, 50)); + assert!(Position::new(10, 50) > Position::new(9, 50)); + assert!(Position::new(11, 51) > Position::new(10, 50)); + assert!(Position::new(11, 49) > Position::new(10, 50)); + } + + /// Checks that the position getters actually retreive correct values. + #[test] + fn position_getters() { + let pos = Position::new(10, 50); + assert_eq!(pos.line_number(), 10); + assert_eq!(pos.column_number(), 50); + } + + /// Checks that the string representation of a position is correct. + #[test] + fn position_to_string() { + let pos = Position::new(10, 50); + + assert_eq!("10:50", pos.to_string()); + assert_eq!("10:50", format!("{}", pos)); + } + + /// Checks that we cannot create an invalid span. + #[test] + #[should_panic] + fn invalid_span() { + let a = Position::new(10, 30); + let b = Position::new(10, 50); + Span::new(b, a); + } + + /// Checks that we can create valid spans. + #[test] + fn span_creation() { + let a = Position::new(10, 30); + let b = Position::new(10, 50); + + let _ = Span::new(a, b); + let _ = Span::new(a, a); + let _ = Span::from(a); + } + + /// Checks that the `PartialEq` implementation of `Span` is consistent. + #[test] + fn span_equality() { + let a = Position::new(10, 50); + let b = Position::new(10, 52); + let c = Position::new(11, 20); + + let span_ab = Span::new(a, b); + let span_ab_2 = Span::new(a, b); + let span_ac = Span::new(a, c); + let span_bc = Span::new(b, c); + + assert_eq!(span_ab, span_ab_2); + assert_ne!(span_ab, span_ac); + assert_ne!(span_ab, span_bc); + assert_ne!(span_bc, span_ac); + + let span_a = Span::from(a); + let span_aa = Span::new(a, a); + + assert_eq!(span_a, span_aa); + } + + /// Checks that the getters retrieve the correct value. + #[test] + fn span_getters() { + let a = Position::new(10, 50); + let b = Position::new(10, 52); + + let span = Span::new(a, b); + + assert_eq!(span.start(), a); + assert_eq!(span.end(), b); + } + + /// Checks that the `Span::contains()` method works properly. + #[test] + fn span_contains() { + let a = Position::new(10, 50); + let b = Position::new(10, 52); + let c = Position::new(11, 20); + let d = Position::new(12, 5); + + let span_ac = Span::new(a, c); + assert!(span_ac.contains(b)); + + let span_ab = Span::new(a, b); + let span_cd = Span::new(c, d); + + assert!(!span_ab.contains(span_cd)); + assert!(span_ab.contains(b)); + + let span_ad = Span::new(a, d); + let span_bc = Span::new(b, c); + + assert!(span_ad.contains(span_bc)); + assert!(!span_bc.contains(span_ad)); + + let span_ac = Span::new(a, c); + let span_bd = Span::new(b, d); + + assert!(!span_ac.contains(span_bd)); + assert!(!span_bd.contains(span_ac)); + } + + /// Checks that the string representation of a span is correct. + #[test] + fn span_to_string() { + let a = Position::new(10, 50); + let b = Position::new(11, 20); + let span = Span::new(a, b); + + assert_eq!("[10:50..11:20]", span.to_string()); + assert_eq!("[10:50..11:20]", format!("{}", span)); + } + + /// Checks that the ordering of spans is correct. + #[test] + fn span_ordering() { + let a = Position::new(10, 50); + let b = Position::new(10, 52); + let c = Position::new(11, 20); + let d = Position::new(12, 5); + + let span_ab = Span::new(a, b); + let span_cd = Span::new(c, d); + + assert!(span_ab < span_cd); + assert!(span_cd > span_ab); + } +} diff --git a/boa/src/syntax/ast/punc.rs b/boa/src/syntax/ast/punctuator.rs similarity index 100% rename from boa/src/syntax/ast/punc.rs rename to boa/src/syntax/ast/punctuator.rs diff --git a/boa/src/syntax/ast/token.rs b/boa/src/syntax/ast/token.rs index d87380bb45..dafcc0ae92 100644 --- a/boa/src/syntax/ast/token.rs +++ b/boa/src/syntax/ast/token.rs @@ -5,11 +5,18 @@ //! //! [spec]: https://tc39.es/ecma262/#sec-tokens -use crate::syntax::ast::{bigint::BigInt, keyword::Keyword, pos::Position, punc::Punctuator}; -use std::fmt::{Debug, Display, Formatter, Result}; +use crate::syntax::{ + ast::{bigint::BigInt, Keyword, Punctuator, Span}, + lexer::LexerError, +}; +use bitflags::bitflags; +use std::{ + fmt::{self, Debug, Display, Formatter}, + str::FromStr, +}; #[cfg(feature = "serde")] -use serde::{Deserialize, Serialize}; +use serde::{Deserialize, Deserializer, Serialize, Serializer}; /// This represents the smallest individual words, phrases, or characters that JavaScript can understand. /// @@ -21,38 +28,31 @@ use serde::{Deserialize, Serialize}; #[derive(Debug, Clone, PartialEq)] pub struct Token { /// The token kind, which contains the actual data of the token. - pub kind: TokenKind, - - /// The token position from origina source code. - pub pos: Position, + pub(crate) kind: TokenKind, + /// The token position in the original source code. + span: Span, } impl Token { /// Create a new detailed token from the token data, line number and column number - pub fn new(kind: TokenKind, line_number: u64, column_number: u64) -> Self { - Self { - kind, - pos: Position::new(line_number, column_number), - } + pub fn new(kind: TokenKind, span: Span) -> Self { + Self { kind, span } } -} -impl Display for Token { - fn fmt(&self, f: &mut Formatter<'_>) -> Result { - write!(f, "{}", self.kind) + /// Gets the kind of the token. + pub fn kind(&self) -> &TokenKind { + &self.kind } -} -/// A continuous sequence of tokens. -pub struct VecToken(Vec); + /// Gets the token span in the original source code. + pub fn span(&self) -> Span { + self.span + } +} -impl Debug for VecToken { - fn fmt(&self, f: &mut Formatter<'_>) -> Result { - let mut buffer = String::new(); - for token in &self.0 { - buffer.push_str(&token.to_string()); - } - write!(f, "{}", buffer) +impl Display for Token { + fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { + write!(f, "{}", self.kind) } } @@ -88,6 +88,125 @@ impl From for NumericLiteral { } } +bitflags! { + #[derive(Default)] + pub struct RegExpFlags: u8 { + const GLOBAL = 0b0000_0001; + const IGNORE_CASE = 0b0000_0010; + const MULTILINE = 0b0000_0100; + const DOT_ALL = 0b0000_1000; + const UNICODE = 0b0001_0000; + const STICKY = 0b0010_0000; + } +} + +impl FromStr for RegExpFlags { + type Err = LexerError; + fn from_str(s: &str) -> Result { + let mut flags = Self::default(); + for c in s.bytes() { + let new_flag = match c { + b'g' => Self::GLOBAL, + b'i' => Self::IGNORE_CASE, + b'm' => Self::MULTILINE, + b's' => Self::DOT_ALL, + b'u' => Self::UNICODE, + b'y' => Self::STICKY, + _ => { + return Err(LexerError::new(format!( + "invalid regular expression flag {}", + char::from(c) + ))) + } + }; + + if !flags.contains(new_flag) { + flags.insert(new_flag); + } else { + return Err(LexerError::new(format!( + "invalid regular expression flag {}", + char::from(c) + ))); + } + } + Ok(flags) + } +} + +impl Display for RegExpFlags { + fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { + use fmt::Write; + + if self.contains(Self::GLOBAL) { + f.write_char('g')?; + } + if self.contains(Self::IGNORE_CASE) { + f.write_char('i')?; + } + if self.contains(Self::MULTILINE) { + f.write_char('m')?; + } + if self.contains(Self::DOT_ALL) { + f.write_char('s')?; + } + if self.contains(Self::UNICODE) { + f.write_char('u')?; + } + if self.contains(Self::STICKY) { + f.write_char('y')?; + } + Ok(()) + } +} + +#[cfg(feature = "serde")] +impl Serialize for RegExpFlags { + fn serialize(&self, serializer: S) -> Result + where + S: Serializer, + { + serializer.serialize_str(&self.to_string()) + } +} + +#[cfg(feature = "serde")] +impl<'de> Deserialize<'de> for RegExpFlags { + fn deserialize(deserializer: D) -> Result + where + D: Deserializer<'de>, + { + use serde::de::{self, Visitor}; + + /// Deserializer visitor implementation for `RegExpFlags`. + #[derive(Debug, Clone, Copy)] + struct RegExpFlagsVisitor; + + impl<'de> Visitor<'de> for RegExpFlagsVisitor { + type Value = RegExpFlags; + + fn expecting(&self, formatter: &mut Formatter<'_>) -> fmt::Result { + formatter.write_str("a string representing JavaScript regular expression flags") + } + + fn visit_str(self, value: &str) -> Result + where + E: de::Error, + { + value.parse().map_err(E::custom) + } + + fn visit_string(self, value: String) -> Result + where + E: de::Error, + { + self.visit_str(&value) + } + } + + deserializer.deserialize_str(RegExpFlagsVisitor) + } +} + /// Represents the type of Token and the data it has inside. #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] #[derive(Clone, PartialEq, Debug)] @@ -99,7 +218,7 @@ pub enum TokenKind { EOF, /// An identifier. - Identifier(String), + Identifier(Box), /// A keyword. /// @@ -118,10 +237,10 @@ pub enum TokenKind { Punctuator(Punctuator), /// A string literal. - StringLiteral(String), + StringLiteral(Box), /// A regular expression, consisting of body and flags. - RegularExpressionLiteral(String, String), + RegularExpressionLiteral(Box, RegExpFlags), /// Indicates the end of a line (`\n`). LineTerminator, @@ -159,7 +278,7 @@ impl TokenKind { /// Creates an `Identifier` token type. pub fn identifier(ident: I) -> Self where - I: Into, + I: Into>, { Self::Identifier(ident.into()) } @@ -185,18 +304,17 @@ impl TokenKind { /// Creates a `StringLiteral` token type. pub fn string_literal(lit: S) -> Self where - S: Into, + S: Into>, { Self::StringLiteral(lit.into()) } /// Creates a `RegularExpressionLiteral` token kind. - pub fn regular_expression_literal(body: B, flags: F) -> Self + pub fn regular_expression_literal(body: B, flags: RegExpFlags) -> Self where - B: Into, - F: Into, + B: Into>, { - Self::RegularExpressionLiteral(body.into(), flags.into()) + Self::RegularExpressionLiteral(body.into(), flags) } /// Creates a `LineTerminator` token kind. @@ -206,7 +324,7 @@ impl TokenKind { } impl Display for TokenKind { - fn fmt(&self, f: &mut Formatter<'_>) -> Result { + fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { match *self { Self::BooleanLiteral(ref val) => write!(f, "{}", val), Self::EOF => write!(f, "end of file"), diff --git a/boa/src/syntax/lexer/mod.rs b/boa/src/syntax/lexer/mod.rs index 4296e8c9d1..25431ed481 100644 --- a/boa/src/syntax/lexer/mod.rs +++ b/boa/src/syntax/lexer/mod.rs @@ -8,8 +8,8 @@ mod tests; use crate::syntax::ast::bigint::BigInt; use crate::syntax::ast::{ - punc::Punctuator, token::{NumericLiteral, Token, TokenKind}, + Position, Punctuator, Span, }; use std::{ char::{decode_utf16, from_u32}, @@ -23,38 +23,38 @@ use std::{ /// If the next value is not an assignment operation it will pattern match the provided values and return the corresponding token. macro_rules! vop { ($this:ident, $assign_op:expr, $op:expr) => ({ - let preview = $this.preview_next().ok_or_else(|| LexerError::new("Could not preview next value"))?; + let preview = $this.preview_next().ok_or_else(|| LexerError::new("could not preview next value"))?; match preview { '=' => { $this.next(); - $this.column_number += 1; + $this.next_column(); $assign_op } _ => $op, } }); ($this:ident, $assign_op:expr, $op:expr, {$($case:pat => $block:expr), +}) => ({ - let preview = $this.preview_next().ok_or_else(|| LexerError::new("Could not preview next value"))?; + let preview = $this.preview_next().ok_or_else(|| LexerError::new("could not preview next value"))?; match preview { '=' => { $this.next(); - $this.column_number += 1; + $this.next_column(); $assign_op }, $($case => { $this.next(); - $this.column_number += 1; + $this.next_column(); $block })+, _ => $op } }); ($this:ident, $op:expr, {$($case:pat => $block:expr),+}) => { - let preview = $this.preview_next().ok_or_else(|| LexerError::new("Could not preview next value"))?; + let preview = $this.preview_next().ok_or_else(|| LexerError::new("could not preview next value"))?; match preview { $($case => { $this.next()?; - $this.column_number += 1; + $this.next_column(); $block })+, _ => $op @@ -64,17 +64,13 @@ macro_rules! vop { /// The `op` macro handles binary operations or assignment operations and converts them into tokens. macro_rules! op { - ($this:ident, $assign_op:expr, $op:expr) => ({ + ($this:ident, $start_pos:expr, $assign_op:expr, $op:expr) => ({ let punc = vop!($this, $assign_op, $op); - $this.push_punc(punc); + $this.push_punc(punc, $start_pos); }); - ($this:ident, $assign_op:expr, $op:expr, {$($case:pat => $block:expr),+}) => ({ + ($this:ident, $start_pos:expr, $assign_op:expr, $op:expr, {$($case:pat => $block:expr),+}) => ({ let punc = vop!($this, $assign_op, $op, {$($case => $block),+}); - $this.push_punc(punc); - }); - ($this:ident, $op:expr, {$($case:pat => $block:expr),+}) => ({ - let punc = vop!($this, $op, {$($case => $block),+}); - $this.push_punc(); + $this.push_punc(punc, $start_pos); }); } @@ -91,9 +87,12 @@ impl LexerError { /// Create a new LexerError struct /// /// * `msg` - The message to show when LexerError is displayed - fn new(msg: &str) -> Self { + pub(crate) fn new(msg: M) -> Self + where + M: Into, + { Self { - details: msg.to_string(), + details: msg.into(), } } } @@ -122,10 +121,8 @@ pub struct Lexer<'a> { /// /// This field is public so you can use them once lexing has finished. pub tokens: Vec, - /// The current line number in the script - line_number: u64, - /// the current column number in the script - column_number: u64, + /// The current position in the source code. + position: Position, /// The full Peekable buffer, an array of [Char]s buffer: Peekable>, } @@ -137,21 +134,62 @@ impl<'a> Lexer<'a> { pub fn new(buffer: &'a str) -> Lexer<'a> { Lexer { tokens: Vec::new(), - line_number: 1, - column_number: 0, + position: Position::new(1, 1), buffer: buffer.chars().peekable(), } } /// Push a token onto the token queue. - fn push_token(&mut self, tk: TokenKind) { - self.tokens - .push(Token::new(tk, self.line_number, self.column_number)) + fn push_token(&mut self, tk: TokenKind, start: Position) { + let end = if let TokenKind::LineTerminator = tk { + self.position + } else { + Position::new( + self.position.line_number(), + self.position.column_number() - 1, + ) + }; + self.tokens.push(Token::new(tk, Span::new(start, end))) } /// Push a punctuation token - fn push_punc(&mut self, punc: Punctuator) { - self.push_token(TokenKind::Punctuator(punc)); + fn push_punc(&mut self, punc: Punctuator, start: Position) { + self.push_token(TokenKind::Punctuator(punc), start); + } + + /// Changes the current position by advancing to the next column. + fn next_column(&mut self) { + let pos = Position::new( + self.position.line_number(), + self.position.column_number() + 1, + ); + self.position = pos; + } + + /// Changes the current position by advancing the given number of columns. + fn move_columns(&mut self, columns: u32) { + let pos = Position::new( + self.position.line_number(), + self.position.column_number() + columns, + ); + self.position = pos; + } + + fn carriage_return(&mut self) { + let pos = Position::new(self.position.line_number(), 1); + self.position = pos; + } + + /// Changes the current position by advancing to the next line. + fn next_line(&mut self) { + let pos = Position::new(self.position.line_number() + 1, 1); + self.position = pos; + } + + /// Changes the current position by advancing the given number of lines. + fn move_lines(&mut self, lines: u32) { + let pos = Position::new(self.position.line_number() + lines, 1); + self.position = pos; } /// next fetches the next token and return it, or a LexerError if there are no more. @@ -202,6 +240,7 @@ impl<'a> Lexer<'a> { fn next_is(&mut self, peek: char) -> bool { let result = self.preview_next() == Some(peek); if result { + self.next_column(); self.buffer.next(); } result @@ -235,29 +274,30 @@ impl<'a> Lexer<'a> { /// This is a helper structure /// /// This structure helps with identifying what numerical type it is and what base is it. + #[derive(Debug, Clone, Copy, PartialEq, Eq)] enum NumericKind { Rational, - Integer(u32), - BigInt(u32), + Integer(u8), + BigInt(u8), } impl NumericKind { /// Get the base of the number kind. - fn base(&self) -> u32 { + fn base(self) -> u32 { match self { Self::Rational => 10, - Self::Integer(ref base) => *base, - Self::BigInt(ref base) => *base, + Self::Integer(base) => base as u32, + Self::BigInt(base) => base as u32, } } /// Converts `self` to BigInt kind. - fn convert_to_bigint(&mut self) { - *self = match *self { + fn to_bigint(self) -> Self { + match self { Self::Rational => unreachable!("can not convert rational number to BigInt"), Self::Integer(base) => Self::BigInt(base), Self::BigInt(base) => Self::BigInt(base), - }; + } } } @@ -265,28 +305,31 @@ impl<'a> Lexer<'a> { let strict_mode = false; let mut buf = ch.to_string(); - let mut position_offset = 0; let mut kind = NumericKind::Integer(10); + let start_pos = self.position; if ch == '0' { match self.preview_next() { None => { - self.push_token(TokenKind::NumericLiteral(NumericLiteral::Integer(0))); - self.column_number += 1; + self.next_column(); + self.push_token( + TokenKind::NumericLiteral(NumericLiteral::Integer(0)), + start_pos, + ); return Ok(()); } Some('x') | Some('X') => { self.next(); - position_offset += 1; + self.next_column(); kind = NumericKind::Integer(16); } Some('o') | Some('O') => { self.next(); - position_offset += 1; + self.next_column(); kind = NumericKind::Integer(8); } Some('b') | Some('B') => { self.next(); - position_offset += 1; + self.next_column(); kind = NumericKind::Integer(2); } Some(ch) if ch.is_ascii_digit() => { @@ -327,7 +370,7 @@ impl<'a> Lexer<'a> { } if self.next_is('n') { - kind.convert_to_bigint() + kind = kind.to_bigint(); } if let NumericKind::Integer(10) = kind { @@ -391,14 +434,12 @@ impl<'a> Lexer<'a> { } } - if let Err(e) = self.check_after_numeric_literal() { - return Err(e); - }; + self.check_after_numeric_literal()?; let num = match kind { NumericKind::BigInt(base) => { NumericLiteral::BigInt( - BigInt::from_str_radix(&buf, base).expect("Could not conver to BigInt") + BigInt::from_str_radix(&buf, base as u32).expect("Could not conver to BigInt") ) } NumericKind::Rational /* base: 10 */ => { @@ -408,7 +449,7 @@ impl<'a> Lexer<'a> { ) } NumericKind::Integer(base) => { - if let Ok(num) = i32::from_str_radix(&buf, base) { + if let Ok(num) = i32::from_str_radix(&buf, base as u32) { NumericLiteral::Integer( num ) @@ -416,7 +457,7 @@ impl<'a> Lexer<'a> { let b = f64::from(base); let mut result = 0.0_f64; for c in buf.chars() { - let digit = f64::from(c.to_digit(base).unwrap()); + let digit = f64::from(c.to_digit(base as u32).unwrap()); result = result * b + digit; } @@ -426,8 +467,8 @@ impl<'a> Lexer<'a> { } }; - self.push_token(TokenKind::NumericLiteral(num)); - self.column_number += (buf.len() as u64) + position_offset - 1; + self.move_columns(buf.len() as u32); + self.push_token(TokenKind::NumericLiteral(num), start_pos); Ok(()) } @@ -450,7 +491,8 @@ impl<'a> Lexer<'a> { if self.preview_next().is_none() { return Ok(()); } - self.column_number += 1; + let start_pos = self.position; + self.next_column(); let ch = self.next(); match ch { '"' | '\'' => { @@ -470,6 +512,7 @@ impl<'a> Lexer<'a> { if self.preview_next().is_none() { return Err(LexerError::new("Unterminated String")); } + let escape_pos = self.position; let escape = self.next(); if escape != '\n' { let escaped_ch = match escape { @@ -487,7 +530,7 @@ impl<'a> Lexer<'a> { } nums.push(self.next()); } - self.column_number += 2; + self.move_columns(2); let as_num = match u64::from_str_radix(&nums, 16) { Ok(v) => v, Err(_) => 0, @@ -495,8 +538,8 @@ impl<'a> Lexer<'a> { match from_u32(as_num as u32) { Some(v) => v, None => panic!( - "{}:{}: {} is not a valid unicode scalar value", - self.line_number, self.column_number, as_num + "{}: {} is not a valid unicode scalar value", + self.position, as_num ), } } @@ -523,8 +566,7 @@ impl<'a> Lexer<'a> { return Err(LexerError::new("Unterminated String")); } self.next(); // '}' - self.column_number += - (s.len() as u64).wrapping_add(3); + self.move_columns(s.len() as u32); c } else { let mut codepoints: Vec = vec![]; @@ -541,8 +583,7 @@ impl<'a> Lexer<'a> { }; codepoints.push(as_num); - self.column_number += - (s.len() as u64).wrapping_add(2); + self.move_columns(s.len() as u32); // Check for another UTF-16 codepoint if self.next_is('\\') && self.next_is('u') { @@ -561,7 +602,7 @@ impl<'a> Lexer<'a> { } '\'' | '"' | '\\' => escape, ch => { - let details = format!("{}:{}: Invalid escape `{}`", self.line_number, self.column_number, ch); + let details = format!("invalid escape sequence `{}` at line {}, column {}", escape_pos.line_number(), escape_pos.column_number(), ch); return Err(LexerError { details }); } }; @@ -571,12 +612,12 @@ impl<'a> Lexer<'a> { next_ch => buf.push(next_ch), } } - let str_length = buf.len() as u64; - self.push_token(TokenKind::StringLiteral(buf)); + let str_length = buf.len() as u32; // Why +1? Quotation marks are not included, // So technically it would be +2, (for both " ") but we want to be 1 less // to compensate for the incrementing at the top - self.column_number += str_length.wrapping_add(1); + self.move_columns( str_length.wrapping_add(1)); + self.push_token(TokenKind::string_literal(buf), start_pos); } _ if ch.is_digit(10) => self.reed_numerical_literal(ch)?, _ if ch.is_alphabetic() || ch == '$' || ch == '_' => { @@ -588,8 +629,7 @@ impl<'a> Lexer<'a> { break; } } - - self.push_token(match buf.as_str() { + let tk = match buf.as_str() { "true" => TokenKind::BooleanLiteral(true), "false" => TokenKind::BooleanLiteral(false), "null" => TokenKind::NullLiteral, @@ -601,33 +641,35 @@ impl<'a> Lexer<'a> { TokenKind::identifier(slice) } } - }); - // Move position forward the length of keyword - self.column_number += (buf.len().wrapping_sub(1)) as u64; + }; + + // Move position forward the length of the token + self.move_columns( (buf.len().wrapping_sub(1)) as u32); + + self.push_token(tk, start_pos); } - ';' => self.push_punc(Punctuator::Semicolon), - ':' => self.push_punc(Punctuator::Colon), + ';' => self.push_punc(Punctuator::Semicolon, start_pos), + ':' => self.push_punc(Punctuator::Colon, start_pos), '.' => { // . or ... if self.next_is('.') { if self.next_is('.') { - self.push_punc(Punctuator::Spread); - self.column_number += 2; + self.push_punc(Punctuator::Spread, start_pos); } else { return Err(LexerError::new("Expecting Token .")); } } else { - self.push_punc(Punctuator::Dot); + self.push_punc(Punctuator::Dot, start_pos); }; } - '(' => self.push_punc(Punctuator::OpenParen), - ')' => self.push_punc(Punctuator::CloseParen), - ',' => self.push_punc(Punctuator::Comma), - '{' => self.push_punc(Punctuator::OpenBlock), - '}' => self.push_punc(Punctuator::CloseBlock), - '[' => self.push_punc(Punctuator::OpenBracket), - ']' => self.push_punc(Punctuator::CloseBracket), - '?' => self.push_punc(Punctuator::Question), + '(' => self.push_punc(Punctuator::OpenParen, start_pos), + ')' => self.push_punc(Punctuator::CloseParen, start_pos), + ',' => self.push_punc(Punctuator::Comma, start_pos), + '{' => self.push_punc(Punctuator::OpenBlock, start_pos), + '}' => self.push_punc(Punctuator::CloseBlock, start_pos), + '[' => self.push_punc(Punctuator::OpenBracket, start_pos), + ']' => self.push_punc(Punctuator::CloseBracket, start_pos), + '?' => self.push_punc(Punctuator::Question, start_pos), // Comments '/' => { if let Some(ch) = self.preview_next() { @@ -639,15 +681,14 @@ impl<'a> Lexer<'a> { break; } } - self.line_number += 1; - self.column_number = 0; + self.next_line() } // block comment '*' => { let mut lines = 0; loop { if self.preview_next().is_none() { - return Err(LexerError::new("Unterminated Multiline Comment")); + return Err(LexerError::new("unterminated multiline comment")); } match self.next() { '*' => { @@ -662,19 +703,19 @@ impl<'a> Lexer<'a> { }, } } - self.line_number += lines; - self.column_number = 0; + self.move_lines(lines); } // division, assigndiv or regex literal _ => { // if we fail to parse a regex literal, store a copy of the current // buffer to restore later on let original_buffer = self.buffer.clone(); + let original_pos = self.position; // first, try to parse a regex literal let mut body = String::new(); let mut regex = false; loop { - self.column_number +=1; + self.next_column(); match self.buffer.next() { // end of body Some('/') => { @@ -684,14 +725,14 @@ impl<'a> Lexer<'a> { // newline/eof not allowed in regex literal n @ Some('\n') | n @ Some('\r') | n @ Some('\u{2028}') | n @ Some('\u{2029}') => { - self.column_number = 0; + self.carriage_return(); if n != Some('\r') { - self.line_number += 1; + self.next_line(); } break }, None => { - self.column_number -= 1; + self.position = Position::new(self.position.line_number(), self.position.column_number()-1); break } // escape sequence @@ -712,19 +753,21 @@ impl<'a> Lexer<'a> { if regex { // body was parsed, now look for flags let flags = self.take_char_while(char::is_alphabetic)?; - self.push_token(TokenKind::RegularExpressionLiteral( - body, flags, - )); + self.move_columns(body.len() as u32 + 1 + flags.len() as u32); + self.push_token(TokenKind::regular_expression_literal( + body, flags.parse()?, + ), start_pos); } else { // failed to parse regex, restore original buffer position and // parse either div or assigndiv self.buffer = original_buffer; + self.position = original_pos; if self.next_is('=') { self.push_token(TokenKind::Punctuator( Punctuator::AssignDiv, - )); + ), start_pos); } else { - self.push_token(TokenKind::Punctuator(Punctuator::Div)); + self.push_token(TokenKind::Punctuator(Punctuator::Div), start_pos); } } } @@ -733,26 +776,26 @@ impl<'a> Lexer<'a> { return Err(LexerError::new("Expecting Token /,*,= or regex")); } } - '*' => op!(self, Punctuator::AssignMul, Punctuator::Mul, { + '*' => op!(self, start_pos, Punctuator::AssignMul, Punctuator::Mul, { '*' => vop!(self, Punctuator::AssignPow, Punctuator::Exp) }), - '+' => op!(self, Punctuator::AssignAdd, Punctuator::Add, { + '+' => op!(self, start_pos, Punctuator::AssignAdd, Punctuator::Add, { '+' => Punctuator::Inc }), - '-' => op!(self, Punctuator::AssignSub, Punctuator::Sub, { + '-' => op!(self, start_pos, Punctuator::AssignSub, Punctuator::Sub, { '-' => { Punctuator::Dec } }), - '%' => op!(self, Punctuator::AssignMod, Punctuator::Mod), - '|' => op!(self, Punctuator::AssignOr, Punctuator::Or, { + '%' => op!(self, start_pos, Punctuator::AssignMod, Punctuator::Mod), + '|' => op!(self, start_pos, Punctuator::AssignOr, Punctuator::Or, { '|' => Punctuator::BoolOr }), - '&' => op!(self, Punctuator::AssignAnd, Punctuator::And, { + '&' => op!(self, start_pos, Punctuator::AssignAnd, Punctuator::And, { '&' => Punctuator::BoolAnd }), - '^' => op!(self, Punctuator::AssignXor, Punctuator::Xor), - '=' => op!(self, if self.next_is('=') { + '^' => op!(self, start_pos, Punctuator::AssignXor, Punctuator::Xor), + '=' => op!(self, start_pos, if self.next_is('=') { Punctuator::StrictEq } else { Punctuator::Eq @@ -761,27 +804,27 @@ impl<'a> Lexer<'a> { Punctuator::Arrow } }), - '<' => op!(self, Punctuator::LessThanOrEq, Punctuator::LessThan, { + '<' => op!(self, start_pos, Punctuator::LessThanOrEq, Punctuator::LessThan, { '<' => vop!(self, Punctuator::AssignLeftSh, Punctuator::LeftSh) }), - '>' => op!(self, Punctuator::GreaterThanOrEq, Punctuator::GreaterThan, { + '>' => op!(self, start_pos, Punctuator::GreaterThanOrEq, Punctuator::GreaterThan, { '>' => vop!(self, Punctuator::AssignRightSh, Punctuator::RightSh, { '>' => vop!(self, Punctuator::AssignURightSh, Punctuator::URightSh) }) }), '!' => op!( self, + start_pos, vop!(self, Punctuator::StrictNotEq, Punctuator::NotEq), Punctuator::Not ), - '~' => self.push_punc(Punctuator::Neg), + '~' => self.push_punc(Punctuator::Neg, start_pos), '\n' | '\u{2028}' | '\u{2029}' => { - self.push_token(TokenKind::LineTerminator); - self.line_number += 1; - self.column_number = 0; + self.next_line(); + self.push_token(TokenKind::LineTerminator, start_pos); } '\r' => { - self.column_number = 0; + self.carriage_return(); } // The rust char::is_whitespace function and the ecma standard use different sets // of characters as whitespaces: @@ -793,7 +836,7 @@ impl<'a> Lexer<'a> { // Unicode Space_Seperator category (minus \u{0020} and \u{00A0} which are allready stated above) '\u{1680}' | '\u{2000}'..='\u{200A}' | '\u{202F}' | '\u{205F}' | '\u{3000}' => (), _ => { - let details = format!("{}:{}: Unexpected '{}'", self.line_number, self.column_number, ch); + let details = format!("Unexpected '{}' at line {}, column {}", start_pos.line_number(), start_pos.column_number(), ch); return Err(LexerError { details }); }, } diff --git a/boa/src/syntax/lexer/tests.rs b/boa/src/syntax/lexer/tests.rs index dcf2131aec..6f8c1e1086 100644 --- a/boa/src/syntax/lexer/tests.rs +++ b/boa/src/syntax/lexer/tests.rs @@ -2,7 +2,11 @@ #![allow(clippy::indexing_slicing)] use super::*; -use crate::syntax::ast::keyword::Keyword; +use crate::syntax::ast::Keyword; + +fn span(start: (u32, u32), end: (u32, u32)) -> Span { + Span::new(Position::new(start.0, start.1), Position::new(end.0, end.1)) +} #[test] fn check_single_line_comment() { @@ -294,31 +298,30 @@ fn check_variable_definition_tokens() { #[test] fn check_positions() { - let s = "console.log(\"hello world\"); // Test"; - // ------123456789 + let s = r#"console.log("hello world\u{2764}"); // Test"#; + // --------123456789 let mut lexer = Lexer::new(s); lexer.lex().expect("failed to lex"); // The first column is 1 (not zero indexed) - assert_eq!(lexer.tokens[0].pos.column_number, 1); - assert_eq!(lexer.tokens[0].pos.line_number, 1); + assert_eq!(lexer.tokens[0].span(), span((1, 1), (1, 7))); + // Dot Token starts on column 8 - assert_eq!(lexer.tokens[1].pos.column_number, 8); - assert_eq!(lexer.tokens[1].pos.line_number, 1); + assert_eq!(lexer.tokens[1].span(), span((1, 8), (1, 8))); + // Log Token starts on column 9 - assert_eq!(lexer.tokens[2].pos.column_number, 9); - assert_eq!(lexer.tokens[2].pos.line_number, 1); + assert_eq!(lexer.tokens[2].span(), span((1, 9), (1, 11))); + // Open parenthesis token starts on column 12 - assert_eq!(lexer.tokens[3].pos.column_number, 12); - assert_eq!(lexer.tokens[3].pos.line_number, 1); + assert_eq!(lexer.tokens[3].span(), span((1, 12), (1, 12))); + // String token starts on column 13 - assert_eq!(lexer.tokens[4].pos.column_number, 13); - assert_eq!(lexer.tokens[4].pos.line_number, 1); - // Close parenthesis token starts on column 26 - assert_eq!(lexer.tokens[5].pos.column_number, 26); - assert_eq!(lexer.tokens[5].pos.line_number, 1); - // Semi Colon token starts on column 27 - assert_eq!(lexer.tokens[6].pos.column_number, 27); - assert_eq!(lexer.tokens[6].pos.line_number, 1); + assert_eq!(lexer.tokens[4].span(), span((1, 13), (1, 33))); + + // Close parenthesis token starts on column 34 + assert_eq!(lexer.tokens[5].span(), span((1, 34), (1, 34))); + + // Semi Colon token starts on column 35 + assert_eq!(lexer.tokens[6].span(), span((1, 35), (1, 35))); } #[test] @@ -329,8 +332,7 @@ fn two_divisions_in_expression() { lexer.lex().expect("failed to lex"); // dbg!(&lexer.tokens); - assert_eq!(lexer.tokens[11].pos.column_number, 37); - assert_eq!(lexer.tokens[11].pos.line_number, 1); + assert_eq!(lexer.tokens[11].span(), span((1, 37), (1, 37))); } #[test] @@ -340,17 +342,10 @@ fn check_line_numbers() { let mut lexer = Lexer::new(s); lexer.lex().expect("failed to lex"); - assert_eq!(lexer.tokens[0].pos.column_number, 1); - assert_eq!(lexer.tokens[0].pos.line_number, 1); - - assert_eq!(lexer.tokens[1].pos.column_number, 2); - assert_eq!(lexer.tokens[1].pos.line_number, 1); - - assert_eq!(lexer.tokens[2].pos.column_number, 1); - assert_eq!(lexer.tokens[2].pos.line_number, 2); - - assert_eq!(lexer.tokens[3].pos.column_number, 2); - assert_eq!(lexer.tokens[3].pos.line_number, 2); + assert_eq!(lexer.tokens[0].span(), span((1, 1), (1, 1))); + assert_eq!(lexer.tokens[1].span(), span((1, 2), (2, 1))); + assert_eq!(lexer.tokens[2].span(), span((2, 1), (2, 1))); + assert_eq!(lexer.tokens[3].span(), span((2, 2), (3, 1))); } // Increment/Decrement @@ -376,9 +371,9 @@ fn check_nan() { match lexer.tokens[3].kind { TokenKind::NumericLiteral(NumericLiteral::Rational(a)) => { - assert_eq!(a.is_nan(), true); + assert!(a.is_nan()); } - _ => assert!(false), + ref other => panic!("Incorrect token kind found for NaN: {}", other), } } @@ -428,10 +423,7 @@ fn hexadecimal_edge_case() { lexer.lex().expect("failed to lex"); assert_eq!(lexer.tokens[0].kind, TokenKind::numeric_literal(0xffff)); assert_eq!(lexer.tokens[1].kind, TokenKind::Punctuator(Punctuator::Dot)); - assert_eq!( - lexer.tokens[2].kind, - TokenKind::Identifier(String::from("ff")) - ); + assert_eq!(lexer.tokens[2].kind, TokenKind::identifier("ff")); assert_eq!( lexer.tokens[3].kind, @@ -459,7 +451,7 @@ fn regex_literal() { lexer.lex().expect("failed to lex"); assert_eq!( lexer.tokens[0].kind, - TokenKind::regular_expression_literal("(?:)", "") + TokenKind::regular_expression_literal("(?:)", "".parse().unwrap()) ); } @@ -469,7 +461,7 @@ fn regex_literal_flags() { lexer.lex().expect("failed to lex"); assert_eq!( lexer.tokens[0].kind, - TokenKind::regular_expression_literal("\\/[^\\/]*\\/*", "gmi") + TokenKind::regular_expression_literal("\\/[^\\/]*\\/*", "gmi".parse().unwrap()) ); } diff --git a/boa/src/syntax/parser/cursor.rs b/boa/src/syntax/parser/cursor.rs index ffc61f3df0..69b116ce3e 100644 --- a/boa/src/syntax/parser/cursor.rs +++ b/boa/src/syntax/parser/cursor.rs @@ -2,8 +2,8 @@ use super::ParseError; use crate::syntax::ast::{ - punc::Punctuator, token::{Token, TokenKind}, + Punctuator, }; /// Token cursor. @@ -119,7 +119,7 @@ impl<'a> Cursor<'a> { /// Returns an error if the next token is not of kind `kind`. /// /// Note: it will consume the next token. - pub(super) fn expect(&mut self, kind: K, routine: &'static str) -> Result<(), ParseError> + pub(super) fn expect(&mut self, kind: K, context: &'static str) -> Result<(), ParseError> where K: Into, { @@ -129,10 +129,10 @@ impl<'a> Cursor<'a> { if next_token.kind == kind { Ok(()) } else { - Err(ParseError::Expected( + Err(ParseError::expected( vec![kind], next_token.clone(), - routine, + context, )) } } @@ -180,7 +180,7 @@ impl<'a> Cursor<'a> { pub(super) fn expect_semicolon( &mut self, do_while: bool, - routine: &'static str, + context: &'static str, ) -> Result<(), ParseError> { match self.peek_semicolon(do_while) { (true, Some(tk)) => match tk.kind { @@ -191,10 +191,10 @@ impl<'a> Cursor<'a> { _ => Ok(()), }, (true, None) => Ok(()), - (false, Some(tk)) => Err(ParseError::Expected( + (false, Some(tk)) => Err(ParseError::expected( vec![TokenKind::Punctuator(Punctuator::Semicolon)], tk.clone(), - routine, + context, )), (false, None) => unreachable!(), } @@ -203,11 +203,7 @@ impl<'a> Cursor<'a> { /// It will make sure that the next token is not a line terminator. /// /// It expects that the token stream does not end here. - pub(super) fn peek_expect_no_lineterminator( - &mut self, - skip: usize, - routine: &'static str, - ) -> Result<(), ParseError> { + pub(super) fn peek_expect_no_lineterminator(&mut self, skip: usize) -> Result<(), ParseError> { let mut count = 0; let mut skipped = 0; loop { @@ -215,7 +211,7 @@ impl<'a> Cursor<'a> { count += 1; if let Some(tk) = token { if skipped == skip && tk.kind == TokenKind::LineTerminator { - break Err(ParseError::Unexpected(tk.clone(), Some(routine))); + break Err(ParseError::unexpected(tk.clone(), None)); } else if skipped == skip && tk.kind != TokenKind::LineTerminator { break Ok(()); } else if tk.kind != TokenKind::LineTerminator { diff --git a/boa/src/syntax/parser/error.rs b/boa/src/syntax/parser/error.rs index 1d83c95357..924010a3ee 100644 --- a/boa/src/syntax/parser/error.rs +++ b/boa/src/syntax/parser/error.rs @@ -1,40 +1,96 @@ //! Error and result implementation for the parser. use crate::syntax::ast::{ - keyword::Keyword, - node::Node, - pos::Position, + position::Position, token::{Token, TokenKind}, + Node, }; use std::fmt; /// Result of a parsing operation. pub type ParseResult = Result; +pub(crate) trait ErrorContext { + fn context(self, context: &'static str) -> Self; +} + +impl ErrorContext for Result { + fn context(self, context: &'static str) -> Self { + self.map_err(|e| e.context(context)) + } +} + /// `ParseError` is an enum which represents errors encounted during parsing an expression #[derive(Debug, Clone)] pub enum ParseError { /// When it expected a certain kind of token, but got another as part of something - Expected(Vec, Token, &'static str), - /// When it expected a certain expression, but got another - ExpectedExpr(&'static str, Node, Position), - /// When it didn't expect this keyword - UnexpectedKeyword(Keyword, Position), + Expected { + expected: Box<[TokenKind]>, + found: Token, + context: &'static str, + }, /// When a token is unexpected - Unexpected(Token, Option<&'static str>), + Unexpected { + found: Token, + message: Option<&'static str>, + }, /// When there is an abrupt end to the parsing AbruptEnd, - /// Out of range error, attempting to set a position where there is no token - RangeError, /// Catch all General Error - General(&'static str, Option), + General { + message: &'static str, + position: Position, + }, +} + +impl ParseError { + /// Changes the context of the error, if any. + fn context(self, new_context: &'static str) -> Self { + match self { + Self::Expected { + expected, found, .. + } => Self::expected(expected, found, new_context), + e => e, + } + } + + /// Creates an `Expected` parsing error. + pub(super) fn expected(expected: E, found: Token, context: &'static str) -> Self + where + E: Into>, + { + Self::Expected { + expected: expected.into(), + found, + context, + } + } + + /// Creates an `Expected` parsing error. + pub(super) fn unexpected(found: Token, message: C) -> Self + where + C: Into>, + { + Self::Unexpected { + found, + message: message.into(), + } + } + + pub(super) fn general(message: &'static str, position: Position) -> Self { + Self::General { message, position } + } } impl fmt::Display for ParseError { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { match self { - Self::Expected(expected, actual, routine) => write!( + Self::Expected { + expected, + found, + context, + } => write!( f, - "Expected {}, got '{}' in {} at line {}, col {}", + "expected {}, got '{}' in {} at line {}, col {}", if expected.len() == 1 { format!( "token '{}'", @@ -62,45 +118,31 @@ impl fmt::Display for ParseError { .collect::() ) }, - actual, - routine, - actual.pos.line_number, - actual.pos.column_number + found, + context, + found.span().start().line_number(), + found.span().start().column_number() ), - Self::ExpectedExpr(expected, actual, pos) => write!( + Self::Unexpected { found, message } => write!( f, - "Expected expression '{}', got '{}' at line {}, col {}", - expected, actual, pos.line_number, pos.column_number - ), - Self::UnexpectedKeyword(keyword, pos) => write!( - f, - "Unexpected keyword: '{}' at line {}, col {}", - keyword, pos.line_number, pos.column_number - ), - Self::Unexpected(tok, msg) => write!( - f, - "Unexpected Token '{}'{} at line {}, col {}", - tok, - if let Some(m) = msg { + "unexpected token '{}'{} at line {}, col {}", + found, + if let Some(m) = message { format!(", {}", m) } else { String::new() }, - tok.pos.line_number, - tok.pos.column_number + found.span().start().line_number(), + found.span().start().column_number() ), Self::AbruptEnd => write!(f, "Abrupt End"), - Self::General(msg, pos) => write!( + Self::General { message, position } => write!( f, - "{}{}", - msg, - if let Some(pos) = pos { - format!(" at line {}, col {}", pos.line_number, pos.column_number) - } else { - String::new() - } + "{} at line {}, col {}", + message, + position.line_number(), + position.column_number() ), - Self::RangeError => write!(f, "RangeError!"), } } } diff --git a/boa/src/syntax/parser/expression/assignment/arrow_function.rs b/boa/src/syntax/parser/expression/assignment/arrow_function.rs index b8166fe97d..ccab9a104b 100644 --- a/boa/src/syntax/parser/expression/assignment/arrow_function.rs +++ b/boa/src/syntax/parser/expression/assignment/arrow_function.rs @@ -10,14 +10,14 @@ use super::AssignmentExpression; use crate::syntax::{ ast::{ - node::{FormalParameter, Node}, - punc::Punctuator, - token::TokenKind, + node::{ArrowFunctionDecl, FormalParameter, Node, StatementList}, + Punctuator, TokenKind, }, parser::{ + error::{ErrorContext, ParseError, ParseResult}, function::{FormalParameters, FunctionBody}, statement::BindingIdentifier, - AllowAwait, AllowIn, AllowYield, Cursor, ParseError, ParseResult, TokenParser, + AllowAwait, AllowIn, AllowYield, Cursor, TokenParser, }, }; @@ -57,40 +57,30 @@ impl ArrowFunction { } impl TokenParser for ArrowFunction { - type Output = Node; + type Output = ArrowFunctionDecl; - fn parse(self, cursor: &mut Cursor<'_>) -> ParseResult { + fn parse(self, cursor: &mut Cursor<'_>) -> Result { let next_token = cursor.peek(0).ok_or(ParseError::AbruptEnd)?; let params = if let TokenKind::Punctuator(Punctuator::OpenParen) = &next_token.kind { // CoverParenthesizedExpressionAndArrowParameterList cursor.expect(Punctuator::OpenParen, "arrow function")?; let params = FormalParameters::new(self.allow_yield, self.allow_await).parse(cursor)?; cursor.expect(Punctuator::CloseParen, "arrow function")?; - params.into_boxed_slice() + params } else { let param = BindingIdentifier::new(self.allow_yield, self.allow_await) .parse(cursor) - .map_err(|e| match e { - ParseError::Expected(mut exp, tok, _) => { - exp.push(Punctuator::OpenParen.into()); - ParseError::Expected(exp, tok, "arrow function") - } - e => e, - })?; - Box::new([FormalParameter { - init: None, - name: param, - is_rest_param: false, - }]) + .context("arrow function")?; + Box::new([FormalParameter::new(param, None, false)]) }; - cursor.peek_expect_no_lineterminator(0, "arrow function")?; + cursor.peek_expect_no_lineterminator(0)?; cursor.expect(Punctuator::Arrow, "arrow function")?; let body = ConciseBody::new(self.allow_in).parse(cursor)?; - Ok(Node::arrow_function_decl(params, body)) + Ok(ArrowFunctionDecl::new(params, body)) } } @@ -113,21 +103,19 @@ impl ConciseBody { } impl TokenParser for ConciseBody { - type Output = Node; + type Output = StatementList; fn parse(self, cursor: &mut Cursor<'_>) -> Result { match cursor.peek(0).ok_or(ParseError::AbruptEnd)?.kind { TokenKind::Punctuator(Punctuator::OpenBlock) => { let _ = cursor.next(); - let body = FunctionBody::new(false, false) - .parse(cursor) - .map(Node::statement_list)?; + let body = FunctionBody::new(false, false).parse(cursor)?; cursor.expect(Punctuator::CloseBlock, "arrow function")?; Ok(body) } - _ => Ok(Node::return_node( + _ => Ok(StatementList::from(vec![Node::return_node( ExpressionBody::new(self.allow_in, false).parse(cursor)?, - )), + )])), } } } diff --git a/boa/src/syntax/parser/expression/assignment/conditional.rs b/boa/src/syntax/parser/expression/assignment/conditional.rs index 874aa950c9..2bea95cc9b 100644 --- a/boa/src/syntax/parser/expression/assignment/conditional.rs +++ b/boa/src/syntax/parser/expression/assignment/conditional.rs @@ -8,7 +8,7 @@ //! [spec]: https://tc39.es/ecma262/#sec-conditional-operator use crate::syntax::{ - ast::{node::Node, punc::Punctuator, token::TokenKind}, + ast::{Node, Punctuator, TokenKind}, parser::{ expression::{AssignmentExpression, LogicalORExpression}, AllowAwait, AllowIn, AllowYield, Cursor, ParseResult, TokenParser, diff --git a/boa/src/syntax/parser/expression/assignment/exponentiation.rs b/boa/src/syntax/parser/expression/assignment/exponentiation.rs index 5da2b31032..fdc54114a3 100644 --- a/boa/src/syntax/parser/expression/assignment/exponentiation.rs +++ b/boa/src/syntax/parser/expression/assignment/exponentiation.rs @@ -9,11 +9,9 @@ use crate::syntax::{ ast::{ - keyword::Keyword, - node::Node, - op::{BinOp, NumOp}, - punc::Punctuator, - token::TokenKind, + node::{BinOp, Node}, + op::NumOp, + Keyword, Punctuator, TokenKind, }, parser::{ expression::{unary::UnaryExpression, update::UpdateExpression}, @@ -80,11 +78,7 @@ impl TokenParser for ExponentiationExpression { let lhs = UpdateExpression::new(self.allow_yield, self.allow_await).parse(cursor)?; if let Some(tok) = cursor.next() { if let TokenKind::Punctuator(Punctuator::Exp) = tok.kind { - return Ok(Node::bin_op( - BinOp::Num(NumOp::Exp), - lhs, - self.parse(cursor)?, - )); + return Ok(BinOp::new(NumOp::Exp, lhs, self.parse(cursor)?).into()); } else { cursor.back(); } diff --git a/boa/src/syntax/parser/expression/assignment/mod.rs b/boa/src/syntax/parser/expression/assignment/mod.rs index bcc3079f77..b45f35880d 100644 --- a/boa/src/syntax/parser/expression/assignment/mod.rs +++ b/boa/src/syntax/parser/expression/assignment/mod.rs @@ -13,7 +13,10 @@ mod exponentiation; use self::{arrow_function::ArrowFunction, conditional::ConditionalExpression}; use crate::syntax::{ - ast::{keyword::Keyword, node::Node, punc::Punctuator, token::TokenKind}, + ast::{ + node::{Assign, BinOp, Node}, + Keyword, Punctuator, TokenKind, + }, parser::{AllowAwait, AllowIn, AllowYield, Cursor, ParseError, ParseResult, TokenParser}, }; pub(super) use exponentiation::ExponentiationExpression; @@ -74,9 +77,7 @@ impl TokenParser for AssignmentExpression { TokenKind::Identifier(_) | TokenKind::Keyword(Keyword::Yield) | TokenKind::Keyword(Keyword::Await) - if cursor - .peek_expect_no_lineterminator(1, "arrow function") - .is_ok() => + if cursor.peek_expect_no_lineterminator(1).is_ok() => { if let Some(tok) = cursor.peek(1) { if tok.kind == TokenKind::Punctuator(Punctuator::Arrow) { @@ -85,7 +86,8 @@ impl TokenParser for AssignmentExpression { self.allow_yield, self.allow_await, ) - .parse(cursor); + .parse(cursor) + .map(Node::ArrowFunctionDecl); } } } @@ -94,6 +96,7 @@ impl TokenParser for AssignmentExpression { if let Some(node) = ArrowFunction::new(self.allow_in, self.allow_yield, self.allow_await) .try_parse(cursor) + .map(Node::ArrowFunctionDecl) { return Ok(node); } @@ -103,17 +106,16 @@ impl TokenParser for AssignmentExpression { let mut lhs = ConditionalExpression::new(self.allow_in, self.allow_yield, self.allow_await) .parse(cursor)?; - // let mut lhs = self.read_block()?; if let Some(tok) = cursor.next() { match tok.kind { TokenKind::Punctuator(Punctuator::Assign) => { - lhs = Node::assign(lhs, self.parse(cursor)?) + lhs = Assign::new(lhs, self.parse(cursor)?).into(); } TokenKind::Punctuator(p) if p.as_binop().is_some() => { let expr = self.parse(cursor)?; let binop = p.as_binop().expect("binop disappeared"); - lhs = Node::bin_op(binop, lhs, expr); + lhs = BinOp::new(binop, lhs, expr).into(); } _ => { cursor.back(); diff --git a/boa/src/syntax/parser/expression/left_hand_side/arguments.rs b/boa/src/syntax/parser/expression/left_hand_side/arguments.rs index 177e9d3ad1..a5caf3a938 100644 --- a/boa/src/syntax/parser/expression/left_hand_side/arguments.rs +++ b/boa/src/syntax/parser/expression/left_hand_side/arguments.rs @@ -8,7 +8,7 @@ //! [spec]: https://tc39.es/ecma262/#prod-Arguments use crate::syntax::{ - ast::{node::Node, punc::Punctuator, token::TokenKind}, + ast::{Node, Punctuator, TokenKind}, parser::{ expression::AssignmentExpression, AllowAwait, AllowYield, Cursor, ParseError, TokenParser, }, @@ -43,9 +43,9 @@ impl Arguments { } impl TokenParser for Arguments { - type Output = Vec; + type Output = Box<[Node]>; - fn parse(self, cursor: &mut Cursor<'_>) -> Result, ParseError> { + fn parse(self, cursor: &mut Cursor<'_>) -> Result { cursor.expect(Punctuator::OpenParen, "arguments")?; let mut args = Vec::new(); loop { @@ -54,7 +54,7 @@ impl TokenParser for Arguments { TokenKind::Punctuator(Punctuator::CloseParen) => break, TokenKind::Punctuator(Punctuator::Comma) => { if args.is_empty() { - return Err(ParseError::Unexpected(next_token.clone(), None)); + return Err(ParseError::unexpected(next_token.clone(), None)); } if cursor.next_if(Punctuator::CloseParen).is_some() { @@ -63,7 +63,7 @@ impl TokenParser for Arguments { } _ => { if !args.is_empty() { - return Err(ParseError::Expected( + return Err(ParseError::expected( vec![ TokenKind::Punctuator(Punctuator::Comma), TokenKind::Punctuator(Punctuator::CloseParen), @@ -89,6 +89,6 @@ impl TokenParser for Arguments { ); } } - Ok(args) + Ok(args.into_boxed_slice()) } } diff --git a/boa/src/syntax/parser/expression/left_hand_side/call.rs b/boa/src/syntax/parser/expression/left_hand_side/call.rs index ce7fd8e47f..899b1cc549 100644 --- a/boa/src/syntax/parser/expression/left_hand_side/call.rs +++ b/boa/src/syntax/parser/expression/left_hand_side/call.rs @@ -9,7 +9,10 @@ use super::arguments::Arguments; use crate::syntax::{ - ast::{node::Node, punc::Punctuator, token::TokenKind}, + ast::{ + node::{Call, Node}, + Punctuator, TokenKind, + }, parser::{ expression::Expression, AllowAwait, AllowYield, Cursor, ParseError, ParseResult, TokenParser, @@ -51,11 +54,11 @@ impl TokenParser for CallExpression { let mut lhs = match cursor.peek(0) { Some(tk) if tk.kind == TokenKind::Punctuator(Punctuator::OpenParen) => { let args = Arguments::new(self.allow_yield, self.allow_await).parse(cursor)?; - Node::call(self.first_member_expr, args) + Node::from(Call::new(self.first_member_expr, args)) } _ => { let next_token = cursor.next().ok_or(ParseError::AbruptEnd)?; - return Err(ParseError::Expected( + return Err(ParseError::expected( vec![TokenKind::Punctuator(Punctuator::OpenParen)], next_token.clone(), "call expression", @@ -67,19 +70,19 @@ impl TokenParser for CallExpression { match tok.kind { TokenKind::Punctuator(Punctuator::OpenParen) => { let args = Arguments::new(self.allow_yield, self.allow_await).parse(cursor)?; - lhs = Node::call(lhs, args); + lhs = Node::from(Call::new(lhs, args)); } TokenKind::Punctuator(Punctuator::Dot) => { let _ = cursor.next().ok_or(ParseError::AbruptEnd)?; // We move the cursor. match &cursor.next().ok_or(ParseError::AbruptEnd)?.kind { TokenKind::Identifier(name) => { - lhs = Node::get_const_field(lhs, name); + lhs = Node::get_const_field(lhs, name.clone()); } TokenKind::Keyword(kw) => { lhs = Node::get_const_field(lhs, kw.to_string()); } _ => { - return Err(ParseError::Expected( + return Err(ParseError::expected( vec![TokenKind::identifier("identifier")], tok.clone(), "call expression", diff --git a/boa/src/syntax/parser/expression/left_hand_side/member.rs b/boa/src/syntax/parser/expression/left_hand_side/member.rs index 932387730f..de13301eaf 100644 --- a/boa/src/syntax/parser/expression/left_hand_side/member.rs +++ b/boa/src/syntax/parser/expression/left_hand_side/member.rs @@ -7,7 +7,10 @@ use super::arguments::Arguments; use crate::syntax::{ - ast::{keyword::Keyword, node::Node, punc::Punctuator, token::TokenKind}, + ast::{ + node::{Call, New, Node}, + Keyword, Punctuator, TokenKind, + }, parser::{ expression::{primary::PrimaryExpression, Expression}, AllowAwait, AllowYield, Cursor, ParseError, ParseResult, TokenParser, @@ -50,9 +53,9 @@ impl TokenParser for MemberExpression { let _ = cursor.next().expect("keyword disappeared"); let lhs = self.parse(cursor)?; let args = Arguments::new(self.allow_yield, self.allow_await).parse(cursor)?; - let call_node = Node::call(lhs, args); + let call_node = Call::new(lhs, args); - Node::new(call_node) + Node::from(New::from(call_node)) } else { PrimaryExpression::new(self.allow_yield, self.allow_await).parse(cursor)? }; @@ -61,10 +64,12 @@ impl TokenParser for MemberExpression { TokenKind::Punctuator(Punctuator::Dot) => { let _ = cursor.next().ok_or(ParseError::AbruptEnd)?; // We move the cursor forward. match &cursor.next().ok_or(ParseError::AbruptEnd)?.kind { - TokenKind::Identifier(name) => lhs = Node::get_const_field(lhs, name), + TokenKind::Identifier(name) => { + lhs = Node::get_const_field(lhs, name.clone()) + } TokenKind::Keyword(kw) => lhs = Node::get_const_field(lhs, kw.to_string()), _ => { - return Err(ParseError::Expected( + return Err(ParseError::expected( vec![TokenKind::identifier("identifier")], tok.clone(), "member expression", diff --git a/boa/src/syntax/parser/expression/left_hand_side/mod.rs b/boa/src/syntax/parser/expression/left_hand_side/mod.rs index 470184276a..737dfdbe85 100644 --- a/boa/src/syntax/parser/expression/left_hand_side/mod.rs +++ b/boa/src/syntax/parser/expression/left_hand_side/mod.rs @@ -13,7 +13,7 @@ mod member; use self::{call::CallExpression, member::MemberExpression}; use crate::syntax::{ - ast::{node::Node, punc::Punctuator, token::TokenKind}, + ast::{Node, Punctuator, TokenKind}, parser::{AllowAwait, AllowYield, Cursor, ParseResult, TokenParser}, }; diff --git a/boa/src/syntax/parser/expression/mod.rs b/boa/src/syntax/parser/expression/mod.rs index 464784449b..87317a6df5 100644 --- a/boa/src/syntax/parser/expression/mod.rs +++ b/boa/src/syntax/parser/expression/mod.rs @@ -18,7 +18,10 @@ mod update; use self::assignment::ExponentiationExpression; pub(super) use self::{assignment::AssignmentExpression, primary::Initializer}; use super::{AllowAwait, AllowIn, AllowYield, Cursor, ParseResult, TokenParser}; -use crate::syntax::ast::{keyword::Keyword, node::Node, punc::Punctuator, token::TokenKind}; +use crate::syntax::ast::{ + node::{BinOp, Node}, + Keyword, Punctuator, TokenKind, +}; // For use in the expression! macro to allow for both Punctuator and Keyword parameters. // Always returns false. @@ -53,19 +56,19 @@ macro_rules! expression { ($name:ident, $lower:ident, [$( $op:path ),*], [$( $lo match tok.kind { TokenKind::Punctuator(op) if $( op == $op )||* => { let _ = cursor.next().expect("token disappeared"); - lhs = Node::bin_op( + lhs = BinOp::new( op.as_binop().expect("Could not get binary operation."), lhs, $lower::new($( self.$low_param ),*).parse(cursor)? - ) + ).into(); } TokenKind::Keyword(op) if $( op == $op )||* => { let _ = cursor.next().expect("token disappeared"); - lhs = Node::bin_op( + lhs = BinOp::new( op.as_binop().expect("Could not get binary operation."), lhs, $lower::new($( self.$low_param ),*).parse(cursor)? - ) + ).into(); } _ => break } diff --git a/boa/src/syntax/parser/expression/primary/array_initializer/mod.rs b/boa/src/syntax/parser/expression/primary/array_initializer/mod.rs index 2dc7847e01..999af9a8c1 100644 --- a/boa/src/syntax/parser/expression/primary/array_initializer/mod.rs +++ b/boa/src/syntax/parser/expression/primary/array_initializer/mod.rs @@ -11,10 +11,12 @@ mod tests; use crate::syntax::{ - ast::{constant::Const, node::Node, punc::Punctuator}, + ast::{ + node::{ArrayDecl, Node}, + Const, Punctuator, + }, parser::{ - expression::AssignmentExpression, AllowAwait, AllowYield, Cursor, ParseError, ParseResult, - TokenParser, + expression::AssignmentExpression, AllowAwait, AllowYield, Cursor, ParseError, TokenParser, }, }; @@ -47,9 +49,9 @@ impl ArrayLiteral { } impl TokenParser for ArrayLiteral { - type Output = Node; + type Output = ArrayDecl; - fn parse(self, cursor: &mut Cursor<'_>) -> ParseResult { + fn parse(self, cursor: &mut Cursor<'_>) -> Result { let mut elements = Vec::new(); loop { @@ -77,6 +79,6 @@ impl TokenParser for ArrayLiteral { cursor.next_if(Punctuator::Comma); } - Ok(Node::array_decl(elements)) + Ok(elements.into()) } } diff --git a/boa/src/syntax/parser/expression/primary/array_initializer/tests.rs b/boa/src/syntax/parser/expression/primary/array_initializer/tests.rs index 1da63882b6..d2b51d6177 100644 --- a/boa/src/syntax/parser/expression/primary/array_initializer/tests.rs +++ b/boa/src/syntax/parser/expression/primary/array_initializer/tests.rs @@ -1,14 +1,14 @@ // ! Tests for array initializer parsing. use crate::syntax::{ - ast::{constant::Const, node::Node}, + ast::{node::ArrayDecl, Const}, parser::tests::check_parser, }; /// Checks an empty array. #[test] fn check_empty() { - check_parser("[]", vec![Node::array_decl(Vec::new())]); + check_parser("[]", vec![ArrayDecl::from(vec![]).into()]); } /// Checks an array with empty slot. @@ -16,7 +16,7 @@ fn check_empty() { fn check_empty_slot() { check_parser( "[,]", - vec![Node::array_decl(vec![Node::Const(Const::Undefined)])], + vec![ArrayDecl::from(vec![Const::Undefined.into()]).into()], ); } @@ -25,11 +25,12 @@ fn check_empty_slot() { fn check_numeric_array() { check_parser( "[1, 2, 3]", - vec![Node::array_decl(vec![ - Node::const_node(1), - Node::const_node(2), - Node::const_node(3), - ])], + vec![ArrayDecl::from(vec![ + Const::from(1).into(), + Const::from(2).into(), + Const::from(3).into(), + ]) + .into()], ); } @@ -38,11 +39,12 @@ fn check_numeric_array() { fn check_numeric_array_trailing() { check_parser( "[1, 2, 3,]", - vec![Node::array_decl(vec![ - Node::const_node(1), - Node::const_node(2), - Node::const_node(3), - ])], + vec![ArrayDecl::from(vec![ + Const::from(1).into(), + Const::from(2).into(), + Const::from(3).into(), + ]) + .into()], ); } @@ -51,12 +53,13 @@ fn check_numeric_array_trailing() { fn check_numeric_array_elision() { check_parser( "[1, 2, , 3]", - vec![Node::array_decl(vec![ - Node::const_node(1), - Node::const_node(2), - Node::Const(Const::Undefined), - Node::const_node(3), - ])], + vec![ArrayDecl::from(vec![ + Const::from(1).into(), + Const::from(2).into(), + Const::Undefined.into(), + Const::from(3).into(), + ]) + .into()], ); } @@ -65,13 +68,14 @@ fn check_numeric_array_elision() { fn check_numeric_array_repeated_elision() { check_parser( "[1, 2, ,, 3]", - vec![Node::array_decl(vec![ - Node::const_node(1), - Node::const_node(2), - Node::Const(Const::Undefined), - Node::Const(Const::Undefined), - Node::const_node(3), - ])], + vec![ArrayDecl::from(vec![ + Const::from(1).into(), + Const::from(2).into(), + Const::Undefined.into(), + Const::Undefined.into(), + Const::from(3).into(), + ]) + .into()], ); } @@ -80,11 +84,12 @@ fn check_numeric_array_repeated_elision() { fn check_combined() { check_parser( "[1, \"a\", 2]", - vec![Node::array_decl(vec![ - Node::const_node(1), - Node::const_node("a"), - Node::const_node(2), - ])], + vec![ArrayDecl::from(vec![ + Const::from(1).into(), + Const::from("a").into(), + Const::from(2).into(), + ]) + .into()], ); } @@ -93,10 +98,11 @@ fn check_combined() { fn check_combined_empty_str() { check_parser( "[1, \"\", 2]", - vec![Node::array_decl(vec![ - Node::const_node(1), - Node::const_node(""), - Node::const_node(2), - ])], + vec![ArrayDecl::from(vec![ + Const::from(1).into(), + Const::from("").into(), + Const::from(2).into(), + ]) + .into()], ); } diff --git a/boa/src/syntax/parser/expression/primary/function_expression.rs b/boa/src/syntax/parser/expression/primary/function_expression.rs index 0f52e10a61..f27bcab26b 100644 --- a/boa/src/syntax/parser/expression/primary/function_expression.rs +++ b/boa/src/syntax/parser/expression/primary/function_expression.rs @@ -8,11 +8,11 @@ //! [spec]: https://tc39.es/ecma262/#prod-FunctionExpression use crate::syntax::{ - ast::{node::Node, punc::Punctuator}, + ast::{node::FunctionExpr, Punctuator}, parser::{ function::{FormalParameters, FunctionBody}, statement::BindingIdentifier, - Cursor, ParseResult, TokenParser, + Cursor, ParseError, TokenParser, }, }; @@ -28,9 +28,9 @@ use crate::syntax::{ pub(super) struct FunctionExpression; impl TokenParser for FunctionExpression { - type Output = Node; + type Output = FunctionExpr; - fn parse(self, cursor: &mut Cursor<'_>) -> ParseResult { + fn parse(self, cursor: &mut Cursor<'_>) -> Result { let name = BindingIdentifier::new(false, false).try_parse(cursor); cursor.expect(Punctuator::OpenParen, "function expression")?; @@ -40,12 +40,10 @@ impl TokenParser for FunctionExpression { cursor.expect(Punctuator::CloseParen, "function expression")?; cursor.expect(Punctuator::OpenBlock, "function expression")?; - let body = FunctionBody::new(false, false) - .parse(cursor) - .map(Node::statement_list)?; + let body = FunctionBody::new(false, false).parse(cursor)?; cursor.expect(Punctuator::CloseBlock, "function expression")?; - Ok(Node::function_expr::<_, String, _, _>(name, params, body)) + Ok(FunctionExpr::new(name, params, body)) } } diff --git a/boa/src/syntax/parser/expression/primary/mod.rs b/boa/src/syntax/parser/expression/primary/mod.rs index 34c1b8f635..62cdfb525b 100644 --- a/boa/src/syntax/parser/expression/primary/mod.rs +++ b/boa/src/syntax/parser/expression/primary/mod.rs @@ -20,8 +20,9 @@ use self::{ use super::Expression; use crate::syntax::{ ast::{ - constant::Const, keyword::Keyword, node::Node, punc::Punctuator, token::NumericLiteral, - token::TokenKind, + node::{Call, Identifier, New, Node}, + token::NumericLiteral, + Const, Keyword, Punctuator, TokenKind, }, parser::{AllowAwait, AllowYield, Cursor, ParseError, ParseResult, TokenParser}, }; @@ -64,7 +65,9 @@ impl TokenParser for PrimaryExpression { match &tok.kind { TokenKind::Keyword(Keyword::This) => Ok(Node::This), // TokenKind::Keyword(Keyword::Arguments) => Ok(Node::new(NodeBase::Arguments, tok.pos)), - TokenKind::Keyword(Keyword::Function) => FunctionExpression.parse(cursor), + TokenKind::Keyword(Keyword::Function) => { + FunctionExpression.parse(cursor).map(Node::from) + } TokenKind::Punctuator(Punctuator::OpenParen) => { let expr = Expression::new(true, self.allow_yield, self.allow_await).parse(cursor)?; @@ -72,30 +75,38 @@ impl TokenParser for PrimaryExpression { Ok(expr) } TokenKind::Punctuator(Punctuator::OpenBracket) => { - ArrayLiteral::new(self.allow_yield, self.allow_await).parse(cursor) + ArrayLiteral::new(self.allow_yield, self.allow_await) + .parse(cursor) + .map(Node::ArrayDecl) } TokenKind::Punctuator(Punctuator::OpenBlock) => { ObjectLiteral::new(self.allow_yield, self.allow_await).parse(cursor) } - TokenKind::BooleanLiteral(boolean) => Ok(Node::const_node(*boolean)), + TokenKind::BooleanLiteral(boolean) => Ok(Const::from(*boolean).into()), // TODO: ADD TokenKind::UndefinedLiteral - TokenKind::Identifier(ref i) if i == "undefined" => Ok(Node::Const(Const::Undefined)), - TokenKind::NullLiteral => Ok(Node::Const(Const::Null)), - TokenKind::Identifier(ident) => Ok(Node::local(ident)), // TODO: IdentifierReference - TokenKind::StringLiteral(s) => Ok(Node::const_node(s)), - TokenKind::NumericLiteral(NumericLiteral::Integer(num)) => Ok(Node::const_node(*num)), - TokenKind::NumericLiteral(NumericLiteral::Rational(num)) => Ok(Node::const_node(*num)), + TokenKind::Identifier(ref i) if i.as_ref() == "undefined" => { + Ok(Const::Undefined.into()) + } + TokenKind::NullLiteral => Ok(Const::Null.into()), + TokenKind::Identifier(ident) => Ok(Identifier::from(ident.as_ref()).into()), // TODO: IdentifierReference + TokenKind::StringLiteral(s) => Ok(Const::from(s.as_ref()).into()), + TokenKind::NumericLiteral(NumericLiteral::Integer(num)) => Ok(Const::from(*num).into()), + TokenKind::NumericLiteral(NumericLiteral::Rational(num)) => { + Ok(Const::from(*num).into()) + } TokenKind::NumericLiteral(NumericLiteral::BigInt(num)) => { - Ok(Node::const_node(num.clone())) + Ok(Const::from(num.clone()).into()) + } + TokenKind::RegularExpressionLiteral(body, flags) => { + Ok(Node::from(New::from(Call::new( + Identifier::from("RegExp"), + vec![ + Const::from(body.as_ref()).into(), + Const::from(flags.to_string()).into(), + ], + )))) } - TokenKind::RegularExpressionLiteral(body, flags) => Ok(Node::new(Node::call( - Node::local("RegExp"), - vec![Node::const_node(body), Node::const_node(flags)], - ))), - _ => Err(ParseError::Unexpected( - tok.clone(), - Some("primary expression"), - )), + _ => Err(ParseError::unexpected(tok.clone(), "primary expression")), } } } diff --git a/boa/src/syntax/parser/expression/primary/object_initializer/mod.rs b/boa/src/syntax/parser/expression/primary/object_initializer/mod.rs index d3638e328a..f1c6785400 100644 --- a/boa/src/syntax/parser/expression/primary/object_initializer/mod.rs +++ b/boa/src/syntax/parser/expression/primary/object_initializer/mod.rs @@ -12,9 +12,9 @@ mod tests; use crate::syntax::{ ast::{ - node::{self, MethodDefinitionKind, Node}, - punc::Punctuator, + node::{self, FunctionExpr, MethodDefinitionKind, Node}, token::{Token, TokenKind}, + Punctuator, }, parser::{ expression::AssignmentExpression, @@ -71,7 +71,7 @@ impl TokenParser for ObjectLiteral { if cursor.next_if(Punctuator::Comma).is_none() { let next_token = cursor.next().ok_or(ParseError::AbruptEnd)?; - return Err(ParseError::Expected( + return Err(ParseError::expected( vec![ TokenKind::Punctuator(Punctuator::Comma), TokenKind::Punctuator(Punctuator::CloseBlock), @@ -129,7 +129,7 @@ impl TokenParser for PropertyDefinition { if cursor.next_if(Punctuator::Colon).is_some() { let val = AssignmentExpression::new(true, self.allow_yield, self.allow_await) .parse(cursor)?; - return Ok(node::PropertyDefinition::Property(prop_name, val)); + return Ok(node::PropertyDefinition::property(prop_name, val)); } if cursor @@ -143,12 +143,9 @@ impl TokenParser for PropertyDefinition { let pos = cursor .peek(0) - .map(|tok| tok.pos) + .map(|tok| tok.span().start()) .ok_or(ParseError::AbruptEnd)?; - Err(ParseError::General( - "expected property definition", - Some(pos), - )) + Err(ParseError::general("expected property definition", pos)) } } @@ -200,17 +197,17 @@ impl TokenParser for MethodDefinition { cursor.expect(Punctuator::CloseParen, "method definition")?; if idn == "get" { if !params.is_empty() { - return Err(ParseError::Unexpected( + return Err(ParseError::unexpected( first_param, - Some("getter functions must have no arguments"), + "getter functions must have no arguments", )); } (MethodDefinitionKind::Get, prop_name, params) } else { if params.len() != 1 { - return Err(ParseError::Unexpected( + return Err(ParseError::unexpected( first_param, - Some("setter functions must have one argument"), + "setter functions must have one argument", )); } (MethodDefinitionKind::Set, prop_name, params) @@ -231,18 +228,16 @@ impl TokenParser for MethodDefinition { TokenKind::Punctuator(Punctuator::OpenBlock), "property method definition", )?; - let body = FunctionBody::new(false, false) - .parse(cursor) - .map(Node::statement_list)?; + let body = FunctionBody::new(false, false).parse(cursor)?; cursor.expect( TokenKind::Punctuator(Punctuator::CloseBlock), "property method definition", )?; - Ok(node::PropertyDefinition::MethodDefinition( + Ok(node::PropertyDefinition::method_definition( methodkind, prop_name, - Node::function_expr::<_, String, _, _>(None, params, body), + FunctionExpr::new(None, params, body), )) } } diff --git a/boa/src/syntax/parser/expression/primary/object_initializer/tests.rs b/boa/src/syntax/parser/expression/primary/object_initializer/tests.rs index 54e82a5592..f3b6590a0c 100644 --- a/boa/src/syntax/parser/expression/primary/object_initializer/tests.rs +++ b/boa/src/syntax/parser/expression/primary/object_initializer/tests.rs @@ -1,5 +1,11 @@ use crate::syntax::{ - ast::node::{FormalParameter, MethodDefinitionKind, Node, PropertyDefinition}, + ast::{ + node::{ + ConstDecl, ConstDeclList, FormalParameter, FunctionExpr, MethodDefinitionKind, Node, + PropertyDefinition, + }, + Const, + }, parser::tests::check_parser, }; @@ -7,8 +13,8 @@ use crate::syntax::{ #[test] fn check_object_literal() { let object_properties = vec![ - PropertyDefinition::property("a", Node::const_node(true)), - PropertyDefinition::property("b", Node::const_node(false)), + PropertyDefinition::property("a", Const::from(true)), + PropertyDefinition::property("b", Const::from(false)), ]; check_parser( @@ -17,10 +23,9 @@ fn check_object_literal() { b: false, }; ", - vec![Node::const_decl(vec![( - String::from("x"), - Node::object(object_properties), - )])], + vec![ + ConstDeclList::from(vec![ConstDecl::new("x", Node::object(object_properties))]).into(), + ], ); } @@ -28,11 +33,11 @@ fn check_object_literal() { #[test] fn check_object_short_function() { let object_properties = vec![ - PropertyDefinition::property("a", Node::const_node(true)), + PropertyDefinition::property("a", Const::from(true)), PropertyDefinition::method_definition( MethodDefinitionKind::Ordinary, "b", - Node::function_expr::<_, String, _, _>(None, Vec::new(), Node::statement_list(vec![])), + FunctionExpr::new(None, vec![], vec![]), ), ]; @@ -42,10 +47,9 @@ fn check_object_short_function() { b() {}, }; ", - vec![Node::const_decl(vec![( - String::from("x"), - Node::object(object_properties), - )])], + vec![ + ConstDeclList::from(vec![ConstDecl::new("x", Node::object(object_properties))]).into(), + ], ); } @@ -53,14 +57,14 @@ fn check_object_short_function() { #[test] fn check_object_short_function_arguments() { let object_properties = vec![ - PropertyDefinition::property("a", Node::const_node(true)), + PropertyDefinition::property("a", Const::from(true)), PropertyDefinition::method_definition( MethodDefinitionKind::Ordinary, "b", - Node::FunctionExpr( + FunctionExpr::new( None, - Box::new([FormalParameter::new("test", None, false)]), - Box::new(Node::StatementList(Box::new([]))), + vec![FormalParameter::new("test", None, false)], + vec![], ), ), ]; @@ -71,25 +75,20 @@ fn check_object_short_function_arguments() { b(test) {} }; ", - vec![Node::const_decl(vec![( - String::from("x"), - Node::object(object_properties), - )])], + vec![ + ConstDeclList::from(vec![ConstDecl::new("x", Node::object(object_properties))]).into(), + ], ); } #[test] fn check_object_getter() { let object_properties = vec![ - PropertyDefinition::property("a", Node::const_node(true)), + PropertyDefinition::property("a", Const::from(true)), PropertyDefinition::method_definition( MethodDefinitionKind::Get, "b", - Node::FunctionExpr( - None, - Box::new([]), - Box::new(Node::statement_list(Vec::new())), - ), + FunctionExpr::new(None, vec![], vec![]), ), ]; @@ -99,24 +98,23 @@ fn check_object_getter() { get b() {} }; ", - vec![Node::const_decl(vec![( - String::from("x"), - Node::object(object_properties), - )])], + vec![ + ConstDeclList::from(vec![ConstDecl::new("x", Node::object(object_properties))]).into(), + ], ); } #[test] fn check_object_setter() { let object_properties = vec![ - PropertyDefinition::property("a", Node::const_node(true)), + PropertyDefinition::property("a", Const::from(true)), PropertyDefinition::method_definition( MethodDefinitionKind::Set, "b", - Node::function_expr::<_, String, _, _>( + FunctionExpr::new( None, vec![FormalParameter::new("test", None, false)], - Node::statement_list(Vec::new()), + vec![], ), ), ]; @@ -127,9 +125,8 @@ fn check_object_setter() { set b(test) {} }; ", - vec![Node::const_decl(vec![( - String::from("x"), - Node::object(object_properties), - )])], + vec![ + ConstDeclList::from(vec![ConstDecl::new("x", Node::object(object_properties))]).into(), + ], ); } diff --git a/boa/src/syntax/parser/expression/primary/tests.rs b/boa/src/syntax/parser/expression/primary/tests.rs index 19f76af5be..e13a4d8401 100644 --- a/boa/src/syntax/parser/expression/primary/tests.rs +++ b/boa/src/syntax/parser/expression/primary/tests.rs @@ -1,10 +1,10 @@ -use crate::syntax::{ast::node::Node, parser::tests::check_parser}; +use crate::syntax::{ast::Const, parser::tests::check_parser}; #[test] fn check_string() { // Check empty string - check_parser("\"\"", vec![Node::const_node("")]); + check_parser("\"\"", vec![Const::from("").into()]); // Check non-empty string - check_parser("\"hello\"", vec![Node::const_node("hello")]); + check_parser("\"hello\"", vec![Const::from("hello").into()]); } diff --git a/boa/src/syntax/parser/expression/tests.rs b/boa/src/syntax/parser/expression/tests.rs index 864783e05e..69816e4dff 100644 --- a/boa/src/syntax/parser/expression/tests.rs +++ b/boa/src/syntax/parser/expression/tests.rs @@ -1,6 +1,9 @@ use crate::syntax::{ - ast::node::Node, - ast::op::{AssignOp, BinOp, BitOp, CompOp, NumOp}, + ast::op::{AssignOp, BitOp, CompOp, NumOp}, + ast::{ + node::{BinOp, Identifier}, + Const, + }, parser::tests::check_parser, }; @@ -9,75 +12,51 @@ use crate::syntax::{ fn check_numeric_operations() { check_parser( "a + b", - vec![Node::bin_op(NumOp::Add, Node::local("a"), Node::local("b"))], + vec![BinOp::new(NumOp::Add, Identifier::from("a"), Identifier::from("b")).into()], ); check_parser( "a+1", - vec![Node::bin_op( - NumOp::Add, - Node::local("a"), - Node::const_node(1), - )], + vec![BinOp::new(NumOp::Add, Identifier::from("a"), Const::from(1)).into()], ); check_parser( "a - b", - vec![Node::bin_op(NumOp::Sub, Node::local("a"), Node::local("b"))], + vec![BinOp::new(NumOp::Sub, Identifier::from("a"), Identifier::from("b")).into()], ); check_parser( "a-1", - vec![Node::bin_op( - NumOp::Sub, - Node::local("a"), - Node::const_node(1), - )], + vec![BinOp::new(NumOp::Sub, Identifier::from("a"), Const::from(1)).into()], ); check_parser( "a / b", - vec![Node::bin_op(NumOp::Div, Node::local("a"), Node::local("b"))], + vec![BinOp::new(NumOp::Div, Identifier::from("a"), Identifier::from("b")).into()], ); check_parser( "a/2", - vec![Node::bin_op( - NumOp::Div, - Node::local("a"), - Node::const_node(2), - )], + vec![BinOp::new(NumOp::Div, Identifier::from("a"), Const::from(2)).into()], ); check_parser( "a * b", - vec![Node::bin_op(NumOp::Mul, Node::local("a"), Node::local("b"))], + vec![BinOp::new(NumOp::Mul, Identifier::from("a"), Identifier::from("b")).into()], ); check_parser( "a*2", - vec![Node::bin_op( - NumOp::Mul, - Node::local("a"), - Node::const_node(2), - )], + vec![BinOp::new(NumOp::Mul, Identifier::from("a"), Const::from(2)).into()], ); check_parser( "a ** b", - vec![Node::bin_op(NumOp::Exp, Node::local("a"), Node::local("b"))], + vec![BinOp::new(NumOp::Exp, Identifier::from("a"), Identifier::from("b")).into()], ); check_parser( "a**2", - vec![Node::bin_op( - NumOp::Exp, - Node::local("a"), - Node::const_node(2), - )], + vec![BinOp::new(NumOp::Exp, Identifier::from("a"), Const::from(2)).into()], ); check_parser( "a % b", - vec![Node::bin_op(NumOp::Mod, Node::local("a"), Node::local("b"))], + vec![BinOp::new(NumOp::Mod, Identifier::from("a"), Identifier::from("b")).into()], ); check_parser( "a%2", - vec![Node::bin_op( - NumOp::Mod, - Node::local("a"), - Node::const_node(2), - )], + vec![BinOp::new(NumOp::Mod, Identifier::from("a"), Const::from(2)).into()], ); } @@ -86,19 +65,20 @@ fn check_numeric_operations() { fn check_complex_numeric_operations() { check_parser( "a + d*(b-3)+1", - vec![Node::bin_op( + vec![BinOp::new( NumOp::Add, - Node::bin_op( + BinOp::new( NumOp::Add, - Node::local("a"), - Node::bin_op( + Identifier::from("a"), + BinOp::new( NumOp::Mul, - Node::local("d"), - Node::bin_op(NumOp::Sub, Node::local("b"), Node::const_node(3)), + Identifier::from("d"), + BinOp::new(NumOp::Sub, Identifier::from("b"), Const::from(3)), ), ), - Node::const_node(1), - )], + Const::from(1), + ) + .into()], ); } @@ -107,87 +87,47 @@ fn check_complex_numeric_operations() { fn check_bitwise_operations() { check_parser( "a & b", - vec![Node::bin_op( - BinOp::Bit(BitOp::And), - Node::local("a"), - Node::local("b"), - )], + vec![BinOp::new(BitOp::And, Identifier::from("a"), Identifier::from("b")).into()], ); check_parser( "a&b", - vec![Node::bin_op( - BinOp::Bit(BitOp::And), - Node::local("a"), - Node::local("b"), - )], + vec![BinOp::new(BitOp::And, Identifier::from("a"), Identifier::from("b")).into()], ); check_parser( "a | b", - vec![Node::bin_op( - BinOp::Bit(BitOp::Or), - Node::local("a"), - Node::local("b"), - )], + vec![BinOp::new(BitOp::Or, Identifier::from("a"), Identifier::from("b")).into()], ); check_parser( "a|b", - vec![Node::bin_op( - BinOp::Bit(BitOp::Or), - Node::local("a"), - Node::local("b"), - )], + vec![BinOp::new(BitOp::Or, Identifier::from("a"), Identifier::from("b")).into()], ); check_parser( "a ^ b", - vec![Node::bin_op( - BinOp::Bit(BitOp::Xor), - Node::local("a"), - Node::local("b"), - )], + vec![BinOp::new(BitOp::Xor, Identifier::from("a"), Identifier::from("b")).into()], ); check_parser( "a^b", - vec![Node::bin_op( - BinOp::Bit(BitOp::Xor), - Node::local("a"), - Node::local("b"), - )], + vec![BinOp::new(BitOp::Xor, Identifier::from("a"), Identifier::from("b")).into()], ); check_parser( "a << b", - vec![Node::bin_op( - BinOp::Bit(BitOp::Shl), - Node::local("a"), - Node::local("b"), - )], + vec![BinOp::new(BitOp::Shl, Identifier::from("a"), Identifier::from("b")).into()], ); check_parser( "a<> b", - vec![Node::bin_op( - BinOp::Bit(BitOp::Shr), - Node::local("a"), - Node::local("b"), - )], + vec![BinOp::new(BitOp::Shr, Identifier::from("a"), Identifier::from("b")).into()], ); check_parser( "a>>b", - vec![Node::bin_op( - BinOp::Bit(BitOp::Shr), - Node::local("a"), - Node::local("b"), - )], + vec![BinOp::new(BitOp::Shr, Identifier::from("a"), Identifier::from("b")).into()], ); } @@ -196,99 +136,56 @@ fn check_bitwise_operations() { fn check_assign_operations() { check_parser( "a += b", - vec![Node::bin_op( - BinOp::Assign(AssignOp::Add), - Node::local("a"), - Node::local("b"), - )], + vec![BinOp::new(AssignOp::Add, Identifier::from("a"), Identifier::from("b")).into()], ); check_parser( "a -= b", - vec![Node::bin_op( - BinOp::Assign(AssignOp::Sub), - Node::local("a"), - Node::local("b"), - )], + vec![BinOp::new(AssignOp::Sub, Identifier::from("a"), Identifier::from("b")).into()], ); check_parser( "a *= b", - vec![Node::bin_op( - BinOp::Assign(AssignOp::Mul), - Node::local("a"), - Node::local("b"), - )], + vec![BinOp::new(AssignOp::Mul, Identifier::from("a"), Identifier::from("b")).into()], ); check_parser( "a **= b", - vec![Node::bin_op( - BinOp::Assign(AssignOp::Exp), - Node::local("a"), - Node::local("b"), - )], + vec![BinOp::new(AssignOp::Exp, Identifier::from("a"), Identifier::from("b")).into()], ); check_parser( "a /= b", - vec![Node::bin_op( - BinOp::Assign(AssignOp::Div), - Node::local("a"), - Node::local("b"), - )], + vec![BinOp::new(AssignOp::Div, Identifier::from("a"), Identifier::from("b")).into()], ); check_parser( "a %= b", - vec![Node::bin_op( - BinOp::Assign(AssignOp::Mod), - Node::local("a"), - Node::local("b"), - )], + vec![BinOp::new(AssignOp::Mod, Identifier::from("a"), Identifier::from("b")).into()], ); check_parser( "a &= b", - vec![Node::bin_op( - BinOp::Assign(AssignOp::And), - Node::local("a"), - Node::local("b"), - )], + vec![BinOp::new(AssignOp::And, Identifier::from("a"), Identifier::from("b")).into()], ); check_parser( "a |= b", - vec![Node::bin_op( - BinOp::Assign(AssignOp::Or), - Node::local("a"), - Node::local("b"), - )], + vec![BinOp::new(AssignOp::Or, Identifier::from("a"), Identifier::from("b")).into()], ); check_parser( "a ^= b", - vec![Node::bin_op( - BinOp::Assign(AssignOp::Xor), - Node::local("a"), - Node::local("b"), - )], + vec![BinOp::new(AssignOp::Xor, Identifier::from("a"), Identifier::from("b")).into()], ); check_parser( "a <<= b", - vec![Node::bin_op( - BinOp::Assign(AssignOp::Shl), - Node::local("a"), - Node::local("b"), - )], + vec![BinOp::new(AssignOp::Shl, Identifier::from("a"), Identifier::from("b")).into()], ); check_parser( "a >>= b", - vec![Node::bin_op( - BinOp::Assign(AssignOp::Shr), - Node::local("a"), - Node::local("b"), - )], + vec![BinOp::new(AssignOp::Shr, Identifier::from("a"), Identifier::from("b")).into()], ); check_parser( "a %= 10 / 2", - vec![Node::bin_op( - BinOp::Assign(AssignOp::Mod), - Node::local("a"), - Node::bin_op(NumOp::Div, Node::const_node(10), Node::const_node(2)), - )], + vec![BinOp::new( + AssignOp::Mod, + Identifier::from("a"), + BinOp::new(NumOp::Div, Const::from(10), Const::from(2)), + ) + .into()], ); } @@ -296,42 +193,42 @@ fn check_assign_operations() { fn check_relational_operations() { check_parser( "a < b", - vec![Node::bin_op( - BinOp::Comp(CompOp::LessThan), - Node::Local(String::from("a")), - Node::Local(String::from("b")), - )], + vec![BinOp::new( + CompOp::LessThan, + Identifier::from("a"), + Identifier::from("b"), + ) + .into()], ); check_parser( "a > b", - vec![Node::bin_op( - BinOp::Comp(CompOp::GreaterThan), - Node::Local(String::from("a")), - Node::Local(String::from("b")), - )], + vec![BinOp::new( + CompOp::GreaterThan, + Identifier::from("a"), + Identifier::from("b"), + ) + .into()], ); check_parser( "a <= b", - vec![Node::bin_op( - BinOp::Comp(CompOp::LessThanOrEqual), - Node::Local(String::from("a")), - Node::Local(String::from("b")), - )], + vec![BinOp::new( + CompOp::LessThanOrEqual, + Identifier::from("a"), + Identifier::from("b"), + ) + .into()], ); check_parser( "a >= b", - vec![Node::bin_op( - BinOp::Comp(CompOp::GreaterThanOrEqual), - Node::Local(String::from("a")), - Node::Local(String::from("b")), - )], + vec![BinOp::new( + CompOp::GreaterThanOrEqual, + Identifier::from("a"), + Identifier::from("b"), + ) + .into()], ); check_parser( "p in o", - vec![Node::bin_op( - BinOp::Comp(CompOp::In), - Node::Local(String::from("p")), - Node::Local(String::from("o")), - )], + vec![BinOp::new(CompOp::In, Identifier::from("p"), Identifier::from("o")).into()], ); } diff --git a/boa/src/syntax/parser/expression/unary.rs b/boa/src/syntax/parser/expression/unary.rs index 14a2c7a29b..7a4449155e 100644 --- a/boa/src/syntax/parser/expression/unary.rs +++ b/boa/src/syntax/parser/expression/unary.rs @@ -8,7 +8,11 @@ //! [spec]: https://tc39.es/ecma262/#sec-unary-operators use crate::syntax::{ - ast::{keyword::Keyword, node::Node, op::UnaryOp, punc::Punctuator, token::TokenKind}, + ast::{ + node::{self, Node}, + op::UnaryOp, + Keyword, Punctuator, TokenKind, + }, parser::{ expression::update::UpdateExpression, AllowAwait, AllowYield, Cursor, ParseError, ParseResult, TokenParser, @@ -50,25 +54,25 @@ impl TokenParser for UnaryExpression { let tok = cursor.next().ok_or(ParseError::AbruptEnd)?; match tok.kind { TokenKind::Keyword(Keyword::Delete) => { - Ok(Node::unary_op(UnaryOp::Delete, self.parse(cursor)?)) + Ok(node::UnaryOp::new(UnaryOp::Delete, self.parse(cursor)?).into()) } TokenKind::Keyword(Keyword::Void) => { - Ok(Node::unary_op(UnaryOp::Void, self.parse(cursor)?)) + Ok(node::UnaryOp::new(UnaryOp::Void, self.parse(cursor)?).into()) } TokenKind::Keyword(Keyword::TypeOf) => { - Ok(Node::unary_op(UnaryOp::TypeOf, self.parse(cursor)?)) + Ok(node::UnaryOp::new(UnaryOp::TypeOf, self.parse(cursor)?).into()) } TokenKind::Punctuator(Punctuator::Add) => { - Ok(Node::unary_op(UnaryOp::Plus, self.parse(cursor)?)) + Ok(node::UnaryOp::new(UnaryOp::Plus, self.parse(cursor)?).into()) } TokenKind::Punctuator(Punctuator::Sub) => { - Ok(Node::unary_op(UnaryOp::Minus, self.parse(cursor)?)) + Ok(node::UnaryOp::new(UnaryOp::Minus, self.parse(cursor)?).into()) } TokenKind::Punctuator(Punctuator::Neg) => { - Ok(Node::unary_op(UnaryOp::Tilde, self.parse(cursor)?)) + Ok(node::UnaryOp::new(UnaryOp::Tilde, self.parse(cursor)?).into()) } TokenKind::Punctuator(Punctuator::Not) => { - Ok(Node::unary_op(UnaryOp::Not, self.parse(cursor)?)) + Ok(node::UnaryOp::new(UnaryOp::Not, self.parse(cursor)?).into()) } _ => { cursor.back(); diff --git a/boa/src/syntax/parser/expression/update.rs b/boa/src/syntax/parser/expression/update.rs index 5ba3d839e9..59e3e233c4 100644 --- a/boa/src/syntax/parser/expression/update.rs +++ b/boa/src/syntax/parser/expression/update.rs @@ -7,7 +7,7 @@ use super::left_hand_side::LeftHandSideExpression; use crate::syntax::{ - ast::{node::Node, op::UnaryOp, punc::Punctuator, token::TokenKind}, + ast::{node, op::UnaryOp, Node, Punctuator, TokenKind}, parser::{AllowAwait, AllowYield, Cursor, ParseError, ParseResult, TokenParser}, }; @@ -45,19 +45,21 @@ impl TokenParser for UpdateExpression { match tok.kind { TokenKind::Punctuator(Punctuator::Inc) => { cursor.next().expect("token disappeared"); - return Ok(Node::unary_op( + return Ok(node::UnaryOp::new( UnaryOp::IncrementPre, LeftHandSideExpression::new(self.allow_yield, self.allow_await) .parse(cursor)?, - )); + ) + .into()); } TokenKind::Punctuator(Punctuator::Dec) => { cursor.next().expect("token disappeared"); - return Ok(Node::unary_op( + return Ok(node::UnaryOp::new( UnaryOp::DecrementPre, LeftHandSideExpression::new(self.allow_yield, self.allow_await) .parse(cursor)?, - )); + ) + .into()); } _ => {} } @@ -67,11 +69,11 @@ impl TokenParser for UpdateExpression { match tok.kind { TokenKind::Punctuator(Punctuator::Inc) => { cursor.next().expect("token disappeared"); - return Ok(Node::unary_op(UnaryOp::IncrementPost, lhs)); + return Ok(node::UnaryOp::new(UnaryOp::IncrementPost, lhs).into()); } TokenKind::Punctuator(Punctuator::Dec) => { cursor.next().expect("token disappeared"); - return Ok(Node::unary_op(UnaryOp::DecrementPost, lhs)); + return Ok(node::UnaryOp::new(UnaryOp::DecrementPost, lhs).into()); } _ => {} } diff --git a/boa/src/syntax/parser/function/mod.rs b/boa/src/syntax/parser/function/mod.rs index dc8ef71f3f..6005404ec4 100644 --- a/boa/src/syntax/parser/function/mod.rs +++ b/boa/src/syntax/parser/function/mod.rs @@ -12,9 +12,8 @@ mod tests; use crate::syntax::{ ast::{ - node::{self, Node}, - punc::Punctuator, - token::TokenKind, + node::{self}, + Punctuator, TokenKind, }, parser::{ expression::Initializer, @@ -52,7 +51,7 @@ impl FormalParameters { } impl TokenParser for FormalParameters { - type Output = Vec; + type Output = Box<[node::FormalParameter]>; fn parse(self, cursor: &mut Cursor<'_>) -> Result { let mut params = Vec::new(); @@ -60,7 +59,7 @@ impl TokenParser for FormalParameters { if cursor.peek(0).ok_or(ParseError::AbruptEnd)?.kind == TokenKind::Punctuator(Punctuator::CloseParen) { - return Ok(params); + return Ok(params.into_boxed_slice()); } loop { @@ -80,19 +79,19 @@ impl TokenParser for FormalParameters { } if rest_param { - return Err(ParseError::Unexpected( + return Err(ParseError::unexpected( cursor .peek_prev() .expect("current token disappeared") .clone(), - Some("rest parameter must be the last formal parameter"), + "rest parameter must be the last formal parameter", )); } cursor.expect(Punctuator::Comma, "parameter list")?; } - Ok(params) + Ok(params.into_boxed_slice()) } } @@ -186,7 +185,7 @@ impl TokenParser for FormalParameter { let init = Initializer::new(true, self.allow_yield, self.allow_await).try_parse(cursor); - Ok(Self::Output::new(param, init.map(Box::new), false)) + Ok(Self::Output::new(param, init, false)) } } @@ -225,12 +224,12 @@ impl FunctionStatementList { } impl TokenParser for FunctionStatementList { - type Output = Vec; + type Output = node::StatementList; fn parse(self, cursor: &mut Cursor<'_>) -> Result { if let Some(tk) = cursor.peek(0) { if tk.kind == Punctuator::CloseBlock.into() { - return Ok(Vec::new()); + return Ok(Vec::new().into()); } } diff --git a/boa/src/syntax/parser/function/tests.rs b/boa/src/syntax/parser/function/tests.rs index cbc9c09fb0..7e0b6c2086 100644 --- a/boa/src/syntax/parser/function/tests.rs +++ b/boa/src/syntax/parser/function/tests.rs @@ -1,5 +1,5 @@ use crate::syntax::{ - ast::node::{FormalParameter, Node}, + ast::node::{ArrowFunctionDecl, BinOp, FormalParameter, FunctionDecl, Identifier, Node}, ast::op::NumOp, parser::tests::check_parser, }; @@ -9,11 +9,12 @@ use crate::syntax::{ fn check_basic() { check_parser( "function foo(a) { return a; }", - vec![Node::function_decl( - "foo", + vec![FunctionDecl::new( + Box::from("foo"), vec![FormalParameter::new("a", None, false)], - Node::statement_list(vec![Node::return_node(Node::local("a"))]), - )], + vec![Node::return_node(Identifier::from("a"))], + ) + .into()], ); } @@ -22,11 +23,12 @@ fn check_basic() { fn check_basic_semicolon_insertion() { check_parser( "function foo(a) { return a }", - vec![Node::function_decl( - "foo", + vec![FunctionDecl::new( + Box::from("foo"), vec![FormalParameter::new("a", None, false)], - Node::statement_list(vec![Node::return_node(Node::local("a"))]), - )], + vec![Node::return_node(Identifier::from("a"))], + ) + .into()], ); } @@ -35,11 +37,12 @@ fn check_basic_semicolon_insertion() { fn check_empty_return() { check_parser( "function foo(a) { return; }", - vec![Node::function_decl( - "foo", + vec![FunctionDecl::new( + Box::from("foo"), vec![FormalParameter::new("a", None, false)], - Node::statement_list(vec![Node::Return(None)]), - )], + vec![Node::Return(None)], + ) + .into()], ); } @@ -48,11 +51,12 @@ fn check_empty_return() { fn check_empty_return_semicolon_insertion() { check_parser( "function foo(a) { return }", - vec![Node::function_decl( - "foo", + vec![FunctionDecl::new( + Box::from("foo"), vec![FormalParameter::new("a", None, false)], - Node::statement_list(vec![Node::Return(None)]), - )], + vec![Node::Return(None)], + ) + .into()], ); } @@ -61,14 +65,15 @@ fn check_empty_return_semicolon_insertion() { fn check_rest_operator() { check_parser( "function foo(a, ...b) {}", - vec![Node::function_decl( - "foo", + vec![FunctionDecl::new( + Box::from("foo"), vec![ FormalParameter::new("a", None, false), FormalParameter::new("b", None, true), ], - Node::StatementList(Box::new([])), - )], + vec![], + ) + .into()], ); } @@ -77,10 +82,7 @@ fn check_rest_operator() { fn check_arrow_only_rest() { check_parser( "(...a) => {}", - vec![Node::arrow_function_decl( - vec![FormalParameter::new("a", None, true)], - Node::StatementList(Box::new([])), - )], + vec![ArrowFunctionDecl::new(vec![FormalParameter::new("a", None, true)], vec![]).into()], ); } @@ -89,14 +91,15 @@ fn check_arrow_only_rest() { fn check_arrow_rest() { check_parser( "(a, b, ...c) => {}", - vec![Node::arrow_function_decl( + vec![ArrowFunctionDecl::new( vec![ FormalParameter::new("a", None, false), FormalParameter::new("b", None, false), FormalParameter::new("c", None, true), ], - Node::StatementList(Box::new([])), - )], + vec![], + ) + .into()], ); } @@ -105,17 +108,18 @@ fn check_arrow_rest() { fn check_arrow() { check_parser( "(a, b) => { return a + b; }", - vec![Node::arrow_function_decl( + vec![ArrowFunctionDecl::new( vec![ FormalParameter::new("a", None, false), FormalParameter::new("b", None, false), ], - Node::statement_list(vec![Node::return_node(Node::bin_op( + vec![Node::return_node(BinOp::new( NumOp::Add, - Node::local("a"), - Node::local("b"), - ))]), - )], + Identifier::from("a"), + Identifier::from("b"), + ))], + ) + .into()], ); } @@ -124,17 +128,18 @@ fn check_arrow() { fn check_arrow_semicolon_insertion() { check_parser( "(a, b) => { return a + b }", - vec![Node::arrow_function_decl( + vec![ArrowFunctionDecl::new( vec![ FormalParameter::new("a", None, false), FormalParameter::new("b", None, false), ], - Node::statement_list(vec![Node::return_node(Node::bin_op( + vec![Node::return_node(BinOp::new( NumOp::Add, - Node::local("a"), - Node::local("b"), - ))]), - )], + Identifier::from("a"), + Identifier::from("b"), + ))], + ) + .into()], ); } @@ -143,13 +148,14 @@ fn check_arrow_semicolon_insertion() { fn check_arrow_epty_return() { check_parser( "(a, b) => { return; }", - vec![Node::arrow_function_decl( + vec![ArrowFunctionDecl::new( vec![ FormalParameter::new("a", None, false), FormalParameter::new("b", None, false), ], - Node::statement_list(vec![Node::Return(None)]), - )], + vec![Node::Return(None)], + ) + .into()], ); } @@ -158,12 +164,13 @@ fn check_arrow_epty_return() { fn check_arrow_empty_return_semicolon_insertion() { check_parser( "(a, b) => { return }", - vec![Node::arrow_function_decl( + vec![ArrowFunctionDecl::new( vec![ FormalParameter::new("a", None, false), FormalParameter::new("b", None, false), ], - Node::statement_list(vec![Node::Return(None)]), - )], + vec![Node::Return(None)], + ) + .into()], ); } diff --git a/boa/src/syntax/parser/mod.rs b/boa/src/syntax/parser/mod.rs index d02130a936..79968cd7d3 100644 --- a/boa/src/syntax/parser/mod.rs +++ b/boa/src/syntax/parser/mod.rs @@ -9,7 +9,7 @@ mod statement; mod tests; use self::error::{ParseError, ParseResult}; -use crate::syntax::ast::{node::Node, token::Token}; +use crate::syntax::ast::{node::StatementList, Token}; use cursor::Cursor; /// Trait implemented by parsers. @@ -103,8 +103,8 @@ impl<'a> Parser<'a> { } /// Parse all expressions in the token array - pub fn parse_all(&mut self) -> ParseResult { - Script.parse(&mut self.cursor).map(Node::statement_list) + pub fn parse_all(&mut self) -> Result { + Script.parse(&mut self.cursor) } } @@ -118,13 +118,13 @@ impl<'a> Parser<'a> { pub struct Script; impl TokenParser for Script { - type Output = Vec; + type Output = StatementList; fn parse(self, cursor: &mut Cursor<'_>) -> Result { if cursor.peek(0).is_some() { ScriptBody.parse(cursor) } else { - Ok(Vec::new()) + Ok(StatementList::from(Vec::new())) } } } @@ -139,7 +139,7 @@ impl TokenParser for Script { pub struct ScriptBody; impl TokenParser for ScriptBody { - type Output = Vec; + type Output = StatementList; fn parse(self, cursor: &mut Cursor<'_>) -> Result { self::statement::StatementList::new(false, false, false, false).parse(cursor) diff --git a/boa/src/syntax/parser/statement/block.rs b/boa/src/syntax/parser/statement/block.rs deleted file mode 100644 index fbb3c47e86..0000000000 --- a/boa/src/syntax/parser/statement/block.rs +++ /dev/null @@ -1,203 +0,0 @@ -//! Block statement parsing. -//! -//! More information: -//! - [MDN documentation][mdn] -//! - [ECMAScript specification][spec] -//! -//! [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/block -//! [spec]: https://tc39.es/ecma262/#sec-block - -use super::{declaration::Declaration, Statement}; -use crate::syntax::{ - ast::{keyword::Keyword, node::Node, punc::Punctuator, token::TokenKind}, - parser::{AllowAwait, AllowReturn, AllowYield, Cursor, ParseError, ParseResult, TokenParser}, -}; - -/// A `BlockStatement` is equivalent to a `Block`. -/// -/// More information: -/// - [ECMAScript specification][spec] -/// -/// [spec]: https://tc39.es/ecma262/#prod-BlockStatement -pub(super) type BlockStatement = Block; - -/// Variable declaration list parsing. -/// -/// More information: -/// - [MDN documentation][mdn] -/// - [ECMAScript specification][spec] -/// -/// [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/block -/// [spec]: https://tc39.es/ecma262/#prod-Block -#[derive(Debug, Clone, Copy)] -pub(super) struct Block { - allow_yield: AllowYield, - allow_await: AllowAwait, - allow_return: AllowReturn, -} - -impl Block { - /// Creates a new `Block` parser. - pub(super) fn new(allow_yield: Y, allow_await: A, allow_return: R) -> Self - where - Y: Into, - A: Into, - R: Into, - { - Self { - allow_yield: allow_yield.into(), - allow_await: allow_await.into(), - allow_return: allow_return.into(), - } - } -} - -impl TokenParser for Block { - type Output = Node; - - fn parse(self, cursor: &mut Cursor<'_>) -> ParseResult { - cursor.expect(Punctuator::OpenBlock, "block")?; - if let Some(tk) = cursor.peek(0) { - if tk.kind == TokenKind::Punctuator(Punctuator::CloseBlock) { - cursor.next(); - return Ok(Node::Block(Box::new([]))); - } - } - - let statement_list = - StatementList::new(self.allow_yield, self.allow_await, self.allow_return, true) - .parse(cursor) - .map(Node::block)?; - cursor.expect(Punctuator::CloseBlock, "block")?; - - Ok(statement_list) - } -} - -/// Reads a list of statements. -/// -/// If `break_when_closingbrase` is `true`, it will stop as soon as it finds a `}` character. -/// -/// More information: -/// - [ECMAScript specification][spec] -/// -/// [spec]: https://tc39.es/ecma262/#prod-StatementList -#[derive(Debug, Clone, Copy)] -pub(in crate::syntax::parser) struct StatementList { - allow_yield: AllowYield, - allow_await: AllowAwait, - allow_return: AllowReturn, - break_when_closingbrase: bool, -} - -impl StatementList { - /// Creates a new `StatementList` parser. - pub(in crate::syntax::parser) fn new( - allow_yield: Y, - allow_await: A, - allow_return: R, - break_when_closingbrase: bool, - ) -> Self - where - Y: Into, - A: Into, - R: Into, - { - Self { - allow_yield: allow_yield.into(), - allow_await: allow_await.into(), - allow_return: allow_return.into(), - break_when_closingbrase, - } - } -} - -impl TokenParser for StatementList { - type Output = Vec; - - fn parse(self, cursor: &mut Cursor<'_>) -> Result { - let mut items = Vec::new(); - - loop { - match cursor.peek(0) { - Some(token) if token.kind == TokenKind::Punctuator(Punctuator::CloseBlock) => { - if self.break_when_closingbrase { - break; - } else { - return Err(ParseError::Unexpected(token.clone(), None)); - } - } - None => { - if self.break_when_closingbrase { - return Err(ParseError::AbruptEnd); - } else { - break; - } - } - _ => {} - } - - let item = - StatementListItem::new(self.allow_yield, self.allow_await, self.allow_return) - .parse(cursor)?; - items.push(item); - - // move the cursor forward for any consecutive semicolon. - while cursor.next_if(Punctuator::Semicolon).is_some() {} - } - - Ok(items) - } -} - -/// Statement list item parsing -/// -/// A statement list item can either be an statement or a declaration. -/// -/// More information: -/// - [MDN documentation][mdn] -/// - [ECMAScript specification][spec] -/// -/// [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements -/// [spec]: https://tc39.es/ecma262/#prod-StatementListItem -#[derive(Debug, Clone, Copy)] -struct StatementListItem { - allow_yield: AllowYield, - allow_await: AllowAwait, - allow_return: AllowReturn, -} - -impl StatementListItem { - /// Creates a new `StatementListItem` parser. - fn new(allow_yield: Y, allow_await: A, allow_return: R) -> Self - where - Y: Into, - A: Into, - R: Into, - { - Self { - allow_yield: allow_yield.into(), - allow_await: allow_await.into(), - allow_return: allow_return.into(), - } - } -} - -impl TokenParser for StatementListItem { - type Output = Node; - - fn parse(self, cursor: &mut Cursor<'_>) -> ParseResult { - let tok = cursor.peek(0).ok_or(ParseError::AbruptEnd)?; - - match tok.kind { - TokenKind::Keyword(Keyword::Function) - | TokenKind::Keyword(Keyword::Const) - | TokenKind::Keyword(Keyword::Let) => { - Declaration::new(self.allow_yield, self.allow_await).parse(cursor) - } - _ => { - Statement::new(self.allow_yield, self.allow_await, self.allow_return).parse(cursor) - } - } - } -} diff --git a/boa/src/syntax/parser/statement/block/mod.rs b/boa/src/syntax/parser/statement/block/mod.rs new file mode 100644 index 0000000000..3db21706cb --- /dev/null +++ b/boa/src/syntax/parser/statement/block/mod.rs @@ -0,0 +1,78 @@ +//! Block statement parsing. +//! +//! More information: +//! - [MDN documentation][mdn] +//! - [ECMAScript specification][spec] +//! +//! [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/block +//! [spec]: https://tc39.es/ecma262/#sec-block + +#[cfg(test)] +mod tests; + +use super::StatementList; +use crate::syntax::{ + ast::{node, Punctuator, TokenKind}, + parser::{AllowAwait, AllowReturn, AllowYield, Cursor, ParseError, TokenParser}, +}; + +/// A `BlockStatement` is equivalent to a `Block`. +/// +/// More information: +/// - [ECMAScript specification][spec] +/// +/// [spec]: https://tc39.es/ecma262/#prod-BlockStatement +pub(super) type BlockStatement = Block; + +/// Variable declaration list parsing. +/// +/// More information: +/// - [MDN documentation][mdn] +/// - [ECMAScript specification][spec] +/// +/// [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/block +/// [spec]: https://tc39.es/ecma262/#prod-Block +#[derive(Debug, Clone, Copy)] +pub(super) struct Block { + allow_yield: AllowYield, + allow_await: AllowAwait, + allow_return: AllowReturn, +} + +impl Block { + /// Creates a new `Block` parser. + pub(super) fn new(allow_yield: Y, allow_await: A, allow_return: R) -> Self + where + Y: Into, + A: Into, + R: Into, + { + Self { + allow_yield: allow_yield.into(), + allow_await: allow_await.into(), + allow_return: allow_return.into(), + } + } +} + +impl TokenParser for Block { + type Output = node::Block; + + fn parse(self, cursor: &mut Cursor<'_>) -> Result { + cursor.expect(Punctuator::OpenBlock, "block")?; + if let Some(tk) = cursor.peek(0) { + if tk.kind == TokenKind::Punctuator(Punctuator::CloseBlock) { + cursor.next(); + return Ok(node::Block::from(vec![])); + } + } + + let statement_list = + StatementList::new(self.allow_yield, self.allow_await, self.allow_return, true) + .parse(cursor) + .map(node::Block::from)?; + cursor.expect(Punctuator::CloseBlock, "block")?; + + Ok(statement_list) + } +} diff --git a/boa/src/syntax/parser/statement/block/tests.rs b/boa/src/syntax/parser/statement/block/tests.rs new file mode 100644 index 0000000000..34a748d1c5 --- /dev/null +++ b/boa/src/syntax/parser/statement/block/tests.rs @@ -0,0 +1,104 @@ +//! Block statement parsing tests. + +use crate::syntax::{ + ast::{ + node::{ + Assign, Block, Call, FunctionDecl, Identifier, Node, UnaryOp, VarDecl, VarDeclList, + }, + op, Const, + }, + parser::tests::check_parser, +}; + +/// Helper function to check a block. +// TODO: #[track_caller]: https://github.com/rust-lang/rust/issues/47809 +fn check_block(js: &str, block: B) +where + B: Into>, +{ + check_parser(js, vec![Block::from(block.into()).into()]); +} + +#[test] +fn empty() { + check_block("{}", vec![]); +} + +#[test] +fn non_empty() { + check_block( + r"{ + var a = 10; + a++; + }", + vec![ + VarDeclList::from(vec![VarDecl::new("a", Some(Const::from(10).into()))]).into(), + UnaryOp::new(op::UnaryOp::IncrementPost, Identifier::from("a")).into(), + ], + ); + + check_block( + r"{ + function hello() { + return 10 + } + + var a = hello(); + a++; + }", + vec![ + FunctionDecl::new( + "hello".to_owned().into_boxed_str(), + vec![], + vec![Node::return_node(Const::from(10))], + ) + .into(), + VarDeclList::from(vec![VarDecl::new( + "a", + Node::from(Call::new(Identifier::from("hello"), vec![])), + )]) + .into(), + UnaryOp::new(op::UnaryOp::IncrementPost, Identifier::from("a")).into(), + ], + ); +} + +#[test] +fn hoisting() { + check_block( + r"{ + var a = hello(); + a++; + + function hello() { return 10 } + }", + vec![ + FunctionDecl::new( + "hello".to_owned().into_boxed_str(), + vec![], + vec![Node::return_node(Const::from(10))], + ) + .into(), + VarDeclList::from(vec![VarDecl::new( + "a", + Node::from(Call::new(Identifier::from("hello"), vec![])), + )]) + .into(), + UnaryOp::new(op::UnaryOp::IncrementPost, Identifier::from("a")).into(), + ], + ); + + check_block( + r"{ + a = 10; + a++; + + var a; + }", + vec![ + Assign::new(Identifier::from("a"), Const::from(10)).into(), + UnaryOp::new(op::UnaryOp::IncrementPost, Identifier::from("a")).into(), + VarDeclList::from(vec![VarDecl::new("a", None)]).into(), + ], + ); +} diff --git a/boa/src/syntax/parser/statement/break_stm/mod.rs b/boa/src/syntax/parser/statement/break_stm/mod.rs index de3119a3cc..d203097b05 100644 --- a/boa/src/syntax/parser/statement/break_stm/mod.rs +++ b/boa/src/syntax/parser/statement/break_stm/mod.rs @@ -10,9 +10,10 @@ #[cfg(test)] mod tests; +use super::LabelIdentifier; use crate::syntax::{ - ast::{keyword::Keyword, node::Node, punc::Punctuator, token::TokenKind}, - parser::{AllowAwait, AllowYield, Cursor, ParseError, ParseResult, TokenParser}, + ast::{Keyword, Node, Punctuator, TokenKind}, + parser::{AllowAwait, AllowYield, Cursor, ParseResult, TokenParser}, }; /// Break statement parsing @@ -49,7 +50,7 @@ impl TokenParser for BreakStatement { fn parse(self, cursor: &mut Cursor<'_>) -> ParseResult { cursor.expect(Keyword::Break, "break statement")?; - if let (true, tok) = cursor.peek_semicolon(false) { + let label = if let (true, tok) = cursor.peek_semicolon(false) { match tok { Some(tok) if tok.kind == TokenKind::Punctuator(Punctuator::Semicolon) => { let _ = cursor.next(); @@ -57,23 +58,14 @@ impl TokenParser for BreakStatement { _ => {} } - return Ok(Node::Break(None)); - } - - let tok = cursor.next().ok_or(ParseError::AbruptEnd)?; - // TODO: LabelIdentifier - let node = if let TokenKind::Identifier(name) = &tok.kind { - Node::break_node(name) + None } else { - return Err(ParseError::Expected( - vec![TokenKind::identifier("identifier")], - tok.clone(), - "break statement", - )); - }; + let label = LabelIdentifier::new(self.allow_yield, self.allow_await).parse(cursor)?; + cursor.expect_semicolon(false, "continue statement")?; - cursor.expect_semicolon(false, "break statement")?; + Some(label) + }; - Ok(node) + Ok(Node::Break(label)) } } diff --git a/boa/src/syntax/parser/statement/break_stm/tests.rs b/boa/src/syntax/parser/statement/break_stm/tests.rs index 1284e15d1b..47f960c12b 100644 --- a/boa/src/syntax/parser/statement/break_stm/tests.rs +++ b/boa/src/syntax/parser/statement/break_stm/tests.rs @@ -1,92 +1,121 @@ -use crate::syntax::{ast::node::Node, parser::tests::check_parser}; +use crate::syntax::{ + ast::{ + node::{Block, Node}, + Const, + }, + parser::tests::check_parser, +}; #[test] -fn check_inline() { +fn inline() { check_parser( "while (true) break;", - vec![Node::while_loop(Node::const_node(true), Node::Break(None))], + vec![Node::while_loop(Const::from(true), Node::Break(None))], ); } #[test] -fn check_new_line() { +fn new_line() { check_parser( "while (true) break;", - vec![Node::while_loop(Node::const_node(true), Node::Break(None))], + vec![Node::while_loop(Const::from(true), Node::Break(None))], ); } #[test] -fn check_inline_block_semicolon_insertion() { +fn inline_block_semicolon_insertion() { check_parser( "while (true) {break}", vec![Node::while_loop( - Node::const_node(true), - Node::block(vec![Node::Break(None)]), + Const::from(true), + Block::from(vec![Node::Break(None)]), )], ); } #[test] -fn check_new_line_semicolon_insertion() { +fn new_line_semicolon_insertion() { check_parser( "while (true) { break test }", vec![Node::while_loop( - Node::const_node(true), - Node::block(vec![Node::break_node("test")]), + Const::from(true), + Block::from(vec![Node::break_node("test")]), )], ); } #[test] -fn check_inline_block() { +fn inline_block() { check_parser( "while (true) {break;}", vec![Node::while_loop( - Node::const_node(true), - Node::block(vec![Node::Break(None)]), + Const::from(true), + Block::from(vec![Node::Break(None)]), )], ); } #[test] -fn check_new_line_block() { +fn new_line_block() { check_parser( "while (true) { break test; }", vec![Node::while_loop( - Node::const_node(true), - Node::block(vec![Node::break_node("test")]), + Const::from(true), + Block::from(vec![Node::break_node("test")]), )], ); } #[test] -fn check_new_line_block_empty() { +fn reserved_label() { + check_parser( + "while (true) { + break await; + }", + vec![Node::while_loop( + Const::from(true), + Block::from(vec![Node::break_node("await")]), + )], + ); + + check_parser( + "while (true) { + break yield; + }", + vec![Node::while_loop( + Const::from(true), + Block::from(vec![Node::break_node("yield")]), + )], + ); +} + +#[test] +fn new_line_block_empty() { check_parser( "while (true) { break; }", vec![Node::while_loop( - Node::const_node(true), - Node::block(vec![Node::Break(None)]), + Const::from(true), + Block::from(vec![Node::Break(None)]), )], ); } #[test] -fn check_new_line_block_empty_semicolon_insertion() { +fn new_line_block_empty_semicolon_insertion() { check_parser( "while (true) { break }", vec![Node::while_loop( - Node::const_node(true), - Node::block(vec![Node::Break(None)]), + Const::from(true), + Block::from(vec![Node::Break(None)]), )], ); } diff --git a/boa/src/syntax/parser/statement/continue_stm/mod.rs b/boa/src/syntax/parser/statement/continue_stm/mod.rs index d43b40db34..7da0b5414c 100644 --- a/boa/src/syntax/parser/statement/continue_stm/mod.rs +++ b/boa/src/syntax/parser/statement/continue_stm/mod.rs @@ -10,9 +10,10 @@ #[cfg(test)] mod tests; +use super::LabelIdentifier; use crate::syntax::{ - ast::{keyword::Keyword, node::Node, punc::Punctuator, token::TokenKind}, - parser::{AllowAwait, AllowYield, Cursor, ParseError, ParseResult, TokenParser}, + ast::{Keyword, Node, Punctuator, TokenKind}, + parser::{AllowAwait, AllowYield, Cursor, ParseResult, TokenParser}, }; /// For statement parsing @@ -49,7 +50,7 @@ impl TokenParser for ContinueStatement { fn parse(self, cursor: &mut Cursor<'_>) -> ParseResult { cursor.expect(Keyword::Continue, "continue statement")?; - if let (true, tok) = cursor.peek_semicolon(false) { + let label = if let (true, tok) = cursor.peek_semicolon(false) { match tok { Some(tok) if tok.kind == TokenKind::Punctuator(Punctuator::Semicolon) => { let _ = cursor.next(); @@ -57,23 +58,14 @@ impl TokenParser for ContinueStatement { _ => {} } - return Ok(Node::Continue(None)); - } - - let tok = cursor.next().ok_or(ParseError::AbruptEnd)?; - // TODO: LabelIdentifier - let node = if let TokenKind::Identifier(name) = &tok.kind { - Node::continue_node(name) + None } else { - return Err(ParseError::Expected( - vec![TokenKind::identifier("identifier")], - tok.clone(), - "continue statement", - )); - }; + let label = LabelIdentifier::new(self.allow_yield, self.allow_await).parse(cursor)?; + cursor.expect_semicolon(false, "continue statement")?; - cursor.expect_semicolon(false, "continue statement")?; + Some(label) + }; - Ok(node) + Ok(Node::Continue(label)) } } diff --git a/boa/src/syntax/parser/statement/continue_stm/tests.rs b/boa/src/syntax/parser/statement/continue_stm/tests.rs index f3a1472304..564bde0a5b 100644 --- a/boa/src/syntax/parser/statement/continue_stm/tests.rs +++ b/boa/src/syntax/parser/statement/continue_stm/tests.rs @@ -1,98 +1,121 @@ -use crate::syntax::{ast::node::Node, parser::tests::check_parser}; +use crate::syntax::{ + ast::{ + node::{Block, Node}, + Const, + }, + parser::tests::check_parser, +}; #[test] -fn check_inline() { +fn inline() { check_parser( "while (true) continue;", - vec![Node::while_loop( - Node::const_node(true), - Node::Continue(None), - )], + vec![Node::while_loop(Const::from(true), Node::Continue(None))], ); } #[test] -fn check_new_line() { +fn new_line() { check_parser( "while (true) continue;", - vec![Node::while_loop( - Node::const_node(true), - Node::Continue(None), - )], + vec![Node::while_loop(Const::from(true), Node::Continue(None))], ); } #[test] -fn check_inline_block_semicolon_insertion() { +fn inline_block_semicolon_insertion() { check_parser( "while (true) {continue}", vec![Node::while_loop( - Node::const_node(true), - Node::block(vec![Node::Continue(None)]), + Const::from(true), + Block::from(vec![Node::Continue(None)]), )], ); } #[test] -fn check_new_line_semicolon_insertion() { +fn new_line_semicolon_insertion() { check_parser( "while (true) { continue test }", vec![Node::while_loop( - Node::const_node(true), - Node::block(vec![Node::continue_node("test")]), + Const::from(true), + Block::from(vec![Node::continue_node("test")]), )], ); } #[test] -fn check_inline_block() { +fn inline_block() { check_parser( "while (true) {continue;}", vec![Node::while_loop( - Node::const_node(true), - Node::block(vec![Node::Continue(None)]), + Const::from(true), + Block::from(vec![Node::Continue(None)]), )], ); } #[test] -fn check_new_line_block() { +fn new_line_block() { check_parser( "while (true) { continue test; }", vec![Node::while_loop( - Node::const_node(true), - Node::block(vec![Node::continue_node("test")]), + Const::from(true), + Block::from(vec![Node::continue_node("test")]), + )], + ); +} + +#[test] +fn reserved_label() { + check_parser( + "while (true) { + continue await; + }", + vec![Node::while_loop( + Const::from(true), + Block::from(vec![Node::continue_node("await")]), + )], + ); + + check_parser( + "while (true) { + continue yield; + }", + vec![Node::while_loop( + Const::from(true), + Block::from(vec![Node::continue_node("yield")]), )], ); } #[test] -fn check_new_line_block_empty() { +fn new_line_block_empty() { check_parser( "while (true) { continue; }", vec![Node::while_loop( - Node::const_node(true), - Node::block(vec![Node::Continue(None)]), + Const::from(true), + Block::from(vec![Node::Continue(None)]), )], ); } #[test] -fn check_new_line_block_empty_semicolon_insertion() { +fn new_line_block_empty_semicolon_insertion() { check_parser( "while (true) { continue }", vec![Node::while_loop( - Node::const_node(true), - Node::block(vec![Node::Continue(None)]), + Const::from(true), + Block::from(vec![Node::Continue(None)]), )], ); } diff --git a/boa/src/syntax/parser/statement/declaration/hoistable.rs b/boa/src/syntax/parser/statement/declaration/hoistable.rs index 691066844b..44a942a908 100644 --- a/boa/src/syntax/parser/statement/declaration/hoistable.rs +++ b/boa/src/syntax/parser/statement/declaration/hoistable.rs @@ -6,10 +6,10 @@ //! [spec]: https://tc39.es/ecma262/#prod-HoistableDeclaration use crate::syntax::{ - ast::{keyword::Keyword, node::Node, punc::Punctuator}, + ast::{node::FunctionDecl, Keyword, Node, Punctuator}, parser::{ function::FormalParameters, function::FunctionBody, statement::BindingIdentifier, - AllowAwait, AllowDefault, AllowYield, Cursor, ParseResult, TokenParser, + AllowAwait, AllowDefault, AllowYield, Cursor, ParseError, ParseResult, TokenParser, }, }; @@ -23,12 +23,12 @@ use crate::syntax::{ pub(super) struct HoistableDeclaration { allow_yield: AllowYield, allow_await: AllowAwait, - allow_default: AllowDefault, + is_default: AllowDefault, } impl HoistableDeclaration { /// Creates a new `HoistableDeclaration` parser. - pub(super) fn new(allow_yield: Y, allow_await: A, allow_default: D) -> Self + pub(super) fn new(allow_yield: Y, allow_await: A, is_default: D) -> Self where Y: Into, A: Into, @@ -37,7 +37,7 @@ impl HoistableDeclaration { Self { allow_yield: allow_yield.into(), allow_await: allow_await.into(), - allow_default: allow_default.into(), + is_default: is_default.into(), } } } @@ -47,8 +47,9 @@ impl TokenParser for HoistableDeclaration { fn parse(self, cursor: &mut Cursor<'_>) -> ParseResult { // TODO: check for generators and async functions + generators - FunctionDeclaration::new(self.allow_yield, self.allow_await, self.allow_default) + FunctionDeclaration::new(self.allow_yield, self.allow_await, self.is_default) .parse(cursor) + .map(Node::from) } } @@ -64,12 +65,12 @@ impl TokenParser for HoistableDeclaration { struct FunctionDeclaration { allow_yield: AllowYield, allow_await: AllowAwait, - allow_default: AllowDefault, + is_default: AllowDefault, } impl FunctionDeclaration { /// Creates a new `FunctionDeclaration` parser. - fn new(allow_yield: Y, allow_await: A, allow_default: D) -> Self + fn new(allow_yield: Y, allow_await: A, is_default: D) -> Self where Y: Into, A: Into, @@ -78,17 +79,18 @@ impl FunctionDeclaration { Self { allow_yield: allow_yield.into(), allow_await: allow_await.into(), - allow_default: allow_default.into(), + is_default: is_default.into(), } } } impl TokenParser for FunctionDeclaration { - type Output = Node; + type Output = FunctionDecl; - fn parse(self, cursor: &mut Cursor<'_>) -> ParseResult { + fn parse(self, cursor: &mut Cursor<'_>) -> Result { cursor.expect(Keyword::Function, "function declaration")?; + // TODO: If self.is_default, then this can be empty. let name = BindingIdentifier::new(self.allow_yield, self.allow_await).parse(cursor)?; cursor.expect(Punctuator::OpenParen, "function declaration")?; @@ -98,12 +100,10 @@ impl TokenParser for FunctionDeclaration { cursor.expect(Punctuator::CloseParen, "function declaration")?; cursor.expect(Punctuator::OpenBlock, "function declaration")?; - let body = FunctionBody::new(self.allow_yield, self.allow_await) - .parse(cursor) - .map(Node::statement_list)?; + let body = FunctionBody::new(self.allow_yield, self.allow_await).parse(cursor)?; cursor.expect(Punctuator::CloseBlock, "function declaration")?; - Ok(Node::function_decl(name, params, body)) + Ok(FunctionDecl::new(name, params, body)) } } diff --git a/boa/src/syntax/parser/statement/declaration/lexical.rs b/boa/src/syntax/parser/statement/declaration/lexical.rs index be0a0eb852..ee9b3fd3f1 100644 --- a/boa/src/syntax/parser/statement/declaration/lexical.rs +++ b/boa/src/syntax/parser/statement/declaration/lexical.rs @@ -8,7 +8,10 @@ //! [spec]: https://tc39.es/ecma262/#sec-let-and-const-declarations use crate::syntax::{ - ast::{keyword::Keyword, node::Node, punc::Punctuator, token::TokenKind}, + ast::{ + node::{ConstDecl, ConstDeclList, LetDecl, LetDeclList, Node}, + Keyword, Punctuator, TokenKind, + }, parser::{ expression::Initializer, statement::BindingIdentifier, AllowAwait, AllowIn, AllowYield, Cursor, ParseError, ParseResult, TokenParser, @@ -108,22 +111,22 @@ impl TokenParser for BindingList { let mut const_decls = Vec::new(); loop { - let lexical_binding = + let (ident, init) = LexicalBinding::new(self.allow_in, self.allow_yield, self.allow_await) .parse(cursor)?; if self.is_const { - if let (ident, Some(init)) = lexical_binding { - const_decls.push((ident, init)); + if let Some(init) = init { + const_decls.push(ConstDecl::new(ident, init)); } else { - return Err(ParseError::Expected( + return Err(ParseError::expected( vec![TokenKind::Punctuator(Punctuator::Assign)], cursor.next().ok_or(ParseError::AbruptEnd)?.clone(), "const declaration", )); } } else { - let_decls.push(lexical_binding); + let_decls.push(LetDecl::new(ident, init)); } match cursor.peek_semicolon(false) { @@ -132,7 +135,7 @@ impl TokenParser for BindingList { let _ = cursor.next(); } _ => { - return Err(ParseError::Expected( + return Err(ParseError::expected( vec![ TokenKind::Punctuator(Punctuator::Semicolon), TokenKind::LineTerminator, @@ -145,9 +148,9 @@ impl TokenParser for BindingList { } if self.is_const { - Ok(Node::const_decl(const_decls)) + Ok(ConstDeclList::from(const_decls).into()) } else { - Ok(Node::let_decl(let_decls)) + Ok(LetDeclList::from(let_decls).into()) } } } @@ -181,9 +184,9 @@ impl LexicalBinding { } impl TokenParser for LexicalBinding { - type Output = (String, Option); + type Output = (Box, Option); - fn parse(self, cursor: &mut Cursor<'_>) -> Result<(String, Option), ParseError> { + fn parse(self, cursor: &mut Cursor<'_>) -> Result { let ident = BindingIdentifier::new(self.allow_yield, self.allow_await).parse(cursor)?; let initializer = Initializer::new(self.allow_in, self.allow_yield, self.allow_await).try_parse(cursor); diff --git a/boa/src/syntax/parser/statement/declaration/mod.rs b/boa/src/syntax/parser/statement/declaration/mod.rs index 11355289c3..c8f44aee9f 100644 --- a/boa/src/syntax/parser/statement/declaration/mod.rs +++ b/boa/src/syntax/parser/statement/declaration/mod.rs @@ -14,7 +14,7 @@ mod tests; use self::{hoistable::HoistableDeclaration, lexical::LexicalDeclaration}; use crate::syntax::{ - ast::{keyword::Keyword, node::Node, token::TokenKind}, + ast::{Keyword, Node, TokenKind}, parser::{AllowAwait, AllowYield, Cursor, ParseError, ParseResult, TokenParser}, }; diff --git a/boa/src/syntax/parser/statement/declaration/tests.rs b/boa/src/syntax/parser/statement/declaration/tests.rs index 21ec1749f4..fbc2f64685 100644 --- a/boa/src/syntax/parser/statement/declaration/tests.rs +++ b/boa/src/syntax/parser/statement/declaration/tests.rs @@ -1,5 +1,11 @@ use crate::syntax::{ - ast::node::Node, + ast::{ + node::{ + ConstDecl, ConstDeclList, FunctionDecl, LetDecl, LetDeclList, Node, VarDecl, + VarDeclList, + }, + Const, + }, parser::tests::{check_invalid, check_parser}, }; @@ -8,10 +14,7 @@ use crate::syntax::{ fn var_declaration() { check_parser( "var a = 5;", - vec![Node::var_decl(vec![( - String::from("a"), - Some(Node::const_node(5)), - )])], + vec![VarDeclList::from(vec![VarDecl::new("a", Some(Const::from(5).into()))]).into()], ); } @@ -20,18 +23,12 @@ fn var_declaration() { fn var_declaration_keywords() { check_parser( "var yield = 5;", - vec![Node::var_decl(vec![( - String::from("yield"), - Some(Node::const_node(5)), - )])], + vec![VarDeclList::from(vec![VarDecl::new("yield", Some(Const::from(5).into()))]).into()], ); check_parser( "var await = 5;", - vec![Node::var_decl(vec![( - String::from("await"), - Some(Node::const_node(5)), - )])], + vec![VarDeclList::from(vec![VarDecl::new("await", Some(Const::from(5).into()))]).into()], ); } @@ -40,10 +37,7 @@ fn var_declaration_keywords() { fn var_declaration_no_spaces() { check_parser( "var a=5;", - vec![Node::var_decl(vec![( - String::from("a"), - Some(Node::const_node(5)), - )])], + vec![VarDeclList::from(vec![VarDecl::new("a", Some(Const::from(5).into()))]).into()], ); } @@ -52,7 +46,7 @@ fn var_declaration_no_spaces() { fn empty_var_declaration() { check_parser( "var a;", - vec![Node::var_decl(vec![(String::from("a"), None)])], + vec![VarDeclList::from(vec![VarDecl::new("a", None)]).into()], ); } @@ -61,11 +55,12 @@ fn empty_var_declaration() { fn multiple_var_declaration() { check_parser( "var a = 5, b, c = 6;", - vec![Node::var_decl(vec![ - (String::from("a"), Some(Node::const_node(5))), - (String::from("b"), None), - (String::from("c"), Some(Node::const_node(6))), - ])], + vec![VarDeclList::from(vec![ + VarDecl::new("a", Some(Const::from(5).into())), + VarDecl::new("b", None), + VarDecl::new("c", Some(Const::from(6).into())), + ]) + .into()], ); } @@ -74,10 +69,7 @@ fn multiple_var_declaration() { fn let_declaration() { check_parser( "let a = 5;", - vec![Node::let_decl(vec![( - String::from("a"), - Some(Node::const_node(5)), - )])], + vec![LetDeclList::from(vec![LetDecl::new("a", Node::from(Const::from(5)))]).into()], ); } @@ -86,18 +78,12 @@ fn let_declaration() { fn let_declaration_keywords() { check_parser( "let yield = 5;", - vec![Node::let_decl(vec![( - String::from("yield"), - Some(Node::const_node(5)), - )])], + vec![LetDeclList::from(vec![LetDecl::new("yield", Node::from(Const::from(5)))]).into()], ); check_parser( "let await = 5;", - vec![Node::let_decl(vec![( - String::from("await"), - Some(Node::const_node(5)), - )])], + vec![LetDeclList::from(vec![LetDecl::new("await", Node::from(Const::from(5)))]).into()], ); } @@ -106,10 +92,7 @@ fn let_declaration_keywords() { fn let_declaration_no_spaces() { check_parser( "let a=5;", - vec![Node::let_decl(vec![( - String::from("a"), - Some(Node::const_node(5)), - )])], + vec![LetDeclList::from(vec![LetDecl::new("a", Node::from(Const::from(5)))]).into()], ); } @@ -118,7 +101,7 @@ fn let_declaration_no_spaces() { fn empty_let_declaration() { check_parser( "let a;", - vec![Node::let_decl(vec![(String::from("a"), None)])], + vec![LetDeclList::from(vec![LetDecl::new("a", None)]).into()], ); } @@ -127,11 +110,12 @@ fn empty_let_declaration() { fn multiple_let_declaration() { check_parser( "let a = 5, b, c = 6;", - vec![Node::let_decl(vec![ - (String::from("a"), Some(Node::const_node(5))), - (String::from("b"), None), - (String::from("c"), Some(Node::const_node(6))), - ])], + vec![LetDeclList::from(vec![ + LetDecl::new("a", Node::from(Const::from(5))), + LetDecl::new("b", None), + LetDecl::new("c", Node::from(Const::from(6))), + ]) + .into()], ); } @@ -140,10 +124,7 @@ fn multiple_let_declaration() { fn const_declaration() { check_parser( "const a = 5;", - vec![Node::const_decl(vec![( - String::from("a"), - Node::const_node(5), - )])], + vec![ConstDeclList::from(ConstDecl::new("a", Const::from(5))).into()], ); } @@ -152,18 +133,12 @@ fn const_declaration() { fn const_declaration_keywords() { check_parser( "const yield = 5;", - vec![Node::const_decl(vec![( - String::from("yield"), - Node::const_node(5), - )])], + vec![ConstDeclList::from(ConstDecl::new("yield", Const::from(5))).into()], ); check_parser( "const await = 5;", - vec![Node::const_decl(vec![( - String::from("await"), - Node::const_node(5), - )])], + vec![ConstDeclList::from(ConstDecl::new("await", Const::from(5))).into()], ); } @@ -172,10 +147,7 @@ fn const_declaration_keywords() { fn const_declaration_no_spaces() { check_parser( "const a=5;", - vec![Node::const_decl(vec![( - String::from("a"), - Node::const_node(5), - )])], + vec![ConstDeclList::from(ConstDecl::new("a", Const::from(5))).into()], ); } @@ -190,10 +162,11 @@ fn empty_const_declaration() { fn multiple_const_declaration() { check_parser( "const a = 5, c = 6;", - vec![Node::const_decl(vec![ - (String::from("a"), Node::const_node(5)), - (String::from("c"), Node::const_node(6)), - ])], + vec![ConstDeclList::from(vec![ + ConstDecl::new("a", Const::from(5)), + ConstDecl::new("c", Const::from(6)), + ]) + .into()], ); } @@ -202,11 +175,7 @@ fn multiple_const_declaration() { fn function_declaration() { check_parser( "function hello() {}", - vec![Node::function_decl( - "hello", - vec![], - Node::statement_list(vec![]), - )], + vec![FunctionDecl::new(Box::from("hello"), vec![], vec![]).into()], ); } @@ -215,19 +184,11 @@ fn function_declaration() { fn function_declaration_keywords() { check_parser( "function yield() {}", - vec![Node::function_decl( - "yield", - vec![], - Node::statement_list(vec![]), - )], + vec![FunctionDecl::new(Box::from("yield"), vec![], vec![]).into()], ); check_parser( "function await() {}", - vec![Node::function_decl( - "await", - vec![], - Node::statement_list(vec![]), - )], + vec![FunctionDecl::new(Box::from("await"), vec![], vec![]).into()], ); } diff --git a/boa/src/syntax/parser/statement/if_stm/mod.rs b/boa/src/syntax/parser/statement/if_stm/mod.rs index 8caeab0101..e20840303f 100644 --- a/boa/src/syntax/parser/statement/if_stm/mod.rs +++ b/boa/src/syntax/parser/statement/if_stm/mod.rs @@ -3,7 +3,7 @@ mod tests; use super::Statement; use crate::syntax::{ - ast::{keyword::Keyword, node::Node, punc::Punctuator, token::TokenKind}, + ast::{Keyword, Node, Punctuator, TokenKind}, parser::{ expression::Expression, AllowAwait, AllowReturn, AllowYield, Cursor, ParseResult, TokenParser, diff --git a/boa/src/syntax/parser/statement/iteration/do_while_statement.rs b/boa/src/syntax/parser/statement/iteration/do_while_statement.rs index d7ac6b8831..5b71869af4 100644 --- a/boa/src/syntax/parser/statement/iteration/do_while_statement.rs +++ b/boa/src/syntax/parser/statement/iteration/do_while_statement.rs @@ -8,7 +8,7 @@ //! [spec]: https://tc39.es/ecma262/#sec-do-while-statement use crate::syntax::{ - ast::{keyword::Keyword, node::Node, punc::Punctuator, token::TokenKind}, + ast::{Keyword, Node, Punctuator, TokenKind}, parser::{ expression::Expression, statement::Statement, AllowAwait, AllowReturn, AllowYield, Cursor, ParseError, ParseResult, TokenParser, @@ -62,7 +62,7 @@ impl TokenParser for DoWhileStatement { let next_token = cursor.peek(0).ok_or(ParseError::AbruptEnd)?; if next_token.kind != TokenKind::Keyword(Keyword::While) { - return Err(ParseError::Expected( + return Err(ParseError::expected( vec![TokenKind::Keyword(Keyword::While)], next_token.clone(), "do while statement", diff --git a/boa/src/syntax/parser/statement/iteration/for_statement.rs b/boa/src/syntax/parser/statement/iteration/for_statement.rs index 7e818ee169..c5d2b340a0 100644 --- a/boa/src/syntax/parser/statement/iteration/for_statement.rs +++ b/boa/src/syntax/parser/statement/iteration/for_statement.rs @@ -8,12 +8,15 @@ //! [spec]: https://tc39.es/ecma262/#sec-for-statement use crate::syntax::{ - ast::{keyword::Keyword, node::Node, punc::Punctuator, token::TokenKind}, + ast::{ + node::{ForLoop, Node}, + Const, Keyword, Punctuator, TokenKind, + }, parser::{ expression::Expression, statement::declaration::Declaration, statement::{variable::VariableDeclarationList, Statement}, - AllowAwait, AllowReturn, AllowYield, Cursor, ParseError, ParseResult, TokenParser, + AllowAwait, AllowReturn, AllowYield, Cursor, ParseError, TokenParser, }, }; @@ -53,16 +56,17 @@ impl ForStatement { } impl TokenParser for ForStatement { - type Output = Node; + type Output = ForLoop; - fn parse(self, cursor: &mut Cursor<'_>) -> ParseResult { + fn parse(self, cursor: &mut Cursor<'_>) -> Result { cursor.expect(Keyword::For, "for statement")?; cursor.expect(Punctuator::OpenParen, "for statement")?; let init = match cursor.peek(0).ok_or(ParseError::AbruptEnd)?.kind { TokenKind::Keyword(Keyword::Var) => Some( VariableDeclarationList::new(false, self.allow_yield, self.allow_await) - .parse(cursor)?, + .parse(cursor) + .map(Node::from)?, ), TokenKind::Keyword(Keyword::Let) | TokenKind::Keyword(Keyword::Const) => { Some(Declaration::new(self.allow_yield, self.allow_await).parse(cursor)?) @@ -74,7 +78,7 @@ impl TokenParser for ForStatement { cursor.expect(Punctuator::Semicolon, "for statement")?; let cond = if cursor.next_if(Punctuator::Semicolon).is_some() { - Node::const_node(true) + Const::from(true).into() } else { let step = Expression::new(true, self.allow_yield, self.allow_await).parse(cursor)?; cursor.expect(Punctuator::Semicolon, "for statement")?; @@ -95,8 +99,7 @@ impl TokenParser for ForStatement { let body = Statement::new(self.allow_yield, self.allow_await, self.allow_return).parse(cursor)?; - let for_node = Node::for_loop::<_, _, _, Node, Node, Node, _>(init, cond, step, body); - - Ok(Node::Block(Box::new([for_node]))) + // TODO: do not encapsulate the `for` in a block just to have an inner scope. + Ok(ForLoop::new(init, cond, step, body)) } } diff --git a/boa/src/syntax/parser/statement/iteration/tests.rs b/boa/src/syntax/parser/statement/iteration/tests.rs index 9c71ae2248..7830318907 100644 --- a/boa/src/syntax/parser/statement/iteration/tests.rs +++ b/boa/src/syntax/parser/statement/iteration/tests.rs @@ -1,6 +1,9 @@ use crate::syntax::{ - ast::node::Node, - ast::op::{AssignOp, BinOp, CompOp, UnaryOp}, + ast::{ + node::{BinOp, Block, Call, Identifier, Node, UnaryOp, VarDecl, VarDeclList}, + op::{self, AssignOp, CompOp}, + Const, + }, parser::tests::check_parser, }; @@ -12,12 +15,13 @@ fn check_do_while() { a += 1; } while (true)"#, vec![Node::do_while_loop( - Node::block(vec![Node::bin_op( - BinOp::Assign(AssignOp::Add), - Node::local("a"), - Node::const_node(1), - )]), - Node::const_node(true), + Block::from(vec![BinOp::new( + AssignOp::Add, + Identifier::from("a"), + Const::from(1), + ) + .into()]), + Const::from(true), )], ); } @@ -29,22 +33,24 @@ fn check_do_while_semicolon_insertion() { r#"var i = 0; do {console.log("hello");} while(i++ < 10) console.log("end");"#, vec![ - Node::var_decl(vec![(String::from("i"), Some(Node::const_node(0)))]), + VarDeclList::from(vec![VarDecl::new("i", Some(Const::from(0).into()))]).into(), Node::do_while_loop( - Node::block(vec![Node::call( - Node::get_const_field(Node::local("console"), "log"), - vec![Node::const_node("hello")], - )]), - Node::bin_op( - BinOp::Comp(CompOp::LessThan), - Node::unary_op(UnaryOp::IncrementPost, Node::local("i")), - Node::const_node(10), + Block::from(vec![Call::new( + Node::get_const_field(Identifier::from("console"), "log"), + vec![Const::from("hello").into()], + ) + .into()]), + BinOp::new( + CompOp::LessThan, + UnaryOp::new(op::UnaryOp::IncrementPost, Identifier::from("i")), + Const::from(10), ), ), - Node::call( - Node::get_const_field(Node::local("console"), "log"), - vec![Node::const_node("end")], - ), + Call::new( + Node::get_const_field(Identifier::from("console"), "log"), + vec![Const::from("end").into()], + ) + .into(), ], ); } diff --git a/boa/src/syntax/parser/statement/iteration/while_statement.rs b/boa/src/syntax/parser/statement/iteration/while_statement.rs index 2b9f765ec0..dd8040ae24 100644 --- a/boa/src/syntax/parser/statement/iteration/while_statement.rs +++ b/boa/src/syntax/parser/statement/iteration/while_statement.rs @@ -1,5 +1,5 @@ use crate::syntax::{ - ast::{keyword::Keyword, node::Node, punc::Punctuator}, + ast::{Keyword, Node, Punctuator}, parser::{ expression::Expression, statement::Statement, AllowAwait, AllowReturn, AllowYield, Cursor, ParseResult, TokenParser, diff --git a/boa/src/syntax/parser/statement/mod.rs b/boa/src/syntax/parser/statement/mod.rs index 368ad3cb9f..cf845e8a41 100644 --- a/boa/src/syntax/parser/statement/mod.rs +++ b/boa/src/syntax/parser/statement/mod.rs @@ -36,7 +36,7 @@ use super::{ expression::Expression, AllowAwait, AllowReturn, AllowYield, Cursor, ParseError, ParseResult, TokenParser, }; -use crate::syntax::ast::{keyword::Keyword, node::Node, punc::Punctuator, token::TokenKind}; +use crate::syntax::ast::{node, Keyword, Node, Punctuator, TokenKind}; /// Statement parsing. /// @@ -99,7 +99,9 @@ impl TokenParser for Statement { .parse(cursor) } TokenKind::Keyword(Keyword::Var) => { - VariableStatement::new(self.allow_yield, self.allow_await).parse(cursor) + VariableStatement::new(self.allow_yield, self.allow_await) + .parse(cursor) + .map(Node::from) } TokenKind::Keyword(Keyword::While) => { WhileStatement::new(self.allow_yield, self.allow_await, self.allow_return) @@ -112,12 +114,13 @@ impl TokenParser for Statement { TokenKind::Keyword(Keyword::For) => { ForStatement::new(self.allow_yield, self.allow_await, self.allow_return) .parse(cursor) + .map(Node::from) } TokenKind::Keyword(Keyword::Return) => { if self.allow_return.0 { ReturnStatement::new(self.allow_yield, self.allow_await).parse(cursor) } else { - Err(ParseError::Unexpected(tok.clone(), Some("statement"))) + Err(ParseError::unexpected(tok.clone(), "statement")) } } TokenKind::Keyword(Keyword::Break) => { @@ -129,6 +132,7 @@ impl TokenParser for Statement { TokenKind::Keyword(Keyword::Try) => { TryStatement::new(self.allow_yield, self.allow_await, self.allow_return) .parse(cursor) + .map(Node::from) } TokenKind::Keyword(Keyword::Throw) => { ThrowStatement::new(self.allow_yield, self.allow_await).parse(cursor) @@ -140,6 +144,7 @@ impl TokenParser for Statement { TokenKind::Punctuator(Punctuator::OpenBlock) => { BlockStatement::new(self.allow_yield, self.allow_await, self.allow_return) .parse(cursor) + .map(Node::from) } // TODO: https://tc39.es/ecma262/#prod-LabelledStatement // TokenKind::Punctuator(Punctuator::Semicolon) => { @@ -189,9 +194,9 @@ impl StatementList { } impl TokenParser for StatementList { - type Output = Vec; + type Output = node::StatementList; - fn parse(self, cursor: &mut Cursor<'_>) -> Result, ParseError> { + fn parse(self, cursor: &mut Cursor<'_>) -> Result { let mut items = Vec::new(); loop { @@ -200,7 +205,7 @@ impl TokenParser for StatementList { if self.break_when_closingbrase { break; } else { - return Err(ParseError::Unexpected(token.clone(), None)); + return Err(ParseError::unexpected(token.clone(), None)); } } None => { @@ -222,7 +227,9 @@ impl TokenParser for StatementList { while cursor.next_if(Punctuator::Semicolon).is_some() {} } - Ok(items) + items.sort_by(Node::hoistable_order); + + Ok(items.into()) } } @@ -317,6 +324,16 @@ impl TokenParser for ExpressionStatement { } } +/// Label identifier parsing. +/// +/// This seems to be the same as a `BindingIdentifier`. +/// +/// More information: +/// - [ECMAScript specification][spec] +/// +/// [spec]: https://tc39.es/ecma262/#prod-LabelIdentifier +type LabelIdentifier = BindingIdentifier; + /// Binding identifier parsing. /// /// More information: @@ -344,18 +361,18 @@ impl BindingIdentifier { } impl TokenParser for BindingIdentifier { - type Output = String; + type Output = Box; - fn parse(self, cursor: &mut Cursor<'_>) -> Result { + fn parse(self, cursor: &mut Cursor<'_>) -> Result { // TODO: strict mode. let next_token = cursor.next().ok_or(ParseError::AbruptEnd)?; match next_token.kind { TokenKind::Identifier(ref s) => Ok(s.clone()), - TokenKind::Keyword(k @ Keyword::Yield) if !self.allow_yield.0 => Ok(k.to_string()), - TokenKind::Keyword(k @ Keyword::Await) if !self.allow_await.0 => Ok(k.to_string()), - _ => Err(ParseError::Expected( + TokenKind::Keyword(k @ Keyword::Yield) if !self.allow_yield.0 => Ok(k.as_str().into()), + TokenKind::Keyword(k @ Keyword::Await) if !self.allow_await.0 => Ok(k.as_str().into()), + _ => Err(ParseError::expected( vec![TokenKind::identifier("identifier")], next_token.clone(), "binding identifier", diff --git a/boa/src/syntax/parser/statement/return_stm/mod.rs b/boa/src/syntax/parser/statement/return_stm/mod.rs index 54a7bf5cce..096b3be17a 100644 --- a/boa/src/syntax/parser/statement/return_stm/mod.rs +++ b/boa/src/syntax/parser/statement/return_stm/mod.rs @@ -2,7 +2,7 @@ mod tests; use crate::syntax::{ - ast::{keyword::Keyword, node::Node, punc::Punctuator, token::TokenKind}, + ast::{Keyword, Node, Punctuator, TokenKind}, parser::{expression::Expression, AllowAwait, AllowYield, Cursor, ParseResult, TokenParser}, }; diff --git a/boa/src/syntax/parser/statement/switch/mod.rs b/boa/src/syntax/parser/statement/switch/mod.rs index 1c187b7f52..c8416149a0 100644 --- a/boa/src/syntax/parser/statement/switch/mod.rs +++ b/boa/src/syntax/parser/statement/switch/mod.rs @@ -2,7 +2,7 @@ mod tests; use crate::syntax::{ - ast::{keyword::Keyword, node::Node, punc::Punctuator}, + ast::{Keyword, Node, Punctuator}, parser::{ expression::Expression, AllowAwait, AllowReturn, AllowYield, Cursor, ParseError, ParseResult, TokenParser, diff --git a/boa/src/syntax/parser/statement/throw/mod.rs b/boa/src/syntax/parser/statement/throw/mod.rs index bbfbfba9db..4d788424e7 100644 --- a/boa/src/syntax/parser/statement/throw/mod.rs +++ b/boa/src/syntax/parser/statement/throw/mod.rs @@ -2,7 +2,7 @@ mod tests; use crate::syntax::{ - ast::{keyword::Keyword, node::Node, punc::Punctuator, token::TokenKind}, + ast::{Keyword, Node, Punctuator, TokenKind}, parser::{expression::Expression, AllowAwait, AllowYield, Cursor, ParseResult, TokenParser}, }; @@ -40,7 +40,7 @@ impl TokenParser for ThrowStatement { fn parse(self, cursor: &mut Cursor<'_>) -> ParseResult { cursor.expect(Keyword::Throw, "throw statement")?; - cursor.peek_expect_no_lineterminator(0, "throw statement")?; + cursor.peek_expect_no_lineterminator(0)?; let expr = Expression::new(true, self.allow_yield, self.allow_await).parse(cursor)?; if let Some(tok) = cursor.peek(0) { diff --git a/boa/src/syntax/parser/statement/throw/tests.rs b/boa/src/syntax/parser/statement/throw/tests.rs index f0919d1870..f8a9357ac4 100644 --- a/boa/src/syntax/parser/statement/throw/tests.rs +++ b/boa/src/syntax/parser/statement/throw/tests.rs @@ -1,9 +1,9 @@ -use crate::syntax::{ast::node::Node, parser::tests::check_parser}; +use crate::syntax::{ + ast::{Const, Node}, + parser::tests::check_parser, +}; #[test] fn check_throw_parsing() { - check_parser( - "throw 'error';", - vec![Node::throw(Node::const_node("error"))], - ); + check_parser("throw 'error';", vec![Node::throw(Const::from("error"))]); } diff --git a/boa/src/syntax/parser/statement/try_stm/catch.rs b/boa/src/syntax/parser/statement/try_stm/catch.rs index addf8a8880..816db052ad 100644 --- a/boa/src/syntax/parser/statement/try_stm/catch.rs +++ b/boa/src/syntax/parser/statement/try_stm/catch.rs @@ -1,8 +1,11 @@ use crate::syntax::{ - ast::{keyword::Keyword, node::Node, punc::Punctuator}, + ast::{ + node::{self, Identifier}, + Keyword, Punctuator, + }, parser::{ statement::{block::Block, BindingIdentifier}, - AllowAwait, AllowReturn, AllowYield, Cursor, ParseError, ParseResult, TokenParser, + AllowAwait, AllowReturn, AllowYield, Cursor, ParseError, TokenParser, }, }; @@ -38,7 +41,7 @@ impl Catch { } impl TokenParser for Catch { - type Output = (Option, Option); + type Output = node::Catch; fn parse(self, cursor: &mut Cursor<'_>) -> Result { cursor.expect(Keyword::Catch, "try statement")?; @@ -52,9 +55,9 @@ impl TokenParser for Catch { }; // Catch block - Ok(( - Some(Block::new(self.allow_yield, self.allow_await, self.allow_return).parse(cursor)?), + Ok(node::Catch::new::<_, Identifier, _>( catch_param, + Block::new(self.allow_yield, self.allow_await, self.allow_return).parse(cursor)?, )) } } @@ -88,12 +91,12 @@ impl CatchParameter { } impl TokenParser for CatchParameter { - type Output = Node; + type Output = Identifier; - fn parse(self, cursor: &mut Cursor<'_>) -> ParseResult { + fn parse(self, cursor: &mut Cursor<'_>) -> Result { // TODO: should accept BindingPattern BindingIdentifier::new(self.allow_yield, self.allow_await) .parse(cursor) - .map(Node::local) + .map(Identifier::from) } } diff --git a/boa/src/syntax/parser/statement/try_stm/finally.rs b/boa/src/syntax/parser/statement/try_stm/finally.rs index 5a7037beca..a6c9bebbda 100644 --- a/boa/src/syntax/parser/statement/try_stm/finally.rs +++ b/boa/src/syntax/parser/statement/try_stm/finally.rs @@ -1,7 +1,7 @@ use crate::syntax::{ - ast::{keyword::Keyword, node::Node}, + ast::{node, Keyword}, parser::{ - statement::block::Block, AllowAwait, AllowReturn, AllowYield, Cursor, ParseResult, + statement::block::Block, AllowAwait, AllowReturn, AllowYield, Cursor, ParseError, TokenParser, }, }; @@ -38,10 +38,14 @@ impl Finally { } impl TokenParser for Finally { - type Output = Node; + type Output = node::Finally; - fn parse(self, cursor: &mut Cursor<'_>) -> ParseResult { + fn parse(self, cursor: &mut Cursor<'_>) -> Result { cursor.expect(Keyword::Finally, "try statement")?; - Ok(Block::new(self.allow_yield, self.allow_await, self.allow_return).parse(cursor)?) + Ok( + Block::new(self.allow_yield, self.allow_await, self.allow_return) + .parse(cursor)? + .into(), + ) } } diff --git a/boa/src/syntax/parser/statement/try_stm/mod.rs b/boa/src/syntax/parser/statement/try_stm/mod.rs index aa015c0081..2e7c490247 100644 --- a/boa/src/syntax/parser/statement/try_stm/mod.rs +++ b/boa/src/syntax/parser/statement/try_stm/mod.rs @@ -8,8 +8,8 @@ use self::catch::Catch; use self::finally::Finally; use super::block::Block; use crate::syntax::{ - ast::{keyword::Keyword, node::Node, token::TokenKind}, - parser::{AllowAwait, AllowReturn, AllowYield, Cursor, ParseError, ParseResult, TokenParser}, + ast::{node::Try, Keyword, TokenKind}, + parser::{AllowAwait, AllowReturn, AllowYield, Cursor, ParseError, TokenParser}, }; /// Try...catch statement parsing @@ -44,9 +44,9 @@ impl TryStatement { } impl TokenParser for TryStatement { - type Output = Node; + type Output = Try; - fn parse(self, cursor: &mut Cursor<'_>) -> ParseResult { + fn parse(self, cursor: &mut Cursor<'_>) -> Result { // TRY cursor.expect(Keyword::Try, "try statement")?; @@ -58,7 +58,7 @@ impl TokenParser for TryStatement { if next_token.kind != TokenKind::Keyword(Keyword::Catch) && next_token.kind != TokenKind::Keyword(Keyword::Finally) { - return Err(ParseError::Expected( + return Err(ParseError::expected( vec![ TokenKind::Keyword(Keyword::Catch), TokenKind::Keyword(Keyword::Finally), @@ -68,10 +68,10 @@ impl TokenParser for TryStatement { )); } - let (catch, param) = if next_token.kind == TokenKind::Keyword(Keyword::Catch) { - Catch::new(self.allow_yield, self.allow_await, self.allow_return).parse(cursor)? + let catch = if next_token.kind == TokenKind::Keyword(Keyword::Catch) { + Some(Catch::new(self.allow_yield, self.allow_await, self.allow_return).parse(cursor)?) } else { - (None, None) + None }; let next_token = cursor.peek(0); @@ -87,11 +87,6 @@ impl TokenParser for TryStatement { None => None, }; - Ok(Node::try_node::<_, _, _, _, Node, Node, Node>( - try_clause, - catch, - param, - finally_block, - )) + Ok(Try::new(try_clause, catch, finally_block)) } } diff --git a/boa/src/syntax/parser/statement/try_stm/tests.rs b/boa/src/syntax/parser/statement/try_stm/tests.rs index cecf9bb144..0f9e244cae 100644 --- a/boa/src/syntax/parser/statement/try_stm/tests.rs +++ b/boa/src/syntax/parser/statement/try_stm/tests.rs @@ -1,5 +1,8 @@ use crate::syntax::{ - ast::node::Node, + ast::{ + node::{Block, Catch, Finally, Identifier, Try, VarDecl, VarDeclList}, + Const, + }, parser::tests::{check_invalid, check_parser}, }; @@ -7,12 +10,7 @@ use crate::syntax::{ fn check_inline_with_empty_try_catch() { check_parser( "try { } catch(e) {}", - vec![Node::try_node::<_, _, _, _, Node, Node, Node>( - Node::block(vec![]), - Node::block(vec![]), - Node::local("e"), - None, - )], + vec![Try::new(vec![], Some(Catch::new("e", vec![])), None).into()], ); } @@ -20,15 +18,12 @@ fn check_inline_with_empty_try_catch() { fn check_inline_with_var_decl_inside_try() { check_parser( "try { var x = 1; } catch(e) {}", - vec![Node::try_node::<_, _, _, _, Node, Node, Node>( - Node::block(vec![Node::var_decl(vec![( - String::from("x"), - Some(Node::const_node(1)), - )])]), - Node::block(vec![]), - Node::local("e"), + vec![Try::new( + vec![VarDeclList::from(vec![VarDecl::new("x", Some(Const::from(1).into()))]).into()], + Some(Catch::new("e", vec![])), None, - )], + ) + .into()], ); } @@ -36,18 +31,17 @@ fn check_inline_with_var_decl_inside_try() { fn check_inline_with_var_decl_inside_catch() { check_parser( "try { var x = 1; } catch(e) { var x = 1; }", - vec![Node::try_node::<_, _, _, _, Node, Node, Node>( - Node::block(vec![Node::var_decl(vec![( - String::from("x"), - Some(Node::const_node(1)), - )])]), - Node::block(vec![Node::var_decl(vec![( - String::from("x"), - Some(Node::const_node(1)), - )])]), - Node::local("e"), + vec![Try::new( + vec![VarDeclList::from(vec![VarDecl::new("x", Some(Const::from(1).into()))]).into()], + Some(Catch::new( + "e", + vec![ + VarDeclList::from(vec![VarDecl::new("x", Some(Const::from(1).into()))]).into(), + ], + )), None, - )], + ) + .into()], ); } @@ -55,12 +49,12 @@ fn check_inline_with_var_decl_inside_catch() { fn check_inline_with_empty_try_catch_finally() { check_parser( "try {} catch(e) {} finally {}", - vec![Node::try_node::<_, _, _, _, Node, Node, Node>( - Node::block(vec![]), - Node::block(vec![]), - Node::local("e"), - Node::block(vec![]), - )], + vec![Try::new( + vec![], + Some(Catch::new("e", vec![])), + Some(Finally::from(vec![])), + ) + .into()], ); } @@ -68,12 +62,7 @@ fn check_inline_with_empty_try_catch_finally() { fn check_inline_with_empty_try_finally() { check_parser( "try {} finally {}", - vec![Node::try_node::<_, _, _, _, Node, Node, Node>( - Node::block(vec![]), - None, - None, - Node::block(vec![]), - )], + vec![Try::new(vec![], None, Some(Finally::from(vec![]))).into()], ); } @@ -81,15 +70,16 @@ fn check_inline_with_empty_try_finally() { fn check_inline_with_empty_try_var_decl_in_finally() { check_parser( "try {} finally { var x = 1; }", - vec![Node::try_node::<_, _, _, _, Node, Node, Node>( - Node::block(vec![]), - None, + vec![Try::new( + vec![], None, - Node::block(vec![Node::var_decl(vec![( - String::from("x"), - Some(Node::const_node(1)), - )])]), - )], + Some(Finally::from(vec![VarDeclList::from(vec![VarDecl::new( + "x", + Some(Const::from(1).into()), + )]) + .into()])), + ) + .into()], ); } @@ -97,15 +87,17 @@ fn check_inline_with_empty_try_var_decl_in_finally() { fn check_inline_empty_try_paramless_catch() { check_parser( "try {} catch { var x = 1; }", - vec![Node::try_node::<_, _, _, _, Node, Node, Node>( - Node::block(vec![]), - Node::block(vec![Node::var_decl(vec![( - String::from("x"), - Some(Node::const_node(1)), - )])]), + vec![Try::new( + Block::from(vec![]), + Some(Catch::new::<_, Identifier, _>( + None, + vec![ + VarDeclList::from(vec![VarDecl::new("x", Some(Const::from(1).into()))]).into(), + ], + )), None, - None, - )], + ) + .into()], ); } @@ -123,3 +115,8 @@ fn check_inline_invalid_catch_without_closing_paren() { fn check_inline_invalid_catch_parameter() { check_invalid("try {} catch(1) {}"); } + +#[test] +fn check_invalide_try_no_catch_finally() { + check_invalid("try {} let a = 10;"); +} diff --git a/boa/src/syntax/parser/statement/variable.rs b/boa/src/syntax/parser/statement/variable.rs index 193d81a491..a5f750140d 100644 --- a/boa/src/syntax/parser/statement/variable.rs +++ b/boa/src/syntax/parser/statement/variable.rs @@ -1,9 +1,12 @@ // use super::lexical_declaration_continuation; use crate::syntax::{ - ast::{keyword::Keyword, node::Node, punc::Punctuator, token::TokenKind}, + ast::{ + node::{VarDecl, VarDeclList}, + Keyword, Punctuator, TokenKind, + }, parser::{ expression::Initializer, statement::BindingIdentifier, AllowAwait, AllowIn, AllowYield, - Cursor, ParseError, ParseResult, TokenParser, + Cursor, ParseError, TokenParser, }, }; @@ -38,9 +41,9 @@ impl VariableStatement { } impl TokenParser for VariableStatement { - type Output = Node; + type Output = VarDeclList; - fn parse(self, cursor: &mut Cursor<'_>) -> ParseResult { + fn parse(self, cursor: &mut Cursor<'_>) -> Result { cursor.expect(Keyword::Var, "variable statement")?; let decl_list = @@ -88,9 +91,9 @@ impl VariableDeclarationList { } impl TokenParser for VariableDeclarationList { - type Output = Node; + type Output = VarDeclList; - fn parse(self, cursor: &mut Cursor<'_>) -> ParseResult { + fn parse(self, cursor: &mut Cursor<'_>) -> Result { let mut list = Vec::new(); loop { @@ -105,7 +108,7 @@ impl TokenParser for VariableDeclarationList { let _ = cursor.next(); } _ => { - return Err(ParseError::Expected( + return Err(ParseError::expected( vec![ TokenKind::Punctuator(Punctuator::Semicolon), TokenKind::LineTerminator, @@ -117,7 +120,7 @@ impl TokenParser for VariableDeclarationList { } } - Ok(Node::var_decl(list)) + Ok(VarDeclList::from(list)) } } @@ -151,7 +154,7 @@ impl VariableDeclaration { } impl TokenParser for VariableDeclaration { - type Output = (String, Option); + type Output = VarDecl; fn parse(self, cursor: &mut Cursor<'_>) -> Result { // TODO: BindingPattern @@ -161,6 +164,6 @@ impl TokenParser for VariableDeclaration { let ident = Initializer::new(self.allow_in, self.allow_yield, self.allow_await).try_parse(cursor); - Ok((name, ident)) + Ok(VarDecl::new(name, ident)) } } diff --git a/boa/src/syntax/parser/tests.rs b/boa/src/syntax/parser/tests.rs index ee613ff59f..071662835e 100644 --- a/boa/src/syntax/parser/tests.rs +++ b/boa/src/syntax/parser/tests.rs @@ -1,9 +1,21 @@ //! Tests for the parser. use super::Parser; -use crate::syntax::{ast::node::Node, ast::op::NumOp, lexer::Lexer}; +use crate::syntax::{ + ast::{ + node::{ + Assign, BinOp, Call, FunctionDecl, Identifier, New, Node, StatementList, UnaryOp, + VarDecl, VarDeclList, + }, + op::{self, NumOp}, + Const, + }, + lexer::Lexer, +}; +/// Checks that the given JavaScript string gives the expected expression. #[allow(clippy::result_unwrap_used)] +// TODO: #[track_caller]: https://github.com/rust-lang/rust/issues/47809 pub(super) fn check_parser(js: &str, expr: L) where L: Into>, @@ -15,10 +27,12 @@ where Parser::new(&lexer.tokens) .parse_all() .expect("failed to parse"), - Node::statement_list(expr) + StatementList::from(expr) ); } +/// Checks that the given javascript string creates a parse error. +// TODO: #[track_caller]: https://github.com/rust-lang/rust/issues/47809 pub(super) fn check_invalid(js: &str) { let mut lexer = Lexer::new(js); lexer.lex().expect("failed to lex"); @@ -31,13 +45,13 @@ pub(super) fn check_invalid(js: &str) { fn check_construct_call_precedence() { check_parser( "new Date().getTime()", - vec![Node::call( + vec![Node::from(Call::new( Node::get_const_field( - Node::new(Node::call(Node::local("Date"), Vec::new())), + New::from(Call::new(Identifier::from("Date"), vec![])), "getTime", ), - Vec::new(), - )], + vec![], + ))], ); } @@ -45,9 +59,48 @@ fn check_construct_call_precedence() { fn assign_operator_precedence() { check_parser( "a = a + 1", - vec![Node::assign( - Node::local("a"), - Node::bin_op(NumOp::Add, Node::local("a"), Node::const_node(1)), - )], + vec![Assign::new( + Identifier::from("a"), + BinOp::new(NumOp::Add, Identifier::from("a"), Const::from(1)), + ) + .into()], + ); +} + +#[test] +fn hoisting() { + check_parser( + r" + var a = hello(); + a++; + + function hello() { return 10 }", + vec![ + FunctionDecl::new( + Box::from("hello"), + vec![], + vec![Node::return_node(Const::from(10))], + ) + .into(), + VarDeclList::from(vec![VarDecl::new( + "a", + Node::from(Call::new(Identifier::from("hello"), vec![])), + )]) + .into(), + UnaryOp::new(op::UnaryOp::IncrementPost, Identifier::from("a")).into(), + ], + ); + + check_parser( + r" + a = 10; + a++; + + var a;", + vec![ + Assign::new(Identifier::from("a"), Const::from(10)).into(), + UnaryOp::new(op::UnaryOp::IncrementPost, Identifier::from("a")).into(), + VarDeclList::from(vec![VarDecl::new("a", None)]).into(), + ], ); } diff --git a/boa_cli/src/main.rs b/boa_cli/src/main.rs index 03dac27604..a24c941a39 100644 --- a/boa_cli/src/main.rs +++ b/boa_cli/src/main.rs @@ -27,10 +27,10 @@ use boa::{ builtins::console::log, - exec::Executor, + exec::Interpreter, forward_val, realm::Realm, - syntax::ast::{node::Node, token::Token}, + syntax::ast::{node::StatementList, token::Token}, }; use std::{ fs::read_to_string, @@ -126,7 +126,7 @@ fn lex_source(src: &str) -> Result, String> { /// /// Returns a error of type String with a message, /// if the token stream has a parsing error. -fn parse_tokens(tokens: Vec) -> Result { +fn parse_tokens(tokens: Vec) -> Result { use boa::syntax::parser::Parser; Parser::new(&tokens) @@ -177,7 +177,7 @@ pub fn main() -> Result<(), std::io::Error> { let realm = Realm::create().register_global_func("print", log); - let mut engine = Executor::new(realm); + let mut engine = Interpreter::new(realm); for file in &args.files { let buffer = read_to_string(file)?; diff --git a/boa_wasm/.gitignore b/boa_wasm/.gitignore new file mode 100644 index 0000000000..0c6117d9fb --- /dev/null +++ b/boa_wasm/.gitignore @@ -0,0 +1 @@ +pkg \ No newline at end of file diff --git a/boa_wasm/src/lib.rs b/boa_wasm/src/lib.rs index c8e8872619..97eab2e5c6 100644 --- a/boa_wasm/src/lib.rs +++ b/boa_wasm/src/lib.rs @@ -1,47 +1,24 @@ -use boa::{ - exec::{Executor, Interpreter}, - realm::Realm, - syntax::{ast::node::Node, lexer::Lexer, parser::Parser}, -}; +use boa::{Executable, Interpreter, Lexer, Parser, Realm}; use wasm_bindgen::prelude::*; -// WASM #[wasm_bindgen] -extern "C" { - // Use `js_namespace` here to bind `console.log(..)` instead of just - // `log(..)` - #[wasm_bindgen(js_namespace = console)] - fn log(s: &str); -} - -#[wasm_bindgen] -pub fn evaluate(src: &str) -> String { - let mut lexer = Lexer::new(&src); - match lexer.lex() { - Ok(_v) => (), - Err(v) => log(&v.to_string()), - } +pub fn evaluate(src: &str) -> Result { + let mut lexer = Lexer::new(src); + lexer + .lex() + .map_err(|e| JsValue::from(format!("Syntax Error: {}", e)))?; let tokens = lexer.tokens; + let expr = Parser::new(&tokens) + .parse_all() + .map_err(|e| JsValue::from(format!("Parsing Error: {}", e)))?; // Setup executor - let node: Node; - - match Parser::new(&tokens).parse_all() { - Ok(v) => { - node = v; - } - Err(_v) => { - log("parsing fail"); - return String::from("parsing failed"); - } - } - // Create new Realm let realm = Realm::create(); - let mut engine: Interpreter = Executor::new(realm); - let result = engine.run(&node); - match result { - Ok(v) => v.to_string(), - Err(v) => format!("{}: {}", "error", v.to_string()), - } + let mut engine = Interpreter::new(realm); + + // Setup executor + expr.run(&mut engine) + .map_err(|e| JsValue::from(format!("Error: {}", e))) + .map(|v| v.to_string()) } diff --git a/index.js b/index.js index 956bc79087..b70fec968f 100644 --- a/index.js +++ b/index.js @@ -57,6 +57,12 @@ rust.then((m) => { function inputHandler(evt) { const text = editor.getValue(); let p = document.querySelector("p.output"); - let result = window.evaluate(text); - p.textContent = `> ${result}`; + + try { + let result = window.evaluate(text); + p.textContent = `> ${result}`; + } catch (err) { + console.error(err); + p.innerHTML = `${err}` + } } diff --git a/yarn.lock b/yarn.lock index 334d07640c..6504ec84e3 100644 --- a/yarn.lock +++ b/yarn.lock @@ -32,9 +32,9 @@ integrity sha512-tHq6qdbT9U1IRSGf14CL0pUlULksvY9OZ+5eEgl1N7t+OA3tGvNpxJCzuKQlsNgCVwbAs670L1vcVQi8j9HjnA== "@types/node@*": - version "14.0.4" - resolved "https://registry.yarnpkg.com/@types/node/-/node-14.0.4.tgz#43a63fc5edce226bed106b31b875165256271107" - integrity sha512-k3NqigXWRzQZVBDS5D1U70A5E8Qk4Kh+Ha/x4M8Bt9pF0X05eggfnC9+63Usc9Q928hRUIpIhTQaXsZwZBl4Ew== + version "14.0.5" + resolved "https://registry.yarnpkg.com/@types/node/-/node-14.0.5.tgz#3d03acd3b3414cf67faf999aed11682ed121f22b" + integrity sha512-90hiq6/VqtQgX8Sp0EzeIsv3r+ellbGj4URKj5j30tLlZvRUpnAe9YbYnjl3pJM93GyXU0tghHhvXHq+5rnCKA== "@types/source-list-map@*": version "0.1.2" @@ -2009,10 +2009,10 @@ http-errors@~1.7.2: statuses ">= 1.5.0 < 2" toidentifier "1.0.0" -"http-parser-js@>=0.4.0 <0.4.11": - version "0.4.10" - resolved "https://registry.yarnpkg.com/http-parser-js/-/http-parser-js-0.4.10.tgz#92c9c1374c35085f75db359ec56cc257cbb93fa4" - integrity sha1-ksnBN0w1CF912zWexWzCV8u5P6Q= +http-parser-js@>=0.5.1: + version "0.5.2" + resolved "https://registry.yarnpkg.com/http-parser-js/-/http-parser-js-0.5.2.tgz#da2e31d237b393aae72ace43882dd7e270a8ff77" + integrity sha512-opCO9ASqg5Wy2FNo7A0sxy71yGbbkJJXLdgMK04Tcypw9jr2MgWbyubb0+WdmDmGnFflO7fRbqbaihh/ENDlRQ== http-proxy-middleware@0.19.1: version "0.19.1" @@ -4338,11 +4338,11 @@ websocket-driver@0.6.5: websocket-extensions ">=0.1.1" websocket-driver@>=0.5.1: - version "0.7.3" - resolved "https://registry.yarnpkg.com/websocket-driver/-/websocket-driver-0.7.3.tgz#a2d4e0d4f4f116f1e6297eba58b05d430100e9f9" - integrity sha512-bpxWlvbbB459Mlipc5GBzzZwhoZgGEZLuqPaR0INBGnPAY1vdBX6hPnoFXiw+3yWxDuHyQjO2oXTMyS8A5haFg== + version "0.7.4" + resolved "https://registry.yarnpkg.com/websocket-driver/-/websocket-driver-0.7.4.tgz#89ad5295bbf64b480abcba31e4953aca706f5760" + integrity sha512-b17KeDIQVjvb0ssuSDF2cYXSg2iztliJ4B9WdsuB6J952qCPKmnVq4DyW5motImXHDC1cBT/1UezrJVsKw5zjg== dependencies: - http-parser-js ">=0.4.0 <0.4.11" + http-parser-js ">=0.5.1" safe-buffer ">=5.1.0" websocket-extensions ">=0.1.1"