diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..c31f9cb --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,79 @@ +name: CI + +on: + push: + branches: [main, "ci/**"] + tags: ["v*"] + pull_request: + +env: + LLVM_VERSION: 21.1.8 + +jobs: + linux: + runs-on: ubuntu-24.04 + steps: + - uses: actions/checkout@v4 + - name: Install LLVM 21, libclang, tools + run: | + wget -q https://apt.llvm.org/llvm.sh + sudo bash llvm.sh 21 + sudo apt-get install -y llvm-21-dev libclang-21-dev ninja-build valgrind + echo "/usr/lib/llvm-21/bin" >> $GITHUB_PATH + - name: Build + run: | + cmake -S . -B build -G Ninja -DLLVM_DIR=/usr/lib/llvm-21/lib/cmake/llvm -DCMAKE_BUILD_TYPE=Release + ninja -C build + build/finch version + - name: Tests + run: tests/run.sh + - name: Tests under valgrind + run: MEMCHECK=1 tests/run.sh + - name: Self-hosting + run: tests/boot.sh + + windows: + runs-on: windows-2022 + defaults: + run: + shell: bash + steps: + - uses: actions/checkout@v4 + - uses: ilammy/msvc-dev-cmd@v1 + - name: Cache LLVM + id: cache-llvm + uses: actions/cache@v4 + with: + path: C:/llvm + key: llvm-${{ env.LLVM_VERSION }}-x86_64-pc-windows-msvc + - name: Download LLVM ${{ env.LLVM_VERSION }} + if: steps.cache-llvm.outputs.cache-hit != 'true' + run: | + curl -sSL -o llvm.tar.xz "https://github.com/llvm/llvm-project/releases/download/llvmorg-$LLVM_VERSION/clang+llvm-$LLVM_VERSION-x86_64-pc-windows-msvc.tar.xz" + mkdir -p /c/llvm + tar -xf llvm.tar.xz -C /c/llvm --strip-components=1 + rm llvm.tar.xz + - name: Build finch.exe + run: | + echo "C:/llvm/bin" >> $GITHUB_PATH + export PATH="/c/llvm/bin:$PATH" + cmake -S . -B build -G Ninja -DCMAKE_BUILD_TYPE=Release -DLLVM_DIR=C:/llvm/lib/cmake/llvm \ + -DCMAKE_MSVC_RUNTIME_LIBRARY=MultiThreaded -DCMAKE_C_COMPILER=cl -DCMAKE_CXX_COMPILER=cl + ninja -C build + cp /c/llvm/bin/libclang.dll build/ + build/finch.exe version + - name: Tests + run: | + export PATH="/c/llvm/bin:$PATH" + tests/run.sh + - name: Package + run: | + mkdir -p dist/finch + cp build/finch.exe build/libclang.dll dist/finch/ + cp README.md LICENSE dist/finch/ + cp -r examples dist/finch/ + cd dist && 7z a -tzip finch-windows-x64.zip finch > /dev/null + - uses: actions/upload-artifact@v4 + with: + name: finch-windows-x64 + path: dist/finch-windows-x64.zip diff --git a/CMakeLists.txt b/CMakeLists.txt index 4d7401f..92c408a 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -17,13 +17,24 @@ add_executable(finch src/codegen_builtins.cpp src/abi.cpp src/cimport.cpp + src/target.cpp ) # The runtime (runtime/finch_rt.c) is embedded into the compiler as a string; # finch compiles it once into ~/.cache/finch and links it into every program. set_property(DIRECTORY APPEND PROPERTY CMAKE_CONFIGURE_DEPENDS ${CMAKE_SOURCE_DIR}/runtime/finch_rt.c) +# (written as several raw string pieces: MSVC limits one literal to 16 KB) file(READ ${CMAKE_SOURCE_DIR}/runtime/finch_rt.c FINCH_RT_SOURCE) -file(WRITE ${CMAKE_BINARY_DIR}/rt_source.inc "static const char *kRuntimeSource = R\"FINCHRT(${FINCH_RT_SOURCE})FINCHRT\";\n") +string(LENGTH "${FINCH_RT_SOURCE}" FINCH_RT_LENGTH) +set(FINCH_RT_INC "static const char *kRuntimeSource =\n") +set(FINCH_RT_POS 0) +while(FINCH_RT_POS LESS FINCH_RT_LENGTH) + string(SUBSTRING "${FINCH_RT_SOURCE}" ${FINCH_RT_POS} 8000 FINCH_RT_PIECE) + string(APPEND FINCH_RT_INC "R\"FINCHRT(${FINCH_RT_PIECE})FINCHRT\"\n") + math(EXPR FINCH_RT_POS "${FINCH_RT_POS} + 8000") +endwhile() +string(APPEND FINCH_RT_INC ";\n") +file(WRITE ${CMAKE_BINARY_DIR}/rt_source.inc "${FINCH_RT_INC}") target_include_directories(finch PRIVATE ${CMAKE_BINARY_DIR}) # libclang reads C headers for `import "stdio.h"` @@ -35,7 +46,11 @@ endif() target_include_directories(finch SYSTEM PRIVATE ${LLVM_INCLUDE_DIRS} ${LIBCLANG_INCLUDE}) target_compile_definitions(finch PRIVATE ${LLVM_DEFINITIONS} FINCH_VERSION="${PROJECT_VERSION}") -target_compile_options(finch PRIVATE -Wall -Wextra) +if(MSVC) + target_compile_options(finch PRIVATE /W3 /utf-8 /EHsc /wd4244 /wd4267 /wd4146 /wd4624) +else() + target_compile_options(finch PRIVATE -Wall -Wextra) +endif() # Link the one shared libLLVM when available: libclang uses it too, and two copies # of LLVM in one process crash on exit. diff --git a/boot/gen.fch b/boot/gen.fch index d9e0980..fb54e1d 100644 --- a/boot/gen.fch +++ b/boot/gen.fch @@ -506,7 +506,18 @@ fn emitPrint(ptr[Gen] g, str v, ast.Type t, bool quoted) { printf(g, "%.*s", "i32 " + n + ", ptr " + p) } } else if k == "ptr" { - printf(g, "%p", "ptr " + v) + isNull := op(g, "icmp eq ptr " + v + ", null") + nul := newLabel(g, "ptr.null") + at := newLabel(g, "ptr.addr") + done := newLabel(g, "ptr.done") + condBr(g, isNull, nul, at) + label(g, nul) + printf(g, "null", "") + br(g, done) + label(g, at) + printf(g, "0x%llx", "i64 " + op(g, "ptrtoint ptr " + v + " to i64")) + br(g, done) + label(g, done) } else if k == "null" { printf(g, "null", "") } else { @@ -618,7 +629,7 @@ fn findFn(ptr[Gen] g, str module, str name) -> int { fn isBuiltin(str n) -> bool { names := ["print", "addr", "str", "int", "char", "float", "bool", "read_file", "write_file", "file_exists", - "shell", "exit", "input", "new", "free", "ptr"] + "shell", "exit", "input", "new", "free", "ptr", "delete_file"] return names.contains(n) } @@ -1646,6 +1657,14 @@ fn call(ptr[Gen] g, ast.Node c) -> Val { r := op(g, "call i32 @finch_write_file(ptr " + path + ", ptr " + text + ")") return Val(v: op(g, "icmp ne i32 " + r + ", 0"), ty: T("bool")) } + if n == "delete_file" { + checkArgs(g, c, 0, 1, n) + p := strArg(g, c.kids[0], "the file name") + heap := strOut(g) + emit(g, "call void @finch_str_copy(ptr " + heap + ", ptr " + spill(g, p) + ")") + r := op(g, "call i32 @finch_delete_file(ptr " + heap + ")") + return Val(v: op(g, "icmp ne i32 " + r + ", 0"), ty: T("bool")) + } if n == "shell" { checkArgs(g, c, 0, 1, n) cmd := strArg(g, c.kids[0], "the command") @@ -2122,6 +2141,7 @@ fn runtimeDecls() -> str { "declare void @finch_read_file(ptr, ptr, ptr, i64)", "declare i32 @finch_write_file(ptr, ptr)", "declare i64 @finch_shell(ptr)", + "declare i32 @finch_delete_file(ptr)", "declare i32 @printf(ptr, ...)", "declare i32 @fflush(ptr)", "declare void @exit(i32) noreturn", diff --git a/runtime/finch_rt.c b/runtime/finch_rt.c index dfbb3c2..c87b2d1 100644 --- a/runtime/finch_rt.c +++ b/runtime/finch_rt.c @@ -15,7 +15,9 @@ #include #include #include +#ifndef _WIN32 #include +#endif typedef struct { char *ptr; @@ -379,17 +381,25 @@ void finch_args(FArr *out, int argc, char **argv) { void finch_input(FStr *out, const FStr *prompt) { if (prompt->len) fwrite(prompt->ptr, 1, (size_t)prompt->len, stdout); fflush(stdout); - char *line = NULL; - size_t size = 0; - ssize_t n = getline(&line, &size, stdin); - if (n < 0) { + // read one line of any length (getline isn't available everywhere) + size_t size = 128, n = 0; + char *line = finch_alloc((int64_t)size); + int c, got = 0; + while ((c = fgetc(stdin)) != EOF) { + got = 1; + if (c == '\n') break; + if (n + 1 >= size) line = must(realloc(line, size *= 2)); + line[n++] = (char)c; + } + if (!got) { free(line); str_static(out, empty); return; } - while (n > 0 && (line[n - 1] == '\n' || line[n - 1] == '\r')) line[--n] = 0; + while (n > 0 && line[n - 1] == '\r') n--; + line[n] = 0; out->ptr = line; - out->len = n; + out->len = (int64_t)n; out->cap = (int64_t)size; } @@ -423,11 +433,20 @@ void finch_read_file(FStr *out, const FStr *path, const char *file, int64_t line int64_t finch_shell(const FStr *cmd) { fflush(stdout); int status = system(cmd->ptr ? cmd->ptr : ""); +#ifdef _WIN32 + return status; // Windows gives the exit code directly +#else if (status == -1) return -1; if (WIFEXITED(status)) return WEXITSTATUS(status); return 128 + (WIFSIGNALED(status) ? WTERMSIG(status) : 0); +#endif } +int finch_delete_file(const FStr *path) { return remove(path->ptr ? path->ptr : "") == 0; } + +// stdin/stdout/stderr are macros on some systems (Windows), so Finch asks for them here +void *finch_std_stream(int i) { return i == 0 ? (void *)stdin : i == 1 ? (void *)stdout : (void *)stderr; } + int finch_write_file(const FStr *path, const FStr *text) { FILE *f = fopen(path->ptr, "wb"); if (!f) return 0; diff --git a/src/abi.cpp b/src/abi.cpp index e44fd0f..497419e 100644 --- a/src/abi.cpp +++ b/src/abi.cpp @@ -3,6 +3,7 @@ // either INTEGER (general registers) or SSE (xmm registers); bigger ones go through memory. #include "codegen_impl.h" +#include "target.h" using namespace llvm; @@ -44,6 +45,18 @@ 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.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 + if (size == 1 || size == 2 || size == 4 || size == 8) { + a.kind = AbiArg::Expand; + a.parts.push_back(b.getIntNTy(size * 8)); + a.coerced = a.parts[0]; + } else { + a.kind = AbiArg::Indirect; + } + return a; + } if (size > 16 || size == 0) { a.kind = AbiArg::Memory; return a; @@ -96,6 +109,7 @@ static Plan makePlan(Codegen &cg, const FType &ret, const std::vector &pa llvm::Type *retTy = cg.ty(ret); if (ret.kind == FType::Str) retTy = cg.b.getPtrTy(); pl.ret = cg.classify(ret); + if (pl.ret.kind == AbiArg::Indirect) pl.ret.kind = AbiArg::Memory; // returned through a hidden pointer if (pl.ret.kind == AbiArg::Memory) { types.push_back(cg.b.getPtrTy()); // sret retTy = cg.b.getVoidTy(); @@ -108,7 +122,7 @@ 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 (ni <= intRegs && ns <= sseRegs) { + if (g_target.windows || (ni <= intRegs && ns <= sseRegs)) { intRegs -= ni; sseRegs -= ns; for (llvm::Type *p : a.parts) types.push_back(p); @@ -116,7 +130,7 @@ static Plan makePlan(Codegen &cg, const FType &ret, const std::vector &pa a.kind = AbiArg::Memory; // out of registers: the whole struct goes on the stack } } - if (a.kind == AbiArg::Memory) types.push_back(cg.b.getPtrTy()); + if (a.kind == AbiArg::Memory || a.kind == AbiArg::Indirect) types.push_back(cg.b.getPtrTy()); if (a.kind == AbiArg::Direct) { if (t.isFloat()) sseRegs--; else intRegs--; @@ -152,6 +166,8 @@ static AttributeList attrsFor(Codegen &cg, const Plan &pl, const FType &ret, con al = al.addParamAttribute(ctx, idx, Attribute::getWithByValType(ctx, cg.ty(params[i]))); al = al.addParamAttribute(ctx, idx, Attribute::getWithAlignment(ctx, Align(align))); idx++; + } else if (a.kind == AbiArg::Indirect) { + idx++; } else if (a.kind == AbiArg::Expand) { idx += a.parts.size(); } else { @@ -221,8 +237,8 @@ Value_ Codegen::emitCCall(const CFunc &f, Function *fn, std::vector args const AbiArg &a = pl.params[i]; if (ps[i].kind == FType::Str) { ll.push_back(cstr(v.v)); - } else if (a.kind == AbiArg::Memory) { - Value *s = tmpOf(ty(ps[i])); + } else if (a.kind == AbiArg::Memory || a.kind == AbiArg::Indirect) { + Value *s = tmpOf(ty(ps[i])); // a copy the callee may change cast(s)->setAlignment(Align(16)); b.CreateStore(v.v, s); ll.push_back(s); @@ -278,7 +294,7 @@ Function *Codegen::cThunk(Fn &fn, Pos p) { std::vector args; for (size_t i = 0; i < fn.params.size(); i++) { const AbiArg &a = pl.params[i]; - if (a.kind == AbiArg::Memory) { + if (a.kind == AbiArg::Memory || a.kind == AbiArg::Indirect) { args.push_back(b.CreateLoad(ty(fn.params[i]), th->getArg(ai++))); } else if (a.kind == AbiArg::Expand) { Value *s = abiSlot(*this, fn.params[i], a); diff --git a/src/cimport.cpp b/src/cimport.cpp index 6df3a0f..0dbd2e4 100644 --- a/src/cimport.cpp +++ b/src/cimport.cpp @@ -1,5 +1,6 @@ #include "cimport.h" #include "error.h" +#include "target.h" #include @@ -7,20 +8,20 @@ #include #include #include +#include namespace { // Ask the system C compiler where it looks for headers, so we find the same stdio.h it would. std::vector systemIncludeDirs() { - const char *cc = std::getenv("CC"); - std::string cmd = std::string(cc ? cc : "cc") + " -E -v -x c /dev/null 2>&1"; + std::string output; + capture(g_target.cc + " -E -v -x c " + nullDevice(), output); std::vector dirs; - FILE *p = popen(cmd.c_str(), "r"); - if (!p) return dirs; - char line[4096]; + std::stringstream lines(output); bool inList = false; - while (std::fgets(line, sizeof line, p)) { - std::string l = line; + for (std::string l; std::getline(lines, l);) { + if (!l.empty() && l.back() == '\r') l.pop_back(); + l += "\n"; if (l.rfind("#include <...> search starts here:", 0) == 0) { inList = true; continue; } if (l.rfind("End of search list.", 0) == 0) break; if (!inList) continue; @@ -31,7 +32,6 @@ std::vector systemIncludeDirs() { if (d.size() > 20 && d.compare(d.size() - 20, 20, " (framework directory)") == 0) continue; dirs.push_back(d); } - pclose(p); return dirs; } @@ -318,7 +318,9 @@ CImports importHeaders(const std::vector &imports, const std::vector args = {"-x", "c", "-std=gnu11"}; + // parse for the target system: sizes (long is 32 bits on Windows) and headers differ + 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); std::vector argv; diff --git a/src/codegen.cpp b/src/codegen.cpp index 373cc12..418cfda 100644 --- a/src/codegen.cpp +++ b/src/codegen.cpp @@ -25,7 +25,10 @@ Codegen::Codegen(LLVMContext &c, const std::vector &ps, const CImports } diUnit = di->createCompileUnit(dwarf::DW_LANG_C, diFiles[0], "finch " FINCH_VERSION, false, "", 0); mod->addModuleFlag(Module::Warning, "Debug Info Version", DEBUG_METADATA_VERSION); - mod->addModuleFlag(Module::Warning, "Dwarf Version", 5); + if (tm.getTargetTriple().isWindowsMSVCEnvironment()) + mod->addModuleFlag(Module::Warning, "CodeView", 1); // what Visual Studio and WinDbg read + else + mod->addModuleFlag(Module::Warning, "Dwarf Version", 5); } } @@ -657,6 +660,12 @@ Value_ Codegen::expr(const Expr &e) { } if (modules[curModule].imports.count(v.name)) failAt(e.pos.file, e.pos.line, e.pos.col, "'" + v.name + "' is a module; use something from it, like " + v.name + ".name(...)"); + // C's standard streams are macros on some systems; the runtime knows them everywhere + if (!cimports.globals.count(v.name) && (v.name == "stdin" || v.name == "stdout" || v.name == "stderr") && + cimports.fns.count("fprintf")) { + int which = v.name == "stdin" ? 0 : v.name == "stdout" ? 1 : 2; + return {b.CreateCall(rt("finch_std_stream"), {b.getInt32(which)}), FType::Ptr}; + } } Place pl = place(e); return {b.CreateLoad(ty(pl.type), pl.addr, v.name), pl.type}; diff --git a/src/codegen_builtins.cpp b/src/codegen_builtins.cpp index 09b4e09..b6c8c25 100644 --- a/src/codegen_builtins.cpp +++ b/src/codegen_builtins.cpp @@ -8,7 +8,7 @@ namespace finch { bool Codegen::isBuiltin(const std::string &n) { static const std::set names = {"print", "addr", "input", "new", "free", "read_file", "write_file", - "file_exists", "exit", "shell"}; + "file_exists", "delete_file", "exit", "shell"}; FType t; return names.count(n) || typeFromName(n, t); } @@ -92,6 +92,8 @@ Function *Codegen::rt(const std::string &name) { {"finch_read_file", {V, {P, P, P, I}}}, {"finch_write_file", {I32, {P, P}}}, {"finch_shell", {I, {P}}}, + {"finch_delete_file", {I32, {P}}}, + {"finch_std_stream", {P, {I32}}}, }; auto it = sigs.find(name); if (it == sigs.end()) { @@ -216,6 +218,15 @@ Value_ Codegen::call(const CallExpr &c) { b.CreateCall(libc("exit", b.getVoidTy(), {b.getInt32Ty()}), {code}); return {nullptr, FType::Void}; } + if (n == "delete_file") { + Value_ path = coerce(expr(one("path")), FType::Str, c.args[0]->pos, "the file name"); + Value *ss = tmpOf(ty(FType::Str)); + b.CreateCall(rt("finch_str_copy"), {ss, tmp(path.v)}); // NUL-terminated copy + Value *r = b.CreateCall(rt("finch_delete_file"), {ss}); + dropAt(ss, FType::Str); + release(path); + return {b.CreateICmpNE(r, b.getInt32(0)), FType::Bool}; + } if (n == "read_file" || n == "file_exists") { Value_ path = coerce(expr(one("path")), FType::Str, c.args[0]->pos, "the file name"); Value *pp = tmp(path.v); diff --git a/src/codegen_impl.h b/src/codegen_impl.h index 58a7f91..ebe3bf0 100644 --- a/src/codegen_impl.h +++ b/src/codegen_impl.h @@ -101,9 +101,11 @@ struct ModuleScope { std::set imports; // Finch modules visible here }; -// How a C struct travels through registers (System V x86-64). +// How a C struct travels: System V x86-64 (Linux) or Microsoft x64 (Windows). struct AbiArg { - enum Kind { Direct, Expand, Memory } kind = Direct; + // Direct: as is. Expand: split into register-sized parts. Memory: on the stack (byval), or + // returned through a hidden pointer (sret). Indirect (Windows): a pointer to a copy the caller made. + enum Kind { Direct, Expand, Memory, Indirect } kind = Direct; std::vector parts; // Expand: one value per eightbyte llvm::Type *coerced = nullptr; // Expand: { parts... } for returns }; diff --git a/src/codegen_types.cpp b/src/codegen_types.cpp index dd57a0a..5de7e4e 100644 --- a/src/codegen_types.cpp +++ b/src/codegen_types.cpp @@ -387,7 +387,18 @@ void Codegen::emitPrint(Value *v, const FType &t, bool quoted) { else if (t.kind == FType::Bool) printf_("%s", {b.CreateSelect(v, b.CreateGlobalString("true", "true"), b.CreateGlobalString("false", "false"))}); else if (t.kind == FType::Str) printf_(quoted ? "\"%.*s\"" : "%.*s", {b.CreateTrunc(b.CreateExtractValue(v, 1), b.getInt32Ty()), cstr(v)}); - else if (t.isPtr()) printf_("%p", {v}); + else if (t.isPtr()) { // the same on every system: null or 0x... + Value *isNull = b.CreateIsNull(v); + BasicBlock *nul = newBlock("ptr.null"), *addr = newBlock("ptr.addr"), *done = newBlock("ptr.done"); + b.CreateCondBr(isNull, nul, addr); + b.SetInsertPoint(nul); + printf_("null", {}); + b.CreateBr(done); + b.SetInsertPoint(addr); + printf_("0x%llx", {b.CreatePtrToInt(v, b.getInt64Ty())}); + b.CreateBr(done); + b.SetInsertPoint(done); + } else if (t.kind == FType::Null) printf_("null", {}); else b.CreateCall(printFn(t), {tmp(v)}); } diff --git a/src/main.cpp b/src/main.cpp index 7dfce5c..72e976b 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -3,6 +3,7 @@ #include "error.h" #include "lexer.h" #include "parser.h" +#include "target.h" #include #include @@ -23,7 +24,6 @@ #include #include #include -#include #include #include "rt_source.inc" // kRuntimeSource: runtime/finch_rt.c, embedded at build time @@ -39,9 +39,10 @@ static void usage() { " finch ir show the generated LLVM IR\n" " finch version show the version\n" "\n" - " -l link a C library, same as link \"lib\" in the file\n" - " -g add debug info (for gdb / lldb)\n" - " -O0 skip optimizations (to read the raw IR)\n"); + " -l link a C library, same as link \"lib\" in the file\n" + " --target build for another system: windows, linux, or an LLVM triple\n" + " -g add debug info (gdb / lldb / Visual Studio)\n" + " -O0 skip optimizations (to read the raw IR)\n"); std::exit(1); } @@ -54,11 +55,6 @@ static bool readFile(const std::string &path, std::string &out) { return true; } -static std::string dirOf(const std::string &path) { - size_t slash = path.find_last_of('/'); - return slash == std::string::npos ? "." : path.substr(0, slash); -} - // ---------- loading the program and its modules ---------- struct Loader { @@ -95,7 +91,7 @@ struct Loader { if (sys::fs::exists(here)) return here; if (const char *fp = std::getenv("FINCH_PATH")) { std::stringstream ss(fp); - for (std::string d; std::getline(ss, d, ':');) + for (std::string d; std::getline(ss, d, pathListSeparator());) if (!d.empty() && sys::fs::exists(d + "/" + name + ".fch")) return d + "/" + name + ".fch"; } return here; @@ -118,11 +114,16 @@ static void optimize(Module &mod, TargetMachine *tm) { pb.buildPerModuleDefaultPipeline(OptimizationLevel::O2).run(mod, mam); } -static TargetMachine *hostMachine() { +static TargetMachine *targetMachine() { InitializeNativeTarget(); InitializeNativeTargetAsmPrinter(); - Triple triple(sys::getDefaultTargetTriple()); + 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()); + std::exit(1); + } std::string err; const Target *target = TargetRegistry::lookupTarget(triple, err); if (!target) { @@ -149,30 +150,11 @@ static void emitObject(Module &mod, TargetMachine *tm, const std::string &path) // ---------- linking ---------- -// Run a shell command; returns its exit status, everything it printed goes to `output`. -static int capture(const std::string &cmd, std::string &output) { - FILE *p = popen((cmd + " 2>&1").c_str(), "r"); - if (!p) return -1; - char buf[4096]; - size_t n; - while ((n = std::fread(buf, 1, sizeof buf, p)) > 0) output.append(buf, n); - int status = pclose(p); - return WIFEXITED(status) ? WEXITSTATUS(status) : -1; -} - -static std::string ccName() { - const char *cc = std::getenv("CC"); - return cc ? cc : "cc"; -} - // The runtime is compiled once per version and kept in ~/.cache/finch. static std::string runtimeObject() { - std::string dir; - if (const char *x = std::getenv("XDG_CACHE_HOME")) dir = std::string(x) + "/finch"; - else if (const char *h = std::getenv("HOME")) dir = std::string(h) + "/.cache/finch"; - else dir = "/tmp/finch-cache"; - size_t hash = std::hash()(std::string(kRuntimeSource) + FINCH_VERSION + ccName()); - std::string obj = dir + "/rt-" + std::to_string(hash) + ".o"; + std::string dir = cacheDir(); + size_t hash = std::hash()(std::string(kRuntimeSource) + FINCH_VERSION + g_target.cc + g_target.triple.str()); + std::string obj = dir + (g_target.windows && !g_target.cross ? "\\" : "/") + "rt-" + std::to_string(hash) + ".o"; if (sys::fs::exists(obj)) return obj; sys::fs::create_directories(dir); @@ -182,10 +164,12 @@ static std::string runtimeObject() { out << kRuntimeSource; } std::string output; - int status = capture(ccName() + " -O2 -fPIC -c '" + src + "' -o '" + part + "'", output); + std::string pic = g_target.windows ? "" : " -fPIC"; + int status = capture(g_target.cc + " -O2" + pic + " -c " + shellQuote(src) + " -o " + shellQuote(part), output); std::remove(src.c_str()); if (status != 0) { - std::fprintf(stderr, "%serror: couldn't build the Finch runtime (is a C compiler installed?)\n", output.c_str()); + std::fprintf(stderr, "%serror: couldn't build the Finch runtime with '%s' (is a C compiler installed?)\n", output.c_str(), + g_target.cc.c_str()); std::exit(1); } sys::fs::rename(part, obj); @@ -195,11 +179,11 @@ 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; - if (capture("pkg-config --libs '" + lib + "'", out) == 0) { + 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; } - return " '-l" + lib + "'"; + return " " + shellQuote("-l" + lib); } // "GLFW/glfw3.h" -> "glfw": a decent first guess for the library's name. @@ -226,6 +210,15 @@ static std::string guessLib(const std::string &header) { } else if ((a = l.find("undefined symbol: ")) != std::string::npos) { a += 18; addOnce(missing, l.substr(a, l.find_first_of(" \n", a) - a)); + } else if ((a = l.find("unresolved external symbol ")) != std::string::npos) { // MSVC link.exe + a += 27; + addOnce(missing, l.substr(a, l.find_first_of(" \n\r", a) - a)); + } else if ((a = l.find("cannot open file '")) != std::string::npos || // MSVC: LNK1104 cannot open file 'x.lib' + (a = l.find("could not open '")) != std::string::npos) { // lld-link + a = l.find('\'', a) + 1; + 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)); @@ -272,19 +265,21 @@ static bool endsWith(const std::string &s, const char *x) { static void link(const std::string &obj, const std::string &exe, const std::vector &libs, const CImports &c) { - std::string cmd = ccName() + " '" + obj + "' '" + runtimeObject() + "' -o '" + exe + "' -lm"; + std::string cmd = g_target.cc + " " + shellQuote(obj) + " " + shellQuote(runtimeObject()) + " -o " + shellQuote(exe); + if (!g_target.msvc) cmd += " -lm"; std::vector temps; for (const std::string &l : libs) { if (endsWith(l, ".c")) { // compile the user's C file std::string o = obj + "." + std::to_string(temps.size()) + ".o", output; - if (capture(ccName() + " -O2 -fPIC -c '" + l + "' -o '" + o + "'", output) != 0) { + std::string pic = g_target.windows ? "" : " -fPIC"; + if (capture(g_target.cc + " -O2" + pic + " -c " + shellQuote(l) + " -o " + shellQuote(o), output) != 0) { std::fprintf(stderr, "%serror: couldn't compile %s\n", output.c_str(), l.c_str()); std::exit(1); } temps.push_back(o); - cmd += " '" + o + "'"; + cmd += " " + shellQuote(o); } else if (endsWith(l, ".o") || endsWith(l, ".a")) { - cmd += " '" + l + "'"; + cmd += " " + shellQuote(l); } else { cmd += libFlags(l); } @@ -294,7 +289,7 @@ static void link(const std::string &obj, const std::string &exe, const std::vect std::remove(obj.c_str()); for (const std::string &t : temps) std::remove(t.c_str()); if (status == 127) { - std::fprintf(stderr, "error: no C compiler found to link with (install gcc or clang, or set CC)\n"); + std::fprintf(stderr, "error: no C compiler found to link with ('%s'; install one, or set FINCH_CC)\n", g_target.cc.c_str()); std::exit(1); } if (status != 0) linkFailed(output, c); @@ -302,18 +297,12 @@ static void link(const std::string &obj, const std::string &exe, const std::vect } static std::string stem(const std::string &path) { - size_t slash = path.find_last_of('/'); + size_t slash = path.find_last_of("/\\"); std::string name = slash == std::string::npos ? path : path.substr(slash + 1); size_t dot = name.rfind('.'); return dot == std::string::npos ? name : name.substr(0, dot); } -static std::string shellQuote(const std::string &s) { - std::string out = "'"; - for (char ch : s) out += ch == '\'' ? std::string("'\\''") : std::string(1, ch); - return out + "'"; -} - int main(int argc, char **argv) { if (argc == 2 && (std::string(argv[1]) == "version" || std::string(argv[1]) == "--version")) { std::printf("finch %s (LLVM %s)\n", FINCH_VERSION, LLVM_VERSION_STRING); @@ -324,11 +313,13 @@ int main(int argc, char **argv) { if (cmd != "run" && cmd != "build" && cmd != "ir") usage(); std::string out = stem(file); std::vector libs, programArgs; - bool optimizeCode = true, debug = false; + std::string targetName; + bool optimizeCode = true, debug = false, outGiven = false; for (int i = 3; i < argc; i++) { std::string a = argv[i]; if (cmd == "run" && !programArgs.empty()) programArgs.push_back(a); - else if (a == "-o" && i + 1 < argc && cmd == "build") out = argv[++i]; + else if (a == "-o" && i + 1 < argc && cmd == "build") out = argv[++i], outGiven = true; + else if (a == "--target" && i + 1 < argc) targetName = argv[++i]; else if (a == "-O0") optimizeCode = false; else if (a == "-g") debug = true; else if (a == "-l" && i + 1 < argc) libs.push_back(argv[++i]); @@ -337,6 +328,9 @@ int main(int argc, char **argv) { else usage(); } + setTarget(targetName); + if (!outGiven) out += g_target.exe; + Loader loader; loader.load(file, "", Pos{}); std::vector allImports; @@ -345,7 +339,7 @@ int main(int argc, char **argv) { allImports.insert(allImports.end(), p.imports.begin(), p.imports.end()); for (const Link &l : p.links) { bool file = endsWith(l.lib, ".c") || endsWith(l.lib, ".o") || endsWith(l.lib, ".a"); - std::string path = file && l.lib[0] != '/' ? dirOf(p.path) + "/" + l.lib : l.lib; + std::string path = file && !sys::path::is_absolute(l.lib) ? dirOf(p.path) + "/" + l.lib : l.lib; if (file && !sys::fs::exists(path)) failAt(l.pos.file, l.pos.line, l.pos.col, "can't find the file '" + path + "'"); libs.push_back(path); } @@ -353,7 +347,7 @@ int main(int argc, char **argv) { } CImports c = importHeaders(allImports, dirs); LLVMContext ctx; - TargetMachine *tm = hostMachine(); + TargetMachine *tm = targetMachine(); std::unique_ptr mod = generate(loader.progs, c, ctx, *tm, debug); if (optimizeCode) optimize(*mod, tm); @@ -365,7 +359,7 @@ int main(int argc, char **argv) { std::string obj = out + ".o"; if (cmd == "run") { SmallString<128> tmp; - sys::fs::createTemporaryFile("finch", "", tmp); + sys::fs::createTemporaryFile("finch", g_target.windows ? "exe" : "", tmp); out = tmp.str().str(); obj = out + ".o"; } @@ -375,13 +369,15 @@ int main(int argc, char **argv) { if (cmd == "run") { std::string line = shellQuote(out); for (const std::string &a : programArgs) line += " " + shellQuote(a); - int status = std::system(line.c_str()); + if (g_target.cross && g_target.windows) line = "wine " + line; // a Windows program on Linux +#ifdef _WIN32 + line = "\"" + line + "\""; // cmd /c strips one pair of outer quotes +#endif + std::string crash; + int status = runCommand(line, &crash); std::remove(out.c_str()); - if (WIFSIGNALED(status)) { - std::fprintf(stderr, "\nthe program crashed: %s\n", strsignal(WTERMSIG(status))); - return 128 + WTERMSIG(status); - } - return WIFEXITED(status) ? WEXITSTATUS(status) : 1; + if (!crash.empty()) std::fprintf(stderr, "\nthe program crashed: %s\n", crash.c_str()); + return status; } return 0; } diff --git a/src/target.cpp b/src/target.cpp new file mode 100644 index 0000000..c89b5b1 --- /dev/null +++ b/src/target.cpp @@ -0,0 +1,112 @@ +#include "target.h" + +#include + +#include +#include +#include + +#ifdef _WIN32 +#define popen _popen +#define pclose _pclose +#else +#include +#endif + +static bool hostIsWindows() { +#ifdef _WIN32 + return true; +#else + return false; +#endif +} + +void setTarget(const std::string &name) { + std::string host = llvm::sys::getDefaultTargetTriple(); + 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"; + 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.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.cc = "clang --target=" + g_target.triple.str(); + else if (hostIsWindows()) g_target.cc = "clang"; + else g_target.cc = "cc"; +} + +std::string shellQuote(const std::string &s) { +#ifdef _WIN32 + // cmd.exe: double quotes; a " inside a file name is impossible on Windows + return "\"" + s + "\""; +#else + std::string out = "'"; + for (char ch : s) out += ch == '\'' ? std::string("'\\''") : std::string(1, ch); + return out + "'"; +#endif +} + +static int exitStatus(int status) { +#ifdef _WIN32 + return status; +#else + if (status == -1) return -1; + if (WIFEXITED(status)) return WEXITSTATUS(status); + if (WIFSIGNALED(status)) return 128 + WTERMSIG(status); + return -1; +#endif +} + +int capture(const std::string &cmd, std::string &output) { + FILE *p = popen((cmd + " 2>&1").c_str(), "r"); + if (!p) return -1; + char buf[4096]; + size_t n; + while ((n = std::fread(buf, 1, sizeof buf, p)) > 0) output.append(buf, n); + return exitStatus(pclose(p)); +} + +int runCommand(const std::string &cmd, std::string *crash) { + std::fflush(stdout); + int status = std::system(cmd.c_str()); +#ifndef _WIN32 + if (crash && status != -1 && WIFSIGNALED(status)) *crash = strsignal(WTERMSIG(status)); +#else + // a crashed Windows program exits with an NTSTATUS code like 0xC0000005 + if (crash && (unsigned)status >= 0xC0000000u) { + char buf[64]; + std::snprintf(buf, sizeof buf, "exception 0x%08X%s", (unsigned)status, + (unsigned)status == 0xC0000005u ? " (access violation)" : (unsigned)status == 0xC00000FDu ? " (stack overflow)" : ""); + *crash = buf; + } +#endif + return exitStatus(status); +} + +const char *nullDevice() { return hostIsWindows() ? "NUL" : "/dev/null"; } + +std::string cacheDir() { +#ifdef _WIN32 + if (const char *l = std::getenv("LOCALAPPDATA")) return std::string(l) + "\\finch"; + if (const char *t = std::getenv("TEMP")) return std::string(t) + "\\finch"; + return "finch-cache"; +#else + if (const char *x = std::getenv("XDG_CACHE_HOME")) return std::string(x) + "/finch"; + if (const char *h = std::getenv("HOME")) return std::string(h) + "/.cache/finch"; + return "/tmp/finch-cache"; +#endif +} + +char pathListSeparator() { return hostIsWindows() ? ';' : ':'; } + +std::string dirOf(const std::string &path) { + size_t slash = hostIsWindows() ? path.find_last_of("/\\") : path.find_last_of('/'); + return slash == std::string::npos ? "." : path.substr(0, slash); +} diff --git a/src/target.h b/src/target.h new file mode 100644 index 0000000..947893c --- /dev/null +++ b/src/target.h @@ -0,0 +1,42 @@ +#pragma once +// The system Finch compiles for, and everything that depends on it: +// the C compiler used to link, how the shell quotes, file names, the C ABI. + +#include + +#include + +struct TargetInfo { + llvm::Triple triple; + 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 + std::string cc; // the C compiler that links programs and builds the runtime + std::string exe; // ".exe" or "" +}; + +inline TargetInfo g_target; + +// "windows" / "linux" / a full triple; "" = the system finch runs on +void setTarget(const std::string &name); + +// Quote one argument for the shell that runs our commands (sh on Unix, cmd.exe on Windows). +std::string shellQuote(const std::string &s); + +// Run a command; its exit status, and everything it printed in `output`. +int capture(const std::string &cmd, std::string &output); + +// Run a command with its output going to the terminal; its exit status (128+n for a signal). +int runCommand(const std::string &cmd, std::string *crash = nullptr); + +// The null device: /dev/null or NUL. +const char *nullDevice(); + +// The folder for cached files (~/.cache/finch, %LOCALAPPDATA%\finch). +std::string cacheDir(); + +// Separator of FINCH_PATH entries (':' or ';'). +char pathListSeparator(); + +// The folder part of a path (both / and \ on Windows). +std::string dirOf(const std::string &path); diff --git a/tests/run.sh b/tests/run.sh index 937e763..71b7a8e 100755 --- a/tests/run.sh +++ b/tests/run.sh @@ -17,7 +17,7 @@ for f in run/*.fch; do want="${f%.fch}.out" input="${f%.fch}.in" [ -f "$input" ] || input=/dev/null - got=$("$FINCH" run "$f" < "$input" 2>&1) + got=$("$FINCH" run "$f" < "$input" 2>&1 | tr -d '\r') # Windows ends lines with \r\n if [ "$got" != "$(cat "$want")" ]; then failed=$((failed + 1)) echo "FAIL $f" @@ -41,6 +41,7 @@ for f in fail/*.fch; do expect=$(head -1 "$f" | sed -n 's|^// expect: ||p') got=$("$FINCH" run "$f" < /dev/null 2>&1) status=$? + got=$(echo "$got" | tr -d '\r') if [ $status -ne 0 ] && [[ "$got" == *"$expect"* ]]; then pass=$((pass + 1)) else diff --git a/tests/run/files.fch b/tests/run/files.fch index 1f7ae5e..7818853 100644 --- a/tests/run/files.fch +++ b/tests/run/files.fch @@ -1,9 +1,10 @@ fn main() { - path := "/tmp/finch_test_file.txt" + path := "finch_test_file.txt" ok := write_file(path, "line one\nline two\n") print(ok, file_exists(path)) text := read_file(path) lines := text.trim().split("\n") print(lines, lines.len) - print(file_exists("/definitely/not/here")) + print(delete_file(path), file_exists(path)) + print(file_exists("definitely/not/here")) } diff --git a/tests/run/files.out b/tests/run/files.out index f9fc6b1..9767fce 100644 --- a/tests/run/files.out +++ b/tests/run/files.out @@ -1,3 +1,4 @@ true true ["line one", "line two"] 2 +true false false diff --git a/tests/run/pointers.out b/tests/run/pointers.out index f51c9b2..127cadc 100644 --- a/tests/run/pointers.out +++ b/tests/run/pointers.out @@ -1,4 +1,4 @@ 42 2 1 99 -true true (nil) +true true null diff --git a/tests/windows.sh b/tests/windows.sh new file mode 100755 index 0000000..da9b42a --- /dev/null +++ b/tests/windows.sh @@ -0,0 +1,50 @@ +#!/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 ]