From 633965cb4462bc3eb3fbea482c09e0371ec4859a Mon Sep 17 00:00:00 2001 From: ASDAlexander77 Date: Sun, 5 Jul 2026 13:24:46 +0100 Subject: [PATCH 1/5] Fix puts/cf.assert signature conflict crashing JIT tests with --no-default-lib MLIR's cf.assert-to-LLVM lowering reserves "puts" as void(ptr) when it needs to print an assertion message. Our PrintOp lowering separately declared "puts" as i32(ptr) (matching libc), so whichever declaration landed first caused "redefinition of reserved function 'puts' of different type" and left 'cf.assert' unlegalized, crashing any JIT/AOT test that both printed and hit a lowered cf.assert. Declare it as void(ptr) instead since the return value was never used. Re-enables test-jit-00-async-await and test-jit-00-for-await on Windows now that they pass; test-jit-00-for-await-yield stays disabled since it hits a separate async-generator runtime hang. --- tslang/lib/TypeScript/LowerToLLVM.cpp | 6 ++++-- tslang/test/tester/CMakeLists.txt | 10 +++------- 2 files changed, 7 insertions(+), 9 deletions(-) diff --git a/tslang/lib/TypeScript/LowerToLLVM.cpp b/tslang/lib/TypeScript/LowerToLLVM.cpp index b066dcd8b..2fb88c302 100644 --- a/tslang/lib/TypeScript/LowerToLLVM.cpp +++ b/tslang/lib/TypeScript/LowerToLLVM.cpp @@ -158,8 +158,10 @@ class PrintOpLowering : public TsLlvmPattern auto i8PtrType = th.getPtrType(); auto ptrType = th.getPtrType(); - // Get a symbol reference to the printf function, inserting it if necessary. - auto putsFuncOp = ch.getOrInsertFunction("puts", th.getFunctionType(rewriter.getI32Type(), ptrType, false)); + // Get a symbol reference to the puts function, inserting it if necessary. + // Note: declared as returning void (not the libc i32) to match the signature MLIR's + // cf.assert-to-LLVM lowering expects when it reserves/looks up "puts" for its own use. + auto putsFuncOp = ch.getOrInsertFunction("puts", th.getFunctionType(th.getVoidType(), ptrType, false)); auto strType = mlir_ts::StringType::get(rewriter.getContext()); diff --git a/tslang/test/tester/CMakeLists.txt b/tslang/test/tester/CMakeLists.txt index c18fdf16e..e62fbb695 100644 --- a/tslang/test/tester/CMakeLists.txt +++ b/tslang/test/tester/CMakeLists.txt @@ -623,14 +623,10 @@ add_test(NAME test-jit-00-safe-cast-field-access COMMAND test-runner -jit "${PRO add_test(NAME test-jit-00-safe-cast-bug COMMAND test-runner -jit "${PROJECT_SOURCE_DIR}/test/tester/tests/00safe_cast_bug.ts") add_test(NAME test-jit-00-optional COMMAND test-runner -jit "${PROJECT_SOURCE_DIR}/test/tester/tests/00optional.ts") add_test(NAME test-jit-01-optional COMMAND test-runner -jit "${PROJECT_SOURCE_DIR}/test/tester/tests/01optional.ts") -if (NOT(WIN32)) -# TODO: crash -#add_test(NAME test-jit-00-async-await COMMAND test-runner -jit "${PROJECT_SOURCE_DIR}/test/tester/tests/00async_await.ts") -# TODO: crash -#add_test(NAME test-jit-00-for-await COMMAND test-runner -jit "${PROJECT_SOURCE_DIR}/test/tester/tests/00for_await.ts") -# TODO: in opt mode, error +add_test(NAME test-jit-00-async-await COMMAND test-runner -jit "${PROJECT_SOURCE_DIR}/test/tester/tests/00async_await.ts") +add_test(NAME test-jit-00-for-await COMMAND test-runner -jit "${PROJECT_SOURCE_DIR}/test/tester/tests/00for_await.ts") +# TODO: async generator / for-await-of hangs at runtime under JIT (unrelated to the puts/cf.assert crash fixed above) #add_test(NAME test-jit-00-for-await-yield COMMAND test-runner -jit "${PROJECT_SOURCE_DIR}/test/tester/tests/00for_await_yield.ts") -endif() if (NOT(WIN32)) add_test(NAME test-jit-00-try-catch COMMAND test-runner -jit "${PROJECT_SOURCE_DIR}/test/tester/tests/00try_catch.ts") add_test(NAME test-jit-01-try-catch COMMAND test-runner -jit "${PROJECT_SOURCE_DIR}/test/tester/tests/01try_catch.ts") From 2d6c81b23536b1452558531689110c99dd2e8d2a Mon Sep 17 00:00:00 2001 From: ASDAlexander77 Date: Sun, 5 Jul 2026 18:29:34 +0100 Subject: [PATCH 2/5] Fix JIT teardown process to prevent deadlocks and ensure proper flushing of stdio on Windows --- tslang/test/tester/CMakeLists.txt | 2 +- tslang/tslang/jit.cpp | 24 ++++++++++++++++++++---- 2 files changed, 21 insertions(+), 5 deletions(-) diff --git a/tslang/test/tester/CMakeLists.txt b/tslang/test/tester/CMakeLists.txt index e62fbb695..8668c4537 100644 --- a/tslang/test/tester/CMakeLists.txt +++ b/tslang/test/tester/CMakeLists.txt @@ -626,7 +626,7 @@ add_test(NAME test-jit-01-optional COMMAND test-runner -jit "${PROJECT_SOURCE_DI add_test(NAME test-jit-00-async-await COMMAND test-runner -jit "${PROJECT_SOURCE_DIR}/test/tester/tests/00async_await.ts") add_test(NAME test-jit-00-for-await COMMAND test-runner -jit "${PROJECT_SOURCE_DIR}/test/tester/tests/00for_await.ts") # TODO: async generator / for-await-of hangs at runtime under JIT (unrelated to the puts/cf.assert crash fixed above) -#add_test(NAME test-jit-00-for-await-yield COMMAND test-runner -jit "${PROJECT_SOURCE_DIR}/test/tester/tests/00for_await_yield.ts") +add_test(NAME test-jit-00-for-await-yield COMMAND test-runner -jit "${PROJECT_SOURCE_DIR}/test/tester/tests/00for_await_yield.ts") if (NOT(WIN32)) add_test(NAME test-jit-00-try-catch COMMAND test-runner -jit "${PROJECT_SOURCE_DIR}/test/tester/tests/00try_catch.ts") add_test(NAME test-jit-01-try-catch COMMAND test-runner -jit "${PROJECT_SOURCE_DIR}/test/tester/tests/01try_catch.ts") diff --git a/tslang/tslang/jit.cpp b/tslang/tslang/jit.cpp index 837577449..0e0f208c3 100644 --- a/tslang/tslang/jit.cpp +++ b/tslang/tslang/jit.cpp @@ -264,13 +264,29 @@ int runJit(int argc, char **argv, mlir::ModuleOp module, CompileOptions &compile } // The JIT program has finished. On Windows/COFF the MLIR/ORC LLJIT platform - // registers process-level atexit glue that faults during teardown, so the - // normal CRT exit path crashes after a successful run. Flush stdio and exit - // the process directly, bypassing the CRT atexit handlers. + // registers process-level atexit glue that faults during teardown (exception + // 0x80000003), so the normal CRT exit path crashes after a successful run. + // ExitProcess is not safe either: it runs DLL_PROCESS_DETACH, and + // TypeScriptRuntime.dll's static AsyncRuntime destructor then waits on its + // thread pool whose worker threads ExitProcess has already terminated, + // deadlocking forever when async work is still pending. TerminateProcess + // skips all teardown, so nothing can crash or deadlock — but it also skips + // the detach-time stdio flush, so flush every CRT the JIT'd code may have + // bound to (ucrtbase buffers its streams separately from our static CRT). fflush(stdout); fflush(stderr); #ifdef _WIN32 - ExitProcess(0); + for (auto crtName : {"ucrtbase.dll", "ucrtbased.dll"}) + { + if (HMODULE crt = GetModuleHandleA(crtName)) + { + if (auto crtFflush = (int(__cdecl *)(FILE *))GetProcAddress(crt, "fflush")) + { + crtFflush(nullptr); // fflush(NULL) flushes all of that CRT's output streams + } + } + } + TerminateProcess(GetCurrentProcess(), 0); #endif return 0; } From b4ebb3267c1e9e661e4b3d7223ccdffce0580168 Mon Sep 17 00:00:00 2001 From: ASDAlexander77 Date: Sun, 5 Jul 2026 18:59:10 +0100 Subject: [PATCH 3/5] Update TODO comment for async generator and for-await-of tests in CMakeLists.txt --- tslang/test/tester/CMakeLists.txt | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/tslang/test/tester/CMakeLists.txt b/tslang/test/tester/CMakeLists.txt index 8668c4537..e33d43c83 100644 --- a/tslang/test/tester/CMakeLists.txt +++ b/tslang/test/tester/CMakeLists.txt @@ -625,7 +625,8 @@ add_test(NAME test-jit-00-optional COMMAND test-runner -jit "${PROJECT_SOURCE_DI add_test(NAME test-jit-01-optional COMMAND test-runner -jit "${PROJECT_SOURCE_DIR}/test/tester/tests/01optional.ts") add_test(NAME test-jit-00-async-await COMMAND test-runner -jit "${PROJECT_SOURCE_DIR}/test/tester/tests/00async_await.ts") add_test(NAME test-jit-00-for-await COMMAND test-runner -jit "${PROJECT_SOURCE_DIR}/test/tester/tests/00for_await.ts") -# TODO: async generator / for-await-of hangs at runtime under JIT (unrelated to the puts/cf.assert crash fixed above) +# TODO: async generator / for-await-of hangs at runtime under JIT +# error: EXEC : LLVM error : Coroutines cannot handle non static allocas yet add_test(NAME test-jit-00-for-await-yield COMMAND test-runner -jit "${PROJECT_SOURCE_DIR}/test/tester/tests/00for_await_yield.ts") if (NOT(WIN32)) add_test(NAME test-jit-00-try-catch COMMAND test-runner -jit "${PROJECT_SOURCE_DIR}/test/tester/tests/00try_catch.ts") From 16fa1123590c3d3b07b339f8abcdd29bf8ee5fd1 Mon Sep 17 00:00:00 2001 From: ASDAlexander77 Date: Sun, 5 Jul 2026 19:22:45 +0100 Subject: [PATCH 4/5] Implement check for presplit coroutine in StringConcatOpLowering to handle heap allocation --- tslang/lib/TypeScript/LowerToLLVM.cpp | 29 ++++++++++++++++++++++++++- 1 file changed, 28 insertions(+), 1 deletion(-) diff --git a/tslang/lib/TypeScript/LowerToLLVM.cpp b/tslang/lib/TypeScript/LowerToLLVM.cpp index 2fb88c302..8e9c5d066 100644 --- a/tslang/lib/TypeScript/LowerToLLVM.cpp +++ b/tslang/lib/TypeScript/LowerToLLVM.cpp @@ -520,6 +520,32 @@ class SetStringLengthOpLowering : public TsLlvmPatterngetParentOp(); parent; parent = parent->getParentOp()) + { + if (auto passthrough = parent->getAttrOfType("passthrough")) + { + for (auto attr : passthrough) + { + if (auto strAttr = dyn_cast(attr)) + { + if (strAttr.getValue() == "presplitcoroutine") + { + return true; + } + } + } + } + } + + return false; +} + class StringConcatOpLowering : public TsLlvmPattern { public: @@ -554,7 +580,8 @@ class StringConcatOpLowering : public TsLlvmPattern size = rewriter.create(loc, llvmIndexType, ValueRange{size, size1.getResult()}); } - auto allocInStack = op.getAllocInStack().has_value() && op.getAllocInStack().value(); + auto allocInStack = op.getAllocInStack().has_value() && op.getAllocInStack().value() + && !isInsidePresplitCoroutine(op); mlir::Value newStringValue = allocInStack ? ch.Alloca(i8PtrTy, size, true) : ch.MemoryAlloc(size); From 2401f243855c4d93893ee5fef8afdfa24a658841 Mon Sep 17 00:00:00 2001 From: ASDAlexander77 Date: Sun, 5 Jul 2026 19:56:21 +0100 Subject: [PATCH 5/5] Add GC root management functions and integrate with JIT memory mapping --- tslang/include/TypeScript/gcwrapper.h | 4 + .../TypeScriptRuntime/TypeScriptRuntime.def | 2 + tslang/lib/TypeScriptRuntime/gc.cpp | 10 +++ tslang/test/tester/CMakeLists.txt | 2 +- tslang/tslang/jit.cpp | 76 ++++++++++++++++++- 5 files changed, 92 insertions(+), 2 deletions(-) diff --git a/tslang/include/TypeScript/gcwrapper.h b/tslang/include/TypeScript/gcwrapper.h index 3759a48b3..e9b666118 100644 --- a/tslang/include/TypeScript/gcwrapper.h +++ b/tslang/include/TypeScript/gcwrapper.h @@ -19,6 +19,10 @@ void _mlir__GC_free(void *ptr); size_t _mlir__GC_get_heap_size(); +void _mlir__GC_add_roots(void *low, void *highPlus1); + +void _mlir__GC_remove_roots(void *low, void *highPlus1); + void _mlir__GC_win32_free_heap(); void *_mlir__GC_malloc_explicitly_typed(size_t size, int64_t descr); diff --git a/tslang/lib/TypeScriptRuntime/TypeScriptRuntime.def b/tslang/lib/TypeScriptRuntime/TypeScriptRuntime.def index c5affeea3..d4b3e5799 100644 --- a/tslang/lib/TypeScriptRuntime/TypeScriptRuntime.def +++ b/tslang/lib/TypeScriptRuntime/TypeScriptRuntime.def @@ -15,6 +15,8 @@ EXPORTS GC_realloc=_mlir__GC_realloc GC_free=_mlir__GC_free GC_get_heap_size=_mlir__GC_get_heap_size + GC_add_roots=_mlir__GC_add_roots + GC_remove_roots=_mlir__GC_remove_roots GC_malloc_explicitly_typed=_mlir__GC_malloc_explicitly_typed GC_make_descriptor=_mlir__GC_make_descriptor diff --git a/tslang/lib/TypeScriptRuntime/gc.cpp b/tslang/lib/TypeScriptRuntime/gc.cpp index 4bb539f6a..e4132696f 100644 --- a/tslang/lib/TypeScriptRuntime/gc.cpp +++ b/tslang/lib/TypeScriptRuntime/gc.cpp @@ -58,6 +58,16 @@ size_t _mlir__GC_get_heap_size() return GC_get_heap_size(); } +void _mlir__GC_add_roots(void *low, void *highPlus1) +{ + GC_add_roots(low, highPlus1); +} + +void _mlir__GC_remove_roots(void *low, void *highPlus1) +{ + GC_remove_roots(low, highPlus1); +} + void _mlir__GC_win32_free_heap() { #ifdef WIN32 diff --git a/tslang/test/tester/CMakeLists.txt b/tslang/test/tester/CMakeLists.txt index e33d43c83..716dee327 100644 --- a/tslang/test/tester/CMakeLists.txt +++ b/tslang/test/tester/CMakeLists.txt @@ -736,7 +736,7 @@ add_test(NAME test-jit-244-arraysome COMMAND test-runner -jit "${PROJECT_SOURCE add_test(NAME test-jit-Grammar_and_types COMMAND test-runner -jit "${PROJECT_SOURCE_DIR}/test/tester/tests/Grammar_and_types.ts") add_test(NAME test-jit-StackTest COMMAND test-runner -jit "${PROJECT_SOURCE_DIR}/test/tester/tests/00stack_test.ts") # TODO: find out why it is crashing -#add_test(NAME test-jit-Raytrace COMMAND test-runner -jit "${PROJECT_SOURCE_DIR}/test/tester/tests/raytrace.ts") +add_test(NAME test-jit-Raytrace COMMAND test-runner -jit "${PROJECT_SOURCE_DIR}/test/tester/tests/raytrace.ts") add_test(NAME test-jit-Path COMMAND test-runner -jit "${PROJECT_SOURCE_DIR}/test/tester/tests/path.ts") add_test(NAME test-jit-Print-Bug-01 COMMAND test-runner -jit "${PROJECT_SOURCE_DIR}/test/tester/tests/01print-bug.ts") add_test(NAME test-jit-arrayLiterals COMMAND test-runner -jit "${PROJECT_SOURCE_DIR}/test/tester/tests/arrayLiterals.ts") diff --git a/tslang/tslang/jit.cpp b/tslang/tslang/jit.cpp index 0e0f208c3..c0d07ff53 100644 --- a/tslang/tslang/jit.cpp +++ b/tslang/tslang/jit.cpp @@ -3,7 +3,11 @@ #include "mlir/ExecutionEngine/ExecutionEngine.h" #include "mlir/IR/BuiltinOps.h" +#include "llvm/ADT/STLExtras.h" +#include "llvm/ExecutionEngine/SectionMemoryManager.h" #include "llvm/Support/DynamicLibrary.h" +#include "llvm/Support/Memory.h" +#include "llvm/Support/Path.h" #include #ifdef _WIN32 @@ -92,6 +96,63 @@ int loadLibrary(mlir::SmallString<256> &libPath, llvm::StringMap &export return 0; } +// JIT'd globals live in RTDyld-allocated RW data sections (plain VirtualAlloc/mmap +// memory), which the Boehm GC does not scan: for AOT binaries the loader-mapped +// data segment is registered as a root set automatically, but in the JIT an object +// reachable only from a global (e.g. a static class member) is collected on the +// first GC cycle and its memory recycled, corrupting later accesses. Register every +// RW data section with the GC as a root range. GC_add_roots/GC_remove_roots are +// resolved dynamically from the already-loaded TypeScriptRuntime library, so this +// stays inert when the GC is disabled or not present. +class GCRootsSectionMemoryMapper : public llvm::SectionMemoryManager::MemoryMapper +{ + // AllocationPurpose lives in the enclosing SectionMemoryManager, not in the + // MemoryMapper base class, so it is not found by unqualified lookup here + using AllocationPurpose = llvm::SectionMemoryManager::AllocationPurpose; + using GCRootsFn = void (*)(void *, void *); + + public: + llvm::sys::MemoryBlock allocateMappedMemory(AllocationPurpose purpose, size_t numBytes, + const llvm::sys::MemoryBlock *const nearBlock, unsigned flags, + std::error_code &errCode) override + { + auto block = llvm::sys::Memory::allocateMappedMemory(numBytes, nearBlock, flags, errCode); + if (purpose == AllocationPurpose::RWData && block.base() != nullptr) + { + if (auto addRoots = reinterpret_cast( + llvm::sys::DynamicLibrary::SearchForAddressOfSymbol("GC_add_roots"))) + { + addRoots(block.base(), static_cast(block.base()) + block.allocatedSize()); + gcRootBlocks.push_back(block.base()); + } + } + + return block; + } + + std::error_code protectMappedMemory(const llvm::sys::MemoryBlock &block, unsigned flags) override + { + return llvm::sys::Memory::protectMappedMemory(block, flags); + } + + std::error_code releaseMappedMemory(llvm::sys::MemoryBlock &block) override + { + if (llvm::is_contained(gcRootBlocks, block.base())) + { + if (auto removeRoots = reinterpret_cast( + llvm::sys::DynamicLibrary::SearchForAddressOfSymbol("GC_remove_roots"))) + { + removeRoots(block.base(), static_cast(block.base()) + block.allocatedSize()); + } + } + + return llvm::sys::Memory::releaseMappedMemory(block); + } + + private: + mlir::SmallVector gcRootBlocks; +}; + int runJit(int argc, char **argv, mlir::ModuleOp module, CompileOptions &compileOptions) { // to avoid false positive memory leak reports in release builds @@ -135,8 +196,17 @@ int runJit(int argc, char **argv, mlir::ModuleOp module, CompileOptions &compile #define LIB_NAME "lib" #define LIB_EXT "so" #endif + // Only locate a GC runtime when the user has not already supplied one via + // --shared-libs: loading a second TypeScriptRuntime copy from a different path + // (e.g. TSLANG_LIB_PATH) creates two independent GC instances, so roots + // registered in one are invisible to collections running in the other and + // GC-heap pointers cross between the two heaps. + auto hasTypeScriptRuntime = llvm::any_of(clSharedLibs, [](const std::string &libPath) { + return llvm::sys::path::stem(libPath).contains_insensitive("TypeScriptRuntime"); + }); + std::string pathTypeScriptLib("../lib/" LIB_NAME "TypeScriptRuntime." LIB_EXT); - if (!disableGC.getValue()) + if (!disableGC.getValue() && !hasTypeScriptRuntime) { auto absPath3 = makeAbsolutePath(mergeWithDefaultLibPath(getTslangLibPath(), LIB_NAME "TypeScriptRuntime." LIB_EXT)); if (absPath3.empty()) @@ -175,7 +245,11 @@ int runJit(int argc, char **argv, mlir::ModuleOp module, CompileOptions &compile mlir::SmallVector sharedLibPaths; sharedLibPaths.append(begin(clSharedLibs), end(clSharedLibs)); + // must outlive the engine: sections are released from the engine's destructor + GCRootsSectionMemoryMapper gcRootsMemoryMapper; + mlir::ExecutionEngineOptions engineOptions; + engineOptions.sectionMemoryMapper = &gcRootsMemoryMapper; engineOptions.transformer = optPipeline; engineOptions.enableObjectDump = dumpObjectFile; engineOptions.enableGDBNotificationListener = !enableOpt;