mirror of
https://github.com/Nexoniarz/Finch.git
synced 2026-11-03 00:44:27 +00:00
export fn and finch build --shared: libraries a C program loads (game scripts)
export fn name(...) is a C function by that name (numbers, bool, char, pointers in and out). --shared builds lib<name>.so / <name>.dll with no main. finch_module_on_fatal(handler) lets the host take runtime errors over (longjmp out) instead of the process exiting. tests/shared.sh: a C host loads one, calls it and survives an index error. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
parent
f9838dfe03
commit
68637a35a7
16 changed files with 148 additions and 10 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
|
||||
|
|
|
|||
|
|
@ -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` | 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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -315,6 +315,7 @@ struct FnDecl {
|
|||
Pos recvPos;
|
||||
Type ret; // Void when there is no `-> type`
|
||||
bool fallible = false; // -> int! / -> ! : can fail with an error
|
||||
bool exported = false; // export fn: callable from C by its own name (a library's entry point)
|
||||
std::vector<Param> params;
|
||||
BlockPtr body;
|
||||
};
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@
|
|||
|
||||
#include "codegen_impl.h"
|
||||
#include "builtins_doc.h"
|
||||
#include "target.h"
|
||||
|
||||
using namespace llvm;
|
||||
|
||||
|
|
@ -36,12 +37,29 @@ Codegen::Codegen(LLVMContext &c, const std::vector<Program> &ps, const CImports
|
|||
std::unique_ptr<Module> Codegen::run() {
|
||||
declareStructs();
|
||||
declareFns();
|
||||
if (!modules[""].fns.count("main")) failAt(0, 1, 1, "there is no main function (add 'fn main() { ... }')");
|
||||
if (g_target.shared) {
|
||||
if (modules[""].fns.count("main")) {
|
||||
const Pos &pos = modules[""].fns["main"].decl->namePos;
|
||||
failAt(pos.file, pos.line, pos.col, "a library (--shared) has no main: the program that loads it calls its 'export fn's");
|
||||
}
|
||||
} else if (!modules[""].fns.count("main")) {
|
||||
failAt(0, 1, 1, "there is no main function (add 'fn main() { ... }')");
|
||||
}
|
||||
for (auto &[name, m] : modules)
|
||||
for (auto &[fname, fn] : m.fns) define(fn);
|
||||
for (auto &[s, ms] : methods)
|
||||
for (auto &[mname, fn] : ms) define(fn);
|
||||
defineMainWrapper();
|
||||
if (!g_target.shared) defineMainWrapper();
|
||||
for (const auto &[module, name] : exportNames) defineExport(modules[module].fns[name]);
|
||||
if (g_target.shared) { // finch_module_on_fatal(handler): the host's say on runtime errors (see finch_rt.c)
|
||||
Function *w = Function::Create(FunctionType::get(b.getVoidTy(), {b.getPtrTy()}, false), Function::ExternalLinkage,
|
||||
"finch_module_on_fatal", mod.get());
|
||||
if (g_target.windows) w->setDLLStorageClass(GlobalValue::DLLExportStorageClass);
|
||||
b.SetInsertPoint(BasicBlock::Create(ctx, "entry", w));
|
||||
b.SetCurrentDebugLocation(DebugLoc());
|
||||
b.CreateCall(rt("finch_on_fatal"), {w->getArg(0)});
|
||||
b.CreateRetVoid();
|
||||
}
|
||||
if (di) di->finalize();
|
||||
|
||||
std::string err;
|
||||
|
|
@ -188,8 +206,44 @@ void Codegen::declare(const Program &p, const FnDecl &fn) {
|
|||
// User functions get a prefix so they never clash with C library names.
|
||||
std::string name = "finch." + (p.module.empty() ? "" : p.module + ".") + (recv ? recv->name + "." : "") + fn.name;
|
||||
f.llvm = Function::Create(FunctionType::get(retType(f), params, false), Function::InternalLinkage, name, mod.get());
|
||||
if (fn.exported) {
|
||||
if (recv) failAt(pos.file, pos.line, pos.col, "methods can't be exported (export a plain fn that calls it)");
|
||||
if (f.fallible) failAt(pos.file, pos.line, pos.col, "an export fn can't fail (C has no errors as values): handle them inside it");
|
||||
auto cType = [](const FType &t) { return t.isNumber() || t.kind == FType::Bool || t.kind == FType::Char || t.kind == FType::Ptr; };
|
||||
for (size_t i = 0; i < f.params.size(); i++)
|
||||
if (!cType(f.params[i]))
|
||||
failAt(fn.params[i].pos.file, fn.params[i].pos.line, fn.params[i].pos.col,
|
||||
"an export fn takes numbers, bool, char and pointers only (this is " + f.params[i].show() +
|
||||
"; pass text as ptr[char] and make it a str inside)");
|
||||
if (f.ret.kind != FType::Void && !cType(f.ret))
|
||||
failAt(pos.file, pos.line, pos.col, "an export fn returns nothing, a number, bool, char or a pointer (not " + f.ret.show() + ")");
|
||||
for (const auto &[mname, other] : modules)
|
||||
if (const auto it = other.fns.find(fn.name); it != other.fns.end() && it->second.decl->exported)
|
||||
failAt(pos.file, pos.line, pos.col, "'" + fn.name + "' is exported twice (from two modules)");
|
||||
}
|
||||
if (recv) methods[recv][fn.name] = f;
|
||||
else m.fns[fn.name] = f;
|
||||
if (fn.exported) exportNames.push_back({p.module, fn.name});
|
||||
}
|
||||
|
||||
// export fn name(...): a C function called `name` that calls the Finch one (bool is a C byte).
|
||||
void Codegen::defineExport(Fn &fn) {
|
||||
auto cTy = [&](const FType &t) -> llvm::Type * { return t.kind == FType::Bool ? b.getInt8Ty() : ty(t); };
|
||||
std::vector<llvm::Type *> params;
|
||||
for (const FType &t : fn.params) params.push_back(cTy(t));
|
||||
llvm::Type *ret = fn.ret.kind == FType::Void ? b.getVoidTy() : cTy(fn.ret);
|
||||
Function *w = Function::Create(FunctionType::get(ret, params, false), Function::ExternalLinkage, fn.decl->name, mod.get());
|
||||
if (g_target.windows) w->setDLLStorageClass(GlobalValue::DLLExportStorageClass);
|
||||
b.SetInsertPoint(BasicBlock::Create(ctx, "entry", w));
|
||||
b.SetCurrentDebugLocation(DebugLoc());
|
||||
std::vector<Value *> args;
|
||||
for (size_t i = 0; i < fn.params.size(); i++) {
|
||||
Value *a = w->getArg(unsigned(i));
|
||||
args.push_back(fn.params[i].kind == FType::Bool ? b.CreateICmpNE(a, b.getInt8(0)) : a);
|
||||
}
|
||||
Value *r = b.CreateCall(fn.llvm, args);
|
||||
if (fn.ret.kind == FType::Void) b.CreateRetVoid();
|
||||
else b.CreateRet(fn.ret.kind == FType::Bool ? b.CreateZExt(r, b.getInt8Ty()) : r);
|
||||
}
|
||||
|
||||
// The real C `main` calls the Finch one and turns its result into an exit code.
|
||||
|
|
|
|||
|
|
@ -50,6 +50,7 @@ Function *Codegen::rt(const std::string &name) {
|
|||
struct Sig { llvm::Type *ret; std::vector<llvm::Type *> params; };
|
||||
std::map<std::string, Sig> sigs = {
|
||||
{"finch_panic", {V, {P, I, P}}},
|
||||
{"finch_on_fatal", {V, {P}}},
|
||||
{"finch_panic_index", {V, {P, I, I, I}}},
|
||||
{"finch_alloc", {P, {I}}},
|
||||
{"finch_zalloc", {P, {I}}},
|
||||
|
|
|
|||
|
|
@ -174,6 +174,8 @@ public:
|
|||
void declare(const Program &p, const FnDecl &fn);
|
||||
void define(Fn &fn);
|
||||
void defineMainWrapper();
|
||||
void defineExport(Fn &fn);
|
||||
std::vector<std::pair<std::string, std::string>> exportNames; // (module, name) of each export fn
|
||||
void block(const BlockStmt &blk);
|
||||
void blockBody(const BlockStmt &blk);
|
||||
void stmt(const Stmt &s);
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@
|
|||
#include <unordered_map>
|
||||
|
||||
static const std::unordered_map<std::string, Tok> keywords = {
|
||||
{"fn", Tok::Fn}, {"return", Tok::Return}, {"if", Tok::If},
|
||||
{"fn", Tok::Fn}, {"export", Tok::Export}, {"return", Tok::Return}, {"if", Tok::If},
|
||||
{"else", Tok::Else}, {"while", Tok::While}, {"for", Tok::For},
|
||||
{"in", Tok::In}, {"break", Tok::Break}, {"continue", Tok::Continue},
|
||||
{"true", Tok::True}, {"false", Tok::False}, {"null", Tok::Null},
|
||||
|
|
@ -154,6 +154,7 @@ const char *tokName(Tok t) {
|
|||
case Tok::String: return "a string";
|
||||
case Tok::Char: return "a char";
|
||||
case Tok::Fn: return "'fn'";
|
||||
case Tok::Export: return "'export'";
|
||||
case Tok::Return: return "'return'";
|
||||
case Tok::If: return "'if'";
|
||||
case Tok::Else: return "'else'";
|
||||
|
|
|
|||
|
|
@ -8,7 +8,7 @@ enum class Tok {
|
|||
Ident, Int, Float, String, Char,
|
||||
|
||||
// keywords
|
||||
Fn, Return, If, Else, While, For, In, Break, Continue, True, False, Null, Import, Link, Struct, Defer,
|
||||
Fn, Export, Return, If, Else, While, For, In, Break, Continue, True, False, Null, Import, Link, Struct, Defer,
|
||||
OrElse, Try, // or, try
|
||||
|
||||
// punctuation
|
||||
|
|
|
|||
|
|
@ -42,6 +42,7 @@ static void usage() {
|
|||
" finch lsp the language server, for editors (VS Code, Kate, ...)\n"
|
||||
"\n"
|
||||
" -l <lib> link a C library, same as link \"lib\" in the file\n"
|
||||
" --shared (build) a library with no main: lib<name>.so / <name>.dll, its export fn's callable from C\n"
|
||||
" --target <name> build for another system: windows, linux, arm64 (Linux), macos,\n"
|
||||
" or an LLVM triple\n"
|
||||
" -g add debug info (gdb / lldb / Visual Studio)\n"
|
||||
|
|
@ -230,6 +231,7 @@ static bool endsWith(const std::string &s, const char *x) {
|
|||
static void link(const std::string &obj, const std::string &exe, const std::vector<std::string> &libs,
|
||||
const CImports &c) {
|
||||
std::string cmd = g_target.cc + " " + shellQuote(obj) + " " + shellQuote(runtimeObject()) + " -o " + shellQuote(exe);
|
||||
if (g_target.shared) cmd += g_target.darwin ? " -dynamiclib" : " -shared";
|
||||
if (!g_target.msvc) cmd += " -lm";
|
||||
std::vector<std::string> temps;
|
||||
for (const std::string &l : libs) {
|
||||
|
|
@ -289,6 +291,7 @@ int main(int argc, char **argv) {
|
|||
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 == "--shared" && cmd == "build") g_target.shared = true;
|
||||
else if (a == "-l" && i + 1 < argc) libs.push_back(argv[++i]);
|
||||
else if (a.rfind("-l", 0) == 0 && a.size() > 2) libs.push_back(a.substr(2));
|
||||
else if (cmd == "run") programArgs.push_back(a);
|
||||
|
|
@ -296,7 +299,8 @@ int main(int argc, char **argv) {
|
|||
}
|
||||
|
||||
setTarget(targetName);
|
||||
if (!outGiven) out += g_target.exe;
|
||||
if (!outGiven) out = g_target.shared ? (g_target.windows ? out + ".dll" : "lib" + out + (g_target.darwin ? ".dylib" : ".so"))
|
||||
: out + g_target.exe;
|
||||
|
||||
Loader loader;
|
||||
loader.load(file, "", Pos{});
|
||||
|
|
|
|||
|
|
@ -180,9 +180,10 @@ private:
|
|||
FnDecl function() {
|
||||
FnDecl fn;
|
||||
fn.pos = pos();
|
||||
fn.exported = accept(Tok::Export);
|
||||
if (at(Tok::Ident) && isTypeName(cur().text))
|
||||
fail(fn.pos.line, fn.pos.col, "functions start with 'fn', like: fn add(int a, int b) -> int { ... }");
|
||||
expect(Tok::Fn, "'fn', 'struct', 'import' or 'link' (every function starts with fn)");
|
||||
expect(Tok::Fn, fn.exported ? "'fn' (export fn name(...))" : "'fn', 'struct', 'import' or 'link' (every function starts with fn)");
|
||||
|
||||
fn.namePos = pos();
|
||||
fn.name = expect(Tok::Ident, "a function name").text;
|
||||
|
|
|
|||
|
|
@ -14,6 +14,7 @@ struct TargetInfo {
|
|||
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 ""
|
||||
bool shared = false; // finch build --shared: a library (.so / .dll / .dylib) with no main
|
||||
};
|
||||
|
||||
inline TargetInfo g_target;
|
||||
|
|
|
|||
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