diff --git a/tslang/include/TypeScript/UncaughtException.h b/tslang/include/TypeScript/UncaughtException.h new file mode 100644 index 000000000..ebb98e248 --- /dev/null +++ b/tslang/include/TypeScript/UncaughtException.h @@ -0,0 +1,294 @@ +#ifndef TYPESCRIPT_UNCAUGHTEXCEPTION_H_ +#define TYPESCRIPT_UNCAUGHTEXCEPTION_H_ + +// What an exception nothing caught threw, as text: the JIT (tslang/jit.cpp) and an executable's runtime +// (TypeScriptAsyncRuntime/UncaughtException.cpp) both say "uncaught exception: ". On Windows it reads +// the exception record of a throw (_CxxThrowException's); on Linux the exception std::terminate() is called +// for, which the C++ runtime has begun to catch. + +#if defined(_WIN32) || (defined(__linux__) && !defined(__ANDROID__)) +#define TSLANG_DESCRIBES_UNCAUGHT 1 + +#ifdef _WIN32 +#include +#else +#include +#include +#include +#endif + +#include +#include +#include +#include +#include +#include + +namespace typescript +{ + +// A thrown number as the program would print it (Number::toString, as ConvertLogic's +// __ts_number_to_string): the fewest digits that read back as the same double, plain decimal for +// a decimal exponent n in -6 < n <= 21, `d.ddde+N` / `e-N` otherwise. `%.17g` said `0.1` as +// 0.10000000000000001. +inline void uncaughtNumberText(char *out, size_t size, double value) +{ + if (value != value) + { + snprintf(out, size, "NaN"); + return; + } + + if (value == 0) + { + snprintf(out, size, "0"); + return; + } + + auto *sign = value < 0 ? "-" : ""; + auto magnitude = value < 0 ? -value : value; + if (magnitude > DBL_MAX) + { + snprintf(out, size, "%sInfinity", sign); + return; + } + + char scientific[32]; + int decimals = 0; + for (; decimals < 16; decimals++) + { + snprintf(scientific, sizeof(scientific), "%.*e", decimals, magnitude); + if (strtod(scientific, nullptr) == magnitude) + { + break; + } + } + + if (decimals == 16) + { + snprintf(scientific, sizeof(scientific), "%.*e", decimals, magnitude); + } + + char digits[32]; + snprintf(digits, sizeof(digits), "%c%.*s", scientific[0], decimals, scientific + 2); + int k = decimals + 1; + int n = atoi(strchr(scientific, 'e') + 1) + 1; + static const char zeros[] = "000000000000000000000"; + if (k <= n && n <= 21) + { + snprintf(out, size, "%s%s%.*s", sign, digits, n - k, zeros); + } + else if (0 < n && n <= 21) + { + snprintf(out, size, "%s%.*s.%s", sign, n, digits, digits + n); + } + else if (-6 < n && n <= 0) + { + snprintf(out, size, "%s0.%.*s%s", sign, -n, zeros, digits); + } + else + { + snprintf(out, size, "%s%c%s%se%+d", sign, digits[0], k > 1 ? "." : "", digits + 1, n - 1); + } +} + +#ifdef _WIN32 +// The thrown value, read from the exception record of a throw (_CxxThrowException's): the object, and its +// ThrowInfo, whose first catchable type names its type (`.N` a number, `.PEAD` a string, `.PEAVName@@` a class). +// Read while the object is alive, in the filter; a record not shaped so is left undescribed. +inline void describeUncaught(EXCEPTION_POINTERS *pointers, char *uncaughtDescription, size_t size) +{ + uncaughtDescription[0] = '\0'; + __try + { + auto *record = pointers->ExceptionRecord; + if (record->NumberParameters < 4) + { + return; + } + + auto *object = reinterpret_cast(record->ExceptionInformation[1]); + auto *throwInfo = reinterpret_cast(record->ExceptionInformation[2]); + auto *imageBase = reinterpret_cast(record->ExceptionInformation[3]); + if (!object || !throwInfo || !imageBase) + { + return; + } + + auto *catchableTypes = reinterpret_cast(imageBase + throwInfo[3]); + auto count = catchableTypes[0]; + if (count < 1) + { + return; + } + + // a type descriptor: the type_info vtable, a spare pointer, then the name + auto typeName = [&](int index) { + auto *catchableType = reinterpret_cast(imageBase + catchableTypes[1 + index]); + return reinterpret_cast(imageBase + catchableType[1] + 2 * sizeof(void *)); + }; + + auto *name = typeName(0); + if (strcmp(name, ".PEAD") == 0) + { + auto *text = *reinterpret_cast(object); + snprintf(uncaughtDescription, size, "%s", text ? text : "null"); + } + else if (strcmp(name, ".N") == 0) + { + uncaughtNumberText(uncaughtDescription, size, *reinterpret_cast(object)); + } + else if (strcmp(name, ".H") == 0) + { + snprintf(uncaughtDescription, size, "%d", *reinterpret_cast(object)); + } + else if (strcmp(name, "._N") == 0) + { + snprintf(uncaughtDescription, size, "%s", *reinterpret_cast(object) ? "true" : "false"); + } + else if (strcmp(name, "._J") == 0) + { + snprintf(uncaughtDescription, size, "%lldn", *reinterpret_cast(object)); + } + else if (strncmp(name, ".PEAV", 5) == 0) + { + // a class: its name, up to the `@@` that ends it + auto *className = name + 5; + auto *end = strstr(className, "@@"); + auto length = end ? static_cast(end - className) : static_cast(strlen(className)); + snprintf(uncaughtDescription, size, "%.*s", length, className); + // an Error (core.ts): after the vtable, `message?: string` - the text, and whether it is set + for (int i = 0; i < count; i++) + { + if (strcmp(typeName(i), ".PEAVError@@") != 0) + { + continue; + } + + auto *instance = *reinterpret_cast(object); + auto *message = instance ? *reinterpret_cast(instance + sizeof(void *)) : nullptr; + auto isSet = instance && *reinterpret_cast(instance + 2 * sizeof(void *)); + if (isSet && message) + { + auto used = strlen(uncaughtDescription); + snprintf(uncaughtDescription + used, size - used, ": %s", message); + } + + break; + } + } + } + __except (EXCEPTION_EXECUTE_HANDLER) + { + uncaughtDescription[0] = '\0'; + } +} +#else +} // namespace typescript + +// the vtable of a class's type_info with one base (a class that extends another), which libstdc++ and +// libc++abi both define +extern "C" void *_ZTVN10__cxxabiv120__si_class_type_infoE[]; + +namespace typescript +{ + +// The exception std::terminate() was called for, when a throw found no catch: the C++ runtime has begun +// to catch it, so it is the current exception. Its type_info names its type (`d` a number, `PKc` a string, +// `P7Failure` a class, thrown as a pointer, see LLVMRTTIHelperVCLinuxConst.h). The Itanium layouts are read +// as they are - tslang is built without RTTI: a pointer's type_info is {vtable, name, flags, pointee}, a +// class's with one base {vtable, name, base}. False when there is no current exception. +inline bool describeCurrentException(char *uncaughtDescription, size_t size) +{ + uncaughtDescription[0] = '\0'; + auto *type = abi::__cxa_current_exception_type(); + if (!type) + { + return false; + } + + // an exception_ptr holds the thrown object's address (libstdc++ and libc++ alike) + auto exception = std::current_exception(); + auto *object = *reinterpret_cast(&exception); + if (!object) + { + return true; + } + + auto *name = type->name(); + if (strcmp(name, "PKc") == 0 || strcmp(name, "Pc") == 0) + { + auto *text = *reinterpret_cast(object); + snprintf(uncaughtDescription, size, "%s", text ? text : "null"); + } + else if (strcmp(name, "d") == 0) + { + uncaughtNumberText(uncaughtDescription, size, *reinterpret_cast(object)); + } + else if (strcmp(name, "f") == 0) + { + uncaughtNumberText(uncaughtDescription, size, *reinterpret_cast(object)); + } + else if (strcmp(name, "i") == 0) + { + snprintf(uncaughtDescription, size, "%d", *reinterpret_cast(object)); + } + else if (strcmp(name, "j") == 0) + { + snprintf(uncaughtDescription, size, "%u", *reinterpret_cast(object)); + } + else if (strcmp(name, "b") == 0) + { + snprintf(uncaughtDescription, size, "%s", *reinterpret_cast(object) ? "true" : "false"); + } + else if (strcmp(name, "x") == 0) + { + snprintf(uncaughtDescription, size, "%lldn", *reinterpret_cast(object)); + } + else if (name[0] == 'P' && isdigit(static_cast(name[1]))) + { + // a class: P + char *className; + auto length = strtol(name + 1, &className, 10); + snprintf(uncaughtDescription, size, "%.*s", static_cast(length), className); + + // an Error (core.ts) among it and its bases: after the vtable, `message?: string` - the text, and + // whether it is set + auto *classType = *reinterpret_cast(reinterpret_cast(type) + + 2 * sizeof(void *) + sizeof(void *)); + while (classType) + { + if (strcmp(classType->name(), "5Error") == 0) + { + auto *instance = *reinterpret_cast(object); + auto *message = instance ? *reinterpret_cast(instance + sizeof(void *)) : nullptr; + auto isSet = instance && *reinterpret_cast(instance + 2 * sizeof(void *)); + if (isSet && message) + { + auto used = strlen(uncaughtDescription); + snprintf(uncaughtDescription + used, size - used, ": %s", message); + } + + break; + } + + auto *vtable = *reinterpret_cast(classType); + if (vtable != static_cast(_ZTVN10__cxxabiv120__si_class_type_infoE + 2)) + { + break; + } + + classType = *reinterpret_cast(reinterpret_cast(classType) + + 2 * sizeof(void *)); + } + } + + return true; +} +#endif + +} // namespace typescript + +#endif // _WIN32 || Linux + +#endif // TYPESCRIPT_UNCAUGHTEXCEPTION_H_ diff --git a/tslang/lib/TypeScriptAsyncRuntime/CMakeLists.txt b/tslang/lib/TypeScriptAsyncRuntime/CMakeLists.txt index c564a7368..1bc05383c 100644 --- a/tslang/lib/TypeScriptAsyncRuntime/CMakeLists.txt +++ b/tslang/lib/TypeScriptAsyncRuntime/CMakeLists.txt @@ -4,6 +4,7 @@ add_mlir_library(TypeScriptAsyncRuntime AsyncGCThreads.cpp ProcessHeap.cpp DynamicRuntime.cpp + UncaughtException.cpp EXCLUDE_FROM_LIBMLIR diff --git a/tslang/lib/TypeScriptAsyncRuntime/UncaughtException.cpp b/tslang/lib/TypeScriptAsyncRuntime/UncaughtException.cpp new file mode 100644 index 000000000..0219db529 --- /dev/null +++ b/tslang/lib/TypeScriptAsyncRuntime/UncaughtException.cpp @@ -0,0 +1,105 @@ +// An exception nothing in a program catches: said on stderr, as the JIT says it (tslang/jit.cpp), and the +// program ends with exit code 1. Left to the C++ runtime, the program ended in abort() - on Windows with +// exit code 3 (or 0xC0000409, a fast fail) and not a word, on Linux with libstdc++'s "terminate called after +// throwing an instance of 'char const*'" and SIGABRT - and what the program had printed, still in stdout's +// buffer, was lost. +// +// An executable tslang links takes this file with `-include:__tslang_report_uncaught` (Windows) or +// `-u __tslang_report_uncaught` (Linux), from tslang/exe.cpp; its initializer sets the handler. + +#include "TypeScript/UncaughtException.h" + +#ifdef TSLANG_DESCRIBES_UNCAUGHT + +#include + +#ifndef _WIN32 +#include +#endif + +namespace +{ + +void sayUncaught(const char *description) +{ + fflush(stdout); + if (description[0]) + { + fprintf(stderr, "error: uncaught exception: %s\n", description); + } + else + { + fprintf(stderr, "error: uncaught exception\n"); + } + + fflush(stderr); +} + +#ifdef _WIN32 + +constexpr DWORD cxxExceptionCode = 0xE06D7363; // 'msc' | 0xE0000000, a throw + +LPTOP_LEVEL_EXCEPTION_FILTER runtimeFilter; + +// in front of the C runtime's own filter, which is set before any initializer runs +LONG WINAPI reportUncaught(EXCEPTION_POINTERS *pointers) +{ + if (pointers->ExceptionRecord->ExceptionCode != cxxExceptionCode) + { + // an access violation and the like: as before + return runtimeFilter ? runtimeFilter(pointers) : EXCEPTION_CONTINUE_SEARCH; + } + + char description[512]; + typescript::describeUncaught(pointers, description, sizeof(description)); + sayUncaught(description); + + // as the JIT does: nothing of the program's teardown runs on a thread an exception has left half done + TerminateProcess(GetCurrentProcess(), 1); + return EXCEPTION_EXECUTE_HANDLER; +} + +int installReportUncaught() +{ + runtimeFilter = SetUnhandledExceptionFilter(reportUncaught); + return 1; +} + +#else + +std::terminate_handler runtimeHandler; + +// std::terminate() with no exception (a std::thread destroyed while joinable, ...) goes on as before +[[noreturn]] void reportUncaught() +{ + char description[512]; + if (!typescript::describeCurrentException(description, sizeof(description))) + { + if (runtimeHandler) + { + runtimeHandler(); + } + + abort(); + } + + sayUncaught(description); + + // nothing of the program's teardown runs on a thread an exception has left half done + _exit(1); +} + +int installReportUncaught() +{ + runtimeHandler = std::set_terminate(reportUncaught); + return 1; +} + +#endif + +} // namespace + +// what the executable's link names to take this file; set once its initializer has run +extern "C" int __tslang_report_uncaught = installReportUncaught(); + +#endif diff --git a/tslang/test/tester/CMakeLists.txt b/tslang/test/tester/CMakeLists.txt index 23df1bbb9..1312a2338 100644 --- a/tslang/test/tester/CMakeLists.txt +++ b/tslang/test/tester/CMakeLists.txt @@ -6020,9 +6020,9 @@ if (NOT WIN32) endif() # An exception nothing catches ends a JIT run with an error that says what was thrown (a class by its -# name), not with LLVM's crash handler reporting a crash of the compiler. Windows only: that is where -# the JIT catches it. -if (WIN32) +# name), not with LLVM's crash handler reporting a crash of the compiler. Windows and Linux: there the JIT +# and an executable's runtime say it (TypeScript/UncaughtException.h). +if (WIN32 OR (CMAKE_SYSTEM_NAME STREQUAL "Linux" AND NOT ANDROID)) set(uncaught_expected_throw_string "error: uncaught exception: not caught") set(uncaught_expected_throw_class "error: uncaught exception: Failure") set(uncaught_expected_throw_number "error: uncaught exception: 2.5") @@ -6037,6 +6037,30 @@ if (WIN32) PROPERTIES PASS_REGULAR_EXPRESSION "${uncaught_expected_${uncaught_name}}" FAIL_REGULAR_EXPRESSION "Stack dump|PLEASE submit") endforeach() + + # and an executable says it too, under every model, with and without --opt (uncaught-exe.cmake) + foreach(uncaught_model gc rc none own) + foreach(uncaught_opt opt noopt) + if (uncaught_opt STREQUAL "opt") + set(uncaught_opt_flags "--opt;--opt_level=3") + else() + set(uncaught_opt_flags "--opt_level=0") + endif() + set(uncaught_test test-compile-${uncaught_model}-${uncaught_opt}-uncaught-${uncaught_name}) + add_test(NAME ${uncaught_test} + COMMAND ${CMAKE_COMMAND} + "-DTSLANG=$" + "-DSOURCE=${PROJECT_SOURCE_DIR}/test/tester/uncaught/${uncaught_name}.ts" + "-DMODEL=${uncaught_model}" + "-DEXPECT=${uncaught_expected_${uncaught_name}}" + "-DWORK_DIR=${CMAKE_CURRENT_BINARY_DIR}/${uncaught_test}" + "-DGC_LIB=${TEST_GC_LIBDIR}" + "-DLLVM_LIB=${LLVM_LIBRARY_DIR}" + "-DTSLANG_LIB=${CMAKE_BINARY_DIR}/lib" + "-DOPT=${uncaught_opt_flags}" + -P "${CMAKE_CURRENT_SOURCE_DIR}/uncaught-exe.cmake") + endforeach() + endforeach() endforeach() endif() diff --git a/tslang/test/tester/uncaught-exe.cmake b/tslang/test/tester/uncaught-exe.cmake new file mode 100644 index 000000000..edb075dd8 --- /dev/null +++ b/tslang/test/tester/uncaught-exe.cmake @@ -0,0 +1,46 @@ +# Builds a program that throws what nothing catches with `tslang --emit=exe` and runs it. It must end with +# exit code 1, its output so far ("before") must not be lost, and stderr must say what was thrown, as the +# JIT says it (EXPECT). Before TypeScriptAsyncRuntime/UncaughtException.cpp an executable called abort(): +# exit code 3, or 0xC0000409 with --opt, and not a word - not even the "before" still in stdout's buffer. + +cmake_minimum_required(VERSION 3.17.3) + +foreach(var TSLANG SOURCE MODEL EXPECT WORK_DIR GC_LIB LLVM_LIB TSLANG_LIB OPT) + if(NOT DEFINED ${var}) + message(FATAL_ERROR "${var} is required") + endif() +endforeach() + +# only the paths given here +set(ENV{TSLANG_LIB_PATH} "") +set(ENV{GC_LIB_PATH} "") +set(ENV{LLVM_LIB_PATH} "") +set(ENV{DEFAULT_LIB_PATH} "") + +file(REMOVE_RECURSE "${WORK_DIR}") +file(MAKE_DIRECTORY "${WORK_DIR}") + +execute_process( + COMMAND "${TSLANG}" --emit=exe -mm=${MODEL} --no-default-lib ${OPT} + "--llvm-lib-path=${LLVM_LIB}" "--tslang-lib-path=${TSLANG_LIB}" "--gc-lib-path=${GC_LIB}" + "${SOURCE}" -o=program.exe + WORKING_DIRECTORY "${WORK_DIR}" + OUTPUT_VARIABLE out + ERROR_VARIABLE err + RESULT_VARIABLE status) +if(NOT status EQUAL 0) + message(FATAL_ERROR "--emit=exe -mm=${MODEL}: exit ${status}: ${out}${err}") +endif() + +execute_process( + COMMAND "${WORK_DIR}/program.exe" + WORKING_DIRECTORY "${WORK_DIR}" + OUTPUT_VARIABLE out + ERROR_VARIABLE err + RESULT_VARIABLE status) +if(NOT status EQUAL 1 OR NOT out MATCHES "^before\r?\n" OR NOT err MATCHES "${EXPECT}") + message(FATAL_ERROR "the program: exit ${status} (expected 1), stdout '${out}' (expected 'before'), " + "stderr '${err}' (expected '${EXPECT}')") +endif() + +message(STATUS "${err}") diff --git a/tslang/tslang/exe.cpp b/tslang/tslang/exe.cpp index 7b8d46087..bb028fbae 100644 --- a/tslang/tslang/exe.cpp +++ b/tslang/tslang/exe.cpp @@ -29,6 +29,8 @@ #include "link.h" +#include + namespace cl = llvm::cl; extern cl::opt emitAction; @@ -488,33 +490,43 @@ static bool resolveWindowsLibPath(const llvm::Triple &triple, std::string &path, return checkWindowsBinaryMachine(triple, lib); } -// A library tslang builds on Windows links with `-include:__tslang_heap_shared`, which the runtime -// (TypeScriptAsyncRuntime.lib) defines since #784. A runtime older than that failed every -// --emit=dll with a bare "unresolved external symbol __tslang_heap_shared" (TSLANG_ISSUES 36): say -// which runtime it is and that it is older than the compiler. `path` is the directory the link -// takes the runtime from (resolveWindowsLibPath has made it the x86 one for x86); a runtime not -// found there is the linker's to report. -static bool checkRuntimeSharesHeap(llvm::StringRef path) +// Whether the runtime in `path` (`archive`: TypeScriptAsyncRuntime.lib, libTypeScriptAsyncRuntime.a) defines +// `symbol`: its archive's symbol table names it (x86 decorates it with one more underscore). None when there is no +// runtime there to read. +static std::optional runtimeDefines(llvm::StringRef path, llvm::StringRef symbol, + llvm::StringRef archive = "TypeScriptAsyncRuntime.lib") { if (path.empty()) { - return true; + return std::nullopt; } llvm::SmallString<256> lib(path); - llvm::sys::path::append(lib, "TypeScriptAsyncRuntime.lib"); + llvm::sys::path::append(lib, archive); auto buffer = llvm::MemoryBuffer::getFile(lib, /*IsText=*/false, /*RequiresNullTerminator=*/false); if (!buffer) { - return true; + return std::nullopt; } - // the archive's symbol table names it (x86 decorates it with one more underscore) - if ((*buffer)->getBuffer().contains("__tslang_heap_shared")) + return (*buffer)->getBuffer().contains(symbol); +} + +// A library tslang builds on Windows links with `-include:__tslang_heap_shared`, which the runtime +// (TypeScriptAsyncRuntime.lib) defines since #784. A runtime older than that failed every +// --emit=dll with a bare "unresolved external symbol __tslang_heap_shared" (TSLANG_ISSUES 36): say +// which runtime it is and that it is older than the compiler. `path` is the directory the link +// takes the runtime from (resolveWindowsLibPath has made it the x86 one for x86); a runtime not +// found there is the linker's to report. +static bool checkRuntimeSharesHeap(llvm::StringRef path) +{ + if (runtimeDefines(path, "__tslang_heap_shared").value_or(true)) { return true; } + llvm::SmallString<256> lib(path); + llvm::sys::path::append(lib, "TypeScriptAsyncRuntime.lib"); llvm::WithColor::error(llvm::errs(), "tslang") << "the TypeScript runtime " << lib << " is older than this tslang: it has no __tslang_heap_shared, " << "which a library (--emit=dll) links with. Point --tslang-lib-path (or TSLANG_LIB_PATH) at the " @@ -758,6 +770,7 @@ int linkBinary(const char *argv0, const LinkRequest &request, CompileOptions &co auto isTslangLibNeeded = true; auto useMimalloc = false; + auto reportUncaught = false; auto os = TheTriple.getOS(); auto arch = TheTriple.getArch(); @@ -1014,6 +1027,14 @@ int linkBinary(const char *argv0, const LinkRequest &request, CompileOptions &co return 1; } + // An executable says what an exception nothing catches threw, as the JIT does, instead of ending + // with no word (TypeScriptAsyncRuntime/UncaughtException.cpp). A runtime older than that has no + // such file, and its program ends as it did. + auto linuxExe = !shared && os == llvm::Triple::Linux && !android; + reportUncaught = (!shared && winX86OrX64 && runtimeDefines(tslangLibPath, "__tslang_report_uncaught").value_or(false)) + || (linuxExe && runtimeDefines(tslangLibPath, "__tslang_report_uncaught", + "libTypeScriptAsyncRuntime.a").value_or(false)); + tslangLibPathOpt = getLibsPathOpt(tslangLibPath); if (!tslangLibPathOpt.empty()) { @@ -1136,6 +1157,18 @@ int linkBinary(const char *argv0, const LinkRequest &request, CompileOptions &co args.push_back("-Wl,--no-whole-archive"); } + // An executable says what an exception nothing catches threw, as the JIT does + // (TypeScriptAsyncRuntime/UncaughtException.cpp): ahead of the runtime, so the GNU linker takes the file + if (reportUncaught && win) + { + args.push_back(arch == llvm::Triple::x86 ? "-Wl,-include:___tslang_report_uncaught" + : "-Wl,-include:__tslang_report_uncaught"); + } + else if (reportUncaught) + { + args.push_back("-Wl,-u,__tslang_report_uncaught"); + } + args.push_back("-lTypeScriptAsyncRuntime"); // A library says it allocates from the program's heap (`__tslang_heap_shared`, with the @@ -1147,6 +1180,7 @@ int linkBinary(const char *argv0, const LinkRequest &request, CompileOptions &co args.push_back(arch == llvm::Triple::x86 ? "-Wl,-include:___tslang_heap_shared" : "-Wl,-include:__tslang_heap_shared"); } + } if (!win && !wasm) diff --git a/tslang/tslang/jit.cpp b/tslang/tslang/jit.cpp index 0af116707..edbbf3b1c 100644 --- a/tslang/tslang/jit.cpp +++ b/tslang/tslang/jit.cpp @@ -44,6 +44,7 @@ #include "llvm/Support/ManagedStatic.h" #include "TypeScript/Defines.h" +#include "TypeScript/UncaughtException.h" #include "jitcache.h" @@ -662,162 +663,35 @@ using JitEntryThunkFn = int (*)(int, char **); #ifdef _WIN32 // What an uncaught exception threw, said after "uncaught exception: ", or empty static char uncaughtDescription[512]; +#elif defined(TSLANG_DESCRIBES_UNCAUGHT) +static std::terminate_handler runtimeTerminateHandler; -// A thrown number as the program would print it (Number::toString, as ConvertLogic's -// __ts_number_to_string): the fewest digits that read back as the same double, plain decimal for -// a decimal exponent n in -6 < n <= 21, `d.ddde+N` / `e-N` otherwise. `%.17g` said `0.1` as -// 0.10000000000000001. -static void numberText(char *out, size_t size, double value) +// An exception nothing in the program catches, on Linux: the C++ runtime calls std::terminate() where it was +// thrown. Said as on Windows (runJit), and the run ends with exit code 1 - not in abort(), which LLVM's crash +// handler reported as a crash of the compiler. +[[noreturn]] static void reportUncaughtInJit() { - if (value != value) + char description[512]; + if (!typescript::describeCurrentException(description, sizeof(description))) { - snprintf(out, size, "NaN"); - return; - } - - if (value == 0) - { - snprintf(out, size, "0"); - return; - } - - auto *sign = value < 0 ? "-" : ""; - auto magnitude = value < 0 ? -value : value; - if (magnitude > DBL_MAX) - { - snprintf(out, size, "%sInfinity", sign); - return; - } - - char scientific[32]; - int decimals = 0; - for (; decimals < 16; decimals++) - { - snprintf(scientific, sizeof(scientific), "%.*e", decimals, magnitude); - if (strtod(scientific, nullptr) == magnitude) + if (runtimeTerminateHandler) { - break; + runtimeTerminateHandler(); } - } - if (decimals == 16) - { - snprintf(scientific, sizeof(scientific), "%.*e", decimals, magnitude); + abort(); } - char digits[32]; - snprintf(digits, sizeof(digits), "%c%.*s", scientific[0], decimals, scientific + 2); - int k = decimals + 1; - int n = atoi(strchr(scientific, 'e') + 1) + 1; - static const char zeros[] = "000000000000000000000"; - if (k <= n && n <= 21) - { - snprintf(out, size, "%s%s%.*s", sign, digits, n - k, zeros); - } - else if (0 < n && n <= 21) - { - snprintf(out, size, "%s%.*s.%s", sign, n, digits, digits + n); - } - else if (-6 < n && n <= 0) - { - snprintf(out, size, "%s0.%.*s%s", sign, -n, zeros, digits); - } - else + fflush(stdout); + auto &os = llvm::WithColor::error(llvm::errs(), "tslang") << "uncaught exception"; + if (description[0]) { - snprintf(out, size, "%s%c%s%se%+d", sign, digits[0], k > 1 ? "." : "", digits + 1, n - 1); + os << ": " << description; } -} - -// The thrown value, read from the exception record of a throw (_CxxThrowException's): the object, and its -// ThrowInfo, whose first catchable type names its type (`.N` a number, `.PEAD` a string, `.PEAVName@@` a class). -// Read while the object is alive, in the filter; a record not shaped so is left undescribed. -static void describeUncaught(EXCEPTION_POINTERS *pointers) -{ - uncaughtDescription[0] = '\0'; - __try - { - auto *record = pointers->ExceptionRecord; - if (record->NumberParameters < 4) - { - return; - } - - auto *object = reinterpret_cast(record->ExceptionInformation[1]); - auto *throwInfo = reinterpret_cast(record->ExceptionInformation[2]); - auto *imageBase = reinterpret_cast(record->ExceptionInformation[3]); - if (!object || !throwInfo || !imageBase) - { - return; - } - auto *catchableTypes = reinterpret_cast(imageBase + throwInfo[3]); - auto count = catchableTypes[0]; - if (count < 1) - { - return; - } - - // a type descriptor: the type_info vtable, a spare pointer, then the name - auto typeName = [&](int index) { - auto *catchableType = reinterpret_cast(imageBase + catchableTypes[1 + index]); - return reinterpret_cast(imageBase + catchableType[1] + 2 * sizeof(void *)); - }; - - auto *name = typeName(0); - auto size = sizeof(uncaughtDescription); - if (strcmp(name, ".PEAD") == 0) - { - auto *text = *reinterpret_cast(object); - snprintf(uncaughtDescription, size, "%s", text ? text : "null"); - } - else if (strcmp(name, ".N") == 0) - { - numberText(uncaughtDescription, size, *reinterpret_cast(object)); - } - else if (strcmp(name, ".H") == 0) - { - snprintf(uncaughtDescription, size, "%d", *reinterpret_cast(object)); - } - else if (strcmp(name, "._N") == 0) - { - snprintf(uncaughtDescription, size, "%s", *reinterpret_cast(object) ? "true" : "false"); - } - else if (strcmp(name, "._J") == 0) - { - snprintf(uncaughtDescription, size, "%lldn", *reinterpret_cast(object)); - } - else if (strncmp(name, ".PEAV", 5) == 0) - { - // a class: its name, up to the `@@` that ends it - auto *className = name + 5; - auto *end = strstr(className, "@@"); - auto length = end ? static_cast(end - className) : static_cast(strlen(className)); - snprintf(uncaughtDescription, size, "%.*s", length, className); - // an Error (core.ts): after the vtable, `message?: string` - the text, and whether it is set - for (int i = 0; i < count; i++) - { - if (strcmp(typeName(i), ".PEAVError@@") != 0) - { - continue; - } - - auto *instance = *reinterpret_cast(object); - auto *message = instance ? *reinterpret_cast(instance + sizeof(void *)) : nullptr; - auto isSet = instance && *reinterpret_cast(instance + 2 * sizeof(void *)); - if (isSet && message) - { - auto used = strlen(uncaughtDescription); - snprintf(uncaughtDescription + used, size - used, ": %s", message); - } - - break; - } - } - } - __except (EXCEPTION_EXECUTE_HANDLER) - { - uncaughtDescription[0] = '\0'; - } + os << "\n"; + os.flush(); + _exit(1); } #endif @@ -835,11 +709,16 @@ static bool callEntryThunk(JitEntryThunkFn entryThunk, int argc, char **argv, in exitCode = entryThunk(argc, argv); return true; } - __except (GetExceptionCode() == cxxExceptionCode ? (describeUncaught(GetExceptionInformation()), EXCEPTION_EXECUTE_HANDLER) + __except (GetExceptionCode() == cxxExceptionCode ? (typescript::describeUncaught(GetExceptionInformation(), uncaughtDescription, sizeof(uncaughtDescription)), EXCEPTION_EXECUTE_HANDLER) : EXCEPTION_CONTINUE_SEARCH) { return false; } +#elif defined(TSLANG_DESCRIBES_UNCAUGHT) + runtimeTerminateHandler = std::set_terminate(reportUncaughtInJit); + exitCode = entryThunk(argc, argv); + std::set_terminate(runtimeTerminateHandler); + return true; #else exitCode = entryThunk(argc, argv); return true;