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<name>.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.
This commit is contained in:
Nexoniarz 2026-10-25 21:03:05 +01:00
commit dd9cbe7611
13 changed files with 212 additions and 13 deletions

View file

@ -34,6 +34,8 @@ jobs:
build/finch version build/finch version
- name: Tests - name: Tests
run: tests/run.sh run: tests/run.sh
- name: Shared library
run: tests/shared.sh
- name: Tests under valgrind - name: Tests under valgrind
if: matrix.arch == 'x64' if: matrix.arch == 'x64'
run: MEMCHECK=1 tests/run.sh run: MEMCHECK=1 tests/run.sh

View file

@ -102,6 +102,7 @@ struct Node {
Type ty // declared type (decl, param, field, fn result) Type ty // declared type (decl, param, field, fn result)
bool flag // decl: has a type; or: the right side is a { block } bool flag // decl: has a type; or: the right side is a { block }
bool fallible // fn: -> T! or -> ! bool fallible // fn: -> T! or -> !
bool exported // export fn: a C function of a library (finch build --shared)
[]Node kids []Node kids
[]str names // named arguments: Point(x: 1) []str names // named arguments: Point(x: 1)
int line int line

View file

@ -58,6 +58,7 @@ struct FnSig {
str recv // a method of this struct ("" for a function) 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 str recvmod // and the module of that struct: "C" for a struct from a header
bool recvEnum // the receiver is an enum 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 []str tnames // an instance of a generic function: its type parameters and what they stand for
[]ast.Type targs []ast.Type targs
str shown // max[int], for messages str shown // max[int], for messages
@ -136,6 +137,7 @@ struct Gen {
[]str mm // names of methods that change self, for the mutation analysis []str mm // names of methods that change self, for the mutation analysis
int nvars // variables declared so far in this function int nvars // variables declared so far in this function
cimport.CImports ci 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 cdone // C functions and variables already declared
str ftarget // the call an enclosing or / try is waiting on (see nodeKey) 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 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") 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 sig.ll = "@finch." + modName(g.module) + "." + f.name
if f.recv.len > 0 { if f.recv.len > 0 {
sig.ll = "@finch." + modName(g.module) + "." + f.recv + "." + f.name 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.params.push(resolve(g, f.kids[k].ty, f.kids[k]))
sig.pnames.push(f.kids[k].name) sig.pnames.push(f.kids[k].name)
} }
if f.exported {
checkExport(g, f, sig)
}
g.sigs.push(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. // 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 { fn cThunk(ptr[Gen] g, int fi, ast.Node at) -> str {
sig := g.sigs[fi] sig := g.sigs[fi]
@ -4438,6 +4507,7 @@ fn runtimeDecls() -> str {
"declare i32 @printf(ptr, ...)", "declare i32 @printf(ptr, ...)",
"declare i32 @fflush(ptr)", "declare i32 @fflush(ptr)",
"declare void @finch_str_from_c(ptr, 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 @finch_str_from_bytes(ptr, ptr)",
"declare void @exit(i32) noreturn", "declare void @exit(i32) noreturn",
"declare void @free(ptr)", "declare void @free(ptr)",
@ -4445,12 +4515,16 @@ fn runtimeDecls() -> str {
return decls.join("\n") + "\n" return decls.join("\n") + "\n"
} }
fn generate([]ast.Program progs, []str files, cimport.CImports ci) -> str { fn generate([]ast.Program progs, []str files, cimport.CImports ci, bool shared) -> str {
gen := Gen(progs: progs, files: files, ci: ci) gen := Gen(progs: progs, files: files, ci: ci, shared: shared)
g := addr(gen) g := addr(gen)
declare(g) declare(g)
main := findFn(g, "", "main") 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() { ... }')") print(files[0] + ":1:1: error: there is no main function (add 'fn main() { ... }')")
exit(1) exit(1)
} }
@ -4460,6 +4534,16 @@ fn generate([]ast.Program progs, []str files, cimport.CImports ci) -> str {
define(g, di) define(g, di)
di += 1 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 // the C entry point calls the Finch main
m := g.sigs[main] m := g.sigs[main]
callText := "call " + retTy(g, m) + " " + m.ll + "(" callText := "call " + retTy(g, m) + " " + m.ll + "("

View file

@ -24,7 +24,7 @@ fn isHex(char c) -> bool {
fn isKeyword(str w) -> bool { fn isKeyword(str w) -> bool {
keywords := ["fn", "return", "if", "else", "while", "for", "in", "break", "continue", "true", "false", 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) return keywords.contains(w)
} }

View file

@ -10,6 +10,7 @@ import ast
import parser import parser
import gen import gen
import cimport import cimport
import "stdlib.h"
fn dirOf(str path) -> str { fn dirOf(str path) -> str {
i := path.len - 1 i := path.len - 1
@ -39,15 +40,21 @@ struct Loader {
[]str files []str files
[]str loaded []str loaded
str std // the standard library's folder, or "" 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. // 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. // A program's own json.fch wins over the library's.
fn findModule(ptr[Loader] l, str from, str name) -> str { fn findModule(ptr[Loader] l, str from, str name) -> str {
local := dirOf(from) + "/" + name + ".fch" local := dirOf(from) + "/" + name + ".fch"
if file_exists(local) || l.std.len == 0 { if file_exists(local) {
return local return local
} }
for d in l.paths {
if file_exists(d + "/" + name + ".fch") {
return d + "/" + name + ".fch"
}
}
shared := l.std + "/" + name + ".fch" shared := l.std + "/" + name + ".fch"
if file_exists(shared) { if file_exists(shared) {
return shared return shared
@ -127,6 +134,8 @@ fn main([]str args) -> int {
usage() usage()
} }
out := stem(file) out := stem(file)
outGiven := false
shared := false
opt := "-O2" opt := "-O2"
[]str extraLibs []str extraLibs
[]str extraDirs []str extraDirs
@ -138,7 +147,10 @@ fn main([]str args) -> int {
progArgs.push(a) progArgs.push(a)
} else if a == "-o" && ai + 1 < args.len && cmd == "build" { } else if a == "-o" && ai + 1 < args.len && cmd == "build" {
out = args[ai + 1] out = args[ai + 1]
outGiven = true
ai += 1 ai += 1
} else if a == "--shared" && cmd == "build" {
shared = true
} else if a == "-O0" { } else if a == "-O0" {
opt = "-O0" opt = "-O0"
} else if a == "-l" && ai + 1 < args.len { } else if a == "-l" && ai + 1 < args.len {
@ -162,6 +174,9 @@ fn main([]str args) -> int {
if cmd == "run" { if cmd == "run" {
out = "/tmp/finch_run_" + stem(file) out = "/tmp/finch_run_" + stem(file)
} }
if shared && !outGiven {
out = "lib" + out + ".so"
}
loader := Loader() loader := Loader()
for cand in [dirOf(args[0]) + "/../std", dirOf(args[0]) + "/std", "std"] { for cand in [dirOf(args[0]) + "/../std", dirOf(args[0]) + "/std", "std"] {
@ -169,6 +184,11 @@ fn main([]str args) -> int {
loader.std = cand loader.std = cand
} }
} }
for d in getenv("FINCH_PATH").split(":") {
if d.len > 0 {
loader.paths.push(d)
}
}
load(addr(loader), file, "") load(addr(loader), file, "")
// C headers, from every file; their folders are searched first for "local.h" // C headers, from every file; their folders are searched first for "local.h"
@ -199,14 +219,17 @@ fn main([]str args) -> int {
print(err) print(err)
return 1 return 1
} }
ir := gen.generate(loader.progs, loader.files, ci) ir := gen.generate(loader.progs, loader.files, ci, shared)
if cmd == "ir" { if cmd == "ir" {
print(ir) print(ir)
return 0 return 0
} }
// the runtime is Finch's C file; FINCH_RT can point somewhere else // 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) { if !file_exists(rt) {
rt = dirOf(args[0]) + "/runtime/finch_rt.c" rt = dirOf(args[0]) + "/runtime/finch_rt.c"
} }
@ -219,7 +242,16 @@ fn main([]str args) -> int {
} }
llPath := out + ".ll" llPath := out + ".ll"
write_file(llPath, ir) 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 + "'") shell("rm -f '" + llPath + "'")
if code != 0 { if code != 0 {
print("error: clang failed") print("error: clang failed")

View file

@ -376,6 +376,7 @@ fn parseStruct(ptr[Parser] p) -> ast.Node {
fn parseFn(ptr[Parser] p) -> ast.Node { fn parseFn(ptr[Parser] p) -> ast.Node {
f := node(p, "fn") f := node(p, "fn")
f.exported = accept(p, "export")
expect(p, "fn", "'fn', 'struct' or 'import'") expect(p, "fn", "'fn', 'struct' or 'import'")
f.name = expect(p, "ident", "a function name").text f.name = expect(p, "ident", "a function name").text
if at(p, "[") { if at(p, "[") {

View file

@ -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 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 [-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 ir file.fch` | print the LLVM IR |
| `finch lsp` | the language server (editors use it) | | `finch lsp` | the language server (editors use it) |
| `finch version` | version | | `finch version` | version |

View file

@ -37,12 +37,26 @@ static char empty[1] = "";
// ---------- runtime errors ---------- // ---------- 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); fflush(stdout);
fprintf(stderr, "%s:%lld: runtime error: %s\n", file, (long long)line, msg); fprintf(stderr, "%s\n", msg);
exit(1); 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) { __attribute__((noreturn, cold)) void finch_panic_index(const char *file, int64_t line, int64_t i, int64_t len) {
char msg[128]; char msg[128];
snprintf(msg, sizeof msg, "index %lld is out of range (the length is %lld)", (long long)i, (long long)len); 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) { static void *must(void *p) {
if (!p) { if (!p) {
fflush(stdout); fflush(stdout);
fprintf(stderr, "runtime error: out of memory\n"); fatal("runtime error: out of memory");
exit(1);
} }
return p; return p;
} }

View file

@ -60,5 +60,13 @@ for f in tests/new/fail/*.fch; do
echo "$got" | head -3 echo "$got" | head -3
fi fi
done 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)" echo "self-hosted compiler: $pass passed, $failed failed, $skipped skipped (features it doesn't have)"
[ $failed -eq 0 ] [ $failed -eq 0 ]

17
tests/shared.sh Executable file
View file

@ -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

17
tests/shared/host.c Normal file
View file

@ -0,0 +1,17 @@
#include <dlfcn.h>
#include <setjmp.h>
#include <stdio.h>
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;
}

1
tests/shared/host.h Normal file
View file

@ -0,0 +1 @@
void host_print(const char *s);

22
tests/shared/mod.fch Normal file
View file

@ -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
}