ruffle/core/src/avm2/property.rs

267 lines
8.3 KiB
Rust
Raw Normal View History

2020-02-15 01:30:19 +00:00
//! Property data structures
use self::Attribute::*;
use crate::avm2::function::Executable;
2020-02-19 19:17:33 +00:00
use crate::avm2::object::{Object, TObject};
2020-02-15 01:30:19 +00:00
use crate::avm2::return_value::ReturnValue;
use crate::avm2::value::Value;
use crate::avm2::{Avm2, Error};
use crate::context::UpdateContext;
use enumset::{EnumSet, EnumSetType};
use gc_arena::{Collect, CollectionContext};
/// Attributes of properties in the AVM runtime.
///
/// TODO: Replace with AVM2 properties for traits
#[derive(EnumSetType, Debug)]
pub enum Attribute {
DontDelete,
ReadOnly,
}
#[allow(clippy::large_enum_variant)]
#[derive(Clone, Debug)]
pub enum Property<'gc> {
Virtual {
get: Option<Executable<'gc>>,
2020-02-15 01:30:19 +00:00
set: Option<Executable<'gc>>,
attributes: EnumSet<Attribute>,
},
Stored {
value: Value<'gc>,
attributes: EnumSet<Attribute>,
},
2020-02-19 19:17:33 +00:00
Slot {
slot_id: u32,
attributes: EnumSet<Attribute>,
},
2020-02-15 01:30:19 +00:00
}
unsafe impl<'gc> Collect for Property<'gc> {
fn trace(&self, cc: CollectionContext) {
match self {
Property::Virtual { get, set, .. } => {
get.trace(cc);
set.trace(cc);
}
Property::Stored { value, .. } => value.trace(cc),
2020-02-19 19:17:33 +00:00
Property::Slot { .. } => {}
2020-02-15 01:30:19 +00:00
}
}
}
impl<'gc> Property<'gc> {
/// Create a new stored property.
pub fn new_stored(value: impl Into<Value<'gc>>) -> Self {
Property::Stored {
value: value.into(),
attributes: EnumSet::from(Attribute::DontDelete),
}
}
/// Create a new stored property.
pub fn new_const(value: impl Into<Value<'gc>>) -> Self {
Property::Stored {
value: value.into(),
attributes: Attribute::ReadOnly | Attribute::DontDelete,
}
}
2020-02-15 01:30:19 +00:00
/// Convert a value into a dynamic property.
pub fn new_dynamic_property(value: impl Into<Value<'gc>>) -> Self {
Property::Stored {
value: value.into(),
attributes: EnumSet::empty(),
}
}
/// Convert a function into a method.
///
/// This applies ReadOnly/DontDelete to the property.
pub fn new_method(fn_obj: Object<'gc>) -> Self {
Property::Stored {
value: fn_obj.into(),
attributes: Attribute::ReadOnly | Attribute::DontDelete,
}
}
/// Create a new, unconfigured virtual property item.
pub fn new_virtual() -> Self {
Property::Virtual {
get: None,
set: None,
attributes: Attribute::ReadOnly | Attribute::DontDelete,
}
}
/// Create a new slot property.
pub fn new_slot(slot_id: u32) -> Self {
Property::Slot {
slot_id,
attributes: EnumSet::from(Attribute::DontDelete),
}
}
/// Install a getter into this property.
///
/// This function errors if attempting to install executables into a
/// non-virtual property.
pub fn install_virtual_getter(&mut self, getter_impl: Executable<'gc>) -> Result<(), Error> {
match self {
Property::Virtual { get, .. } => *get = Some(getter_impl),
Property::Stored { .. } => return Err("Not a virtual property".into()),
2020-02-19 19:17:33 +00:00
Property::Slot { .. } => return Err("Not a virtual property".into()),
};
Ok(())
}
/// Install a setter into this property.
///
/// This function errors if attempting to install executables into a
/// non-virtual property.
pub fn install_virtual_setter(&mut self, setter_impl: Executable<'gc>) -> Result<(), Error> {
match self {
Property::Virtual { set, .. } => *set = Some(setter_impl),
Property::Stored { .. } => return Err("Not a virtual property".into()),
2020-02-19 19:17:33 +00:00
Property::Slot { .. } => return Err("Not a virtual property".into()),
};
Ok(())
}
2020-02-15 01:30:19 +00:00
/// Get the value of a property slot.
///
/// This function yields `ReturnValue` because some properties may be
/// user-defined.
pub fn get(
&self,
avm: &mut Avm2<'gc>,
context: &mut UpdateContext<'_, 'gc, '_>,
this: Object<'gc>,
Completely overhaul the way traits are defined on objects. Previously, we were treating ES4 classes like syntactic sugar over a prototype chain (like ES6 classes); e.g. each declared trait was set in the given prototype and then property look-ups happened as normal. This already caused problems with virtual properties, which could be partially-defined in subclasses and required careful checks to make sure we stopped checking the prototype chain on the *correct* half of the property. However, this is a hint of a larger problem, which is that ES4 classes don't actually define anything on the prototype chain. Instead, the instance itself constructs class properties and methods on itself. This allows things like methods automatically binding `this`, which isn't included in this commit but will be implemented really soon. The prototype chain still exists even on pure ES4 classes, due to the need for backwards compatibility with ES3 code. Object, for example, still defines it's methods as prototype methods and thus there needs to be a prototype chain to reach them. I actually could have gotten away with using the prototype chain if AS3 *hadn't* retained this "legacy" detail of ES3 allowing this class/prototype distinction to leak out into upcoming tests. We still actually use the prototype chain for one other thing: trait resolution. When we look for a trait to install onto an object, we pull traits from the prototype chain using a special set of `TObject` methods. This happens in opposite order from normal prototype lookups so that subclassing and verification can proceed correctly. `super` somehow became even harder to implement: we now actually construct the parent class so we can get traits from it, which is going to complicate method binding as mentioned above.
2020-03-04 00:39:49 +00:00
base_proto: Option<Object<'gc>>,
2020-02-15 01:30:19 +00:00
) -> Result<ReturnValue<'gc>, Error> {
match self {
Property::Virtual { get: Some(get), .. } => {
Completely overhaul the way traits are defined on objects. Previously, we were treating ES4 classes like syntactic sugar over a prototype chain (like ES6 classes); e.g. each declared trait was set in the given prototype and then property look-ups happened as normal. This already caused problems with virtual properties, which could be partially-defined in subclasses and required careful checks to make sure we stopped checking the prototype chain on the *correct* half of the property. However, this is a hint of a larger problem, which is that ES4 classes don't actually define anything on the prototype chain. Instead, the instance itself constructs class properties and methods on itself. This allows things like methods automatically binding `this`, which isn't included in this commit but will be implemented really soon. The prototype chain still exists even on pure ES4 classes, due to the need for backwards compatibility with ES3 code. Object, for example, still defines it's methods as prototype methods and thus there needs to be a prototype chain to reach them. I actually could have gotten away with using the prototype chain if AS3 *hadn't* retained this "legacy" detail of ES3 allowing this class/prototype distinction to leak out into upcoming tests. We still actually use the prototype chain for one other thing: trait resolution. When we look for a trait to install onto an object, we pull traits from the prototype chain using a special set of `TObject` methods. This happens in opposite order from normal prototype lookups so that subclassing and verification can proceed correctly. `super` somehow became even harder to implement: we now actually construct the parent class so we can get traits from it, which is going to complicate method binding as mentioned above.
2020-03-04 00:39:49 +00:00
get.exec(Some(this), &[], avm, context, base_proto)
}
Property::Virtual { get: None, .. } => Ok(Value::Undefined.into()),
2020-02-15 01:30:19 +00:00
Property::Stored { value, .. } => Ok(value.to_owned().into()),
2020-02-19 19:17:33 +00:00
Property::Slot { slot_id, .. } => this.get_slot(*slot_id).map(|v| v.into()),
2020-02-15 01:30:19 +00:00
}
}
/// Set a property slot.
///
/// This function returns a `ReturnValue` which should be resolved. The
/// resulting `Value` is unimportant and should be discarded.
///
/// This function cannot set slot properties and will panic if one
/// is encountered.
2020-02-15 01:30:19 +00:00
pub fn set(
&mut self,
avm: &mut Avm2<'gc>,
context: &mut UpdateContext<'_, 'gc, '_>,
this: Object<'gc>,
Completely overhaul the way traits are defined on objects. Previously, we were treating ES4 classes like syntactic sugar over a prototype chain (like ES6 classes); e.g. each declared trait was set in the given prototype and then property look-ups happened as normal. This already caused problems with virtual properties, which could be partially-defined in subclasses and required careful checks to make sure we stopped checking the prototype chain on the *correct* half of the property. However, this is a hint of a larger problem, which is that ES4 classes don't actually define anything on the prototype chain. Instead, the instance itself constructs class properties and methods on itself. This allows things like methods automatically binding `this`, which isn't included in this commit but will be implemented really soon. The prototype chain still exists even on pure ES4 classes, due to the need for backwards compatibility with ES3 code. Object, for example, still defines it's methods as prototype methods and thus there needs to be a prototype chain to reach them. I actually could have gotten away with using the prototype chain if AS3 *hadn't* retained this "legacy" detail of ES3 allowing this class/prototype distinction to leak out into upcoming tests. We still actually use the prototype chain for one other thing: trait resolution. When we look for a trait to install onto an object, we pull traits from the prototype chain using a special set of `TObject` methods. This happens in opposite order from normal prototype lookups so that subclassing and verification can proceed correctly. `super` somehow became even harder to implement: we now actually construct the parent class so we can get traits from it, which is going to complicate method binding as mentioned above.
2020-03-04 00:39:49 +00:00
base_proto: Option<Object<'gc>>,
2020-02-15 01:30:19 +00:00
new_value: impl Into<Value<'gc>>,
) -> Result<ReturnValue<'gc>, Error> {
2020-02-15 01:30:19 +00:00
match self {
Property::Virtual { set, .. } => {
if let Some(function) = set {
return function.exec(
Some(this),
&[new_value.into()],
avm,
context,
Completely overhaul the way traits are defined on objects. Previously, we were treating ES4 classes like syntactic sugar over a prototype chain (like ES6 classes); e.g. each declared trait was set in the given prototype and then property look-ups happened as normal. This already caused problems with virtual properties, which could be partially-defined in subclasses and required careful checks to make sure we stopped checking the prototype chain on the *correct* half of the property. However, this is a hint of a larger problem, which is that ES4 classes don't actually define anything on the prototype chain. Instead, the instance itself constructs class properties and methods on itself. This allows things like methods automatically binding `this`, which isn't included in this commit but will be implemented really soon. The prototype chain still exists even on pure ES4 classes, due to the need for backwards compatibility with ES3 code. Object, for example, still defines it's methods as prototype methods and thus there needs to be a prototype chain to reach them. I actually could have gotten away with using the prototype chain if AS3 *hadn't* retained this "legacy" detail of ES3 allowing this class/prototype distinction to leak out into upcoming tests. We still actually use the prototype chain for one other thing: trait resolution. When we look for a trait to install onto an object, we pull traits from the prototype chain using a special set of `TObject` methods. This happens in opposite order from normal prototype lookups so that subclassing and verification can proceed correctly. `super` somehow became even harder to implement: we now actually construct the parent class so we can get traits from it, which is going to complicate method binding as mentioned above.
2020-03-04 00:39:49 +00:00
base_proto,
);
2020-02-15 01:30:19 +00:00
}
Ok(Value::Undefined.into())
2020-02-15 01:30:19 +00:00
}
Property::Stored {
value, attributes, ..
} => {
if !attributes.contains(ReadOnly) {
2020-02-22 21:21:28 +00:00
*value = new_value.into();
2020-02-15 01:30:19 +00:00
}
Ok(Value::Undefined.into())
2020-02-15 01:30:19 +00:00
}
Property::Slot { .. } => panic!("Cannot recursively set slots"),
2020-02-15 01:30:19 +00:00
}
}
2020-02-22 21:21:28 +00:00
/// Init a property slot.
///
/// The difference between `set` and `init` is that this function does not
/// respect `ReadOnly` and will allow initializing nominally `const`
/// properties, at least once. Virtual properties with no setter cannot be
/// initialized.
///
/// This function returns a `ReturnValue` which should be resolved. The
/// resulting `Value` is unimportant and should be discarded.
///
/// This function cannot initialize slot properties and will panic if one
/// is encountered.
2020-02-22 21:21:28 +00:00
pub fn init(
&mut self,
avm: &mut Avm2<'gc>,
context: &mut UpdateContext<'_, 'gc, '_>,
this: Object<'gc>,
Completely overhaul the way traits are defined on objects. Previously, we were treating ES4 classes like syntactic sugar over a prototype chain (like ES6 classes); e.g. each declared trait was set in the given prototype and then property look-ups happened as normal. This already caused problems with virtual properties, which could be partially-defined in subclasses and required careful checks to make sure we stopped checking the prototype chain on the *correct* half of the property. However, this is a hint of a larger problem, which is that ES4 classes don't actually define anything on the prototype chain. Instead, the instance itself constructs class properties and methods on itself. This allows things like methods automatically binding `this`, which isn't included in this commit but will be implemented really soon. The prototype chain still exists even on pure ES4 classes, due to the need for backwards compatibility with ES3 code. Object, for example, still defines it's methods as prototype methods and thus there needs to be a prototype chain to reach them. I actually could have gotten away with using the prototype chain if AS3 *hadn't* retained this "legacy" detail of ES3 allowing this class/prototype distinction to leak out into upcoming tests. We still actually use the prototype chain for one other thing: trait resolution. When we look for a trait to install onto an object, we pull traits from the prototype chain using a special set of `TObject` methods. This happens in opposite order from normal prototype lookups so that subclassing and verification can proceed correctly. `super` somehow became even harder to implement: we now actually construct the parent class so we can get traits from it, which is going to complicate method binding as mentioned above.
2020-03-04 00:39:49 +00:00
base_proto: Option<Object<'gc>>,
2020-02-22 21:21:28 +00:00
new_value: impl Into<Value<'gc>>,
) -> Result<ReturnValue<'gc>, Error> {
2020-02-22 21:21:28 +00:00
match self {
Property::Virtual { set, .. } => {
if let Some(function) = set {
return function.exec(
Some(this),
&[new_value.into()],
avm,
context,
Completely overhaul the way traits are defined on objects. Previously, we were treating ES4 classes like syntactic sugar over a prototype chain (like ES6 classes); e.g. each declared trait was set in the given prototype and then property look-ups happened as normal. This already caused problems with virtual properties, which could be partially-defined in subclasses and required careful checks to make sure we stopped checking the prototype chain on the *correct* half of the property. However, this is a hint of a larger problem, which is that ES4 classes don't actually define anything on the prototype chain. Instead, the instance itself constructs class properties and methods on itself. This allows things like methods automatically binding `this`, which isn't included in this commit but will be implemented really soon. The prototype chain still exists even on pure ES4 classes, due to the need for backwards compatibility with ES3 code. Object, for example, still defines it's methods as prototype methods and thus there needs to be a prototype chain to reach them. I actually could have gotten away with using the prototype chain if AS3 *hadn't* retained this "legacy" detail of ES3 allowing this class/prototype distinction to leak out into upcoming tests. We still actually use the prototype chain for one other thing: trait resolution. When we look for a trait to install onto an object, we pull traits from the prototype chain using a special set of `TObject` methods. This happens in opposite order from normal prototype lookups so that subclassing and verification can proceed correctly. `super` somehow became even harder to implement: we now actually construct the parent class so we can get traits from it, which is going to complicate method binding as mentioned above.
2020-03-04 00:39:49 +00:00
base_proto,
);
2020-02-22 21:21:28 +00:00
}
Ok(Value::Undefined.into())
2020-02-22 21:21:28 +00:00
}
Property::Stored { value, .. } => {
*value = new_value.into();
Ok(Value::Undefined.into())
2020-02-22 21:21:28 +00:00
}
Property::Slot { .. } => panic!("Cannot recursively init slots"),
}
}
/// Retrieve the slot ID of a property.
///
/// This function yields `None` if this property is not a slot.
pub fn slot_id(&self) -> Option<u32> {
match self {
Property::Slot { slot_id, .. } => Some(*slot_id),
_ => None,
2020-02-22 21:21:28 +00:00
}
}
2020-02-15 01:30:19 +00:00
pub fn can_delete(&self) -> bool {
match self {
Property::Virtual { attributes, .. } => !attributes.contains(DontDelete),
Property::Stored { attributes, .. } => !attributes.contains(DontDelete),
2020-02-19 19:17:33 +00:00
Property::Slot { attributes, .. } => !attributes.contains(DontDelete),
2020-02-15 01:30:19 +00:00
}
}
pub fn is_overwritable(&self) -> bool {
match self {
Property::Virtual {
attributes, set, ..
} => !attributes.contains(ReadOnly) && !set.is_none(),
Property::Stored { attributes, .. } => !attributes.contains(ReadOnly),
2020-02-19 19:17:33 +00:00
Property::Slot { attributes, .. } => !attributes.contains(ReadOnly),
2020-02-15 01:30:19 +00:00
}
}
}