Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
244 changes: 18 additions & 226 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# new Global
# Module global

Stage: To be presented for advancement to stage 1 at a future TC-39 plenary.
Stage: 1

Champions:

Expand All @@ -9,168 +9,17 @@ Champions:
- Richard Gibson (RGN), Agoric, @gibson042
- Mark S. Miller (MM), Agoric, @erights

## Synopsis
## Problem statement

A way to evaluate a module and its dependencies in the context of a new global scope within the same Realm

Provide a `globalThis.Global` constructor that produces a new instance of
`globalThis` with a fresh set of evaluators: `eval`, `Function`,
`AsyncFunction`, `GeneratorFunction`, and `AsyncGeneratorFunction` that effect
evaluation with the new `Global` and its associated module map.
The constructor returns the new global with all of the internal slots of
`globalThis` and configurable copies of all the property descriptors from
`globalThis` or just those specified in the array of `keys`.
Dynamic `import` within these new evaluators is bound to the new global.
Dynamic `import` of a `ModuleSource` within these evaluators
instantiates that module in the new global's module map and in the lexical
scope of the new global.

This proposal does not attempt to create a new category of global object, but
creates a mechanism for replicating the existing types such that import and
evaluation behavior can be scoped to different instances.

> This proposal picks up from the previous proposal for
> [Evaluators](https://github.com/tc39/proposal-compartments/blob/7e60fdbce66ef2d97370007afeb807192c653333/3-evaluator.md)
> from the [HardenedJS](https://hardenedjs.org) [`Compartment` proposal][proposal-compartments] and depends upon [proposal-import-hook][],
> [proposal-esm-phase-imports][], and [proposal-source-phase-imports][].

## Interfaces

```ts
interface Global {
constructor({
keys?: string[],
importHook?: ImportHook,
importMetaHook?: ImportMetaHook,
})

Global: typeof Global,
eval: typeof eval,
Function: typeof Function,
// Consequently, internal slots for
// AsyncFunction , GeneratorFunction, AsyncGeneratorFunction

// ... and properties copied from globalThis filtered by keys
}
```

<details>
<summary>proper typescript definition</summary>

```ts
interface Global<K extends keyof typeof globalThis = never> extends Pick<typeof globalThis, K> {
constructor({
keys?: K[],
importHook?: ImportHook,
importMetaHook?: ImportMetaHook,
})

Global: typeof Global,
eval: typeof eval,
Function: typeof Function
}

````

</details>

_Example_

```js
const newGlobal = new globalThis.Global({
keys: ['Buffer'],
importHook,
importMetaHook,
});

newGlobal.process = { env: process.env }
```

The `Global` constructor copies properties for `keys` (or all properties if
`keys` not specified) from the `globalThis` it originates from, except
`configurable` even if they were not.

Produces a _global_ with fresh:

- `Global` - a new `Global` constructor that will use the new _global_ for
purposes of duplicating internal slots, the property descriptors of copied
`keys`, and its `importHook`.
- `Function` and `eval` - evaluators that execute code with the _global_ as the
global scope and `importHook`,`importMetaHook` used for all imports encountered
in the evaluated code
- All other function constructors, which can be accessed through `eval` and
their corresponding, undeniable syntax, like `global.eval('async () =>
{}').constructor`.

The global does not require a fresh `ModuleSource` because
the source is paired with the global by use of dynamic `import` in global evaluation,
as in `new Global().eval('specifier => import(specifier)')(specifier)`.

### Invariants:



- By default the new global would get the same prototype as parent
- The [[Prototype]] **MUST** be settable

```js
const newGlobal = new Global();
Object.setPrototypeOf(newGlobal, Object.prototype);
```

All properties grafted by default

```js
globalThis.x = {};
const newGlobal = new globalThis.Global();
newGlobal.Object === globalThis.Object;
newGlobal.x === globalThis.x;
```

Properties can be selectively grafted

```js
globalThis.x = {};
globalThis.y = {};
const newGlobal = new Global({
keys: ["y"],
});
newGlobal.x === undefined;
newGlobal.y === globalThis.y;
newGlobal.Object === undefined;
```

Own unique evaluators, but shared prototypes

```js
const newGlobal = new Global();
newGlobal.eval !== thisGlobal.eval;
newGlobal.Global !== thisGlobal.Global;
newGlobal.Function !== thisGlobal.Function;
newGlobal.Function.prototype === thisGlobal.Function.prototype;
```

Other unique intrinsic evaluators also share prototypes

```js
const newGlobal = new Global();
newGlobal.eval("Object.getPrototypeOf(async () => {})") ===
Object.getPrototypeOf(async () => {});
newGlobal.eval("Object.getPrototypeOf(function *() {})") ===
Object.getPrototypeOf(function* () {});
newGlobal.eval("Object.getPrototypeOf(async function *() {})") ===
Object.getPrototypeOf(async function* () {});
```

Inherits host import hook and module map

```js
const newGlobal = new Global();
const fs1 = await import("node:fs");
const fs2 = await newGlobal.eval('import("node:fs")');
fs1 === fs2; // if present
```

---

## Motivation

### Domain Specific Languages
Expand All @@ -185,7 +34,7 @@ and the hazard is not limited to intrinsics that have anticipated this
problem with work-arounds like `Array.isArray` or thenable `Promise` adoption.

Some of these tools work around this problem by using the platforms existing
facility for creating a new `Global`, albeit an iframe or the Node.js `vm`
facility for creating a new global context, albeit an iframe or the Node.js `vm`
module.
Then, they are obliged to graft the intrinsics of one realm over the other,
which leaks for the cases of syntactically undeniable Realm-specific intrinsics
Expand All @@ -194,26 +43,9 @@ implementer to be vigilant to the extent that they graft every intrinsic from
one realm to another.
We have found such arrangements to be fragile and leaky. Also costly in memory efficiency and developer time.

New `Global` provide an alternate solution: evaluate modules or scripts in a
This proposal provides an alternate solution: evaluate modules or scripts in a
separate global scope with shared intrinsics.

```js
const dslGlobal = const new Global();
dslGlobal.describe = () => {}
dslGlobal.before = () => {}
dslGlobal.after = () => {};

const source = await import.source(entrypoint);
await dslGlobal.eval('s => import(s)')(source);
```

In this example, only the entrypoint module for the DSL sees additional
globals.
The `source` adopts the import hook associated with `dslGlobal` by
virtue of using the `dslGlobal`'s dynamic `import`.
Current DSLs cannot execute concurrently or depend on dynamic scope to track
the entrypoint that called each DSL verb.

### Enforcing the principle of least authority

On the web, the same origin policy has become sufficiently effective at
Expand Down Expand Up @@ -250,41 +82,18 @@ With `Global` constructor comes the ability to isolate fragments of the applicat
AI generated sources from independently working agents can come with colliding names for global variables to use and may need separate global scopes to collaborate or coexist. Similarly a misguided attempt at an inline polyfill by an AI or a package author could be prevented by freezing the parts of the new global in which the unreliable code subsequently runs.
Using a new global instead of a new Realm avoids the issues like identity discontinuity impeding the composition of software where function calls need to happen across the isolated and non-isolated code.

The isolation use case depends also on the interaction with `importHook` and `ModuleSource` as described in

https://github.com/endojs/proposal-import-hook/?tab=readme-ov-file#new-global


### Incremental or in-context execution

There are tools that currently use much more complex and costly mechanisms (similar to the ones described in [Domain Specific Languages](#domain-specific-languages) among other) to provide the ability to execute fragments of JavaScript code in a very specific context of the tool.

That includes REPLs, inline code execution results in editors (eg. [Quokka.js](https://quokkajs.com/)) and various use cases of IDEs in the browser.

Maintaining the global state between executions of user-provided code snippets would benefit from a `Global` constructor.
Maintaining the global state between executions of user-provided code snippets would benefit from the ability to control scope

## Intersection Semantics

### Shared Structs

We expect that the new global, like old globals, would have both its own module
map and also shared struct prototype registry, such that a module executed
within that global would produce its own shared struct prototypes.
This gives platforms a place to stand to ensure that separate globals do not
share any undeniable mutable state.

### Import Hook

The interaction between importHook and Global is described in the importHook proposal
https://github.com/endojs/proposal-import-hook/?tab=readme-ov-file#new-global

### Get Intrinsic

see https://github.com/tc39/proposal-get-intrinsic

A `new Global` object would need to be the source for `Reflect.getIntrinsic` to get the correct evaluators (including `%AsyncFunction%` etc.) from the internal slots and preserve the limited scope of the _global_ if `keys` were set.

`Reflect` would need to be unique own property of a new _global_
TBD

## Design Questions

Expand All @@ -305,47 +114,30 @@ while (pro = Object.getPrototypeOf(pro)) {
[object WindowProperties]
[object EventTarget]
[object Object]
null
```
```
// web extension contentscript
[object Window]
[object WindowProperties]
null
```
```
// Node.js
[object Object]
[object Object]
null
```
```
// Deno
[object Window]
[object EventTarget]
[object Object]
null
```
```
// Hermes
[object Object]
undefined
```


### Backward compatibility and the `constructor` field on a global

`globalThis` already has a constructor in the browser and that constructor is
`Window`, an _Illegal constructor_ as one can inform themselves by attempting
to invoke it.

```js
globalThis.constructor === Window;
const g1 = new Global();

// Which of the following should be true?

g1.globalThis.constructor === Global; // Gets in the way of the web standards potentially
g1.globalThis.constructor === g1.globalThis.Global; // definitely not
g1.globalThis.constructor === g1.globalThis.Window; // maybe?
g1.globalThis.Window === Global; // Would Window no longer be an Illegal constructor?
```


[proposal-source-phase-imports]: https://github.com/tc39/proposal-source-phase-imports
[proposal-esm-phase-imports]: https://github.com/tc39/proposal-esm-phase-imports
[proposal-compartments]: https://github.com/tc39/proposal-compartments
[proposal-import-hook]: https://github.com/endojs/proposal-import-hook

````
Loading