Bug 1499140 - Support dynamic module import in the shell r=jandem
authorJon Coppeard <jcoppeard@mozilla.com>
Mon, 22 Oct 2018 11:28:17 +0100
changeset 490633 41812db6cabaf796d6672786efa109ab771e3b2f
parent 490632 6592655e860e73040175e3a0de9d7a1ade2dea89
child 490634 41ded240a012ad72cb57fed5b84603d438bf173c
push id247
push userfmarier@mozilla.com
push dateSat, 27 Oct 2018 01:06:44 +0000
reviewersjandem
bugs1499140
milestone64.0a1
Bug 1499140 - Support dynamic module import in the shell r=jandem
js/src/jit-test/tests/modules/dynamic-import-error.js
js/src/jit-test/tests/modules/dynamic-import-expression.js
js/src/jit-test/tests/modules/dynamic-import-ion.js
js/src/jit-test/tests/modules/dynamic-import-lazy.js
js/src/jit-test/tests/modules/dynamic-import-module.js
js/src/jit-test/tests/modules/dynamic-import-script.js
js/src/shell/ModuleLoader.js
js/src/shell/js.cpp
js/src/tests/lib/jittests.py
new file mode 100644
--- /dev/null
+++ b/js/src/jit-test/tests/modules/dynamic-import-error.js
@@ -0,0 +1,14 @@
+// |jit-test| module
+
+let result = null;
+let error = null;
+let promise = import("nonexistent.js");
+promise.then((ns) => {
+    result = ns;
+}).catch((e) => {
+    error = e;
+});
+
+drainJobQueue();
+assertEq(result, null);
+assertEq(error instanceof Error, true);
--- a/js/src/jit-test/tests/modules/dynamic-import-expression.js
+++ b/js/src/jit-test/tests/modules/dynamic-import-expression.js
@@ -72,14 +72,8 @@ assertParseThrowsSyntaxError("import(");
 assertParseThrowsSyntaxError("import(1,");
 assertParseThrowsSyntaxError("import(1, 2");
 assertParseThrowsSyntaxError("import(1, 2)");
 assertParseThrowsSyntaxError("x = import");
 assertParseThrowsSyntaxError("x = import(");
 assertParseThrowsSyntaxError("x = import(1,");
 assertParseThrowsSyntaxError("x = import(1, 2");
 assertParseThrowsSyntaxError("x = import(1, 2)");
-
-// import() is not implemented.
-assertThrowsInstanceOf(() => eval("import('foo')"),
-                       SyntaxError);
-assertThrowsInstanceOf(() => parseModule("import('foo')"),
-                       SyntaxError);
new file mode 100644
--- /dev/null
+++ b/js/src/jit-test/tests/modules/dynamic-import-ion.js
@@ -0,0 +1,16 @@
+// Even with --ion-eager, this needs to be run twice before it executes the
+// ion-compiled version.
+for (let i = 0; i < 2; i++) {
+    let result = null;
+    let error = null;
+    let promise = import("../../modules/module1.js");
+    promise.then((ns) => {
+        result = ns;
+    }).catch((e) => {
+        error = e;
+    });
+
+    drainJobQueue();
+    assertEq(error, null);
+    assertEq(result.a, 1);
+}
--- a/js/src/jit-test/tests/modules/dynamic-import-lazy.js
+++ b/js/src/jit-test/tests/modules/dynamic-import-lazy.js
@@ -1,6 +1,4 @@
-// |jit-test| error: SyntaxError
-
 function lazyilyParsedFunction()
 {
     return import("/module1.js");
 }
new file mode 100644
--- /dev/null
+++ b/js/src/jit-test/tests/modules/dynamic-import-module.js
@@ -0,0 +1,39 @@
+// |jit-test| module
+
+function testImport(path, name, value) {
+    let result = null;
+    let error = null;
+    let promise = import(path);
+    promise.then((ns) => {
+        result = ns;
+    }).catch((e) => {
+        error = e;
+    });
+
+    drainJobQueue();
+    assertEq(error, null);
+    assertEq(result[name], value);
+}
+
+// Resolved via module load path.
+testImport("module1.js", "a", 1);
+
+// Relative path resolved relative to this script.
+testImport("../../modules/module1a.js", "a", 2);
+
+// Import inside function.
+function f() {
+    testImport("../../modules/module2.js", "b", 2);
+}
+f();
+
+// Import inside direct eval.
+eval(`testImport("../../modules/module3.js", "c", 3)`);
+
+// Import inside indirect eval.
+const indirect = eval;
+const defineTestFunc = testImport.toSource();
+indirect(defineTestFunc + `testImport("../../modules/module3.js");`);
+
+// Import inside dynamic function.
+Function(defineTestFunc + `testImport("../../modules/module3.js");`)();
new file mode 100644
--- /dev/null
+++ b/js/src/jit-test/tests/modules/dynamic-import-script.js
@@ -0,0 +1,45 @@
+function testImport(path, name, value) {
+    let result = null;
+    let error = null;
+    let promise = import(path);
+    promise.then((ns) => {
+        result = ns;
+    }).catch((e) => {
+        error = e;
+    });
+
+    drainJobQueue();
+    assertEq(error, null);
+    assertEq(result[name], value);
+}
+
+// Resolved via module load path.
+testImport("module1.js", "a", 1);
+
+// Relative path resolved relative to this script.
+testImport("../../modules/module1a.js", "a", 2);
+
+// Import inside function.
+function f() {
+    testImport("../../modules/module2.js", "b", 2);
+}
+f();
+
+// Import inside eval.
+eval(`testImport("../../modules/module3.js", "c", 3)`);
+
+// Import inside indirect eval.
+const indirect = eval;
+const defineTestFunc = testImport.toSource();
+indirect(defineTestFunc + `testImport("../../modules/module3.js");`);
+
+// Import inside dynamic function.
+Function(defineTestFunc + `testImport("../../modules/module3.js");`)();
+
+// Import in eval in promise handler.
+let ran = false;
+Promise
+    .resolve(`import("../../modules/module3.js").then(() => { ran = true; })`)
+    .then(eval)
+drainJobQueue();
+assertEq(ran, true);
--- a/js/src/shell/ModuleLoader.js
+++ b/js/src/shell/ModuleLoader.js
@@ -1,15 +1,16 @@
 /* -*- Mode: javascript; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 4 -*- */
 /* This Source Code Form is subject to the terms of the Mozilla Public
  * License, v. 2.0. If a copy of the MPL was not distributed with this
  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
 
 /* global getModuleLoadPath setModuleLoadHook setModuleResolveHook setModuleMetadataHook */
 /* global getModulePrivate setModulePrivate parseModule os */
+/* global setModuleDynamicImportHook finishDynamicModuleImport abortDynamicModuleImport */
 
 // A basic synchronous module loader for testing the shell.
 {
 // Save standard built-ins before scripts can modify them.
 const ArrayPrototypeJoin = Array.prototype.join;
 const MapPrototypeGet = Map.prototype.get;
 const MapPrototypeHas = Map.prototype.has;
 const MapPrototypeSet = Map.prototype.set;
@@ -19,17 +20,16 @@ const StringPrototypeIndexOf = String.pr
 const StringPrototypeLastIndexOf = String.prototype.lastIndexOf;
 const StringPrototypeStartsWith = String.prototype.startsWith;
 const StringPrototypeSubstring = String.prototype.substring;
 const ErrorClass = Error;
 
 const ReflectLoader = new class {
     constructor() {
         this.registry = new Map();
-        this.modulePaths = new Map();
         this.loadPath = getModuleLoadPath();
     }
 
     resolve(name, referencingInfo) {
         if (os.path.isAbsolute(name))
             return name;
 
         let loadPath = this.loadPath;
@@ -171,17 +171,17 @@ const ReflectLoader = new class {
         module.declarationInstantiation();
         return module.evaluation();
     }
 
     importRoot(path) {
         return this.loadAndExecute(path);
     }
 
-    ["import"](name, referrer) {
+    ["import"](name, referencingInfo) {
         let path = this.resolve(name, null);
         return this.loadAndExecute(path);
     }
 
     populateImportMeta(moduleInfo, metaObject) {
         // For the shell, use the module's normalized path as the base URL.
 
         let path;
@@ -191,19 +191,28 @@ const ReflectLoader = new class {
             path = "(unknown)";
         }
         metaObject.url = path;
     }
 };
 
 setModuleLoadHook((path) => ReflectLoader.importRoot(path));
 
-setModuleResolveHook((module, requestName) => {
-    let path = ReflectLoader.resolve(requestName, module);
+setModuleResolveHook((referencingInfo, requestName) => {
+    let path = ReflectLoader.resolve(requestName, referencingInfo);
     return ReflectLoader.loadAndParse(path);
 });
 
 setModuleMetadataHook((module, metaObject) => {
     ReflectLoader.populateImportMeta(module, metaObject);
 });
 
+setModuleDynamicImportHook((referencingInfo, specifier, promise) => {
+    try {
+        let path = ReflectLoader.resolve(specifier, referencingInfo);
+        ReflectLoader.loadAndExecute(path);
+        finishDynamicModuleImport(referencingInfo, specifier, promise);
+    } catch (err) {
+        abortDynamicModuleImport(referencingInfo, specifier, promise, err);
+    }
+});
+
 }
-
--- a/js/src/shell/js.cpp
+++ b/js/src/shell/js.cpp
@@ -188,16 +188,17 @@ enum JSShellExitCode {
 };
 
 // Define use of application-specific slots on the shell's global object.
 enum GlobalAppSlot
 {
     GlobalAppSlotModuleLoadHook,           // Shell-specific; load a module graph
     GlobalAppSlotModuleResolveHook,        // HostResolveImportedModule
     GlobalAppSlotModuleMetadataHook,       // HostPopulateImportMeta
+    GlobalAppSlotModuleDynamicImportHook,  // HostImportModuleDynamically
     GlobalAppSlotCount
 };
 static_assert(GlobalAppSlotCount <= JSCLASS_GLOBAL_APPLICATION_SLOTS,
               "Too many applications slots defined for shell global");
 
 /*
  * Note: This limit should match the stack limit set by the browser in
  *       js/xpconnect/src/XPCJSContext.cpp
@@ -845,16 +846,42 @@ EnvironmentPreparer::invoke(HandleObject
 
     AutoRealm ar(cx, global);
     AutoReportException are(cx);
     if (!closure(cx)) {
         return;
     }
 }
 
+static bool
+RegisterScriptPathWithModuleLoader(JSContext* cx, HandleScript script, const char* filename)
+{
+    // Set the private value associated with a script to a object containing the
+    // script's filename so that the module loader can use it to resolve
+    // relative imports.
+
+    RootedString path(cx, JS_NewStringCopyZ(cx, filename));
+    if (!path) {
+        return false;
+    }
+
+    RootedObject infoObject(cx, JS_NewPlainObject(cx));
+    if (!infoObject) {
+        return false;
+    }
+
+    RootedValue pathValue(cx, StringValue(path));
+    if (!JS_DefineProperty(cx, infoObject, "path", pathValue, 0)) {
+        return false;
+    }
+
+    JS::SetScriptPrivate(script, ObjectValue(*infoObject));
+    return true;
+}
+
 static MOZ_MUST_USE bool
 RunFile(JSContext* cx, const char* filename, FILE* file, bool compileOnly)
 {
     SkipUTF8BOM(file);
 
     // To support the UNIX #! shell hack, gobble the first line if it starts
     // with '#'.
     int ch = fgetc(file);
@@ -878,16 +905,20 @@ RunFile(JSContext* cx, const char* filen
                .setNoScriptRval(true);
 
         if (!JS::CompileUtf8File(cx, options, file, &script)) {
             return false;
         }
         MOZ_ASSERT(script);
     }
 
+    if (!RegisterScriptPathWithModuleLoader(cx, script, filename)) {
+        return false;
+    }
+
     #ifdef DEBUG
         if (dumpEntrainedVariables) {
             AnalyzeEntrainedVariables(cx, script);
         }
     #endif
     if (!compileOnly) {
         if (!JS_ExecuteScript(cx, script)) {
             return false;
@@ -914,25 +945,28 @@ RunBinAST(JSContext* cx, const char* fil
                .setNoScriptRval(true);
 
         script = JS::DecodeBinAST(cx, options, file);
         if (!script) {
             return false;
         }
     }
 
+    if (!RegisterScriptPathWithModuleLoader(cx, script, filename)) {
+        return false;
+    }
+
     return JS_ExecuteScript(cx, script);
 }
 
 #endif // JS_BUILD_BINAST
 
 static bool
 InitModuleLoader(JSContext* cx)
 {
-
     // Decompress and evaluate the embedded module loader source to initialize
     // the module loader for the current compartment.
 
     uint32_t srcLen = moduleloader::GetRawScriptsSize();
     auto src = cx->make_pod_array<char>(srcLen);
     if (!src || !DecompressString(moduleloader::compressedSources, moduleloader::GetCompressedSize(),
                                   reinterpret_cast<unsigned char*>(src.get()), srcLen))
     {
@@ -4835,16 +4869,119 @@ ShellGetModulePrivate(JSContext* cx, uns
         return ReportArgumentTypeError(cx, args[0], "module object");
     }
 
     args.rval().set(JS::GetModulePrivate(&args[0].toObject()));
     return true;
 }
 
 static bool
+SetModuleDynamicImportHook(JSContext* cx, unsigned argc, Value* vp)
+{
+    CallArgs args = CallArgsFromVp(argc, vp);
+    if (args.length() != 1) {
+        JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr, JSMSG_MORE_ARGS_NEEDED,
+                                  "setModuleDynamicImportHook", "0", "s");
+        return false;
+    }
+
+    if (!args[0].isObject() || !args[0].toObject().is<JSFunction>()) {
+        const char* typeName = InformalValueTypeName(args[0]);
+        JS_ReportErrorASCII(cx, "expected hook function, got %s", typeName);
+        return false;
+    }
+
+    Handle<GlobalObject*> global = cx->global();
+    global->setReservedSlot(GlobalAppSlotModuleDynamicImportHook, args[0]);
+
+    args.rval().setUndefined();
+    return true;
+}
+
+static bool
+FinishDynamicModuleImport(JSContext* cx, unsigned argc, Value* vp)
+{
+    CallArgs args = CallArgsFromVp(argc, vp);
+    if (args.length() != 3) {
+        JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr, JSMSG_MORE_ARGS_NEEDED,
+                                  "finishDynamicModuleImport", "0", "s");
+        return false;
+    }
+
+    if (!args[1].isString()) {
+        return ReportArgumentTypeError(cx, args[1], "String");
+    }
+
+    if (!args[2].isObject() || !args[2].toObject().is<PromiseObject>()) {
+        return ReportArgumentTypeError(cx, args[2], "PromiseObject");
+    }
+
+    RootedString specifier(cx, args[1].toString());
+    Rooted<PromiseObject*> promise(cx, &args[2].toObject().as<PromiseObject>());
+
+    return js::FinishDynamicModuleImport(cx, args[0], specifier, promise);
+}
+
+static bool
+AbortDynamicModuleImport(JSContext* cx, unsigned argc, Value* vp)
+{
+    CallArgs args = CallArgsFromVp(argc, vp);
+    if (args.length() != 4) {
+        JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr, JSMSG_MORE_ARGS_NEEDED,
+                                  "abortDynamicModuleImport", "0", "s");
+        return false;
+    }
+
+    if (!args[1].isString()) {
+        return ReportArgumentTypeError(cx, args[1], "String");
+    }
+
+    if (!args[2].isObject() || !args[2].toObject().is<PromiseObject>()) {
+        return ReportArgumentTypeError(cx, args[2], "PromiseObject");
+    }
+
+    if (!args[3].isObject() || !args[3].toObject().is<ErrorObject>()) {
+        return ReportArgumentTypeError(cx, args[3], "ErrorObject");
+    }
+
+    RootedString specifier(cx, args[1].toString());
+    Rooted<PromiseObject*> promise(cx, &args[2].toObject().as<PromiseObject>());
+    Rooted<ErrorObject*> error(cx, &args[3].toObject().as<ErrorObject>());
+
+    Rooted<Value> value(cx, ObjectValue(*error));
+    cx->setPendingException(value);
+    return js::FinishDynamicModuleImport(cx, args[0], specifier, promise);
+}
+
+static bool
+ShellModuleDynamicImportHook(JSContext* cx, HandleValue referencingPrivate, HandleString specifier,
+                             HandleObject promise)
+{
+    Handle<GlobalObject*> global = cx->global();
+    RootedValue hookValue(cx, global->getReservedSlot(GlobalAppSlotModuleDynamicImportHook));
+    if (hookValue.isUndefined()) {
+        JS_ReportErrorASCII(cx, "Module resolve hook not set");
+        return false;
+    }
+    MOZ_ASSERT(hookValue.toObject().is<JSFunction>());
+
+    JS::AutoValueArray<3> args(cx);
+    args[0].set(referencingPrivate);
+    args[1].setString(specifier);
+    args[2].setObject(*promise);
+
+    RootedValue result(cx);
+    if (!JS_CallFunctionValue(cx, nullptr, hookValue, args, &result)) {
+        return false;
+    }
+
+    return true;
+}
+
+static bool
 GetModuleLoadPath(JSContext* cx, unsigned argc, Value* vp)
 {
     CallArgs args = CallArgsFromVp(argc, vp);
 
     ShellContext* sc = GetShellContext(cx);
     if (sc->moduleLoadPath) {
         JSString* str = JS_NewStringCopyZ(cx, sc->moduleLoadPath.get());
         if (!str) {
@@ -8054,27 +8191,43 @@ static const JSFunctionSpecWithHelp shel
 
     JS_FN_HELP("setModuleLoadHook", SetModuleLoadHook, 1, 0,
 "setModuleLoadHook(function(path))",
 "  Set the shell specific module load hook to |function|.\n"
 "  This hook is used to load a module graph.  It should be implemented by the\n"
 "  module loader."),
 
     JS_FN_HELP("setModuleResolveHook", SetModuleResolveHook, 1, 0,
-"setModuleResolveHook(function(module, specifier) {})",
+"setModuleResolveHook(function(referrer, specifier))",
 "  Set the HostResolveImportedModule hook to |function|.\n"
 "  This hook is used to look up a previously loaded module object.  It should\n"
 "  be implemented by the module loader."),
 
     JS_FN_HELP("setModuleMetadataHook", SetModuleMetadataHook, 1, 0,
 "setModuleMetadataHook(function(module) {})",
 "  Set the HostPopulateImportMeta hook to |function|.\n"
 "  This hook is used to create the metadata object returned by import.meta for\n"
 "  a module.  It should be implemented by the module loader."),
 
+    JS_FN_HELP("setModuleDynamicImportHook", SetModuleDynamicImportHook, 1, 0,
+"setModuleDynamicImportHook(function(referrer, specifier, promise))",
+"  Set the HostImportModuleDynamically hook to |function|.\n"
+"  This hook is used to dynamically import a module.  It should\n"
+"  be implemented by the module loader."),
+
+    JS_FN_HELP("finishDynamicModuleImport", FinishDynamicModuleImport, 3, 0,
+"finishDynamicModuleImport(referrer, specifier, promise)",
+"  The module loader's dynamic import hook should call this when the module has"
+"  been loaded successfully."),
+
+    JS_FN_HELP("abortDynamicModuleImport", AbortDynamicModuleImport, 4, 0,
+"abortDynamicModuleImport(referrer, specifier, promise, error)",
+"  The module loader's dynamic import hook should call this when the module "
+"  import has failed."),
+
     JS_FN_HELP("setModulePrivate", ShellSetModulePrivate, 2, 0,
 "setModulePrivate(scriptObject, privateValue)",
 "  Associate a private value with a module object.\n"),
 
     JS_FN_HELP("getModulePrivate", ShellGetModulePrivate, 2, 0,
 "getModulePrivate(scriptObject)",
 "  Get the private value associated with a module object.\n"),
 
@@ -10784,16 +10937,17 @@ main(int argc, char** argv, char** envp)
         JS_SetGCParameter(cx, JSGC_DYNAMIC_MARK_SLICE, 1);
         JS_SetGCParameter(cx, JSGC_SLICE_TIME_BUDGET, 10);
     }
 #endif
 
     js::SetPreserveWrapperCallback(cx, DummyPreserveWrapperCallback);
 
     JS::SetModuleResolveHook(cx->runtime(), ShellModuleResolveHook);
+    JS::SetModuleDynamicImportHook(cx->runtime(), ShellModuleDynamicImportHook);
     JS::SetModuleMetadataHook(cx->runtime(), CallModuleMetadataHook);
 
     result = Shell(cx, &op, envp);
 
 #ifdef DEBUG
     if (OOM_printAllocationCount) {
         printf("OOM max count: %" PRIu64 "\n", js::oom::counter);
     }
--- a/js/src/tests/lib/jittests.py
+++ b/js/src/tests/lib/jittests.py
@@ -367,18 +367,18 @@ class JitTest:
         cmd = prefix + ['--js-cache', JitTest.CacheDir]
         cmd += list(set(self.jitflags))
         for expr in exprs:
             cmd += ['-e', expr]
         for inc in self.other_includes:
             cmd += ['-f', libdir + inc]
         if self.skip_if_cond:
             cmd += ['-e', "if ({}) quit({})".format(self.skip_if_cond, self.SKIPPED_EXIT_STATUS)]
+        cmd += ['--module-load-path', moduledir]
         if self.is_module:
-            cmd += ['--module-load-path', moduledir]
             cmd += ['--module', path]
         elif self.is_binast:
             # In builds with BinAST, this will run the test file. In builds without,
             # It's a no-op and the tests will silently pass.
             cmd += ['-B', path]
         elif self.test_reflect_stringify is None:
             cmd += ['-f', path]
         else: