mirror of
https://github.com/Nexoniarz/Finch.git
synced 2026-11-03 00:44:27 +00:00
- on stderr (finch ir stays the IR), as yellow warnings in editors (@W in the index), once per place - str, arrays and maps inside a struct don't count: passing it copies only their handles - none in the compiler, the examples or the tests other than the new one - docs: the warning instead of "not planned"; what was and wasn't checked for Android Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016GzNP6r8dg6WDxUVZy8wjL
530 lines
30 KiB
Markdown
530 lines
30 KiB
Markdown
# Finch for Engineers
|
||
|
||
**The internals of Finch 2.5, whose compiler is written in Finch.** How source becomes tokens, an AST and LLVM IR *as text*,
|
||
which `clang` compiles and links with a small C runtime; how C headers are read with libclang from Finch itself and how
|
||
the C calling conventions are reproduced by hand; the type and ownership rules; how the compiler builds itself from a
|
||
seed; and the language server.
|
||
|
||
Assumed: you can read Finch (see the [technicians' guide](for-technicians.md)) and have at least seen LLVM IR.
|
||
The compiler's first version, in C++ on LLVM's C++ API, is kept on the `before-bootstrap` branch; this guide describes
|
||
the Finch one.
|
||
|
||
> 🇵🇱 Polska wersja: [dla-inzynierow.md](../pl/dla-inzynierow.md)
|
||
|
||
---
|
||
|
||
## Contents
|
||
|
||
1. [Architecture](#1-architecture)
|
||
2. [Source tree](#2-source-tree)
|
||
3. [Front end: lexer, parser, modules](#3-front-end-lexer-parser-modules)
|
||
4. [Code generation](#4-code-generation)
|
||
5. [C import and the C ABI](#5-c-import-and-the-c-abi)
|
||
6. [Driver, linking and targets](#6-driver-linking-and-targets)
|
||
7. [Type system](#7-type-system)
|
||
8. [Ownership and memory](#8-ownership-and-memory)
|
||
9. [Generics, enums and match](#9-generics-enums-and-match)
|
||
10. [Bootstrapping](#10-bootstrapping)
|
||
11. [The language server](#11-the-language-server)
|
||
12. [Semantics vs. C](#12-semantics-vs-c)
|
||
13. [Known limitations](#13-known-limitations)
|
||
|
||
---
|
||
|
||
## 1. Architecture
|
||
|
||
The compiler has no LLVM library inside it. It writes LLVM IR as **text** and lets `clang` do optimisation, code
|
||
generation and linking. That is what makes it small (about 8k lines) and what lets it be written in Finch itself.
|
||
|
||
```
|
||
main.fch ─► loader ── lex ── parse ──► Program (+ every imported module, recursively)
|
||
│
|
||
`import "x.h"` ─► cimport.fch (libclang, called from Finch) ─► C functions, structs, constants
|
||
│
|
||
▼
|
||
gen.fch ─► generate(): declare structs, declare functions, define bodies
|
||
type check and emit IR in one walk; ABI classification for C calls
|
||
│ (IR text)
|
||
▼
|
||
linker.fch ─► clang -c prog.ll ─► clang prog.o rt-<hash>.o [your .c/.o/.a] -lm [pkg-config / -l]
|
||
│
|
||
▼
|
||
executable (finch run: run from a temp file, then delete it)
|
||
```
|
||
|
||
Design decisions that shape the rest:
|
||
|
||
- **Fail fast.** The first error prints `file:line:col: error: …` with the source line and a caret, and the compiler stops.
|
||
No error recovery, so later phases never see half-valid input.
|
||
- **One walk.** `gen.fch` type checks and emits IR in one pass over the AST. Each expression yields an IR operand, its
|
||
Finch type (a string kind: `int`, `f32`, `str`, `array`, `map`, `struct`, `enum`, `named`…) and flags.
|
||
- **Let clang do the work.** Locals are `alloca`s promoted by `mem2reg`. Optimisation is clang's `-O2` (or `-O0` with `-g`).
|
||
- **Values, not references.** Arrays, strings and structs have value semantics; ownership is decided at compile time (§8).
|
||
- **A tiny C runtime** (`runtime/finch_rt.c`, ~760 lines) does what is tedious in IR: strings, array growth, maps, input,
|
||
files, panics. It is compiled once and cached in `~/.cache/finch`.
|
||
|
||
---
|
||
|
||
## 2. Source tree
|
||
|
||
| File | Lines | Responsibility |
|
||
|---|---|---|
|
||
| `src/lexer.fch` | 470 | Tokens, positions, `"""` text, `"\(x)"` turned into `("…" + str(x) + "…")` tokens |
|
||
| `src/ast.fch` | 120 | One `Node` struct with a `kind` string, children, type names, positions (`line`, `endLine`) |
|
||
| `src/parser.fch` | 910 | Recursive descent: declarations, statements, expressions, generics (`[T]`), `enum`, `match`, `export`, `link`, `const`, `test "…" { }` |
|
||
| `src/gen.fch` | 6450 | Type check + IR: structs, functions, methods, scopes and drops, statements, expressions, built-ins, maps, errors as values, generics (monomorphisation), enums and `match`, `-g` debug metadata, the `index` mode |
|
||
| `src/cimport.fch` | 790 | libclang: functions, structs and unions (fields, offsets, padding), enums, constants, globals, macros (also struct-valued) |
|
||
| `src/platform.fch` | 90 | `Target`: triple, data layout, C compiler, signedness of `char` per system |
|
||
| `src/linker.fch` | 430 | The cache (runtime, program object, C/C++ files), libraries (`link`, pkg-config), link-error advice |
|
||
| `src/main.fch` | 515 | Driver: CLI, loader with module lookup and overlays, `build/run/test/fmt/ir/index/lsp` |
|
||
| `src/manifest.fch` | 240 | `finch.toml`: a small TOML reader, per-target lists |
|
||
| `src/fmt.fch` | 225 | `finch fmt` and LSP formatting |
|
||
| `src/lsp.fch` | 910 | The language server (§11) |
|
||
| `src/docs.fch` | 110 | One description of the built-ins, shared by hover and completion |
|
||
| `std/*.fch` | 1040 | `json`, `path`, `time` (+`_windows`), `proc` (+`_windows`), `fs`, `thread` |
|
||
| `runtime/finch_rt.c` | 1150 | The C runtime: text, arrays, maps, files, float printing, fs, threads, the test runner, the call chain |
|
||
| `bootstrap/` | | `build.sh`, `install.sh`, the seeds (§10) |
|
||
|
||
---
|
||
|
||
## 3. Front end: lexer, parser, modules
|
||
|
||
**Lexer.** Newlines end statements; there is no indentation rule. Tokens carry file, line and column, which every error uses.
|
||
|
||
**Parser.** Recursive descent, one function per construct. Notable points: types are parsed into a name string
|
||
(`[]str`, `map[str]int`, `ptr[char]`, `Stack[int]`); `fn Type.method` declares a method; `fn name[T]` and `struct Name[T]`
|
||
declare templates; `enum` and `match` have their own nodes; `export fn` marks a C-visible function; a block records
|
||
its `endLine` (used by the language server). The old `fn name(...) type` order is a parse error with a hint.
|
||
|
||
**Modules.** `import name` looks, in order, at `name_<os>.fch` next to the importing file (`windows`, `linux`, `macos`),
|
||
`name.fch` there, the folders of `FINCH_PATH`, then the standard library `std/`. Each module is parsed once; its names
|
||
are used as `module.name`. `import "x.h"` goes to the C importer instead.
|
||
|
||
A module is identified by its **file**: the key is its path from the main file's folder (or from a `FINCH_PATH` folder or
|
||
`std/`) without `.fch` and without an `_<os>` suffix, e.g. `ui/button`. `Program.imports` holds the name each file uses
|
||
(`button`, or `btn` for `import ui/button as btn`) and `Program.modules` the key; `modFor()` maps one to the other.
|
||
Symbols use the key with `/` turned into `$` (`@finch.ui$button.make`).
|
||
|
||
**Constants.** `const NAME = expr` is a top-level node; `constValue()` evaluates the expression at each use, in the
|
||
defining module with no local scopes, so it folds to a literal (integer literal operators are folded in `arith`).
|
||
`checkConst()` allows only literals, names, operators, members and numeric conversions; `constBusy` catches cycles.
|
||
|
||
**Lexer extras.** `1e9` / `2.5e-3` are float tokens (the text is normalised to have a `.` for LLVM); `"""` strings are
|
||
dedented by the closing line's indentation in `longString()`.
|
||
|
||
---
|
||
|
||
## 4. Code generation
|
||
|
||
`gen.fch` is one big `Gen` struct plus functions over it. The passes are:
|
||
|
||
1. `declareStructs`: every Finch and C struct gets an LLVM type (C structs with their real padding); recursive-by-value
|
||
structs are refused.
|
||
2. `declareFns`: signatures, with a *borrow analysis* for parameters (a parameter that is never stored is passed without
|
||
a copy).
|
||
3. `define`: each body, type checked and emitted. Scopes track what must be dropped when they end (`defer` runs first).
|
||
4. `defineMainWrapper`: the C `main(argc, argv)`, and `finch_on_fatal` for embedders.
|
||
5. With `-g`, DWARF metadata is attached as the IR is written (use `-O0`).
|
||
|
||
Generics are **monomorphised**: a call `max(1, 2)` infers `T`, then the template's AST is generated again with `T` bound,
|
||
under a mangled name (`max$int`). `match` over an enum is exhaustive (a missing variant is an error, `else` covers the
|
||
rest); on a value it is a chain of comparisons. Enum values are `i32`; `Color(n)` is range checked; printing uses a
|
||
generated name table.
|
||
|
||
Errors as values: `-> T!` returns a pair (value, error text); `try` returns early on error, `or` handles it, `error("…")`
|
||
makes one.
|
||
|
||
---
|
||
|
||
## 5. C import and the C ABI
|
||
|
||
`cimport.fch` calls **libclang** directly from Finch (it is itself written with `import "clang-c/Index.h"`), parses the
|
||
header with the target's flags, and walks the cursors. Types map as: `const char *` parameters → `str`, other
|
||
`char *` → `ptr[char]`, a `char *` result → `str`; integer types by size and signedness (`char` is unsigned on ARM64
|
||
Linux); structs and enums by layout.
|
||
|
||
LLVM IR does not do the C calling convention for you, so `gen.fch` reproduces it:
|
||
|
||
- `classify` – System V x86-64: a struct up to 16 bytes goes in registers by eightbyte class (INTEGER/SSE), larger in memory;
|
||
- `classifyArm64` – AAPCS64, including homogeneous float aggregates (HFA);
|
||
- the Microsoft x64 rules (1, 2, 4, 8-byte structs by value in a register, the rest by pointer);
|
||
- `makePlan` turns the classification into a call plan; `cThunk` makes `addr(fn)` callable from C (callbacks).
|
||
|
||
The same code is what lets the compiler call libclang, whose API passes `CXCursor`, `CXType` and `CXString` by value.
|
||
|
||
**Macros.** A `#define` that is one number or a string literal is read from its tokens. One whose tokens could be a
|
||
constant expression (no braces, semicolons or statement keywords) is evaluated by clang: a second parse of the header
|
||
adds `static const __auto_type __finch_m_NAME = (NAME);` for each, and `clang_Cursor_Evaluate` gives an int, a double
|
||
or a string. Those that are not constants only produce errors in that second parse and are dropped.
|
||
|
||
**Unions** are a `CStruct` with `isUnion`: the LLVM type is `{ [n x iA] }` (size and alignment from clang), every field has
|
||
offset 0, and a field is read through memory. They can't be passed by value to C (`unsupported` says why).
|
||
|
||
---
|
||
|
||
## 6. Driver, linking and targets
|
||
|
||
`finch build|run|ir|index file.fch [-o out] [-O0] [-g] [-l lib] [-I dir] [--target t] [--shared] [--overlay path=file]`.
|
||
`ir` prints the IR; `index` prints the records the language server uses (§11); `--shared` makes a shared library of the
|
||
`export fn`s.
|
||
|
||
`--target windows|linux|arm64|macos|android|<triple>` changes the triple and data layout, the C `char` signedness, the ABI rules
|
||
above, the OS variant of modules (`name_windows.fch`), and the C compiler: `FINCH_CC` or `clang --target=…`. Linking
|
||
asks `pkg-config` for flags for `link "lib"`; when the linker fails the output is analysed and the likely missing
|
||
`link "…"` is suggested. The runtime object is keyed by a hash of its source and the target.
|
||
|
||
Environment: `FINCH_PATH` (module folders), `FINCH_RT` (runtime source), `FINCH_CC` (C compiler/linker; the IR is always
|
||
compiled by `clang`, never by `CC`), `FINCH_LIBCLANG` (for `bootstrap/build.sh`).
|
||
|
||
The compiler finds `runtime/` and `std/` next to itself (`../`, `./`) or under `../share/finch/` (`bootstrap/install.sh`).
|
||
|
||
**The build cache** (`host.cacheDir()`: `$XDG_CACHE_HOME/finch`, `~/.cache/finch`, `%LOCALAPPDATA%\finch`):
|
||
|
||
- `ir/<hash of main file + target + flags>.o` with a `.key` holding a hash of clang, the flags and the whole IR. One slot
|
||
per program, so the cache does not grow with every edit; the same IR gives the same object without running clang.
|
||
- `c/<hash of source + compiler + flags + target>.o` for `link "x.c"` / `.cpp`, built with `-MMD`; the `.key` is a hash of
|
||
every file the `.d` lists (the source and its headers). Any change in one of them rebuilds it.
|
||
- Objects are written to a `.part<pid>` file and renamed, so two builds at once don't see half a file.
|
||
Hashes are two independent 32-bit ones (FNV-1a and djb2), 64 bits in all.
|
||
|
||
**`finch.toml`** (`src/manifest.fch`) is searched from the main file's folder (or the working folder) upwards. It is read
|
||
into `values["section.key"]` and `lists["section.key"]`; `list(key, targets)` concatenates the top-level list and those of
|
||
`[target.<os>]` and `[target.<os>-<cpu>]`. Paths are resolved against the manifest's folder. Entries feed the same
|
||
places as the command line: `paths` → the loader, `include` → `-I` (libclang and C files), `links` → `link`, `libdirs`
|
||
and `ldflags` → the link command, `cflags` → C/C++ files.
|
||
|
||
**C++ files**: a `.cpp`/`.cc`/`.cxx` in `links` is compiled by `cxxFor(cc)` (`FINCH_CXX`, or `clang` → `clang++`,
|
||
`…gcc` → `…g++`, `cc` → `c++`), and then that driver links, which brings in the C++ standard library.
|
||
|
||
**Android**: `--target android` is `aarch64-linux-android24`; the C compiler is the NDK's
|
||
`toolchains/llvm/prebuilt/<host>/bin/aarch64-linux-android24-clang` (from `ANDROID_NDK_HOME`/`ANDROID_NDK_ROOT`), the link
|
||
adds `-llog` and no `-lpthread` (bionic has it in libc). The OS variant of modules is `linux`.
|
||
|
||
**`finch test`** compiles like `run`, with `Options.test`: every function is still defined, but `@main` calls
|
||
`finch_test_run(name, fn)` for each `test` block of the user's modules (`__test_<n>`, a `void` function made by the
|
||
parser), then `finch_test_summary()`. The runtime runs each test under `setjmp`; `fatal()` (every runtime error and a
|
||
failed `assert`) `longjmp`s back through `fatal_hook`, so one test failing doesn't end the run. Memory the failed test
|
||
owned at that point is not freed.
|
||
|
||
**`finch fmt`** is line-based: it tracks `{ }`, `( )`/`[ ]`, strings (with `\( )`), `'…'`, `"""` and `/* */` per line.
|
||
A line that starts at bracket depth 0 and doesn't continue a line ending in an operator or `,` is re-indented to
|
||
4 × brace depth; other lines (continuations, `"""` bodies) are shifted by the same amount as the line that started the
|
||
statement, so their shape (and the `"""` dedent) is kept.
|
||
---
|
||
|
||
## 7. Type system
|
||
|
||
### LLVM representation
|
||
|
||
| Finch | LLVM |
|
||
|---|---|
|
||
| `bool` | `i1` |
|
||
| `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; see src/gen.fch) |
|
||
| `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 (§5) |
|
||
|
||
Signedness lives only in the Finch type and selects `sdiv/udiv`, `ashr/lshr`, `icmp s*/u*`,
|
||
`sext/zext`, `sitofp/uitofp`, `fptosi/fptoui`.
|
||
|
||
### Values
|
||
|
||
```cpp
|
||
struct Value_ { Value *v; FType type; bool literal; bool fresh; };
|
||
```
|
||
|
||
- **`literal`**: a number written in the code or an imported C constant. It may become any integer type
|
||
it fits (`fits()`), or `f32`. It survives unary minus and literal-only arithmetic that folds to a
|
||
constant; explicit conversions clear it (`u8(250)` is a typed constant, so `u8(250) + 10` wraps to 4).
|
||
- **`fresh`**: an owning value (str/array/owning struct) that nobody holds yet: the result of a call,
|
||
a literal, a concatenation, a constructor. It must be stored (moved) or released (§8).
|
||
|
||
### Implicit conversions: `coerce(v, want)`
|
||
|
||
Identical types; `null` → any pointer; typed pointer ↔ untyped `ptr`; integer → integer if the value is a
|
||
fitting literal or the type `widens()` (same signedness and more bits, or unsigned → strictly bigger
|
||
signed); integer → float; `f32` → `f64`; literal `f64` → `f32`. Nothing else: the error suggests the
|
||
explicit conversion. Owning types convert only to themselves. Array literals take their element type
|
||
from the slot they flow into (`exprWant()`), so `[]f32 xs = [1, 2.5]` works.
|
||
|
||
### Binary operands: `unify(l, r)`
|
||
|
||
A literal side adapts to the other (also a float literal next to an `f32`: `x * 2.0` stays `f32`); otherwise the side
|
||
that widens grows; `i32` + `u32` is an error; int + float → float; f32 + f64 → f64. There is no C integer promotion:
|
||
`u8 + u8` stays `u8`. Two integer literals fold to a literal, so `FLAG_A | FLAG_B` still fits a `u64` or `u32`.
|
||
|
||
### Explicit conversions: `convert()`
|
||
|
||
| From → To | IR |
|
||
|---|---|
|
||
| int/char ↔ int/char | `trunc`/`sext`/`zext` by source signedness |
|
||
| bool → int | `zext` |
|
||
| float ↔ int | `fptosi`/`fptoui`, `sitofp`/`uitofp` |
|
||
| float ↔ float | `fpext`/`fptrunc` |
|
||
| int → bool | `icmp ne 0` |
|
||
| number/bool/char → str | `finch_str_from_int/uint/float/char/bool` (fresh) |
|
||
| str → int/float | `finch_str_to_int/float` (panics on bad text) |
|
||
| `[]u8`/`[]char` → str | `finch_str_from_bytes` |
|
||
| ptr → str | `finch_str_from_c` (borrowed, `cap = -1`) |
|
||
| ptr ↔ ptr | nothing |
|
||
| int → ptr / ptr → int | `inttoptr` / `ptrtoint` |
|
||
|
||
### Comparisons
|
||
|
||
Pointers/null: `icmp eq/ne ptr` only. `str`: `finch_str_eq` (length + `memcmp`) for `==`/`!=`,
|
||
`finch_str_cmp` for ordering. Floats: `oeq olt ole ogt oge` and `une` for `!=`. Integers/char by
|
||
signedness (char unsigned). Bool: equality only. Arrays/structs: error.
|
||
|
||
---
|
||
|
||
---
|
||
|
||
## 8. Ownership and memory
|
||
|
||
### 9.1 The model
|
||
|
||
Every value of an **owning type** (`str`, `[]T`, or a Finch struct with an owning field) has exactly one
|
||
owner: a variable, an array element, a struct field, or a heap cell from `new()`. The owner drops the
|
||
value when it goes away. There is no reference counting and no garbage collector; every decision is static.
|
||
|
||
| Situation | What happens |
|
||
|---|---|
|
||
| `x := expr` / `T x = expr` / `x = expr` / field, element, `push` | **`own(v)`**: a fresh value is moved in; a borrowed one is deep-copied |
|
||
| `x = …` (owning) | the new value is computed first, the old one dropped after the store |
|
||
| end of a block, `return`, `break`, `continue` | owned variables of every scope being left are dropped (newest first), defers run |
|
||
| `return local` | the variable is **moved out** (marked `moved` while the cleanups for that return are emitted) |
|
||
| passing an argument | **lent**: the callee gets a shallow copy and never drops it; fresh arguments are released by the caller after the call |
|
||
| a parameter the function changes | copied on entry and owned by the callee (decided by `mutates()`, below) |
|
||
| 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
|
||
(`finch_str_own`). Strings from C are borrowed with `cap = -1` and always copied before being kept.
|
||
|
||
### 9.2 Parameter borrowing: `mutates()`
|
||
|
||
When declaring a function, every owning parameter is checked with `mutates(name, body)`: an
|
||
assignment whose root variable (`rootVar()`: through `.field` and `[i]`) is the parameter, a changing
|
||
method on it (`push pop insert remove clear resize sort reverse`), or `addr(…)` of it. Unchanged
|
||
parameters are borrowed (`Fn::borrowParam`); changed ones are copied once at entry. The analysis is
|
||
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:
|
||
|
||
- `str`: `finch_str_drop` (frees if `cap > 0`) / `finch_str_copy` (shares literals, duplicates the rest).
|
||
- `[]T` and owning structs: generated helpers `finch.drop.<key>` and `finch.copy.<key>`, created once per
|
||
type (`dropFn`, `copyFn`, keyed by `typeKey()`, e.g. `arr_S_main_Player`). Arrays drop each owning
|
||
element and then `finch_arr_free`; copies call `finch_arr_clone_raw` and then deep-copy owning
|
||
elements over the raw bits. Structs drop/copy only their owning fields.
|
||
|
||
Helpers are emitted in their own functions (`HelperScope` saves the builder position and switches off
|
||
debug locations), may call each other recursively (`struct Node { []Node kids }`), and are inlined by O2.
|
||
|
||
### 9.4 What it looks like
|
||
|
||
```c
|
||
fn greet(str name) -> str {
|
||
msg := "Hi, " + name
|
||
return msg
|
||
}
|
||
|
||
fn main() {
|
||
a := ["x"]
|
||
b := a
|
||
b.push(greet("Ola"))
|
||
print(b[1])
|
||
}
|
||
```
|
||
|
||
The `-O0` IR of `main` (abridged):
|
||
|
||
```llvm
|
||
%3 = call ptr @finch_alloc(i64 24) ; ["x"]: one element on the heap
|
||
store { ptr, i64, i64 } { ptr @str.1, i64 1, i64 0 }, ptr %4 ; the literal, cap 0
|
||
...
|
||
store { ptr, i64, i64 } %7, ptr %a ; moved into a (fresh)
|
||
%a1 = load { ptr, i64, i64 }, ptr %a
|
||
store { ptr, i64, i64 } %a1, ptr %1
|
||
call void @finch.copy.arr_str(ptr %2, ptr %1) ; b := a copies
|
||
%8 = load { ptr, i64, i64 }, ptr %2
|
||
store { ptr, i64, i64 } %8, ptr %b
|
||
%9 = call { ptr, i64, i64 } @finch.greet({ ptr, i64, i64 } { ptr @str.2, i64 3, i64 0 })
|
||
...
|
||
call void @finch_arr_reserve(ptr %b, i64 %12, i64 24) ; push: grow if needed
|
||
store { ptr, i64, i64 } %9, ptr %15 ; greet's result moved in, no copy
|
||
...
|
||
%20 = icmp uge i64 1, %19 ; b[1]: bounds check
|
||
br i1 %20, label %out.of.range, label %in.range
|
||
...
|
||
call void @finch.drop.arr_str(ptr %b) ; end of main: newest first
|
||
call void @finch.drop.arr_str(ptr %a)
|
||
ret void
|
||
```
|
||
|
||
and `greet` returns `msg` without copying (`ret … %5` after loading the slot: the variable was moved out).
|
||
|
||
### 9.5 defer
|
||
|
||
`defer stmt` pushes a `Cleanup{isDefer, body, order = current varOrder}` into the current scope; it
|
||
emits nothing there. Whenever the scope's cleanups are emitted (falling off the end of the block,
|
||
`return`, `break`/`continue` leaving it) the body is generated **at that exit**, with `deferLimit = order`
|
||
so that `lookup()` hides variables declared after the defer: those are already dropped by then
|
||
(cleanups run in reverse). `return`/`break`/`continue` inside a deferred statement are errors (`inDefer`).
|
||
Defer bodies are therefore duplicated per exit path, which is what makes them free on the normal path.
|
||
|
||
### 9.6 Manual memory
|
||
|
||
`new(v)` allocates `sizeof(T)` with `finch_alloc`, stores `own(v)` and returns `ptr[T]`. `free(p)` drops
|
||
the pointee if `T` is owning (skipping null) and calls libc `free`. Pointers never own anything:
|
||
`addr(x)` can dangle after `x`'s block ends; this is not checked.
|
||
|
||
### 9.7 Verifying it
|
||
|
||
`MEMCHECK=1 tests/run.sh` builds every test program and runs it under valgrind with
|
||
`--leak-check=full --errors-for-leak-kinds=all`: zero leaks and zero invalid accesses across all of them,
|
||
including returns from inside loops, `continue`/`break` with owned locals, defers, mutation during
|
||
iteration, nested arrays of structs, and C struct passing.
|
||
|
||
---
|
||
|
||
---
|
||
|
||
## 9. Generics, enums and match
|
||
|
||
See §4 for how they are generated. For the rules a user sees, read sections 23 and 24 of the
|
||
[technicians' guide](for-technicians.md). The tests are `tests/new/generics.fch`, `tests/new/enums.fch` and their refusals
|
||
in `tests/new/fail/`.
|
||
|
||
---
|
||
|
||
## 10. Bootstrapping
|
||
|
||
The compiler is built from a **seed**: its own IR, committed as `bootstrap/<system>.ll` (see
|
||
[bootstrap/README.md](../../bootstrap/README.md)). `bootstrap/build.sh`:
|
||
|
||
```
|
||
seed .ll ──clang──► finch0 ──builds src/──► finch1 ──builds src/──► build/finch
|
||
```
|
||
|
||
and stops unless `finch1` and `finch` produce byte-identical IR (a fixpoint: the compiler compiles itself the same way
|
||
whatever built it). `bootstrap/build.sh seed` rewrites the seed from the current compiler; do it whenever the compiler's
|
||
own source starts to use a feature the committed seed lacks.
|
||
|
||
A seed is per system because the C calling convention for the libclang calls is baked into it (§5).
|
||
Linux x86-64 and Linux ARM64 have seeds. macOS and Windows would need seeds made with Apple's and Microsoft's headers.
|
||
|
||
---
|
||
|
||
## 11. The language server
|
||
|
||
`finch lsp` speaks JSON-RPC over stdin/stdout (`Content-Length` framing, `std/json`). It does not type check; it runs
|
||
`finch index file.fch --overlay path=copy …` for every change, where the overlays are the editor's unsaved text. In `index`
|
||
mode the compiler prints, besides IR-free output, tab-separated records:
|
||
|
||
| Record | Meaning |
|
||
|---|---|
|
||
| `@R file line col len defFile defLine defCol hover` | a reference: what hover shows and where the definition is |
|
||
| `@V name type file line col fnFile fnLine fnEnd` | a variable and the function it lives in |
|
||
| `@F module name recv file line col endLine signature params` | a function or method |
|
||
| `@S` / `@SF`, `@E`, `@I` | structs (and fields), enums, imports |
|
||
|
||
The first error is printed as `path:line:col: error: message` and becomes a diagnostic. When code does not compile, the
|
||
server keeps answering completion and signature help from the last version that did. `tests/lsp_test.py` is the acceptance
|
||
test (22 checks).
|
||
|
||
---
|
||
|
||
## 12. Semantics vs. C
|
||
|
||
| Situation | C | Finch 2.0 |
|
||
|---|---|---|
|
||
| Signed integer overflow | UB | defined wraparound (no `nsw`) |
|
||
| Integer division by zero, `INT_MIN / -1` | UB | compile error if constant, otherwise runtime error |
|
||
| Array index out of range | UB | runtime error (also for `[N]T` fields of C structs) |
|
||
| Null dereference | UB | runtime error for `.value`, `.field`, `[i]` |
|
||
| Use after free / double free of arrays, strings, structs | common bugs | impossible: no manual freeing of those |
|
||
| Memory leak of arrays, strings, structs | common bug | impossible without `new` |
|
||
| `x & 1 == 0` | `x & (1 == 0)` | `(x & 1) == 0` |
|
||
| Implicit narrowing, signed/unsigned mixing | silent | compile errors |
|
||
| Uninitialized variables | UB | impossible (zero or defaults) |
|
||
| Missing return | UB if used | compile error |
|
||
| Order of evaluation | mostly unspecified | left to right; in an assignment the right side first |
|
||
| Shift ≥ width, out-of-range float→int | UB | still poison (not checked yet) |
|
||
| Dangling pointer from `addr()` / `new` + `free` | UB | still UB |
|
||
|
||
---
|
||
|
||
---
|
||
|
||
### The chain of calls on runtime errors
|
||
|
||
With `Options.trace` (default for `run`, `test`, `-g`, `-O0`; `--trace`/`--no-trace`; never with `--shared`) every Finch
|
||
function gets a frame on its stack:
|
||
|
||
```llvm
|
||
%frame = alloca { ptr, ptr, ptr, i64 } ; caller's frame, name, file, line
|
||
store ptr (load @finch_trace), ...field 0
|
||
store ptr @"name", ...field 1 ; ptr @"file", field 2 ; i64 <line>, field 3
|
||
store ptr %frame, ptr @finch_trace
|
||
```
|
||
|
||
`@finch_trace` is a `thread_local` global in the program's module (initial-exec model unless shared or Windows);
|
||
each statement stores its line into field 3. After every call to a Finch function the caller stores its own frame back
|
||
into `@finch_trace`, which also undoes frames left by a callee that returned early, so no epilogue is needed on the many
|
||
return paths. C-callable wrappers (`addr(fn)` thunks, `export fn`) save and restore `@finch_trace` around their call.
|
||
`main` calls `finch_trace_init(top, set)` with two tiny functions that read and write it, which is how the runtime
|
||
(`print_trace()` in `fatal()`, and the test runner) gets at a thread-local it cannot name. Cost: about 3x on `fib(40)`,
|
||
which is nothing but calls; that is why a plain `finch build` leaves it out.
|
||
|
||
### Threads and atomics
|
||
|
||
`std/thread.fch` is a thin wrapper over the runtime (`finch_thread_spawn/join`, `finch_mutex_*`: pthreads, or
|
||
`CreateThread` and `CRITICAL_SECTION` on Windows). The thread function is any `fn f(ptr)`, passed with `addr(f)`, which
|
||
makes the C-callable thunk. `atomic_add/sub/swap` lower to `atomicrmw add/sub/xchg … seq_cst`, `atomic_load/store` to
|
||
`load/store atomic … seq_cst, align <size>`, and `atomic_cas` to `cmpxchg … seq_cst seq_cst` returning the `i1`. They
|
||
take a `ptr[T]` to any integer type. Finch's arrays, text and maps are not synchronised; sharing one between threads
|
||
needs a mutex.
|
||
|
||
### Warnings
|
||
|
||
`warn()` prints `file:line:col: warning: …` to stderr (so `finch ir` stays pure IR) or, with `index`, an `@W` record the
|
||
language server publishes with severity 2. One per source position. Two are given: a struct parameter, and the element of
|
||
`for x in list`, whose plain data (`byteSize()`: numbers, nested structs, C `[N]T`; `str`/arrays/maps count 0, as only
|
||
their handle is copied) is 256 bytes or more.
|
||
|
||
### Struct arguments by address
|
||
|
||
`argWant()` (call arguments only, not assignments): when a parameter is `ptr[S]` and the argument is a plain variable
|
||
of type `S`, the argument is the variable's address, as if `addr(x)` had been written. Only variables, so nothing is
|
||
evaluated twice and temporaries are never pointed to.
|
||
|
||
## 13. Known limitations
|
||
|
||
- The compiler runs on Linux x86-64 and ARM64 only (no seed for macOS or Windows yet). Programs can be built for all three.
|
||
- macOS output is only checked as IR; Windows and ARM64 are run under Wine and qemu in CI.
|
||
- One error per run, no recovery.
|
||
- `-g` debug info is best with `-O0`.
|
||
- The driver uses `sh` utilities (`rm`, `mkdir`), so it needs a POSIX shell.
|
||
- Generics are monomorphised (code size grows with instantiations); there are no trait bounds, a wrong use is an error
|
||
inside the instantiated body.
|
||
- A failed test's owned memory is not freed (the test runner `longjmp`s out of it).
|
||
- `--target android` is checked to the object file only; linking needs the NDK and has not been run on a device.
|
||
|