diff options
author | davidot <davidot@serenityos.org> | 2022-01-19 01:22:58 +0100 |
---|---|---|
committer | Linus Groh <mail@linusgroh.de> | 2022-01-22 01:21:18 +0000 |
commit | 779e6774670632d5430244a5d3d7789f4942c161 (patch) | |
tree | 9ea8bd64e8f8cb647cf0b11d37377e7845a0b5af /Userland/Libraries | |
parent | 1b8ccf9a6688b6880b73a1dfab934cceadb4c951 (diff) | |
download | serenity-779e6774670632d5430244a5d3d7789f4942c161.zip |
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).
Diffstat (limited to 'Userland/Libraries')
-rw-r--r-- | Userland/Libraries/LibJS/Interpreter.cpp | 28 | ||||
-rw-r--r-- | Userland/Libraries/LibJS/Runtime/ErrorTypes.h | 1 | ||||
-rw-r--r-- | Userland/Libraries/LibJS/Runtime/VM.cpp | 168 | ||||
-rw-r--r-- | Userland/Libraries/LibJS/Runtime/VM.h | 19 |
4 files changed, 210 insertions, 6 deletions
diff --git a/Userland/Libraries/LibJS/Interpreter.cpp b/Userland/Libraries/LibJS/Interpreter.cpp index 1aee2a2444..8fda3bad4c 100644 --- a/Userland/Libraries/LibJS/Interpreter.cpp +++ b/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() diff --git a/Userland/Libraries/LibJS/Runtime/ErrorTypes.h b/Userland/Libraries/LibJS/Runtime/ErrorTypes.h index 826cb74e3f..b83c4bf899 100644 --- a/Userland/Libraries/LibJS/Runtime/ErrorTypes.h +++ b/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") \ diff --git a/Userland/Libraries/LibJS/Runtime/VM.cpp b/Userland/Libraries/LibJS/Runtime/VM.cpp index 92b9417f90..4773c24459 100644 --- a/Userland/Libraries/LibJS/Runtime/VM.cpp +++ b/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; +} + } diff --git a/Userland/Libraries/LibJS/Runtime/VM.h b/Userland/Libraries/LibJS/Runtime/VM.h index 2ecc876e97..ba68592c4e 100644 --- a/Userland/Libraries/LibJS/Runtime/VM.h +++ b/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 |