Skip to content

avm1: Fix Object < Object comparison #6548

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

Merged
merged 5 commits into from
Mar 29, 2022
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
8 changes: 2 additions & 6 deletions core/src/avm1/activation.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1473,9 +1473,7 @@ impl<'a, 'gc, 'gc_context> Activation<'a, 'gc, 'gc_context> {
// ECMA-262 s. 11.8.1
let a = self.context.avm1.pop();
let b = self.context.avm1.pop();
let result = b
.abstract_lt(a, self)?
.map_or(Value::Undefined, Value::from);
let result = b.abstract_lt(a, self)?;
self.context.avm1.push(result);
Ok(FrameControl::Continue)
}
Expand All @@ -1484,9 +1482,7 @@ impl<'a, 'gc, 'gc_context> Activation<'a, 'gc, 'gc_context> {
// ECMA-262 s. 11.8.2
let a = self.context.avm1.pop();
let b = self.context.avm1.pop();
let result = a
.abstract_lt(b, self)?
.map_or(Value::Undefined, Value::from);
let result = a.abstract_lt(b, self)?;
self.context.avm1.push(result);
Ok(FrameControl::Continue)
}
Expand Down
69 changes: 46 additions & 23 deletions core/src/avm1/value.rs
Original file line number Diff line number Diff line change
Expand Up @@ -224,7 +224,9 @@ impl<'gc> Value<'gc> {
activation: &mut Activation<'_, 'gc, '_>,
) -> Result<Value<'gc>, Error<'gc>> {
Ok(match self {
Value::Object(object) => object.call_method("valueOf".into(), &[], activation)?,
Value::Object(object) if object.as_display_object().is_none() => {
Copy link
Contributor

@relrelb relrelb Mar 28, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not directly related to this PR, but I think StageObject should be a separate enum member of Value rather than a "subclass" of Value::Object, since I have seen this special case exclusion too many times. This probably means that StageObject won't be able to implement TObject anymore, so it's not trivial at all, but I still have a feeling that StageObject and Object are more different than they have in common. Maybe this worth re-thinking as part of #5492.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I had the same thought while writing this PR. This would fit nicely in with #5492.

object.call_method("valueOf".into(), &[], activation)?
}
val => val.to_owned(),
})
}
Expand Down Expand Up @@ -271,20 +273,35 @@ impl<'gc> Value<'gc> {
&self,
other: Value<'gc>,
activation: &mut Activation<'_, 'gc, '_>,
) -> Result<Option<bool>, Error<'gc>> {
) -> Result<Value<'gc>, Error<'gc>> {
// If either parameter's `valueOf` results in a non-movieclip object, immediately return false.
// This is the common case for objects because `Object.prototype.valueOf` returns the same object.
// For example, `{} < {}` is false.
let prim_self = self.to_primitive_num(activation)?;
if matches!(prim_self, Value::Object(o) if o.as_display_object().is_none()) {
return Ok(false.into());
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Seems like if both operands are plain objects, then both valueOf callbacks should be invoked:

var a = { valueOf: function() { trace("in a.valueOf"); } };
var b = { valueOf: function() { trace("in b.valueOf"); } };
trace(a < b); // traces "a.valueOf", "b.valueOf"
trace(b < a); // traces "b.valueOf", "a.valueOf"

If I get it right, Ruffle with this PR would bail-out early and invoke only the first valueOf.
So I think the correct order is something like this:

  1. a.to_primitive_num()
  2. b.to_primitive_num()
  3. if prim_self is an object, return false
  4. if prim_other is an object, return false

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The PR runs this example correctly -- the valueOfs in this case return undefined and not an Object, so the early-exit doesn't happen.

The early-exit does happen if you add return {} to the valueOf calls.

}
let prim_other = other.to_primitive_num(activation)?;

if let (Value::String(a), Value::String(b)) = (&prim_self, &prim_other) {
return Ok(a.to_string().bytes().lt(b.to_string().bytes()).into());
if matches!(prim_other, Value::Object(o) if o.as_display_object().is_none()) {
return Ok(false.into());
}

let num_self = prim_self.primitive_as_number(activation);
let num_other = prim_other.primitive_as_number(activation);

Ok(num_self
.partial_cmp(&num_other)
.map(|o| o == std::cmp::Ordering::Less))
let result = match (prim_self, prim_other) {
(Value::String(a), Value::String(b)) => {
let a = a.to_string();
let b = b.to_string();
a.bytes().lt(b.bytes()).into()
}
(a, b) => {
// Coerce to number and compare, with any NaN resulting in undefined.
let a = a.primitive_as_number(activation);
let b = b.primitive_as_number(activation);
a.partial_cmp(&b).map_or(Value::Undefined, |o| {
Value::Bool(o == std::cmp::Ordering::Less)
})
}
};
Ok(result)
}

/// ECMA-262 2nd edition s. 11.9.3 Abstract equality comparison algorithm
Expand Down Expand Up @@ -583,19 +600,22 @@ mod test {
let a = Value::Number(1.0);
let b = Value::Number(2.0);

assert_eq!(a.abstract_lt(b, activation).unwrap(), Some(true));
assert_eq!(a.abstract_lt(b, activation).unwrap(), Value::Bool(true));

let nan = Value::Number(f64::NAN);
assert_eq!(a.abstract_lt(nan, activation).unwrap(), None);
assert_eq!(a.abstract_lt(nan, activation).unwrap(), Value::Undefined);

let inf = Value::Number(f64::INFINITY);
assert_eq!(a.abstract_lt(inf, activation).unwrap(), Some(true));
assert_eq!(a.abstract_lt(inf, activation).unwrap(), Value::Bool(true));

let neg_inf = Value::Number(f64::NEG_INFINITY);
assert_eq!(a.abstract_lt(neg_inf, activation).unwrap(), Some(false));
assert_eq!(
a.abstract_lt(neg_inf, activation).unwrap(),
Value::Bool(false)
);

let zero = Value::Number(0.0);
assert_eq!(a.abstract_lt(zero, activation).unwrap(), Some(false));
assert_eq!(a.abstract_lt(zero, activation).unwrap(), Value::Bool(false));

Ok(())
});
Expand All @@ -607,19 +627,22 @@ mod test {
let a = Value::Number(1.0);
let b = Value::Number(2.0);

assert_eq!(b.abstract_lt(a, activation).unwrap(), Some(false));
assert_eq!(b.abstract_lt(a, activation).unwrap(), Value::Bool(false));

let nan = Value::Number(f64::NAN);
assert_eq!(nan.abstract_lt(a, activation).unwrap(), None);
assert_eq!(nan.abstract_lt(a, activation).unwrap(), Value::Undefined);

let inf = Value::Number(f64::INFINITY);
assert_eq!(inf.abstract_lt(a, activation).unwrap(), Some(false));
assert_eq!(inf.abstract_lt(a, activation).unwrap(), Value::Bool(false));

let neg_inf = Value::Number(f64::NEG_INFINITY);
assert_eq!(neg_inf.abstract_lt(a, activation).unwrap(), Some(true));
assert_eq!(
neg_inf.abstract_lt(a, activation).unwrap(),
Value::Bool(true)
);

let zero = Value::Number(0.0);
assert_eq!(zero.abstract_lt(a, activation).unwrap(), Some(true));
assert_eq!(zero.abstract_lt(a, activation).unwrap(), Value::Bool(true));

Ok(())
});
Expand All @@ -631,7 +654,7 @@ mod test {
let a = Value::String(AvmString::new_utf8(activation.context.gc_context, "a"));
let b = Value::String(AvmString::new_utf8(activation.context.gc_context, "b"));

assert_eq!(a.abstract_lt(b, activation).unwrap(), Some(true));
assert_eq!(a.abstract_lt(b, activation).unwrap(), Value::Bool(true));

Ok(())
})
Expand All @@ -643,7 +666,7 @@ mod test {
let a = Value::String(AvmString::new_utf8(activation.context.gc_context, "a"));
let b = Value::String(AvmString::new_utf8(activation.context.gc_context, "b"));

assert_eq!(b.abstract_lt(a, activation).unwrap(), Some(false));
assert_eq!(b.abstract_lt(a, activation).unwrap(), Value::Bool(false));

Ok(())
})
Expand Down
2 changes: 1 addition & 1 deletion tests/tests/regression_tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -684,7 +684,7 @@ swf_tests! {
(stage_property_representation, "avm1/stage_property_representation", 1),
(strictequals_swf6, "avm1/strictequals_swf6", 1),
(strictly_equals, "avm1/strictly_equals", 1),
#[ignore] (string_coercion, "avm1/string_coercion", 1),
(string_coercion, "avm1/string_coercion", 1),
(string_methods_negative_args, "avm1/string_methods_negative_args", 1),
(string_methods, "avm1/string_methods", 1),
(string_ops_swf6, "avm1/string_ops_swf6", 1),
Expand Down
51 changes: 51 additions & 0 deletions tests/tests/swfs/avm1/lessthan2_swf5/output.txt
Original file line number Diff line number Diff line change
Expand Up @@ -1174,3 +1174,54 @@ true
// "A" < "A":
false

// {} < 0:
false

// {} < "0":
false

// {} < {}
false

// {valueOf} < 0
obj.valueOf returning 1
false

// 0 < {valueOf}
obj.valueOf returning 1
true

// {valueOf} < {}
obj.valueOf returning 1
false

// {} < {valueOf}
false

// {valueOf} < {valueOf}
obj.valueOf returning 1
obj.valueOf returning 1
false

// {} < {objNoValueOf}
false

// {objNoValueOf} < {}
obj.valueOf returning undefined
false

// {valueOf} < {objNoValueOf}
obj.valueOf returning 1
obj.valueOf returning undefined
false

// {objNoValueOf} < {valueOf}
obj.valueOf returning undefined
obj.valueOf returning 1
true

// {objNoValueOf} < {objNoValueOf}
obj.valueOf returning undefined
obj.valueOf returning undefined
false

Binary file modified tests/tests/swfs/avm1/lessthan2_swf5/test.fla
Binary file not shown.
Binary file modified tests/tests/swfs/avm1/lessthan2_swf5/test.swf
Binary file not shown.
51 changes: 51 additions & 0 deletions tests/tests/swfs/avm1/lessthan2_swf6/output.txt
Original file line number Diff line number Diff line change
Expand Up @@ -1174,3 +1174,54 @@ true
// "A" < "A":
false

// {} < 0:
false

// {} < "0":
false

// {} < {}
false

// {valueOf} < 0
obj.valueOf returning 1
false

// 0 < {valueOf}
obj.valueOf returning 1
true

// {valueOf} < {}
obj.valueOf returning 1
false

// {} < {valueOf}
false

// {valueOf} < {valueOf}
obj.valueOf returning 1
obj.valueOf returning 1
false

// {} < {objNoValueOf}
false

// {objNoValueOf} < {}
obj.valueOf returning undefined
false

// {valueOf} < {objNoValueOf}
obj.valueOf returning 1
obj.valueOf returning undefined
false

// {objNoValueOf} < {valueOf}
obj.valueOf returning undefined
obj.valueOf returning 1
true

// {objNoValueOf} < {objNoValueOf}
obj.valueOf returning undefined
obj.valueOf returning undefined
false

Binary file modified tests/tests/swfs/avm1/lessthan2_swf6/test.fla
Binary file not shown.
Binary file modified tests/tests/swfs/avm1/lessthan2_swf6/test.swf
Binary file not shown.
51 changes: 51 additions & 0 deletions tests/tests/swfs/avm1/lessthan2_swf7/output.txt
Original file line number Diff line number Diff line change
Expand Up @@ -1174,3 +1174,54 @@ true
// "A" < "A":
false

// {} < 0:
false

// {} < "0":
false

// {} < {}
false

// {valueOf} < 0
obj.valueOf returning 1
false

// 0 < {valueOf}
obj.valueOf returning 1
true

// {valueOf} < {}
obj.valueOf returning 1
false

// {} < {valueOf}
false

// {valueOf} < {valueOf}
obj.valueOf returning 1
obj.valueOf returning 1
false

// {} < {objNoValueOf}
false

// {objNoValueOf} < {}
obj.valueOf returning undefined
false

// {valueOf} < {objNoValueOf}
obj.valueOf returning 1
obj.valueOf returning undefined
undefined

// {objNoValueOf} < {valueOf}
obj.valueOf returning undefined
obj.valueOf returning 1
undefined

// {objNoValueOf} < {objNoValueOf}
obj.valueOf returning undefined
obj.valueOf returning undefined
undefined

Binary file modified tests/tests/swfs/avm1/lessthan2_swf7/test.fla
Binary file not shown.
Binary file modified tests/tests/swfs/avm1/lessthan2_swf7/test.swf
Binary file not shown.
8 changes: 8 additions & 0 deletions tests/tests/swfs/avm1/string_coercion/output.txt
Original file line number Diff line number Diff line change
Expand Up @@ -107,3 +107,11 @@ sX
NaN
NaN
NaN
Function toString tests
[type Function]
[type Function]
[type Function]
Function string tests, toString deleted
[type Function]
undefined
[type Function]
Binary file modified tests/tests/swfs/avm1/string_coercion/test.fla
Binary file not shown.
Binary file modified tests/tests/swfs/avm1/string_coercion/test.swf
Binary file not shown.