Skip to content
Draft
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
38 changes: 38 additions & 0 deletions INTERPRETING.md
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,27 @@ properties of the global scope prior to test execution.
tests (via the `async` flag, described below).
- **`$262`** An ordinary object with the following properties:
- **`AbstractModuleSource`** - a reference to the `%AbstractModuleSource%` constructor which does not appear as a property of the global object.
- **`createModuleSource`** - a function which accepts two arguments and
returns a new
[ModuleSource](https://tc39.es/proposal-source-phase-imports/#sec-module-source-objects)
object with a valid `[[ModuleSourceRecord]]` internal slot:

$262.createModuleSource(sourceText, id)

- `sourceText` -- a string of ECMAScript module source text.
- `id` -- a string used as the module's registry key. Two calls with
the same `id` produce ModuleSource objects that share the same
underlying Module Record identity; two calls with different `id`
values produce distinct Module Records. The host must resolve any
module specifiers in `sourceText` against this `id`. The `id`
becomes the `import.meta.moduleId` for that module (see
`importMetaHook`).

The returned object has `%ModuleSource.prototype%` as its prototype
(which inherits from `%AbstractModuleSource.prototype%`).

**Tests using this function are guarded with the
`esm-phase-imports` feature flag.**
- **`createRealm`** - a function which creates a new [ECMAScript
Realm](https://tc39.github.io/ecma262/#sec-code-realms),
defines this API on the new realm's global object, and returns the `$262`
Expand All @@ -70,6 +91,23 @@ properties of the global scope prior to test execution.

- **`gc`** - a function that wraps the host's garbage collection invocation mechanism, if such a capability exists. Must throw an exception if no capability exists. This is necessary for testing the semantics of any feature that relies on garbage collection, e.g. the `WeakRef` API.
- **`global`** - a reference to the global object on which `$262` was initially defined
- **`importMetaHook`** - the host's implementation of
[HostGetImportMetaProperties](https://tc39.github.io/ecma262/#sec-hostgetimportmetaproperties)
must set the following properties on every `import.meta` object:

- `moduleId` -- a string. The host's registry key for the module.
For modules created via `$262.createModuleSource(sourceText, id)`,
this must be the `id` that was passed. For file-based modules, the
value is implementation-defined. In all cases, two modules that
share the same Module Record must have the same `moduleId`, and
two modules backed by different Module Records must have different
`moduleId` values.

This contract allows tests to verify module identity without depending
on host-specific URL schemes.

**Tests relying on this hook are guarded with the
`esm-phase-imports` feature flag.**
- **`IsHTMLDDA`** - (present only in implementations that can provide it) an
object that:

Expand Down
4 changes: 4 additions & 0 deletions features.txt
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,10 @@ source-phase-imports
## test262 special specifier
source-phase-imports-module-source

# ESM Phase Imports
## https://github.com/tc39/proposal-esm-phase-imports
esm-phase-imports

# Atomics.pause
# https://github.com/tc39/proposal-atomics-microwait
Atomics.pause
Expand Down
35 changes: 35 additions & 0 deletions test/built-ins/ModuleSource/constructor-throws.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
// Copyright (C) 2025 Ecma International. All rights reserved.
// This code is governed by the BSD license found in the LICENSE file.
/*---
esid: sec-modulesource
description: >
ModuleSource() throws a TypeError.
info: |
ModuleSource ( )

1. Throw a *TypeError* exception.

The ModuleSource constructor is not intended to be called directly.
It always throws a TypeError.
features: [source-phase-imports, esm-phase-imports]
flags: [module]
---*/

assert.sameValue(typeof $262.createModuleSource, 'function');

var baseId = import.meta.moduleId.slice(0, import.meta.moduleId.lastIndexOf('/') + 1);

// Get a reference to the ModuleSource constructor via the prototype chain.
var src = $262.createModuleSource('export var x = 1;', baseId + 'ctor-throws');
var ModuleSource = Object.getPrototypeOf(src).constructor;

assert.sameValue(typeof ModuleSource, 'function',
'ModuleSource should be a function');

assert.throws(TypeError, function () {
ModuleSource();
}, 'Calling ModuleSource() without new should throw TypeError');

assert.throws(TypeError, function () {
new ModuleSource();
}, 'Calling new ModuleSource() should throw TypeError');
24 changes: 24 additions & 0 deletions test/built-ins/ModuleSource/identity/distinct-sources.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
// Copyright (C) 2025 Ecma International. All rights reserved.
// This code is governed by the BSD license found in the LICENSE file.
/*---
esid: sec-module-source-objects
description: >
Two ModuleSource objects created from the same source text are not identical.
info: |
Each call to $262.createModuleSource with a different id returns a new,
distinct ModuleSource object backed by a different Module Record.
features: [source-phase-imports, esm-phase-imports]
flags: [module]
---*/

assert.sameValue(typeof $262.createModuleSource, 'function');

var baseId = import.meta.moduleId.slice(0, import.meta.moduleId.lastIndexOf('/') + 1);

var src1 = $262.createModuleSource('export var x = 1;', baseId + 'distinct-a');
var src2 = $262.createModuleSource('export var x = 1;', baseId + 'distinct-b');

assert.notSameValue(src1, src2,
'Two ModuleSource objects created with different ids must not be ===');
assert.sameValue(typeof src1, 'object');
assert.sameValue(typeof src2, 'object');
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
// Copyright (C) 2025 Ecma International. All rights reserved.
// This code is governed by the BSD license found in the LICENSE file.
/*---
esid: sec-module-source-objects
description: >
A ModuleSource created in another realm has that realm's prototype chain,
not the current realm's. instanceof checks reflect this difference.
info: |
Each realm has its own %ModuleSource.prototype% and
%AbstractModuleSource.prototype% intrinsics. A ModuleSource object created
in realm A has realm A's %ModuleSource.prototype% as its [[Prototype]].
When accessed from realm B, the object's prototype chain still belongs to
realm A.

This is the standard cross-realm prototype behavior for all built-in
objects in ECMAScript. The instanceof operator checks the prototype chain
against the constructor's .prototype from the realm where instanceof is
evaluated, so a cross-realm ModuleSource is not an instanceof the current
realm's ModuleSource or AbstractModuleSource constructors.
features: [source-phase-imports, esm-phase-imports, cross-realm]
flags: [module]
---*/

assert.sameValue(typeof $262.createModuleSource, 'function');
assert.sameValue(typeof $262.AbstractModuleSource, 'function');

var baseId = import.meta.moduleId.slice(0, import.meta.moduleId.lastIndexOf('/') + 1);

var otherRealm = $262.createRealm();
var other$262 = otherRealm.global.$262;

// Create a ModuleSource in the other realm.
var crossRealmSrc = other$262.createModuleSource('export var x = 1;', baseId + 'cross-realm-proto');

// Create a ModuleSource in the current realm for comparison.
var localSrc = $262.createModuleSource('export var x = 2;', baseId + 'local-proto');

// The cross-realm source's prototype chain belongs to the other realm.
var crossProto = Object.getPrototypeOf(crossRealmSrc);
var localProto = Object.getPrototypeOf(localSrc);

// Each realm has distinct %ModuleSource.prototype% objects.
assert.notSameValue(crossProto, localProto,
'Cross-realm ModuleSource should have a different [[Prototype]] than local ModuleSource');

// The cross-realm source's grandparent is the other realm's %AbstractModuleSource.prototype%.
var crossGrandProto = Object.getPrototypeOf(crossProto);
var localGrandProto = Object.getPrototypeOf(localProto);

assert.notSameValue(crossGrandProto, localGrandProto,
'Cross-realm AbstractModuleSource.prototype should differ from local AbstractModuleSource.prototype');

// The cross-realm source is an instanceof the other realm's constructors.
assert(crossRealmSrc instanceof other$262.AbstractModuleSource,
'Cross-realm source should be instanceof other realm AbstractModuleSource');

// The cross-realm source is NOT an instanceof the current realm's constructors
// (standard cross-realm prototype semantics).
assert(!(crossRealmSrc instanceof $262.AbstractModuleSource),
'Cross-realm source should NOT be instanceof current realm AbstractModuleSource');

// The local source is an instanceof the current realm's constructors.
assert(localSrc instanceof $262.AbstractModuleSource,
'Local source should be instanceof current realm AbstractModuleSource');

// The local source is NOT an instanceof the other realm's constructors.
assert(!(localSrc instanceof other$262.AbstractModuleSource),
'Local source should NOT be instanceof other realm AbstractModuleSource');
38 changes: 38 additions & 0 deletions test/built-ins/ModuleSource/identity/prototype-chain.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
// Copyright (C) 2025 Ecma International. All rights reserved.
// This code is governed by the BSD license found in the LICENSE file.
/*---
esid: sec-module-source-objects
description: >
ModuleSource instances have the correct prototype chain.
info: |
A ModuleSource object has %ModuleSource.prototype% as its [[Prototype]],
which in turn has %AbstractModuleSource.prototype% as its [[Prototype]].
features: [source-phase-imports, esm-phase-imports]
flags: [module]
---*/

assert.sameValue(typeof $262.createModuleSource, 'function');
assert.sameValue(typeof $262.AbstractModuleSource, 'function');

var baseId = import.meta.moduleId.slice(0, import.meta.moduleId.lastIndexOf('/') + 1);

var src = $262.createModuleSource('export var x = 1;', baseId + 'proto-chain');

var srcProto = Object.getPrototypeOf(src);
var abstractProto = $262.AbstractModuleSource.prototype;

// ModuleSource instance's [[Prototype]] is %ModuleSource.prototype%
assert.notSameValue(srcProto, abstractProto,
'ModuleSource.prototype should be distinct from AbstractModuleSource.prototype');

// %ModuleSource.prototype%'s [[Prototype]] is %AbstractModuleSource.prototype%
assert.sameValue(Object.getPrototypeOf(srcProto), abstractProto,
'ModuleSource.prototype.[[Prototype]] should be AbstractModuleSource.prototype');

// %AbstractModuleSource.prototype%'s [[Prototype]] is %Object.prototype%
assert.sameValue(Object.getPrototypeOf(abstractProto), Object.prototype,
'AbstractModuleSource.prototype.[[Prototype]] should be Object.prototype');

// The instance is an instanceof AbstractModuleSource
assert(src instanceof $262.AbstractModuleSource,
'ModuleSource instance should be instanceof AbstractModuleSource');
57 changes: 57 additions & 0 deletions test/built-ins/ModuleSource/identity/tostringtag-cross-realm.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
// Copyright (C) 2025 Ecma International. All rights reserved.
// This code is governed by the BSD license found in the LICENSE file.
/*---
esid: sec-get-%abstractmodulesource%25.prototype.@@tostringtag
description: >
The @@toStringTag getter works on cross-realm ModuleSource objects because
it uses a brand check ([[ModuleSourceRecord]] internal slot), not a
prototype check.
info: |
get %AbstractModuleSource%.prototype [ %Symbol.toStringTag% ]

1. Let O be the this value.
2. If O is not an Object, return undefined.
3. Let module be GetModuleSourceRecord(O).
4. If module is ~not-a-source~, return undefined.
5. Let name be module.[[ModuleSourceKind]].
6. Assert: name is a String.
7. Return name.

GetModuleSourceRecord checks for the [[ModuleSourceRecord]] internal slot,
which is an object brand — it works regardless of which realm created the
object. A ModuleSource from another realm still has the slot, so the getter
returns "ModuleSource".
features: [source-phase-imports, esm-phase-imports, cross-realm]
flags: [module]
---*/

assert.sameValue(typeof $262.createModuleSource, 'function');
assert.sameValue(typeof $262.AbstractModuleSource, 'function');

var baseId = import.meta.moduleId.slice(0, import.meta.moduleId.lastIndexOf('/') + 1);

var otherRealm = $262.createRealm();
var otherCreateModuleSource = otherRealm.global.$262.createModuleSource;

// Create a ModuleSource in the other realm.
var crossRealmSrc = otherCreateModuleSource('export var x = 1;', baseId + 'cross-realm-tag');

// The @@toStringTag getter from the current realm should still work on
// a cross-realm ModuleSource, because it uses the [[ModuleSourceRecord]]
// internal slot (brand check), not a prototype chain check.
var getter = Object.getOwnPropertyDescriptor(
$262.AbstractModuleSource.prototype, Symbol.toStringTag
).get;

assert.sameValue(getter.call(crossRealmSrc), 'ModuleSource',
'Current realm @@toStringTag getter should return "ModuleSource" for a cross-realm ModuleSource');

// Direct property access uses the other realm's prototype chain, so
// crossRealmSrc[Symbol.toStringTag] resolves through the other realm's
// AbstractModuleSource.prototype getter — which also works.
assert.sameValue(crossRealmSrc[Symbol.toStringTag], 'ModuleSource',
'Direct @@toStringTag access on a cross-realm ModuleSource should return "ModuleSource"');

// Object.prototype.toString also works cross-realm.
assert.sameValue(Object.prototype.toString.call(crossRealmSrc), '[object ModuleSource]',
'Object.prototype.toString should return "[object ModuleSource]" for a cross-realm ModuleSource');
36 changes: 36 additions & 0 deletions test/built-ins/ModuleSource/identity/tostringtag-descriptor.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
// Copyright (C) 2025 Ecma International. All rights reserved.
// This code is governed by the BSD license found in the LICENSE file.
/*---
esid: sec-get-%abstractmodulesource%25.prototype.@@tostringtag
description: >
The @@toStringTag property on %AbstractModuleSource%.prototype has the
correct property descriptor: accessor with set=undefined,
enumerable=false, configurable=true; the getter's name is
"get [Symbol.toStringTag]".
info: |
get %AbstractModuleSource%.prototype [ %Symbol.toStringTag% ]

This property has the attributes { [[Enumerable]]: false, [[Configurable]]: true }.
The initial value of the "name" property of this function is "get [Symbol.toStringTag]".
features: [source-phase-imports, esm-phase-imports]
flags: [module]
---*/

assert.sameValue(typeof $262.AbstractModuleSource, 'function');

var desc = Object.getOwnPropertyDescriptor(
$262.AbstractModuleSource.prototype, Symbol.toStringTag
);

assert.sameValue(typeof desc.get, 'function',
'@@toStringTag should be an accessor property with a get function');
assert.sameValue(desc.set, undefined,
'@@toStringTag set accessor should be undefined');
assert.sameValue(desc.enumerable, false,
'@@toStringTag should not be enumerable');
assert.sameValue(desc.configurable, true,
'@@toStringTag should be configurable');

// The getter's name property.
assert.sameValue(desc.get.name, 'get [Symbol.toStringTag]',
'The getter name should be "get [Symbol.toStringTag]"');
46 changes: 46 additions & 0 deletions test/built-ins/ModuleSource/identity/tostringtag-non-source.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
// Copyright (C) 2025 Ecma International. All rights reserved.
// This code is governed by the BSD license found in the LICENSE file.
/*---
esid: sec-get-%abstractmodulesource%25.prototype.@@tostringtag
description: >
The @@toStringTag getter returns undefined for non-objects and objects
without a [[ModuleSourceRecord]] internal slot.
info: |
get %AbstractModuleSource%.prototype [ %Symbol.toStringTag% ]

1. Let O be the this value.
2. If O is not an Object, return undefined.
3. Let module be GetModuleSourceRecord(O).
4. If module is ~not-a-source~, return undefined.
...
features: [source-phase-imports, esm-phase-imports]
flags: [module]
---*/

assert.sameValue(typeof $262.AbstractModuleSource, 'function');

var getter = Object.getOwnPropertyDescriptor(
$262.AbstractModuleSource.prototype, Symbol.toStringTag
).get;

// Non-object this values return undefined.
assert.sameValue(getter.call(undefined), undefined,
'undefined this should return undefined');
assert.sameValue(getter.call(null), undefined,
'null this should return undefined');
assert.sameValue(getter.call(42), undefined,
'number this should return undefined');
assert.sameValue(getter.call('string'), undefined,
'string this should return undefined');
assert.sameValue(getter.call(true), undefined,
'boolean this should return undefined');
assert.sameValue(getter.call(Symbol()), undefined,
'symbol this should return undefined');

// Objects without [[ModuleSourceRecord]] return undefined.
assert.sameValue(getter.call({}), undefined,
'plain object should return undefined');
assert.sameValue(getter.call([]), undefined,
'array should return undefined');
assert.sameValue(getter.call(function () {}), undefined,
'function should return undefined');
Loading