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/TypeScript/MLIRGenAccessCall.cpp b/tslang/lib/TypeScript/MLIRGenAccessCall.cpp index 61126340a..7e0740c65 100644 --- a/tslang/lib/TypeScript/MLIRGenAccessCall.cpp +++ b/tslang/lib/TypeScript/MLIRGenAccessCall.cpp @@ -1576,9 +1576,21 @@ namespace mlirgen return mlir::success(); } - // special case when TypePredicateType is used in generic function and failed constraints if (auto symbolRefOp = actualFuncRefValue.getDefiningOp()) { + // The collector's own API - lib.native.d.ts declares GC_gcollect and the weak-link + // functions WeakRef is built on - exists only in a `gc` program: no other model links + // the collector, so a call to it was an unresolved symbol at link time instead + if (!compileOptions.needsGCRuntime() && symbolRefOp.getIdentifier().starts_with("GC_")) + { + emitError(location, "") << "'" << symbolRefOp.getIdentifier() + << "' is part of the garbage collector, which -mm=" + << memoryModelName(compileOptions.memoryModel) + << " does not have; it needs -mm=gc, as does WeakRef, which is built on it"; + return mlir::failure(); + } + + // special case when TypePredicateType is used in generic function and failed constraints if (symbolRefOp.getIdentifier() == "") { if (auto funcType = mlir::dyn_cast(symbolRefOp.getType())) 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..d21958d9a 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") @@ -2400,6 +2419,23 @@ foreach(call_arity_case ${call_arity_cases}) FAIL_REGULAR_EXPRESSION "Stack dump|Assertion failed") endforeach() +# A call to the collector's API (GC_gcollect, or the weak-link functions DefaultLib's WeakRef uses) +# is an error under every model but gc, which alone links the collector - it used to reach the +# linker as an unresolved symbol. Under gc the same file compiles. +foreach(gc_api_model rc none own) + add_test(NAME test-compile-gc-api-outside-gc-${gc_api_model} + COMMAND $ --emit=obj --no-default-lib -mm=${gc_api_model} + "${PROJECT_SOURCE_DIR}/test/tester/collector-api/gc_collect.ts" + -o "${CMAKE_CURRENT_BINARY_DIR}/gc-api-${gc_api_model}.obj") + set_tests_properties(test-compile-gc-api-outside-gc-${gc_api_model} + PROPERTIES PASS_REGULAR_EXPRESSION "'GC_gcollect' is part of the garbage collector, which -mm=${gc_api_model} does not have" + FAIL_REGULAR_EXPRESSION "Stack dump|Assertion failed") +endforeach() +add_test(NAME test-compile-gc-api-gc + COMMAND $ --emit=obj --no-default-lib -mm=gc + "${PROJECT_SOURCE_DIR}/test/tester/collector-api/gc_collect.ts" + -o "${CMAKE_CURRENT_BINARY_DIR}/gc-api-gc.obj") + # A type alias that contains itself is one error, not a stack overflow (issue #370). The follow-on # messages ("can't find type", "generic type ... can't be found") would only repeat it. A stack # overflow prints nothing, so it fails the PASS expression. 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") diff --git a/tslang/test/tester/collector-api/gc_collect.ts b/tslang/test/tester/collector-api/gc_collect.ts new file mode 100644 index 000000000..f43bb1976 --- /dev/null +++ b/tslang/test/tester/collector-api/gc_collect.ts @@ -0,0 +1,8 @@ +// A call to the collector's own API, as lib.native.d.ts declares it. Only a `gc` program has the +// collector: under any other model this is a compile error, not an unresolved symbol at link time. +declare function GC_gcollect(): void; + +function main() { + GC_gcollect(); + print("done."); +}