Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Implement Array.prototype.reduceRight #579

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
97 changes: 94 additions & 3 deletions boa/src/builtins/array/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -127,9 +127,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");
Expand Down Expand Up @@ -1024,6 +1024,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) {
Expand Down Expand Up @@ -1056,6 +1146,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,
Expand Down
123 changes: 123 additions & 0 deletions boa/src/builtins/array/tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand Down