From b6b223389769e85f6238e751d8bd957d278f770c Mon Sep 17 00:00:00 2001 From: Ted Campbell Date: Thu, 16 Jul 2020 14:30:00 +0000 Subject: [PATCH] Bug 1496852 - Support JavaScript export-ns-from syntax r=jorendorff 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 https://github.com/tc39/ecma262/pull/1174 Depends on D80984 Differential Revision: https://phabricator.services.mozilla.com/D80777 --- js/src/builtin/Module.js | 38 ++++++++++--- js/src/builtin/ModuleObject.cpp | 2 +- js/src/builtin/TestingFunctions.cpp | 4 ++ js/src/frontend/Parser.cpp | 54 ++++++++++++++++--- js/src/jit-test/lib/syntax.js | 7 +++ js/src/jit-test/modules/export-ns.js | 1 + .../jit-test/tests/modules/export-ns-from.js | 10 ++++ js/src/vm/CommonPropertyNames.h | 1 + js/src/vm/EnvironmentObject.cpp | 5 +- js/src/vm/SelfHosting.cpp | 19 +++++++ 10 files changed, 124 insertions(+), 17 deletions(-) create mode 100644 js/src/jit-test/modules/export-ns.js create mode 100644 js/src/jit-test/tests/modules/export-ns-from.js diff --git a/js/src/builtin/Module.js b/js/src/builtin/Module.js index eac752346483..0c183f388537 100644 --- a/js/src/builtin/Module.js +++ b/js/src/builtin/Module.js @@ -136,9 +136,9 @@ function ModuleResolveExport(exportName, resolveSet = new_List()) 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); } @@ -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; @@ -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); + } } } diff --git a/js/src/builtin/ModuleObject.cpp b/js/src/builtin/ModuleObject.cpp index a293962ae566..63eb2c35f83c 100644 --- a/js/src/builtin/ModuleObject.cpp +++ b/js/src/builtin/ModuleObject.cpp @@ -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 { diff --git a/js/src/builtin/TestingFunctions.cpp b/js/src/builtin/TestingFunctions.cpp index d7f068f75f8f..10d41220c52b 100644 --- a/js/src/builtin/TestingFunctions.cpp +++ b/js/src/builtin/TestingFunctions.cpp @@ -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) diff --git a/js/src/frontend/Parser.cpp b/js/src/frontend/Parser.cpp index 329dc8305098..85eb8a684287 100644 --- a/js/src/frontend/Parser.cpp +++ b/js/src/frontend/Parser.cpp @@ -2333,6 +2333,17 @@ Parser::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(); @@ -5578,13 +5589,44 @@ Parser::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); diff --git a/js/src/jit-test/lib/syntax.js b/js/src/jit-test/lib/syntax.js index 2805b0e75f0d..67b9e303d072 100644 --- a/js/src/jit-test/lib/syntax.js +++ b/js/src/jit-test/lib/syntax.js @@ -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); diff --git a/js/src/jit-test/modules/export-ns.js b/js/src/jit-test/modules/export-ns.js new file mode 100644 index 000000000000..0d2e49908899 --- /dev/null +++ b/js/src/jit-test/modules/export-ns.js @@ -0,0 +1 @@ +export * as ns from "module1.js"; diff --git a/js/src/jit-test/tests/modules/export-ns-from.js b/js/src/jit-test/tests/modules/export-ns-from.js new file mode 100644 index 000000000000..2e79d77f4294 --- /dev/null +++ b/js/src/jit-test/tests/modules/export-ns-from.js @@ -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); diff --git a/js/src/vm/CommonPropertyNames.h b/js/src/vm/CommonPropertyNames.h index dd8520614207..9573339fe77f 100644 --- a/js/src/vm/CommonPropertyNames.h +++ b/js/src/vm/CommonPropertyNames.h @@ -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") \ diff --git a/js/src/vm/EnvironmentObject.cpp b/js/src/vm/EnvironmentObject.cpp index 84647b9997a5..3f309b97309c 100644 --- a/js/src/vm/EnvironmentObject.cpp +++ b/js/src/vm/EnvironmentObject.cpp @@ -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 diff --git a/js/src/vm/SelfHosting.cpp b/js/src/vm/SelfHosting.cpp index be0dd7fc8496..3b31dc716053 100644 --- a/js/src/vm/SelfHosting.cpp +++ b/js/src/vm/SelfHosting.cpp @@ -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()); + MOZ_ASSERT(args[1].toObject().is()); + 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) { @@ -2679,6 +2696,8 @@ static const JSFunctionSpec intrinsic_functions[] = { JS_FN("IsModuleEnvironment", intrinsic_IsInstanceOfBuiltin, 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),