Skip to content

Commit

Permalink
Bug 1496852 - Support JavaScript export-ns-from syntax r=jorendorff
Browse files Browse the repository at this point in the history
Add support for `export * as ns from "a";` syntax. This was added in
ECMAScript 2020.

One wrinkle in the implementation is that the parser decides whether to use a
lexical vs indirect binding before module linking. Instead, we reserve a
hidden environment slot called "*namespace*" for each module that can be
referenced by indirect binding maps as needed.

Spec is a needs-consensus PR at tc39/ecma262#1174

Depends on D80984

Differential Revision: https://phabricator.services.mozilla.com/D80777
  • Loading branch information
moztcampbell committed Jul 16, 2020
1 parent c60e5c7 commit 9842602
Show file tree
Hide file tree
Showing 10 changed files with 124 additions and 17 deletions.
38 changes: 30 additions & 8 deletions js/src/builtin/Module.js
Original file line number Diff line number Diff line change
Expand Up @@ -136,9 +136,9 @@ function ModuleResolveExport(exportName, resolveSet = [])
if (exportName === e.exportName) {
let importedModule = CallModuleResolveHook(module, e.moduleRequest,
MODULE_STATUS_UNLINKED);

// TODO: Step 7.a.ii

if (e.importName === "*") {
return {module: importedModule, bindingName: "*namespace*"};
}
return callFunction(importedModule.resolveExport, importedModule, e.importName,
resolveSet);
}
Expand Down Expand Up @@ -232,8 +232,18 @@ function ModuleNamespaceCreate(module, exports)
let name = exports[i];
let binding = callFunction(module.resolveExport, module, name);
assert(IsResolvedBinding(binding), "Failed to resolve binding");
// TODO: ES2020 9.4.6.7 Module Namespace Exotic Object [[Get]], Step 10.
AddModuleNamespaceBinding(ns, name, binding.module, binding.bindingName);
// ES2020 9.4.6.7 Module Namespace Exotic Object [[Get]], Step 10.
if (binding.bindingName === "*namespace*") {
let namespace = GetModuleNamespace(binding.module);

// The spec uses an immutable binding here but we have already
// generated bytecode for an indirect binding. Instead, use an
// indirect binding to "*namespace*" slot of the target environment.
EnsureModuleEnvironmentNamespace(binding.module, namespace);
AddModuleNamespaceBinding(ns, name, binding.module, binding.bindingName);
} else {
AddModuleNamespaceBinding(ns, name, binding.module, binding.bindingName);
}
}

return ns;
Expand Down Expand Up @@ -478,9 +488,21 @@ function InitializeEnvironment()
imp.lineNumber, imp.columnNumber);
}

// TODO: Step 9.d.iii

CreateImportBinding(env, imp.localName, resolution.module, resolution.bindingName);
if (resolution.bindingName === "*namespace*") {
let namespace = GetModuleNamespace(resolution.module);

// This should be CreateNamespaceBinding, but we have already
// generated bytecode assuming an indirect binding. Instead,
// ensure a special "*namespace*"" binding exists on the target
// module's environment. We then generate an indirect binding to
// this synthetic binding.
EnsureModuleEnvironmentNamespace(resolution.module, namespace);
CreateImportBinding(env, imp.localName, resolution.module,
resolution.bindingName);
} else {
CreateImportBinding(env, imp.localName, resolution.module,
resolution.bindingName);
}
}
}

Expand Down
2 changes: 1 addition & 1 deletion js/src/builtin/ModuleObject.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -1300,7 +1300,7 @@ ModuleBuilder::buildTables()
return false;
}
}
} else if (exp->importName() == cx_->names().star) {
} else if (exp->importName() == cx_->names().star && !exp->exportName()) {
if (!starExportEntries_.append(exp))
return false;
} else {
Expand Down
4 changes: 4 additions & 0 deletions js/src/builtin/TestingFunctions.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -3876,6 +3876,10 @@ GetModuleEnvironmentNames(JSContext* cx, unsigned argc, Value* vp)
if (!JS_Enumerate(cx, env, &ids))
return false;

// The "*namespace*" binding is a detail of current implementation so hide
// it to give stable results in tests.
ids.eraseIfEqual(NameToId(cx->names().starNamespaceStar));

uint32_t length = ids.length();
RootedArrayObject array(cx, NewDenseFullyAllocatedArray(cx, length));
if (!array)
Expand Down
54 changes: 48 additions & 6 deletions js/src/frontend/Parser.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -2333,6 +2333,17 @@ Parser<FullParseHandler, char16_t>::moduleBody(ModuleSharedContext* modulesc)
p->value()->setClosedOver();
}

// Reserve an environment slot for a "*namespace*" psuedo-binding and mark as
// closed-over. We do not know until module linking if this will be used.
if (!noteDeclaredName(context->names().starNamespaceStar, DeclarationKind::Const,
pos())) {
return nullptr;
}
modulepc.varScope()
.lookupDeclaredName(context->names().starNamespaceStar)
->value()
->setClosedOver();

if (!FoldConstants(context, &pn, this))
return null();

Expand Down Expand Up @@ -5578,13 +5589,44 @@ Parser<ParseHandler, CharT>::exportBatch(uint32_t begin)
if (!kid)
return null();

// Handle the form |export *| by adding a special export batch
// specifier to the list.
Node exportSpec = handler.newExportBatchSpec(pos());
if (!exportSpec)
return null();
bool foundAs;
if (!tokenStream.matchToken(&foundAs, TokenKind::As)) {
return null();
}

if (foundAs) {
MUST_MATCH_TOKEN_FUNC(TokenKindIsPossibleIdentifierName, JSMSG_NO_EXPORT_NAME);

Node exportName = newName(anyChars.currentName());
if (!exportName) {
return null();
}

if (!checkExportedNameForClause(exportName)) {
return null();
}

handler.addList(kid, exportSpec);
Node importName = newName(context->names().star);
if (!importName) {
return null();
}

Node exportSpec = handler.newExportSpec(importName, exportName);
if (!exportSpec) {
return null();
}

handler.addList(kid, exportSpec);
} else {
// Handle the form |export *| by adding a special export batch
// specifier to the list.
Node exportSpec = handler.newExportBatchSpec(pos());
if (!exportSpec) {
return null();
}

handler.addList(kid, exportSpec);
}

MUST_MATCH_TOKEN(TokenKind::From, JSMSG_FROM_AFTER_EXPORT_STAR);

Expand Down
7 changes: 7 additions & 0 deletions js/src/jit-test/lib/syntax.js
Original file line number Diff line number Diff line change
Expand Up @@ -451,6 +451,13 @@ function test_syntax(postfixes, check_error, ignore_opts) {
test("export * from 'a' ", opts);
test("export * from 'a'; ", opts);

test("export * ", opts);
test("export * as ", opts);
test("export * as ns ", opts);
test("export * as ns from ", opts);
test("export * as ns from 'a' ", opts);
test("export * as ns from 'a'; ", opts);

test("export function ", opts);
test("export function f ", opts);
test("export function f( ", opts);
Expand Down
1 change: 1 addition & 0 deletions js/src/jit-test/modules/export-ns.js
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export * as ns from "module1.js";
10 changes: 10 additions & 0 deletions js/src/jit-test/tests/modules/export-ns-from.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
// |jit-test| module

import { ns } from "export-ns.js";

load(libdir + 'asserts.js');

assertEq(isProxy(ns), true);
assertEq(ns.a, 1);
assertThrowsInstanceOf(function() { eval("delete ns"); }, SyntaxError);
assertThrowsInstanceOf(function() { ns = null; }, TypeError);
1 change: 1 addition & 0 deletions js/src/vm/CommonPropertyNames.h
Original file line number Diff line number Diff line change
Expand Up @@ -380,6 +380,7 @@
macro(StarGeneratorNext, StarGeneratorNext, "StarGeneratorNext") \
macro(StarGeneratorReturn, StarGeneratorReturn, "StarGeneratorReturn") \
macro(StarGeneratorThrow, StarGeneratorThrow, "StarGeneratorThrow") \
macro(starNamespaceStar, starNamespaceStar, "*namespace*") \
macro(start, start, "start") \
macro(startTimestamp, startTimestamp, "startTimestamp") \
macro(state, state, "state") \
Expand Down
5 changes: 3 additions & 2 deletions js/src/vm/EnvironmentObject.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -736,11 +736,12 @@ static inline bool IsUnscopableDotName(JSContext* cx, HandleId id) {
#ifdef DEBUG
static bool IsInternalDotName(JSContext* cx, HandleId id) {
return JSID_IS_ATOM(id, cx->names().dotThis) ||
JSID_IS_ATOM(id, cx->names().dotGenerator); /* || The following aren't currently implemented in Waterfox
JSID_IS_ATOM(id, cx->names().dotGenerator) /* || The following aren't currently implemented in Waterfox
JSID_IS_ATOM(id, cx->names().dotInitializers) ||
JSID_IS_ATOM(id, cx->names().dotFieldKeys) ||
JSID_IS_ATOM(id, cx->names().dotStaticInitializers) ||
JSID_IS_ATOM(id, cx->names().dotStaticFieldKeys); */
JSID_IS_ATOM(id, cx->names().dotStaticFieldKeys) */ ||
JSID_IS_ATOM(id, cx->names().starNamespaceStar);
}
#endif

Expand Down
19 changes: 19 additions & 0 deletions js/src/vm/SelfHosting.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -2166,6 +2166,23 @@ intrinsic_CreateNamespaceBinding(JSContext* cx, unsigned argc, Value* vp)
return true;
}

static bool intrinsic_EnsureModuleEnvironmentNamespace(JSContext* cx,
unsigned argc,
Value* vp) {
CallArgs args = CallArgsFromVp(argc, vp);
MOZ_ASSERT(args.length() == 2);
RootedModuleObject module(cx, &args[0].toObject().as<ModuleObject>());
MOZ_ASSERT(args[1].toObject().is<ModuleNamespaceObject>());
RootedModuleEnvironmentObject environment(cx, &module->initialEnvironment());
// The property already exists in the evironment but is not writable, so set
// the slot directly.
RootedShape shape(cx, environment->lookup(cx, cx->names().starNamespaceStar));
MOZ_ASSERT(shape);
environment->setSlot(shape->slot(), args[1]);
args.rval().setUndefined();
return true;
}

static bool
intrinsic_InstantiateModuleFunctionDeclarations(JSContext* cx, unsigned argc, Value* vp)
{
Expand Down Expand Up @@ -2678,6 +2695,8 @@ static const JSFunctionSpec intrinsic_functions[] = {
JS_FN("IsModuleEnvironment", intrinsic_IsInstanceOfBuiltin<ModuleEnvironmentObject>, 1, 0),
JS_FN("CreateImportBinding", intrinsic_CreateImportBinding, 4, 0),
JS_FN("CreateNamespaceBinding", intrinsic_CreateNamespaceBinding, 3, 0),
JS_FN("EnsureModuleEnvironmentNamespace",
intrinsic_EnsureModuleEnvironmentNamespace, 1, 0),
JS_FN("InstantiateModuleFunctionDeclarations",
intrinsic_InstantiateModuleFunctionDeclarations, 1, 0),
JS_FN("ExecuteModule", intrinsic_ExecuteModule, 1, 0),
Expand Down

0 comments on commit 9842602

Please sign in to comment.