From 750eca3e85d2fff392d0c6f33d1e7a92ca6e8ac0 Mon Sep 17 00:00:00 2001 From: Nexoniarz Date: Sun, 25 Oct 2026 21:33:19 +0100 Subject: [PATCH] --target: windows, linux, arm64, macos or a triple, in the compiler written in Finch Targets, the target's C compiler (FINCH_CC) for linking, C import for the target (--target to libclang, its headers), the AAPCS64 and Microsoft x64 ways of passing structs by value (and the System V one), dllexport for libraries on Windows, .exe names, and running the program through wine or qemu-aarch64. std/ gets time_windows.fch and proc_windows.fch: a module has a version per system (name_windows.fch, name_macos.fch, name_linux.fch) that wins over name.fch. stdin, stdout and stderr work where C makes them macros. Run for real: all 27 programs under tests/run and tests/new pass on ARM64 Linux under qemu and on Windows under Wine (MinGW), structs by value and callbacks included. macOS is only checked as far as clang accepts the IR; there is no emulator for it here. --- boot/cimport.fch | 15 ++-- boot/gen.fch | 136 +++++++++++++++++++++++++++++++----- boot/main.fch | 96 +++++++++++++++++++------ boot/platform.fch | 90 ++++++++++++++++++++++++ std/finch_win.h | 6 ++ std/proc_windows.fch | 45 ++++++++++++ std/time_windows.fch | 75 ++++++++++++++++++++ tests/cross.sh | 6 +- tests/new/std_time_proc.fch | 6 +- tests/new/std_time_proc.out | 3 +- 10 files changed, 426 insertions(+), 52 deletions(-) create mode 100644 boot/platform.fch create mode 100644 std/finch_win.h create mode 100644 std/proc_windows.fch create mode 100644 std/time_windows.fch diff --git a/boot/cimport.fch b/boot/cimport.fch index 5699f78..378c66a 100644 --- a/boot/cimport.fch +++ b/boot/cimport.fch @@ -99,10 +99,10 @@ fn fixed(ast.Type elem, int count) -> ast.Type { // ---------- the system's header folders ---------- // Ask the system C compiler where it looks for headers, so we find the same stdio.h it would. -fn systemIncludeArgs() -> []str { +fn systemIncludeArgs(str cc) -> []str { []str dirs tmp := "/tmp/finch_cc_dirs.txt" - shell("cc -E -v -x c /dev/null 2> '" + tmp + "' > /dev/null") + shell(cc + " -E -v -x c /dev/null 2> '" + tmp + "' > /dev/null") if !file_exists(tmp) { return dirs } @@ -504,16 +504,23 @@ fn visit(ptr[Importer] imp, CXCursor c) { } // Read every header in `headers`; `dirs` are searched first for "local.h" headers. -fn importHeaders([]Header headers, []str dirs) -> CImports! { +// `target` is the LLVM triple the program is built for ("" = this system): sizes and headers depend on it. +fn importHeaders([]Header headers, []str dirs, str target, bool windows, str ccFor) -> CImports! { imp := Importer() if headers.len == 0 { return imp.out } []str args = ["-x", "c", "-std=gnu11"] + if target.len > 0 { + args.push("--target=" + target) + } + if windows { + args.push("-D_USE_MATH_DEFINES") // M_PI and friends, as on Linux + } for d in dirs { args.push("-I" + d) } - for d in systemIncludeArgs() { + for d in systemIncludeArgs(ccFor) { args.push(d) } []ptr[char] argv diff --git a/boot/gen.fch b/boot/gen.fch index 87f6f15..c694f03 100644 --- a/boot/gen.fch +++ b/boot/gen.fch @@ -5,6 +5,7 @@ // (so programs behave the same) but never frees them: it is the bootstrap compiler. import ast import cimport +import platform struct Var { str name @@ -21,6 +22,7 @@ struct Options { bool shared // finch build --shared bool debug // -g str cwd // the folder the compiler was started in (for the debug info) + platform.Target target } // A generic struct or function, kept as written until something asks for a version of it. @@ -146,6 +148,7 @@ struct Gen { cimport.CImports ci bool shared // building a library (finch build --shared): no main, its export fns are the way in bool debug // -g: debug info (DWARF, as LLVM metadata) + platform.Target tg // the system the program is for str cwd []str md // the metadata, numbered by position: !0 = md[0] map[str]int mdcache // metadata made already: types by key, locations by line:column @@ -3164,6 +3167,16 @@ fn ref(ptr[Gen] g, ast.Node e, bool write) -> Ref { } return Ref(isPlace: false, val: Val(v: str(c.i), ty: T("int"), lit: true)) } + // C's standard streams are macros on some systems; the runtime knows them everywhere + if id < 0 && !g.ci.globals.has(e.name) && g.ci.fns.has("fprintf") && (e.name == "stdin" || e.name == "stdout" || e.name == "stderr") { + which := "2" + if e.name == "stdin" { + which = "0" + } else if e.name == "stdout" { + which = "1" + } + return Ref(isPlace: false, val: Val(v: op(g, "call ptr @finch_std_stream(i32 " + which + ")"), ty: T("ptr"))) + } if id < 0 && g.ci.globals.has(e.name) { gt := resolve(g, g.ci.globals[e.name], e) if !g.cdone.contains("g:" + e.name) { @@ -3589,6 +3602,49 @@ fn leavesOf(ptr[Gen] g, ast.Type t, int base) -> []Leaf { return out } +// AAPCS64 (ARM64 Linux, Apple): a group of one to four floats or doubles travels in vector registers; +// anything else up to 16 bytes in general registers; bigger is copied by the caller and passed by address +fn classifyArm64(ptr[Gen] g, ast.Type t, Abi first, int size, int align) -> Abi { + a := first + ls := leavesOf(g, t, 0) + hfa := ls.len > 0 && ls.len <= 4 + for l in ls { + if !l.isFloat || l.isDouble != ls[0].isDouble { + hfa = false + } + } + if hfa { + ft := "float" + if ls[0].isDouble { + ft = "double" + } + a.kind = "expand" + a.parts.push("[" + str(ls.len) + " x " + ft + "]") + []str elems + for l in ls { + elems.push(ft) + } + a.coerced = "{ " + elems.join(", ") + " }" + return a + } + if size > 16 { + a.kind = "indirect" + return a + } + a.kind = "expand" + if align >= 16 { + a.parts.push("i128") + a.coerced = "i128" + } else if size <= 8 { + a.parts.push("i64") + a.coerced = "i" + str(size * 8) + } else { + a.parts.push("[2 x i64]") + a.coerced = "[2 x i64]" + } + return a +} + fn classify(ptr[Gen] g, ast.Type t) -> Abi { a := Abi(kind: "direct") if t.kind != "struct" { @@ -3600,6 +3656,21 @@ fn classify(ptr[Gen] g, ast.Type t) -> Abi { if sd.calign > 8 { a.align = sd.calign } + if g.tg.arm64 { + return classifyArm64(g, t, a, size, sd.calign) + } + if g.tg.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 = "expand" + a.parts.push("i" + str(size * 8)) + a.coerced = a.parts[0] + } else { + a.kind = "indirect" + } + return a + } if size > 16 || size == 0 { a.kind = "memory" return a @@ -3663,6 +3734,9 @@ fn makePlan(ptr[Gen] g, ast.Type ret, []ast.Type params) -> Plan { if ret.kind != "void" { pl.retLL = cLL(g, ret) } + if pl.ret.kind == "indirect" { + pl.ret.kind = "memory" // returned through a hidden pointer + } if pl.ret.kind == "memory" { pl.sret = true pl.retLL = "void" @@ -3682,7 +3756,8 @@ fn makePlan(ptr[Gen] g, ast.Type ret, []ast.Type params) -> Plan { ns += 1 } } - if ni <= intRegs && ns <= sseRegs { + // only System V counts registers here; elsewhere the backend places each part + if g.tg.windows || g.tg.arm64 || (ni <= intRegs && ns <= sseRegs) { intRegs -= ni sseRegs -= ns } else { @@ -3754,7 +3829,7 @@ fn exportWrapper(ptr[Gen] g, FnSig sig) -> str { params.push("i8 zeroext %e" + str(k)) args.push("i1 " + op(g, "icmp ne i8 %e" + str(k) + ", 0")) } else { - params.push(cLL(g, t) + cExt(t) + " %e" + str(k)) + params.push(cLL(g, t) + cExt(g, t) + " %e" + str(k)) args.push(llty(g, t) + " %e" + str(k)) } } @@ -3770,10 +3845,14 @@ fn exportWrapper(ptr[Gen] g, FnSig sig) -> str { emit(g, "ret i8 " + op(g, "zext i1 " + op(g, text) + " to i8")) } else { retLL = llty(g, sig.ret) - retExt = cExt(sig.ret) + retExt = cExt(g, sig.ret) emit(g, "ret " + retLL + " " + op(g, text)) } - endHelper(g, s, "define" + retExt + " " + retLL + " @" + sig.name + "(" + params.join(", ") + ")") + dll := "" + if g.tg.windows { + dll = " dllexport" + } + endHelper(g, s, "define" + dll + retExt + " " + retLL + " @" + sig.name + "(" + params.join(", ") + ")") return sig.name } @@ -3814,8 +3893,12 @@ fn cThunk(ptr[Gen] g, int fi, ast.Node at) -> str { hdr.push("ptr byval(" + llty(g, t) + ") align " + str(a.align) + " %c" + str(ai)) args.push(llty(g, t) + " " + load(g, llty(g, t), "%c" + str(ai))) ai += 1 + } else if a.kind == "indirect" { + hdr.push("ptr %c" + str(ai)) + args.push(llty(g, t) + " " + load(g, llty(g, t), "%c" + str(ai))) + ai += 1 } else if a.kind == "expand" { - slot := alloca(g, "[16 x i8]") + slot := alloca(g, "[32 x i8]") for k in 0..a.parts.len { hdr.push(a.parts[k] + " %c" + str(ai)) store(g, a.parts[k], "%c" + str(ai), op(g, "getelementptr inbounds i8, ptr " + slot + ", i64 " + str(8 * k))) @@ -3823,7 +3906,7 @@ fn cThunk(ptr[Gen] g, int fi, ast.Node at) -> str { } args.push(llty(g, t) + " " + load(g, llty(g, t), slot)) } else { - hdr.push(cLL(g, t) + cExt(t) + " %c" + str(ai)) + hdr.push(cLL(g, t) + cExt(g, t) + " %c" + str(ai)) args.push(llty(g, t) + " %c" + str(ai)) ai += 1 } @@ -3837,11 +3920,11 @@ fn cThunk(ptr[Gen] g, int fi, ast.Node at) -> str { store(g, llty(g, sig.ret), op(g, text), sretName) emit(g, "ret void") } else if pl.ret.kind == "expand" { - slot := alloca(g, "[16 x i8]") + slot := alloca(g, "[32 x i8]") store(g, llty(g, sig.ret), op(g, text), slot) emit(g, "ret " + pl.ret.coerced + " " + load(g, pl.ret.coerced, slot)) } else { - retExt = cExt(sig.ret) + retExt = cExt(g, sig.ret) emit(g, "ret " + cLL(g, sig.ret) + " " + op(g, text)) } endHelper(g, s, "define internal" + retExt + " " + pl.retLL + " " + name + "(" + hdr.join(", ") + ")") @@ -3880,15 +3963,18 @@ fn enumFrom(ptr[Gen] g, ast.Node c, int ei) -> Val { // ---------- calling C ---------- // How C extends a number narrower than 32 bits: plain `char` is signed on x86. -fn cExt(ast.Type t) -> str { +fn cExt(ptr[Gen] g, ast.Type t) -> str { if t.kind == "bool" || isUnsigned(t) { if bitsOf(t) < 32 { return " zeroext" } } - if (isSigned(t) || t.kind == "char") && bitsOf(t) < 32 { + if (isSigned(t) || (t.kind == "char" && platform.charIsSigned(g.tg))) && bitsOf(t) < 32 { return " signext" } + if t.kind == "char" && bitsOf(t) < 32 { + return " zeroext" + } return "" } @@ -3939,8 +4025,13 @@ fn callC(ptr[Gen] g, ast.Node c, cimport.CFunc f) -> Val { spec := "ptr byval(" + llty(g, pt) + ") align " + str(a.align) ptys.push(spec) args.push(spec + " " + slot) + } else if a.kind == "indirect" { + slot := alloca(g, llty(g, pt)) + store(g, llty(g, pt), v.v, slot) + ptys.push("ptr") + args.push("ptr " + slot) } else if a.kind == "expand" { - slot := alloca(g, "[16 x i8]") + slot := alloca(g, "[32 x i8]") store(g, llty(g, pt), v.v, slot) for k in 0..a.parts.len { x := load(g, a.parts[k], op(g, "getelementptr inbounds i8, ptr " + slot + ", i64 " + str(8 * k))) @@ -3948,11 +4039,11 @@ fn callC(ptr[Gen] g, ast.Node c, cimport.CFunc f) -> Val { args.push(a.parts[k] + " " + x) } } else { - ptys.push(cLL(g, pt) + cExt(pt)) + ptys.push(cLL(g, pt) + cExt(g, pt)) if pt.kind == "str" { args.push("ptr " + cstr(g, v.v)) } else { - args.push(llty(g, pt) + cExt(pt) + " " + v.v) + args.push(llty(g, pt) + cExt(g, pt) + " " + v.v) } } continue @@ -3986,7 +4077,7 @@ fn callC(ptr[Gen] g, ast.Node c, cimport.CFunc f) -> Val { retLL := pl.retLL retExt := "" if pl.ret.kind == "direct" { - retExt = cExt(ret) + retExt = cExt(g, ret) } tail := "" if f.variadic { @@ -4016,7 +4107,7 @@ fn callC(ptr[Gen] g, ast.Node c, cimport.CFunc f) -> Val { } r := op(g, text) if pl.ret.kind == "expand" { - slot := alloca(g, "[16 x i8]") + slot := alloca(g, "[32 x i8]") store(g, pl.ret.coerced, r, slot) return Val(v: load(g, llty(g, ret), slot), ty: ret) } @@ -4796,6 +4887,7 @@ fn runtimeDecls() -> str { "declare i32 @fflush(ptr)", "declare void @finch_str_from_c(ptr, ptr)", "declare void @finch_on_fatal(ptr)", + "declare ptr @finch_std_stream(i32)", "declare void @finch_str_from_bytes(ptr, ptr)", "declare void @exit(i32) noreturn", "declare void @free(ptr)", @@ -4804,7 +4896,11 @@ fn runtimeDecls() -> str { } fn assemble(ptr[Gen] g) -> str { - text := "; generated by the Finch compiler written in Finch\n\n" + g.head.join("\n") + "\n\n" + runtimeDecls() + "\n" + text := "; generated by the Finch compiler written in Finch\n\n" + if g.tg.cross { + text = text + "target triple = \"" + g.tg.triple + "\"\n\n" + } + text = text + g.head.join("\n") + "\n\n" + runtimeDecls() + "\n" if g.debug { text = text + "declare void @llvm.dbg.declare(metadata, metadata, metadata)\n\n" } @@ -4817,7 +4913,7 @@ fn assemble(ptr[Gen] g) -> str { fn generate([]ast.Program progs, []str files, cimport.CImports ci, Options opt) -> str { shared := opt.shared - gen := Gen(progs: progs, files: files, ci: ci, shared: opt.shared, debug: opt.debug, cwd: opt.cwd, dbgScope: -1) + gen := Gen(progs: progs, files: files, ci: ci, shared: opt.shared, debug: opt.debug, cwd: opt.cwd, tg: opt.target, dbgScope: -1) g := addr(gen) if g.debug { // !0 the compile unit, !1 and !2 the module flags, then the source files @@ -4852,7 +4948,11 @@ fn generate([]ast.Program progs, []str files, cimport.CImports ci, Options opt) } if shared { // finch_module_on_fatal(handler): the host's say on runtime errors (see runtime/finch_rt.c) - g.out.push("define void @finch_module_on_fatal(ptr %handler) {\nentry:\n call void @finch_on_fatal(ptr %handler)\n ret void\n}\n") + dll := "" + if g.tg.windows { + dll = " dllexport" + } + g.out.push("define" + dll + " void @finch_module_on_fatal(ptr %handler) {\nentry:\n call void @finch_on_fatal(ptr %handler)\n ret void\n}\n") return assemble(g) } // the C entry point calls the Finch main diff --git a/boot/main.fch b/boot/main.fch index 3802b2c..936ccc3 100644 --- a/boot/main.fch +++ b/boot/main.fch @@ -10,6 +10,7 @@ import ast import parser import gen import cimport +import platform import "stdlib.h" fn dirOf(str path) -> str { @@ -39,6 +40,7 @@ struct Loader { []ast.Program progs []str files []str loaded + str os // "windows", "macos" or "linux": a module may have a version for each (time_windows.fch) str std // the standard library's folder, or "" []str paths // FINCH_PATH: more folders to look for modules in } @@ -46,18 +48,29 @@ struct Loader { // import json: the file next to the one that imports it, else the standard library's. // A program's own json.fch wins over the library's. fn findModule(ptr[Loader] l, str from, str name) -> str { + // in each folder, the version for this system first: time_windows.fch, then time.fch local := dirOf(from) + "/" + name + ".fch" + if file_exists(dirOf(from) + "/" + name + "_" + l.os + ".fch") { + return dirOf(from) + "/" + name + "_" + l.os + ".fch" + } if file_exists(local) { return local } for d in l.paths { + if file_exists(d + "/" + name + "_" + l.os + ".fch") { + return d + "/" + name + "_" + l.os + ".fch" + } if file_exists(d + "/" + name + ".fch") { return d + "/" + name + ".fch" } } - shared := l.std + "/" + name + ".fch" - if file_exists(shared) { - return shared + if l.std.len > 0 { + if file_exists(l.std + "/" + name + "_" + l.os + ".fch") { + return l.std + "/" + name + "_" + l.os + ".fch" + } + if file_exists(l.std + "/" + name + ".fch") { + return l.std + "/" + name + ".fch" + } } return local } @@ -112,7 +125,9 @@ fn usage() { print(" -O0 no optimization (faster to build)") print(" -l link a library (the same as link \"lib\" in the program)") print(" -I look for imported C headers there too") - print("not in this compiler yet: -g, --target, lsp. They are in the C++ one (branch before-bootstrap).") + print(" -g debug info (for gdb and lldb; use -O0 with it)") + print(" --target build for another system: windows, linux, arm64 (Linux), macos, or an LLVM triple") + print("not in this compiler yet: lsp. It is in the C++ one (branch before-bootstrap).") exit(1) } @@ -137,6 +152,7 @@ fn main([]str args) -> int { outGiven := false shared := false debug := false + targetName := "" opt := "-O2" []str extraLibs []str extraDirs @@ -164,9 +180,9 @@ fn main([]str args) -> int { ai += 1 } else if a == "-g" { debug = true - } else if a == "--target" { - print("error: '--target' is not in the compiler written in Finch yet; the C++ one has it (branch before-bootstrap)") - return 1 + } else if a == "--target" && ai + 1 < args.len { + targetName = args[ai + 1] + ai += 1 } else if cmd == "run" { progArgs.push(a) } else { @@ -174,14 +190,28 @@ fn main([]str args) -> int { } ai += 1 } + tg := platform.select(targetName) if cmd == "run" { - out = "/tmp/finch_run_" + stem(file) - } - if shared && !outGiven { - out = "lib" + out + ".so" + out = "/tmp/finch_run_" + stem(file) + tg.exe + } else if shared && !outGiven { + if tg.windows { + out = out + ".dll" + } else if tg.darwin { + out = "lib" + out + ".dylib" + } else { + out = "lib" + out + ".so" + } + } else if !outGiven { + out = out + tg.exe } loader := Loader() + loader.os = "linux" + if tg.windows { + loader.os = "windows" + } else if tg.darwin { + loader.os = "macos" + } for cand in [dirOf(args[0]) + "/../std", dirOf(args[0]) + "/std", "std"] { if loader.std.len == 0 && file_exists(cand + "/path.fch") { loader.std = cand @@ -218,11 +248,15 @@ fn main([]str args) -> int { } } } - ci := cimport.importHeaders(headers, dirs) or { + triple := "" + if tg.cross { + triple = tg.triple + } + ci := cimport.importHeaders(headers, dirs, triple, tg.windows, tg.cc) or { print(err) return 1 } - ir := gen.generate(loader.progs, loader.files, ci, gen.Options(shared: shared, debug: debug, cwd: getenv("PWD"))) + ir := gen.generate(loader.progs, loader.files, ci, gen.Options(shared: shared, debug: debug, cwd: getenv("PWD"), target: tg)) if cmd == "ir" { print(ir) return 0 @@ -245,19 +279,37 @@ fn main([]str args) -> int { } llPath := out + ".ll" write_file(llPath, ir) - sharedFlags := "" + // the IR is always compiled by clang (CC may be gcc, which can't read it); FINCH_CC picks the linker for another system + clang := getenv("FINCH_CC") + if clang.len == 0 || tg.cross { + clang = "clang" + } + extra := "" if shared { - sharedFlags = " -shared -fPIC" + extra = " -fPIC" } if debug { - sharedFlags = sharedFlags + " -g" // or clang drops the debug info that is in the IR + extra = extra + " -g" // or clang drops the debug info that is in the IR } - // the compiler of the IR is always a clang (CC may be gcc, which can't read it); FINCH_CC picks another clang - cc := getenv("FINCH_CC") - if cc.len == 0 { - cc = "clang" + code := 0 + if tg.cross { + // an object for the target, then the target's own C compiler links it with the runtime + obj := out + ".o" + code = shell("clang --target=" + tg.triple + " " + opt + extra + " -w -c '" + llPath + "' -o '" + obj + "'") + if code == 0 { + linkShared := "" + if shared { + linkShared = " -shared" + } + code = shell(tg.cc + " '" + obj + "' '" + rt + "' -o '" + out + "' -lm" + linkShared + " " + libs.join(" ")) + } + shell("rm -f '" + obj + "'") + } else { + if shared { + extra = extra + " -shared" + } + code = shell(clang + " " + opt + extra + " -w '" + llPath + "' '" + rt + "' -o '" + out + "' -lm " + libs.join(" ")) } - code := shell(cc + " " + opt + sharedFlags + " -w '" + llPath + "' '" + rt + "' -o '" + out + "' -lm " + libs.join(" ")) shell("rm -f '" + llPath + "'") if code != 0 { print("error: clang failed") @@ -268,7 +320,7 @@ fn main([]str args) -> int { for a in progArgs { quoted.push(quote(a)) } - status := shell(quote(out) + " " + quoted.join(" ")) + status := shell(tg.runner + quote(out) + " " + quoted.join(" ")) shell("rm -f " + quote(out)) return status } diff --git a/boot/platform.fch b/boot/platform.fch new file mode 100644 index 0000000..345c25a --- /dev/null +++ b/boot/platform.fch @@ -0,0 +1,90 @@ +// The system a program is built for, and what depends on it: the C compiler that links, the file +// name of the program, how it is run here, and (through the flags) the C calling convention. +// The same as src/target.cpp. +import "stdlib.h" + +struct Target { + str triple // "" = the system the compiler runs on + bool windows // Microsoft x64 calling convention, .exe + bool msvc + bool darwin // macOS + bool arm64 // AAPCS64 + bool cross // another system than the one the compiler runs on + str exe // ".exe" or "" + str cc // the C compiler that links (the IR is always compiled by clang) + str runner // how to run a program for it here: "", "wine ", "qemu-aarch64 " +} + +fn firstLine(str text) -> str { + parts := text.trim().split("\n") + return parts[0].trim() +} + +// what clang calls the system it runs on, like x86_64-pc-linux-gnu +fn hostTriple() -> str { + tmp := "/tmp/finch_triple.txt" + shell("clang -dumpmachine > '" + tmp + "' 2> /dev/null") + out := read_file(tmp) or "" + shell("rm -f '" + tmp + "'") + if out.trim().len == 0 { + return "x86_64-unknown-linux-gnu" + } + return firstLine(out) +} + +// "windows", "linux", "macos", "arm64" (ARM64 Linux), or a full LLVM triple; "" = this system +fn select(str name) -> Target { + host := hostTriple() + t := name + if name.len == 0 { + t = host + } else if name == "windows" { + t = "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" { + if host.starts_with("aarch64") || host.starts_with("arm64") { + t = "arm64-apple-macosx14.0.0" + } else { + t = "x86_64-apple-macosx14.0.0" + } + } + tg := Target(triple: t) + tg.windows = t.contains("windows") || t.contains("mingw") || t.contains("msvc") || t.contains("win32") + tg.msvc = t.contains("msvc") + tg.darwin = t.contains("apple") || t.contains("darwin") || t.contains("macos") + tg.arm64 = t.starts_with("aarch64") || t.starts_with("arm64") + hostArm := host.starts_with("aarch64") || host.starts_with("arm64") + hostDarwin := host.contains("apple") || host.contains("darwin") + tg.cross = tg.windows || tg.darwin != hostDarwin || tg.arm64 != hostArm + tg.exe = "" + if tg.windows { + tg.exe = ".exe" + } + // FINCH_CC chooses the C compiler for any target + fc := getenv("FINCH_CC") + if fc.len > 0 { + tg.cc = fc + } else if tg.cross && tg.windows { + tg.cc = "x86_64-w64-mingw32-gcc" + } else if tg.cross && tg.arm64 && !tg.darwin { + tg.cc = "aarch64-linux-gnu-gcc" + } else if tg.cross { + tg.cc = "clang --target=" + t + } else { + tg.cc = "cc" + } + if tg.cross && tg.windows { + tg.runner = "wine " + } else if tg.cross && tg.arm64 && !tg.darwin { + tg.runner = "qemu-aarch64 " + } + return tg +} + +// C's plain `char`: signed on x86 and Apple, not on ARM64 Linux +fn charIsSigned(Target t) -> bool { + return !t.arm64 || t.darwin || t.windows +} diff --git a/std/finch_win.h b/std/finch_win.h new file mode 100644 index 0000000..2261709 --- /dev/null +++ b/std/finch_win.h @@ -0,0 +1,6 @@ +/* The Windows functions std/time_windows.fch needs. Declared here, not by including : + libclang can't parse that header with MinGW's intrinsics. Both are in kernel32. */ +unsigned long long GetTickCount64(void); +void Sleep(unsigned long milliseconds); +int QueryPerformanceCounter(long long *count); +int QueryPerformanceFrequency(long long *frequency); diff --git a/std/proc_windows.fch b/std/proc_windows.fch new file mode 100644 index 0000000..eb60e7f --- /dev/null +++ b/std/proc_windows.fch @@ -0,0 +1,45 @@ +// Programs, for Windows. The same functions as proc.fch (that file is for Linux and macOS). +import "stdlib.h" +import "process.h" +import "direct.h" +import "time.h" + +fn run(str command) -> int { + return shell(command) +} + +fn output(str command) -> str! { + tmp := proc_env("TEMP", ".") + "\\finch_out_" + str(getpid()) + "_" + str(int(time(null))) + code := shell(command + " > \"" + tmp + "\" 2>&1") + text := read_file(tmp) or "" + delete_file(tmp) + if code != 0 { + return error("'" + command + "' exited with code " + str(code) + ": " + text.trim()) + } + return text +} + +fn proc_env(str name, str fallback) -> str { + v := getenv(name) + if v.len == 0 { + return fallback + } + return v +} + +fn env(str name, str fallback) -> str { + return proc_env(name, fallback) +} + +fn pid() -> int { + return int(getpid()) +} + +fn cwd() -> str { + []char buf + for i in 0..4096 { + buf.push(char(0)) + } + getcwd(buf.ptr, 4096) + return str(buf.ptr) +} diff --git a/std/time_windows.fch b/std/time_windows.fch new file mode 100644 index 0000000..80ecbe9 --- /dev/null +++ b/std/time_windows.fch @@ -0,0 +1,75 @@ +// Time, for Windows. The same functions as time.fch (that file is for Linux and macOS). +import "time.h" +import "finch_win.h" + +struct Date { + int year + int month // 1 to 12 + int day // 1 to 31 + int hour + int minute + int second + int weekday // 0 is Sunday + int yearday // 1 to 366 +} + +fn now() -> int { + return int(time(null)) +} + +// milliseconds on a clock that only goes forward (the performance counter: GetTickCount64 ticks every 15 ms) +fn millis() -> int { + i64 count = 0 + i64 freq = 1 + QueryPerformanceFrequency(addr(freq)) + QueryPerformanceCounter(addr(count)) + return int(count / freq) * 1000 + int(count % freq) * 1000 / int(freq) +} + +fn sleep(int ms) { + Sleep(u32(ms)) +} + +fn parts(tm t) -> Date { + return Date(year: int(t.tm_year) + 1900, month: int(t.tm_mon) + 1, day: int(t.tm_mday), hour: int(t.tm_hour), + minute: int(t.tm_min), second: int(t.tm_sec), weekday: int(t.tm_wday), yearday: int(t.tm_yday) + 1) +} + +fn date(int unix) -> Date { + tm t + i64 stamp = unix + _gmtime64_s(addr(t), addr(stamp)) + return parts(t) +} + +fn local(int unix) -> Date { + tm t + i64 stamp = unix + _localtime64_s(addr(t), addr(stamp)) + return parts(t) +} + +fn format(int unix, str pattern) -> str { + tm t + i64 stamp = unix + _localtime64_s(addr(t), addr(stamp)) + []char buf + for i in 0..256 { + buf.push(char(0)) + } + strftime(buf.ptr, 256, pattern, addr(t)) + return str(buf.ptr) +} + +fn days_in_month(int year, int month) -> int { + if month == 2 { + if year % 4 == 0 && (year % 100 != 0 || year % 400 == 0) { + return 29 + } + return 28 + } + if month == 4 || month == 6 || month == 9 || month == 11 { + return 30 + } + return 31 +} diff --git a/tests/cross.sh b/tests/cross.sh index da58412..c1ea693 100755 --- a/tests/cross.sh +++ b/tests/cross.sh @@ -4,7 +4,7 @@ # 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' +# --run 'FINCH_CC=aarch64-unknown-linux-gnu-gcc QEMU_LD_PREFIX= 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")" @@ -24,7 +24,7 @@ run_exe() { # program, stdin -> output without Windows' \r $runner "$1" < "$2" 2>&1 | tr -d '\r' } -for f in run/*.fch; do +for f in run/*.fch new/*.fch; do grep -q "^fn main" "$f" || continue input="${f%.fch}.in" [ -f "$input" ] || input=/dev/null @@ -40,7 +40,7 @@ for f in run/*.fch; do fi done -for f in fail/*.fch; do +for f in fail/*.fch new/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 diff --git a/tests/new/std_time_proc.fch b/tests/new/std_time_proc.fch index d425dff..a1be4d6 100644 --- a/tests/new/std_time_proc.fch +++ b/tests/new/std_time_proc.fch @@ -8,12 +8,12 @@ fn main() -> ! { a := time.millis() time.sleep(30) waited := time.millis() - a - print(waited >= 25 && waited < 500) + print(waited >= 20 && waited < 500) print(time.now() > 1700000000, time.format(0, "%Y") == "1970" || time.format(0, "%Y") == "1969") - print((try proc.output("echo hello; echo world")).trim()) + print((try proc.output("echo hello")).trim()) print(proc.run("exit 3")) - bad := proc.output("echo oops; exit 2") or err + bad := proc.output("exit 2") or err print(bad) print(proc.env("FINCH_SURELY_NOT_SET", "fallback"), proc.pid() > 0, proc.cwd().len > 0) } diff --git a/tests/new/std_time_proc.out b/tests/new/std_time_proc.out index 5a5b4e5..22f1057 100644 --- a/tests/new/std_time_proc.out +++ b/tests/new/std_time_proc.out @@ -3,7 +3,6 @@ true true true hello -world 3 -'echo oops; exit 2' exited with code 2: oops +'exit 2' exited with code 2: fallback true true