avm2: Implement `Array.sort`

This commit is contained in:
David Wendt 2020-09-03 20:22:42 -04:00 committed by Mike Welsh
parent bb19699739
commit 0aa2c50118
6 changed files with 716 additions and 1 deletions

View File

@ -12,7 +12,7 @@ use crate::avm2::value::Value;
use crate::avm2::Error;
use enumset::{EnumSet, EnumSetType};
use gc_arena::{GcCell, MutationContext};
use std::cmp::min;
use std::cmp::{min, Ordering};
use std::mem::swap;
/// Implements `Array`'s instance initializer.
@ -786,6 +786,222 @@ enum SortOptions {
Numeric,
}
/// Identity closure shim which exists purely to decorate closure types with
/// the HRTB necessary to accept an activation.
fn constrain<'a, 'gc, 'ctxt, F>(f: F) -> F
where
F: FnMut(&mut Activation<'a, 'gc, 'ctxt>, Value<'gc>, Value<'gc>) -> Result<Ordering, Error>,
{
f
}
/// Sort array storage.
///
/// This function expects it's values to have been pre-enumerated. They will be
/// sorted in-place. It is the caller's responsibility to place the resulting
/// half of the sorted array wherever.
///
/// This function will reverse the sort order if `Descending` sort is requested.
///
/// This function will return `false` in the event that the `UniqueSort`
/// constraint has been violated (`sort_func` returned `Ordering::Equal`). In
/// this case, you should cancel the in-place sorting operation and return 0 to
/// the caller. In the event that this function yields a runtime error, the
/// contents of the `values` array will be sorted in a random order.
fn sort_inner<'a, 'gc, 'ctxt, C>(
activation: &mut Activation<'a, 'gc, 'ctxt>,
values: &mut [(usize, Option<Value<'gc>>)],
options: EnumSet<SortOptions>,
mut sort_func: C,
) -> Result<bool, Error>
where
C: FnMut(&mut Activation<'a, 'gc, 'ctxt>, Value<'gc>, Value<'gc>) -> Result<Ordering, Error>,
{
let mut unique_sort_satisfied = true;
let mut error_signal = Ok(());
values.sort_unstable_by(|(_a_index, a), (_b_index, b)| {
let unresolved_a = a.clone().unwrap_or(Value::Undefined);
let unresolved_b = b.clone().unwrap_or(Value::Undefined);
if matches!(unresolved_a, Value::Undefined) && matches!(unresolved_b, Value::Undefined) {
unique_sort_satisfied = false;
return Ordering::Equal;
} else if matches!(unresolved_a, Value::Undefined) {
return Ordering::Greater;
} else if matches!(unresolved_b, Value::Undefined) {
return Ordering::Less;
}
match sort_func(
activation,
a.clone().unwrap_or(Value::Undefined),
b.clone().unwrap_or(Value::Undefined),
) {
Ok(Ordering::Equal) => {
unique_sort_satisfied = false;
Ordering::Equal
}
Ok(v) if options.contains(SortOptions::Descending) => v.reverse(),
Ok(v) => v,
Err(e) => {
error_signal = Err(e);
Ordering::Less
}
}
});
error_signal?;
Ok(!options.contains(SortOptions::UniqueSort) || unique_sort_satisfied)
}
fn compare_string_case_sensitive<'gc>(
activation: &mut Activation<'_, 'gc, '_>,
a: Value<'gc>,
b: Value<'gc>,
) -> Result<Ordering, Error> {
let string_a = a.coerce_to_string(activation)?;
let string_b = b.coerce_to_string(activation)?;
Ok(string_a.cmp(&string_b))
}
fn compare_string_case_insensitive<'gc>(
activation: &mut Activation<'_, 'gc, '_>,
a: Value<'gc>,
b: Value<'gc>,
) -> Result<Ordering, Error> {
let string_a = a.coerce_to_string(activation)?.to_lowercase();
let string_b = b.coerce_to_string(activation)?.to_lowercase();
Ok(string_a.cmp(&string_b))
}
fn compare_numeric<'gc>(
activation: &mut Activation<'_, 'gc, '_>,
a: Value<'gc>,
b: Value<'gc>,
) -> Result<Ordering, Error> {
let num_a = a.coerce_to_number(activation)?;
let num_b = b.coerce_to_number(activation)?;
if num_a.is_nan() && num_b.is_nan() {
Ok(Ordering::Equal)
} else if num_a.is_nan() {
Ok(Ordering::Greater)
} else if num_b.is_nan() {
Ok(Ordering::Less)
} else {
Ok(num_a.partial_cmp(&num_b).unwrap())
}
}
/// Impl `Array.sort`
pub fn sort<'gc>(
activation: &mut Activation<'_, 'gc, '_>,
this: Option<Object<'gc>>,
args: &[Value<'gc>],
) -> Result<Value<'gc>, Error> {
if let Some(this) = this {
let (compare_fnc, options) = if args.len() > 1 {
(
Some(
args.get(0)
.cloned()
.unwrap_or(Value::Undefined)
.coerce_to_object(activation)?,
),
args.get(1)
.cloned()
.unwrap_or_else(|| 0.into())
.coerce_to_enumset(activation)?,
)
} else {
(
None,
args.get(0)
.cloned()
.unwrap_or_else(|| 0.into())
.coerce_to_enumset(activation)?,
)
};
let mut values = if let Some(array) = this.as_array_storage() {
array
.iter()
.enumerate()
.collect::<Vec<(usize, Option<Value<'gc>>)>>()
} else {
return Ok(0.into());
};
let unique_satisified = if let Some(v) = compare_fnc {
sort_inner(
activation,
&mut values,
options,
constrain(|activation, a, b| {
let order = v
.call(None, &[a, b], activation, None)?
.coerce_to_number(activation)?;
if order > 0.0 {
Ok(Ordering::Greater)
} else if order < 0.0 {
Ok(Ordering::Less)
} else {
Ok(Ordering::Equal)
}
}),
)?
} else if options.contains(SortOptions::Numeric) {
sort_inner(activation, &mut values, options, compare_numeric)?
} else if options.contains(SortOptions::CaseInsensitive) {
sort_inner(
activation,
&mut values,
options,
compare_string_case_insensitive,
)?
} else {
sort_inner(
activation,
&mut values,
options,
compare_string_case_sensitive,
)?
};
if unique_satisified {
if options.contains(SortOptions::ReturnIndexedArray) {
return build_array(
activation,
ArrayStorage::from_storage(
values
.iter()
.map(|(i, _v)| Some(i.clone().into()))
.collect(),
),
);
} else {
let mut new_array =
ArrayStorage::from_storage(values.iter().map(|(_i, v)| v.clone()).collect());
if let Some(mut old_array) =
this.as_array_storage_mut(activation.context.gc_context)
{
swap(&mut *old_array, &mut new_array);
}
return Ok(this.into());
}
}
}
Ok(0.into())
}
/// Construct `Array`'s class.
pub fn create_class<'gc>(mc: MutationContext<'gc, '_>) -> GcCell<'gc, Class<'gc>> {
let class = Class::new(
@ -896,6 +1112,11 @@ pub fn create_class<'gc>(mc: MutationContext<'gc, '_>) -> GcCell<'gc, Class<'gc>
Method::from_builtin(splice),
));
class.write(mc).define_instance_trait(Trait::from_method(
QName::new(Namespace::public_namespace(), "sort"),
Method::from_builtin(sort),
));
class.write(mc).define_class_trait(Trait::from_const(
QName::new(Namespace::public_namespace(), "CASEINSENSITIVE"),
Multiname::from(QName::new(Namespace::public_namespace(), "uint")),

View File

@ -379,6 +379,7 @@ swf_tests! {
(as3_array_unshift, "avm2/array_unshift", 1),
(as3_array_slice, "avm2/array_slice", 1),
(as3_array_splice, "avm2/array_splice", 1),
(as3_array_sort, "avm2/array_sort", 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,196 @@
package {
public class Test {
}
}
function assert_array(a) {
for (var i = 0; i < a.length; i += 1) {
trace(a[i]);
}
}
function length_based_comparison(a, b) {
if ("length" in a) {
if ("length" in b) {
return a.length - b.length;
} else {
return a.length - b;
}
} else {
if ("length" in b) {
return a - b.length;
} else {
return a - b;
}
}
}
function sub_comparison(a, b) {
return a - b;
}
function lbc(a, b) {
trace(a);
trace(b);
var x = length_based_comparison(a, b);
trace(x);
return x;
}
function sc(a, b) {
trace(a);
trace(b);
var x = sub_comparison(a, b);
trace(x);
return x;
}
function fresh_array() {
trace("//var a = new Array(5,3,1,\"Abc\",\"2\",\"aba\",false,null,\"zzz\")");
var a = new Array(5,3,1,"Abc","2","aba",false,null,"zzz");
trace("//a[11] = \"not a hole\";");
a[11] = "not a hole";
return a;
}
function fresh_array_b() {
trace("//var b = new Array(5,3,\"2\",false,true,NaN)");
var b = new Array(5,3,"2",false,true,NaN);
return b;
}
function check_holes(a) {
trace("//Array.prototype[10] = \"hole10\";");
Array.prototype[10] = "hole10";
trace("//Array.prototype[11] = \"hole11\";");
Array.prototype[11] = "hole11";
trace("//Array.prototype[12] = \"hole12\";");
Array.prototype[12] = "hole12";
trace("//(contents of previous array)");
assert_array(a);
trace("//(cleaning up our holes...)");
delete Array.prototype[10];
delete Array.prototype[11];
delete Array.prototype[12];
trace("//Array.prototype[9] = undefined;");
Array.prototype[9] = undefined;
trace("//Array.prototype[10] = \"hole in slot 10\";");
Array.prototype[10] = "hole in slot 10";
}
var a = fresh_array();
trace("//Array.prototype[9] = undefined;");
Array.prototype[9] = undefined;
trace("//Array.prototype[10] = \"hole in slot 10\";");
Array.prototype[10] = "hole in slot 10";
trace("//a.sort(Array.UNIQUESORT) === 0");
trace(a.sort(Array.UNIQUESORT) === 0);
a = fresh_array();
trace("//(contents of a.sort(Array.RETURNINDEXEDARRAY))");
assert_array(a.sort(Array.RETURNINDEXEDARRAY));
trace("//(contents of a.sort())");
assert_array(a.sort());
check_holes(a);
a = fresh_array();
trace("//(contents of a.sort(Array.CASEINSENSITIVE | Array.RETURNINDEXEDARRAY))");
assert_array(a.sort(Array.CASEINSENSITIVE | Array.RETURNINDEXEDARRAY));
trace("//(contents of a.sort(Array.CASEINSENSITIVE))");
assert_array(a.sort(Array.CASEINSENSITIVE));
check_holes(a);
a = fresh_array();
trace("//(contents of a.sort(Array.DESCENDING | Array.RETURNINDEXEDARRAY))");
assert_array(a.sort(Array.DESCENDING | Array.RETURNINDEXEDARRAY));
trace("//(contents of a.sort(Array.DESCENDING))");
assert_array(a.sort(Array.DESCENDING));
check_holes(a);
a = fresh_array();
trace("//(contents of a.sort(Array.CASEINSENSITIVE | Array.DESCENDING | Array.RETURNINDEXEDARRAY))");
assert_array(a.sort(Array.CASEINSENSITIVE | Array.DESCENDING | Array.RETURNINDEXEDARRAY));
trace("//(contents of a.sort(Array.CASEINSENSITIVE | Array.DESCENDING))");
assert_array(a.sort(Array.CASEINSENSITIVE | Array.DESCENDING));
check_holes(a);
a = fresh_array();
trace("//var b = new Array(5,3,2,1,\"2\",false,true,NaN)");
var b = new Array(5,3,2,1,"2",false,true,NaN);
trace("//b.sort(Array.NUMERIC | Array.UNIQUESORT) === 0");
trace(b.sort(Array.NUMERIC | Array.UNIQUESORT) === 0);
b = fresh_array_b();
trace("//(contents of b.sort(Array.NUMERIC | Array.RETURNINDEXEDARRAY))");
assert_array(b.sort(Array.NUMERIC | Array.RETURNINDEXEDARRAY));
trace("//(contents of b.sort(Array.NUMERIC))");
assert_array(b.sort(Array.NUMERIC));
check_holes(b);
b = fresh_array_b();
trace("//(contents of b.sort(Array.NUMERIC | 1))");
assert_array(b.sort(Array.NUMERIC | 1));
b = fresh_array_b();
trace("//(contents of b.sort(Array.NUMERIC | Array.DESCENDING | Array.RETURNINDEXEDARRAY))");
assert_array(b.sort(Array.NUMERIC | Array.DESCENDING | Array.RETURNINDEXEDARRAY));
trace("//(contents of b.sort(16 | Array.DESCENDING))");
assert_array(b.sort(16 | Array.DESCENDING));
check_holes(b);
trace("//var a = new Array(7,2,1,\"3\",\"4\")");
var a = new Array(7,2,1,"3","4");
trace("//(contents of a.sort(sub_comparison))");
assert_array(a.sort(sub_comparison));
trace("//(contents of a.sort(sub_comparison, 2))");
assert_array(a.sort(sub_comparison, 2));
trace("//(contents of a.sort(sub_comparison, Array.RETURNINDEXEDARRAY))");
assert_array(a.sort(sub_comparison, Array.RETURNINDEXEDARRAY));
trace("//(contents of a.sort(sub_comparison, Array.DESCENDING | 8))");
assert_array(a.sort(sub_comparison, Array.DESCENDING | 8));
trace("//a.sort(sub_comparison, Array.UNIQUESORT) === 0");
trace(a.sort(sub_comparison, Array.UNIQUESORT) === 0);
trace("//var c = new Array(3,\"abc\")");
var c = new Array(3,"abc");
trace("//c.sort(sub_comparison, Array.UNIQUESORT) === 0");
trace(c.sort(sub_comparison, Array.UNIQUESORT) === 0);
trace("//var d = new Array(3,\"4\")");
var d = new Array(3,"4");
trace("//(contents of d.sort(sub_comparison, 4))");
assert_array(d.sort(sub_comparison, 4));

View File

@ -0,0 +1,297 @@
//var a = new Array(5,3,1,"Abc","2","aba",false,null,"zzz")
//a[11] = "not a hole";
//Array.prototype[9] = undefined;
//Array.prototype[10] = "hole in slot 10";
//a.sort(Array.UNIQUESORT) === 0
false
//var a = new Array(5,3,1,"Abc","2","aba",false,null,"zzz")
//a[11] = "not a hole";
//(contents of a.sort(Array.RETURNINDEXEDARRAY))
2
4
1
0
3
5
6
10
11
7
8
9
//(contents of a.sort())
1
2
3
5
Abc
aba
false
hole in slot 10
not a hole
null
zzz
undefined
//Array.prototype[10] = "hole10";
//Array.prototype[11] = "hole11";
//Array.prototype[12] = "hole12";
//(contents of previous array)
1
2
3
5
Abc
aba
false
hole in slot 10
not a hole
null
zzz
hole11
//(cleaning up our holes...)
//Array.prototype[9] = undefined;
//Array.prototype[10] = "hole in slot 10";
//var a = new Array(5,3,1,"Abc","2","aba",false,null,"zzz")
//a[11] = "not a hole";
//(contents of a.sort(Array.CASEINSENSITIVE | Array.RETURNINDEXEDARRAY))
2
4
1
0
5
3
6
10
11
7
8
9
//(contents of a.sort(Array.CASEINSENSITIVE))
1
2
3
5
aba
Abc
false
hole in slot 10
not a hole
null
zzz
undefined
//Array.prototype[10] = "hole10";
//Array.prototype[11] = "hole11";
//Array.prototype[12] = "hole12";
//(contents of previous array)
1
2
3
5
aba
Abc
false
hole in slot 10
not a hole
null
zzz
hole11
//(cleaning up our holes...)
//Array.prototype[9] = undefined;
//Array.prototype[10] = "hole in slot 10";
//var a = new Array(5,3,1,"Abc","2","aba",false,null,"zzz")
//a[11] = "not a hole";
//(contents of a.sort(Array.DESCENDING | Array.RETURNINDEXEDARRAY))
8
7
11
10
6
5
3
0
1
4
2
9
//(contents of a.sort(Array.DESCENDING))
zzz
null
not a hole
hole in slot 10
false
aba
Abc
5
3
2
1
undefined
//Array.prototype[10] = "hole10";
//Array.prototype[11] = "hole11";
//Array.prototype[12] = "hole12";
//(contents of previous array)
zzz
null
not a hole
hole in slot 10
false
aba
Abc
5
3
2
1
hole11
//(cleaning up our holes...)
//Array.prototype[9] = undefined;
//Array.prototype[10] = "hole in slot 10";
//var a = new Array(5,3,1,"Abc","2","aba",false,null,"zzz")
//a[11] = "not a hole";
//(contents of a.sort(Array.CASEINSENSITIVE | Array.DESCENDING | Array.RETURNINDEXEDARRAY))
8
7
11
10
6
3
5
0
1
4
2
9
//(contents of a.sort(Array.CASEINSENSITIVE | Array.DESCENDING))
zzz
null
not a hole
hole in slot 10
false
Abc
aba
5
3
2
1
undefined
//Array.prototype[10] = "hole10";
//Array.prototype[11] = "hole11";
//Array.prototype[12] = "hole12";
//(contents of previous array)
zzz
null
not a hole
hole in slot 10
false
Abc
aba
5
3
2
1
hole11
//(cleaning up our holes...)
//Array.prototype[9] = undefined;
//Array.prototype[10] = "hole in slot 10";
//var a = new Array(5,3,1,"Abc","2","aba",false,null,"zzz")
//a[11] = "not a hole";
//var b = new Array(5,3,2,1,"2",false,true,NaN)
//b.sort(Array.NUMERIC | Array.UNIQUESORT) === 0
true
//var b = new Array(5,3,"2",false,true,NaN)
//(contents of b.sort(Array.NUMERIC | Array.RETURNINDEXEDARRAY))
3
4
2
1
0
5
//(contents of b.sort(Array.NUMERIC))
false
true
2
3
5
NaN
//Array.prototype[10] = "hole10";
//Array.prototype[11] = "hole11";
//Array.prototype[12] = "hole12";
//(contents of previous array)
false
true
2
3
5
NaN
//(cleaning up our holes...)
//Array.prototype[9] = undefined;
//Array.prototype[10] = "hole in slot 10";
//var b = new Array(5,3,"2",false,true,NaN)
//(contents of b.sort(Array.NUMERIC | 1))
false
true
2
3
5
NaN
//var b = new Array(5,3,"2",false,true,NaN)
//(contents of b.sort(Array.NUMERIC | Array.DESCENDING | Array.RETURNINDEXEDARRAY))
5
0
1
2
4
3
//(contents of b.sort(16 | Array.DESCENDING))
NaN
5
3
2
true
false
//Array.prototype[10] = "hole10";
//Array.prototype[11] = "hole11";
//Array.prototype[12] = "hole12";
//(contents of previous array)
NaN
5
3
2
true
false
//(cleaning up our holes...)
//Array.prototype[9] = undefined;
//Array.prototype[10] = "hole in slot 10";
//var a = new Array(7,2,1,"3","4")
//(contents of a.sort(sub_comparison))
1
2
3
4
7
//(contents of a.sort(sub_comparison, 2))
7
4
3
2
1
//(contents of a.sort(sub_comparison, Array.RETURNINDEXEDARRAY))
4
3
2
1
0
//(contents of a.sort(sub_comparison, Array.DESCENDING | 8))
0
1
2
3
4
//a.sort(sub_comparison, Array.UNIQUESORT) === 0
false
//var c = new Array(3,"abc")
//c.sort(sub_comparison, Array.UNIQUESORT) === 0
true
//var d = new Array(3,"4")
//(contents of d.sort(sub_comparison, 4))
3
4

Binary file not shown.

Binary file not shown.