From bbd7dd2945b7ceab4bc2a63ef04510af8d14c2ae Mon Sep 17 00:00:00 2001 From: benjaminflin Date: Tue, 21 Jul 2020 15:31:12 -0700 Subject: [PATCH] Implement Array.prototype.reduceRight (#579) --- boa/src/builtins/array/mod.rs | 97 ++++++++++++++++++++++++- boa/src/builtins/array/tests.rs | 123 ++++++++++++++++++++++++++++++++ 2 files changed, 217 insertions(+), 3 deletions(-) diff --git a/boa/src/builtins/array/mod.rs b/boa/src/builtins/array/mod.rs index 19650b84ea..294cc31859 100644 --- a/boa/src/builtins/array/mod.rs +++ b/boa/src/builtins/array/mod.rs @@ -126,9 +126,9 @@ impl Array { 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()); - // } + for n in 0..length { + this.set_field(n.to_string(), Value::undefined()); + } } 1 if args[0].is_double() => { return ctx.throw_range_error("invalid array length"); @@ -1022,6 +1022,96 @@ impl Array { Ok(accumulator) } + /// `Array.prototype.reduceRight( callbackFn [ , initialValue ] )` + /// + /// The reduceRight method traverses right to left starting from the last defined value in the array, + /// accumulating a value using a given callback function. It returns the accumulated value. + /// + /// More information: + /// - [ECMAScript reference][spec] + /// - [MDN documentation][mdn] + /// + /// [spec]: https://tc39.es/ecma262/#sec-array.prototype.reduceright + /// [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/reduceRight + pub(crate) fn reduce_right( + this: &Value, + args: &[Value], + interpreter: &mut Interpreter, + ) -> ResultValue { + let this = interpreter.to_object(this)?; + let callback = match args.get(0) { + Some(value) if value.is_function() => value, + _ => return interpreter.throw_type_error("reduceRight was called without a callback"), + }; + let initial_value = args.get(1).cloned().unwrap_or_else(Value::undefined); + let mut length = interpreter.to_length(&this.get_field("length"))?; + if length == 0 { + if initial_value.is_undefined() { + return interpreter.throw_type_error( + "reduceRight was called on an empty array and with no initial value", + ); + } else { + // early return to prevent usize subtraction errors + return Ok(initial_value); + } + } + let mut k = length - 1; + let mut accumulator = if initial_value.is_undefined() { + let mut k_present = false; + loop { + if this.has_field(&k.to_string()) { + k_present = true; + break; + } + // check must be done at the end to prevent usize subtraction error + if k == 0 { + break; + } + k -= 1; + } + if !k_present { + return interpreter.throw_type_error( + "reduceRight was called on an empty array and with no initial value", + ); + } + let result = this.get_field(k.to_string()); + k -= 1; + result + } else { + initial_value + }; + loop { + if this.has_field(&k.to_string()) { + let arguments = [ + accumulator, + this.get_field(k.to_string()), + Value::from(k), + this.clone(), + ]; + accumulator = interpreter.call(&callback, &Value::undefined(), &arguments)?; + /* We keep track of possibly shortened length in order to prevent unnecessary iteration. + It may also be necessary to do this since shortening the array length does not + delete array elements. See: https://github.com/boa-dev/boa/issues/557 */ + length = min(length, interpreter.to_length(&this.get_field("length"))?); + + // move k to the last defined element if necessary or return if the length was set to 0 + if k >= length { + if length == 0 { + return Ok(accumulator); + } else { + k = length - 1; + continue; + } + } + } + if k == 0 { + break; + } + k -= 1; + } + Ok(accumulator) + } + /// Initialise the `Array` object on the global object. #[inline] pub(crate) fn init(global: &Value) -> (&str, Value) { @@ -1054,6 +1144,7 @@ impl Array { make_builtin_fn(Self::slice, "slice", &prototype, 2); make_builtin_fn(Self::some, "some", &prototype, 2); make_builtin_fn(Self::reduce, "reduce", &prototype, 2); + make_builtin_fn(Self::reduce_right, "reduceRight", &prototype, 2); let array = make_constructor_fn( Self::NAME, diff --git a/boa/src/builtins/array/tests.rs b/boa/src/builtins/array/tests.rs index 26dc512c1c..219331fa0c 100644 --- a/boa/src/builtins/array/tests.rs +++ b/boa/src/builtins/array/tests.rs @@ -899,6 +899,129 @@ fn reduce() { assert_eq!(result, "Reduce was called without a callback"); } +#[test] +fn reduce_right() { + let realm = Realm::create(); + let mut engine = Interpreter::new(realm); + + let init = r#" + var arr = [1, 2, 3, 4]; + function sub(acc, x) { + return acc - x; + } + + function subIdx(acc, _, idx) { + return acc - idx; + } + + function subLen(acc, _x, _idx, arr) { + return acc - arr.length; + } + + function subResize(acc, x, idx, arr) { + if(idx == arr.length - 1) { + arr.length = 1; + } + return acc - x; + } + function subResize0(acc, x, idx, arr) { + if(idx == arr.length - 2) { + arr.length = 0; + } + return acc - x; + } + var delArray = [1, 2, 3, 4, 5]; + delete delArray[0]; + delete delArray[1]; + delete delArray[3]; + + "#; + forward(&mut engine, init); + + // empty array + let result = forward(&mut engine, "[].reduceRight(sub, 0)"); + assert_eq!(result, "0"); + + // simple with initial value + let result = forward(&mut engine, "arr.reduceRight(sub, 0)"); + assert_eq!(result, "-10"); + + // without initial value + let result = forward(&mut engine, "arr.reduceRight(sub)"); + assert_eq!(result, "-2"); + + // with some items missing + let result = forward(&mut engine, "delArray.reduceRight(sub, 0)"); + assert_eq!(result, "-8"); + + // with index + let result = forward(&mut engine, "arr.reduceRight(subIdx)"); + assert_eq!(result, "1"); + + // with array + let result = forward(&mut engine, "arr.reduceRight(subLen)"); + assert_eq!(result, "-8"); + + // resizing the array as reduce progresses + let result = forward(&mut engine, "arr.reduceRight(subResize, 0)"); + assert_eq!(result, "-5"); + + // reset array + forward(&mut engine, "arr = [1, 2, 3, 4];"); + + // resizing the array to 0 as reduce progresses + let result = forward(&mut engine, "arr.reduceRight(subResize0, 0)"); + assert_eq!(result, "-7"); + + // Empty array + let result = forward( + &mut engine, + r#" + try { + [].reduceRight((acc, x) => acc + x); + } catch(e) { + e.message + } + "#, + ); + assert_eq!( + result, + "reduceRight was called on an empty array and with no initial value" + ); + + // Array with no defined elements + let result = forward( + &mut engine, + r#" + try { + var arr = [0, 1]; + delete arr[0]; + delete arr[1]; + arr.reduceRight((acc, x) => acc + x); + } catch(e) { + e.message + } + "#, + ); + assert_eq!( + result, + "reduceRight was called on an empty array and with no initial value" + ); + + // No callback + let result = forward( + &mut engine, + r#" + try { + arr.reduceRight(""); + } catch(e) { + e.message + } + "#, + ); + assert_eq!(result, "reduceRight was called without a callback"); +} + #[test] fn call_array_constructor_with_one_argument() { let realm = Realm::create();