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

@ -20,9 +20,13 @@
#include <stdlib.h>
#include <string.h>
#ifdef _WIN32
#define WIN32_LEAN_AND_MEAN
#define NOMINMAX
#include <windows.h>
#include <direct.h>
#include <io.h>
#else
#include <pthread.h>
#include <dirent.h>
#include <sys/stat.h>
#include <unistd.h>
@ -881,6 +885,114 @@ const char *finch_fs_data_home(void) {
return buf;
}
// ---------- threads (std/thread.fch, through std/finch_thread.h) ----------
typedef struct {
void (*fn)(void *);
void *arg;
} ThreadStart;
#ifdef _WIN32
static DWORD WINAPI thread_main(LPVOID p) {
ThreadStart st = *(ThreadStart *)p;
free(p);
st.fn(st.arg);
return 0;
}
#else
static void *thread_main(void *p) {
ThreadStart st = *(ThreadStart *)p;
free(p);
st.fn(st.arg);
return NULL;
}
#endif
// run fn(arg) on a new thread; NULL if it couldn't start
void *finch_thread_spawn(void (*fn)(void *), void *arg) {
ThreadStart *st = finch_alloc((int64_t)sizeof(ThreadStart));
st->fn = fn;
st->arg = arg;
#ifdef _WIN32
HANDLE h = CreateThread(NULL, 0, thread_main, st, 0, NULL);
if (!h) {
free(st);
return NULL;
}
return h;
#else
pthread_t *t = finch_alloc((int64_t)sizeof(pthread_t));
if (pthread_create(t, NULL, thread_main, st) != 0) {
free(st);
free(t);
return NULL;
}
return t;
#endif
}
// wait for the thread to end; 1 if that worked
int finch_thread_join(void *t) {
if (!t) return 0;
#ifdef _WIN32
int ok = WaitForSingleObject((HANDLE)t, INFINITE) == WAIT_OBJECT_0;
CloseHandle((HANDLE)t);
return ok;
#else
int ok = pthread_join(*(pthread_t *)t, NULL) == 0;
free(t);
return ok;
#endif
}
void *finch_mutex_new(void) {
#ifdef _WIN32
CRITICAL_SECTION *m = finch_alloc((int64_t)sizeof(CRITICAL_SECTION));
InitializeCriticalSection(m);
#else
pthread_mutex_t *m = finch_alloc((int64_t)sizeof(pthread_mutex_t));
pthread_mutex_init(m, NULL);
#endif
return m;
}
void finch_mutex_lock(void *m) {
#ifdef _WIN32
EnterCriticalSection((CRITICAL_SECTION *)m);
#else
pthread_mutex_lock((pthread_mutex_t *)m);
#endif
}
void finch_mutex_unlock(void *m) {
#ifdef _WIN32
LeaveCriticalSection((CRITICAL_SECTION *)m);
#else
pthread_mutex_unlock((pthread_mutex_t *)m);
#endif
}
void finch_mutex_free(void *m) {
if (!m) return;
#ifdef _WIN32
DeleteCriticalSection((CRITICAL_SECTION *)m);
#else
pthread_mutex_destroy((pthread_mutex_t *)m);
#endif
free(m);
}
int finch_cpu_count(void) {
#ifdef _WIN32
SYSTEM_INFO si;
GetSystemInfo(&si);
return (int)si.dwNumberOfProcessors;
#else
long n = sysconf(_SC_NPROCESSORS_ONLN);
return n > 0 ? (int)n : 1;
#endif
}
// ---------- maps ----------
// map[K]V = { entries; len; used; cap; index; icap; adds }
// entries: `used` slots of es bytes, each { uint64 hash; K key; V value }, in insertion order.

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

8
std/finch_thread.h Normal file
View file

@ -0,0 +1,8 @@
/* The runtime's thread functions, for std/thread.fch (they are in runtime/finch_rt.c, linked into every program). */
void *finch_thread_spawn(void (*fn)(void *), void *arg);
int finch_thread_join(void *thread);
void *finch_mutex_new(void);
void finch_mutex_lock(void *mutex);
void finch_mutex_unlock(void *mutex);
void finch_mutex_free(void *mutex);
int finch_cpu_count(void);

65
std/thread.fch Normal file
View file

@ -0,0 +1,65 @@
// Threads: run a function next to the rest of the program, and a lock for what they share.
//
// import thread
// fn work(ptr arg) { a thread's function takes one ptr (to whatever it needs)
// ptr[Job] job = arg
// ...
// }
// t := try thread.spawn(addr(work), addr(job))
// t.join() wait for it; `job` must live until then
//
// m := thread.mutex()
// defer m.close()
// m.lock() ... m.unlock() one thread at a time in between
// atomic_add(addr(count), 1) (built in) numbers changed by many threads at once
//
// What a thread is given is shared, not copied: don't let it outlive the block it comes from, and
// change it only under a lock (or with atomic_...).
import "finch_thread.h"
struct Thread {
ptr handle
}
struct Mutex {
ptr handle
}
// start fn(arg) on a new thread; fn is addr(f) of a fn f(ptr arg)
fn spawn(ptr fn_, ptr arg) -> Thread! {
h := finch_thread_spawn(fn_, arg)
if h == null {
return error("can't start a thread")
}
return Thread(handle: h)
}
// wait until the thread has finished
fn Thread.join() {
if self.handle != null {
finch_thread_join(self.handle)
self.handle = null
}
}
fn mutex() -> Mutex {
return Mutex(handle: finch_mutex_new())
}
fn Mutex.lock() {
finch_mutex_lock(self.handle)
}
fn Mutex.unlock() {
finch_mutex_unlock(self.handle)
}
fn Mutex.close() {
finch_mutex_free(self.handle)
self.handle = null
}
// how many threads the computer runs at the same time
fn cpus() -> int {
return int(finch_cpu_count())
}

View file

@ -124,6 +124,12 @@ syms = c.request("textDocument/documentSymbol", {"textDocument": td})
names = sorted(s["name"] for s in syms)
check("document symbols", names == ["Point", "add", "main"], names)
messy = "fn main() {\n x := 1 \n\n\n\tprint(x)\n}"
c.notify("textDocument/didChange", {"textDocument": {"uri": uri, "version": 5}, "contentChanges": [{"text": messy}]})
fm = c.request("textDocument/formatting", {"textDocument": td, "options": {"tabSize": 4, "insertSpaces": True}})
check("formatting", fm and fm[0]["newText"] == "fn main() {\n x := 1\n\n print(x)\n}\n", fm)
c.notify("textDocument/didChange", {"textDocument": {"uri": uri, "version": 5}, "contentChanges": [{"text": src}]})
bad = src.replace("total := add(p.x, p.y)", 'total := add(p.x, "y")')
c.notify("textDocument/didChange", {"textDocument": {"uri": uri, "version": 6}, "contentChanges": [{"text": bad}]})
diags = c.diagnostics(uri)

23
tests/new/auto_addr.fch Normal file
View file

@ -0,0 +1,23 @@
struct Body {
float x
float y
}
fn push(ptr[Body] b, float dx) {
b.x += dx
}
fn Body.moved(float dx) -> Body {
return Body(x: self.x + dx, y: self.y)
}
fn main() {
b := Body(x: 1.0, y: 2.0)
push(b, 0.5) // the same as push(addr(b), 0.5)
push(addr(b), 0.5)
print(b.x)
p := addr(b)
push(p, 1.0) // already a ptr: as it is
print(b.x)
print(b.moved(1.0).x)
}

3
tests/new/auto_addr.out Normal file
View file

@ -0,0 +1,3 @@
2
3
4

View file

@ -24,4 +24,6 @@ fn main() {
t.as.f = 0.5
print(t.as.f)
print(abs(-2.5), abs(-3), min(EV_KEY, 7)) // Finch's abs, not C's int-only one
bg := RAYWHITE // #define RAYWHITE (Color){ 245, 245, 245, 255 }
print(bg.r, bg.a, DARK.g, DARK.a, HALF_UP.x, HALF_UP.y)
}

View file

@ -5,3 +5,4 @@ true 32
32 2
0.5
2.5 3 7
245 255 20 0 0.5 -1

View file

@ -22,6 +22,14 @@ typedef struct { int kind; union { int i; float f; } as; } Tagged;
#define LONG_NAME "a" "b"
#define NOT_A_CONSTANT do { } while (0)
typedef struct { unsigned char r, g, b, a; } Color;
typedef struct { float x, y; } Vec2;
#define CLITERAL(type) (type)
#define RAYWHITE CLITERAL(Color){ 245, 245, 245, 255 }
#define DARK (Color){ 20, 20, 20 }
#define HALF_UP (Vec2){ 0.5f, -1 }
#define NOT_A_STRUCT_CONST (Color){ 1, 2, (unsigned char)(long)&poll_event, 4 }
static inline int fill_key(Event *e, int key) {
e->key.type = EV_KEY;
e->key.key = key;

View file

@ -0,0 +1,13 @@
// expect: argument 'b' must be ptr[Body], but this is Body
struct Body {
float x
}
fn push(ptr[Body] b) {
b.x += 1.0
}
fn make() -> Body {
return Body(x: 1.0)
}
fn main() {
push(make())
}

View file

@ -0,0 +1,21 @@
// "\(x)" in text
struct P {
int x
float y
}
fn main() {
name := "Ada"
hp := 7
p := P(x: 3, y: 0.25)
print("hi \(name), hp \(hp), next \(hp + 1)")
print("p = (\(p.x), \(p.y, 2)) \("nested \(name)")")
map[str]int m
m["k"] = 5
print("\(m["k"]) \(str(1.0 / 3.0, 3))")
print("no code here: \\(x) and \t tab")
long := """
speed \(p.y * 4.0)
"""
print(long)
}

View file

@ -0,0 +1,5 @@
hi Ada, hp 7, next 8
p = (3, 0.25) nested Ada
5 0.333
no code here: \(x) and tab
speed 1

55
tests/new/threads.fch Normal file
View file

@ -0,0 +1,55 @@
import thread
struct Shared {
[]int seen
}
struct Job {
ptr[int] counter
int adds
ptr[thread.Mutex] lock
ptr[Shared] common
int id
}
fn work(ptr arg) {
ptr[Job] job = arg
for i in 0..job.adds {
atomic_add(job.counter, 1)
}
job.lock.lock()
job.common.seen.push(job.id)
job.lock.unlock()
}
fn main() {
count := 0
m := thread.mutex()
defer m.close()
both := Shared()
[]Job jobs
for i in 0..4 {
jobs.push(Job(counter: addr(count), adds: 100000, lock: addr(m), common: addr(both), id: i))
}
[]thread.Thread ts
for i in 0..4 {
ts.push(thread.spawn(addr(work), addr(jobs[i])) or {
print("can't start")
exit(1)
})
}
for i in 0..4 {
ts[i].join()
}
print(atomic_load(addr(count)))
print(both.seen.len)
print(thread.cpus() > 0)
u32 flags = 5
print(atomic_swap(addr(flags), 9))
print(atomic_cas(addr(flags), 9, 1))
print(atomic_cas(addr(flags), 9, 2))
atomic_store(addr(flags), 7)
print(atomic_sub(addr(flags), 2))
print(flags)
}

8
tests/new/threads.out Normal file
View file

@ -0,0 +1,8 @@
400000
4
true
5
true
false
7
5

View file

@ -56,6 +56,24 @@ for f in fail/*.fch new/fail/*.fch; do
fi
done
# the commands around the compiler: finch test, the chain of calls, finch fmt, a finch.toml project
check_tool() { # name, what it printed, the file with what it should print
if [ "$2" == "$(tr -d '\r' < "$3")" ]; then
pass=$((pass + 1))
else
failed=$((failed + 1))
echo "FAIL $1"
diff <(echo "$2") "$3" | head -20
fi
}
check_tool "finch test" "$(cd tool && "$FINCH" test test_blocks.fch 2>&1 | tr -d '\r')" tool/test_blocks.out
check_tool "the chain of calls" "$(cd tool && "$FINCH" run trace.fch 2>&1 | tr -d '\r')" tool/trace.out
cp tool/fmt_in.fch "$tmp/fmt.fch"
"$FINCH" fmt "$tmp/fmt.fch"
check_tool "finch fmt" "$(cat "$tmp/fmt.fch")" tool/fmt_out.fch
check_tool "finch fmt (again)" "$("$FINCH" fmt --check tool/fmt_out.fch 2>&1)" /dev/null
check_tool "finch.toml" "$(cd tool/project && "$FINCH" run 2>&1 | tr -d '\r')" tool/project/expected.out
# the same programs with debug info (-g -O0): the metadata has to be valid for every kind of thing a program has
for f in new/enums.fch new/generics.fch new/std_json.fch run/basics.fch run/methods_basic.fch run/maps.fch run/errors.fch \
run/defer.fch run/c_structs.fch run/memory.fch; do

24
tests/tool/fmt_in.fch Normal file
View file

@ -0,0 +1,24 @@
struct P {
int x
int y
}
fn main() {
p := P(x: 1,
y: 2)
msg := """
a {
b
"""
if p.x > 0 {
print("{ \(p.x) }") // } not counted
} else {
print(msg)
}
s := "a" +
"b"
print(s)
}

21
tests/tool/fmt_out.fch Normal file
View file

@ -0,0 +1,21 @@
struct P {
int x
int y
}
fn main() {
p := P(x: 1,
y: 2)
msg := """
a {
b
"""
if p.x > 0 {
print("{ \(p.x) }") // } not counted
} else {
print(msg)
}
s := "a" +
"b"
print(s)
}

View file

@ -0,0 +1 @@
hello world 42

View file

@ -0,0 +1,14 @@
# a project: finch run / build / test here need no file and no flags
name = "mygame"
entry = "src/main.fch"
paths = ["src"]
include = ["native"]
links = [
"native/glue.cpp", # C++, built with the C++ compiler
]
cflags = ["-DGLUE_BASE=40"]
[target.linux]
links = ["m"]
[target.windows]
links = ["ws2_32"]

View file

@ -0,0 +1,9 @@
// C++ behind a C header: extern "C" gives the function a name C (and Finch) can call
#include "glue.h"
#include <numeric>
#include <vector>
extern "C" int glue_answer(void) {
std::vector<int> v = {1, 1};
return GLUE_BASE + std::accumulate(v.begin(), v.end(), 0);
}

View file

@ -0,0 +1,7 @@
#ifdef __cplusplus
extern "C" {
#endif
int glue_answer(void);
#ifdef __cplusplus
}
#endif

View file

@ -0,0 +1,3 @@
fn hello() -> str {
return "world"
}

View file

@ -0,0 +1,6 @@
import "glue.h"
import game/world
fn main() {
print("hello \(world.hello()) \(glue_answer())")
}

View file

@ -0,0 +1,30 @@
// finch test: each test "..." block runs on its own; a failed assert or a runtime error fails only that one
fn add(int a, int b) -> int {
return a + b
}
fn second([]int xs) -> int {
return xs[1]
}
test "add works" {
assert(add(2, 3) == 5)
assert(add(-1, 1) == 0, "zero")
}
test "a failing assert" {
x := add(1, 2)
assert(x == 4, "x is \(x), not 4")
}
test "a runtime error fails only this test" {
print(second([1]))
}
test "still runs after the others" {
assert("\(1.5, 2) and \(add(1, 1))" == "1.50 and 2")
}
fn main() {
print("main is not run by finch test")
}

View file

@ -0,0 +1,8 @@
ok add works
FAIL a failing assert
test_blocks.fch:17: runtime error: assert failed: x is 3, not 4
FAIL a runtime error fails only this test
test_blocks.fch:7: runtime error: index 1 is out of range (the length is 1)
ok still runs after the others
2 passed, 2 failed

16
tests/tool/trace.fch Normal file
View file

@ -0,0 +1,16 @@
struct Enemy {
[]int path
}
fn Enemy.next(int i) -> int {
return self.path[i]
}
fn step(Enemy e) -> int {
return e.next(3)
}
fn main() {
e := Enemy(path: [1, 2])
print(step(e))
}

4
tests/tool/trace.out Normal file
View file

@ -0,0 +1,4 @@
trace.fch:6: runtime error: index 3 is out of range (the length is 2)
in Enemy.next (trace.fch:6)
in step (trace.fch:10)
in main (trace.fch:15)