From 1c69da1dfa0fbc6f8a7daa20dd8f5ae6da31d2da Mon Sep 17 00:00:00 2001 From: Callum Ward Date: Sun, 7 Jul 2019 17:37:48 +0100 Subject: [PATCH] Implement Array.prototype: concat(), push(), pop() and join() (#52) * First draft of Array prototype object * First draft of Array prototype object * Update to working Array object * Remove unneeded HashMap import * Fix indexing by removing private internal HashMap * Fix formatting issues with cargo fmt * Implement Array.prototype.concat() * Implement Array.prototype.concat() * Cargo format fix * Implement push, pop and join * Add documenation links to new methods --- src/lib/js/array.rs | 148 +++++++++++++++++++++++++++++++++++++++----- 1 file changed, 134 insertions(+), 14 deletions(-) diff --git a/src/lib/js/array.rs b/src/lib/js/array.rs index ce7f5360f4..47c5add4e4 100644 --- a/src/lib/js/array.rs +++ b/src/lib/js/array.rs @@ -3,28 +3,55 @@ use crate::js::object::{Property, PROTOTYPE}; use crate::js::value::{from_value, to_value, ResultValue, Value, ValueData}; use gc::Gc; +/// 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) +fn create_array_object(array_obj: Value, array_contents: Vec) -> ResultValue { + let array_obj_ptr = array_obj.clone(); + + // Wipe existing contents of the array object + let orig_length: i32 = from_value(array_obj.get_field_slice("length")).unwrap(); + for n in 0..orig_length { + array_obj_ptr.remove_prop(&n.to_string()); + } + + for (n, value) in array_contents.iter().enumerate() { + array_obj_ptr.set_field(n.to_string(), value.clone()); + } + + array_obj_ptr.set_field_slice("length", to_value(array_contents.len() as i32)); + Ok(array_obj_ptr) +} + +/// Utility function which takes an existing array object and puts additional +/// values on the end, correctly rewriting the length +fn add_to_array_object(array_ptr: Value, add_values: Vec) -> ResultValue { + let orig_length: i32 = from_value(array_ptr.get_field_slice("length")).unwrap(); + + for (n, value) in add_values.iter().enumerate() { + let new_index = orig_length + (n as i32); + array_ptr.set_field(new_index.to_string(), value.clone()); + } + + array_ptr.set_field_slice("length", to_value(orig_length + add_values.len() as i32)); + + Ok(array_ptr) +} + /// Create a new array pub fn make_array(this: Value, _: Value, args: Vec) -> ResultValue { - let this_ptr = this.clone(); // Make a new Object which will internally represent the Array (mapping // between indices and values): this creates an Object with no prototype + this.set_field_slice("length", to_value(0i32)); match args.len() { - 0 => { - this_ptr.set_field_slice("length", to_value(0i32)); - } + 0 => create_array_object(this, Vec::new()), 1 => { - let length_chosen: i32 = from_value(args[0].clone()).unwrap(); - this_ptr.set_field_slice("length", to_value(length_chosen)); - } - n => { - this_ptr.set_field_slice("length", to_value(n)); - for k in 0..n { - let index_str = k.to_string(); - this_ptr.set_field(index_str, args[k].clone()); - } + let array = create_array_object(this, Vec::new()).unwrap(); + let size: i32 = from_value(args[0].clone()).unwrap(); + array.set_field_slice("length", to_value(size)); + Ok(array) } + _ => create_array_object(this, args), } - Ok(this_ptr) } /// Get an array's length @@ -34,6 +61,91 @@ pub fn get_array_length(this: Value, _: Value, _: Vec) -> ResultValue { Ok(this.get_field_slice("length")) } +/// 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. +/// https://tc39.es/ecma262/#sec-array.prototype.concat +pub fn concat(this: Value, _: Value, args: Vec) -> ResultValue { + if args.len() == 0 { + // 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(); + + let this_length: i32 = from_value(this.get_field_slice("length")).unwrap(); + for n in 0..this_length { + new_values.push(this.get_field(n.to_string())); + } + + for concat_array in args { + let concat_length: i32 = from_value(concat_array.get_field_slice("length")).unwrap(); + for n in 0..concat_length { + new_values.push(concat_array.get_field(n.to_string())); + } + } + + create_array_object(this, new_values) +} + +/// 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. +/// https://tc39.es/ecma262/#sec-array.prototype.push +pub fn push(this: Value, _: Value, args: Vec) -> ResultValue { + let new_array = add_to_array_object(this, args)?; + Ok(new_array.get_field_slice("length")) +} + +/// Array.prototype.pop ( ) +/// +/// The last element of the array is removed from the array and returned. +/// https://tc39.es/ecma262/#sec-array.prototype.pop +pub fn pop(this: Value, _: Value, _: Vec) -> ResultValue { + let curr_length: i32 = from_value(this.get_field_slice("length")).unwrap(); + if curr_length < 1 { + return Err(to_value( + "Cannot pop() on an array with zero length".to_string(), + )); + } + let pop_index = curr_length - 1; + let pop_value: Value = this.get_field(pop_index.to_string()); + this.remove_prop(&pop_index.to_string()); + this.set_field_slice("length", to_value(pop_index)); + Ok(pop_value) +} + +/// 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. +/// https://tc39.es/ecma262/#sec-array.prototype.join +pub fn join(this: Value, _: Value, args: Vec) -> ResultValue { + let separator: String; + + if args.len() > 0 { + separator = args[0].to_string(); + } else { + separator = ",".to_string(); + } + + let mut elem_strs: Vec = Vec::new(); + let length: i32 = from_value(this.get_field_slice("length")).unwrap(); + for n in 0..length { + let elem_str: String = this.get_field(n.to_string()).to_string(); + elem_strs.push(elem_str); + } + + Ok(to_value(elem_strs.join(&separator))) +} + /// Create a new `Array` object pub fn _create(global: &Value) -> Value { let array = to_value(make_array as NativeFunctionData); @@ -47,6 +159,14 @@ pub fn _create(global: &Value) -> Value { set: Gc::new(ValueData::Undefined), }; proto.set_prop_slice("length", length); + let concat_func = to_value(concat as NativeFunctionData); + concat_func.set_field_slice("length", to_value(1 as i32)); + proto.set_field_slice("concat", concat_func); + let push_func = to_value(push as NativeFunctionData); + push_func.set_field_slice("length", to_value(1 as i32)); + proto.set_field_slice("push", push_func); + proto.set_field_slice("pop", to_value(pop as NativeFunctionData)); + proto.set_field_slice("join", to_value(join as NativeFunctionData)); array.set_field_slice(PROTOTYPE, proto); array }