-
Notifications
You must be signed in to change notification settings - Fork 903
Description
I'm creating this issue to track the things I know of which need to be tackled to separate objects and scopes in our implementation.
Separating scopes and objects
Future performance optimisations become significantly easier when we can separate the concepts of scopes and objects. Specifically it makes traversal through parents one dimensional rather than two dimensional as scopes do not have prototypes and objects to not have parents (at least not in way that should ever be important when resolving a property).
To separate the concepts of scopes and objects in Rhino without disturbing too many APIs it would be useful to preserve a few things.
- We would like to keep the existing APIs on
Scriptablewhich define property access in terms likeObject get(String name, Scriptable start). Crucially thestartargument when looking up a property on an object must always be an object. - We would like our new scopes to offer a similar API like this:
Object get(String name, Scope start). Again, thestartargument when looking up something on a scope should always be a scope. - We would like to be able to use the same underlying property / slot map implementation. That means that either this implementation must duplicate methods to provide tow versions of each method, and this must also happen on various slot definitions, or we need to do something with parameterised types in order to preserve our guarantees.
- Pretty much any solution that satisfies points 1. to 3. prevents us from having objects that are both scopes and objects.
My proposed way to handle this is with the following type hierarchy:
SuperType<T extends SuperType<T>>(SuperTypeis chosen as a deliberately bad name here, we can discuss what it should actually be called at a later date).Scriptable extends SuperType<Scriptable>Scope extends SuperType<Scope>
This allows a large number of uses of property access and definition to remain unchanged, especially in user code, and requires simple modification of our slot map code. It also prevents us form accidentally (or purposefully) passing a scope as an object or an object as a scope, but all of this is at the price of not having objects which are both scopes and objects.
Since Scopes only ever have name bindings we could theoretically make the super type handle those, but we might reasonably use symbol properties for internal data, and the interface is simpler if the two halves of the hierarchy are the same.
There are other small differences between the two halves of the hierarchy. Scopes have parents, while objects have prototypes, but a parent is of a scope is always a scope, and a prototype of an object is always an object, so they are really a common operation that can be written as T getAncestor().
So, what problems do we need to resolve to make this separation achievable?
TopLevel is both a scope and an object (#2164)
Because TopLevel fulfils all the roles mentioned earlier, it is currently a scope and an object and we need to separate those two parts (as the spec does). TopLevel will not present any significant optimisation issue because, although it is a scope it never has a parent and can effectively delegate to the prototype chain of the global object. We have also allow initialisation of a context on any ScriptableObject, but this requires checks in several places to check if the scope is a TopLevel, and leads to subtle bugs with prototypes which should only be defined as intrinsics and are not exposed on the global object. Maybe we should shift to insisting the scope is a TopLevel but allowing for a custom global object.
with scopes
We use with scopes in a few places, often mostly unnecessarily. They are a scope which holds an object as its prototype and delegates to that object. They present a significant optimisation barrier as lookup must be done on the object's prototype chain, and then on the parent scope chain. Hence, although they should be supported (even though they are deprecated) we should endeavour to refactor way from their use wherever possible.
__parent__ property
__parent__ is no longer in the specification, and for good reason as it captures and gives direct access to scope objects in ways that allow for bad resource leaks and can break many implementation assumptions. However a small amount of code in the wild may still this feature, and to support that we should be able to wrap a scope in an object that is effectively the reverse of with. This object should likely be intentionally limited to prevent any attempt to manipulate the prototype chain or define accessor properties. I have not found any code that relies on mutating __parent__ after an object has been created, and we should likely not support that.
Custom module systems
Any custom module system which has a name space concept will likely either need to make those scopes with scopes, or must do something similar to __parent__. The former approach would likely present larger optimisation issues.
Avoid setting of parent scope on objects
We should be able to avoid setting parent scope on almost all objects. The exception is function objects whose parent scope is their declaration scope, and we have already separated that concept at the interface level. We can start to enforce that parent scope is not set unless __parent__ is required.