mirror of
https://github.com/Nexoniarz/Finch.git
synced 2026-11-03 00:44:27 +00:00
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:
parent
94e41484dd
commit
dd9cbe7611
13 changed files with 212 additions and 13 deletions
2
.github/workflows/ci.yml
vendored
2
.github/workflows/ci.yml
vendored
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
92
boot/gen.fch
92
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 + "("
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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")
|
||||
|
|
|
|||
|
|
@ -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, "[") {
|
||||
|
|
|
|||
|
|
@ -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 |
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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 ]
|
||||
|
|
|
|||
17
tests/shared.sh
Executable file
17
tests/shared.sh
Executable 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
17
tests/shared/host.c
Normal 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
1
tests/shared/host.h
Normal file
|
|
@ -0,0 +1 @@
|
|||
void host_print(const char *s);
|
||||
22
tests/shared/mod.fch
Normal file
22
tests/shared/mod.fch
Normal 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
|
||||
}
|
||||
Loading…
Reference in a new issue