finch.toml, threads and atomics, finch fmt, C++ files, struct #defines, a struct variable given for its ptr

- finch.toml: name, entry, paths, include, links, libdirs, cflags, ldflags, and [target.windows] etc.;
  finch run / build / test with no file use its entry
- import thread: spawn, join, mutex, cpus; atomic_add/sub/load/store/swap/cas built in
- finch fmt [--check]: indentation by { }, trailing spaces, empty lines; also as LSP formatting
- link "x.cpp" (or in finch.toml): built with the C++ compiler, which then links (FINCH_CXX)
- #define RAYWHITE (Color){ 245, 245, 245, 255 } imports as a Color
- push(body) where push takes ptr[Body]: the same as push(addr(body))
- --target android (NDK clang from ANDROID_NDK_HOME, -llog)

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 11:17:10 +01:00
commit d428498efd
40 changed files with 1398 additions and 18 deletions

View file

@ -28,6 +28,18 @@ struct CConst { // enum values and #define constants
str text // a float as written, so no digits are lost: 3.14159265358979323846
bool isStr // #define NAME "text"
str s
str structName // #define RAYWHITE (Color){ 245, 245, 245, 255 }: the struct, and its fields in order
[]str fields // each as a Finch literal: 245, 0.5
[]bool fieldFloat
}
// what visitMacroVar looks for inside (Color){ ... }
struct InitFind {
bool found
CXCursor list
bool ok
[]str fields
[]bool floats
}
struct CField {
@ -381,7 +393,7 @@ fn macroTokens(ptr[Importer] imp, CXCursor c) -> MacroTokens {
u32 n = 0
clang_tokenize(imp.tu, clang_getCursorExtent(c), addr(tokens), addr(n))
out := MacroTokens(safe: true)
allowed := ["(", ")", "+", "-", "*", "/", "%", "<<", ">>", "|", "&", "^", "~", "!", "<", ">", "<=", ">=", "==", "!=", "&&", "||", "?", ":", ","]
allowed := ["(", ")", "+", "-", "*", "/", "%", "<<", ">>", "|", "&", "^", "~", "!", "<", ">", "<=", ">=", "==", "!=", "&&", "||", "?", ":", ",", "{", "}"]
words := ["sizeof", "unsigned", "signed", "int", "long", "short", "char", "float", "double", "const", "_Bool"]
depth := 0
for k in 1..int(n) { // token 0 is the macro's name
@ -678,6 +690,72 @@ fn evalMacros(ptr[Importer] imp, ptr index, Header h, []ptr[char] argv) {
clang_disposeTranslationUnit(tu)
}
// a float as a Finch literal: 2.0, not 2
fn floatText(float v) -> str {
t := str(v)
if !t.contains(".") && !t.contains("e") {
t = t + ".0"
}
return t
}
// (Color){ 245, 245, 245, 255 }: a struct whose fields are all numbers, in order
fn structMacro(ptr[Importer] imp, str name, CXCursor c, CXType t) {
sname := recordName(imp, t)
if !imp.out.structs.has(sname) {
return
}
find := InitFind(ok: true)
clang_visitChildren(c, addr(visitFindInit), addr(find))
if !find.found {
return
}
clang_visitChildren(find.list, addr(visitInitField), addr(find))
if !find.ok || find.fields.len == 0 || find.fields.len > imp.out.structs[sname].fields.len {
return
}
imp.out.consts[name] = CConst(structName: sname, fields: find.fields, fieldFloat: find.floats)
}
fn visitFindInit(CXCursor c, CXCursor parent, ptr data) -> i32 {
ptr[InitFind] f = data
if clang_getCursorKind(c) == CXCursor_InitListExpr {
f.found = true
f.list = c
return CXChildVisit_Break
}
return CXChildVisit_Recurse
}
fn visitInitField(CXCursor c, CXCursor parent, ptr data) -> i32 {
ptr[InitFind] f = data
r := clang_Cursor_Evaluate(c)
if r == null {
f.ok = false
return CXChildVisit_Break
}
k := clang_EvalResult_getKind(r)
if k == CXEval_Int {
f.fields.push(str(int(clang_EvalResult_getAsLongLong(r))))
f.floats.push(false)
} else if k == CXEval_Float {
v := clang_EvalResult_getAsDouble(r)
if v == v && v - v == 0.0 {
f.fields.push(floatText(v))
f.floats.push(true)
} else {
f.ok = false
}
} else {
f.ok = false // a struct in the struct, a pointer, a text: not a simple constant
}
clang_EvalResult_dispose(r)
if !f.ok {
return CXChildVisit_Break
}
return CXChildVisit_Continue
}
fn visitMacroVar(CXCursor c, CXCursor parent, ptr data) -> i32 {
ptr[Importer] imp = data
if clang_getCursorKind(c) != CXCursor_VarDecl {
@ -688,8 +766,16 @@ fn visitMacroVar(CXCursor c, CXCursor parent, ptr data) -> i32 {
return CXChildVisit_Continue
}
name = name.sub(10, name.len)
if imp.out.consts.has(name) {
return CXChildVisit_Continue
}
t := clang_getCanonicalType(clang_getCursorType(c))
if t.kind == CXType_Record {
structMacro(imp, name, c, t)
return CXChildVisit_Continue
}
r := clang_Cursor_Evaluate(c)
if r == null || imp.out.consts.has(name) {
if r == null {
return CXChildVisit_Continue
}
k := clang_EvalResult_getKind(r)
@ -698,7 +784,7 @@ fn visitMacroVar(CXCursor c, CXCursor parent, ptr data) -> i32 {
} else if k == CXEval_Float {
v := clang_EvalResult_getAsDouble(r)
if v == v && v - v == 0.0 { // not nan or inf
imp.out.consts[name] = CConst(isFloat: true, f: v, text: str(v))
imp.out.consts[name] = CConst(isFloat: true, f: v, text: floatText(v))
}
} else if k == CXEval_StrLiteral {
imp.out.consts[name] = CConst(isStr: true, s: clang_EvalResult_getAsStr(r))

View file

@ -26,6 +26,12 @@ fn builtins() -> []Doc {
doc("max", "max(a, b, ...) -> number", "The biggest of the numbers, in the type they all fit."),
doc("abs", "abs(x) -> number", "x without its minus sign (any number type)."),
doc("clamp", "clamp(x, lo, hi) -> number", "x, but at least lo and at most hi."),
doc("atomic_add", "atomic_add(ptr[int] p, n) -> int", "Adds n to the number at p in one step other threads can't split; gives back what was there before."),
doc("atomic_sub", "atomic_sub(ptr[int] p, n) -> int", "Takes n from the number at p in one step; gives back what was there before."),
doc("atomic_load", "atomic_load(ptr[int] p) -> int", "Reads the number at p, as another thread last wrote it."),
doc("atomic_store", "atomic_store(ptr[int] p, n)", "Writes n to p so other threads see it whole."),
doc("atomic_swap", "atomic_swap(ptr[int] p, n) -> int", "Puts n at p and gives back what was there, in one step."),
doc("atomic_cas", "atomic_cas(ptr[int] p, old, new) -> bool", "Puts new at p only if old is there (compare and swap); true if it did."),
doc("addr", "addr(x) -> ptr[T]", "A pointer to a variable, field or element. addr(function) gives C a callback."),
doc("new", "new(value) -> ptr[T]", "Puts a copy of the value on the heap. Give it back with free(...)."),
doc("free", "free(ptr p)", "Gives back memory from new(...) (or from C), freeing what the value owns."),

File diff suppressed because one or more lines are too long

224
src/fmt.fch Normal file
View file

@ -0,0 +1,224 @@
// finch fmt: the indentation by { }, 4 spaces a level; no spaces at line ends; at most one empty line
// in a row; one line end at the end of the file. Nothing else is moved: lines that go on inside ( or [,
// """ text and /* */ comments keep their shape, shifted along with the line they belong to.
struct Scan {
int braces // { } open at the start of the line
int parens // ( [ open
bool inLong // inside """ text
bool inBlock // inside /* */
char last // the last thing of code on the line ('"' for text): + or , means the next line goes on
}
// how deep a line's leading spaces go (a tab is 4)
fn indentOf(str line) -> int {
n := 0
for c in line {
if c == ' ' {
n += 1
} else if c == '\t' {
n += 4
} else {
break
}
}
return n
}
// how many spaces and tabs a line starts with
fn leading(str line) -> int {
i := 0
while i < line.len && (line[i] == ' ' || line[i] == '\t') {
i += 1
}
return i
}
fn trimEnd(str line) -> str {
n := line.len
while n > 0 && (line[n - 1] == ' ' || line[n - 1] == '\t' || line[n - 1] == '\r') {
n -= 1
}
return line.sub(0, n)
}
fn spaces(int n) -> str {
[]str out
for i in 0..n {
out.push(" ")
}
return out.join("")
}
fn startsWith(str line, int i, str what) -> bool {
return i + what.len <= line.len && line.sub(i, i + what.len) == what
}
// past one line: what is open at its end
fn scanLine(str line, ptr[Scan] s) {
s.last = ' '
i := 0
while i < line.len {
c := line[i]
if s.inLong {
if startsWith(line, i, "\"\"\"") {
s.inLong = false
i += 3
} else {
i += 1
}
continue
}
if s.inBlock {
if startsWith(line, i, "*/") {
s.inBlock = false
i += 2
} else {
i += 1
}
continue
}
if startsWith(line, i, "//") {
return
}
if startsWith(line, i, "/*") {
s.inBlock = true
i += 2
continue
}
if startsWith(line, i, "\"\"\"") {
s.inLong = true
i += 3
continue
}
if c == '"' {
i = pastString(line, i + 1)
s.last = '"'
continue
}
if c == '\'' {
i += 1
while i < line.len && line[i] != '\'' {
if line[i] == '\\' {
i += 1
}
i += 1
}
i += 1
s.last = '"'
continue
}
if c != ' ' && c != '\t' && c != '\r' {
s.last = c
}
if c == '{' {
s.braces += 1
} else if c == '}' {
s.braces -= 1
} else if c == '(' || c == '[' {
s.parens += 1
} else if c == ')' || c == ']' {
s.parens -= 1
}
i += 1
}
}
// past "text" (i is just after the opening quote), with \(code) that may have its own "text" in it
fn pastString(str line, int start) -> int {
i := start
while i < line.len {
c := line[i]
if c == '"' {
return i + 1
}
if c == '\\' && i + 1 < line.len && line[i + 1] == '(' {
depth := 1
i += 2
while i < line.len && depth > 0 {
if line[i] == '"' {
i = pastString(line, i + 1)
continue
}
if line[i] == '(' {
depth += 1
} else if line[i] == ')' {
depth -= 1
}
i += 1
}
continue
}
if c == '\\' {
i += 1
}
i += 1
}
return i
}
fn format(str text) -> str {
lines := text.replace("\r\n", "\n").split("\n")
[]str out
s := Scan()
stmtWas := 0 // where the line that started the statement was, and where it is now
stmtNow := 0
blank := 0
for raw in lines {
goesOn := s.last == '+' || s.last == '-' || s.last == '*' || s.last == '/' || s.last == ',' || s.last == '=' || s.last == '|' || s.last == '&' || s.last == '%'
own := !s.inLong && !s.inBlock && s.parens <= 0 && !goesOn // the line starts something of its own
if s.inLong {
// """ text as it is, moved with its statement (its closing line says how much is cut off)
out.push(shifted(raw, stmtNow - stmtWas))
scanLine(raw, addr(s))
continue
}
line := trimEnd(raw)
if line.len == 0 {
if s.inBlock {
out.push("")
} else {
blank += 1
}
continue
}
if blank > 0 && out.len > 0 {
out.push("")
}
blank = 0
body := line.sub(leading(line), line.len)
if own {
depth := s.braces
if body.starts_with("}") {
depth -= 1
}
if depth < 0 {
depth = 0
}
stmtWas = indentOf(line)
stmtNow = depth * 4
out.push(spaces(stmtNow) + body)
} else {
out.push(shifted(line, stmtNow - stmtWas))
}
scanLine(line, addr(s))
}
while out.len > 0 && out[out.len - 1].len == 0 {
out.pop()
}
return out.join("\n") + "\n"
}
// the line moved right (or left) by `by`, keeping what it has after its indentation
fn shifted(str line, int by) -> str {
n := indentOf(line)
body := line.sub(leading(line), line.len)
if body.len == 0 {
return line
}
to := n + by
if to < 0 {
to = 0
}
return spaces(to) + body
}

View file

@ -3458,6 +3458,19 @@ fn orElse(ptr[Gen] g, ast.Node e, bool discard) -> Val {
return Val(v: op(g, "phi " + llty(g, v.ty) + " [ " + fv + ", %" + badEnd + " ], [ " + v.v + ", %" + ok + " ]"), ty: v.ty, fresh: owning(g, v.ty))
}
// An argument: a struct variable given where a ptr to that struct goes is passed by its address,
// as if it were addr(x) (worldDef for b2CreateWorld(const b2WorldDef*), body for move(ptr[Body] b))
fn argWant(ptr[Gen] g, ast.Node a, ast.Type want) -> Val {
if a.kind == "var" && want.kind == "ptr" && want.elem.len == 1 && want.elem[0].kind == "struct" {
id := lookup(g, a.name)
if id >= 0 && ast.same(varAt(g, id).ty, want.elem[0]) {
pl := place(g, a, true)
return Val(v: pl.addr, ty: ast.ptrTo(pl.ty))
}
}
return exprWant(g, a, want)
}
fn exprWant(ptr[Gen] g, ast.Node e, ast.Type want) -> Val {
if e.kind == "array" {
return arrayLit(g, e, want)
@ -3900,6 +3913,21 @@ fn ref(ptr[Gen] g, ast.Node e, bool write) -> Ref {
id := lookup(g, e.name)
if id < 0 && g.ci.consts.has(e.name) {
c := g.ci.consts[e.name]
if c.structName.len > 0 {
// RAYWHITE: Color(245, 245, 245, 255)
si := findStruct(g, "C", c.structName)
[]ast.Node args
[]str names
for i in 0..c.fields.len {
kind := "int"
if c.fieldFloat[i] {
kind = "float"
}
args.push(ast.Node(kind: kind, text: c.fields[i], line: e.line, col: e.col, file: e.file))
names.push(g.structs[si].fnames[i]) // by name: C leaves the fields not given at 0
}
return Ref(isPlace: false, val: construct(g, si, args, names, e))
}
if c.isStr {
return Ref(isPlace: false, val: strConst(g, c.s))
}
@ -4986,7 +5014,7 @@ fn callC(ptr[Gen] g, ast.Node c, cimport.CFunc f) -> Val {
if i < f.params.len {
pt := ptypes[i]
a := pl.params[i]
v := coerce(g, exprWant(g, c.kids[i], pt), pt, c.kids[i], "argument " + str(i + 1) + " of " + f.name)
v := coerce(g, argWant(g, c.kids[i], pt), pt, c.kids[i], "argument " + str(i + 1) + " of " + f.name)
temps.push(v)
hold(g, v)
if a.kind == "memory" {
@ -5391,6 +5419,9 @@ fn call(ptr[Gen] g, ast.Node c) -> Val {
if n == "assert" {
return assertCall(g, c)
}
if ["atomic_add", "atomic_sub", "atomic_load", "atomic_store", "atomic_swap", "atomic_cas"].contains(n) {
return atomicCall(g, c)
}
if lookup(g, n) >= 0 {
fail(g, c, "'" + n + "' is a variable, not a function")
}
@ -5502,6 +5533,55 @@ fn mathCall(ptr[Gen] g, ast.Node c) -> Val {
return out
}
fn atomicAlign(ptr[Gen] g, ast.Type t) -> str {
return str(bitsOf(t) / 8)
}
// atomic_add(p, n) and the rest: one step on a whole number that other threads see all at once.
// p is a ptr[int] (or any whole number type) to it; add, sub and swap give back what was there before,
// cas(p, old, new) puts new there only if old is there, and says whether it did.
fn atomicCall(ptr[Gen] g, ast.Node c) -> Val {
n := c.name
if g.index {
noteRef(g, c, c.col, n.len, docs.hover(docs.find(docs.builtins(), n)), "", 0, 0)
}
want := 2
if n == "atomic_load" {
want = 1
} else if n == "atomic_cas" {
want = 3
}
checkArgs(g, c, 0, want, n)
p := expr(g, c.kids[0])
if p.ty.kind != "ptr" || p.ty.elem.len == 0 || !isInt(p.ty.elem[0]) {
fail(g, c.kids[0], n + "(...) works on a whole number through its address, like " + n + "(addr(count), ...) -- this is " + ast.show(p.ty))
}
t := p.ty.elem[0]
lt := llty(g, t)
[]str vs
for i in 1..c.kids.len {
vs.push(coerce(g, exprWant(g, c.kids[i], t), t, c.kids[i], "the value").v)
}
if n == "atomic_load" {
return Val(v: op(g, "load atomic " + lt + ", ptr " + p.v + " seq_cst, align " + atomicAlign(g, t)), ty: t)
}
if n == "atomic_store" {
emit(g, "store atomic " + lt + " " + vs[0] + ", ptr " + p.v + " seq_cst, align " + atomicAlign(g, t))
return Val(ty: T("void"))
}
if n == "atomic_cas" {
r := op(g, "cmpxchg ptr " + p.v + ", " + lt + " " + vs[0] + ", " + lt + " " + vs[1] + " seq_cst seq_cst")
return Val(v: op(g, "extractvalue { " + lt + ", i1 } " + r + ", 1"), ty: T("bool"))
}
how := "add"
if n == "atomic_sub" {
how = "sub"
} else if n == "atomic_swap" {
how = "xchg"
}
return Val(v: op(g, "atomicrmw " + how + " ptr " + p.v + ", " + lt + " " + vs[0] + " seq_cst"), ty: t)
}
// the smaller (o "<") or bigger (o ">") of two numbers, in the type both fit
fn pick(ptr[Gen] g, str o, Val a, Val b, ast.Node at) -> Val {
l := a
@ -5537,7 +5617,7 @@ fn callFn(ptr[Gen] g, int fi, ast.Node c, int first, str shown, str selfAddr, st
mark := g.pending.len
for k in 0..sig.params.len {
a := c.kids[first + k]
v := coerce(g, exprWant(g, a, sig.params[k]), sig.params[k], a, "argument '" + sig.pnames[k] + "'")
v := coerce(g, argWant(g, a, sig.params[k]), sig.params[k], a, "argument '" + sig.pnames[k] + "'")
// the argument lives inside the receiver (c.merge(c)): the method gets its own copy
if owning(g, sig.params[k]) && !v.fresh && selfRoot.len > 0 && rootVar(a) == selfRoot {
v = Val(v: copyValue(g, v.v, v.ty), ty: v.ty, fresh: true)

View file

@ -81,3 +81,18 @@ fn removeTree(str path) {
fn move(str from, str to) {
shell("mv " + quote(from) + " " + quote(to))
}
// the files under dir (and its folders) whose names end with ext, like ".fch"
fn listFiles(str dir, str ext) -> []str {
tmp := tempDir() + "/finch_files_" + str(pid()) + ".txt"
run("find " + quote(dir) + " -type f -name " + quote("*" + ext) + " > " + quote(tmp) + " 2> " + nullDevice())
text := read_file(tmp) or ""
remove(tmp)
[]str out
for line in text.replace("\r", "").split("\n") {
if line.ends_with(ext) {
out.push(line)
}
}
return out
}

View file

@ -102,3 +102,18 @@ fn move(str from, str to) {
delete_file(to)
rename(from, to)
}
// the files under dir (and its folders) whose names end with ext, like ".fch"
fn listFiles(str dir, str ext) -> []str {
tmp := tempDir() + "/finch_files_" + str(pid()) + ".txt"
run("dir /s /b " + quote(dir.replace("/", "\\") + "\\*" + ext) + " > " + quote(tmp) + " 2> " + nullDevice())
text := read_file(tmp) or ""
remove(tmp)
[]str out
for line in text.replace("\r", "").split("\n") {
if line.ends_with(ext) {
out.push(line)
}
}
return out
}

View file

@ -41,6 +41,33 @@ fn quote(str a) -> str {
return host.quote(a)
}
// a file of the program's own (next to the .fch file), not a library's name
fn isOwnCode(str l) -> bool {
return l.ends_with(".c") || l.ends_with(".o") || l.ends_with(".a") || l.ends_with(".lib") || isCpp(l)
}
fn isCpp(str l) -> bool {
return l.ends_with(".cpp") || l.ends_with(".cc") || l.ends_with(".cxx")
}
// the C++ compiler that goes with a C compiler: clang -> clang++, x86_64-w64-mingw32-gcc -> ...-g++, cc -> c++
fn cxxFor(str cc) -> str {
if getenv("FINCH_CXX").len > 0 {
return getenv("FINCH_CXX")
}
words := cc.split(" ")
first := words[0]
if first.ends_with("clang") {
first = first + "++"
} else if first.ends_with("gcc") {
first = first.sub(0, first.len - 3) + "g++"
} else if first.ends_with("cc") {
first = first.sub(0, first.len - 2) + "c++"
}
words[0] = first
return words.join(" ")
}
// 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(": ")
@ -333,10 +360,22 @@ fn build(Build b, platform.Target tg, cimport.CImports ci) -> int {
host.remove(log)
return 1
}
cmd := cc + " " + quote(obj) + " " + quote(rtobj) + " -o " + quote(b.out)
// with C++ files the C++ compiler links, so its library comes along
linkWith := cc
for l in b.links {
if isCpp(l) {
linkWith = cxxFor(cc)
}
}
cmd := linkWith + " " + quote(obj) + " " + quote(rtobj) + " -o " + quote(b.out)
if !tg.msvc {
cmd = cmd + " -lm"
}
if tg.android {
cmd = cmd + " -llog" // bionic has threads built in
} else if !tg.windows && !tg.darwin {
cmd = cmd + " -lpthread" // std/thread (in glibc since 2.34, needed before)
}
if b.ldflags.len > 0 {
cmd = cmd + b.ldflags // finch.toml: libdirs and ldflags
}
@ -351,19 +390,23 @@ fn build(Build b, platform.Target tg, cimport.CImports ci) -> int {
}
}
for l in b.links {
if l.ends_with(".c") {
// the program's own C file, built along with it (and kept in the cache until it changes)
if l.ends_with(".c") || isCpp(l) {
// the program's own C or C++ file, built along with it (and kept in the cache until it changes)
pic := " -fPIC"
if tg.windows {
pic = ""
}
o := cObject(l, cc, " -O2" + pic + b.cflags, tg, log) or {
compiler := cc
if isCpp(l) {
compiler = cxxFor(cc)
}
o := cObject(l, compiler, " -O2" + pic + b.cflags, tg, log) or {
print(err)
host.remove(log)
return 1
}
cmd = cmd + " " + quote(o)
} else if l.ends_with(".o") || l.ends_with(".a") {
} else if l.ends_with(".o") || l.ends_with(".a") || l.ends_with(".lib") {
cmd = cmd + " " + quote(l)
} else {
cmd = cmd + " " + libFlags(l, tg)

View file

@ -7,6 +7,7 @@
import json
import host
import docs
import fmt
import "stdio.h"
import "stdlib.h"
@ -785,6 +786,7 @@ fn Server.answer(json.Json msg) {
caps.set("hoverProvider", json.new_bool(true))
caps.set("definitionProvider", json.new_bool(true))
caps.set("documentSymbolProvider", json.new_bool(true))
caps.set("documentFormattingProvider", json.new_bool(true))
comp := json.new_object()
dot := json.new_list()
dot.push(json.new_text("."))
@ -839,6 +841,32 @@ fn Server.answer(json.Json msg) {
empty.set("diagnostics", json.new_list())
notify("textDocument/publishDiagnostics", empty)
}
"textDocument/formatting" -> {
// finch fmt on the open text: one edit that puts the whole text in its place
path := pathOf(params)
edits := json.new_list()
if self.docs.has(path) {
old := self.docs[path]
neat := fmt.format(old)
if neat != old {
lines := old.split("\n")
whole := json.new_object()
start := json.new_object()
start.set("line", num(0))
start.set("character", num(0))
end := json.new_object()
end.set("line", num(lines.len))
end.set("character", num(0))
whole.set("start", start)
whole.set("end", end)
edit := json.new_object()
edit.set("range", whole)
edit.set("newText", json.new_text(neat))
edits.push(edit)
}
}
reply(id, edits)
}
"textDocument/hover", "textDocument/definition", "textDocument/completion", "textDocument/documentSymbol", "textDocument/signatureHelp" -> {
path := pathOf(params)
line := params.get("position").get("line").int_or(0) + 1

View file

@ -15,6 +15,8 @@ import linker
import lsp
import host
import embedded
import manifest
import fmt
import "stdlib.h"
fn dirOf(str path) -> str {
@ -160,6 +162,8 @@ fn usage() {
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 fmt [--check] [files/dirs] indent .fch files by { }, 4 spaces (--check: only list those it would change)")
print(" (with a finch.toml, run, build and test can leave out the file: its entry is used)")
print(" finch version show the version")
print(" finch lsp the language server, for editors (VS Code, Kate, ...)")
print("options:")
@ -190,23 +194,98 @@ fn unpack() -> str {
return dir
}
// finch fmt [--check] [files or folders]: the indentation and empty lines of .fch files (all under here by default)
fn format([]str args) -> int {
check := false
[]str targets
for i in 2..args.len {
if args[i] == "--check" {
check = true
} else {
targets.push(args[i])
}
}
if targets.len == 0 {
targets.push(".")
}
[]str files
for t in targets {
if t.ends_with(".fch") {
files.push(t)
} else {
for f in host.listFiles(t, ".fch") {
files.push(f)
}
}
}
changed := 0
for f in files {
text := read_file(f) or {
print("error: can't read " + f)
return 1
}
neat := fmt.format(text)
if neat != text {
changed += 1
if check {
print(f)
} else {
write_file(f, neat)
}
}
}
if check && changed > 0 {
print(str(changed) + " file(s) are not formatted (finch fmt changes them)")
return 1
}
return 0
}
fn main([]str args) -> int {
if args.len == 2 && (args[1] == "version" || args[1] == "--version") {
print("Finch 2.4.0 (the compiler is written in Finch)")
return 0
}
if args.len >= 2 && args[1] == "fmt" {
return format(args)
}
if args.len == 2 && args[1] == "lsp" {
return lsp.run(args[0]) // the language server, for editors
}
if args.len < 3 {
if args.len < 2 {
usage()
}
cmd := args[1]
file := args[2]
if cmd != "build" && cmd != "run" && cmd != "ir" && cmd != "index" && cmd != "test" {
usage()
}
file := ""
ai := 2
if args.len > 2 && !args[2].starts_with("-") {
file = args[2]
ai = 3
}
// finch.toml: next to the file, or above it (with no file: from here, and its entry is the file)
from := host.cwd()
if file.len > 0 {
from = dirOf(absolute(file))
}
man := manifest.find(from) or {
print(err)
return 1
}
if file.len == 0 {
entry := man.value("entry", "")
if !man.found || entry.len == 0 {
print("error: which file? (finch " + cmd + " <file.fch>, or a finch.toml with entry = \"main.fch\")")
return 1
}
file = man.resolve(entry)
}
out := stem(file)
if man.found && man.value("name", "").len > 0 {
out = man.value("name", "")
}
outGiven := false
shared := false
debug := false
@ -219,7 +298,6 @@ fn main([]str args) -> int {
[]str overlayPaths
[]str overlayFiles
[]str progArgs
ai := 3
while ai < args.len {
a := args[ai]
if cmd == "run" && progArgs.len > 0 { // after the first argument of the program, all of them are its
@ -298,6 +376,48 @@ fn main([]str args) -> int {
}
loader.overlayPaths = overlayPaths
loader.overlayFiles = overlayFiles
// what finch.toml adds, for every system and for this one
[]str targets
sys := "linux"
if tg.windows {
sys = "windows"
} else if tg.darwin {
sys = "macos"
} else if tg.android {
sys = "android"
}
arch := "x86_64"
if tg.arm64 {
arch = "arm64"
}
targets.push(sys)
targets.push(sys + "-" + arch)
ldflags := ""
cflags := ""
if man.found {
for d in man.list("paths", targets) {
loader.paths.push(man.resolve(d))
}
for d in man.list("include", targets) {
extraDirs.push(man.resolve(d))
}
for l in man.list("links", targets) {
if l.ends_with(".c") || l.ends_with(".o") || l.ends_with(".a") || l.ends_with(".cpp") || l.ends_with(".cc") {
extraLibs.push(man.resolve(l))
} else {
extraLibs.push(l)
}
}
for d in man.list("libdirs", targets) {
ldflags = ldflags + " -L" + linker.quote(man.resolve(d))
}
for f in man.list("ldflags", targets) {
ldflags = ldflags + " " + linker.quote(f)
}
for f in man.list("cflags", targets) {
cflags = cflags + " " + linker.quote(f)
}
}
for d in getenv("FINCH_PATH").split(host.pathSeparator()) {
if d.len > 0 {
loader.paths.push(d)
@ -322,7 +442,7 @@ fn main([]str args) -> int {
}
}
for l in prog.links {
if l.ends_with(".c") || l.ends_with(".o") || l.ends_with(".a") {
if linker.isOwnCode(l) {
links.push(d + "/" + l) // the program's own C code, next to the .fch file
} else {
links.push(l)
@ -377,7 +497,8 @@ fn main([]str args) -> int {
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 {
cflagList.push(cflags)
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(""), ldflags: ldflags), tg, ci) != 0 {
return 1
}
if cmd == "run" || cmd == "test" {

243
src/manifest.fch Normal file
View file

@ -0,0 +1,243 @@
// finch.toml: what a project needs to build, in one file next to its code (instead of FINCH_PATH,
// C_INCLUDE_PATH, -I, -l and linker flags on the command line).
//
// name = "game" the program finch build makes
// entry = "src/main.fch" what finch run / build / test use when no file is given
// paths = ["src", "lib"] folders with modules
// include = ["vendor/SDL3/include"] folders with C headers (also for link "x.c" files)
// links = ["SDL3"] libraries
// libdirs = ["vendor/SDL3/lib"] folders with libraries
// cflags = ["-DFAST"] for the program's own C files
// ldflags = ["-Wl,-rpath,."] for the linker
//
// [target.windows] added only when building for that system:
// links = ["ws2_32"] windows, linux, macos, android, or with the CPU: linux-arm64
//
// Folders are relative to finch.toml. Only this part of TOML is read: [sections], key = "text",
// key = ["a", "b"] (over several lines too), numbers, true/false and # comments.
struct Manifest {
bool found
str path
str dir
map[str]str values // "section.key" (top level: "key") -> a single value
map[str][]str lists // "section.key" -> a list
}
fn dirOf(str path) -> str {
i := path.len - 1
while i >= 0 && path[i] != '/' && path[i] != '\\' {
i -= 1
}
if i < 0 {
return "."
}
if i == 0 {
return "/"
}
return path.sub(0, i)
}
// finch.toml in `dir` or the folders above it
fn find(str dir) -> Manifest! {
d := dir
while true {
p := d + "/finch.toml"
if d == "/" {
p = "/finch.toml"
}
if file_exists(p) {
return try parse(p, try read_file(p))
}
up := dirOf(d)
if up == d || d == "." || d.len == 0 {
break
}
d = up
}
return Manifest()
}
struct Reader {
str text
int i
int line
str path
}
fn Reader.fail(str msg) -> str {
return self.path + ":" + str(self.line) + ": error: " + msg
}
fn Reader.at() -> char {
if self.i < self.text.len {
return self.text[self.i]
}
return char(0)
}
// past spaces, and past line ends and comments too when `lines`
fn Reader.skip(bool lines) {
while self.i < self.text.len {
c := self.text[self.i]
if c == ' ' || c == '\t' || c == '\r' {
self.i += 1
} else if c == '#' {
while self.i < self.text.len && self.text[self.i] != '\n' {
self.i += 1
}
} else if c == '\n' && lines {
self.i += 1
self.line += 1
} else {
return
}
}
}
fn isKeyChar(char c) -> bool {
return (c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z') || (c >= '0' && c <= '9') || c == '_' || c == '-' || c == '.'
}
fn Reader.word() -> str {
start := self.i
while self.i < self.text.len && isKeyChar(self.text[self.i]) {
self.i += 1
}
return self.text.sub(start, self.i)
}
// "text" (with \" \\ \n \t) or 'text' (as it is)
fn Reader.quoted() -> str! {
q := self.at()
self.i += 1
[]str out
while self.at() != q {
c := self.at()
if c == char(0) || c == '\n' {
return error(self.fail("this text is never closed"))
}
if c == '\\' && q == '"' {
self.i += 1
e := self.at()
if e == 'n' {
c = '\n'
} else if e == 't' {
c = '\t'
} else {
c = e
}
}
out.push(str(c))
self.i += 1
}
self.i += 1
return out.join("")
}
// a single value: text, a number, true or false (all kept as text)
fn Reader.scalar() -> str! {
c := self.at()
if c == '"' || c == '\'' {
return try self.quoted()
}
w := self.word()
if w.len == 0 {
return error(self.fail("expected a value: \"text\", a number, true, false or a [list]"))
}
return w
}
fn parse(str path, str text) -> Manifest! {
m := Manifest(found: true, path: path, dir: dirOf(path))
r := Reader(text: text, i: 0, line: 1, path: path)
section := ""
while true {
r.skip(true)
if r.i >= r.text.len {
break
}
if r.at() == '[' {
r.i += 1
r.skip(false)
section = r.word()
r.skip(false)
if r.at() != ']' || section.len == 0 {
return error(r.fail("a section looks like [name] or [target.windows]"))
}
r.i += 1
continue
}
key := r.word()
if key.len == 0 {
return error(r.fail("expected a key, like name = \"game\""))
}
r.skip(false)
if r.at() != '=' {
return error(r.fail("expected '=' after '" + key + "'"))
}
r.i += 1
r.skip(false)
full := key
if section.len > 0 {
full = section + "." + key
}
if r.at() == '[' {
r.i += 1
[]str items
while true {
r.skip(true)
if r.at() == ']' {
r.i += 1
break
}
items.push(try r.scalar())
r.skip(true)
if r.at() == ',' {
r.i += 1
} else if r.at() != ']' {
return error(r.fail("expected ',' or ']' in the list of '" + key + "'"))
}
}
m.lists[full] = items
} else {
m.values[full] = try r.scalar()
}
r.skip(false)
if r.i < r.text.len && r.at() != '\n' {
return error(r.fail("one key per line"))
}
}
return m
}
fn Manifest.value(str key, str fallback) -> str {
return self.values.get(key, fallback)
}
// the list `key` from the top level and from the sections for this system, in that order
fn Manifest.list(str key, []str targets) -> []str {
[]str out
if self.lists.has(key) {
for x in self.lists[key] {
out.push(x)
}
}
for t in targets {
k := "target." + t + "." + key
if self.lists.has(k) {
for x in self.lists[k] {
out.push(x)
}
}
}
return out
}
// a folder or file of the manifest, from where finch.toml is
fn Manifest.resolve(str p) -> str {
if p.starts_with("/") || (p.len > 2 && p[1] == ':') {
return p
}
return self.dir + "/" + p
}

View file

@ -257,7 +257,7 @@ fn parseProgram(ptr[Parser] p) -> ast.Program {
} else if at(p, "link") {
lt := next(p)
lib := expect(p, "string", "a library name in quotes, like link \"glfw\"").text
ownCode := lib.ends_with(".c") || lib.ends_with(".o") || lib.ends_with(".a") // your own C code, next to the .fch file
ownCode := lib.ends_with(".c") || lib.ends_with(".o") || lib.ends_with(".a") || lib.ends_with(".cpp") || lib.ends_with(".cc") || lib.ends_with(".cxx") || lib.ends_with(".lib") // your own C or C++ code, next to the .fch file
if !ownCode && (lib.starts_with("lib") || lib.contains(".so") || lib.contains("/")) {
fail(p, lt, "write just the library's name: for libglfw.so that's link \"glfw\"")
}

View file

@ -11,6 +11,7 @@ struct Target {
bool darwin // macOS
bool arm64 // AAPCS64
bool cross // another system than the one the compiler runs on
bool android // Android (bionic): a .so that an app loads
str exe // ".exe" or ""
str cc // the C compiler that links (the IR is always compiled by clang)
str runner // how to run a program for it here: "", "wine ", "qemu-aarch64 "
@ -45,6 +46,8 @@ fn select(str name) -> Target {
t = "x86_64-unknown-linux-gnu"
} else if name == "arm64" || name == "aarch64" || name == "linux-arm64" {
t = "aarch64-unknown-linux-gnu"
} else if name == "android" {
t = "aarch64-linux-android24"
} else if name == "macos" {
if hostName.starts_with("aarch64") || hostName.starts_with("arm64") {
t = "arm64-apple-macosx14.0.0"
@ -60,6 +63,7 @@ fn select(str name) -> Target {
tg.msvc = t.contains("msvc")
tg.darwin = t.contains("apple") || t.contains("darwin") || t.contains("macos")
tg.arm64 = t.starts_with("aarch64") || t.starts_with("arm64")
tg.android = t.contains("android")
hostArm := hostName.starts_with("aarch64") || hostName.starts_with("arm64")
hostDarwin := hostName.contains("apple") || hostName.contains("darwin")
hostWin := hostName.contains("windows") || hostName.contains("mingw") || hostName.contains("msvc")
@ -72,6 +76,8 @@ fn select(str name) -> Target {
fc := getenv("FINCH_CC")
if fc.len > 0 {
tg.cc = fc
} else if tg.android {
tg.cc = androidClang(t)
} else if tg.cross && tg.windows {
tg.cc = "x86_64-w64-mingw32-gcc"
} else if tg.cross && tg.arm64 && !tg.darwin {
@ -85,12 +91,30 @@ fn select(str name) -> Target {
}
if tg.cross && tg.windows {
tg.runner = "wine "
} else if tg.cross && tg.arm64 && !tg.darwin {
} else if tg.cross && tg.arm64 && !tg.darwin && !tg.android {
tg.runner = "qemu-aarch64 "
}
return tg
}
// The NDK's clang for this Android triple (ANDROID_NDK_HOME or ANDROID_NDK_ROOT says where the NDK is)
fn androidClang(str triple) -> str {
ndk := getenv("ANDROID_NDK_HOME")
if ndk.len == 0 {
ndk = getenv("ANDROID_NDK_ROOT")
}
if ndk.len == 0 {
return triple + "-clang"
}
for h in ["linux-x86_64", "darwin-x86_64", "windows-x86_64"] {
cc := ndk + "/toolchains/llvm/prebuilt/" + h + "/bin/" + triple + "-clang"
if file_exists(cc) || file_exists(cc + ".cmd") {
return cc
}
}
return triple + "-clang"
}
// C's plain `char`: signed on x86 and Apple, not on ARM64 Linux
fn charIsSigned(Target t) -> bool {
return !t.arm64 || t.darwin || t.windows