"\(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 <errno.h>
#include <math.h>
#include <setjmp.h>
#include <stdarg.h>
#include <stdint.h>
#include <stdio.h>
@ -50,10 +51,38 @@ static char empty[1] = "";
static void (*fatal_hook)(const char *msg);
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) {
if (fatal_hook) fatal_hook(msg);
fflush(stdout);
fprintf(stderr, "%s\n", msg);
print_trace();
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_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 ----------
static void str_heap(FStr *out, const char *p, int64_t len) {

View file

@ -123,4 +123,5 @@ struct Program {
[]Node enums
[]Node fns
[]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
str cwd // the folder the compiler was started in (for the debug info)
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.
@ -163,6 +166,9 @@ struct Ref {
struct Gen {
[]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 files
[]str head
@ -782,6 +788,8 @@ fn boundsCheck(ptr[Gen] g, str i, str len, ast.Node at) {
// ---------- helper functions (generated once per type) ----------
struct Saved {
str frame
str frameLine
[]str body
[]str allocas
int tmp
@ -790,8 +798,10 @@ struct 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.frame = ""
g.frameLine = ""
[]str empty
g.body = empty
g.allocas = empty
@ -807,6 +817,8 @@ fn endHelper(ptr[Gen] g, Saved s, str header) {
g.tmp = s.tmp
g.term = s.term
g.dbgScope = s.dbgScope
g.frame = s.frame
g.frameLine = s.frameLine
}
// for i in 0..n, inside the current function
@ -2258,6 +2270,25 @@ fn define(ptr[Gen] g, int si) {
[]Scope none
g.scopes = none
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
if g.cur.recv.len > 0 {
// 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) {
g.dbgLine = s.line
g.dbgCol = s.col
if g.frameLine.len > 0 && !g.term {
emit(g, "store i64 " + str(s.line) + ", ptr " + g.frameLine)
}
k := s.kind
if k == "block" {
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(", ") + ")"
savedTrace := traceSave(g)
retLL := "void"
retExt := ""
if sig.ret.kind == "void" {
emit(g, text)
tracedEmit(g, text, savedTrace)
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"))
emit(g, "ret i8 " + op(g, "zext i1 " + tracedCall(g, text, savedTrace) + " to i8"))
} else {
retLL = llty(g, sig.ret)
retExt = cExt(g, sig.ret)
emit(g, "ret " + retLL + " " + op(g, text))
emit(g, "ret " + retLL + " " + tracedCall(g, text, savedTrace))
}
dll := ""
if g.tg.windows {
@ -4753,6 +4788,29 @@ fn exportWrapper(ptr[Gen] g, FnSig sig) -> str {
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.
fn cThunk(ptr[Gen] g, int fi, ast.Node at) -> str {
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(", ") + ")"
savedTrace := traceSave(g)
retExt := ""
if sig.ret.kind == "void" {
emit(g, text)
tracedEmit(g, text, savedTrace)
emit(g, "ret void")
} 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")
} else if pl.ret.kind == "expand" {
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))
} else {
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(", ") + ")")
return name
@ -5329,6 +5388,9 @@ fn call(ptr[Gen] g, ast.Node c) -> Val {
if ["min", "max", "abs", "clamp"].contains(n) {
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 {
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
// 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 {
n := c.name
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 + "(...)")
}
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 {
r := op(g, text)
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 @finch_args(ptr, i32, 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 i32 @finch_file_exists(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 + 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 {
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
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.trace = opt.trace && !opt.shared
if g.debug {
// !0 the compile unit, !1 and !2 the module flags, then the source files
g.md.push("")
@ -6149,6 +6300,15 @@ fn generate([]ast.Program progs, []str files, cimport.CImports ci, Options opt)
}
declare(g)
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 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")
@ -6212,6 +6372,6 @@ fn generate([]ast.Program progs, []str files, cimport.CImports ci, Options opt)
callText = "%r = " + callText
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)
}

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
// 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 {
for k in 0..3 {
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 \"\"\")")
}
ch := peek(lx, 0)
if ch == '\\' && peek(lx, 1) == '(' {
start := lx.i
advance(lx)
if ch == '\\' {
e := peek(lx, 0)
advance(lx)
ch = escape(lx, e)
} else if ch == '\r' {
skipEmbedded(lx, t)
parts.push(lx.src.sub(start, lx.i))
continue
}
advance(lx)
if ch == '\r' {
continue
}
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 {
advance(lx)
@ -129,6 +138,156 @@ fn longString(ptr[Lexer] lx, Token t) -> str {
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 {
if e == 'n' {
return '\n'
@ -150,7 +309,12 @@ fn escape(ptr[Lexer] lx, char e) -> char {
}
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)
[]Token out
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 '.'
}
} else if c == '"' && peek(lx, 1) == '"' && peek(lx, 2) == '"' {
t.kind = "string"
t.text = longString(lx, t)
for tk in cook(lx, longString(lx, t), 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 == '\'' {
quote := c
t.kind = "string"

View file

@ -18,10 +18,108 @@ fn fingerprint(str text) -> str {
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 {
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
fn libFlags(str name, platform.Target tg) -> str {
if name.ends_with(".framework") {
@ -203,6 +301,9 @@ struct Build {
str extra // -g, -fPIC ...
bool shared
[]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.
@ -222,20 +323,13 @@ fn build(Build b, platform.Target tg, cimport.CImports ci) -> int {
print(err)
return 1
}
ll := b.out + ".ll"
obj := b.out + ".o"
write_file(ll, b.ir)
target := ""
if tg.cross || tg.windows {
target = " --target=" + tg.triple
}
log := b.out + ".log"
code := host.run(clang + target + " " + b.opt + b.extra + " -w -c " + quote(ll) + " -o " + quote(obj) + " > " + quote(log) + " 2>&1")
host.remove(ll)
if code != 0 {
print(read_file(log) or "")
print("error: clang couldn't compile the program's IR")
host.remove(obj)
obj := irObject(clang, target + " " + b.opt + b.extra, b.ir, b.name + "\n" + tg.triple, log) or {
print(err)
host.remove(log)
return 1
}
@ -243,6 +337,9 @@ fn build(Build b, platform.Target tg, cimport.CImports ci) -> int {
if !tg.msvc {
cmd = cmd + " -lm"
}
if b.ldflags.len > 0 {
cmd = cmd + b.ldflags // finch.toml: libdirs and ldflags
}
if getenv("FINCH_LDFLAGS").len > 0 {
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"
}
}
[]str temps
for l in b.links {
if l.ends_with(".c") {
// the program's own C file, built along with it
o := b.out + "." + str(temps.len) + ".o"
// the program's own C file, built along with it (and kept in the cache until it changes)
pic := " -fPIC"
if tg.windows {
pic = ""
}
c := host.run(cc + " -O2" + pic + " -c " + quote(l) + " -o " + quote(o) + " > " + quote(log) + " 2>&1")
if c != 0 {
print((read_file(log) or "") + "error: couldn't compile " + l)
host.remove(obj)
o := cObject(l, cc, " -O2" + pic + b.cflags, tg, log) or {
print(err)
host.remove(log)
host.remove(o)
return 1
}
temps.push(o)
cmd = cmd + " " + quote(o)
} else if l.ends_with(".o") || l.ends_with(".a") {
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")
output := read_file(log) or ""
host.remove(obj)
host.remove(log)
for t in temps {
host.remove(t)
}
if status == 127 {
print("error: no C compiler found to link with ('" + cc + "'; install one, or set FINCH_CC)")
return 1

View file

@ -28,6 +28,14 @@ fn dirOf(str path) -> str {
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 {
name := path.sub(dirOf(path).len + 1, path.len)
if dirOf(path) == "." && !path.starts_with("./") {
@ -150,6 +158,7 @@ fn usage() {
print("usage:")
print(" finch run <file.fch> [args...] build and run 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 version show the version")
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(" -I <dir> look for imported C headers there too")
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")
exit(1)
}
@ -192,13 +203,15 @@ fn main([]str args) -> int {
}
cmd := args[1]
file := args[2]
if cmd != "build" && cmd != "run" && cmd != "ir" && cmd != "index" {
if cmd != "build" && cmd != "run" && cmd != "ir" && cmd != "index" && cmd != "test" {
usage()
}
out := stem(file)
outGiven := false
shared := false
debug := false
trace := cmd != "build" // finch build: only with --trace, -g or -O0 (the chain costs speed)
traceGiven := false
targetName := ""
opt := "-O2"
[]str extraLibs
@ -234,6 +247,12 @@ fn main([]str args) -> int {
ai += 1
} else if a == "-g" {
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 {
targetName = args[ai + 1]
ai += 1
@ -244,8 +263,11 @@ fn main([]str args) -> int {
}
ai += 1
}
if !traceGiven && (debug || opt == "-O0") {
trace = true
}
tg := platform.select(targetName)
if cmd == "run" {
if cmd == "run" || cmd == "test" {
out = host.tempDir() + "/finch_run_" + str(host.pid()) + "_" + stem(file) + tg.exe
} else if shared && !outGiven {
if tg.windows {
@ -315,7 +337,13 @@ fn main([]str args) -> int {
print(err)
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" {
print(ir)
return 0
@ -345,10 +373,14 @@ fn main([]str args) -> int {
if debug {
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
}
if cmd == "run" {
if cmd == "run" || cmd == "test" {
[]str quoted
for a in progArgs {
quoted.push(linker.quote(a))

View file

@ -266,6 +266,19 @@ fn parseProgram(ptr[Parser] p) -> ast.Program {
prog.structs.push(parseStruct(p))
} else if at(p, "const") {
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") {
e := parseEnum(p)
prog.enums.push(e)