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" };