Jelajahi Sumber

LibJS: Implement HostResolveImportedModule for LibJS

This loads modules with relative paths from the referencing module.
In this commit the only way to run a module is via the interpreter
which can link and evaluate a module (and all its dependencies).
davidot 3 tahun lalu
induk
melakukan
779e677467

+ 24 - 4
Userland/Libraries/LibJS/Interpreter.cpp

@@ -130,11 +130,31 @@ ThrowCompletionOr<Value> Interpreter::run(Script& script_record)
     return *result.value();
 }
 
-ThrowCompletionOr<Value> Interpreter::run(SourceTextModule&)
+ThrowCompletionOr<Value> Interpreter::run(SourceTextModule& module)
 {
-    auto* error = InternalError::create(global_object(), "Can't run modules directly yet :^(");
-    vm().throw_exception(global_object(), Value { error });
-    return throw_completion(error);
+    // FIXME: This is not a entry point as defined in the spec, but is convenient.
+    //        To avoid work we use link_and_eval_module however that can already be
+    //        dangerous if the vm loaded other modules.
+    auto& vm = this->vm();
+    VERIFY(!vm.exception());
+
+    VM::InterpreterExecutionScope scope(*this);
+
+    auto evaluated_or_error = vm.link_and_eval_module({}, module);
+    // This error does not set vm.exception so we set that here for the stuff that needs it
+    if (evaluated_or_error.is_throw_completion()) {
+        auto* error = vm.heap().allocate<Exception>(global_object(), *(evaluated_or_error.throw_completion().value()));
+        vm.set_exception(*error);
+        return evaluated_or_error.throw_completion();
+    }
+    VERIFY(!vm.exception());
+
+    vm.run_queued_promise_jobs();
+
+    vm.run_queued_finalization_registry_cleanup_jobs();
+
+    VERIFY(!vm.exception());
+    return js_undefined();
 }
 
 GlobalObject& Interpreter::global_object()

+ 1 - 0
Userland/Libraries/LibJS/Runtime/ErrorTypes.h

@@ -67,6 +67,7 @@
     M(JsonMalformed, "Malformed JSON string")                                                                                           \
     M(MissingRequiredProperty, "Required property {} is missing or undefined")                                                          \
     M(ModuleNoEnvironment, "Cannot find module environment for imported binding")                                                       \
+    M(ModuleNotFound, "Cannot find/open module: '{}'")                                                                                  \
     M(NegativeExponent, "Exponent must be positive")                                                                                    \
     M(NonExtensibleDefine, "Cannot define property {} on non-extensible object")                                                        \
     M(NotAConstructor, "{} is not a constructor")                                                                                       \

+ 167 - 1
Userland/Libraries/LibJS/Runtime/VM.cpp

@@ -1,14 +1,16 @@
 /*
  * Copyright (c) 2020-2021, Andreas Kling <kling@serenityos.org>
  * Copyright (c) 2020-2022, Linus Groh <linusg@serenityos.org>
- * Copyright (c) 2021, David Tuin <davidot@serenityos.org>
+ * Copyright (c) 2021-2022, David Tuin <davidot@serenityos.org>
  *
  * SPDX-License-Identifier: BSD-2-Clause
  */
 
 #include <AK/Debug.h>
+#include <AK/LexicalPath.h>
 #include <AK/ScopeGuard.h>
 #include <AK/StringBuilder.h>
+#include <LibCore/File.h>
 #include <LibJS/Interpreter.h>
 #include <LibJS/Runtime/AbstractOperations.h>
 #include <LibJS/Runtime/Array.h>
@@ -26,6 +28,7 @@
 #include <LibJS/Runtime/Symbol.h>
 #include <LibJS/Runtime/TemporaryClearException.h>
 #include <LibJS/Runtime/VM.h>
+#include <LibJS/SourceTextModule.h>
 
 namespace JS {
 
@@ -43,6 +46,10 @@ VM::VM(OwnPtr<CustomData> custom_data)
         m_single_ascii_character_strings[i] = m_heap.allocate_without_global_object<PrimitiveString>(String::formatted("{:c}", i));
     }
 
+    host_resolve_imported_module = [&](ScriptOrModule referencing_script_or_module, ModuleRequest const& specifier) {
+        return resolve_imported_module(move(referencing_script_or_module), specifier);
+    };
+
 #define __JS_ENUMERATE(SymbolName, snake_name) \
     m_well_known_symbol_##snake_name = js_symbol(*this, "Symbol." #SymbolName, false);
     JS_ENUMERATE_WELL_KNOWN_SYMBOLS
@@ -682,4 +689,163 @@ ScriptOrModule VM::get_active_script_or_module() const
     return m_execution_context_stack[0]->script_or_module;
 }
 
+VM::StoredModule* VM::get_stored_module(ScriptOrModule const&, String const& filepath)
+{
+    // Note the spec says:
+    // Each time this operation is called with a specific referencingScriptOrModule, specifier pair as arguments
+    // it must return the same Module Record instance if it completes normally.
+    // Currently, we ignore the referencing script or module but this might not be correct in all cases.
+    auto end_or_module = m_loaded_modules.find_if([&](StoredModule const& stored_module) {
+        return stored_module.filepath == filepath;
+    });
+    if (end_or_module.is_end())
+        return nullptr;
+    return &(*end_or_module);
+}
+
+ThrowCompletionOr<void> VM::link_and_eval_module(Badge<Interpreter>, SourceTextModule& module)
+{
+    return link_and_eval_module(module);
+}
+
+ThrowCompletionOr<void> VM::link_and_eval_module(SourceTextModule& module)
+{
+    auto filepath = module.filename();
+
+    auto module_or_end = m_loaded_modules.find_if([&](StoredModule const& stored_module) {
+        return stored_module.module.ptr() == &module;
+    });
+
+    StoredModule* stored_module;
+
+    if (module_or_end.is_end()) {
+        dbgln_if(JS_MODULE_DEBUG, "[JS MODULE] Warning introducing module via link_and_eval_module {}", module.filename());
+        if (m_loaded_modules.size() > 0) {
+            dbgln("Using link_and_eval module as entry point is not allowed if it is not the first module!");
+            VERIFY_NOT_REACHED();
+        }
+        m_loaded_modules.empend(
+            &module,
+            module.filename(),
+            module,
+            true);
+        stored_module = &m_loaded_modules.last();
+    } else {
+        stored_module = module_or_end.operator->();
+        if (stored_module->has_once_started_linking) {
+            dbgln_if(JS_MODULE_DEBUG, "[JS MODULE] Module already has started linking once {}", module.filename());
+            return {};
+        }
+        stored_module->has_once_started_linking = true;
+    }
+
+    dbgln_if(JS_MODULE_DEBUG, "[JS MODULE] Linking module {}", filepath);
+    auto linked_or_error = module.link(*this);
+    if (linked_or_error.is_error())
+        return linked_or_error.throw_completion();
+
+    dbgln_if(JS_MODULE_DEBUG, "[JS MODULE] Linking passed, now evaluating module {}", filepath);
+    auto evaluated_or_error = module.evaluate(*this);
+
+    VERIFY(!exception());
+
+    if (evaluated_or_error.is_error())
+        return evaluated_or_error.throw_completion();
+
+    auto* evaluated_value = evaluated_or_error.value();
+
+    run_queued_promise_jobs();
+    VERIFY(m_promise_jobs.is_empty());
+
+    // FIXME: This will break if we start doing promises actually asynchronously.
+    VERIFY(evaluated_value->state() != Promise::State::Pending);
+
+    if (evaluated_value->state() == Promise::State::Rejected) {
+        VERIFY(!exception());
+        return JS::throw_completion(evaluated_value->result());
+    }
+
+    dbgln_if(JS_MODULE_DEBUG, "[JS MODULE] Evaluating passed for module {}", module.filename());
+    return {};
+}
+
+// 16.2.1.7 HostResolveImportedModule ( referencingScriptOrModule, specifier ), https://tc39.es/ecma262/#sec-hostresolveimportedmodule
+ThrowCompletionOr<NonnullRefPtr<Module>> VM::resolve_imported_module(ScriptOrModule referencing_script_or_module, ModuleRequest const& specifier)
+{
+    if (!specifier.assertions.is_empty())
+        return throw_completion<InternalError>(current_realm()->global_object(), ErrorType::NotImplemented, "HostResolveImportedModule with assertions");
+
+    // An implementation of HostResolveImportedModule must conform to the following requirements:
+    //  - If it completes normally, the [[Value]] slot of the completion must contain an instance of a concrete subclass of Module Record.
+    //  - If a Module Record corresponding to the pair referencingScriptOrModule, specifier does not exist or cannot be created, an exception must be thrown.
+    //  - Each time this operation is called with a specific referencingScriptOrModule, specifier pair as arguments it must return the same Module Record instance if it completes normally.
+
+    StringView base_filename = referencing_script_or_module.visit(
+        [&](Empty) {
+            return "."sv;
+        },
+        [&](auto* script_or_module) {
+            return script_or_module->filename();
+        });
+
+    LexicalPath base_path { base_filename };
+    auto filepath = LexicalPath::absolute_path(base_path.dirname(), specifier.module_specifier);
+
+#if JS_MODULE_DEBUG
+    String referencing_module_string = referencing_script_or_module.visit(
+        [&](Empty) -> String {
+            return ".";
+        },
+        [&](auto* script_or_module) {
+            if constexpr (IsSame<Script*, decltype(script_or_module)>) {
+                return String::formatted("Script @ {}", script_or_module);
+            }
+            return String::formatted("Module @ {}", script_or_module);
+        });
+
+    dbgln_if(JS_MODULE_DEBUG, "[JS MODULE] resolve_imported_module({}, {})", referencing_module_string, filepath);
+    dbgln_if(JS_MODULE_DEBUG, "[JS MODULE]     resolved {} + {} -> {}", base_path, specifier.module_specifier, filepath);
+#endif
+
+    auto* loaded_module_or_end = get_stored_module(referencing_script_or_module, filepath);
+    if (loaded_module_or_end != nullptr) {
+        dbgln_if(JS_MODULE_DEBUG, "[JS MODULE] resolve_imported_module({}) already loaded at {}", filepath, loaded_module_or_end->module.ptr());
+        return loaded_module_or_end->module;
+    }
+
+    dbgln_if(JS_MODULE_DEBUG, "[JS MODULE] reading and parsing module {}", filepath);
+
+    auto& global_object = current_realm()->global_object();
+
+    auto file_or_error = Core::File::open(filepath, Core::OpenMode::ReadOnly);
+
+    if (file_or_error.is_error()) {
+        return throw_completion<SyntaxError>(global_object, ErrorType::ModuleNotFound, specifier.module_specifier);
+    }
+
+    // FIXME: Don't read the file in one go.
+    auto file_content = file_or_error.value()->read_all();
+    StringView content_view { file_content.data(), file_content.size() };
+
+    // Note: We treat all files as module, so if a script does not have exports it just runs it.
+    auto module_or_errors = SourceTextModule::parse(content_view, *current_realm(), filepath);
+
+    if (module_or_errors.is_error()) {
+        VERIFY(module_or_errors.error().size() > 0);
+        return throw_completion<SyntaxError>(global_object, module_or_errors.error().first().to_string());
+    }
+
+    auto module = module_or_errors.release_value();
+    dbgln_if(JS_MODULE_DEBUG, "[JS MODULE] resolve_imported_module(...) parsed {} to {}", filepath, module.ptr());
+
+    // We have to set it here already in case it references itself.
+    m_loaded_modules.empend(
+        referencing_script_or_module,
+        filepath,
+        module,
+        false);
+
+    return module;
+}
+
 }

+ 18 - 1
Userland/Libraries/LibJS/Runtime/VM.h

@@ -1,7 +1,7 @@
 /*
  * Copyright (c) 2020, Andreas Kling <kling@serenityos.org>
  * Copyright (c) 2020-2022, Linus Groh <linusg@serenityos.org>
- * Copyright (c) 2021, David Tuin <davidot@serenityos.org>
+ * Copyright (c) 2021-2022, David Tuin <davidot@serenityos.org>
  *
  * SPDX-License-Identifier: BSD-2-Clause
  */
@@ -239,6 +239,9 @@ public:
     void save_execution_context_stack();
     void restore_execution_context_stack();
 
+    // Do not call this method unless you are sure this is the only and first module to be loaded in this vm.
+    ThrowCompletionOr<void> link_and_eval_module(Badge<Interpreter>, SourceTextModule& module);
+
     ScriptOrModule get_active_script_or_module() const;
 
     Function<ThrowCompletionOr<NonnullRefPtr<Module>>(ScriptOrModule, ModuleRequest const&)> host_resolve_imported_module;
@@ -256,6 +259,9 @@ private:
     ThrowCompletionOr<void> property_binding_initialization(BindingPattern const& binding, Value value, Environment* environment, GlobalObject& global_object);
     ThrowCompletionOr<void> iterator_binding_initialization(BindingPattern const& binding, Iterator& iterator_record, Environment* environment, GlobalObject& global_object);
 
+    ThrowCompletionOr<NonnullRefPtr<Module>> resolve_imported_module(ScriptOrModule referencing_script_or_module, ModuleRequest const& specifier);
+    ThrowCompletionOr<void> link_and_eval_module(SourceTextModule& module);
+
     Exception* m_exception { nullptr };
 
     HashMap<String, PrimitiveString*> m_string_cache;
@@ -278,6 +284,17 @@ private:
     PrimitiveString* m_empty_string { nullptr };
     PrimitiveString* m_single_ascii_character_strings[128] {};
 
+    struct StoredModule {
+        ScriptOrModule referencing_script_or_module;
+        String filepath;
+        NonnullRefPtr<Module> module;
+        bool has_once_started_linking { false };
+    };
+
+    StoredModule* get_stored_module(ScriptOrModule const& script_or_module, String const& filepath);
+
+    Vector<StoredModule> m_loaded_modules;
+
 #define __JS_ENUMERATE(SymbolName, snake_name) \
     Symbol* m_well_known_symbol_##snake_name { nullptr };
     JS_ENUMERATE_WELL_KNOWN_SYMBOLS