mirror of
https://github.com/Nexoniarz/Finch.git
synced 2026-11-03 08:54:25 +00:00
- 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>
1123 lines
64 KiB
Markdown
1123 lines
64 KiB
Markdown
# Finch for Engineers
|
||
|
||
**The complete internals of Finch 2.0.** How source becomes tokens, tokens an AST, and the AST
|
||
LLVM IR, which is then optimized, emitted and linked against a small C runtime. The exact type
|
||
rules; the ownership model and how it lowers to copies and drops; how every construct becomes IR
|
||
(with real compiler output); how C headers, C structs and the System V ABI are handled; modules,
|
||
debug info, and the self-hosted compiler in `boot/`. Every function in the compiler is listed with
|
||
what it does and how it differs from its neighbours.
|
||
|
||
Assumed: you know C/C++ and have at least seen LLVM IR.
|
||
For the language from a user's point of view, see the [technicians' guide](for-technicians.md).
|
||
|
||
> 🇵🇱 Polska wersja: [dla-inzynierow.md](../pl/dla-inzynierow.md)
|
||
|
||
---
|
||
|
||
## Contents
|
||
|
||
1. [Architecture](#1-architecture)
|
||
2. [Source tree](#2-source-tree)
|
||
3. [Lexer](#3-lexer)
|
||
4. [Grammar and parser](#4-grammar-and-parser)
|
||
5. [AST](#5-ast)
|
||
6. [Loading: files and modules](#6-loading-files-and-modules)
|
||
7. [Semantic model](#7-semantic-model)
|
||
8. [Type system](#8-type-system)
|
||
9. [Ownership and memory](#9-ownership-and-memory)
|
||
10. [Code generation](#10-code-generation)
|
||
11. [Runtime checks](#11-runtime-checks)
|
||
12. [The C runtime](#12-the-c-runtime)
|
||
13. [C interop: headers, structs, ABI](#13-c-interop-headers-structs-abi)
|
||
14. [Optimization, emission, linking](#14-optimization-emission-linking)
|
||
15. [Debug info](#15-debug-info)
|
||
16. [Diagnostics](#16-diagnostics)
|
||
17. [The self-hosted compiler (boot/)](#17-the-self-hosted-compiler-boot)
|
||
18. [Build system](#18-build-system)
|
||
19. [Testing](#19-testing)
|
||
20. [Semantics vs. C](#20-semantics-vs-c)
|
||
21. [Function reference](#21-function-reference)
|
||
22. [Extending the compiler](#22-extending-the-compiler)
|
||
23. [Targets: Windows, macOS, ARM64](#23-targets-windows-macos-arm64)
|
||
24. [The language server](#24-the-language-server)
|
||
25. [Editor integrations](#25-editor-integrations)
|
||
26. [Known limitations](#26-known-limitations)
|
||
|
||
---
|
||
|
||
## 1. Architecture
|
||
|
||
Finch is a single-pass front end in front of LLVM: type checking and IR generation are one walk
|
||
over the AST, with no intermediate representation of its own.
|
||
|
||
```
|
||
main.fch ──► Loader::load() ── lex() ── parse() ──► Program (+ each imported module, recursively)
|
||
│
|
||
all `import "x.h"` ──► importHeaders() (libclang) ──► CImports
|
||
│
|
||
hostMachine() ──► TargetMachine (triple + data layout)│
|
||
▼
|
||
generate(progs, cimports, tm, debug) ──► Codegen::run()
|
||
declareStructs() all Finch + C structs, LLVM struct bodies, cycle check
|
||
declareFns() every function's signature (+ "borrow" analysis of parameters)
|
||
define() each body: type check + IRBuilder, scopes, drops, runtime checks
|
||
defineMainWrapper the C `main(argc, argv)`
|
||
DIBuilder::finalize (with -g) ; verifyModule
|
||
▼
|
||
optimize() PassBuilder default<O2> (skipped with -O0)
|
||
emitObject() legacy PM + addPassesToEmitFile ──► prog.o
|
||
link() cc prog.o ~/.cache/finch/rt-<hash>.o [your .c/.o/.a] -lm [pkg-config/-l…]
|
||
▼
|
||
executable (finch run: executed from a temp file with the remaining arguments, then deleted)
|
||
```
|
||
|
||
Design decisions that shape the rest:
|
||
|
||
- **Fail fast.** The first error prints a diagnostic and exits (`failAt()` in `src/error.h`). No error
|
||
recovery, so later phases never see half-valid input.
|
||
- **One walk.** `Codegen` carries scopes, loops, the current function and module. Each expression
|
||
returns its IR value, its Finch type and two flags (`Value_`, §8).
|
||
- **Let LLVM do the work.** Locals are `alloca`s promoted by `mem2reg`/SROA. Finch builds φ-nodes by
|
||
hand only for `&&`/`||`.
|
||
- **Values, not references.** Arrays, strings and structs have value semantics: assignment copies, the
|
||
end of a block drops. Ownership is decided at compile time with no reference counting (§9).
|
||
- **A tiny C runtime** (`runtime/finch_rt.c`, ~430 lines) does what would be tedious in IR: string
|
||
operations, array growth, input, files, panics. It is embedded in the compiler and cached.
|
||
|
||
---
|
||
|
||
## 2. Source tree
|
||
|
||
| File | Lines | Responsibility |
|
||
|---|---|---|
|
||
| `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` | 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` | 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) |
|
||
|
||
---
|
||
|
||
## 3. Lexer
|
||
|
||
`lex(file)` makes one pass over `g_files[file].text` and returns `std::vector<Token>` ending in `Tok::End`.
|
||
|
||
```cpp
|
||
struct Token { Tok kind; std::string text; int line, col; bool newlineBefore; };
|
||
```
|
||
|
||
- **Newlines are not tokens.** Each token records `newlineBefore`; the parser decides what it means (§4).
|
||
This keeps newline handling out of every list rule in the grammar.
|
||
- **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 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
|
||
only when a digit follows, so `0..10` is `Int DotDot Int`.
|
||
- **Strings / chars:** escapes `\n \t \r \0 \\ \" \'`. No newline inside a literal.
|
||
- **Operators:** maximal munch (`->` before `-`, `<<`/`<=` before `<`, `&&` before `&`, `..` before `.`,
|
||
`:=` before `:`). `:` alone is a token (named arguments).
|
||
- **Comments:** `//` and non-nesting `/* */`.
|
||
|
||
---
|
||
|
||
## 4. Grammar and parser
|
||
|
||
`Parser` (`src/parser.cpp`) is hand-written recursive descent with one token of lookahead, plus up to
|
||
three in `atDeclaration()`.
|
||
|
||
```ebnf
|
||
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 [ "." IDENT ] "(" [ param { "," param } ] ")" [ "->" ( type [ "!" ] | "!" ) ] block ;
|
||
(* fn Point.move: a method; "!": can fail *)
|
||
param = type 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 | try | orelse ;
|
||
for = "for" IDENT [ "," IDENT ] "in" expr ( ".." expr block | block ) ; (* range | for-each *)
|
||
assignop = "=" | "+=" | "-=" | "*=" | "/=" | "%=" ;
|
||
|
||
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 | "try" postfix | postfix ;
|
||
postfix = primary { "." IDENT [ args ] | "[" expr "]" } ;
|
||
primary = INT | FLOAT | STRING | CHAR | "true" | "false" | "null"
|
||
| IDENT [ args ] | "(" expr ")" | "[" [ expr { "," expr } [","] ] "]"
|
||
| "[" ":" "]" | "[" expr ":" expr { "," expr ":" expr } [","] "]" ; (* maps *)
|
||
args = "(" [ arg { "," arg } [","] ] ")" ; arg = [ IDENT ":" ] expr ;
|
||
```
|
||
|
||
**Statement termination.** After a statement, `endOfStatement()` requires `}`, end of file, or a token
|
||
with `newlineBefore`. Inside expressions `sameLine()` (`parenDepth > 0 || !newlineBefore`) decides whether
|
||
an operator, `.`, `[` or a call's `(` continues the expression. `parenDepth` grows inside `( )` and `[ ]`
|
||
and is saved/reset to 0 when entering `{ }`.
|
||
|
||
**Precedence is Go's**, not C's: `&` binds like `*`, and `|`/`^` like `+`, all above comparisons, so
|
||
`x & MASK == 0` means `(x & MASK) == 0`.
|
||
|
||
**Recognising statements.** `atDeclaration()` is true for `[` `]` (an array type), `TYPE IDENT`,
|
||
`ptr [`, and `IDENT . IDENT IDENT` (a module's type). Otherwise the parser **parses a whole expression,
|
||
then looks at the next token**: an assignment operator makes it a target (it must be a `Var`, `Member` or
|
||
`Index`), otherwise it must be a call or method call, or it's `this value is computed but never used`.
|
||
|
||
**Struct literals are calls**: `Point(1, 2)` or `Point(x: 1, y: 2)`. Brace-based literals (`Point{…}`)
|
||
would be ambiguous with blocks (`if done {`); a call needs no special grammar, and named arguments
|
||
(`IDENT ":"` in `args()`) are only accepted by constructors at code generation time.
|
||
|
||
---
|
||
|
||
## 5. AST
|
||
|
||
All nodes are in `src/ast.h`: small class hierarchies with an explicit `kind`, dispatched with
|
||
`switch` + `static_cast`.
|
||
|
||
| ExprKind | Node | Fields |
|
||
|---|---|---|
|
||
| `Int` `Float` `Bool` `Char` `Str` | literals | `value` |
|
||
| `Null` | `NullExpr` | |
|
||
| `Var` | `VarExpr` | `name` |
|
||
| `Unary` | `UnaryExpr` | `op` (`- ! ~`), `operand` |
|
||
| `Binary` | `BinaryExpr` | `BinOp op`, `lhs`, `rhs` |
|
||
| `Call` | `CallExpr` | `callee` (a name), `args`, `argNames` |
|
||
| `Member` | `MemberExpr` | `obj`, `field` (`.x`, `.len`, `.ptr`, `.value`) |
|
||
| `Index` | `IndexExpr` | `obj`, `index` |
|
||
| `ArrayLit` | `ArrayLitExpr` | `elems` |
|
||
| `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`, `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
|
||
no first-class functions, and resolving the name late lets one `CallExpr` be a built-in, a conversion
|
||
(`u8(x)`), a struct constructor, a Finch function or a C function.
|
||
|
||
There is no "load" node: whether `x` means its address or its value depends on context (`ref()` vs
|
||
`expr()`, §10.2).
|
||
|
||
### `Type`
|
||
|
||
```cpp
|
||
struct Type {
|
||
enum Kind { Void, Bool, Char, I8, I16, I32, I64, U8, U16, U32, U64, F32, F64,
|
||
Str, Ptr, Null, Array, Map, Fixed, Struct, Named };
|
||
Kind kind;
|
||
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
|
||
};
|
||
```
|
||
|
||
The parser produces `Named` for struct names; `Codegen::resolve()` turns them into `Struct` with an
|
||
`info` pointer. `Fixed` (`[N]T`) only comes from C struct fields. `int` is `I64` and `float` is `F64`.
|
||
Equality is structural; two `Struct`s are equal iff they share `info`.
|
||
|
||
---
|
||
|
||
## 6. Loading: files and modules
|
||
|
||
`Loader::load(path, module, from)` in `main.cpp` reads a file, appends it to `g_files` (positions carry
|
||
the file index in `Pos::file`), lexes and parses it, then loads each `import name` that wasn't seen yet:
|
||
`name.fch` next to the importing file, or in a `FINCH_PATH` folder. The result is a `std::vector<Program>`,
|
||
main file first. Cycles are fine: a module is marked loaded before its own imports are followed.
|
||
|
||
All files become **one LLVM module**. Each Finch module has a `ModuleScope` (functions, structs, the set
|
||
of modules it imports). Names are looked up in the current module; another module's names are reachable
|
||
only qualified (`geometry.length`, `geometry.Vec`) and only if imported there. Functions are emitted as
|
||
`finch.<module>.<name>` (`finch.<name>` in the main file), struct types as `finch.<module>.<Name>`.
|
||
|
||
---
|
||
|
||
## 7. Semantic model
|
||
|
||
### Declaration order
|
||
|
||
`Codegen::run()` first declares **all structs** of all modules plus all imported C structs, resolves
|
||
every field type and sets the LLVM bodies (`declareStructs` → `resolveStruct`), then declares **all
|
||
function signatures** (`declare`), then defines bodies. So order inside a file and across modules never
|
||
matters, and recursion (also mutual, also across modules) works.
|
||
|
||
`resolveStruct` detects by-value cycles with a three-state flag (`struct A { A inner }` is an error with
|
||
the advice to use `ptr[A]` or `[]A`). Pointers and arrays of a struct don't need its layout, so
|
||
`struct Node { []Node kids; ptr[Node] parent }` is fine: they resolve the name "shallowly" (`resolveT(…, deep=false)`).
|
||
|
||
### Scopes and variables
|
||
|
||
```cpp
|
||
struct Var { Value *slot; FType type; bool readonly; std::string readonlyWhy;
|
||
bool owned; int order; bool moved; };
|
||
struct Cleanup { bool isDefer; std::string var; const Stmt *body; int order; };
|
||
struct Scope { unordered_map<string, Var> vars; vector<Cleanup> cleanups; };
|
||
```
|
||
|
||
A scope is pushed for the function's parameters, each `{ }`, each `for` (holds `i`), each `for … in`
|
||
(the hidden list temporary) and each iteration of it (holds `x`).
|
||
|
||
- **No shadowing:** `addVar()` rejects a name found in *any* enclosing scope, a function of the module,
|
||
an imported module name or a built-in.
|
||
- **Read-only:** range and for-each variables (`readonlyWhy` gives the specific message).
|
||
- `order` numbers declarations; `defer` uses it to hide variables declared after it (§9.5).
|
||
|
||
### Name resolution
|
||
|
||
Value position `x`: local variable → C constant → C global → (module name: error with a hint) → error.
|
||
Call `n(...)`: built-ins (`print addr input new free exit shell read_file write_file file_exists`) →
|
||
type names (conversion) → function of the current module → struct of the current module / C struct
|
||
(constructor) → C function → error (with a hint if the function exists in another module, or a
|
||
`headerHint` for well-known libc names).
|
||
|
||
---
|
||
|
||
## 8. 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; §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*`,
|
||
`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 (§9).
|
||
|
||
### 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; 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`.
|
||
|
||
### 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.
|
||
|
||
---
|
||
|
||
## 9. 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.
|
||
|
||
---
|
||
|
||
## 10. Code generation
|
||
|
||
### 10.1 Variables
|
||
|
||
Every variable, including parameters, gets an `alloca` at the top of the entry block (`slot()`;
|
||
C structs get their C alignment). `addVar()` stores the initial value, registers the variable, adds a
|
||
drop cleanup if it owns its value, and (with `-g`) declares it to the debugger.
|
||
|
||
### 10.2 Places: `ref()`, `place()`, `expr()`
|
||
|
||
```cpp
|
||
struct LRef { bool isPlace; Place pl; Value_ val; }; // an address, or (if none) a value
|
||
```
|
||
|
||
`ref(e, forWrite)` walks a `Var`/`Member`/`Index` chain and returns an **address** when one exists:
|
||
|
||
- `Var` → its `alloca` (or a C global).
|
||
- `.field` on a struct place → `getelementptr` to the field (`llvmIndex`; C structs skip padding elements).
|
||
- through a pointer (`p.value`, `p.field`, `p[i]`) → load the pointer, check it isn't null, address the target.
|
||
`p.value` on a pointer to a struct that has its own `value` field means that field.
|
||
- `a[i]` on an array/str place → load `{ptr,len,cap}`, `boundsCheck(i, len)`, `getelementptr`. For
|
||
`forWrite` on a `str`, `finch_str_own` first (copy-on-write for literals). `[N]T` checks against `N`.
|
||
- `.len` / `.ptr` → values.
|
||
- a temporary (call result…) has no address: its field/element is extracted, copied if owning, and the
|
||
temporary released.
|
||
|
||
`expr()` on these kinds calls `ref()` and loads from the address: this is where every `load` of a
|
||
variable, field or element comes from. `place()` demands an address (assignment targets, `addr()`).
|
||
|
||
### 10.3 Assignment
|
||
|
||
`x = v`: evaluate `v` **first** (it may reallocate memory the target lives in: `a[0] = f()` where `f`
|
||
pushes to `a`), then `place(target, forWrite)`, `coerce`, `own`, load the old value, store, drop the old
|
||
one. Compound `x += v`: evaluate `v`, load the target, `arith()`, store, drop the old value (for `str +=`).
|
||
|
||
### 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. 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`.
|
||
|
||
### 10.5 Control flow
|
||
|
||
The builder's current block is the source of truth for reachability (`terminated()`): statements after
|
||
a terminator are errors (`this code can never run`). `continueAt()` deletes a merge block nobody jumps
|
||
to. `if`/`while`/`for` are the classic block layouts; `while true` without `break` leaves the function
|
||
terminated (no `return` needed after it). `for i in a..b` evaluates both ends once; `continue` goes to
|
||
`for.step`. `for x in list` reads the list's length from its address **every round** (pushing inside the
|
||
loop is safe) and keeps the index in a stack slot. `break`/`continue` emit the cleanups of the scopes
|
||
opened inside the loop (`Loop::scopeDepth`) before jumping.
|
||
|
||
### 10.6 Short-circuit `&&` / `||`
|
||
|
||
The only hand-built φ: the left side ends in block `from`, the right side in `rhsEnd`;
|
||
`phi i1 [!isAnd, from], [rhs, rhsEnd]`.
|
||
|
||
### 10.7 Calls
|
||
|
||
`callFinch()` coerces arguments to parameter types (array literals via `exprWant`), passes owning ones
|
||
shallowly, calls, then releases fresh arguments. Results of owning type are fresh.
|
||
`construct()` builds a struct from `zeroinitializer` with `insertvalue`: positional (all fields) or named
|
||
(missing ones take the field's default expression, evaluated in the struct's module with the caller's
|
||
scopes swapped out, or zero). C calls are in §13.
|
||
|
||
### 10.8 Printing
|
||
|
||
`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
|
||
|
||
Arrays: `push` (`finch_arr_reserve` + store + len++), `pop`/`remove` (move out, `remove_gap`),
|
||
`insert` (`insert_gap`), `clear`/`resize` (drop the elements that go away, `finch_arr_resize`),
|
||
`find`/`contains` (a loop with `compare(Eq)`), `slice` (new array, `copyAt` per element), `reverse`
|
||
(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.
|
||
|
||
---
|
||
|
||
## 11. Runtime checks
|
||
|
||
All runtime errors go through `finch_panic(file, line, msg)` / `finch_panic_index(file, line, i, len)`
|
||
(declared `noreturn cold`), so the failing branch is laid out cold and the check costs a compare and a
|
||
well-predicted branch. LLVM removes checks it can prove (constant indexes in range, loop-hoisted bounds).
|
||
|
||
| Check | Where |
|
||
|---|---|
|
||
| division/remainder by zero (constant zero: compile error) | `checkDivisor()` |
|
||
| `MIN / -1` for signed types | `checkDivisor()` |
|
||
| 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` (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.
|
||
|
||
---
|
||
|
||
## 12. The C runtime
|
||
|
||
`runtime/finch_rt.c` is plain C with no global state. CMake embeds it into the compiler as a raw string
|
||
(`rt_source.inc`); on first use `runtimeObject()` compiles it with `$CC -O2 -fPIC -c` into
|
||
`~/.cache/finch/rt-<hash>.o`, where the hash covers the source, the Finch version and the C compiler.
|
||
It is linked statically into every program.
|
||
|
||
The shared layout:
|
||
|
||
```c
|
||
typedef struct { char *ptr; int64_t len; int64_t cap; } FStr; // ptr always NUL-terminated
|
||
typedef struct { void *ptr; int64_t len; int64_t cap; } FArr;
|
||
// str cap: > 0 heap (owned), 0 literal (static), -1 borrowed from C (copy before keeping)
|
||
```
|
||
|
||
Functions take and return these **by pointer**, so their C ABI is trivial (no struct classification):
|
||
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`), 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.
|
||
|
||
---
|
||
|
||
## 13. C interop: headers, structs, ABI
|
||
|
||
### 13.1 Importing headers
|
||
|
||
`importHeaders(imports, dirs)` parses each `import "x.h"` as its own translation unit from an in-memory
|
||
`#include "x.h"`, with `-x c -std=gnu11`, `-I` for every `.fch` file's folder, and `-isystem` for each
|
||
directory the system C compiler searches (read from `$CC -E -v -x c /dev/null`, which is what makes it work
|
||
on NixOS). `gnu11` because glibc hides `M_PI` and friends in strict ISO mode.
|
||
|
||
The `Importer` visits top-level cursors:
|
||
|
||
| Cursor | Result |
|
||
|---|---|
|
||
| `FunctionDecl` | `CFunc` (signature mapped in **signature context**; `static` and K&R functions recorded as unsupported) |
|
||
| `TypedefDecl` of a record | names the record (`typedef struct {…} CXCursor` → `CXCursor`) |
|
||
| `StructDecl` (definition) | names the record by its tag |
|
||
| `VarDecl` `extern` | `CGlobal` (mapped in **data context**) |
|
||
| `EnumConstantDecl`, numeric `MacroDefinition` | `CConst` (literal) |
|
||
|
||
`mapType()` works on canonical types and maps integers by size. The context decides `char *`: a `str`
|
||
in function signatures (Finch converts at the call), but `ptr[char]` inside structs, globals and behind
|
||
pointers (where the memory layout must stay a single pointer). Records by value become
|
||
`Named{name, "C"}` and are defined with `defineRecord()`: every field (`clang_Type_visitFields`) with its
|
||
byte offset, size and alignment, nested records, constant arrays (`Fixed`). Unions and bit fields are kept
|
||
as hidden bytes and mark the struct unsupported for by-value calls; pointers to anything else (void,
|
||
functions, incomplete structs) become `ptr`.
|
||
|
||
### 13.2 Laying out C structs
|
||
|
||
`resolveStruct()` builds a **packed** LLVM struct that mirrors C byte for byte: an `[n x i8]` pad before
|
||
each field whose offset is ahead of the current position, the field itself (only if its LLVM size matches
|
||
C's), and tail padding up to C's `sizeof`. Field accesses use the recorded `llvmIndex`. Packing makes the
|
||
layout exact regardless of LLVM's alignment rules; allocas and temporaries get the C alignment explicitly.
|
||
|
||
### 13.3 The System V x86-64 calling convention
|
||
|
||
`classify(T)` (in `abi.cpp`) flattens a struct into scalar leaves with byte offsets (through nested
|
||
structs and fixed arrays) and:
|
||
|
||
- **MEMORY** if it is larger than 16 bytes or has a misaligned leaf: passed as a pointer with
|
||
`byval(T) align ≥8`, returned through a hidden first `sret(T)` pointer.
|
||
- otherwise each eightbyte is **INTEGER** if any leaf in it is an integer/pointer, else **SSE**. Parts:
|
||
INTEGER → `iN` for the bytes that are left (`i32` for a 4-byte `Color`), SSE → `double`, `<2 x float>`
|
||
or `float`. One part is passed as is; two parts as two arguments, or returned as `{a, b}`.
|
||
|
||
`makePlan()` walks the parameters counting the 6 integer and 8 SSE argument registers; a struct whose
|
||
parts don't fit any more goes to the stack (MEMORY) as a whole, like clang does. Small integer
|
||
parameters get `signext`/`zeroext`. `emitCCall()` stores each struct argument into a slot big enough for
|
||
both the struct and its register form, loads the parts at offsets 0 and 8, calls with the planned function
|
||
type and attributes, and rebuilds struct results the same way.
|
||
|
||
`cThunk(fn)` is the reverse for `addr(fn)`: an internal function with the C signature that reassembles
|
||
struct parameters from registers or `byval` memory, calls the Finch function and lowers its result. Only
|
||
C-compatible signatures are accepted.
|
||
|
||
`tests/run/c_structs.fch` exercises `{float,float}`, `{float,float,float}`, `{u8×4}`, `{double,int}`,
|
||
`{i64,i64}`, a 24-byte struct, a struct with a `char[8]`, nested structs, register exhaustion with 13
|
||
struct arguments, and callbacks taking and returning structs, against a C library compiled with
|
||
`link "abi.c"`. The results match the same calls made from C.
|
||
|
||
### 13.4 Linking C code
|
||
|
||
`link "name"` tries `pkg-config --libs name`, then `-lname`. `link "file.c"` compiles the file (relative
|
||
to the `.fch` file) with `$CC -O2 -fPIC -c` into a temporary object; `.o` and `.a` files are passed
|
||
through. When linking fails, `linkFailed()` maps undefined symbols back to the header that declared them
|
||
and prints the `link` line to add.
|
||
|
||
---
|
||
|
||
## 14. Optimization, emission, linking
|
||
|
||
- **Target:** `sys::getDefaultTargetTriple()`, CPU `generic`, `Reloc::PIC_`. The data layout is set on the
|
||
module before any IR is built (alignment of loads/stores depends on it).
|
||
- **Optimization:** `PassBuilder::buildPerModuleDefaultPipeline(O2)`: clang's `-O2` middle end. `-O0`
|
||
skips it (`finch ir file.fch -O0` shows `Codegen`'s raw output).
|
||
- **Emission:** legacy `PassManager` + `addPassesToEmitFile(ObjectFile)`.
|
||
- **Linking:** `$CC prog.o rt.o [user objects] -lm [libs]`. On Nix the cc wrapper turns `-L` paths into
|
||
`RPATH`, so programs run without `LD_LIBRARY_PATH`.
|
||
- **`run`:** a temporary executable, run with the remaining command-line arguments, deleted afterwards;
|
||
a signal is reported as `the program crashed: <name>` with exit status `128 + n`.
|
||
|
||
Performance notes: user functions are `internal`, so LLVM inlines and specializes them freely
|
||
(recursive `fib(40)`: ≈0.19 s vs. ≈0.23 s for `clang -O2` on the same C on the author's machine);
|
||
bounds-checked array code matches C on a sieve.
|
||
|
||
---
|
||
|
||
## 15. Debug info
|
||
|
||
With `-g`, `Codegen` creates a `DIBuilder`, a compile unit (`DW_LANG_C`, so gdb's C expression syntax
|
||
works for `print p.x`), a `DIFile` per source file, a `DISubprogram` per function, parameter variables
|
||
(`createParameterVariable`, so `bt` shows `length (v=…)`) and local variables (`insertDeclare` on their
|
||
allocas), and a `DILocation` per statement and call (`setLoc`). Types: basic types with Finch names
|
||
(`int`, `u8`, `float`), `str`/arrays as `{ptr, len, cap}` structs, pointers, fixed arrays, and structs with
|
||
member offsets from the LLVM `StructLayout`. Recursive structs use a replaceable forward declaration.
|
||
Generated helpers carry no locations. Use `-g -O0` for the best experience; with O2 some variables are optimized out.
|
||
|
||
---
|
||
|
||
## 16. Diagnostics
|
||
|
||
`failAt(file, line, col, msg)` prints `path:line:col: error: msg` and the source line with a caret
|
||
(tabs preserved, columns counted in characters), then exits with 1. Conventions: binary operators report
|
||
the operator, calls the callee, argument errors the argument. Messages say what is wrong and, when
|
||
there's an obvious fix, how to fix it. There are no warnings. An LLVM verifier failure prints
|
||
`internal compiler error (please report)` and exits with 2.
|
||
|
||
---
|
||
|
||
## 17. The self-hosted compiler (`boot/`)
|
||
|
||
`boot/` is a second Finch compiler written in Finch (≈3,100 lines):
|
||
|
||
| File | Contents |
|
||
|---|---|
|
||
| `boot/lexer.fch` | the lexer (tokens are `{kind, text, line, col, nl}`; kinds are words, keywords and operators are their own kind) |
|
||
| `boot/ast.fch` | `Type` (`kind`, `elem` as a 0/1-element array, `name`, `module`), one generic `Node` for everything, `Program` |
|
||
| `boot/parser.fch` | the same grammar and newline rules as the C++ parser, precedence climbing for binary operators |
|
||
| `boot/gen.fch` | type checker + **textual LLVM IR** generator: scopes, places, coercions, copies, helpers, printing, runtime checks, array/str methods |
|
||
| `boot/main.fch` | loader for modules and the driver: writes `.ll`, runs `clang -O2 file.ll runtime/finch_rt.c` |
|
||
|
||
It supports the core language: `int float bool char str`, `[]T`, structs (defaults, named/positional
|
||
constructors), `ptr[T]`/`addr`/`new`/`free`/`null`, all statements except `defer`, modules, and the
|
||
built-ins and methods the C++ compiler has, minus C imports, sized integers and `defer`.
|
||
It implements the same value semantics and borrow analysis (copies where the main compiler copies) but
|
||
**never frees**: as a bootstrap compiler it leaves memory to the operating system.
|
||
|
||
The compiler state is one `Gen` struct passed as `ptr[Gen]`: Finch has no globals, and passing a
|
||
pointer is how a function changes its caller's value. Without `defer` or a map type, scopes are arrays of
|
||
arrays searched linearly; helper functions (copy/print/compare per type) are generated by temporarily
|
||
swapping the output buffers (`beginHelper`/`endHelper`).
|
||
|
||
**The fixpoint.** `tests/boot.sh`:
|
||
|
||
1. `build/finch` (C++) compiles `boot/` → **stage 1**,
|
||
2. stage 1 compiles `boot/` → **stage 2** (and its IR),
|
||
3. stage 2 compiles `boot/` → **stage 3** IR,
|
||
|
||
and requires the stage 2 and stage 3 IR to be **byte-identical** (≈32,000 lines). It then compiles every
|
||
test in `tests/run` that stays within the subset with the self-hosted compiler and compares the output
|
||
with the expected one. It takes about 2.3 s for the self-hosted compiler to compile itself.
|
||
|
||
---
|
||
|
||
## 18. Build system
|
||
|
||
- `find_package(LLVM CONFIG)`; **if the shared `LLVM` target exists Finch links that single library.**
|
||
`libclang.so` links `libLLVM.so` itself; linking the static component libraries as well put two copies of
|
||
LLVM in one process, and the duplicated global objects crashed at exit (a double free in a global
|
||
destructor). It also made the binary 82 MB instead of ~20 MB.
|
||
- 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.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.
|
||
|
||
---
|
||
|
||
## 19. Testing
|
||
|
||
- `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 **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.
|
||
|
||
---
|
||
|
||
## 20. 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 |
|
||
|
||
---
|
||
|
||
## 21. Function reference
|
||
|
||
### Driver (`main.cpp`)
|
||
|
||
| Function | Does |
|
||
|---|---|
|
||
| `Loader::load` / `findModule` | read, lex, parse a file and recursively its modules / locate `name.fch` |
|
||
| `hostMachine`, `optimize`, `emitObject` | target machine, O2 pipeline, object file |
|
||
| `runtimeObject` | compile and cache the runtime |
|
||
| `capture`, `libFlags`, `link` | run a command / pkg-config or `-l` / the link step, compiling `link "x.c"` files |
|
||
| `linkFailed`, `guessLib` | turn linker output into advice |
|
||
|
||
### Lexer and parser
|
||
|
||
| Function | Does | Differs from |
|
||
|---|---|---|
|
||
| `lex` / `tokName` | text → tokens / names for messages | |
|
||
| `accept` / `expect` / `unexpected` | consume if it matches / consume or fail / fail with context | |
|
||
| `sameLine` / `endOfStatement` | may this token continue the expression? / did the statement end? | |
|
||
| `atDeclaration` / `type` | lookahead for a declaration / parse a type | |
|
||
| `args` | `( [name:] expr, … )` for calls, methods and constructors | |
|
||
| `postfix` | `.field`, `.method(…)`, `[i]` chains | `primary` parses the start of a chain |
|
||
|
||
### Codegen: program and statements (`codegen.cpp`)
|
||
|
||
| Function | Does | Differs from |
|
||
|---|---|---|
|
||
| `declareStructs` / `declareFns` / `declare` | all types / all signatures / one signature + borrow analysis | |
|
||
| `define` / `defineMainWrapper` | one body / the C entry point | |
|
||
| `pushScope` / `popScope` | open / close a scope (`popScope` emits its cleanups if reachable) | |
|
||
| `emitCleanups(downTo)` / `emitScopeCleanups(i)` | cleanups of several scopes without popping (return/break) / of one scope | |
|
||
| `slot` / `tmpOf` / `tmp` | a variable's alloca / an anonymous one / an anonymous one holding a value | |
|
||
| `lookup` / `addVar` | find a visible variable (respects `deferLimit`) / declare one | |
|
||
| `stmt`, `varDecl`, `assign`, `ifStmt`, `whileStmt`, `forStmt`, `forEachStmt`, `returnStmt`, `jump` | statements | |
|
||
| `rootVar` / `mutates` / `mutatesExpr` | the variable an l-value starts from / does a statement or expression change it? | |
|
||
|
||
### Codegen: expressions
|
||
|
||
| Function | Does | Differs from |
|
||
|---|---|---|
|
||
| `expr` / `exprWant` | an r-value / the same, with an expected type for array literals | |
|
||
| `ref` / `place` | an address if any, else a value / an address or an error | `expr` loads from `ref` |
|
||
| `member` / `index` | `.x`, `.len`, `.ptr`, `.value`, auto-deref / `[i]` with bounds checks and copy-on-write | |
|
||
| `arrayLit` | `[…]` → heap array | |
|
||
| `unary`, `binary`, `arith`/`arithOp`, `compare`, `logic` | operators (`arith` adds the literal flag) | |
|
||
| `unify` / `coerce` / `convert` | two operands toward each other / one value into a slot / explicit `T(x)` | |
|
||
|
||
### Codegen: types and ownership (`codegen_types.cpp`)
|
||
|
||
| Function | Does |
|
||
|---|---|
|
||
| `ty`, `resolve`/`resolveT`, `findStruct`, `resolveStruct` | LLVM type; Named → Struct (deep or shallow); struct lookup; layout + cycle check |
|
||
| `owning`, `zero`, `defaultValue` | does a type own memory; zero constant; zero or the struct's defaults |
|
||
| `own` / `release` | something storable (move or copy) / drop an unkept fresh value |
|
||
| `copyValue`, `dropValue`, `copyAt`, `dropAt`, `copyFn`, `dropFn`, `typeKey` | copying and dropping by value, by address, and the per-type helpers |
|
||
| `forN` | an internal `for i in 0..n` loop used by helpers |
|
||
| `strConst`, `cstr`, `strFromC`, `elemSize` | literal str constant; str → `char*`; `char*` → borrowed str; element size |
|
||
| `emitPrint`, `printFn`, `printf_` | printing |
|
||
| `diType`, `setLoc` | debug info |
|
||
|
||
### Codegen: calls (`codegen_builtins.cpp`) and ABI (`abi.cpp`)
|
||
|
||
| Function | Does |
|
||
|---|---|
|
||
| `call` / `method` | resolve a call / a method or module-qualified call |
|
||
| `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` | ABI classification (System V, Microsoft x64, AAPCS64), register plan, declaration, call, callback thunk |
|
||
|
||
---
|
||
|
||
## 22. Extending the compiler
|
||
|
||
**A built-in function:** add the name to `isBuiltin()`, handle it in `call()`, implement it (in IR, or as
|
||
a runtime function declared in `rt()`'s table), add `tests/run/` and `tests/fail/` cases. If it should
|
||
exist in the self-hosted compiler too, add it to `boot/gen.fch`'s `call()` and `runtimeDecls()`.
|
||
|
||
**An array or str method:** add a branch in `arrayMethod()` or the str part of `method()`; if it changes
|
||
the array, add its name to the `changing` sets in `method()` and `mutatesExpr()`, so borrowing stays correct.
|
||
|
||
**An operator:** lexer token + `tokName`, `BinOp`, the right precedence function in the parser, `opName`,
|
||
emission in `arithOp`/`compare`, an explanation in `badOperands`.
|
||
|
||
**A statement:** `StmtKind` + node, `Parser::statement`, `Codegen::stmt`; if it transfers control, respect
|
||
`terminated()`, use `continueAt()` for merge blocks, and emit cleanups (`emitCleanups`) before jumping out of scopes.
|
||
|
||
**A new owning type:** extend `owning()`, `dropAt`/`copyAt` (and their helpers), `emitPrint`, `diType`.
|
||
|
||
---
|
||
|
||
## 23. Targets: Windows, macOS, ARM64
|
||
|
||
`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`.
|
||
- **Processes:** `capture()` / `runCommand()` wrap `popen` / `system` (`_popen` on Windows) and decode exit
|
||
statuses (POSIX `WIFEXITED`/signals, or Windows exit codes and NTSTATUS crashes like `0xC0000005`).
|
||
`shellQuote()` quotes for `sh` (`'…'`) or `cmd.exe` (`"…"`); a command line handed to `cmd /c` that
|
||
starts with a quote gets one extra pair of quotes.
|
||
- **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:** `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.
|
||
- **Printing pointers** is the same everywhere: `null` or `0x…`.
|
||
|
||
### The Microsoft x64 calling convention
|
||
|
||
`classify()` switches on `g_target.windows`. A struct of exactly 1, 2, 4 or 8 bytes travels as **one
|
||
integer** of that size, even if it holds floats (`{float, float}` → `i64`). Any other struct is
|
||
**Indirect**: the caller copies it to its own stack and passes the copy's address (no `byval`: the callee
|
||
receives a plain pointer). Results of 1/2/4/8 bytes come back in `rax` as an integer; anything else
|
||
through a hidden `sret` pointer. Each argument takes one slot, so there is no register counting.
|
||
`cThunk()` mirrors this for callbacks.
|
||
|
||
`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
|
||
(static libraries, `/MT`, so `CMAKE_MSVC_RUNTIME_LIBRARY=MultiThreaded`). Two Windows details in
|
||
`CMakeLists.txt`: that package names the DIA SDK library at the path of the machine LLVM was built on, so
|
||
it is redirected to the installed Visual Studio; and the embedded runtime is written as several raw
|
||
string pieces, because MSVC limits one literal to 16 KB. LLVM is initialized with the native target only,
|
||
so the static build doesn't pull in every backend.
|
||
|
||
---
|
||
|
||
## 24. The language server
|
||
|
||
`finch lsp` (`src/lsp.cpp`) speaks LSP over stdin/stdout with `llvm::json`. On `didOpen`/`didChange`
|
||
it runs the real front end on the document: `Loader` (with the open documents' text as overrides),
|
||
`importHeaders` (cached per set of imports, since reading headers is the slow part), and `generate()`
|
||
without optimizing or emitting. Two switches make the compiler usable inside a server:
|
||
|
||
- `g_throwErrors`: `failAt()` throws `FinchError{file, line, col, msg}` instead of printing and exiting.
|
||
The server publishes it as a diagnostic (for the right file, also inside an imported module) and clears
|
||
diagnostics that disappeared.
|
||
- `g_index`: while generating code, `Codegen::note()` records a `SymRef` for every name it resolves
|
||
(variables, parameters, fields, functions, constructors, C functions and constants, built-ins, methods,
|
||
module members): its span, a markdown hover, and the definition position. `define()` and
|
||
`declareStructs()` add `FnInfo` / `StructIndex`; `addVar()` adds `VarInfo` with the enclosing function's
|
||
line range. Positions of names come from `namePos` fields the parser records.
|
||
|
||
Requests use that index:
|
||
|
||
| Request | From |
|
||
|---|---|
|
||
| hover, definition | the `SymRef` under the cursor |
|
||
| completion | keywords, types, built-ins, variables of the enclosing function, functions, structs, imported modules, C names matching the typed prefix; after `name.`: struct fields (also through `ptr[T]`), array/str methods, or a module's members |
|
||
| signatureHelp | scans back to the unmatched `(`, counts top-level commas, looks up the Finch / C / built-in signature |
|
||
| documentSymbol | functions and structs (with fields) of the file |
|
||
|
||
Half-typed code (`p.`, `add(1, `) doesn't compile, so the server keeps the **last successful** analysis of
|
||
each document and merges its functions, structs and variables in for completion and signature help.
|
||
`tests/lsp_test.py` drives the server like an editor and checks every feature.
|
||
|
||
`src/builtins_doc.h` is the single description of built-ins and methods, used for hover, completion and
|
||
signature help (and mirrored by the cheat sheets).
|
||
|
||
---
|
||
|
||
## 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
|
||
`ProcessExecution` tasks (no shell quoting, the `$finch` problem matcher turns `file:line:col: error:`
|
||
lines into Problems). `syntaxes/finch.tmLanguage.json` is the TextMate grammar, tested by `npm test` with
|
||
`vscode-textmate` + `vscode-oniguruma` (the engine VS Code itself uses);
|
||
`language-configuration.json` gives comments, brackets, auto-closing and indentation; `snippets/` the
|
||
templates. CI packages it with `vsce` into `finch-lang.vsix`.
|
||
|
||
## 26. Known limitations
|
||
|
||
- 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`.
|
||
- 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.
|