Skip to content

One garbage collector per process: linkage choice, import checks, Linux, default-library tests - #310

Merged
ASDAlexander77 merged 4 commits into
mainfrom
gc-shared-lib-auto
Sep 13, 2026
Merged

ASDAlexander77 merged 4 commits into
mainfrom
gc-shared-lib-auto

Conversation

@ASDAlexander77

@ASDAlexander77 ASDAlexander77 commented Sep 12, 2026

Copy link
Copy Markdown
Owner

Summary

Steps 3, 4, 5 and 6 of tslang/docs/single-gc-collector-design.md, plus Linux and the CMake template. The rule: one garbage collector per process. With two, one collector frees strings the other still holds, and the program reads wrong values without crashing.

Step 3: tslang picks how to link the collector (Windows)

After #309, the runtime and the default-library DLL take the collector from gc.dll. A user's own shared library and the program that imports it still used the static gc.lib unless the user pointed --gc-lib-path somewhere else by hand.

Under -mm=gc on Windows:

  • Links the shared collector:
    • --emit=dll
    • --emit=exe for a program that imports a tslang shared library. tslang tracks this in CompileOptions::importsSharedLibrary, set by finding LoadLibraryPermanentlyOp in the generated module before the passes lower it away.
  • Keeps the static gc.lib: --emit=exe for a program that imports none. It still ships as a single file, and no gc.dll is copied.
  • Copies gc.dll beside the output after a successful shared link. If it can't be copied, a warning names the file to ship.
  • Finds the shared collector by checking, in order:
    1. --gc-shared-lib-path (new)
    2. GC_SHARED_LIB_PATH
    3. <gc-lib-path>/gcdll, the release package layout
    4. --gc-lib-path itself, when gc.dll sits beside it or in ../bin. This keeps the default library's build_core.bat working unchanged.
  • Errors out when none of those has a shared collector, instead of silently falling back to the static gc.lib.

Step 4: importing a library with its own collector is an error

Under -mm=gc on Windows, importing a shared library that contains a collector fails at compile time, for --emit=exe and --emit=jit alike:

error: shared library 'foo.dll' links its own garbage collector (the static gc.lib). Objects crossing between it and this module can be freed while still in use. Rebuild it with tslang --emit=dll, which links gc.dll.

The JIT runs the same check on TypeScriptDefaultLib.dll before loading it. That catches a stale default library.

This does not use the __tsgc_ marker the design proposed. A marker is written at compile time, and the compile cannot know how the library gets linked: test-runner links its shared libraries with lld, the default-lib DLL carries no markers, and older libraries have none. Instead, Dump::containsGarbageCollector looks for the string GC_INITIAL_HEAP_SIZE in the binary's data sections. Boehm's GC_init reads that environment variable, so the string is in every binary that contains the collector and in none that only calls it. Checked against a static-gc default-lib DLL and gc.dll (present), and the release gc/rc default-lib DLLs and TypeScriptRuntime.dll (absent).

Linux

Measured in WSL with the v0.0-pre-alpha81 release, using the gc_single_collector pair:

Case Result Collection off
exe + user .so, linked as before assertion failed 0 bad
JIT + user .so 0 bad 0 bad
exe linked with --whole-archive libgc.a + --export-dynamic-symbol=GC_* 0 bad 0 bad

The JIT already works because libTypeScriptRuntime.so exports GC_*, and the .so's calls bind to it. A plain exe exports nothing, so the .so ran its own copy. exe.cpp now links an importing --emit=exe with the whole collector, exported. The default library's .so links no collector, so nothing else changes on Linux. This was verified with a hand link only. No Linux build of this branch was available locally, so the Linux CI run is the test of the compiler change.

Step 6, template, debug default library

  • docs/memory-models.md lists what to ship beside a program that loads a tslang DLL (gc.dll, TypeScriptDefaultLib.dll), shows the new error, and describes Linux. The zip already ships gc.dll and gcdll/ (One garbage collector per process: TypeScriptRuntime.dll on gc.dll (Windows) #309).
  • CMake template (docs/how/cmake_tslang): new TSLANG_SHARED_GC option, off by default. On Windows it links gcdll/gc.lib and copies gc.dll beside the program. On Linux it adds the two export options. Not built as part of this PR.
  • The debug default-lib DLL only needed a rebuild: build_core.bat already links the debug gcdll. It now imports gc.dll.

Tests

test-compile-gc-shared-auto (shared-collector-auto.cmake, Windows) runs tslang --emit=dll and --emit=exe itself, because test-runner never exercises the compiler's choice. It covers six cases:

  1. The library links gc.dll and gets it copied beside it.
  2. The program that imports it runs clean.
  3. --gc-lib-path pointing at a shared build is enough on its own.
  4. A program with no imports runs with no gc.dll near it, and none is copied.
  5. A library build with only a static collector fails, and the error names gc.dll.
  6. A library linked against the static gc.lib is refused when imported, by --emit=exe and by --emit=jit.

Teeth: with the compiler from before step 3, a library and its importer built with only --gc-lib-path=<static> fail the gc_single_collector assertion; with collection suppressed, the same binary prints 0 bad. Before step 4 there was no import check, so case 6's import succeeded.

Step 5: tests with the real default library

Every test-runner test passes --no-default-lib, so the suite never had the default library in the same process as other gc code. Two new tests fix that: test-jit-gc-defaultlib-collector and test-compile-gc-defaultlib-collector (defaultlib-collector.cmake, sources in test/tester/defaultlib/). They run tslang with the default library in four programs:

  • JIT, the program alone: the held strings (padStart) and the churn (repeat) are both allocated in the default-lib DLL.
  • JIT, importing a user shared library.
  • Exe alone, with the static default library.
  • Exe importing a user DLL that links TypeScriptDefaultLib.dll. The library builds the held strings and does the churn.

The sources are outside tests/ because they don't compile without the default library, and the ownership verifier compiles every file in tests/ with --no-default-lib.

CI: the compiler suite runs before any default library is built, so there the tests report skipped. Both jobs of create-release.yml gain a Test Compiler with Default Library step after Build Default Library. It sets DEFAULT_LIB_PATH to the library just built and TSLANG_REQUIRE_DEFAULT_LIB=1, which turns a missing library into a failure.

Gap found by the tests: step 4 only checks libraries imported by name, and a user DLL links TypeScriptDefaultLib.dll without importing it. --emit=dll under gc on Windows now refuses a default-lib DLL that contains a collector.

Teeth, against a default library built by its own script with GC_SHARED_LIB_PATH pointing at the static gc.lib:

Case Result
both tests, rebuilt default library pass, bad: 0 in all four programs
both tests, static-collector default library fail: --emit=dll refuses the default-lib DLL
exe + user DLL built correctly, stale DLL swapped in at run time assertion failed
the same, collection suppressed bad: 0

A machine whose DEFAULT_LIB_PATH points at an installation with a default library from before #309 fails these two tests. That is intended: --emit=dll and JIT runs against that library now fail the same way.

Test plan

  • test-compile-gc-shared-auto passes, including case 6
  • The JIT refuses a default-lib DLL that contains a collector, and runs normally with the rebuilt release and debug default libraries
  • test-jit-gc-defaultlib-collector and test-compile-gc-defaultlib-collector pass against the rebuilt default library and fail against a static-collector one
  • Full ctest, Windows release, with DEFAULT_LIB_PATH at the rebuilt default library: 2,707 of 2,707
  • CI green on Windows and Linux (Linux is the first real run of the exe.cpp export change)
  • The release workflow's Test Compiler with Default Library step passes on both jobs (not yet run anywhere)

This branch's second commit also bumps tag.bat / tag_del.bat to v0.0-pre-alpha82.

🤖 Generated with Claude Code

ASDAlexander77 and others added 4 commits September 13, 2026 00:38
A user's shared library and the program that imports it still linked the
static gc.lib unless --gc-lib-path was swapped by hand, so they got two
collectors and one freed what the other held.

Now --emit=dll, and --emit=exe for a program that imports a tslang shared
library (CompileOptions::importsSharedLibrary, found as LoadLibraryPermanentlyOp
before lowering), link gc.dll's import library and copy gc.dll beside the
output. The shared collector is found through --gc-shared-lib-path,
GC_SHARED_LIB_PATH, <gc-lib-path>/gcdll (the release package), or
--gc-lib-path itself when it is a shared build; with none found the build
stops instead of linking a collector of its own. A lone program keeps the
static gc.lib. Linux unchanged.

test-compile-gc-shared-auto drives tslang --emit=dll/exe itself (five cases).
With the previous compiler a static-only library + importer fail the
gc_single_collector assertion. Suite: 2,705 of 2,705.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
… gc-*

The previous commit registered test-compile-gc-shared-auto but its script,
gc-shared-auto.cmake, matched the root .gitignore's gc-* (meant for the gc-8.x
source folders) and was never added. Renamed to shared-collector-auto.cmake.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@ASDAlexander77 ASDAlexander77 changed the title Let tslang choose the garbage collector's linkage (Windows, -mm=gc) One garbage collector per process: linkage choice, import checks, Linux, default-library tests Sep 13, 2026
@ASDAlexander77
ASDAlexander77 merged commit 3b1ec29 into main Sep 13, 2026
2 of 3 checks passed
@ASDAlexander77
ASDAlexander77 deleted the gc-shared-lib-auto branch September 13, 2026 11:09
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant