211 lines
8.1 KiB
Rust
211 lines
8.1 KiB
Rust
//! Property data structures
|
|
|
|
use crate::avm2::object::TObject;
|
|
use crate::avm2::Activation;
|
|
use crate::avm2::ClassObject;
|
|
use crate::avm2::Error;
|
|
use crate::avm2::Multiname;
|
|
use crate::avm2::TranslationUnit;
|
|
use crate::avm2::Value;
|
|
use gc_arena::MutationContext;
|
|
use gc_arena::{Collect, Gc};
|
|
|
|
#[derive(Debug, Collect, Clone, Copy)]
|
|
#[collect(no_drop)]
|
|
pub enum Property {
|
|
Virtual { get: Option<u32>, set: Option<u32> },
|
|
Method { disp_id: u32 },
|
|
Slot { slot_id: u32 },
|
|
ConstSlot { slot_id: u32 },
|
|
}
|
|
|
|
/// The type of a `Slot`/`ConstSlot` property, represented
|
|
/// as a lazily-resolved class. This also implements the
|
|
/// property-specific coercion logic applied when setting
|
|
/// or initializing a property.
|
|
///
|
|
/// The class resolution needs to be lazy, since we can have
|
|
/// a cycle of property type references between classes
|
|
/// (e.g. Class1 has `var prop1:Class2`, and Class2
|
|
/// has `var prop2:Class1`).
|
|
///
|
|
/// Additionally, property class resolution uses special
|
|
/// logic, different from normal "runtime" class resolution,
|
|
/// that allows private types to be referenced.
|
|
#[derive(Collect, Clone)]
|
|
#[collect(no_drop)]
|
|
pub enum PropertyClass<'gc> {
|
|
/// The type `*` (Multiname::is_any()). This allows
|
|
/// `Value::Undefined`, so it needs to be distinguished
|
|
/// from the `Object` class
|
|
Any,
|
|
Class(ClassObject<'gc>),
|
|
Name(Gc<'gc, (Multiname<'gc>, Option<TranslationUnit<'gc>>)>),
|
|
}
|
|
|
|
impl<'gc> PropertyClass<'gc> {
|
|
pub fn name(
|
|
mc: MutationContext<'gc, '_>,
|
|
name: Multiname<'gc>,
|
|
unit: Option<TranslationUnit<'gc>>,
|
|
) -> Self {
|
|
PropertyClass::Name(Gc::allocate(mc, (name, unit)))
|
|
}
|
|
|
|
/// Returns `value` coerced to the type of this `PropertyClass`.
|
|
/// The bool is `true` if this `PropertyClass` was just modified
|
|
/// to cache a class resolution, and `false` if it was not modified.
|
|
pub fn coerce(
|
|
&mut self,
|
|
activation: &mut Activation<'_, 'gc>,
|
|
value: Value<'gc>,
|
|
) -> Result<(Value<'gc>, bool), Error<'gc>> {
|
|
let (class, changed) = match self {
|
|
PropertyClass::Class(class) => (Some(*class), false),
|
|
PropertyClass::Name(gc) => {
|
|
let (name, unit) = &**gc;
|
|
let outcome = resolve_class_private(name, *unit, activation)?;
|
|
let class = match outcome {
|
|
ResolveOutcome::Class(class) => {
|
|
*self = PropertyClass::Class(class);
|
|
(Some(class), true)
|
|
}
|
|
ResolveOutcome::Any => {
|
|
*self = PropertyClass::Any;
|
|
(None, true)
|
|
}
|
|
ResolveOutcome::NotFound => {
|
|
// FP allows a class to reference its own type in a static initializer:
|
|
// `class Foo { static var INSTANCE: Foo = new Foo(); }`
|
|
// When this happens, the `ClassObject` for `Foo` will not yet
|
|
// be available when we perform the coercion, since we're still
|
|
// in the process of constructing it.
|
|
//
|
|
// Fortunately, a coercion to a non-primitive class either
|
|
// succeeds with the value unchanged, or fails (if the object
|
|
// is not an instance of the class). Therefore, we can just check
|
|
// if the class name and domain match our property name, without
|
|
// actually needing to perform resolution. This does not handle subclasses,
|
|
// but that's fine - a superclass cannot reference a subclass from a class
|
|
// initializer.
|
|
//
|
|
// We should eventually be able to remove this when we refactor
|
|
// `Class`/`ClassObject` to be closer to what avmplus does.
|
|
if let Some(object) = value.as_object() {
|
|
if let Some(class) = object.instance_of() {
|
|
if name.contains_name(&class.inner_class_definition().read().name())
|
|
&& unit.map(|u| u.domain())
|
|
== Some(class.class_scope().domain())
|
|
{
|
|
// Even though resolution succeeded, we haven't modified
|
|
// this `PropertyClass`, so return `false`
|
|
return Ok((value, false));
|
|
}
|
|
}
|
|
} else if matches!(value, Value::Null) || matches!(value, Value::Undefined)
|
|
{
|
|
//AVM2 properties are nullable, so null is always an instance of the class
|
|
return Ok((Value::Null, false));
|
|
}
|
|
|
|
return Err(Error::from(format!(
|
|
"Attempted to perform (private) resolution of nonexistent type {name:?}",
|
|
)));
|
|
}
|
|
};
|
|
class
|
|
}
|
|
PropertyClass::Any => (None, false),
|
|
};
|
|
|
|
if let Some(class) = class {
|
|
Ok((value.coerce_to_type(activation, class)?, changed))
|
|
} else {
|
|
// We have a type of '*' ("any"), so don't
|
|
// perform any coercions
|
|
Ok((value, changed))
|
|
}
|
|
}
|
|
|
|
pub fn get_name(&self, mc: MutationContext<'gc, '_>) -> Multiname<'gc> {
|
|
match self {
|
|
PropertyClass::Class(class) => class.inner_class_definition().read().name().into(),
|
|
PropertyClass::Name(gc) => gc.0.clone(),
|
|
PropertyClass::Any => Multiname::any(mc),
|
|
}
|
|
}
|
|
}
|
|
|
|
enum ResolveOutcome<'gc> {
|
|
Class(ClassObject<'gc>),
|
|
Any,
|
|
NotFound,
|
|
}
|
|
|
|
/// Resolves a class definition referenced by the type of a property.
|
|
/// This supports private (`Namespace::Private`) classes,
|
|
/// and does not use the `ScopeStack`/`ScopeChain`.
|
|
///
|
|
/// This is an internal operation used to resolve property type names.
|
|
/// It does not correspond to any opcode or native method.
|
|
fn resolve_class_private<'gc>(
|
|
name: &Multiname<'gc>,
|
|
unit: Option<TranslationUnit<'gc>>,
|
|
activation: &mut Activation<'_, 'gc>,
|
|
) -> Result<ResolveOutcome<'gc>, Error<'gc>> {
|
|
// A Property may have a type of '*' (which corresponds to 'Multiname::any()')
|
|
// We don't want to perform any coercions in this case - in particular,
|
|
// this means that the property can have a value of `Undefined`.
|
|
// If the type is `Object`, then a value of `Undefind` gets coerced
|
|
// to `Null`
|
|
if name.is_any_name() {
|
|
Ok(ResolveOutcome::Any)
|
|
} else {
|
|
// First, check the domain for an exported (non-private) class.
|
|
// If the property we're resolving for lacks a `TranslationUnit`,
|
|
// use the stage domain
|
|
let domain = unit.map_or(activation.avm2().stage_domain, |u| u.domain());
|
|
let globals = if let Some((_, mut script)) = domain.get_defining_script(name)? {
|
|
script.globals(&mut activation.context)?
|
|
} else if unit.is_some() {
|
|
return Err(format!("Could not find script for class trait {name:?}").into());
|
|
} else {
|
|
return Err(format!("Missing script and translation unit for class {name:?}").into());
|
|
};
|
|
|
|
Ok(globals
|
|
.get_property(name, activation)?
|
|
.as_object()
|
|
.and_then(|o| o.as_class_object())
|
|
.map_or(ResolveOutcome::NotFound, ResolveOutcome::Class))
|
|
}
|
|
}
|
|
|
|
impl Property {
|
|
pub fn new_method(disp_id: u32) -> Self {
|
|
Property::Method { disp_id }
|
|
}
|
|
|
|
pub fn new_getter(disp_id: u32) -> Self {
|
|
Property::Virtual {
|
|
get: Some(disp_id),
|
|
set: None,
|
|
}
|
|
}
|
|
|
|
pub fn new_setter(disp_id: u32) -> Self {
|
|
Property::Virtual {
|
|
get: None,
|
|
set: Some(disp_id),
|
|
}
|
|
}
|
|
|
|
pub fn new_slot(slot_id: u32) -> Self {
|
|
Property::Slot { slot_id }
|
|
}
|
|
|
|
pub fn new_const_slot(slot_id: u32) -> Self {
|
|
Property::ConstSlot { slot_id }
|
|
}
|
|
}
|