ruffle/core/src/avm2/scope.rs

241 lines
7.7 KiB
Rust
Raw Normal View History

2020-02-10 19:54:55 +00:00
//! Represents AVM2 scope chain resolution.
use crate::avm2::names::QName;
use crate::avm2::object::{Object, TObject};
use crate::avm2::return_value::ReturnValue;
use crate::avm2::script_object::ScriptObject;
use crate::avm2::value::Value;
use crate::avm2::{Avm2, Error};
use crate::context::UpdateContext;
use enumset::EnumSet;
use gc_arena::{Collect, GcCell, MutationContext};
use std::cell::Ref;
/// Indicates what kind of scope a scope is.
#[derive(Copy, Clone, Debug, PartialEq, Collect)]
#[collect(no_drop)]
pub enum ScopeClass {
/// Scope represents global or closure scope.
GlobalOrClosure,
/// Scope represents an object added to the scope chain with `with`.
/// It is not inherited when closures are defined. Furthermore, a `with`
/// scope gains the ability to be searched for dynamic properties.
With,
}
/// Represents a scope chain for an AVM2 activation.
#[derive(Debug, Collect)]
#[collect(no_drop)]
pub struct Scope<'gc> {
parent: Option<GcCell<'gc, Scope<'gc>>>,
class: ScopeClass,
values: Object<'gc>,
}
impl<'gc> Scope<'gc> {
/// Push a scope onto the stack, producing a new scope chain that's one
/// item longer.
pub fn push_scope(
scope_stack: Option<GcCell<'gc, Scope<'gc>>>,
object: Object<'gc>,
mc: MutationContext<'gc, '_>,
) -> GcCell<'gc, Self> {
GcCell::allocate(
mc,
Self {
parent: scope_stack,
class: ScopeClass::GlobalOrClosure,
values: object,
},
)
}
/// Construct a with scope to be used as the scope during a with block.
///
/// A with block adds an object to the top of the scope chain, so unqualified
/// references will try to resolve on that object first.
pub fn push_with(
scope_stack: Option<GcCell<'gc, Scope<'gc>>>,
with_object: Object<'gc>,
mc: MutationContext<'gc, '_>,
) -> GcCell<'gc, Self> {
GcCell::allocate(
mc,
Scope {
parent: scope_stack,
class: ScopeClass::With,
values: with_object,
},
)
}
/// Construct a closure scope to be used as the scope stack when invoking a
/// function.
///
/// This function filters With scopes from the scope chain. If all scopes
/// are filtered, this function returns None, representing an empty scope
/// stack.
pub fn new_closure_scope(
mut parent: GcCell<'gc, Self>,
mc: MutationContext<'gc, '_>,
) -> Option<GcCell<'gc, Self>> {
let mut bottom_scope = None;
let mut top_scope: Option<GcCell<'gc, Self>> = None;
loop {
if parent.read().class != ScopeClass::With {
let next_scope = GcCell::allocate(
mc,
Self {
parent: None,
class: parent.read().class,
values: parent.read().values,
},
);
if bottom_scope.is_none() {
bottom_scope = Some(next_scope);
}
if let Some(ref scope) = top_scope {
scope.write(mc).parent = Some(next_scope);
}
top_scope = Some(next_scope);
}
let grandparent = parent.read().parent;
if let Some(grandparent) = grandparent {
parent = grandparent;
} else {
break;
}
}
bottom_scope
}
/// Construct an arbitrary scope
pub fn new(
parent: GcCell<'gc, Self>,
class: ScopeClass,
with_object: Object<'gc>,
) -> Scope<'gc> {
Scope {
parent: Some(parent),
class,
values: with_object,
}
}
/// Returns a reference to the current local scope object.
pub fn locals(&self) -> &Object<'gc> {
&self.values
}
/// Returns a reference to the current local scope object for mutation.
#[allow(dead_code)]
pub fn locals_mut(&mut self) -> &mut Object<'gc> {
&mut self.values
}
/// Returns a reference to the parent scope object.
pub fn parent(&self) -> Option<Ref<Scope<'gc>>> {
match self.parent {
Some(ref p) => Some(p.read()),
None => None,
}
}
/// Resolve a particular value in the scope chain.
///
/// Because scopes are object chains, the same rules for `Object::get`
/// still apply here. This function is allowed to yield `None` to indicate
/// that the result will be calculated on the AVM stack.
pub fn resolve(
&self,
name: &QName,
avm: &mut Avm2<'gc>,
context: &mut UpdateContext<'_, 'gc, '_>,
this: Object<'gc>,
) -> Result<ReturnValue<'gc>, Error> {
if self.locals().has_property(name) {
return self.locals().get_property(name, avm, context);
}
if let Some(scope) = self.parent() {
return scope.resolve(name, avm, context, this);
}
//TODO: Should undefined variables halt execution?
Ok(Value::Undefined.into())
}
/// Check if a particular property in the scope chain is defined.
pub fn is_defined(&self, name: &QName) -> bool {
if self.locals().has_property(name) {
return true;
}
if let Some(scope) = self.parent() {
return scope.is_defined(name);
}
false
}
/// Update a particular value in the scope chain.
///
/// Traverses the scope chain in search of a value. If it's found, it's overwritten.
/// The traversal stops at Target scopes, which represents the movie clip timeline
/// the code is executing in.
/// If the value is not found, it is defined on this Target scope.
pub fn set(
&self,
name: &QName,
value: Value<'gc>,
avm: &mut Avm2<'gc>,
context: &mut UpdateContext<'_, 'gc, '_>,
this: Object<'gc>,
) -> Result<(), Error> {
if self.locals().has_property(name) && self.locals().is_property_overwritable(name) {
// Value found on this object, so overwrite it.
// Or we've hit the executing movie clip, so create it here.
self.locals().set_property(name, value, avm, context)
} else if let Some(scope) = self.parent() {
// Traverse the scope chain in search of the value.
scope.set(name, value, avm, context, this)
} else {
// This probably shouldn't happen -- all AVM2 code runs in reference to some movieclip,
// so we should always have a movieclip scope.
// Define on the top-level scope.
debug_assert!(false, "Scope::set: No top-level movie clip scope");
self.locals().set_property(name, value, avm, context)
}
}
/// Set a particular value in the locals for this scope.
///
/// By convention, the locals for a given function are always defined as
/// stored (e.g. not virtual) properties on the lowest object in the scope
/// chain. As a result, this function always force sets a property on the
/// local object and does not traverse the scope chain.
pub fn define(&self, name: &QName, value: impl Into<Value<'gc>>, mc: MutationContext<'gc, '_>) {
self.locals()
.define_value(mc, name, value.into(), EnumSet::empty());
}
/// Delete a value from scope
pub fn delete(&self, name: &QName, mc: MutationContext<'gc, '_>) -> bool {
if self.locals().has_property(name) {
return self.locals().delete(mc, name);
}
if let Some(scope) = self.parent() {
return scope.delete(name, mc);
}
false
}
}