From 16017c03c87df2535efbd07abc41c00a1a03ea92 Mon Sep 17 00:00:00 2001 From: ASDAlexander77 Date: Wed, 30 Sep 2026 19:15:04 +0100 Subject: [PATCH] Add JIT test for throwing exceptions without TypeScriptRuntime and implement type_info vtable handling --- tslang/test/tester/CMakeLists.txt | 10 +++++ .../tester/jit-throw-without-runtime.cmake | 39 +++++++++++++++++++ .../tests/00jit_throw_without_runtime.ts | 28 +++++++++++++ tslang/tslang/jit.cpp | 14 +++++++ 4 files changed, 91 insertions(+) create mode 100644 tslang/test/tester/jit-throw-without-runtime.cmake create mode 100644 tslang/test/tester/tests/00jit_throw_without_runtime.ts diff --git a/tslang/test/tester/CMakeLists.txt b/tslang/test/tester/CMakeLists.txt index 5ca97d9a2..927980a5e 100644 --- a/tslang/test/tester/CMakeLists.txt +++ b/tslang/test/tester/CMakeLists.txt @@ -2219,6 +2219,16 @@ add_test(NAME test-compile-debug-info-opt -DTESTS_DIR=${PROJECT_SOURCE_DIR}/test/tester/tests -P ${CMAKE_CURRENT_SOURCE_DIR}/debug-info-opt.cmake) +# A throw under the JIT without TypeScriptRuntime (which test-runner always passes), under every +# model but gc - see jit-throw-without-runtime.cmake. Windows only: the symbol is MSVC's type_info vtable. +if (WIN32) + add_test(NAME test-jit-throw-without-runtime + COMMAND ${CMAKE_COMMAND} + -DTSLANG=$ + -DTESTS_DIR=${PROJECT_SOURCE_DIR}/test/tester/tests + -P ${CMAKE_CURRENT_SOURCE_DIR}/jit-throw-without-runtime.cmake) +endif() + # tslang choosing the collector's linkage itself under -mm=gc - see shared-collector-auto.cmake. if (WIN32) if ("${CMAKE_BUILD_TYPE}" STREQUAL "Release") diff --git a/tslang/test/tester/jit-throw-without-runtime.cmake b/tslang/test/tester/jit-throw-without-runtime.cmake new file mode 100644 index 000000000..1aa2d09f6 --- /dev/null +++ b/tslang/test/tester/jit-throw-without-runtime.cmake @@ -0,0 +1,39 @@ +# Runs a program that throws under the JIT with NO --shared-libs, under every model but gc. +# +# `test-runner` always passes `--shared-libs=TypeScriptRuntime.dll`, and that DLL exports type_info's +# vtable (??_7type_info@@6B@), which every type descriptor a throw emits points at. A JIT run loads +# the DLL by itself only under gc; under rc, none and own without it, nothing in the process +# exported the vtable, and any program with a `throw` failed to materialize: "Symbols not found: +# [ ??_7type_info@@6B@ ]". The JIT now binds it itself (see jitTypeInfoVftable in jit.cpp). + +if(NOT DEFINED TSLANG OR NOT DEFINED TESTS_DIR) + message(FATAL_ERROR "TSLANG and TESTS_DIR are both required") +endif() + +set(name 00jit_throw_without_runtime.ts) + +set(failures "") +foreach(model rc none own) + execute_process( + COMMAND "${TSLANG}" --emit=jit -mm=${model} --no-default-lib "${TESTS_DIR}/${name}" + OUTPUT_VARIABLE output + ERROR_VARIABLE diagnostics + RESULT_VARIABLE status) + + if(NOT status EQUAL 0 OR NOT output MATCHES "ALL DONE") + # the first error, not a warning printed ahead of it + if(diagnostics MATCHES "[^\n]*error:[^\n]*") + set(first_line "${CMAKE_MATCH_0}") + else() + string(REGEX REPLACE "\n.*" "" first_line "${diagnostics}") + endif() + list(APPEND failures " -mm=${model}: exit ${status}: ${first_line}") + endif() +endforeach() + +if(failures) + string(REPLACE ";" "\n" report "${failures}") + message(FATAL_ERROR "a throw under the JIT without TypeScriptRuntime:\n${report}") +endif() + +message(STATUS "a throw under the JIT without TypeScriptRuntime runs under rc, none and own") diff --git a/tslang/test/tester/tests/00jit_throw_without_runtime.ts b/tslang/test/tester/tests/00jit_throw_without_runtime.ts new file mode 100644 index 000000000..cfe4c7c09 --- /dev/null +++ b/tslang/test/tester/tests/00jit_throw_without_runtime.ts @@ -0,0 +1,28 @@ +// Run by jit-throw-without-runtime.cmake under the JIT with no --shared-libs: every throw needs +// type_info's vtable, which only TypeScriptRuntime.dll exported, and the JIT loads that for gc alone. +class Failure { + constructor(public code: number) {} +} + +function throwNumber() { + throw 1; +} + +function throwString() { + throw "text"; +} + +function throwClass() { + throw new Failure(3); +} + +function main() { + let caught = 0; + + try { throwNumber(); } catch (e) { caught++; } + try { throwString(); } catch (e) { caught++; } + try { throwClass(); } catch (e) { caught++; } + + assert(caught == 3, "every throw is caught"); + print("ALL DONE"); +} diff --git a/tslang/tslang/jit.cpp b/tslang/tslang/jit.cpp index 98af51789..ecc2bc661 100644 --- a/tslang/tslang/jit.cpp +++ b/tslang/tslang/jit.cpp @@ -22,6 +22,7 @@ #include #include #include +#include #ifdef _WIN32 #include #include @@ -434,6 +435,16 @@ static void jitCxxThrowException(void *exceptionObject, void *throwInfo) // exception personality so throw and catch sides use the same CRT extern "C" EXCEPTION_DISPOSITION __CxxFrameHandler3(struct _EXCEPTION_RECORD *, void *, struct _CONTEXT *, struct _DISPATCHER_CONTEXT *); + +// The vtable of type_info (??_7type_info@@6B@), which every type descriptor a throw emits points +// at. tslang.exe links the CRT statically, so no DLL in the process exports it - only +// TypeScriptRuntime.dll, which re-exports its own copy and which the JIT loads for `gc` alone. +// Without it every rc/none/own program that throws failed to materialize. Every type_info object +// starts with this pointer, so take it from one: the static CRT's, like __CxxFrameHandler3's. +static void *jitTypeInfoVftable() +{ + return *reinterpret_cast(&typeid(int)); +} #endif #define JIT_ENTRY_THUNK_NAME "__tslang_jit_main" @@ -720,6 +731,7 @@ static int prepareJitProcess(CompileOptions &compileOptions) // jitCxxThrowException above). addSym("__CxxFrameHandler3", (void *)&__CxxFrameHandler3); addSym("_CxxThrowException", (void *)&jitCxxThrowException); + addSym("??_7type_info@@6B@", jitTypeInfoVftable()); #endif } #endif @@ -879,6 +891,8 @@ static std::unique_ptr createJit(llvm::orc::JITTargetMachineBu // fixes up the throw-site image base (see jitCxxThrowException above) addOverride("__CxxFrameHandler3", (void *)&__CxxFrameHandler3); addOverride("_CxxThrowException", (void *)&jitCxxThrowException); + // see jitTypeInfoVftable above + addOverride("??_7type_info@@6B@", jitTypeInfoVftable()); #endif // The stand-in decided above. It goes here rather than through // DynamicLibrary::AddSymbol because the process generator resolves from export tables