From 6b5c882af344b4ae8b17b4c820a1c2d01d2e0b81 Mon Sep 17 00:00:00 2001 From: davidot Date: Thu, 27 Jan 2022 14:51:21 +0100 Subject: [PATCH] LibJS: Add support for JSON modules We now have one supported assertion: 'type' if that is 'json' we attempt to parse the module as JSON. --- Userland/Libraries/LibJS/CMakeLists.txt | 1 + Userland/Libraries/LibJS/Module.cpp | 7 - Userland/Libraries/LibJS/Runtime/VM.cpp | 52 ++++-- Userland/Libraries/LibJS/Runtime/VM.h | 5 +- Userland/Libraries/LibJS/SyntheticModule.cpp | 159 ++++++++++++++++++ Userland/Libraries/LibJS/SyntheticModule.h | 36 ++++ .../LibJS/Tests/modules/json-module.json | 7 + .../LibJS/Tests/modules/json-modules.js | 34 ++++ 8 files changed, 277 insertions(+), 24 deletions(-) create mode 100644 Userland/Libraries/LibJS/SyntheticModule.cpp create mode 100644 Userland/Libraries/LibJS/SyntheticModule.h create mode 100644 Userland/Libraries/LibJS/Tests/modules/json-module.json create mode 100644 Userland/Libraries/LibJS/Tests/modules/json-modules.js diff --git a/Userland/Libraries/LibJS/CMakeLists.txt b/Userland/Libraries/LibJS/CMakeLists.txt index befb3c7d8b1..3fb7ad49d44 100644 --- a/Userland/Libraries/LibJS/CMakeLists.txt +++ b/Userland/Libraries/LibJS/CMakeLists.txt @@ -227,6 +227,7 @@ set(SOURCES Script.cpp SourceTextModule.cpp SyntaxHighlighter.cpp + SyntheticModule.cpp Token.cpp ) diff --git a/Userland/Libraries/LibJS/Module.cpp b/Userland/Libraries/LibJS/Module.cpp index 72856a2503e..79d5e5b1278 100644 --- a/Userland/Libraries/LibJS/Module.cpp +++ b/Userland/Libraries/LibJS/Module.cpp @@ -24,11 +24,7 @@ Module::~Module() // 16.2.1.5.1.1 InnerModuleLinking ( module, stack, index ), https://tc39.es/ecma262/#sec-InnerModuleLinking ThrowCompletionOr Module::inner_module_linking(VM& vm, Vector&, u32 index) { - // Note: Until we have something extending module which is not SourceTextModule we crash. - VERIFY_NOT_REACHED(); - // 1. If module is not a Cyclic Module Record, then - // a. Perform ? module.Link(). TRY(link(vm)); // b. Return index. @@ -38,9 +34,6 @@ ThrowCompletionOr Module::inner_module_linking(VM& vm, Vector&, u3 // 16.2.1.5.2.1 InnerModuleEvaluation ( module, stack, index ), https://tc39.es/ecma262/#sec-innermoduleevaluation ThrowCompletionOr Module::inner_module_evaluation(VM& vm, Vector&, u32 index) { - // Note: Until we have something extending module which is not SourceTextModule we crash. - VERIFY_NOT_REACHED(); - // 1. If module is not a Cyclic Module Record, then // a. Let promise be ! module.Evaluate(). auto* promise = TRY(evaluate(vm)); diff --git a/Userland/Libraries/LibJS/Runtime/VM.cpp b/Userland/Libraries/LibJS/Runtime/VM.cpp index 6c80fe2bba3..3ebd0d2497c 100644 --- a/Userland/Libraries/LibJS/Runtime/VM.cpp +++ b/Userland/Libraries/LibJS/Runtime/VM.cpp @@ -29,6 +29,7 @@ #include #include #include +#include namespace JS { @@ -89,7 +90,7 @@ VM::VM(OwnPtr custom_data) }; host_get_supported_import_assertions = [&] { - return Vector {}; + return Vector { "type" }; }; #define __JS_ENUMERATE(SymbolName, snake_name) \ @@ -728,12 +729,18 @@ 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) +VM::StoredModule* VM::get_stored_module(ScriptOrModule const&, String const& filepath, String const&) { // 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. + + // Editor's Note from https://tc39.es/proposal-json-modules/#sec-hostresolveimportedmodule + // The above text implies that is recommended but not required that hosts do not use moduleRequest.[[Assertions]] + // as part of the module cache key. In either case, an exception thrown from an import with a given assertion list + // does not rule out success of another import with the same specifier but a different assertion list. + auto end_or_module = m_loaded_modules.find_if([&](StoredModule const& stored_module) { return stored_module.filepath == filepath; }); @@ -747,7 +754,7 @@ ThrowCompletionOr VM::link_and_eval_module(Badge, SourceTextM return link_and_eval_module(module); } -ThrowCompletionOr VM::link_and_eval_module(SourceTextModule& module) +ThrowCompletionOr VM::link_and_eval_module(Module& module) { auto filepath = module.filename(); @@ -766,6 +773,7 @@ ThrowCompletionOr VM::link_and_eval_module(SourceTextModule& module) m_loaded_modules.empend( &module, module.filename(), + String {}, // Null type module, true); stored_module = &m_loaded_modules.last(); @@ -852,7 +860,13 @@ ThrowCompletionOr> VM::resolve_imported_module(ScriptOrMod 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); + // We only allow "type" as a supported assertion so it is the only valid key that should ever arrive here. + VERIFY(module_request.assertions.is_empty() || (module_request.assertions.size() == 1 && module_request.assertions.first().key == "type")); + auto module_type = module_request.assertions.is_empty() ? String {} : module_request.assertions.first().value; + + dbgln_if(JS_MODULE_DEBUG, "[JS MODULE] module at {} has type {} [is_null={}]", module_request.module_specifier, module_type, module_type.is_null()); + + auto* loaded_module_or_end = get_stored_module(referencing_script_or_module, filepath, module_type); 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; @@ -872,21 +886,32 @@ ThrowCompletionOr> VM::resolve_imported_module(ScriptOrMod 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); + auto module = TRY([&]() -> ThrowCompletionOr> { + // If assertions has an entry entry such that entry.[[Key]] is "type", let type be entry.[[Value]]. The following requirements apply: + // If type is "json", then this algorithm must either invoke ParseJSONModule and return the resulting Completion Record, or throw an exception. + if (module_type == "json"sv) { + dbgln_if(JS_MODULE_DEBUG, "[JS MODULE] reading and parsing JSON module {}", filepath); + return parse_json_module(content_view, *current_realm(), filepath); + } - if (module_or_errors.is_error()) { - VERIFY(module_or_errors.error().size() > 0); - return throw_completion(global_object, module_or_errors.error().first().to_string()); - } + dbgln_if(JS_MODULE_DEBUG, "[JS MODULE] reading and parsing as SourceTextModule module {}", filepath); + // 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(global_object, module_or_errors.error().first().to_string()); + } + return module_or_errors.release_value(); + }()); - 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_type, module, false); @@ -939,11 +964,8 @@ void VM::import_module_dynamically(ScriptOrModule referencing_script_or_module, clear_exception(); promise->reject(*module_or_error.throw_completion().value()); } else { - // Note: If you are here because this VERIFY is failing overwrite host_import_module_dynamically - // because this is LibJS internal logic which won't always work auto module = module_or_error.release_value(); - VERIFY(is(*module)); - auto& source_text_module = static_cast(*module); + auto& source_text_module = static_cast(*module); auto evaluated_or_error = link_and_eval_module(source_text_module); diff --git a/Userland/Libraries/LibJS/Runtime/VM.h b/Userland/Libraries/LibJS/Runtime/VM.h index 3bc0394827a..f8b9e38e2b8 100644 --- a/Userland/Libraries/LibJS/Runtime/VM.h +++ b/Userland/Libraries/LibJS/Runtime/VM.h @@ -250,7 +250,7 @@ private: 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& module_request); - ThrowCompletionOr link_and_eval_module(SourceTextModule& module); + ThrowCompletionOr link_and_eval_module(Module& module); 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); @@ -280,11 +280,12 @@ private: struct StoredModule { ScriptOrModule referencing_script_or_module; String filepath; + String type; NonnullRefPtr module; bool has_once_started_linking { false }; }; - StoredModule* get_stored_module(ScriptOrModule const& script_or_module, String const& filepath); + StoredModule* get_stored_module(ScriptOrModule const& script_or_module, String const& filepath, String const& type); Vector m_loaded_modules; diff --git a/Userland/Libraries/LibJS/SyntheticModule.cpp b/Userland/Libraries/LibJS/SyntheticModule.cpp new file mode 100644 index 00000000000..418fc8d6500 --- /dev/null +++ b/Userland/Libraries/LibJS/SyntheticModule.cpp @@ -0,0 +1,159 @@ +/* + * Copyright (c) 2022, David Tuin + * + * SPDX-License-Identifier: BSD-2-Clause + */ + +#include +#include +#include +#include +#include +#include + +namespace JS { + +// 1.2.1 CreateSyntheticModule ( exportNames, evaluationSteps, realm, hostDefined ), https://tc39.es/proposal-json-modules/#sec-synthetic-module-records +SyntheticModule::SyntheticModule(Vector export_names, SyntheticModule::EvaluationFunction evaluation_steps, Realm& realm, StringView filename) + : Module(realm, filename) + , m_export_names(move(export_names)) + , m_evaluation_steps(move(evaluation_steps)) +{ + // 1. Return Synthetic Module Record { [[Realm]]: realm, [[Environment]]: undefined, [[Namespace]]: undefined, [[HostDefined]]: hostDefined, [[ExportNames]]: exportNames, [[EvaluationSteps]]: evaluationSteps }. +} + +// 1.2.3.1 GetExportedNames( exportStarSet ), https://tc39.es/proposal-json-modules/#sec-smr-getexportednames +ThrowCompletionOr> JS::SyntheticModule::get_exported_names(VM&, Vector) +{ + // 1. Return module.[[ExportNames]]. + return m_export_names; +} + +// 1.2.3.2 ResolveExport( exportName, resolveSet ), https://tc39.es/proposal-json-modules/#sec-smr-resolveexport +ThrowCompletionOr JS::SyntheticModule::resolve_export(VM&, FlyString const& export_name, Vector) +{ + // 1. If module.[[ExportNames]] does not contain exportName, return null. + if (!m_export_names.contains_slow(export_name)) + return ResolvedBinding::null(); + + // 2. Return ResolvedBinding Record { [[Module]]: module, [[BindingName]]: exportName }. + return ResolvedBinding { ResolvedBinding::BindingName, this, export_name }; +} + +// 1.2.3.3 Link ( ), https://tc39.es/proposal-json-modules/#sec-smr-instantiate +ThrowCompletionOr JS::SyntheticModule::link(VM& vm) +{ + // Note: Has some changes from PR: https://github.com/tc39/proposal-json-modules/pull/13. + // Which includes renaming it from Instantiate ( ) to Link ( ). + + // 1. Let realm be module.[[Realm]]. + // 2. Assert: realm is not undefined. + // Note: This must be true because we use a reference. + + auto& global_object = realm().global_object(); + + // 3. Let env be NewModuleEnvironment(realm.[[GlobalEnv]]). + auto* environment = vm.heap().allocate(global_object, &realm().global_environment()); + + // 4. Set module.[[Environment]] to env. + set_environment(environment); + + // 5. For each exportName in module.[[ExportNames]], + for (auto& export_name : m_export_names) { + // a. Perform ! envRec.CreateMutableBinding(exportName, false). + environment->create_mutable_binding(global_object, export_name, false); + + // b. Perform ! envRec.InitializeBinding(exportName, undefined). + environment->initialize_binding(global_object, export_name, js_undefined()); + } + + // 6. Return undefined. + // Note: This return value is never visible to the outside so we use void. + return {}; +} + +// 1.2.3.4 Evaluate ( ), https://tc39.es/proposal-json-modules/#sec-smr-Evaluate +ThrowCompletionOr JS::SyntheticModule::evaluate(VM& vm) +{ + // Note: Has some changes from PR: https://github.com/tc39/proposal-json-modules/pull/13. + // 1. Suspend the currently running execution context. + // FIXME: We don't have suspend yet. + + // 2. Let moduleContext be a new ECMAScript code execution context. + ExecutionContext module_context { vm.heap() }; + + // 3. Set the Function of moduleContext to null. + // Note: This is the default value. + + // 4. Set the Realm of moduleContext to module.[[Realm]]. + module_context.realm = &realm(); + + // 5. Set the ScriptOrModule of moduleContext to module. + module_context.script_or_module = this; + + // 6. Set the VariableEnvironment of moduleContext to module.[[Environment]]. + module_context.variable_environment = environment(); + + // 7. Set the LexicalEnvironment of moduleContext to module.[[Environment]]. + module_context.lexical_environment = environment(); + + // 8. Push moduleContext on to the execution context stack; moduleContext is now the running execution context. + vm.push_execution_context(module_context, realm().global_object()); + + // 9. Let result be the result of performing module.[[EvaluationSteps]](module). + auto result = m_evaluation_steps(*this); + + // 10. Suspend moduleContext and remove it from the execution context stack. + vm.pop_execution_context(); + + // 11. Resume the context that is now on the top of the execution context stack as the running execution context. + + // 12. Return Completion(result). + // Note: Because we expect it to return a promise we convert this here. + auto* promise = Promise::create(realm().global_object()); + if (result.is_error()) { + VERIFY(result.throw_completion().value().has_value()); + promise->reject(*result.throw_completion().value()); + } else { + // Note: This value probably isn't visible to JS code? But undefined is fine anyway. + promise->fulfill(js_undefined()); + } + return promise; +} + +// 1.2.2 SetSyntheticModuleExport ( module, exportName, exportValue ), https://tc39.es/proposal-json-modules/#sec-setsyntheticmoduleexport +ThrowCompletionOr SyntheticModule::set_synthetic_module_export(FlyString const& export_name, Value export_value) +{ + // Note: Has some changes from PR: https://github.com/tc39/proposal-json-modules/pull/13. + // 1. Return ? module.[[Environment]].SetMutableBinding(name, value, true). + return environment()->set_mutable_binding(realm().global_object(), export_name, export_value, true); +} + +// 1.3 CreateDefaultExportSyntheticModule ( defaultExport ), https://tc39.es/proposal-json-modules/#sec-create-default-export-synthetic-module +NonnullRefPtr SyntheticModule::create_default_export_synthetic_module(Value default_export, Realm& realm, StringView filename) +{ + // Note: Has some changes from PR: https://github.com/tc39/proposal-json-modules/pull/13. + // 1. Let closure be the a Abstract Closure with parameters (module) that captures defaultExport and performs the following steps when called: + auto closure = [default_export = make_handle(default_export)](SyntheticModule& module) -> ThrowCompletionOr { + // a. Return ? module.SetSyntheticExport("default", defaultExport). + return module.set_synthetic_module_export("default", default_export.value()); + }; + + // 2. Return CreateSyntheticModule("default", closure, realm) + return adopt_ref(*new SyntheticModule({ "default" }, move(closure), realm, filename)); +} + +// 1.4 ParseJSONModule ( source ), https://tc39.es/proposal-json-modules/#sec-parse-json-module +ThrowCompletionOr> parse_json_module(StringView source_text, Realm& realm, StringView filename) +{ + // 1. Let jsonParse be realm's intrinsic object named "%JSON.parse%". + auto* json_parse = realm.global_object().json_parse_function(); + + // 2. Let json be ? Call(jsonParse, undefined, « sourceText »). + auto json = TRY(call(realm.global_object(), *json_parse, js_undefined(), js_string(realm.vm(), source_text))); + + // 3. Return CreateDefaultExportSyntheticModule(json, realm, hostDefined). + return SyntheticModule::create_default_export_synthetic_module(json, realm, filename); +} + +} diff --git a/Userland/Libraries/LibJS/SyntheticModule.h b/Userland/Libraries/LibJS/SyntheticModule.h new file mode 100644 index 00000000000..7f812e55ac4 --- /dev/null +++ b/Userland/Libraries/LibJS/SyntheticModule.h @@ -0,0 +1,36 @@ +/* + * Copyright (c) 2022, David Tuin + * + * SPDX-License-Identifier: BSD-2-Clause + */ + +#pragma once + +#include + +namespace JS { + +// 1.2 Synthetic Module Records, https://tc39.es/proposal-json-modules/#sec-synthetic-module-records +class SyntheticModule final : public Module { +public: + using EvaluationFunction = Function(SyntheticModule&)>; + + SyntheticModule(Vector export_names, EvaluationFunction evaluation_steps, Realm& realm, StringView filename); + + static NonnullRefPtr create_default_export_synthetic_module(Value default_export, Realm& realm, StringView filename); + + ThrowCompletionOr set_synthetic_module_export(FlyString const& export_name, Value export_value); + + virtual ThrowCompletionOr link(VM& vm) override; + virtual ThrowCompletionOr evaluate(VM& vm) override; + virtual ThrowCompletionOr> get_exported_names(VM& vm, Vector export_star_set) override; + virtual ThrowCompletionOr resolve_export(VM& vm, FlyString const& export_name, Vector resolve_set) override; + +private: + Vector m_export_names; // [[ExportNames]] + EvaluationFunction m_evaluation_steps; // [[EvaluationSteps]] +}; + +ThrowCompletionOr> parse_json_module(StringView source_text, Realm& realm, StringView filename); + +} diff --git a/Userland/Libraries/LibJS/Tests/modules/json-module.json b/Userland/Libraries/LibJS/Tests/modules/json-module.json new file mode 100644 index 00000000000..c5fc66d4eae --- /dev/null +++ b/Userland/Libraries/LibJS/Tests/modules/json-module.json @@ -0,0 +1,7 @@ +{ + "value": "value", + "array": [1, 2, 3], + "map": { + "innerValue": "innerValue" + } +} diff --git a/Userland/Libraries/LibJS/Tests/modules/json-modules.js b/Userland/Libraries/LibJS/Tests/modules/json-modules.js new file mode 100644 index 00000000000..84d4f796280 --- /dev/null +++ b/Userland/Libraries/LibJS/Tests/modules/json-modules.js @@ -0,0 +1,34 @@ +describe("basic behavior", () => { + test("can import json modules", () => { + let passed = false; + let error = null; + let result = null; + + import("./json-module.json", { assert: { type: "json" } }) + .then(jsonObj => { + passed = true; + result = jsonObj; + }) + .catch(err => { + error = err; + }); + + runQueuedPromiseJobs(); + + if (error) throw error; + + console.log(JSON.stringify(result)); + expect(passed).toBeTrue(); + + expect(result).not.toBeNull(); + expect(result).not.toBeUndefined(); + + const jsonResult = result.default; + expect(jsonResult).not.toBeNull(); + expect(jsonResult).not.toBeUndefined(); + + expect(jsonResult).toHaveProperty("value", "value"); + expect(jsonResult).toHaveProperty("array", [1, 2, 3]); + expect(jsonResult).toHaveProperty("map", { innerValue: "innerValue" }); + }); +});