From 5b621135e450a56eee7ebf735fd9e6385b154ec8 Mon Sep 17 00:00:00 2001 From: ASDAlexander77 Date: Sat, 12 Sep 2026 23:25:44 +0100 Subject: [PATCH 1/2] Propose one garbage collector per process The gc default-library DLL links the static gc.lib, so it runs a collector of its own beside TypeScriptRuntime.dll (JIT) and beside gc.dll (exe + user DLL). Measured: 2000/2000 and 1984/2000 held strings read freed memory, 0 with collection suppressed. The proposal takes Boehm from gc.dll in every binary that can share a process, and keeps the static gc.lib for a lone exe. Co-Authored-By: Claude Opus 5 --- tslang/docs/single-gc-collector-design.md | 191 ++++++++++++++++++++++ 1 file changed, 191 insertions(+) create mode 100644 tslang/docs/single-gc-collector-design.md diff --git a/tslang/docs/single-gc-collector-design.md b/tslang/docs/single-gc-collector-design.md new file mode 100644 index 000000000..dca0632f6 --- /dev/null +++ b/tslang/docs/single-gc-collector-design.md @@ -0,0 +1,191 @@ +# One collector per process: design proposal + +Status: **proposal**, nothing implemented. Branch `fix-single-gc-collector`. + +## Problem + +Under `-mm=gc`, every tslang binary that links Boehm statically carries its **own collector**: +its own heap, its own roots, its own mark bits. When two such binaries share a process and one +of them holds a pointer to an object the other allocated, the owning collector never sees that +reference. So it frees the object, the memory is reused, and the holder reads a plausible wrong +value. Nothing crashes; the data is silently wrong. + +Item 5ao (reference-counting-evaluation.md §9.76) fixed this for **exe + user DLL** by linking +both against a Boehm DLL. It missed the binary that is in almost every process: the **default +library DLL**. + +### Evidence (2026-09-12) + +`TypeScriptDefaultLib.dll` (`defaultlib/dll/release/gc`) imports only `ntdll`, `WINHTTP` and +`KERNEL32`, yet contains Boehm's internals (`GC_mark_some`, `GC_stop_world`, `GC_thr_init`). +It was linked with `-lgc` against the static `gc.lib`. + +Repro: hold 2000 strings built by the default library (`padStart`) in an array the program +allocated, then churn strings of **different** content (`repeat`) so a collection runs. Each +case was also run with `GC_INITIAL_HEAP_SIZE=1GB` as a no-collection control. + +| Process | Collectors | Held strings wrong | Control | +| --- | --- | --- | --- | +| AOT exe, static default lib | 1 | 0 / 2000 | - | +| JIT, default lib (the normal `--emit=jit` path) | 2: `TypeScriptRuntime.dll` + default-lib DLL | **2000 / 2000** | 0 | +| AOT exe + user `-shared` DLL, both already on `gc.dll` | 2: `gc.dll` + default-lib DLL | **1984 / 2000** | 0 | + +The suite never saw this because `test-runner` passes `--no-default-lib` to every test. + +## Where Boehm is linked today + +| Binary | How it gets Boehm | Loaded together with | +| --- | --- | --- | +| user exe (`--emit=exe`) | `-lgc` from `GC_LIB_PATH`: static, or `gcdll` if pointed there | user DLLs it imports | +| user DLL (`--emit=dll`) | same `-lgc` | the exe; the default-lib DLL (links `dll/` import lib) | +| `TypeScriptDefaultLib.dll` | `-lgc`, built with the **static** `gc.lib` | JIT runs; every user DLL | +| `TypeScriptDefaultLib.lib` (`lib/`) | none: an archive, the exe links Boehm | nothing (merged into the exe) | +| `TypeScriptRuntime.dll` | `BDWgc::gc` static (`GC_NOT_DLL` in `gc.cpp`); re-exports `GC_*` via `.def` | JIT code; the default-lib DLL | + +The JIT resolves `GC_malloc`/`GC_add_roots`/... by name through `SearchForAddressOfSymbol`, +which finds `TypeScriptRuntime.dll`'s exports. The default-lib DLL's calls were bound at link +time to its own copy. `jit.cpp` already guards against a *second `TypeScriptRuntime` copy* for +exactly this reason, but not against the default library. + +## The rule + +> **One process, one Boehm.** A `gc` binary that can share a process with another `gc` binary +> must take its collector from `gc.dll`. A binary that is guaranteed to be alone may keep the +> static `gc.lib`. + +"Alone" means only one case: an AOT exe that links the static default library and imports no +tslang shared library. That is the common deployment, and it keeps shipping a single file. + +## Proposed design + +### 1. `TypeScriptRuntime.dll` takes Boehm from `gc.dll` + +Link `TypeScriptRuntime` against the shared BDWgc package (`3rdParty/gcdll/...`) and drop +`GC_NOT_DLL` on Windows in `gc.cpp`. The `.def` re-exports stay as they are; they now forward to +`gc.dll`. Install `gc.dll` into `bin/` next to `TypeScriptRuntime.dll`, so the loader finds it +for JIT runs. + +Result: JIT code, the runtime's `GC_add_roots` for JIT sections, the async runtime's thread +registration, and anything else in the JIT process share one collector, as long as step 2 also +lands. + +### 2. The default-library DLL takes Boehm from `gc.dll` + +In the default library's build scripts, build the **`gc` DLL** with `GC_LIB_PATH` pointing at +the `gcdll` import library. Keep the static `lib/` archive unchanged; it does not link Boehm. +Only the `gc` model is affected, because `rc` and `none` have no collector. + +Result: JIT (`TypeScriptRuntime.dll` + default-lib DLL) and exe + user DLL + default-lib DLL all +share `gc.dll`. + +### 3. `tslang` chooses the GC flavour itself + +Today `--gc-lib-path` is one directory and the user must know to swap it. Instead: + +- Add `--gc-shared-lib-path` / `GC_SHARED_LIB_PATH` for the import library and `gc.dll`. The + release package already ships it as `gcdll/`, so the default is `/gcdll`. +- `exe.cpp` links the **shared** flavour when: + - `--emit=dll`, or + - `--emit=exe` and the module imports a tslang shared library. MLIRGen already knows this: it + emits `LoadLibraryPermanentlyOp` for `import './x'`, and it reads the imported library's + symbols at compile time. Record the fact in `CompileOptions` or a module attribute. +- Otherwise it links the static `gc.lib`, as today. +- When the shared flavour is used, copy `gc.dll` next to the output, or print exactly which file + to ship. A missing `gc.dll` is a loader error at startup, which is loud; the bug being fixed is + silent. + +### 4. Refuse or warn on a mixed process at compile time + +MLIRGen already reads a marker symbol from an imported DLL (`__tsmm___`) and +warns when the memory models differ. Add a second marker, **`__tsgc__...`**, +emitted by every `gc` binary. When importing a `gc` library that linked Boehm statically, emit +an error, or at least a warning naming 5ao: + +```text +error: shared library 'foo.dll' links its own garbage collector (static gc.lib). +Objects crossing between it and this module can be freed while still in use. +Rebuild it with tslang --emit=dll (which uses gc.dll). +``` + +A library with no marker predates this change. Warn, don't error, so existing binaries still +load. + +### 5. Tests that include the default library + +Add both repros to the suite **without** `--no-default-lib`: + +- `test-jit-gc-defaultlib-collector`: JIT, held default-lib strings + different-content churn. +- `test-compile-gc-shared-defaultlib-collector`: exe + user DLL + default-lib DLL. + +Teeth rules from the RC work apply: + +- The churn must allocate content **different** from what is held; same-content churn cannot + fail (§9.76). +- Build the expected value with the same method as the held one. A hand-built + `"#".repeat(n) + ...` compared unequal to a correct value during this investigation. +- Confirm each test fails on today's binaries before landing the fix. + +This needs a `test-runner` switch to keep the default library for named tests, plus the +per-test working directory the shared path already has (`gc.dll` and `TypeScriptDefaultLib.dll` +must sit beside the binaries **at compile time too**, because importing a DLL loads it). + +### 6. Packaging + +- Windows zip: `gc.dll` in the root beside `tslang.exe` and `TypeScriptRuntime.dll`, for JIT. + Keep `gcdll/gc.lib` for linking user programs. +- The default-lib `dll/` tree is built against `gcdll` (step 2). +- `docs/memory-models.md`: a program that loads a tslang DLL ships `gc.dll` **and** + `TypeScriptDefaultLib.dll` beside the exe. + +## Alternatives considered + +**A. Always `gc.dll`, drop the static `gc.lib`.** Simplest to reason about: one collector by +construction. Rejected as the default because every standalone exe, the common case, would have +to ship a second file for no benefit. It stays a one-line fallback if step 3's detection turns +out to be unreliable. + +**B. DLLs import `GC_*` from their host instead of from `gc.dll`.** Windows binds an import to a +DLL *name*, and the host differs: the exe under AOT, `TypeScriptRuntime.dll` under JIT. Doing this +needs a runtime-resolved function table (`GetModuleHandle(NULL)` / `SearchForAddressOfSymbol`) +consulted on every allocation, which means new codegen, an extra indirection, and a new failure +mode if the table is missing. It buys nothing over a shared `gc.dll`. + +**C. Make the collectors see each other's roots** (`GC_add_roots` across heaps). Each collector +would still own its heap and free lists. It would have to register every other heap's live +ranges dynamically as they grow, and marking through a foreign heap does not stop the owner from +sweeping. Fragile at best; rejected. + +## Risks and open questions + +- **CRT.** `gc.dll` is built `/MT` (static CRT), so it has its own CRT heap. Boehm allocates its + heap with `VirtualAlloc` and does not hand CRT memory to callers, so this should be safe, but + verify `GC_win32_free_heap` (called from `destroy_gcruntime`) and teardown under the JIT's + unconditional `TerminateProcess` exit in `jit.cpp`. +- **Threads.** `GC_enable_threads` / `GC_allow_register_threads` must be called on the collector + in `gc.dll`. After step 1 the runtime's export forwards there, but check the JIT stand-in in + `jit.cpp` (`jitEnableGCThreads`) resolves to the same collector. +- **Linux.** Not measured. ELF symbol interposition may already route every module's public + `GC_*` calls to the first-loaded copy, which would hide the bug. Boehm's hidden internals make + that unreliable either way. Run both repros on Linux before changing anything there. +- **Debug trees.** Need the debug shared Boehm (`scripts/build_gc_debug_shared_vs.bat`) and a + debug `dll/debug/gc` default lib built against it. +- **Performance.** An allocation becomes one indirect call through the import table. Expected to + be negligible next to the allocation itself; confirm with `raytrace` under the AOT harness. +- **Old binaries.** Default-lib DLLs and user DLLs built before this change keep their private + collector until rebuilt. Step 4's marker is how they get found. + +## Validation plan + +1. Build `TypeScriptRuntime` and the `gc` default-lib DLL against `gcdll`. +2. Re-run both repros: expect **0 / 2000** with collection on. Also re-run each with the old + binaries to confirm they still fail, so the repro keeps its teeth. +3. Full `ctest` on Windows, release and debug; the Linux suite unchanged. +4. The new default-library tests from step 5 in CI. +5. `raytrace` memory and time, before and after, under `gc`. + +## Suggested PR split + +1. Runtime + default-lib DLL on `gc.dll`, plus the two regression tests (steps 1, 2, 5). This + closes the bug for JIT, the most exposed path. +2. `tslang` picks the flavour and copies `gc.dll` (step 3), plus packaging and docs (step 6). +3. The `__tsgc_` marker and the import-time diagnostic (step 4). From 5ba6a2c87dd72ccc837c147a19a2678750efb628 Mon Sep 17 00:00:00 2001 From: ASDAlexander77 Date: Sat, 12 Sep 2026 23:40:42 +0100 Subject: [PATCH 2/2] Give a JIT process one garbage collector: TypeScriptRuntime.dll on gc.dll TypeScriptRuntime.dll linked Boehm statically, so a JIT run that loads a gc shared library - a user's, or the default library's DLL - had two collectors, and one freed strings only the other's memory referenced (2000/2000 under the JIT). On Windows the runtime now takes Boehm from gc.dll: the top-level CMake defines an imported tslang_gc_shared target from 3rdParty/gcdll (TSLANG_GC_SHARED_PREFIX, GC_DLL), fails to configure without it, and copies gc.dll into bin/. prepare_3rdParty.bat builds it; the release zip ships it and passes GC_SHARED_LIB_PATH to the default-library build, whose DLL now links gc.dll too (TypeScriptCompilerDefaultLib, same branch). New test pair gc_single_collector: the library builds the held strings AND churns, so its collector is the one that must run. Fails with the old runtime, passes with the new one; JIT + default-lib DLL and exe + user DLL + default-lib DLL repros go from 2000/2000 and 1984/2000 bad to 0. Linux unchanged. Suite: 2,704 of 2,704. Co-Authored-By: Claude Opus 5 --- .github/workflows/create-release.yml | 4 +- docs/memory-models.md | 4 ++ prepare_3rdParty.bat | 10 +++++ tslang/CMakeLists.txt | 34 +++++++++++++++++ tslang/docs/single-gc-collector-design.md | 37 ++++++++++++++++++- tslang/lib/TypeScriptRuntime/CMakeLists.txt | 23 ++++++++++-- tslang/lib/TypeScriptRuntime/gc.cpp | 4 +- tslang/test/tester/CMakeLists.txt | 8 +++- .../tests/export_gc_single_collector.ts | 24 ++++++++++++ .../tests/import_gc_single_collector.ts | 30 +++++++++++++++ 10 files changed, 171 insertions(+), 7 deletions(-) create mode 100644 tslang/test/tester/tests/export_gc_single_collector.ts create mode 100644 tslang/test/tester/tests/import_gc_single_collector.ts diff --git a/.github/workflows/create-release.yml b/.github/workflows/create-release.yml index 7d8f0db30..963124d95 100644 --- a/.github/workflows/create-release.yml +++ b/.github/workflows/create-release.yml @@ -177,6 +177,8 @@ jobs: env: TOOL_PATH: ${{github.workspace}}/__build/tslang/msbuild/x64/release/bin GC_LIB_PATH: ${{github.workspace}}/__build/gc/msbuild/x64/release/${{ env.BUILD_TYPE }} + # the DLL build links Boehm from gc.dll (one collector per process) + GC_SHARED_LIB_PATH: ${{github.workspace}}/3rdParty/gcdll/x64/release/lib LLVM_LIB_PATH: ${{github.workspace}}/3rdParty/llvm/x64/release/lib TSLANG_LIB_PATH: ${{github.workspace}}/__build/tslang/msbuild/x64/release/lib run: | @@ -209,7 +211,7 @@ jobs: run: | New-Item -ItemType Directory -Force -Path .\gcdll_stage\gcdll | Out-Null Copy-Item -Path ..\3rdParty\gcdll\x64\release\lib\gc.lib, ..\3rdParty\gcdll\x64\release\bin\gc.dll -Destination .\gcdll_stage\gcdll -ErrorAction Stop - Get-ChildItem -Path .\tslang\msbuild\x64\release\bin\tslang.exe, .\tslang\msbuild\x64\release\bin\TypeScriptRuntime.dll, .\gc\msbuild\x64\release\${{ env.BUILD_TYPE }}\gc.lib, .\tslang\msbuild\x64\release\lib\TypeScriptAsyncRuntime.lib, ..\3rdParty\llvm\x64\release\lib\LLVMSupport.lib, ..\3rdParty\llvm\x64\release\bin\wasm-ld.exe, ..\TypeScriptCompilerDefaultLib\__build, .\gcdll_stage | Compress-Archive -DestinationPath ..\tslang.zip + Get-ChildItem -Path .\tslang\msbuild\x64\release\bin\tslang.exe, .\tslang\msbuild\x64\release\bin\TypeScriptRuntime.dll, .\tslang\msbuild\x64\release\bin\gc.dll, .\gc\msbuild\x64\release\${{ env.BUILD_TYPE }}\gc.lib, .\tslang\msbuild\x64\release\lib\TypeScriptAsyncRuntime.lib, ..\3rdParty\llvm\x64\release\lib\LLVMSupport.lib, ..\3rdParty\llvm\x64\release\bin\wasm-ld.exe, ..\TypeScriptCompilerDefaultLib\__build, .\gcdll_stage | Compress-Archive -DestinationPath ..\tslang.zip shell: pwsh - name: Archive Zip of Windows Asset diff --git a/docs/memory-models.md b/docs/memory-models.md index efcb56afc..ea7cb9646 100644 --- a/docs/memory-models.md +++ b/docs/memory-models.md @@ -117,6 +117,10 @@ three. ## Shared libraries and `-mm=gc` **A program that loads a tslang shared library must link Boehm as a DLL, not statically.** +The same rule is why, on Windows, `TypeScriptRuntime.dll` (the JIT's runtime) and the default +library's `TypeScriptDefaultLib.dll` take the collector from `gc.dll`: under the JIT, or beside a +user's shared library, they share a process with other `gc` code, and one static collector +among them frees what the others hold. `gc.dll` ships beside `tslang.exe`. If the executable and the library each link `gc.lib` statically, each gets its own collector, with its own heap and its own idea of what the roots are. The library's collector does not scan diff --git a/prepare_3rdParty.bat b/prepare_3rdParty.bat index f6f31a2b6..52fccb710 100644 --- a/prepare_3rdParty.bat +++ b/prepare_3rdParty.bat @@ -40,4 +40,14 @@ IF EXIST ".\3rdParty\gc\x64\%BUILD%\lib\gc.lib" ( xcopy /E /H /C /I /Y .\3rdParty\libatomic_ops-%LIBATOMIC_OPS_VER%\ .\3rdParty\gc-%GC_VER%\libatomic_ops\ cd %p% @call scripts\build_gc_%BUILD%_%TOOL%.bat +) + +rem Boehm as a DLL too: TypeScriptRuntime.dll, the default library's DLL and user shared +rem libraries take the collector from gc.dll so a process has only one. +rem See tslang/docs/single-gc-collector-design.md. +IF EXIST ".\3rdParty\gcdll\x64\%BUILD%\lib\gc.lib" ( + echo "No need to build shared GC (%BUILD%)" +) ELSE ( + cd %p% + @call scripts\build_gc_%BUILD%_shared_%TOOL%.bat ) \ No newline at end of file diff --git a/tslang/CMakeLists.txt b/tslang/CMakeLists.txt index 2b81acc6e..874b4e85a 100644 --- a/tslang/CMakeLists.txt +++ b/tslang/CMakeLists.txt @@ -102,6 +102,40 @@ if (NOT BDWgc_FOUND) message(FATAL_ERROR "Failed to find BDWgc (garbage collector)") endif() +# Boehm built as a DLL (gc.dll + its import library gc.lib). One process must have one +# collector: every Boehm linked statically brings its own heap and roots, and a collector frees +# objects that only another binary's memory still references. So anything that shares a process +# with other gc binaries - TypeScriptRuntime.dll under the JIT, the default library's DLL, user +# shared libraries - takes Boehm from gc.dll. See docs/single-gc-collector-design.md. +# Built by scripts/build_gc_{release,debug}_shared_vs.bat (prepare_3rdParty.bat calls it). +string(TOLOWER "${CMAKE_BUILD_TYPE}" CMAKE_BUILD_TYPE_LOWERCASE) +if ("${TSLANG_GC_SHARED_PREFIX}" STREQUAL "") + if (MSVC OR WIN32) + get_filename_component(TSLANG_GC_SHARED_PREFIX "${CMAKE_SOURCE_DIR}/../3rdParty/gcdll/x64/${CMAKE_BUILD_TYPE_LOWERCASE}" REALPATH BASE_DIR "${CMAKE_SOURCE_DIR}") + else() + get_filename_component(TSLANG_GC_SHARED_PREFIX "${CMAKE_SOURCE_DIR}/../3rdParty/gcdll/${CMAKE_BUILD_TYPE_LOWERCASE}" REALPATH BASE_DIR "${CMAKE_SOURCE_DIR}") + endif() +endif() +message(STATUS "TSLANG_GC_SHARED_PREFIX is ${TSLANG_GC_SHARED_PREFIX}") + +if (WIN32) + if (NOT EXISTS "${TSLANG_GC_SHARED_PREFIX}/lib/gc.lib" OR NOT EXISTS "${TSLANG_GC_SHARED_PREFIX}/bin/gc.dll") + message(FATAL_ERROR "shared Boehm (gc.dll) not found at ${TSLANG_GC_SHARED_PREFIX}. " + "TypeScriptRuntime.dll needs it so a JIT process has one collector. " + "Build it with scripts/build_gc_${CMAKE_BUILD_TYPE_LOWERCASE}_shared_vs.bat, " + "or point TSLANG_GC_SHARED_PREFIX at an existing install.") + endif() + + # Same headers as the static package; GC_DLL makes them declare the API dllimport. + get_target_property(TSLANG_GC_INCLUDE_DIRS BDWgc::gc INTERFACE_INCLUDE_DIRECTORIES) + add_library(tslang_gc_shared SHARED IMPORTED GLOBAL) + set_target_properties(tslang_gc_shared PROPERTIES + IMPORTED_IMPLIB "${TSLANG_GC_SHARED_PREFIX}/lib/gc.lib" + IMPORTED_LOCATION "${TSLANG_GC_SHARED_PREFIX}/bin/gc.dll" + INTERFACE_INCLUDE_DIRECTORIES "${TSLANG_GC_INCLUDE_DIRS}" + INTERFACE_COMPILE_DEFINITIONS "GC_DLL") +endif() + message(STATUS "Using MLIRConfig.cmake in: ${MLIR_CMAKE_DIR}") message(STATUS "Using LLVMConfig.cmake in: ${LLVM_CMAKE_DIR}") message(STATUS "Using ClangConfig.cmake in: ${CLANG_CMAKE_DIR}") diff --git a/tslang/docs/single-gc-collector-design.md b/tslang/docs/single-gc-collector-design.md index dca0632f6..91925528a 100644 --- a/tslang/docs/single-gc-collector-design.md +++ b/tslang/docs/single-gc-collector-design.md @@ -1,6 +1,7 @@ # One collector per process: design proposal -Status: **proposal**, nothing implemented. Branch `fix-single-gc-collector`. +Status: **PR 1 implemented** (steps 1, 2, 5 - Windows) on branch `fix-single-gc-collector`; +steps 3, 4, 6 and Linux still open. See [Progress](#progress) at the end. ## Problem @@ -189,3 +190,37 @@ sweeping. Fragile at best; rejected. closes the bug for JIT, the most exposed path. 2. `tslang` picks the flavour and copies `gc.dll` (step 3), plus packaging and docs (step 6). 3. The `__tsgc_` marker and the import-time diagnostic (step 4). + +## Progress + +### PR 1 - runtime and default-lib DLL on `gc.dll` (Windows) + +- **Step 1.** The top-level CMakeLists defines an imported `tslang_gc_shared` target from + `3rdParty/gcdll/x64/` (`TSLANG_GC_SHARED_PREFIX`, with `GC_DLL` so the headers declare the + API `dllimport`). Configuring fails on Windows if it is missing. `TypeScriptRuntime` links it and + copies `gc.dll` into `bin/`; `gc.cpp` no longer forces `GC_NOT_DLL` when `GC_DLL` is set. + `prepare_3rdParty.bat` builds the shared Boehm, and the release zip ships `bin/gc.dll` in its + root. `TypeScriptRuntime.dll` now imports `gc.dll`. +- **Step 2.** In the default library's `scripts/build_core.bat`, the DLL step links + `--gc-lib-path=%GC_SHARED_LIB_PATH%` (default `..\TypeScriptCompiler\3rdParty\gcdll\x64\\lib`; + the release workflow sets it). `TypeScriptDefaultLib.dll` now imports `gc.dll`. Separate repo, + same branch name. +- **Step 5.** `import_gc_single_collector.ts` / `export_gc_single_collector.ts`, registered as + `test-jit-shared-export-import-gc-single-collector` and + `test-compile-shared-export-import-gc-single-collector`. It needs no default library: the + library side builds the held strings **and** churns, so the library's collector is the one that + must run. The existing owned-returns test churned in the importer, which is why its JIT variant + passed with two collectors. + +Measured (release, before → after, each "before" failing and passing again with collection +suppressed): + +| Case | Before | After | +| --- | --- | --- | +| new test, `-jit -shared` | assertion failed | 0 bad | +| new test, AOT `-shared` | 0 bad | 0 bad | +| JIT + default-lib DLL repro | 2000 / 2000 bad | 0 bad | +| exe + user DLL + default-lib DLL repro | 1984 / 2000 bad | 0 bad | + +Still open: the tests that would include the default library itself (the suite still passes +`--no-default-lib`), steps 3, 4 and 6, the debug default-lib build, and all of Linux. diff --git a/tslang/lib/TypeScriptRuntime/CMakeLists.txt b/tslang/lib/TypeScriptRuntime/CMakeLists.txt index 858ccbeee..23fa004fa 100644 --- a/tslang/lib/TypeScriptRuntime/CMakeLists.txt +++ b/tslang/lib/TypeScriptRuntime/CMakeLists.txt @@ -6,21 +6,38 @@ else() set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -frtti") endif() +# On Windows the runtime takes Boehm from gc.dll rather than linking it in: under the JIT this +# DLL shares the process with the default library's DLL and with user shared libraries, and a +# second static collector frees what the others still hold. See the top-level CMakeLists and +# docs/single-gc-collector-design.md. Linux is unchanged until it has been measured. +if (WIN32) + set(TSLANG_RUNTIME_GC tslang_gc_shared) +else() + set(TSLANG_RUNTIME_GC BDWgc::gc) +endif() + add_mlir_library(TypeScriptRuntime SHARED TypeScriptGC.cpp gc.cpp MemRuntime.cpp - AsyncRuntime.cpp - DynamicRuntime.cpp + AsyncRuntime.cpp + DynamicRuntime.cpp mlir_init.cpp EXCLUDE_FROM_LIBMLIR LINK_LIBS PRIVATE - BDWgc::gc + ${TSLANG_RUNTIME_GC} ) +if (WIN32) + # gc.dll beside TypeScriptRuntime.dll (and tslang.exe), where the loader looks for it. + add_custom_command(TARGET TypeScriptRuntime POST_BUILD + COMMAND ${CMAKE_COMMAND} -E copy_if_different + "${TSLANG_GC_SHARED_PREFIX}/bin/gc.dll" "$") +endif() + if(MSVC) # Export the runtime symbols under the names JIT-compiled code expects so the # MLIR ExecutionEngine resolves them via the JITDylib path (no init callback, diff --git a/tslang/lib/TypeScriptRuntime/gc.cpp b/tslang/lib/TypeScriptRuntime/gc.cpp index 2c0681b23..1f28e3e9b 100644 --- a/tslang/lib/TypeScriptRuntime/gc.cpp +++ b/tslang/lib/TypeScriptRuntime/gc.cpp @@ -12,7 +12,9 @@ #define GC_INSIDE_DLL #define GC_NAMESPACE -#if defined _WIN32 || defined _WIN64 || defined PLATFORM_ANDROID || defined __ANDROID__ +// GC_DLL comes from the build when the runtime takes Boehm from gc.dll (Windows - see +// lib/TypeScriptRuntime/CMakeLists.txt); only a statically linked Boehm needs GC_NOT_DLL. +#if !defined GC_DLL && (defined _WIN32 || defined _WIN64 || defined PLATFORM_ANDROID || defined __ANDROID__) #define GC_NOT_DLL #endif diff --git a/tslang/test/tester/CMakeLists.txt b/tslang/test/tester/CMakeLists.txt index a8e4abaeb..6cae407c4 100644 --- a/tslang/test/tester/CMakeLists.txt +++ b/tslang/test/tester/CMakeLists.txt @@ -82,7 +82,7 @@ get_filename_component(TEST_GC_LIBDIR "${BDWgc_DIR}/../.." ABSOLUTE) # statically get two collectors, and the one inside the library then frees objects the # executable is still holding - item 5ao. Built by scripts/build_gc_release_shared_vs.bat; # when it is absent the shared-library tests are skipped rather than run wrong. -set(TEST_GC_SHARED_PREFIX "${PROJECT_SOURCE_DIR}/../3rdParty/gcdll/x64/${CMAKE_BUILD_TYPE_LOWERCASE}") +set(TEST_GC_SHARED_PREFIX "${TSLANG_GC_SHARED_PREFIX}") if (EXISTS "${TEST_GC_SHARED_PREFIX}/lib/gc.lib" OR EXISTS "${TEST_GC_SHARED_PREFIX}/lib/libgc.so") set(TSLANG_HAVE_SHARED_GC TRUE) else() @@ -1053,6 +1053,11 @@ add_test(NAME test-compile-shared-export-import-object-literal-with-class-types # the base's) nor the synthetic base-class storage field (which shifted every # subsequent field's offset in the importer). add_test(NAME test-compile-shared-export-import-owned-returns COMMAND test-runner -shared "${PROJECT_SOURCE_DIR}/test/tester/tests/import_owned_returns.ts" "${PROJECT_SOURCE_DIR}/test/tester/tests/export_owned_returns.ts") +# One collector per process (docs/single-gc-collector-design.md): the library builds strings only +# the importer holds, then churns different ones, so the LIBRARY's collector has to run and has to +# see the importer's array. Under -jit this failed (2000 of 2000 freed) while TypeScriptRuntime.dll +# linked Boehm statically beside a library on gc.dll. +add_test(NAME test-compile-shared-export-import-gc-single-collector COMMAND test-runner -shared "${PROJECT_SOURCE_DIR}/test/tester/tests/import_gc_single_collector.ts" "${PROJECT_SOURCE_DIR}/test/tester/tests/export_gc_single_collector.ts") add_test(NAME test-compile-shared-export-import-class-extends COMMAND test-runner -shared "${PROJECT_SOURCE_DIR}/test/tester/tests/import_class_extends.ts" "${PROJECT_SOURCE_DIR}/test/tester/tests/export_class_extends.ts") add_test(NAME test-compile-shared-export-import-class-extends-implements-diamond COMMAND test-runner -shared "${PROJECT_SOURCE_DIR}/test/tester/tests/import_class_extends_implements_diamond.ts" "${PROJECT_SOURCE_DIR}/test/tester/tests/export_class_extends_implements_diamond.ts") add_test(NAME test-compile-shared-export-import-class-extends-multilevel COMMAND test-runner -shared "${PROJECT_SOURCE_DIR}/test/tester/tests/import_class_extends_multilevel.ts" "${PROJECT_SOURCE_DIR}/test/tester/tests/export_class_extends_multilevel.ts") @@ -1134,6 +1139,7 @@ add_test(NAME test-jit-shared-decl-emit-class COMMAND test-runner -jit -shared " add_test(NAME test-jit-shared-export-import-class-interface COMMAND test-runner -jit -shared "${PROJECT_SOURCE_DIR}/test/tester/tests/import_class_interface.ts" "${PROJECT_SOURCE_DIR}/test/tester/tests/export_class_interface.ts") add_test(NAME test-jit-shared-export-import-object-literal-with-class-types COMMAND test-runner -jit -shared "${PROJECT_SOURCE_DIR}/test/tester/tests/import_object_literal_with_class_types.ts" "${PROJECT_SOURCE_DIR}/test/tester/tests/export_object_literal_with_class_types.ts") add_test(NAME test-jit-shared-export-import-owned-returns COMMAND test-runner -jit -shared "${PROJECT_SOURCE_DIR}/test/tester/tests/import_owned_returns.ts" "${PROJECT_SOURCE_DIR}/test/tester/tests/export_owned_returns.ts") +add_test(NAME test-jit-shared-export-import-gc-single-collector COMMAND test-runner -jit -shared "${PROJECT_SOURCE_DIR}/test/tester/tests/import_gc_single_collector.ts" "${PROJECT_SOURCE_DIR}/test/tester/tests/export_gc_single_collector.ts") add_test(NAME test-jit-shared-export-import-class-extends COMMAND test-runner -jit -shared "${PROJECT_SOURCE_DIR}/test/tester/tests/import_class_extends.ts" "${PROJECT_SOURCE_DIR}/test/tester/tests/export_class_extends.ts") add_test(NAME test-jit-shared-export-import-class-extends-multilevel COMMAND test-runner -jit -shared "${PROJECT_SOURCE_DIR}/test/tester/tests/import_class_extends_multilevel.ts" "${PROJECT_SOURCE_DIR}/test/tester/tests/export_class_extends_multilevel.ts") add_test(NAME test-jit-shared-export-import-class-extends-implements-diamond COMMAND test-runner -jit -shared "${PROJECT_SOURCE_DIR}/test/tester/tests/import_class_extends_implements_diamond.ts" "${PROJECT_SOURCE_DIR}/test/tester/tests/export_class_extends_implements_diamond.ts") diff --git a/tslang/test/tester/tests/export_gc_single_collector.ts b/tslang/test/tester/tests/export_gc_single_collector.ts new file mode 100644 index 000000000..6d22c5e05 --- /dev/null +++ b/tslang/test/tester/tests/export_gc_single_collector.ts @@ -0,0 +1,24 @@ +namespace G { + + // The library side of import_gc_single_collector.ts. Both the strings the importer holds + // and the churn are allocated HERE, so this module's collector is the one that has to run + // and the one that must be able to see the importer's references. A process with two + // collectors - one in this library, one in the importer or in TypeScriptRuntime.dll - frees + // the held strings. See docs/single-gc-collector-design.md. + + export function makeKey(i: number): string { + return `key-${i}-end`; + } + + export function churn(n: number): number { + let total = 0; + for (let j = 0; j < n; j++) { + // `let`, not `const`: compiled as an imported module (not as the entry file), a `const` + // declared in this loop body fails with "can't resolve name: s" - a separate bug. + let s = `zzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzz-${j}`; + total = total + s.length; + } + + return total; + } +} diff --git a/tslang/test/tester/tests/import_gc_single_collector.ts b/tslang/test/tester/tests/import_gc_single_collector.ts new file mode 100644 index 000000000..b4d514fbc --- /dev/null +++ b/tslang/test/tester/tests/import_gc_single_collector.ts @@ -0,0 +1,30 @@ +import './export_gc_single_collector' + +// One collector per process. The shared library builds strings that only THIS module's array +// holds, then allocates hard with different content. If the library's collector is not the +// importer's - two static Boehms, or a JIT whose TypeScriptRuntime.dll carries its own - it +// cannot see the array, frees the strings and reuses their memory. The churn must differ from +// what is held, or a freed string is rebuilt with identical bytes and reads back correct +// (reference-counting-evaluation.md 9.76). See docs/single-gc-collector-design.md. + +function main() { + const N = 2000; + + let held: string[] = []; + for (let i = 0; i < N; i++) { + held.push(G.makeKey(i)); + } + + const total = G.churn(300000); + + let bad = 0; + for (let i = 0; i < N; i++) { + if (held[i] != `key-${i}-end`) bad = bad + 1; + } + + print("bad:", bad); + assert(bad == 0, "strings built by the shared library were freed while still held"); + assert(total > 0, "churn"); + + print("done."); +}