Finch 2.4.0: methods, maps, errors as values; str() of any value; macOS and ARM64 docs; Kate support

- fn Struct.method(...) with self by reference, also on C structs
- map[K]V: insertion-ordered hash maps, literals, m[k] += 1, has/get/remove/keys/values
- -> T! / -> !, return error(...), or <value>, or { err }, try; fallible int/float/read_file/write_file/delete_file
- for i, x in list / for k, v in map
- print and str() share one text formatter
- LSP: method and map completion, method signatures; grammar and snippets for VS Code and Kate
- tests: 74 (methods, errors, maps + 11 failure cases); docs at all three levels, en and pl

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Nexoniarz 2026-10-09 01:11:12 +02:00
commit b872be3176
55 changed files with 3400 additions and 336 deletions

View file

@ -16,7 +16,7 @@ Everything in Finch on one page. For explanations, see the [guides](../../README
| `finch lsp` | the language server (editors use it) |
| `finch version` | version |
| `-l lib` | link a C library (like `link "lib"`) |
| `--target windows` / `linux` / triple | build for another system |
| `--target windows` / `linux` / `arm64` / `macos` / triple | build for another system |
| `-g` | debug info (gdb, lldb, Visual Studio) |
| `-O0` | no optimizations |
@ -71,6 +71,7 @@ One statement per line, no semicolons, `{` on the same line, no brackets around
| `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 |
@ -100,6 +101,8 @@ if a > b { … } else if a == b { … } else { … }
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
```
@ -121,6 +124,47 @@ Parameters behave like copies. Order in the file doesn't matter. A comma after t
---
## Methods
```c
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
```c
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 |
@ -155,6 +199,23 @@ Parameters behave like copies. Order in the file doesn't matter. A comma after t
---
## 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 |
@ -189,6 +250,8 @@ Parameters behave like copies. Order in the file doesn't matter. A comma after t
| `char(66)` | `'B'` |
| `bool(0)` | `false` |
| `str(42)`, `str(1.5)`, `str(true)`, `str('c')` | text |
| `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 |
@ -210,6 +273,7 @@ Automatic only when nothing is lost: smaller → bigger number, any int → floa
| `delete_file(path)` | bool |
| `shell(command)` | run a command, its exit code |
| `exit(code)` | stop the program now |
| `error(message)` | `return error("…")` in a `-> T!` function |
| `addr(x)` | pointer to x; `addr(fn)` = C callback |
| `new(value)` | value on the heap → `ptr[T]` |
| `free(p)` | give it back |
@ -281,11 +345,17 @@ shapes.Box other // in declarations
| `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` |
| `can't read the file "…"` | `read_file` (without `or`) |
| `the key "x" is not in the map` | `m["x"]` |
---
## VS Code (extension "Finch")
## 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 |
|---|---|
@ -295,11 +365,12 @@ shapes.Box other // in declarations
| hover | type / signature / docs |
| `Ctrl+Space` | completion (after `.`: fields, methods, module members) |
| `Ctrl+Shift+O` | functions and structs in the file |
| `main`, `fn`, `fnr`, `struct`, `for`, `foreach`, `if`, `ife`, `while`, `input` + Tab | snippets |
| `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 |
---
## Keywords
`fn return if else while for in break continue true false null import link struct defer`
`fn return if else while for in break continue true false null import link struct defer or try`
(and `self` inside methods)

View file

@ -77,7 +77,7 @@ To check it works, type:
./build/finch version
```
You should see something like `finch 2.3.0`.
You should see something like `finch 2.4.0`.
---
@ -505,8 +505,68 @@ fn main() {
}
```
If you also want to know *where* you are in the list, give `for` two names: the first one counts
0, 1, 2…, the second one is the value:
```c
fn main() {
for i, fruit in ["apple", "banana", "cherry"] {
print(i + 1, fruit)
}
}
```
```
1 apple
2 banana
3 cherry
```
Lists can do more: `contains`, `find`, `insert`, `remove`, `sort`, `reverse`, `join`.
The [guide for technicians](for-technicians.md#arrays) lists them all.
The [guide for technicians](for-technicians.md#10-arrays-and-maps) lists them all.
### Looking things up by name: maps
A list finds things by their place: `fruits[0]`. Sometimes you want to find things by a **name**
instead, like looking up a word in a dictionary, or a phone number in a phone book.
That's a **map**: each **key** (the name) has a **value**:
```c
fn main() {
phone := ["Ola": "555-1234", "Jan": "555-9876"]
print(phone["Ola"]) // 555-1234
phone["Ewa"] = "555-0000" // add a new name
print(phone.len) // 3
if phone.has("Tom") {
print("Tom is here")
} else {
print("no Tom")
}
for name, number in phone { // every name with its number
print(name, number)
}
}
```
- `["key": value, ...]` makes a map. Keys are often text, but numbers work too.
- `phone["Ola"]` gets the value for a key. Asking for a key that isn't there stops the program,
so check first with `phone.has("Tom")`, or use `phone.get("Tom", "unknown")`.
- `for name, number in phone` visits every key with its value, in the order you added them.
Maps are great for **counting**. A new key starts at 0, so `+= 1` just works:
```c
fn main() {
map[str]int votes // an empty map: text keys, whole-number values
for v in ["cat", "dog", "cat", "cat", "fish"] {
votes[v] += 1
}
print(votes) // {"cat": 3, "dog": 1, "fish": 1}
}
```
---
@ -618,6 +678,36 @@ fn main() {
And you can keep many of them in a list: `[]Book shelf`, then `shelf.push(b)`.
### Commands for your struct: methods
A struct can have its own commands. Write the struct's name, a dot, and the command's name.
Inside, `self` is the struct the command was used on:
```c
struct Player {
str name
int lives = 3
}
fn Player.hit() {
self.lives -= 1
print(self.name, "has", self.lives, "lives left")
}
fn Player.is_alive() -> bool {
return self.lives > 0
}
fn main() {
p := Player(name: "Ola")
p.hit() // Ola has 2 lives left
p.hit() // Ola has 1 lives left
print(p.is_alive()) // true
}
```
`p.hit()` changes `p` itself, so after two hits `p.lives` really is 1.
---
## 15. Saving to a file
@ -680,6 +770,57 @@ Here we simply misspelled `age`.
| `index 5 is out of range (the length is 3)` | You asked a list for a place it doesn't have | Remember counting starts at 0; check `.len` |
| `can't turn "abc" into int` | `int(...)` got text that isn't a number | Check what the person typed |
| `can't use '+' on str and int` | Text plus a number | Use `str(...)` on the number |
| `'f' can fail, so say what happens then` | You used a command that can fail without saying what to do | Add `or ...` (see below) |
### Things that can fail: `or` and `try`
Some things can go wrong *while the program runs*: the person types `abc` where you wanted a number,
or a file isn't there. Instead of stopping the program, you can say what to do then, with **`or`**:
```c
fn main() {
age := int(input("Your age? ")) or 0 // not a number? use 0
print("Next year you'll be", age + 1)
note := read_file("note.txt") or "(no note yet)"
print(note)
}
```
`or` can also be followed by a block. Inside it, `err` is the message that says what went wrong:
```c
fn main() {
while true {
n := int(input("Pick a number: ")) or {
print("That's not a number, try again")
continue
}
print("You picked", n)
break
}
}
```
Your own functions can fail too. Put `!` after the result type, and use `return error("...")`:
```c
fn safe_divide(int a, int b) -> int! {
if b == 0 {
return error("can't divide by zero")
}
return a / b
}
fn main() {
print(safe_divide(10, 2) or -1) // 5
print(safe_divide(10, 0) or -1) // -1
}
```
Finch won't let you forget: calling `safe_divide(10, 0)` without `or` is an error that tells you
to handle it. (You can also pass the problem on with `try`; the
[guide for technicians](for-technicians.md#errors-as-values) explains it.)
---
@ -882,10 +1023,13 @@ struct Pet { // your own kind of value
int age = 1
}
// lists: nums := [1, 2, 3] nums.push(4) nums[0] nums.len for n in nums { }
// lists: nums := [1, 2, 3] nums.push(4) nums[0] nums.len for n in nums { } for i, n in nums { }
// maps: ages := ["Ola": 9] ages["Jan"] = 10 ages["Ola"] ages.has("Tom") for name, age in ages { }
// text: "a" + "b" str(42) int("42") s.len s.upper() s.contains("x")
// ask: name := input("Name? ")
// files: write_file("f.txt", text) read_file("f.txt")
// methods: fn Pet.grow() { self.age += 1 } p.grow()
// failing: n := int(text) or 0 x := read_file("f.txt") or { print(err) ... }
```
---
@ -905,7 +1049,9 @@ struct Pet { // your own kind of value
| **condition** | A yes/no question, like `x > 5` |
| **list / array** | Many values kept in order, like `[1, 2, 3]` |
| **index** | The position of a value in a list, starting at 0 |
| **struct** | Your own kind of value, made of named parts |
| **struct** | Your own kind of value made of named parts |
| **method** | A command that belongs to a struct, like `p.hit()` |
| **map** | Values you find by a key (a name), like a dictionary |
| **input** | What the person types into the program |
| **error** | A message saying something is wrong, and where |
| **bug** | A mistake in a program |

View file

@ -38,9 +38,9 @@ For the language from a user's point of view, see the [technicians' guide](for-t
20. [Semantics vs. C](#20-semantics-vs-c)
21. [Function reference](#21-function-reference)
22. [Extending the compiler](#22-extending-the-compiler)
23. [Targets and Windows](#23-targets-and-windows)
23. [Targets: Windows, macOS, ARM64](#23-targets-windows-macos-arm64)
24. [The language server](#24-the-language-server)
25. [The VS Code extension](#25-the-vs-code-extension)
25. [Editor integrations](#25-editor-integrations)
26. [Known limitations](#26-known-limitations)
---
@ -90,20 +90,24 @@ Design decisions that shape the rest:
| File | Lines | Responsibility |
|---|---|---|
| `src/error.h` | 40 | `g_files` (all sources), `failAt(file, line, col, msg)`, `fail(line, col, msg)` |
| `src/error.h` | 50 | `g_files` (all sources), `failAt(file, line, col, msg)`, `fail(line, col, msg)`, `FinchError` |
| `src/lexer.h/.cpp` | 250 | `Tok`, `Token`, `lex(fileIndex)`, `tokName()` |
| `src/ast.h` | 310 | `Type` (incl. Array, Fixed, Struct, Named), all `Expr`/`Stmt` nodes, `FnDecl`, `StructDecl`, `Import`, `Link`, `Program` |
| `src/parser.h/.cpp` | 540 | Recursive-descent `Parser`, `typeFromName()` |
| `src/cimport.h/.cpp` | 420 | libclang import: functions, C structs (fields, offsets), constants, globals |
| `src/ast.h` | 360 | `Type` (incl. Array, Map, Fixed, Struct, Named), all `Expr`/`Stmt` nodes, `FnDecl`, `StructDecl`, `Import`, `Link`, `Program` |
| `src/parser.h/.cpp` | 610 | Recursive-descent `Parser`, `typeFromName()` |
| `src/loader.h/.cpp` | 90 | Reading files and modules (`Loader`), `FINCH_PATH` |
| `src/target.h/.cpp` | 210 | `g_target`: triple, C compiler, processes, paths; backend initialisation |
| `src/cimport.h/.cpp` | 430 | libclang import: functions, C structs (fields, offsets), constants, globals |
| `src/codegen.h` | 15 | `generate()` |
| `src/codegen_impl.h` | 265 | The `Codegen` class and its helper structs, shared by the four files below |
| `src/codegen.cpp` | 1090 | Program, declarations, scopes and cleanups, statements, expressions, places (`ref`), operators, mutation analysis |
| `src/codegen_types.cpp` | 500 | LLVM types, type resolution, struct layout, coercions, ownership (copy/drop helpers), printing, DWARF types |
| `src/codegen_builtins.cpp` | 710 | Calls, constructors, built-ins, conversions, array and str methods, runtime declarations, panics |
| `src/abi.cpp` | 310 | System V x86-64 classification, C calls with structs by value, C-callable thunks |
| `src/main.cpp` | 390 | Driver: CLI, loader, target machine, O2, object emission, runtime cache, linking, link-error advice, `run` |
| `runtime/finch_rt.c` | 440 | The runtime library (§12) |
| `boot/*.fch` | 3080 | The self-hosted compiler (§17) |
| `src/codegen_impl.h` | 310 | The `Codegen` class and its helper structs, shared by the files below |
| `src/codegen.cpp` | 1460 | Program, declarations (functions, methods), scopes and cleanups, statements, expressions, places (`ref`), operators, errors as values, mutation analysis |
| `src/codegen_types.cpp` | 580 | LLVM types, type resolution, struct layout, coercions, ownership (copy/drop helpers), text formatting, debug types |
| `src/codegen_builtins.cpp` | 890 | Calls, methods, constructors, built-ins, conversions, array and str methods, runtime declarations, panics |
| `src/codegen_map.cpp` | 240 | Maps: indexing, literals, methods, iteration |
| `src/abi.cpp` | 370 | C calling conventions (System V x86-64, Microsoft x64, AAPCS64), C calls with structs by value, C-callable thunks |
| `src/lsp.cpp`, `src/index.h`, `src/builtins_doc.h` | 700 | The language server (§24) |
| `src/main.cpp` | 350 | Driver: CLI, target machine, O2, object emission, runtime cache, linking, link-error advice, `run` |
| `runtime/finch_rt.c` | 740 | The runtime library (§12) |
| `boot/*.fch` | 3100 | The self-hosted compiler (§17) |
---
@ -120,7 +124,8 @@ struct Token { Tok kind; std::string text; int line, col; bool newlineBefore; };
- **Positions:** 1-based; `col` counts characters (UTF-8 continuation bytes don't advance it), so
carets line up under `"Błąd"`. A stray non-ASCII character is reported whole, with a hint that names are ASCII-only.
- **Identifiers / keywords:** `[A-Za-z_][A-Za-z0-9_]*`. Keywords:
`fn return if else while for in break continue true false null import link struct defer`.
`fn return if else while for in break continue true false null import link struct defer or try`.
(`self` and `map` are ordinary identifiers with a meaning in context: inside a method, before `[`.)
**Type names are not keywords**: `int`, `u8`, `ptr`… are `Ident`s recognised by the parser through
`typeFromName()`, so `u8(x)` is an ordinary call and the lexer doesn't depend on the type list.
- **Numbers:** decimal or `0x` hex with `_` separators. No octal (`010` is 10). A `.` belongs to a number
@ -142,27 +147,31 @@ program = { import | link | struct | function } ;
import = "import" ( STRING | IDENT ) ; (* "x.h" = C header, name = Finch module *)
link = "link" STRING ; (* "glfw" | "file.c" | "file.o" | "lib.a" *)
struct = "struct" IDENT "{" { type IDENT [ "=" expr ] NEWLINE } "}" ;
function = "fn" IDENT "(" [ param { "," param } ] ")" [ "->" type ] block ;
function = "fn" IDENT [ "." IDENT ] "(" [ param { "," param } ] ")" [ "->" ( type [ "!" ] | "!" ) ] block ;
(* fn Point.move: a method; "!": can fail *)
param = type IDENT ;
type = "[" "]" type | "ptr" [ "[" type "]" ] | TYPENAME | IDENT [ "." IDENT ] ;
type = "[" "]" type | "map" "[" type "]" type | "ptr" [ "[" type "]" ]
| TYPENAME | IDENT [ "." IDENT ] ;
block = "{" { statement TERMINATOR } "}" ;
statement = if | while | for | return | "break" | "continue" | "defer" statement | block
| type IDENT [ "=" expr ] (* declaration, see atDeclaration *)
| IDENT ":=" expr
| target assignop expr (* target: var, field, element, p.value *)
| call | method ;
for = "for" IDENT "in" expr ( ".." expr block | block ) ; (* range | for-each *)
| call | method | try | orelse ;
for = "for" IDENT [ "," IDENT ] "in" expr ( ".." expr block | block ) ; (* range | for-each *)
assignop = "=" | "+=" | "-=" | "*=" | "/=" | "%=" ;
expr = or ; or = and { "||" and } ; and = cmp { "&&" cmp } ;
expr = or [ "or" ( block | expr ) ] ; (* f(x) or 0, f(x) or { ... }: lowest, right to left *)
or = and { "||" and } ; and = cmp { "&&" cmp } ;
cmp = add { ( "==" | "!=" | "<" | "<=" | ">" | ">=" ) add } ;
add = mul { ( "+" | "-" | "|" | "^" ) mul } ;
mul = unary { ( "*" | "/" | "%" | "<<" | ">>" | "&" ) unary } ;
unary = ( "-" | "!" | "~" ) unary | postfix ;
unary = ( "-" | "!" | "~" ) unary | "try" postfix | postfix ;
postfix = primary { "." IDENT [ args ] | "[" expr "]" } ;
primary = INT | FLOAT | STRING | CHAR | "true" | "false" | "null"
| IDENT [ args ] | "(" expr ")" | "[" [ expr { "," expr } [","] ] "]" ;
| IDENT [ args ] | "(" expr ")" | "[" [ expr { "," expr } [","] ] "]"
| "[" ":" "]" | "[" expr ":" expr { "," expr ":" expr } [","] "]" ; (* maps *)
args = "(" [ arg { "," arg } [","] ] ")" ; arg = [ IDENT ":" ] expr ;
```
@ -201,11 +210,16 @@ All nodes are in `src/ast.h`: small class hierarchies with an explicit `kind`, d
| `Member` | `MemberExpr` | `obj`, `field` (`.x`, `.len`, `.ptr`, `.value`) |
| `Index` | `IndexExpr` | `obj`, `index` |
| `ArrayLit` | `ArrayLitExpr` | `elems` |
| `Method` | `MethodExpr` | `obj`, `name`, `args`, `argNames`; also `module.fch(...)` |
| `Method` | `MethodExpr` | `obj`, `name`, `args`, `argNames`; also `module.fn(...)` and `p.method(...)` |
| `MapLit` | `MapLitExpr` | `keys`, `values` (`[:]` has none) |
| `Try` | `TryExpr` | `call` |
| `OrElse` | `OrElseExpr` | `call`, and either `fallback` (an expression) or `block` |
| StmtKind | Node |
|---|---|
| `Block` `VarDecl` `Assign` `Expr` `If` `While` `For` `ForEach` `Return` `Break` `Continue` `Defer` | as named; `ForEachStmt` has `var`, `list`, `body`; `DeferStmt` holds one statement |
| `Block` `VarDecl` `Assign` `Expr` `If` `While` `For` `ForEach` `Return` `Break` `Continue` `Defer` | as named; `ForEachStmt` has `var`, `var2` (`for i, x` / `for k, v`), `list`, `body`; `DeferStmt` holds one statement |
`FnDecl` has `recv` (the struct of a method, `""` otherwise) and `fallible` (`-> T!`).
`math.sqrt(x)` parses as a `MethodExpr` on `VarExpr("math")`; whether `math` is a module or a variable
is decided during code generation. Calls name their callee instead of holding an expression: Finch has
@ -220,9 +234,10 @@ There is no "load" node: whether `x` means its address or its value depends on c
```cpp
struct Type {
enum Kind { Void, Bool, Char, I8, I16, I32, I64, U8, U16, U32, U64, F32, F64,
Str, Ptr, Null, Array, Fixed, Struct, Named };
Str, Ptr, Null, Array, Map, Fixed, Struct, Named };
Kind kind;
std::shared_ptr<Type> elem; // Ptr (null = untyped `ptr`), Array, Fixed
std::shared_ptr<Type> elem; // Ptr (null = untyped `ptr`), Array, Fixed; Map: the value
std::shared_ptr<Type> key; // Map
long long count; // Fixed
std::string name, module; // Named (as written) / Struct (resolved)
StructInfo *info; // Struct
@ -299,9 +314,12 @@ type names (conversion) → function of the current module → struct of the cur
| `char` `i8` `u8` / `i16` `u16` / `i32` `u32` / `int` `u64` | `i8` / `i16` / `i32` / `i64` |
| `f32` / `float` | `float` / `double` |
| `str`, `[]T` | `{ ptr, i64, i64 }` (pointer, length, capacity) |
| `map[K]V` | `{ ptr, i64, i64, i64, ptr, i64 }` (entries, len, used, cap, index, icap; §10.12) |
| `ptr`, `ptr[T]`, `null` | `ptr` (opaque) |
| `[N]T` | `[N x T]` |
| Finch struct | named struct type, natural layout |
| a method's `self` | `ptr` (the first parameter) |
| the result of `-> T!` | `{ i1 ok, T value, str error }` (`i8` in place of `T` for `-> !`) |
| C struct | named **packed** struct with explicit padding bytes (§13.2) |
Signedness lives only in the Finch type and selects `sdiv/udiv`, `ashr/lshr`, `icmp s*/u*`,
@ -375,6 +393,8 @@ value when it goes away. There is no reference counting and no garbage collector
| a temporary nobody keeps (`print(a + b)`, `f(g())`, an expression statement) | **`release(v)`**: dropped right after use |
| `a.pop()`, `a.remove(i)` | the element is moved out (fresh) |
| `for x in list` | `x` borrows each element; if the body may change `list`, each element is copied instead |
| a method call `v.m()` | `self` **is** `v` (its address is passed): no copy; changes are visible to the caller |
| maps | own their keys and values like arrays own elements; `m[k] = v` moves/copies `v` in and drops the old value |
A string literal is a fresh value with `cap = 0`: moving it costs nothing, dropping it is skipped
(`release()` ignores constants), and writing into it (`s[i] = c`) first makes a heap copy
@ -389,6 +409,11 @@ parameters are borrowed (`Fn::borrowParam`); changed ones are copied once at ent
syntactic and conservative: anything that *might* change the parameter makes a copy, which is always correct.
The same analysis decides whether `for x in list` must copy elements.
Methods take part: `declareFns()` first finds which methods change `self` (`mutates("self", body)`),
repeating until nothing changes, because calling a changing method on `self` changes `self` too. Their
names go into `mutatingMethods`, which `mutatesExpr()` treats like `push`: `p.move(1)` changes `p`.
Only then is `borrowParam` computed for every function and method.
### 9.3 Copy and drop
`dropAt(addr, T)` / `copyAt(dst, src, T)` work on addresses:
@ -507,7 +532,9 @@ one. Compound `x += v`: evaluate `v`, load the target, `arith()`, store, drop th
### 10.4 Functions and `main`
Functions are `internal` with their natural LLVM signature (aggregates by value). Owning parameters are
copied at entry only if `borrowParam` is false. `fn main` is the internal `finch.main`; a separate
copied at entry only if `borrowParam` is false. Methods (§10.10) get `self`'s address as an extra first
parameter; fallible functions (§10.11) return `{ i1, T, str }`. `fn main() -> !` / `-> int!` makes the
wrapper print `error: <message>` to stderr and exit with 1 on failure. `fn main` is the internal `finch.main`; a separate
external `i32 main(i32, ptr)` (`defineMainWrapper`) builds `[]str args` with `finch_args` if requested,
calls it, drops the arguments and returns the exit code. A function that falls off its end gets the
scope cleanups and `ret void`, or the error `can reach its end without returning T`.
@ -537,9 +564,12 @@ scopes swapped out, or zero). C calls are in §13.
### 10.8 Printing
`print` emits `printf` per argument with a format chosen by type (`%lld`, `%llu`, `%g`, `%c`, `%.*s` for
`str` with its length, `%p`), separated by spaces, then `\n`. Arrays, fixed arrays and structs call
generated helpers `finch.print.<key>` that print `[1, 2]` / `Point(x: 1, y: 2)`, quoting strings and chars inside.
`print` emits `printf` per scalar argument with a format chosen by type (`%lld`, `%llu`, `%g`, `%c`,
`%.*s` for `str` with its length), separated by spaces, then `\n`. Everything else (pointers, arrays,
fixed arrays, maps, structs) is first **formatted** into a heap `str` buffer by `emitFormat()` and
printed with `finch_buf_print`. Composite types get a generated helper `finch.format.<key>(buf, value*)`
that appends `[1, 2]` / `{"a": 1}` / `Point(x: 1, y: 2)`, quoting strings and chars inside. `str(x)` of
an array, map or struct runs the same helper, so `str(x)` is always what `print(x)` shows.
### 10.9 Array and str methods
@ -549,6 +579,65 @@ Arrays: `push` (`finch_arr_reserve` + store + len++), `pop`/`remove` (move out,
(swap loop), `sort` (libc `qsort` with a generated comparator `finch.cmp.<key>`), `join` (runtime).
Methods that change the array require an address (a variable/field/element).
Strings: `sub find contains starts_with ends_with split trim upper lower replace repeat bytes`, all in the runtime.
`for i, x in list` adds a read-only `int` index variable next to the element.
### 10.10 Methods
`fn Point.move(int dx)` is declared like a function named `finch.[module.]Point.move` with a leading
`ptr` parameter and stored in `Codegen::methods[StructInfo*]` (so methods follow the struct into every
module that uses it, and C structs can have methods too). In `define()`, `self` is a `Var` whose `slot`
**is that pointer argument**: `self.x` is a GEP from the caller's own struct, and `self = …` writes the
caller's value. No copy happens, ever.
At a call `obj.m(args)` (`structMethod()`), `ref(obj)` gives an address when `obj` is a place (a variable,
field, element, `m[key]`), the loaded pointer for a `ptr[T]` (null-checked), or a stack temporary for a
value (`Point(1, 2).length()`), which is dropped after the call. Two safety rules come from the mutation
analysis: a method that changes `self` can't be called on a read-only loop variable, and if such a method
is given an argument that lives inside the receiver (`c.merge(c)`, `p.add(p.items)`), the argument is
copied first, so `self` can't free memory the argument still points to.
### 10.11 Errors as values
A fallible function (`-> T!`) returns `{ i1 ok, T value, str error }`; on failure `value` is zero, so the
caller never has to drop it. `return v` wraps `{1, v, zero}` (`retValue()`); `return error(msg)` evaluates
the message, runs the cleanups and returns `{0, zero, msg}` (`retError()`).
Calling a fallible function is only allowed as the operand of `or` / `try`. `fallible(call)` sets
`fallibleTarget` to the call expression; `callFinch()` (and the fallible built-ins) compare their own
`CallExpr`/`MethodExpr` against it, and if it matches, leave `fallibleFailed` (`i1`) and `fallibleMsg`
(`str`) for the caller; if a fallible call doesn't match, it's the compile error *'f' can fail, so say what
happens then*. Nested handlers save and restore the three fields.
| Form | Lowering |
|---|---|
| `f(x) or fallback` | `br failed, or.failed, or.ok`; the failed block drops the message and evaluates the fallback (only there); a φ joins the two values |
| `f(x) or { … }` | the failed block declares `err` (owning the message) and runs the block. If the value is used, the block must end in a terminator; as a statement it may fall through, and the success path releases the unused value |
| `try f(x)` | the failed block runs `emitCleanups(0)` and returns `{0, zero, msg}` from the current function, which must be fallible |
Built-ins with a failure mode check `&call == fallibleTarget` and switch to `finch_str_try_int`,
`finch_str_try_float`, `finch_try_read_file`, `finch_try_write_file`, `finch_try_delete_file`, which
return 1 / 0 and fill an error `str` instead of panicking. The cost on success is one predictable branch;
nothing is allocated unless a failure happens.
### 10.12 Maps
The runtime (§12) keeps an insertion-ordered hash table, in the style of CPython's `dict`: an `entries`
array of `{ i64 hash, K key, V value }` slots in insertion order (hash 0 = removed), plus an `index` of
`icap` slots (power of two, ≥ 2 × cap) with linear probing (−1 empty, −2 removed, else an entry number).
The runtime only hashes (FNV-1a + a 64-bit mix; text keys by content, others by bytes), finds and places
entries; the compiler stores, copies and drops keys and values, exactly like array elements.
| Code | Lowering |
|---|---|
| `m[k]` read | `finch_map_find`; −1 → `finch_map_missing` (noreturn, with the key as text); else a place at `entry.value` |
| `m[k]` write (`=`, `+=`, a changing method, `addr`) | `mapSlot()`: `finch_map_slot` returns the entry, adding it if missing; a new entry gets a copy of the key and the value type's default |
| `["a": 1, …]` | a zeroed map + `mapSlot` per entry (the first entry decides the types; number values are all `float` if one has a dot) |
| `has`, `get`, `remove`, `clear`, `keys`, `values` | `codegen_map.cpp`; `get`'s default is evaluated only when the key is missing |
| `for k, v in m` | a loop over `0..used` that skips removed slots, re-reading `entries` and `used` every round |
| copy / drop / format | helpers like arrays': `finch_map_clone_raw` then deep copies of owning keys and values; drop each live entry, then `finch_map_free` |
Removing marks the slot (and the index entry) removed; slots are packed when the table would grow while
at least a quarter of them are removed. Removing the last key resets the table.
---
@ -565,7 +654,9 @@ well-predicted branch. LLVM removes checks it can prove (constant indexes in ran
| array/str/fixed-array index out of range (`icmp uge`, so negative indexes too) | `boundsCheck()` |
| `.value`, `.field`, `[i]` through null | `member()`, `index()` |
| `pop()` on empty, `slice()`/`sub()` out of range | methods, runtime |
| bad text in `int(s)`/`float(s)`, unreadable file in `read_file` | runtime |
| bad text in `int(s)`/`float(s)`, unreadable file in `read_file` (without `or` / `try`) | runtime |
| `m[key]` read with a missing key | `mapIndex()` |
| a method called through a null `ptr[T]` | `structMethod()` |
A 20-million-element sieve with a bounds check on every access runs as fast as the same C (≈0.09 s):
the checks are hoisted or folded.
@ -591,8 +682,11 @@ Functions take and return these **by pointer**, so their C ABI is trivial (no st
string building (`concat`, `from_int/uint/float/char/bool`, `sub`, `trim`, `upper`, `lower`, `replace`,
`split`, `join`, `repeat`, `from_bytes`), comparison (`eq`, `cmp`, `find`, `starts`, `ends`), ownership
(`copy`, `own`, `drop`, `from_c`), arrays (`reserve`, `make`, `resize`, `clone_raw`, `free`,
`insert_gap`, `remove_gap`), `args`, `input`, files, `shell`, and the panics. Allocation failure prints
`out of memory` and exits.
`insert_gap`, `remove_gap`), maps (`map_find`, `map_slot`, `map_remove_at`, `map_clear`, `map_free`,
`map_clone_raw`, `map_missing`; §10.12), text buffers for `print`/`str` (`buf_add`, `buf_int`, …,
`buf_print`), the fallible variants (`str_try_int`, `str_try_float`, `try_read_file`, `try_write_file`,
`try_delete_file`, `report_error`), `args`, `input`, files, `shell`, and the panics. Allocation failure
prints `out of memory` and exits.
---
@ -749,7 +843,7 @@ with the expected one. It takes about 2.3 s for the self-hosted compiler to comp
- libclang: `find_path(clang-c/Index.h)` + `find_library(clang)`.
- `runtime/finch_rt.c` is read at configure time into `build/rt_source.inc` as a raw string literal;
`CMAKE_CONFIGURE_DEPENDS` re-runs the configure step when it changes.
- `FINCH_VERSION` from `project(VERSION 2.3.0)`. C++17, `-Wall -Wextra` (MSVC: `/W3`), warning-free.
- `FINCH_VERSION` from `project(VERSION 2.4.0)`. C++17, `-Wall -Wextra` (MSVC: `/W3`), warning-free.
- `shell.nix` lists `llvmPackages.clang` before `llvmPackages.libclang`: the latter also ships an
unwrapped `clang` that can't find the system headers.
@ -759,9 +853,12 @@ with the expected one. It takes about 2.3 s for the self-hosted compiler to comp
- `tests/run.sh`: every `tests/run/*.fch` with `fn main` must print exactly its `.out` (stdin from `.in`
if present); files without `main` are modules or helpers. Every `tests/fail/*.fch` must fail with the text
from its `// expect:` line. Currently **59 passed, 0 failed**.
from its `// expect:` line. Currently **74 passed, 0 failed**.
- `MEMCHECK=1 tests/run.sh`: the same, plus valgrind on every program (no leaks, no invalid access).
- `tests/boot.sh`: the self-hosting fixpoint and the subset run (§17).
- `tests/cross.sh windows|arm64`: every test built with `--target` and run under Wine / QEMU.
- `python3 tests/lsp_test.py`: the language server, driven like an editor.
- CI (`.github/workflows/ci.yml`) runs all of it on Linux x86-64, Linux ARM64, macOS ARM64 and Windows.
---
@ -850,11 +947,14 @@ with the expected one. It takes about 2.3 s for the self-hosted compiler to comp
| Function | Does |
|---|---|
| `call` / `method` | resolve a call / a method or module-qualified call |
| `callFinch` / `construct` / `callC` | Finch function / struct constructor / C function |
| `callFinch` / `construct` / `callC` | Finch function or method / struct constructor / C function |
| `structMethod` | `obj.m(...)`: the receiver's address, safety checks, the call |
| `print`, `addrOf`, `convert`, `arrayMethod` | built-ins |
| `fallible`, `tryExpr`, `orElse`, `retValue`, `retError` (`codegen.cpp`) | errors as values (§10.11) |
| `mapIndex`, `mapLit`, `mapMethod`, `mapSlot`, `forEachEntry` (`codegen_map.cpp`) | maps (§10.12) |
| `rt` / `libc` | declare a runtime / libc function |
| `fileName`, `panicIf`, `boundsCheck`, `checkDivisor` | runtime checks |
| `classify`, `makePlan`, `declareC`, `emitCCall`, `cThunk` | System V classification, register plan, declaration, call, callback thunk |
| `classify`, `makePlan`, `declareC`, `emitCCall`, `cThunk` | ABI classification (System V, Microsoft x64, AAPCS64), register plan, declaration, call, callback thunk |
---
@ -877,12 +977,15 @@ emission in `arithOp`/`compare`, an explanation in `badOperands`.
---
## 23. Targets and Windows
## 23. Targets: Windows, macOS, ARM64
`src/target.h` holds `g_target`: the LLVM triple, `windows` / `msvc` / `cross` flags, the C compiler (`cc`)
and the executable suffix. `setTarget()` picks the C compiler: `FINCH_CC`, else `CC` (not when
cross-compiling), else `x86_64-w64-mingw32-gcc` for `--target windows` from Linux, `clang` on Windows,
`cc` on Unix. Everything platform-dependent goes through it:
`src/target.h` holds `g_target`: the LLVM triple, `windows` / `msvc` / `darwin` / `cross` flags, the C
compiler (`cc`) and the executable suffix. `setTarget()` maps `windows`, `linux`, `arm64`, `macos` (or a full
triple) to a triple and picks the C compiler: `FINCH_CC`, else `CC` (not when cross-compiling), else
`x86_64-w64-mingw32-gcc` / `aarch64-linux-gnu-gcc` for Windows / ARM64 Linux from another system,
`clang --target=<triple>` for other cross targets, `clang` on Windows, `cc` on Unix. `initTargets()`
registers the X86 and AArch64 backends (whichever this LLVM has; CMake defines `FINCH_TARGET_X86` /
`FINCH_TARGET_AARCH64` and links their libraries). Everything platform-dependent goes through it:
- **Headers:** libclang parses with `--target=<triple>` (so `long` is 32 bits on Windows and the right
headers are used) and, on Windows, `-D_USE_MATH_DEFINES`. Include folders come from `<cc> -E -v`.
@ -893,7 +996,12 @@ cross-compiling), else `x86_64-w64-mingw32-gcc` for `--target windows` from Linu
- **Linking:** no `-lm` with MSVC; `.exe` names; the runtime cache key includes the triple and the C compiler.
MSVC linker messages (`LNK2019 unresolved external symbol`, `LNK1104`/`LNK1181 cannot open file`) are
translated like GNU ld's.
- **Running:** a Windows program built on Linux runs with `wine`.
- **Running:** `runPrefix()`: a Windows program built on Linux runs with `wine`, an ARM64 Linux one
elsewhere with `qemu-aarch64`.
- **macOS:** `<cc> -E -v` lists `(framework directory)` entries; they become `-iframework` for libclang,
so `import "OpenGL/gl.h"` works. `link "Cocoa.framework"` becomes `-framework Cocoa`. Apple's linker
messages (`Undefined symbols for architecture arm64: "_f"`, `ld: library 'x' not found`,
`framework 'X' not found`) are translated like the others, without Mach-O's leading `_`.
- **Debug info:** CodeView for MSVC targets, DWARF elsewhere.
- **Runtime:** no `getline` or `sys/wait.h` on Windows; `stdin`/`stdout`/`stderr` are macros there, so
`finch_std_stream(i)` gives them to Finch when a header has no `extern` variable for them.
@ -908,10 +1016,30 @@ receives a plain pointer). Results of 1/2/4/8 bytes come back in `rax` as an int
through a hidden `sret` pointer. Each argument takes one slot, so there is no register counting.
`cThunk()` mirrors this for callbacks.
`tests/windows.sh` builds every test with `--target windows`, runs it with Wine and compares the output:
`tests/cross.sh windows` builds every test with `--target windows`, runs it with Wine and compares the output:
all of them pass, including `c_structs` (which then exercises the Microsoft rules against a C library built
with MinGW). CI additionally builds `finch.exe` with MSVC on Windows Server and runs `tests/run.sh` there.
### AAPCS64 (ARM64 Linux and Apple)
`classify()` mirrors clang's AArch64 lowering:
- A **homogeneous floating-point aggregate** (1–4 members, all `float` or all `double`, nested structs
and arrays flattened) goes in vector registers: passed as `[n x float]` / `[n x double]`, returned as
`{ float, … }`. On Linux the argument also gets `alignstack(8)`, as clang does; Apple doesn't.
- Any other struct of up to 16 bytes goes in general registers: `i64` (≤ 8 bytes) or `[2 x i64]`
(`i128` if its alignment is 16); it is returned as an integer of its exact size or `[2 x i64]`.
- Bigger structs are **Indirect**: a copy on the caller's stack, passed by address (no `byval`); returned
through `sret`.
- There is no register counting: the backend places each part.
- C's plain `char` is **unsigned** on ARM64 Linux and signed on Apple and x86, which matters for the
sign/zero extension of `char` arguments and results (`charIsSigned()`).
- Apple passes the variadic part of `printf(...)` on the stack; LLVM does that from the `...` in the
function type.
`tests/cross.sh arm64` runs the whole suite (including the struct ABI test against a C library built by
GCC) under QEMU, and CI runs it natively on ARM64 Linux and Apple Silicon.
### Building finch.exe
CI uses Visual Studio 2022, Ninja and the official `clang+llvm-21.x-x86_64-pc-windows-msvc` package
@ -957,7 +1085,19 @@ signature help (and mirrored by the cheat sheets).
---
## 25. The VS Code extension
## 25. Editor integrations
### Kate
`editors/kate/finch.xml` is a KSyntaxHighlighting definition (the engine of Kate, KWrite and KDevelop):
keyword lists that match `builtins_doc.h`, a context for `fn Type.method`, strings with escapes, and
folding regions for `{ }` and block comments. `lspclient.json` is the entry for Kate's LSP
client (`highlightingModeRegex: ^Finch$`); `install.sh` copies the definition and merges the entry into
`~/.config/kate/lspclient/settings.json`. Kate sends more requests than VS Code (`documentHighlight`,
`foldingRange`, …); the server answers every request it doesn't implement with `MethodNotFound`, so no
client waits.
### VS Code
`editors/vscode` is a plain JavaScript extension: `extension.js` starts `finch lsp` through
`vscode-languageclient` (path from the `finch.path` setting) and adds **Run** / **Build** as
@ -969,10 +1109,15 @@ templates. CI packages it with `vsce` into `finch-lang.vsix`.
## 26. Known limitations
- No methods on structs, generics, maps, `match`, closures, error values (`int("x")`, `read_file` panic).
- No generics, interfaces, `match`, closures or Finch-side enums.
- Strings are bytes: `.len`, `s[i]`, `upper()` are not Unicode-aware.
- Changing a map's keys inside a `for` over the same map is not detected (it is memory-safe, but entries
may be skipped or seen twice).
- A `try` / `or` failure in the middle of an expression doesn't release fresh temporaries computed
earlier in that expression (a small leak on the failure path only).
- Dangling pointers (`addr` of a local that went away, use after `free`) are not detected.
- Shift amounts and float→int conversions are not range-checked (LLVM poison, as in C).
- C: unions and bit-field structs by value, function-like macros, `long double`.
- Two systems: Linux and Windows, both x86-64 (System V and Microsoft x64 ABIs). No macOS or ARM yet.
- The self-hosted compiler has no C imports, sized integers or `defer`, and doesn't free memory.
- Targets: x86-64 and ARM64 (Linux, macOS, Windows on x86-64). No Windows on ARM64, 32-bit, or WebAssembly.
- The self-hosted compiler has no C imports, sized integers, `defer`, maps, methods or errors as values,
and doesn't free memory.

View file

@ -20,16 +20,16 @@ compiler works inside. For that, see the [guide for engineers](for-engineers.md)
7. [Operators](#7-operators)
8. [Control flow](#8-control-flow)
9. [Functions](#9-functions)
10. [Arrays](#10-arrays)
10. [Arrays and maps](#10-arrays-and-maps)
11. [Text (str)](#11-text-str)
12. [Structs](#12-structs)
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. [Editor: VS Code](#19-editor-vs-code)
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)
@ -60,7 +60,9 @@ messages that tell you how to fix the problem.
## 2. Installing
Finch runs on **Linux and Windows, x86-64**, and is built with **LLVM 21**.
Finch runs on **Linux** (x86-64 and ARM64), **macOS** (Apple Silicon and Intel) and **Windows** (x86-64),
and is built with **LLVM 21**. Each [release](https://github.com/Nexoniarz/Finch/releases) has a ready
`finch` for Windows, Linux x86-64, Linux ARM64 and macOS ARM64.
### Windows
@ -76,8 +78,30 @@ Finch runs on **Linux and Windows, x86-64**, and is built with **LLVM 21**.
Programs are normal `.exe` files. To build from source on Windows instead, follow `.github/workflows/ci.yml`
(Visual Studio 2022 + the `clang+llvm-21.x-x86_64-pc-windows-msvc` package from LLVM's releases).
### macOS
```sh
brew install llvm@21 # Finch's LLVM; programs are linked with Apple's clang
```
Then either unpack **`finch-macos-arm64.tar.gz`** from the releases (it uses Homebrew's `llvm@21`),
or build it yourself (Xcode command line tools: `xcode-select --install`):
```sh
git clone https://github.com/Nexoniarz/Finch.git && cd Finch
cmake -S . -B build -G Ninja -DCMAKE_BUILD_TYPE=Release -DLLVM_DIR="$(brew --prefix llvm@21)/lib/cmake/llvm"
ninja -C build
./build/finch version
```
Apple frameworks link with `link "Cocoa.framework"` (`-framework Cocoa`), and their headers import as usual:
`import "OpenGL/gl.h"`.
### Linux
The ready `finch-linux-x64.tar.gz` / `finch-linux-arm64.tar.gz` need LLVM 21's libraries
(Debian/Ubuntu: `libllvm21 libclang1-21` from [apt.llvm.org](https://apt.llvm.org)) and a C compiler (`cc`).
Building it yourself works the same on x86-64 and ARM64 (Raspberry Pi 4/5, Graviton, …).
Other LLVM versions will likely fail to compile, because LLVM's C++ API changes between releases.
### Option A: Nix (recommended, nothing to install by hand)
@ -88,7 +112,7 @@ cd Finch
nix-shell # downloads LLVM, libclang, clang, cmake, ninja, pkg-config
cmake -S . -B build -G Ninja
ninja -C build
./build/finch version # finch 2.3.0 (LLVM 21.x)
./build/finch version # finch 2.4.0 (LLVM 21.x)
```
### Option B: your distribution's packages
@ -109,16 +133,27 @@ cmake -S . -B build -G Ninja -DLLVM_DIR=/usr/lib/llvm-21/lib/cmake/llvm
ninja -C build
```
### Building Windows programs on Linux
### Building for another system
With the MinGW-w64 cross compiler installed (`x86_64-w64-mingw32-gcc`; on Nix
`pkgsCross.mingwW64.buildPackages.gcc`):
`--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 windows # runs it with Wine, if installed
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
@ -137,12 +172,14 @@ Finch's small runtime library, which is kept in `~/.cache/finch`. Set `CC` to us
finch run <file.fch> [args...] compile and run right away
finch build <file.fch> [-o name] compile into a program (default name: the file's name)
finch ir <file.fch> print the LLVM IR (for the curious)
finch lsp the language server, for editors
finch version show the version
options:
-l <lib> link a C library, same as link "lib" in the file
-g add debug info (for gdb / lldb)
-O0 skip optimizations
-l <lib> link a C library, same as link "lib" in the file
--target <name> build for another system: windows, linux, arm64, macos, or an LLVM triple
-g add debug info (for gdb / lldb / Visual Studio)
-O0 skip optimizations
```
Examples:
@ -227,6 +264,7 @@ x += 1 // also -= *= /= %=
| `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`, `\\`, `\"`, `\'`.
@ -333,6 +371,14 @@ for i in 0..10 { // i = 0, 1, …, 9 (the end is not included)
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 {`.
@ -375,11 +421,13 @@ fn no_result(str message) { // no "->": returns nothing
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
## 10. Arrays and maps
```c
nums := [5, 3, 8] // an array of int
@ -408,6 +456,44 @@ grid := [[1, 2], [3, 4]] // arrays of arrays
**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)
@ -437,7 +523,7 @@ to C as `char*` works.
---
## 12. Structs
## 12. Structs and methods
```c
struct Player {
@ -457,7 +543,34 @@ print(p) // Player(name: "Ola", lives: 2, scores: [10], pos: Point(x: 5, y
- 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).
- Structs have no methods. Write functions that take the struct: `fn heal(Player p) -> Player`.
### 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).
---
@ -520,14 +633,16 @@ Using `.value`, `.field` or `[i]` on a `null` pointer **stops the program** with
|---|---|
| `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. |
| `write_file(path, text)` | Writes (replaces) the file; gives back `true` if it worked. |
| `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)). |
| `int(x)`, `str(x)`, `u8(x)`, … | Conversions (see [Conversions](#conversions)). |
| `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. |
---
@ -669,6 +784,54 @@ Both work directly; `examples/opengl.fch` and `examples/vulkan.fch` are complete
## 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
```
@ -691,11 +854,30 @@ Instead of undefined behavior (as in C), Finch stops with a message and exit cod
| `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 |
| `runtime error: can't read the file "…"` | `read_file` on a missing or unreadable file (handle it with `or`) |
| `runtime error: the key "x" is not in the map …` | `m["x"]` when the map has no such key (use `.has` or `.get`) |
| `runtime error: called .f() on a null pointer` | a method called through a `null` `ptr[T]` |
---
## 19. Editor: VS Code
## 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,
@ -703,8 +885,12 @@ go to definition (F12), an outline of the file, parameter hints, snippets, and a
(`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. Other editors that speak the
Language Server Protocol (Neovim, Helix, Zed, Emacs, Sublime) can use `finch lsp` directly.
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
@ -737,6 +923,9 @@ Visual Studio debugger and WinDbg read.
| A library is not found when the program starts | It was linked from a folder the system doesn't search. Run inside the same `nix-shell`, or install it system-wide. |
| Windows: `finch` can't link (`LNK…` errors about `libcmt`, `kernel32`) | Install Visual Studio Build Tools with *Desktop development with C++*. |
| Windows: `finch.exe` doesn't start (missing `libclang.dll`) | Keep `libclang.dll` next to `finch.exe`. |
| macOS: `finch` doesn't start (`Library not loaded: …libLLVM…`) | `brew install llvm@21`. |
| macOS: `can't find the C header 'stdio.h'` | Install the command line tools: `xcode-select --install`. |
| `--target arm64`: `no C compiler found to link with` | Install `aarch64-linux-gnu-gcc` (Debian: `gcc-aarch64-linux-gnu`), or set `FINCH_CC`. |
| `clang` inside `nix-shell` can't find `stdio.h` | You have an old `shell.nix`: `llvmPackages.clang` must be listed before `llvmPackages.libclang`. |
| A program never ends | A loop condition never becomes false. Press Ctrl+C. |
@ -755,17 +944,18 @@ Finch/
│ ├── fail/ programs that must fail, with the expected error in line 1
│ ├── run.sh the test runner (MEMCHECK=1 also checks memory with valgrind)
│ └── boot.sh builds the self-hosted compiler with itself and compares
├── editors/vscode/ the VS Code extension
├── 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
└── CMakeLists.txt
```
```sh
tests/run.sh # → 59 passed, 0 failed
tests/run.sh # → 74 passed, 0 failed
MEMCHECK=1 tests/run.sh # the same under valgrind: no leaks, no bad memory access
tests/boot.sh # the self-hosting check (needs clang)
tests/windows.sh # every test built for Windows and run with Wine
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
python3 tests/lsp_test.py # the language server
```
@ -773,12 +963,11 @@ python3 tests/lsp_test.py # the language server
## 23. Current limits
Finch 2.0 is a complete small language, but not a finished one. Not there yet:
Not there yet:
- Methods on structs, generics, maps/dictionaries, `match`. (Use arrays of structs and functions.)
- Error values: errors in `int("x")` or `read_file` stop the program. Check first (`file_exists`).
- Generics (your own `List[T]`), interfaces, `match`, enums written in Finch.
- Unicode-aware text: `.len`, `s[i]` and `upper()` work on bytes / ASCII.
- Threads.
- Platforms other than Linux and Windows on x86-64 (macOS, ARM).
- Windows on ARM64, 32-bit systems, WebAssembly.
See the [roadmap in the README](../../README.md#roadmap).