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

Conversation

Herschel
Copy link
Member

@Herschel Herschel commented Mar 27, 2022

{} < {} should return false, but was incorrectly returning undefined in Ruffle. Stumbled on this while trying to add a test for #6544.

If the parameters to the LessThan2 action are a non-movieclip object, Object.valueOf is called. If the result of valueOf is a non-movieclip Object, the comparison immediately returns false. This is the usual case because Object.prototype.valueOf returns this object itself.

In the other case where valueOf returns a primitive value or a display object, the comparison continues as normal, coercing the value to number, where any NaN results in the comparison returning undefined.

  • Change Value::to_primitive_num to not call valueOf on display objects.
  • Change abstract_lt to return false immediately if the result of to_primitive_num is an object (excluding display objects).
  • Unignore string_coercion test as this now works properly.
  • Add various checks for object < object to the lessthan2 tests.
  • Adds a test for avm1: String(function) returns [type Function] #6544

The Less2 op returns false if the `valueOf` of either parameter
results in an object. The exception is display objects:
`mc < mc` returns undefined.
All callers converted the result to `Value` anyway.
@Herschel Herschel requested a review from relrelb March 27, 2022 23:04
@Toad06
Copy link
Member

Toad06 commented Mar 28, 2022

Tested the PR and it looks good to me.

Though this is not really in the scope of this PR, I've found another issue with valueOf when used with the Add2 action:

var o = new Object();
o.valueOf = function()
{
   return "ABC";
};
trace(123 + o); // NaN (expected: "123ABC")

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.

@@ -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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants