mirror of
https://github.com/Nexoniarz/Finch.git
synced 2026-11-03 16:54:25 +00:00
"\(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:
parent
3ba2dcd5d6
commit
c830c780fe
8 changed files with 582 additions and 45 deletions
|
|
@ -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
178
src/gen.fch
178
src/gen.fch
|
|
@ -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)
|
||||
}
|
||||
|
|
|
|||
187
src/lexer.fch
187
src/lexer.fch
|
|
@ -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)
|
||||
advance(lx)
|
||||
if ch == '\\' {
|
||||
e := peek(lx, 0)
|
||||
if ch == '\\' && peek(lx, 1) == '(' {
|
||||
start := lx.i
|
||||
advance(lx)
|
||||
ch = escape(lx, e)
|
||||
} else if ch == '\r' {
|
||||
advance(lx)
|
||||
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"
|
||||
|
|
|
|||
131
src/linker.fch
131
src/linker.fch
|
|
@ -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
|
||||
|
|
|
|||
42
src/main.fch
42
src/main.fch
|
|
@ -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))
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
Loading…
Reference in a new issue