diff --git a/Meta/Lagom/CMakeLists.txt b/Meta/Lagom/CMakeLists.txt index af7e4eb714..8b61fe063a 100644 --- a/Meta/Lagom/CMakeLists.txt +++ b/Meta/Lagom/CMakeLists.txt @@ -474,6 +474,15 @@ if (BUILD_LAGOM) add_executable(js ../../Userland/Utilities/js.cpp) target_link_libraries(js LibCrypto LibJS LibLine LibLocale LibMain LibTextCodec Threads::Threads) + if (EMSCRIPTEN) + add_executable(libjs Wasm/js_repl.cpp) + target_link_libraries(libjs LibJS) + target_link_options(libjs PRIVATE + -sEXPORTED_FUNCTIONS=_initialize_repl,_execute + -sEXPORTED_RUNTIME_METHODS=allocateUTF8 + -sERROR_ON_UNDEFINED_SYMBOLS=0 + -sENVIRONMENT=web) + endif() add_executable(markdown-check ../../Userland/Utilities/markdown-check.cpp) target_link_libraries(markdown-check LibMarkdown LibMain) diff --git a/Meta/Lagom/Wasm/js_repl.cpp b/Meta/Lagom/Wasm/js_repl.cpp new file mode 100644 index 0000000000..76674d3af0 --- /dev/null +++ b/Meta/Lagom/Wasm/js_repl.cpp @@ -0,0 +1,388 @@ +/* + * Copyright (c) 2020-2021, Andreas Kling + * Copyright (c) 2020-2022, Linus Groh + * Copyright (c) 2020-2022, Ali Mohammad Pur + * + * SPDX-License-Identifier: BSD-2-Clause + */ + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#ifndef AK_OS_EMSCRIPTEN +# error "This program is for Emscripten only" +#endif + +#include + +class ReplConsoleClient; + +RefPtr g_vm; +OwnPtr g_interpreter; +OwnPtr g_console_client; +JS::Handle g_last_value = JS::make_handle(JS::js_undefined()); + +EM_JS(void, user_display, (char const* string, u32 length), { globalDisplayToUser(UTF8ToString(string, length)); }); + +template +void display(CheckedFormatString format_string, Args const&... args) +{ + auto string = String::formatted(format_string.view(), args...); + user_display(string.characters(), string.length()); +} + +template +void displayln(CheckedFormatString format_string, Args const&... args) +{ + display(format_string.view(), args...); + user_display("\n", 1); +} + +void displayln() { user_display("\n", 1); } + +class UserDisplayStream final : public Core::Stream::Stream { + virtual ErrorOr read(Bytes) override { return Error::from_string_view("Not readable"sv); }; + virtual bool is_writable() const override { return true; } + virtual ErrorOr write(ReadonlyBytes bytes) override + { + user_display(bit_cast(bytes.data()), bytes.size()); + return bytes.size(); + } + virtual bool is_eof() const override { return true; } + virtual bool is_open() const override { return true; } + virtual void close() override { } +}; + +ErrorOr print(JS::Value value) +{ + UserDisplayStream stream; + JS::PrintContext print_context { + .vm = *g_vm, + .stream = stream, + .strip_ansi = true, + }; + return JS::print(value, print_context); +} + +class ReplObject final : public JS::GlobalObject { + JS_OBJECT(ReplObject, JS::GlobalObject); + +public: + ReplObject(JS::Realm& realm) + : GlobalObject(realm) + { + } + virtual void initialize(JS::Realm&) override; + virtual ~ReplObject() override = default; + +private: + JS_DECLARE_NATIVE_FUNCTION(last_value_getter); + JS_DECLARE_NATIVE_FUNCTION(print); +}; + +static bool s_dump_ast = false; +static bool s_run_bytecode = false; +static bool s_opt_bytecode = false; +static bool s_as_module = false; +static bool s_print_last_result = false; + +static bool parse_and_run(JS::Interpreter& interpreter, StringView source, StringView source_name) +{ + enum class ReturnEarly { + No, + Yes, + }; + + JS::ThrowCompletionOr result { JS::js_undefined() }; + + auto run_script_or_module = [&](auto& script_or_module) { + if (s_dump_ast) + script_or_module->parse_node().dump(0); + + if (JS::Bytecode::g_dump_bytecode || s_run_bytecode) { + auto executable_result = JS::Bytecode::Generator::generate(script_or_module->parse_node()); + if (executable_result.is_error()) { + result = g_vm->throw_completion(executable_result.error().to_string()); + return ReturnEarly::No; + } + + auto executable = executable_result.release_value(); + executable->name = source_name; + if (s_opt_bytecode) { + auto& passes = JS::Bytecode::Interpreter::optimization_pipeline(); + passes.perform(*executable); + } + + if (JS::Bytecode::g_dump_bytecode) + executable->dump(); + + if (s_run_bytecode) { + JS::Bytecode::Interpreter bytecode_interpreter(interpreter.realm()); + auto result_or_error = bytecode_interpreter.run_and_return_frame(*executable, nullptr); + if (result_or_error.value.is_error()) + result = result_or_error.value.release_error(); + else + result = result_or_error.frame->registers[0]; + } else { + return ReturnEarly::Yes; + } + } else { + result = interpreter.run(*script_or_module); + } + + return ReturnEarly::No; + }; + + if (!s_as_module) { + auto script_or_error = JS::Script::parse(source, interpreter.realm(), source_name); + if (script_or_error.is_error()) { + auto error = script_or_error.error()[0]; + auto hint = error.source_location_hint(source); + if (!hint.is_empty()) + displayln("{}", hint); + displayln("{}", error.to_string()); + result = interpreter.vm().throw_completion(error.to_string()); + } else { + auto return_early = run_script_or_module(script_or_error.value()); + if (return_early == ReturnEarly::Yes) + return true; + } + } else { + auto module_or_error = JS::SourceTextModule::parse(source, interpreter.realm(), source_name); + if (module_or_error.is_error()) { + auto error = module_or_error.error()[0]; + auto hint = error.source_location_hint(source); + if (!hint.is_empty()) + displayln("{}", hint); + displayln(error.to_string()); + result = interpreter.vm().throw_completion(error.to_string()); + } else { + auto return_early = run_script_or_module(module_or_error.value()); + if (return_early == ReturnEarly::Yes) + return true; + } + } + + auto handle_exception = [&](JS::Value thrown_value) { + display("Uncaught exception: "); + (void)print(thrown_value); + + if (!thrown_value.is_object() || !is(thrown_value.as_object())) + return; + auto& traceback = static_cast(thrown_value.as_object()).traceback(); + if (traceback.size() > 1) { + unsigned repetitions = 0; + for (size_t i = 0; i < traceback.size(); ++i) { + auto& traceback_frame = traceback[i]; + if (i + 1 < traceback.size()) { + auto& next_traceback_frame = traceback[i + 1]; + if (next_traceback_frame.function_name == traceback_frame.function_name) { + repetitions++; + continue; + } + } + if (repetitions > 4) { + // If more than 5 (1 + >4) consecutive function calls with the same name, print + // the name only once and show the number of repetitions instead. This prevents + // printing ridiculously large call stacks of recursive functions. + displayln(" -> {}", traceback_frame.function_name); + displayln(" {} more calls", repetitions); + } else { + for (size_t j = 0; j < repetitions + 1; ++j) + displayln(" -> {}", traceback_frame.function_name); + } + repetitions = 0; + } + } + }; + + if (!result.is_error()) + g_last_value = JS::make_handle(result.value()); + + if (result.is_error()) { + VERIFY(result.throw_completion().value().has_value()); + handle_exception(*result.release_error().value()); + return false; + } + + if (s_print_last_result) { + (void)print(result.value()); + display("\n"); + } + + return true; +} + +void ReplObject::initialize(JS::Realm& realm) +{ + Base::initialize(realm); + + define_direct_property("global", this, JS::Attribute::Enumerable); + u8 attr = JS::Attribute::Configurable | JS::Attribute::Writable | JS::Attribute::Enumerable; + define_native_function(realm, "print", print, 1, attr); + + define_native_accessor( + realm, + "_", + [](JS::VM&) { + return g_last_value.value(); + }, + [](JS::VM& vm) -> JS::ThrowCompletionOr { + auto& global_object = vm.get_global_object(); + VERIFY(is(global_object)); + displayln("Disable writing last value to '_'"); + + // We must delete first otherwise this setter gets called recursively. + TRY(global_object.internal_delete(JS::PropertyKey { "_" })); + + auto value = vm.argument(0); + TRY(global_object.internal_set(JS::PropertyKey { "_" }, value, &global_object)); + return value; + }, + attr); +} + +JS_DEFINE_NATIVE_FUNCTION(ReplObject::print) +{ + auto result = ::print(vm.argument(0)); + if (result.is_error()) + return g_vm->throw_completion(String::formatted("Failed to print value: {}", result.error())); + + displayln(); + + return JS::js_undefined(); +} + +class ReplConsoleClient final : public JS::ConsoleClient { +public: + ReplConsoleClient(JS::Console& console) + : ConsoleClient(console) + { + } + + virtual void clear() override + { + display("FIXME: clear"); + m_group_stack_depth = 0; + } + + virtual void end_group() override + { + if (m_group_stack_depth > 0) + m_group_stack_depth--; + } + + // 2.3. Printer(logLevel, args[, options]), https://console.spec.whatwg.org/#printer + virtual JS::ThrowCompletionOr printer(JS::Console::LogLevel log_level, PrinterArguments arguments) override + { + String indent = String::repeated(" "sv, m_group_stack_depth); + + if (log_level == JS::Console::LogLevel::Trace) { + auto trace = arguments.get(); + StringBuilder builder; + if (!trace.label.is_empty()) + builder.appendff("{}{}\n", indent, trace.label); + + for (auto& function_name : trace.stack) + builder.appendff("{}-> {}\n", indent, function_name); + + displayln("{}", builder.string_view()); + return JS::js_undefined(); + } + + if (log_level == JS::Console::LogLevel::Group || log_level == JS::Console::LogLevel::GroupCollapsed) { + auto group = arguments.get(); + displayln("{}{}", indent, group.label); + m_group_stack_depth++; + return JS::js_undefined(); + } + + auto output = String::join(' ', arguments.get>()); + switch (log_level) { + case JS::Console::LogLevel::Debug: + displayln("{}{}", indent, output); + break; + case JS::Console::LogLevel::Error: + case JS::Console::LogLevel::Assert: + displayln("{}{}", indent, output); + break; + case JS::Console::LogLevel::Info: + displayln("{}(i) {}", indent, output); + break; + case JS::Console::LogLevel::Log: + displayln("{}{}", indent, output); + break; + case JS::Console::LogLevel::Warn: + case JS::Console::LogLevel::CountReset: + displayln("{}{}", indent, output); + break; + default: + displayln("{}{}", indent, output); + break; + } + return JS::js_undefined(); + } + +private: + int m_group_stack_depth { 0 }; +}; + +extern "C" int initialize_repl() +{ + g_vm = JS::VM::create(); + g_vm->enable_default_host_import_module_dynamically_hook(); + + // NOTE: These will print out both warnings when using something like Promise.reject().catch(...) - + // which is, as far as I can tell, correct - a promise is created, rejected without handler, and a + // handler then attached to it. The Node.js REPL doesn't warn in this case, so it's something we + // might want to revisit at a later point and disable warnings for promises created this way. + g_vm->on_promise_unhandled_rejection = [](auto& promise) { + display("WARNING: A promise was rejected without any handlers"); + display(" (result: "); + (void)print(promise.result()); + displayln(")"); + }; + g_vm->on_promise_rejection_handled = [](auto& promise) { + display("WARNING: A handler was added to an already rejected promise"); + display(" (result: "); + (void)print(promise.result()); + displayln(")"); + }; + OwnPtr interpreter; + + s_print_last_result = true; + interpreter = JS::Interpreter::create(*g_vm); + auto& console_object = *interpreter->realm().intrinsics().console_object(); + g_console_client = make(console_object.console()); + console_object.console().set_client(*g_console_client); + g_interpreter = move(interpreter); + + return 0; +} + +extern "C" bool execute(char const* s, u32 length) +{ + return parse_and_run(*g_interpreter, { s, length }, "REPL"sv); +}