ruffle/core/src/avm2/script_object.rs

908 lines
28 KiB
Rust
Raw Normal View History

//! Default AVM2 object impl
use crate::avm1::AvmString;
use crate::avm2::activation::Activation;
use crate::avm2::class::Class;
use crate::avm2::function::Executable;
use crate::avm2::names::{Namespace, QName};
2020-02-10 19:54:55 +00:00
use crate::avm2::object::{Object, ObjectPtr, TObject};
2020-02-15 01:30:19 +00:00
use crate::avm2::property::Property;
use crate::avm2::r#trait::Trait;
use crate::avm2::return_value::ReturnValue;
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
use crate::avm2::scope::Scope;
use crate::avm2::slot::Slot;
use crate::avm2::value::Value;
use crate::avm2::Error;
use crate::context::UpdateContext;
use gc_arena::{Collect, GcCell, MutationContext};
use std::collections::HashMap;
use std::fmt::Debug;
/// Default implementation of `avm2::Object`.
#[derive(Clone, Collect, Debug, Copy)]
#[collect(no_drop)]
pub struct ScriptObject<'gc>(GcCell<'gc, ScriptObjectData<'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
/// Information necessary for a script object to have a class attached to it.
///
/// Classes can be attached to a `ScriptObject` such that the class's traits
/// are instantiated on-demand. Either class or instance traits can be
/// instantiated.
///
/// Trait instantiation obeys prototyping rules: prototypes provide their
/// instances with classes to pull traits from.
#[derive(Clone, Collect, Debug)]
#[collect(no_drop)]
pub enum ScriptObjectClass<'gc> {
/// Instantiate instance traits, for prototypes.
InstancePrototype(GcCell<'gc, Class<'gc>>, Option<GcCell<'gc, Scope<'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
/// Instantiate class traits, for class constructors.
ClassConstructor(GcCell<'gc, Class<'gc>>, Option<GcCell<'gc, Scope<'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
/// Do not instantiate any class or instance traits.
NoClass,
}
/// Base data common to all `TObject` implementations.
///
/// Host implementations of `TObject` should embed `ScriptObjectData` and
/// forward any trait method implementations it does not overwrite to this
/// struct.
#[derive(Clone, Collect, Debug)]
#[collect(no_drop)]
pub struct ScriptObjectData<'gc> {
/// Properties stored on this object.
values: HashMap<QName<'gc>, Property<'gc>>,
2020-02-13 03:47:56 +00:00
2020-02-19 19:17:33 +00:00
/// Slots stored on this object.
slots: Vec<Slot<'gc>>,
2020-02-19 19:17:33 +00:00
/// Methods stored on this object.
methods: Vec<Option<Object<'gc>>>,
/// Implicit prototype of this script object.
2020-02-13 03:47:56 +00:00
proto: Option<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
/// The class that this script object represents.
class: ScriptObjectClass<'gc>,
Implement `hasnext`, `hasnext2`, `nextname`, `nextvalue`, and the underlying enumeration machinery that powers it. I have... significant reservations with the way object enumeration happens in AVM2. For comparison, AVM1 enumeration works like this: You enumerate the entire object at once, producing a list of property names, which are then pushed onto the stack after a sentinel value. This is a properly abstract way to handle property enumeration. In AVM2, they completely replaced this with index-based enumeration. What this means is that you hand the object an index and it gives you back a name or value. There's also an instruction that will give you the next index in the object. The only advantage I can think of is that it results in less stack manipulation if you want to bail out of iteration early. You just jump out of your loop and kill the registers you don't care about. The disadvantage is that it locks the object representation down pretty hard. They also screwed up the definition of `hasnext`, and thus the VM is stuck enumerating properties from 1. This is because `hasnext` and `hasnext2` increment the index value before checking the object. Code generated by Animate 2020 (which I suspect to be the final version of that software that generates AVM2 code) initializes the index at hero, and then does `hasnext2`, hence we have to start from one. I actually cheated a little and added a separate `Vec` for storing enumerant names. I strongly suspect that Adobe's implementation has objects be inherently slot-oriented, and named properties are just hashmap lookups to slots. This would allow enumerating the slots to get names out of the object.
2020-03-06 02:26:01 +00:00
/// Enumeratable property names.
enumerants: Vec<QName<'gc>>,
/// Interfaces implemented by this object. (prototypes only)
interfaces: Vec<Object<'gc>>,
}
impl<'gc> TObject<'gc> for ScriptObject<'gc> {
fn get_property_local(
self,
reciever: Object<'gc>,
name: &QName<'gc>,
activation: &mut Activation<'_, 'gc>,
context: &mut UpdateContext<'_, 'gc, '_>,
) -> Result<Value<'gc>, Error> {
let rv = self
.0
.read()
.get_property_local(reciever, name, activation)?;
rv.resolve(activation, context)
}
fn set_property_local(
self,
reciever: Object<'gc>,
name: &QName<'gc>,
value: Value<'gc>,
activation: &mut Activation<'_, 'gc>,
context: &mut UpdateContext<'_, 'gc, '_>,
) -> Result<(), Error> {
let rv = self
.0
.write(context.gc_context)
.set_property_local(reciever, name, value, activation, context)?;
rv.resolve(activation, context)?;
Ok(())
}
fn init_property_local(
2020-02-22 21:21:28 +00:00
self,
reciever: Object<'gc>,
name: &QName<'gc>,
2020-02-22 21:21:28 +00:00
value: Value<'gc>,
activation: &mut Activation<'_, 'gc>,
2020-02-22 21:21:28 +00:00
context: &mut UpdateContext<'_, 'gc, '_>,
) -> Result<(), Error> {
let rv = self
.0
2020-02-22 21:21:28 +00:00
.write(context.gc_context)
.init_property_local(reciever, name, value, activation, context)?;
rv.resolve(activation, context)?;
Ok(())
2020-02-22 21:21:28 +00:00
}
fn is_property_overwritable(
self,
gc_context: MutationContext<'gc, '_>,
name: &QName<'gc>,
) -> bool {
2020-03-04 04:03:35 +00:00
self.0.write(gc_context).is_property_overwritable(name)
}
fn delete_property(&self, gc_context: MutationContext<'gc, '_>, name: &QName<'gc>) -> bool {
2020-03-04 04:03:35 +00:00
self.0.write(gc_context).delete_property(name)
2020-02-21 19:52:24 +00:00
}
2020-02-19 19:17:33 +00:00
fn get_slot(self, id: u32) -> Result<Value<'gc>, Error> {
self.0.read().get_slot(id)
}
fn set_slot(
self,
id: u32,
value: Value<'gc>,
mc: MutationContext<'gc, '_>,
) -> Result<(), Error> {
self.0.write(mc).set_slot(id, value, mc)
}
2020-02-22 21:21:28 +00:00
fn init_slot(
self,
id: u32,
value: Value<'gc>,
mc: MutationContext<'gc, '_>,
) -> Result<(), Error> {
self.0.write(mc).init_slot(id, value, mc)
}
fn get_method(self, id: u32) -> Option<Object<'gc>> {
self.0.read().get_method(id)
}
fn get_trait(self, name: &QName<'gc>) -> Result<Vec<Trait<'gc>>, Error> {
self.0.read().get_trait(name)
}
fn get_provided_trait(
&self,
name: &QName<'gc>,
known_traits: &mut Vec<Trait<'gc>>,
) -> Result<(), Error> {
self.0.read().get_provided_trait(name, known_traits)
}
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
fn get_scope(self) -> Option<GcCell<'gc, Scope<'gc>>> {
self.0.read().get_scope()
}
fn resolve_any(self, local_name: AvmString<'gc>) -> Result<Option<Namespace<'gc>>, Error> {
self.0.read().resolve_any(local_name)
}
fn resolve_any_trait(
self,
local_name: AvmString<'gc>,
) -> Result<Option<Namespace<'gc>>, Error> {
self.0.read().resolve_any_trait(local_name)
}
fn has_own_property(self, name: &QName<'gc>) -> Result<bool, Error> {
self.0.read().has_own_property(name)
2020-02-12 00:42:47 +00:00
}
fn has_trait(self, name: &QName<'gc>) -> Result<bool, Error> {
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
self.0.read().has_trait(name)
}
fn provides_trait(self, name: &QName<'gc>) -> Result<bool, Error> {
self.0.read().provides_trait(name)
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
}
fn has_instantiated_property(self, name: &QName<'gc>) -> bool {
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
self.0.read().has_instantiated_property(name)
}
fn has_own_virtual_getter(self, name: &QName<'gc>) -> bool {
self.0.read().has_own_virtual_getter(name)
}
fn has_own_virtual_setter(self, name: &QName<'gc>) -> bool {
self.0.read().has_own_virtual_setter(name)
}
2020-02-13 03:47:56 +00:00
fn proto(&self) -> Option<Object<'gc>> {
self.0.read().proto
}
fn get_enumerant_name(&self, index: u32) -> Option<QName<'gc>> {
Implement `hasnext`, `hasnext2`, `nextname`, `nextvalue`, and the underlying enumeration machinery that powers it. I have... significant reservations with the way object enumeration happens in AVM2. For comparison, AVM1 enumeration works like this: You enumerate the entire object at once, producing a list of property names, which are then pushed onto the stack after a sentinel value. This is a properly abstract way to handle property enumeration. In AVM2, they completely replaced this with index-based enumeration. What this means is that you hand the object an index and it gives you back a name or value. There's also an instruction that will give you the next index in the object. The only advantage I can think of is that it results in less stack manipulation if you want to bail out of iteration early. You just jump out of your loop and kill the registers you don't care about. The disadvantage is that it locks the object representation down pretty hard. They also screwed up the definition of `hasnext`, and thus the VM is stuck enumerating properties from 1. This is because `hasnext` and `hasnext2` increment the index value before checking the object. Code generated by Animate 2020 (which I suspect to be the final version of that software that generates AVM2 code) initializes the index at hero, and then does `hasnext2`, hence we have to start from one. I actually cheated a little and added a separate `Vec` for storing enumerant names. I strongly suspect that Adobe's implementation has objects be inherently slot-oriented, and named properties are just hashmap lookups to slots. This would allow enumerating the slots to get names out of the object.
2020-03-06 02:26:01 +00:00
self.0.read().get_enumerant_name(index)
}
fn property_is_enumerable(&self, name: &QName<'gc>) -> bool {
2020-03-08 01:16:48 +00:00
self.0.read().property_is_enumerable(name)
}
2020-03-08 23:12:17 +00:00
fn set_local_property_is_enumerable(
&self,
mc: MutationContext<'gc, '_>,
name: &QName<'gc>,
2020-03-08 23:12:17 +00:00
is_enumerable: bool,
) -> Result<(), Error> {
self.0
.write(mc)
.set_local_property_is_enumerable(name, is_enumerable)
}
fn as_ptr(&self) -> *const ObjectPtr {
self.0.as_ptr() as *const ObjectPtr
}
2020-02-19 23:53:21 +00:00
fn construct(
&self,
_activation: &mut Activation<'_, 'gc>,
2020-02-19 23:53:21 +00:00
context: &mut UpdateContext<'_, 'gc, '_>,
_args: &[Value<'gc>],
) -> Result<Object<'gc>, Error> {
let this: Object<'gc> = Object::ScriptObject(*self);
Ok(ScriptObject::object(context.gc_context, this))
}
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
fn derive(
&self,
_activation: &mut Activation<'_, '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
context: &mut UpdateContext<'_, 'gc, '_>,
class: GcCell<'gc, Class<'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
scope: Option<GcCell<'gc, Scope<'gc>>>,
) -> Result<Object<'gc>, Error> {
let this: Object<'gc> = Object::ScriptObject(*self);
Ok(ScriptObject::prototype(
context.gc_context,
this,
class,
scope,
))
}
fn to_string(&self, _mc: MutationContext<'gc, '_>) -> Result<Value<'gc>, Error> {
Ok("[object Object]".into())
}
fn value_of(&self, _mc: MutationContext<'gc, '_>) -> Result<Value<'gc>, Error> {
Ok(Value::Object(Object::from(*self)))
}
fn install_method(
&mut self,
mc: MutationContext<'gc, '_>,
name: QName<'gc>,
disp_id: u32,
function: Object<'gc>,
) {
self.0.write(mc).install_method(name, disp_id, function)
}
fn install_getter(
&mut self,
mc: MutationContext<'gc, '_>,
name: QName<'gc>,
disp_id: u32,
function: Object<'gc>,
) -> Result<(), Error> {
self.0.write(mc).install_getter(name, disp_id, function)
}
fn install_setter(
&mut self,
mc: MutationContext<'gc, '_>,
name: QName<'gc>,
disp_id: u32,
function: Object<'gc>,
) -> Result<(), Error> {
self.0.write(mc).install_setter(name, disp_id, function)
}
fn install_dynamic_property(
&mut self,
mc: MutationContext<'gc, '_>,
name: QName<'gc>,
value: Value<'gc>,
) -> Result<(), Error> {
self.0.write(mc).install_dynamic_property(name, value)
}
fn install_slot(
&mut self,
mc: MutationContext<'gc, '_>,
name: QName<'gc>,
id: u32,
value: Value<'gc>,
) {
self.0.write(mc).install_slot(name, id, value)
}
fn install_const(
&mut self,
mc: MutationContext<'gc, '_>,
name: QName<'gc>,
id: u32,
value: Value<'gc>,
) {
self.0.write(mc).install_const(name, id, value)
}
fn interfaces(&self) -> Vec<Object<'gc>> {
self.0.read().interfaces()
}
fn set_interfaces(&self, context: MutationContext<'gc, '_>, iface_list: Vec<Object<'gc>>) {
self.0.write(context).set_interfaces(iface_list)
}
}
2020-02-10 19:54:55 +00:00
impl<'gc> ScriptObject<'gc> {
2020-02-13 03:47:56 +00:00
/// Construct a bare object with no base class.
///
/// This is *not* the same thing as an object literal, which actually does
/// have a base class: `Object`.
2020-02-10 19:54:55 +00:00
pub fn bare_object(mc: MutationContext<'gc, '_>) -> Object<'gc> {
ScriptObject(GcCell::allocate(
mc,
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
ScriptObjectData::base_new(None, ScriptObjectClass::NoClass),
))
.into()
2020-02-10 19:54:55 +00:00
}
/// Construct a bare class prototype with no base class.
///
/// This appears to be used specifically for interfaces, which have no base
/// class.
pub fn bare_prototype(
mc: MutationContext<'gc, '_>,
class: GcCell<'gc, Class<'gc>>,
scope: Option<GcCell<'gc, Scope<'gc>>>,
) -> Object<'gc> {
let script_class = ScriptObjectClass::InstancePrototype(class, scope);
ScriptObject(GcCell::allocate(
mc,
ScriptObjectData::base_new(None, script_class),
))
.into()
}
/// Construct an object with a prototype.
pub fn object(mc: MutationContext<'gc, '_>, proto: Object<'gc>) -> Object<'gc> {
ScriptObject(GcCell::allocate(
mc,
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
ScriptObjectData::base_new(Some(proto), ScriptObjectClass::NoClass),
))
.into()
}
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
/// Construct a prototype for an ES4 class.
pub fn prototype(
mc: MutationContext<'gc, '_>,
proto: Object<'gc>,
class: GcCell<'gc, Class<'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
scope: Option<GcCell<'gc, Scope<'gc>>>,
) -> 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
let script_class = ScriptObjectClass::InstancePrototype(class, scope);
ScriptObject(GcCell::allocate(
mc,
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
ScriptObjectData::base_new(Some(proto), script_class),
))
.into()
}
2020-02-10 19:54:55 +00:00
}
impl<'gc> ScriptObjectData<'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
pub fn base_new(proto: Option<Object<'gc>>, trait_source: ScriptObjectClass<'gc>) -> Self {
ScriptObjectData {
values: HashMap::new(),
2020-02-19 19:17:33 +00:00
slots: Vec::new(),
methods: Vec::new(),
proto,
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
class: trait_source,
Implement `hasnext`, `hasnext2`, `nextname`, `nextvalue`, and the underlying enumeration machinery that powers it. I have... significant reservations with the way object enumeration happens in AVM2. For comparison, AVM1 enumeration works like this: You enumerate the entire object at once, producing a list of property names, which are then pushed onto the stack after a sentinel value. This is a properly abstract way to handle property enumeration. In AVM2, they completely replaced this with index-based enumeration. What this means is that you hand the object an index and it gives you back a name or value. There's also an instruction that will give you the next index in the object. The only advantage I can think of is that it results in less stack manipulation if you want to bail out of iteration early. You just jump out of your loop and kill the registers you don't care about. The disadvantage is that it locks the object representation down pretty hard. They also screwed up the definition of `hasnext`, and thus the VM is stuck enumerating properties from 1. This is because `hasnext` and `hasnext2` increment the index value before checking the object. Code generated by Animate 2020 (which I suspect to be the final version of that software that generates AVM2 code) initializes the index at hero, and then does `hasnext2`, hence we have to start from one. I actually cheated a little and added a separate `Vec` for storing enumerant names. I strongly suspect that Adobe's implementation has objects be inherently slot-oriented, and named properties are just hashmap lookups to slots. This would allow enumerating the slots to get names out of the object.
2020-03-06 02:26:01 +00:00
enumerants: Vec::new(),
interfaces: Vec::new(),
}
}
pub fn get_property_local(
&self,
reciever: Object<'gc>,
name: &QName<'gc>,
activation: &mut Activation<'_, 'gc>,
) -> Result<ReturnValue<'gc>, Error> {
2020-02-15 01:30:19 +00:00
let prop = self.values.get(name);
if let Some(prop) = prop {
prop.get(reciever, activation.base_proto().or(self.proto))
2020-02-15 01:30:19 +00:00
} else {
Ok(Value::Undefined.into())
2020-02-15 01:30:19 +00:00
}
}
pub fn set_property_local(
&mut self,
reciever: Object<'gc>,
name: &QName<'gc>,
value: Value<'gc>,
activation: &mut Activation<'_, 'gc>,
context: &mut UpdateContext<'_, 'gc, '_>,
) -> Result<ReturnValue<'gc>, Error> {
let slot_id = if let Some(prop) = self.values.get(name) {
if let Some(slot_id) = prop.slot_id() {
Some(slot_id)
} else {
None
}
} else {
None
};
if let Some(slot_id) = slot_id {
self.set_slot(slot_id, value, context.gc_context)?;
Ok(Value::Undefined.into())
} else if self.values.contains_key(name) {
let prop = self.values.get_mut(name).unwrap();
let proto = self.proto;
prop.set(reciever, activation.base_proto().or(proto), value)
} else {
2020-02-15 01:30:19 +00:00
//TODO: Not all classes are dynamic like this
Implement `hasnext`, `hasnext2`, `nextname`, `nextvalue`, and the underlying enumeration machinery that powers it. I have... significant reservations with the way object enumeration happens in AVM2. For comparison, AVM1 enumeration works like this: You enumerate the entire object at once, producing a list of property names, which are then pushed onto the stack after a sentinel value. This is a properly abstract way to handle property enumeration. In AVM2, they completely replaced this with index-based enumeration. What this means is that you hand the object an index and it gives you back a name or value. There's also an instruction that will give you the next index in the object. The only advantage I can think of is that it results in less stack manipulation if you want to bail out of iteration early. You just jump out of your loop and kill the registers you don't care about. The disadvantage is that it locks the object representation down pretty hard. They also screwed up the definition of `hasnext`, and thus the VM is stuck enumerating properties from 1. This is because `hasnext` and `hasnext2` increment the index value before checking the object. Code generated by Animate 2020 (which I suspect to be the final version of that software that generates AVM2 code) initializes the index at hero, and then does `hasnext2`, hence we have to start from one. I actually cheated a little and added a separate `Vec` for storing enumerant names. I strongly suspect that Adobe's implementation has objects be inherently slot-oriented, and named properties are just hashmap lookups to slots. This would allow enumerating the slots to get names out of the object.
2020-03-06 02:26:01 +00:00
self.enumerants.push(name.clone());
2020-02-15 01:30:19 +00:00
self.values
.insert(name.clone(), Property::new_dynamic_property(value));
Ok(Value::Undefined.into())
}
}
2020-02-12 00:42:47 +00:00
pub fn init_property_local(
2020-02-22 21:21:28 +00:00
&mut self,
reciever: Object<'gc>,
name: &QName<'gc>,
2020-02-22 21:21:28 +00:00
value: Value<'gc>,
activation: &mut Activation<'_, 'gc>,
2020-02-22 21:21:28 +00:00
context: &mut UpdateContext<'_, 'gc, '_>,
) -> Result<ReturnValue<'gc>, Error> {
2020-02-22 21:21:28 +00:00
if let Some(prop) = self.values.get_mut(name) {
if let Some(slot_id) = prop.slot_id() {
self.init_slot(slot_id, value, context.gc_context)?;
Ok(Value::Undefined.into())
} else {
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
let proto = self.proto;
prop.init(reciever, activation.base_proto().or(proto), value)
}
2020-02-22 21:21:28 +00:00
} else {
//TODO: Not all classes are dynamic like this
self.values
.insert(name.clone(), Property::new_dynamic_property(value));
Ok(Value::Undefined.into())
}
2020-02-22 21:21:28 +00:00
}
pub fn is_property_overwritable(&self, name: &QName<'gc>) -> bool {
2020-03-04 04:03:35 +00:00
self.values
.get(name)
.map(|p| p.is_overwritable())
.unwrap_or(true)
}
pub fn delete_property(&mut self, name: &QName<'gc>) -> bool {
2020-02-21 19:52:24 +00:00
let can_delete = if let Some(prop) = self.values.get(name) {
prop.can_delete()
} else {
false
};
if can_delete {
self.values.remove(name);
}
can_delete
}
2020-02-19 19:17:33 +00:00
pub fn get_slot(&self, id: u32) -> Result<Value<'gc>, Error> {
//TODO: slot inheritance, I think?
2020-02-19 19:17:33 +00:00
self.slots
.get(id as usize)
.cloned()
.ok_or_else(|| format!("Slot index {} out of bounds!", id).into())
.map(|slot| slot.get().unwrap_or(Value::Undefined))
2020-02-19 19:17:33 +00:00
}
/// Set a slot by it's index.
pub fn set_slot(
&mut self,
id: u32,
value: Value<'gc>,
_mc: MutationContext<'gc, '_>,
2020-02-19 19:17:33 +00:00
) -> Result<(), Error> {
if let Some(slot) = self.slots.get_mut(id as usize) {
slot.set(value)
2020-02-19 19:17:33 +00:00
} else {
Err(format!("Slot index {} out of bounds!", id).into())
}
}
2020-02-22 21:21:28 +00:00
/// Set a slot by it's index.
pub fn init_slot(
&mut self,
id: u32,
value: Value<'gc>,
_mc: MutationContext<'gc, '_>,
) -> Result<(), Error> {
if let Some(slot) = self.slots.get_mut(id as usize) {
slot.init(value)
} else {
Err(format!("Slot index {} out of bounds!", id).into())
}
}
/// Retrieve a method from the method table.
pub fn get_method(&self, id: u32) -> Option<Object<'gc>> {
self.methods.get(id as usize).and_then(|v| *v)
}
pub fn get_trait(&self, name: &QName<'gc>) -> Result<Vec<Trait<'gc>>, Error> {
match &self.class {
//Class constructors have local traits only.
ScriptObjectClass::ClassConstructor(..) => {
let mut known_traits = Vec::new();
self.get_provided_trait(name, &mut known_traits)?;
Ok(known_traits)
}
//Prototypes do not have traits available locally, but they provide
//traits instead.
ScriptObjectClass::InstancePrototype(..) => Ok(Vec::new()),
//Instances walk the prototype chain to build a list of known
//traits provided by the classes attached to those prototypes.
ScriptObjectClass::NoClass => {
let mut known_traits = Vec::new();
let mut chain = Vec::new();
let mut proto = self.proto();
while let Some(p) = proto {
chain.push(p);
proto = p.proto();
}
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
for proto in chain.iter().rev() {
proto.get_provided_trait(name, &mut known_traits)?;
}
Ok(known_traits)
}
}
}
pub fn get_provided_trait(
&self,
name: &QName<'gc>,
known_traits: &mut Vec<Trait<'gc>>,
) -> Result<(), Error> {
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
match &self.class {
ScriptObjectClass::ClassConstructor(class, ..) => {
class.read().lookup_class_traits(name, known_traits)
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
}
ScriptObjectClass::InstancePrototype(class, ..) => {
class.read().lookup_instance_traits(name, known_traits)
}
ScriptObjectClass::NoClass => Ok(()),
}
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
}
pub fn has_trait(&self, name: &QName<'gc>) -> Result<bool, Error> {
match &self.class {
//Class constructors have local traits only.
ScriptObjectClass::ClassConstructor(..) => self.provides_trait(name),
//Prototypes do not have traits available locally, but we walk
//through them to find traits (see `provides_trait`)
ScriptObjectClass::InstancePrototype(..) => Ok(false),
//Instances walk the prototype chain to build a list of known
//traits provided by the classes attached to those prototypes.
ScriptObjectClass::NoClass => {
let mut proto = self.proto();
while let Some(p) = proto {
if p.provides_trait(name)? {
return Ok(true);
}
proto = p.proto();
}
Ok(false)
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
}
}
}
pub fn provides_trait(&self, name: &QName<'gc>) -> Result<bool, Error> {
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
match &self.class {
ScriptObjectClass::ClassConstructor(class, ..) => {
Ok(class.read().has_class_trait(name))
}
ScriptObjectClass::InstancePrototype(class, ..) => {
Ok(class.read().has_instance_trait(name))
}
ScriptObjectClass::NoClass => Ok(false),
}
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
}
pub fn get_scope(&self) -> Option<GcCell<'gc, Scope<'gc>>> {
match &self.class {
ScriptObjectClass::ClassConstructor(_class, scope) => *scope,
ScriptObjectClass::InstancePrototype(_class, scope) => *scope,
ScriptObjectClass::NoClass => self.proto().and_then(|proto| proto.get_scope()),
}
}
pub fn resolve_any(&self, local_name: AvmString<'gc>) -> Result<Option<Namespace<'gc>>, Error> {
for (key, _value) in self.values.iter() {
if key.local_name() == local_name {
return Ok(Some(key.namespace().clone()));
}
}
match self.class {
ScriptObjectClass::ClassConstructor(..) => self.resolve_any_trait(local_name),
ScriptObjectClass::NoClass => self.resolve_any_trait(local_name),
_ => Ok(None),
}
}
pub fn resolve_any_trait(
&self,
local_name: AvmString<'gc>,
) -> Result<Option<Namespace<'gc>>, Error> {
if let Some(proto) = self.proto {
let proto_trait_name = proto.resolve_any_trait(local_name)?;
if let Some(ns) = proto_trait_name {
return Ok(Some(ns));
}
}
match &self.class {
ScriptObjectClass::ClassConstructor(class, ..) => {
Ok(class.read().resolve_any_class_trait(local_name))
}
ScriptObjectClass::InstancePrototype(class, ..) => {
Ok(class.read().resolve_any_instance_trait(local_name))
}
ScriptObjectClass::NoClass => Ok(None),
}
}
pub fn has_own_property(&self, name: &QName<'gc>) -> Result<bool, Error> {
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
Ok(self.values.get(name).is_some() || self.has_trait(name)?)
}
pub fn has_instantiated_property(&self, name: &QName<'gc>) -> bool {
2020-02-12 00:42:47 +00:00
self.values.get(name).is_some()
}
2020-02-13 03:47:56 +00:00
pub fn has_own_virtual_getter(&self, name: &QName<'gc>) -> bool {
match self.values.get(name) {
Some(Property::Virtual { get: Some(_), .. }) => true,
_ => false,
}
}
pub fn has_own_virtual_setter(&self, name: &QName<'gc>) -> bool {
match self.values.get(name) {
Some(Property::Virtual { set: Some(_), .. }) => true,
_ => false,
}
}
2020-02-13 03:47:56 +00:00
pub fn proto(&self) -> Option<Object<'gc>> {
self.proto
}
pub fn get_enumerant_name(&self, index: u32) -> Option<QName<'gc>> {
Implement `hasnext`, `hasnext2`, `nextname`, `nextvalue`, and the underlying enumeration machinery that powers it. I have... significant reservations with the way object enumeration happens in AVM2. For comparison, AVM1 enumeration works like this: You enumerate the entire object at once, producing a list of property names, which are then pushed onto the stack after a sentinel value. This is a properly abstract way to handle property enumeration. In AVM2, they completely replaced this with index-based enumeration. What this means is that you hand the object an index and it gives you back a name or value. There's also an instruction that will give you the next index in the object. The only advantage I can think of is that it results in less stack manipulation if you want to bail out of iteration early. You just jump out of your loop and kill the registers you don't care about. The disadvantage is that it locks the object representation down pretty hard. They also screwed up the definition of `hasnext`, and thus the VM is stuck enumerating properties from 1. This is because `hasnext` and `hasnext2` increment the index value before checking the object. Code generated by Animate 2020 (which I suspect to be the final version of that software that generates AVM2 code) initializes the index at hero, and then does `hasnext2`, hence we have to start from one. I actually cheated a little and added a separate `Vec` for storing enumerant names. I strongly suspect that Adobe's implementation has objects be inherently slot-oriented, and named properties are just hashmap lookups to slots. This would allow enumerating the slots to get names out of the object.
2020-03-06 02:26:01 +00:00
// NOTE: AVM2 object enumeration is one of the weakest parts of an
// otherwise well-designed VM. Notably, because of the way they
// implemented `hasnext` and `hasnext2`, all enumerants start from ONE.
// Hence why we have to `checked_sub` here in case some miscompiled
// code doesn't check for the zero index, which is actually a failure
// sentinel.
let true_index = (index as usize).checked_sub(1)?;
self.enumerants.get(true_index).cloned()
}
pub fn property_is_enumerable(&self, name: &QName<'gc>) -> bool {
2020-03-08 01:16:48 +00:00
self.enumerants.contains(name)
}
2020-03-08 23:12:17 +00:00
pub fn set_local_property_is_enumerable(
&mut self,
name: &QName<'gc>,
2020-03-08 23:12:17 +00:00
is_enumerable: bool,
) -> Result<(), Error> {
if is_enumerable && self.values.contains_key(name) && !self.enumerants.contains(name) {
// Traits are never enumerable
if self.has_trait(name)? {
return Ok(());
}
self.enumerants.push(name.clone());
} else if !is_enumerable && self.enumerants.contains(name) {
let mut index = None;
for (i, other_name) in self.enumerants.iter().enumerate() {
if other_name == name {
index = Some(i);
}
}
if let Some(index) = index {
self.enumerants.remove(index);
}
}
Ok(())
}
pub fn class(&self) -> &ScriptObjectClass<'gc> {
&self.class
}
/// Install a method into the object.
pub fn install_method(&mut self, name: QName<'gc>, disp_id: u32, function: Object<'gc>) {
if disp_id > 0 {
if self.methods.len() <= disp_id as usize {
self.methods
.resize_with(disp_id as usize + 1, Default::default);
}
*self.methods.get_mut(disp_id as usize).unwrap() = Some(function);
}
self.values.insert(name, Property::new_method(function));
}
/// Install a getter into the object.
///
/// This is a little more complicated than methods, since virtual property
/// slots can be installed in two parts. Thus, we need to support
/// installing them in either order.
pub fn install_getter(
&mut self,
name: QName<'gc>,
disp_id: u32,
function: Object<'gc>,
) -> Result<(), Error> {
let executable: Result<Executable<'gc>, Error> = function
.as_executable()
.ok_or_else(|| "Attempted to install getter without a valid method".into());
let executable = executable?;
if disp_id > 0 {
if self.methods.len() <= disp_id as usize {
self.methods
.resize_with(disp_id as usize + 1, Default::default);
}
*self.methods.get_mut(disp_id as usize).unwrap() = Some(function);
}
if !self.values.contains_key(&name) {
self.values.insert(name.clone(), Property::new_virtual());
}
self.values
.get_mut(&name)
.unwrap()
.install_virtual_getter(executable)
}
/// Install a setter into the object.
///
/// This is a little more complicated than methods, since virtual property
/// slots can be installed in two parts. Thus, we need to support
/// installing them in either order.
pub fn install_setter(
&mut self,
name: QName<'gc>,
disp_id: u32,
function: Object<'gc>,
) -> Result<(), Error> {
let executable: Result<Executable<'gc>, Error> = function
.as_executable()
.ok_or_else(|| "Attempted to install setter without a valid method".into());
let executable = executable?;
if disp_id > 0 {
if self.methods.len() <= disp_id as usize {
self.methods
.resize_with(disp_id as usize + 1, Default::default);
}
*self.methods.get_mut(disp_id as usize).unwrap() = Some(function);
}
if !self.values.contains_key(&name) {
self.values.insert(name.clone(), Property::new_virtual());
}
self.values
.get_mut(&name)
.unwrap()
.install_virtual_setter(executable)
}
pub fn install_dynamic_property(
&mut self,
name: QName<'gc>,
value: Value<'gc>,
) -> Result<(), Error> {
self.values
.insert(name, Property::new_dynamic_property(value));
Ok(())
}
/// Install a slot onto the object.
///
/// Slot number zero indicates a slot ID that is unknown and should be
/// allocated by the VM - as far as I know, there is no way to discover
/// slot IDs, so we don't allocate a slot for them at all.
pub fn install_slot(&mut self, name: QName<'gc>, id: u32, value: Value<'gc>) {
if id == 0 {
self.values.insert(name, Property::new_stored(value));
} else {
self.values.insert(name, Property::new_slot(id));
if self.slots.len() < id as usize + 1 {
self.slots.resize_with(id as usize + 1, Default::default);
}
if let Some(slot) = self.slots.get_mut(id as usize) {
*slot = Slot::new(value);
}
}
}
/// Install a const onto the object.
///
/// Slot number zero indicates a slot ID that is unknown and should be
/// allocated by the VM - as far as I know, there is no way to discover
/// slot IDs, so we don't allocate a slot for them at all.
pub fn install_const(&mut self, name: QName<'gc>, id: u32, value: Value<'gc>) {
if id == 0 {
self.values.insert(name, Property::new_const(value));
} else {
self.values.insert(name, Property::new_slot(id));
if self.slots.len() < id as usize + 1 {
self.slots.resize_with(id as usize + 1, Default::default);
}
if let Some(slot) = self.slots.get_mut(id as usize) {
*slot = Slot::new_const(value);
}
}
}
/// Enumerate all interfaces implemented by this object.
pub fn interfaces(&self) -> Vec<Object<'gc>> {
self.interfaces.clone()
}
/// Set the interface list for this object.
pub fn set_interfaces(&mut self, iface_list: Vec<Object<'gc>>) {
self.interfaces = iface_list;
}
}