This also incurred a large number of ancillary changes, as it turns out nearly every native object is currently pulling a prototype and sticking it into an object. Right now, I have it instead pulling the constructor out of the prototype, but a future PR will also remove `system_prototypes` as well.
Other ancillary changes include:
* `Domain` now supports partial initialization to avoid an order-of-events issue. Accessing domain memory on a partially-initialized `Domain` will panic.
* `Domain` construction requires a full `activation` now, except for `global_scope` which needs to be initialized later with valid domain memory before user code runs.
* Pretty much every native object constructor now takes a proto/constr pair
* Trait lookup was rewritten to handle this. It's still buggy - seven tests don't work
* `TObject.construct` now actually does the full object construction dance. This allows `ClassObject` to implement the ES4 object construction pathway directly while `FunctionObject` maintains ES3 compatibility.
This is a tentative commit; there are still seven failing tests that I need to fix.
This entirely abolishes the "global scope object" in AVM2. I even had to redefine several global object functions to work with the bottom of the scope stack, which seems to be where ASC likes to stick the script scope.
This is the same way that AVM1 actions run and it appears that frame scripts work exactly the same way. It fixes all outstanding bugs with movie clip navigation in AVM2 and allows me to remove a lot of weird workarounds I was writing for the old, incorrect behavior.
I'm also removing the "last run script frame" rule as `run_frame_internal` already had rules to prevent stopped clips from rerunning actions.
This requires the use of an intermediary enum called `AvmObject` which can hold either object representation. Currently, it's mostly just being unwrapped as AVM1 objects, which we will need to fix.
This has some particularly annoying consequences for initialization order: notably, we can't actually create any ES4 classes using the standard machinery until after the three objects I just mentioned get created. Ergo, we have to create them through lower-level means, handing prototypes around, and then initialize AVM2's system prototypes list for it.
When we start adding more system prototypes, we'll also have to fill the extras with blank objects and then slot them in as we create them.
This was surprisingly tricky - due to the need to look up superclasses, class trait instantiation requires an active `Activation` and `UpdateContext`. We can't get those during VM instance creation, since the player needs the VM first before it can give it a context to work with. Ergo, we have to tear the global scope initialization in two. At the first possible moment, the player calls a new `load_player_globals` method that initializes all class traits in global scope.
Namespaces as values adds a bunch of extra special cases to the coercion and equality rules that don't really belong there. Namespace itself just returns it's URI as a string, so we can just make `NamespaceObject` do that and then treat it the same way we treat boxed primitives.
This also results in a far reduced role for `ReturnValue`, since I also took the liberty of removing most of it's use. Furthermore, I also made it apply equally to native and AVM2 code, which ensures all native implementations of methods don't double-borrow.
In AVM1, `ReturnValue` was actually removed entirely, because it's not needed. I attempted to do the same, but the fact that we're currently embedding `ScriptObjectData` in native objects means that we need it for virtual properties. Otherwise, virtual property implementations will see locked objects, which is bad.
The intention is to completely replace all usage of `Avm2XYZEntry` with `Class`, `Trait`, and `Method`. This will allow runtime-provided global class traits to coexist with those provided by user code.
Inspired by Dinnerbone's PR doing the exact same thing to AVM1.
On AVM2 we have a bit of a subtle issue: the base implementation of `set_property_local` and `init_property_local` *must* return `ReturnValue`s to avoid double-borrows. Each implementation of `TObject` must resolve them before returning.
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.
Our already odd `super` handling throws up another subtlety regarding bound recievers. Since we have to construct an instance of a parent class in order to get traits on it, we also have to make sure that we initialize traits with the correct reciever. I'll demonstrate here:
```let mut base = base_proto.construct(avm, context, &[])?;
let name = base.resolve_multiname(&multiname).unwrap();
let value = base.get_property(object, &name, avm, context)?.resolve(avm, context)?```
In this case, if `name` is the name of a method, getter, or setter trait, then `get_property` will instantiate that trait on `base` but bound to `reciever`. This is correct behavior for this case, but more generally, trait instantiation is permenant and therefore there's potential for confusing shenanigans if you `get_property` with the wrong reciever.
To be very clear, `reciever` should *always* be the same object that is getting `get_property` et. all called on it. In the event that you need to instantiate traits with a different `reciever`, you should construct a one-off object and retrieve prototypes from that.
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.
Normally, `set_property` only affects the object it was called on, which makes sense: otherwise, we couldn't override values that originate from a class prototype without accidentally monkey-patching the prototype. However, virtual setters only exist in prototypes and need to be accessible from child objects.
The solution to this is to have a specific method to check if a virtual setter exists. Virtual setters are then resolved through the prototype chain. If no virtual setter exists, then the reciever object is handed the value.
Note that we always use the `reciever` object rather than `self` so that `setsuper` can work correctly. In `setsuper`, we resolve the base class, and then set properties on it with the actual object in question as it's reciever. If a virtual setter is called, it will get the actual object it should be manipulating; and otherwise, prototypes will not be modified or consulted.
This required the reintroduction of dedicated reciever parameters to `Object.get_property_local` and `Object.set_property`, which I had removed from the AVM1 code I copied it from. It turns out being able to change the reciever was actually necessary in order to make super set/get work.
The previous system primarily relied on `Executable` to automatically start and continue a super chain. This works, but only for class hierarchies without *override gaps* - methods that override another method not defined by the direct superclass of the method. In that case, the override method would be called twice as the `base_class` was moved up one prototype at a time, which is wrong.
The new system relies on the call site to accurately report the prototype from which the current method was retrieved from. Super calls then start the resolution process *from the superclass of this prototype*, to ensure that the already-called method is skipped.
It should be noted that the proper `base_class` for things like `callmethod`, `callstatic`, `call`, `get`/`set` methods, and other call opcodes that don't use property look-up are best-effort guesses that may need to be amended later with better tests.
To facilitate `base_proto` resolution, a new `Object` method has been added. It's similar to `get_property`, but instead returns the closest prototype that can resolve the given `QName`, rather than the actual property's `ReturnValue`. Call operations use this to resolve the `base_proto`, and then resolve the method being called in `base_proto`. The existing `exec_super` method was removed and a `base_proto` method added to `exec` and `call`.
This works primarily by retaining the current superclass prototype in the activation object and then using it to retrieve the super method.
For constructors, we implement the `constructor` property, which is probably not the correct way to do this.
This currently treats `coerce_a` as a no-op. Strictly speaking, this is for type verification purposes, but we currently don't type-verify ABC code. Ergo, this requires no VM support at this time.
Also, implement a method table that method traits can optionally add themselves to.
Also also, add the ability to invoke a method without a `this` object. This required a non-trivial refactoring of the activation machinery, and changes to the signature of `NativeFunction`, and all native AVM2 functions.
In the future, the `unwrap_stack_frame` mechanism should be expanded upon to allow running exception handlers and recovering from a Rust error - but not today.
Notably, this also removes `new_closure_scope` as it is not needed. AVM1 does not capture `with` scopes in closures, but AVM2 (as well as modern ECMAScript) does.
All constant pools in an ABC file are actually numbered starting from one; there's an implicit 0 entry not stored in the file that the runtime is expected to retrieve when pulling constants from the pool.
The AVM2/ABC spec only mentions this in passing.
This allows the AVM to declare classes, which necessitated some refactoring to avoid double-borrows or having to do something "magic" that would dodge virtual properties.