"\(x)" in text, test blocks with assert and finch test, a build cache, the chain of calls on runtime errors

- "x is \(x)", "\(speed, 2)" (two digits after the point), also in """ text
- test "name" { ... } blocks; assert(cond) and assert(cond, why); finch test runs each on its own
- the program's object and its C files (link "x.c") are kept in the cache until they or their headers change
- runtime errors show the calls that led there (finch run, finch test, -g, -O0, --trace)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016GzNP6r8dg6WDxUVZy8wjL
This commit is contained in:
Nexoniarz 2026-10-29 10:59:30 +01:00
commit c830c780fe
8 changed files with 582 additions and 45 deletions

View file

@ -13,6 +13,7 @@
#include <ctype.h> #include <ctype.h>
#include <errno.h> #include <errno.h>
#include <math.h> #include <math.h>
#include <setjmp.h>
#include <stdarg.h> #include <stdarg.h>
#include <stdint.h> #include <stdint.h>
#include <stdio.h> #include <stdio.h>
@ -50,10 +51,38 @@ static char empty[1] = "";
static void (*fatal_hook)(const char *msg); static void (*fatal_hook)(const char *msg);
void finch_on_fatal(void (*hook)(const char *msg)) { fatal_hook = hook; } void finch_on_fatal(void (*hook)(const char *msg)) { fatal_hook = hook; }
// The chain of Finch calls, kept by the program itself (see @finch_trace in the compiler's output):
// each frame is { the caller's frame, the function's name, its file, the line it is at }.
typedef struct FFrame {
struct FFrame *prev;
const char *name;
const char *file;
int64_t line;
} FFrame;
static void *(*trace_top)(void);
static void (*trace_set)(void *);
void finch_trace_init(void *(*top)(void), void (*set)(void *)) {
trace_top = top;
trace_set = set;
}
static void print_trace(void) {
if (!trace_top) return;
FFrame *f = trace_top();
for (int n = 0; f; n++, f = f->prev) {
if (n == 30) {
fprintf(stderr, " ...\n");
break;
}
fprintf(stderr, " in %s (%s:%lld)\n", f->name, f->file, (long long)f->line);
}
}
static __attribute__((noreturn)) void fatal(const char *msg) { static __attribute__((noreturn)) void fatal(const char *msg) {
if (fatal_hook) fatal_hook(msg); if (fatal_hook) fatal_hook(msg);
fflush(stdout); fflush(stdout);
fprintf(stderr, "%s\n", msg); fprintf(stderr, "%s\n", msg);
print_trace();
exit(1); exit(1);
} }
@ -81,6 +110,50 @@ static void *must(void *p) {
void *finch_alloc(int64_t n) { return must(malloc(n ? (size_t)n : 1)); } void *finch_alloc(int64_t n) { return must(malloc(n ? (size_t)n : 1)); }
void *finch_zalloc(int64_t n) { return must(calloc(1, n ? (size_t)n : 1)); } void *finch_zalloc(int64_t n) { return must(calloc(1, n ? (size_t)n : 1)); }
// ---------- assert and finch test ----------
__attribute__((noreturn, cold)) void finch_assert_failed(const char *file, int64_t line, const FStr *why) {
char msg[600];
if (why->len) snprintf(msg, sizeof msg, "assert failed: %.*s", (int)(why->len > 500 ? 500 : why->len), why->ptr);
else snprintf(msg, sizeof msg, "assert failed");
finch_panic(file, line, msg);
}
// Each test runs on its own: a runtime error (a failed assert too) ends that test, and the next one runs.
static jmp_buf test_jump;
static char test_message[1100];
static int64_t tests_passed, tests_failed;
static void test_fatal(const char *msg) {
snprintf(test_message, sizeof test_message, "%s", msg);
longjmp(test_jump, 1);
}
void finch_test_run(const char *name, void (*fn)(void)) {
void (*saved)(const char *) = fatal_hook;
void *chain = trace_top ? trace_top() : NULL;
test_message[0] = 0;
fatal_hook = test_fatal;
int failed = setjmp(test_jump);
if (!failed) fn();
fatal_hook = saved;
if (trace_set) trace_set(chain); // a test that stopped early left its calls on the chain
fflush(stdout);
if (failed) {
tests_failed++;
printf("FAIL %s\n %s\n", name, test_message);
} else {
tests_passed++;
printf("ok %s\n", name);
}
fflush(stdout);
}
int finch_test_summary(void) {
printf("\n%lld passed, %lld failed\n", (long long)tests_passed, (long long)tests_failed);
return tests_failed ? 1 : 0;
}
// ---------- strings ---------- // ---------- strings ----------
static void str_heap(FStr *out, const char *p, int64_t len) { static void str_heap(FStr *out, const char *p, int64_t len) {

View file

@ -123,4 +123,5 @@ struct Program {
[]Node enums []Node enums
[]Node fns []Node fns
[]Node consts // const NAME = value []Node consts // const NAME = value
[]str tests // the functions made from test "..." { } blocks, in order
} }

File diff suppressed because one or more lines are too long

View file

@ -30,6 +30,9 @@ struct Options {
bool debug // -g bool debug // -g
str cwd // the folder the compiler was started in (for the debug info) str cwd // the folder the compiler was started in (for the debug info)
platform.Target target platform.Target target
bool trace // runtime errors show the chain of calls (off for --shared and --no-trace)
bool test // finch test: main runs the test "..." blocks instead
[]str testModules // the modules whose tests run ("" is the main file); not the standard library's
} }
// A generic struct or function, kept as written until something asks for a version of it. // A generic struct or function, kept as written until something asks for a version of it.
@ -163,6 +166,9 @@ struct Ref {
struct Gen { struct Gen {
[]ast.Program progs []ast.Program progs
bool trace // each function keeps a frame { prev, name, file, line } in @finch_trace
str frame // the current function's frame ("" in helpers)
str frameLine // where its line goes
[]str constBusy // the constants being worked out: a loop between them is an error []str constBusy // the constants being worked out: a loop between them is an error
[]str files []str files
[]str head []str head
@ -782,6 +788,8 @@ fn boundsCheck(ptr[Gen] g, str i, str len, ast.Node at) {
// ---------- helper functions (generated once per type) ---------- // ---------- helper functions (generated once per type) ----------
struct Saved { struct Saved {
str frame
str frameLine
[]str body []str body
[]str allocas []str allocas
int tmp int tmp
@ -790,8 +798,10 @@ struct Saved {
} }
fn beginHelper(ptr[Gen] g) -> Saved { fn beginHelper(ptr[Gen] g) -> Saved {
s := Saved(body: g.body, allocas: g.allocas, tmp: g.tmp, term: g.term, dbgScope: g.dbgScope) s := Saved(body: g.body, allocas: g.allocas, tmp: g.tmp, term: g.term, dbgScope: g.dbgScope, frame: g.frame, frameLine: g.frameLine)
g.dbgScope = -1 // a helper made on the side has no source lines g.dbgScope = -1 // a helper made on the side has no source lines
g.frame = ""
g.frameLine = ""
[]str empty []str empty
g.body = empty g.body = empty
g.allocas = empty g.allocas = empty
@ -807,6 +817,8 @@ fn endHelper(ptr[Gen] g, Saved s, str header) {
g.tmp = s.tmp g.tmp = s.tmp
g.term = s.term g.term = s.term
g.dbgScope = s.dbgScope g.dbgScope = s.dbgScope
g.frame = s.frame
g.frameLine = s.frameLine
} }
// for i in 0..n, inside the current function // for i in 0..n, inside the current function
@ -2258,6 +2270,25 @@ fn define(ptr[Gen] g, int si) {
[]Scope none []Scope none
g.scopes = none g.scopes = none
pushScope(g) pushScope(g)
g.frame = ""
g.frameLine = ""
if g.trace {
// this call on the chain a runtime error shows: in move (game.fch:12)
shownName := g.cur.name
if g.cur.recv.len > 0 {
shownName = g.cur.recv + "." + g.cur.name
}
if f.text.len > 0 && g.cur.name.starts_with("__test_") {
shownName = "test \"" + f.text + "\""
}
g.frame = alloca(g, "{ ptr, ptr, ptr, i64 }")
emit(g, "store ptr " + load(g, "ptr", "@finch_trace") + ", ptr " + g.frame)
emit(g, "store ptr " + strGlobal(g, shownName) + ", ptr " + op(g, "getelementptr inbounds { ptr, ptr, ptr, i64 }, ptr " + g.frame + ", i32 0, i32 1"))
emit(g, "store ptr " + strGlobal(g, g.files[f.file]) + ", ptr " + op(g, "getelementptr inbounds { ptr, ptr, ptr, i64 }, ptr " + g.frame + ", i32 0, i32 2"))
g.frameLine = op(g, "getelementptr inbounds { ptr, ptr, ptr, i64 }, ptr " + g.frame + ", i32 0, i32 3")
emit(g, "store i64 " + str(f.line) + ", ptr " + g.frameLine)
emit(g, "store ptr " + g.frame + ", ptr @finch_trace")
}
[]str params []str params
if g.cur.recv.len > 0 { if g.cur.recv.len > 0 {
// self is the caller's struct itself, reached through its address // self is the caller's struct itself, reached through its address
@ -2328,6 +2359,9 @@ fn block(ptr[Gen] g, ast.Node b, bool scope) {
fn stmt(ptr[Gen] g, ast.Node s) { fn stmt(ptr[Gen] g, ast.Node s) {
g.dbgLine = s.line g.dbgLine = s.line
g.dbgCol = s.col g.dbgCol = s.col
if g.frameLine.len > 0 && !g.term {
emit(g, "store i64 " + str(s.line) + ", ptr " + g.frameLine)
}
k := s.kind k := s.kind
if k == "block" { if k == "block" {
block(g, s, true) block(g, s, true)
@ -4731,19 +4765,20 @@ fn exportWrapper(ptr[Gen] g, FnSig sig) -> str {
} }
} }
text := "call " + llty(g, sig.ret) + " " + sig.ll + "(" + args.join(", ") + ")" text := "call " + llty(g, sig.ret) + " " + sig.ll + "(" + args.join(", ") + ")"
savedTrace := traceSave(g)
retLL := "void" retLL := "void"
retExt := "" retExt := ""
if sig.ret.kind == "void" { if sig.ret.kind == "void" {
emit(g, text) tracedEmit(g, text, savedTrace)
emit(g, "ret void") emit(g, "ret void")
} else if sig.ret.kind == "bool" { } else if sig.ret.kind == "bool" {
retLL = "i8" retLL = "i8"
retExt = " zeroext" retExt = " zeroext"
emit(g, "ret i8 " + op(g, "zext i1 " + op(g, text) + " to i8")) emit(g, "ret i8 " + op(g, "zext i1 " + tracedCall(g, text, savedTrace) + " to i8"))
} else { } else {
retLL = llty(g, sig.ret) retLL = llty(g, sig.ret)
retExt = cExt(g, sig.ret) retExt = cExt(g, sig.ret)
emit(g, "ret " + retLL + " " + op(g, text)) emit(g, "ret " + retLL + " " + tracedCall(g, text, savedTrace))
} }
dll := "" dll := ""
if g.tg.windows { if g.tg.windows {
@ -4753,6 +4788,29 @@ fn exportWrapper(ptr[Gen] g, FnSig sig) -> str {
return sig.name return sig.name
} }
// A call from C into Finch (a callback, an export fn) leaves the chain of calls as it found it
fn traceSave(ptr[Gen] g) -> str {
if !g.trace {
return ""
}
return load(g, "ptr", "@finch_trace")
}
fn tracedCall(ptr[Gen] g, str text, str saved) -> str {
r := op(g, text)
if saved.len > 0 {
emit(g, "store ptr " + saved + ", ptr @finch_trace")
}
return r
}
fn tracedEmit(ptr[Gen] g, str text, str saved) {
emit(g, text)
if saved.len > 0 {
emit(g, "store ptr " + saved + ", ptr @finch_trace")
}
}
// 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]
@ -4809,20 +4867,21 @@ fn cThunk(ptr[Gen] g, int fi, ast.Node at) -> str {
} }
} }
text := "call " + llty(g, sig.ret) + " " + sig.ll + "(" + args.join(", ") + ")" text := "call " + llty(g, sig.ret) + " " + sig.ll + "(" + args.join(", ") + ")"
savedTrace := traceSave(g)
retExt := "" retExt := ""
if sig.ret.kind == "void" { if sig.ret.kind == "void" {
emit(g, text) tracedEmit(g, text, savedTrace)
emit(g, "ret void") emit(g, "ret void")
} else if pl.sret { } else if pl.sret {
store(g, llty(g, sig.ret), op(g, text), sretName) store(g, llty(g, sig.ret), tracedCall(g, text, savedTrace), sretName)
emit(g, "ret void") emit(g, "ret void")
} else if pl.ret.kind == "expand" { } else if pl.ret.kind == "expand" {
slot := alloca(g, "[32 x i8]") slot := alloca(g, "[32 x i8]")
store(g, llty(g, sig.ret), op(g, text), slot) store(g, llty(g, sig.ret), tracedCall(g, text, savedTrace), slot)
emit(g, "ret " + pl.ret.coerced + " " + load(g, pl.ret.coerced, slot)) emit(g, "ret " + pl.ret.coerced + " " + load(g, pl.ret.coerced, slot))
} else { } else {
retExt = cExt(g, sig.ret) retExt = cExt(g, sig.ret)
emit(g, "ret " + cLL(g, sig.ret) + " " + op(g, text)) emit(g, "ret " + cLL(g, sig.ret) + " " + tracedCall(g, text, savedTrace))
} }
endHelper(g, s, "define internal" + retExt + " " + pl.retLL + " " + name + "(" + hdr.join(", ") + ")") endHelper(g, s, "define internal" + retExt + " " + pl.retLL + " " + name + "(" + hdr.join(", ") + ")")
return name return name
@ -5329,6 +5388,9 @@ fn call(ptr[Gen] g, ast.Node c) -> Val {
if ["min", "max", "abs", "clamp"].contains(n) { if ["min", "max", "abs", "clamp"].contains(n) {
return mathCall(g, c) // only when nothing of yours (or C's abs) has the name return mathCall(g, c) // only when nothing of yours (or C's abs) has the name
} }
if n == "assert" {
return assertCall(g, c)
}
if lookup(g, n) >= 0 { if lookup(g, n) >= 0 {
fail(g, c, "'" + n + "' is a variable, not a function") fail(g, c, "'" + n + "' is a variable, not a function")
} }
@ -5337,6 +5399,58 @@ fn call(ptr[Gen] g, ast.Node c) -> Val {
} }
// min(a, b, ...), max(a, b, ...), abs(x), clamp(x, lo, hi) for any numbers // min(a, b, ...), max(a, b, ...), abs(x), clamp(x, lo, hi) for any numbers
// assert(cond) and assert(cond, "why"): stops the program (in finch test: fails the test) when cond is false
fn assertCall(ptr[Gen] g, ast.Node c) -> Val {
if c.kids.len != 1 && c.kids.len != 2 {
fail(g, c, "assert takes a condition, and maybe a message: assert(x > 0, \"x must be positive\")")
}
cond := coerce(g, expr(g, c.kids[0]), T("bool"), c.kids[0], "the condition of assert")
bad := newLabel(g, "assert.failed")
ok := newLabel(g, "assert.ok")
condBr(g, cond.v, ok, bad)
label(g, bad)
file := strGlobal(g, g.files[c.file])
msg := strConst(g, "")
if c.kids.len == 2 {
msg = strArg(g, c.kids[1], "the message")
}
emit(g, "call void @finch_assert_failed(ptr " + file + ", i64 " + str(c.line) + ", ptr " + spill(g, msg) + ")")
emit(g, "unreachable")
g.term = true
label(g, ok)
return Val(ty: T("void"))
}
fn traceInit() -> str {
return " call void @finch_trace_init(ptr @finch_trace_top, ptr @finch_trace_set)\n"
}
// finch test: a main that runs every test "..." block of these modules, each on its own
fn testMain(ptr[Gen] g, []str modules) {
body := ""
n := 0
for pr in g.progs {
if !modules.contains(pr.module) {
continue
}
for i in 0..pr.tests.len {
fi := findFn(g, pr.module, pr.tests[i])
name := g.progs[g.sigs[fi].prog].fns[g.sigs[fi].index].text
shown := name
if pr.module.len > 0 {
shown = pr.module + ": " + name
}
body = body + " call void @finch_test_run(ptr " + strGlobal(g, shown) + ", ptr " + g.sigs[fi].ll + ")\n"
n += 1
}
}
if n == 0 {
print(g.files[0] + ":1:1: error: there are no tests here (write them as test \"what it checks\" { ... })")
exit(1)
}
g.out.push("define i32 @main(i32 %argc, ptr %argv) {\nentry:\n" + traceInit() + body + " %r = call i32 @finch_test_summary()\n ret i32 %r\n}\n")
}
fn mathCall(ptr[Gen] g, ast.Node c) -> Val { fn mathCall(ptr[Gen] g, ast.Node c) -> Val {
n := c.name n := c.name
if g.index { if g.index {
@ -5448,6 +5562,30 @@ fn emitCall(ptr[Gen] g, FnSig sig, ast.Node c, []str args, str shown) -> Val {
fail(g, c, "'" + shown + "' can fail, so say what happens then: " + shown + "(...) or <value>, " + shown + "(...) or { ... }, or try " + shown + "(...)") fail(g, c, "'" + shown + "' can fail, so say what happens then: " + shown + "(...) or <value>, " + shown + "(...) or { ... }, or try " + shown + "(...)")
} }
text := "call " + retTy(g, sig) + " " + sig.ll + "(" + args.join(", ") + ")" text := "call " + retTy(g, sig) + " " + sig.ll + "(" + args.join(", ") + ")"
if g.frame.len > 0 {
// the callee put its frame on the chain; it is gone once the call is back
out := Val(ty: sig.ret)
r := ""
if sig.ret.kind == "void" && !sig.fallible {
emit(g, text)
} else {
r = op(g, text)
}
emit(g, "store ptr " + g.frame + ", ptr @finch_trace")
if sig.fallible {
ok := op(g, "extractvalue " + retTy(g, sig) + " " + r + ", 0")
g.ffailed = op(g, "xor i1 " + ok + ", true")
g.fmsg = op(g, "extractvalue " + retTy(g, sig) + " " + r + ", 2")
if sig.ret.kind == "void" {
return out
}
return Val(v: op(g, "extractvalue " + retTy(g, sig) + " " + r + ", 1"), ty: sig.ret, fresh: owning(g, sig.ret))
}
if sig.ret.kind == "void" {
return out
}
return Val(v: r, ty: sig.ret, fresh: owning(g, sig.ret))
}
if sig.fallible { if sig.fallible {
r := op(g, text) r := op(g, text)
ok := op(g, "extractvalue " + retTy(g, sig) + " " + r + ", 0") ok := op(g, "extractvalue " + retTy(g, sig) + " " + r + ", 0")
@ -6093,6 +6231,10 @@ fn runtimeDecls() -> str {
"declare void @qsort(ptr, i64, i64, ptr)", "declare void @qsort(ptr, i64, i64, ptr)",
"declare void @finch_args(ptr, i32, ptr)", "declare void @finch_args(ptr, i32, ptr)",
"declare void @finch_args_free(ptr)", "declare void @finch_args_free(ptr)",
"declare void @finch_assert_failed(ptr, i64, ptr)",
"declare void @finch_test_run(ptr, ptr)",
"declare void @finch_trace_init(ptr, ptr)",
"declare i32 @finch_test_summary()",
"declare void @finch_input(ptr, ptr)", "declare void @finch_input(ptr, ptr)",
"declare i32 @finch_file_exists(ptr)", "declare i32 @finch_file_exists(ptr)",
"declare i32 @finch_str_try_int(ptr, ptr, ptr)", "declare i32 @finch_str_try_int(ptr, ptr, ptr)",
@ -6123,6 +6265,14 @@ fn assemble(ptr[Gen] g) -> str {
text = text + "target triple = \"" + g.tg.triple + "\"\n\n" text = text + "target triple = \"" + g.tg.triple + "\"\n\n"
} }
text = text + g.head.join("\n") + "\n\n" + runtimeDecls() + "\n" text = text + g.head.join("\n") + "\n\n" + runtimeDecls() + "\n"
// the chain of calls a runtime error shows; the runtime reads it through these two
tls := "thread_local(initialexec)"
if g.shared || g.tg.windows {
tls = "thread_local"
}
text = text + "@finch_trace = internal " + tls + " global ptr null\n"
text = text + "define hidden ptr @finch_trace_top() {\nentry:\n %t = load ptr, ptr @finch_trace\n ret ptr %t\n}\n"
text = text + "define hidden void @finch_trace_set(ptr %t) {\nentry:\n store ptr %t, ptr @finch_trace\n ret void\n}\n\n"
if g.debug { if g.debug {
text = text + "declare void @llvm.dbg.declare(metadata, metadata, metadata)\n\n" text = text + "declare void @llvm.dbg.declare(metadata, metadata, metadata)\n\n"
} }
@ -6137,6 +6287,7 @@ fn generate([]ast.Program progs, []str files, cimport.CImports ci, Options opt)
shared := opt.shared shared := opt.shared
gen := Gen(progs: progs, files: files, ci: ci, shared: opt.shared, debug: opt.debug, index: opt.index, cwd: opt.cwd, tg: opt.target, dbgScope: -1) gen := Gen(progs: progs, files: files, ci: ci, shared: opt.shared, debug: opt.debug, index: opt.index, cwd: opt.cwd, tg: opt.target, dbgScope: -1)
g := addr(gen) g := addr(gen)
g.trace = opt.trace && !opt.shared
if g.debug { if g.debug {
// !0 the compile unit, !1 and !2 the module flags, then the source files // !0 the compile unit, !1 and !2 the module flags, then the source files
g.md.push("") g.md.push("")
@ -6149,6 +6300,15 @@ fn generate([]ast.Program progs, []str files, cimport.CImports ci, Options opt)
} }
declare(g) declare(g)
main := findFn(g, "", "main") main := findFn(g, "", "main")
if opt.test {
di := 0
while di < g.sigs.len {
define(g, di)
di += 1
}
testMain(g, opt.testModules)
return assemble(g)
}
if shared { if shared {
if main >= 0 { 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") 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")
@ -6212,6 +6372,6 @@ fn generate([]ast.Program progs, []str files, cimport.CImports ci, Options opt)
callText = "%r = " + callText callText = "%r = " + callText
ret = " %code = trunc i64 %r to i32\n ret i32 %code\n" ret = " %code = trunc i64 %r to i32\n ret i32 %code\n"
} }
g.out.push("define i32 @main(i32 %argc, ptr %argv) {\nentry:\n" + pre + " " + callText + "\n" + after + ret + "}\n") g.out.push("define i32 @main(i32 %argc, ptr %argv) {\nentry:\n" + traceInit() + pre + " " + callText + "\n" + after + ret + "}\n")
return assemble(g) return assemble(g)
} }

View file

@ -80,6 +80,7 @@ fn operator(ptr[Lexer] lx) -> str {
// """ text over several lines """: the line break after the opening quotes is dropped, and so is // """ text over several lines """: the line break after the opening quotes is dropped, and so is
// the indentation of the closing quotes, from every line (the text can be indented with the code). // the indentation of the closing quotes, from every line (the text can be indented with the code).
// Gives the text as written (escapes and \(...) still in it); cook() finishes it.
fn longString(ptr[Lexer] lx, Token t) -> str { fn longString(ptr[Lexer] lx, Token t) -> str {
for k in 0..3 { for k in 0..3 {
advance(lx) advance(lx)
@ -90,15 +91,23 @@ fn longString(ptr[Lexer] lx, Token t) -> str {
fail(lx, t.line, t.col, "text is never closed (missing \"\"\")") fail(lx, t.line, t.col, "text is never closed (missing \"\"\")")
} }
ch := peek(lx, 0) ch := peek(lx, 0)
advance(lx) if ch == '\\' && peek(lx, 1) == '(' {
if ch == '\\' { start := lx.i
e := peek(lx, 0)
advance(lx) advance(lx)
ch = escape(lx, e) advance(lx)
} else if ch == '\r' { skipEmbedded(lx, t)
parts.push(lx.src.sub(start, lx.i))
continue
}
advance(lx)
if ch == '\r' {
continue continue
} }
parts.push(str(ch)) parts.push(str(ch))
if ch == '\\' && lx.i < lx.src.len {
parts.push(str(peek(lx, 0))) // \" doesn't end the text
advance(lx)
}
} }
for k in 0..3 { for k in 0..3 {
advance(lx) advance(lx)
@ -129,6 +138,156 @@ fn longString(ptr[Lexer] lx, Token t) -> str {
return body return body
} }
// "text" as written, up to the closing quote (which is skipped); escapes and \(...) are still in it
fn rawString(ptr[Lexer] lx, Token t) -> str {
advance(lx)
start := lx.i
while peek(lx, 0) != '"' {
if lx.i >= lx.src.len || peek(lx, 0) == '\n' {
fail(lx, t.line, t.col, "string is never closed (missing \")")
}
if peek(lx, 0) == '\\' && peek(lx, 1) == '(' {
advance(lx)
advance(lx)
skipEmbedded(lx, t)
continue
}
if peek(lx, 0) == '\\' {
advance(lx) // the escaped character can't end the string
}
advance(lx)
}
raw := lx.src.sub(start, lx.i)
advance(lx)
return raw
}
// past the code of a \( ... ), up to and including its )
fn skipEmbedded(ptr[Lexer] lx, Token t) {
depth := 1
while depth > 0 {
c := peek(lx, 0)
if lx.i >= lx.src.len || c == '\n' {
fail(lx, t.line, t.col, "a \\( in this text is never closed (missing ')')")
}
if c == '(' {
depth += 1
} else if c == ')' {
depth -= 1
} else if c == '"' || c == '\'' {
advance(lx)
while peek(lx, 0) != c {
if lx.i >= lx.src.len || peek(lx, 0) == '\n' {
fail(lx, t.line, t.col, "a text inside \\( ... ) is never closed")
}
if peek(lx, 0) == '\\' {
advance(lx)
}
advance(lx)
}
}
advance(lx)
}
}
fn punct(str k, Token at) -> Token {
return Token(kind: k, text: k, line: at.line, col: at.col, nl: false)
}
// The tokens of a text: one string, or for "x is \(x)" the tokens of ("x is " + str(x) + "").
// \(v, 2) is str(v, 2): a number with two digits after the point.
fn cook(ptr[Lexer] lx, str raw, Token t) -> []Token {
[]Token toks
[]str parts
pieces := 0
i := 0
while i < raw.len {
ch := raw[i]
if ch == '\\' && i + 1 < raw.len && raw[i + 1] == '(' {
j := i + 2
depth := 1
while depth > 0 {
c := raw[j]
if c == '(' {
depth += 1
} else if c == ')' {
depth -= 1
} else if c == '"' || c == '\'' {
j += 1
while raw[j] != c {
if raw[j] == '\\' {
j += 1
}
j += 1
}
}
j += 1
}
code := raw.sub(i + 2, j - 1)
if code.trim().len == 0 {
fail(lx, t.line, t.col, "\\( ) needs a value inside, like \\(x)")
}
if pieces == 0 {
open := punct("(", t)
open.nl = t.nl
toks.push(open)
}
toks.push(Token(kind: "string", text: parts.join(""), line: t.line, col: t.col, nl: false))
[]str none
parts = none
toks.push(punct("+", t))
toks.push(Token(kind: "ident", text: "str", line: t.line, col: t.col, nl: false))
toks.push(punct("(", t))
inner := lexFrom(code, lx.path, t.line, t.col + 3 + i)
for k in 0..inner.len - 1 {
tk := inner[k]
tk.nl = false
toks.push(tk)
}
toks.push(punct(")", t))
toks.push(punct("+", t))
pieces += 1
i = j
continue
}
if ch == '\\' {
i += 1
ch = escapeAt(lx, raw[i], t.line, t.col)
}
parts.push(str(ch))
i += 1
}
last := Token(kind: "string", text: parts.join(""), line: t.line, col: t.col, nl: false)
if pieces == 0 {
last.nl = t.nl
toks.push(last)
return toks
}
toks.push(last)
toks.push(punct(")", t))
return toks
}
fn escapeAt(ptr[Lexer] lx, char e, int line, int col) -> char {
if e == 'n' {
return '\n'
}
if e == 't' {
return '\t'
}
if e == 'r' {
return '\r'
}
if e == '0' {
return char(0)
}
if e == '\\' || e == '"' || e == '\'' {
return e
}
fail(lx, line, col, "unknown escape \\" + str(e) + " in this text (\\n \\t \\r \\0 \\\\ \\\" \\' and \\(value))")
return e
}
fn escape(ptr[Lexer] lx, char e) -> char { fn escape(ptr[Lexer] lx, char e) -> char {
if e == 'n' { if e == 'n' {
return '\n' return '\n'
@ -150,7 +309,12 @@ fn escape(ptr[Lexer] lx, char e) -> char {
} }
fn lex(str src, str path) -> []Token { fn lex(str src, str path) -> []Token {
lexer := Lexer(src: src, path: path, i: 0, line: 1, col: 1, nl: true) return lexFrom(src, path, 1, 1)
}
// `src` starts at this line and column of the file (the code inside a "\(...)")
fn lexFrom(str src, str path, int firstLine, int firstCol) -> []Token {
lexer := Lexer(src: src, path: path, i: 0, line: firstLine, col: firstCol, nl: true)
lx := addr(lexer) lx := addr(lexer)
[]Token out []Token out
while lx.i < lx.src.len { while lx.i < lx.src.len {
@ -237,8 +401,15 @@ fn lex(str src, str path) -> []Token {
t.text = (mant + lx.src.sub(expStart, lx.i)).replace("_", "") // LLVM wants the '.' t.text = (mant + lx.src.sub(expStart, lx.i)).replace("_", "") // LLVM wants the '.'
} }
} else if c == '"' && peek(lx, 1) == '"' && peek(lx, 2) == '"' { } else if c == '"' && peek(lx, 1) == '"' && peek(lx, 2) == '"' {
t.kind = "string" for tk in cook(lx, longString(lx, t), t) {
t.text = longString(lx, t) out.push(tk)
}
continue
} else if c == '"' {
for tk in cook(lx, rawString(lx, t), t) {
out.push(tk)
}
continue
} else if c == '"' || c == '\'' { } else if c == '"' || c == '\'' {
quote := c quote := c
t.kind = "string" t.kind = "string"

View file

@ -18,10 +18,108 @@ fn fingerprint(str text) -> str {
return parts.join("") return parts.join("")
} }
// two different 32-bit hashes: names a cached object after everything it was built from
fn fingerprint2(str text) -> str {
u32 h = 2166136261
u32 k = 5381
for c in text {
h = (h ^ u32(c)) * 16777619
k = k * 33 + u32(c)
}
digits := "0123456789abcdef"
[]str parts
for i in 0..8 {
parts.push(str(digits[int((h >> u32(28 - 4 * i)) & 15)]))
}
for i in 0..8 {
parts.push(str(digits[int((k >> u32(28 - 4 * i)) & 15)]))
}
return parts.join("")
}
fn quote(str a) -> str { fn quote(str a) -> str {
return host.quote(a) return host.quote(a)
} }
// The files a C file was built from, as the compiler's -MMD wrote them: "x.o: x.c x.h \\n y.h"
fn depFiles(str dep) -> []str {
colon := dep.find(": ")
[]str out
if colon < 0 {
return out
}
text := dep.sub(colon + 2, dep.len).replace("\\\r\n", " ").replace("\\\n", " ").replace("\\ ", str(char(1)))
for w in text.replace("\n", " ").replace("\t", " ").split(" ") {
if w.trim().len > 0 {
out.push(w.trim().replace(str(char(1)), " "))
}
}
return out
}
// what the files say now, as one fingerprint ("" if one is gone)
fn depsKey([]str files, str flags) -> str {
[]str all
all.push(flags)
for f in files {
text := read_file(f) or {
return ""
}
all.push(f)
all.push(text)
}
return fingerprint2(all.join("\n"))
}
// A C file of the program (link "x.c"), built once and kept until it or a header it includes changes.
fn cObject(str src, str cc, str flags, platform.Target tg, str log) -> str! {
dir := host.cacheDir() + "/c"
host.makeDirs(dir)
slot := dir + "/" + fingerprint2(src + "\n" + cc + "\n" + flags + "\n" + tg.triple)
obj := slot + ".o"
key := cc + flags + tg.triple
if file_exists(obj) && file_exists(slot + ".d") && file_exists(slot + ".key") {
was := read_file(slot + ".key") or ""
if was.len > 0 && was == depsKey(depFiles(read_file(slot + ".d") or ""), key) {
return obj
}
}
part := slot + ".part" + str(host.pid())
code := host.run(cc + flags + " -MMD -MF " + quote(slot + ".d") + " -c " + quote(src) + " -o " + quote(part) + " > " + quote(log) + " 2>&1")
if code != 0 {
host.remove(part)
return error((read_file(log) or "") + "error: couldn't compile " + src)
}
host.move(part, obj)
write_file(slot + ".key", depsKey(depFiles(read_file(slot + ".d") or ""), key))
return obj
}
// The program's IR, compiled once: the same IR with the same flags gives the same object.
// One slot per program (`name`), so the cache does not grow with every change.
fn irObject(str clang, str flags, str ir, str name, str log) -> str! {
dir := host.cacheDir() + "/ir"
host.makeDirs(dir)
slot := dir + "/" + fingerprint2(name + "\n" + flags)
obj := slot + ".o"
key := fingerprint2(clang + flags + "\n" + ir)
if file_exists(obj) && (read_file(slot + ".key") or "") == key {
return obj
}
ll := slot + ".part" + str(host.pid()) + ".ll"
part := slot + ".part" + str(host.pid()) + ".o"
write_file(ll, ir)
code := host.run(clang + flags + " -w -c " + quote(ll) + " -o " + quote(part) + " > " + quote(log) + " 2>&1")
host.remove(ll)
if code != 0 {
host.remove(part)
return error((read_file(log) or "") + "\nerror: clang couldn't compile the program's IR")
}
host.move(part, obj)
write_file(slot + ".key", key)
return obj
}
// "-lname", or whatever pkg-config says the library needs // "-lname", or whatever pkg-config says the library needs
fn libFlags(str name, platform.Target tg) -> str { fn libFlags(str name, platform.Target tg) -> str {
if name.ends_with(".framework") { if name.ends_with(".framework") {
@ -203,6 +301,9 @@ struct Build {
str extra // -g, -fPIC ... str extra // -g, -fPIC ...
bool shared bool shared
[]str links // what the program asked to link: a library name, a .c file (with its folder), a .o or .a []str links // what the program asked to link: a library name, a .c file (with its folder), a .o or .a
str name // the main file (names its slot in the cache)
str cflags // for the program's own C files: -I dirs, finch.toml's cflags
str ldflags // more for the linker: finch.toml's ldflags, -L dirs
} }
// IR -> program. 0 if it worked. // IR -> program. 0 if it worked.
@ -222,20 +323,13 @@ fn build(Build b, platform.Target tg, cimport.CImports ci) -> int {
print(err) print(err)
return 1 return 1
} }
ll := b.out + ".ll"
obj := b.out + ".o"
write_file(ll, b.ir)
target := "" target := ""
if tg.cross || tg.windows { if tg.cross || tg.windows {
target = " --target=" + tg.triple target = " --target=" + tg.triple
} }
log := b.out + ".log" log := b.out + ".log"
code := host.run(clang + target + " " + b.opt + b.extra + " -w -c " + quote(ll) + " -o " + quote(obj) + " > " + quote(log) + " 2>&1") obj := irObject(clang, target + " " + b.opt + b.extra, b.ir, b.name + "\n" + tg.triple, log) or {
host.remove(ll) print(err)
if code != 0 {
print(read_file(log) or "")
print("error: clang couldn't compile the program's IR")
host.remove(obj)
host.remove(log) host.remove(log)
return 1 return 1
} }
@ -243,6 +337,9 @@ fn build(Build b, platform.Target tg, cimport.CImports ci) -> int {
if !tg.msvc { if !tg.msvc {
cmd = cmd + " -lm" cmd = cmd + " -lm"
} }
if b.ldflags.len > 0 {
cmd = cmd + b.ldflags // finch.toml: libdirs and ldflags
}
if getenv("FINCH_LDFLAGS").len > 0 { if getenv("FINCH_LDFLAGS").len > 0 {
cmd = cmd + " " + getenv("FINCH_LDFLAGS") // more flags for the linker, like -L/where/libclang/is cmd = cmd + " " + getenv("FINCH_LDFLAGS") // more flags for the linker, like -L/where/libclang/is
} }
@ -253,24 +350,18 @@ fn build(Build b, platform.Target tg, cimport.CImports ci) -> int {
cmd = cmd + " -shared" cmd = cmd + " -shared"
} }
} }
[]str temps
for l in b.links { for l in b.links {
if l.ends_with(".c") { if l.ends_with(".c") {
// the program's own C file, built along with it // the program's own C file, built along with it (and kept in the cache until it changes)
o := b.out + "." + str(temps.len) + ".o"
pic := " -fPIC" pic := " -fPIC"
if tg.windows { if tg.windows {
pic = "" pic = ""
} }
c := host.run(cc + " -O2" + pic + " -c " + quote(l) + " -o " + quote(o) + " > " + quote(log) + " 2>&1") o := cObject(l, cc, " -O2" + pic + b.cflags, tg, log) or {
if c != 0 { print(err)
print((read_file(log) or "") + "error: couldn't compile " + l)
host.remove(obj)
host.remove(log) host.remove(log)
host.remove(o)
return 1 return 1
} }
temps.push(o)
cmd = cmd + " " + quote(o) cmd = cmd + " " + quote(o)
} else if l.ends_with(".o") || l.ends_with(".a") { } else if l.ends_with(".o") || l.ends_with(".a") {
cmd = cmd + " " + quote(l) cmd = cmd + " " + quote(l)
@ -280,11 +371,7 @@ fn build(Build b, platform.Target tg, cimport.CImports ci) -> int {
} }
status := host.run(cmd + " > " + quote(log) + " 2>&1") status := host.run(cmd + " > " + quote(log) + " 2>&1")
output := read_file(log) or "" output := read_file(log) or ""
host.remove(obj)
host.remove(log) host.remove(log)
for t in temps {
host.remove(t)
}
if status == 127 { if status == 127 {
print("error: no C compiler found to link with ('" + cc + "'; install one, or set FINCH_CC)") print("error: no C compiler found to link with ('" + cc + "'; install one, or set FINCH_CC)")
return 1 return 1

View file

@ -28,6 +28,14 @@ fn dirOf(str path) -> str {
return path.sub(0, i) return path.sub(0, i)
} }
// the path from the root, so the same file is the same in the cache wherever finch is started
fn absolute(str path) -> str {
if path.starts_with("/") || (path.len > 2 && path[1] == ':') {
return path
}
return host.cwd() + "/" + path
}
fn stem(str path) -> str { fn stem(str path) -> str {
name := path.sub(dirOf(path).len + 1, path.len) name := path.sub(dirOf(path).len + 1, path.len)
if dirOf(path) == "." && !path.starts_with("./") { if dirOf(path) == "." && !path.starts_with("./") {
@ -150,6 +158,7 @@ fn usage() {
print("usage:") print("usage:")
print(" finch run <file.fch> [args...] build and run a program") print(" finch run <file.fch> [args...] build and run a program")
print(" finch build <file.fch> [-o name] build a program") print(" finch build <file.fch> [-o name] build a program")
print(" finch test <file.fch> run its test \"...\" { } blocks (and those of the modules it imports)")
print(" finch ir <file.fch> print the LLVM IR") print(" finch ir <file.fch> print the LLVM IR")
print(" finch version show the version") print(" finch version show the version")
print(" finch lsp the language server, for editors (VS Code, Kate, ...)") print(" finch lsp the language server, for editors (VS Code, Kate, ...)")
@ -158,6 +167,8 @@ fn usage() {
print(" -l <lib> link a library (the same as link \"lib\" in the program)") print(" -l <lib> link a library (the same as link \"lib\" in the program)")
print(" -I <dir> look for imported C headers there too") print(" -I <dir> look for imported C headers there too")
print(" -g debug info (for gdb and lldb; use -O0 with it)") print(" -g debug info (for gdb and lldb; use -O0 with it)")
print(" --trace runtime errors show the chain of calls (finch run, finch test, -g and -O0 do it anyway)")
print(" --no-trace they don't (finch build without -g or -O0 doesn't, it costs speed)")
print(" --target <name> build for another system: windows, linux, arm64 (Linux), macos, or an LLVM triple") print(" --target <name> build for another system: windows, linux, arm64 (Linux), macos, or an LLVM triple")
exit(1) exit(1)
} }
@ -192,13 +203,15 @@ fn main([]str args) -> int {
} }
cmd := args[1] cmd := args[1]
file := args[2] file := args[2]
if cmd != "build" && cmd != "run" && cmd != "ir" && cmd != "index" { if cmd != "build" && cmd != "run" && cmd != "ir" && cmd != "index" && cmd != "test" {
usage() usage()
} }
out := stem(file) out := stem(file)
outGiven := false outGiven := false
shared := false shared := false
debug := false debug := false
trace := cmd != "build" // finch build: only with --trace, -g or -O0 (the chain costs speed)
traceGiven := false
targetName := "" targetName := ""
opt := "-O2" opt := "-O2"
[]str extraLibs []str extraLibs
@ -234,6 +247,12 @@ fn main([]str args) -> int {
ai += 1 ai += 1
} else if a == "-g" { } else if a == "-g" {
debug = true debug = true
} else if a == "--no-trace" {
trace = false
traceGiven = true
} else if a == "--trace" {
trace = true
traceGiven = true
} else if a == "--target" && ai + 1 < args.len { } else if a == "--target" && ai + 1 < args.len {
targetName = args[ai + 1] targetName = args[ai + 1]
ai += 1 ai += 1
@ -244,8 +263,11 @@ fn main([]str args) -> int {
} }
ai += 1 ai += 1
} }
if !traceGiven && (debug || opt == "-O0") {
trace = true
}
tg := platform.select(targetName) tg := platform.select(targetName)
if cmd == "run" { if cmd == "run" || cmd == "test" {
out = host.tempDir() + "/finch_run_" + str(host.pid()) + "_" + stem(file) + tg.exe out = host.tempDir() + "/finch_run_" + str(host.pid()) + "_" + stem(file) + tg.exe
} else if shared && !outGiven { } else if shared && !outGiven {
if tg.windows { if tg.windows {
@ -315,7 +337,13 @@ fn main([]str args) -> int {
print(err) print(err)
return 1 return 1
} }
ir := gen.generate(loader.progs, loader.files, ci, gen.Options(shared: shared, debug: debug, index: cmd == "index", cwd: host.cwd(), target: tg)) []str testModules
for prog in loader.progs {
if !prog.path.starts_with(loader.std + "/") {
testModules.push(prog.module) // your own files' tests, not the standard library's
}
}
ir := gen.generate(loader.progs, loader.files, ci, gen.Options(shared: shared, debug: debug, index: cmd == "index", cwd: host.cwd(), target: tg, test: cmd == "test", testModules: testModules, trace: trace))
if cmd == "ir" || cmd == "index" { if cmd == "ir" || cmd == "index" {
print(ir) print(ir)
return 0 return 0
@ -345,10 +373,14 @@ fn main([]str args) -> int {
if debug { if debug {
extra = extra + " -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
} }
if linker.build(linker.Build(out: out, ir: ir, rt: rt, opt: opt, extra: extra, shared: shared, links: links), tg, ci) != 0 { []str cflagList
for d in extraDirs {
cflagList.push(" -I" + linker.quote(d))
}
if linker.build(linker.Build(out: out, ir: ir, rt: rt, opt: opt, extra: extra, shared: shared, links: links, name: absolute(file), cflags: cflagList.join("")), tg, ci) != 0 {
return 1 return 1
} }
if cmd == "run" { if cmd == "run" || cmd == "test" {
[]str quoted []str quoted
for a in progArgs { for a in progArgs {
quoted.push(linker.quote(a)) quoted.push(linker.quote(a))

View file

@ -266,6 +266,19 @@ fn parseProgram(ptr[Parser] p) -> ast.Program {
prog.structs.push(parseStruct(p)) prog.structs.push(parseStruct(p))
} else if at(p, "const") { } else if at(p, "const") {
prog.consts.push(parseConst(p)) prog.consts.push(parseConst(p))
} else if at(p, "ident") && cur(p).text == "test" && peekKind(p, 1) == "string" {
// test "adds up" { ... }: a function finch test runs (its name is the text)
t := next(p)
f := node(p, "fn")
f.line = t.line
f.col = t.col
f.text = next(p).text
f.name = "__test_" + str(prog.tests.len)
f.ty = ast.simple("void")
f.kids.push(parseBlock(p))
f.endLine = f.kids[0].endLine
prog.tests.push(f.name)
prog.fns.push(f)
} else if at(p, "enum") { } else if at(p, "enum") {
e := parseEnum(p) e := parseEnum(p)
prog.enums.push(e) prog.enums.push(e)