mirror of
https://github.com/Nexoniarz/Finch.git
synced 2026-11-03 00:44:27 +00:00
- technicians' guides: section 31 rewritten around kernels (masks, the end of a list, moving lanes, int8 dot products, choosing instructions at build and at run time) - cheat sheets and engineers' guides: the new operations and their IR; how features are found - Kate and VS Code: @cpu, boolxN and the new built-ins highlighted Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Psc42moPLwP43Cmb9oZqSR
1554 lines
62 KiB
Markdown
1554 lines
62 KiB
Markdown
# 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](for-engineers.md).
|
||
|
||
> 🇵🇱 Polska wersja: [dla-technikow.md](../pl/dla-technikow.md)
|
||
|
||
---
|
||
|
||
## Contents
|
||
|
||
1. [What Finch is](#1-what-finch-is)
|
||
2. [Installing](#2-installing)
|
||
3. [The finch command](#3-the-finch-command)
|
||
4. [How a Finch file looks](#4-how-a-finch-file-looks)
|
||
5. [Variables](#5-variables)
|
||
6. [Types](#6-types)
|
||
7. [Operators](#7-operators)
|
||
8. [Control flow](#8-control-flow)
|
||
9. [Functions](#9-functions)
|
||
10. [Arrays and maps](#10-arrays-and-maps)
|
||
11. [Text (str)](#11-text-str)
|
||
12. [Structs and methods](#12-structs-and-methods)
|
||
13. [Memory: who frees what](#13-memory-who-frees-what)
|
||
14. [Pointers](#14-pointers)
|
||
15. [Built-in functions](#15-built-in-functions)
|
||
16. [Modules](#16-modules)
|
||
17. [Using C libraries](#17-using-c-libraries)
|
||
18. [Errors](#18-errors)
|
||
19. [Editors: VS Code, Kate and others](#19-editors-vs-code-kate-and-others)
|
||
20. [Debugging](#20-debugging)
|
||
21. [Troubleshooting](#21-troubleshooting)
|
||
22. [Project layout and tests](#22-project-layout-and-tests)
|
||
23. [Enums and match](#23-enums-and-match)
|
||
24. [Generics](#24-generics)
|
||
25. [The standard library](#25-the-standard-library)
|
||
26. [Current limits](#26-current-limits)
|
||
27. [Tests: test blocks and finch test](#27-tests-test-blocks-and-finch-test)
|
||
28. [A project: finch.toml](#28-a-project-finchtoml)
|
||
29. [Threads](#29-threads)
|
||
30. [C++, Rust and Android](#30-c-rust-and-android)
|
||
31. [Vectors (SIMD)](#31-vectors-simd)
|
||
|
||
---
|
||
|
||
## 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](for-engineers.md)). It runs on **Linux** (x86-64 and
|
||
ARM64), **macOS** (Apple Silicon and Intel) and **Windows** (x86-64), and builds programs for all of them. On every
|
||
system it needs **clang** (it compiles each program with it) and **libclang** from LLVM 21 (it reads C headers with it).
|
||
|
||
### Option A: a ready build
|
||
|
||
Download the archive for your system from the [releases](https://github.com/Nexoniarz/Finch/releases) and unpack it.
|
||
Keep `runtime/` and `std/` next to the `finch` file (or install it, below).
|
||
|
||
| System | Archive | Also install |
|
||
|---|---|---|
|
||
| Linux x86-64 | `finch-linux-x64.tar.gz` | clang 21 and libclang (table below) |
|
||
| Linux ARM64 (Raspberry Pi 4/5, Graviton…) | `finch-linux-arm64.tar.gz` | the same |
|
||
| macOS, Apple Silicon | `finch-macos-arm64.tar.gz` | `xcode-select --install` (clang and the system headers), `brew install llvm@21` (libclang) |
|
||
| macOS, Intel | `finch-macos-x64.tar.gz` | the same |
|
||
| Windows x64 | `finch-windows-x64.zip` (`finch.exe` with `libclang.dll`) | [llvm-mingw](https://github.com/mstorsjo/llvm-mingw/releases) (the `ucrt-x86_64` zip): unpack it and put its `bin` folder on your PATH |
|
||
|
||
On Linux:
|
||
|
||
| Distribution | Packages |
|
||
|---|---|
|
||
| Debian / Ubuntu | `clang-21 libclang-21-dev` from [apt.llvm.org](https://apt.llvm.org), `pkg-config` |
|
||
| Fedora | `clang clang-devel pkgconf` |
|
||
| Arch | `clang pkgconf` |
|
||
| Nix | `nix-shell` in the repository gives all of it |
|
||
|
||
On Windows, llvm-mingw gives Finch `clang`, the C headers and the linker in one folder; Windows programs built with it
|
||
need nothing else to run. Keep `libclang.dll` next to `finch.exe`.
|
||
|
||
### Option B: build it yourself
|
||
|
||
```sh
|
||
git clone https://github.com/Nexoniarz/Finch.git
|
||
cd Finch
|
||
nix-shell # Linux: 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`.
|
||
|
||
On a Mac, with Homebrew's LLVM:
|
||
|
||
```sh
|
||
brew install llvm@21
|
||
export FINCH_CLANG="$(brew --prefix llvm@21)/bin/clang"
|
||
export FINCH_LIBCLANG="-L$(brew --prefix llvm@21)/lib -lclang"
|
||
export FINCH_LLVM_INCLUDE="$(brew --prefix llvm@21)/include"
|
||
bootstrap/build.sh
|
||
```
|
||
|
||
On Windows, in Git Bash, with llvm-mingw on the PATH and the official LLVM for Windows (for `libclang.dll` and its headers)
|
||
unpacked to `C:/llvm`:
|
||
|
||
```sh
|
||
export FINCH_LIBCLANG="C:/llvm/bin/libclang.dll"
|
||
export FINCH_LLVM_INCLUDE="C:/llvm/include"
|
||
export FINCH_LDFLAGS="-LC:/llvm/bin"
|
||
bootstrap/build.sh # build/finch.exe, with libclang.dll copied next to it
|
||
```
|
||
|
||
### 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>` | |
|
||
|
||
```sh
|
||
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)
|
||
|
||
```sh
|
||
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 test <file.fch> run its test "..." { } blocks (section 27)
|
||
finch fmt [--check] [files/dirs] tidy the indentation of .fch files (all under here by default)
|
||
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
|
||
-I <dir> look for C headers there too
|
||
--target <name> build for another system: windows, linux, arm64, macos, android, or an LLVM triple
|
||
--cpu <name> use every instruction of a processor: native (this one), x86-64-v3 (AVX2),
|
||
x86-64-v4 (AVX-512), apple-m1, ... (section 31)
|
||
-g add debug info (for gdb and lldb)
|
||
-O0 skip optimizations
|
||
--trace runtime errors show the chain of calls (on anyway for run, test, -g and -O0)
|
||
--no-trace they don't (a plain finch build doesn't: keeping the chain makes calls slower)
|
||
```
|
||
|
||
In a folder with a `finch.toml` (section 28), `finch run`, `finch build` and `finch test` need no file name.
|
||
|
||
The compiled program and the C files it builds (`link "x.c"`) are kept in a cache (`~/.cache/finch`, or
|
||
`%LOCALAPPDATA%` on Windows). The second build of the same code, or of code whose C files did not change, skips that work.
|
||
|
||
`finch fmt` changes only the layout: four spaces per `{ }` level, no spaces at line ends, at most one empty line in a row.
|
||
Lines that continue an expression (inside `( )` or after a `+`) and `"""` text keep their shape. `finch fmt --check`
|
||
changes nothing and fails if a file would change (for CI). Editors with the language server format on request.
|
||
|
||
Examples:
|
||
|
||
```sh
|
||
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
|
||
|
||
```c
|
||
// 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 {`, not `if (x > 5) {`.
|
||
- Functions and structs can be in any order in the file.
|
||
- `main` is `fn main()`, `fn main() -> int` (returns the exit code) or `fn main([]str args)`
|
||
(gets the command-line arguments; `args[0]` is the program's own path).
|
||
|
||
---
|
||
|
||
## 5. Variables
|
||
|
||
```c
|
||
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 enum match defer or try const export`
|
||
|
||
### Constants
|
||
|
||
```c
|
||
const WIDTH = 640
|
||
const HEIGHT = WIDTH / 2 // from other constants
|
||
const f32 GRAVITY = 9.8 // with a type
|
||
const TITLE = "My game"
|
||
```
|
||
|
||
A constant is written at the top level of a file, next to the functions, and can't be changed. It is made of
|
||
numbers, text, `true`/`false`, chars, other constants and operators; a module's constant is `shapes.SIDES`.
|
||
|
||
---
|
||
|
||
## 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:
|
||
|
||
```c
|
||
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`.
|
||
|
||
### Decimal numbers: what is guaranteed
|
||
|
||
`f32` and `f64` are IEEE 754 numbers, and Finch computes them **exactly as written**, the same on every processor and
|
||
with every `--cpu`:
|
||
|
||
- Each operation rounds once, in the order written. `a * b - c * d` is two multiplies and a subtract, never folded into a
|
||
fused multiply-add (C compilers often do that by default, which changes the last digits).
|
||
- Nothing is reordered or simplified: `(a + b) + c` is not turned into `a + (b + c)`, and `x * 0.0` is not assumed to be 0
|
||
(it is NaN when `x` is). The one exception is `v.sum()` on a vector, which adds the lanes in the fastest order.
|
||
|
||
When speed matters more than the last bit, ask for it by name:
|
||
|
||
| | |
|
||
|---|---|
|
||
| `fma(a, b, c)` | `a * b + c` with a single rounding, always. One instruction where the processor has FMA (with `--cpu`, on ARM64); a slower library call elsewhere. |
|
||
| `muladd(a, b, c)` | `a * b + c` the fastest way on this processor: fused where it can be, a multiply and an add where not. |
|
||
|
||
Both work on numbers and on vectors (`v.fma(b, c)`, `v.muladd(b, c)`).
|
||
|
||
**Numbers as bits.** File formats and model weights store numbers as raw bits. These look at the bits without
|
||
converting the value (all of them also work lane by lane on vectors):
|
||
|
||
| | |
|
||
|---|---|
|
||
| `f32_bits(x)` / `f32_from_bits(u)` | `f32` ↔ the `u32` with the same bits (`f32_bits(1.0)` is `0x3f800000`) |
|
||
| `f64_bits(x)` / `f64_from_bits(u)` | `f64` ↔ `u64` |
|
||
| `f16_to_f32(h)` / `f32_to_f16(x)` | half precision, kept in a `u16`, ↔ `f32` (rounded to the nearest) |
|
||
| `bf16_to_f32(b)` / `f32_to_bf16(x)` | bfloat16, kept in a `u16`, ↔ `f32` (rounded to the nearest, ties to even) |
|
||
|
||
```c
|
||
[]u16 weights = read_weights() // f16 numbers from a file
|
||
v := f16_to_f32(u16x8(weights, 0)) // 8 of them as f32, at once
|
||
```
|
||
|
||
---
|
||
|
||
## 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 != 0` means `(flags & 4) != 0`.
|
||
|
||
Notes:
|
||
|
||
- `/` on whole numbers rounds toward zero: `7 / 2` is `3`. `%` takes the sign of the left side.
|
||
- Dividing by zero **stops the program** with `runtime error: division by zero` and 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 against `null`. A `str` is never `null`.
|
||
- Arrays and structs can't be compared with `==`; compare their parts.
|
||
|
||
---
|
||
|
||
## 8. Control flow
|
||
|
||
`if` can also give a value; each branch holds one value, and the `else` is needed:
|
||
|
||
```c
|
||
label := if score > 100 { "high" } else if score > 10 { "mid" } else { "low" }
|
||
f32 speed = if running { 2.5 } else { 1.0 }
|
||
```
|
||
|
||
```c
|
||
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; write `if x != 0 {`.
|
||
- In `for i in a..b`, both ends are evaluated **once**. `i` is an `int` and cannot be changed in the loop.
|
||
- In `for x in list`, `x` cannot 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 a `break` or `return`.
|
||
- Code after `return`, `break` or `continue` in 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:
|
||
|
||
```c
|
||
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
|
||
|
||
```c
|
||
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](#14-pointers).
|
||
- A function with `-> type` must 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](#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
|
||
|
||
```c
|
||
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.
|
||
|
||
```c
|
||
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}` |
|
||
|
||
```c
|
||
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)
|
||
|
||
```c
|
||
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) |
|
||
|
||
Values go into text with `\(...)`: any code, and `, 2` after a number for two digits after the point.
|
||
A `\` before the `(` you want to keep: `"\\(not code)"`.
|
||
|
||
```c
|
||
print("hp \(hp) of \(max_hp), speed \(v, 2), name \(p.name.upper())")
|
||
```
|
||
|
||
`str` keeps its length, so `.len` is instant, and it is always NUL-terminated, so passing it
|
||
to C as `char*` works. The other way round, a C function that gives back a `char *` gives you a `str`
|
||
(a copy is made when you keep it), and `str(p)` reads a `ptr[char]` you got some other way.
|
||
|
||
Text over several lines goes between `"""`. The line break after the opening quotes is dropped, and so is the
|
||
indentation of the closing quotes, from every line, so the text can be indented with your code:
|
||
|
||
```c
|
||
shader := """
|
||
#version 330
|
||
void main() { gl_Position = vec4(0.0); }
|
||
"""
|
||
```
|
||
|
||
Numbers as text: `str(x)` is the shortest text that reads back as the same number (`0.1`, `0.30000000000000004`,
|
||
`1e+20`); `str(x, 2)` always has two digits after the point (`3.14`).
|
||
|
||
---
|
||
|
||
## 12. Structs and methods
|
||
|
||
```c
|
||
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 `[]Node` for 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.
|
||
|
||
```c
|
||
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 `self` on a loop variable (`for p in team { p.heal(1) }`) is an error,
|
||
because the loop variable is read-only; use `for i in 0..team.len { team[i].heal(1) }`.
|
||
- Like functions, methods can fail (`-> !`); see [Errors as values](#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 list` moves 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):
|
||
|
||
```c
|
||
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 |
|
||
|
||
```c
|
||
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](#13-memory-who-frees-what)). |
|
||
| `fma(a, b, c)`, `muladd(a, b, c)` | `a * b + c` with one rounding / the fastest way (see [Decimal numbers](#decimal-numbers-what-is-guaranteed)). |
|
||
| `f32_bits(x)`, `f32_from_bits(u)`, `f16_to_f32(h)`, … | A number's bits, and half-precision numbers (see [Decimal numbers](#decimal-numbers-what-is-guaranteed)). |
|
||
| `int(x)`, `str(x)`, `u8(x)`, … | Conversions (see [Conversions](#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 `:`).
|
||
|
||
```c
|
||
// shapes.fch
|
||
struct Box {
|
||
float w
|
||
float h
|
||
}
|
||
|
||
fn area(Box b) -> float {
|
||
return b.w * b.h
|
||
}
|
||
```
|
||
|
||
```c
|
||
// 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.
|
||
- Modules in folders: `import ui/button` loads `ui/button.fch` and is used as `button.draw()`.
|
||
- Another name: `import ui/button as btn` → `btn.draw()`. Two imports can't use the same name.
|
||
|
||
---
|
||
|
||
## 17. Using C libraries
|
||
|
||
### Step 1: import the header
|
||
|
||
```c
|
||
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,
|
||
- **`enum` values** and **simple `#define` constants** (`M_PI`, `GL_COLOR_BUFFER_BIT`),
|
||
- **global variables**, like `stdout` and `stderr`.
|
||
|
||
### Step 2: link the library
|
||
|
||
```c
|
||
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
|
||
|
||
```c
|
||
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 when `GL_GLEXT_PROTOTYPES` is defined. Finch can't define C macros, so put the includes in a
|
||
tiny header next to your program and import that:
|
||
|
||
```c
|
||
// 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 use `addr(...)`
|
||
(`glGenBuffers(1, addr(vbo))`), buffer data uses `.ptr` (`vertices.ptr`), and byte offsets use
|
||
`ptr(...)` (`glVertexAttribPointer(1, 3, GL_FLOAT, GL_FALSE, stride, ptr(8))`).
|
||
- **Vulkan**: `import "vulkan/vulkan.h"` and `link "vulkan"`. Vulkan's create-info structs are built with
|
||
named fields (missing ones are zero), handles are `ptr`, and lists come from the usual "call twice"
|
||
pattern: once for the count, once with `list.ptr` after `list.resize(count)`. Function-like macros such as
|
||
`VK_MAKE_API_VERSION` aren't available; compute the number instead (`1 << 22` is Vulkan 1.0).
|
||
|
||
### What does not work yet
|
||
|
||
- C **unions** and structs with **bit fields** passed to or returned from a C function **by value** (pointers to them
|
||
work, and so do unions as variables: `SDL_Event e`, `e.type`, `e.key.key`, `SDL_PollEvent(addr(e))`).
|
||
- **Function-like macros** (`#define MAX(a, b) ...`). Macros that are a number, a text, an expression of them
|
||
(`(1u << 3)`, `((Uint64)1)`, `(A | B)`) or a struct of numbers (raylib's `RAYWHITE`, `(Color){ 245, 245, 245, 255 }`) work.
|
||
- `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")`:
|
||
|
||
```c
|
||
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) |
|
||
|
||
```c
|
||
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]` |
|
||
| `runtime error: assert failed: …` | `assert(cond, "…")` with a false `cond` |
|
||
|
||
Under the message comes the chain of calls that led there, the innermost first:
|
||
|
||
```
|
||
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)
|
||
```
|
||
|
||
`finch run`, `finch test`, `-g` and `-O0` builds keep it; a plain `finch build` leaves it out because keeping it costs
|
||
time in every call (about 3x in a program that is nothing but tiny calls, much less in a real one). `--trace` puts it in.
|
||
|
||
---
|
||
|
||
## 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)
|
||
|
||
```sh
|
||
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
|
||
|
||
```sh
|
||
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 the debug info is DWARF as well: use the `lldb` that comes with llvm-mingw, or gdb.
|
||
|
||
---
|
||
|
||
## 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. |
|
||
| `--cpu native`: the program stops with `Illegal instruction` on another computer | It was built for the processor that built it. Build without `--cpu`, or with an older one (`x86-64-v2`). |
|
||
| `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: `no C compiler found to link with ('clang')` | Put llvm-mingw's `bin` folder on your PATH (section 2). |
|
||
| 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, fs, thread
|
||
├── 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, wordcount, c_import, simd, 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
|
||
│ ├── tool/ finch test, finch fmt, the chain of calls, a finch.toml project
|
||
│ ├── 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
|
||
```
|
||
|
||
```sh
|
||
tests/run.sh # programs, expected errors, -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.
|
||
|
||
### Variants that hold values
|
||
|
||
A variant can carry values. Make one with `Shape.Circle(2.0)`, take them out with `match`, which names them:
|
||
|
||
```c
|
||
enum Shape {
|
||
Circle(float)
|
||
Rect(float, float)
|
||
Point
|
||
}
|
||
|
||
fn Shape.area() -> float {
|
||
return match self {
|
||
Circle(r) -> 3.14 * r * r
|
||
Rect(w, h) -> w * h
|
||
Point -> 0.0
|
||
}
|
||
}
|
||
```
|
||
|
||
`_` ignores a value. `match` must handle every variant (or end with `else`). Printing shows `Rect(2, 3.5)`. Such an enum is
|
||
copied and freed like a struct; it can't be compared with `==` (match on it). Variants of these enums can't have numbers.
|
||
|
||
## 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` ... |
|
||
| `fs` | `read_bytes(p)` / `write_bytes(p, data)` (`[]u8`, any bytes), `exists`, `is_dir`, `mkdir` (with the folders above), `list` (sorted names), `remove_dir`, `data_dir(app)` (a folder for the user's saves) |
|
||
| `thread` | `spawn(addr(work), addr(data))`, `t.join()`, `mutex()` with `lock`, `unlock`, `close`, `cpus()` (section 29) |
|
||
|
||
`min(a, b, ...)`, `max(a, b, ...)`, `abs(x)` and `clamp(x, lo, hi)` work on any numbers without an import (a function of
|
||
your own with the same name wins).
|
||
|
||
```
|
||
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.
|
||
- Unicode-aware text: `.len`, `s[i]` and `upper()` work on bytes / ASCII.
|
||
- Checks that stop two threads from changing the same array at once (use a mutex).
|
||
- Windows on ARM64, 32-bit systems, WebAssembly.
|
||
|
||
### Warnings about big copies
|
||
|
||
A struct with 256 bytes or more of plain data (numbers, structs of numbers, C's `[N]T`) is copied byte by byte when it
|
||
is a parameter (`fn f(Body b)`) and in `for x in bodies`. Finch says so, and still builds the program:
|
||
|
||
```
|
||
game.fch:27:11: warning: 'b' is a Body (264 bytes), copied on every call to weight; take ptr[Body] to share it instead (callers can still pass the variable as it is)
|
||
game.fch:45:5: warning: each Body (264 bytes) is copied into 'x'; for i in 0..bodies.len with bodies[i] works on it in place
|
||
```
|
||
|
||
Text, arrays and maps inside a struct don't count: passing the struct copies only their handles, not what they hold.
|
||
`b := a` is not warned about: it says "copy" in the code. A method never copies `self`. Editors show the warnings in yellow.
|
||
|
||
See the [roadmap in the README](../../README.md#roadmap).
|
||
|
||
## 27. Tests: test blocks and finch test
|
||
|
||
Tests go next to the code they test:
|
||
|
||
```c
|
||
fn damage(int hp, int hit) -> int {
|
||
return max(hp - hit, 0)
|
||
}
|
||
|
||
test "damage never goes below zero" {
|
||
assert(damage(5, 10) == 0)
|
||
assert(damage(5, 2) == 3, "got \(damage(5, 2))")
|
||
}
|
||
```
|
||
|
||
`finch test game.fch` runs every `test` block of the file and of the modules it imports (not of the standard library),
|
||
each on its own: a failed `assert` or any runtime error ends only that test, and the next one runs.
|
||
|
||
```
|
||
ok damage never goes below zero
|
||
FAIL loading a level
|
||
level.fch:31: runtime error: can't read the file "levels/1.txt"
|
||
|
||
1 passed, 1 failed
|
||
```
|
||
|
||
The exit code is 1 when a test failed, so CI can use it. `finch run` and `finch build` leave test blocks out.
|
||
`assert(cond)` also works outside tests: it stops the program with `assert failed` when `cond` is false.
|
||
|
||
## 28. A project: finch.toml
|
||
|
||
A `finch.toml` next to the code says once what would otherwise go on every command line:
|
||
|
||
```toml
|
||
name = "game" # the program finch build makes (from the entry)
|
||
entry = "src/main.fch" # used when finch run / build / test get no file
|
||
paths = ["src", "lib"] # folders with Finch modules
|
||
include = ["vendor/SDL3/include"] # folders with C headers (also for your .c / .cpp files)
|
||
links = ["SDL3", "native/glue.cpp"] # libraries, and your own .c / .cpp / .o / .a files
|
||
libdirs = ["vendor/SDL3/lib"] # folders with libraries
|
||
cflags = ["-DFAST"] # for your .c / .cpp files
|
||
ldflags = ["-Wl,-rpath,$ORIGIN"] # for the linker
|
||
cpu = "native" # the same as --cpu native (section 31)
|
||
|
||
[target.windows] # added only when building for Windows
|
||
links = ["ws2_32"]
|
||
libdirs = ["vendor/SDL3/lib/x64"]
|
||
|
||
[target.linux-arm64] # system and CPU: windows, linux, macos, android, and -x86_64 / -arm64
|
||
libdirs = ["vendor/SDL3/lib/arm64"]
|
||
```
|
||
|
||
Folders are relative to `finch.toml`. Finch looks for it in the file's folder and the folders above, so
|
||
`finch run src/tools/editor.fch` inside the project uses it too. Only this much of TOML is read: `[sections]`,
|
||
`key = "text"`, `key = ["a", "b"]` (over several lines too), numbers, `true`/`false` and `#` comments.
|
||
|
||
## 29. Threads
|
||
|
||
```c
|
||
import thread
|
||
|
||
struct Chunk {
|
||
[]float samples
|
||
float peak
|
||
}
|
||
|
||
fn analyse(ptr arg) { // a thread's function takes one ptr
|
||
ptr[Chunk] c = arg
|
||
for s in c.samples {
|
||
c.peak = max(c.peak, abs(s))
|
||
}
|
||
}
|
||
|
||
fn main() {
|
||
[]Chunk chunks = load_chunks()
|
||
[]thread.Thread running
|
||
for i in 0..chunks.len {
|
||
running.push(thread.spawn(addr(analyse), addr(chunks[i])) or {
|
||
print(err)
|
||
exit(1)
|
||
})
|
||
}
|
||
for i in 0..running.len {
|
||
running[i].join() // wait for all of them
|
||
}
|
||
}
|
||
```
|
||
|
||
What a thread gets is shared, not copied, so:
|
||
|
||
- it must live until `join()` (here `chunks` lives to the end of `main`);
|
||
- don't change the array the threads point into (no `push`) until they are joined;
|
||
- what more than one thread changes goes under a lock, or is a number changed with `atomic_...`.
|
||
|
||
```c
|
||
m := thread.mutex()
|
||
defer m.close()
|
||
m.lock()
|
||
log.push("done") // one thread at a time between lock and unlock
|
||
m.unlock()
|
||
|
||
atomic_add(addr(done), 1) // done is an int shared by the threads
|
||
n := atomic_load(addr(done))
|
||
```
|
||
|
||
`atomic_add`, `atomic_sub`, `atomic_swap` give back the old value; `atomic_cas(p, old, new)` puts `new` only if `old`
|
||
is there and says whether it did. `thread.cpus()` says how many threads the machine runs at once.
|
||
|
||
## 30. C++, Rust and Android
|
||
|
||
Finch talks to other languages through C, the way every language does.
|
||
|
||
**C++**: put what Finch calls behind a C header, with `extern "C"` on the C++ side:
|
||
|
||
```cpp
|
||
// glue.h
|
||
#ifdef __cplusplus
|
||
extern "C" {
|
||
#endif
|
||
int physics_step(float dt);
|
||
#ifdef __cplusplus
|
||
}
|
||
#endif
|
||
```
|
||
|
||
and `import "glue.h"` with `link "glue.cpp"` (or `links = ["glue.cpp"]` in `finch.toml`). The `.cpp` file is built with
|
||
the C++ compiler (`clang++` or `g++`, or `FINCH_CXX`), which then also links, so the C++ library comes along.
|
||
A C++ library that has a C API (Box2D 3, Jolt's JoltC, PhysX's C wrapper) is imported like any C library.
|
||
|
||
**Rust**: make a `staticlib` and export C functions:
|
||
|
||
```rust
|
||
// Cargo.toml: [lib] crate-type = ["staticlib"]
|
||
#[no_mangle]
|
||
pub extern "C" fn path_len(x: f32, y: f32) -> f32 { (x * x + y * y).sqrt() }
|
||
```
|
||
|
||
Write the header (`float path_len(float x, float y);`, or let `cbindgen` write it), then `import "pathfind.h"` and
|
||
`link "target/release/libpathfind.a"`. On Linux a Rust static library also needs `links = ["dl", "pthread"]`.
|
||
|
||
**Android**: `finch build game.fch --target android --shared` makes `libgame.so` for 64-bit ARM Android (API 24 and
|
||
up) with the NDK's clang, found through `ANDROID_NDK_HOME` (or `FINCH_CC`). Load it from an app, for example through
|
||
SDL3's Android project (the library's `SDL_main` is an `export fn`). Thread-local data goes through the emulated TLS
|
||
that older Android versions need.
|
||
|
||
---
|
||
|
||
## 31. Vectors (SIMD)
|
||
|
||
A processor can add or multiply several numbers with one instruction: 4 floats at once with SSE, 8 with AVX2, 16 with
|
||
AVX-512, 4 with ARM's NEON. That is SIMD (single instruction, multiple data), and it is how image filters, audio, physics
|
||
and neural networks get their speed. In Finch it is a type: **`f32x8` is 8 `f32` side by side**.
|
||
|
||
```c
|
||
a := f32x4(1.0, 2.0, 3.0, 4.0)
|
||
b := f32x4(0.5) // 0.5 in every lane
|
||
print(a + b) // f32x4(1.5, 2.5, 3.5, 4.5)
|
||
print(a * 2.0) // a plain number counts for every lane: f32x4(2, 4, 6, 8)
|
||
print(a.sum(), a[2], a.len) // 10 3 4
|
||
```
|
||
|
||
**The types** are a number type, `x`, and how many lanes: `f32x4` `f32x8` `f32x16`, `f64x2` `f64x4` `f64x8`,
|
||
`i8x16` `i8x32` `i8x64`, `i16x8` `i16x16`, `i32x4` `i32x8` `i32x16`, `i64x2` `i64x4`, and the same with `u` (unsigned).
|
||
2 to 64 lanes, at most 512 bits in all. A vector is a plain value like an `int`: it goes into variables, struct fields,
|
||
arrays, maps and function parameters, and `print` and `str` show it.
|
||
|
||
**Making one:**
|
||
|
||
| | |
|
||
|---|---|
|
||
| `f32x8(x)` | `x` in every lane |
|
||
| `f32x8(a, b, c, d, e, f, g, h)` | one number per lane |
|
||
| `f32x8(list, i)` | `list[i]` … `list[i+7]` from a `[]f32` (or through a `ptr[f32]`); stops the program if they aren't all in the list |
|
||
| `f32x8(v)` | another 8-lane vector converted lane by lane: `f32x8(ints)`, `i32x8(floats)` (cuts the fractions), `i16x8(i32s)` |
|
||
|
||
**Working with it:**
|
||
|
||
| | |
|
||
|---|---|
|
||
| `+ - * /` | lane by lane (`/` on floats only: x86 and ARM can't divide whole numbers lane by lane) |
|
||
| `& \| ^ << >> ~` | lane by lane, on whole-number lanes |
|
||
| `-v` | every lane negated |
|
||
| `v[i]`, `v[i] = x`, `v.len` | one lane; the number of lanes |
|
||
| `min(v, w)`, `max(v, w)`, `abs(v)`, `clamp(v, lo, hi)` | lane by lane; `lo`, `hi` and `w` can be plain numbers |
|
||
| `v.sum()`, `v.min()`, `v.max()` | all the lanes made into one number |
|
||
| `v.dot(w)` | `(v * w).sum()` |
|
||
| `v.sqrt()`, `v.floor()`, `v.ceil()`, `v.round()` | every lane (float lanes) |
|
||
| `v.fma(b, c)` | `v * b + c` with one rounding, always (the same on every processor) |
|
||
| `v.muladd(b, c)` | `v * b + c` the fastest way here: one instruction where the processor can (the last bit may differ between processors) |
|
||
| `v.store(list, i)` | writes the lanes to `list[i]` … (or through a `ptr`) |
|
||
|
||
**A real loop.** The usual shape: as many whole vectors as fit, then the rest one by one.
|
||
|
||
```c
|
||
fn dot([]f32 a, []f32 b) -> f32 {
|
||
acc := f32x8(0.0)
|
||
i := 0
|
||
while i + 8 <= a.len {
|
||
acc = f32x8(a, i).muladd(f32x8(b, i), acc)
|
||
i += 8
|
||
}
|
||
total := acc.sum()
|
||
while i < a.len {
|
||
total += a[i] * b[i]
|
||
i += 1
|
||
}
|
||
return total
|
||
}
|
||
```
|
||
|
||
`examples/simd.fch` times this against the plain loop.
|
||
|
||
### Masks: comparing, choosing, the end of a list
|
||
|
||
Comparing vectors compares every lane, so the answer is a **mask**: one `true`/`false` a lane, of type `boolx8` (for 8 lanes).
|
||
|
||
```c
|
||
v := f32x8(-2.0, -1.0, 0.0, 1.0, 2.0, 3.0, -4.0, 5.0)
|
||
m := v > 0.0 // boolx8(false, false, false, true, true, true, false, true)
|
||
relu := select(m, v, 0.0) // v where the mask is true, 0 elsewhere
|
||
print(m.count(), m.any(), m.all()) // 4 true false
|
||
if (v == w).all() { ... } // a condition needs one bool: all() or any()
|
||
```
|
||
|
||
| | |
|
||
|---|---|
|
||
| `v < w`, `<=`, `>`, `>=`, `==`, `!=` | a mask (a plain number on one side counts for every lane) |
|
||
| `select(m, a, b)` | lane by lane: `a` where `m` is true, `b` where false (either may be a plain number) |
|
||
| `m.any()`, `m.all()`, `m.count()` | is any lane true, are all, how many |
|
||
| `m & n`, `m \| n`, `m ^ n`, `!m` | masks combined |
|
||
| `m[i]` | one lane, as a `bool` |
|
||
| `i32x8(m)`, `boolx8(v)` | true is 1; a lane that isn't 0 is true |
|
||
|
||
**The end of a list.** A list is rarely a whole number of vectors long. Two forms touch only the lanes that are inside it:
|
||
|
||
| | |
|
||
|---|---|
|
||
| `f32x8(list, i, fill)` | `list[i]` … as far as the list goes; the lanes past its end are `fill` |
|
||
| `v.store_part(list, i)` | writes only the lanes that fit in the list |
|
||
|
||
```c
|
||
fn add_one([]f32 xs) {
|
||
i := 0
|
||
while i < xs.len { // no separate loop for the rest
|
||
(f32x8(xs, i, 0.0) + 1.0).store_part(xs, i)
|
||
i += 8
|
||
}
|
||
}
|
||
```
|
||
|
||
### Moving lanes
|
||
|
||
| | |
|
||
|---|---|
|
||
| `v.shuffle(3, 2, 1, 0)` | the lanes with these numbers (written in the code), in this order; as many lanes as numbers |
|
||
| `v.lo()`, `v.hi()` | the lower and the upper half |
|
||
| `concat(a, b)` | `a`'s lanes, then `b`'s |
|
||
| `table.lookup(idx)` | for every lane of `idx`: `table[idx % table.len]`. A byte table (`u8x16`, `i8x16`) is one `pshufb` on x86, `tbl` on ARM |
|
||
|
||
Unpacking 4-bit numbers (two to a byte, as quantized models store them) is shuffle, mask and lookup:
|
||
|
||
```c
|
||
fn unpack4(u8x8 packed, i8x16 meaning) -> i8x16 {
|
||
pairs := packed.shuffle(0, 0, 1, 1, 2, 2, 3, 3, 4, 4, 5, 5, 6, 6, 7, 7) // each byte twice
|
||
odd := u8x16(0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1) == 1
|
||
nibbles := select(odd, pairs >> 4, pairs & 15) // low nibble, then high
|
||
return meaning.lookup(nibbles) // 0..15 -> what it stands for
|
||
}
|
||
```
|
||
|
||
### Int8 dot products: dot_add
|
||
|
||
`acc.dot_add(a, b)` is the step of a quantized model's matrix multiply. `acc` is an `i32x8` of sums; `a` and `b` have four
|
||
times as many bytes (`u8x32` and `i8x32`; `i8` × `i8` and `u8` × `u8` work too). Each lane of `acc` gains the sum of four
|
||
products, `a[4k]*b[4k] + … + a[4k+3]*b[4k+3]`, exactly (no saturation), and the sums stay in the vector:
|
||
|
||
```c
|
||
acc := i32x8(0)
|
||
i := 0
|
||
while i + 32 <= weights.len {
|
||
acc = acc.dot_add(u8x32(activations, i), i8x32(weights, i))
|
||
i += 32
|
||
}
|
||
total := acc.sum() // once, at the end
|
||
```
|
||
|
||
It is one instruction where the processor has one: `vpdpbusd` with AVX-VNNI (Intel since Alder Lake, AMD Zen 5) or
|
||
AVX-512 VNNI, `usdot`/`sdot`/`udot` on ARM with the dot-product features. Elsewhere it is a few instructions
|
||
(`pmaddwd` and adds), with the same result. Which one is used depends on `--cpu` or the function's `@cpu` (below).
|
||
|
||
### Choosing the instructions: --cpu, @cpu and cpu_has
|
||
|
||
By default a program runs on every processor of its system (on x86-64 that means SSE2, so an `f32x8` is done as two
|
||
halves). Three ways to use more:
|
||
|
||
**`--cpu`, for the whole program.** `--cpu native` (or `cpu = "native"` in `finch.toml`) lets LLVM use everything the
|
||
processor that builds it has: AVX2 and FMA, AVX-512, VNNI, or NEON's newer parts on ARM. The program then needs a processor
|
||
like that one. To pick a level instead: `--cpu x86-64-v3` (AVX2, most PCs since 2015), `--cpu x86-64-v4` (AVX-512),
|
||
`--cpu alderlake`, `--cpu apple-m1`, `--cpu neoverse-v1`.
|
||
|
||
**`@cpu`, for one function, and `cpu_has` to choose at run time.** One program can carry an AVX-512 version, an AVX2
|
||
version and a plain one, and pick the best the computer it runs on has:
|
||
|
||
```c
|
||
@cpu("avx512f,avx512bw,avx512vl,avx512vnni")
|
||
fn matmul_avx512([]u8 a, []i8 b, []i32 out) { ... } // may use AVX-512 inside
|
||
|
||
@cpu("x86-64-v3")
|
||
fn matmul_avx2([]u8 a, []i8 b, []i32 out) { ... } // a level works too
|
||
|
||
fn matmul([]u8 a, []i8 b, []i32 out) {
|
||
if cpu_has("avx512vnni") && cpu_has("avx512vl") {
|
||
matmul_avx512(a, b, out)
|
||
} else if cpu_has("avx2") {
|
||
matmul_avx2(a, b, out)
|
||
} else {
|
||
matmul_plain(a, b, out)
|
||
}
|
||
}
|
||
```
|
||
|
||
- The features are x86's `sse3 ssse3 sse4.1 sse4.2 popcnt avx avx2 fma f16c bmi1 bmi2 avx512f avx512bw avx512dq avx512vl
|
||
avx512vnni avx512bf16 avx512fp16 avxvnni`, the levels `x86-64-v2`/`v3`/`v4`, and ARM's `neon fp16 dotprod i8mm bf16 sve sve2`.
|
||
- Names of the other architecture are left out when building, so the x86 and ARM versions sit side by side in one file
|
||
(and `cpu_has` says `false` for them).
|
||
- Call a `@cpu` function only after `cpu_has` said yes: on a processor without the feature it stops with *Illegal instruction*.
|
||
- A vector wider than 16 bytes (`f32x8`, `i32x16` …) can't be passed between functions with different `@cpu`: the two
|
||
sides would expect it in different registers. Pass the list (or a `ptr`) instead; Finch says so if you try.
|
||
|
||
Lanes wider than the processor's registers are split by LLVM, and the same code builds for x86-64 and ARM64 alike.
|
||
`v.sum()` on float lanes adds them in whatever order is fastest, so the last digits can differ from adding them one by one.
|
||
|
||
On NixOS, Nix's clang wrapper removes `-march=native` inside `nix-shell`. Finch tells the wrapper to keep it, so
|
||
`--cpu native` works there too; if it ever has no effect, Finch warns.
|