diff --git a/tslang/include/TypeScript/Config.h b/tslang/include/TypeScript/Config.h index a304f9ebe..6cab1a570 100644 --- a/tslang/include/TypeScript/Config.h +++ b/tslang/include/TypeScript/Config.h @@ -32,12 +32,6 @@ #define TUPLE_TYPE_PACKED false #define UNION_TYPE_PACKED true -#ifdef WIN32 -#define WIN_LOADSHAREDLIBS 1 -#else -#define LINUX_LOADSHAREDLIBS 1 -#endif - #define USE_NEW_AS_METHOD true #define ADD_STATIC_MEMBERS_TO_VTABLE true diff --git a/tslang/include/TypeScript/DataStructs.h b/tslang/include/TypeScript/DataStructs.h index 7801e0abf..80c698d7c 100644 --- a/tslang/include/TypeScript/DataStructs.h +++ b/tslang/include/TypeScript/DataStructs.h @@ -28,6 +28,8 @@ struct CompileOptions } bool isWasm; bool isWindows; + // Bionic has its own assert entry points (__assert/__assert2), not glibc's __assert_fail + bool isAndroid = false; bool isExecutable; bool isDLL; diff --git a/tslang/include/TypeScript/LowerToLLVM/AssertLogic.h b/tslang/include/TypeScript/LowerToLLVM/AssertLogic.h index b646e2fde..761fa95be 100644 --- a/tslang/include/TypeScript/LowerToLLVM/AssertLogic.h +++ b/tslang/include/TypeScript/LowerToLLVM/AssertLogic.h @@ -31,6 +31,9 @@ class AssertLogic LLVMCodeHelperBase ch; CodeLogicHelper clh; Location loc; + // the target's CRT decides how a failure is reported, not the host's + bool isWindows; + bool isAndroid; protected: mlir::Type sizeType; @@ -38,14 +41,16 @@ class AssertLogic public: AssertLogic(Operation *op, PatternRewriter &rewriter, TypeConverterHelper &tch, Location loc, CompileOptions &compileOptions) - : op(op), rewriter(rewriter), th(rewriter), ch(op, rewriter, tch.typeConverter, compileOptions), clh(op, rewriter), loc(loc) + : op(op), rewriter(rewriter), th(rewriter), ch(op, rewriter, tch.typeConverter, compileOptions), clh(op, rewriter), loc(loc), + isWindows(compileOptions.isWindows), isAndroid(compileOptions.isAndroid) { sizeType = th.getIndexType(); typeOfValueType = th.getPtrType(); } AssertLogic(Operation *op, PatternRewriter &rewriter, const TypeConverter *typeConverter, Location loc, CompileOptions &compileOptions) - : op(op), rewriter(rewriter), th(rewriter), ch(op, rewriter, typeConverter, compileOptions), clh(op, rewriter), loc(loc) + : op(op), rewriter(rewriter), th(rewriter), ch(op, rewriter, typeConverter, compileOptions), clh(op, rewriter), loc(loc), + isWindows(compileOptions.isWindows), isAndroid(compileOptions.isAndroid) { sizeType = th.getIndexType(); typeOfValueType = th.getPtrType(); @@ -69,7 +74,7 @@ class AssertLogic rewriter.setInsertionPointToStart(continuationBlock); } - // `_assert` and `__assert_fail` abort, which flushes no stream: what the program printed before + // `_assert`, `__assert` and `__assert_fail` abort, which flushes no stream: what the program printed before // the failure was lost whenever stdout was not a console (a pipe, a file, the test runner) void flushOutput() { @@ -94,7 +99,8 @@ class AssertLogic private: // Splits the block at the insertion point and ends the first half with a branch on // `condValue`: on to the continuation, which is returned, or to a new block that reports the - // failure (`_assert` on Windows, `__assert_fail` elsewhere) and never returns. + // failure (`_assert` on Windows, `__assert` on Android, `__assert_fail` elsewhere) and never + // returns. mlir::Block *failUnless(mlir::Value condValue, std::string msg, mlir::Value message) { auto unreachable = clh.FindUnreachableBlockOrCreate(); @@ -103,13 +109,6 @@ class AssertLogic auto [line, column] = lineAndColumn; auto i8PtrTy = th.getPtrType(); -#ifdef WIN32 - auto assertFuncOp = - ch.getOrInsertFunction("_assert", th.getFunctionType(th.getVoidType(), {i8PtrTy, i8PtrTy, rewriter.getI32Type()})); -#else - auto assertFuncOp = ch.getOrInsertFunction( - "__assert_fail", th.getFunctionType(th.getVoidType(), {i8PtrTy, i8PtrTy, rewriter.getI32Type(), i8PtrTy})); -#endif auto *opBlock = rewriter.getInsertionBlock(); auto opPosition = rewriter.getInsertionPoint(); @@ -131,12 +130,26 @@ class AssertLogic mlir::Value lineNumberRes = rewriter.create(loc, rewriter.getI32Type(), rewriter.getI32IntegerAttr(line)); flushOutput(); -#ifdef WIN32 - rewriter.create(loc, assertFuncOp, ValueRange{messageOrConstant(message, msgCst), fileCst, lineNumberRes}); -#else - mlir::Value funcName = rewriter.create(loc, i8PtrTy); - rewriter.create(loc, assertFuncOp, ValueRange{messageOrConstant(message, msgCst), fileCst, lineNumberRes, funcName}); -#endif + if (isWindows) + { + auto assertFuncOp = + ch.getOrInsertFunction("_assert", th.getFunctionType(th.getVoidType(), {i8PtrTy, i8PtrTy, rewriter.getI32Type()})); + rewriter.create(loc, assertFuncOp, ValueRange{messageOrConstant(message, msgCst), fileCst, lineNumberRes}); + } + else if (isAndroid) + { + // Bionic exports no __assert_fail; __assert(file, line, msg) takes the file first + auto assertFuncOp = + ch.getOrInsertFunction("__assert", th.getFunctionType(th.getVoidType(), {i8PtrTy, rewriter.getI32Type(), i8PtrTy})); + rewriter.create(loc, assertFuncOp, ValueRange{fileCst, lineNumberRes, messageOrConstant(message, msgCst)}); + } + else + { + auto assertFuncOp = ch.getOrInsertFunction( + "__assert_fail", th.getFunctionType(th.getVoidType(), {i8PtrTy, i8PtrTy, rewriter.getI32Type(), i8PtrTy})); + mlir::Value funcName = rewriter.create(loc, i8PtrTy); + rewriter.create(loc, assertFuncOp, ValueRange{messageOrConstant(message, msgCst), fileCst, lineNumberRes, funcName}); + } rewriter.create(loc, unreachable); rewriter.setInsertionPointToEnd(opBlock); diff --git a/tslang/lib/TypeScript/LowerToLLVM.cpp b/tslang/lib/TypeScript/LowerToLLVM.cpp index ee545257f..cf246454e 100644 --- a/tslang/lib/TypeScript/LowerToLLVM.cpp +++ b/tslang/lib/TypeScript/LowerToLLVM.cpp @@ -105,13 +105,10 @@ class ConvertFOpLowering : public TsLlvmPattern auto llvmI32Type = tch.convertType(th.getI32Type()); auto llvmIndexType = tch.convertType(rewriter.getIndexType()); -#ifdef WIN32 + // the target's CRT, not the host's: sprintf_s is MSVC only auto sprintfFuncOp = ch.getOrInsertFunction( - "sprintf_s", th.getFunctionType(rewriter.getI32Type(), {th.getPtrType(), llvmIndexType, th.getPtrType()}, true)); -#else - auto sprintfFuncOp = ch.getOrInsertFunction( - "snprintf", th.getFunctionType(rewriter.getI32Type(), {th.getPtrType(), llvmIndexType, th.getPtrType()}, true)); -#endif + tsLlvmContext->compileOptions.isWindows ? "sprintf_s" : "snprintf", + th.getFunctionType(rewriter.getI32Type(), {th.getPtrType(), llvmIndexType, th.getPtrType()}, true)); auto bufferSizeValue = transformed.getBufferSize(); auto newStringValue = ch.MemoryAlloc(bufferSizeValue, MemoryAllocSet::Atomic); diff --git a/tslang/lib/TypeScript/MLIRGenClasses.cpp b/tslang/lib/TypeScript/MLIRGenClasses.cpp index f47474b73..8f6da5547 100644 --- a/tslang/lib/TypeScript/MLIRGenClasses.cpp +++ b/tslang/lib/TypeScript/MLIRGenClasses.cpp @@ -1291,11 +1291,8 @@ genContext); if (!newClassPtr->isImport) { declarationMode = false; -#ifdef WIN32 - varClass.comdat = Select::ExactMatch; -#else - varClass.comdat = Select::Any; -#endif + // decided by the target, not the host: ELF only supports Any (and NoDeduplicate) + varClass.comdat = compileOptions.isWindows ? Select::ExactMatch : Select::Any; } else if (newClassPtr->isDeclaration) { diff --git a/tslang/lib/TypeScript/MLIRGenModule.cpp b/tslang/lib/TypeScript/MLIRGenModule.cpp index 5abc23ed5..5a56fecdb 100644 --- a/tslang/lib/TypeScript/MLIRGenModule.cpp +++ b/tslang/lib/TypeScript/MLIRGenModule.cpp @@ -1617,27 +1617,25 @@ namespace mlirgen auto stringVal = valueAttr.getValue(); + // The library is named as the target names it, as --emit=dll does (utils.cpp): the path is + // also what the program loads at run time, so a host's naming would not be found there. + auto targetIsWindows = llvm::Triple(compileOptions.moduleTargetTriple).getOS() == llvm::Triple::Win32; + std::string fullPath; fullPath += stringVal; -#ifdef WIN_LOADSHAREDLIBS -#endif -#ifdef LINUX_LOADSHAREDLIBS - // rebuild file path - auto fileName = sys::path::filename(stringVal); - auto path = stringVal.substr(0, stringVal.size() - fileName.size()); - fullPath = path; - fullPath += "lib"; - fullPath += fileName; -#endif + if (!targetIsWindows) + { + // rebuild file path + auto fileName = sys::path::filename(stringVal); + auto path = stringVal.substr(0, stringVal.size() - fileName.size()); + fullPath = path; + fullPath += "lib"; + fullPath += fileName; + } if (sys::path::extension(fullPath) == "") { -#ifdef WIN_LOADSHAREDLIBS - fullPath += ".dll"; -#endif -#ifdef LINUX_LOADSHAREDLIBS - fullPath += ".so"; -#endif + fullPath += targetIsWindows ? ".dll" : ".so"; } if (sys::fs::exists(fullPath)) diff --git a/tslang/test/tester/CMakeLists.txt b/tslang/test/tester/CMakeLists.txt index 10b0fa007..d1a5016c8 100644 --- a/tslang/test/tester/CMakeLists.txt +++ b/tslang/test/tester/CMakeLists.txt @@ -2408,6 +2408,14 @@ if (WIN32) endforeach() endif() +# The CRT functions a program calls follow its target, not the host - see target-crt.cmake. IR +# only, so every host runs it. +add_test(NAME test-compile-target-crt + COMMAND ${CMAKE_COMMAND} + "-DTSLANG=$" + "-DWORK_DIR=${CMAKE_CURRENT_BINARY_DIR}/target-crt" + -P "${CMAKE_CURRENT_SOURCE_DIR}/target-crt.cmake") + # One collector per process with the real default library in it - see defaultlib-collector.cmake. # Skipped (not failed) when no default library has been built; the release workflows run them after # building one. The sources live outside tests/: without the default library they do not compile, diff --git a/tslang/test/tester/foreign-target-import.cmake b/tslang/test/tester/foreign-target-import.cmake index 3239038b5..81b60c335 100644 --- a/tslang/test/tester/foreign-target-import.cmake +++ b/tslang/test/tester/foreign-target-import.cmake @@ -7,6 +7,10 @@ # compiled for x86_64 Linux. That target has the host's arch but another OS, so the DLL is read as # a file, as a PE32+ image. The importer is only compiled, never linked or run. # +# An import is looked for under the target's name for a library, which is also what the program +# loads at run time: `import './shared'` is ./libshared.so for x86_64 Linux. The x64 DLL is +# therefore written as libshared.so; it is still a PE image inside. +# # MODE=read the declarations are read: the importer compiles, and uses what they declare. # MODE=errors a file that is not a PE image, and a DLL for another machine, are refused with an # error that says so. @@ -52,7 +56,7 @@ set(foreign -mtriple=x86_64-unknown-linux-gnu) if(MODE STREQUAL "read") run("--emit=dll" "${WORK_DIR}" TRUE "${TSLANG}" --emit=dll ${common} "--llvm-lib-path=${LLVM_LIB}" "--tslang-lib-path=${TSLANG_LIB}" - "${library}" -o shared.dll) + "${library}" -o libshared.so) # use_shared.ts calls test1/test2 and prints val_str, which only the DLL's declarations # declare: without them it does not compile ("can't resolve name"). @@ -72,15 +76,20 @@ if(MODE STREQUAL "read") endif() endforeach() + # ...from the library under the target's name for it + if(NOT ir MATCHES "libshared\\.so\\\\00\"") + message(FATAL_ERROR "the program does not load libshared.so:\n${ir}") + endif() + message(STATUS "declarations read from an x64 DLL's file for a foreign target") elseif(MODE STREQUAL "errors") # 1. A file that is not a PE image. file(MAKE_DIRECTORY "${WORK_DIR}/not-pe") - file(WRITE "${WORK_DIR}/not-pe/foo.dll" "not a DLL\n") + file(WRITE "${WORK_DIR}/not-pe/libfoo.so" "not a DLL\n") file(WRITE "${WORK_DIR}/not-pe/main.ts" "import './foo'\n\nfunction main() {\n print(\"done.\");\n}\n") run("importing a non-PE file for a foreign target" "${WORK_DIR}/not-pe" FALSE "${TSLANG}" --emit=obj ${common} ${foreign} main.ts -o main.o) - if(NOT run_output MATCHES "cannot read declarations from '[^']*foo\\.dll': for a target other than the host, only PE DLLs can be imported") + if(NOT run_output MATCHES "cannot read declarations from '[^']*libfoo\\.so': for a target other than the host, only PE DLLs can be imported") message(FATAL_ERROR "importing a non-PE file failed for another reason:\n${run_output}") endif() diff --git a/tslang/test/tester/target-crt.cmake b/tslang/test/tester/target-crt.cmake new file mode 100644 index 000000000..7e75238a4 --- /dev/null +++ b/tslang/test/tester/target-crt.cmake @@ -0,0 +1,70 @@ +# The C runtime a program calls into is the target's, not the host's. +# +# A failing assert, a number turned into a string and a class's comdat were once chosen with +# `#ifdef WIN32` in the compiler, so a Windows-hosted tslang gave an Android or Linux object the +# MSVC CRT's `_assert`/`sprintf_s` and an `exactmatch` comdat, which ELF cannot hold. Each triple +# here is compiled to LLVM IR only (never linked or run), from any host, and checked for its own: +# +# x86_64-pc-windows-msvc _assert(msg, file, line), sprintf_s, comdat exactmatch +# x86_64-unknown-linux-gnu __assert_fail(msg, file, line, func), snprintf, comdat any +# x86_64-linux-android __assert(file, line, msg) - Bionic has no __assert_fail -, snprintf, +# comdat any + +cmake_minimum_required(VERSION 3.17.3) + +foreach(var TSLANG WORK_DIR) + if(NOT DEFINED ${var}) + message(FATAL_ERROR "${var} is required") + endif() +endforeach() + +file(REMOVE_RECURSE "${WORK_DIR}") +file(MAKE_DIRECTORY "${WORK_DIR}") + +file(WRITE "${WORK_DIR}/crt.ts" [=[ +class Foo { + x = 1; +} + +function main() { + const f = new Foo(); + const n = 3.5; + const s = "v" + n; + assert(s.length > f.x, "too short"); + print(s); +} +]=]) + +# check( ) +function(check triple assert_decl sprintf_name comdat_kind) + execute_process(COMMAND "${TSLANG}" --emit=llvm -mm=none --no-default-lib -mtriple=${triple} crt.ts -o ${triple}.ll + WORKING_DIRECTORY "${WORK_DIR}" + OUTPUT_VARIABLE out + ERROR_VARIABLE err + RESULT_VARIABLE status) + if(NOT status EQUAL 0) + message(FATAL_ERROR "--emit=llvm for ${triple}: exit ${status}\n${out}\n${err}") + endif() + + file(READ "${WORK_DIR}/${triple}.ll" ir) + if(NOT ir MATCHES "declare void @${assert_decl}") + message(FATAL_ERROR "${triple}: a failing assert does not call '${assert_decl}':\n${ir}") + endif() + if(NOT ir MATCHES "declare i32 @${sprintf_name}\\(") + message(FATAL_ERROR "${triple}: a number is not turned into a string with '${sprintf_name}':\n${ir}") + endif() + if(NOT ir MATCHES "\\$Foo\\.\\.size = comdat ${comdat_kind}") + message(FATAL_ERROR "${triple}: the class's comdat is not '${comdat_kind}':\n${ir}") + endif() + foreach(name ${ARGN}) + if(ir MATCHES "declare [^\n]*@${name}\\(") + message(FATAL_ERROR "${triple}: declares '${name}', which is another CRT's:\n${ir}") + endif() + endforeach() +endfunction() + +check(x86_64-pc-windows-msvc "_assert\\(ptr, ptr, i32\\)" sprintf_s exactmatch __assert_fail __assert snprintf) +check(x86_64-unknown-linux-gnu "__assert_fail\\(ptr, ptr, i32, ptr\\)" snprintf any _assert __assert sprintf_s) +check(x86_64-linux-android "__assert\\(ptr, i32, ptr\\)" snprintf any _assert __assert_fail sprintf_s) + +message(STATUS "assert, number-to-string and class comdats follow the target's CRT") diff --git a/tslang/tslang/opts.cpp b/tslang/tslang/opts.cpp index cf80fb928..b2c12540b 100644 --- a/tslang/tslang/opts.cpp +++ b/tslang/tslang/opts.cpp @@ -56,6 +56,7 @@ CompileOptions prepareOptions() compileOptions.lldbDebugInfo = lldbDebugInfo.getValue(); compileOptions.moduleTargetTriple = moduleTargetTriple; compileOptions.isWindows = TheTriple.isKnownWindowsMSVCEnvironment(); + compileOptions.isAndroid = TheTriple.isAndroid(); compileOptions.isWasm = TheTriple.getArch() == llvm::Triple::wasm64 || TheTriple.getArch() == llvm::Triple::wasm32; compileOptions.targetInfo = TargetInfo::fromTriple( TheTriple, llvm::Triple(llvm::sys::getDefaultTargetTriple()));