From 2c8184e5dbb130ced0b5c6cd2887cbc2e9cf5a84 Mon Sep 17 00:00:00 2001 From: ASDAlexander77 Date: Tue, 20 Oct 2026 23:24:30 +0100 Subject: [PATCH] A shared library reads the default library's data through its DLL `tslang --emit=dll` links the default library's DLL, and a DLL exports data only as `__imp_`. 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.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 --- tslang/lib/TypeScript/MLIRGenImpl.h | 20 +++- tslang/lib/TypeScript/MLIRGenModule.cpp | 19 +++- tslang/test/tester/CMakeLists.txt | 15 +++ .../test/tester/defaultlib-data-import.cmake | 105 ++++++++++++++++++ .../defaultlib/export_defaultlib_data.ts | 25 +++++ .../defaultlib/import_defaultlib_data.ts | 15 +++ 6 files changed, 192 insertions(+), 7 deletions(-) create mode 100644 tslang/test/tester/defaultlib-data-import.cmake create mode 100644 tslang/test/tester/defaultlib/export_defaultlib_data.ts create mode 100644 tslang/test/tester/defaultlib/import_defaultlib_data.ts diff --git a/tslang/lib/TypeScript/MLIRGenImpl.h b/tslang/lib/TypeScript/MLIRGenImpl.h index 79310141e..eb65db6a8 100644 --- a/tslang/lib/TypeScript/MLIRGenImpl.h +++ b/tslang/lib/TypeScript/MLIRGenImpl.h @@ -2443,6 +2443,19 @@ class MLIRGenImpl return mlir::success(); } + // Whether a global is the default library's data and the program links the library's DLL, as + // a shared library does (exe.cpp; Android links the static one). On Windows a DLL exports + // data only through `__imp_`, which a plain external global does not reach, so + // `PrimeHelpers.hashCollisionThreshold`, read by a Map instantiated here, failed to link. Its + // functions need nothing: the import library gives each of them a thunk. Only an ambient + // declaration: lib.generics.ts is the library's source compiled into the program. + bool isImportedFromSharedDefaultLib(const struct VariableDeclarationInfo &variableDeclarationInfo) + { + return variableDeclarationInfo.isExternal && compileOptions.isDLL && !compileOptions.isAndroid && + !compileOptions.noDefaultLib && sourceFile && + defaultLibFiles.contains(convertWideToUTF8(sourceFile->resolvedPath)); + } + mlir::LogicalResult createGlobalVariable(mlir::Location location, struct VariableDeclarationInfo &variableDeclarationInfo, const GenContext &genContext) { // generate only for real pass @@ -2469,7 +2482,7 @@ class MLIRGenImpl attrs.push_back({mlir::StringAttr::get(builder.getContext(), "export"), mlir::UnitAttr::get(builder.getContext())}); } - if (variableDeclarationInfo.isImport) + if (variableDeclarationInfo.isImport || isImportedFromSharedDefaultLib(variableDeclarationInfo)) { attrs.push_back({mlir::StringAttr::get(builder.getContext(), "import"), mlir::UnitAttr::get(builder.getContext())}); } @@ -14886,6 +14899,11 @@ class MLIRGenImpl // imports a) stops instead of recursing until the stack overflows. llvm::StringSet<> filesInProgress; + // The default library's files - lib.d.ts and every file it references - by file key. Its + // ambient data is defined in its DLL when the program links that (see + // isImportedFromSharedDefaultLib). + llvm::StringSet<> defaultLibFiles; + // Type aliases whose declaration is being turned into a type, by full name: one that meets // itself on the way is circular (resolveTypeByNameInNamespace). llvm::StringSet<> typeAliasesInProgress; diff --git a/tslang/lib/TypeScript/MLIRGenModule.cpp b/tslang/lib/TypeScript/MLIRGenModule.cpp index fcc348140..c95c71414 100644 --- a/tslang/lib/TypeScript/MLIRGenModule.cpp +++ b/tslang/lib/TypeScript/MLIRGenModule.cpp @@ -180,9 +180,10 @@ namespace mlirgen // `referencingDir`: a relative path is that file's, as in TypeScript, so it is looked for // there first. Then as before - the working directory, the main file's directory, the // default lib's - which a reference relative to the main file from a file elsewhere - // still relies on. - std::function loadReferencedFile = - [&](const string &includeFileName, SourceFile referencing, const ts::data::FileReference *reference, StringRef referencingDir) { + // still relies on. `fromDefaultLib`: the file is the default lib or one it references. + std::function loadReferencedFile = + [&](const string &includeFileName, SourceFile referencing, const ts::data::FileReference *reference, StringRef referencingDir, + bool fromDefaultLib) { auto includeFileNameUtf8 = convertWideToUTF8(includeFileName); std::string actualFilePath; @@ -223,6 +224,11 @@ namespace mlirgen return; } + if (fromDefaultLib) + { + defaultLibFiles.insert(includeFilePath); + } + auto id = sourceMgr.AddNewSourceBuffer(std::move(*includeBuf), SMLoc()); SmallString<256> fullPath; @@ -249,21 +255,22 @@ namespace mlirgen auto includeFileDir = sys::path::parent_path(actualFilePath).str(); for (auto &refFile : includeFile->referencedFiles) { - loadReferencedFile(refFile.fileName, includeFile, &refFile, includeFileDir); + loadReferencedFile(refFile.fileName, includeFile, &refFile, includeFileDir, fromDefaultLib); } includeFiles.push_back(includeFile); }; + // only the default lib is in filesToProcess for (auto &fileToProcess : filesToProcess) { - loadReferencedFile(fileToProcess, sourceFile, nullptr, StringRef()); + loadReferencedFile(fileToProcess, sourceFile, nullptr, StringRef(), true); } auto sourceFileDir = sys::path::parent_path(fullPath).str(); for (auto &refFile : sourceFile->referencedFiles) { - loadReferencedFile(refFile.fileName, sourceFile, &refFile, sourceFileDir); + loadReferencedFile(refFile.fileName, sourceFile, &refFile, sourceFileDir, false); } if (anyMissing) diff --git a/tslang/test/tester/CMakeLists.txt b/tslang/test/tester/CMakeLists.txt index 83825b714..c2947ed62 100644 --- a/tslang/test/tester/CMakeLists.txt +++ b/tslang/test/tester/CMakeLists.txt @@ -2769,6 +2769,21 @@ if (TSLANG_TEST_X86) LABELS x86) endif() +# A shared library reading the default library's data from its DLL - see +# defaultlib-data-import.cmake. Skipped (not failed) when no default library has been built, like the +# collector tests above, whose options it shares. +add_test(NAME test-compile-shared-defaultlib-data + COMMAND ${CMAKE_COMMAND} + "-DTSLANG=$" + "-DTESTS_DIR=${CMAKE_CURRENT_SOURCE_DIR}/defaultlib" + "-DWORK_DIR=${CMAKE_CURRENT_BINARY_DIR}/shared-defaultlib-data" + "-DLLVM_LIB=${defaultlib_collector_llvm_lib}" + "-DTSLANG_LIB=${CMAKE_BINARY_DIR}/lib" + "-DOPT=${defaultlib_collector_opt}" + "-DDEFAULT_LIB_CANDIDATES=${PROJECT_SOURCE_DIR}/../TypeScriptCompilerDefaultLib/__build;${PROJECT_SOURCE_DIR}/../../TypeScriptCompilerDefaultLib/__build" + -P "${CMAKE_CURRENT_SOURCE_DIR}/defaultlib-data-import.cmake") +set_tests_properties(test-compile-shared-defaultlib-data PROPERTIES SKIP_REGULAR_EXPRESSION "SKIPPED:") + # tsbindgen: bindings generated from test/bindgen/fixture.h call the C library compiled from # fixture.c, linked (--emit=exe) and under the JIT; `namespace` does it again through # --namespace/--strip-prefix, which @linkname binds; `cpp` calls the C++ classes of diff --git a/tslang/test/tester/defaultlib-data-import.cmake b/tslang/test/tester/defaultlib-data-import.cmake new file mode 100644 index 000000000..fd59aae60 --- /dev/null +++ b/tslang/test/tester/defaultlib-data-import.cmake @@ -0,0 +1,105 @@ +# A shared library that reads data defined in the default library (-mm=rc). +# +# `tslang --emit=dll` links the default library's DLL, and a DLL exports data only as +# `__imp_`: a plain external global does not reach it. So a library whose Map read +# PrimeHelpers.hashCollisionThreshold failed to link ("unresolved external symbol +# PrimeHelpers.hashCollisionThreshold referenced in function Map.tryInsert"), +# while --emit=exe (the static default library) and --emit=jit (bound from the DLL's exports) did +# not show it. Every test-runner test passes --no-default-lib, so none had the default library's +# data in a DLL. This links export_defaultlib_data.ts as a DLL with the real default library and +# runs a program that imports it. +# +# The default library is a separate build (TypeScriptCompilerDefaultLib). With none found the test +# prints SKIPPED and CTest reports it skipped. + +cmake_minimum_required(VERSION 3.17.3) + +foreach(var TSLANG TESTS_DIR WORK_DIR LLVM_LIB TSLANG_LIB OPT DEFAULT_LIB_CANDIDATES) + if(NOT DEFINED ${var}) + message(FATAL_ERROR "${var} is required") + endif() +endforeach() + +if(WIN32) + set(exe_suffix ".exe") + set(dll_prefix "") + set(dll_suffix ".dll") + set(pic "") +else() + set(exe_suffix "") + set(dll_prefix "lib") + set(dll_suffix ".so") + set(pic "-relocation-model=pic") +endif() + +# The folder of the shared default library for this target, build and model, from tslang itself +execute_process(COMMAND "${TSLANG}" --print-default-lib-dir=dll ${OPT} -mm=rc + OUTPUT_VARIABLE default_lib_dll_subdir + OUTPUT_STRIP_TRAILING_WHITESPACE + RESULT_VARIABLE status) +if(NOT status EQUAL 0 OR default_lib_dll_subdir STREQUAL "") + message(FATAL_ERROR "tslang --print-default-lib-dir=dll ${OPT} -mm=rc: exit ${status}") +endif() + +# DEFAULT_LIB_PATH at run time wins, so a workflow can point at the library it just built +set(default_lib "") +foreach(candidate "$ENV{DEFAULT_LIB_PATH}" ${DEFAULT_LIB_CANDIDATES}) + if(NOT candidate STREQUAL "" AND IS_DIRECTORY "${candidate}/${default_lib_dll_subdir}") + set(default_lib "${candidate}") + break() + endif() +endforeach() + +if(default_lib STREQUAL "") + if("$ENV{TSLANG_REQUIRE_DEFAULT_LIB}" STREQUAL "1") + message(FATAL_ERROR "no default library built in ${default_lib_dll_subdir} under DEFAULT_LIB_PATH='$ENV{DEFAULT_LIB_PATH}'") + endif() + message("SKIPPED: no default library built in ${default_lib_dll_subdir} (set DEFAULT_LIB_PATH to the folder holding 'defaultlib')") + return() +endif() + +set(default_lib_dll_dir "${default_lib}/${default_lib_dll_subdir}") +message(STATUS "default library: ${default_lib}") + +set(ENV{GC_LIB_PATH} "") +set(ENV{DEFAULT_LIB_PATH} "") + +file(REMOVE_RECURSE "${WORK_DIR}") +file(MAKE_DIRECTORY "${WORK_DIR}") + +set(common -mm=rc ${OPT} ${pic} "--default-lib-path=${default_lib}" "--llvm-lib-path=${LLVM_LIB}" "--tslang-lib-path=${TSLANG_LIB}") + +# the loader has to find the library and the default library's DLL +if(WIN32) + set(run_env "PATH=${WORK_DIR};$ENV{PATH}") +else() + set(run_env "LD_LIBRARY_PATH=${WORK_DIR}:${default_lib_dll_dir}:$ENV{LD_LIBRARY_PATH}") +endif() + +# run( ) - fails unless it exits 0 +function(run what) + execute_process(COMMAND ${CMAKE_COMMAND} -E env "${run_env}" ${ARGN} + WORKING_DIRECTORY "${WORK_DIR}" + OUTPUT_VARIABLE out + ERROR_VARIABLE err + RESULT_VARIABLE status) + if(NOT status EQUAL 0) + message(FATAL_ERROR "${what}: exit ${status}\n${out}\n${err}") + endif() + set(out "${out}" PARENT_SCOPE) +endfunction() + +run("--emit=dll reading the default library's data" + "${TSLANG}" --emit=dll ${common} "${TESTS_DIR}/export_defaultlib_data.ts" + -o "${dll_prefix}export_defaultlib_data${dll_suffix}") +file(COPY "${default_lib_dll_dir}/${dll_prefix}TypeScriptDefaultLib${dll_suffix}" DESTINATION "${WORK_DIR}") + +run("--emit=exe importing it" + "${TSLANG}" --emit=exe ${common} "${TESTS_DIR}/import_defaultlib_data.ts" -o "main${exe_suffix}") + +run("the program" "${WORK_DIR}/main${exe_suffix}") +if(NOT out MATCHES "size: 300" OR NOT out MATCHES "done\\.") + message(FATAL_ERROR "the program:\n${out}") +endif() + +message(STATUS "a shared library reads the default library's data from its DLL") diff --git a/tslang/test/tester/defaultlib/export_defaultlib_data.ts b/tslang/test/tester/defaultlib/export_defaultlib_data.ts new file mode 100644 index 000000000..be307c592 --- /dev/null +++ b/tslang/test/tester/defaultlib/export_defaultlib_data.ts @@ -0,0 +1,25 @@ +namespace L { + + // The library side of import_defaultlib_data.ts. Linked with the default library's DLL, it reads + // data that DLL defines: PrimeHelpers.hashCollisionThreshold (core.d.ts, read by the Map + // instantiated here), Math.PI (a const in `declare namespace Math`) and Number.EPSILON (a static + // field of `declare class Number`). A DLL exports data only as `__imp_`, so each has to be + // read through that, or the library does not link. + + export function mapSize(n: number): number { + const m = new Map(); + for (let i = 0; i < n; i++) { + m.set(`k${i}`, i); + } + + return m.size; + } + + export function pi(): number { + return Math.PI; + } + + export function epsilon(): number { + return Number.EPSILON; + } +} diff --git a/tslang/test/tester/defaultlib/import_defaultlib_data.ts b/tslang/test/tester/defaultlib/import_defaultlib_data.ts new file mode 100644 index 000000000..8d6020710 --- /dev/null +++ b/tslang/test/tester/defaultlib/import_defaultlib_data.ts @@ -0,0 +1,15 @@ +import './export_defaultlib_data' + +// Calls a shared library that reads data defined in the default library's DLL. See +// export_defaultlib_data.ts. + +function main() { + const size = L.mapSize(300); + print("size:", size); + assert(size == 300, "Map in a shared library"); + + assert(L.pi() > 3.14159 && L.pi() < 3.1416, "Math.PI in a shared library"); + assert(L.epsilon() > 0 && L.epsilon() < 1e-15, "Number.EPSILON in a shared library"); + + print("done."); +}