diff --git a/core/src/avm2/globals/array.rs b/core/src/avm2/globals/array.rs index 8b518b2dc..efa03ba46 100644 --- a/core/src/avm2/globals/array.rs +++ b/core/src/avm2/globals/array.rs @@ -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, 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>` 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>, +) -> Result, 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>, + args: &[Value<'gc>], +) -> Result, 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>, + args: &[Value<'gc>], +) -> Result, 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>, + args: &[Value<'gc>], +) -> Result, 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>, + args: &[Value<'gc>], +) -> Result, 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>, + args: &[Value<'gc>], +) -> Result, 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 } diff --git a/core/tests/regression_tests.rs b/core/tests/regression_tests.rs index 76804bc60..5a85455a9 100644 --- a/core/tests/regression_tests.rs +++ b/core/tests/regression_tests.rs @@ -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. diff --git a/core/tests/swfs/avm2/array_every/Test.as b/core/tests/swfs/avm2/array_every/Test.as new file mode 100644 index 000000000..291723bc5 --- /dev/null +++ b/core/tests/swfs/avm2/array_every/Test.as @@ -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; +})); \ No newline at end of file diff --git a/core/tests/swfs/avm2/array_every/output.txt b/core/tests/swfs/avm2/array_every/output.txt new file mode 100644 index 000000000..83fa7569f --- /dev/null +++ b/core/tests/swfs/avm2/array_every/output.txt @@ -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 diff --git a/core/tests/swfs/avm2/array_every/test.fla b/core/tests/swfs/avm2/array_every/test.fla new file mode 100644 index 000000000..b27a5e649 Binary files /dev/null and b/core/tests/swfs/avm2/array_every/test.fla differ diff --git a/core/tests/swfs/avm2/array_every/test.swf b/core/tests/swfs/avm2/array_every/test.swf new file mode 100644 index 000000000..9206b548e Binary files /dev/null and b/core/tests/swfs/avm2/array_every/test.swf differ diff --git a/core/tests/swfs/avm2/array_filter/Test.as b/core/tests/swfs/avm2/array_filter/Test.as new file mode 100644 index 000000000..232983f40 --- /dev/null +++ b/core/tests/swfs/avm2/array_filter/Test.as @@ -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); \ No newline at end of file diff --git a/core/tests/swfs/avm2/array_filter/output.txt b/core/tests/swfs/avm2/array_filter/output.txt new file mode 100644 index 000000000..d0997b24b --- /dev/null +++ b/core/tests/swfs/avm2/array_filter/output.txt @@ -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 diff --git a/core/tests/swfs/avm2/array_filter/test.fla b/core/tests/swfs/avm2/array_filter/test.fla new file mode 100644 index 000000000..b27a5e649 Binary files /dev/null and b/core/tests/swfs/avm2/array_filter/test.fla differ diff --git a/core/tests/swfs/avm2/array_filter/test.swf b/core/tests/swfs/avm2/array_filter/test.swf new file mode 100644 index 000000000..cd4b22f05 Binary files /dev/null and b/core/tests/swfs/avm2/array_filter/test.swf differ diff --git a/core/tests/swfs/avm2/array_foreach/Test.as b/core/tests/swfs/avm2/array_foreach/Test.as new file mode 100644 index 000000000..0557022fe --- /dev/null +++ b/core/tests/swfs/avm2/array_foreach/Test.as @@ -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); \ No newline at end of file diff --git a/core/tests/swfs/avm2/array_foreach/output.txt b/core/tests/swfs/avm2/array_foreach/output.txt new file mode 100644 index 000000000..c1bd08f5d --- /dev/null +++ b/core/tests/swfs/avm2/array_foreach/output.txt @@ -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 diff --git a/core/tests/swfs/avm2/array_foreach/test.fla b/core/tests/swfs/avm2/array_foreach/test.fla new file mode 100644 index 000000000..b27a5e649 Binary files /dev/null and b/core/tests/swfs/avm2/array_foreach/test.fla differ diff --git a/core/tests/swfs/avm2/array_foreach/test.swf b/core/tests/swfs/avm2/array_foreach/test.swf new file mode 100644 index 000000000..81f173efa Binary files /dev/null and b/core/tests/swfs/avm2/array_foreach/test.swf differ diff --git a/core/tests/swfs/avm2/array_map/Test.as b/core/tests/swfs/avm2/array_map/Test.as new file mode 100644 index 000000000..6a6673259 --- /dev/null +++ b/core/tests/swfs/avm2/array_map/Test.as @@ -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); \ No newline at end of file diff --git a/core/tests/swfs/avm2/array_map/output.txt b/core/tests/swfs/avm2/array_map/output.txt new file mode 100644 index 000000000..1328742b3 --- /dev/null +++ b/core/tests/swfs/avm2/array_map/output.txt @@ -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 diff --git a/core/tests/swfs/avm2/array_map/test.fla b/core/tests/swfs/avm2/array_map/test.fla new file mode 100644 index 000000000..b27a5e649 Binary files /dev/null and b/core/tests/swfs/avm2/array_map/test.fla differ diff --git a/core/tests/swfs/avm2/array_map/test.swf b/core/tests/swfs/avm2/array_map/test.swf new file mode 100644 index 000000000..d4c189398 Binary files /dev/null and b/core/tests/swfs/avm2/array_map/test.swf differ diff --git a/core/tests/swfs/avm2/array_some/Test.as b/core/tests/swfs/avm2/array_some/Test.as new file mode 100644 index 000000000..963cfdfd4 --- /dev/null +++ b/core/tests/swfs/avm2/array_some/Test.as @@ -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; +})); \ No newline at end of file diff --git a/core/tests/swfs/avm2/array_some/output.txt b/core/tests/swfs/avm2/array_some/output.txt new file mode 100644 index 000000000..081356adf --- /dev/null +++ b/core/tests/swfs/avm2/array_some/output.txt @@ -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 diff --git a/core/tests/swfs/avm2/array_some/test.fla b/core/tests/swfs/avm2/array_some/test.fla new file mode 100644 index 000000000..b27a5e649 Binary files /dev/null and b/core/tests/swfs/avm2/array_some/test.fla differ diff --git a/core/tests/swfs/avm2/array_some/test.swf b/core/tests/swfs/avm2/array_some/test.swf new file mode 100644 index 000000000..52cab2d32 Binary files /dev/null and b/core/tests/swfs/avm2/array_some/test.swf differ