From af6692f4a5fd1afbd74a690cb2d8c74722deb214 Mon Sep 17 00:00:00 2001 From: ASDAlexander77 Date: Wed, 30 Sep 2026 19:57:19 +0100 Subject: [PATCH] An async program links without the collector under rc, none and own Every program that awaits anything, built ahead of time under rc, none or own, failed to link: TypeScriptAsyncRuntime.lib(AsyncRuntime.obj) : error LNK2019: unresolved external symbol GC_allow_register_threads referenced in function GC_enable_threads (and GC_register_my_thread, GC_unregister_my_thread, GC_get_stack_base) The async runtime registers its pool threads with the collector under gc (AsyncGCThreads.h, from #308). At run time that is inert under the other models - it only switches on through GC_enable_threads, which only the GC pass injects - but it sat in the scheduler's own object file, which every async program links, and only gc programs link gc.lib. `none` linked before #308. GC_enable_threads and the collector calls move to an object file of their own (AsyncGCThreadsCommon.inc, built as AsyncGCThreads.cpp in both TypeScriptAsyncRuntime and TypeScriptRuntime). The scheduler reaches them through two hooks that GC_enable_threads installs, so only the call in a gc program pulls that object in. The scheduler no longer includes gc.h. test-runner links every model with lld and gc.lib itself, which is why the rc and none corpus runs of 00async_await.ts passed. New test-compile-async-without-collector (async-without-collector.cmake) builds 00async_await.ts with `tslang --emit=exe` under rc, none and own, with no GC_LIB_PATH, and runs it. DefaultLib's sleep.ts, which failed the same way under rc, links and runs. 00async_gc_threading.ts under gc (the fault the registration fixed, about 6 runs in 10 at -O0 without it): 0 of 20 failures at -O0 and at -O3. Co-Authored-By: Claude Opus 5.5 --- tslang/include/TypeScript/AsyncGCThreads.h | 47 +++++++------ tslang/lib/AsyncGCThreadsCommon.inc | 39 +++++++++++ tslang/lib/AsyncRuntimeCommon.inc | 9 +-- .../TypeScriptAsyncRuntime/AsyncGCThreads.cpp | 4 ++ .../lib/TypeScriptAsyncRuntime/CMakeLists.txt | 3 +- .../lib/TypeScriptRuntime/AsyncGCThreads.cpp | 3 + tslang/lib/TypeScriptRuntime/AsyncRuntime.cpp | 1 - tslang/lib/TypeScriptRuntime/CMakeLists.txt | 1 + .../TypeScriptRuntime/TypeScriptRuntime.def | 2 +- tslang/test/tester/CMakeLists.txt | 19 ++++++ .../test/tester/async-without-collector.cmake | 68 +++++++++++++++++++ 11 files changed, 161 insertions(+), 35 deletions(-) create mode 100644 tslang/lib/AsyncGCThreadsCommon.inc create mode 100644 tslang/lib/TypeScriptAsyncRuntime/AsyncGCThreads.cpp create mode 100644 tslang/lib/TypeScriptRuntime/AsyncGCThreads.cpp create mode 100644 tslang/test/tester/async-without-collector.cmake diff --git a/tslang/include/TypeScript/AsyncGCThreads.h b/tslang/include/TypeScript/AsyncGCThreads.h index 683627251..8bfe76a50 100644 --- a/tslang/include/TypeScript/AsyncGCThreads.h +++ b/tslang/include/TypeScript/AsyncGCThreads.h @@ -21,30 +21,34 @@ // registers can be freed underneath it. Hence the registration below. // // Only a `gc` build calls GC_enable_threads - the GC pass injects the call beside GC_init, and -// that pass runs for no other model - so `rc` and `none` reach GCThreadRegistration with the flag -// still false and do nothing, which is right: they never initialize the collector at all. -// Boehm's own GC_is_init_called cannot stand in for the flag, because the collector initializes -// itself on first use and so answers yes in programs that never meant to collect anything. - -#define GC_THREADS -#include "gc.h" +// that pass runs for no other model. Boehm's own GC_is_init_called cannot stand in for that, +// because the collector initializes itself on first use and so answers yes in programs that never +// meant to collect anything. +// +// Nothing here touches the collector itself: GC_enable_threads, and the registration it installs +// as the hooks below, are in AsyncGCThreadsCommon.inc - an object file of its own, which only the +// call to GC_enable_threads pulls in. Every async program links the scheduler; had the scheduler +// called GC_register_my_thread itself, every `rc`, `none` and `own` program that awaits anything +// would need the collector to link, and they do not link it. namespace typescript { namespace asyncgc { -inline bool &threadsEnabledFlag() +// Left null unless GC_enable_threads ran, which it does once, from the entry point, before any +// coroutine can be handed to the pool. +struct ThreadHooks { - static bool enabled = false; - return enabled; -} + // true when this call registered the thread (and so has to unregister it) + bool (*registerThread)() = nullptr; + void (*unregisterThread)() = nullptr; +}; -// Called once, from the entry point, before any coroutine can be handed to the pool. -inline void enableThreads() +inline ThreadHooks &threadHooks() { - GC_allow_register_threads(); - threadsEnabledFlag() = true; + static ThreadHooks hooks; + return hooks; } // Registered per task rather than per thread because the pool offers no thread-entry hook. @@ -55,15 +59,10 @@ class ThreadRegistration public: ThreadRegistration() : registered(false) { - if (!threadsEnabledFlag()) - { - return; - } - - struct GC_stack_base sb; - if (GC_get_stack_base(&sb) == GC_SUCCESS) + auto registerThread = threadHooks().registerThread; + if (registerThread != nullptr) { - registered = GC_register_my_thread(&sb) == GC_SUCCESS; + registered = registerThread(); } } @@ -71,7 +70,7 @@ class ThreadRegistration { if (registered) { - GC_unregister_my_thread(); + threadHooks().unregisterThread(); } } diff --git a/tslang/lib/AsyncGCThreadsCommon.inc b/tslang/lib/AsyncGCThreadsCommon.inc new file mode 100644 index 000000000..13473a437 --- /dev/null +++ b/tslang/lib/AsyncGCThreadsCommon.inc @@ -0,0 +1,39 @@ +// The async runtime's side of the collector, in an object file of its own - see AsyncGCThreads.h. +// +// Shared by TypeScriptRuntime/AsyncGCThreads.cpp (the JIT's DLL) and +// TypeScriptAsyncRuntime/AsyncGCThreads.cpp (the static library ahead-of-time builds link). In the +// static library nothing but GC_enable_threads refers to this object, and only a `gc` program +// calls that, so only a `gc` program pulls it in - and with it the collector's thread API. + +#define GC_THREADS +#include "gc.h" + +#include "TypeScript/AsyncGCThreads.h" + +namespace +{ + +bool registerThisThread() +{ + struct GC_stack_base sb; + return GC_get_stack_base(&sb) == GC_SUCCESS && GC_register_my_thread(&sb) == GC_SUCCESS; +} + +void unregisterThisThread() +{ + GC_unregister_my_thread(); +} + +} // namespace + +// Called once from the entry point: the GC pass injects the call beside GC_init, so it happens +// only in a `gc` build. When linked into the shared TypeScriptRuntime, this symbol is also +// exported under this name in TypeScriptRuntime.def for the JIT to resolve. +extern "C" void GC_enable_threads() +{ + GC_allow_register_threads(); + + auto &hooks = typescript::asyncgc::threadHooks(); + hooks.registerThread = ®isterThisThread; + hooks.unregisterThread = &unregisterThisThread; +} diff --git a/tslang/lib/AsyncRuntimeCommon.inc b/tslang/lib/AsyncRuntimeCommon.inc index 9cca2db09..4f580cd07 100644 --- a/tslang/lib/AsyncRuntimeCommon.inc +++ b/tslang/lib/AsyncRuntimeCommon.inc @@ -29,18 +29,11 @@ #include #include +// only the hooks: GC_enable_threads and the collector calls are in AsyncGCThreadsCommon.inc #include "TypeScript/AsyncGCThreads.h" using namespace mlir::runtime; -// Called once from the entry point: the GC pass injects the call beside GC_init, so it happens -// only in a `gc` build. When linked into the shared TypeScriptRuntime, this symbol is also -// exported under this name in TypeScriptRuntime.def for the JIT to resolve. -extern "C" void GC_enable_threads() -{ - typescript::asyncgc::enableThreads(); -} - //===----------------------------------------------------------------------===// // Async runtime API. //===----------------------------------------------------------------------===// diff --git a/tslang/lib/TypeScriptAsyncRuntime/AsyncGCThreads.cpp b/tslang/lib/TypeScriptAsyncRuntime/AsyncGCThreads.cpp new file mode 100644 index 000000000..7c9e59b09 --- /dev/null +++ b/tslang/lib/TypeScriptAsyncRuntime/AsyncGCThreads.cpp @@ -0,0 +1,4 @@ +// GC_enable_threads and the pool's thread registration with the collector, kept out of +// AsyncRuntime.cpp so that only a `gc` program - the one caller of GC_enable_threads - links them. +// See AsyncGCThreads.h. +#include "../AsyncGCThreadsCommon.inc" diff --git a/tslang/lib/TypeScriptAsyncRuntime/CMakeLists.txt b/tslang/lib/TypeScriptAsyncRuntime/CMakeLists.txt index 4f7ea1168..c564a7368 100644 --- a/tslang/lib/TypeScriptAsyncRuntime/CMakeLists.txt +++ b/tslang/lib/TypeScriptAsyncRuntime/CMakeLists.txt @@ -1,12 +1,13 @@ add_mlir_library(TypeScriptAsyncRuntime STATIC AsyncRuntime.cpp + AsyncGCThreads.cpp ProcessHeap.cpp DynamicRuntime.cpp EXCLUDE_FROM_LIBMLIR - # For gc.h only - AsyncGCThreads.h registers the pool's workers with the collector when the + # For gc.h only - AsyncGCThreads.cpp registers the pool's workers with the collector when the # program is a `gc` one. The collector itself is linked into the executable, not into here. LINK_LIBS PRIVATE BDWgc::gc diff --git a/tslang/lib/TypeScriptRuntime/AsyncGCThreads.cpp b/tslang/lib/TypeScriptRuntime/AsyncGCThreads.cpp new file mode 100644 index 000000000..2868a3625 --- /dev/null +++ b/tslang/lib/TypeScriptRuntime/AsyncGCThreads.cpp @@ -0,0 +1,3 @@ +// GC_enable_threads and the pool's thread registration with the collector - the same file the +// static TypeScriptAsyncRuntime builds, where it has to be an object of its own. See AsyncGCThreads.h. +#include "../AsyncGCThreadsCommon.inc" diff --git a/tslang/lib/TypeScriptRuntime/AsyncRuntime.cpp b/tslang/lib/TypeScriptRuntime/AsyncRuntime.cpp index 63ab7802b..da8d18ec7 100644 --- a/tslang/lib/TypeScriptRuntime/AsyncRuntime.cpp +++ b/tslang/lib/TypeScriptRuntime/AsyncRuntime.cpp @@ -21,7 +21,6 @@ #ifdef MLIR_ASYNCRUNTIME_DEFINE_FUNCTIONS -// before the .inc: gc.h, which it includes, brings windows.h and its min/max macros #include "llvm/ADT/StringMap.h" #include "../AsyncRuntimeCommon.inc" diff --git a/tslang/lib/TypeScriptRuntime/CMakeLists.txt b/tslang/lib/TypeScriptRuntime/CMakeLists.txt index f85b0c26f..6f003aabc 100644 --- a/tslang/lib/TypeScriptRuntime/CMakeLists.txt +++ b/tslang/lib/TypeScriptRuntime/CMakeLists.txt @@ -14,6 +14,7 @@ add_mlir_library(TypeScriptRuntime gc.cpp MemRuntime.cpp AsyncRuntime.cpp + AsyncGCThreads.cpp DynamicRuntime.cpp mlir_init.cpp diff --git a/tslang/lib/TypeScriptRuntime/TypeScriptRuntime.def b/tslang/lib/TypeScriptRuntime/TypeScriptRuntime.def index 930b76985..71311de5e 100644 --- a/tslang/lib/TypeScriptRuntime/TypeScriptRuntime.def +++ b/tslang/lib/TypeScriptRuntime/TypeScriptRuntime.def @@ -24,7 +24,7 @@ EXPORTS GC_gcollect=_mlir__GC_gcollect ; --- tells the collector it is multi-threaded and lets the async runtime's pool workers - ; --- register themselves (AsyncRuntime.cpp / AsyncGCThreads.h); injected beside GC_init + ; --- register themselves (AsyncGCThreads.cpp / AsyncGCThreads.h); injected beside GC_init GC_enable_threads ; --- memory runtime (MemRuntime.cpp, extern "C") --- diff --git a/tslang/test/tester/CMakeLists.txt b/tslang/test/tester/CMakeLists.txt index 927980a5e..60f1bb82a 100644 --- a/tslang/test/tester/CMakeLists.txt +++ b/tslang/test/tester/CMakeLists.txt @@ -2229,6 +2229,25 @@ if (WIN32) -P ${CMAKE_CURRENT_SOURCE_DIR}/jit-throw-without-runtime.cmake) endif() +# An async program linked by tslang itself (test-runner links with gc.lib whatever the model) under +# every model but gc - see async-without-collector.cmake. +if (WIN32) + if ("${CMAKE_BUILD_TYPE}" STREQUAL "Release") + set(async_without_collector_opt "--opt;--opt_level=3") + else() + set(async_without_collector_opt "--di;--opt_level=0") + endif() + add_test(NAME test-compile-async-without-collector + COMMAND ${CMAKE_COMMAND} + "-DTSLANG=$" + "-DTESTS_DIR=${PROJECT_SOURCE_DIR}/test/tester/tests" + "-DWORK_DIR=${CMAKE_CURRENT_BINARY_DIR}/async-without-collector" + "-DLLVM_LIB=${LLVM_LIBRARY_DIR}" + "-DTSLANG_LIB=${CMAKE_BINARY_DIR}/lib" + "-DOPT=${async_without_collector_opt}" + -P "${CMAKE_CURRENT_SOURCE_DIR}/async-without-collector.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/async-without-collector.cmake b/tslang/test/tester/async-without-collector.cmake new file mode 100644 index 000000000..14dd1c23b --- /dev/null +++ b/tslang/test/tester/async-without-collector.cmake @@ -0,0 +1,68 @@ +# An async program built by `tslang --emit=exe` under every model but gc - which links no collector. +# +# The async runtime registers its pool threads with the collector under gc (AsyncGCThreads.h). That +# code used to sit in the scheduler's own object file, so every program that awaits anything pulled +# in references to GC_allow_register_threads, GC_register_my_thread, GC_unregister_my_thread and +# GC_get_stack_base - and under rc, none and own the compiler links no gc.lib, so none of them +# linked: "unresolved external symbol GC_allow_register_threads". It is an object file of its own +# now, pulled in only by the call to GC_enable_threads that the GC pass puts in a gc program. +# +# This drives `tslang --emit=exe` itself, which `test-runner` does not: it links with lld directly, +# and always with gc.lib, so the rc and none corpus runs of the same file never saw it. + +cmake_minimum_required(VERSION 3.17.3) + +foreach(var TSLANG TESTS_DIR WORK_DIR LLVM_LIB TSLANG_LIB OPT) + if(NOT DEFINED ${var}) + message(FATAL_ERROR "${var} is required") + endif() +endforeach() + +# Only what the command line says: an inherited GC_LIB_PATH would put the collector back in reach. +set(ENV{GC_LIB_PATH} "") + +file(REMOVE_RECURSE "${WORK_DIR}") +file(MAKE_DIRECTORY "${WORK_DIR}") + +set(name 00async_await) + +set(failures "") +foreach(model rc none own) + execute_process( + COMMAND "${TSLANG}" --emit=exe -mm=${model} --no-default-lib ${OPT} + "--llvm-lib-path=${LLVM_LIB}" "--tslang-lib-path=${TSLANG_LIB}" + "${TESTS_DIR}/${name}.ts" -o=${name}.${model}.exe + WORKING_DIRECTORY "${WORK_DIR}" + OUTPUT_VARIABLE out + ERROR_VARIABLE err + RESULT_VARIABLE status) + + if(NOT status EQUAL 0) + # the first error, not a warning printed ahead of it + if("${out}${err}" MATCHES "[^\n]*error[^\n]*") + set(first_line "${CMAKE_MATCH_0}") + else() + string(REGEX REPLACE "\n.*" "" first_line "${err}") + endif() + list(APPEND failures " -mm=${model}: --emit=exe: exit ${status}: ${first_line}") + continue() + endif() + + execute_process( + COMMAND "${WORK_DIR}/${name}.${model}.exe" + WORKING_DIRECTORY "${WORK_DIR}" + OUTPUT_VARIABLE out + ERROR_VARIABLE err + RESULT_VARIABLE status) + + if(NOT status EQUAL 0 OR NOT out MATCHES "done\\.") + list(APPEND failures " -mm=${model}: the program: exit ${status}") + endif() +endforeach() + +if(failures) + string(REPLACE ";" "\n" report "${failures}") + message(FATAL_ERROR "an async program without the collector:\n${report}") +endif() + +message(STATUS "an async program links and runs without the collector under rc, none and own")