bootstrap/install.sh and a share/finch lookup for the runtime and std; an ARM64 Linux seed; bootstrap/README.md; CI builds from the seed on x64 and ARM64 and runs every test on Windows (Wine) and ARM64 (qemu); README, install and layout sections of the guides (EN+PL) no longer talk about CMake; editors know enum, match and export. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015f73FesGZGYPkmDvUcj6QX
39 KiB
Finch for Technicians
For people who know their way around a computer, but little or no programming.
This guide covers installing Finch, using the finch tool, the whole language, memory,
modules, C libraries, debugging and fixing common problems. It does not cover how the
compiler works inside. For that, see the guide for engineers.
🇵🇱 Polska wersja: dla-technikow.md
Contents
- What Finch is
- Installing
- The finch command
- How a Finch file looks
- Variables
- Types
- Operators
- Control flow
- Functions
- Arrays and maps
- Text (str)
- Structs and methods
- Memory: who frees what
- Pointers
- Built-in functions
- Modules
- Using C libraries
- Errors
- Editors: VS Code, Kate and others
- Debugging
- Troubleshooting
- Project layout and tests
- Enums and match
- Generics
- The standard library
- Current limits
1. What Finch is
Finch is a compiled programming language. The finch tool translates your .fch
file into a real program (a native executable, like the ones made from C), using the
LLVM compiler toolkit, the same backend that powers clang, Rust and Swift.
What that gives you:
- Speed close to C. No interpreter, virtual machine or garbage collector. Array bounds checks and other safety checks cost almost nothing after optimization.
- Small, standalone programs that you can copy and run.
- Memory handled for you. Lists and text are freed automatically at the end of the block that owns them, with no garbage collector involved.
- Direct access to C libraries.
import "stdio.h"and call anything in it, even functions that take structs.
The design rules: one way to do each thing, C-like syntax without C's traps, and error messages that tell you how to fix the problem.
2. Installing
The Finch compiler is written in Finch (see the engineers' guide). It runs on Linux (x86-64 and ARM64) and builds programs for Linux, Windows and macOS. It needs clang 21 and libclang (LLVM 21) to read C headers and to compile its output.
The compiler does not run on macOS or Windows yet; it needs a seed made with that system's headers
(bootstrap/README.md says why). Until then, build your Windows and macOS programs from Linux
with --target (below), or use the older C++ compiler from the before-bootstrap branch.
Option A: a ready build (Linux)
Download finch-linux-x64.tar.gz or finch-linux-arm64.tar.gz from the
releases and unpack it. Keep runtime/ and std/ next to the finch file
(or install it, below). You also need clang 21 and its libclang library:
| Distribution | Packages |
|---|---|
| Debian / Ubuntu | clang-21 libclang-21-dev from apt.llvm.org, pkg-config |
| Fedora | clang clang-devel pkgconf |
| Arch | clang pkgconf |
| Nix | nix-shell in the repository gives all of it |
Option B: build it yourself
git clone https://github.com/Nexoniarz/Finch.git
cd Finch
nix-shell # clang, libclang, pkg-config (or install them with your distribution)
bootstrap/build.sh # build/finch
./build/finch version
bootstrap/build.sh compiles the seed (bootstrap/<system>.ll, the compiler's own LLVM IR) into a first compiler, builds
src/ with it, builds src/ again with the result and checks the two are identical. If libclang isn't where the linker
looks, tell it: FINCH_LIBCLANG="-L/usr/lib/llvm-21/lib -lclang-21" bootstrap/build.sh.
Building for another system
--target builds a program for another system. Finch needs that system's C compiler to link:
--target |
Builds | C compiler used | finch run uses |
|---|---|---|---|
windows |
a Windows .exe |
x86_64-w64-mingw32-gcc (MinGW-w64) |
Wine |
arm64 |
an ARM64 Linux program | aarch64-linux-gnu-gcc |
qemu-aarch64 |
linux |
an x86-64 Linux program | clang --target=… |
|
macos |
a macOS program (on a Mac: for the other processor) | clang --target=… |
|
| any LLVM triple | e.g. aarch64-unknown-linux-gnu |
clang --target=<triple> |
finch build game.fch --target windows # game.exe
finch run game.fch --target arm64 # builds for ARM64 and runs it with QEMU
FINCH_CC=aarch64-unknown-linux-gnu-gcc finch build tool.fch --target arm64 # another compiler name
On Nix the compilers are pkgsCross.mingwW64.buildPackages.gcc and
pkgsCross.aarch64-multiplatform.buildPackages.gcc.
Putting finch on your PATH (optional)
sudo bootstrap/install.sh # /usr/local/bin/finch, and /usr/local/share/finch/{runtime,std}
bootstrap/install.sh ~/.local # or into your own folder
finch version
At run time Finch needs clang (it compiles each program's IR and links it). The small runtime library is built once and
kept in ~/.cache/finch. FINCH_CC names a different compiler to link with; FINCH_PATH adds folders to look for modules in.
3. The finch command
finch run <file.fch> [args...] compile and run right away
finch build <file.fch> [-o name] compile into a program (default name: the file's name)
finch ir <file.fch> print the LLVM IR (for the curious)
finch lsp the language server, for editors
finch version show the version
options:
-l <lib> link a C library, same as link "lib" in the file
--target <name> build for another system: windows, linux, arm64, macos, or an LLVM triple
-g add debug info (for gdb / lldb / Visual Studio)
-O0 skip optimizations
Examples:
finch run hello.fch # prints the output
finch run tool.fch input.txt -v # arguments after the file go to the program
finch build game.fch -o mygame # creates ./mygame
finch build game.fch -g -O0 # a build for the debugger
finch run passes on your program's exit code. If the program crashes, it says how
(for example the program crashed: Segmentation fault).
4. How a Finch file looks
// comments start with two slashes
/* or span
several lines */
import "math.h" // C headers (optional), at the top
import shapes // Finch modules (optional): shapes.fch next to this file
link "m" // C libraries or C files to link (optional)
struct Point { // your own types
int x
int y
}
fn main() { // the program starts here
print("Hello")
}
Rules worth knowing:
- One statement per line. There are no semicolons. To split a long expression, break the line
after an operator (
a +⏎ b) or anywhere inside brackets( )or[ ]. - Curly braces are always required after
if,else,for,while, and the opening{goes on the same line. - No brackets around conditions:
if x > 5 {, notif (x > 5) {. - Functions and structs can be in any order in the file.
mainisfn main(),fn main() -> int(returns the exit code) orfn main([]str args)(gets the command-line arguments;args[0]is the program's own path).
5. Variables
x := 5 // new variable, type inferred (int)
int y = 10 // new variable, type written explicitly
int z // no value: starts at zero ("" for str, [] for arrays, null for pointers)
Point p // a struct with no value: zero, or the struct's default values
x = 7 // assign
x += 1 // also -= *= /= %=
- A name can be declared once in a function. Finch does not allow reusing a name in an inner block (no "shadowing"), because that is a common source of confusion.
- Variables exist from their declaration to the end of the
{ }block they are in. - Names: ASCII letters, digits and
_, not starting with a digit. Non-ASCII letters are allowed only inside strings. Type names (int,u8, …), built-in function names and keywords are reserved.
Keywords: fn return if else while for in break continue true false null import link struct defer
6. Types
Everyday types
| Type | Holds | Example |
|---|---|---|
int |
whole number, 64-bit (same as i64) |
42, -7, 0xFF, 1_000_000 |
float |
decimal number, 64-bit (same as f64) |
3.14 |
bool |
true or false |
|
char |
one byte-sized character | 'A', '\n' |
str |
text | "hello\tworld" |
[]T |
a list (array) of T |
[1, 2, 3] |
map[K]V |
values of type V found by a key of type K |
["a": 1, "b": 2] |
| your structs | named groups of values | Point(1, 2) |
String and char escapes: \n new line, \t tab, \r, \0, \\, \", \'.
Sized types (for files, hardware, C libraries)
| Signed | Range | Unsigned | Range |
|---|---|---|---|
i8 |
−128 … 127 | u8 |
0 … 255 |
i16 |
−32 768 … 32 767 | u16 |
0 … 65 535 |
i32 |
about ±2.1 billion | u32 |
0 … about 4.29 billion |
i64 |
about ±9.2 × 10¹⁸ | u64 |
0 … about 1.8 × 10¹⁹ |
f32 is a 32-bit decimal number (about 7 significant digits), f64 a 64-bit one (about 15–16).
Conversions
Finch converts automatically only when nothing can be lost:
| From | To | Automatic? |
|---|---|---|
| a smaller whole number | a bigger one of the same signedness (i8→i32, u8→u64) |
✅ |
| unsigned | a strictly bigger signed (u8→i16, u32→i64) |
✅ |
| any whole number | float / f32 |
✅ |
f32 |
f64 |
✅ |
null |
any pointer | ✅ |
| any pointer | ptr, and ptr to any pointer |
✅ |
| anything else | ❌ write it: type(value) |
Number literals adapt. A number written in the code fits into any type that can hold it:
u8 b = 200 works, u8 b = 300 is an error. The same applies to constants imported from C headers.
Explicit conversions use the type name like a function:
int(3.99) // 3 (cuts off the fraction)
float(7) / 2 // 3.5
u8(300) // 44 (keeps the lowest 8 bits)
int('A') // 65
char(66) // 'B'
bool(0) // false (any non-zero number is true)
str(42) // "42" (also floats, bools, chars; str([]u8) turns bytes into text)
int("42") // 42 (stops the program if the text isn't a number)
float("2.5") // 2.5
ptr(p) // a typed pointer as an untyped one
ptr(16) // a number as a pointer (C APIs that pass offsets as pointers, like OpenGL)
int(p) // a pointer as a number (its address)
str(p) // a C char pointer as text
Fixed-size numbers wrap around, like real hardware: a u8 holding 255 plus 1 becomes 0.
Calculations happen in the type of the values involved, so u8(250) + 10 is 4, not 260.
7. Operators
By priority (higher binds tighter)
| Priority | Operators | Meaning |
|---|---|---|
| 5 | * / % << >> & |
multiply, divide, remainder, shift left/right, bitwise AND |
| 4 | + - | ^ |
add (and join text), subtract, bitwise OR, bitwise XOR |
| 3 | == != < <= > >= |
comparisons |
| 2 | && |
logical AND |
| 1 | || |
logical OR |
Unary (in front of a value): -x negate, !x logical NOT, ~x flip all bits.
Different from C, on purpose: bit operators bind tighter than comparisons, so
flags & 4 != 0means(flags & 4) != 0.
Notes:
/on whole numbers rounds toward zero:7 / 2is3.%takes the sign of the left side.- Dividing by zero stops the program with
runtime error: division by zeroand the line number. &&and||only evaluate the right side if needed.- Text:
+joins,==!=compare contents,<>compare alphabetically (byte by byte). - Pointers can be compared with
==and!=, including againstnull. Astris nevernull. - Arrays and structs can't be compared with
==; compare their parts.
8. Control flow
if x > 10 {
...
} else if x > 0 {
...
} else {
...
}
while count > 0 {
count -= 1
}
for i in 0..10 { // i = 0, 1, …, 9 (the end is not included)
if i == 3 { continue }
if i == 8 { break }
}
for name in names { // every element of an array (or every char of a str)
print(name)
}
for i, name in names { // with its index: 0, 1, 2, ...
print(i, name)
}
for key, value in ages { // every entry of a map, in the order the keys were added
print(key, value) // (for key in ages: just the keys)
}
- A condition must be a
bool.if x {with a number is an error; writeif x != 0 {. - In
for i in a..b, both ends are evaluated once.iis anintand cannot be changed in the loop. - In
for x in list,xcannot be changed either. To change elements, loop over the indexes:for i in 0..list.len { list[i] = ... }. Adding to the list inside the loop is fine. while true { ... }loops until abreakorreturn.- Code after
return,breakorcontinuein the same block is an error, because it can never run.
defer
defer runs a statement when the current block ends, however it ends: normally, by return,
break or continue. Several defers run in reverse order. Handy for cleanup:
f := fopen("data.txt", "r")
defer fclose(f)
// ... use f; it is closed when the block ends, on every path
A deferred statement can't contain return, break or continue.
9. Functions
fn name(type1 param1, type2 param2) -> return_type {
...
return value
}
fn no_result(str message) { // no "->": returns nothing
print(message)
}
- Arguments behave like copies: changing a parameter never changes the caller's variable. (Finch only really copies when needed: a function that doesn't change an array gets it for free.) To change the caller's variable, pass a pointer: see Pointers.
- A function with
-> typemust return a value on every path. Finch checks this. - A function cannot be defined inside another function.
- A function that can fail is written
-> int!(or-> !with no result); see Errors as values. - Long calls can be split over several lines inside the brackets; a comma after the last argument is fine.
10. Arrays and maps
nums := [5, 3, 8] // an array of int
[]str names // an empty array of str
grid := [[1, 2], [3, 4]] // arrays of arrays
[]f32 xs = [1, 2.5] // the type decides what the numbers become
| Operation | Meaning |
|---|---|
a.len |
number of elements |
a[i] |
element i (from 0); out of range stops the program with a clear message |
a[i] = x |
change an element |
a.push(x) |
add at the end |
a.pop() |
remove the last one and give it back |
a.insert(i, x) |
put x at position i, moving the rest |
a.remove(i) |
remove position i and give it back |
a.clear() |
remove everything |
a.resize(n) |
make it n long (new elements are zero) |
a.contains(x), a.find(x) |
is x there? its index, or -1 (numbers, chars, bools, str) |
a.sort(), a.reverse() |
in place (numbers, chars, str) |
a.slice(s, e) |
a new array with elements s … e-1 |
a.join(sep) |
for []str: one text with sep between the parts |
a.ptr |
the address of the first element (for C) |
print(a) |
prints [1, 2, 3] |
Assigning copies: after b := a, changing b does not change a.
Maps
A map[K]V finds values by a key. Keys can be whole numbers, char, bool or str; values can be
anything (arrays, structs, other maps). Iterating goes in the order the keys were first added.
ages := ["anna": 31, "bob": 25] // a map literal: map[str]int
map[str][]str groups // an empty map
map[int]str names = [:] // [:] is an empty map where the type is known
| Operation | Meaning |
|---|---|
m[k] |
the value for k; a missing key stops the program with the key "k" is not in the map |
m[k] = v |
add or replace |
m[k] += 1, m[k].push(x), m[k].field = … |
changing a missing key adds it first, with the value type's default (0, "", [], …) |
m.len |
the number of keys |
m.has(k) |
is k there? |
m.get(k, default) |
the value, or default (only computed when needed) |
m.remove(k) |
removes k; true if it was there |
m.clear() |
removes everything |
m.keys(), m.values() |
new arrays, in insertion order |
print(m), str(m) |
{"anna": 31, "bob": 25} |
map[str]int counts
for word in text.split(" ") {
counts[word] += 1 // counting: a new word starts at 0
}
for word, n in counts {
print(word, n)
}
Lookups are hash-based (average constant time). Like arrays, maps own their keys and values,
copy on assignment, and are freed at the end of their block. Don't add or remove keys of a map
inside a for over that same map; collect them in an array and change the map after the loop.
11. Text (str)
s := "Hello"
s += ", world" // join
print(s.len, s[0]) // 12 H (.len counts bytes; ą, ż, … take 2)
s[0] = 'J' // change a character
| Method | Gives back |
|---|---|
s.sub(a, b) |
the part from a to b-1 |
s.find(t) |
where t starts, or -1 |
s.contains(t), s.starts_with(t), s.ends_with(t) |
bool |
s.split(sep) |
[]str; split("") gives single characters |
s.trim() |
without spaces/newlines at both ends |
s.upper(), s.lower() |
A–Z / a–z changed (ASCII letters) |
s.replace(a, b) |
every a replaced by b |
s.repeat(n) |
s n times |
s.bytes() |
[]u8 with the bytes |
s.ptr |
a ptr[char] for C (valid while s lives) |
str keeps its length, so .len is instant, and it is always NUL-terminated, so passing it
to C as char* works.
12. Structs and methods
struct Player {
str name
int lives = 3 // a default value
[]int scores
Point pos // structs inside structs
}
p := Player(name: "Ola") // by name: missing fields get their default (or zero)
q := Point(1, 2) // in order: then every field must be given
p.lives -= 1
p.scores.push(10)
p.pos.x = 5
print(p) // Player(name: "Ola", lives: 2, scores: [10], pos: Point(x: 5, y: 0))
- Assigning or passing a struct copies it, together with its arrays and text.
- A struct can't contain itself directly; use
ptr[Node]or[]Nodefor that field (trees, lists).
Methods
A method is a function that belongs to a struct: fn Struct.name(...). Inside, self is the struct
it was called on, and it is the caller's value itself, not a copy: changes to self stay.
fn Player.heal(int amount) {
self.lives += amount
}
fn Player.is_alive() -> bool {
return self.lives > 0
}
p := Player(name: "Ola")
p.heal(2) // p.lives is now 5
if p.is_alive() { ... }
team[0].heal(1) // on an array element, a field, m[key], through a ptr[Player] ...
- A method goes in the same file (module) as its struct, and is available wherever the struct is.
- Methods can also be added to structs from C headers:
fn Vector2.length() -> f32 { ... }. - A method can't have the name of a field.
- Calling a method that changes
selfon a loop variable (for p in team { p.heal(1) }) is an error, because the loop variable is read-only; usefor i in 0..team.len { team[i].heal(1) }. - Like functions, methods can fail (
-> !); see Errors as values.
13. Memory: who frees what
You never free arrays, text or structs. Finch does it for you, at a predictable moment:
when the block that owns the value ends (}), or earlier on return, break, continue.
- A variable owns its value. Assigning copies it, so two variables never share one list.
- A value produced by a function call or an expression is moved where it's stored: no copy.
return listmoves the variable out of the function: no copy.- Function parameters are lent: the function gets the caller's value without a copy. If the function changes the parameter, Finch makes it a private copy first. You never notice except in speed.
For memory you manage yourself (linked lists, trees, sharing between structures):
node := new(Node(value: 1)) // put a value on the heap, get a ptr[Node]
defer free(node) // free(...) gives it back (and frees its arrays and text)
Memory from C (malloc, library objects) is freed with C's functions, as in C.
14. Pointers
A pointer holds the address of a value. Finch uses words instead of C's * and &:
| C | Finch | Meaning |
|---|---|---|
int *p |
ptr[int] p |
p points to an int |
&x |
addr(x) |
the address of x (also addr(a[i]), addr(s.field)) |
*p |
p.value |
the value p points to |
p->field |
p.field |
a field of the struct p points to |
p[i] |
p[i] |
C-style indexing (no bounds check) |
NULL |
null |
points nowhere |
void * |
ptr |
points to something of unknown type |
fn double(ptr[int] p) {
p.value = p.value * 2
}
x := 21
double(addr(x)) // x is 42
If a struct has its own field called value, then p.value on a pointer to it means that field.
Using .value, .field or [i] on a null pointer stops the program with a clear message.
⚠️ A pointer to a variable must not be used after the variable's block has ended. Finch does not check this.
15. Built-in functions
| Function | What it does |
|---|---|
print(a, b, ...) |
Prints all values separated by spaces, then a new line. Works with every type, including arrays and structs. |
input(), input("question") |
Reads one line typed by the user (without the newline). Empty text at end of input. |
read_file(path) |
The whole file as str. Stops the program with a clear message if it can't be read, unless handled: read_file(p) or .... |
write_file(path, text) |
Writes (replaces) the file; gives back true if it worked. With or / try, the failure carries the reason. |
file_exists(path) |
true / false |
delete_file(path) |
Deletes the file; true if it worked (or the reason, with or / try). |
error(message) |
The failure a fallible function returns: return error("..."). |
shell(command) |
Runs a shell command, gives back its exit code. |
exit(code) |
Ends the program right away. |
addr(x) |
A pointer to x; addr(function) gives C a function pointer (callback). |
new(value), free(p) |
Manual heap memory (see Memory). |
int(x), str(x), u8(x), … |
Conversions (see Conversions). str(x) works on every type and gives the same text print shows. int(text) or 0 handles text that isn't a number. |
16. Modules
Split a program into files. import shapes loads shapes.fch from the same folder (or from a
folder listed in the FINCH_PATH environment variable, separated by :).
// shapes.fch
struct Box {
float w
float h
}
fn area(Box b) -> float {
return b.w * b.h
}
// main.fch
import shapes
fn main() {
b := shapes.Box(2, 3)
print(shapes.area(b))
shapes.Box other // a module's type in a declaration
}
- Everything in a module is used with the module's name in front:
shapes.area,shapes.Box. - A module doesn't have
main. Modules can import other modules.
17. Using C libraries
Step 1: import the header
import "stdio.h"
import "GLFW/glfw3.h" // a path inside the system include folders
import "mylib.h" // a header next to your .fch file
Finch reads the header with libclang (clang's C parser) and makes these usable:
- functions, including ones with a variable number of arguments, like
printf, - structs, by value and by pointer (
Vector2,SDL_Rect,CXCursor, …), with their fields, enumvalues and simple#defineconstants (M_PI,GL_COLOR_BUFFER_BIT),- global variables, like
stdoutandstderr.
Step 2: link the library
link "glfw" // uses libglfw.so (pkg-config is asked first, if installed)
link "helpers.c" // compile and link your own C file (next to the .fch file)
link "prebuilt.a" // or an object file / static library
If you forget the link line, Finch tells you exactly what is missing:
error: glfwInit, glfwCreateWindow, glfwMakeContextCurrent and 5 more come from "GLFW/glfw3.h", but its library isn't linked
...
link "glfw"
How C types appear in Finch
| C | Finch |
|---|---|
char, signed char, unsigned char |
char, i8, u8 |
short, int, long, long long |
i16, i32, i64, i64 (by actual size) |
unsigned … |
u16, u32, u64 |
float, double |
f32, f64 |
_Bool / bool |
bool |
enum |
the matching signed integer |
char * as a parameter or result |
str |
char * inside a struct or behind a pointer |
ptr[char] (use str(p) to read it) |
struct X by value |
X, a struct with the same fields |
struct X *, int *, … |
ptr[X], ptr[i32], … |
int arr[16] inside a struct |
[16]i32: index it and use .len |
void *, function pointers, incomplete structs |
ptr |
Example: structs and callbacks
import "raylib.h"
link "raylib"
fn main() {
InitWindow(800, 450, "Finch")
while !WindowShouldClose() {
BeginDrawing()
ClearBackground(Color(r: 30, g: 30, b: 40, a: 255))
DrawCircleV(Vector2(400, 225), 50, Color(255, 136, 0, 255))
EndDrawing()
}
CloseWindow()
}
To give C a function to call back (like qsort's comparator), use addr(myFunction).
Its parameters must be C types (numbers, pointers, C structs).
Graphics: OpenGL and Vulkan
Both work directly; examples/opengl.fch and examples/vulkan.fch are complete programs.
-
OpenGL 3+ on Linux: the modern functions (
glCreateShader,glGenVertexArrays, …) are only declared whenGL_GLEXT_PROTOTYPESis defined. Finch can't define C macros, so put the includes in a tiny header next to your program and import that:// opengl.h #define GL_GLEXT_PROTOTYPES #include <GL/gl.h> #include <GL/glext.h> #include <GLFW/glfw3.h>Then
import "opengl.h",link "glfw",link "GL". Out-parameters useaddr(...)(glGenBuffers(1, addr(vbo))), buffer data uses.ptr(vertices.ptr), and byte offsets useptr(...)(glVertexAttribPointer(1, 3, GL_FLOAT, GL_FALSE, stride, ptr(8))). -
Vulkan:
import "vulkan/vulkan.h"andlink "vulkan". Vulkan's create-info structs are built with named fields (missing ones are zero), handles areptr, and lists come from the usual "call twice" pattern: once for the count, once withlist.ptrafterlist.resize(count). Function-like macros such asVK_MAKE_API_VERSIONaren't available; compute the number instead (1 << 22is Vulkan 1.0).
What does not work yet
- C unions by value, and structs with bit fields by value (pointers to them work).
- Function-like macros and macros that are not a single number.
long double, 128-bit integers.
18. Errors
Errors as values
Some failures are normal: the user types abc, a file is missing. Finch handles them without
exceptions and without hidden control flow. A function that can fail says so with ! after its result
type, and fails with return error("message"):
fn parse_port(str text) -> int! {
n := int(text) or { return error("'" + text + "' is not a number") }
if n < 1 || n > 65535 {
return error("port " + str(n) + " is out of range")
}
return n
}
fn save(str path, str text) -> ! { // can fail, gives back nothing
try write_file(path, text)
}
The caller must handle the failure, in one of three ways (forgetting is a compile error):
| Code | On failure |
|---|---|
port := parse_port(s) or 8080 |
use this value instead (it's only computed on failure; it can be another call with or) |
port := parse_port(s) or { print(err); return } |
run the block; err is the message (a str). The block must leave (return, break, continue, exit) unless the result isn't used. |
port := try parse_port(s) |
fail the current function with the same error (it must be fallible too) |
fn load(str path) -> Config! {
text := try read_file(path)
port := try parse_port(text.trim())
return Config(port: port)
}
fn main() -> ! { // main can fail too: the error is printed, and the exit code is 1
config := try load("app.conf")
...
}
Built-ins that can fail work the same way: int(text), float(text), read_file, write_file,
delete_file. Without or / try they behave as before (int("x") stops the program,
write_file gives back a bool).
Under the hood a fallible function returns its value plus a flag and the message: no allocation on success, no unwinding, and the cost of a failure is building its message.
Compile errors
game.fch:12:9: error: 'score' must be int, but this is str
12 | score = "high"
| ^
Format: file:line:column: error: explanation, then the line with a ^ under the spot.
Runtime errors
Instead of undefined behavior (as in C), Finch stops with a message and exit code 1:
| Message | Cause |
|---|---|
runtime error: division by zero |
/ or % by a variable that was 0 |
runtime error: division overflows … |
the smallest signed number divided by −1 |
runtime error: index 5 is out of range (the length is 3) |
a[5] on a shorter array or str |
runtime error: pop() on an empty array |
|
runtime error: used .value on a null pointer |
also .field and [i] through null |
runtime error: can't turn "x" into int |
int(...) / float(...) on text that isn't a number |
runtime error: can't read the file "…" |
read_file on a missing or unreadable file (handle it with or) |
runtime error: the key "x" is not in the map … |
m["x"] when the map has no such key (use .has or .get) |
runtime error: called .f() on a null pointer |
a method called through a null ptr[T] |
19. Editors: VS Code, Kate and others
Every editor feature comes from the same language server, finch lsp, which uses the real compiler:
errors as you type, types and docs on hover, go to definition, completion (fields, methods, map and array
methods after ., module members, names from C headers), the file's outline, and parameter hints.
Kate (and KWrite, KDevelop)
editors/kate/install.sh
It installs the highlighting (~/.local/share/org.kde.syntax-highlighting/syntax/finch.xml) and adds finch
to Kate's LSP client settings, keeping your other servers. Restart Kate and enable the LSP Client plugin.
To run programs, add finch run %f as a target in the Build & Run plugin; Finch's
file:line:column: error: messages are then clickable. Details: editors/kate/README.md.
VS Code
The Finch extension (editors/vscode, also attached to each release as a .vsix file) gives you:
highlighting, errors as you type, completion (also fields and methods after .), types on hover,
go to definition (F12), an outline of the file, parameter hints, snippets, and a ▶ Run button
(Ctrl+F5). Errors from Finch: Build This File appear in the Problems panel.
Install: Extensions → … → Install from VSIX… → pick finch-lang-*.vsix. The extension runs finch
from your PATH; if it is somewhere else, set Finch: Path in the settings.
Others
Neovim, Helix, Zed, Emacs, Sublime and any other editor that speaks the Language Server Protocol:
run finch lsp for files ending in .fch (stdin/stdout, no options).
20. Debugging
finch build game.fch -g -O0 -o game
gdb ./game
(gdb) break game.fch:12
(gdb) run
(gdb) bt # where are we, through which functions
(gdb) info locals # all variables
(gdb) print player.lives
-g adds debug info (lines, functions, variables, struct fields). -O0 keeps every variable
visible; without it the optimizer may remove some. On Windows, -g writes CodeView, which the
Visual Studio debugger and WinDbg read.
21. Troubleshooting
| Problem | Solution |
|---|---|
can't find the C header 'x.h' |
The library's development files are missing. Install them (Debian: libx-dev; Nix: add to shell.nix). |
… come from "x.h", but its library isn't linked |
Add link "name" at the top of the file. |
the library 'x' wasn't found |
Not installed, or a wrong name. On Nix, add it to shell.nix and run inside nix-shell. |
the C function 'f' can't be used from Finch yet |
It uses a union, bit fields or another unsupported type by value. Look for a variant that takes pointers. |
can't find the module 'x' |
Put x.fch next to the importing file, or set FINCH_PATH. |
couldn't build the Finch runtime |
No working C compiler. Install gcc or clang, or set CC. |
| A library is not found when the program starts | It was linked from a folder the system doesn't search. Run inside the same nix-shell, or install it system-wide. |
Windows: finch can't link (LNK… errors about libcmt, kernel32) |
Install Visual Studio Build Tools with Desktop development with C++. |
Windows: finch.exe doesn't start (missing libclang.dll) |
Keep libclang.dll next to finch.exe. |
macOS: finch doesn't start (Library not loaded: …libLLVM…) |
brew install llvm@21. |
macOS: can't find the C header 'stdio.h' |
Install the command line tools: xcode-select --install. |
--target arm64: no C compiler found to link with |
Install aarch64-linux-gnu-gcc (Debian: gcc-aarch64-linux-gnu), or set FINCH_CC. |
clang inside nix-shell can't find stdio.h |
You have an old shell.nix: llvmPackages.clang must be listed before llvmPackages.libclang. |
| A program never ends | A loop condition never becomes false. Press Ctrl+C. |
22. Project layout and tests
Finch/
├── src/ the compiler, in Finch (lexer, parser, gen: LLVM IR as text, cimport: libclang, lsp, main ...)
├── std/ the standard library, in Finch: json, path, time, proc
├── runtime/ finch_rt.c: the small runtime (text, arrays, maps, input, files)
├── bootstrap/ build.sh, install.sh and the seeds (the compiler's own IR, one per system)
├── examples/ hello, tour, types, structs, todo, guess, c_import, window, opengl, vulkan, raylib, llvm
├── tests/
│ ├── run/ new/ programs + the exact output they must print (.out), input (.in)
│ ├── fail/ new/fail/ programs that must fail, with the expected error in line 1
│ ├── run.sh the test runner (MEMCHECK=1 also checks memory with valgrind)
│ ├── cross.sh every test built for Windows / ARM64 and run under Wine / QEMU
│ └── lsp_test.py the language server, talked to like an editor does
├── editors/ the VS Code extension (vscode/), Kate highlighting and LSP setup (kate/)
├── docs/ this documentation (en, pl), including the cheat sheet
└── shell.nix the Nix development environment
tests/run.sh # programs, refusals, -g builds, a shared library, the language server
MEMCHECK=1 tests/run.sh # the same under valgrind: no leaks, no bad memory access
tests/cross.sh windows # every test built for Windows and run with Wine
tests/cross.sh arm64 # every test built for ARM64 Linux and run with QEMU
23. Enums and match
An enum is a type with a short list of named values.
enum Color { Red, Green, Blue }
enum Status { Ok = 200, NotFound = 404 } // give a variant a number if it needs one
c := Color.Green
print(c) // Green
print(int(Status.NotFound)) // 404
print(Color(2)) // Blue: a number turned into the enum (checked: Color(7) is an error)
Variants are written Color.Red. They can be compared with == and != (not with <), used as map keys,
struct fields and array elements, and an enum can have methods like a struct: fn Color.next() -> Color { ... }.
match picks what to do from a value, instead of a ladder of if:
match c {
Red -> print("warm") // inside a match on an enum, the bare name is enough
Green, Blue -> print("cool") // several values for one arm
}
size := match n { // as an expression: it gives a value
1, 2, 3 -> "small"
4, 5 -> "medium"
else -> "large"
}
match word {
"quit" -> {
print("bye")
return
}
else -> print("what?")
}
match works on enums, whole numbers, chars, text and bools. The arms are tried in order. A match on an enum has to
handle every variant or end with else (the compiler says which are missing); any other match needs else.
In an expression, every arm gives the same type.
24. Generics
Write a function or a struct once for any type, with the type as a name in square brackets:
fn max[T](T a, T b) -> T {
if a > b {
return a
}
return b
}
struct Stack[T] {
[]T items
}
fn Stack[T].push(T x) {
self.items.push(x)
}
print(max(3, 9), max("apple", "pear")) // T is worked out from the arguments
print(max[float](1, 2.5)) // or written out, where it can't be worked out
s := Stack[int]() // a generic struct is always written with its type
s.push(7)
Several parameters work (struct Pair[A, B]), and so do nested types (Stack[Pair[int, str]]). Each use makes its
own copy of the code, so a generic is as fast as code written by hand. There are no constraints: if T can't do
what the function does (a > b on a struct), the error says so and tells which version it was building, like
(while building bigger[Point]).
25. The standard library
Modules that come with Finch, written in Finch. import json finds json.fch next to your file first, and the
library's second, so your own file of the same name wins.
| Module | What it does |
|---|---|
path |
join, base, dir, ext, stem, parts, clean, is_abs: paths as text, separated by / |
time |
now(), millis() (a clock for measuring), sleep(ms), date(t), local(t), format(t, "%Y-%m-%d"), days_in_month |
proc |
run(cmd) (the exit code), output(cmd) (what it printed; an error if it failed), env(name, fallback), pid(), cwd() |
json |
parse(text), stringify(v), pretty(v), and the value type Json with get, at, set, push, has, str_or, int_or ... |
import json
import path
doc := try json.parse(read_file("config.json"))
name := doc.get("name").str_or("unnamed") // a missing key is null, so chains never fail
doc.set("name", json.new_text(name + "!"))
write_file(path.join("out", "config.json"), json.pretty(doc))
26. Current limits
Not there yet:
- Interfaces, and enums whose variants carry values (
Circle(radius)). - Unicode-aware text:
.len,s[i]andupper()work on bytes / ASCII. - Threads.
- Windows on ARM64, 32-bit systems, WebAssembly.
See the roadmap in the README.