From 484c14fe771e9e471a4a1fabe6654ba08428fbfc Mon Sep 17 00:00:00 2001 From: Nexoniarz Date: Fri, 9 Oct 2026 00:34:00 +0200 Subject: [PATCH] ARM64 and macOS: AAPCS64 struct passing, aarch64/macos targets, Apple frameworks and ld messages, tests/cross.sh, CI on ARM64 Linux and macOS Co-Authored-By: Claude Opus 5.5 --- .github/workflows/ci.yml | 46 ++++++++++++++++++++++++++++++- CMakeLists.txt | 17 +++++++++++- src/abi.cpp | 54 ++++++++++++++++++++++++++++++++----- src/cimport.cpp | 14 ++++++---- src/codegen_impl.h | 1 + src/lsp.cpp | 3 +-- src/main.cpp | 32 +++++++++++++++------- src/target.cpp | 48 ++++++++++++++++++++++++++++++++- src/target.h | 9 ++++++- tests/boot.sh | 2 +- tests/cross.sh | 58 ++++++++++++++++++++++++++++++++++++++++ tests/windows.sh | 50 ---------------------------------- 12 files changed, 256 insertions(+), 78 deletions(-) create mode 100755 tests/cross.sh delete mode 100755 tests/windows.sh diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 083eb50..75c1b39 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -11,7 +11,14 @@ env: jobs: linux: - runs-on: ubuntu-24.04 + strategy: + fail-fast: false + matrix: + include: + - { os: ubuntu-24.04, arch: x64 } + - { os: ubuntu-24.04-arm, arch: arm64 } + name: linux-${{ matrix.arch }} + runs-on: ${{ matrix.os }} steps: - uses: actions/checkout@v4 - name: Install LLVM 21, libclang, tools @@ -28,11 +35,48 @@ jobs: - name: Tests run: tests/run.sh - name: Tests under valgrind + if: matrix.arch == 'x64' run: MEMCHECK=1 tests/run.sh - name: Self-hosting run: tests/boot.sh - name: Language server run: python3 tests/lsp_test.py + - name: Package + run: | + mkdir -p dist/finch && cp build/finch README.md LICENSE dist/finch/ && cp -r examples dist/finch/ + tar -czf finch-linux-${{ matrix.arch }}.tar.gz -C dist finch + - uses: actions/upload-artifact@v4 + with: + name: finch-linux-${{ matrix.arch }} + path: finch-linux-${{ matrix.arch }}.tar.gz + + macos: + runs-on: macos-15 + steps: + - uses: actions/checkout@v4 + - name: Install LLVM 21 + run: | + brew install llvm@21 ninja + echo "$(brew --prefix llvm@21)/bin" >> $GITHUB_PATH + - name: Build + run: | + cmake -S . -B build -G Ninja -DCMAKE_BUILD_TYPE=Release -DLLVM_DIR="$(brew --prefix llvm@21)/lib/cmake/llvm" + ninja -C build + build/finch version + - name: Tests + run: CC=/usr/bin/clang tests/run.sh + - name: Self-hosting + run: CC=/usr/bin/clang tests/boot.sh + - name: Language server + run: python3 tests/lsp_test.py + - name: Package + run: | + mkdir -p dist/finch && cp build/finch README.md LICENSE dist/finch/ && cp -r examples dist/finch/ + tar -czf finch-macos-arm64.tar.gz -C dist finch + - uses: actions/upload-artifact@v4 + with: + name: finch-macos-arm64 + path: finch-macos-arm64.tar.gz vscode: runs-on: ubuntu-24.04 diff --git a/CMakeLists.txt b/CMakeLists.txt index e484a11..1fab202 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -67,9 +67,24 @@ endif() # Link the one shared libLLVM when available: libclang uses it too, and two copies # of LLVM in one process crash on exit. +# Finch generates code for x86-64 and ARM64; use whichever of them this LLVM was built with. +set(FINCH_BACKENDS) +foreach(t X86 AArch64) + if(t IN_LIST LLVM_TARGETS_TO_BUILD) + list(APPEND FINCH_BACKENDS ${t}) + string(TOUPPER ${t} T) + target_compile_definitions(finch PRIVATE FINCH_TARGET_${T}) + endif() +endforeach() +message(STATUS "Finch backends: ${FINCH_BACKENDS}") + if(TARGET LLVM) set(LLVM_LIBS LLVM) else() - llvm_map_components_to_libnames(LLVM_LIBS core support passes native) + set(FINCH_COMPONENTS core support passes native) + foreach(t ${FINCH_BACKENDS}) + list(APPEND FINCH_COMPONENTS ${t}codegen ${t}asmparser ${t}desc ${t}info) + endforeach() + llvm_map_components_to_libnames(LLVM_LIBS ${FINCH_COMPONENTS}) endif() target_link_libraries(finch PRIVATE ${LLVM_LIBS} ${LIBCLANG}) diff --git a/src/abi.cpp b/src/abi.cpp index 091f2fd..7209102 100644 --- a/src/abi.cpp +++ b/src/abi.cpp @@ -1,6 +1,7 @@ -// The C calling convention for structs passed and returned by value (System V x86-64), -// the way clang does it: small structs travel in registers, split into eightbytes that are -// either INTEGER (general registers) or SSE (xmm registers); bigger ones go through memory. +// The C calling convention for structs passed and returned by value, the way clang does it: +// System V x86-64 (Linux, macOS): small structs split into eightbytes, INTEGER or SSE registers +// Microsoft x64 (Windows): 1/2/4/8 bytes in one register, everything else by address +// AAPCS64 (ARM64 Linux, Apple): float groups in vector registers, else up to 16 bytes in x registers #include "codegen_impl.h" #include "target.h" @@ -45,6 +46,39 @@ AbiArg Codegen::classify(const FType &t) { AbiArg a; if (t.kind != FType::Struct) return a; // Direct uint64_t size = dl->getTypeAllocSize(ty(t)); + if (g_target.triple.isAArch64()) { + // A homogeneous float aggregate (1-4 floats or 1-4 doubles) travels in vector registers. + std::vector ls; + leaves(*this, t, 0, ls); + bool hfa = !ls.empty() && ls.size() <= 4; + for (const Leaf &l : ls) hfa &= l.isFloat && l.isDouble == ls[0].isDouble; + if (hfa) { + llvm::Type *ft = ls[0].isDouble ? (llvm::Type *)b.getDoubleTy() : b.getFloatTy(); + a.kind = AbiArg::Expand; + a.parts.push_back(ArrayType::get(ft, ls.size())); + a.coerced = StructType::get(ctx, std::vector(ls.size(), ft)); + a.stackAlign8 = !g_target.triple.isOSDarwin(); + return a; + } + if (size > 16) { + a.kind = AbiArg::Indirect; // a copy, passed by address; returned through sret + return a; + } + // up to 16 bytes in general registers: i64 or [2 x i64]; returned as an exact-size integer + uint64_t align = dl->getABITypeAlign(ty(t)).value(); + a.kind = AbiArg::Expand; + if (align >= 16) { + a.parts.push_back(b.getInt128Ty()); + a.coerced = b.getInt128Ty(); + } else if (size <= 8) { + a.parts.push_back(b.getInt64Ty()); + a.coerced = b.getIntNTy(size * 8); + } else { + a.parts.push_back(ArrayType::get(b.getInt64Ty(), 2)); + a.coerced = a.parts[0]; + } + return a; + } if (g_target.windows) { // Microsoft x64: 1, 2, 4 or 8 bytes travel as one integer (even if they hold floats); // anything else is copied by the caller and passed by address @@ -122,7 +156,8 @@ static Plan makePlan(Codegen &cg, const FType &ret, const std::vector &pa if (a.kind == AbiArg::Expand) { int ni = 0, ns = 0; for (llvm::Type *p : a.parts) (p->isIntegerTy() ? ni : ns)++; - if (g_target.windows || (ni <= intRegs && ns <= sseRegs)) { + // only System V counts registers here; elsewhere the backend places each part + if (g_target.windows || g_target.triple.isAArch64() || (ni <= intRegs && ns <= sseRegs)) { intRegs -= ni; sseRegs -= ns; for (llvm::Type *p : a.parts) types.push_back(p); @@ -143,9 +178,15 @@ static Plan makePlan(Codegen &cg, const FType &ret, const std::vector &pa return pl; } +// Is C's plain `char` signed? Yes on x86 and Apple; no on ARM64 Linux. +static bool charIsSigned() { + return !g_target.triple.isAArch64() || g_target.triple.isOSDarwin() || g_target.triple.isOSWindows(); +} + static Attribute::AttrKind extAttr(const FType &t) { if (t.bits() >= 32 || !(t.isInt() || t.kind == FType::Bool || t.kind == FType::Char)) return Attribute::None; - return t.isSigned() || t.kind == FType::Char ? Attribute::SExt : Attribute::ZExt; + if (t.kind == FType::Char) return charIsSigned() ? Attribute::SExt : Attribute::ZExt; + return t.isSigned() ? Attribute::SExt : Attribute::ZExt; } // Attributes that tell LLVM how C passes each piece: sret, byval, signext/zeroext. @@ -169,6 +210,7 @@ static AttributeList attrsFor(Codegen &cg, const Plan &pl, const FType &ret, con } else if (a.kind == AbiArg::Indirect) { idx++; } else if (a.kind == AbiArg::Expand) { + if (a.stackAlign8) al = al.addParamAttribute(ctx, idx, Attribute::getWithStackAlignment(ctx, Align(8))); idx += a.parts.size(); } else { if (Attribute::AttrKind k = extAttr(params[i]); k != Attribute::None) al = al.addParamAttribute(ctx, idx, k); @@ -230,7 +272,7 @@ Value_ Codegen::emitCCall(const CFunc &f, Function *fn, std::vector args if (t.kind == FType::F32) x = b.CreateFPExt(x, b.getDoubleTy()); else if (t.kind == FType::Str) x = cstr(x); else if (t.kind == FType::Bool || ((t.isInt() || t.kind == FType::Char) && t.bits() < 32)) - x = b.CreateIntCast(x, b.getInt32Ty(), t.isSigned() || t.kind == FType::Char); + x = b.CreateIntCast(x, b.getInt32Ty(), t.isSigned() || (t.kind == FType::Char && charIsSigned())); ll.push_back(x); continue; } diff --git a/src/cimport.cpp b/src/cimport.cpp index 0dbd2e4..df4d263 100644 --- a/src/cimport.cpp +++ b/src/cimport.cpp @@ -12,8 +12,9 @@ namespace { -// Ask the system C compiler where it looks for headers, so we find the same stdio.h it would. -std::vector systemIncludeDirs() { +// Ask the system C compiler where it looks for headers (and, on macOS, frameworks), so we find +// the same stdio.h it would. Returns ready -isystem / -iframework arguments. +std::vector systemIncludeArgs() { std::string output; capture(g_target.cc + " -E -v -x c " + nullDevice(), output); std::vector dirs; @@ -29,8 +30,11 @@ std::vector systemIncludeDirs() { size_t z = l.find_last_not_of(" \n"); if (a == std::string::npos) continue; std::string d = l.substr(a, z - a + 1); - if (d.size() > 20 && d.compare(d.size() - 20, 20, " (framework directory)") == 0) continue; - dirs.push_back(d); + // macOS: "/…/System/Library/Frameworks (framework directory)" holds and friends + if (d.size() > 22 && d.compare(d.size() - 22, 22, " (framework directory)") == 0) + dirs.push_back("-iframework" + d.substr(0, d.size() - 22)); + else + dirs.push_back("-isystem" + d); } return dirs; } @@ -322,7 +326,7 @@ CImports importHeaders(const std::vector &imports, const std::vector args = {"-x", "c", "-std=gnu11", "--target=" + g_target.triple.str()}; if (g_target.windows) args.push_back("-D_USE_MATH_DEFINES"); // M_PI and friends, as on Linux for (const std::string &d : dirs) args.push_back("-I" + d); - for (const std::string &d : systemIncludeDirs()) args.push_back("-isystem" + d); + for (const std::string &d : systemIncludeArgs()) args.push_back(d); std::vector argv; for (const std::string &a : args) argv.push_back(a.c_str()); diff --git a/src/codegen_impl.h b/src/codegen_impl.h index 16b5f22..ea1959d 100644 --- a/src/codegen_impl.h +++ b/src/codegen_impl.h @@ -111,6 +111,7 @@ struct AbiArg { enum Kind { Direct, Expand, Memory, Indirect } kind = Direct; std::vector parts; // Expand: one value per eightbyte llvm::Type *coerced = nullptr; // Expand: { parts... } for returns + bool stackAlign8 = false; // AArch64 Linux: HFA arguments that end up on the stack }; class Codegen { diff --git a/src/lsp.cpp b/src/lsp.cpp index ae68e49..ff3a306 100644 --- a/src/lsp.cpp +++ b/src/lsp.cpp @@ -165,8 +165,7 @@ int Server::run() { _setmode(_fileno(stdin), _O_BINARY); _setmode(_fileno(stdout), _O_BINARY); #endif - InitializeNativeTarget(); - InitializeNativeTargetAsmPrinter(); + initTargets(); setTarget(""); std::string err; const Target *target = TargetRegistry::lookupTarget(g_target.triple, err); diff --git a/src/main.cpp b/src/main.cpp index 9cc36b8..d886d0d 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -65,13 +65,10 @@ static void optimize(Module &mod, TargetMachine *tm) { } static TargetMachine *targetMachine() { - InitializeNativeTarget(); - InitializeNativeTargetAsmPrinter(); - + initTargets(); const Triple &triple = g_target.triple; - if (triple.getArch() != Triple(sys::getDefaultTargetTriple()).getArch()) { - std::fprintf(stderr, "error: Finch can build for other systems on the same processor (like Windows from Linux), but not for %s yet\n", - triple.getArchName().str().c_str()); + if (!triple.isX86() && !triple.isAArch64()) { + std::fprintf(stderr, "error: Finch generates code for x86-64 and ARM64, not for %s\n", triple.getArchName().str().c_str()); std::exit(1); } std::string err; @@ -129,6 +126,9 @@ static std::string runtimeObject() { // pkg-config knows the right paths for a library if it has one; otherwise plain -l. static std::string libFlags(const std::string &lib) { std::string out; + // macOS: link "Cocoa.framework" -> -framework Cocoa + if (lib.size() > 10 && lib.compare(lib.size() - 10, 10, ".framework") == 0) + return " -framework " + shellQuote(lib.substr(0, lib.size() - 10)); if (!g_target.cross && capture("pkg-config --libs " + shellQuote(lib), out) == 0) { while (!out.empty() && std::isspace((unsigned char)out.back())) out.pop_back(); return " " + out; @@ -170,9 +170,21 @@ static std::string guessLib(const std::string &header) { std::string lib = l.substr(a, l.find('\'', a) - a); if (lib.size() > 4 && lib.compare(lib.size() - 4, 4, ".lib") == 0) lib = lib.substr(0, lib.size() - 4); addOnce(missingLibs, lib); - } else if ((a = l.find("cannot find -l")) != std::string::npos) { - a += 14; - addOnce(missingLibs, l.substr(a, l.find_first_of(": \n", a) - a)); + } else if ((a = l.find("cannot find -l")) != std::string::npos || + (a = l.find("library not found for -l")) != std::string::npos) { // older Apple ld + a = l.find("-l", a) + 2; + addOnce(missingLibs, l.substr(a, l.find_first_of(": \n\r", a) - a)); + } else if ((a = l.find("ld: library '")) != std::string::npos) { // Apple ld: library 'x' not found + a += 13; + addOnce(missingLibs, l.substr(a, l.find('\'', a) - a)); + } else if ((a = l.find("framework '")) != std::string::npos && l.find("not found") != std::string::npos) { + a += 11; + addOnce(missingLibs, l.substr(a, l.find('\'', a) - a) + ".framework"); + } else if (l.rfind(" \"", 0) == 0 && (a = l.find("\", referenced from:")) != std::string::npos) { + // Apple ld: Undefined symbols for architecture arm64:\n "_name", referenced from: + std::string name = l.substr(3, a - 3); + if (g_target.darwin && !name.empty() && name[0] == '_') name.erase(0, 1); + addOnce(missing, name); } } if (missing.empty() && missingLibs.empty()) { @@ -323,7 +335,7 @@ int main(int argc, char **argv) { if (cmd == "run") { std::string line = shellQuote(out); for (const std::string &a : programArgs) line += " " + shellQuote(a); - if (g_target.cross && g_target.windows) line = "wine " + line; // a Windows program on Linux + line = runPrefix() + line; // wine / qemu for programs built for another system #ifdef _WIN32 line = "\"" + line + "\""; // cmd /c strips one pair of outer quotes #endif diff --git a/src/target.cpp b/src/target.cpp index c89b5b1..3c533d4 100644 --- a/src/target.cpp +++ b/src/target.cpp @@ -1,5 +1,6 @@ #include "target.h" +#include #include #include @@ -21,27 +22,72 @@ static bool hostIsWindows() { #endif } +// The Finch-generated code needs these LLVM backends (the same C functions LLVM's own tools call). +extern "C" { +#ifdef FINCH_TARGET_X86 +void LLVMInitializeX86TargetInfo(); +void LLVMInitializeX86Target(); +void LLVMInitializeX86TargetMC(); +void LLVMInitializeX86AsmPrinter(); +#endif +#ifdef FINCH_TARGET_AARCH64 +void LLVMInitializeAArch64TargetInfo(); +void LLVMInitializeAArch64Target(); +void LLVMInitializeAArch64TargetMC(); +void LLVMInitializeAArch64AsmPrinter(); +#endif +} + +void initTargets() { +#ifdef FINCH_TARGET_X86 + LLVMInitializeX86TargetInfo(); + LLVMInitializeX86Target(); + LLVMInitializeX86TargetMC(); + LLVMInitializeX86AsmPrinter(); +#endif +#ifdef FINCH_TARGET_AARCH64 + LLVMInitializeAArch64TargetInfo(); + LLVMInitializeAArch64Target(); + LLVMInitializeAArch64TargetMC(); + LLVMInitializeAArch64AsmPrinter(); +#endif + llvm::InitializeNativeTarget(); + llvm::InitializeNativeTargetAsmPrinter(); +} + void setTarget(const std::string &name) { std::string host = llvm::sys::getDefaultTargetTriple(); + llvm::Triple hostTriple(llvm::Triple::normalize(host)); std::string t = name; if (name.empty()) t = host; else if (name == "windows") t = hostIsWindows() ? host : "x86_64-w64-mingw32"; else if (name == "linux") t = "x86_64-unknown-linux-gnu"; + else if (name == "arm64" || name == "aarch64" || name == "linux-arm64") t = "aarch64-unknown-linux-gnu"; + else if (name == "macos") t = hostTriple.isAArch64() ? "arm64-apple-macosx14.0.0" : "x86_64-apple-macosx14.0.0"; g_target.triple = llvm::Triple(llvm::Triple::normalize(t)); g_target.windows = g_target.triple.isOSWindows(); g_target.msvc = g_target.triple.isWindowsMSVCEnvironment(); - g_target.cross = llvm::Triple(llvm::Triple::normalize(host)).getOS() != g_target.triple.getOS(); + g_target.darwin = g_target.triple.isOSDarwin(); + g_target.cross = hostTriple.getOS() != g_target.triple.getOS() || hostTriple.getArch() != g_target.triple.getArch(); g_target.exe = g_target.windows ? ".exe" : ""; // FINCH_CC chooses the C compiler for any target; CC only when not cross-compiling. if (const char *fc = std::getenv("FINCH_CC")) g_target.cc = fc; else if (!g_target.cross && std::getenv("CC")) g_target.cc = std::getenv("CC"); else if (g_target.cross && g_target.windows) g_target.cc = "x86_64-w64-mingw32-gcc"; + else if (g_target.cross && g_target.triple.isAArch64() && g_target.triple.isOSLinux()) g_target.cc = "aarch64-linux-gnu-gcc"; else if (g_target.cross) g_target.cc = "clang --target=" + g_target.triple.str(); else if (hostIsWindows()) g_target.cc = "clang"; else g_target.cc = "cc"; } +std::string runPrefix() { + if (!g_target.cross) return ""; + if (g_target.windows && !hostIsWindows()) return "wine "; + if (g_target.triple.isAArch64() && g_target.triple.isOSLinux()) return "qemu-aarch64 "; + return ""; +} + std::string shellQuote(const std::string &s) { #ifdef _WIN32 // cmd.exe: double quotes; a " inside a file name is impossible on Windows diff --git a/src/target.h b/src/target.h index 947893c..eb18c34 100644 --- a/src/target.h +++ b/src/target.h @@ -8,6 +8,7 @@ struct TargetInfo { llvm::Triple triple; + bool darwin = false; // macOS bool windows = false; // Windows (MSVC or MinGW): Microsoft x64 calling convention, .exe bool msvc = false; // the MSVC environment (link with clang + the Windows SDK) bool cross = false; // compiling for another system than the one we run on @@ -17,9 +18,15 @@ struct TargetInfo { inline TargetInfo g_target; -// "windows" / "linux" / a full triple; "" = the system finch runs on +// "windows", "linux", "macos", "arm64" (ARM64 Linux), or a full LLVM triple; "" = the system finch runs on void setTarget(const std::string &name); +// Register the LLVM backends Finch can generate code for (x86-64, ARM64). +void initTargets(); + +// How to run a program built for g_target on this machine ("" = directly, "wine", "qemu-aarch64"). +std::string runPrefix(); + // Quote one argument for the shell that runs our commands (sh on Unix, cmd.exe on Windows). std::string shellQuote(const std::string &s); diff --git a/tests/boot.sh b/tests/boot.sh index ef4748e..4e1b969 100755 --- a/tests/boot.sh +++ b/tests/boot.sh @@ -25,7 +25,7 @@ for f in tests/run/*.fch; do # (checked in the test and in the modules it imports) files="$f" for m in $(sed -n 's/^import \([a-z_]*\)$/\1/p' "$f"); do files="$files tests/run/$m.fch"; done - if grep -qE '^import "|^link |\b(u8|u16|u32|u64|i8|i16|i32|f32|f64)\b|defer |0x' $files; then + if grep -qE '^import "|^link |defer |0x' $files || grep -qwE 'u8|u16|u32|u64|i8|i16|i32|f32|f64' $files; then skipped=$((skipped + 1)) continue fi diff --git a/tests/cross.sh b/tests/cross.sh new file mode 100755 index 0000000..da58412 --- /dev/null +++ b/tests/cross.sh @@ -0,0 +1,58 @@ +#!/usr/bin/env bash +# Builds every test for another system and runs it there through an emulator: +# tests/cross.sh windows x86_64-w64-mingw32, run with Wine +# tests/cross.sh arm64 aarch64 Linux, run with qemu-aarch64 +# e.g. nix-shell -p pkgsCross.mingwW64.buildPackages.gcc wine64 --run 'tests/cross.sh windows' +# nix-shell -p pkgsCross.aarch64-multiplatform.buildPackages.gcc qemu \ +# --run 'FINCH_CC=aarch64-unknown-linux-gnu-gcc tests/cross.sh arm64' +# (On a real Windows/ARM64/macOS machine, use tests/run.sh with that system's finch.) +set -u +cd "$(dirname "$0")" +target=${1:?usage: tests/cross.sh windows|arm64} +FINCH=${FINCH:-../build/finch} +export WINEDEBUG=-all +case $target in + windows) runner=wine; ext=.exe ;; + arm64) runner=qemu-aarch64; ext= ;; + *) echo "unknown target $target"; exit 2 ;; +esac +pass=0; failed=0 +tmp=$(mktemp -d) +trap 'rm -rf "$tmp"' EXIT + +run_exe() { # program, stdin -> output without Windows' \r + $runner "$1" < "$2" 2>&1 | tr -d '\r' +} + +for f in run/*.fch; do + grep -q "^fn main" "$f" || continue + input="${f%.fch}.in" + [ -f "$input" ] || input=/dev/null + exe="$tmp/$(basename "${f%.fch}")$ext" + if ! "$FINCH" build "$f" --target "$target" -o "$exe" > "$tmp/err" 2>&1; then + failed=$((failed + 1)); echo "FAIL (build) $f"; head -5 "$tmp/err"; continue + fi + got=$(run_exe "$exe" "$input") + if [ "$got" == "$(tr -d '\r' < "${f%.fch}.out")" ]; then + pass=$((pass + 1)) + else + failed=$((failed + 1)); echo "FAIL $f"; diff <(echo "$got") "${f%.fch}.out" | head -10 + fi +done + +for f in fail/*.fch; do + expect=$(head -1 "$f" | sed -n 's|^// expect: ||p') + exe="$tmp/$(basename "${f%.fch}")$ext" + if "$FINCH" build "$f" --target "$target" -o "$exe" > "$tmp/out" 2>&1; then + got=$(run_exe "$exe" /dev/null) + else + got=$(cat "$tmp/out") + fi + if [[ "$got" == *"$expect"* ]]; then + pass=$((pass + 1)) + else + failed=$((failed + 1)); echo "FAIL $f (wanted: $expect)"; echo "$got" | head -5 + fi +done +echo "$target ($runner): $pass passed, $failed failed" +[ $failed -eq 0 ] diff --git a/tests/windows.sh b/tests/windows.sh deleted file mode 100755 index da9b42a..0000000 --- a/tests/windows.sh +++ /dev/null @@ -1,50 +0,0 @@ -#!/usr/bin/env bash -# Cross-compiles every test for Windows (x86_64-w64-mingw32) and runs it under Wine. -# Needs x86_64-w64-mingw32-gcc and wine, e.g.: -# nix-shell -p pkgsCross.mingwW64.buildPackages.gcc wine64 --run tests/windows.sh -# (On a real Windows machine, use tests/run.sh with finch.exe instead.) -set -u -cd "$(dirname "$0")" -FINCH=${FINCH:-../build/finch} -export WINEDEBUG=-all -pass=0; failed=0 -tmp=$(mktemp -d) -trap 'rm -rf "$tmp"' EXIT - -run_exe() { # exe, stdin -> output without Windows' \r - wine "$1" < "$2" 2>&1 | tr -d '\r' -} - -for f in run/*.fch; do - grep -q "^fn main" "$f" || continue - input="${f%.fch}.in" - [ -f "$input" ] || input=/dev/null - exe="$tmp/$(basename "${f%.fch}").exe" - if ! "$FINCH" build "$f" --target windows -o "$exe" > "$tmp/err" 2>&1; then - failed=$((failed + 1)); echo "FAIL (build) $f"; head -5 "$tmp/err"; continue - fi - got=$(run_exe "$exe" "$input") - if [ "$got" == "$(cat "${f%.fch}.out")" ]; then - pass=$((pass + 1)) - else - failed=$((failed + 1)); echo "FAIL $f"; diff <(echo "$got") "${f%.fch}.out" | head -10 - fi -done - -for f in fail/*.fch; do - expect=$(head -1 "$f" | sed -n 's|^// expect: ||p') - exe="$tmp/$(basename "${f%.fch}").exe" - if got=$("$FINCH" build "$f" --target windows -o "$exe" 2>&1); then - got=$(run_exe "$exe" /dev/null); status=$? - status=$( [ -n "$got" ] && echo 1 || echo 0 ) - else - status=1 - fi - if [[ "$got" == *"$expect"* ]]; then - pass=$((pass + 1)) - else - failed=$((failed + 1)); echo "FAIL $f (wanted: $expect)"; echo "$got" | head -5 - fi -done -echo "windows (wine): $pass passed, $failed failed" -[ $failed -eq 0 ]