Structs, arrays, owned strings, memory model, modules, debug info

- struct with default values; constructors Point(1, 2) / Point(x: 1); field access, auto-deref through pointers
- []T arrays with bounds checks, literals, for-each, push/pop/insert/remove/clear/resize/find/contains/join/slice/reverse/sort
- str carries its length: .len, s[i], +, comparison, sub/find/split/trim/upper/lower/replace/repeat/bytes, str(x), int(s), float(s)
- Memory: values own their memory and are freed at the end of their block; assignment copies, temporaries move;
  parameters are lent and only copied when the function changes them; defer; new()/free() for manual heap memory
- input(), read_file/write_file/file_exists, exit(), fn main([]str args)
- Finch modules: import name, module.fn(), module.Struct
- C structs from headers (fields, fixed arrays, nested); passed and returned by value with the System V x86-64 ABI;
  addr(fn) gives C a callback
- link "file.c" compiles and links your own C code
- -g emits DWARF debug info (gdb/lldb)
- Small C runtime (runtime/finch_rt.c) embedded in the compiler, cached in ~/.cache/finch
- Tests: 58, all clean under valgrind (MEMCHECK=1 tests/run.sh)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Nexoniarz 2026-10-08 18:57:37 +02:00
commit 1dc976bd8f
59 changed files with 4577 additions and 1047 deletions

View file

@ -0,0 +1,4 @@
// expect: compare the parts one by one
fn main() {
print([1] == [1])
}

4
tests/fail/bad_int.fn Normal file
View file

@ -0,0 +1,4 @@
// expect: can't turn "12x" into int
fn main() {
print(int("12x"))
}

View file

@ -0,0 +1,4 @@
// expect: return can't be used inside defer
fn main() {
defer return
}

View file

@ -0,0 +1,6 @@
// expect: the loop variable 'x' can't be changed
fn main() {
for x in [1, 2] {
x = 3
}
}

View file

@ -0,0 +1,4 @@
// expect: an empty [] needs a type
fn main() {
a := []
}

5
tests/fail/free_value.fn Normal file
View file

@ -0,0 +1,5 @@
// expect: arrays, str and structs are freed by Finch
fn main() {
x := 5
free(x)
}

View file

@ -0,0 +1,5 @@
// expect: runtime error: index 5 is out of range (the length is 3)
fn main() {
a := [1, 2, 3]
print(a[5])
}

View file

@ -0,0 +1,3 @@
// expect: can't find the module 'nothere'
import nothere
fn main() {}

View file

@ -0,0 +1,4 @@
// expect: to use a module, add import geometry
fn main() {
v := geometry.Vec(1, 2)
}

5
tests/fail/pop_empty.fn Normal file
View file

@ -0,0 +1,5 @@
// expect: runtime error: pop() on an empty array
fn main() {
[]int a
a.pop()
}

5
tests/fail/push_type.fn Normal file
View file

@ -0,0 +1,5 @@
// expect: the element must be int, but this is str
fn main() {
a := [1, 2]
a.push("x")
}

6
tests/fail/str_null.fn Normal file
View file

@ -0,0 +1,6 @@
// expect: a str is never null
fn main() {
s := "a"
if s == null {
}
}

View file

@ -0,0 +1,4 @@
// expect: turn the other side into text first: str(...)
fn main() {
s := "a" + 5
}

View file

@ -0,0 +1,8 @@
// expect: P has 2 fields, but got 1 values
struct P {
int x
int y
}
fn main() {
p := P(1)
}

View file

@ -0,0 +1,8 @@
// expect: the struct P has no field 'y' (it has: x)
struct P {
int x
}
fn main() {
p := P(1)
print(p.y)
}

View file

@ -0,0 +1,5 @@
// expect: contains itself; use ptr[A] or []A
struct A {
A inner
}
fn main() {}

View file

@ -1,27 +1,45 @@
#!/usr/bin/env bash
# Finch test suite.
# tests/run/*.fn must compile and print exactly what is in the matching .out file
# (files without `fn main` are modules or helpers and are skipped;
# a matching .in file is fed to the program as its input)
# tests/fail/*.fn must fail; the first line is "// expect: <text that must appear in the error>"
# MEMCHECK=1 tests/run.sh also runs every program under valgrind: no leaks, no bad memory access
set -u
cd "$(dirname "$0")"
FINCH=${FINCH:-../build/finch}
pass=0; failed=0
tmp=$(mktemp -d)
trap 'rm -rf "$tmp"' EXIT
for f in run/*.fn; do
grep -q "^fn main" "$f" || continue
want="${f%.fn}.out"
got=$("$FINCH" run "$f" 2>&1)
if [ "$got" == "$(cat "$want")" ]; then
pass=$((pass + 1))
else
input="${f%.fn}.in"
[ -f "$input" ] || input=/dev/null
got=$("$FINCH" run "$f" < "$input" 2>&1)
if [ "$got" != "$(cat "$want")" ]; then
failed=$((failed + 1))
echo "FAIL $f"
diff <(echo "$got") "$want" | head -20
continue
fi
if [ "${MEMCHECK:-0}" = 1 ]; then
exe="$tmp/$(basename "${f%.fn}")"
"$FINCH" build "$f" -o "$exe" >/dev/null 2>&1
if ! valgrind -q --leak-check=full --errors-for-leak-kinds=all --error-exitcode=99 "$exe" < "$input" >/dev/null 2>"$tmp/vg"; then
failed=$((failed + 1))
echo "MEMCHECK FAIL $f"
head -20 "$tmp/vg"
continue
fi
fi
pass=$((pass + 1))
done
for f in fail/*.fn; do
expect=$(head -1 "$f" | sed -n 's|^// expect: ||p')
got=$("$FINCH" run "$f" 2>&1)
got=$("$FINCH" run "$f" < /dev/null 2>&1)
status=$?
if [ $status -ne 0 ] && [[ "$got" == *"$expect"* ]]; then
pass=$((pass + 1))

17
tests/run/abi.c Normal file
View file

@ -0,0 +1,17 @@
#include "abi.h"
#include <string.h>
Vec2 vec2_add(Vec2 a, Vec2 b) { return (Vec2){a.x + b.x, a.y + b.y}; }
Vec3 vec3_scale(Vec3 v, float k) { return (Vec3){v.x * k, v.y * k, v.z * k}; }
Color color_invert(Color c) { return (Color){255 - c.r, 255 - c.g, 255 - c.b, c.a}; }
Mixed mixed_bump(Mixed m) { return (Mixed){m.d + 0.5, m.i + 1}; }
Pair pair_swap(Pair p) { return (Pair){p.b, p.a}; }
Big big_sum(Big a, Big b) { return (Big){a.a + b.a, a.b + b.b, a.c + b.c}; }
int64_t big_total(Big b) { return b.a + b.b + b.c; }
int named_len(Named n) { return (int)strlen(n.name) * 100 + n.count; }
Sprite sprite_move(Sprite s, float dx) { s.pos.x += dx; s.color.a = 7; return s; }
int item_sum(Item *it) { int s = 0; for (; it; it = it->next) s += it->value; return s; }
double many(Vec2 a, Vec2 b, Vec2 c, Vec2 d, Vec2 e, Vec2 f, Vec2 g, Vec2 h, Vec2 i, Pair p1, Pair p2, Pair p3, Pair p4) {
return a.x + b.x + c.x + d.x + e.x + f.x + g.x + h.x + i.x + i.y + p1.a + p2.b + p3.a + p4.b;
}
int64_t apply_pair(Pair (*fn)(Pair, int64_t), Pair p, int64_t k) { Pair r = fn(p, k); return r.a * 1000 + r.b; }
float apply_vec(Vec2 (*fn)(Vec2), Vec2 v) { Vec2 r = fn(v); return r.x + r.y; }

25
tests/run/abi.h Normal file
View file

@ -0,0 +1,25 @@
#include <stdint.h>
typedef struct { float x, y; } Vec2; // SSE: <2 x float>
typedef struct { float x, y, z; } Vec3; // SSE, SSE (float tail)
typedef struct { uint8_t r, g, b, a; } Color; // INTEGER: i32
typedef struct { double d; int i; } Mixed; // SSE + INTEGER
typedef struct { int64_t a, b; } Pair; // INTEGER, INTEGER
typedef struct { int64_t a, b, c; } Big; // MEMORY (24 bytes)
typedef struct { char name[8]; int count; } Named; // fixed array inside
typedef struct { Vec2 pos; Color color; } Sprite; // nested
typedef struct Item { int value; struct Item *next; } Item;
Vec2 vec2_add(Vec2 a, Vec2 b);
Vec3 vec3_scale(Vec3 v, float k);
Color color_invert(Color c);
Mixed mixed_bump(Mixed m);
Pair pair_swap(Pair p);
Big big_sum(Big a, Big b);
int64_t big_total(Big b);
int named_len(Named n);
Sprite sprite_move(Sprite s, float dx);
int item_sum(Item *it);
// many structs: runs out of registers, later ones go on the stack
double many(Vec2 a, Vec2 b, Vec2 c, Vec2 d, Vec2 e, Vec2 f, Vec2 g, Vec2 h, Vec2 i, Pair p1, Pair p2, Pair p3, Pair p4);
int64_t apply_pair(Pair (*fn)(Pair, int64_t), Pair p, int64_t k);
float apply_vec(Vec2 (*fn)(Vec2), Vec2 v);

34
tests/run/c_structs.fn Normal file
View file

@ -0,0 +1,34 @@
import "abi.h"
link "abi.c"
fn pairTimes(Pair p, int k) -> Pair {
return Pair(a: p.a * k, b: p.b + k)
}
fn flip(Vec2 v) -> Vec2 {
return Vec2(x: v.y, y: v.x * 10)
}
fn main() {
print(vec2_add(Vec2(1.5, 2), Vec2(x: 0.5, y: 3)))
print(vec3_scale(Vec3(1, 2, 3), 2))
print(color_invert(Color(r: 10, g: 20, b: 30, a: 255)))
print(mixed_bump(Mixed(d: 1.25, i: 41)))
print(pair_swap(Pair(1, 2)))
big := big_sum(Big(1, 2, 3), Big(10, 20, 30))
print(big, big_total(big))
Named n
n.name[0] = 'h'
n.name[1] = 'i'
n.count = 5
print(named_len(n), n.name.len)
s := sprite_move(Sprite(pos: Vec2(1, 1), color: Color(1, 2, 3, 4)), 2.5)
print(s)
v := Vec2(1, 0)
print(many(v, v, v, v, v, v, v, v, Vec2(2, 3), Pair(1, 0), Pair(0, 2), Pair(3, 0), Pair(0, 4)))
print(apply_pair(addr(pairTimes), Pair(2, 3), 5))
print(apply_vec(addr(flip), Vec2(1, 2)))
second := Item(value: 2, next: null)
first := Item(value: 1, next: addr(second))
print(item_sum(addr(first)))
}

12
tests/run/c_structs.out Normal file
View file

@ -0,0 +1,12 @@
Vec2(x: 2, y: 5)
Vec3(x: 2, y: 4, z: 6)
Color(r: 245, g: 235, b: 225, a: 255)
Mixed(d: 1.75, i: 42)
Pair(a: 2, b: 1)
Big(a: 11, b: 22, c: 33) 66
205 8
Sprite(pos: Vec2(x: 3.5, y: 1), color: Color(r: 1, g: 2, b: 3, a: 7))
23
10008
12
3

20
tests/run/defer.fn Normal file
View file

@ -0,0 +1,20 @@
fn work(int n) -> int {
defer print("cleanup", n)
if n > 1 {
return n * 10
}
defer print("only when small")
return n
}
fn main() {
print(work(1))
print(work(2))
for i in 0..3 {
defer print("end of round", i)
if i == 1 {
continue
}
print("round", i)
}
}

10
tests/run/defer.out Normal file
View file

@ -0,0 +1,10 @@
only when small
cleanup 1
1
cleanup 2
20
round 0
end of round 0
end of round 1
round 2
end of round 2

9
tests/run/files.fn Normal file
View file

@ -0,0 +1,9 @@
fn main() {
path := "/tmp/finch_test_file.txt"
ok := write_file(path, "line one\nline two\n")
print(ok, file_exists(path))
text := read_file(path)
lines := text.trim().split("\n")
print(lines, lines.len)
print(file_exists("/definitely/not/here"))
}

3
tests/run/files.out Normal file
View file

@ -0,0 +1,3 @@
true true
["line one", "line two"] 2
false

18
tests/run/geometry.fn Normal file
View file

@ -0,0 +1,18 @@
import "math.h"
struct Vec {
float x
float y
}
fn length(Vec v) -> float {
return sqrt(v.x * v.x + v.y * v.y)
}
fn add(Vec a, Vec b) -> Vec {
return Vec(a.x + b.x, a.y + b.y)
}
fn describe(Vec v) -> str {
return "(" + str(v.x) + ", " + str(v.y) + ") of length " + str(length(v))
}

7
tests/run/input.fn Normal file
View file

@ -0,0 +1,7 @@
fn main() {
name := input("Name? ")
age := int(input("Age? "))
print("Hi", name, "next year you will be", age + 1)
rest := input()
print("rest:", rest, rest.len)
}

2
tests/run/input.in Normal file
View file

@ -0,0 +1,2 @@
Ola
41

2
tests/run/input.out Normal file
View file

@ -0,0 +1,2 @@
Name? Age? Hi Ola next year you will be 42
rest: 0

98
tests/run/memory.fn Normal file
View file

@ -0,0 +1,98 @@
struct Node {
str name
ptr[Node] next
[]str tags
}
fn build(int n) -> []str {
[]str out
out.resize(n)
return out
}
fn firstLong([]str words) -> str {
for w in words {
tmp := w + "!"
if w.len > 3 {
return tmp
}
}
return "none"
}
fn loops() -> int {
count := 0
for i in 0..10 {
s := "item " + str(i)
if i == 2 {
continue
}
if i == 6 {
break
}
count += s.len
}
while true {
t := "x" + "y"
if t.len == 2 {
break
}
}
return count
}
fn deferred() -> str {
log := "start"
defer print("deferred ran, log was:", log)
log = log + " middle"
names := ["a", "b"]
defer print("names:", names)
return log + " end"
}
fn main([]str args) {
print(args.len > 0)
print(firstLong(["hi", "hello", "hey"]))
print(firstLong(["a"]))
print(loops())
print(deferred())
s := "ab"
s = s + s
s = s
print(s)
list := ["one", "two", "three"]
for w in list {
if w == "two" {
list.push("four")
}
}
print(list)
last := list.pop()
print(last, list)
head := new(Node(name: "first", tags: ["t1"]))
head.next = new(Node(name: "second"))
print(head.name, head.next.name, head.tags)
n := head
while n != null {
print("node", n.name)
n = n.next
}
free(head.next)
free(head)
[]str words
for i in 0..3 {
words.push("w" + str(i))
}
words.clear()
words.resize(2)
print(words, words.len)
print([1, 2, 3].slice(1, 3), "abc".bytes())
x := ["keep"]
y := x
x[0] = "changed"
print(x, y)
}

16
tests/run/memory.out Normal file
View file

@ -0,0 +1,16 @@
true
hello!
none
30
names: ["a", "b"]
deferred ran, log was: start middle
start middle end
abab
["one", "two", "three", "four"]
four ["one", "two", "three"]
first second ["t1"]
node first
node second
["", ""] 2
[2, 3] [97, 98, 99]
["changed"] ["keep"]

18
tests/run/modules.fn Normal file
View file

@ -0,0 +1,18 @@
import geometry
import shapes
fn main() {
v := geometry.Vec(3, 4)
print(geometry.length(v))
w := geometry.add(v, geometry.Vec(x: 1, y: 1))
print(geometry.describe(w))
geometry.Vec z
print(z)
b := shapes.Box(size: geometry.Vec(2, 5))
print(shapes.area(b), b)
total := 0
for i in 0..3 {
total += i
}
print("total", total)
}

5
tests/run/modules.out Normal file
View file

@ -0,0 +1,5 @@
5
(4, 5) of length 6.40312
Vec(x: 0, y: 0)
10 Box(corner: Vec(x: 0, y: 0), size: Vec(x: 2, y: 5))
total 3

View file

@ -0,0 +1,23 @@
struct Node {
int value
ptr[Node] next
}
struct Box {
int size
}
fn main() {
last := new(Node(value: 2, next: null))
first := Node(value: 1, next: last)
p := addr(first)
print(p.value, p.next.value)
p.value = 10
print(first.value)
b := Box(3)
q := addr(b)
print(q.value, q.size)
q.value = Box(7)
print(b)
free(last)
}

View file

@ -0,0 +1,4 @@
1 2
10
Box(size: 3) 3
Box(size: 7)

10
tests/run/shapes.fn Normal file
View file

@ -0,0 +1,10 @@
import geometry
struct Box {
geometry.Vec corner
geometry.Vec size
}
fn area(Box b) -> float {
return b.size.x * b.size.y
}

View file

@ -0,0 +1,78 @@
struct Point {
int x
int y
}
struct Player {
str name = "nobody"
Point pos
[]int scores
int lives = 3
}
fn moved(Point p, int dx) -> Point {
return Point(x: p.x + dx, y: p.y)
}
fn total([]int xs) -> int {
sum := 0
for x in xs {
sum += x
}
return sum
}
fn addScore(Player p, int s) -> Player {
p.scores.push(s)
return p
}
fn main() {
p := Point(1, 2)
print(p, p.x, p.y)
q := moved(p, 10)
print(q)
pl := Player(name: "Ola")
print(pl)
pl.scores.push(10)
pl.scores.push(20)
pl.pos.x = 5
print(pl.name, pl.lives, pl.scores, pl.pos)
copy := pl
copy.name = "Tom"
copy.scores.push(30)
print(pl.name, pl.scores.len, copy.name, copy.scores.len)
pl2 := addScore(pl, 99)
print(pl.scores, pl2.scores)
nums := [5, 3, 8, 1]
nums.sort()
print(nums, total(nums), nums.len)
print(nums.pop(), nums)
nums.insert(0, 42)
nums.remove(1)
print(nums, nums.contains(8), nums.find(3))
names := ["b", "a", "c"]
names.sort()
print(names, names.join("-"))
s := "Hello"
s += ", Finch"
print(s, s.len, s[0], s.upper(), s.sub(0, 5))
words := "a,b,,c".split(",")
print(words, words.len)
print(" trim ".trim(), "x".repeat(3), "abc".replace("b", "BB"), "hello".contains("ell"))
print(str(42) + "!", str(3.5), str(true), int("123") + 1, float("2.5") * 2)
s[0] = 'J'
print(s)
for c in "hi" {
print(c)
}
grid := [[1, 2], [3, 4]]
grid[1].push(5)
print(grid, grid[1][2])
}

View file

@ -0,0 +1,18 @@
Point(x: 1, y: 2) 1 2
Point(x: 11, y: 2)
Player(name: "Ola", pos: Point(x: 0, y: 0), scores: [], lives: 3)
Ola 3 [10, 20] Point(x: 5, y: 0)
Ola 2 Tom 3
[10, 20] [10, 20, 99]
[1, 3, 5, 8] 17 4
8 [1, 3, 5]
[42, 3, 5] false 1
["a", "b", "c"] a-b-c
Hello, Finch 12 H HELLO, FINCH Hello
["a", "b", "", "c"] 4
trim xxx aBBc true
42! 3.5 true 124 5
Jello, Finch
h
i
[[1, 2], [3, 4, 5]] 5