Skip to content

A shared library reads the default library's data through its DLL - #630

Merged
ASDAlexander77 merged 1 commit into
mainfrom
defaultlib-data-dllimport
Oct 20, 2026
Merged

ASDAlexander77 merged 1 commit into
mainfrom
defaultlib-data-dllimport

Conversation

@ASDAlexander77

Copy link
Copy Markdown
Owner

Problem

tslang --emit=dll failed to link any program that reads data defined in the default library:

function main() {
    const m = new Map<string, number>();
    m.set("a", 1);
    print(m.size);
}
error LNK2019: unresolved external symbol PrimeHelpers.hashCollisionThreshold referenced in function Map<!ts.string,!ts.number>.tryInsert

A shared library links the default library's DLL (exe.cpp), and a DLL exports data only as __imp_<name>. The import library has no plain PrimeHelpers.hashCollisionThreshold, only __imp_PrimeHelpers.hashCollisionThreshold. The default library's ambient data in core.d.ts / lib.d.ts carries no @dllimport marker, so it was emitted as a plain external global. Functions were not affected because the import library gives each one a thunk. --emit=exe links the static default library, and --emit=jit binds the symbol from the DLL's exports, so neither showed the bug.

The same failure hits Math.PI (a const inside declare namespace Math), Number.EPSILON / MAX_SAFE_INTEGER (static fields of declare class Number) and RangeError..rtti (read by instanceof).

Fix

  • MLIRGenModule.cpp: loadReferencedFile records each file loaded for lib.d.ts (the file and everything it references) in defaultLibFiles.
  • MLIRGenImpl.h: createGlobalVariable gives the import attribute to an ambient (isExternal) global declared in one of those files when the program links the shared default library (isDLL, not Android, default lib on). ExportFixPass turns that into dllimport on Windows MSVC and drops it on other targets, the same path a user library's @dllimport declarations take. The global stays VariableType::External, so it is a pure external dllimport declaration with no initializer.

lib.generics.ts is the library's source compiled into the program, so its definitions are not ambient and are unaffected. --emit=exe and --emit=jit objects are unchanged: the gate is isDLL, so there is no LNK4217 and no __imp_ for RTDyld to resolve. I checked that the exe's IR still has a plain external global for these symbols. Out of scope on purpose: an --emit=obj object that is later linked with -shared by hand still gets plain externals.

Test

test-compile-shared-defaultlib-data (test/tester/defaultlib-data-import.cmake, sources in test/tester/defaultlib/) builds a -mm=rc DLL with the real default library that reads PrimeHelpers.hashCollisionThreshold (through a Map), Math.PI and Number.EPSILON. It then builds and runs an exe that imports the DLL. Like the default-library collector tests, it is skipped when no default library has been built.

  • Before the fix: LNK2019 on all three symbols. After the fix: passes with --opt and with --di.
  • ctest -C Release -R "shared|import|defaultlib": 536/536 passed (1 test was already disabled). That includes test-{jit,compile}-gc-defaultlib-collector, which ran and were not skipped.
  • I also ran a manual DLL that uses Error/RangeError with instanceof in a try/catch, Set, Date, Number.MAX_SAFE_INTEGER and Math.E. The unfixed compiler fails on 4 symbols. The fixed one links, and a host exe calling the DLL prints the expected values.

🤖 Generated with Claude Code

`tslang --emit=dll` links the default library's DLL, and a DLL exports data
only as `__imp_<name>`. The default library's ambient data (core.d.ts's
PrimeHelpers.hashCollisionThreshold, `declare namespace Math`'s PI,
`declare class Number`'s EPSILON) was a plain external global, so a library
using Map failed to link: "unresolved external symbol
PrimeHelpers.hashCollisionThreshold referenced in function
Map<!ts.string,!ts.number>.tryInsert". Functions were fine: the import
library gives each one a thunk.

The files loaded for lib.d.ts are now recorded, and an ambient global
declared in one of them gets the `import` attribute when the program links
the shared default library (isDLL, not Android). ExportFixPass makes it
dllimport on Windows and drops it elsewhere. --emit=exe and --emit=jit
objects are unchanged.

Test: test-compile-shared-defaultlib-data links a DLL reading all three
and runs a program importing it; skipped without a built default library.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@ASDAlexander77
ASDAlexander77 merged commit 0ea81e6 into main Oct 20, 2026
2 checks passed
@ASDAlexander77
ASDAlexander77 deleted the defaultlib-data-dllimport branch October 20, 2026 22:44
ASDAlexander77 added a commit that referenced this pull request Oct 20, 2026
#630 made --emit=dll read the default library's data through `__imp_`,
but that does not carry over to --jit-native. Its DLL links the JIT
cache's objects, which are compiled for the JIT's static large code model,
and x86 codegen gives a dllimport global no `__imp_` reference under that
model (X86Subtarget::classifyGlobalReference). Checked: with the data
marked dllimport for JIT objects too, the in-memory JIT still runs, but
the objects keep plain `Math.PI` references and the DLL link fails without
the static default library. The comment and the design doc said this was
the same bug as --emit=dll; they now give the actual reason.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
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