From f568939568b69231d5bebae8a9199ee3359e36e3 Mon Sep 17 00:00:00 2001 From: davidot Date: Thu, 27 Jan 2022 02:44:03 +0100 Subject: [PATCH] LibJS: Implement the import assertions proposal The hard part of parsing them in import statements and calls was already done so this is just removing some check which threw before on assertions. And filtering the assertions based on the result of a new host hook. --- Userland/Libraries/LibJS/AST.cpp | 131 ++++++++++++++---- Userland/Libraries/LibJS/AST.h | 12 ++ Userland/Libraries/LibJS/CyclicModule.cpp | 14 +- Userland/Libraries/LibJS/CyclicModule.h | 4 +- .../Libraries/LibJS/Runtime/PromiseReaction.h | 19 +++ Userland/Libraries/LibJS/Runtime/VM.cpp | 54 +++++--- Userland/Libraries/LibJS/Runtime/VM.h | 10 +- Userland/Libraries/LibJS/SourceTextModule.cpp | 90 +++++++++--- Userland/Libraries/LibJS/SourceTextModule.h | 2 +- .../LibJS/Tests/modules/basic-modules.js | 13 +- .../Tests/modules/import-with-assertions.mjs | 4 + 11 files changed, 270 insertions(+), 83 deletions(-) create mode 100644 Userland/Libraries/LibJS/Tests/modules/import-with-assertions.mjs diff --git a/Userland/Libraries/LibJS/AST.cpp b/Userland/Libraries/LibJS/AST.cpp index ae68e31e90..17a06a2805 100644 --- a/Userland/Libraries/LibJS/AST.cpp +++ b/Userland/Libraries/LibJS/AST.cpp @@ -9,6 +9,7 @@ #include #include #include +#include #include #include #include @@ -3227,41 +3228,111 @@ void ImportCall::dump(int indent) const } // 13.3.10.1 Runtime Semantics: Evaluation, https://tc39.es/ecma262/#sec-import-call-runtime-semantics-evaluation +// Also includes assertions from proposal: https://tc39.es/proposal-import-assertions/#sec-import-call-runtime-semantics-evaluation Completion ImportCall::execute(Interpreter& interpreter, GlobalObject& global_object) const { InterpreterNodeScope node_scope { interpreter, *this }; - // 1. Let referencingScriptOrModule be ! GetActiveScriptOrModule(). + + // 2.1.1.1 EvaluateImportCall ( specifierExpression [ , optionsExpression ] ), https://tc39.es/proposal-import-assertions/#sec-evaluate-import-call + // 1. Let referencingScriptOrModule be ! GetActiveScriptOrModule(). auto referencing_script_or_module = interpreter.vm().get_active_script_or_module(); - if (m_options) - return interpreter.vm().throw_completion(global_object, ErrorType::NotImplemented, "import call with assertions/options"); - - // 2. Let argRef be the result of evaluating AssignmentExpression. - // 3. Let specifier be ? GetValue(argRef). + // 2. Let specifierRef be the result of evaluating specifierExpression. + // 3. Let specifier be ? GetValue(specifierRef). auto specifier = TRY(m_specifier->execute(interpreter, global_object)); - // 4. Let promiseCapability be ! NewPromiseCapability(%Promise%). + auto options_value = js_undefined(); + // 4. If optionsExpression is present, then + if (m_options) { + // a. Let optionsRef be the result of evaluating optionsExpression. + // b. Let options be ? GetValue(optionsRef). + options_value = TRY(m_options->execute(interpreter, global_object)).release_value(); + } + // 5. Else, + // a. Let options be undefined. + // Note: options_value is undefined by default. + + // 6. Let promiseCapability be ! NewPromiseCapability(%Promise%). auto promise_capability = MUST(new_promise_capability(global_object, global_object.promise_constructor())); - VERIFY(!interpreter.exception()); - // 5. Let specifierString be ToString(specifier). - auto specifier_string = specifier->to_string(global_object); + // 7. Let specifierString be ToString(specifier). + // 8. IfAbruptRejectPromise(specifierString, promiseCapability). + auto specifier_string = TRY_OR_REJECT_WITH_VALUE(global_object, promise_capability, specifier->to_string(global_object)); - // 6. IfAbruptRejectPromise(specifierString, promiseCapability). - // Note: Since we have to use completions and not ThrowCompletionOr's in AST we have to do this manually. - if (specifier_string.is_throw_completion()) { - // FIXME: We shouldn't have to clear this exception - interpreter.vm().clear_exception(); - (void)TRY(call(global_object, promise_capability.reject, js_undefined(), *specifier_string.throw_completion().value())); - return Value { promise_capability.promise }; + // 9. Let assertions be a new empty List. + Vector assertions; + + // 10. If options is not undefined, then + if (!options_value.is_undefined()) { + // a. If Type(options) is not Object, + if (!options_value.is_object()) { + auto* error = TypeError::create(global_object, String::formatted(ErrorType::NotAnObject.message(), "ImportOptions")); + // i. Perform ! Call(promiseCapability.[[Reject]], undefined, « a newly created TypeError object »). + MUST(call(global_object, *promise_capability.reject, js_undefined(), error)); + + // ii. Return promiseCapability.[[Promise]]. + return Value { promise_capability.promise }; + } + + // b. Let assertionsObj be Get(options, "assert"). + // c. IfAbruptRejectPromise(assertionsObj, promiseCapability). + auto assertion_object = TRY_OR_REJECT_WITH_VALUE(global_object, promise_capability, options_value.get(global_object, interpreter.vm().names.assert)); + + // d. If assertionsObj is not undefined, + if (!assertion_object.is_undefined()) { + // i. If Type(assertionsObj) is not Object, + if (!assertion_object.is_object()) { + auto* error = TypeError::create(global_object, String::formatted(ErrorType::NotAnObject.message(), "ImportOptionsAssertions")); + // 1. Perform ! Call(promiseCapability.[[Reject]], undefined, « a newly created TypeError object »). + MUST(call(global_object, *promise_capability.reject, js_undefined(), error)); + + // 2. Return promiseCapability.[[Promise]]. + return Value { promise_capability.promise }; + } + + // ii. Let keys be EnumerableOwnPropertyNames(assertionsObj, key). + // iii. IfAbruptRejectPromise(keys, promiseCapability). + auto keys = TRY_OR_REJECT_WITH_VALUE(global_object, promise_capability, assertion_object.as_object().enumerable_own_property_names(Object::PropertyKind::Key)); + + // iv. Let supportedAssertions be ! HostGetSupportedImportAssertions(). + auto supported_assertions = interpreter.vm().host_get_supported_import_assertions(); + + // v. For each String key of keys, + for (auto const& key : keys) { + auto property_key = MUST(key.to_property_key(global_object)); + + // 1. Let value be Get(assertionsObj, key). + // 2. IfAbruptRejectPromise(value, promiseCapability). + auto value = TRY_OR_REJECT_WITH_VALUE(global_object, promise_capability, assertion_object.get(global_object, property_key)); + + // 3. If Type(value) is not String, then + if (!value.is_string()) { + auto* error = TypeError::create(global_object, String::formatted(ErrorType::NotAString.message(), "Import Assertion option value")); + // a. Perform ! Call(promiseCapability.[[Reject]], undefined, « a newly created TypeError object »). + MUST(call(global_object, *promise_capability.reject, js_undefined(), error)); + + // b. Return promiseCapability.[[Promise]]. + return Value { promise_capability.promise }; + } + + // 4. If supportedAssertions contains key, then + if (supported_assertions.contains_slow(property_key.to_string())) { + // a. Append { [[Key]]: key, [[Value]]: value } to assertions. + assertions.empend(property_key.to_string(), value.as_string().string()); + } + } + } + // e. Sort assertions by the code point order of the [[Key]] of each element. NOTE: This sorting is observable only in that hosts are prohibited from distinguishing among assertions by the order they occur in. + // Note: This is done when constructing the ModuleRequest. } - ModuleRequest request { specifier_string.release_value() }; + // 11. Let moduleRequest be a new ModuleRequest Record { [[Specifier]]: specifierString, [[Assertions]]: assertions }. + ModuleRequest request { specifier_string, assertions }; - // 7. Perform ! HostImportModuleDynamically(referencingScriptOrModule, specifierString, promiseCapability). - interpreter.vm().host_import_module_dynamically(referencing_script_or_module, request, promise_capability); + // 12. Perform ! HostImportModuleDynamically(referencingScriptOrModule, moduleRequest, promiseCapability). + interpreter.vm().host_import_module_dynamically(referencing_script_or_module, move(request), promise_capability); - // 8. Return promiseCapability.[[Promise]]. + // 13. Return promiseCapability.[[Promise]]. return Value { promise_capability.promise }; } @@ -4110,13 +4181,10 @@ void ScopeNode::add_hoisted_function(NonnullRefPtr declarat } // 16.2.1.11 Runtime Semantics: Evaluation, https://tc39.es/ecma262/#sec-module-semantics-runtime-semantics-evaluation -Completion ImportStatement::execute(Interpreter& interpreter, GlobalObject& global_object) const +Completion ImportStatement::execute(Interpreter& interpreter, GlobalObject&) const { InterpreterNodeScope node_scope { interpreter, *this }; - if (!m_module_request.assertions.is_empty()) - return interpreter.vm().throw_completion(global_object, ErrorType::NotImplemented, "import statement with assertions/options"); - // 1. Return NormalCompletion(empty). return normal_completion({}); } @@ -4460,4 +4528,17 @@ ThrowCompletionOr Program::global_declaration_instantiation(Interpreter& i return {}; } +ModuleRequest::ModuleRequest(FlyString module_specifier_, Vector assertions_) + : module_specifier(move(module_specifier_)) + , assertions(move(assertions_)) +{ + // Perform step 10.e. from EvaluateImportCall, https://tc39.es/proposal-import-assertions/#sec-evaluate-import-call + // or step 2. from 2.7 Static Semantics: AssertClauseToAssertions, https://tc39.es/proposal-import-assertions/#sec-assert-clause-to-assertions + // e. / 2. Sort assertions by the code point order of the [[Key]] of each element. + // NOTE: This sorting is observable only in that hosts are prohibited from distinguishing among assertions by the order they occur in. + quick_sort(assertions, [](Assertion const& lhs, Assertion const& rhs) { + return lhs.key < rhs.key; + }); +} + } diff --git a/Userland/Libraries/LibJS/AST.h b/Userland/Libraries/LibJS/AST.h index 1403878a9d..6f58e6357b 100644 --- a/Userland/Libraries/LibJS/AST.h +++ b/Userland/Libraries/LibJS/AST.h @@ -257,6 +257,8 @@ struct ModuleRequest { { } + ModuleRequest(FlyString module_specifier, Vector assertions); + void add_assertion(String key, String value) { assertions.empend(move(key), move(value)); @@ -309,6 +311,7 @@ public: bool has_bound_name(FlyString const& name) const; Vector const& entries() const { return m_entries; } ModuleRequest const& module_request() const { return m_module_request; } + ModuleRequest& module_request() { return m_module_request; } private: ModuleRequest m_module_request; @@ -406,6 +409,12 @@ public: return *m_statement; } + ModuleRequest& module_request() + { + VERIFY(!m_module_request.module_specifier.is_null()); + return m_module_request; + } + private: RefPtr m_statement; Vector m_entries; @@ -448,6 +457,9 @@ public: NonnullRefPtrVector const& imports() const { return m_imports; } NonnullRefPtrVector const& exports() const { return m_exports; } + NonnullRefPtrVector& imports() { return m_imports; } + NonnullRefPtrVector& exports() { return m_exports; } + bool has_top_level_await() const { return m_has_top_level_await; } void set_has_top_level_await() { m_has_top_level_await = true; } diff --git a/Userland/Libraries/LibJS/CyclicModule.cpp b/Userland/Libraries/LibJS/CyclicModule.cpp index 9650222997..9276d01be2 100644 --- a/Userland/Libraries/LibJS/CyclicModule.cpp +++ b/Userland/Libraries/LibJS/CyclicModule.cpp @@ -10,7 +10,7 @@ namespace JS { -CyclicModule::CyclicModule(Realm& realm, StringView filename, bool has_top_level_await, Vector requested_modules) +CyclicModule::CyclicModule(Realm& realm, StringView filename, bool has_top_level_await, Vector requested_modules) : Module(realm, filename) , m_requested_modules(move(requested_modules)) , m_has_top_level_await(has_top_level_await) @@ -93,7 +93,14 @@ ThrowCompletionOr CyclicModule::inner_module_linking(VM& vm, Vector CyclicModule::inner_module_evaluation(VM& vm, Vector evaluate(VM& vm) override; protected: - CyclicModule(Realm& realm, StringView filename, bool has_top_level_await, Vector requested_modules); + CyclicModule(Realm& realm, StringView filename, bool has_top_level_await, Vector requested_modules); virtual ThrowCompletionOr inner_module_linking(VM& vm, Vector& stack, u32 index) override; virtual ThrowCompletionOr inner_module_evaluation(VM& vm, Vector& stack, u32 index) override; @@ -49,7 +49,7 @@ protected: ThrowCompletionOr m_evaluation_error; // [[EvaluationError]] Optional m_dfs_index; // [[DFSIndex]] Optional m_dfs_ancestor_index; // [[DFSAncestorIndex]] - Vector m_requested_modules; // [[RequestedModules]] + Vector m_requested_modules; // [[RequestedModules]] CyclicModule* m_cycle_root; // [[CycleRoot]] bool m_has_top_level_await { false }; // [[HasTLA]] bool m_async_evaluation { false }; // [[AsyncEvaluation]] diff --git a/Userland/Libraries/LibJS/Runtime/PromiseReaction.h b/Userland/Libraries/LibJS/Runtime/PromiseReaction.h index 2d2c941412..bb31e6fc96 100644 --- a/Userland/Libraries/LibJS/Runtime/PromiseReaction.h +++ b/Userland/Libraries/LibJS/Runtime/PromiseReaction.h @@ -38,6 +38,25 @@ struct PromiseCapability { _temporary_try_or_reject_result.release_value(); \ }) +// 27.2.1.1.1 IfAbruptRejectPromise ( value, capability ), https://tc39.es/ecma262/#sec-ifabruptrejectpromise +#define TRY_OR_REJECT_WITH_VALUE(global_object, capability, expression) \ + ({ \ + auto _temporary_try_or_reject_result = (expression); \ + /* 1. If value is an abrupt completion, then */ \ + if (_temporary_try_or_reject_result.is_error()) { \ + global_object.vm().clear_exception(); \ + \ + /* a. Perform ? Call(capability.[[Reject]], undefined, « value.[[Value]] »). */ \ + TRY(JS::call(global_object, *capability.reject, js_undefined(), *_temporary_try_or_reject_result.release_error().value())); \ + \ + /* b. Return capability.[[Promise]]. */ \ + return Value { capability.promise }; \ + } \ + \ + /* 2. Else if value is a Completion Record, set value to value.[[Value]]. */ \ + _temporary_try_or_reject_result.release_value(); \ + }) + // 27.2.1.5 NewPromiseCapability ( C ), https://tc39.es/ecma262/#sec-newpromisecapability ThrowCompletionOr new_promise_capability(GlobalObject& global_object, Value constructor); diff --git a/Userland/Libraries/LibJS/Runtime/VM.cpp b/Userland/Libraries/LibJS/Runtime/VM.cpp index 951a589379..6c80fe2bba 100644 --- a/Userland/Libraries/LibJS/Runtime/VM.cpp +++ b/Userland/Libraries/LibJS/Runtime/VM.cpp @@ -88,6 +88,10 @@ VM::VM(OwnPtr custom_data) host_finalize_import_meta = [&](Object*, SourceTextModule const&) { }; + host_get_supported_import_assertions = [&] { + return Vector {}; + }; + #define __JS_ENUMERATE(SymbolName, snake_name) \ m_well_known_symbol_##snake_name = js_symbol(*this, "Symbol." #SymbolName, false); JS_ENUMERATE_WELL_KNOWN_SYMBOLS @@ -805,15 +809,21 @@ ThrowCompletionOr VM::link_and_eval_module(SourceTextModule& module) } // 16.2.1.7 HostResolveImportedModule ( referencingScriptOrModule, specifier ), https://tc39.es/ecma262/#sec-hostresolveimportedmodule -ThrowCompletionOr> VM::resolve_imported_module(ScriptOrModule referencing_script_or_module, ModuleRequest const& specifier) +ThrowCompletionOr> VM::resolve_imported_module(ScriptOrModule referencing_script_or_module, ModuleRequest const& module_request) { - if (!specifier.assertions.is_empty()) - return throw_completion(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. + // - If a Module Record corresponding to the pair referencingScriptOrModule, moduleRequest does not exist or cannot be created, an exception must be thrown. + // - Each time this operation is called with a specific referencingScriptOrModule, moduleRequest.[[Specifier]], moduleRequest.[[Assertions]] triple + // as arguments it must return the same Module Record instance if it completes normally. + // * It is recommended but not required that implementations additionally conform to the following stronger constraint: + // each time this operation is called with a specific referencingScriptOrModule, moduleRequest.[[Specifier]] pair as arguments it must return the same Module Record instance if it completes normally. + // - moduleRequest.[[Assertions]] must not influence the interpretation of the module or the module specifier; + // instead, it may be used to determine whether the algorithm completes normally or with an abrupt completion. + + // Multiple different referencingScriptOrModule, moduleRequest.[[Specifier]] pairs may map to the same Module Record instance. + // The actual mapping semantic is host-defined but typically a normalization process is applied to specifier as part of the mapping process. + // A typical normalization process would include actions such as alphabetic case folding and expansion of relative and abbreviated path specifiers. StringView base_filename = referencing_script_or_module.visit( [&](Empty) { @@ -824,7 +834,7 @@ ThrowCompletionOr> VM::resolve_imported_module(ScriptOrMod }); LexicalPath base_path { base_filename }; - auto filepath = LexicalPath::absolute_path(base_path.dirname(), specifier.module_specifier); + auto filepath = LexicalPath::absolute_path(base_path.dirname(), module_request.module_specifier); #if JS_MODULE_DEBUG String referencing_module_string = referencing_script_or_module.visit( @@ -839,7 +849,7 @@ ThrowCompletionOr> VM::resolve_imported_module(ScriptOrMod }); 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); + dbgln_if(JS_MODULE_DEBUG, "[JS MODULE] resolved {} + {} -> {}", base_path, module_request.module_specifier, filepath); #endif auto* loaded_module_or_end = get_stored_module(referencing_script_or_module, filepath); @@ -855,7 +865,7 @@ ThrowCompletionOr> VM::resolve_imported_module(ScriptOrMod auto file_or_error = Core::File::open(filepath, Core::OpenMode::ReadOnly); if (file_or_error.is_error()) { - return throw_completion(global_object, ErrorType::ModuleNotFound, specifier.module_specifier); + return throw_completion(global_object, ErrorType::ModuleNotFound, module_request.module_specifier); } // FIXME: Don't read the file in one go. @@ -884,27 +894,27 @@ ThrowCompletionOr> VM::resolve_imported_module(ScriptOrMod } // 16.2.1.8 HostImportModuleDynamically ( referencingScriptOrModule, specifier, promiseCapability ), https://tc39.es/ecma262/#sec-hostimportmoduledynamically -void VM::import_module_dynamically(ScriptOrModule referencing_script_or_module, ModuleRequest const& specifier, PromiseCapability promise_capability) +void VM::import_module_dynamically(ScriptOrModule referencing_script_or_module, ModuleRequest module_request, PromiseCapability promise_capability) { auto& global_object = current_realm()->global_object(); // Success path: - // - At some future time, the host environment must perform FinishDynamicImport(referencingScriptOrModule, specifier, promiseCapability, promise), + // - At some future time, the host environment must perform FinishDynamicImport(referencingScriptOrModule, moduleRequest, promiseCapability, promise), // where promise is a Promise resolved with undefined. // - Any subsequent call to HostResolveImportedModule after FinishDynamicImport has completed, - // given the arguments referencingScriptOrModule and specifier, must complete normally. + // given the arguments referencingScriptOrModule and moduleRequest, must complete normally. // - The completion value of any subsequent call to HostResolveImportedModule after FinishDynamicImport has completed, - // given the arguments referencingScriptOrModule and specifier, must be a module which has already been evaluated, + // given the arguments referencingScriptOrModule and moduleRequest, must be a module which has already been evaluated, // i.e. whose Evaluate concrete method has already been called and returned a normal completion. // Failure path: // - At some future time, the host environment must perform - // FinishDynamicImport(referencingScriptOrModule, specifier, promiseCapability, promise), + // FinishDynamicImport(referencingScriptOrModule, moduleRequest, promiseCapability, promise), // where promise is a Promise rejected with an error representing the cause of failure. auto* promise = Promise::create(global_object); ScopeGuard finish_dynamic_import = [&] { - host_finish_dynamic_import(referencing_script_or_module, specifier, promise_capability, promise); + host_finish_dynamic_import(referencing_script_or_module, move(module_request), promise_capability, promise); }; // Generally within ECMA262 we always get a referencing_script_or_moulde. However, ShadowRealm gives an explicit null. @@ -915,15 +925,15 @@ void VM::import_module_dynamically(ScriptOrModule referencing_script_or_module, // If there is no ScriptOrModule in any of the execution contexts if (referencing_script_or_module.has()) { // Throw an error for now - promise->reject(InternalError::create(global_object, String::formatted(ErrorType::ModuleNotFoundNoReferencingScript.message(), specifier.module_specifier))); + promise->reject(InternalError::create(global_object, String::formatted(ErrorType::ModuleNotFoundNoReferencingScript.message(), module_request.module_specifier))); return; } } VERIFY(!exception()); // Note: If host_resolve_imported_module returns a module it has been loaded successfully and the next call in finish_dynamic_import will retrieve it again. - auto module_or_error = host_resolve_imported_module(referencing_script_or_module, specifier); - dbgln_if(JS_MODULE_DEBUG, "[JS MODULE] HostImportModuleDynamically(..., {}) -> {}", specifier.module_specifier, module_or_error.is_error() ? "failed" : "passed"); + auto module_or_error = host_resolve_imported_module(referencing_script_or_module, module_request); + dbgln_if(JS_MODULE_DEBUG, "[JS MODULE] HostImportModuleDynamically(..., {}) -> {}", module_request.module_specifier, module_or_error.is_error() ? "failed" : "passed"); if (module_or_error.is_throw_completion()) { // Note: We should not leak the exception thrown in host_resolve_imported_module. clear_exception(); @@ -952,17 +962,17 @@ void VM::import_module_dynamically(ScriptOrModule referencing_script_or_module, } // 16.2.1.9 FinishDynamicImport ( referencingScriptOrModule, specifier, promiseCapability, innerPromise ), https://tc39.es/ecma262/#sec-finishdynamicimport -void VM::finish_dynamic_import(ScriptOrModule referencing_script_or_module, ModuleRequest const& specifier, PromiseCapability promise_capability, Promise* inner_promise) +void VM::finish_dynamic_import(ScriptOrModule referencing_script_or_module, ModuleRequest module_request, PromiseCapability promise_capability, Promise* inner_promise) { - dbgln_if(JS_MODULE_DEBUG, "[JS MODULE] finish_dynamic_import on {}", specifier.module_specifier); + dbgln_if(JS_MODULE_DEBUG, "[JS MODULE] finish_dynamic_import on {}", module_request.module_specifier); // 1. Let fulfilledClosure be a new Abstract Closure with parameters (result) that captures referencingScriptOrModule, specifier, and promiseCapability and performs the following steps when called: - auto fulfilled_closure = [referencing_script_or_module, specifier, promise_capability](VM& vm, GlobalObject& global_object) -> ThrowCompletionOr { + auto fulfilled_closure = [referencing_script_or_module, module_request = move(module_request), promise_capability](VM& vm, GlobalObject& global_object) -> ThrowCompletionOr { auto result = vm.argument(0); // a. Assert: result is undefined. VERIFY(result.is_undefined()); // b. Let moduleRecord be ! HostResolveImportedModule(referencingScriptOrModule, specifier). - auto module_record = MUST(vm.host_resolve_imported_module(referencing_script_or_module, specifier)); + auto module_record = MUST(vm.host_resolve_imported_module(referencing_script_or_module, module_request)); // c. Assert: Evaluate has already been invoked on moduleRecord and successfully completed. // Note: If HostResolveImportedModule returns a module evaluate will have been called on it. diff --git a/Userland/Libraries/LibJS/Runtime/VM.h b/Userland/Libraries/LibJS/Runtime/VM.h index 296c4afe91..3bc0394827 100644 --- a/Userland/Libraries/LibJS/Runtime/VM.h +++ b/Userland/Libraries/LibJS/Runtime/VM.h @@ -233,12 +233,14 @@ public: ScriptOrModule get_active_script_or_module() const; Function>(ScriptOrModule, ModuleRequest const&)> host_resolve_imported_module; - Function host_import_module_dynamically; + Function host_import_module_dynamically; Function host_finish_dynamic_import; Function(SourceTextModule const&)> host_get_import_meta_properties; Function host_finalize_import_meta; + Function()> host_get_supported_import_assertions; + void enable_default_host_import_module_dynamically_hook(); private: @@ -247,11 +249,11 @@ private: ThrowCompletionOr property_binding_initialization(BindingPattern const& binding, Value value, Environment* environment, GlobalObject& global_object); ThrowCompletionOr iterator_binding_initialization(BindingPattern const& binding, Iterator& iterator_record, Environment* environment, GlobalObject& global_object); - ThrowCompletionOr> resolve_imported_module(ScriptOrModule referencing_script_or_module, ModuleRequest const& specifier); + ThrowCompletionOr> resolve_imported_module(ScriptOrModule referencing_script_or_module, ModuleRequest const& module_request); ThrowCompletionOr link_and_eval_module(SourceTextModule& module); - void import_module_dynamically(ScriptOrModule referencing_script_or_module, ModuleRequest const& specifier, PromiseCapability promise_capability); - void finish_dynamic_import(ScriptOrModule referencing_script_or_module, ModuleRequest const& specifier, PromiseCapability promise_capability, Promise* inner_promise); + void import_module_dynamically(ScriptOrModule referencing_script_or_module, ModuleRequest module_request, PromiseCapability promise_capability); + void finish_dynamic_import(ScriptOrModule referencing_script_or_module, ModuleRequest module_request, PromiseCapability promise_capability, Promise* inner_promise); Exception* m_exception { nullptr }; diff --git a/Userland/Libraries/LibJS/SourceTextModule.cpp b/Userland/Libraries/LibJS/SourceTextModule.cpp index e198690cab..7c6487f366 100644 --- a/Userland/Libraries/LibJS/SourceTextModule.cpp +++ b/Userland/Libraries/LibJS/SourceTextModule.cpp @@ -13,49 +13,92 @@ namespace JS { +// 2.7 Static Semantics: AssertClauseToAssertions, https://tc39.es/proposal-import-assertions/#sec-assert-clause-to-assertions +static Vector assert_clause_to_assertions(Vector const& source_assertions, Vector const& supported_import_assertions) +{ + // AssertClause : assert { AssertEntries ,opt } + // 1. Let assertions be AssertClauseToAssertions of AssertEntries. + Vector assertions; + + // AssertEntries : AssertionKey : StringLiteral + // AssertEntries : AssertionKey : StringLiteral , AssertEntries + // 1. Let supportedAssertions be !HostGetSupportedImportAssertions(). + + for (auto& assertion : source_assertions) { + // 2. Let key be StringValue of AssertionKey. + // 3. If supportedAssertions contains key, + if (supported_import_assertions.contains_slow(assertion.key)) { + // a. Let entry be a Record { [[Key]]: key, [[Value]]: StringValue of StringLiteral }. + assertions.empend(assertion); + } + } + + // 2. Sort assertions by the code point order of the [[Key]] of each element. NOTE: This sorting is observable only in that hosts are prohibited from distinguishing among assertions by the order they occur in. + // Note: The sorting is done in construction of the ModuleRequest object. + + // 3. Return assertions. + return assertions; +} + // 16.2.1.3 Static Semantics: ModuleRequests, https://tc39.es/ecma262/#sec-static-semantics-modulerequests -static Vector module_requests(Program const& program) +static Vector module_requests(Program& program, Vector const& supported_import_assertions) { // A List of all the ModuleSpecifier strings used by the module represented by this record to request the importation of a module. // Note: The List is source text occurrence ordered! struct RequestedModuleAndSourceIndex { - FlyString requested_module; - u64 source_index; - - bool operator<(RequestedModuleAndSourceIndex const& rhs) const - { - return source_index < rhs.source_index; - } + u64 source_index { 0 }; + ModuleRequest* module_request { nullptr }; }; Vector requested_modules_with_indices; - for (auto const& import_statement : program.imports()) { - requested_modules_with_indices.append({ import_statement.module_request().module_specifier.view(), - import_statement.source_range().start.offset }); + for (auto& import_statement : program.imports()) { + requested_modules_with_indices.empend(import_statement.source_range().start.offset, &import_statement.module_request()); } - for (auto const& export_statement : program.exports()) { - for (auto const& export_entry : export_statement.entries()) { + for (auto& export_statement : program.exports()) { + for (auto& export_entry : export_statement.entries()) { if (!export_entry.is_module_request()) continue; - requested_modules_with_indices.append({ export_entry.module_request().module_specifier.view(), - export_statement.source_range().start.offset }); + requested_modules_with_indices.empend(export_statement.source_range().start.offset, &export_statement.module_request()); } } - quick_sort(requested_modules_with_indices); + // Note: The List is source code occurrence ordered. https://tc39.es/proposal-import-assertions/#table-cyclic-module-fields + quick_sort(requested_modules_with_indices, [&](RequestedModuleAndSourceIndex const& lhs, RequestedModuleAndSourceIndex const& rhs) { + return lhs.source_index < rhs.source_index; + }); - Vector requested_modules_in_source_order; + Vector requested_modules_in_source_order; requested_modules_in_source_order.ensure_capacity(requested_modules_with_indices.size()); for (auto& module : requested_modules_with_indices) { - requested_modules_in_source_order.append(module.requested_module); + // 2.10 Static Semantics: ModuleRequests https://tc39.es/proposal-import-assertions/#sec-static-semantics-modulerequests + if (module.module_request->assertions.is_empty()) { + // ExportDeclaration : export ExportFromClause FromClause ; + // ImportDeclaration : import ImportClause FromClause ; + + // 1. Let specifier be StringValue of the StringLiteral contained in FromClause. + // 2. Return a ModuleRequest Record { [[Specifer]]: specifier, [[Assertions]]: an empty List }. + requested_modules_in_source_order.empend(module.module_request->module_specifier); + } else { + // ExportDeclaration : export ExportFromClause FromClause AssertClause ; + // ImportDeclaration : import ImportClause FromClause AssertClause ; + + // 1. Let specifier be StringValue of the StringLiteral contained in FromClause. + // 2. Let assertions be AssertClauseToAssertions of AssertClause. + auto assertions = assert_clause_to_assertions(module.module_request->assertions, supported_import_assertions); + // Note: We have to modify the assertions in place because else it might keep non supported ones + module.module_request->assertions = move(assertions); + + // 3. Return a ModuleRequest Record { [[Specifer]]: specifier, [[Assertions]]: assertions }. + requested_modules_in_source_order.empend(module.module_request->module_specifier, module.module_request->assertions); + } } return requested_modules_in_source_order; } -SourceTextModule::SourceTextModule(Realm& realm, StringView filename, bool has_top_level_await, NonnullRefPtr body, Vector requested_modules, +SourceTextModule::SourceTextModule(Realm& realm, StringView filename, bool has_top_level_await, NonnullRefPtr body, Vector requested_modules, Vector import_entries, Vector local_export_entries, Vector indirect_export_entries, Vector star_export_entries, RefPtr default_export) @@ -81,8 +124,12 @@ Result, Vector> SourceTextModule: if (parser.has_errors()) return parser.errors(); + // Needed for 2.7 Static Semantics: AssertClauseToAssertions, https://tc39.es/proposal-import-assertions/#sec-assert-clause-to-assertions + // 1. Let supportedAssertions be !HostGetSupportedImportAssertions(). + auto supported_assertions = realm.vm().host_get_supported_import_assertions(); + // 3. Let requestedModules be the ModuleRequests of body. - auto requested_modules = module_requests(*body); + auto requested_modules = module_requests(*body, supported_assertions); // 4. Let importEntries be ImportEntries of body. Vector import_entries; @@ -288,9 +335,6 @@ Completion SourceTextModule::initialize_environment(VM& vm) // 7. For each ImportEntry Record in of module.[[ImportEntries]], do for (auto& import_entry : m_import_entries) { - if (!import_entry.module_request().assertions.is_empty()) - return vm.throw_completion(global_object, ErrorType::NotImplemented, "import statements with assertions"); - // a. Let importedModule be ! HostResolveImportedModule(module, in.[[ModuleRequest]]). auto imported_module = MUST(vm.host_resolve_imported_module(this, import_entry.module_request())); // b. NOTE: The above call cannot fail because imported module requests are a subset of module.[[RequestedModules]], and these have been resolved earlier in this algorithm. diff --git a/Userland/Libraries/LibJS/SourceTextModule.h b/Userland/Libraries/LibJS/SourceTextModule.h index a4e60b6ffa..302e9f8019 100644 --- a/Userland/Libraries/LibJS/SourceTextModule.h +++ b/Userland/Libraries/LibJS/SourceTextModule.h @@ -44,7 +44,7 @@ protected: virtual Completion execute_module(VM& vm, Optional capability) override; private: - SourceTextModule(Realm&, StringView filename, bool has_top_level_await, NonnullRefPtr body, Vector requested_modules, + SourceTextModule(Realm&, StringView filename, bool has_top_level_await, NonnullRefPtr body, Vector requested_modules, Vector import_entries, Vector local_export_entries, Vector indirect_export_entries, Vector star_export_entries, RefPtr default_export); diff --git a/Userland/Libraries/LibJS/Tests/modules/basic-modules.js b/Userland/Libraries/LibJS/Tests/modules/basic-modules.js index eb927ab63c..23efa91826 100644 --- a/Userland/Libraries/LibJS/Tests/modules/basic-modules.js +++ b/Userland/Libraries/LibJS/Tests/modules/basic-modules.js @@ -9,14 +9,14 @@ function validTestModule(filename) { } } -function expectModulePassed(filename) { +function expectModulePassed(filename, options = undefined) { validTestModule(filename); let moduleLoaded = false; let moduleResult = null; let thrownError = null; - import(filename) + import(filename, options) .then(result => { moduleLoaded = true; moduleResult = result; @@ -130,6 +130,11 @@ describe("testing behavior", () => { test("expectModulePassed works", () => { expectModulePassed("./single-const-export.mjs"); }); + + test("can call expectModulePassed with options", () => { + expectModulePassed("./single-const-export.mjs", { key: "value" }); + expectModulePassed("./single-const-export.mjs", { key1: "value1", key2: "value2" }); + }); }); describe("in- and exports", () => { @@ -173,6 +178,10 @@ describe("in- and exports", () => { "Invalid or ambiguous export entry 'default'" ); }); + + test("can import with (useless) assertions", () => { + expectModulePassed("./import-with-assertions.mjs"); + }); }); describe("loops", () => { diff --git a/Userland/Libraries/LibJS/Tests/modules/import-with-assertions.mjs b/Userland/Libraries/LibJS/Tests/modules/import-with-assertions.mjs new file mode 100644 index 0000000000..e2381201c7 --- /dev/null +++ b/Userland/Libraries/LibJS/Tests/modules/import-with-assertions.mjs @@ -0,0 +1,4 @@ +import * as self from "./import-with-assertions.mjs" assert { "key": "value", key2: "value2", default: "shouldwork" }; +import "./import-with-assertions.mjs" assert { "key": "value", key2: "value2", default: "shouldwork" }; + +export { passed } from "./module-with-default.mjs" assert { "key": "value", key2: "value2", default: "shouldwork" };