Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

AVM1 DefineFunction support #63

Merged
merged 44 commits into from
Oct 7, 2019
Merged

Conversation

kmeisthax
Copy link
Member

@kmeisthax kmeisthax commented Sep 18, 2019

This PR constitutes an implementation of activation objects, scope chains, and user-defined functions for ActionScript 1/2 (AVM1) code. User functions are defined as a reference to some part of the SWF they were loaded from, as well as the version of that SWF (so we can emulate version-specific behavior).

TODOs

  • Activation object stack
  • Scope chains
  • User-defined function representation
  • ActionDefineFunction impl
  • ActionCallFunction impl
  • Implicit return through off-the-end execution
  • Argument locals
  • Immutable this
  • arguments object
  • Closures
  • ActionWith impl
  • ActionCall impl (call a timeline frame as if it was a function)
  • ActionDelete / ActionDelete2 impl
  • ActionEnumerate impl
  • Current clip/timeline as local scope for timeline actions
  • Registers (global and local)
  • Register push to stack
  • ActionStoreRegister impl
  • ActionGetMember / ActionSetMember impl
  • ActionDefineFunction2 impl

@kmeisthax kmeisthax force-pushed the avm-functiondef branch 2 times, most recently from 592d74a to e9ddf44 Compare September 21, 2019 22:38
@kmeisthax
Copy link
Member Author

It turns out that registers are easy to implement and are part of the SWF5 action model anyway, so I've decided to pull DF2 into scope. Also, I need to implement GetMember / SetMember support to get my arguments tests working, so that's in scope now, too.

///
/// # Warnings
///
/// It is incorrect to call this function multiple times in the same stack.

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Shouldn't this be enforced, then? At least in development builds.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not sure what the appropriate way to do this. The best I could think of would be to set a flag in the AVM and use that to panic, but I'm not sure if that's a good idea.

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You can detect dev / test builds with if cfg! and have the flag only in those… but I don't know if that's good practice.

Alternatively, you could have a helper function / macro with a dummy implementation when !cfg(debug_assertions), which seems safer.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I implemented something that just logs an error message if this particular situation occurs for now.

@kmeisthax
Copy link
Member Author

I'm going to start prepping this PR to actually be pulled in. In preparation for that, here's an updated set of tests. The specific ones that I added for the branch are the following:

  • closure-scope - passes aside from timeline order
  • delete - passes aside from timeline order
  • timeline-function-def - passes aside from timeline order
  • variable-args - passes aside from timeline order
  • with-scope - SWF5 and SWF6/AS1 versions pass, SWF6/AS2 and all SWF7 versions fail in different ways
  • cross-clip-this - requires clip instances as variables in order to work
  • custom-clip-methods - requires clip instances as variables in order to work

There's also two tests in here for cross-movie function calls. I have not yet implemented the full AVM1 version negotiation algorithm yet, and we can't load movies from other files yet, so these tests are nowhere close to passing.

@kmeisthax
Copy link
Member Author

It fails some clippy lint that I think we're planning on removing anyway, but I think this is ready to merge now.

I had to fix tellTarget by having it construct a new scope chain when you call it. I'm a bit worried about how it interacts with things like closure scope, though. I may need to write more tests...

@kmeisthax kmeisthax changed the title [DRAFT] AVM1 DefineFunction support AVM1 DefineFunction support Oct 4, 2019
@kmeisthax kmeisthax marked this pull request as ready for review October 4, 2019 23:31
1. We no longer implicitly return Undefined unless we're specifically returning from a function (this also keeps actions from filling the stack with Undefined)
2. With scopes are now always inserted behind the current set of locals rather than overriding them
3. `ActionSubtract` now subtracts (instead of adds)
…ust hand the avm an activation object directly
…verwriting them should trigger their setters.

Define, since it's intended for setting locals only, always uses force-set and does not trigger setters.
…ate a new scope chain based off the selected active clip.
 * Remove clone calls from Copy objects
 * Used Iterator::cloned() instead of manually cloning
 * Pass swf::Function into AvmFunction2::new()
 * Use action_clone_sprite
Copy link
Member

@Herschel Herschel left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for the work and nicely commented code! Submitted some feedback for us to consider in the future.

Ideally we can add some SWF tests for this functionality (look at core/tests/avm1 for an example, a test can easily be added to the swf_tests! macro).

Action(Avm1Function<'gc>),

/// ActionScript data defined by a previous `DefineFunction2` action.
Action2(Avm1Function2<'gc>),
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is there a benefit to having a separate Avm1Function and Avm1Function2? For DefineFunction1, could we just default all the preload flags to false to get rid of some code duplication?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hmm, you might be right. DF2 is a strict superset of DF1, I think...

}
}

impl<'gc> Activation<'gc> {
Copy link
Member

@Herschel Herschel Oct 7, 2019

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is bikeshedding, but the ECMAScript spec technically calls this object an "Execution Context", of which the "Activation object" is a single part. The Execution Context contains "this + locals + scope chain", while activation object is just the "locals" part. I don't think the distinction is significant, but it might be worth putting a note in the comments.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Right. Actually, we're even a little further off from ECMAScript spec: Locals are handled by creating a blank object and sticking it on the end of the current scope chain rather than keeping them in the execution context as a separate concern. I don't believe there is any particular observable change from the viewpoint of running code, but I always have to keep that in mind when the spec is talking about scope chains.

///
/// Registers are stored in a `GcCell` so that rescopes (e.g. with) use the
/// same register set.
local_registers: Option<GcCell<'gc, Vec<Value<'gc>>>>,
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Eventually it'd be nice to use SmallVec here, since most functions will only use a few registers and it could avoid an allocation.

Comment on lines +63 to +106
pub fn new_closure_scope(
mut parent: GcCell<'gc, Self>,
mc: MutationContext<'gc, '_>,
) -> GcCell<'gc, Self> {
let mut closure_scope_list = Vec::new();

loop {
if parent.read().class != ScopeClass::With {
closure_scope_list.push(parent);
}

let grandparent = parent.read().parent;
if let Some(grandparent) = grandparent {
parent = grandparent;
} else {
break;
}
}

let mut parent_scope = None;
for scope in closure_scope_list.iter().rev() {
parent_scope = Some(GcCell::allocate(
mc,
Scope {
parent: parent_scope,
class: scope.read().class,
values: scope.read().values,
},
));
}

if let Some(parent_scope) = parent_scope {
parent_scope
} else {
GcCell::allocate(
mc,
Scope {
parent: None,
class: ScopeClass::Global,
values: GcCell::allocate(mc, Object::bare_object()),
},
)
}
}
Copy link
Member

@Herschel Herschel Oct 7, 2019

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

My gut feels like we could avoid the Vec and GcCell allocations here, since this is basically a clone of the scope chain with certain scope classes filtered out, if I understand correctly. For example, I think the Vec could be avoided by constructing the Scope objects in the initial loop and fixing up the parent of the previous one as you go along.

Maybe longer term Scope can use an iterator-like API for the iteration in resolve etc., and these just could ignore certain ScopeClasses as appropriate while iterating. Then the scope chains will only be appended to and never have to be copied?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The iterator approach seems difficult, because we might have a function defined in a with block that calls another function with another with block, etc. So it wouldn't be as simple as "resolve without with blocks", since we only want those defined in the current function.

Removing the extra allocations is an easy perf win though.

@Herschel Herschel merged commit aa9dcf5 into ruffle-rs:master Oct 7, 2019
@kmeisthax kmeisthax mentioned this pull request Oct 12, 2019
11 tasks
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants