From dd9cbe7611816228c91a58831426c62323523a9f Mon Sep 17 00:00:00 2001 From: Nexoniarz Date: Sun, 25 Oct 2026 21:03:05 +0100 Subject: [PATCH] export fn and finch build --shared, in the compiler written in Finch; FINCH_PATH, FINCH_RT, FINCH_CC export fn name(...) is a C function of a library; build --shared makes lib.so with no main and a finch_module_on_fatal(handler) the host can take runtime errors over with. Ported from the embed work (the runtime, its test and the docs are that work's; the compiler side is new). The driver reads FINCH_PATH (more module folders), FINCH_RT (the runtime's file) and FINCH_CC (the clang). tests/boot.sh also runs tests/shared.sh with the compiler under test. 41 passed. --- .github/workflows/ci.yml | 2 + boot/ast.fch | 1 + boot/gen.fch | 92 ++++++++++++++++++++++++++++++++++++++-- boot/lexer.fch | 2 +- boot/main.fch | 40 +++++++++++++++-- boot/parser.fch | 1 + docs/en/cheatsheet.md | 1 + runtime/finch_rt.c | 21 +++++++-- tests/boot.sh | 8 ++++ tests/shared.sh | 17 ++++++++ tests/shared/host.c | 17 ++++++++ tests/shared/host.h | 1 + tests/shared/mod.fch | 22 ++++++++++ 13 files changed, 212 insertions(+), 13 deletions(-) create mode 100755 tests/shared.sh create mode 100644 tests/shared/host.c create mode 100644 tests/shared/host.h create mode 100644 tests/shared/mod.fch diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index a3e6484..9d05a54 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -34,6 +34,8 @@ jobs: build/finch version - name: Tests run: tests/run.sh + - name: Shared library + run: tests/shared.sh - name: Tests under valgrind if: matrix.arch == 'x64' run: MEMCHECK=1 tests/run.sh diff --git a/boot/ast.fch b/boot/ast.fch index d8c5921..39e19a2 100644 --- a/boot/ast.fch +++ b/boot/ast.fch @@ -102,6 +102,7 @@ struct Node { Type ty // declared type (decl, param, field, fn result) bool flag // decl: has a type; or: the right side is a { block } bool fallible // fn: -> T! or -> ! + bool exported // export fn: a C function of a library (finch build --shared) []Node kids []str names // named arguments: Point(x: 1) int line diff --git a/boot/gen.fch b/boot/gen.fch index af70c7f..45ae143 100644 --- a/boot/gen.fch +++ b/boot/gen.fch @@ -58,6 +58,7 @@ struct FnSig { str recv // a method of this struct ("" for a function) str recvmod // and the module of that struct: "C" for a struct from a header bool recvEnum // the receiver is an enum + bool exported // export fn: also a C function of a library []str tnames // an instance of a generic function: its type parameters and what they stand for []ast.Type targs str shown // max[int], for messages @@ -136,6 +137,7 @@ struct Gen { []str mm // names of methods that change self, for the mutation analysis int nvars // variables declared so far in this function cimport.CImports ci + bool shared // building a library (finch build --shared): no main, its export fns are the way in []str cdone // C functions and variables already declared str ftarget // the call an enclosing or / try is waiting on (see nodeKey) str ffailed // set by that call: an i1, true if it failed @@ -1223,7 +1225,7 @@ fn declare(ptr[Gen] g) { fail(g, f, "'" + f.name + "' is a built-in function, pick another name") } } - sig := FnSig(module: g.module, name: f.name, recv: f.recv, recvmod: recvmod, recvEnum: recvEnum, fallible: f.fallible, prog: pi, index: fi) + sig := FnSig(module: g.module, name: f.name, recv: f.recv, recvmod: recvmod, recvEnum: recvEnum, fallible: f.fallible, exported: f.exported, prog: pi, index: fi) sig.ll = "@finch." + modName(g.module) + "." + f.name if f.recv.len > 0 { sig.ll = "@finch." + modName(g.module) + "." + f.recv + "." + f.name @@ -1236,6 +1238,9 @@ fn declare(ptr[Gen] g) { sig.params.push(resolve(g, f.kids[k].ty, f.kids[k])) sig.pnames.push(f.kids[k].name) } + if f.exported { + checkExport(g, f, sig) + } g.sigs.push(sig) } } @@ -3420,6 +3425,70 @@ fn checkByValue(ptr[Gen] g, ast.Node at, str fname, ast.Type t, str what) { } } +// export fn: what a C program can be given, and call +fn cPlain(ast.Type t) -> bool { + return isNumber(t) || t.kind == "bool" || t.kind == "char" || t.kind == "ptr" +} + +fn checkExport(ptr[Gen] g, ast.Node f, FnSig sig) { + if f.recv.len > 0 { + fail(g, f, "methods can't be exported (export a plain fn that calls it)") + } + if f.fallible { + fail(g, f, "an export fn can't fail (C has no errors as values): handle them inside it") + } + if f.tparams.len > 0 { + fail(g, f, "an export fn can't be generic") + } + for k in 0..sig.params.len { + if !cPlain(sig.params[k]) { + fail(g, f.kids[k], "an export fn takes numbers, bool, char and pointers only (this is " + ast.show(sig.params[k]) + "; pass text as ptr[char] and make it a str inside)") + } + } + if sig.ret.kind != "void" && !cPlain(sig.ret) { + fail(g, f, "an export fn returns nothing, a number, bool, char or a pointer (not " + ast.show(sig.ret) + ")") + } + for other in g.sigs { + if other.exported && other.name == sig.name { + fail(g, f, "'" + sig.name + "' is exported twice (from two modules)") + } + } +} + +// export fn name(...): a C function called `name` that calls the Finch one (a bool is a C byte) +fn exportWrapper(ptr[Gen] g, FnSig sig) -> str { + []str params + []str args + s := beginHelper(g) + for k in 0..sig.params.len { + t := sig.params[k] + if t.kind == "bool" { + 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)) + args.push(llty(g, t) + " %e" + str(k)) + } + } + text := "call " + llty(g, sig.ret) + " " + sig.ll + "(" + args.join(", ") + ")" + retLL := "void" + retExt := "" + if sig.ret.kind == "void" { + emit(g, text) + emit(g, "ret void") + } else if sig.ret.kind == "bool" { + retLL = "i8" + retExt = " zeroext" + emit(g, "ret i8 " + op(g, "zext i1 " + op(g, text) + " to i8")) + } else { + retLL = llty(g, sig.ret) + retExt = cExt(sig.ret) + emit(g, "ret " + retLL + " " + op(g, text)) + } + endHelper(g, s, "define" + retExt + " " + retLL + " @" + sig.name + "(" + params.join(", ") + ")") + return sig.name +} + // A C-callable door into a Finch function, for addr(fn): callbacks like qsort's or libclang's. fn cThunk(ptr[Gen] g, int fi, ast.Node at) -> str { sig := g.sigs[fi] @@ -4438,6 +4507,7 @@ fn runtimeDecls() -> str { "declare i32 @printf(ptr, ...)", "declare i32 @fflush(ptr)", "declare void @finch_str_from_c(ptr, ptr)", + "declare void @finch_on_fatal(ptr)", "declare void @finch_str_from_bytes(ptr, ptr)", "declare void @exit(i32) noreturn", "declare void @free(ptr)", @@ -4445,12 +4515,16 @@ fn runtimeDecls() -> str { return decls.join("\n") + "\n" } -fn generate([]ast.Program progs, []str files, cimport.CImports ci) -> str { - gen := Gen(progs: progs, files: files, ci: ci) +fn generate([]ast.Program progs, []str files, cimport.CImports ci, bool shared) -> str { + gen := Gen(progs: progs, files: files, ci: ci, shared: shared) g := addr(gen) declare(g) main := findFn(g, "", "main") - if main < 0 { + if shared { + if main >= 0 { + fail(g, g.progs[g.sigs[main].prog].fns[g.sigs[main].index], "a library (--shared) has no main: the program that loads it calls its 'export fn's") + } + } else if main < 0 { print(files[0] + ":1:1: error: there is no main function (add 'fn main() { ... }')") exit(1) } @@ -4460,6 +4534,16 @@ fn generate([]ast.Program progs, []str files, cimport.CImports ci) -> str { define(g, di) di += 1 } + for sg in g.sigs { + if sg.exported { + exportWrapper(g, sg) + } + } + 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") + return "; generated by the Finch compiler written in Finch\n\n" + g.head.join("\n") + "\n\n" + runtimeDecls() + "\n" + g.out.join("\n") + } // the C entry point calls the Finch main m := g.sigs[main] callText := "call " + retTy(g, m) + " " + m.ll + "(" diff --git a/boot/lexer.fch b/boot/lexer.fch index 05f2317..2e661bc 100644 --- a/boot/lexer.fch +++ b/boot/lexer.fch @@ -24,7 +24,7 @@ fn isHex(char c) -> bool { fn isKeyword(str w) -> bool { keywords := ["fn", "return", "if", "else", "while", "for", "in", "break", "continue", "true", "false", - "null", "import", "link", "struct", "defer", "or", "try", "enum", "match"] + "null", "import", "link", "struct", "defer", "or", "try", "enum", "match", "export"] return keywords.contains(w) } diff --git a/boot/main.fch b/boot/main.fch index 599fdc3..e1e2a27 100644 --- a/boot/main.fch +++ b/boot/main.fch @@ -10,6 +10,7 @@ import ast import parser import gen import cimport +import "stdlib.h" fn dirOf(str path) -> str { i := path.len - 1 @@ -39,15 +40,21 @@ struct Loader { []str files []str loaded str std // the standard library's folder, or "" + []str paths // FINCH_PATH: more folders to look for modules in } // 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 { local := dirOf(from) + "/" + name + ".fch" - if file_exists(local) || l.std.len == 0 { + if file_exists(local) { return local } + for d in l.paths { + if file_exists(d + "/" + name + ".fch") { + return d + "/" + name + ".fch" + } + } shared := l.std + "/" + name + ".fch" if file_exists(shared) { return shared @@ -127,6 +134,8 @@ fn main([]str args) -> int { usage() } out := stem(file) + outGiven := false + shared := false opt := "-O2" []str extraLibs []str extraDirs @@ -138,7 +147,10 @@ fn main([]str args) -> int { progArgs.push(a) } else if a == "-o" && ai + 1 < args.len && cmd == "build" { out = args[ai + 1] + outGiven = true ai += 1 + } else if a == "--shared" && cmd == "build" { + shared = true } else if a == "-O0" { opt = "-O0" } else if a == "-l" && ai + 1 < args.len { @@ -162,6 +174,9 @@ fn main([]str args) -> int { if cmd == "run" { out = "/tmp/finch_run_" + stem(file) } + if shared && !outGiven { + out = "lib" + out + ".so" + } loader := Loader() for cand in [dirOf(args[0]) + "/../std", dirOf(args[0]) + "/std", "std"] { @@ -169,6 +184,11 @@ fn main([]str args) -> int { loader.std = cand } } + for d in getenv("FINCH_PATH").split(":") { + if d.len > 0 { + loader.paths.push(d) + } + } load(addr(loader), file, "") // C headers, from every file; their folders are searched first for "local.h" @@ -199,14 +219,17 @@ fn main([]str args) -> int { print(err) return 1 } - ir := gen.generate(loader.progs, loader.files, ci) + ir := gen.generate(loader.progs, loader.files, ci, shared) if cmd == "ir" { print(ir) return 0 } // the runtime is Finch's C file; FINCH_RT can point somewhere else - rt := dirOf(args[0]) + "/../runtime/finch_rt.c" + rt := getenv("FINCH_RT") + if rt.len == 0 { + rt = dirOf(args[0]) + "/../runtime/finch_rt.c" + } if !file_exists(rt) { rt = dirOf(args[0]) + "/runtime/finch_rt.c" } @@ -219,7 +242,16 @@ fn main([]str args) -> int { } llPath := out + ".ll" write_file(llPath, ir) - code := shell("clang " + opt + " -w '" + llPath + "' '" + rt + "' -o '" + out + "' -lm " + libs.join(" ")) + sharedFlags := "" + if shared { + sharedFlags = " -shared -fPIC" + } + // 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 := shell(cc + " " + opt + sharedFlags + " -w '" + llPath + "' '" + rt + "' -o '" + out + "' -lm " + libs.join(" ")) shell("rm -f '" + llPath + "'") if code != 0 { print("error: clang failed") diff --git a/boot/parser.fch b/boot/parser.fch index 4dcf215..38b2279 100644 --- a/boot/parser.fch +++ b/boot/parser.fch @@ -376,6 +376,7 @@ fn parseStruct(ptr[Parser] p) -> ast.Node { fn parseFn(ptr[Parser] p) -> ast.Node { f := node(p, "fn") + f.exported = accept(p, "export") expect(p, "fn", "'fn', 'struct' or 'import'") f.name = expect(p, "ident", "a function name").text if at(p, "[") { diff --git a/docs/en/cheatsheet.md b/docs/en/cheatsheet.md index 778de47..4bc41ca 100644 --- a/docs/en/cheatsheet.md +++ b/docs/en/cheatsheet.md @@ -12,6 +12,7 @@ Everything in Finch on one page. For explanations, see the [guides](../../README |---|---| | `finch run file.fch [args…]` | compile and run (args go to the program) | | `finch build file.fch [-o name]` | make a program (`name.exe` on Windows) | +| `finch build file.fch --shared [-I dir]` | make a library (`libfile.so`, `file.dll`) that a C program loads; its `export fn`s are its C functions (numbers, bool, char, pointers) | | `finch ir file.fch` | print the LLVM IR | | `finch lsp` | the language server (editors use it) | | `finch version` | version | diff --git a/runtime/finch_rt.c b/runtime/finch_rt.c index 4cb2d13..d825f64 100644 --- a/runtime/finch_rt.c +++ b/runtime/finch_rt.c @@ -37,12 +37,26 @@ static char empty[1] = ""; // ---------- runtime errors ---------- -__attribute__((noreturn, cold)) void finch_panic(const char *file, int64_t line, const char *msg) { +// A program that loads Finch libraries (finch build --shared) can take runtime errors over +// instead of the process exiting: finch_module_on_fatal(handler) in each library. The handler +// gets the message and must not return (longjmp out, or exit itself). +static void (*fatal_hook)(const char *msg); +void finch_on_fatal(void (*hook)(const char *msg)) { fatal_hook = hook; } + +static __attribute__((noreturn)) void fatal(const char *msg) { + if (fatal_hook) fatal_hook(msg); fflush(stdout); - fprintf(stderr, "%s:%lld: runtime error: %s\n", file, (long long)line, msg); + fprintf(stderr, "%s\n", msg); exit(1); } +__attribute__((noreturn, cold)) void finch_panic(const char *file, int64_t line, const char *msg) { + char full[1024]; + snprintf(full, sizeof full, "%s:%lld: runtime error: %s", file, (long long)line, msg); + fflush(stdout); + fatal(full); +} + __attribute__((noreturn, cold)) void finch_panic_index(const char *file, int64_t line, int64_t i, int64_t len) { char msg[128]; snprintf(msg, sizeof msg, "index %lld is out of range (the length is %lld)", (long long)i, (long long)len); @@ -52,8 +66,7 @@ __attribute__((noreturn, cold)) void finch_panic_index(const char *file, int64_t static void *must(void *p) { if (!p) { fflush(stdout); - fprintf(stderr, "runtime error: out of memory\n"); - exit(1); + fatal("runtime error: out of memory"); } return p; } diff --git a/tests/boot.sh b/tests/boot.sh index fe17b83..e91e31b 100755 --- a/tests/boot.sh +++ b/tests/boot.sh @@ -60,5 +60,13 @@ for f in tests/new/fail/*.fch; do echo "$got" | head -3 fi done +# a library a C program loads (finch build --shared), with the compiler under test +if FINCH_RT="$PWD/runtime/finch_rt.c" FINCH="$boot" tests/shared.sh > "$tmp/shared.out" 2>&1; then + pass=$((pass + 1)) +else + failed=$((failed + 1)) + echo "FAIL tests/shared.sh" + head -5 "$tmp/shared.out" +fi echo "self-hosted compiler: $pass passed, $failed failed, $skipped skipped (features it doesn't have)" [ $failed -eq 0 ] diff --git a/tests/shared.sh b/tests/shared.sh new file mode 100755 index 0000000..83ea020 --- /dev/null +++ b/tests/shared.sh @@ -0,0 +1,17 @@ +#!/bin/sh +# finch build --shared: a library a C program loads, calls by name, and that hands runtime +# errors to the host instead of exiting. +set -e +cd "$(dirname "$0")/shared" +FINCH=${FINCH:-../../build/finch} +tmp=$(mktemp -d) +trap 'rm -rf "$tmp"' EXIT +"$FINCH" build mod.fch --shared -o "$tmp/libmod.so" +${CC:-cc} host.c -rdynamic -o "$tmp/host" -ldl +out=$("$tmp/host" "$tmp/libmod.so") +expected='add 42 +even 1 0 +host_print: hello Rakya +caught: mod.fch:17: runtime error: index 7 is out of range (the length is 3) +still alive' +if [ "$out" = "$expected" ]; then echo "shared library: passed"; else printf 'shared library: FAILED\n%s\n' "$out"; exit 1; fi diff --git a/tests/shared/host.c b/tests/shared/host.c new file mode 100644 index 0000000..9c5dfbb --- /dev/null +++ b/tests/shared/host.c @@ -0,0 +1,17 @@ +#include +#include +#include +static jmp_buf jb; +void host_print(const char *s) { printf("host_print: %s\n", s); } +static void onFatal(const char *m) { printf("caught: %s\n", m); longjmp(jb, 1); } +int main(int argc, char **argv) { + void *h = dlopen(argc > 1 ? argv[1] : "./libmod.so", RTLD_NOW | RTLD_LOCAL); + if (!h) { printf("%s\n", dlerror()); return 1; } + ((void (*)(void (*)(const char *)))dlsym(h, "finch_module_on_fatal"))(onFatal); + printf("add %ld\n", ((long (*)(long, long))dlsym(h, "add"))(2, 40)); + printf("even %d %d\n", ((_Bool (*)(long))dlsym(h, "is_even"))(4), ((_Bool (*)(long))dlsym(h, "is_even"))(5)); + ((void (*)(const char *))dlsym(h, "hello"))("Rakya"); + if (!setjmp(jb)) ((long (*)(long))dlsym(h, "crash"))(7); + printf("still alive\n"); + return 0; +} diff --git a/tests/shared/host.h b/tests/shared/host.h new file mode 100644 index 0000000..13358a4 --- /dev/null +++ b/tests/shared/host.h @@ -0,0 +1 @@ +void host_print(const char *s); diff --git a/tests/shared/mod.fch b/tests/shared/mod.fch new file mode 100644 index 0000000..19cb22a --- /dev/null +++ b/tests/shared/mod.fch @@ -0,0 +1,22 @@ +import "host.h" + +fn greet(str who) -> str { + return "hello " + who +} + +export fn add(int a, int b) -> int { + return a + b +} + +export fn hello(ptr[char] name) { + host_print(greet(str(name))) +} + +export fn crash(int i) -> int { + xs := [1, 2, 3] + return xs[i] +} + +export fn is_even(int n) -> bool { + return n % 2 == 0 +}