Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 19 additions & 1 deletion tslang/lib/TypeScript/MLIRGenImpl.h
Original file line number Diff line number Diff line change
Expand Up @@ -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_<name>`, 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
Expand All @@ -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())});
}
Expand Down Expand Up @@ -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;
Expand Down
19 changes: 13 additions & 6 deletions tslang/lib/TypeScript/MLIRGenModule.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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<void(const string &, SourceFile, const ts::data::FileReference *, StringRef)> 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<void(const string &, SourceFile, const ts::data::FileReference *, StringRef, bool)> loadReferencedFile =
[&](const string &includeFileName, SourceFile referencing, const ts::data::FileReference *reference, StringRef referencingDir,
bool fromDefaultLib) {
auto includeFileNameUtf8 = convertWideToUTF8(includeFileName);

std::string actualFilePath;
Expand Down Expand Up @@ -223,6 +224,11 @@ namespace mlirgen
return;
}

if (fromDefaultLib)
{
defaultLibFiles.insert(includeFilePath);
}

auto id = sourceMgr.AddNewSourceBuffer(std::move(*includeBuf), SMLoc());

SmallString<256> fullPath;
Expand All @@ -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)
Expand Down
15 changes: 15 additions & 0 deletions tslang/test/tester/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -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=$<TARGET_FILE:tslang>"
"-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
Expand Down
105 changes: 105 additions & 0 deletions tslang/test/tester/defaultlib-data-import.cmake
Original file line number Diff line number Diff line change
@@ -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_<name>`: 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<!ts.string,!ts.number>.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(<what> <command...>) - 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")
25 changes: 25 additions & 0 deletions tslang/test/tester/defaultlib/export_defaultlib_data.ts
Original file line number Diff line number Diff line change
@@ -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_<name>`, so each has to be
// read through that, or the library does not link.

export function mapSize(n: number): number {
const m = new Map<string, number>();
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;
}
}
15 changes: 15 additions & 0 deletions tslang/test/tester/defaultlib/import_defaultlib_data.ts
Original file line number Diff line number Diff line change
@@ -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.");
}
Loading