[gjs/ewlsh/top-level-await] Enable top-level await




commit c1b8be86b10e6b59405c74d89ff1ad30af94b792
Author: Evan Welsh <contact evanwelsh com>
Date:   Sun Jan 30 10:06:15 2022 -0800

    Enable top-level await

 gjs/context-private.h              |   3 +
 gjs/context.cpp                    |  92 +++++++++++++--
 gjs/engine.cpp                     |   2 +-
 gjs/internal.cpp                   |  11 +-
 gjs/module.cpp                     |  27 ++---
 js.gresource.xml                   |   1 -
 modules/internal/internalLoader.js | 229 ------------------------------------
 modules/internal/loader.js         | 231 +++++++++++++++++++++++++++++++++++--
 8 files changed, 324 insertions(+), 272 deletions(-)
---
diff --git a/gjs/context-private.h b/gjs/context-private.h
index 350f646a0..9ef774b21 100644
--- a/gjs/context-private.h
+++ b/gjs/context-private.h
@@ -122,6 +122,7 @@ class GjsContextPrivate : public JS::JobQueue {
     bool m_draining_job_queue : 1;
     bool m_should_profile : 1;
     bool m_exec_as_module : 1;
+    bool m_unhandled_exception : 1;
     bool m_should_listen_sigusr2 : 1;
 
     // If profiling is enabled, we record the durations and reason for GC mark
@@ -182,6 +183,7 @@ class GjsContextPrivate : public JS::JobQueue {
     }
     void main_loop_hold() { m_main_loop.hold(); }
     void main_loop_release() { m_main_loop.release(); }
+
     [[nodiscard]] GjsProfiler* profiler() const { return m_profiler; }
     [[nodiscard]] const GjsAtoms& atoms() const { return *m_atoms; }
     [[nodiscard]] bool destroying() const { return m_destroying.load(); }
@@ -231,6 +233,7 @@ class GjsContextPrivate : public JS::JobQueue {
     void schedule_gc(void) { schedule_gc_internal(true); }
     void schedule_gc_if_needed(void);
 
+    void report_unhandled_exception() { m_unhandled_exception = true; }
     void exit(uint8_t exit_code);
     [[nodiscard]] bool should_exit(uint8_t* exit_code_p) const;
 
diff --git a/gjs/context.cpp b/gjs/context.cpp
index 3adc4d76e..a7d4b3749 100644
--- a/gjs/context.cpp
+++ b/gjs/context.cpp
@@ -523,8 +523,64 @@ gjs_context_constructed(GObject *object)
     setup_dump_heap();
 }
 
+static bool on_context_module_rejected_fallible(JSContext* cx, unsigned argc,
+                                                JS::Value* vp) {
+    JS::CallArgs args = JS::CallArgsFromVp(argc, vp);
+    JS::HandleValue error = args.get(0);
+
+    GjsContextPrivate* gjs_cx = GjsContextPrivate::from_cx(cx);
+    gjs_cx->report_unhandled_exception();
+
+    gjs_log_exception_full(cx, error, nullptr, G_LOG_LEVEL_CRITICAL);
+
+    gjs_cx->main_loop_release();
+
+    args.rval().setUndefined();
+    return true;
+}
+
+static bool on_context_module_rejected_infallible(JSContext* cx, unsigned,
+                                                  JS::Value*) {
+    GjsContextPrivate::from_cx(cx)->main_loop_release();
+
+    g_error("Failed to load context module.");
+    return false;
+}
+
+static bool on_context_module_resolved(JSContext* cx, unsigned, JS::Value*) {
+    GjsContextPrivate::from_cx(cx)->main_loop_release();
+
+    return true;
+}
+
+static bool add_promise_reactions(JSContext* cx, JS::HandleValue promise,
+                                  JSNative resolve, JSNative reject,
+                                  const std::string& debug_tag) {
+    JS::RootedObject promiseObject(cx, promise.toObjectOrNull());
+    if (!promiseObject)
+        return false;
+
+    std::string resolved_tag = debug_tag + " async resolved";
+    std::string rejected_tag = debug_tag + " async rejected";
+
+    JS::RootedFunction onRejected(
+        cx,
+        js::NewFunctionWithReserved(cx, reject, 1, 0, resolved_tag.c_str()));
+    JS::RootedFunction onResolved(
+        cx,
+        js::NewFunctionWithReserved(cx, resolve, 1, 0, rejected_tag.c_str()));
+    if (!onRejected || !onResolved)
+        return false;
+
+    JS::RootedObject resolved(cx, JS_GetFunctionObject(onResolved));
+    JS::RootedObject rejected(cx, JS_GetFunctionObject(onRejected));
+
+    return JS::AddPromiseReactions(cx, promiseObject, resolved, rejected);
+}
+
 static void load_context_module(JSContext* cx, const char* uri,
                                 const char* debug_identifier) {
+    GjsContextPrivate::from_cx(cx)->main_loop_hold();
     JS::RootedObject loader(cx, gjs_module_load(cx, uri, uri));
 
     if (!loader) {
@@ -537,11 +593,15 @@ static void load_context_module(JSContext* cx, const char* uri,
         g_error("Failed to instantiate %s module.", debug_identifier);
     }
 
-    JS::RootedValue ignore(cx);
-    if (!JS::ModuleEvaluate(cx, loader, &ignore)) {
+    JS::RootedValue evaluationPromise(cx);
+    if (!JS::ModuleEvaluate(cx, loader, &evaluationPromise)) {
         gjs_log_exception(cx);
         g_error("Failed to evaluate %s module.", debug_identifier);
     }
+
+    g_assert(add_promise_reactions(
+        cx, evaluationPromise, on_context_module_resolved,
+        on_context_module_rejected_infallible, "context"));
 }
 
 GjsContextPrivate::GjsContextPrivate(JSContext* cx, GjsContext* public_context)
@@ -655,22 +715,21 @@ GjsContextPrivate::GjsContextPrivate(JSContext* cx, GjsContext* public_context)
         g_error("Failed to define module global in internal global.");
     }
 
-    if (!gjs_load_internal_module(cx, "internalLoader")) {
+    if (!gjs_load_internal_module(cx, "loader")) {
         gjs_log_exception(cx);
         g_error("Failed to load internal module loaders.");
     }
 
-    load_context_module(cx,
-                        "resource:///org/gnome/gjs/modules/internal/loader.js",
-                        "module loader");
-
     {
         JSAutoRealm ar(cx, global);
+
         load_context_module(
             cx, "resource:///org/gnome/gjs/modules/esm/_bootstrap/default.js",
             "ESM bootstrap");
     }
 
+    m_main_loop.spin(this);
+
     start_draining_job_queue();
 }
 
@@ -1258,6 +1317,13 @@ bool GjsContextPrivate::handle_exit_code(bool no_sync_error_pending,
         return false;
     }
 
+    if (m_unhandled_exception) {
+        g_set_error(error, GJS_ERROR, GJS_ERROR_FAILED,
+                    "%s %s threw an exception", source_type, identifier);
+        *exit_code = 1;
+        return false;
+    }
+
     // Assume success if no error was thrown and should exit isn't
     // set
     if (no_sync_error_pending) {
@@ -1349,8 +1415,16 @@ bool GjsContextPrivate::eval_module(const char* identifier,
         return false;
     }
 
-    JS::RootedValue ignore(m_cx);
-    bool ok = JS::ModuleEvaluate(m_cx, obj, &ignore);
+    JS::RootedValue evaluationPromise(m_cx);
+    bool ok = JS::ModuleEvaluate(m_cx, obj, &evaluationPromise);
+
+    if (ok) {
+        GjsContextPrivate::from_cx(m_cx)->main_loop_hold();
+
+        ok = add_promise_reactions(
+            m_cx, evaluationPromise, on_context_module_resolved,
+            on_context_module_rejected_fallible, "context");
+    }
 
     if (ok)
         m_main_loop.spin(this);
diff --git a/gjs/engine.cpp b/gjs/engine.cpp
index 1da606b8d..9468947c3 100644
--- a/gjs/engine.cpp
+++ b/gjs/engine.cpp
@@ -169,7 +169,7 @@ JSContext* gjs_create_js_context(GjsContextPrivate* uninitialized_gjs) {
     }
     JS::ContextOptionsRef(cx)
         .setAsmJS(enable_jit)
-        .setTopLevelAwait(false)
+        .setTopLevelAwait(true)
         .setPrivateClassFields(true)
         .setPrivateClassMethods(true);
 
diff --git a/gjs/internal.cpp b/gjs/internal.cpp
index 9b2e552a6..0533f549c 100644
--- a/gjs/internal.cpp
+++ b/gjs/internal.cpp
@@ -88,15 +88,8 @@ bool gjs_load_internal_module(JSContext* cx, const char* identifier) {
     JS::RootedObject internal_global(cx, gjs_get_internal_global(cx));
     JSAutoRealm ar(cx, internal_global);
 
-    JS::RootedObject module(cx, JS::CompileModule(cx, options, buf));
-    JS::RootedObject registry(cx, gjs_get_module_registry(internal_global));
-
-    JS::RootedId key(cx, gjs_intern_string_to_id(cx, full_path));
-
-    JS::RootedValue ignore(cx);
-    if (!gjs_global_registry_set(cx, registry, key, module) ||
-        !JS::ModuleInstantiate(cx, module) ||
-        !JS::ModuleEvaluate(cx, module, &ignore)) {
+    JS::RootedValue ignored(cx);
+    if (!JS::Evaluate(cx, options, buf, &ignored)) {
         return false;
     }
 
diff --git a/gjs/module.cpp b/gjs/module.cpp
index d1aee4941..bea00dd8b 100644
--- a/gjs/module.cpp
+++ b/gjs/module.cpp
@@ -540,7 +540,7 @@ JSObject* gjs_module_resolve(JSContext* cx, JS::HandleValue importingModulePriv,
 // Can fail in JS::FinishDynamicModuleImport(), but will assert if anything
 // fails in fetching the stashed values, since that would be a serious GJS bug.
 GJS_JSAPI_RETURN_CONVENTION
-static bool finish_import(JSContext* cx, JS::DynamicImportStatus status,
+static bool finish_import(JSContext* cx, JS::HandleObject evaluationPromise,
                           const JS::CallArgs& args) {
     GjsContextPrivate* priv = GjsContextPrivate::from_cx(cx);
     priv->main_loop_release();
@@ -566,20 +566,15 @@ static bool finish_import(JSContext* cx, JS::DynamicImportStatus status,
 
     args.rval().setUndefined();
 
-    return JS::FinishDynamicModuleImport_NoTLA(
-        cx, status, importing_module_priv, module_request, internal_promise);
+    return JS::FinishDynamicModuleImport(cx, evaluationPromise,
+                                         importing_module_priv, module_request,
+                                         internal_promise);
 }
 
 // Failing a JSAPI function may result either in an exception pending on the
 // context, in which case we must call JS::FinishDynamicModuleImport() to reject
 // the internal promise; or in an uncatchable exception such as OOM, in which
 // case we must not call JS::FinishDynamicModuleImport().
-GJS_JSAPI_RETURN_CONVENTION
-static bool fail_import(JSContext* cx, const JS::CallArgs& args) {
-    if (JS_IsExceptionPending(cx))
-        return finish_import(cx, JS::DynamicImportStatus::Failed, args);
-    return false;
-}
 
 GJS_JSAPI_RETURN_CONVENTION
 static bool import_rejected(JSContext* cx, unsigned argc, JS::Value* vp) {
@@ -591,14 +586,12 @@ static bool import_rejected(JSContext* cx, unsigned argc, JS::Value* vp) {
     // FinishDynamicModuleImport will reject the internal_promise with it.
     JS_SetPendingException(cx, args.get(0),
                            JS::ExceptionStackBehavior::DoNotCapture);
-
-    return finish_import(cx, JS::DynamicImportStatus::Failed, args);
+    return finish_import(cx, nullptr, args);
 }
 
 GJS_JSAPI_RETURN_CONVENTION
 static bool import_resolved(JSContext* cx, unsigned argc, JS::Value* vp) {
     JS::CallArgs args = JS::CallArgsFromVp(argc, vp);
-
     gjs_debug(GJS_DEBUG_IMPORTER, "Async import promise resolved");
 
     JS::RootedObject global(cx, gjs_get_import_global(cx));
@@ -607,12 +600,14 @@ static bool import_resolved(JSContext* cx, unsigned argc, JS::Value* vp) {
     g_assert(args[0].isObject());
     JS::RootedObject module(cx, &args[0].toObject());
 
-    JS::RootedValue ignore(cx);
+    JS::RootedValue evaluationPromise(cx);
     if (!JS::ModuleInstantiate(cx, module) ||
-        !JS::ModuleEvaluate(cx, module, &ignore))
-        return fail_import(cx, args);
+        !JS::ModuleEvaluate(cx, module, &evaluationPromise))
+        return finish_import(cx, nullptr, args);
 
-    return finish_import(cx, JS::DynamicImportStatus::Ok, args);
+    JS::RootedObject evaluationPromiseObject(
+        cx, evaluationPromise.toObjectOrNull());
+    return finish_import(cx, evaluationPromiseObject, args);
 }
 
 bool gjs_dynamic_module_resolve(JSContext* cx,
diff --git a/js.gresource.xml b/js.gresource.xml
index 4d3fde355..e12dea125 100644
--- a/js.gresource.xml
+++ b/js.gresource.xml
@@ -4,7 +4,6 @@
 <gresources>
   <gresource prefix="/org/gnome/gjs">
     <!-- Internal modules -->
-    <file>modules/internal/internalLoader.js</file>
     <file>modules/internal/loader.js</file>
 
     <!-- ESM-based modules -->
diff --git a/modules/internal/loader.js b/modules/internal/loader.js
index 187933965..599533828 100644
--- a/modules/internal/loader.js
+++ b/modules/internal/loader.js
@@ -1,7 +1,223 @@
 // SPDX-License-Identifier: MIT OR LGPL-2.0-or-later
 // SPDX-FileCopyrightText: 2020 Evan Welsh <contact evanwelsh com>
 
-import {ImportError, InternalModuleLoader, ModulePrivate} from './internalLoader.js';
+/** @typedef {{ uri: string; scheme: string; host: string; path: string; query: Query }} Uri */
+
+/**
+ * Use '__internal: never' to prevent any object from being type compatible with Module
+ * because it is an internal type.
+ *
+ * @typedef {{__internal: never;}} Module
+ */
+/** @typedef {typeof moduleGlobalThis | typeof globalThis} Global */
+/** @typedef {{ load(uri: Uri): [contents: string, internal: boolean]; }} SchemeHandler */
+/** @typedef {{ [key: string]: string | undefined; }} Query */
+/** @typedef {(uri: string, contents: string) => Module} CompileFunc */
+
+/**
+ * Thrown when there is an error importing a module.
+ */
+class ImportError extends Error {
+    /**
+     * @param {string | undefined} message the import error message
+     */
+    constructor(message) {
+        super(message);
+
+        this.name = 'ImportError';
+    }
+}
+
+/**
+ * ModulePrivate is the "private" object of every module.
+ */
+class ModulePrivate {
+    /**
+     *
+     * @param {string} id the module's identifier
+     * @param {string} uri the module's URI
+     * @param {boolean} [internal] whether this module is "internal"
+     */
+    constructor(id, uri, internal = false) {
+        this.id = id;
+        this.uri = uri;
+        this.internal = internal;
+    }
+}
+
+/**
+ * Returns whether a string represents a relative path (e.g. ./, ../)
+ *
+ * @param {string} path a path to check if relative
+ * @returns {boolean}
+ */
+function isRelativePath(path) {
+    // Check if the path is relative. Note that this doesn't mean "relative
+    // path" in the GLib sense, as in "not absolute" — it means a relative path
+    // module specifier, which must start with a '.' or '..' path component.
+    return path.startsWith('./') || path.startsWith('../');
+}
+
+/**
+ * Handles resolving and loading URIs.
+ *
+ * @class
+ */
+class InternalModuleLoader {
+    /**
+     * @param {typeof globalThis} global the global object to handle module
+     *   resolution
+     * @param {(string, string) => import("../types").Module} compileFunc the
+     *   function to compile a source into a module for a particular global
+     *   object. Should be compileInternalModule() for InternalModuleLoader,
+     *   but overridden in ModuleLoader
+     */
+    constructor(global, compileFunc) {
+        this.global = global;
+        this.compileFunc = compileFunc;
+    }
+
+    /**
+     * Loads a file or resource URI synchronously
+     *
+     * @param {Uri} uri the file or resource URI to load
+     * @returns {[contents: string, internal?: boolean] | null}
+     */
+    loadURI(uri) {
+        if (uri.scheme === 'file' || uri.scheme === 'resource')
+            return [loadResourceOrFile(uri.uri)];
+
+        return null;
+    }
+
+    /**
+     * Resolves an import specifier given an optional parent importer.
+     *
+     * @param {string} specifier the import specifier
+     * @param {string | null} [parentURI] the URI of the module importing the specifier
+     * @returns {Uri | null}
+     */
+    resolveSpecifier(specifier, parentURI = null) {
+        try {
+            const uri = parseURI(specifier);
+
+            if (uri)
+                return uri;
+        } catch (err) {
+            // If it can't be parsed as a URI, try a relative path or return null.
+        }
+
+        if (isRelativePath(specifier)) {
+            if (!parentURI)
+                throw new ImportError('Cannot import relative path when module path is unknown.');
+
+            return this.resolveRelativePath(specifier, parentURI);
+        }
+
+        return null;
+    }
+
+    /**
+     * Resolves a path relative to a URI, throwing an ImportError if
+     * the parentURI isn't valid.
+     *
+     * @param {string} relativePath the relative path to resolve against the base URI
+     * @param {string} importingModuleURI the URI of the module triggering this
+     *   resolve
+     * @returns {Uri}
+     */
+    resolveRelativePath(relativePath, importingModuleURI) {
+        // Ensure the parent URI is valid.
+        parseURI(importingModuleURI);
+
+        // Handle relative imports from URI-based modules.
+        const relativeURI = resolveRelativeResourceOrFile(importingModuleURI, relativePath);
+        if (!relativeURI)
+            throw new ImportError('File does not have a valid parent!');
+        return parseURI(relativeURI);
+    }
+
+    /**
+     * Compiles a module source text with the module's URI
+     *
+     * @param {ModulePrivate} priv a module private object
+     * @param {string} text the module source text to compile
+     * @returns {Module}
+     */
+    compileModule(priv, text) {
+        const compiled = this.compileFunc(priv.uri, text);
+
+        setModulePrivate(compiled, priv);
+
+        return compiled;
+    }
+
+    /**
+     * @param {string} specifier the specifier (e.g. relative path, root package) to resolve
+     * @param {string | null} importingModuleURI the URI of the module
+     *   triggering this resolve
+     *
+     * @returns {Module | null}
+     */
+    resolveModule(specifier, importingModuleURI) {
+        const registry = getRegistry(this.global);
+
+        // Check if the module has already been loaded
+        let module = registry.get(specifier);
+        if (module)
+            return module;
+
+        // 1) Resolve path and URI-based imports.
+        const uri = this.resolveSpecifier(specifier, importingModuleURI);
+        if (uri) {
+            module = registry.get(uri.uri);
+
+            // Check if module is already loaded (relative handling)
+            if (module)
+                return module;
+
+            const result = this.loadURI(uri);
+            if (!result)
+                return null;
+
+            const [text, internal = false] = result;
+
+            const priv = new ModulePrivate(uri.uri, uri.uri, internal);
+            const compiled = this.compileModule(priv, text);
+
+            registry.set(uri.uri, compiled);
+            return compiled;
+        }
+
+        return null;
+    }
+
+    moduleResolveHook(importingModulePriv, specifier) {
+        const resolved = this.resolveModule(specifier, importingModulePriv.uri ?? null);
+        if (!resolved)
+            throw new ImportError(`Module not found: ${specifier}`);
+
+        return resolved;
+    }
+
+    moduleLoadHook(id, uri) {
+        const priv = new ModulePrivate(id, uri);
+
+        const result = this.loadURI(parseURI(uri));
+        // result can only be null if `this` is InternalModuleLoader. If `this`
+        // is ModuleLoader, then loadURI() will have thrown
+        if (!result)
+            throw new ImportError(`URI not found: ${uri}`);
+
+        const [text] = result;
+        const compiled = this.compileModule(priv, text);
+
+        const registry = getRegistry(this.global);
+        registry.set(id, compiled);
+
+        return compiled;
+    }
+}
 
 class ModuleLoader extends InternalModuleLoader {
     /**
@@ -24,7 +240,7 @@ class ModuleLoader extends InternalModuleLoader {
         ]);
 
         /**
-         * @type {Map<string, import("./internalLoader.js").SchemeHandler>}
+         * @type {Map<string, SchemeHandler>}
          *
          * A map of handlers for URI schemes (e.g. gi://)
          */
@@ -49,7 +265,7 @@ class ModuleLoader extends InternalModuleLoader {
 
     /**
      * @param {string} scheme the URI scheme to register
-     * @param {import("./internalLoader.js").SchemeHandler} handler a handler
+     * @param {SchemeHandler} handler a handler
      */
     registerScheme(scheme, handler) {
         this.schemeHandlers.set(scheme, handler);
@@ -58,7 +274,7 @@ class ModuleLoader extends InternalModuleLoader {
     /**
      * Overrides InternalModuleLoader.loadURI
      *
-     * @param {import("./internalLoader.js").Uri} uri a Uri object to load
+     * @param {Uri} uri a Uri object to load
      */
     loadURI(uri) {
         if (uri.scheme) {
@@ -110,7 +326,7 @@ class ModuleLoader extends InternalModuleLoader {
      * Resolves a module import with optional handling for relative imports.
      * Overrides InternalModuleLoader.moduleResolveHook
      *
-     * @param {import("./internalLoader.js").ModulePrivate} importingModulePriv
+     * @param {ModulePrivate} importingModulePriv
      *   the private object of the module initiating the import
      * @param {string} specifier the module specifier to resolve for an import
      * @returns {import("./internalLoader").Module}
@@ -223,7 +439,7 @@ function generateGIModule(namespace, version) {
 
 moduleLoader.registerScheme('gi', {
     /**
-     * @param {import("./internalLoader.js").Uri} uri the URI to load
+     * @param {Uri} uri the URI to load
      */
     load(uri) {
         const namespace = uri.host;
@@ -232,10 +448,11 @@ moduleLoader.registerScheme('gi', {
         return [generateGIModule(namespace, version), true];
     },
     /**
-     * @param {import("./internalLoader.js").Uri} uri the URI to load asynchronously
+     * @param {Uri} uri the URI to load asynchronously
      */
     loadAsync(uri) {
         // gi: only does string manipulation, so it is safe to use the same code for sync and async.
         return this.load(uri);
     },
 });
+


[Date Prev][Date Next]   [Thread Prev][Thread Next]   [Thread Index] [Date Index] [Author Index]