Implement numerical coercions according to ECMA-262 3rd Edition spec.

This commit is contained in:
David Wendt 2020-06-23 23:41:08 -04:00 committed by Mike Welsh
parent 24fd30652d
commit 736a94a244
3 changed files with 216 additions and 1 deletions

View File

@ -475,6 +475,7 @@ impl<'a, 'gc, 'gc_context> Activation<'a, 'gc, 'gc_context> {
Op::NewClass { index } => self.op_new_class(method, index),
Op::CoerceA => self.op_coerce_a(),
Op::ConvertB => self.op_convert_b(),
Op::ConvertD => self.op_convert_d(context),
Op::Jump { offset } => self.op_jump(offset, reader),
Op::IfTrue { offset } => self.op_if_true(offset, reader),
Op::IfFalse { offset } => self.op_if_false(offset, reader),
@ -1316,6 +1317,17 @@ impl<'a, 'gc, 'gc_context> Activation<'a, 'gc, 'gc_context> {
Ok(FrameControl::Continue)
}
fn op_convert_d(
&mut self,
context: &mut UpdateContext<'_, 'gc, '_>,
) -> Result<FrameControl<'gc>, Error> {
let value = self.avm2.pop().coerce_to_number(self, context)?;
self.avm2.push(Value::Number(value));
Ok(FrameControl::Continue)
}
fn op_jump(
&mut self,
offset: i32,

View File

@ -80,6 +80,22 @@ impl<'gc> Namespace<'gc> {
pub fn is_private(&self) -> bool {
matches!(self, Self::Private(_))
}
/// Get the string value of this namespace, ignoring it's type.
///
/// TODO: Is this *actually* the namespace URI?
pub fn as_uri(&self) -> &str {
match self {
Self::Namespace(s) => s,
Self::Package(s) => s,
Self::PackageInternal(s) => s,
Self::Protected(s) => s,
Self::Explicit(s) => s,
Self::StaticProtected(s) => s,
Self::Private(s) => s,
Self::Any => "",
}
}
}
/// A `QName`, likely "qualified name", consists of a namespace and name string.

View File

@ -1,14 +1,30 @@
//! AVM2 values
use crate::avm2::activation::Activation;
use crate::avm2::names::Namespace;
use crate::avm2::object::Object;
use crate::avm2::names::QName;
use crate::avm2::object::{Object, TObject};
use crate::avm2::script::TranslationUnit;
use crate::avm2::string::AvmString;
use crate::avm2::Error;
use crate::context::UpdateContext;
use gc_arena::{Collect, MutationContext};
use std::f64::NAN;
use swf::avm2::types::{DefaultValue as AbcDefaultValue, Index};
/// Indicate what kind of primitive coercion would be preferred when coercing
/// objects.
#[derive(Eq, PartialEq)]
pub enum Hint {
/// Prefer string coercion (e.g. call `toString` preferentially over
/// `valueOf`)
String,
/// Prefer numerical coercion (e.g. call `valueOf` preferentially over
/// `toString`)
Number,
}
/// An AVM2 value.
///
/// TODO: AVM2 also needs Scope, Namespace, and XML values.
@ -235,6 +251,18 @@ impl<'gc> Value<'gc> {
}
}
/// Yields `true` if the given value is a primitive value.
///
/// Note: Boxed primitive values are not considered primitive - it is
/// expected that their `toString`/`valueOf` handlers have already had a
/// chance to unbox the primitive contained within.
pub fn is_primitive(&self) -> bool {
match self {
Value::Object(_) | Value::Namespace(_) => false,
_ => true,
}
}
/// Coerce the value to a boolean.
///
/// Boolean coercion happens according to the rules specified in the ES4
@ -249,4 +277,163 @@ impl<'gc> Value<'gc> {
Value::Object(_) => true,
}
}
/// Coerce the value to a primitive.
///
/// This function is guaranteed to return either a primitive value, or a
/// `TypeError`.
///
/// The `Hint` parameter selects if the coercion prefers `toString` or
/// `valueOf`. If the preferred function is not available, it's opposite
/// will be called. If neither function successfully generates a primitive,
/// a `TypeError` will be raised.
///
/// Primitive conversions occur according to ECMA-262 3rd Edition's
/// ToPrimitive algorithm which appears to match AVM2.
pub fn coerce_to_primitive(
&self,
hint: Hint,
activation: &mut Activation<'_, 'gc>,
context: &mut UpdateContext<'_, 'gc, '_>,
) -> Result<Value<'gc>, Error> {
match self {
Value::Object(o) if hint == Hint::String => {
let mut prim = self.clone();
let mut object = *o;
if let Value::Object(f) = object.get_property(
*o,
&QName::dynamic_name("toString"),
activation,
context,
)? {
prim = f.call(Some(*o), &[], activation, context, None)?;
}
if prim.is_primitive() {
return Ok(prim);
}
if let Value::Object(f) =
object.get_property(*o, &QName::dynamic_name("valueOf"), activation, context)?
{
prim = f.call(Some(*o), &[], activation, context, None)?;
}
if prim.is_primitive() {
return Ok(prim);
}
Err("TypeError: cannot convert object to string".into())
}
Value::Object(o) if hint == Hint::Number => {
let mut prim = self.clone();
let mut object = *o;
if let Value::Object(f) =
object.get_property(*o, &QName::dynamic_name("valueOf"), activation, context)?
{
prim = f.call(Some(*o), &[], activation, context, None)?;
}
if prim.is_primitive() {
return Ok(prim);
}
if let Value::Object(f) = object.get_property(
*o,
&QName::dynamic_name("toString"),
activation,
context,
)? {
prim = f.call(Some(*o), &[], activation, context, None)?;
}
if prim.is_primitive() {
return Ok(prim);
}
Err("TypeError: cannot convert object to number".into())
}
_ => Ok(self.clone()),
}
}
/// Coerce the value to a floating-point number.
///
/// This function returns the resulting floating-point directly; or a
/// TypeError if the value is an `Object` that cannot be converted to a
/// primitive value.
///
/// Numerical conversions occur according to ECMA-262 3rd Edition's
/// ToNumber algorithm which appears to match AVM2.
pub fn coerce_to_number(
&self,
activation: &mut Activation<'_, 'gc>,
context: &mut UpdateContext<'_, 'gc, '_>,
) -> Result<f64, Error> {
Ok(match self {
Value::Undefined => f64::NAN,
Value::Null => 0.0,
Value::Bool(true) => 1.0,
Value::Bool(false) => 0.0,
Value::Number(n) => *n,
Value::String(s) => {
let strim = s.trim();
if strim.is_empty() {
0.0
} else if strim.starts_with("0x") || strim.starts_with("0X") {
let mut n: f64 = 0.0;
for c in strim[2..].chars() {
n *= 16.0;
n += match c {
'0' => 0.0,
'1' => 1.0,
'2' => 2.0,
'3' => 3.0,
'4' => 4.0,
'5' => 5.0,
'6' => 6.0,
'7' => 7.0,
'8' => 8.0,
'9' => 9.0,
'a' | 'A' => 10.0,
'b' | 'B' => 11.0,
'c' | 'C' => 12.0,
'd' | 'D' => 13.0,
'e' | 'E' => 14.0,
'f' | 'F' => 15.0,
_ => return Ok(f64::NAN),
};
}
n
} else {
let (sign, digits) = if strim.starts_with('+') {
(1.0, &strim[1..])
} else if strim.starts_with('-') {
(-1.0, &strim[1..])
} else {
(1.0, strim)
};
if digits == "Infinity" {
return Ok(sign * f64::INFINITY);
}
//TODO: This is slightly more permissive than ES3 spec, as
//Rust documentation claims it will accept "inf" as f64
//infinity.
sign * digits.parse().unwrap_or(f64::NAN)
}
}
Value::Namespace(ns) => {
Value::String(AvmString::new(context.gc_context, ns.as_uri().to_string()))
.coerce_to_number(activation, context)?
}
Value::Object(_) => self
.coerce_to_primitive(Hint::Number, activation, context)?
.coerce_to_number(activation, context)?,
})
}
}