avm2: Make numbers appear to be of any numeric type that can represent them.

This also prevents an exception from being fired when testing `undefined` or `null`, which are valid inputs to `istype`.
This commit is contained in:
David Wendt 2021-06-11 22:30:18 -04:00
parent 974fe152ff
commit e38b1bc281
8 changed files with 517 additions and 5 deletions

View File

@ -2381,7 +2381,7 @@ impl<'a, 'gc, 'gc_context> Activation<'a, 'gc, 'gc_context> {
method: Gc<'gc, BytecodeMethod<'gc>>, method: Gc<'gc, BytecodeMethod<'gc>>,
type_name_index: Index<AbcMultiname>, type_name_index: Index<AbcMultiname>,
) -> Result<FrameControl<'gc>, Error> { ) -> Result<FrameControl<'gc>, Error> {
let value = self.context.avm2.pop().coerce_to_object(self)?; let value = self.context.avm2.pop();
let multiname = self.pool_multiname_static(method, type_name_index)?; let multiname = self.pool_multiname_static(method, type_name_index)?;
let found: Result<Value<'gc>, Error> = if let Some(scope) = self.scope() { let found: Result<Value<'gc>, Error> = if let Some(scope) = self.scope() {
@ -2400,7 +2400,7 @@ impl<'a, 'gc, 'gc_context> Activation<'a, 'gc, 'gc_context> {
}); });
let type_object = found?.coerce_to_object(self)?; let type_object = found?.coerce_to_object(self)?;
let is_instance_of = value.is_of_type(type_object)?; let is_instance_of = value.is_of_type(self, type_object)?;
self.context.avm2.push(is_instance_of); self.context.avm2.push(is_instance_of);
Ok(FrameControl::Continue) Ok(FrameControl::Continue)
@ -2408,9 +2408,9 @@ impl<'a, 'gc, 'gc_context> Activation<'a, 'gc, 'gc_context> {
fn op_is_type_late(&mut self) -> Result<FrameControl<'gc>, Error> { fn op_is_type_late(&mut self) -> Result<FrameControl<'gc>, Error> {
let type_object = self.context.avm2.pop().coerce_to_object(self)?; let type_object = self.context.avm2.pop().coerce_to_object(self)?;
let value = self.context.avm2.pop().coerce_to_object(self)?; let value = self.context.avm2.pop();
let is_instance_of = value.is_of_type(type_object)?; let is_instance_of = value.is_of_type(self, type_object)?;
self.context.avm2.push(is_instance_of); self.context.avm2.push(is_instance_of);

View File

@ -1059,6 +1059,15 @@ pub trait TObject<'gc>: 'gc + Collect + Debug + Into<Object<'gc>> + Clone + Copy
None None
} }
/// Unwrap this object as an immutable primitive value.
///
/// This function should not be called in cases where a normal `Value`
/// coercion would do. It *only* accounts for boxed primitives, and not
/// `valueOf`.
fn as_primitive(&self) -> Option<Ref<Value<'gc>>> {
None
}
/// Unwrap this object as a mutable primitive value. /// Unwrap this object as a mutable primitive value.
fn as_primitive_mut(&self, _mc: MutationContext<'gc, '_>) -> Option<RefMut<Value<'gc>>> { fn as_primitive_mut(&self, _mc: MutationContext<'gc, '_>) -> Option<RefMut<Value<'gc>>> {
None None

View File

@ -1,6 +1,6 @@
//! Boxed primitives //! Boxed primitives
use std::cell::RefMut; use std::cell::{Ref, RefMut};
use crate::avm2::activation::Activation; use crate::avm2::activation::Activation;
use crate::avm2::class::Class; use crate::avm2::class::Class;
@ -141,4 +141,8 @@ impl<'gc> TObject<'gc> for PrimitiveObject<'gc> {
fn as_primitive_mut(&self, mc: MutationContext<'gc, '_>) -> Option<RefMut<Value<'gc>>> { fn as_primitive_mut(&self, mc: MutationContext<'gc, '_>) -> Option<RefMut<Value<'gc>>> {
Some(RefMut::map(self.0.write(mc), |pod| &mut pod.primitive)) Some(RefMut::map(self.0.write(mc), |pod| &mut pod.primitive))
} }
fn as_primitive(&self) -> Option<Ref<Value<'gc>>> {
Some(Ref::map(self.0.read(), |pod| &pod.primitive))
}
} }

View File

@ -631,6 +631,75 @@ impl<'gc> Value<'gc> {
Ok(self.clone()) Ok(self.clone())
} }
/// Determine if this value is any kind of number.
pub fn is_number(&self) -> bool {
match self {
Value::Number(_) => true,
Value::Integer(_) => true,
Value::Unsigned(_) => true,
Value::Object(o) => o.as_primitive().map(|p| p.is_number()).unwrap_or(false),
_ => false,
}
}
/// Determine if this value is a number representable as a u32 without loss
/// of precision.
#[allow(clippy::float_cmp)]
pub fn is_u32(&self) -> bool {
match self {
Value::Number(n) => *n == n.round() && n.is_sign_positive() && *n <= u32::MAX as f64,
Value::Integer(i) => i.is_positive() || *i == 0,
Value::Unsigned(_) => true,
Value::Object(o) => o.as_primitive().map(|p| p.is_u32()).unwrap_or(false),
_ => false,
}
}
/// Determine if this value is a number representable as an i32 without
/// loss of precision.
#[allow(clippy::float_cmp)]
pub fn is_i32(&self) -> bool {
match self {
Value::Number(n) => *n == n.round() && *n >= i32::MIN as f64 && *n <= i32::MAX as f64,
Value::Integer(_) => true,
Value::Unsigned(u) => *u <= i32::MAX as u32,
Value::Object(o) => o.as_primitive().map(|p| p.is_u32()).unwrap_or(false),
_ => false,
}
}
/// Determine if this value is of a given type.
///
/// This implements a particularly unusual rule: primitive numeric values
/// considered instances of all numeric types that can represent them. For
/// example, 5 is simultaneously an instance of `int`, `uint`, and
/// `Number`.
pub fn is_of_type(
&self,
activation: &mut Activation<'_, 'gc, '_>,
type_object: Object<'gc>,
) -> Result<bool, Error> {
if let Some(type_class) = type_object.as_class() {
if type_class.read().name() == &QName::new(Namespace::public(), "Number") {
return Ok(self.is_number());
}
if type_class.read().name() == &QName::new(Namespace::public(), "uint") {
return Ok(self.is_u32());
}
if type_class.read().name() == &QName::new(Namespace::public(), "int") {
return Ok(self.is_i32());
}
}
if let Ok(o) = self.coerce_to_object(activation) {
o.is_of_type(type_object)
} else {
Ok(false)
}
}
/// Determine if two values are abstractly equal to each other. /// Determine if two values are abstractly equal to each other.
/// ///
/// This abstract equality algorithm is intended to match ECMA-262 3rd /// This abstract equality algorithm is intended to match ECMA-262 3rd

View File

@ -0,0 +1,232 @@
package {
public class Test {
}
}
class CoercibleAsIntString {
public function toString() {
return "-99.13";
}
}
class CoercibleAsNonIntString {
public function toString() {
return "TEST FAIL";
}
}
class CoercibleAsValue {
public function valueOf() {
return 23.16;
}
}
class NotCoercibleAsValue {
public function valueOf() {
return "TEST FAIL";
}
}
trace("// == int tests == ");
trace("//undefined is int");
trace(undefined is int);
trace("//null is int");
trace(null is int);
trace("//true is int");
trace(true is int);
trace("//false is int");
trace(false is int);
trace("//0 is int");
trace(0 is int);
trace("//1 is int");
trace(1 is int);
trace("//5.12 is int");
trace(5.12 is int);
trace("//(5.12 - .12) is int");
trace((5.12 - .12) is int);
trace("//-6 is int");
trace(-6 is int);
trace("//\"12.23\" is int");
trace("12.23" is int);
trace("//\"true\" is int");
trace("true" is int);
trace("//\"false\" is int");
trace("false" is int);
trace("//new CoercibleAsIntString() is int");
trace(new CoercibleAsIntString() is int);
trace("//new CoercibleAsNonIntString() is int");
trace(new CoercibleAsNonIntString() is int);
trace("//new CoercibleAsValue() is int");
trace(new CoercibleAsValue() is int);
trace("//new NotCoercibleAsValue() is int");
trace(new NotCoercibleAsValue() is int);
trace("// == uint tests == ");
trace("//undefined is uint");
trace(undefined is uint);
trace("//null is uint");
trace(null is uint);
trace("//true is uint");
trace(true is uint);
trace("//false is uint");
trace(false is uint);
trace("//0 is uint");
trace(0 is uint);
trace("//1 is uint");
trace(1 is uint);
trace("//5.12 is uint");
trace(5.12 is uint);
trace("//(5.12 - .12) is uint");
trace((5.12 - .12) is uint);
trace("//-6 is uint");
trace(-6 is uint);
trace("//\"12.23\" is uint");
trace("12.23" is uint);
trace("//\"true\" is uint");
trace("true" is uint);
trace("//\"false\" is uint");
trace("false" is uint);
trace("//new CoercibleAsIntString() is uint");
trace(new CoercibleAsIntString() is uint);
trace("//new CoercibleAsNonIntString() is uint");
trace(new CoercibleAsNonIntString() is uint);
trace("//new CoercibleAsValue() is uint");
trace(new CoercibleAsValue() is uint);
trace("//new NotCoercibleAsValue() is uint");
trace(new NotCoercibleAsValue() is uint);
trace("// == Number tests == ");
trace("//undefined is Number");
trace(undefined is Number);
trace("//null is Number");
trace(null is Number);
trace("//true is Number");
trace(true is Number);
trace("//false is Number");
trace(false is Number);
trace("//0 is Number");
trace(0 is Number);
trace("//1 is Number");
trace(1 is Number);
trace("//5.12 is Number");
trace(5.12 is Number);
trace("//(5.12 - .12) is Number");
trace((5.12 - .12) is Number);
trace("//-6 is Number");
trace(-6 is Number);
trace("//\"12.23\" is Number");
trace("12.23" is Number);
trace("//\"true\" is Number");
trace("true" is Number);
trace("//\"false\" is Number");
trace("false" is Number);
trace("//new CoercibleAsIntString() is Number");
trace(new CoercibleAsIntString() is Number);
trace("//new CoercibleAsNonIntString() is Number");
trace(new CoercibleAsNonIntString() is Number);
trace("//new CoercibleAsValue() is Number");
trace(new CoercibleAsValue() is Number);
trace("//new NotCoercibleAsValue() is Number");
trace(new NotCoercibleAsValue() is Number);
trace("// == Boolean tests == ");
trace("//undefined is Boolean");
trace(undefined is Boolean);
trace("//null is Boolean");
trace(null is Boolean);
trace("//true is Boolean");
trace(true is Boolean);
trace("//false is Boolean");
trace(false is Boolean);
trace("//0 is Boolean");
trace(0 is Boolean);
trace("//1 is Boolean");
trace(1 is Boolean);
trace("//5.12 is Boolean");
trace(5.12 is Boolean);
trace("//(5.12 - .12) is Boolean");
trace((5.12 - .12) is Boolean);
trace("//-6 is Boolean");
trace(-6 is Boolean);
trace("//\"12.23\" is Boolean");
trace("12.23" is Boolean);
trace("//\"true\" is Boolean");
trace("true" is Boolean);
trace("//\"false\" is Boolean");
trace("false" is Boolean);
trace("//new CoercibleAsIntString() is Boolean");
trace(new CoercibleAsIntString() is Boolean);
trace("//new CoercibleAsNonIntString() is Boolean");
trace(new CoercibleAsNonIntString() is Boolean);
trace("//new CoercibleAsValue() is Boolean");
trace(new CoercibleAsValue() is Boolean);
trace("//new NotCoercibleAsValue() is Boolean");
trace(new NotCoercibleAsValue() is Boolean);
trace("// == String tests == ");
trace("//undefined is String");
trace(undefined is String);
trace("//null is String");
trace(null is String);
trace("//true is String");
trace(true is String);
trace("//false is String");
trace(false is String);
trace("//0 is String");
trace(0 is String);
trace("//1 is String");
trace(1 is String);
trace("//5.12 is String");
trace(5.12 is String);
trace("//(5.12 - .12) is String");
trace((5.12 - .12) is String);
trace("//-6 is String");
trace(-6 is String);
trace("//\"12.23\" is String");
trace("12.23" is String);
trace("//\"true\" is String");
trace("true" is String);
trace("//\"false\" is String");
trace("false" is String);
trace("//new CoercibleAsIntString() is String");
trace(new CoercibleAsIntString() is String);
trace("//new CoercibleAsNonIntString() is String");
trace(new CoercibleAsNonIntString() is String);
trace("//new CoercibleAsValue() is String");
trace(new CoercibleAsValue() is String);
trace("//new NotCoercibleAsValue() is String");
trace(new NotCoercibleAsValue() is String);
trace("// == Object tests == ");
trace("//undefined is Object");
trace(undefined is Object);
trace("//null is Object");
trace(null is Object);
trace("//true is Object");
trace(true is Object);
trace("//false is Object");
trace(false is Object);
trace("//0 is Object");
trace(0 is Object);
trace("//1 is Object");
trace(1 is Object);
trace("//5.12 is Object");
trace(5.12 is Object);
trace("//(5.12 - .12) is Object");
trace((5.12 - .12) is Object);
trace("//-6 is Object");
trace(-6 is Object);
trace("//\"12.23\" is Object");
trace("12.23" is Object);
trace("//\"true\" is Object");
trace("true" is Object);
trace("//\"false\" is Object");
trace("false" is Object);
trace("//new CoercibleAsIntString() is Object");
trace(new CoercibleAsIntString() is Object);
trace("//new CoercibleAsNonIntString() is Object");
trace(new CoercibleAsNonIntString() is Object);
trace("//new CoercibleAsValue() is Object");
trace(new CoercibleAsValue() is Object);
trace("//new NotCoercibleAsValue() is Object");
trace(new NotCoercibleAsValue() is Object);

View File

@ -0,0 +1,198 @@
// == int tests ==
//undefined is int
false
//null is int
false
//true is int
false
//false is int
false
//0 is int
true
//1 is int
true
//5.12 is int
false
//(5.12 - .12) is int
true
//-6 is int
true
//"12.23" is int
false
//"true" is int
false
//"false" is int
false
//new CoercibleAsIntString() is int
false
//new CoercibleAsNonIntString() is int
false
//new CoercibleAsValue() is int
false
//new NotCoercibleAsValue() is int
false
// == uint tests ==
//undefined is uint
false
//null is uint
false
//true is uint
false
//false is uint
false
//0 is uint
true
//1 is uint
true
//5.12 is uint
false
//(5.12 - .12) is uint
true
//-6 is uint
false
//"12.23" is uint
false
//"true" is uint
false
//"false" is uint
false
//new CoercibleAsIntString() is uint
false
//new CoercibleAsNonIntString() is uint
false
//new CoercibleAsValue() is uint
false
//new NotCoercibleAsValue() is uint
false
// == Number tests ==
//undefined is Number
false
//null is Number
false
//true is Number
false
//false is Number
false
//0 is Number
true
//1 is Number
true
//5.12 is Number
true
//(5.12 - .12) is Number
true
//-6 is Number
true
//"12.23" is Number
false
//"true" is Number
false
//"false" is Number
false
//new CoercibleAsIntString() is Number
false
//new CoercibleAsNonIntString() is Number
false
//new CoercibleAsValue() is Number
false
//new NotCoercibleAsValue() is Number
false
// == Boolean tests ==
//undefined is Boolean
false
//null is Boolean
false
//true is Boolean
true
//false is Boolean
true
//0 is Boolean
false
//1 is Boolean
false
//5.12 is Boolean
false
//(5.12 - .12) is Boolean
false
//-6 is Boolean
false
//"12.23" is Boolean
false
//"true" is Boolean
false
//"false" is Boolean
false
//new CoercibleAsIntString() is Boolean
false
//new CoercibleAsNonIntString() is Boolean
false
//new CoercibleAsValue() is Boolean
false
//new NotCoercibleAsValue() is Boolean
false
// == String tests ==
//undefined is String
false
//null is String
false
//true is String
false
//false is String
false
//0 is String
false
//1 is String
false
//5.12 is String
false
//(5.12 - .12) is String
false
//-6 is String
false
//"12.23" is String
true
//"true" is String
true
//"false" is String
true
//new CoercibleAsIntString() is String
false
//new CoercibleAsNonIntString() is String
false
//new CoercibleAsValue() is String
false
//new NotCoercibleAsValue() is String
false
// == Object tests ==
//undefined is Object
false
//null is Object
false
//true is Object
true
//false is Object
true
//0 is Object
true
//1 is Object
true
//5.12 is Object
true
//(5.12 - .12) is Object
true
//-6 is Object
true
//"12.23" is Object
true
//"true" is Object
true
//"false" is Object
true
//new CoercibleAsIntString() is Object
true
//new CoercibleAsNonIntString() is Object
true
//new CoercibleAsValue() is Object
true
//new NotCoercibleAsValue() is Object
true

Binary file not shown.

Binary file not shown.