Finch Cheat Sheet
Everything in Finch on one page. For explanations, see the guides.
🇵🇱 Polska wersja: sciaga.md
The finch command
| Command |
Does |
finch run file.fch [args…] |
compile and run (args go to the program) |
finch build file.fch [-o name] |
make a program (name.exe on Windows) |
finch build file.fch --shared [-I dir] |
make a library (libfile.so, file.dll) that a C program loads; its export fns are its C functions (numbers, bool, char, pointers) |
finch test file.fch |
run its test "…" { } blocks, each on its own (and those of the modules it imports) |
finch fmt [--check] [files/folders] |
indent by { } with 4 spaces, no spaces at line ends, at most one empty line (--check: only list what it would change) |
finch run / build / test with no file |
in a folder with a finch.toml: its entry |
finch ir file.fch |
print the LLVM IR |
finch lsp |
the language server (editors use it) |
finch version |
version |
-l lib |
link a C library (like link "lib") |
--target windows / linux / arm64 / macos / android / triple |
build for another system (android: the NDK's clang from ANDROID_NDK_HOME, usually with --shared) |
-g |
debug info (gdb, lldb, Visual Studio) |
-O0 |
no optimizations |
--trace / --no-trace |
runtime errors show the chain of calls; on for run, test, -g, -O0, off for a plain build (it costs speed) |
Environment: FINCH_PATH (folders with modules), FINCH_CC (the C compiler that links), FINCH_CXX (for .cpp files).
A program's compiled code and its link "x.c" files are kept in the cache (~/.cache/finch) until they or their headers change.
finch.toml (next to the code; finch run there needs no file and no flags):
name = "game" # what finch build makes
entry = "src/main.fch"
paths = ["src"] # folders with modules
include = ["vendor/SDL3/include"] # C headers
links = ["SDL3", "native/glue.cpp"] # libraries and your own .c / .cpp / .o / .a
libdirs = ["vendor/SDL3/lib"]
cflags = ["-DFAST"] # for your .c / .cpp files
ldflags = ["-Wl,-rpath,."]
[target.windows] # only when building for that system (also linux, macos, android, linux-arm64)
links = ["ws2_32"]
A file
import "stdio.h" // a C header
import shapes // a Finch module: shapes.fch
link "m" // a C library, or "mine.c" / ".o" / ".a"
struct Point {
int x
int y = 0 // default value
}
fn main() {
print("Hello")
}
main can be fn main(), fn main() -> int (exit code) or fn main([]str args).
One statement per line, no semicolons, { on the same line, no brackets around conditions.
Variables
| Code |
Meaning |
x := 5 |
new variable, type inferred |
int x = 5 |
new variable, type written |
int x |
zero / "" / [] / null / the struct's defaults (also of the structs inside it) |
x = 7 |
assign |
x += 1 -= *= /= %= |
update |
const W = 640, const f32 G = 9.8 |
a constant (top level of a file; shapes.W from a module) |
Types
| Type |
Example |
int (= i64) |
42, -7, 0xFF, 1_000_000 |
float (= f64) |
3.14, 1e9, 2.5e-3 |
bool |
true, false |
char |
'A', '\n' |
str |
"text\t\"quoted\"", "hp \(hp), speed \(v, 2)" (code in the text; , 2 = two digits after the point), """ several lines """ (the closing quotes' indentation is removed) |
i8 i16 i32 i64 |
signed, sized |
u8 u16 u32 u64 |
unsigned, sized |
f32 f64 |
floats |
[]T |
array: [1, 2, 3], []str names |
map[K]V |
map: ["a": 1], map[str]int counts, [:] |
ptr[T], ptr |
pointer, untyped pointer |
Name / module.Name |
a struct |
Escapes: \n \t \r \0 \\ \" \'.
Operators
| Priority |
Operators |
| 5 |
* / % << >> & |
| 4 |
+ - | ^ (+ also joins text) |
| 3 |
== != < <= > >= |
| 2 |
&& |
| 1 |
|| |
| unary |
-x !x ~x |
x & 1 == 0 means (x & 1) == 0.
Control flow
if a > b { … } else if a == b { … } else { … }
x := if a > b { a } else { b } // as a value: one value per branch, else needed
while x > 0 { … }
for i in 0..10 { … } // 0 … 9
for item in list { … } // every element (or char of a str)
for i, item in list { … } // with its index
for key, value in m { … } // every map entry, in insertion order
break continue return value
defer cleanup() // runs when the block ends
Functions
fn add(int a, int b) -> int {
return a + b
}
fn greet(str name) { // no result
print("Hi", name)
}
Parameters behave like copies. Order in the file doesn't matter. A comma after the last argument is allowed.
Methods
fn Point.move(int dx, int dy) { // self is the Point it's called on (not a copy)
self.x += dx
self.y += dy
}
fn Point.dist() -> float { … }
p.move(1, 2) list[0].move(1, 1) ptr_to_point.move(0, 1)
Also on structs from C: fn Vector2.length() -> f32 { … }.
Errors as values
fn parse(str s) -> int! { // ! = can fail
if s.len == 0 {
return error("empty")
}
return int(s) or { return error("not a number: " + s) }
}
fn save(str p, str t) -> ! { // can fail, no result
try write_file(p, t)
}
| Code |
On failure |
x := parse(s) or 0 |
use 0 |
x := parse(s) or { print(err); return } |
run the block (err: the message) |
x := try parse(s) |
fail the current function too |
fn main() -> ! |
a failure prints error: …, exit code 1 |
Fallible built-ins: int(s), float(s), read_file, write_file, delete_file.
Structs
| Code |
Meaning |
struct P { … } |
define (one field per line, = default optional) |
P(1, 2) |
create, all fields in order |
P(x: 1) |
create by name, the rest default/zero |
p.x, p.x = 5 |
read, change a field |
b := a |
a full copy |
Arrays []T
| Code |
Gives |
a.len |
number of elements |
a[i], a[i] = x |
element (checked) |
a.push(x) |
add at the end |
a.pop() |
remove the last, give it back |
a.insert(i, x) |
put at position i |
a.remove(i) |
remove position i, give it back |
a.clear() |
empty it |
a.resize(n) |
length n (new elements are zero) |
a.find(x) |
index or -1 |
a.contains(x) |
bool |
a.sort() |
sort (numbers, chars, str) |
a.reverse() |
reverse |
a.slice(s, e) |
new array of s … e-1 |
a.join(sep) |
[]str → one str |
a.ptr |
address of the first element (for C) |
Maps map[K]V
| Code |
Gives |
m := ["a": 1, "b": 2] |
a map (keys: numbers, char, bool, str) |
m[k] |
value (missing key: the program stops) |
m[k] = v |
add / replace |
m[k] += 1, m[k].push(x) |
a missing key starts at the default (0, [], …) |
m.len |
number of keys |
m.has(k) |
bool |
m.get(k, default) |
value or default |
m.remove(k) |
bool: was it there |
m.clear() |
empty it |
m.keys(), m.values() |
arrays, insertion order |
Text str
| Code |
Gives |
s.len |
bytes |
s[i], s[i] = 'x' |
char |
a + b |
joined text |
"x = \(x)" |
the value of any code in the text (\(x, 2): a float with 2 digits) |
s.sub(a, b) |
part a … b-1 |
s.find(t) |
position or -1 |
s.contains(t) |
bool |
s.starts_with(t), s.ends_with(t) |
bool |
s.split(sep) |
[]str |
s.trim() |
no spaces at the ends |
s.upper(), s.lower() |
A–Z / a–z |
s.replace(a, b) |
every a → b |
s.repeat(n) |
n times |
s.bytes() |
[]u8 |
s.ptr |
ptr[char] (for C; a str also goes to a const char * parameter as it is) |
== != < > |
compare |
Conversions
| Code |
Result |
int(3.9) |
3 |
int("42") |
42 (stops the program if it isn't a number) |
int('A') |
65 |
float(7), float("2.5") |
7, 2.5 |
u8(300) |
44 (keeps the low bits) |
char(66) |
'B' |
bool(0) |
false |
str(42), str(1.5), str(true), str('c') |
text (a float: the shortest text that is the same number) |
str(3.14159, 2) |
"3.14" (that many digits after the point) |
str([1, 2]), str(m), str(point) |
the same text print shows |
int("x") or 0, float(s) or 0.0 |
a number, or the fallback |
str(bytes) |
[]u8 → text |
str(p) |
C char* → text |
ptr(p), ptr(16) |
untyped pointer, from a pointer or a number |
int(p) |
pointer → address |
Automatic only when nothing is lost: smaller → bigger number, any int → float, f32 → f64.
Built-in functions
| Function |
Does |
print(a, b, …) |
print, spaces between, newline at the end |
input(), input("Question? ") |
read a line |
read_file(path) |
file → str |
write_file(path, text) |
str → file, true if it worked |
file_exists(path) |
bool |
delete_file(path) |
bool |
shell(command) |
run a command, its exit code |
exit(code) |
stop the program now (also ends an or { } block) |
min(a, b, …), max(a, b, …), abs(x), clamp(x, lo, hi) |
for any numbers (your own function of that name wins) |
error(message) |
return error("…") in a -> T! function |
addr(x) |
pointer to x; addr(fn) = C callback |
assert(cond), assert(cond, "why") |
stop with a runtime error when cond is false (in a test block: that test fails) |
atomic_add(p, n), atomic_sub, atomic_load(p), atomic_store(p, n), atomic_swap(p, n), atomic_cas(p, old, new) |
a whole number other threads change too, through its ptr (addr(count)) |
new(value) |
value on the heap → ptr[T] |
free(p) |
give it back |
Pointers
| Finch |
C |
ptr[int] p |
int *p |
addr(x) |
&x (a struct variable given where ptr[ThatStruct] goes: push(body) = push(addr(body))) |
p.value |
*p |
p.field |
p->field |
p[i] |
p[i] |
null |
NULL |
ptr |
void * |
Memory
- Arrays, text and structs free themselves at the end of their block. No
free needed.
b := a copies; a call's result moves; return x moves.
- Passing a struct:
fn f(Body b) works on its own copy (the caller's stays as it was), fn f(ptr[Body] b) changes the caller's (f(body) or f(addr(body))), a method gets self by address, never a copy.
new(...) + free(...) (often with defer) for linked lists, trees, sharing.
- C memory: C's own functions (
malloc / free, fclose, …).
Modules
import shapes // shapes.fch next to this file (or in FINCH_PATH)
shapes.area(b) // its functions
shapes.Box(2, 3) // its structs
shapes.Box other // in declarations
import ui/button // ui/button.fch: button.draw()
import ui/button as btn // another name: btn.draw()
C
| Code |
Meaning |
import "x.h" |
functions, structs, enums, #define numbers, globals |
link "name" |
libname.so / name.lib (pkg-config first) |
link "file.c", link "glue.cpp" |
compile your own C or C++ file (C++: functions in extern "C") |
addr(myFn) |
pass a Finch function as a callback |
| C type |
Finch |
int / long long / unsigned |
i32 / i64 / u32 (by size) |
float / double |
f32 / f64 |
char * (parameter, result) |
str |
char * (field, behind a pointer) |
ptr[char] |
struct X |
X |
T * |
ptr[T] |
void *, handles |
ptr |
T x[N] (in a struct) |
[N]T |
union U |
U: U u, u.field, U(field: v), by pointer to C (not by value) |
#define N 4, "text", (1u << 3), ((Uint64)2) |
constants (not function-like macros) |
#define RAYWHITE (Color){ 245, 245, 245, 255 } |
a Color |
char * is str: a C function that takes or returns char * takes or gives a Finch str (no .ptr, no conversion); only a char * field or a char ** stays ptr[char].
C++ and Rust: through a C header. C++ with extern "C" (and link "x.cpp"), Rust with #[no_mangle] pub extern "C" fn built as a staticlib (link "target/release/libgame.a") and a header (cbindgen writes one).
A C function's char * result is a str already; structs by value work with the System V, Windows x64 and ARM64 Linux calling conventions (tested in emulators; macOS by CI).
Runtime errors (the program stops with file and line)
| Message |
Cause |
division by zero |
/ or % by 0 |
index 5 is out of range (the length is 3) |
a[5] |
pop() on an empty array |
|
used .value on a null pointer |
.value / .field / [i] through null |
can't turn "x" into int |
int("x") |
can't read the file "…" |
read_file (without or) |
the key "x" is not in the map |
m["x"] |
assert failed: why |
assert(cond, "why") |
Below the message, the calls that led there (finch run, finch test, -g, -O0, --trace):
game.fch:6: runtime error: index 3 is out of range (the length is 2)
in Enemy.next (game.fch:6)
in step (game.fch:10)
in main (game.fch:15)
Tests
test "jump height" { // next to the code; finch test file.fch runs them, finch run/build leave them out
p := Player()
p.jump()
assert(p.y > 0.0, "y is \(p.y)")
}
Editors
Kate: editors/kate/install.sh, then enable the LSP Client plugin; run with Build & Run → finch run %f.
Any LSP editor: finch lsp.
VS Code (extension "Finch")
| Key / action |
Does |
▶ / Ctrl+F5 |
run the file |
Finch: Build This File |
build; errors go to Problems |
F12 |
go to definition |
| hover |
type / signature / docs |
Ctrl+Space |
completion (after .: fields, methods, module members) |
Ctrl+Shift+O |
functions and structs in the file |
main, fn, fnr, fnf, method, struct, for, foreach, forkv, map, orb, if, ife, while, input + Tab |
snippets |
setting finch.path |
where finch / finch.exe is |
Enums and match
enum Color { Red, Green, Blue } enum Status { Ok = 200, NotFound = 404 }
c := Color.Green int(c) Color(2) c == Color.Red // == and != only; Color(7) is an error
fn Color.next() -> Color { ... } // methods work
match c { Red -> ... Green, Blue -> { ... } else -> ... } // bare variant names; every variant or else
x := match n { 1, 2 -> "few" else -> "many" } // an expression: every arm the same type
Generics
fn max[T](T a, T b) -> T { ... } max(3, 9) max[float](1, 2.5) // worked out, or written out
struct Stack[T] { []T items } fn Stack[T].push(T x) { ... } // the type goes with it:
s := Stack[int]() Stack[int] t Pair[str, int]("age", 31) // never just Stack()
Standard library (import name; your own file of that name wins)
|
|
path |
join(a, b) base(p) dir(p) ext(p) stem(p) parts(p) clean(p) is_abs(p) |
time |
now() millis() sleep(ms) date(t) local(t) format(t, fmt) days_in_month(y, m) |
proc |
run(cmd) output(cmd) (fails with its output) env(name, fallback) pid() cwd() |
thread |
spawn(addr(work), addr(data)) → t.join(); mutex() → m.lock() m.unlock() m.close(); cpus() (a thread's function: fn work(ptr arg)) |
fs |
read_bytes(p) write_bytes(p, data) exists(p) is_dir(p) mkdir(p) list(dir) remove_dir(p) data_dir(app) |
json |
parse(text) stringify(v) pretty(v) new_text/number/bool/null/list/object v.get(k) v.at(i) v.set(k, x) v.push(x) v.has(k) v.str_or(d) v.int_or(d) v.float_or(d) v.bool_or(d) |
Keywords
fn return if else while for in break continue true false null import link struct enum match defer or try const export
(test and as only where they mean something: test "name" { }, import x as y)
(and self inside methods)