avm2: Implement `forEach`, `map`, `filter`, `every`, and `some` on `Array`.

This also comes with some refactoring: building the resulting array object and resolving holes is now done in helper methods.
This commit is contained in:
David Wendt 2020-08-29 17:55:09 -04:00 committed by Mike Welsh
parent 0eeee72be6
commit 832bbdd711
22 changed files with 448 additions and 27 deletions

View File

@ -64,6 +64,25 @@ pub fn length<'gc>(
Ok(Value::Undefined)
}
/// Bundle an already-constructed `ArrayStorage` in an `Object`.
pub fn build_array<'gc>(
activation: &mut Activation<'_, 'gc, '_>,
array: ArrayStorage<'gc>,
) -> Result<Value<'gc>, Error> {
Ok(ArrayObject::from_array(
array,
activation
.context
.avm2
.system_prototypes
.as_ref()
.map(|sp| sp.array)
.unwrap(),
activation.context.gc_context,
)
.into())
}
/// Implements `Array.concat`
#[allow(clippy::map_clone)] //You can't clone `Option<Ref<T>>` without it
pub fn concat<'gc>(
@ -83,18 +102,30 @@ pub fn concat<'gc>(
}
}
Ok(ArrayObject::from_array(
base_array,
activation
.context
.avm2
.system_prototypes
.as_ref()
.map(|sp| sp.array)
.unwrap(),
activation.context.gc_context,
)
.into())
build_array(activation, base_array)
}
/// Resolves array holes.
fn resolve_array_hole<'gc>(
activation: &mut Activation<'_, 'gc, '_>,
this: Object<'gc>,
i: usize,
item: Option<Value<'gc>>,
) -> Result<Value<'gc>, Error> {
item.map(Ok).unwrap_or_else(|| {
this.proto()
.map(|mut p| {
p.get_property(
p,
&QName::new(
Namespace::public_namespace(),
AvmString::new(activation.context.gc_context, format!("{}", i)),
),
activation,
)
})
.unwrap_or(Ok(Value::Undefined))
})
}
/// Implements `Array.join`
@ -114,22 +145,9 @@ pub fn join<'gc>(
let mut accum = Vec::new();
for (i, item) in array.iter().enumerate() {
let item = item.map(Ok).unwrap_or_else(|| {
this.proto()
.map(|mut p| {
p.get_property(
p,
&QName::new(
Namespace::public_namespace(),
AvmString::new(activation.context.gc_context, format!("{}", i)),
),
activation,
)
})
.unwrap_or(Ok(Value::Undefined))
});
let item = resolve_array_hole(activation, this, i, item)?;
accum.push(item?.coerce_to_string(activation)?.to_string());
accum.push(item.coerce_to_string(activation)?.to_string());
}
return Ok(AvmString::new(
@ -161,6 +179,208 @@ pub fn value_of<'gc>(
join(activation, this, &[",".into()])
}
/// Implements `Array.forEach`
pub fn for_each<'gc>(
activation: &mut Activation<'_, 'gc, '_>,
this: Option<Object<'gc>>,
args: &[Value<'gc>],
) -> Result<Value<'gc>, Error> {
if let Some(this) = this {
if let Some(array) = this.as_array_storage() {
let callback = args
.get(0)
.cloned()
.unwrap_or(Value::Undefined)
.coerce_to_object(activation)?;
let reciever = args
.get(1)
.cloned()
.unwrap_or(Value::Null)
.coerce_to_object(activation)
.ok();
for (i, item) in array.iter().enumerate() {
let item = resolve_array_hole(activation, this, i, item)?;
callback.call(
reciever,
&[item, i.into(), this.into()],
activation,
reciever.and_then(|r| r.proto()),
)?;
}
}
}
Ok(Value::Undefined)
}
/// Implements `Array.map`
pub fn map<'gc>(
activation: &mut Activation<'_, 'gc, '_>,
this: Option<Object<'gc>>,
args: &[Value<'gc>],
) -> Result<Value<'gc>, Error> {
if let Some(this) = this {
if let Some(array) = this.as_array_storage() {
let callback = args
.get(0)
.cloned()
.unwrap_or(Value::Undefined)
.coerce_to_object(activation)?;
let reciever = args
.get(1)
.cloned()
.unwrap_or(Value::Null)
.coerce_to_object(activation)
.ok();
let mut new_array = ArrayStorage::new(0);
for (i, item) in array.iter().enumerate() {
let item = resolve_array_hole(activation, this, i, item)?;
let new_item = callback.call(
reciever,
&[item, i.into(), this.into()],
activation,
reciever.and_then(|r| r.proto()),
)?;
new_array.push(new_item);
}
return build_array(activation, new_array);
}
}
Ok(Value::Undefined)
}
/// Implements `Array.filter`
pub fn filter<'gc>(
activation: &mut Activation<'_, 'gc, '_>,
this: Option<Object<'gc>>,
args: &[Value<'gc>],
) -> Result<Value<'gc>, Error> {
if let Some(this) = this {
if let Some(array) = this.as_array_storage() {
let callback = args
.get(0)
.cloned()
.unwrap_or(Value::Undefined)
.coerce_to_object(activation)?;
let reciever = args
.get(1)
.cloned()
.unwrap_or(Value::Null)
.coerce_to_object(activation)
.ok();
let mut new_array = ArrayStorage::new(0);
for (i, item) in array.iter().enumerate() {
let item = resolve_array_hole(activation, this, i, item)?;
let is_allowed = callback
.call(
reciever,
&[item.clone(), i.into(), this.into()],
activation,
reciever.and_then(|r| r.proto()),
)?
.coerce_to_boolean();
if is_allowed {
new_array.push(item);
}
}
return build_array(activation, new_array);
}
}
Ok(Value::Undefined)
}
/// Implements `Array.every`
pub fn every<'gc>(
activation: &mut Activation<'_, 'gc, '_>,
this: Option<Object<'gc>>,
args: &[Value<'gc>],
) -> Result<Value<'gc>, Error> {
if let Some(this) = this {
if let Some(array) = this.as_array_storage() {
let callback = args
.get(0)
.cloned()
.unwrap_or(Value::Undefined)
.coerce_to_object(activation)?;
let reciever = args
.get(1)
.cloned()
.unwrap_or(Value::Null)
.coerce_to_object(activation)
.ok();
let mut is_every = true;
for (i, item) in array.iter().enumerate() {
let item = resolve_array_hole(activation, this, i, item)?;
is_every &= callback
.call(
reciever,
&[item, i.into(), this.into()],
activation,
reciever.and_then(|r| r.proto()),
)?
.coerce_to_boolean();
}
return Ok(is_every.into());
}
}
Ok(Value::Undefined)
}
/// Implements `Array.some`
pub fn some<'gc>(
activation: &mut Activation<'_, 'gc, '_>,
this: Option<Object<'gc>>,
args: &[Value<'gc>],
) -> Result<Value<'gc>, Error> {
if let Some(this) = this {
if let Some(array) = this.as_array_storage() {
let callback = args
.get(0)
.cloned()
.unwrap_or(Value::Undefined)
.coerce_to_object(activation)?;
let reciever = args
.get(1)
.cloned()
.unwrap_or(Value::Null)
.coerce_to_object(activation)
.ok();
let mut is_some = false;
for (i, item) in array.iter().enumerate() {
let item = resolve_array_hole(activation, this, i, item)?;
is_some |= callback
.call(
reciever,
&[item, i.into(), this.into()],
activation,
reciever.and_then(|r| r.proto()),
)?
.coerce_to_boolean();
}
return Ok(is_some.into());
}
}
Ok(Value::Undefined)
}
/// Construct `Array`'s class.
pub fn create_class<'gc>(mc: MutationContext<'gc, '_>) -> GcCell<'gc, Class<'gc>> {
let class = Class::new(
@ -196,5 +416,30 @@ pub fn create_class<'gc>(mc: MutationContext<'gc, '_>) -> GcCell<'gc, Class<'gc>
Method::from_builtin(value_of),
));
class.write(mc).define_instance_trait(Trait::from_method(
QName::new(Namespace::public_namespace(), "forEach"),
Method::from_builtin(for_each),
));
class.write(mc).define_instance_trait(Trait::from_method(
QName::new(Namespace::public_namespace(), "map"),
Method::from_builtin(map),
));
class.write(mc).define_instance_trait(Trait::from_method(
QName::new(Namespace::public_namespace(), "filter"),
Method::from_builtin(filter),
));
class.write(mc).define_instance_trait(Trait::from_method(
QName::new(Namespace::public_namespace(), "every"),
Method::from_builtin(every),
));
class.write(mc).define_instance_trait(Trait::from_method(
QName::new(Namespace::public_namespace(), "some"),
Method::from_builtin(some),
));
class
}

View File

@ -364,6 +364,11 @@ swf_tests! {
(as3_array_tostring, "avm2/array_tostring", 1),
(as3_array_valueof, "avm2/array_valueof", 1),
(as3_array_join, "avm2/array_join", 1),
(as3_array_foreach, "avm2/array_foreach", 1),
(as3_array_map, "avm2/array_map", 1),
(as3_array_filter, "avm2/array_filter", 1),
(as3_array_every, "avm2/array_every", 1),
(as3_array_some, "avm2/array_some", 1),
}
// TODO: These tests have some inaccuracies currently, so we use approx_eq to test that numeric values are close enough.

View File

@ -0,0 +1,31 @@
package {
public class Test {
}
}
function assert_array(a) {
for (var i = 0; i < a.length; i += 1) {
trace(a[i]);
}
}
trace("//var a = new Array(5,3,1,9,16)");
var a = new Array(5,3,1,9,16);
trace("//trace(a.every(function (val) { return val === 5; }));");
trace(a.every(function (val) {
return val === 5;
}));
trace("//trace(a.every(function (val) { return val !== 20; }));");
trace(a.every(function (val) {
return val !== 20;
}));
trace("//var b = new Array();");
var b = new Array();
trace("//trace(b.every(function (val) { return val === 5; }));");
trace(b.every(function (val) {
return val === 5;
}));

View File

@ -0,0 +1,8 @@
//var a = new Array(5,3,1,9,16)
//trace(a.every(function (val) { return val === 5; }));
false
//trace(a.every(function (val) { return val !== 20; }));
true
//var b = new Array();
//trace(b.every(function (val) { return val === 5; }));
true

Binary file not shown.

Binary file not shown.

View File

@ -0,0 +1,21 @@
package {
public class Test {
}
}
function assert_array(a) {
for (var i = 0; i < a.length; i += 1) {
trace(a[i]);
}
}
trace("//var a = new Array(5,3,1,9,16)");
var a = new Array(5,3,1,9,16);
trace("//var b = a.filter(function (val) { ... });");
var b = a.filter(function (val) {
return val <= 5;
});
trace("//(contents of b)");
assert_array(b);

View File

@ -0,0 +1,6 @@
//var a = new Array(5,3,1,9,16)
//var b = a.filter(function (val) { ... });
//(contents of b)
5
3
1

Binary file not shown.

Binary file not shown.

View File

@ -0,0 +1,19 @@
package {
public class Test {
}
}
trace("//var a = new Array(5,\"abc\")");
var a = new Array(5,"abc");
trace("//a.forEach(function (val) { ... }, a);");
a.forEach(function (val, index, array) {
trace("//(in callback) this === a;")
trace(this === a);
trace("//val");
trace(val);
trace("//index");
trace(index);
trace("//array === a");
trace(array === a);
}, a);

View File

@ -0,0 +1,18 @@
//var a = new Array(5,"abc")
//a.forEach(function (val) { ... }, a);
//(in callback) this === a;
true
//val
5
//index
0
//array === a
true
//(in callback) this === a;
true
//val
abc
//index
1
//array === a
true

Binary file not shown.

Binary file not shown.

View File

@ -0,0 +1,21 @@
package {
public class Test {
}
}
function assert_array(a) {
for (var i = 0; i < a.length; i += 1) {
trace(a[i]);
}
}
trace("//var a = new Array(5,3,1,9,16)");
var a = new Array(5,3,1,9,16);
trace("//var b = a.map(function (val) { return val + 1; });");
var b = a.map(function (val) {
return val + 1;
});
trace("//(contents of b)");
assert_array(b);

View File

@ -0,0 +1,8 @@
//var a = new Array(5,3,1,9,16)
//var b = a.map(function (val) { return val + 1; });
//(contents of b)
6
4
2
10
17

Binary file not shown.

Binary file not shown.

View File

@ -0,0 +1,31 @@
package {
public class Test {
}
}
function assert_array(a) {
for (var i = 0; i < a.length; i += 1) {
trace(a[i]);
}
}
trace("//var a = new Array(5,3,1,9,16)");
var a = new Array(5,3,1,9,16);
trace("//trace(a.some(function (val) { return val === 5; }));");
trace(a.some(function (val) {
return val === 5;
}));
trace("//trace(a.some(function (val) { return val === 20; }));");
trace(a.some(function (val) {
return val === 20;
}));
trace("//var b = new Array();");
var b = new Array();
trace("//trace(b.some(function (val) { return val === 20; }));");
trace(b.some(function (val) {
return val === 20;
}));

View File

@ -0,0 +1,8 @@
//var a = new Array(5,3,1,9,16)
//trace(a.some(function (val) { return val === 5; }));
true
//trace(a.some(function (val) { return val === 20; }));
false
//var b = new Array();
//trace(b.some(function (val) { return val === 20; }));
false

Binary file not shown.

Binary file not shown.