Finch/docs/en/cheatsheet.md
Claude 0ceaaa27c4
Floats as written, never fused; fma and muladd; f32/f64 bits, f16 and bf16
- a * b + c is two roundings on every processor and --cpu: the IR has no fast-math flags, and clang
  compiles it with -ffp-contract=on (checked in the assembly: never fused, also with FMA hardware)
- fma(a, b, c) / v.fma(b, c): one rounding, always (llvm.fma)
- muladd(a, b, c) / v.muladd(b, c): the fastest way here (llvm.fmuladd), what v.fma was before
- f32_bits / f32_from_bits / f64_bits / f64_from_bits: bitcasts, also of vectors
- f16_to_f32 / f32_to_f16 (LLVM half), bf16_to_f32 / f32_to_bf16 (integer ops, ties to even, NaN kept)
- docs (en, pl): what is guaranteed about floats, the new built-ins

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Psc42moPLwP43Cmb9oZqSR
2026-11-01 15:25:40 +00:00

18 KiB
Raw Blame History

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)
-O0 no optimizations
--cpu native / x86-64-v3 / apple-m1 … use every instruction of that processor (AVX2, AVX-512, NEON); the program then needs one like it
--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,."]
cpu = "native"                      # the same as --cpu native

[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
f32x8, i8x32, f64x4, u16x16 … vector: N numbers worked on at once (SIMD), see below
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

Vectors f32x8 (SIMD)

Types: f32x4/8/16, f64x2/4/8, i8x16/32/64, i16x8/16/32, i32x4/8/16, i64x2/4/8, and u… the same; at most 512 bits.

Code Gives
f32x8(x) x in every lane
f32x8(a, b, …, h) one number per lane
f32x8(list, i) list[i] … list[i+7] (a []f32 or ptr[f32])
f32x8(v) an 8-lane vector converted lane by lane (f32x8(ints), i32x8(floats))
v + w, v * 2.0, -v lane by lane; a plain number counts for every lane (/ only on floats)
& | ^ << >> ~ lane by lane, whole-number lanes
v == w, v != w bool: every lane equal / not
v[i], v[i] = x, v.len a lane; the number of lanes
min(v, w), max, abs(v), clamp(v, lo, hi) lane by lane
v.sum(), v.min(), v.max(), v.dot(w) one number from all the lanes
v.sqrt(), v.floor(), v.ceil(), v.round() every lane (float lanes)
v.fma(b, c), v.muladd(b, c) v * b + c: one rounding always / the fastest way here
v.store(list, i) lanes into list[i] …

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)
fma(a, b, c), muladd(a, b, c) a * b + c with one rounding / fused where the processor can (a * b + c written out is never fused)
f32_bits(x), f32_from_bits(u), f64_bits, f64_from_bits a float's bits as u32/u64 and back, unchanged
f16_to_f32(h), f32_to_f16(x), bf16_to_f32(b), f32_to_bf16(x) half and bfloat16 numbers (in a u16) ↔ f32; also on vectors
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. A struct with 256+ bytes of numbers passed by value or in for x in list gets a warning.
  • 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)