Skip to content

Separation of TopLevel and globalThis #2164

@aardvark179

Description

@aardvark179

This is one of the tasks outline in #2163

The EcmaScript spec makes a firm distinction between Objects and environment records (which are what the spec calls scopes), but there are two places where these concepts collide in subtle ways. To explain that let's start by looking at what operations the spec defines on environment records:

Environment methods

The spec
defines
a number of methods on environment records:

  • HasBinding
  • CreateMutableBinding
  • CreateImmutableBinding
  • InitializeBinding
  • SetMutableBinding
  • GetBindingValue
  • DeleteBinding
  • HasThisBinding
  • HasSuperBinding
  • WithBaseObject

The definition of var, and let and const, are then specified in terms of these operations. Speciifically var uses SetMutableBinding if the binding does not already exists, while let and const use the CreateMutableBinding and CreateImmutableBinding operations.

There is also a hierarchy of environment record types:

  • Environment Record
    • Declaration Environment Record
      • Function Environment Record
      • Module Environment Record
    • Object Environment Record
    • Global Environment Record

We need to talk about a couple of these to make sense of things later on.

Declaration Environment Record

This is pretty much what we think of when we think about a scope. It binds names (which are always strings) to values along with information about whether those bindings are mutable, initialised, and strict.

The Object Environment Record

The Object Environment Record is used in with expressions where the properties of an object are exposed as a scope, but it is also used as part of the Global Environment Record. There is one extremely important thing to note about it however, and that is the definition of CreateImmutableBinding

The CreateImmutableBinding concrete method of an Object Environment Record is never used within this specification.

So, from that statement we can tell that we will never have a statement like

const foo = 'bar'

which creates a binding on an Object Environment Record. So something interesting is happening in with statements. If you do

let o = {foo: 'bar'}
with (o) {
    const foo = 'baz';
    console.log(foo);
}
console.log(o);

Will run, and log 'baz' and 'bar'. The block statement introduces a new Declaration Environment Record as the current execution environment, and so const foo = 'bar' is never executes against the Object Environment Record, and it isn't possible to have that const declaration without introducing that new scope.

Global Environment Record

Now we know about Declaration and Object environment records we can talk about Global Environment Records. They are defined as a composition of a Declaration Environment Record, a global object, and an Object Environment Record. Why a global object and a separate Object Environment Record? Well, the specification allows for the global object, and globalThis to be different objects. I'm unsure of the original rationale for allowing that difference, but I can see good reasons for it to support historical behaviour, ensure security, or make performance guarantees.

The operations on the Global Environment Record are generally defined by checking for bindings on the declaration have and then delegating to the object half, except for the CreateMutableBinding and CreateImmutableB which never delegate to the Object Environment Record. This means that, assuming the global object and globalThis are the same then:

var a = 'Hello!';
console.log(Object.getOwnPropertyDescriptor(this, 'a'));

should give us

{ value: 'Hello!', writable: true, enumerable: true, configurable: false } 

while

let a = 'Hello!';
console.log(Object.getOwnPropertyDescriptor(this, 'a'));

will log undefined.

Realms

The Global Environment Record doesn't define everything that's at the top of an EcmaScript execution context. A Realm Record holds a Global Environment Record along with the set of intrinsics (such as %Object.prototype% and some other information.

TopLevel in Rhino

TopLevel currently serves multiple roles. It is:

  1. The realm as it is the place where we hold intrinsic definitions
  2. The Declaration Environment Record half of the Global Environment Record as it holds any global const definitions.
  3. The Object Environment Record half of the Global Environment Record as it hold all the var and function definitions that are not created via the create binding methods.
  4. It is globalThis and the this passed to functions under various circumstances

Decomposing TopLevel into the scope and the globalThis part allows us to later make TopLevel scope, while keeping globalThis as an object.

It would also allow us to make context initialisation always take happen on a TopLevel while still supporting a custom global object as the globalThis object. This would allow use to always put intrinsics on the TopLevel and have more reliable behaviour than we currently do for generator functions and similar which only have intrinsics.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions