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

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

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

View file

@ -69,6 +69,19 @@ jobs:
run: CC=/usr/bin/clang tests/boot.sh run: CC=/usr/bin/clang tests/boot.sh
- name: Language server - name: Language server
run: python3 tests/lsp_test.py run: python3 tests/lsp_test.py
- name: Apple frameworks
run: |
cat > fw.fch <<'EOF'
import "CoreFoundation/CoreFoundation.h"
link "CoreFoundation.framework"
fn main() {
print(CFAbsoluteTimeGetCurrent() > 0.0)
}
EOF
out=$(CC=/usr/bin/clang build/finch run fw.fch)
echo "$out"
test "$out" = "true"
- name: Package - name: Package
run: | run: |
mkdir -p dist/finch && cp build/finch README.md LICENSE dist/finch/ && cp -r examples dist/finch/ mkdir -p dist/finch && cp build/finch README.md LICENSE dist/finch/ && cp -r examples dist/finch/

View file

@ -1,5 +1,5 @@
cmake_minimum_required(VERSION 3.20) cmake_minimum_required(VERSION 3.20)
project(finch VERSION 2.3.0 LANGUAGES C CXX) # C: LLVM's CMake config checks C headers on some systems project(finch VERSION 2.4.0 LANGUAGES C CXX) # C: LLVM's CMake config checks C headers on some systems
set(CMAKE_CXX_STANDARD 17) set(CMAKE_CXX_STANDARD 17)
set(CMAKE_CXX_STANDARD_REQUIRED ON) set(CMAKE_CXX_STANDARD_REQUIRED ON)
@ -15,6 +15,7 @@ add_executable(finch
src/codegen.cpp src/codegen.cpp
src/codegen_types.cpp src/codegen_types.cpp
src/codegen_builtins.cpp src/codegen_builtins.cpp
src/codegen_map.cpp
src/abi.cpp src/abi.cpp
src/cimport.cpp src/cimport.cpp
src/target.cpp src/target.cpp

View file

@ -38,6 +38,8 @@ fn main() {
- **Safer than C, where it is cheap.** No silent narrowing and no signed/unsigned mixing. Out-of-range - **Safer than C, where it is cheap.** No silent narrowing and no signed/unsigned mixing. Out-of-range
indexes, division by zero and null pointers stop with a clear runtime error instead of undefined behavior. indexes, division by zero and null pointers stop with a clear runtime error instead of undefined behavior.
Operator precedence that does not bite (`x & 1 == 0` means what it says). Operator precedence that does not bite (`x & 1 == 0` means what it says).
- **Practical.** Maps, methods, errors as values (`int(s) or 0`, `try read_file(p)`), modules, any C
library, and a language server for your editor. Runs on Linux, macOS and Windows, on x86-64 and ARM64.
- **Errors that talk like a person:** - **Errors that talk like a person:**
``` ```
game.fch:3:13: error: 'y' must be i32, but this is int (use i32(...) to convert) game.fch:3:13: error: 'y' must be i32, but this is int (use i32(...) to convert)
@ -51,11 +53,18 @@ fn main() {
## Quick start ## Quick start
**Windows:** download `finch-windows-x64.zip` from the [releases](https://github.com/Nexoniarz/Finch/releases), Ready builds are in the [releases](https://github.com/Nexoniarz/Finch/releases):
install LLVM 21 and Visual Studio Build Tools (C++), and see the
[install steps](docs/en/for-technicians.md#windows). **VS Code:** install the `finch-lang` `.vsix` from the same release.
**Linux** x86-64, LLVM 21: | System | Download | Also needs |
|---|---|---|
| Windows x64 | `finch-windows-x64.zip` | LLVM 21, Visual Studio Build Tools (C++); [steps](docs/en/for-technicians.md#windows) |
| macOS (Apple Silicon) | `finch-macos-arm64.tar.gz` | `brew install llvm@21`; [steps](docs/en/for-technicians.md#macos) |
| Linux x64 / ARM64 | `finch-linux-x64.tar.gz` / `finch-linux-arm64.tar.gz` | LLVM 21 libraries, a C compiler; [steps](docs/en/for-technicians.md#linux) |
Editors: **Kate** (`editors/kate/install.sh`), **VS Code** (the `finch-lang` `.vsix` from the release), or any
editor with LSP support (`finch lsp`).
**Building it yourself** (Linux or macOS, LLVM 21):
```sh ```sh
git clone https://github.com/Nexoniarz/Finch.git git clone https://github.com/Nexoniarz/Finch.git
@ -66,7 +75,7 @@ ninja -C build
./build/finch run examples/hello.fch ./build/finch run examples/hello.fch
./build/finch build examples/tour.fch -o tour && ./tour ./build/finch build examples/tour.fch -o tour && ./tour
tests/run.sh # 59 passed, 0 failed tests/run.sh # 74 passed, 0 failed
tests/boot.sh # the compiler written in Finch builds itself tests/boot.sh # the compiler written in Finch builds itself
``` ```
@ -86,17 +95,20 @@ Pick the guide that fits you:
| | | | | |
|---|---| |---|---|
| Variables | `x := 5` (inferred) or `int x = 5` | | Variables | `x := 5` (inferred) or `int x = 5` |
| Types | `int` `float` `bool` `char` `str`, sized `i8`…`i64` `u8`…`u64` `f32` `f64`, arrays `[]T`, pointers `ptr[T]` | | Types | `int` `float` `bool` `char` `str`, sized `i8`…`i64` `u8`…`u64` `f32` `f64`, arrays `[]T`, maps `map[K]V`, pointers `ptr[T]` |
| Functions | `fn add(int a, int b) -> int { return a + b }` | | Functions | `fn add(int a, int b) -> int { return a + b }` |
| Structs | `struct Point { … }` with one field per line (`int x`, `int y = 0`); create with `Point(1, 2)` or `Point(x: 1)` | | Structs | `struct Point { … }` with one field per line (`int x`, `int y = 0`); create with `Point(1, 2)` or `Point(x: 1)` |
| Arrays | `nums := [1, 2, 3]`, `nums.push(4)`, `nums[0]`, `nums.len`, `for n in nums { }` | | Methods | `fn Point.move(int dx) { self.x += dx }` → `p.move(3)` |
| Arrays | `nums := [1, 2, 3]`, `nums.push(4)`, `nums[0]`, `nums.len`, `for i, n in nums { }` |
| Maps | `ages := ["ann": 31]`, `ages["bob"] = 25`, `ages.has("cy")`, `counts[w] += 1`, `for k, v in ages { }` |
| Errors | `fn parse(str s) -> int! { … return error("bad") }` → `parse(s) or 0`, `parse(s) or { … }`, `try parse(s)` |
| Text | `"a" + "b"`, `str(42)`, `int("42")`, `s.split(",")`, `s.upper()`, `s[0]` | | Text | `"a" + "b"`, `str(42)`, `int("42")`, `s.split(",")`, `s.upper()`, `s[0]` |
| Control | `if` / `else if` / `else`, `while`, `for i in 0..10`, `break`, `continue`, `defer` | | Control | `if` / `else if` / `else`, `while`, `for i in 0..10`, `break`, `continue`, `defer` |
| Memory | automatic for arrays, text and structs; `new(...)` / `free(...)` for your own heap structures | | Memory | automatic for arrays, text and structs; `new(...)` / `free(...)` for your own heap structures |
| Modules | `import shapes` → `shapes.area(b)` | | Modules | `import shapes` → `shapes.area(b)` |
| C interop | `import "stdio.h"`, `link "glfw"`, `link "mine.c"`, `addr(fn)` for callbacks | | C interop | `import "stdio.h"`, `link "glfw"`, `link "mine.c"`, `addr(fn)` for callbacks |
| I/O | `print(...)`, `input("? ")`, `read_file`, `write_file`, `shell` | | I/O | `print(...)`, `input("? ")`, `read_file`, `write_file`, `shell` |
| Tools | `finch run/build/ir`, `-g` for gdb, `-O0` | | Tools | `finch run/build/ir/lsp`, `--target windows/arm64/macos`, `-g` for gdb/lldb, `-O0` |
## Examples ## Examples
@ -106,6 +118,7 @@ Pick the guide that fits you:
| [`examples/tour.fch`](examples/tour.fch) | variables, functions, conditions, loops | | [`examples/tour.fch`](examples/tour.fch) | variables, functions, conditions, loops |
| [`examples/guess.fch`](examples/guess.fch) | a guessing game: input, loops, C's `rand` | | [`examples/guess.fch`](examples/guess.fch) | a guessing game: input, loops, C's `rand` |
| [`examples/todo.fch`](examples/todo.fch) | structs, arrays, strings and files | | [`examples/todo.fch`](examples/todo.fch) | structs, arrays, strings and files |
| [`examples/wordcount.fch`](examples/wordcount.fch) | maps, methods and errors as values: the most frequent words in a file |
| [`examples/structs.fch`](examples/structs.fch) | structs, arrays of structs, a linked list with `new`/`free` | | [`examples/structs.fch`](examples/structs.fch) | structs, arrays of structs, a linked list with `new`/`free` |
| [`examples/types.fch`](examples/types.fch) | sized numbers and pointers | | [`examples/types.fch`](examples/types.fch) | sized numbers and pointers |
| [`examples/c_import.fch`](examples/c_import.fch) | `printf`, `math.h`, `malloc`/`free`, `stderr` | | [`examples/c_import.fch`](examples/c_import.fch) | `printf`, `math.h`, `malloc`/`free`, `stderr` |
@ -132,19 +145,20 @@ Pick the guide that fits you:
- [x] Bootstrap: a Finch compiler written in Finch that builds itself - [x] Bootstrap: a Finch compiler written in Finch that builds itself
- [x] Windows: `finch.exe`, `.exe` programs, the Microsoft x64 ABI; cross-building from Linux (`--target windows`) - [x] Windows: `finch.exe`, `.exe` programs, the Microsoft x64 ABI; cross-building from Linux (`--target windows`)
- [x] Language server (`finch lsp`) and a VS Code extension - [x] Language server (`finch lsp`) and a VS Code extension
- [ ] Maps, methods on structs, `match` - [x] macOS and ARM64 (Linux and Apple Silicon), with their C calling conventions; Kate support
- [ ] Error values instead of stopping the program - [x] Methods on structs, maps, errors as values (`!`, `or`, `try`)
- [ ] More platforms (macOS, ARM64) - [ ] Generics, `match`, enums
- [ ] A standard library in Finch (JSON, paths, time, processes)
## Layout ## Layout
``` ```
src/ compiler: lexer, parser, AST, codegen, C import + ABI, driver (C++17, ~4.5k lines) src/ compiler: lexer, parser, AST, codegen, C import + ABI, language server, driver (C++17, ~7k lines)
runtime/ the small C runtime linked into every program runtime/ the small C runtime linked into every program
boot/ the Finch compiler written in Finch (~3k lines) boot/ the Finch compiler written in Finch (~3k lines)
examples/ example programs examples/ example programs
editors/ the VS Code extension editors/ VS Code extension, Kate highlighting and LSP setup
tests/ run/ (golden output), fail/ (expected errors), run.sh, boot.sh, windows.sh, lsp_test.py tests/ run/ (golden output), fail/ (expected errors), run.sh, boot.sh, cross.sh, lsp_test.py
docs/ en/ and pl/ guides for three audiences docs/ en/ and pl/ guides for three audiences
``` ```

View file

@ -38,6 +38,8 @@ fn main() {
- **Bezpieczniejszy niż C tam, gdzie to tanie.** Nie ma cichego zwężania typów ani mieszania liczb ze znakiem - **Bezpieczniejszy niż C tam, gdzie to tanie.** Nie ma cichego zwężania typów ani mieszania liczb ze znakiem
i bez znaku. Indeks poza zakresem, dzielenie przez zero i null zatrzymują program czytelnym błędem, zamiast i bez znaku. Indeks poza zakresem, dzielenie przez zero i null zatrzymują program czytelnym błędem, zamiast
powodować niezdefiniowane zachowanie. Priorytety operatorów nie gryzą (`x & 1 == 0` znaczy to, co widać). powodować niezdefiniowane zachowanie. Priorytety operatorów nie gryzą (`x & 1 == 0` znaczy to, co widać).
- **Praktyczny.** Mapy, metody, błędy jako wartości (`int(s) or 0`, `try read_file(p)`), moduły, każda
biblioteka C i serwer języka dla twojego edytora. Działa na Linuksie, macOS i Windowsie, na x86-64 i ARM64.
- **Błędy mówią po ludzku:** - **Błędy mówią po ludzku:**
``` ```
gra.fch:3:13: error: 'y' must be i32, but this is int (use i32(...) to convert) gra.fch:3:13: error: 'y' must be i32, but this is int (use i32(...) to convert)
@ -51,11 +53,18 @@ fn main() {
## Szybki start ## Szybki start
**Windows:** pobierz `finch-windows-x64.zip` z [wydań](https://github.com/Nexoniarz/Finch/releases), Gotowe wersje są w [wydaniach](https://github.com/Nexoniarz/Finch/releases):
zainstaluj LLVM 21 i Visual Studio Build Tools (C++), a potem przejdź przez
[kroki instalacji](docs/pl/dla-technikow.md#windows). **VS Code:** zainstaluj plik `.vsix` `finch-lang` z tego samego wydania.
**Linux** x86-64, LLVM 21: | System | Plik | Potrzebuje też |
|---|---|---|
| Windows x64 | `finch-windows-x64.zip` | LLVM 21, Visual Studio Build Tools (C++); [kroki](docs/pl/dla-technikow.md#windows) |
| macOS (Apple Silicon) | `finch-macos-arm64.tar.gz` | `brew install llvm@21`; [kroki](docs/pl/dla-technikow.md#macos) |
| Linux x64 / ARM64 | `finch-linux-x64.tar.gz` / `finch-linux-arm64.tar.gz` | biblioteki LLVM 21, kompilator C; [kroki](docs/pl/dla-technikow.md#linux) |
Edytory: **Kate** (`editors/kate/install.sh`), **VS Code** (plik `.vsix` `finch-lang` z wydania) albo każdy
edytor obsługujący LSP (`finch lsp`).
**Samodzielne budowanie** (Linux albo macOS, LLVM 21):
```sh ```sh
git clone https://github.com/Nexoniarz/Finch.git git clone https://github.com/Nexoniarz/Finch.git
@ -66,7 +75,7 @@ ninja -C build
./build/finch run examples/hello.fch ./build/finch run examples/hello.fch
./build/finch build examples/tour.fch -o tour && ./tour ./build/finch build examples/tour.fch -o tour && ./tour
tests/run.sh # 59 passed, 0 failed tests/run.sh # 74 passed, 0 failed
tests/boot.sh # kompilator napisany w Finchu buduje sam siebie tests/boot.sh # kompilator napisany w Finchu buduje sam siebie
``` ```
@ -86,17 +95,20 @@ Wybierz przewodnik dla siebie:
| | | | | |
|---|---| |---|---|
| Zmienne | `x := 5` (typ zgadnięty) albo `int x = 5` | | Zmienne | `x := 5` (typ zgadnięty) albo `int x = 5` |
| Typy | `int` `float` `bool` `char` `str`, z rozmiarem `i8`…`i64` `u8`…`u64` `f32` `f64`, tablice `[]T`, wskaźniki `ptr[T]` | | Typy | `int` `float` `bool` `char` `str`, z rozmiarem `i8`…`i64` `u8`…`u64` `f32` `f64`, tablice `[]T`, mapy `map[K]V`, wskaźniki `ptr[T]` |
| Funkcje | `fn add(int a, int b) -> int { return a + b }` | | Funkcje | `fn add(int a, int b) -> int { return a + b }` |
| Struktury | `struct Punkt { … }` z jednym polem na linijkę (`int x`, `int y = 0`); tworzenie przez `Punkt(1, 2)` albo `Punkt(x: 1)` | | Struktury | `struct Punkt { … }` z jednym polem na linijkę (`int x`, `int y = 0`); tworzenie przez `Punkt(1, 2)` albo `Punkt(x: 1)` |
| Tablice | `liczby := [1, 2, 3]`, `liczby.push(4)`, `liczby[0]`, `liczby.len`, `for n in liczby { }` | | Metody | `fn Punkt.przesun(int dx) { self.x += dx }` → `p.przesun(3)` |
| Tablice | `liczby := [1, 2, 3]`, `liczby.push(4)`, `liczby[0]`, `liczby.len`, `for i, n in liczby { }` |
| Mapy | `wiek := ["ania": 31]`, `wiek["bob"] = 25`, `wiek.has("cy")`, `ile[s] += 1`, `for k, v in wiek { }` |
| Błędy | `fn parsuj(str s) -> int! { … return error("zły") }` → `parsuj(s) or 0`, `parsuj(s) or { … }`, `try parsuj(s)` |
| Tekst | `"a" + "b"`, `str(42)`, `int("42")`, `s.split(",")`, `s.upper()`, `s[0]` | | Tekst | `"a" + "b"`, `str(42)`, `int("42")`, `s.split(",")`, `s.upper()`, `s[0]` |
| Sterowanie | `if` / `else if` / `else`, `while`, `for i in 0..10`, `break`, `continue`, `defer` | | Sterowanie | `if` / `else if` / `else`, `while`, `for i in 0..10`, `break`, `continue`, `defer` |
| Pamięć | automatyczna dla tablic, tekstów i struktur; `new(...)` / `free(...)` dla własnych struktur na stercie | | Pamięć | automatyczna dla tablic, tekstów i struktur; `new(...)` / `free(...)` dla własnych struktur na stercie |
| Moduły | `import ksztalty` → `ksztalty.pole(p)` | | Moduły | `import ksztalty` → `ksztalty.pole(p)` |
| Współpraca z C | `import "stdio.h"`, `link "glfw"`, `link "moje.c"`, `addr(fn)` dla callbacków | | Współpraca z C | `import "stdio.h"`, `link "glfw"`, `link "moje.c"`, `addr(fn)` dla callbacków |
| Wejście/wyjście | `print(...)`, `input("? ")`, `read_file`, `write_file`, `shell` | | Wejście/wyjście | `print(...)`, `input("? ")`, `read_file`, `write_file`, `shell` |
| Narzędzia | `finch run/build/ir`, `-g` dla gdb, `-O0` | | Narzędzia | `finch run/build/ir/lsp`, `--target windows/arm64/macos`, `-g` dla gdb/lldb, `-O0` |
## Przykłady ## Przykłady
@ -106,6 +118,7 @@ Wybierz przewodnik dla siebie:
| [`examples/tour.fch`](examples/tour.fch) | zmienne, funkcje, warunki, pętle | | [`examples/tour.fch`](examples/tour.fch) | zmienne, funkcje, warunki, pętle |
| [`examples/guess.fch`](examples/guess.fch) | gra w zgadywanie: wejście, pętle, `rand` z C | | [`examples/guess.fch`](examples/guess.fch) | gra w zgadywanie: wejście, pętle, `rand` z C |
| [`examples/todo.fch`](examples/todo.fch) | struktury, tablice, teksty i pliki | | [`examples/todo.fch`](examples/todo.fch) | struktury, tablice, teksty i pliki |
| [`examples/wordcount.fch`](examples/wordcount.fch) | mapy, metody i błędy jako wartości: najczęstsze słowa w pliku |
| [`examples/structs.fch`](examples/structs.fch) | struktury, tablice struktur, lista wiązana z `new`/`free` | | [`examples/structs.fch`](examples/structs.fch) | struktury, tablice struktur, lista wiązana z `new`/`free` |
| [`examples/types.fch`](examples/types.fch) | liczby z rozmiarem i wskaźniki | | [`examples/types.fch`](examples/types.fch) | liczby z rozmiarem i wskaźniki |
| [`examples/c_import.fch`](examples/c_import.fch) | `printf`, `math.h`, `malloc`/`free`, `stderr` | | [`examples/c_import.fch`](examples/c_import.fch) | `printf`, `math.h`, `malloc`/`free`, `stderr` |
@ -132,19 +145,20 @@ Wybierz przewodnik dla siebie:
- [x] Bootstrap: kompilator Fincha napisany w Finchu, który buduje sam siebie - [x] Bootstrap: kompilator Fincha napisany w Finchu, który buduje sam siebie
- [x] Windows: `finch.exe`, programy `.exe`, ABI Microsoft x64; budowanie na Windowsa z Linuksa (`--target windows`) - [x] Windows: `finch.exe`, programy `.exe`, ABI Microsoft x64; budowanie na Windowsa z Linuksa (`--target windows`)
- [x] Serwer języka (`finch lsp`) i rozszerzenie VS Code - [x] Serwer języka (`finch lsp`) i rozszerzenie VS Code
- [ ] Mapy, metody w strukturach, `match` - [x] macOS i ARM64 (Linux i Apple Silicon) z ich konwencjami wywołań C; obsługa Kate
- [ ] Błędy jako wartości zamiast zatrzymywania programu - [x] Metody w strukturach, mapy, błędy jako wartości (`!`, `or`, `try`)
- [ ] Więcej platform (macOS, ARM64) - [ ] Typy generyczne, `match`, enumy
- [ ] Biblioteka standardowa w Finchu (JSON, ścieżki, czas, procesy)
## Struktura ## Struktura
``` ```
src/ kompilator: lekser, parser, AST, codegen, import C + ABI, sterownik (C++17, ok. 4,5 tys. linii) src/ kompilator: lekser, parser, AST, codegen, import C + ABI, serwer języka, sterownik (C++17, ok. 7 tys. linii)
runtime/ mały runtime w C dołączany do każdego programu runtime/ mały runtime w C dołączany do każdego programu
boot/ kompilator Fincha napisany w Finchu (ok. 3 tys. linii) boot/ kompilator Fincha napisany w Finchu (ok. 3 tys. linii)
examples/ przykładowe programy examples/ przykładowe programy
editors/ rozszerzenie VS Code editors/ rozszerzenie VS Code, podświetlanie i konfiguracja LSP dla Kate
tests/ run/ (wzorcowe wyjście), fail/ (oczekiwane błędy), run.sh, boot.sh, windows.sh, lsp_test.py tests/ run/ (wzorcowe wyjście), fail/ (oczekiwane błędy), run.sh, boot.sh, cross.sh, lsp_test.py
docs/ przewodniki en/ i pl/ dla trzech grup odbiorców docs/ przewodniki en/ i pl/ dla trzech grup odbiorców
``` ```

View file

@ -16,7 +16,7 @@ Everything in Finch on one page. For explanations, see the [guides](../../README
| `finch lsp` | the language server (editors use it) | | `finch lsp` | the language server (editors use it) |
| `finch version` | version | | `finch version` | version |
| `-l lib` | link a C library (like `link "lib"`) | | `-l lib` | link a C library (like `link "lib"`) |
| `--target windows` / `linux` / triple | build for another system | | `--target windows` / `linux` / `arm64` / `macos` / triple | build for another system |
| `-g` | debug info (gdb, lldb, Visual Studio) | | `-g` | debug info (gdb, lldb, Visual Studio) |
| `-O0` | no optimizations | | `-O0` | no optimizations |
@ -71,6 +71,7 @@ One statement per line, no semicolons, `{` on the same line, no brackets around
| `u8 u16 u32 u64` | unsigned, sized | | `u8 u16 u32 u64` | unsigned, sized |
| `f32 f64` | floats | | `f32 f64` | floats |
| `[]T` | array: `[1, 2, 3]`, `[]str names` | | `[]T` | array: `[1, 2, 3]`, `[]str names` |
| `map[K]V` | map: `["a": 1]`, `map[str]int counts`, `[:]` |
| `ptr[T]`, `ptr` | pointer, untyped pointer | | `ptr[T]`, `ptr` | pointer, untyped pointer |
| `Name` / `module.Name` | a struct | | `Name` / `module.Name` | a struct |
@ -100,6 +101,8 @@ if a > b { … } else if a == b { … } else { … }
while x > 0 { … } while x > 0 { … }
for i in 0..10 { … } // 0 … 9 for i in 0..10 { … } // 0 … 9
for item in list { … } // every element (or char of a str) for item in list { … } // every element (or char of a str)
for i, item in list { … } // with its index
for key, value in m { … } // every map entry, in insertion order
break continue return value break continue return value
defer cleanup() // runs when the block ends defer cleanup() // runs when the block ends
``` ```
@ -121,6 +124,47 @@ Parameters behave like copies. Order in the file doesn't matter. A comma after t
--- ---
## Methods
```c
fn Point.move(int dx, int dy) { // self is the Point it's called on (not a copy)
self.x += dx
self.y += dy
}
fn Point.dist() -> float { … }
p.move(1, 2) list[0].move(1, 1) ptr_to_point.move(0, 1)
```
Also on structs from C: `fn Vector2.length() -> f32 { … }`.
---
## Errors as values
```c
fn parse(str s) -> int! { // ! = can fail
if s.len == 0 {
return error("empty")
}
return int(s) or { return error("not a number: " + s) }
}
fn save(str p, str t) -> ! { // can fail, no result
try write_file(p, t)
}
```
| Code | On failure |
|---|---|
| `x := parse(s) or 0` | use 0 |
| `x := parse(s) or { print(err); return }` | run the block (`err`: the message) |
| `x := try parse(s)` | fail the current function too |
| `fn main() -> !` | a failure prints `error: …`, exit code 1 |
Fallible built-ins: `int(s)`, `float(s)`, `read_file`, `write_file`, `delete_file`.
---
## Structs ## Structs
| Code | Meaning | | Code | Meaning |
@ -155,6 +199,23 @@ Parameters behave like copies. Order in the file doesn't matter. A comma after t
--- ---
## Maps `map[K]V`
| Code | Gives |
|---|---|
| `m := ["a": 1, "b": 2]` | a map (keys: numbers, char, bool, str) |
| `m[k]` | value (missing key: the program stops) |
| `m[k] = v` | add / replace |
| `m[k] += 1`, `m[k].push(x)` | a missing key starts at the default (0, `[]`, …) |
| `m.len` | number of keys |
| `m.has(k)` | bool |
| `m.get(k, default)` | value or default |
| `m.remove(k)` | bool: was it there |
| `m.clear()` | empty it |
| `m.keys()`, `m.values()` | arrays, insertion order |
---
## Text `str` ## Text `str`
| Code | Gives | | Code | Gives |
@ -189,6 +250,8 @@ Parameters behave like copies. Order in the file doesn't matter. A comma after t
| `char(66)` | `'B'` | | `char(66)` | `'B'` |
| `bool(0)` | `false` | | `bool(0)` | `false` |
| `str(42)`, `str(1.5)`, `str(true)`, `str('c')` | text | | `str(42)`, `str(1.5)`, `str(true)`, `str('c')` | text |
| `str([1, 2])`, `str(m)`, `str(point)` | the same text `print` shows |
| `int("x") or 0`, `float(s) or 0.0` | a number, or the fallback |
| `str(bytes)` | `[]u8` → text | | `str(bytes)` | `[]u8` → text |
| `str(p)` | C `char*` → text | | `str(p)` | C `char*` → text |
| `ptr(p)`, `ptr(16)` | untyped pointer, from a pointer or a number | | `ptr(p)`, `ptr(16)` | untyped pointer, from a pointer or a number |
@ -210,6 +273,7 @@ Automatic only when nothing is lost: smaller → bigger number, any int → floa
| `delete_file(path)` | bool | | `delete_file(path)` | bool |
| `shell(command)` | run a command, its exit code | | `shell(command)` | run a command, its exit code |
| `exit(code)` | stop the program now | | `exit(code)` | stop the program now |
| `error(message)` | `return error("…")` in a `-> T!` function |
| `addr(x)` | pointer to x; `addr(fn)` = C callback | | `addr(x)` | pointer to x; `addr(fn)` = C callback |
| `new(value)` | value on the heap → `ptr[T]` | | `new(value)` | value on the heap → `ptr[T]` |
| `free(p)` | give it back | | `free(p)` | give it back |
@ -281,11 +345,17 @@ shapes.Box other // in declarations
| `pop() on an empty array` | | | `pop() on an empty array` | |
| `used .value on a null pointer` | `.value` / `.field` / `[i]` through `null` | | `used .value on a null pointer` | `.value` / `.field` / `[i]` through `null` |
| `can't turn "x" into int` | `int("x")` | | `can't turn "x" into int` | `int("x")` |
| `can't read the file "…"` | `read_file` | | `can't read the file "…"` | `read_file` (without `or`) |
| `the key "x" is not in the map` | `m["x"]` |
--- ---
## VS Code (extension "Finch") ## Editors
Kate: `editors/kate/install.sh`, then enable the **LSP Client** plugin; run with **Build & Run** → `finch run %f`.
Any LSP editor: `finch lsp`.
### VS Code (extension "Finch")
| Key / action | Does | | Key / action | Does |
|---|---| |---|---|
@ -295,11 +365,12 @@ shapes.Box other // in declarations
| hover | type / signature / docs | | hover | type / signature / docs |
| `Ctrl+Space` | completion (after `.`: fields, methods, module members) | | `Ctrl+Space` | completion (after `.`: fields, methods, module members) |
| `Ctrl+Shift+O` | functions and structs in the file | | `Ctrl+Shift+O` | functions and structs in the file |
| `main`, `fn`, `fnr`, `struct`, `for`, `foreach`, `if`, `ife`, `while`, `input` + Tab | snippets | | `main`, `fn`, `fnr`, `fnf`, `method`, `struct`, `for`, `foreach`, `forkv`, `map`, `orb`, `if`, `ife`, `while`, `input` + Tab | snippets |
| setting `finch.path` | where finch / finch.exe is | | setting `finch.path` | where finch / finch.exe is |
--- ---
## Keywords ## Keywords
`fn return if else while for in break continue true false null import link struct defer` `fn return if else while for in break continue true false null import link struct defer or try`
(and `self` inside methods)

View file

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

View file

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

View file

@ -20,16 +20,16 @@ compiler works inside. For that, see the [guide for engineers](for-engineers.md)
7. [Operators](#7-operators) 7. [Operators](#7-operators)
8. [Control flow](#8-control-flow) 8. [Control flow](#8-control-flow)
9. [Functions](#9-functions) 9. [Functions](#9-functions)
10. [Arrays](#10-arrays) 10. [Arrays and maps](#10-arrays-and-maps)
11. [Text (str)](#11-text-str) 11. [Text (str)](#11-text-str)
12. [Structs](#12-structs) 12. [Structs and methods](#12-structs-and-methods)
13. [Memory: who frees what](#13-memory-who-frees-what) 13. [Memory: who frees what](#13-memory-who-frees-what)
14. [Pointers](#14-pointers) 14. [Pointers](#14-pointers)
15. [Built-in functions](#15-built-in-functions) 15. [Built-in functions](#15-built-in-functions)
16. [Modules](#16-modules) 16. [Modules](#16-modules)
17. [Using C libraries](#17-using-c-libraries) 17. [Using C libraries](#17-using-c-libraries)
18. [Errors](#18-errors) 18. [Errors](#18-errors)
19. [Editor: VS Code](#19-editor-vs-code) 19. [Editors: VS Code, Kate and others](#19-editors-vs-code-kate-and-others)
20. [Debugging](#20-debugging) 20. [Debugging](#20-debugging)
21. [Troubleshooting](#21-troubleshooting) 21. [Troubleshooting](#21-troubleshooting)
22. [Project layout and tests](#22-project-layout-and-tests) 22. [Project layout and tests](#22-project-layout-and-tests)
@ -60,7 +60,9 @@ messages that tell you how to fix the problem.
## 2. Installing ## 2. Installing
Finch runs on **Linux and Windows, x86-64**, and is built with **LLVM 21**. Finch runs on **Linux** (x86-64 and ARM64), **macOS** (Apple Silicon and Intel) and **Windows** (x86-64),
and is built with **LLVM 21**. Each [release](https://github.com/Nexoniarz/Finch/releases) has a ready
`finch` for Windows, Linux x86-64, Linux ARM64 and macOS ARM64.
### Windows ### Windows
@ -76,8 +78,30 @@ Finch runs on **Linux and Windows, x86-64**, and is built with **LLVM 21**.
Programs are normal `.exe` files. To build from source on Windows instead, follow `.github/workflows/ci.yml` Programs are normal `.exe` files. To build from source on Windows instead, follow `.github/workflows/ci.yml`
(Visual Studio 2022 + the `clang+llvm-21.x-x86_64-pc-windows-msvc` package from LLVM's releases). (Visual Studio 2022 + the `clang+llvm-21.x-x86_64-pc-windows-msvc` package from LLVM's releases).
### macOS
```sh
brew install llvm@21 # Finch's LLVM; programs are linked with Apple's clang
```
Then either unpack **`finch-macos-arm64.tar.gz`** from the releases (it uses Homebrew's `llvm@21`),
or build it yourself (Xcode command line tools: `xcode-select --install`):
```sh
git clone https://github.com/Nexoniarz/Finch.git && cd Finch
cmake -S . -B build -G Ninja -DCMAKE_BUILD_TYPE=Release -DLLVM_DIR="$(brew --prefix llvm@21)/lib/cmake/llvm"
ninja -C build
./build/finch version
```
Apple frameworks link with `link "Cocoa.framework"` (`-framework Cocoa`), and their headers import as usual:
`import "OpenGL/gl.h"`.
### Linux ### Linux
The ready `finch-linux-x64.tar.gz` / `finch-linux-arm64.tar.gz` need LLVM 21's libraries
(Debian/Ubuntu: `libllvm21 libclang1-21` from [apt.llvm.org](https://apt.llvm.org)) and a C compiler (`cc`).
Building it yourself works the same on x86-64 and ARM64 (Raspberry Pi 4/5, Graviton, …).
Other LLVM versions will likely fail to compile, because LLVM's C++ API changes between releases. Other LLVM versions will likely fail to compile, because LLVM's C++ API changes between releases.
### Option A: Nix (recommended, nothing to install by hand) ### Option A: Nix (recommended, nothing to install by hand)
@ -88,7 +112,7 @@ cd Finch
nix-shell # downloads LLVM, libclang, clang, cmake, ninja, pkg-config nix-shell # downloads LLVM, libclang, clang, cmake, ninja, pkg-config
cmake -S . -B build -G Ninja cmake -S . -B build -G Ninja
ninja -C build ninja -C build
./build/finch version # finch 2.3.0 (LLVM 21.x) ./build/finch version # finch 2.4.0 (LLVM 21.x)
``` ```
### Option B: your distribution's packages ### Option B: your distribution's packages
@ -109,16 +133,27 @@ cmake -S . -B build -G Ninja -DLLVM_DIR=/usr/lib/llvm-21/lib/cmake/llvm
ninja -C build ninja -C build
``` ```
### Building Windows programs on Linux ### Building for another system
With the MinGW-w64 cross compiler installed (`x86_64-w64-mingw32-gcc`; on Nix `--target` builds a program for another system. Finch needs that system's C compiler to link:
`pkgsCross.mingwW64.buildPackages.gcc`):
| `--target` | Builds | C compiler used | `finch run` uses |
|---|---|---|---|
| `windows` | a Windows `.exe` | `x86_64-w64-mingw32-gcc` (MinGW-w64) | Wine |
| `arm64` | an ARM64 Linux program | `aarch64-linux-gnu-gcc` | `qemu-aarch64` |
| `linux` | an x86-64 Linux program | `clang --target=…` | |
| `macos` | a macOS program (on a Mac: for the other processor) | `clang --target=…` | |
| any LLVM triple | e.g. `aarch64-unknown-linux-gnu` | `clang --target=<triple>` | |
```sh ```sh
finch build game.fch --target windows # game.exe finch build game.fch --target windows # game.exe
finch run game.fch --target windows # runs it with Wine, if installed finch run game.fch --target arm64 # builds for ARM64 and runs it with QEMU
FINCH_CC=aarch64-unknown-linux-gnu-gcc finch build tool.fch --target arm64 # another compiler name
``` ```
On Nix the compilers are `pkgsCross.mingwW64.buildPackages.gcc` and
`pkgsCross.aarch64-multiplatform.buildPackages.gcc`.
### Putting `finch` on your PATH (optional) ### Putting `finch` on your PATH (optional)
```sh ```sh
@ -137,12 +172,14 @@ Finch's small runtime library, which is kept in `~/.cache/finch`. Set `CC` to us
finch run <file.fch> [args...] compile and run right away finch run <file.fch> [args...] compile and run right away
finch build <file.fch> [-o name] compile into a program (default name: the file's name) finch build <file.fch> [-o name] compile into a program (default name: the file's name)
finch ir <file.fch> print the LLVM IR (for the curious) finch ir <file.fch> print the LLVM IR (for the curious)
finch lsp the language server, for editors
finch version show the version finch version show the version
options: options:
-l <lib> link a C library, same as link "lib" in the file -l <lib> link a C library, same as link "lib" in the file
-g add debug info (for gdb / lldb) --target <name> build for another system: windows, linux, arm64, macos, or an LLVM triple
-O0 skip optimizations -g add debug info (for gdb / lldb / Visual Studio)
-O0 skip optimizations
``` ```
Examples: Examples:
@ -227,6 +264,7 @@ x += 1 // also -= *= /= %=
| `char` | one byte-sized character | `'A'`, `'\n'` | | `char` | one byte-sized character | `'A'`, `'\n'` |
| `str` | text | `"hello\tworld"` | | `str` | text | `"hello\tworld"` |
| `[]T` | a list (array) of `T` | `[1, 2, 3]` | | `[]T` | a list (array) of `T` | `[1, 2, 3]` |
| `map[K]V` | values of type `V` found by a key of type `K` | `["a": 1, "b": 2]` |
| your structs | named groups of values | `Point(1, 2)` | | your structs | named groups of values | `Point(1, 2)` |
String and char escapes: `\n` new line, `\t` tab, `\r`, `\0`, `\\`, `\"`, `\'`. String and char escapes: `\n` new line, `\t` tab, `\r`, `\0`, `\\`, `\"`, `\'`.
@ -333,6 +371,14 @@ for i in 0..10 { // i = 0, 1, …, 9 (the end is not included)
for name in names { // every element of an array (or every char of a str) for name in names { // every element of an array (or every char of a str)
print(name) print(name)
} }
for i, name in names { // with its index: 0, 1, 2, ...
print(i, name)
}
for key, value in ages { // every entry of a map, in the order the keys were added
print(key, value) // (for key in ages: just the keys)
}
``` ```
- A condition must be a `bool`. `if x {` with a number is an error; write `if x != 0 {`. - A condition must be a `bool`. `if x {` with a number is an error; write `if x != 0 {`.
@ -375,11 +421,13 @@ fn no_result(str message) { // no "->": returns nothing
To change the caller's variable, pass a pointer: see [Pointers](#14-pointers). To change the caller's variable, pass a pointer: see [Pointers](#14-pointers).
- A function with `-> type` must return a value on every path. Finch checks this. - A function with `-> type` must return a value on every path. Finch checks this.
- A function cannot be defined inside another function. - A function cannot be defined inside another function.
- A function that can fail is written `-> int!` (or `-> !` with no result); see
[Errors as values](#errors-as-values).
- Long calls can be split over several lines inside the brackets; a comma after the last argument is fine. - Long calls can be split over several lines inside the brackets; a comma after the last argument is fine.
--- ---
## 10. Arrays ## 10. Arrays and maps
```c ```c
nums := [5, 3, 8] // an array of int nums := [5, 3, 8] // an array of int
@ -408,6 +456,44 @@ grid := [[1, 2], [3, 4]] // arrays of arrays
**Assigning copies:** after `b := a`, changing `b` does not change `a`. **Assigning copies:** after `b := a`, changing `b` does not change `a`.
### Maps
A `map[K]V` finds values by a key. Keys can be whole numbers, `char`, `bool` or `str`; values can be
anything (arrays, structs, other maps). Iterating goes in the order the keys were first added.
```c
ages := ["anna": 31, "bob": 25] // a map literal: map[str]int
map[str][]str groups // an empty map
map[int]str names = [:] // [:] is an empty map where the type is known
```
| Operation | Meaning |
|---|---|
| `m[k]` | the value for `k`; a missing key stops the program with `the key "k" is not in the map` |
| `m[k] = v` | add or replace |
| `m[k] += 1`, `m[k].push(x)`, `m[k].field = …` | changing a missing key adds it first, with the value type's default (0, `""`, `[]`, …) |
| `m.len` | the number of keys |
| `m.has(k)` | is `k` there? |
| `m.get(k, default)` | the value, or `default` (only computed when needed) |
| `m.remove(k)` | removes `k`; `true` if it was there |
| `m.clear()` | removes everything |
| `m.keys()`, `m.values()` | new arrays, in insertion order |
| `print(m)`, `str(m)` | `{"anna": 31, "bob": 25}` |
```c
map[str]int counts
for word in text.split(" ") {
counts[word] += 1 // counting: a new word starts at 0
}
for word, n in counts {
print(word, n)
}
```
Lookups are hash-based (average constant time). Like arrays, maps own their keys and values,
copy on assignment, and are freed at the end of their block. Don't add or remove keys of a map
inside a `for` over that same map; collect them in an array and change the map after the loop.
--- ---
## 11. Text (str) ## 11. Text (str)
@ -437,7 +523,7 @@ to C as `char*` works.
--- ---
## 12. Structs ## 12. Structs and methods
```c ```c
struct Player { struct Player {
@ -457,7 +543,34 @@ print(p) // Player(name: "Ola", lives: 2, scores: [10], pos: Point(x: 5, y
- Assigning or passing a struct copies it, together with its arrays and text. - Assigning or passing a struct copies it, together with its arrays and text.
- A struct can't contain itself directly; use `ptr[Node]` or `[]Node` for that field (trees, lists). - A struct can't contain itself directly; use `ptr[Node]` or `[]Node` for that field (trees, lists).
- Structs have no methods. Write functions that take the struct: `fn heal(Player p) -> Player`.
### Methods
A method is a function that belongs to a struct: `fn Struct.name(...)`. Inside, `self` is the struct
it was called on, and it is the caller's value itself, not a copy: changes to `self` stay.
```c
fn Player.heal(int amount) {
self.lives += amount
}
fn Player.is_alive() -> bool {
return self.lives > 0
}
p := Player(name: "Ola")
p.heal(2) // p.lives is now 5
if p.is_alive() { ... }
team[0].heal(1) // on an array element, a field, m[key], through a ptr[Player] ...
```
- A method goes in the same file (module) as its struct, and is available wherever the struct is.
- Methods can also be added to **structs from C headers**: `fn Vector2.length() -> f32 { ... }`.
- A method can't have the name of a field.
- Calling a method that changes `self` on a loop variable (`for p in team { p.heal(1) }`) is an error,
because the loop variable is read-only; use `for i in 0..team.len { team[i].heal(1) }`.
- Like functions, methods can fail (`-> !`); see [Errors as values](#errors-as-values).
--- ---
@ -520,14 +633,16 @@ Using `.value`, `.field` or `[i]` on a `null` pointer **stops the program** with
|---|---| |---|---|
| `print(a, b, ...)` | Prints all values separated by spaces, then a new line. Works with every type, including arrays and structs. | | `print(a, b, ...)` | Prints all values separated by spaces, then a new line. Works with every type, including arrays and structs. |
| `input()`, `input("question")` | Reads one line typed by the user (without the newline). Empty text at end of input. | | `input()`, `input("question")` | Reads one line typed by the user (without the newline). Empty text at end of input. |
| `read_file(path)` | The whole file as `str`. Stops the program with a clear message if it can't be read. | | `read_file(path)` | The whole file as `str`. Stops the program with a clear message if it can't be read, unless handled: `read_file(p) or ...`. |
| `write_file(path, text)` | Writes (replaces) the file; gives back `true` if it worked. | | `write_file(path, text)` | Writes (replaces) the file; gives back `true` if it worked. With `or` / `try`, the failure carries the reason. |
| `file_exists(path)` | `true` / `false` | | `file_exists(path)` | `true` / `false` |
| `delete_file(path)` | Deletes the file; `true` if it worked (or the reason, with `or` / `try`). |
| `error(message)` | The failure a fallible function returns: `return error("...")`. |
| `shell(command)` | Runs a shell command, gives back its exit code. | | `shell(command)` | Runs a shell command, gives back its exit code. |
| `exit(code)` | Ends the program right away. | | `exit(code)` | Ends the program right away. |
| `addr(x)` | A pointer to `x`; `addr(function)` gives C a function pointer (callback). | | `addr(x)` | A pointer to `x`; `addr(function)` gives C a function pointer (callback). |
| `new(value)`, `free(p)` | Manual heap memory (see [Memory](#13-memory-who-frees-what)). | | `new(value)`, `free(p)` | Manual heap memory (see [Memory](#13-memory-who-frees-what)). |
| `int(x)`, `str(x)`, `u8(x)`, … | Conversions (see [Conversions](#conversions)). | | `int(x)`, `str(x)`, `u8(x)`, … | Conversions (see [Conversions](#conversions)). `str(x)` works on every type and gives the same text `print` shows. `int(text) or 0` handles text that isn't a number. |
--- ---
@ -669,6 +784,54 @@ Both work directly; `examples/opengl.fch` and `examples/vulkan.fch` are complete
## 18. Errors ## 18. Errors
### Errors as values
Some failures are normal: the user types `abc`, a file is missing. Finch handles them without
exceptions and without hidden control flow. A function that can fail says so with `!` after its result
type, and fails with `return error("message")`:
```c
fn parse_port(str text) -> int! {
n := int(text) or { return error("'" + text + "' is not a number") }
if n < 1 || n > 65535 {
return error("port " + str(n) + " is out of range")
}
return n
}
fn save(str path, str text) -> ! { // can fail, gives back nothing
try write_file(path, text)
}
```
The caller **must** handle the failure, in one of three ways (forgetting is a compile error):
| Code | On failure |
|---|---|
| `port := parse_port(s) or 8080` | use this value instead (it's only computed on failure; it can be another call with `or`) |
| `port := parse_port(s) or { print(err); return }` | run the block; `err` is the message (a `str`). The block must leave (`return`, `break`, `continue`, `exit`) unless the result isn't used. |
| `port := try parse_port(s)` | fail the current function with the same error (it must be fallible too) |
```c
fn load(str path) -> Config! {
text := try read_file(path)
port := try parse_port(text.trim())
return Config(port: port)
}
fn main() -> ! { // main can fail too: the error is printed, and the exit code is 1
config := try load("app.conf")
...
}
```
Built-ins that can fail work the same way: `int(text)`, `float(text)`, `read_file`, `write_file`,
`delete_file`. Without `or` / `try` they behave as before (`int("x")` stops the program,
`write_file` gives back a `bool`).
Under the hood a fallible function returns its value plus a flag and the message: no allocation on
success, no unwinding, and the cost of a failure is building its message.
### Compile errors ### Compile errors
``` ```
@ -691,11 +854,30 @@ Instead of undefined behavior (as in C), Finch stops with a message and exit cod
| `runtime error: pop() on an empty array` | | | `runtime error: pop() on an empty array` | |
| `runtime error: used .value on a null pointer` | also `.field` and `[i]` through `null` | | `runtime error: used .value on a null pointer` | also `.field` and `[i]` through `null` |
| `runtime error: can't turn "x" into int` | `int(...)` / `float(...)` on text that isn't a number | | `runtime error: can't turn "x" into int` | `int(...)` / `float(...)` on text that isn't a number |
| `runtime error: can't read the file "…"` | `read_file` on a missing or unreadable file | | `runtime error: can't read the file "…"` | `read_file` on a missing or unreadable file (handle it with `or`) |
| `runtime error: the key "x" is not in the map …` | `m["x"]` when the map has no such key (use `.has` or `.get`) |
| `runtime error: called .f() on a null pointer` | a method called through a `null` `ptr[T]` |
--- ---
## 19. Editor: VS Code ## 19. Editors: VS Code, Kate and others
Every editor feature comes from the same language server, `finch lsp`, which uses the real compiler:
errors as you type, types and docs on hover, go to definition, completion (fields, methods, map and array
methods after `.`, module members, names from C headers), the file's outline, and parameter hints.
### Kate (and KWrite, KDevelop)
```sh
editors/kate/install.sh
```
It installs the highlighting (`~/.local/share/org.kde.syntax-highlighting/syntax/finch.xml`) and adds `finch`
to Kate's LSP client settings, keeping your other servers. Restart Kate and enable the **LSP Client** plugin.
To run programs, add `finch run %f` as a target in the **Build & Run** plugin; Finch's
`file:line:column: error:` messages are then clickable. Details: `editors/kate/README.md`.
### VS Code
The **Finch** extension (`editors/vscode`, also attached to each release as a `.vsix` file) gives you: The **Finch** extension (`editors/vscode`, also attached to each release as a `.vsix` file) gives you:
highlighting, errors as you type, completion (also fields and methods after `.`), types on hover, highlighting, errors as you type, completion (also fields and methods after `.`), types on hover,
@ -703,8 +885,12 @@ go to definition (F12), an outline of the file, parameter hints, snippets, and a
(`Ctrl+F5`). Errors from **Finch: Build This File** appear in the Problems panel. (`Ctrl+F5`). Errors from **Finch: Build This File** appear in the Problems panel.
Install: Extensions → `…` → *Install from VSIX…* → pick `finch-lang-*.vsix`. The extension runs `finch` Install: Extensions → `…` → *Install from VSIX…* → pick `finch-lang-*.vsix`. The extension runs `finch`
from your PATH; if it is somewhere else, set **Finch: Path** in the settings. Other editors that speak the from your PATH; if it is somewhere else, set **Finch: Path** in the settings.
Language Server Protocol (Neovim, Helix, Zed, Emacs, Sublime) can use `finch lsp` directly.
### Others
Neovim, Helix, Zed, Emacs, Sublime and any other editor that speaks the Language Server Protocol:
run `finch lsp` for files ending in `.fch` (stdin/stdout, no options).
## 20. Debugging ## 20. Debugging
@ -737,6 +923,9 @@ Visual Studio debugger and WinDbg read.
| A library is not found when the program starts | It was linked from a folder the system doesn't search. Run inside the same `nix-shell`, or install it system-wide. | | A library is not found when the program starts | It was linked from a folder the system doesn't search. Run inside the same `nix-shell`, or install it system-wide. |
| Windows: `finch` can't link (`LNK…` errors about `libcmt`, `kernel32`) | Install Visual Studio Build Tools with *Desktop development with C++*. | | Windows: `finch` can't link (`LNK…` errors about `libcmt`, `kernel32`) | Install Visual Studio Build Tools with *Desktop development with C++*. |
| Windows: `finch.exe` doesn't start (missing `libclang.dll`) | Keep `libclang.dll` next to `finch.exe`. | | Windows: `finch.exe` doesn't start (missing `libclang.dll`) | Keep `libclang.dll` next to `finch.exe`. |
| macOS: `finch` doesn't start (`Library not loaded: …libLLVM…`) | `brew install llvm@21`. |
| macOS: `can't find the C header 'stdio.h'` | Install the command line tools: `xcode-select --install`. |
| `--target arm64`: `no C compiler found to link with` | Install `aarch64-linux-gnu-gcc` (Debian: `gcc-aarch64-linux-gnu`), or set `FINCH_CC`. |
| `clang` inside `nix-shell` can't find `stdio.h` | You have an old `shell.nix`: `llvmPackages.clang` must be listed before `llvmPackages.libclang`. | | `clang` inside `nix-shell` can't find `stdio.h` | You have an old `shell.nix`: `llvmPackages.clang` must be listed before `llvmPackages.libclang`. |
| A program never ends | A loop condition never becomes false. Press Ctrl+C. | | A program never ends | A loop condition never becomes false. Press Ctrl+C. |
@ -755,17 +944,18 @@ Finch/
│ ├── fail/ programs that must fail, with the expected error in line 1 │ ├── fail/ programs that must fail, with the expected error in line 1
│ ├── run.sh the test runner (MEMCHECK=1 also checks memory with valgrind) │ ├── run.sh the test runner (MEMCHECK=1 also checks memory with valgrind)
│ └── boot.sh builds the self-hosted compiler with itself and compares │ └── boot.sh builds the self-hosted compiler with itself and compares
├── editors/vscode/ the VS Code extension ├── editors/ the VS Code extension (vscode/), Kate highlighting and LSP setup (kate/)
├── docs/ this documentation (en, pl), including the cheat sheet ├── docs/ this documentation (en, pl), including the cheat sheet
├── shell.nix the Nix development environment ├── shell.nix the Nix development environment
└── CMakeLists.txt └── CMakeLists.txt
``` ```
```sh ```sh
tests/run.sh # → 59 passed, 0 failed tests/run.sh # → 74 passed, 0 failed
MEMCHECK=1 tests/run.sh # the same under valgrind: no leaks, no bad memory access MEMCHECK=1 tests/run.sh # the same under valgrind: no leaks, no bad memory access
tests/boot.sh # the self-hosting check (needs clang) tests/boot.sh # the self-hosting check (needs clang)
tests/windows.sh # every test built for Windows and run with Wine tests/cross.sh windows # every test built for Windows and run with Wine
tests/cross.sh arm64 # every test built for ARM64 Linux and run with QEMU
python3 tests/lsp_test.py # the language server python3 tests/lsp_test.py # the language server
``` ```
@ -773,12 +963,11 @@ python3 tests/lsp_test.py # the language server
## 23. Current limits ## 23. Current limits
Finch 2.0 is a complete small language, but not a finished one. Not there yet: Not there yet:
- Methods on structs, generics, maps/dictionaries, `match`. (Use arrays of structs and functions.) - Generics (your own `List[T]`), interfaces, `match`, enums written in Finch.
- Error values: errors in `int("x")` or `read_file` stop the program. Check first (`file_exists`).
- Unicode-aware text: `.len`, `s[i]` and `upper()` work on bytes / ASCII. - Unicode-aware text: `.len`, `s[i]` and `upper()` work on bytes / ASCII.
- Threads. - Threads.
- Platforms other than Linux and Windows on x86-64 (macOS, ARM). - Windows on ARM64, 32-bit systems, WebAssembly.
See the [roadmap in the README](../../README.md#roadmap). See the [roadmap in the README](../../README.md#roadmap).

View file

@ -38,9 +38,9 @@ Język z perspektywy użytkownika opisuje [przewodnik dla techników](dla-techni
20. [Semantyka a C](#20-semantyka-a-c) 20. [Semantyka a C](#20-semantyka-a-c)
21. [Spis funkcji](#21-spis-funkcji) 21. [Spis funkcji](#21-spis-funkcji)
22. [Rozbudowa kompilatora](#22-rozbudowa-kompilatora) 22. [Rozbudowa kompilatora](#22-rozbudowa-kompilatora)
23. [Platformy i Windows](#23-platformy-i-windows) 23. [Platformy: Windows, macOS, ARM64](#23-platformy-windows-macos-arm64)
24. [Serwer języka](#24-serwer-języka) 24. [Serwer języka](#24-serwer-języka)
25. [Rozszerzenie VS Code](#25-rozszerzenie-vs-code) 25. [Integracje z edytorami](#25-integracje-z-edytorami)
26. [Znane ograniczenia](#26-znane-ograniczenia) 26. [Znane ograniczenia](#26-znane-ograniczenia)
--- ---
@ -90,20 +90,24 @@ Decyzje, które kształtują resztę:
| Plik | Linie | Odpowiedzialność | | Plik | Linie | Odpowiedzialność |
|---|---|---| |---|---|---|
| `src/error.h` | 40 | `g_files` (wszystkie źródła), `failAt(file, line, col, msg)`, `fail(line, col, msg)` | | `src/error.h` | 50 | `g_files` (wszystkie źródła), `failAt(file, line, col, msg)`, `fail(line, col, msg)`, `FinchError` |
| `src/lexer.h/.cpp` | 250 | `Tok`, `Token`, `lex(indeksPliku)`, `tokName()` | | `src/lexer.h/.cpp` | 250 | `Tok`, `Token`, `lex(indeksPliku)`, `tokName()` |
| `src/ast.h` | 310 | `Type` (z Array, Fixed, Struct, Named), wszystkie węzły `Expr`/`Stmt`, `FnDecl`, `StructDecl`, `Import`, `Link`, `Program` | | `src/ast.h` | 360 | `Type` (z Array, Map, Fixed, Struct, Named), wszystkie węzły `Expr`/`Stmt`, `FnDecl`, `StructDecl`, `Import`, `Link`, `Program` |
| `src/parser.h/.cpp` | 540 | Parser zstępujący `Parser`, `typeFromName()` | | `src/parser.h/.cpp` | 610 | Parser zstępujący `Parser`, `typeFromName()` |
| `src/cimport.h/.cpp` | 420 | Import przez libclang: funkcje, struktury C (pola, przesunięcia), stałe, zmienne globalne | | `src/loader.h/.cpp` | 90 | Wczytywanie plików i modułów (`Loader`), `FINCH_PATH` |
| `src/target.h/.cpp` | 210 | `g_target`: triple, kompilator C, procesy, ścieżki; inicjalizacja backendów |
| `src/cimport.h/.cpp` | 430 | Import przez libclang: funkcje, struktury C (pola, przesunięcia), stałe, zmienne globalne |
| `src/codegen.h` | 15 | `generate()` | | `src/codegen.h` | 15 | `generate()` |
| `src/codegen_impl.h` | 265 | Klasa `Codegen` i jej struktury pomocnicze, wspólne dla czterech plików poniżej | | `src/codegen_impl.h` | 310 | Klasa `Codegen` i jej struktury pomocnicze, wspólne dla plików poniżej |
| `src/codegen.cpp` | 1090 | Program, deklaracje, zasięgi i sprzątanie, instrukcje, wyrażenia, miejsca (`ref`), operatory, analiza modyfikacji | | `src/codegen.cpp` | 1460 | Program, deklaracje (funkcje, metody), zasięgi i sprzątanie, instrukcje, wyrażenia, miejsca (`ref`), operatory, błędy jako wartości, analiza modyfikacji |
| `src/codegen_types.cpp` | 500 | Typy LLVM, rozwiązywanie typów, układ struktur, konwersje, własność (helpery kopiowania/zwalniania), wypisywanie, typy DWARF | | `src/codegen_types.cpp` | 580 | Typy LLVM, rozwiązywanie typów, układ struktur, konwersje, własność (helpery kopiowania/zwalniania), zamiana na tekst, typy dla debuggera |
| `src/codegen_builtins.cpp` | 710 | Wywołania, konstruktory, funkcje wbudowane, konwersje, metody tablic i tekstów, deklaracje runtime'u, panic | | `src/codegen_builtins.cpp` | 890 | Wywołania, metody, konstruktory, funkcje wbudowane, konwersje, metody tablic i tekstów, deklaracje runtime'u, panic |
| `src/abi.cpp` | 310 | Klasyfikacja System V x86-64, wywołania C ze strukturami przez wartość, wrappery wywoływalne z C | | `src/codegen_map.cpp` | 240 | Mapy: indeksowanie, literały, metody, przechodzenie |
| `src/main.cpp` | 390 | Sterownik: CLI, ładowanie, maszyna docelowa, O2, emisja, cache runtime'u, linkowanie, porady przy błędach linkera, `run` | | `src/abi.cpp` | 370 | Konwencje wywołań C (System V x86-64, Microsoft x64, AAPCS64), wywołania C ze strukturami przez wartość, wrappery wywoływalne z C |
| `runtime/finch_rt.c` | 440 | Biblioteka uruchomieniowa (§12) | | `src/lsp.cpp`, `src/index.h`, `src/builtins_doc.h` | 700 | Serwer języka (§24) |
| `boot/*.fch` | 3080 | Kompilator samohostujący (§17) | | `src/main.cpp` | 350 | Sterownik: CLI, maszyna docelowa, O2, emisja, cache runtime'u, linkowanie, porady przy błędach linkera, `run` |
| `runtime/finch_rt.c` | 740 | Biblioteka uruchomieniowa (§12) |
| `boot/*.fch` | 3100 | Kompilator samohostujący (§17) |
--- ---
@ -120,7 +124,8 @@ struct Token { Tok kind; std::string text; int line, col; bool newlineBefore; };
- **Pozycje:** od 1; `col` liczy znaki (bajty kontynuacji UTF-8 jej nie zwiększają), więc `^` trafia - **Pozycje:** od 1; `col` liczy znaki (bajty kontynuacji UTF-8 jej nie zwiększają), więc `^` trafia
pod `"Błąd"`. Zabłąkany znak spoza ASCII jest pokazywany w całości z podpowiedzią, że nazwy są tylko ASCII. pod `"Błąd"`. Zabłąkany znak spoza ASCII jest pokazywany w całości z podpowiedzią, że nazwy są tylko ASCII.
- **Identyfikatory / słowa kluczowe:** `[A-Za-z_][A-Za-z0-9_]*`. Słowa kluczowe: - **Identyfikatory / słowa kluczowe:** `[A-Za-z_][A-Za-z0-9_]*`. Słowa kluczowe:
`fn return if else while for in break continue true false null import link struct defer`. `fn return if else while for in break continue true false null import link struct defer or try`.
(`self` i `map` to zwykłe identyfikatory ze znaczeniem zależnym od miejsca: w metodzie, przed `[`.)
**Nazwy typów nie są słowami kluczowymi**: `int`, `u8`, `ptr`… to `Ident`, rozpoznawane przez parser **Nazwy typów nie są słowami kluczowymi**: `int`, `u8`, `ptr`… to `Ident`, rozpoznawane przez parser
za pomocą `typeFromName()`, więc `u8(x)` jest zwykłym wywołaniem, a lekser nie zależy od listy typów. za pomocą `typeFromName()`, więc `u8(x)` jest zwykłym wywołaniem, a lekser nie zależy od listy typów.
- **Liczby:** dziesiętne albo `0x` szesnastkowe z separatorami `_`. Bez ósemkowych (`010` to 10). - **Liczby:** dziesiętne albo `0x` szesnastkowe z separatorami `_`. Bez ósemkowych (`010` to 10).
@ -142,27 +147,31 @@ program = { import | link | struct | function } ;
import = "import" ( STRING | IDENT ) ; (* "x.h" = nagłówek C, nazwa = moduł Fincha *) import = "import" ( STRING | IDENT ) ; (* "x.h" = nagłówek C, nazwa = moduł Fincha *)
link = "link" STRING ; (* "glfw" | "plik.c" | "plik.o" | "lib.a" *) link = "link" STRING ; (* "glfw" | "plik.c" | "plik.o" | "lib.a" *)
struct = "struct" IDENT "{" { type IDENT [ "=" expr ] NOWA_LINIA } "}" ; struct = "struct" IDENT "{" { type IDENT [ "=" expr ] NOWA_LINIA } "}" ;
function = "fn" IDENT "(" [ param { "," param } ] ")" [ "->" type ] block ; function = "fn" IDENT [ "." IDENT ] "(" [ param { "," param } ] ")" [ "->" ( type [ "!" ] | "!" ) ] block ;
(* fn Punkt.przesun: metoda; "!": może się nie udać *)
param = type IDENT ; param = type IDENT ;
type = "[" "]" type | "ptr" [ "[" type "]" ] | TYPENAME | IDENT [ "." IDENT ] ; type = "[" "]" type | "map" "[" type "]" type | "ptr" [ "[" type "]" ]
| TYPENAME | IDENT [ "." IDENT ] ;
block = "{" { statement TERMINATOR } "}" ; block = "{" { statement TERMINATOR } "}" ;
statement = if | while | for | return | "break" | "continue" | "defer" statement | block statement = if | while | for | return | "break" | "continue" | "defer" statement | block
| type IDENT [ "=" expr ] (* deklaracja, zobacz atDeclaration *) | type IDENT [ "=" expr ] (* deklaracja, zobacz atDeclaration *)
| IDENT ":=" expr | IDENT ":=" expr
| target assignop expr (* cel: zmienna, pole, element, p.value *) | target assignop expr (* cel: zmienna, pole, element, p.value *)
| call | method ; | call | method | try | orelse ;
for = "for" IDENT "in" expr ( ".." expr block | block ) ; (* zakres | po elementach *) for = "for" IDENT [ "," IDENT ] "in" expr ( ".." expr block | block ) ; (* zakres | po elementach *)
assignop = "=" | "+=" | "-=" | "*=" | "/=" | "%=" ; assignop = "=" | "+=" | "-=" | "*=" | "/=" | "%=" ;
expr = or ; or = and { "||" and } ; and = cmp { "&&" cmp } ; expr = or [ "or" ( block | expr ) ] ; (* f(x) or 0, f(x) or { ... }: najniższy, od prawej *)
or = and { "||" and } ; and = cmp { "&&" cmp } ;
cmp = add { ( "==" | "!=" | "<" | "<=" | ">" | ">=" ) add } ; cmp = add { ( "==" | "!=" | "<" | "<=" | ">" | ">=" ) add } ;
add = mul { ( "+" | "-" | "|" | "^" ) mul } ; add = mul { ( "+" | "-" | "|" | "^" ) mul } ;
mul = unary { ( "*" | "/" | "%" | "<<" | ">>" | "&" ) unary } ; mul = unary { ( "*" | "/" | "%" | "<<" | ">>" | "&" ) unary } ;
unary = ( "-" | "!" | "~" ) unary | postfix ; unary = ( "-" | "!" | "~" ) unary | "try" postfix | postfix ;
postfix = primary { "." IDENT [ args ] | "[" expr "]" } ; postfix = primary { "." IDENT [ args ] | "[" expr "]" } ;
primary = INT | FLOAT | STRING | CHAR | "true" | "false" | "null" primary = INT | FLOAT | STRING | CHAR | "true" | "false" | "null"
| IDENT [ args ] | "(" expr ")" | "[" [ expr { "," expr } [","] ] "]" ; | IDENT [ args ] | "(" expr ")" | "[" [ expr { "," expr } [","] ] "]"
| "[" ":" "]" | "[" expr ":" expr { "," expr ":" expr } [","] "]" ; (* mapy *)
args = "(" [ arg { "," arg } [","] ] ")" ; arg = [ IDENT ":" ] expr ; args = "(" [ arg { "," arg } [","] ] ")" ; arg = [ IDENT ":" ] expr ;
``` ```
@ -202,11 +211,16 @@ Wszystkie węzły są w `src/ast.h`: małe hierarchie klas z jawnym `kind`, rozg
| `Member` | `MemberExpr` | `obj`, `field` (`.x`, `.len`, `.ptr`, `.value`) | | `Member` | `MemberExpr` | `obj`, `field` (`.x`, `.len`, `.ptr`, `.value`) |
| `Index` | `IndexExpr` | `obj`, `index` | | `Index` | `IndexExpr` | `obj`, `index` |
| `ArrayLit` | `ArrayLitExpr` | `elems` | | `ArrayLit` | `ArrayLitExpr` | `elems` |
| `Method` | `MethodExpr` | `obj`, `name`, `args`, `argNames`; także `modul.fch(...)` | | `Method` | `MethodExpr` | `obj`, `name`, `args`, `argNames`; także `modul.fn(...)` i `p.metoda(...)` |
| `MapLit` | `MapLitExpr` | `keys`, `values` (`[:]` nie ma żadnych) |
| `Try` | `TryExpr` | `call` |
| `OrElse` | `OrElseExpr` | `call` oraz `fallback` (wyrażenie) albo `block` |
| StmtKind | Węzeł | | StmtKind | Węzeł |
|---|---| |---|---|
| `Block` `VarDecl` `Assign` `Expr` `If` `While` `For` `ForEach` `Return` `Break` `Continue` `Defer` | jak w nazwie; `ForEachStmt` ma `var`, `list`, `body`; `DeferStmt` trzyma jedną instrukcję | | `Block` `VarDecl` `Assign` `Expr` `If` `While` `For` `ForEach` `Return` `Break` `Continue` `Defer` | jak w nazwie; `ForEachStmt` ma `var`, `var2` (`for i, x` / `for k, v`), `list`, `body`; `DeferStmt` trzyma jedną instrukcję |
`FnDecl` ma `recv` (struktura metody, w innych przypadkach `""`) i `fallible` (`-> T!`).
`math.sqrt(x)` parsuje się jako `MethodExpr` na `VarExpr("math")`; to, czy `math` jest modułem, czy zmienną, `math.sqrt(x)` parsuje się jako `MethodExpr` na `VarExpr("math")`; to, czy `math` jest modułem, czy zmienną,
rozstrzyga się przy generowaniu kodu. Wywołania nazywają funkcję, zamiast trzymać wyrażenie: Finch nie ma rozstrzyga się przy generowaniu kodu. Wywołania nazywają funkcję, zamiast trzymać wyrażenie: Finch nie ma
@ -220,9 +234,10 @@ Nie ma węzła „load”: to, czy `x` znaczy adres, czy wartość, zależy od k
```cpp ```cpp
struct Type { struct Type {
enum Kind { Void, Bool, Char, I8, I16, I32, I64, U8, U16, U32, U64, F32, F64, enum Kind { Void, Bool, Char, I8, I16, I32, I64, U8, U16, U32, U64, F32, F64,
Str, Ptr, Null, Array, Fixed, Struct, Named }; Str, Ptr, Null, Array, Map, Fixed, Struct, Named };
Kind kind; Kind kind;
std::shared_ptr<Type> elem; // Ptr (null = `ptr` bez typu), Array, Fixed std::shared_ptr<Type> elem; // Ptr (null = `ptr` bez typu), Array, Fixed; Map: wartość
std::shared_ptr<Type> key; // Map
long long count; // Fixed long long count; // Fixed
std::string name, module; // Named (jak napisano) / Struct (rozwiązany) std::string name, module; // Named (jak napisano) / Struct (rozwiązany)
StructInfo *info; // Struct StructInfo *info; // Struct
@ -302,9 +317,12 @@ nazwy typów (konwersja) → funkcja bieżącego modułu → struktura bieżące
| `char` `i8` `u8` / `i16` `u16` / `i32` `u32` / `int` `u64` | `i8` / `i16` / `i32` / `i64` | | `char` `i8` `u8` / `i16` `u16` / `i32` `u32` / `int` `u64` | `i8` / `i16` / `i32` / `i64` |
| `f32` / `float` | `float` / `double` | | `f32` / `float` | `float` / `double` |
| `str`, `[]T` | `{ ptr, i64, i64 }` (wskaźnik, długość, pojemność) | | `str`, `[]T` | `{ ptr, i64, i64 }` (wskaźnik, długość, pojemność) |
| `map[K]V` | `{ ptr, i64, i64, i64, ptr, i64 }` (entries, len, used, cap, index, icap; §10.12) |
| `ptr`, `ptr[T]`, `null` | `ptr` (nieprzezroczysty) | | `ptr`, `ptr[T]`, `null` | `ptr` (nieprzezroczysty) |
| `[N]T` | `[N x T]` | | `[N]T` | `[N x T]` |
| struktura Fincha | nazwany typ struct, naturalny układ | | struktura Fincha | nazwany typ struct, naturalny układ |
| `self` metody | `ptr` (pierwszy parametr) |
| wynik `-> T!` | `{ i1 ok, T value, str error }` (`i8` w miejscu `T` dla `-> !`) |
| struktura C | nazwany **upakowany** (packed) struct z jawnymi bajtami wypełnienia (§13.2) | | struktura C | nazwany **upakowany** (packed) struct z jawnymi bajtami wypełnienia (§13.2) |
Znakowość istnieje tylko w typie Fincha i wybiera `sdiv/udiv`, `ashr/lshr`, `icmp s*/u*`, Znakowość istnieje tylko w typie Fincha i wybiera `sdiv/udiv`, `ashr/lshr`, `icmp s*/u*`,
@ -380,6 +398,8 @@ każda decyzja zapada statycznie.
| wartość tymczasowa, której nikt nie trzyma (`print(a + b)`, `f(g())`, instrukcja-wyrażenie) | **`release(v)`**: zwalniana zaraz po użyciu | | wartość tymczasowa, której nikt nie trzyma (`print(a + b)`, `f(g())`, instrukcja-wyrażenie) | **`release(v)`**: zwalniana zaraz po użyciu |
| `a.pop()`, `a.remove(i)` | element jest przenoszony na zewnątrz (fresh) | | `a.pop()`, `a.remove(i)` | element jest przenoszony na zewnątrz (fresh) |
| `for x in lista` | `x` pożycza kolejne elementy; jeśli ciało może zmieniać `lista`, elementy są kopiowane | | `for x in lista` | `x` pożycza kolejne elementy; jeśli ciało może zmieniać `lista`, elementy są kopiowane |
| wywołanie metody `v.m()` | `self` **to jest** `v` (przekazywany jest jego adres): bez kopii; zmiany widzi wywołujący |
| mapy | są właścicielami kluczy i wartości tak jak tablice elementów; `m[k] = v` przenosi/kopiuje `v` i zwalnia starą wartość |
Literał tekstowy to wartość fresh z `cap = 0`: przeniesienie nic nie kosztuje, zwalnianie jest pomijane Literał tekstowy to wartość fresh z `cap = 0`: przeniesienie nic nie kosztuje, zwalnianie jest pomijane
(`release()` ignoruje stałe), a zapis do niego (`s[i] = c`) najpierw robi kopię na stercie (`finch_str_own`). (`release()` ignoruje stałe), a zapis do niego (`s[i] = c`) najpierw robi kopię na stercie (`finch_str_own`).
@ -394,6 +414,11 @@ parametry są pożyczane (`Fn::borrowParam`), zmieniane kopiowane raz na wejści
i zachowawcza: wszystko, co *może* zmienić parametr, powoduje kopię, co zawsze jest poprawne. i zachowawcza: wszystko, co *może* zmienić parametr, powoduje kopię, co zawsze jest poprawne.
Ta sama analiza decyduje, czy `for x in lista` musi kopiować elementy. Ta sama analiza decyduje, czy `for x in lista` musi kopiować elementy.
Metody biorą w tym udział: `declareFns()` najpierw ustala, które metody zmieniają `self`
(`mutates("self", ciało)`), powtarzając, aż nic się nie zmieni, bo wywołanie zmieniającej metody na `self`
też zmienia `self`. Ich nazwy trafiają do `mutatingMethods`, które `mutatesExpr()` traktuje jak `push`:
`p.przesun(1)` zmienia `p`. Dopiero potem liczone jest `borrowParam` dla każdej funkcji i metody.
### 9.3 Kopiowanie i zwalnianie ### 9.3 Kopiowanie i zwalnianie
`dropAt(addr, T)` / `copyAt(dst, src, T)` działają na adresach: `dropAt(addr, T)` / `copyAt(dst, src, T)` działają na adresach:
@ -513,7 +538,10 @@ wartości (dla `str +=`).
### 10.4 Funkcje i `main` ### 10.4 Funkcje i `main`
Funkcje są `internal` z naturalną sygnaturą LLVM (agregaty przez wartość). Parametry posiadające Funkcje są `internal` z naturalną sygnaturą LLVM (agregaty przez wartość). Parametry posiadające
pamięć są kopiowane na wejściu tylko wtedy, gdy `borrowParam` jest fałszywe. `fn main` to wewnętrzna pamięć są kopiowane na wejściu tylko wtedy, gdy `borrowParam` jest fałszywe. Metody (§10.10) dostają adres
`self` jako dodatkowy pierwszy parametr; funkcje, które mogą się nie udać (§10.11), zwracają `{ i1, T, str }`.
`fn main() -> !` / `-> int!` sprawia, że wrapper przy porażce wypisuje `error: <komunikat>` na stderr
i kończy z kodem 1. `fn main` to wewnętrzna
`finch.main`; osobna zewnętrzna `i32 main(i32, ptr)` (`defineMainWrapper`) buduje `[]str args` przez `finch.main`; osobna zewnętrzna `i32 main(i32, ptr)` (`defineMainWrapper`) buduje `[]str args` przez
`finch_args`, jeśli trzeba, woła ją, zwalnia argumenty i zwraca kod wyjścia. Funkcja, która dochodzi do `finch_args`, jeśli trzeba, woła ją, zwalnia argumenty i zwraca kod wyjścia. Funkcja, która dochodzi do
swojego końca, dostaje sprzątanie zasięgów i `ret void` albo błąd `can reach its end without returning T`. swojego końca, dostaje sprzątanie zasięgów i `ret void` albo błąd `can reach its end without returning T`.
@ -543,10 +571,13 @@ zasięgami wywołującego, albo zero). Wywołania C opisuje §13.
### 10.8 Wypisywanie ### 10.8 Wypisywanie
`print` emituje `printf` dla każdego argumentu z formatem wybranym wg typu (`%lld`, `%llu`, `%g`, `%c`, `print` emituje `printf` dla każdego argumentu skalarnego z formatem wybranym wg typu (`%lld`, `%llu`,
`%.*s` dla `str` z jego długością, `%p`), oddzielając spacjami, a na końcu `\n`. Tablice, tablice o stałym `%g`, `%c`, `%.*s` dla `str` z jego długością), oddzielając spacjami, a na końcu `\n`. Wszystko inne
rozmiarze i struktury wołają generowane helpery `finch.print.<klucz>`, które wypisują `[1, 2]` / (wskaźniki, tablice, tablice o stałym rozmiarze, mapy, struktury) jest najpierw **zamieniane na tekst**
`Punkt(x: 1, y: 2)`, biorąc teksty i znaki w środku w cudzysłów. w buforze `str` na stercie przez `emitFormat()` i wypisywane przez `finch_buf_print`. Typy złożone dostają
generowany helper `finch.format.<klucz>(buf, wartość*)`, który dopisuje `[1, 2]` / `{"a": 1}` /
`Punkt(x: 1, y: 2)`, biorąc teksty i znaki w środku w cudzysłów. `str(x)` tablicy, mapy albo struktury
używa tego samego helpera, więc `str(x)` to zawsze to, co pokazuje `print(x)`.
### 10.9 Metody tablic i tekstów ### 10.9 Metody tablic i tekstów
@ -556,6 +587,66 @@ Tablice: `push` (`finch_arr_reserve` + zapis + len++), `pop`/`remove` (przeniesi
`reverse` (pętla zamian), `sort` (`qsort` z libc z generowaną porównywarką `finch.cmp.<klucz>`), `join` (runtime). `reverse` (pętla zamian), `sort` (`qsort` z libc z generowaną porównywarką `finch.cmp.<klucz>`), `join` (runtime).
Metody zmieniające tablicę wymagają adresu (zmienna/pole/element). Metody zmieniające tablicę wymagają adresu (zmienna/pole/element).
Teksty: `sub find contains starts_with ends_with split trim upper lower replace repeat bytes`, wszystkie w runtime. Teksty: `sub find contains starts_with ends_with split trim upper lower replace repeat bytes`, wszystkie w runtime.
`for i, x in lista` dodaje obok elementu zmienną-indeks typu `int` tylko do odczytu.
### 10.10 Metody
`fn Punkt.przesun(int dx)` jest deklarowana jak funkcja `finch.[modul.]Punkt.przesun` z dodatkowym
pierwszym parametrem `ptr` i trzymana w `Codegen::methods[StructInfo*]` (więc metody idą za strukturą do
każdego modułu, który jej używa, a struktury z C też mogą mieć metody). W `define()` `self` to `Var`, którego
`slot` **jest tym argumentem-wskaźnikiem**: `self.x` to GEP od struktury samego wywołującego, a `self = …`
zapisuje wartość wywołującego. Nigdy nie ma kopii.
Przy wywołaniu `obj.m(args)` (`structMethod()`) `ref(obj)` daje adres, gdy `obj` jest miejscem (zmienna,
pole, element, `m[klucz]`), wczytany wskaźnik dla `ptr[T]` (sprawdzany na null) albo tymczasowe miejsce na
stosie dla wartości (`Punkt(1, 2).dlugosc()`), zwalniane po wywołaniu. Z analizy modyfikacji wynikają dwie
reguły bezpieczeństwa: metody zmieniającej `self` nie można wywołać na zmiennej pętli (tylko do odczytu),
a jeśli taka metoda dostaje argument żyjący wewnątrz odbiorcy (`c.merge(c)`, `p.add(p.items)`), argument
jest najpierw kopiowany, żeby `self` nie mógł zwolnić pamięci, na którą argument wciąż wskazuje.
### 10.11 Błędy jako wartości
Funkcja, która może się nie udać (`-> T!`), zwraca `{ i1 ok, T value, str error }`; przy porażce `value`
jest zerem, więc wywołujący nigdy nie musi go zwalniać. `return v` opakowuje `{1, v, zero}` (`retValue()`);
`return error(msg)` liczy komunikat, wykonuje sprzątanie i zwraca `{0, zero, msg}` (`retError()`).
Wywołanie takiej funkcji jest dozwolone tylko jako argument `or` / `try`. `fallible(wywołanie)` ustawia
`fallibleTarget` na wyrażenie wywołania; `callFinch()` (i funkcje wbudowane, które mogą się nie udać)
porównują z nim swoje `CallExpr`/`MethodExpr` i jeśli pasuje, zostawiają `fallibleFailed` (`i1`)
i `fallibleMsg` (`str`) dla wywołującego; jeśli wywołanie takiej funkcji nie pasuje, to błąd kompilacji
*'f' can fail, so say what happens then*. Zagnieżdżone obsługi zapisują i przywracają te trzy pola.
| Forma | Tłumaczenie |
|---|---|
| `f(x) or zapasowa` | `br failed, or.failed, or.ok`; blok porażki zwalnia komunikat i liczy wartość zapasową (tylko tam); φ łączy obie wartości |
| `f(x) or { … }` | blok porażki deklaruje `err` (właściciel komunikatu) i wykonuje blok. Jeśli wartość jest używana, blok musi kończyć się skokiem; jako instrukcja może przejść dalej, a ścieżka sukcesu zwalnia nieużytą wartość |
| `try f(x)` | blok porażki wykonuje `emitCleanups(0)` i zwraca `{0, zero, msg}` z bieżącej funkcji, która też musi móc się nie udać |
Funkcje wbudowane z trybem porażki sprawdzają `&wywołanie == fallibleTarget` i przełączają się na
`finch_str_try_int`, `finch_str_try_float`, `finch_try_read_file`, `finch_try_write_file`,
`finch_try_delete_file`, które zwracają 1 / 0 i wypełniają `str` z błędem zamiast robić panic. Koszt przy
sukcesie to jedna przewidywalna gałąź; nic nie jest alokowane, dopóki nie zdarzy się porażka.
### 10.12 Mapy
Runtime (§12) trzyma tablicę haszującą zachowującą kolejność wstawiania, w stylu `dict` z CPythona:
tablica `entries` z miejscami `{ i64 hash, K key, V value }` w kolejności wstawiania (hash 0 = usunięte)
oraz `index` z `icap` miejscami (potęga dwójki, ≥ 2 × cap) z sondowaniem liniowym (−1 puste, −2 usunięte,
w innym razie numer wpisu). Runtime tylko haszuje (FNV-1a + 64-bitowe mieszanie; klucze tekstowe po treści,
pozostałe po bajtach), znajduje i umieszcza wpisy; kompilator zapisuje, kopiuje i zwalnia klucze i wartości,
dokładnie tak jak elementy tablic.
| Kod | Tłumaczenie |
|---|---|
| odczyt `m[k]` | `finch_map_find`; −1 → `finch_map_missing` (noreturn, z kluczem jako tekst); w innym razie miejsce `entry.value` |
| zapis `m[k]` (`=`, `+=`, metoda zmieniająca, `addr`) | `mapSlot()`: `finch_map_slot` zwraca wpis, dodając go, jeśli go nie ma; nowy wpis dostaje kopię klucza i wartość domyślną typu |
| `["a": 1, …]` | wyzerowana mapa + `mapSlot` dla każdego wpisu (pierwszy wpis ustala typy; liczbowe wartości są wszystkie `float`, jeśli któraś ma kropkę) |
| `has`, `get`, `remove`, `clear`, `keys`, `values` | `codegen_map.cpp`; wartość domyślna `get` jest liczona tylko wtedy, gdy klucza brak |
| `for k, v in m` | pętla po `0..used` pomijająca usunięte miejsca, wczytująca `entries` i `used` w każdym obrocie |
| kopia / zwolnienie / tekst | helpery jak dla tablic: `finch_map_clone_raw`, a potem głębokie kopie posiadających kluczy i wartości; zwolnienie każdego żywego wpisu, potem `finch_map_free` |
Usunięcie oznacza miejsce (i wpis w indeksie) jako usunięte; miejsca są upychane, gdy tablica miałaby
urosnąć, a co najmniej ćwierć z nich jest usunięta. Usunięcie ostatniego klucza resetuje tablicę.
--- ---
@ -573,7 +664,9 @@ udowodnić (stałe indeksy w zakresie, kontrole wyciągnięte z pętli).
| indeks tablicy/tekstu/tablicy stałej poza zakresem (`icmp uge`, więc także ujemne) | `boundsCheck()` | | indeks tablicy/tekstu/tablicy stałej poza zakresem (`icmp uge`, więc także ujemne) | `boundsCheck()` |
| `.value`, `.pole`, `[i]` przez null | `member()`, `index()` | | `.value`, `.pole`, `[i]` przez null | `member()`, `index()` |
| `pop()` na pustej, `slice()`/`sub()` poza zakresem | metody, runtime | | `pop()` na pustej, `slice()`/`sub()` poza zakresem | metody, runtime |
| zły tekst w `int(s)`/`float(s)`, nieczytelny plik w `read_file` | runtime | | zły tekst w `int(s)`/`float(s)`, nieczytelny plik w `read_file` (bez `or` / `try`) | runtime |
| odczyt `m[klucz]` z brakującym kluczem | `mapIndex()` |
| metoda wywołana przez `null` w `ptr[T]` | `structMethod()` |
Sito na 20 milionów elementów z kontrolą zakresu przy każdym dostępie działa tak szybko jak to samo w C Sito na 20 milionów elementów z kontrolą zakresu przy każdym dostępie działa tak szybko jak to samo w C
(ok. 0,09 s): kontrole są wyciągane z pętli albo zwijane. (ok. 0,09 s): kontrole są wyciągane z pętli albo zwijane.
@ -599,8 +692,11 @@ Funkcje przyjmują i zwracają te struktury **przez wskaźnik**, więc ich ABI j
struktur): budowanie tekstów (`concat`, `from_int/uint/float/char/bool`, `sub`, `trim`, `upper`, `lower`, struktur): budowanie tekstów (`concat`, `from_int/uint/float/char/bool`, `sub`, `trim`, `upper`, `lower`,
`replace`, `split`, `join`, `repeat`, `from_bytes`), porównania (`eq`, `cmp`, `find`, `starts`, `ends`), `replace`, `split`, `join`, `repeat`, `from_bytes`), porównania (`eq`, `cmp`, `find`, `starts`, `ends`),
własność (`copy`, `own`, `drop`, `from_c`), tablice (`reserve`, `make`, `resize`, `clone_raw`, `free`, własność (`copy`, `own`, `drop`, `from_c`), tablice (`reserve`, `make`, `resize`, `clone_raw`, `free`,
`insert_gap`, `remove_gap`), `args`, `input`, pliki, `shell` i panic. Brak pamięci wypisuje `insert_gap`, `remove_gap`), mapy (`map_find`, `map_slot`, `map_remove_at`, `map_clear`, `map_free`,
`out of memory` i kończy proces. `map_clone_raw`, `map_missing`; §10.12), bufory tekstu dla `print`/`str` (`buf_add`, `buf_int`, …,
`buf_print`), warianty mogące się nie udać (`str_try_int`, `str_try_float`, `try_read_file`,
`try_write_file`, `try_delete_file`, `report_error`), `args`, `input`, pliki, `shell` i panic. Brak pamięci
wypisuje `out of memory` i kończy proces.
--- ---
@ -759,7 +855,7 @@ wynik z oczekiwanym. Kompilacja samego siebie zajmuje kompilatorowi samohostują
- libclang: `find_path(clang-c/Index.h)` + `find_library(clang)`. - libclang: `find_path(clang-c/Index.h)` + `find_library(clang)`.
- `runtime/finch_rt.c` jest wczytywany przy konfiguracji do `build/rt_source.inc` jako surowy literał - `runtime/finch_rt.c` jest wczytywany przy konfiguracji do `build/rt_source.inc` jako surowy literał
napisowy; `CMAKE_CONFIGURE_DEPENDS` ponawia konfigurację, gdy plik się zmieni. napisowy; `CMAKE_CONFIGURE_DEPENDS` ponawia konfigurację, gdy plik się zmieni.
- `FINCH_VERSION` z `project(VERSION 2.3.0)`. C++17, `-Wall -Wextra` (MSVC: `/W3`), bez ostrzeżeń. - `FINCH_VERSION` z `project(VERSION 2.4.0)`. C++17, `-Wall -Wextra` (MSVC: `/W3`), bez ostrzeżeń.
- `shell.nix` wymienia `llvmPackages.clang` przed `llvmPackages.libclang`: ten drugi dostarcza też - `shell.nix` wymienia `llvmPackages.clang` przed `llvmPackages.libclang`: ten drugi dostarcza też
„gołego” `clang`, który nie widzi nagłówków systemowych. „gołego” `clang`, który nie widzi nagłówków systemowych.
@ -769,9 +865,12 @@ wynik z oczekiwanym. Kompilacja samego siebie zajmuje kompilatorowi samohostują
- `tests/run.sh`: każdy `tests/run/*.fch` z `fn main` musi wypisać dokładnie swój `.out` (stdin z `.in`, jeśli - `tests/run.sh`: każdy `tests/run/*.fch` z `fn main` musi wypisać dokładnie swój `.out` (stdin z `.in`, jeśli
jest); pliki bez `main` to moduły albo pliki pomocnicze. Każdy `tests/fail/*.fch` musi się nie udać z tekstem jest); pliki bez `main` to moduły albo pliki pomocnicze. Każdy `tests/fail/*.fch` musi się nie udać z tekstem
z linijki `// expect:`. Obecnie **59 passed, 0 failed**. z linijki `// expect:`. Obecnie **74 passed, 0 failed**.
- `MEMCHECK=1 tests/run.sh`: to samo plus valgrind na każdym programie (bez wycieków i złych dostępów). - `MEMCHECK=1 tests/run.sh`: to samo plus valgrind na każdym programie (bez wycieków i złych dostępów).
- `tests/boot.sh`: punkt stały samohostowania i przebieg na podzbiorze (§17). - `tests/boot.sh`: punkt stały samohostowania i przebieg na podzbiorze (§17).
- `tests/cross.sh windows|arm64`: każdy test zbudowany z `--target` i uruchomiony w Wine / QEMU.
- `python3 tests/lsp_test.py`: serwer języka, sterowany jak przez edytor.
- CI (`.github/workflows/ci.yml`) uruchamia to wszystko na Linuksie x86-64, Linuksie ARM64, macOS ARM64 i Windowsie.
--- ---
@ -860,11 +959,14 @@ wynik z oczekiwanym. Kompilacja samego siebie zajmuje kompilatorowi samohostują
| Funkcja | Robi | | Funkcja | Robi |
|---|---| |---|---|
| `call` / `method` | rozwiązuje wywołanie / metodę albo wywołanie z modułu | | `call` / `method` | rozwiązuje wywołanie / metodę albo wywołanie z modułu |
| `callFinch` / `construct` / `callC` | funkcja Fincha / konstruktor struktury / funkcja C | | `callFinch` / `construct` / `callC` | funkcja lub metoda Fincha / konstruktor struktury / funkcja C |
| `structMethod` | `obj.m(...)`: adres odbiorcy, kontrole bezpieczeństwa, wywołanie |
| `print`, `addrOf`, `convert`, `arrayMethod` | funkcje wbudowane | | `print`, `addrOf`, `convert`, `arrayMethod` | funkcje wbudowane |
| `fallible`, `tryExpr`, `orElse`, `retValue`, `retError` (`codegen.cpp`) | błędy jako wartości (§10.11) |
| `mapIndex`, `mapLit`, `mapMethod`, `mapSlot`, `forEachEntry` (`codegen_map.cpp`) | mapy (§10.12) |
| `rt` / `libc` | deklaruje funkcję runtime'u / libc | | `rt` / `libc` | deklaruje funkcję runtime'u / libc |
| `fileName`, `panicIf`, `boundsCheck`, `checkDivisor` | kontrole w czasie działania | | `fileName`, `panicIf`, `boundsCheck`, `checkDivisor` | kontrole w czasie działania |
| `classify`, `makePlan`, `declareC`, `emitCCall`, `cThunk` | klasyfikacja System V, plan rejestrów, deklaracja, wywołanie, wrapper dla callbacków | | `classify`, `makePlan`, `declareC`, `emitCCall`, `cThunk` | klasyfikacja ABI (System V, Microsoft x64, AAPCS64), plan rejestrów, deklaracja, wywołanie, wrapper dla callbacków |
--- ---
@ -889,12 +991,16 @@ przed wyskokiem z zasięgów.
--- ---
## 23. Platformy i Windows ## 23. Platformy: Windows, macOS, ARM64
`src/target.h` trzyma `g_target`: triple LLVM, flagi `windows` / `msvc` / `cross`, kompilator C (`cc`) `src/target.h` trzyma `g_target`: triple LLVM, flagi `windows` / `msvc` / `darwin` / `cross`, kompilator C
i rozszerzenie plików wykonywalnych. `setTarget()` wybiera kompilator C: `FINCH_CC`, potem `CC` (nie przy (`cc`) i rozszerzenie plików wykonywalnych. `setTarget()` zamienia `windows`, `linux`, `arm64`, `macos`
kompilacji krzyżowej), potem `x86_64-w64-mingw32-gcc` dla `--target windows` z Linuksa, `clang` na (albo pełny triple) na triple i wybiera kompilator C: `FINCH_CC`, potem `CC` (nie przy kompilacji
Windowsie, `cc` na Uniksie. Przez tę warstwę idzie wszystko, co zależy od platformy: krzyżowej), potem `x86_64-w64-mingw32-gcc` / `aarch64-linux-gnu-gcc` dla Windowsa / Linuksa ARM64 z innego
systemu, `clang --target=<triple>` dla pozostałych celów krzyżowych, `clang` na Windowsie, `cc` na Uniksie.
`initTargets()` rejestruje backendy X86 i AArch64 (te, które ma dany LLVM; CMake definiuje
`FINCH_TARGET_X86` / `FINCH_TARGET_AARCH64` i linkuje ich biblioteki). Przez tę warstwę idzie wszystko,
co zależy od platformy:
- **Nagłówki:** libclang parsuje z `--target=<triple>` (więc `long` ma 32 bity na Windowsie i używane są - **Nagłówki:** libclang parsuje z `--target=<triple>` (więc `long` ma 32 bity na Windowsie i używane są
właściwe nagłówki), a na Windowsie z `-D_USE_MATH_DEFINES`. Katalogi nagłówków pochodzą z `<cc> -E -v`. właściwe nagłówki), a na Windowsie z `-D_USE_MATH_DEFINES`. Katalogi nagłówków pochodzą z `<cc> -E -v`.
@ -905,7 +1011,12 @@ Windowsie, `cc` na Uniksie. Przez tę warstwę idzie wszystko, co zależy od pla
- **Linkowanie:** bez `-lm` przy MSVC; nazwy `.exe`; klucz cache runtime'u obejmuje triple i kompilator C. - **Linkowanie:** bez `-lm` przy MSVC; nazwy `.exe`; klucz cache runtime'u obejmuje triple i kompilator C.
Komunikaty linkera MSVC (`LNK2019 unresolved external symbol`, `LNK1104`/`LNK1181 cannot open file`) są Komunikaty linkera MSVC (`LNK2019 unresolved external symbol`, `LNK1104`/`LNK1181 cannot open file`) są
tłumaczone tak jak komunikaty GNU ld. tłumaczone tak jak komunikaty GNU ld.
- **Uruchamianie:** program dla Windowsa zbudowany na Linuksie uruchamia się przez `wine`. - **Uruchamianie:** `runPrefix()`: program dla Windowsa zbudowany na Linuksie uruchamia się przez `wine`,
program dla Linuksa ARM64 na innym procesorze przez `qemu-aarch64`.
- **macOS:** `<cc> -E -v` wypisuje wpisy `(framework directory)`; zamieniają się w `-iframework` dla
libclang, więc `import "OpenGL/gl.h"` działa. `link "Cocoa.framework"` daje `-framework Cocoa`. Komunikaty
linkera Apple (`Undefined symbols for architecture arm64: "_f"`, `ld: library 'x' not found`,
`framework 'X' not found`) są tłumaczone jak pozostałe, bez wiodącego `_` z Mach-O.
- **Informacje dla debuggera:** CodeView dla celów MSVC, DWARF w pozostałych. - **Informacje dla debuggera:** CodeView dla celów MSVC, DWARF w pozostałych.
- **Runtime:** na Windowsie nie ma `getline` ani `sys/wait.h`; `stdin`/`stdout`/`stderr` są tam makrami, - **Runtime:** na Windowsie nie ma `getline` ani `sys/wait.h`; `stdin`/`stdout`/`stderr` są tam makrami,
więc `finch_std_stream(i)` udostępnia je Finchowi, gdy nagłówek nie ma dla nich zmiennej `extern`. więc `finch_std_stream(i)` udostępnia je Finchowi, gdy nagłówek nie ma dla nich zmiennej `extern`.
@ -920,10 +1031,30 @@ kopii (bez `byval`: funkcja dostaje zwykły wskaźnik). Wyniki 1/2/4/8-bajtowe w
wszystko inne przez ukryty wskaźnik `sret`. Każdy argument zajmuje jedno miejsce, więc nie ma liczenia wszystko inne przez ukryty wskaźnik `sret`. Każdy argument zajmuje jedno miejsce, więc nie ma liczenia
rejestrów. `cThunk()` robi to samo w drugą stronę dla callbacków. rejestrów. `cThunk()` robi to samo w drugą stronę dla callbacków.
`tests/windows.sh` buduje każdy test z `--target windows`, uruchamia go w Wine i porównuje wynik: przechodzą `tests/cross.sh windows` buduje każdy test z `--target windows`, uruchamia go w Wine i porównuje wynik: przechodzą
wszystkie, łącznie z `c_structs` (który wtedy sprawdza reguły Microsoftu na bibliotece C zbudowanej MinGW). wszystkie, łącznie z `c_structs` (który wtedy sprawdza reguły Microsoftu na bibliotece C zbudowanej MinGW).
CI dodatkowo buduje `finch.exe` przez MSVC na Windows Server i uruchamia tam `tests/run.sh`. CI dodatkowo buduje `finch.exe` przez MSVC na Windows Server i uruchamia tam `tests/run.sh`.
### AAPCS64 (Linux ARM64 i Apple)
`classify()` odwzorowuje to, jak clang tłumaczy wywołania na AArch64:
- **Jednorodny agregat zmiennoprzecinkowy** (1–4 elementy, wszystkie `float` albo wszystkie `double`,
zagnieżdżone struktury i tablice spłaszczone) idzie w rejestrach wektorowych: przekazywany jako
`[n x float]` / `[n x double]`, zwracany jako `{ float, … }`. Na Linuksie argument dostaje też
`alignstack(8)`, tak jak u clanga; Apple tego nie robi.
- Każda inna struktura do 16 bajtów idzie w rejestrach ogólnych: `i64` (≤ 8 bajtów) albo `[2 x i64]`
(`i128`, jeśli jej wyrównanie to 16); zwracana jest jako liczba całkowita dokładnie jej rozmiaru albo `[2 x i64]`.
- Większe struktury są **pośrednie (Indirect)**: kopia na stosie wywołującego, przekazana przez adres
(bez `byval`); zwracane przez `sret`.
- Nie ma liczenia rejestrów: każdą część rozmieszcza backend.
- Zwykły `char` w C jest **bez znaku** na Linuksie ARM64, a ze znakiem na Apple i x86, co ma znaczenie przy
rozszerzaniu argumentów i wyników typu `char` (`charIsSigned()`).
- Apple przekazuje zmienną część `printf(...)` przez stos; LLVM robi to na podstawie `...` w typie funkcji.
`tests/cross.sh arm64` uruchamia cały zestaw testów (łącznie z testem ABI struktur na bibliotece C
zbudowanej GCC) w QEMU, a CI robi to natywnie na Linuksie ARM64 i Apple Silicon.
### Budowanie finch.exe ### Budowanie finch.exe
CI używa Visual Studio 2022, Ninja i oficjalnego pakietu `clang+llvm-21.x-x86_64-pc-windows-msvc` CI używa Visual Studio 2022, Ninja i oficjalnego pakietu `clang+llvm-21.x-x86_64-pc-windows-msvc`
@ -970,7 +1101,19 @@ i podpowiedziach parametrów (i odwzorowany w ściągach).
--- ---
## 25. Rozszerzenie VS Code ## 25. Integracje z edytorami
### Kate
`editors/kate/finch.xml` to definicja KSyntaxHighlighting (silnika Kate, KWrite i KDevelop): listy słów
kluczowych zgodne z `builtins_doc.h`, kontekst dla `fn Typ.metoda`, teksty ze znakami specjalnymi oraz
regiony zwijania dla `{ }` i komentarzy blokowych. `lspclient.json` to wpis dla klienta LSP w Kate
(`highlightingModeRegex: ^Finch$`); `install.sh` kopiuje definicję i dołącza wpis do
`~/.config/kate/lspclient/settings.json`. Kate wysyła więcej zapytań niż VS Code (`documentHighlight`,
`foldingRange`, …); serwer odpowiada na każde nieobsługiwane zapytanie błędem `MethodNotFound`, więc żaden
klient nie czeka.
### VS Code
`editors/vscode` to zwykłe rozszerzenie w JavaScripcie: `extension.js` uruchamia `finch lsp` przez `editors/vscode` to zwykłe rozszerzenie w JavaScripcie: `extension.js` uruchamia `finch lsp` przez
`vscode-languageclient` (ścieżka z ustawienia `finch.path`) i dodaje **Run** / **Build** jako zadania `vscode-languageclient` (ścieżka z ustawienia `finch.path`) i dodaje **Run** / **Build** jako zadania
@ -982,10 +1125,15 @@ szablony. CI pakuje je przez `vsce` do `finch-lang.vsix`.
## 26. Znane ograniczenia ## 26. Znane ograniczenia
- Brak metod w strukturach, typów generycznych, map, `match`, domknięć i błędów jako wartości (`int("x")`, `read_file` robią panic). - Brak typów generycznych, interfejsów, `match`, domknięć i enumów pisanych w Finchu.
- Teksty to bajty: `.len`, `s[i]`, `upper()` nie znają Unicode. - Teksty to bajty: `.len`, `s[i]`, `upper()` nie znają Unicode.
- Zmiana kluczy mapy wewnątrz `for` po tej samej mapie nie jest wykrywana (jest bezpieczna dla pamięci,
ale wpisy mogą zostać pominięte albo odwiedzone dwa razy).
- Porażka `try` / `or` w środku wyrażenia nie zwalnia świeżych wartości tymczasowych policzonych wcześniej
w tym wyrażeniu (mały wyciek, tylko na ścieżce porażki).
- Wiszące wskaźniki (`addr` zmiennej, która zniknęła, użycie po `free`) nie są wykrywane. - Wiszące wskaźniki (`addr` zmiennej, która zniknęła, użycie po `free`) nie są wykrywane.
- Wielkość przesunięć i konwersje float→int nie są sprawdzane (poison w LLVM, jak w C). - Wielkość przesunięć i konwersje float→int nie są sprawdzane (poison w LLVM, jak w C).
- C: unie i struktury z polami bitowymi przez wartość, makra-funkcje, `long double`. - C: unie i struktury z polami bitowymi przez wartość, makra-funkcje, `long double`.
- Dwa systemy: Linux i Windows, oba x86-64 (ABI System V i Microsoft x64). Jeszcze bez macOS i ARM. - Cele: x86-64 i ARM64 (Linux, macOS; Windows na x86-64). Bez Windowsa na ARM64, systemów 32-bitowych i WebAssembly.
- Kompilator samohostujący nie ma importu C, liczb z rozmiarem ani `defer` i nie zwalnia pamięci. - Kompilator samohostujący nie ma importu C, liczb z rozmiarem, `defer`, map, metod ani błędów jako
wartości i nie zwalnia pamięci.

View file

@ -77,7 +77,7 @@ Jeśli ostatnia linijka skończy się bez słowa `error`, Finch jest gotowy.
./build/finch version ./build/finch version
``` ```
Powinieneś zobaczyć coś w stylu `finch 2.3.0`. Powinieneś zobaczyć coś w stylu `finch 2.4.0`.
--- ---
@ -510,8 +510,68 @@ fn main() {
} }
``` ```
Jeśli chcesz też wiedzieć, *w którym miejscu* listy jesteś, daj `for` dwie nazwy: pierwsza liczy
0, 1, 2…, druga to wartość:
```c
fn main() {
for i, owoc in ["jabłko", "banan", "wiśnia"] {
print(i + 1, owoc)
}
}
```
```
1 jabłko
2 banan
3 wiśnia
```
Listy umieją więcej: `contains` (czy zawiera), `find` (znajdź), `insert` (wstaw), `remove` (usuń), Listy umieją więcej: `contains` (czy zawiera), `find` (znajdź), `insert` (wstaw), `remove` (usuń),
`sort` (sortuj), `reverse` (odwróć), `join` (połącz). Wszystkie opisuje [przewodnik dla techników](dla-technikow.md#tablice). `sort` (sortuj), `reverse` (odwróć), `join` (połącz). Wszystkie opisuje [przewodnik dla techników](dla-technikow.md#10-tablice-i-mapy).
### Szukanie po nazwie: mapy
Lista znajduje rzeczy po miejscu: `owoce[0]`. Czasem chcesz znaleźć coś po **nazwie**, tak jak
słowo w słowniku albo numer w książce telefonicznej. Do tego służy **mapa** (`map`): każdy
**klucz** (nazwa) ma swoją **wartość**:
```c
fn main() {
telefon := ["Ola": "555-1234", "Jan": "555-9876"]
print(telefon["Ola"]) // 555-1234
telefon["Ewa"] = "555-0000" // nowa osoba
print(telefon.len) // 3
if telefon.has("Tomek") {
print("Tomek jest")
} else {
print("nie ma Tomka")
}
for imie, numer in telefon { // każde imię z jego numerem
print(imie, numer)
}
}
```
- `["klucz": wartość, ...]` tworzy mapę. Klucze to zwykle tekst, ale mogą być też liczby.
- `telefon["Ola"]` daje wartość dla klucza. Pytanie o klucz, którego nie ma, zatrzymuje program,
więc najpierw sprawdź `telefon.has("Tomek")` (czy ma) albo użyj `telefon.get("Tomek", "nie wiem")`.
- `for imie, numer in telefon` odwiedza każdy klucz z wartością, w kolejności dodawania.
Mapy świetnie nadają się do **liczenia**. Nowy klucz zaczyna od 0, więc `+= 1` po prostu działa:
```c
fn main() {
map[str]int glosy // pusta mapa: klucze to tekst, wartości to liczby
for g in ["kot", "pies", "kot", "kot", "rybka"] {
glosy[g] += 1
}
print(glosy) // {"kot": 3, "pies": 1, "rybka": 1}
}
```
--- ---
@ -625,6 +685,36 @@ fn main() {
Możesz też trzymać wiele struktur na liście: `[]Ksiazka polka`, a potem `polka.push(k)`. Możesz też trzymać wiele struktur na liście: `[]Ksiazka polka`, a potem `polka.push(k)`.
### Polecenia twojej struktury: metody
Struktura może mieć własne polecenia (**metody**). Napisz nazwę struktury, kropkę i nazwę polecenia.
W środku `self` (ang. „sam”) to struktura, na której użyto polecenia:
```c
struct Gracz {
str imie
int zycia = 3
}
fn Gracz.trafiony() {
self.zycia -= 1
print(self.imie, "ma jeszcze", self.zycia, "życia")
}
fn Gracz.zyje() -> bool {
return self.zycia > 0
}
fn main() {
g := Gracz(imie: "Ola")
g.trafiony() // Ola ma jeszcze 2 życia
g.trafiony() // Ola ma jeszcze 1 życia
print(g.zyje()) // true
}
```
`g.trafiony()` zmienia samo `g`, więc po dwóch trafieniach `g.zycia` naprawdę wynosi 1.
--- ---
## 15. Zapisywanie do pliku ## 15. Zapisywanie do pliku
@ -688,6 +778,57 @@ Tutaj po prostu źle napisaliśmy `wiek`. Komunikat znaczy: „nie ma zmiennej o
| `index 5 is out of range (the length is 3)` | Zapytałeś listę o miejsce, którego nie ma | Pamiętaj, że liczymy od 0; sprawdź `.len` | | `index 5 is out of range (the length is 3)` | Zapytałeś listę o miejsce, którego nie ma | Pamiętaj, że liczymy od 0; sprawdź `.len` |
| `can't turn "abc" into int` | `int(...)` dostał tekst, który nie jest liczbą | Sprawdź, co wpisał użytkownik | | `can't turn "abc" into int` | `int(...)` dostał tekst, który nie jest liczbą | Sprawdź, co wpisał użytkownik |
| `can't use '+' on str and int` | Tekst plus liczba | Zamień liczbę na tekst przez `str(...)` | | `can't use '+' on str and int` | Tekst plus liczba | Zamień liczbę na tekst przez `str(...)` |
| `'f' can fail, so say what happens then` | Użyłeś polecenia, które może się nie udać, i nie napisałeś, co wtedy | Dopisz `or ...` (niżej) |
### Rzeczy, które mogą się nie udać: `or` i `try`
Niektóre rzeczy mogą pójść źle *w trakcie działania programu*: ktoś wpisze `abc` zamiast liczby
albo pliku nie ma. Zamiast zatrzymywać program, możesz powiedzieć, co wtedy zrobić, słowem **`or`** („albo”):
```c
fn main() {
wiek := int(input("Ile masz lat? ")) or 0 // to nie liczba? weź 0
print("Za rok będziesz mieć", wiek + 1)
notatka := read_file("notatka.txt") or "(jeszcze brak notatki)"
print(notatka)
}
```
Po `or` może też być blok. W środku `err` to komunikat, co poszło nie tak:
```c
fn main() {
while true {
n := int(input("Podaj liczbę: ")) or {
print("To nie jest liczba, spróbuj jeszcze raz")
continue
}
print("Wybrałeś", n)
break
}
}
```
Twoje własne funkcje też mogą się nie udać. Dopisz `!` po typie wyniku i użyj `return error("...")`:
```c
fn bezpieczne_dzielenie(int a, int b) -> int! {
if b == 0 {
return error("nie można dzielić przez zero")
}
return a / b
}
fn main() {
print(bezpieczne_dzielenie(10, 2) or -1) // 5
print(bezpieczne_dzielenie(10, 0) or -1) // -1
}
```
Finch nie pozwoli ci zapomnieć: wywołanie `bezpieczne_dzielenie(10, 0)` bez `or` to błąd, który
przypomina, żeby to obsłużyć. (Można też przekazać problem dalej przez `try`; wyjaśnia to
[przewodnik dla techników](dla-technikow.md#błędy-jako-wartości).)
--- ---
@ -889,10 +1030,13 @@ struct Zwierzak { // własny rodzaj wartości
int wiek = 1 int wiek = 1
} }
// listy: liczby := [1, 2, 3] liczby.push(4) liczby[0] liczby.len for n in liczby { } // listy: liczby := [1, 2, 3] liczby.push(4) liczby[0] liczby.len for n in liczby { } for i, n in liczby { }
// mapy: wiek := ["Ola": 9] wiek["Jan"] = 10 wiek["Ola"] wiek.has("Tomek") for imie, lata in wiek { }
// tekst: "a" + "b" str(42) int("42") s.len s.upper() s.contains("x") // tekst: "a" + "b" str(42) int("42") s.len s.upper() s.contains("x")
// pytanie: imie := input("Imię? ") // pytanie: imie := input("Imię? ")
// pliki: write_file("p.txt", tekst) read_file("p.txt") // pliki: write_file("p.txt", tekst) read_file("p.txt")
// metody: fn Zwierzak.urosnij() { self.wiek += 1 } z.urosnij()
// porażki: n := int(tekst) or 0 x := read_file("p.txt") or { print(err) ... }
``` ```
--- ---
@ -913,6 +1057,8 @@ struct Zwierzak { // własny rodzaj wartości
| **lista / tablica** | Wiele wartości po kolei, np. `[1, 2, 3]` | | **lista / tablica** | Wiele wartości po kolei, np. `[1, 2, 3]` |
| **indeks** | Pozycja wartości na liście, liczona od 0 | | **indeks** | Pozycja wartości na liście, liczona od 0 |
| **struktura** (struct) | Własny rodzaj wartości złożony z nazwanych części | | **struktura** (struct) | Własny rodzaj wartości złożony z nazwanych części |
| **metoda** | Polecenie należące do struktury, np. `g.trafiony()` |
| **mapa** (map) | Wartości znajdowane po kluczu (nazwie), jak w słowniku |
| **wejście** (input) | To, co użytkownik wpisuje do programu | | **wejście** (input) | To, co użytkownik wpisuje do programu |
| **błąd** (error) | Komunikat, że coś jest nie tak, i gdzie | | **błąd** (error) | Komunikat, że coś jest nie tak, i gdzie |
| **bug** | Pomyłka w programie | | **bug** | Pomyłka w programie |
@ -934,6 +1080,11 @@ struct Zwierzak { // własny rodzaj wartości
| `input` | wejście (pytanie) | | `input` | wejście (pytanie) |
| `len` (od *length*) | długość | | `len` (od *length*) | długość |
| `error` | błąd | | `error` | błąd |
| `or` | albo (co zrobić, gdy się nie uda) |
| `try` | spróbuj (a jak się nie uda, przekaż błąd dalej) |
| `self` | sam (struktura, na której użyto metody) |
| `map` | mapa (słownik) |
| `has` / `get` | ma / weź |
--- ---

View file

@ -20,16 +20,16 @@ kompilator działa w środku. Do tego służy [przewodnik dla inżynierów](dla-
7. [Operatory](#7-operatory) 7. [Operatory](#7-operatory)
8. [Sterowanie przebiegiem](#8-sterowanie-przebiegiem) 8. [Sterowanie przebiegiem](#8-sterowanie-przebiegiem)
9. [Funkcje](#9-funkcje) 9. [Funkcje](#9-funkcje)
10. [Tablice](#10-tablice) 10. [Tablice i mapy](#10-tablice-i-mapy)
11. [Tekst (str)](#11-tekst-str) 11. [Tekst (str)](#11-tekst-str)
12. [Struktury](#12-struktury) 12. [Struktury i metody](#12-struktury-i-metody)
13. [Pamięć: kto co zwalnia](#13-pamięć-kto-co-zwalnia) 13. [Pamięć: kto co zwalnia](#13-pamięć-kto-co-zwalnia)
14. [Wskaźniki](#14-wskaźniki) 14. [Wskaźniki](#14-wskaźniki)
15. [Funkcje wbudowane](#15-funkcje-wbudowane) 15. [Funkcje wbudowane](#15-funkcje-wbudowane)
16. [Moduły](#16-moduły) 16. [Moduły](#16-moduły)
17. [Biblioteki z C](#17-biblioteki-z-c) 17. [Biblioteki z C](#17-biblioteki-z-c)
18. [Błędy](#18-błędy) 18. [Błędy](#18-błędy)
19. [Edytor: VS Code](#19-edytor-vs-code) 19. [Edytory: VS Code, Kate i inne](#19-edytory-vs-code-kate-i-inne)
20. [Debugowanie](#20-debugowanie) 20. [Debugowanie](#20-debugowanie)
21. [Rozwiązywanie problemów](#21-rozwiązywanie-problemów) 21. [Rozwiązywanie problemów](#21-rozwiązywanie-problemów)
22. [Struktura projektu i testy](#22-struktura-projektu-i-testy) 22. [Struktura projektu i testy](#22-struktura-projektu-i-testy)
@ -60,7 +60,9 @@ i komunikaty błędów, które mówią, jak problem naprawić.
## 2. Instalacja ## 2. Instalacja
Finch działa na **Linuksie i Windowsie, x86-64**, i jest budowany z **LLVM 21**. Finch działa na **Linuksie** (x86-64 i ARM64), **macOS** (Apple Silicon i Intel) i **Windowsie** (x86-64),
i jest budowany z **LLVM 21**. Każde [wydanie](https://github.com/Nexoniarz/Finch/releases) ma gotowy
`finch` dla Windowsa, Linuksa x86-64, Linuksa ARM64 i macOS ARM64.
### Windows ### Windows
@ -75,8 +77,30 @@ Finch działa na **Linuksie i Windowsie, x86-64**, i jest budowany z **LLVM 21**
Programy to zwykłe pliki `.exe`. Żeby zbudować Fincha ze źródeł na Windowsie, idź za `.github/workflows/ci.yml` Programy to zwykłe pliki `.exe`. Żeby zbudować Fincha ze źródeł na Windowsie, idź za `.github/workflows/ci.yml`
(Visual Studio 2022 + pakiet `clang+llvm-21.x-x86_64-pc-windows-msvc` z wydań LLVM). (Visual Studio 2022 + pakiet `clang+llvm-21.x-x86_64-pc-windows-msvc` z wydań LLVM).
### macOS
```sh
brew install llvm@21 # LLVM dla Fincha; programy linkuje clang od Apple
```
Potem rozpakuj **`finch-macos-arm64.tar.gz`** z wydań (korzysta z `llvm@21` z Homebrew)
albo zbuduj go sam (narzędzia wiersza poleceń Xcode: `xcode-select --install`):
```sh
git clone https://github.com/Nexoniarz/Finch.git && cd Finch
cmake -S . -B build -G Ninja -DCMAKE_BUILD_TYPE=Release -DLLVM_DIR="$(brew --prefix llvm@21)/lib/cmake/llvm"
ninja -C build
./build/finch version
```
Frameworki Apple linkuje się przez `link "Cocoa.framework"` (`-framework Cocoa`), a ich nagłówki importuje
się normalnie: `import "OpenGL/gl.h"`.
### Linux ### Linux
Gotowe `finch-linux-x64.tar.gz` / `finch-linux-arm64.tar.gz` potrzebują bibliotek LLVM 21
(Debian/Ubuntu: `libllvm21 libclang1-21` z [apt.llvm.org](https://apt.llvm.org)) i kompilatora C (`cc`).
Samodzielne budowanie wygląda tak samo na x86-64 i ARM64 (Raspberry Pi 4/5, Graviton, …).
Z innymi wersjami LLVM Finch prawdopodobnie się nie skompiluje, bo API C++ LLVM zmienia się między wydaniami. Z innymi wersjami LLVM Finch prawdopodobnie się nie skompiluje, bo API C++ LLVM zmienia się między wydaniami.
### Opcja A: Nix (zalecana, nic nie instalujesz ręcznie) ### Opcja A: Nix (zalecana, nic nie instalujesz ręcznie)
@ -87,7 +111,7 @@ cd Finch
nix-shell # pobiera LLVM, libclang, clang, cmake, ninja, pkg-config nix-shell # pobiera LLVM, libclang, clang, cmake, ninja, pkg-config
cmake -S . -B build -G Ninja cmake -S . -B build -G Ninja
ninja -C build ninja -C build
./build/finch version # finch 2.3.0 (LLVM 21.x) ./build/finch version # finch 2.4.0 (LLVM 21.x)
``` ```
### Opcja B: pakiety z twojej dystrybucji ### Opcja B: pakiety z twojej dystrybucji
@ -108,16 +132,27 @@ cmake -S . -B build -G Ninja -DLLVM_DIR=/usr/lib/llvm-21/lib/cmake/llvm
ninja -C build ninja -C build
``` ```
### Budowanie programów na Windowsa pod Linuksem ### Budowanie na inny system
Z zainstalowanym kompilatorem krzyżowym MinGW-w64 (`x86_64-w64-mingw32-gcc`; na Niksie `--target` buduje program na inny system. Do linkowania Finch potrzebuje kompilatora C dla tego systemu:
`pkgsCross.mingwW64.buildPackages.gcc`):
| `--target` | Buduje | Kompilator C | `finch run` używa |
|---|---|---|---|
| `windows` | `.exe` dla Windowsa | `x86_64-w64-mingw32-gcc` (MinGW-w64) | Wine |
| `arm64` | program dla Linuksa ARM64 | `aarch64-linux-gnu-gcc` | `qemu-aarch64` |
| `linux` | program dla Linuksa x86-64 | `clang --target=…` | |
| `macos` | program dla macOS (na Macu: dla drugiego procesora) | `clang --target=…` | |
| dowolny triple LLVM | np. `aarch64-unknown-linux-gnu` | `clang --target=<triple>` | |
```sh ```sh
finch build gra.fch --target windows # gra.exe finch build gra.fch --target windows # gra.exe
finch run gra.fch --target windows # uruchamia ją w Wine, jeśli jest zainstalowane finch run gra.fch --target arm64 # buduje na ARM64 i uruchamia w QEMU
FINCH_CC=aarch64-unknown-linux-gnu-gcc finch build narzedzie.fch --target arm64 # inna nazwa kompilatora
``` ```
Na Niksie te kompilatory to `pkgsCross.mingwW64.buildPackages.gcc` i
`pkgsCross.aarch64-multiplatform.buildPackages.gcc`.
### Dodanie `finch` do PATH (opcjonalnie) ### Dodanie `finch` do PATH (opcjonalnie)
```sh ```sh
@ -137,12 +172,14 @@ Inny kompilator wskażesz zmienną `CC`.
finch run <plik.fch> [argumenty...] skompiluj i od razu uruchom finch run <plik.fch> [argumenty...] skompiluj i od razu uruchom
finch build <plik.fch> [-o nazwa] skompiluj do programu (domyślna nazwa: nazwa pliku) finch build <plik.fch> [-o nazwa] skompiluj do programu (domyślna nazwa: nazwa pliku)
finch ir <plik.fch> pokaż LLVM IR (dla ciekawskich) finch ir <plik.fch> pokaż LLVM IR (dla ciekawskich)
finch lsp serwer języka, dla edytorów
finch version pokaż wersję finch version pokaż wersję
opcje: opcje:
-l <lib> dolinkuj bibliotekę C, to samo co link "lib" w pliku -l <lib> dolinkuj bibliotekę C, to samo co link "lib" w pliku
-g dodaj informacje dla debuggera (gdb / lldb) --target <nazwa> zbuduj na inny system: windows, linux, arm64, macos albo triple LLVM
-O0 bez optymalizacji -g dodaj informacje dla debuggera (gdb / lldb / Visual Studio)
-O0 bez optymalizacji
``` ```
Przykłady: Przykłady:
@ -226,6 +263,7 @@ x += 1 // także -= *= /= %=
| `char` | jeden znak wielkości bajtu | `'A'`, `'\n'` | | `char` | jeden znak wielkości bajtu | `'A'`, `'\n'` |
| `str` | tekst | `"cześć\tświecie"` | | `str` | tekst | `"cześć\tświecie"` |
| `[]T` | lista (tablica) elementów `T` | `[1, 2, 3]` | | `[]T` | lista (tablica) elementów `T` | `[1, 2, 3]` |
| `map[K]V` | wartości typu `V` znajdowane po kluczu typu `K` | `["a": 1, "b": 2]` |
| twoje struktury | nazwane grupy wartości | `Punkt(1, 2)` | | twoje struktury | nazwane grupy wartości | `Punkt(1, 2)` |
Znaki specjalne w tekstach: `\n` nowa linia, `\t` tabulator, `\r`, `\0`, `\\`, `\"`, `\'`. Znaki specjalne w tekstach: `\n` nowa linia, `\t` tabulator, `\r`, `\0`, `\\`, `\"`, `\'`.
@ -332,6 +370,14 @@ for i in 0..10 { // i = 0, 1, …, 9 (koniec nie jest wliczany)
for imie in imiona { // każdy element tablicy (albo każdy znak str) for imie in imiona { // każdy element tablicy (albo każdy znak str)
print(imie) print(imie)
} }
for i, imie in imiona { // razem z indeksem: 0, 1, 2, ...
print(i, imie)
}
for klucz, wartosc in wiek { // każdy wpis mapy, w kolejności dodawania kluczy
print(klucz, wartosc) // (for klucz in wiek: same klucze)
}
``` ```
- Warunek musi być typu `bool`. `if x {` z liczbą to błąd; napisz `if x != 0 {`. - Warunek musi być typu `bool`. `if x {` z liczbą to błąd; napisz `if x != 0 {`.
@ -375,11 +421,13 @@ fn bez_wyniku(str wiadomosc) { // bez "->": nic nie zwraca
Żeby zmienić zmienną wywołującego, przekaż wskaźnik, zobacz [Wskaźniki](#14-wskaźniki). Żeby zmienić zmienną wywołującego, przekaż wskaźnik, zobacz [Wskaźniki](#14-wskaźniki).
- Funkcja z `-> typ` musi zwrócić wartość na każdej ścieżce. Finch to sprawdza. - Funkcja z `-> typ` musi zwrócić wartość na każdej ścieżce. Finch to sprawdza.
- Funkcji nie można definiować wewnątrz innej funkcji. - Funkcji nie można definiować wewnątrz innej funkcji.
- Funkcję, która może się nie udać, zapisuje się `-> int!` (albo `-> !` bez wyniku); zobacz
[Błędy jako wartości](#błędy-jako-wartości).
- Długie wywołania możesz rozbić na kilka linijek wewnątrz nawiasów; przecinek po ostatnim argumencie jest dozwolony. - Długie wywołania możesz rozbić na kilka linijek wewnątrz nawiasów; przecinek po ostatnim argumencie jest dozwolony.
--- ---
## 10. Tablice ## 10. Tablice i mapy
```c ```c
liczby := [5, 3, 8] // tablica int liczby := [5, 3, 8] // tablica int
@ -408,6 +456,45 @@ siatka := [[1, 2], [3, 4]] // tablice tablic
**Przypisanie kopiuje:** po `b := a` zmiana `b` nie zmienia `a`. **Przypisanie kopiuje:** po `b := a` zmiana `b` nie zmienia `a`.
### Mapy
`map[K]V` znajduje wartości po kluczu. Kluczem może być liczba całkowita, `char`, `bool` albo `str`;
wartością cokolwiek (tablice, struktury, inne mapy). Przechodzenie pętlą idzie w kolejności, w jakiej
klucze zostały dodane po raz pierwszy.
```c
wiek := ["anna": 31, "bob": 25] // literał mapy: map[str]int
map[str][]str grupy // pusta mapa
map[int]str nazwy = [:] // [:] to pusta mapa tam, gdzie typ jest znany
```
| Operacja | Znaczenie |
|---|---|
| `m[k]` | wartość dla `k`; brak klucza zatrzymuje program komunikatem `the key "k" is not in the map` |
| `m[k] = v` | dodaj albo zastąp |
| `m[k] += 1`, `m[k].push(x)`, `m[k].pole = …` | zmiana brakującego klucza najpierw go dodaje, z wartością domyślną typu (0, `""`, `[]`, …) |
| `m.len` | liczba kluczy |
| `m.has(k)` | czy `k` jest w mapie? |
| `m.get(k, domyslna)` | wartość albo `domyslna` (liczona tylko w razie potrzeby) |
| `m.remove(k)` | usuwa `k`; `true`, jeśli był |
| `m.clear()` | usuwa wszystko |
| `m.keys()`, `m.values()` | nowe tablice, w kolejności dodawania |
| `print(m)`, `str(m)` | `{"anna": 31, "bob": 25}` |
```c
map[str]int ile
for slowo in tekst.split(" ") {
ile[slowo] += 1 // liczenie: nowe słowo zaczyna od 0
}
for slowo, n in ile {
print(slowo, n)
}
```
Wyszukiwanie używa haszowania (średnio stały czas). Tak jak tablice, mapy są właścicielami kluczy
i wartości, kopiują się przy przypisaniu i zwalniają na końcu bloku. Nie dodawaj ani nie usuwaj kluczy
mapy wewnątrz `for` po tej samej mapie; zbierz je do tablicy i zmień mapę po pętli.
--- ---
## 11. Tekst (str) ## 11. Tekst (str)
@ -437,7 +524,7 @@ przekazanie go do C jako `char*` działa.
--- ---
## 12. Struktury ## 12. Struktury i metody
```c ```c
struct Gracz { struct Gracz {
@ -457,7 +544,34 @@ print(g) // Gracz(imie: "Ola", zycia: 2, wyniki: [10], pozycja: Punkt(x: 5,
- Przypisanie albo przekazanie struktury ją kopiuje, razem z jej tablicami i tekstami. - Przypisanie albo przekazanie struktury ją kopiuje, razem z jej tablicami i tekstami.
- Struktura nie może zawierać samej siebie wprost; dla takiego pola użyj `ptr[Wezel]` albo `[]Wezel` (drzewa, listy). - Struktura nie może zawierać samej siebie wprost; dla takiego pola użyj `ptr[Wezel]` albo `[]Wezel` (drzewa, listy).
- Struktury nie mają metod. Pisz funkcje, które je przyjmują: `fn wylecz(Gracz g) -> Gracz`.
### Metody
Metoda to funkcja należąca do struktury: `fn Struktura.nazwa(...)`. W środku `self` to struktura,
na której ją wywołano, i to jest sama wartość wywołującego, nie kopia: zmiany w `self` zostają.
```c
fn Gracz.wylecz(int ile) {
self.zycia += ile
}
fn Gracz.zyje() -> bool {
return self.zycia > 0
}
g := Gracz(imie: "Ola")
g.wylecz(2) // g.zycia wynosi teraz 5
if g.zyje() { ... }
druzyna[0].wylecz(1) // na elemencie tablicy, polu, m[klucz], przez ptr[Gracz] ...
```
- Metoda jest w tym samym pliku (module) co jej struktura i jest dostępna wszędzie, gdzie ta struktura.
- Metody można też dopisywać do **struktur z nagłówków C**: `fn Vector2.length() -> f32 { ... }`.
- Metoda nie może nazywać się tak jak pole.
- Wywołanie metody zmieniającej `self` na zmiennej pętli (`for g in druzyna { g.wylecz(1) }`) to błąd,
bo zmienna pętli jest tylko do odczytu; użyj `for i in 0..druzyna.len { druzyna[i].wylecz(1) }`.
- Tak jak funkcje, metody mogą się nie udać (`-> !`); zobacz [Błędy jako wartości](#błędy-jako-wartości).
--- ---
@ -520,14 +634,16 @@ Użycie `.value`, `.pole` albo `[i]` na wskaźniku `null` **zatrzymuje program**
|---|---| |---|---|
| `print(a, b, ...)` | Wypisuje wszystkie wartości oddzielone spacjami i przechodzi do nowej linii. Działa z każdym typem, także z tablicami i strukturami. | | `print(a, b, ...)` | Wypisuje wszystkie wartości oddzielone spacjami i przechodzi do nowej linii. Działa z każdym typem, także z tablicami i strukturami. |
| `input()`, `input("pytanie")` | Czyta jedną linijkę wpisaną przez użytkownika (bez znaku nowej linii). Na końcu wejścia daje pusty tekst. | | `input()`, `input("pytanie")` | Czyta jedną linijkę wpisaną przez użytkownika (bez znaku nowej linii). Na końcu wejścia daje pusty tekst. |
| `read_file(sciezka)` | Cały plik jako `str`. Jeśli nie da się go przeczytać, zatrzymuje program z czytelnym komunikatem. | | `read_file(sciezka)` | Cały plik jako `str`. Jeśli nie da się go przeczytać, zatrzymuje program z czytelnym komunikatem, chyba że obsłużysz to: `read_file(p) or ...`. |
| `write_file(sciezka, tekst)` | Zapisuje (zastępuje) plik; oddaje `true`, jeśli się udało. | | `write_file(sciezka, tekst)` | Zapisuje (zastępuje) plik; oddaje `true`, jeśli się udało. Z `or` / `try` porażka niesie przyczynę. |
| `file_exists(sciezka)` | `true` / `false` | | `file_exists(sciezka)` | `true` / `false` |
| `delete_file(sciezka)` | Usuwa plik; `true`, jeśli się udało (albo przyczyna, z `or` / `try`). |
| `error(komunikat)` | Porażka, którą zwraca funkcja mogąca się nie udać: `return error("...")`. |
| `shell(polecenie)` | Uruchamia polecenie powłoki i oddaje jego kod wyjścia. | | `shell(polecenie)` | Uruchamia polecenie powłoki i oddaje jego kod wyjścia. |
| `exit(kod)` | Natychmiast kończy program. | | `exit(kod)` | Natychmiast kończy program. |
| `addr(x)` | Wskaźnik na `x`; `addr(funkcja)` daje C wskaźnik na funkcję (callback). | | `addr(x)` | Wskaźnik na `x`; `addr(funkcja)` daje C wskaźnik na funkcję (callback). |
| `new(wartosc)`, `free(p)` | Ręczna pamięć na stercie (zobacz [Pamięć](#13-pamięć-kto-co-zwalnia)). | | `new(wartosc)`, `free(p)` | Ręczna pamięć na stercie (zobacz [Pamięć](#13-pamięć-kto-co-zwalnia)). |
| `int(x)`, `str(x)`, `u8(x)`, … | Zamiany typów (zobacz [Zamiana typów](#zamiana-typów)). | | `int(x)`, `str(x)`, `u8(x)`, … | Zamiany typów (zobacz [Zamiana typów](#zamiana-typów)). `str(x)` działa na każdym typie i daje ten sam tekst, co `print`. `int(tekst) or 0` obsługuje tekst, który nie jest liczbą. |
--- ---
@ -669,6 +785,54 @@ Oba działają bezpośrednio; `examples/opengl.fch` i `examples/vulkan.fch` to k
## 18. Błędy ## 18. Błędy
### Błędy jako wartości
Niektóre porażki są normalne: użytkownik wpisze `abc`, pliku nie ma. Finch obsługuje je bez wyjątków
i bez ukrytych skoków. Funkcja, która może się nie udać, mówi to znakiem `!` po typie wyniku i kończy się
porażką przez `return error("komunikat")`:
```c
fn port_z_tekstu(str tekst) -> int! {
n := int(tekst) or { return error("'" + tekst + "' to nie liczba") }
if n < 1 || n > 65535 {
return error("port " + str(n) + " jest poza zakresem")
}
return n
}
fn zapisz(str sciezka, str tekst) -> ! { // może się nie udać, nic nie oddaje
try write_file(sciezka, tekst)
}
```
Wywołujący **musi** obsłużyć porażkę, na jeden z trzech sposobów (zapomnienie to błąd kompilacji):
| Kod | W razie porażki |
|---|---|
| `port := port_z_tekstu(s) or 8080` | użyj tej wartości (liczonej tylko przy porażce; może to być kolejne wywołanie z `or`) |
| `port := port_z_tekstu(s) or { print(err); return }` | wykonaj blok; `err` to komunikat (`str`). Blok musi wyjść (`return`, `break`, `continue`, `exit`), chyba że wynik nie jest używany. |
| `port := try port_z_tekstu(s)` | zakończ bieżącą funkcję tym samym błędem (musi też móc się nie udać) |
```c
fn wczytaj(str sciezka) -> Konfig! {
tekst := try read_file(sciezka)
port := try port_z_tekstu(tekst.trim())
return Konfig(port: port)
}
fn main() -> ! { // main też może się nie udać: błąd zostanie wypisany, a kod wyjścia to 1
konfig := try wczytaj("app.conf")
...
}
```
Funkcje wbudowane, które mogą się nie udać, działają tak samo: `int(tekst)`, `float(tekst)`, `read_file`,
`write_file`, `delete_file`. Bez `or` / `try` zachowują się jak wcześniej (`int("x")` zatrzymuje program,
`write_file` oddaje `bool`).
Pod spodem funkcja, która może się nie udać, zwraca wartość z flagą i komunikatem: bez alokacji przy
sukcesie, bez zwijania stosu, a porażka kosztuje tyle, co zbudowanie komunikatu.
### Błędy kompilacji ### Błędy kompilacji
``` ```
@ -692,11 +856,31 @@ Zamiast niezdefiniowanego zachowania (jak w C) Finch zatrzymuje się z komunikat
| `runtime error: pop() on an empty array` | | | `runtime error: pop() on an empty array` | |
| `runtime error: used .value on a null pointer` | także `.pole` i `[i]` przez `null` | | `runtime error: used .value on a null pointer` | także `.pole` i `[i]` przez `null` |
| `runtime error: can't turn "x" into int` | `int(...)` / `float(...)` na tekście, który nie jest liczbą | | `runtime error: can't turn "x" into int` | `int(...)` / `float(...)` na tekście, który nie jest liczbą |
| `runtime error: can't read the file "…"` | `read_file` na brakującym albo nieczytelnym pliku | | `runtime error: can't read the file "…"` | `read_file` na brakującym albo nieczytelnym pliku (obsłuż to przez `or`) |
| `runtime error: the key "x" is not in the map …` | `m["x"]`, gdy mapa nie ma takiego klucza (użyj `.has` albo `.get`) |
| `runtime error: called .f() on a null pointer` | metoda wywołana przez `null` w `ptr[T]` |
--- ---
## 19. Edytor: VS Code ## 19. Edytory: VS Code, Kate i inne
Wszystkie funkcje edytorów pochodzą z jednego serwera języka, `finch lsp`, który używa prawdziwego
kompilatora: błędy na bieżąco, typy i opisy po najechaniu myszą, idź do definicji, podpowiedzi (pola,
metody, metody map i tablic po `.`, zawartość modułów, nazwy z nagłówków C), konspekt pliku i podpowiedzi
parametrów.
### Kate (oraz KWrite, KDevelop)
```sh
editors/kate/install.sh
```
Instaluje podświetlanie (`~/.local/share/org.kde.syntax-highlighting/syntax/finch.xml`) i dopisuje `finch`
do ustawień klienta LSP w Kate, zachowując twoje pozostałe serwery. Uruchom Kate ponownie i włącz wtyczkę
**LSP Client**. Żeby uruchamiać programy, dodaj `finch run %f` jako cel we wtyczce **Build & Run**; komunikaty
Fincha `plik:linia:kolumna: error:` są wtedy klikalne. Szczegóły: `editors/kate/README.md`.
### VS Code
Rozszerzenie **Finch** (`editors/vscode`, dołączane też do każdego wydania jako plik `.vsix`) daje: Rozszerzenie **Finch** (`editors/vscode`, dołączane też do każdego wydania jako plik `.vsix`) daje:
podświetlanie składni, błędy na bieżąco, podpowiedzi (także pola i metody po `.`), typy po najechaniu podświetlanie składni, błędy na bieżąco, podpowiedzi (także pola i metody po `.`), typy po najechaniu
@ -704,8 +888,12 @@ myszą, idź do definicji (F12), konspekt pliku, podpowiedzi parametrów, szablo
(`Ctrl+F5`). Błędy z **Finch: Build This File** trafiają do panelu Problems. (`Ctrl+F5`). Błędy z **Finch: Build This File** trafiają do panelu Problems.
Instalacja: Rozszerzenia → `…` → *Install from VSIX…* → wybierz `finch-lang-*.vsix`. Rozszerzenie uruchamia Instalacja: Rozszerzenia → `…` → *Install from VSIX…* → wybierz `finch-lang-*.vsix`. Rozszerzenie uruchamia
`finch` z PATH; jeśli jest gdzie indziej, ustaw **Finch: Path** w ustawieniach. Inne edytory obsługujące `finch` z PATH; jeśli jest gdzie indziej, ustaw **Finch: Path** w ustawieniach.
Language Server Protocol (Neovim, Helix, Zed, Emacs, Sublime) mogą używać `finch lsp` bezpośrednio.
### Inne
Neovim, Helix, Zed, Emacs, Sublime i każdy inny edytor obsługujący Language Server Protocol:
uruchamiaj `finch lsp` dla plików `.fch` (stdin/stdout, bez opcji).
## 20. Debugowanie ## 20. Debugowanie
@ -738,6 +926,9 @@ CodeView, który czyta debugger Visual Studio i WinDbg.
| Biblioteka nie znajduje się przy starcie programu | Zlinkowano ją z folderu, którego system nie przeszukuje. Uruchamiaj w tym samym `nix-shell` albo zainstaluj ją systemowo. | | Biblioteka nie znajduje się przy starcie programu | Zlinkowano ją z folderu, którego system nie przeszukuje. Uruchamiaj w tym samym `nix-shell` albo zainstaluj ją systemowo. |
| Windows: `finch` nie linkuje (błędy `LNK…` o `libcmt`, `kernel32`) | Zainstaluj Visual Studio Build Tools z *Desktop development with C++*. | | Windows: `finch` nie linkuje (błędy `LNK…` o `libcmt`, `kernel32`) | Zainstaluj Visual Studio Build Tools z *Desktop development with C++*. |
| Windows: `finch.exe` się nie uruchamia (brak `libclang.dll`) | Trzymaj `libclang.dll` obok `finch.exe`. | | Windows: `finch.exe` się nie uruchamia (brak `libclang.dll`) | Trzymaj `libclang.dll` obok `finch.exe`. |
| macOS: `finch` się nie uruchamia (`Library not loaded: …libLLVM…`) | `brew install llvm@21`. |
| macOS: `can't find the C header 'stdio.h'` | Zainstaluj narzędzia wiersza poleceń: `xcode-select --install`. |
| `--target arm64`: `no C compiler found to link with` | Zainstaluj `aarch64-linux-gnu-gcc` (Debian: `gcc-aarch64-linux-gnu`) albo ustaw `FINCH_CC`. |
| `clang` w `nix-shell` nie widzi `stdio.h` | Masz stary `shell.nix`: `llvmPackages.clang` musi być przed `llvmPackages.libclang`. | | `clang` w `nix-shell` nie widzi `stdio.h` | Masz stary `shell.nix`: `llvmPackages.clang` musi być przed `llvmPackages.libclang`. |
| Program nigdy się nie kończy | Warunek pętli nigdy nie staje się fałszywy. Naciśnij Ctrl+C. | | Program nigdy się nie kończy | Warunek pętli nigdy nie staje się fałszywy. Naciśnij Ctrl+C. |
@ -756,17 +947,18 @@ Finch/
│ ├── fail/ programy, które muszą się nie udać, z oczekiwanym błędem w 1. linijce │ ├── fail/ programy, które muszą się nie udać, z oczekiwanym błędem w 1. linijce
│ ├── run.sh uruchamia testy (MEMCHECK=1 sprawdza też pamięć valgrindem) │ ├── run.sh uruchamia testy (MEMCHECK=1 sprawdza też pamięć valgrindem)
│ └── boot.sh buduje kompilator samohostujący nim samym i porównuje │ └── boot.sh buduje kompilator samohostujący nim samym i porównuje
├── editors/vscode/ rozszerzenie VS Code ├── editors/ rozszerzenie VS Code (vscode/), podświetlanie i konfiguracja LSP dla Kate (kate/)
├── docs/ ta dokumentacja (en, pl), łącznie ze ściągą ├── docs/ ta dokumentacja (en, pl), łącznie ze ściągą
├── shell.nix środowisko deweloperskie Nix ├── shell.nix środowisko deweloperskie Nix
└── CMakeLists.txt └── CMakeLists.txt
``` ```
```sh ```sh
tests/run.sh # → 59 passed, 0 failed tests/run.sh # → 74 passed, 0 failed
MEMCHECK=1 tests/run.sh # to samo pod valgrindem: bez wycieków i złych dostępów do pamięci MEMCHECK=1 tests/run.sh # to samo pod valgrindem: bez wycieków i złych dostępów do pamięci
tests/boot.sh # sprawdzenie samohostowania (wymaga clanga) tests/boot.sh # sprawdzenie samohostowania (wymaga clanga)
tests/windows.sh # każdy test zbudowany na Windowsa i uruchomiony w Wine tests/cross.sh windows # każdy test zbudowany na Windowsa i uruchomiony w Wine
tests/cross.sh arm64 # każdy test zbudowany na Linuksa ARM64 i uruchomiony w QEMU
python3 tests/lsp_test.py # serwer języka python3 tests/lsp_test.py # serwer języka
``` ```
@ -774,12 +966,11 @@ python3 tests/lsp_test.py # serwer języka
## 23. Obecne ograniczenia ## 23. Obecne ograniczenia
Finch 2.0 to kompletny mały język, ale nie skończony. Jeszcze nie ma: Jeszcze nie ma:
- Metod w strukturach, typów generycznych, słowników (map), `match`. (Używaj tablic struktur i funkcji.) - Typów generycznych (własnego `Lista[T]`), interfejsów, `match`, enumów pisanych w Finchu.
- Błędów jako wartości: błąd w `int("x")` albo `read_file` zatrzymuje program. Sprawdzaj wcześniej (`file_exists`).
- Tekstu świadomego Unicode: `.len`, `s[i]` i `upper()` działają na bajtach / ASCII. - Tekstu świadomego Unicode: `.len`, `s[i]` i `upper()` działają na bajtach / ASCII.
- Wątków. - Wątków.
- Platform innych niż Linux i Windows na x86-64 (macOS, ARM). - Windowsa na ARM64, systemów 32-bitowych, WebAssembly.
Plan rozwoju jest w [README](../../README.pl.md#plan-rozwoju). Plan rozwoju jest w [README](../../README.pl.md#plan-rozwoju).

View file

@ -16,7 +16,7 @@ Cały Finch na jednej stronie. Wyjaśnienia są w [przewodnikach](../../README.p
| `finch lsp` | serwer języka (korzystają z niego edytory) | | `finch lsp` | serwer języka (korzystają z niego edytory) |
| `finch version` | wersja | | `finch version` | wersja |
| `-l lib` | dolinkuj bibliotekę C (jak `link "lib"`) | | `-l lib` | dolinkuj bibliotekę C (jak `link "lib"`) |
| `--target windows` / `linux` / triple | zbuduj dla innego systemu | | `--target windows` / `linux` / `arm64` / `macos` / triple | zbuduj dla innego systemu |
| `-g` | informacje dla debuggera (gdb, lldb, Visual Studio) | | `-g` | informacje dla debuggera (gdb, lldb, Visual Studio) |
| `-O0` | bez optymalizacji | | `-O0` | bez optymalizacji |
@ -71,6 +71,7 @@ Jedna instrukcja na linijkę, bez średników, `{` w tej samej linijce, bez nawi
| `u8 u16 u32 u64` | bez znaku, z rozmiarem | | `u8 u16 u32 u64` | bez znaku, z rozmiarem |
| `f32 f64` | zmiennoprzecinkowe | | `f32 f64` | zmiennoprzecinkowe |
| `[]T` | tablica: `[1, 2, 3]`, `[]str imiona` | | `[]T` | tablica: `[1, 2, 3]`, `[]str imiona` |
| `map[K]V` | mapa: `["a": 1]`, `map[str]int ile`, `[:]` |
| `ptr[T]`, `ptr` | wskaźnik, wskaźnik bez typu | | `ptr[T]`, `ptr` | wskaźnik, wskaźnik bez typu |
| `Nazwa` / `modul.Nazwa` | struktura | | `Nazwa` / `modul.Nazwa` | struktura |
@ -100,6 +101,8 @@ if a > b { … } else if a == b { … } else { … }
while x > 0 { … } while x > 0 { … }
for i in 0..10 { … } // 0 … 9 for i in 0..10 { … } // 0 … 9
for el in lista { … } // każdy element (albo znak str) for el in lista { … } // każdy element (albo znak str)
for i, el in lista { … } // razem z indeksem
for klucz, wart in m { … } // każdy wpis mapy, w kolejności dodawania
break continue return wartosc break continue return wartosc
defer sprzatanie() // uruchomi się na końcu bloku defer sprzatanie() // uruchomi się na końcu bloku
``` ```
@ -121,6 +124,47 @@ Parametry zachowują się jak kopie. Kolejność w pliku nie ma znaczenia. Przec
--- ---
## Metody
```c
fn Punkt.przesun(int dx, int dy) { // self to Punkt, na którym wywołano (nie kopia)
self.x += dx
self.y += dy
}
fn Punkt.odleglosc() -> float { … }
p.przesun(1, 2) lista[0].przesun(1, 1) wsk_na_punkt.przesun(0, 1)
```
Także na strukturach z C: `fn Vector2.length() -> f32 { … }`.
---
## Błędy jako wartości
```c
fn parsuj(str s) -> int! { // ! = może się nie udać
if s.len == 0 {
return error("pusty")
}
return int(s) or { return error("to nie liczba: " + s) }
}
fn zapisz(str p, str t) -> ! { // może się nie udać, bez wyniku
try write_file(p, t)
}
```
| Kod | Przy porażce |
|---|---|
| `x := parsuj(s) or 0` | użyj 0 |
| `x := parsuj(s) or { print(err); return }` | wykonaj blok (`err`: komunikat) |
| `x := try parsuj(s)` | zakończ porażką też bieżącą funkcję |
| `fn main() -> !` | porażka wypisuje `error: …`, kod wyjścia 1 |
Funkcje wbudowane, które mogą się nie udać: `int(s)`, `float(s)`, `read_file`, `write_file`, `delete_file`.
---
## Struktury ## Struktury
| Kod | Znaczenie | | Kod | Znaczenie |
@ -155,6 +199,23 @@ Parametry zachowują się jak kopie. Kolejność w pliku nie ma znaczenia. Przec
--- ---
## Mapy `map[K]V`
| Kod | Daje |
|---|---|
| `m := ["a": 1, "b": 2]` | mapa (klucze: liczby, char, bool, str) |
| `m[k]` | wartość (brak klucza: program się zatrzymuje) |
| `m[k] = v` | dodaj / zastąp |
| `m[k] += 1`, `m[k].push(x)` | brakujący klucz zaczyna od wartości domyślnej (0, `[]`, …) |
| `m.len` | liczba kluczy |
| `m.has(k)` | czy jest |
| `m.get(k, domyslna)` | wartość albo domyślna |
| `m.remove(k)` | bool: czy był |
| `m.clear()` | wyczyść |
| `m.keys()`, `m.values()` | tablice, w kolejności dodawania |
---
## Tekst `str` ## Tekst `str`
| Kod | Daje | | Kod | Daje |
@ -189,6 +250,8 @@ Parametry zachowują się jak kopie. Kolejność w pliku nie ma znaczenia. Przec
| `char(66)` | `'B'` | | `char(66)` | `'B'` |
| `bool(0)` | `false` | | `bool(0)` | `false` |
| `str(42)`, `str(1.5)`, `str(true)`, `str('c')` | tekst | | `str(42)`, `str(1.5)`, `str(true)`, `str('c')` | tekst |
| `str([1, 2])`, `str(m)`, `str(punkt)` | ten sam tekst, który pokazuje `print` |
| `int("x") or 0`, `float(s) or 0.0` | liczba albo wartość zapasowa |
| `str(bajty)` | `[]u8` → tekst | | `str(bajty)` | `[]u8` → tekst |
| `str(p)` | `char*` z C → tekst | | `str(p)` | `char*` z C → tekst |
| `ptr(p)`, `ptr(16)` | wskaźnik bez typu, ze wskaźnika albo liczby | | `ptr(p)`, `ptr(16)` | wskaźnik bez typu, ze wskaźnika albo liczby |
@ -210,6 +273,7 @@ Automatycznie tylko wtedy, gdy nic nie ginie: mniejsza → większa liczba, dowo
| `delete_file(sciezka)` | usuń plik | | `delete_file(sciezka)` | usuń plik |
| `shell(polecenie)` | uruchom polecenie, jego kod wyjścia | | `shell(polecenie)` | uruchom polecenie, jego kod wyjścia |
| `exit(kod)` | zakończ program od razu | | `exit(kod)` | zakończ program od razu |
| `error(komunikat)` | `return error("…")` w funkcji `-> T!` |
| `addr(x)` | wskaźnik na x; `addr(fn)` = callback dla C | | `addr(x)` | wskaźnik na x; `addr(fn)` = callback dla C |
| `new(wartosc)` | wartość na stercie → `ptr[T]` | | `new(wartosc)` | wartość na stercie → `ptr[T]` |
| `free(p)` | oddaj ją | | `free(p)` | oddaj ją |
@ -281,11 +345,17 @@ ksztalty.Pudlo inne // w deklaracjach
| `pop() on an empty array` | `pop()` na pustej tablicy | | `pop() on an empty array` | `pop()` na pustej tablicy |
| `used .value on a null pointer` | `.value` / `.pole` / `[i]` przez `null` | | `used .value on a null pointer` | `.value` / `.pole` / `[i]` przez `null` |
| `can't turn "x" into int` | `int("x")` | | `can't turn "x" into int` | `int("x")` |
| `can't read the file "…"` | `read_file` | | `can't read the file "…"` | `read_file` (bez `or`) |
| `the key "x" is not in the map` | `m["x"]` |
--- ---
## VS Code (rozszerzenie „Finch”) ## Edytory
Kate: `editors/kate/install.sh`, potem włącz wtyczkę **LSP Client**; uruchamianie przez **Build & Run** → `finch run %f`.
Każdy edytor z LSP: `finch lsp`.
### VS Code (rozszerzenie „Finch”)
| Klawisz / akcja | Robi | | Klawisz / akcja | Robi |
|---|---| |---|---|
@ -295,7 +365,7 @@ ksztalty.Pudlo inne // w deklaracjach
| najechanie myszą | typ / sygnatura / opis | | najechanie myszą | typ / sygnatura / opis |
| `Ctrl+Spacja` | podpowiedzi (po `.`: pola, metody, zawartość modułu) | | `Ctrl+Spacja` | podpowiedzi (po `.`: pola, metody, zawartość modułu) |
| `Ctrl+Shift+O` | funkcje i struktury w pliku | | `Ctrl+Shift+O` | funkcje i struktury w pliku |
| `main`, `fn`, `fnr`, `struct`, `for`, `foreach`, `if`, `ife`, `while`, `input` + Tab | szablony | | `main`, `fn`, `fnr`, `fnf`, `method`, `struct`, `for`, `foreach`, `forkv`, `map`, `orb`, `if`, `ife`, `while`, `input` + Tab | szablony |
| ustawienie `finch.path` | gdzie jest finch / finch.exe | | ustawienie `finch.path` | gdzie jest finch / finch.exe |
--- ---
@ -314,3 +384,7 @@ ksztalty.Pudlo inne // w deklaracjach
| `import` / `link` | zaimportuj / dolinkuj | | `import` / `link` | zaimportuj / dolinkuj |
| `struct` | struktura | | `struct` | struktura |
| `defer` | odłóż (na koniec bloku) | | `defer` | odłóż (na koniec bloku) |
| `or` | albo (co zrobić przy porażce) |
| `try` | spróbuj (porażkę przekaż dalej) |
| `self` | sam (w metodzie: struktura, na której ją wywołano) |
| `map` | mapa |

View file

@ -5,7 +5,7 @@
Install: copy to ~/.local/share/org.kde.syntax-highlighting/syntax/finch.xml Install: copy to ~/.local/share/org.kde.syntax-highlighting/syntax/finch.xml
(or run editors/kate/install.sh), then restart Kate. (or run editors/kate/install.sh), then restart Kate.
--> -->
<language name="Finch" version="1" kateversion="5.0" section="Sources" extensions="*.fch" <language name="Finch" version="2" kateversion="5.0" section="Sources" extensions="*.fch"
mimetype="text/x-finch" indenter="cstyle" author="The Finch authors" license="Apache-2.0"> mimetype="text/x-finch" indenter="cstyle" author="The Finch authors" license="Apache-2.0">
<highlighting> <highlighting>
<list name="keywords"> <list name="keywords">
@ -22,6 +22,8 @@
<item>continue</item> <item>continue</item>
<item>return</item> <item>return</item>
<item>defer</item> <item>defer</item>
<item>or</item>
<item>try</item>
</list> </list>
<list name="imports"> <list name="imports">
<item>import</item> <item>import</item>
@ -31,6 +33,7 @@
<item>true</item> <item>true</item>
<item>false</item> <item>false</item>
<item>null</item> <item>null</item>
<item>self</item>
</list> </list>
<list name="types"> <list name="types">
<item>int</item> <item>int</item>
@ -39,6 +42,7 @@
<item>char</item> <item>char</item>
<item>str</item> <item>str</item>
<item>ptr</item> <item>ptr</item>
<item>map</item>
<item>i8</item> <item>i8</item>
<item>i16</item> <item>i16</item>
<item>i32</item> <item>i32</item>
@ -62,6 +66,7 @@
<item>addr</item> <item>addr</item>
<item>new</item> <item>new</item>
<item>free</item> <item>free</item>
<item>error</item>
</list> </list>
<contexts> <contexts>
@ -99,8 +104,11 @@
<AnyChar String="()[],.:" attribute="Symbol"/> <AnyChar String="()[],.:" attribute="Symbol"/>
</context> </context>
<!-- fn name( or fn Type.method( -->
<context name="FunctionName" attribute="Normal Text" lineEndContext="#pop" fallthroughContext="#pop"> <context name="FunctionName" attribute="Normal Text" lineEndContext="#pop" fallthroughContext="#pop">
<DetectSpaces/> <DetectSpaces/>
<RegExpr String="[A-Za-z_][A-Za-z0-9_]*(?=\.)" attribute="Data Type"/>
<DetectChar char="." attribute="Symbol"/>
<RegExpr String="[A-Za-z_][A-Za-z0-9_]*" attribute="Function Definition" context="#pop"/> <RegExpr String="[A-Za-z_][A-Za-z0-9_]*" attribute="Function Definition" context="#pop"/>
</context> </context>

View file

@ -1,5 +1,11 @@
# Changelog # Changelog
## 2.4.0
- Highlighting for methods (`fn Point.move`), maps (`map[K]V`), `or`, `try`, `self` and `error(...)`.
- Completion for struct methods and map methods; parameter hints for method calls.
- Snippets: `method`, `fnf` (a function that can fail), `map`, `forkv`, `orb`.
## 2.3.0 ## 2.3.0
- First release: highlighting, snippets, language server, Run and Build. - First release: highlighting, snippets, language server, Run and Build.

View file

@ -1,12 +1,12 @@
{ {
"name": "finch-lang", "name": "finch-lang",
"version": "2.3.0", "version": "2.4.0",
"lockfileVersion": 3, "lockfileVersion": 3,
"requires": true, "requires": true,
"packages": { "packages": {
"": { "": {
"name": "finch-lang", "name": "finch-lang",
"version": "2.3.0", "version": "2.4.0",
"license": "Apache-2.0", "license": "Apache-2.0",
"dependencies": { "dependencies": {
"vscode-languageclient": "^9.0.1" "vscode-languageclient": "^9.0.1"

View file

@ -2,7 +2,7 @@
"name": "finch-lang", "name": "finch-lang",
"displayName": "Finch", "displayName": "Finch",
"description": "The Finch programming language: highlighting, errors as you type, completion, hover, go to definition, and a Run button.", "description": "The Finch programming language: highlighting, errors as you type, completion, hover, go to definition, and a Run button.",
"version": "2.3.0", "version": "2.4.0",
"publisher": "nexoniarz", "publisher": "nexoniarz",
"license": "Apache-2.0", "license": "Apache-2.0",
"icon": "images/icon.png", "icon": "images/icon.png",

View file

@ -1,16 +1,160 @@
{ {
"main": { "prefix": "main", "body": ["fn main() {", "\t$0", "}"], "description": "The program's start" }, "main": {
"main with arguments": { "prefix": "mainargs", "body": ["fn main([]str args) {", "\t$0", "}"], "description": "main that gets the command-line arguments" }, "prefix": "main",
"function": { "prefix": "fn", "body": ["fn ${1:name}(${2}) {", "\t$0", "}"], "description": "A function" }, "body": [
"function with result": { "prefix": "fnr", "body": ["fn ${1:name}(${2}) -> ${3:int} {", "\t$0", "}"], "description": "A function that returns a value" }, "fn main() {",
"struct": { "prefix": "struct", "body": ["struct ${1:Name} {", "\t${2:int} ${3:field}", "}"], "description": "A struct" }, "\t$0",
"if": { "prefix": "if", "body": ["if ${1:condition} {", "\t$0", "}"] }, "}"
"if else": { "prefix": "ife", "body": ["if ${1:condition} {", "\t$2", "} else {", "\t$0", "}"] }, ],
"for range": { "prefix": "for", "body": ["for ${1:i} in ${2:0}..${3:n} {", "\t$0", "}"], "description": "Count from start to end-1" }, "description": "The program's start"
"for each": { "prefix": "foreach", "body": ["for ${1:item} in ${2:list} {", "\t$0", "}"], "description": "Every element of an array or str" }, },
"while": { "prefix": "while", "body": ["while ${1:condition} {", "\t$0", "}"] }, "main with arguments": {
"print": { "prefix": "print", "body": ["print(${1})"] }, "prefix": "mainargs",
"input": { "prefix": "input", "body": ["${1:answer} := input(\"${2:Question? }\")"] }, "body": [
"import C header": { "prefix": "importc", "body": ["import \"${1:stdio.h}\""] }, "fn main([]str args) {",
"defer": { "prefix": "defer", "body": ["defer ${1}"], "description": "Run this when the block ends" } "\t$0",
"}"
],
"description": "main that gets the command-line arguments"
},
"function": {
"prefix": "fn",
"body": [
"fn ${1:name}(${2}) {",
"\t$0",
"}"
],
"description": "A function"
},
"function with result": {
"prefix": "fnr",
"body": [
"fn ${1:name}(${2}) -> ${3:int} {",
"\t$0",
"}"
],
"description": "A function that returns a value"
},
"struct": {
"prefix": "struct",
"body": [
"struct ${1:Name} {",
"\t${2:int} ${3:field}",
"}"
],
"description": "A struct"
},
"if": {
"prefix": "if",
"body": [
"if ${1:condition} {",
"\t$0",
"}"
]
},
"if else": {
"prefix": "ife",
"body": [
"if ${1:condition} {",
"\t$2",
"} else {",
"\t$0",
"}"
]
},
"for range": {
"prefix": "for",
"body": [
"for ${1:i} in ${2:0}..${3:n} {",
"\t$0",
"}"
],
"description": "Count from start to end-1"
},
"for each": {
"prefix": "foreach",
"body": [
"for ${1:item} in ${2:list} {",
"\t$0",
"}"
],
"description": "Every element of an array or str"
},
"while": {
"prefix": "while",
"body": [
"while ${1:condition} {",
"\t$0",
"}"
]
},
"print": {
"prefix": "print",
"body": [
"print(${1})"
]
},
"input": {
"prefix": "input",
"body": [
"${1:answer} := input(\"${2:Question? }\")"
]
},
"import C header": {
"prefix": "importc",
"body": [
"import \"${1:stdio.h}\""
]
},
"defer": {
"prefix": "defer",
"body": [
"defer ${1}"
],
"description": "Run this when the block ends"
},
"method": {
"prefix": "method",
"body": [
"fn ${1:Type}.${2:name}(${3}) {",
"\t$0",
"}"
],
"description": "A method: self is the struct it is called on"
},
"fallible function": {
"prefix": "fnf",
"body": [
"fn ${1:name}(${2}) -> ${3:int}! {",
"\t$0",
"}"
],
"description": "A function that can fail: return error(\"...\")"
},
"map": {
"prefix": "map",
"body": [
"map[${1:str}]${2:int} ${3:name}"
],
"description": "An empty map"
},
"or block": {
"prefix": "orb",
"body": [
"or {",
"\t${1:print(err)}",
"\t${2:return}",
"}"
],
"description": "Handle a failure: err is the message"
},
"for key, value": {
"prefix": "forkv",
"body": [
"for ${1:key}, ${2:value} in ${3:map} {",
"\t$0",
"}"
],
"description": "Every key and value of a map (or index and element of an array)"
}
} }

View file

@ -85,12 +85,18 @@
} }
}, },
"function": { "function": {
"match": "\\b(fn)\\s+([A-Za-z_][A-Za-z0-9_]*)", "match": "\\b(fn)\\s+(?:([A-Za-z_][A-Za-z0-9_]*)(\\.))?([A-Za-z_][A-Za-z0-9_]*)",
"captures": { "captures": {
"1": { "1": {
"name": "storage.type.function.finch" "name": "storage.type.function.finch"
}, },
"2": { "2": {
"name": "entity.name.type.finch"
},
"3": {
"name": "punctuation.accessor.finch"
},
"4": {
"name": "entity.name.function.finch" "name": "entity.name.function.finch"
} }
} }
@ -111,7 +117,7 @@
}, },
{ {
"name": "keyword.control.finch", "name": "keyword.control.finch",
"match": "\\b(if|else|while|for|in|break|continue|return|defer)\\b" "match": "\\b(if|else|while|for|in|break|continue|return|defer|or|try)\\b"
}, },
{ {
"name": "storage.type.function.finch", "name": "storage.type.function.finch",
@ -126,11 +132,15 @@
"match": "\\b(true|false|null)\\b" "match": "\\b(true|false|null)\\b"
}, },
{ {
"name": "storage.type.primitive.finch", "name": "variable.language.self.finch",
"match": "\\b(int|float|bool|char|str|ptr|i8|i16|i32|i64|u8|u16|u32|u64|f32|f64)\\b" "match": "\\bself\\b"
}, },
{ {
"match": "\\b(print|input|read_file|write_file|file_exists|delete_file|shell|exit|addr|new|free)\\s*(?=\\()", "name": "storage.type.primitive.finch",
"match": "\\b(int|float|bool|char|str|ptr|map|i8|i16|i32|i64|u8|u16|u32|u64|f32|f64)\\b"
},
{
"match": "\\b(print|input|read_file|write_file|file_exists|delete_file|shell|exit|addr|new|free|error)\\s*(?=\\()",
"captures": { "captures": {
"1": { "1": {
"name": "support.function.builtin.finch" "name": "support.function.builtin.finch"
@ -238,4 +248,4 @@
] ]
} }
} }
} }

View file

@ -20,6 +20,12 @@ const code = [
' p := Point(x: 1)', ' p := Point(x: 1)',
' return a + p.x', ' return a + p.x',
'}', '}',
'fn Point.move(int dx) -> int! {',
' map[str]int m',
' n := try parse(s) or 0',
' self.x += dx',
' return error("no")',
'}',
]; ];
const expect = [ // [line, token text, scope that must be present] const expect = [ // [line, token text, scope that must be present]
[0, 'import', 'keyword.control.import'], [0, '"stdio.h"', 'string.quoted.double'], [0, 'import', 'keyword.control.import'], [0, '"stdio.h"', 'string.quoted.double'],
@ -32,6 +38,11 @@ const expect = [ // [line, token text, scope that must be present]
[7, 'c', 'string.quoted.single'], [7, '3.5', 'constant.numeric.float'], [7, 'true', 'constant.language'], [7, 'null', 'constant.language'], [7, 'c', 'string.quoted.single'], [7, '3.5', 'constant.numeric.float'], [7, 'true', 'constant.language'], [7, 'null', 'constant.language'],
[8, 'Point', 'entity.name.type'], [8, 'Point', 'entity.name.type'],
[9, 'return', 'keyword.control'], [9, 'return', 'keyword.control'],
[11, 'Point', 'entity.name.type'], [11, 'move', 'entity.name.function'], [11, '!', 'keyword.operator'],
[12, 'map', 'storage.type.primitive'],
[13, 'try', 'keyword.control'], [13, 'or', 'keyword.control'],
[14, 'self', 'variable.language.self'],
[15, 'error', 'support.function.builtin'],
]; ];
registry.loadGrammar('source.finch').then((g) => { registry.loadGrammar('source.finch').then((g) => {
let state = vsctm.INITIAL, toks = []; let state = vsctm.INITIAL, toks = [];

62
examples/wordcount.fch Normal file
View file

@ -0,0 +1,62 @@
// Counts the words in a file: maps, methods and errors as values.
// finch run examples/wordcount.fch README.md
struct Counter {
map[str]int counts
int total
}
// lower-case letters and digits only: "Hello," -> "hello"
fn clean(str word) -> str {
out := ""
for c in word.lower() {
if (c >= 'a' && c <= 'z') || (c >= '0' && c <= '9') {
out += str(c)
}
}
return out
}
fn Counter.add(str text) {
for word in text.split(" ") {
w := clean(word)
if w.len > 0 {
self.counts[w] += 1 // a new word starts at 0
self.total += 1
}
}
}
// the n most frequent words, most frequent first
fn Counter.top(int n) -> []str {
words := self.counts.keys()
for i in 0..words.len {
for j in i + 1..words.len {
if self.counts[words[j]] > self.counts[words[i]] {
w := words[i]
words[i] = words[j]
words[j] = w
}
}
}
if words.len > n {
words.resize(n)
}
return words
}
fn main([]str args) -> ! {
if args.len < 2 {
return error("usage: wordcount <file>")
}
text := try read_file(args[1]) // a missing file ends the program with a clear message
Counter c
for line in text.split("\n") {
c.add(line)
}
print(c.total, "words,", c.counts.len, "different")
for i, w in c.top(10) {
print(i + 1, w, c.counts[w])
}
}

View file

@ -8,9 +8,11 @@
// cap == 0 : a string literal, valid forever, never written to // cap == 0 : a string literal, valid forever, never written to
// cap == -1: borrowed from C, must be copied before it is kept // cap == -1: borrowed from C, must be copied before it is kept
// []T = { T *ptr; int64 len; int64 cap } owns ptr when cap > 0 // []T = { T *ptr; int64 len; int64 cap } owns ptr when cap > 0
// map[K]V: see "maps" below
#include <ctype.h> #include <ctype.h>
#include <errno.h> #include <errno.h>
#include <stdarg.h>
#include <stdint.h> #include <stdint.h>
#include <stdio.h> #include <stdio.h>
#include <stdlib.h> #include <stdlib.h>
@ -148,33 +150,87 @@ void finch_str_from_char(FStr *out, char c) { str_heap(out, &c, 1); }
void finch_str_from_bool(FStr *out, int b) { str_static(out, b ? "true" : "false"); } void finch_str_from_bool(FStr *out, int b) { str_static(out, b ? "true" : "false"); }
static void bad_number(const FStr *s, const char *kind, const char *file, int64_t line) { static void number_error(char *msg, size_t size, const FStr *s, const char *kind) {
char msg[200]; snprintf(msg, size, "can't turn \"%.*s\" into %s", (int)(s->len > 100 ? 100 : s->len), s->ptr ? s->ptr : "", kind);
snprintf(msg, sizeof msg, "can't turn \"%.*s\" into %s", (int)(s->len > 100 ? 100 : s->len), s->ptr, kind);
finch_panic(file, line, msg);
} }
int64_t finch_str_to_int(const FStr *s, const char *file, int64_t line) { static int parse_int(const FStr *s, int64_t *out) {
const char *p = s->ptr; const char *p = s->ptr ? s->ptr : "";
while (isspace((unsigned char)*p)) p++; while (isspace((unsigned char)*p)) p++;
char *end; char *end;
errno = 0; errno = 0;
long long v = strtoll(p, &end, 10); long long v = strtoll(p, &end, 10);
while (isspace((unsigned char)*end)) end++; while (isspace((unsigned char)*end)) end++;
if (end == p || *end || errno == ERANGE) bad_number(s, "int", file, line); *out = v;
return v; return !(end == p || *end || errno == ERANGE);
} }
double finch_str_to_float(const FStr *s, const char *file, int64_t line) { static int parse_float(const FStr *s, double *out) {
const char *p = s->ptr; const char *p = s->ptr ? s->ptr : "";
while (isspace((unsigned char)*p)) p++; while (isspace((unsigned char)*p)) p++;
char *end; char *end;
double v = strtod(p, &end); double v = strtod(p, &end);
while (isspace((unsigned char)*end)) end++; while (isspace((unsigned char)*end)) end++;
if (end == p || *end) bad_number(s, "float", file, line); *out = v;
return !(end == p || *end);
}
int64_t finch_str_to_int(const FStr *s, const char *file, int64_t line) {
int64_t v;
if (!parse_int(s, &v)) {
char msg[200];
number_error(msg, sizeof msg, s, "int");
finch_panic(file, line, msg);
}
return v; return v;
} }
double finch_str_to_float(const FStr *s, const char *file, int64_t line) {
double v;
if (!parse_float(s, &v)) {
char msg[200];
number_error(msg, sizeof msg, s, "float");
finch_panic(file, line, msg);
}
return v;
}
// ---------- errors as values: int(s) or 0, try read_file(p) ... ----------
// Each gives back 1 on success; on failure 0, with the message in *err.
static void set_error(FStr *err, const char *msg) { str_heap(err, msg, (int64_t)strlen(msg)); }
int finch_str_try_int(const FStr *s, int64_t *out, FStr *err) {
if (parse_int(s, out)) return 1;
char msg[200];
number_error(msg, sizeof msg, s, "int");
set_error(err, msg);
*out = 0;
return 0;
}
int finch_str_try_float(const FStr *s, double *out, FStr *err) {
if (parse_float(s, out)) return 1;
char msg[200];
number_error(msg, sizeof msg, s, "float");
set_error(err, msg);
*out = 0;
return 0;
}
static void file_error(FStr *err, const char *what, const FStr *path) {
char msg[300];
snprintf(msg, sizeof msg, "can't %s the file \"%.*s\": %s", what, (int)(path->len > 200 ? 200 : path->len),
path->ptr ? path->ptr : "", strerror(errno));
set_error(err, msg);
}
// when `fn main() -> !` fails
void finch_report_error(const FStr *msg) {
fflush(stdout);
fprintf(stderr, "error: %.*s\n", (int)msg->len, msg->ptr ? msg->ptr : "");
}
void finch_str_sub(FStr *out, const FStr *s, int64_t start, int64_t end, const char *file, int64_t line) { void finch_str_sub(FStr *out, const FStr *s, int64_t start, int64_t end, const char *file, int64_t line) {
if (start < 0 || end > s->len || start > end) { if (start < 0 || end > s->len || start > end) {
char msg[160]; char msg[160];
@ -303,6 +359,58 @@ void finch_str_repeat(FStr *out, const FStr *s, int64_t n) {
void finch_str_from_bytes(FStr *out, const FArr *bytes) { str_heap(out, bytes->ptr, bytes->len); } void finch_str_from_bytes(FStr *out, const FArr *bytes) { str_heap(out, bytes->ptr, bytes->len); }
// ---------- building text: print(x) and str(x) of arrays, maps and structs ----------
// A buffer starts as an empty str and grows on the heap.
void finch_buf_add(FStr *b, const char *p, int64_t n) {
if (b->len + n + 1 > b->cap) {
int64_t cap = b->cap > 0 ? b->cap * 2 : 32;
while (cap < b->len + n + 1) cap *= 2;
b->ptr = must(realloc(b->cap > 0 ? b->ptr : NULL, (size_t)cap));
b->cap = cap;
}
if (n) memcpy(b->ptr + b->len, p, (size_t)n);
b->len += n;
b->ptr[b->len] = 0;
}
static void buf_fmt(FStr *b, const char *fmt, ...) __attribute__((format(printf, 2, 3)));
static void buf_fmt(FStr *b, const char *fmt, ...) {
char t[64];
va_list ap;
va_start(ap, fmt);
int n = vsnprintf(t, sizeof t, fmt, ap);
va_end(ap);
finch_buf_add(b, t, n);
}
void finch_buf_int(FStr *b, int64_t v) { buf_fmt(b, "%lld", (long long)v); }
void finch_buf_uint(FStr *b, uint64_t v) { buf_fmt(b, "%llu", (unsigned long long)v); }
void finch_buf_float(FStr *b, double v) { buf_fmt(b, "%g", v); }
void finch_buf_bool(FStr *b, int v) { finch_buf_add(b, v ? "true" : "false", v ? 4 : 5); }
void finch_buf_ptr(FStr *b, const void *p) {
if (p) buf_fmt(b, "0x%llx", (unsigned long long)(uintptr_t)p);
else finch_buf_add(b, "null", 4);
}
void finch_buf_char(FStr *b, char c, int quoted) {
if (quoted) finch_buf_add(b, "'", 1);
finch_buf_add(b, &c, 1);
if (quoted) finch_buf_add(b, "'", 1);
}
void finch_buf_str(FStr *b, const FStr *s, int quoted) {
if (quoted) finch_buf_add(b, "\"", 1);
finch_buf_add(b, s->ptr, s->len);
if (quoted) finch_buf_add(b, "\"", 1);
}
// print the buffer and free it
void finch_buf_print(FStr *b) {
if (b->len) fwrite(b->ptr, 1, (size_t)b->len, stdout);
if (b->cap > 0) free(b->ptr);
}
// ---------- arrays ---------- // ---------- arrays ----------
void finch_arr_reserve(FArr *a, int64_t need, int64_t es) { void finch_arr_reserve(FArr *a, int64_t need, int64_t es) {
@ -409,8 +517,27 @@ int finch_file_exists(const FStr *path) {
return f != NULL; return f != NULL;
} }
int finch_try_read_file(FStr *out, const FStr *path, FStr *err) {
FILE *f = fopen(path->ptr ? path->ptr : "", "rb");
if (!f) {
file_error(err, "read", path);
return 0;
}
fseek(f, 0, SEEK_END);
long size = ftell(f);
fseek(f, 0, SEEK_SET);
char *m = finch_alloc(size + 1);
size_t got = fread(m, 1, (size_t)size, f);
fclose(f);
m[got] = 0;
out->ptr = m;
out->len = (int64_t)got;
out->cap = size + 1;
return 1;
}
void finch_read_file(FStr *out, const FStr *path, const char *file, int64_t line) { void finch_read_file(FStr *out, const FStr *path, const char *file, int64_t line) {
FILE *f = fopen(path->ptr, "rb"); FILE *f = fopen(path->ptr ? path->ptr : "", "rb");
if (!f) { if (!f) {
char msg[300]; char msg[300];
snprintf(msg, sizeof msg, "can't read the file \"%.*s\": %s", (int)(path->len > 200 ? 200 : path->len), snprintf(msg, sizeof msg, "can't read the file \"%.*s\": %s", (int)(path->len > 200 ? 200 : path->len),
@ -444,12 +571,174 @@ int64_t finch_shell(const FStr *cmd) {
int finch_delete_file(const FStr *path) { return remove(path->ptr ? path->ptr : "") == 0; } int finch_delete_file(const FStr *path) { return remove(path->ptr ? path->ptr : "") == 0; }
int finch_try_delete_file(const FStr *path, FStr *err) {
if (remove(path->ptr ? path->ptr : "") == 0) return 1;
file_error(err, "delete", path);
return 0;
}
// stdin/stdout/stderr are macros on some systems (Windows), so Finch asks for them here // stdin/stdout/stderr are macros on some systems (Windows), so Finch asks for them here
void *finch_std_stream(int i) { return i == 0 ? (void *)stdin : i == 1 ? (void *)stdout : (void *)stderr; } void *finch_std_stream(int i) { return i == 0 ? (void *)stdin : i == 1 ? (void *)stdout : (void *)stderr; }
int finch_write_file(const FStr *path, const FStr *text) { int finch_write_file(const FStr *path, const FStr *text) {
FILE *f = fopen(path->ptr, "wb"); FILE *f = fopen(path->ptr ? path->ptr : "", "wb");
if (!f) return 0; if (!f) return 0;
size_t put = fwrite(text->ptr, 1, (size_t)text->len, f); size_t put = text->len ? fwrite(text->ptr, 1, (size_t)text->len, f) : 0;
return fclose(f) == 0 && put == (size_t)text->len; return fclose(f) == 0 && put == (size_t)text->len;
} }
int finch_try_write_file(const FStr *path, const FStr *text, FStr *err) {
errno = 0;
if (finch_write_file(path, text)) return 1;
if (!errno) errno = EIO;
file_error(err, "write", path);
return 0;
}
// ---------- maps ----------
// map[K]V = { entries; len; used; cap; index; icap }
// entries: `used` slots of es bytes, each { uint64 hash; K key; V value }, in insertion order.
// A removed entry keeps its slot (hash 0) until the next resize packs the slots.
// index: icap slots (a power of two, at least 2 * cap): -1 empty, -2 removed, else an entry number.
// The compiler stores, copies and drops keys and values; the runtime only finds and places entries.
// Keys are compared byte for byte (numbers, chars, bools) or as text (kstr).
typedef struct {
char *entries;
int64_t len, used, cap;
int64_t *index;
int64_t icap;
} FMap;
static uint64_t map_hash(const void *key, int64_t ks, int kstr) {
const unsigned char *p;
size_t n;
if (kstr) {
const FStr *s = key;
p = (const unsigned char *)s->ptr;
n = (size_t)s->len;
} else {
p = key;
n = (size_t)ks;
}
uint64_t h = 1469598103934665603ULL; // FNV-1a, then mixed so the low bits spread well
for (size_t i = 0; i < n; i++) {
h ^= p[i];
h *= 1099511628211ULL;
}
h ^= h >> 31;
h *= 0xbf58476d1ce4e5b9ULL;
h ^= h >> 29;
return h | 1; // 0 marks a removed entry
}
static int map_key_eq(const char *a, const void *b, int64_t ks, int kstr) {
if (!kstr) return memcmp(a, b, (size_t)ks) == 0;
const FStr *x = (const FStr *)a, *y = b;
return x->len == y->len && (x->len == 0 || memcmp(x->ptr, y->ptr, (size_t)x->len) == 0);
}
static void map_reindex(FMap *m, int64_t es) {
if (m->len != m->used) { // pack the slots of removed entries away
int64_t j = 0;
for (int64_t i = 0; i < m->used; i++) {
char *e = m->entries + i * es;
if (*(uint64_t *)e == 0) continue;
if (i != j) memmove(m->entries + j * es, e, (size_t)es);
j++;
}
m->used = j;
}
int64_t icap = 16;
while (icap < m->cap * 2) icap *= 2;
free(m->index);
m->index = finch_alloc(icap * (int64_t)sizeof(int64_t));
memset(m->index, 0xff, (size_t)icap * sizeof(int64_t));
m->icap = icap;
for (int64_t i = 0; i < m->used; i++) {
uint64_t h = *(uint64_t *)(m->entries + i * es);
int64_t s = (int64_t)(h & (uint64_t)(icap - 1));
while (m->index[s] != -1) s = (s + 1) & (icap - 1);
m->index[s] = i;
}
}
// the entry number of `key`, or -1
int64_t finch_map_find(const FMap *m, const void *key, int64_t es, int64_t ks, int32_t kstr) {
if (m->len == 0) return -1;
uint64_t h = map_hash(key, ks, kstr);
for (int64_t s = (int64_t)(h & (uint64_t)(m->icap - 1));; s = (s + 1) & (m->icap - 1)) {
int64_t i = m->index[s];
if (i == -1) return -1;
if (i < 0) continue;
char *e = m->entries + i * es;
if (*(uint64_t *)e == h && map_key_eq(e + 8, key, ks, kstr)) return i;
}
}
// The entry for `key`. A new one is zeroed except its hash (*added = 1); the compiler then stores the key.
int64_t finch_map_slot(FMap *m, const void *key, int64_t es, int64_t ks, int32_t kstr, int32_t *added) {
int64_t i = finch_map_find(m, key, es, ks, kstr);
*added = i < 0;
if (i >= 0) return i;
if (m->used == m->cap) {
if (m->len * 4 < m->cap * 3) { // many removed slots: pack them instead of growing
map_reindex(m, es);
} else {
m->cap = m->cap ? m->cap * 2 : 8;
m->entries = must(realloc(m->entries, (size_t)(m->cap * es)));
map_reindex(m, es);
}
}
uint64_t h = map_hash(key, ks, kstr);
i = m->used++;
char *e = m->entries + i * es;
memset(e, 0, (size_t)es);
*(uint64_t *)e = h;
int64_t s = (int64_t)(h & (uint64_t)(m->icap - 1));
while (m->index[s] >= 0) s = (s + 1) & (m->icap - 1);
m->index[s] = i;
m->len++;
return i;
}
// Forget entry i (its key and value were already dropped by the compiler).
void finch_map_remove_at(FMap *m, int64_t i, int64_t es) {
char *e = m->entries + i * es;
uint64_t h = *(uint64_t *)e;
int64_t s = (int64_t)(h & (uint64_t)(m->icap - 1));
while (m->index[s] != i) s = (s + 1) & (m->icap - 1);
m->index[s] = -2;
*(uint64_t *)e = 0;
if (--m->len == 0) {
m->used = 0;
memset(m->index, 0xff, (size_t)m->icap * sizeof(int64_t));
}
}
void finch_map_clear(FMap *m) {
m->len = m->used = 0;
if (m->index) memset(m->index, 0xff, (size_t)m->icap * sizeof(int64_t));
}
void finch_map_free(FMap *m) {
free(m->entries);
free(m->index);
}
// A copy of the slots and the index; the compiler then copies the keys and values that own memory.
void finch_map_clone_raw(FMap *out, const FMap *m, int64_t es) {
*out = *m;
if (!m->cap) return;
out->entries = finch_alloc(m->cap * es);
memcpy(out->entries, m->entries, (size_t)(m->used * es));
out->index = finch_alloc(m->icap * (int64_t)sizeof(int64_t));
memcpy(out->index, m->index, (size_t)m->icap * sizeof(int64_t));
}
__attribute__((noreturn, cold)) void finch_map_missing(const char *file, int64_t line, const FStr *key) {
char msg[200];
snprintf(msg, sizeof msg, "the key %.*s is not in the map (check with .has(key), or use .get(key, default))",
(int)(key->len > 100 ? 100 : key->len), key->ptr ? key->ptr : "");
finch_panic(file, line, msg);
}

View file

@ -11,12 +11,14 @@ struct Type {
Void, Bool, Char, I8, I16, I32, I64, U8, U16, U32, U64, F32, F64, Void, Bool, Char, I8, I16, I32, I64, U8, U16, U32, U64, F32, F64,
Str, Ptr, Null, Str, Ptr, Null,
Array, // []T: growable, owns its elements Array, // []T: growable, owns its elements
Map, // map[K]V: a hash map that keeps insertion order, owns its keys and values
Fixed, // [N]T: only inside C structs Fixed, // [N]T: only inside C structs
Struct, // a resolved struct Struct, // a resolved struct
Named, // a struct name as written, before codegen resolves it Named, // a struct name as written, before codegen resolves it
}; };
Kind kind = Void; Kind kind = Void;
std::shared_ptr<Type> elem; // Ptr (null = untyped `ptr`), Array, Fixed std::shared_ptr<Type> elem; // Ptr (null = untyped `ptr`), Array, Fixed; Map: the value
std::shared_ptr<Type> key; // Map
long long count = 0; // Fixed long long count = 0; // Fixed
std::string name; // Named / Struct std::string name; // Named / Struct
std::string module; // Named: written qualifier ("" = none); Struct: defining module std::string module; // Named: written qualifier ("" = none); Struct: defining module
@ -34,6 +36,12 @@ struct Type {
a.elem = std::make_shared<Type>(t); a.elem = std::make_shared<Type>(t);
return a; return a;
} }
static Type mapOf(const Type &k, const Type &v) {
Type m(Map);
m.key = std::make_shared<Type>(k);
m.elem = std::make_shared<Type>(v);
return m;
}
bool isInt() const { return kind >= I8 && kind <= U64; } bool isInt() const { return kind >= I8 && kind <= U64; }
bool isSigned() const { return kind >= I8 && kind <= I64; } bool isSigned() const { return kind >= I8 && kind <= I64; }
@ -58,6 +66,7 @@ struct Type {
if (!elem || !o.elem) return !elem && !o.elem; if (!elem || !o.elem) return !elem && !o.elem;
return *elem == *o.elem; return *elem == *o.elem;
case Array: return *elem == *o.elem; case Array: return *elem == *o.elem;
case Map: return *key == *o.key && *elem == *o.elem;
case Fixed: return count == o.count && *elem == *o.elem; case Fixed: return count == o.count && *elem == *o.elem;
case Struct: return info == o.info; case Struct: return info == o.info;
case Named: return name == o.name && module == o.module; case Named: return name == o.name && module == o.module;
@ -86,6 +95,7 @@ struct Type {
case Ptr: return elem ? "ptr[" + elem->show() + "]" : "ptr"; case Ptr: return elem ? "ptr[" + elem->show() + "]" : "ptr";
case Null: return "null"; case Null: return "null";
case Array: return "[]" + elem->show(); case Array: return "[]" + elem->show();
case Map: return "map[" + key->show() + "]" + elem->show();
case Fixed: return "[" + std::to_string(count) + "]" + elem->show(); case Fixed: return "[" + std::to_string(count) + "]" + elem->show();
case Struct: case Struct:
case Named: return module.empty() ? name : module + "." + name; case Named: return module.empty() ? name : module + "." + name;
@ -104,7 +114,8 @@ struct Pos {
// ---------- expressions ---------- // ---------- expressions ----------
enum class ExprKind { Int, Float, Bool, Char, Str, Null, Var, Unary, Binary, Call, Member, Index, ArrayLit, Method }; enum class ExprKind { Int, Float, Bool, Char, Str, Null, Var, Unary, Binary, Call, Member, Index, ArrayLit, Method,
MapLit, Try, OrElse };
struct Expr { struct Expr {
ExprKind kind; ExprKind kind;
@ -179,7 +190,11 @@ struct ArrayLitExpr : Expr { // [1, 2, 3]
std::vector<ExprPtr> elems; std::vector<ExprPtr> elems;
explicit ArrayLitExpr(Pos p) : Expr(ExprKind::ArrayLit, p) {} explicit ArrayLitExpr(Pos p) : Expr(ExprKind::ArrayLit, p) {}
}; };
// obj.name(args): a.push(x), s.find("x"), and math.sqrt(2) / math.Vec(1, 2) for modules struct MapLitExpr : Expr { // ["a": 1, "b": 2], or [:] for an empty map
std::vector<ExprPtr> keys, values;
explicit MapLitExpr(Pos p) : Expr(ExprKind::MapLit, p) {}
};
// obj.name(args): a.push(x), s.find("x"), p.move(1, 2), and math.sqrt(2) / math.Vec(1, 2) for modules
struct MethodExpr : Expr { struct MethodExpr : Expr {
ExprPtr obj; ExprPtr obj;
std::string name; std::string name;
@ -188,6 +203,21 @@ struct MethodExpr : Expr {
MethodExpr(Pos p, ExprPtr o, std::string n) : Expr(ExprKind::Method, p), obj(std::move(o)), name(std::move(n)) {} MethodExpr(Pos p, ExprPtr o, std::string n) : Expr(ExprKind::Method, p), obj(std::move(o)), name(std::move(n)) {}
}; };
struct BlockStmt;
// try f(x): if f fails, the current function fails with the same error
struct TryExpr : Expr {
ExprPtr call;
TryExpr(Pos p, ExprPtr c) : Expr(ExprKind::Try, p), call(std::move(c)) {}
};
// f(x) or fallback, f(x) or { ...err... }
struct OrElseExpr : Expr {
ExprPtr call;
ExprPtr fallback; // or a value
std::unique_ptr<BlockStmt> block; // or a block that sees `err`
OrElseExpr(Pos p, ExprPtr c) : Expr(ExprKind::OrElse, p), call(std::move(c)) {}
~OrElseExpr() override;
};
// ---------- statements ---------- // ---------- statements ----------
enum class StmtKind { VarDecl, Assign, Expr, If, While, For, ForEach, Return, Break, Continue, Block, Defer }; enum class StmtKind { VarDecl, Assign, Expr, If, While, For, ForEach, Return, Break, Continue, Block, Defer };
@ -206,6 +236,7 @@ struct BlockStmt : Stmt {
explicit BlockStmt(Pos p) : Stmt(StmtKind::Block, p) {} explicit BlockStmt(Pos p) : Stmt(StmtKind::Block, p) {}
}; };
using BlockPtr = std::unique_ptr<BlockStmt>; using BlockPtr = std::unique_ptr<BlockStmt>;
inline OrElseExpr::~OrElseExpr() = default;
struct VarDeclStmt : Stmt { struct VarDeclStmt : Stmt {
bool hasType; // `int x = 5` vs `x := 5` bool hasType; // `int x = 5` vs `x := 5`
@ -246,7 +277,9 @@ struct ForStmt : Stmt {
struct ForEachStmt : Stmt { struct ForEachStmt : Stmt {
std::string var; std::string var;
Pos varPos; Pos varPos;
ExprPtr list; // for var in list (an array or a str) std::string var2; // for i, x in list / for k, v in map ("" = one variable)
Pos var2Pos;
ExprPtr list; // for var in list (an array, a str or a map)
BlockPtr body; BlockPtr body;
ForEachStmt(Pos p) : Stmt(StmtKind::ForEach, p) {} ForEachStmt(Pos p) : Stmt(StmtKind::ForEach, p) {}
}; };
@ -278,7 +311,10 @@ struct FnDecl {
Pos pos; Pos pos;
Pos namePos; Pos namePos;
std::string name; std::string name;
std::string recv; // fn Point.move(...): "Point" (a method; `self` is the Point it is called on)
Pos recvPos;
Type ret; // Void when there is no `-> type` Type ret; // Void when there is no `-> type`
bool fallible = false; // -> int! / -> ! : can fail with an error
std::vector<Param> params; std::vector<Param> params;
BlockPtr body; BlockPtr body;
}; };

View file

@ -11,18 +11,19 @@ struct BuiltinDoc {
inline const BuiltinDoc kBuiltins[] = { inline const BuiltinDoc kBuiltins[] = {
{"print", "print(a, b, ...)", "Prints the values separated by spaces, then a new line. Works with every type."}, {"print", "print(a, b, ...)", "Prints the values separated by spaces, then a new line. Works with every type."},
{"input", "input(str question = \"\") -> str", "Shows the question and reads one line typed by the user, without the newline."}, {"input", "input(str question = \"\") -> str", "Shows the question and reads one line typed by the user, without the newline."},
{"read_file", "read_file(str path) -> str", "The whole file as text. Stops the program if it can't be read."}, {"read_file", "read_file(str path) -> str", "The whole file as text. Stops the program if it can't be read, unless handled: read_file(p) or { ... }, try read_file(p)."},
{"write_file", "write_file(str path, str text) -> bool", "Writes (replaces) the file. true if it worked."}, {"write_file", "write_file(str path, str text) -> bool", "Writes (replaces) the file. true if it worked; with or / try, the reason it didn't."},
{"file_exists", "file_exists(str path) -> bool", "Is there a file at this path?"}, {"file_exists", "file_exists(str path) -> bool", "Is there a file at this path?"},
{"delete_file", "delete_file(str path) -> bool", "Deletes the file. true if it worked."}, {"delete_file", "delete_file(str path) -> bool", "Deletes the file. true if it worked; with or / try, the reason it didn't."},
{"error", "error(str message)", "The failure a fallible function returns: return error(\"...\"). The caller handles it with or / try."},
{"shell", "shell(str command) -> int", "Runs a command in the system's shell and gives back its exit code."}, {"shell", "shell(str command) -> int", "Runs a command in the system's shell and gives back its exit code."},
{"exit", "exit(int code)", "Ends the program right away with this exit code."}, {"exit", "exit(int code)", "Ends the program right away with this exit code."},
{"addr", "addr(x) -> ptr[T]", "A pointer to a variable, field or element. addr(function) gives C a callback."}, {"addr", "addr(x) -> ptr[T]", "A pointer to a variable, field or element. addr(function) gives C a callback."},
{"new", "new(value) -> ptr[T]", "Puts a copy of the value on the heap. Give it back with free(...)."}, {"new", "new(value) -> ptr[T]", "Puts a copy of the value on the heap. Give it back with free(...)."},
{"free", "free(ptr p)", "Gives back memory from new(...) (or from C), freeing what the value owns."}, {"free", "free(ptr p)", "Gives back memory from new(...) (or from C), freeing what the value owns."},
{"str", "str(x) -> str", "Turns a number, bool, char, []u8 or C char pointer into text."}, {"str", "str(x) -> str", "Turns a number, bool, char, []u8 or C char pointer into text."},
{"int", "int(x) -> int", "Converts to a whole number: cuts off fractions, parses text (stops the program if it isn't a number)."}, {"int", "int(x) -> int", "Converts to a whole number: cuts off fractions, parses text (stops the program if it isn't a number, unless handled: int(s) or 0)."},
{"float", "float(x) -> float", "Converts to a decimal number; parses text."}, {"float", "float(x) -> float", "Converts to a decimal number; parses text (int(s) or 0.0 handles bad text)."},
{"char", "char(int n) -> char", "The character with this code."}, {"char", "char(int n) -> char", "The character with this code."},
{"bool", "bool(int n) -> bool", "true for any number but 0."}, {"bool", "bool(int n) -> bool", "true for any number but 0."},
{"ptr", "ptr(x) -> ptr", "An untyped pointer: from a typed pointer, or from a number (an address or offset for C)."}, {"ptr", "ptr(x) -> ptr", "An untyped pointer: from a typed pointer, or from a number (an address or offset for C)."},
@ -62,8 +63,18 @@ inline const BuiltinDoc kStrMethods[] = {
{"bytes", "bytes() -> []u8", "The bytes of the text."}, {"bytes", "bytes() -> []u8", "The bytes of the text."},
}; };
inline const char *kKeywords[] = {"fn", "return", "if", "else", "while", "for", "in", "break", "continue", "true", inline const BuiltinDoc kMapMethods[] = {
"false", "null", "import", "link", "struct", "defer"}; {"len", ".len -> int", "The number of keys."},
{"has", "has(K key) -> bool", "Is the key in the map?"},
{"get", "get(K key, V default) -> V", "The value for key, or default if the key isn't there."},
{"remove", "remove(K key) -> bool", "Removes the key and its value; true if it was there."},
{"clear", "clear()", "Removes everything."},
{"keys", "keys() -> []K", "The keys, in the order they were added."},
{"values", "values() -> []V", "The values, in the order their keys were added."},
};
inline const char *kTypeNames[] = {"int", "float", "bool", "char", "str", "ptr", "i8", "i16", "i32", "i64", inline const char *kKeywords[] = {"fn", "return", "if", "else", "while", "for", "in", "break", "continue", "true",
"false", "null", "import", "link", "struct", "defer", "or", "try", "self"};
inline const char *kTypeNames[] = {"int", "float", "bool", "char", "str", "ptr", "map", "i8", "i16", "i32", "i64",
"u8", "u16", "u32", "u64", "f32", "f64"}; "u8", "u16", "u32", "u64", "f32", "f64"};

View file

@ -39,6 +39,8 @@ std::unique_ptr<Module> Codegen::run() {
if (!modules[""].fns.count("main")) failAt(0, 1, 1, "there is no main function (add 'fn main() { ... }')"); if (!modules[""].fns.count("main")) failAt(0, 1, 1, "there is no main function (add 'fn main() { ... }')");
for (auto &[name, m] : modules) for (auto &[name, m] : modules)
for (auto &[fname, fn] : m.fns) define(fn); for (auto &[fname, fn] : m.fns) define(fn);
for (auto &[s, ms] : methods)
for (auto &[mname, fn] : ms) define(fn);
defineMainWrapper(); defineMainWrapper();
if (di) di->finalize(); if (di) di->finalize();
@ -102,19 +104,72 @@ void Codegen::declareStructs() {
void Codegen::declareFns() { void Codegen::declareFns() {
for (const Program &p : progs) for (const Program &p : progs)
for (const FnDecl &fn : p.fns) declare(p, fn); for (const FnDecl &fn : p.fns) declare(p, fn);
// Which methods change `self`? Calling one of them on something changes it too, so repeat until stable.
for (bool changed = true; changed;) {
changed = false;
for (auto &[s, ms] : methods)
for (auto &[name, f] : ms)
if (!f.mutatesSelf && mutates("self", *f.decl->body)) {
f.mutatesSelf = true;
mutatingMethods.insert(name);
changed = true;
}
}
// Owning parameters the function never changes are lent to it instead of copied.
auto borrow = [&](Fn &f) {
for (size_t i = 0; i < f.params.size(); i++)
f.borrowParam.push_back(owning(f.params[i]) && !mutates(f.decl->params[i].name, *f.decl->body));
};
for (auto &[name, m] : modules)
for (auto &[fname, f] : m.fns) borrow(f);
for (auto &[s, ms] : methods)
for (auto &[name, f] : ms) borrow(f);
}
FType Codegen::structType(StructInfo *s) {
FType t(FType::Struct);
t.info = s;
t.name = s->name;
t.module = s->module == "C" ? "" : s->module;
return t;
}
llvm::Type *Codegen::retType(const Fn &f) {
if (!f.fallible) return ty(f.ret);
llvm::Type *v = f.ret.kind == FType::Void ? (llvm::Type *)b.getInt8Ty() : ty(f.ret);
return StructType::get(ctx, {b.getInt1Ty(), v, ty(FType::Str)});
} }
void Codegen::declare(const Program &p, const FnDecl &fn) { void Codegen::declare(const Program &p, const FnDecl &fn) {
ModuleScope &m = modules[p.module]; ModuleScope &m = modules[p.module];
Pos pos = fn.pos; Pos pos = fn.pos;
if (m.fns.count(fn.name)) failAt(pos.file, pos.line, pos.col, "function '" + fn.name + "' is defined twice");
if (isBuiltin(fn.name)) failAt(pos.file, pos.line, pos.col, "'" + fn.name + "' is a built-in function, pick another name");
if (m.structs.count(fn.name)) failAt(pos.file, pos.line, pos.col, "'" + fn.name + "' is already the name of a struct");
curModule = p.module; curModule = p.module;
StructInfo *recv = nullptr;
if (!fn.recv.empty()) { // fn Point.move(...)
Pos rp = fn.recvPos;
recv = findStruct("", fn.recv, false);
if (!recv)
failAt(rp.file, rp.line, rp.col, "there is no struct named '" + fn.recv + "' here (a method goes in the same file as its struct, or on a struct from C)");
resolveStruct(recv);
for (auto &f : recv->fields)
if (f.name == fn.name && !f.hidden)
failAt(fn.namePos.file, fn.namePos.line, fn.namePos.col, "the struct " + recv->name + " already has a field named '" + fn.name + "'");
if (methods[recv].count(fn.name)) failAt(pos.file, pos.line, pos.col, "the method " + fn.recv + "." + fn.name + " is defined twice");
for (const Param &prm : fn.params)
if (prm.name == "self")
failAt(prm.namePos.file, prm.namePos.line, prm.namePos.col, "'self' is already the " + recv->name + " the method is called on");
} else {
if (m.fns.count(fn.name)) failAt(pos.file, pos.line, pos.col, "function '" + fn.name + "' is defined twice");
if (isBuiltin(fn.name)) failAt(pos.file, pos.line, pos.col, "'" + fn.name + "' is a built-in function, pick another name");
if (m.structs.count(fn.name)) failAt(pos.file, pos.line, pos.col, "'" + fn.name + "' is already the name of a struct");
}
Fn f; Fn f;
f.decl = &fn; f.decl = &fn;
f.module = p.module; f.module = p.module;
f.recv = recv;
f.fallible = fn.fallible;
f.ret = resolve(fn.ret, pos); f.ret = resolve(fn.ret, pos);
for (const Param &prm : fn.params) f.params.push_back(resolve(prm.type, prm.pos)); for (const Param &prm : fn.params) f.params.push_back(resolve(prm.type, prm.pos));
@ -128,14 +183,13 @@ void Codegen::declare(const Program &p, const FnDecl &fn) {
} }
std::vector<llvm::Type *> params; std::vector<llvm::Type *> params;
for (size_t i = 0; i < f.params.size(); i++) { if (recv) params.push_back(b.getPtrTy()); // self
params.push_back(ty(f.params[i])); for (size_t i = 0; i < f.params.size(); i++) params.push_back(ty(f.params[i]));
f.borrowParam.push_back(owning(f.params[i]) && !mutates(fn.params[i].name, *fn.body));
}
// User functions get a prefix so they never clash with C library names. // User functions get a prefix so they never clash with C library names.
std::string name = "finch." + (p.module.empty() ? "" : p.module + ".") + fn.name; std::string name = "finch." + (p.module.empty() ? "" : p.module + ".") + (recv ? recv->name + "." : "") + fn.name;
f.llvm = Function::Create(FunctionType::get(ty(f.ret), params, false), Function::InternalLinkage, name, mod.get()); f.llvm = Function::Create(FunctionType::get(retType(f), params, false), Function::InternalLinkage, name, mod.get());
m.fns[fn.name] = f; if (recv) methods[recv][fn.name] = f;
else m.fns[fn.name] = f;
} }
// The real C `main` calls the Finch one and turns its result into an exit code. // The real C `main` calls the Finch one and turns its result into an exit code.
@ -155,6 +209,17 @@ void Codegen::defineMainWrapper() {
} }
Value *r = b.CreateCall(fm.llvm, args); Value *r = b.CreateCall(fm.llvm, args);
if (argsArr) dropValue(argsArr, fm.params[0]); if (argsArr) dropValue(argsArr, fm.params[0]);
if (fm.fallible) { // fn main() -> !: a failure is printed, and the exit code is 1
BasicBlock *bad = BasicBlock::Create(ctx, "main.failed", w), *good = BasicBlock::Create(ctx, "main.ok", w);
b.CreateCondBr(b.CreateExtractValue(r, 0), good, bad);
b.SetInsertPoint(bad);
Value *msg = tmp(b.CreateExtractValue(r, 2));
b.CreateCall(rt("finch_report_error"), {msg});
b.CreateCall(rt("finch_str_drop"), {msg});
b.CreateRet(b.getInt32(1));
b.SetInsertPoint(good);
if (fm.ret.kind != FType::Void) r = b.CreateExtractValue(r, 1);
}
if (fm.ret.kind == FType::Void) b.CreateRet(b.getInt32(0)); if (fm.ret.kind == FType::Void) b.CreateRet(b.getInt32(0));
else b.CreateRet(b.CreateIntCast(r, b.getInt32Ty(), true)); else b.CreateRet(b.CreateIntCast(r, b.getInt32Ty(), true));
} }
@ -164,7 +229,8 @@ void Codegen::define(Fn &fn) {
curFn = &fn; curFn = &fn;
curModule = fn.module; curModule = fn.module;
if (g_index) { if (g_index) {
FnInfo fi{fn.module, d.name, signature(fn), "", {}, d.namePos.file, d.namePos.line, d.namePos.col, d.body->end.line}; std::string shown = fn.recv ? fn.recv->name + "." + d.name : d.name;
FnInfo fi{fn.module, shown, signature(fn), "", {}, d.namePos.file, d.namePos.line, d.namePos.col, d.body->end.line};
for (size_t i = 0; i < fn.params.size(); i++) fi.params.push_back(fn.params[i].show() + " " + d.params[i].name); for (size_t i = 0; i < fn.params.size(); i++) fi.params.push_back(fn.params[i].show() + " " + d.params[i].name);
g_index->fns.push_back(fi); g_index->fns.push_back(fi);
note(d.namePos, d.name.size(), "```finch\n" + signature(fn) + "\n```", d.namePos); note(d.namePos, d.name.size(), "```finch\n" + signature(fn) + "\n```", d.namePos);
@ -186,12 +252,33 @@ void Codegen::define(Fn &fn) {
varOrder = 0; varOrder = 0;
pushScope(); pushScope();
unsigned i = 0; unsigned i = 0;
for (Argument &arg : f->args()) { auto argIt = f->arg_begin();
if (fn.recv) { // `self` is the caller's struct itself, reached through its address
Argument &selfArg = *argIt++;
selfArg.setName("self");
Var v;
v.slot = &selfArg;
v.type = structType(fn.recv);
v.order = ++varOrder;
v.pos = d.recvPos;
scopes.back().vars["self"] = v;
if (g_index) g_index->vars.push_back({"self", v.type.show(), d.pos.file, d.pos.line, d.pos.col, d.pos.file, d.pos.line, d.body->end.line});
if (di) {
FType pt = FType::ptrTo(v.type);
AllocaInst *a = slot(pt, "self.addr");
b.CreateStore(&selfArg, a);
DILocalVariable *dv = di->createParameterVariable(diFn, "self", 1, diFiles[d.pos.file], d.pos.line, diType(pt));
di->insertDeclare(a, dv, di->createExpression(), DILocation::get(ctx, d.pos.line, d.pos.col, diFn), b.GetInsertBlock());
}
}
for (; argIt != f->arg_end(); ++argIt) {
Argument &arg = *argIt;
const Param &p = d.params[i]; const Param &p = d.params[i];
const FType &t = fn.params[i]; const FType &t = fn.params[i];
arg.setName(p.name); arg.setName(p.name);
if (owning(t) && !fn.borrowParam[i]) addVar(p.namePos, p.name, t, copyValue(&arg, t), true, false, i + 1); // changed inside: own a copy unsigned argNo = i + 1 + (fn.recv ? 1 : 0);
else addVar(p.namePos, p.name, t, &arg, false, false, i + 1); if (owning(t) && !fn.borrowParam[i]) addVar(p.namePos, p.name, t, copyValue(&arg, t), true, false, argNo); // changed inside: own a copy
else addVar(p.namePos, p.name, t, &arg, false, false, argNo);
i++; i++;
} }
blockBody(*d.body); blockBody(*d.body);
@ -199,7 +286,7 @@ void Codegen::define(Fn &fn) {
if (fn.ret.kind != FType::Void) if (fn.ret.kind != FType::Void)
failAt(d.pos.file, d.pos.line, d.pos.col, "function '" + d.name + "' can reach its end without returning " + fn.ret.show()); failAt(d.pos.file, d.pos.line, d.pos.col, "function '" + d.name + "' can reach its end without returning " + fn.ret.show());
emitCleanups(0); emitCleanups(0);
b.CreateRetVoid(); retValue(nullptr);
} }
scopes.clear(); scopes.clear();
diFn = nullptr; diFn = nullptr;
@ -315,7 +402,11 @@ void Codegen::stmt(const Stmt &s) {
case StmtKind::Block: block(static_cast<const BlockStmt &>(s)); break; case StmtKind::Block: block(static_cast<const BlockStmt &>(s)); break;
case StmtKind::VarDecl: varDecl(static_cast<const VarDeclStmt &>(s)); break; case StmtKind::VarDecl: varDecl(static_cast<const VarDeclStmt &>(s)); break;
case StmtKind::Assign: assign(static_cast<const AssignStmt &>(s)); break; case StmtKind::Assign: assign(static_cast<const AssignStmt &>(s)); break;
case StmtKind::Expr: release(expr(*static_cast<const ExprStmt &>(s).expr)); break; case StmtKind::Expr: {
const Expr &x = *static_cast<const ExprStmt &>(s).expr;
release(x.kind == ExprKind::OrElse ? orElse(static_cast<const OrElseExpr &>(x), true) : expr(x));
break;
}
case StmtKind::If: ifStmt(static_cast<const IfStmt &>(s)); break; case StmtKind::If: ifStmt(static_cast<const IfStmt &>(s)); break;
case StmtKind::While: whileStmt(static_cast<const WhileStmt &>(s)); break; case StmtKind::While: whileStmt(static_cast<const WhileStmt &>(s)); break;
case StmtKind::For: forStmt(static_cast<const ForStmt &>(s)); break; case StmtKind::For: forStmt(static_cast<const ForStmt &>(s)); break;
@ -486,7 +577,8 @@ void Codegen::forStmt(const ForStmt &s) {
popScope(); popScope();
} }
// for x in list: x is each element (or each char of a str) // for x in list: x is each element (or each char of a str); for i, x in list: also its index;
// for key in map / for key, value in map: in the order the keys were added
void Codegen::forEachStmt(const ForEachStmt &s) { void Codegen::forEachStmt(const ForEachStmt &s) {
pushScope(); pushScope();
LRef r = ref(*s.list); LRef r = ref(*s.list);
@ -500,9 +592,50 @@ void Codegen::forEachStmt(const ForEachStmt &s) {
lt = r.val.type; lt = r.val.type;
addr = addVar(s.pos, "$list" + std::to_string(varOrder), lt, own(r.val), true).slot; addr = addVar(s.pos, "$list" + std::to_string(varOrder), lt, own(r.val), true).slot;
} }
if (lt.kind == FType::Map) {
FType kt = *lt.key, vt = *lt.elem;
StructType *ET = mapEntryTy(lt);
llvm::Type *MT = ty(lt);
// if the loop changes the map, keys and values are copied, so they can't disappear under us
bool copyKV = live && mutates(rootVar(*s.list), *s.body);
Value *i = slot(FType::I64, "$i");
b.CreateStore(b.getInt64(0), i);
BasicBlock *condBB = newBlock("map.cond"), *check = newBlock("map.check"), *body = newBlock("map.body"),
*step = newBlock("map.step"), *end = newBlock("map.end");
b.CreateBr(condBB);
b.SetInsertPoint(condBB);
Value *iv = b.CreateLoad(b.getInt64Ty(), i);
b.CreateCondBr(b.CreateICmpSLT(iv, b.CreateLoad(b.getInt64Ty(), b.CreateStructGEP(MT, addr, 2))), check, end);
b.SetInsertPoint(check);
Value *entry = b.CreateInBoundsGEP(ET, b.CreateLoad(b.getPtrTy(), b.CreateStructGEP(MT, addr, 0)), iv);
Value *hash = b.CreateLoad(b.getInt64Ty(), b.CreateStructGEP(ET, entry, 0));
b.CreateCondBr(b.CreateICmpEQ(hash, b.getInt64(0)), step, body); // a removed entry
b.SetInsertPoint(body);
loops.push_back({step, end, scopes.size()});
pushScope();
std::string why = " can't be changed (to change the map, use m[key] = ...)";
Value *kv = b.CreateLoad(ty(kt), b.CreateStructGEP(ET, entry, 1));
Var &k = addVar(s.varPos, s.var, kt, copyKV ? copyValue(kv, kt) : kv, copyKV, true);
k.readonlyWhy = "the loop variable '" + s.var + "'" + why;
if (!s.var2.empty()) {
Value *vv = b.CreateLoad(ty(vt), b.CreateStructGEP(ET, entry, 2));
Var &v = addVar(s.var2Pos, s.var2, vt, copyKV ? copyValue(vv, vt) : vv, copyKV, true);
v.readonlyWhy = "the loop variable '" + s.var2 + "'" + why;
}
block(*s.body);
popScope();
loops.pop_back();
if (!terminated()) b.CreateBr(step);
b.SetInsertPoint(step);
b.CreateStore(b.CreateAdd(b.CreateLoad(b.getInt64Ty(), i), b.getInt64(1)), i);
b.CreateBr(condBB);
b.SetInsertPoint(end);
popScope();
return;
}
if (lt.kind != FType::Array && lt.kind != FType::Str && lt.kind != FType::Fixed) if (lt.kind != FType::Array && lt.kind != FType::Str && lt.kind != FType::Fixed)
failAt(s.list->pos.file, s.list->pos.line, s.list->pos.col, failAt(s.list->pos.file, s.list->pos.line, s.list->pos.col,
"for ... in needs an array or a str, but this is " + lt.show() + " (for a range of numbers write 0..n)"); "for ... in needs an array, a str or a map, but this is " + lt.show() + " (for a range of numbers write 0..n)");
FType et = lt.kind == FType::Str ? FType(FType::Char) : *lt.elem; FType et = lt.kind == FType::Str ? FType(FType::Char) : *lt.elem;
// If the loop changes the list, each element is copied, so it can't disappear under us. // If the loop changes the list, each element is copied, so it can't disappear under us.
bool copyElems = live && owning(et) && mutates(rootVar(*s.list), *s.body); bool copyElems = live && owning(et) && mutates(rootVar(*s.list), *s.body);
@ -531,8 +664,16 @@ void Codegen::forEachStmt(const ForEachStmt &s) {
Value *elem = b.CreateLoad(ty(et), elemAddr); Value *elem = b.CreateLoad(ty(et), elemAddr);
loops.push_back({step, end, scopes.size()}); loops.push_back({step, end, scopes.size()});
pushScope(); pushScope();
Var &xv = addVar(s.varPos, s.var, et, copyElems ? copyValue(elem, et) : elem, copyElems, true); std::string elemVar = s.var;
xv.readonlyWhy = "the loop variable '" + s.var + "' can't be changed (to change the list, loop with for i in 0..list.len and use list[i])"; Pos elemPos = s.varPos;
if (!s.var2.empty()) { // for i, x in list
Var &index = addVar(s.varPos, s.var, FType::I64, iv, false, true);
index.readonlyWhy = "the loop variable '" + s.var + "' can't be changed";
elemVar = s.var2;
elemPos = s.var2Pos;
}
Var &xv = addVar(elemPos, elemVar, et, copyElems ? copyValue(elem, et) : elem, copyElems, true);
xv.readonlyWhy = "the loop variable '" + elemVar + "' can't be changed (to change the list, use list[i] = ...)";
block(*s.body); block(*s.body);
popScope(); popScope();
loops.pop_back(); loops.pop_back();
@ -546,14 +687,46 @@ void Codegen::forEachStmt(const ForEachStmt &s) {
popScope(); popScope();
} }
void Codegen::retValue(Value *v) {
if (!curFn->fallible) {
if (v) b.CreateRet(v);
else b.CreateRetVoid();
return;
}
Value *r = Constant::getNullValue(curFn->llvm->getReturnType());
r = b.CreateInsertValue(r, b.getInt1(true), 0);
if (v) r = b.CreateInsertValue(r, v, 1);
b.CreateRet(r);
}
void Codegen::retError(Value *msg) {
Value *r = Constant::getNullValue(curFn->llvm->getReturnType());
b.CreateRet(b.CreateInsertValue(r, msg, 2));
}
void Codegen::returnStmt(const ReturnStmt &s) { void Codegen::returnStmt(const ReturnStmt &s) {
const FnDecl &d = *curFn->decl; const FnDecl &d = *curFn->decl;
if (inDefer) failAt(s.pos.file, s.pos.line, s.pos.col, "return can't be used inside defer"); if (inDefer) failAt(s.pos.file, s.pos.line, s.pos.col, "return can't be used inside defer");
// return error("...")
if (s.value && s.value->kind == ExprKind::Call && static_cast<const CallExpr &>(*s.value).callee == "error") {
auto &c = static_cast<const CallExpr &>(*s.value);
if (!curFn->fallible)
failAt(c.pos.file, c.pos.line, c.pos.col, "'" + d.name + "' can't fail, so it can't return an error (make it fallible: -> " +
(curFn->ret.kind == FType::Void ? std::string("!") : curFn->ret.show() + "!") + ")");
if (g_index)
for (const BuiltinDoc &bd : kBuiltins)
if (std::string(bd.name) == "error") note(c.pos, 5, "```finch\n" + std::string(bd.signature) + "\n```\n" + bd.doc);
checkArgs(c.args, c.argNames, 1, c.pos, "error");
Value *msg = own(coerce(expr(*c.args[0]), FType::Str, c.args[0]->pos, "the error message"));
emitCleanups(0);
retError(msg);
return;
}
if (curFn->ret.kind == FType::Void) { if (curFn->ret.kind == FType::Void) {
if (s.value) if (s.value)
failAt(s.value->pos.file, s.value->pos.line, s.value->pos.col, "'" + d.name + "' doesn't return a value (it has no '-> type')"); failAt(s.value->pos.file, s.value->pos.line, s.value->pos.col, "'" + d.name + "' doesn't return a value (it has no '-> type')");
emitCleanups(0); emitCleanups(0);
b.CreateRetVoid(); retValue(nullptr);
return; return;
} }
if (!s.value) failAt(s.pos.file, s.pos.line, s.pos.col, "'" + d.name + "' must return " + curFn->ret.show()); if (!s.value) failAt(s.pos.file, s.pos.line, s.pos.col, "'" + d.name + "' must return " + curFn->ret.show());
@ -569,7 +742,7 @@ void Codegen::returnStmt(const ReturnStmt &s) {
it->second.moved = true; it->second.moved = true;
emitCleanups(0); emitCleanups(0);
scopes[idx].vars[name].moved = false; scopes[idx].vars[name].moved = false;
b.CreateRet(v); retValue(v);
return; return;
} }
break; break;
@ -578,7 +751,7 @@ void Codegen::returnStmt(const ReturnStmt &s) {
Value_ v = coerce(exprWant(*s.value, curFn->ret), curFn->ret, s.value->pos, "the returned value"); Value_ v = coerce(exprWant(*s.value, curFn->ret), curFn->ret, s.value->pos, "the returned value");
Value *out = own(v); Value *out = own(v);
emitCleanups(0); emitCleanups(0);
b.CreateRet(out); retValue(out);
} }
// ---------- mutation analysis (does a function or loop change a variable?) ---------- // ---------- mutation analysis (does a function or loop change a variable?) ----------
@ -595,6 +768,17 @@ std::string Codegen::rootVar(const Expr &e) {
bool Codegen::mutatesExpr(const std::string &name, const Expr &e) { bool Codegen::mutatesExpr(const std::string &name, const Expr &e) {
static const std::set<std::string> changing = {"push", "pop", "insert", "remove", "clear", "resize", "sort", "reverse"}; static const std::set<std::string> changing = {"push", "pop", "insert", "remove", "clear", "resize", "sort", "reverse"};
switch (e.kind) { switch (e.kind) {
case ExprKind::Try: return mutatesExpr(name, *static_cast<const TryExpr &>(e).call);
case ExprKind::OrElse: {
auto &x = static_cast<const OrElseExpr &>(e);
return mutatesExpr(name, *x.call) || (x.fallback && mutatesExpr(name, *x.fallback)) || (x.block && mutates(name, *x.block));
}
case ExprKind::MapLit: {
auto &x = static_cast<const MapLitExpr &>(e);
for (size_t k = 0; k < x.keys.size(); k++)
if (mutatesExpr(name, *x.keys[k]) || mutatesExpr(name, *x.values[k])) return true;
return false;
}
case ExprKind::Unary: return mutatesExpr(name, *static_cast<const UnaryExpr &>(e).operand); case ExprKind::Unary: return mutatesExpr(name, *static_cast<const UnaryExpr &>(e).operand);
case ExprKind::Binary: { case ExprKind::Binary: {
auto &x = static_cast<const BinaryExpr &>(e); auto &x = static_cast<const BinaryExpr &>(e);
@ -618,7 +802,7 @@ bool Codegen::mutatesExpr(const std::string &name, const Expr &e) {
return false; return false;
case ExprKind::Method: { case ExprKind::Method: {
auto &m = static_cast<const MethodExpr &>(e); auto &m = static_cast<const MethodExpr &>(e);
if (changing.count(m.name) && rootVar(*m.obj) == name) return true; if ((changing.count(m.name) || mutatingMethods.count(m.name)) && rootVar(*m.obj) == name) return true;
if (mutatesExpr(name, *m.obj)) return true; if (mutatesExpr(name, *m.obj)) return true;
for (auto &a : m.args) for (auto &a : m.args)
if (mutatesExpr(name, *a)) return true; if (mutatesExpr(name, *a)) return true;
@ -667,9 +851,96 @@ bool Codegen::mutates(const std::string &name, const Stmt &s) {
Value_ Codegen::exprWant(const Expr &e, const FType &want) { Value_ Codegen::exprWant(const Expr &e, const FType &want) {
if (e.kind == ExprKind::ArrayLit) return arrayLit(static_cast<const ArrayLitExpr &>(e), &want); if (e.kind == ExprKind::ArrayLit) return arrayLit(static_cast<const ArrayLitExpr &>(e), &want);
if (e.kind == ExprKind::MapLit) return mapLit(static_cast<const MapLitExpr &>(e), &want);
return expr(e); return expr(e);
} }
// ---------- errors as values ----------
// Emit a call that may fail; `failed` (i1) and `msg` (str) tell how it went.
Value_ Codegen::fallible(const Expr &call, Value *&failed, Value *&msg) {
Pos p = call.pos;
if (call.kind != ExprKind::Call && call.kind != ExprKind::Method)
failAt(p.file, p.line, p.col, "only a function call can fail; 'or' and 'try' go after one, like int(text) or 0");
const Expr *savedTarget = fallibleTarget;
Value *savedFailed = fallibleFailed, *savedMsg = fallibleMsg;
fallibleTarget = &call;
fallibleFailed = fallibleMsg = nullptr;
Value_ v = expr(call);
failed = fallibleFailed;
msg = fallibleMsg;
fallibleTarget = savedTarget;
fallibleFailed = savedFailed;
fallibleMsg = savedMsg;
if (!failed) failAt(p.file, p.line, p.col, "this call can't fail, so it needs no 'or' or 'try'");
return v;
}
// try f(x): on failure, the current function fails with the same error
Value_ Codegen::tryExpr(const TryExpr &t) {
Pos p = t.pos;
if (inDefer) failAt(p.file, p.line, p.col, "try can't be used inside defer");
if (!curFn->fallible)
failAt(p.file, p.line, p.col, "try passes the error on, so '" + curFn->decl->name + "' must be able to fail too (-> " +
(curFn->ret.kind == FType::Void ? std::string("!") : curFn->ret.show() + "!") +
"); or handle it here: ... or <value> / ... or { ... }");
Value *failed, *msg;
Value_ v = fallible(*t.call, failed, msg);
BasicBlock *bad = newBlock("try.failed"), *ok = newBlock("try.ok");
b.CreateCondBr(failed, bad, ok);
b.SetInsertPoint(bad);
emitCleanups(0);
retError(msg);
b.SetInsertPoint(ok);
return v;
}
// f(x) or fallback / f(x) or { ... err ... }
Value_ Codegen::orElse(const OrElseExpr &e, bool discard) {
Value *failed, *msg;
Value_ v = fallible(*e.call, failed, msg);
BasicBlock *bad = newBlock("or.failed"), *ok = newBlock("or.ok");
b.CreateCondBr(failed, bad, ok);
b.SetInsertPoint(bad);
if (e.block) {
pushScope();
addVar(e.block->pos, "err", FType::Str, msg, true);
blockBody(*e.block);
bool left = terminated();
if (!left && v.type.kind != FType::Void && !discard)
failAt(e.block->end.file, e.block->end.line, e.block->end.col,
"this block must leave with return, break, continue or exit(...): after a failure there is no value to go on with (or give one: ... or <value>)");
popScope();
if (discard && v.type.kind != FType::Void) { // read_file(p) or { ... } as a statement: drop the unused value
BasicBlock *join = newBlock("or.end");
if (!left) b.CreateBr(join);
b.SetInsertPoint(ok);
release(v);
b.CreateBr(join);
b.SetInsertPoint(join);
return {nullptr, FType::Void};
}
if (!left) b.CreateBr(ok);
b.SetInsertPoint(ok);
return v;
}
Pos fp = e.fallback->pos;
if (v.type.kind == FType::Void)
failAt(fp.file, fp.line, fp.col, "this call gives back no value, so there is nothing to replace; handle the failure with or { ... }");
dropValue(msg, FType::Str);
Value *fv = own(coerce(exprWant(*e.fallback, v.type), v.type, fp, "the value after 'or'"));
BasicBlock *badEnd = b.GetInsertBlock();
BasicBlock *join = newBlock("or.end");
b.CreateBr(join);
b.SetInsertPoint(ok);
b.CreateBr(join);
b.SetInsertPoint(join);
PHINode *phi = b.CreatePHI(ty(v.type), 2);
phi->addIncoming(fv, badEnd);
phi->addIncoming(v.v, ok);
return {phi, v.type, false, owning(v.type)};
}
Value_ Codegen::expr(const Expr &e) { Value_ Codegen::expr(const Expr &e) {
switch (e.kind) { switch (e.kind) {
case ExprKind::Int: return {b.getInt64(static_cast<const IntExpr &>(e).value), FType::I64, true}; case ExprKind::Int: return {b.getInt64(static_cast<const IntExpr &>(e).value), FType::I64, true};
@ -711,6 +982,9 @@ Value_ Codegen::expr(const Expr &e) {
case ExprKind::Call: return call(static_cast<const CallExpr &>(e)); case ExprKind::Call: return call(static_cast<const CallExpr &>(e));
case ExprKind::Method: return method(static_cast<const MethodExpr &>(e)); case ExprKind::Method: return method(static_cast<const MethodExpr &>(e));
case ExprKind::ArrayLit: return arrayLit(static_cast<const ArrayLitExpr &>(e), nullptr); case ExprKind::ArrayLit: return arrayLit(static_cast<const ArrayLitExpr &>(e), nullptr);
case ExprKind::MapLit: return mapLit(static_cast<const MapLitExpr &>(e), nullptr);
case ExprKind::Try: return tryExpr(static_cast<const TryExpr &>(e));
case ExprKind::OrElse: return orElse(static_cast<const OrElseExpr &>(e));
} }
return {nullptr, FType::Void}; return {nullptr, FType::Void};
} }
@ -785,6 +1059,19 @@ LRef Codegen::member(const MemberExpr &m, bool forWrite) {
t = *t.elem; t = *t.elem;
} }
if (t.kind == FType::Map) {
if (m.field != "len") failAt(p.file, p.line, p.col, "a map has .len (the number of keys), not '." + m.field + "'");
if (forWrite) failAt(p.file, p.line, p.col, ".len can't be changed directly (use m[key] = ..., remove or clear)");
note(Pos{p.line, p.col + 1, p.file}, 3, "`.len -> int`: the number of keys");
Value *len;
if (obj.isPlace) {
len = b.CreateLoad(b.getInt64Ty(), b.CreateStructGEP(ty(t), obj.pl.addr, 1));
} else {
len = b.CreateExtractValue(obj.val.v, 1);
release(obj.val);
}
return {false, {}, {len, FType::I64}};
}
if (t.kind == FType::Array || t.kind == FType::Str || t.kind == FType::Fixed) { if (t.kind == FType::Array || t.kind == FType::Str || t.kind == FType::Fixed) {
if (m.field == "len" || m.field == "ptr") if (m.field == "len" || m.field == "ptr")
note(Pos{p.line, p.col + 1, p.file}, m.field.size(), note(Pos{p.line, p.col + 1, p.file}, m.field.size(),
@ -839,6 +1126,7 @@ LRef Codegen::index(const IndexExpr &e, bool forWrite) {
Pos p = e.pos; Pos p = e.pos;
LRef obj = ref(*e.obj, forWrite); LRef obj = ref(*e.obj, forWrite);
FType t = obj.isPlace ? obj.pl.type : obj.val.type; FType t = obj.isPlace ? obj.pl.type : obj.val.type;
if (t.kind == FType::Map) return mapIndex(e, obj, t, forWrite);
Value *i = coerce(expr(*e.index), FType::I64, e.index->pos, "an index").v; Value *i = coerce(expr(*e.index), FType::I64, e.index->pos, "an index").v;
if (t.isPtr()) { // C-style: p[i], no bounds known if (t.isPtr()) { // C-style: p[i], no bounds known
@ -854,7 +1142,7 @@ LRef Codegen::index(const IndexExpr &e, bool forWrite) {
return {false, {}, {b.CreateLoad(ty(*t.elem), b.CreateInBoundsGEP(ty(t), a, {b.getInt64(0), i})), *t.elem}}; return {false, {}, {b.CreateLoad(ty(*t.elem), b.CreateInBoundsGEP(ty(t), a, {b.getInt64(0), i})), *t.elem}};
} }
if (t.kind != FType::Array && t.kind != FType::Str) if (t.kind != FType::Array && t.kind != FType::Str)
failAt(p.file, p.line, p.col, "[ ] works on arrays, str and pointers, not on " + t.show()); failAt(p.file, p.line, p.col, "[ ] works on arrays, maps, str and pointers, not on " + t.show());
FType et = t.kind == FType::Str ? FType(FType::Char) : *t.elem; FType et = t.kind == FType::Str ? FType(FType::Char) : *t.elem;
if (t.kind == FType::Str && forWrite) { if (t.kind == FType::Str && forWrite) {
if (!obj.isPlace) failAt(p.file, p.line, p.col, "this is a temporary str, it can't be changed"); if (!obj.isPlace) failAt(p.file, p.line, p.col, "this is a temporary str, it can't be changed");
@ -1139,10 +1427,12 @@ void Codegen::note(Pos at, size_t len, const std::string &hover, Pos def) {
} }
std::string Codegen::signature(const Fn &fn) { std::string Codegen::signature(const Fn &fn) {
std::string s = "fn " + (fn.module.empty() ? "" : fn.module + ".") + fn.decl->name + "("; std::string owner = fn.recv ? fn.recv->name + "." : fn.module.empty() ? "" : fn.module + ".";
std::string s = "fn " + owner + fn.decl->name + "(";
for (size_t i = 0; i < fn.params.size(); i++) s += (i ? ", " : "") + fn.params[i].show() + " " + fn.decl->params[i].name; for (size_t i = 0; i < fn.params.size(); i++) s += (i ? ", " : "") + fn.params[i].show() + " " + fn.decl->params[i].name;
s += ")"; s += ")";
if (fn.ret.kind != FType::Void) s += " -> " + fn.ret.show(); if (fn.ret.kind != FType::Void) s += " -> " + fn.ret.show() + (fn.fallible ? "!" : "");
else if (fn.fallible) s += " -> !";
return s; return s;
} }

View file

@ -9,7 +9,7 @@ namespace finch {
bool Codegen::isBuiltin(const std::string &n) { bool Codegen::isBuiltin(const std::string &n) {
static const std::set<std::string> names = {"print", "addr", "input", "new", "free", "read_file", "write_file", static const std::set<std::string> names = {"print", "addr", "input", "new", "free", "read_file", "write_file",
"file_exists", "delete_file", "exit", "shell"}; "file_exists", "delete_file", "exit", "shell", "error"};
FType t; FType t;
return names.count(n) || typeFromName(n, t); return names.count(n) || typeFromName(n, t);
} }
@ -95,6 +95,28 @@ Function *Codegen::rt(const std::string &name) {
{"finch_shell", {I, {P}}}, {"finch_shell", {I, {P}}},
{"finch_delete_file", {I32, {P}}}, {"finch_delete_file", {I32, {P}}},
{"finch_std_stream", {P, {I32}}}, {"finch_std_stream", {P, {I32}}},
{"finch_str_try_int", {I32, {P, P, P}}},
{"finch_str_try_float", {I32, {P, P, P}}},
{"finch_try_read_file", {I32, {P, P, P}}},
{"finch_try_write_file", {I32, {P, P, P}}},
{"finch_try_delete_file", {I32, {P, P}}},
{"finch_report_error", {V, {P}}},
{"finch_map_find", {I, {P, P, I, I, I32}}},
{"finch_map_slot", {I, {P, P, I, I, I32, P}}},
{"finch_map_remove_at", {V, {P, I, I}}},
{"finch_map_clear", {V, {P}}},
{"finch_map_free", {V, {P}}},
{"finch_map_clone_raw", {V, {P, P, I}}},
{"finch_map_missing", {V, {P, I, P}}},
{"finch_buf_add", {V, {P, P, I}}},
{"finch_buf_int", {V, {P, I}}},
{"finch_buf_uint", {V, {P, I}}},
{"finch_buf_float", {V, {P, D}}},
{"finch_buf_bool", {V, {P, I32}}},
{"finch_buf_ptr", {V, {P, P}}},
{"finch_buf_char", {V, {P, b.getInt8Ty(), I32}}},
{"finch_buf_str", {V, {P, P, I32}}},
{"finch_buf_print", {V, {P}}},
}; };
auto it = sigs.find(name); auto it = sigs.find(name);
if (it == sigs.end()) { if (it == sigs.end()) {
@ -103,11 +125,11 @@ Function *Codegen::rt(const std::string &name) {
} }
Function *f = Function::Create(FunctionType::get(it->second.ret, it->second.params, false), Function::ExternalLinkage, Function *f = Function::Create(FunctionType::get(it->second.ret, it->second.params, false), Function::ExternalLinkage,
name, mod.get()); name, mod.get());
if (name == "finch_panic" || name == "finch_panic_index") { if (name == "finch_panic" || name == "finch_panic_index" || name == "finch_map_missing") {
f->addFnAttr(Attribute::NoReturn); f->addFnAttr(Attribute::NoReturn);
f->addFnAttr(Attribute::Cold); f->addFnAttr(Attribute::Cold);
} }
if (name == "finch_str_from_char") f->addParamAttr(1, Attribute::SExt); if (name == "finch_str_from_char" || name == "finch_buf_char") f->addParamAttr(1, Attribute::SExt);
return f; return f;
} }
@ -185,6 +207,14 @@ Value_ Codegen::call(const CallExpr &c) {
return *c.args[0]; return *c.args[0];
}; };
bool canFail = &c == fallibleTarget; // inside `... or ...` / `try ...`: report failures instead of stopping
// a failure: set the flag and message that `or` / `try` look at
auto failedIf = [&](Value *ok, Value *errSlot) {
fallibleFailed = b.CreateICmpEQ(ok, b.getInt32(0));
fallibleMsg = b.CreateLoad(ty(FType::Str), errSlot);
};
if (n == "error")
failAt(p.file, p.line, p.col, "error(...) makes an error to return from a function that can fail: return error(\"...\")");
if (n == "print") { if (n == "print") {
checkArgs(c.args, c.argNames, 0, p, n, true); checkArgs(c.args, c.argNames, 0, p, n, true);
return print(c.args, p); return print(c.args, p);
@ -237,6 +267,13 @@ Value_ Codegen::call(const CallExpr &c) {
Value_ path = coerce(expr(one("path")), FType::Str, c.args[0]->pos, "the file name"); Value_ path = coerce(expr(one("path")), FType::Str, c.args[0]->pos, "the file name");
Value *ss = tmpOf(ty(FType::Str)); Value *ss = tmpOf(ty(FType::Str));
b.CreateCall(rt("finch_str_copy"), {ss, tmp(path.v)}); // NUL-terminated copy b.CreateCall(rt("finch_str_copy"), {ss, tmp(path.v)}); // NUL-terminated copy
if (canFail) {
Value *err = tmp(zero(FType::Str));
failedIf(b.CreateCall(rt("finch_try_delete_file"), {ss, err}), err);
dropAt(ss, FType::Str);
release(path);
return {nullptr, FType::Void};
}
Value *r = b.CreateCall(rt("finch_delete_file"), {ss}); Value *r = b.CreateCall(rt("finch_delete_file"), {ss});
dropAt(ss, FType::Str); dropAt(ss, FType::Str);
release(path); release(path);
@ -248,6 +285,10 @@ Value_ Codegen::call(const CallExpr &c) {
Value_ res; Value_ res;
if (n == "file_exists") { if (n == "file_exists") {
res = {b.CreateICmpNE(b.CreateCall(rt("finch_file_exists"), {pp}), b.getInt32(0)), FType::Bool}; res = {b.CreateICmpNE(b.CreateCall(rt("finch_file_exists"), {pp}), b.getInt32(0)), FType::Bool};
} else if (canFail) {
Value *out = tmp(zero(FType::Str)), *err = tmp(zero(FType::Str));
failedIf(b.CreateCall(rt("finch_try_read_file"), {out, pp, err}), err);
res = {b.CreateLoad(ty(FType::Str), out), FType::Str, false, true};
} else { } else {
Value *out = tmpOf(ty(FType::Str)); Value *out = tmpOf(ty(FType::Str));
b.CreateCall(rt("finch_read_file"), {out, pp, fileName(p), b.getInt64(p.line)}); b.CreateCall(rt("finch_read_file"), {out, pp, fileName(p), b.getInt64(p.line)});
@ -269,6 +310,13 @@ Value_ Codegen::call(const CallExpr &c) {
checkArgs(c.args, c.argNames, 2, p, n); checkArgs(c.args, c.argNames, 2, p, n);
Value_ path = coerce(expr(*c.args[0]), FType::Str, c.args[0]->pos, "the file name"); Value_ path = coerce(expr(*c.args[0]), FType::Str, c.args[0]->pos, "the file name");
Value_ text = coerce(expr(*c.args[1]), FType::Str, c.args[1]->pos, "the text"); Value_ text = coerce(expr(*c.args[1]), FType::Str, c.args[1]->pos, "the text");
if (canFail) {
Value *err = tmp(zero(FType::Str));
failedIf(b.CreateCall(rt("finch_try_write_file"), {tmp(path.v), tmp(text.v), err}), err);
release(path);
release(text);
return {nullptr, FType::Void};
}
Value *ok = b.CreateCall(rt("finch_write_file"), {tmp(path.v), tmp(text.v)}); Value *ok = b.CreateCall(rt("finch_write_file"), {tmp(path.v), tmp(text.v)});
release(path); release(path);
release(text); release(text);
@ -277,13 +325,13 @@ Value_ Codegen::call(const CallExpr &c) {
FType target; FType target;
if (typeFromName(n, target)) { if (typeFromName(n, target)) {
checkArgs(c.args, c.argNames, 1, p, n); checkArgs(c.args, c.argNames, 1, p, n);
return convert(target, c.args, p, n); return convert(target, c.args, p, n, canFail);
} }
ModuleScope &m = modules[curModule]; ModuleScope &m = modules[curModule];
if (auto it = m.fns.find(n); it != m.fns.end()) { if (auto it = m.fns.find(n); it != m.fns.end()) {
note(p, n.size(), "```finch\n" + signature(it->second) + "\n```", it->second.decl->namePos); note(p, n.size(), "```finch\n" + signature(it->second) + "\n```", it->second.decl->namePos);
return callFinch(it->second, c.args, c.argNames, p, n); return callFinch(it->second, c.args, c.argNames, p, n, &c);
} }
if (StructInfo *s = findStruct("", n, false)) { if (StructInfo *s = findStruct("", n, false)) {
note(p, n.size(), structHover(s), s->decl ? s->decl->namePos : Pos{}); note(p, n.size(), structHover(s), s->decl ? s->decl->namePos : Pos{});
@ -303,20 +351,33 @@ Value_ Codegen::call(const CallExpr &c) {
} }
Value_ Codegen::callFinch(const Fn &fn, const std::vector<ExprPtr> &args, const std::vector<std::string> &names, Pos p, Value_ Codegen::callFinch(const Fn &fn, const std::vector<ExprPtr> &args, const std::vector<std::string> &names, Pos p,
const std::string &shown) { const std::string &shown, const Expr *site, Value *self, const std::string &selfRoot) {
const FnDecl &d = *fn.decl; const FnDecl &d = *fn.decl;
if (fn.module.empty() && d.name == "main") failAt(p.file, p.line, p.col, "main can't be called, it is where the program starts"); if (fn.module.empty() && d.name == "main" && !fn.recv) failAt(p.file, p.line, p.col, "main can't be called, it is where the program starts");
if (fn.fallible && site != fallibleTarget)
failAt(p.file, p.line, p.col, "'" + shown + "' can fail, so say what happens then: " + shown + "(...) or <value>, " + shown +
"(...) or { ... }, or try " + shown + "(...)");
checkArgs(args, names, fn.params.size(), p, shown); checkArgs(args, names, fn.params.size(), p, shown);
std::vector<Value *> vals; std::vector<Value *> vals;
std::vector<Value_> fresh; std::vector<Value_> fresh;
if (self) vals.push_back(self);
for (size_t i = 0; i < args.size(); i++) { for (size_t i = 0; i < args.size(); i++) {
Value_ v = coerce(exprWant(*args[i], fn.params[i]), fn.params[i], args[i]->pos, "argument '" + d.params[i].name + "'"); Value_ v = coerce(exprWant(*args[i], fn.params[i]), fn.params[i], args[i]->pos, "argument '" + d.params[i].name + "'");
// a method that changes self gets its own copy of anything that lives inside self (p.add(p.items))
if (fn.mutatesSelf && !selfRoot.empty() && owning(v.type) && !v.fresh && rootVar(*args[i]) == selfRoot)
v = {copyValue(v.v, v.type), v.type, false, true};
vals.push_back(v.v); // owning values are lent: the function never drops them vals.push_back(v.v); // owning values are lent: the function never drops them
if (v.fresh) fresh.push_back(v); if (v.fresh) fresh.push_back(v);
} }
setLoc(p); setLoc(p);
Value *r = b.CreateCall(fn.llvm, vals); Value *r = b.CreateCall(fn.llvm, vals);
for (auto &v : fresh) release(v); for (auto &v : fresh) release(v);
if (fn.fallible) {
fallibleFailed = b.CreateNot(b.CreateExtractValue(r, 0));
fallibleMsg = b.CreateExtractValue(r, 2);
if (fn.ret.kind == FType::Void) return {nullptr, FType::Void};
return {b.CreateExtractValue(r, 1), fn.ret, false, owning(fn.ret)};
}
if (fn.ret.kind == FType::Void) return {nullptr, FType::Void}; if (fn.ret.kind == FType::Void) return {nullptr, FType::Void};
return {r, fn.ret, false, owning(fn.ret)}; return {r, fn.ret, false, owning(fn.ret)};
} }
@ -433,7 +494,7 @@ Value_ Codegen::addrOf(const std::vector<ExprPtr> &args, Pos p) {
} }
// Explicit conversions: int(x), u8(x), float(x), char(x), bool(x), str(x), ptr(x) ... // Explicit conversions: int(x), u8(x), float(x), char(x), bool(x), str(x), ptr(x) ...
Value_ Codegen::convert(const FType &to, const std::vector<ExprPtr> &args, Pos, const std::string &name) { Value_ Codegen::convert(const FType &to, const std::vector<ExprPtr> &args, Pos, const std::string &name, bool canFail) {
Value_ v = expr(*args[0]); Value_ v = expr(*args[0]);
const FType &from = v.type; const FType &from = v.type;
Pos p = args[0]->pos; Pos p = args[0]->pos;
@ -449,12 +510,28 @@ Value_ Codegen::convert(const FType &to, const std::vector<ExprPtr> &args, Pos,
else if (from.kind == FType::Array && (from.elem->kind == FType::U8 || from.elem->kind == FType::Char)) { else if (from.kind == FType::Array && (from.elem->kind == FType::U8 || from.elem->kind == FType::Char)) {
b.CreateCall(rt("finch_str_from_bytes"), {out, tmp(v.v)}); b.CreateCall(rt("finch_str_from_bytes"), {out, tmp(v.v)});
release(v); release(v);
} else if (from.kind == FType::Array || from.kind == FType::Fixed || from.kind == FType::Map || from.kind == FType::Struct) {
b.CreateStore(zero(FType::Str), out); // the same text print(...) shows
emitFormat(out, v.v, from, false);
release(v);
} else { } else {
failAt(p.file, p.line, p.col, "can't turn " + from.show() + " into str"); failAt(p.file, p.line, p.col, "can't turn " + from.show() + " into str");
} }
return {b.CreateLoad(ty(FType::Str), out), FType::Str, false, true}; return {b.CreateLoad(ty(FType::Str), out), FType::Str, false, true};
} }
if (from == to) return v; if (from == to) return v;
if (from.kind == FType::Str && (to.isInt() || to.isFloat()) && canFail) { // int(text) or 0
Value *sp = tmp(v.v), *err = tmp(zero(FType::Str));
Value *out = tmp(to.isInt() ? (Value *)b.getInt64(0) : ConstantFP::get(b.getDoubleTy(), 0));
Value *ok = b.CreateCall(rt(to.isInt() ? "finch_str_try_int" : "finch_str_try_float"), {sp, out, err});
fallibleFailed = b.CreateICmpEQ(ok, b.getInt32(0));
fallibleMsg = b.CreateLoad(ty(FType::Str), err);
Value_ r;
if (to.isInt()) r = {intCast(b.CreateLoad(b.getInt64Ty(), out), FType::I64, to), to};
else r = {b.CreateFPCast(b.CreateLoad(b.getDoubleTy(), out), ty(to)), to};
release(v);
return r;
}
if (from.kind == FType::Str && (to.isInt() || to.isFloat())) { if (from.kind == FType::Str && (to.isInt() || to.isFloat())) {
Value *sp = tmp(v.v); Value *sp = tmp(v.v);
Value_ r; Value_ r;
@ -494,7 +571,7 @@ Value_ Codegen::method(const MethodExpr &m) {
Pos namePos{p.line, p.col + 1, p.file}; Pos namePos{p.line, p.col + 1, p.file};
if (auto it = mi->second.fns.find(m.name); it != mi->second.fns.end()) { if (auto it = mi->second.fns.find(m.name); it != mi->second.fns.end()) {
note(namePos, m.name.size(), "```finch\n" + signature(it->second) + "\n```", it->second.decl->namePos); note(namePos, m.name.size(), "```finch\n" + signature(it->second) + "\n```", it->second.decl->namePos);
return callFinch(it->second, m.args, m.argNames, p, mn + "." + m.name); return callFinch(it->second, m.args, m.argNames, p, mn + "." + m.name, &m);
} }
if (auto it = mi->second.structs.find(m.name); it != mi->second.structs.end()) { if (auto it = mi->second.structs.find(m.name); it != mi->second.structs.end()) {
note(namePos, m.name.size(), structHover(it->second), it->second->decl->namePos); note(namePos, m.name.size(), structHover(it->second), it->second->decl->namePos);
@ -507,8 +584,20 @@ Value_ Codegen::method(const MethodExpr &m) {
"there is no variable or module named '" + mn + "' (to use a module, add import " + mn + " at the top)"); "there is no variable or module named '" + mn + "' (to use a module, add import " + mn + " at the top)");
} }
static const std::set<std::string> changing = {"push", "pop", "insert", "remove", "clear", "resize", "sort", "reverse"}; static const std::set<std::string> changing = {"push", "pop", "insert", "remove", "clear", "resize", "sort", "reverse"};
LRef r = ref(*m.obj, changing.count(m.name) > 0); // writing through a map's m[key] adds the key if it's missing: groups[k].push(x)
LRef r = ref(*m.obj, changing.count(m.name) > 0 || mutatingMethods.count(m.name) > 0);
FType t = r.isPlace ? r.pl.type : r.val.type; FType t = r.isPlace ? r.pl.type : r.val.type;
if (t.kind == FType::Struct || (t.isPtr() && t.elem && t.elem->kind == FType::Struct)) return structMethod(m, r, t);
if (changing.count(m.name) && (t.kind == FType::Array || t.kind == FType::Map)) {
std::string root = rootVar(*m.obj);
if (Var *v = root.empty() ? nullptr : lookup(root); v && v->readonly) failAt(p.file, p.line, p.col, v->readonlyWhy);
}
if (t.kind == FType::Map) {
Value_ recv = r.isPlace ? Value_{r.pl.addr, t} : Value_{tmp(r.val.v), t, false, r.val.fresh};
Value_ out = mapMethod(m, &recv);
if (!r.isPlace && r.val.fresh) dropAt(recv.v, t);
return out;
}
if (g_index && (t.kind == FType::Array || t.kind == FType::Str)) { if (g_index && (t.kind == FType::Array || t.kind == FType::Str)) {
auto look = [&](const BuiltinDoc *first, const BuiltinDoc *last) { auto look = [&](const BuiltinDoc *first, const BuiltinDoc *last) {
for (; first != last; ++first) for (; first != last; ++first)
@ -589,11 +678,47 @@ Value_ Codegen::method(const MethodExpr &m) {
if (!r.isPlace) release(sv); if (!r.isPlace) release(sv);
return res; return res;
} }
if (t.kind == FType::Struct)
failAt(p.file, p.line, p.col, "structs don't have methods; write a function and call it: " + m.name + "(value, ...)");
failAt(p.file, p.line, p.col, t.show() + " has no methods"); failAt(p.file, p.line, p.col, t.show() + " has no methods");
} }
// p.move(1, 2): a method of a struct (also through a ptr[Struct]); self is passed by address
Value_ Codegen::structMethod(const MethodExpr &m, LRef &r, const FType &t) {
Pos p = m.pos;
FType st = t.isPtr() ? *t.elem : t;
auto ms = methods.find(st.info);
const Fn *fn = nullptr;
if (ms != methods.end())
if (auto it = ms->second.find(m.name); it != ms->second.end()) fn = &it->second;
if (!fn) {
std::string names;
if (ms != methods.end())
for (auto &[n, f] : ms->second) names += (names.empty() ? "" : ", ") + n;
if (names.empty())
failAt(p.file, p.line, p.col, "the struct " + st.show() + " has no methods; add one with fn " + st.name + "." + m.name + "(...) { ... }");
failAt(p.file, p.line, p.col, "the struct " + st.show() + " has no method '" + m.name + "' (it has: " + names + ")");
}
note(Pos{p.line, p.col + 1, p.file}, m.name.size(), "```finch\n" + signature(*fn) + "\n```", fn->decl->namePos);
std::string root = rootVar(*m.obj);
if (fn->mutatesSelf && !root.empty() && !t.isPtr())
if (Var *v = lookup(root); v && v->readonly) failAt(p.file, p.line, p.col, v->readonlyWhy);
Value *self;
bool temp = false;
if (t.isPtr()) {
self = r.isPlace ? b.CreateLoad(b.getPtrTy(), r.pl.addr) : r.val.v;
panicIf(b.CreateIsNull(self), "called ." + m.name + "() on a null pointer", p);
root.clear(); // self lives somewhere else
} else if (r.isPlace) {
self = r.pl.addr;
} else {
self = tmp(r.val.v); // a temporary struct: Point(1, 2).length()
temp = true;
}
Value_ out = callFinch(*fn, m.args, m.argNames, p, st.name + "." + m.name, &m, self, root);
if (temp && r.val.fresh && owning(st)) dropAt(self, st);
return out;
}
// An element compare for find/contains/sort: -1, 0, 1 (only numbers, chars, bools and str) // An element compare for find/contains/sort: -1, 0, 1 (only numbers, chars, bools and str)
static bool comparable(const FType &t) { static bool comparable(const FType &t) {
return t.isNumber() || t.kind == FType::Char || t.kind == FType::Bool || t.kind == FType::Str; return t.isNumber() || t.kind == FType::Char || t.kind == FType::Bool || t.kind == FType::Str;

View file

@ -80,6 +80,9 @@ struct Fn {
llvm::Function *cThunk = nullptr; llvm::Function *cThunk = nullptr;
FType ret; // resolved types FType ret; // resolved types
std::vector<FType> params; std::vector<FType> params;
StructInfo *recv = nullptr; // a method of this struct: the first LLVM parameter is `self`'s address
bool mutatesSelf = false; // the method changes `self`
bool fallible = false; // -> T!: returns { i1 ok, T value, str error }
}; };
struct Loop { struct Loop {
@ -139,6 +142,13 @@ public:
int deferLimit = -1; // while emitting a defer: hide variables declared after it int deferLimit = -1; // while emitting a defer: hide variables declared after it
int inDefer = 0; int inDefer = 0;
std::map<std::string, llvm::Function *> helpers; // drop/copy/print per type std::map<std::string, llvm::Function *> helpers; // drop/copy/print per type
std::map<StructInfo *, std::map<std::string, Fn>> methods;
std::set<std::string> mutatingMethods; // names of methods that change self (for the mutation analysis)
// errors as values: the call that `or` / `try` is handling reports here whether it failed
const Expr *fallibleTarget = nullptr;
llvm::Value *fallibleFailed = nullptr; // i1
llvm::Value *fallibleMsg = nullptr; // str
// debug info (-g) // debug info (-g)
bool debug; bool debug;
@ -164,6 +174,9 @@ public:
void forStmt(const ForStmt &s); void forStmt(const ForStmt &s);
void forEachStmt(const ForEachStmt &s); void forEachStmt(const ForEachStmt &s);
void returnStmt(const ReturnStmt &s); void returnStmt(const ReturnStmt &s);
llvm::Type *retType(const Fn &f);
void retValue(llvm::Value *v); // return from the current function (wrapped as success if it can fail)
void retError(llvm::Value *msg); // fail the current function with the str `msg`
void jump(bool isBreak, Pos p); void jump(bool isBreak, Pos p);
llvm::Value *condition(const Expr &e); llvm::Value *condition(const Expr &e);
void continueAt(llvm::BasicBlock *bb, llvm::BasicBlock *deadEnd); void continueAt(llvm::BasicBlock *bb, llvm::BasicBlock *deadEnd);
@ -194,6 +207,10 @@ public:
LRef member(const MemberExpr &m, bool forWrite); LRef member(const MemberExpr &m, bool forWrite);
LRef index(const IndexExpr &e, bool forWrite); LRef index(const IndexExpr &e, bool forWrite);
Value_ arrayLit(const ArrayLitExpr &a, const FType *want); Value_ arrayLit(const ArrayLitExpr &a, const FType *want);
Value_ fallible(const Expr &call, llvm::Value *&failed, llvm::Value *&msg);
Value_ tryExpr(const TryExpr &t);
Value_ orElse(const OrElseExpr &e, bool discard = false); // discard: a statement, the value isn't used
FType structType(StructInfo *s);
std::string rootVar(const Expr &e); std::string rootVar(const Expr &e);
bool mutates(const std::string &name, const Stmt &s); bool mutates(const std::string &name, const Stmt &s);
bool mutatesExpr(const std::string &name, const Expr &e); bool mutatesExpr(const std::string &name, const Expr &e);
@ -225,7 +242,9 @@ public:
std::string typeKey(const FType &t); std::string typeKey(const FType &t);
llvm::Value *tmp(llvm::Value *v); // a stack copy of v, to pass by address llvm::Value *tmp(llvm::Value *v); // a stack copy of v, to pass by address
void emitPrint(llvm::Value *v, const FType &t, bool quoted); void emitPrint(llvm::Value *v, const FType &t, bool quoted);
llvm::Function *printFn(const FType &t); void emitFormat(llvm::Value *buf, llvm::Value *v, const FType &t, bool quoted); // append v as text to the str at buf
void bufText(llvm::Value *buf, const std::string &text);
llvm::Function *formatFn(const FType &t);
void printf_(const std::string &fmt, std::vector<llvm::Value *> args); void printf_(const std::string &fmt, std::vector<llvm::Value *> args);
llvm::Value *strConst(const std::string &s); llvm::Value *strConst(const std::string &s);
llvm::Value *cstr(llvm::Value *strValue); // str -> char* for C llvm::Value *cstr(llvm::Value *strValue); // str -> char* for C
@ -236,13 +255,26 @@ public:
Value_ call(const CallExpr &c); Value_ call(const CallExpr &c);
Value_ method(const MethodExpr &m); Value_ method(const MethodExpr &m);
Value_ callFinch(const Fn &fn, const std::vector<ExprPtr> &args, const std::vector<std::string> &names, Pos p, Value_ callFinch(const Fn &fn, const std::vector<ExprPtr> &args, const std::vector<std::string> &names, Pos p,
const std::string &shownName); const std::string &shownName, const Expr *site, llvm::Value *self = nullptr,
const std::string &selfRoot = "");
Value_ construct(StructInfo *s, const std::vector<ExprPtr> &args, const std::vector<std::string> &names, Pos p); Value_ construct(StructInfo *s, const std::vector<ExprPtr> &args, const std::vector<std::string> &names, Pos p);
Value_ callC(const CFunc &f, const std::vector<ExprPtr> &args, Pos p); Value_ callC(const CFunc &f, const std::vector<ExprPtr> &args, Pos p);
Value_ convert(const FType &to, const std::vector<ExprPtr> &args, Pos p, const std::string &name); Value_ convert(const FType &to, const std::vector<ExprPtr> &args, Pos p, const std::string &name, bool canFail = false);
Value_ print(const std::vector<ExprPtr> &args, Pos p); Value_ print(const std::vector<ExprPtr> &args, Pos p);
Value_ addrOf(const std::vector<ExprPtr> &args, Pos p); Value_ addrOf(const std::vector<ExprPtr> &args, Pos p);
Value_ arrayMethod(const MethodExpr &m, Value_ *recv); Value_ arrayMethod(const MethodExpr &m, Value_ *recv);
Value_ structMethod(const MethodExpr &m, LRef &r, const FType &t);
// ---------- codegen_map.cpp: maps ----------
llvm::StructType *mapEntryTy(const FType &t);
Value_ mapLit(const MapLitExpr &e, const FType *want);
LRef mapIndex(const IndexExpr &e, LRef &obj, const FType &t, bool forWrite);
Value_ mapMethod(const MethodExpr &m, Value_ *recv);
std::vector<llvm::Value *> mapArgs(llvm::Value *map, llvm::Value *keyAddr, const FType &t);
llvm::Value *mapSlot(llvm::Value *map, const FType &t, const Value_ &key, Pos p, bool fill);
llvm::Value *mapEntry(llvm::Value *map, const FType &t, llvm::Value *i);
void forEachEntry(llvm::Value *map, const FType &t, const std::function<void(llvm::Value *i, llvm::Value *entry)> &body);
llvm::Value *keyText(const Value_ &key);
bool isBuiltin(const std::string &n); bool isBuiltin(const std::string &n);
std::string headerHint(const std::string &name); std::string headerHint(const std::string &name);
void checkArgs(const std::vector<ExprPtr> &args, const std::vector<std::string> &names, size_t want, Pos p, void checkArgs(const std::vector<ExprPtr> &args, const std::vector<std::string> &names, size_t want, Pos p,

242
src/codegen_map.cpp Normal file
View file

@ -0,0 +1,242 @@
// Code generation, part 4: maps.
//
// map[K]V is { entries, len, used, cap, index, icap } (see runtime/finch_rt.c). The runtime finds and
// places entries; the compiler stores, copies and drops the keys and values in them, like it does
// for array elements. An entry is { i64 hash, K key, V value }; hash 0 marks a removed entry.
#include "codegen_impl.h"
#include "builtins_doc.h"
using namespace llvm;
namespace finch {
StructType *Codegen::mapEntryTy(const FType &t) {
return StructType::get(ctx, {b.getInt64Ty(), ty(*t.key), ty(*t.elem)});
}
// The runtime's (map, key, entry size, key size, key is str) arguments.
std::vector<Value *> Codegen::mapArgs(Value *map, Value *keyAddr, const FType &t) {
return {map, keyAddr, b.getInt64(dl->getTypeAllocSize(mapEntryTy(t))), elemSize(*t.key),
b.getInt32(t.key->kind == FType::Str ? 1 : 0)};
}
Value *Codegen::mapEntry(Value *map, const FType &t, Value *i) {
Value *entries = b.CreateLoad(b.getPtrTy(), b.CreateStructGEP(ty(t), map, 0));
return b.CreateInBoundsGEP(mapEntryTy(t), entries, i);
}
// The entry for `key`, added if it's missing: the key is copied in, and the value is its type's
// default when `fill` (m[k] += 1 starts from 0).
Value *Codegen::mapSlot(Value *map, const FType &t, const Value_ &key, Pos p, bool fill) {
StructType *ET = mapEntryTy(t);
Value *keyAddr = tmp(key.v);
Value *added = tmpOf(b.getInt32Ty());
std::vector<Value *> args = mapArgs(map, keyAddr, t);
args.push_back(added);
Value *entry = mapEntry(map, t, b.CreateCall(rt("finch_map_slot"), args));
BasicBlock *fresh = newBlock("map.new"), *have = newBlock("map.have");
b.CreateCondBr(b.CreateICmpNE(b.CreateLoad(b.getInt32Ty(), added), b.getInt32(0)), fresh, have);
b.SetInsertPoint(fresh);
copyAt(b.CreateStructGEP(ET, entry, 1), keyAddr, *t.key);
if (fill) b.CreateStore(own(defaultValue(*t.elem, p)), b.CreateStructGEP(ET, entry, 2));
b.CreateBr(have);
b.SetInsertPoint(have);
return entry;
}
// Every live entry, in insertion order: body(entry number, entry address).
void Codegen::forEachEntry(Value *map, const FType &t, const std::function<void(Value *, Value *)> &body) {
Value *used = b.CreateLoad(b.getInt64Ty(), b.CreateStructGEP(ty(t), map, 2));
forN(used, [&](Value *i) {
Value *entry = mapEntry(map, t, i);
Value *hash = b.CreateLoad(b.getInt64Ty(), b.CreateStructGEP(mapEntryTy(t), entry, 0));
BasicBlock *live = newBlock("entry.live"), *next = newBlock("entry.next");
b.CreateCondBr(b.CreateICmpNE(hash, b.getInt64(0)), live, next);
b.SetInsertPoint(live);
body(i, entry);
b.CreateBr(next);
b.SetInsertPoint(next);
});
}
// A key as text for error messages: "bob" or 42
Value *Codegen::keyText(const Value_ &key) {
Value *out = tmpOf(ty(FType::Str));
const FType &t = key.type;
if (t.kind == FType::Str) {
Value *q = tmp(strConst("\"")), *half = tmpOf(ty(FType::Str));
b.CreateCall(rt("finch_str_concat"), {half, q, tmp(key.v)});
b.CreateCall(rt("finch_str_concat"), {out, half, q});
} else if (t.isSigned()) {
b.CreateCall(rt("finch_str_from_int"), {out, b.CreateSExt(key.v, b.getInt64Ty())});
} else if (t.isUnsigned()) {
b.CreateCall(rt("finch_str_from_uint"), {out, b.CreateZExt(key.v, b.getInt64Ty())});
} else if (t.kind == FType::Char) {
b.CreateCall(rt("finch_str_from_char"), {out, key.v});
} else {
b.CreateCall(rt("finch_str_from_bool"), {out, b.CreateZExt(key.v, b.getInt32Ty())});
}
return out;
}
// m[key]: reading a missing key stops the program; writing (m[k] = v, m[k] += 1, m[k].push(x)) adds it
LRef Codegen::mapIndex(const IndexExpr &e, LRef &obj, const FType &t, bool forWrite) {
Pos p = e.pos;
FType kt = *t.key, vt = *t.elem;
Value_ key = coerce(exprWant(*e.index, kt), kt, e.index->pos, "the key");
if (forWrite) {
if (!obj.isPlace) failAt(p.file, p.line, p.col, "this is a temporary map, it can't be changed");
Value *entry = mapSlot(obj.pl.addr, t, key, p, true);
release(key);
return {true, {b.CreateStructGEP(mapEntryTy(t), entry, 2), vt}, {}};
}
Value *map = obj.isPlace ? obj.pl.addr : tmp(obj.val.v);
Value *i = b.CreateCall(rt("finch_map_find"), mapArgs(map, tmp(key.v), t));
BasicBlock *missing = newBlock("map.missing"), *found = newBlock("map.found");
b.CreateCondBr(b.CreateICmpSLT(i, b.getInt64(0)), missing, found);
b.SetInsertPoint(missing);
b.CreateCall(rt("finch_map_missing"), {fileName(p), b.getInt64(p.line), keyText(key)});
b.CreateUnreachable();
b.SetInsertPoint(found);
release(key);
Value *valueAddr = b.CreateStructGEP(mapEntryTy(t), mapEntry(map, t, i), 2);
if (obj.isPlace) return {true, {valueAddr, vt}, {}};
Value *v = b.CreateLoad(ty(vt), valueAddr); // from a temporary map: take the value, drop the rest
Value_ out{v, vt};
if (owning(vt) && obj.val.fresh) out = {copyValue(v, vt), vt, false, true};
release(obj.val);
return {false, {}, out};
}
// ["a": 1, "b": 2], or [:] where the type is known
Value_ Codegen::mapLit(const MapLitExpr &e, const FType *want) {
Pos p = e.pos;
FType t;
Value_ k0, v0; // the first entry, when it decides the types
bool first = false;
if (want && want->kind == FType::Map) {
t = *want;
} else if (e.keys.empty()) {
failAt(p.file, p.line, p.col, "an empty [:] needs a type, like: map[str]int m = [:]");
} else {
// number values written in the code are all floats if any of them has a dot
bool allNum = true, anyFloat = false;
for (auto &x : e.values) {
const Expr *y = x.get();
if (y->kind == ExprKind::Unary && static_cast<const UnaryExpr *>(y)->op == '-') y = static_cast<const UnaryExpr *>(y)->operand.get();
if (y->kind == ExprKind::Float) anyFloat = true;
else if (y->kind != ExprKind::Int) allNum = false;
}
k0 = expr(*e.keys[0]);
v0 = expr(*e.values[0]);
Pos kp = e.keys[0]->pos, vp = e.values[0]->pos;
if (k0.type.kind == FType::Void || k0.type.kind == FType::Null) failAt(kp.file, kp.line, kp.col, "a map key needs a value with a type");
if (v0.type.kind == FType::Void || v0.type.kind == FType::Null) failAt(vp.file, vp.line, vp.col, "a map value needs a value with a type");
t = resolve(FType::mapOf(k0.type, allNum && anyFloat ? FType(FType::F64) : v0.type), kp);
first = true;
}
Value *map = tmp(zero(t));
StructType *ET = mapEntryTy(t);
for (size_t i = 0; i < e.keys.size(); i++) {
Value_ k = i == 0 && first ? k0 : exprWant(*e.keys[i], *t.key);
k = coerce(k, *t.key, e.keys[i]->pos, "every key of this map");
Value_ v = i == 0 && first ? v0 : exprWant(*e.values[i], *t.elem);
Value *vv = own(coerce(v, *t.elem, e.values[i]->pos, "every value of this map"));
Value *valueAddr = b.CreateStructGEP(ET, mapSlot(map, t, k, p, false), 2);
dropAt(valueAddr, *t.elem); // a key given twice: the last value wins
b.CreateStore(vv, valueAddr);
release(k);
}
return {b.CreateLoad(ty(t), map), t, false, true};
}
Value_ Codegen::mapMethod(const MethodExpr &m, Value_ *recv) {
Pos p = m.pos;
const std::string &n = m.name;
FType t = recv->type, kt = *t.key, vt = *t.elem;
StructType *ET = mapEntryTy(t);
Value *map = recv->v;
auto need = [&](size_t k) { checkArgs(m.args, m.argNames, k, p, n); };
auto keyArg = [&]() { return coerce(exprWant(*m.args[0], kt), kt, m.args[0]->pos, "the key"); };
if (g_index)
for (const BuiltinDoc &d : kMapMethods)
if (n == d.name) note(Pos{p.line, p.col + 1, p.file}, n.size(), "```finch\n" + std::string(d.signature) + "\n```\n" + d.doc);
if (n == "has") {
need(1);
Value_ k = keyArg();
Value *i = b.CreateCall(rt("finch_map_find"), mapArgs(map, tmp(k.v), t));
release(k);
return {b.CreateICmpSGE(i, b.getInt64(0)), FType::Bool};
}
if (n == "get") { // m.get(key, default): the default is only computed when the key is missing
need(2);
Value_ k = keyArg();
Value *i = b.CreateCall(rt("finch_map_find"), mapArgs(map, tmp(k.v), t));
release(k);
BasicBlock *found = newBlock("get.found"), *missing = newBlock("get.missing"), *end = newBlock("get.end");
b.CreateCondBr(b.CreateICmpSGE(i, b.getInt64(0)), found, missing);
b.SetInsertPoint(found);
Value *v = b.CreateLoad(ty(vt), b.CreateStructGEP(ET, mapEntry(map, t, i), 2));
v = copyValue(v, vt);
BasicBlock *foundEnd = b.GetInsertBlock();
b.CreateBr(end);
b.SetInsertPoint(missing);
Value *d = own(coerce(exprWant(*m.args[1], vt), vt, m.args[1]->pos, "the default"));
BasicBlock *missingEnd = b.GetInsertBlock();
b.CreateBr(end);
b.SetInsertPoint(end);
PHINode *phi = b.CreatePHI(ty(vt), 2);
phi->addIncoming(v, foundEnd);
phi->addIncoming(d, missingEnd);
return {phi, vt, false, owning(vt)};
}
if (n == "remove") { // true if the key was there
need(1);
Value_ k = keyArg();
Value *i = b.CreateCall(rt("finch_map_find"), mapArgs(map, tmp(k.v), t));
release(k);
BasicBlock *hit = newBlock("remove.hit"), *end = newBlock("remove.end");
Value *was = b.CreateICmpSGE(i, b.getInt64(0));
b.CreateCondBr(was, hit, end);
b.SetInsertPoint(hit);
Value *entry = mapEntry(map, t, i);
dropAt(b.CreateStructGEP(ET, entry, 1), kt);
dropAt(b.CreateStructGEP(ET, entry, 2), vt);
b.CreateCall(rt("finch_map_remove_at"), {map, i, b.getInt64(dl->getTypeAllocSize(ET))});
b.CreateBr(end);
b.SetInsertPoint(end);
return {was, FType::Bool};
}
if (n == "clear") {
need(0);
if (owning(kt) || owning(vt))
forEachEntry(map, t, [&](Value *, Value *entry) {
dropAt(b.CreateStructGEP(ET, entry, 1), kt);
dropAt(b.CreateStructGEP(ET, entry, 2), vt);
});
b.CreateCall(rt("finch_map_clear"), {map});
return {nullptr, FType::Void};
}
if (n == "keys" || n == "values") { // a new array, in insertion order
need(0);
FType et = n == "keys" ? kt : vt;
unsigned field = n == "keys" ? 1 : 2;
FType at = FType::arrayOf(et);
Value *out = tmpOf(ty(at));
Value *len = b.CreateLoad(b.getInt64Ty(), b.CreateStructGEP(ty(t), map, 1));
b.CreateCall(rt("finch_arr_make"), {out, len, elemSize(et)});
Value *data = b.CreateLoad(b.getPtrTy(), b.CreateStructGEP(ty(at), out, 0));
Value *j = tmp(b.getInt64(0));
forEachEntry(map, t, [&](Value *, Value *entry) {
Value *jv = b.CreateLoad(b.getInt64Ty(), j);
copyAt(b.CreateInBoundsGEP(ty(et), data, jv), b.CreateStructGEP(ET, entry, field), et);
b.CreateStore(b.CreateAdd(jv, b.getInt64(1)), j);
});
return {b.CreateLoad(ty(at), out), at, false, true};
}
failAt(p.file, p.line, p.col, "a map has no method '" + n + "' (it has: has, get, remove, clear, keys, values; and m[key], .len)");
}
} // namespace finch

View file

@ -25,6 +25,10 @@ llvm::Type *Codegen::ty(const FType &t) {
case FType::F64: return b.getDoubleTy(); case FType::F64: return b.getDoubleTy();
case FType::Str: case FType::Str:
case FType::Array: return StructType::get(ctx, {b.getPtrTy(), b.getInt64Ty(), b.getInt64Ty()}); case FType::Array: return StructType::get(ctx, {b.getPtrTy(), b.getInt64Ty(), b.getInt64Ty()});
case FType::Map: {
llvm::Type *P = b.getPtrTy(), *I = b.getInt64Ty();
return StructType::get(ctx, {P, I, I, I, P, I}); // entries, len, used, cap, index, icap
}
case FType::Ptr: case FType::Ptr:
case FType::Null: return b.getPtrTy(); case FType::Null: return b.getPtrTy();
case FType::Fixed: return ArrayType::get(ty(*t.elem), t.count); case FType::Fixed: return ArrayType::get(ty(*t.elem), t.count);
@ -60,6 +64,14 @@ FType Codegen::resolveT(const FType &t, Pos p, bool deep) {
if (!t.elem) return t; if (!t.elem) return t;
return FType::ptrTo(resolveT(*t.elem, p, false)); return FType::ptrTo(resolveT(*t.elem, p, false));
case FType::Array: return FType::arrayOf(resolveT(*t.elem, p, false)); case FType::Array: return FType::arrayOf(resolveT(*t.elem, p, false));
case FType::Map: {
FType k = resolveT(*t.key, p, true);
if (!k.isInt() && k.kind != FType::Char && k.kind != FType::Bool && k.kind != FType::Str)
failAt(p.file, p.line, p.col, "a map key must be a whole number, a char, a bool or a str, not " + k.show());
FType v = resolveT(*t.elem, p, false);
if (v.kind == FType::Void) failAt(p.file, p.line, p.col, "a map value can't have the type 'nothing'");
return FType::mapOf(k, v);
}
case FType::Fixed: { case FType::Fixed: {
FType f = t; FType f = t;
f.elem = std::make_shared<FType>(resolveT(*t.elem, p, deep)); f.elem = std::make_shared<FType>(resolveT(*t.elem, p, deep));
@ -136,7 +148,8 @@ void Codegen::resolveStruct(StructInfo *s) {
bool Codegen::owning(const FType &t) { bool Codegen::owning(const FType &t) {
switch (t.kind) { switch (t.kind) {
case FType::Str: case FType::Str:
case FType::Array: return true; case FType::Array:
case FType::Map: return true;
case FType::Struct: case FType::Struct:
resolveStruct(t.info); resolveStruct(t.info);
return t.info->owning; return t.info->owning;
@ -279,6 +292,7 @@ void Codegen::forN(Value *n, const std::function<void(Value *)> &body) {
std::string Codegen::typeKey(const FType &t) { std::string Codegen::typeKey(const FType &t) {
switch (t.kind) { switch (t.kind) {
case FType::Array: return "arr_" + typeKey(*t.elem); case FType::Array: return "arr_" + typeKey(*t.elem);
case FType::Map: return "map_" + typeKey(*t.key) + "_" + typeKey(*t.elem);
case FType::Fixed: return "fix" + std::to_string(t.count) + "_" + typeKey(*t.elem); case FType::Fixed: return "fix" + std::to_string(t.count) + "_" + typeKey(*t.elem);
case FType::Ptr: return "ptr"; case FType::Ptr: return "ptr";
case FType::Struct: return "S_" + (t.info->module.empty() ? std::string("main") : t.info->module) + "_" + t.info->name; case FType::Struct: return "S_" + (t.info->module.empty() ? std::string("main") : t.info->module) + "_" + t.info->name;
@ -315,6 +329,14 @@ Function *Codegen::dropFn(const FType &t) {
forN(len, [&](Value *i) { dropAt(b.CreateInBoundsGEP(ty(et), data, i), et); }); forN(len, [&](Value *i) { dropAt(b.CreateInBoundsGEP(ty(et), data, i), et); });
} }
b.CreateCall(rt("finch_arr_free"), {a}); b.CreateCall(rt("finch_arr_free"), {a});
} else if (t.kind == FType::Map) {
FType kt = *t.key, vt = *t.elem;
if (owning(kt) || owning(vt))
forEachEntry(a, t, [&](Value *, Value *entry) {
dropAt(b.CreateStructGEP(mapEntryTy(t), entry, 1), kt);
dropAt(b.CreateStructGEP(mapEntryTy(t), entry, 2), vt);
});
b.CreateCall(rt("finch_map_free"), {a});
} else if (t.kind == FType::Struct) { } else if (t.kind == FType::Struct) {
for (auto &fl : t.info->fields) for (auto &fl : t.info->fields)
if (owning(fl.type)) dropAt(b.CreateStructGEP(t.info->llvm, a, fl.llvmIndex), fl.type); if (owning(fl.type)) dropAt(b.CreateStructGEP(t.info->llvm, a, fl.llvmIndex), fl.type);
@ -342,6 +364,16 @@ Function *Codegen::copyFn(const FType &t) {
copyAt(b.CreateInBoundsGEP(ty(et), d, i), b.CreateInBoundsGEP(ty(et), s, i), et); copyAt(b.CreateInBoundsGEP(ty(et), d, i), b.CreateInBoundsGEP(ty(et), s, i), et);
}); });
} }
} else if (t.kind == FType::Map) {
StructType *ET = mapEntryTy(t);
FType kt = *t.key, vt = *t.elem;
b.CreateCall(rt("finch_map_clone_raw"), {dst, src, b.getInt64(dl->getTypeAllocSize(ET))});
if (owning(kt) || owning(vt)) // the slots were copied byte for byte: now give the copy its own keys and values
forEachEntry(src, t, [&](Value *i, Value *from) {
Value *to = mapEntry(dst, t, i);
copyAt(b.CreateStructGEP(ET, to, 1), b.CreateStructGEP(ET, from, 1), kt);
copyAt(b.CreateStructGEP(ET, to, 2), b.CreateStructGEP(ET, from, 2), vt);
});
} else if (t.kind == FType::Struct) { } else if (t.kind == FType::Struct) {
b.CreateStore(b.CreateLoad(ty(t), src), dst); b.CreateStore(b.CreateLoad(ty(t), src), dst);
for (auto &fl : t.info->fields) for (auto &fl : t.info->fields)
@ -388,30 +420,37 @@ void Codegen::emitPrint(Value *v, const FType &t, bool quoted) {
else if (t.kind == FType::Bool) printf_("%s", {b.CreateSelect(v, b.CreateGlobalString("true", "true"), b.CreateGlobalString("false", "false"))}); else if (t.kind == FType::Bool) printf_("%s", {b.CreateSelect(v, b.CreateGlobalString("true", "true"), b.CreateGlobalString("false", "false"))});
else if (t.kind == FType::Str) else if (t.kind == FType::Str)
printf_(quoted ? "\"%.*s\"" : "%.*s", {b.CreateTrunc(b.CreateExtractValue(v, 1), b.getInt32Ty()), cstr(v)}); printf_(quoted ? "\"%.*s\"" : "%.*s", {b.CreateTrunc(b.CreateExtractValue(v, 1), b.getInt32Ty()), cstr(v)});
else if (t.isPtr()) { // the same on every system: null or 0x... else { // pointers, arrays, maps, structs: built as text first (the same text str(x) gives)
Value *isNull = b.CreateIsNull(v); Value *buf = tmp(zero(FType::Str));
BasicBlock *nul = newBlock("ptr.null"), *addr = newBlock("ptr.addr"), *done = newBlock("ptr.done"); emitFormat(buf, v, t, quoted);
b.CreateCondBr(isNull, nul, addr); b.CreateCall(rt("finch_buf_print"), {buf});
b.SetInsertPoint(nul);
printf_("null", {});
b.CreateBr(done);
b.SetInsertPoint(addr);
printf_("0x%llx", {b.CreatePtrToInt(v, b.getInt64Ty())});
b.CreateBr(done);
b.SetInsertPoint(done);
} }
else if (t.kind == FType::Null) printf_("null", {});
else b.CreateCall(printFn(t), {tmp(v)});
} }
Function *Codegen::printFn(const FType &t) { void Codegen::bufText(Value *buf, const std::string &text) {
std::string key = "print." + typeKey(t); b.CreateCall(rt("finch_buf_add"), {buf, b.CreateGlobalString(text, "txt"), b.getInt64(text.size())});
}
void Codegen::emitFormat(Value *buf, Value *v, const FType &t, bool quoted) {
if (t.isSigned()) b.CreateCall(rt("finch_buf_int"), {buf, b.CreateSExt(v, b.getInt64Ty())});
else if (t.isUnsigned()) b.CreateCall(rt("finch_buf_uint"), {buf, b.CreateZExt(v, b.getInt64Ty())});
else if (t.isFloat()) b.CreateCall(rt("finch_buf_float"), {buf, b.CreateFPExt(v, b.getDoubleTy())});
else if (t.kind == FType::Char) b.CreateCall(rt("finch_buf_char"), {buf, v, b.getInt32(quoted)});
else if (t.kind == FType::Bool) b.CreateCall(rt("finch_buf_bool"), {buf, b.CreateZExt(v, b.getInt32Ty())});
else if (t.kind == FType::Str) b.CreateCall(rt("finch_buf_str"), {buf, tmp(v), b.getInt32(quoted)});
else if (t.isPtr() || t.kind == FType::Null) b.CreateCall(rt("finch_buf_ptr"), {buf, v}); // the same everywhere: null or 0x...
else b.CreateCall(formatFn(t), {buf, tmp(v)});
}
// One function per array / map / struct type: (str *buf, T *value)
Function *Codegen::formatFn(const FType &t) {
std::string key = "format." + typeKey(t);
if (auto it = helpers.find(key); it != helpers.end()) return it->second; if (auto it = helpers.find(key); it != helpers.end()) return it->second;
Function *f = Function::Create(FunctionType::get(b.getVoidTy(), {b.getPtrTy()}, false), Function::InternalLinkage, Function *f = Function::Create(FunctionType::get(b.getVoidTy(), {b.getPtrTy(), b.getPtrTy()}, false),
"finch." + key, mod.get()); Function::InternalLinkage, "finch." + key, mod.get());
helpers[key] = f; helpers[key] = f;
HelperScope hs(*this, f); HelperScope hs(*this, f);
Value *a = f->getArg(0); Value *buf = f->getArg(0), *a = f->getArg(1);
if (t.kind == FType::Array || t.kind == FType::Fixed) { if (t.kind == FType::Array || t.kind == FType::Fixed) {
FType et = *t.elem; FType et = *t.elem;
Value *data, *len; Value *data, *len;
@ -422,26 +461,44 @@ Function *Codegen::printFn(const FType &t) {
data = b.CreateLoad(b.getPtrTy(), b.CreateStructGEP(ty(t), a, 0)); data = b.CreateLoad(b.getPtrTy(), b.CreateStructGEP(ty(t), a, 0));
len = b.CreateLoad(b.getInt64Ty(), b.CreateStructGEP(ty(t), a, 1)); len = b.CreateLoad(b.getInt64Ty(), b.CreateStructGEP(ty(t), a, 1));
} }
printf_("[", {}); bufText(buf, "[");
forN(len, [&](Value *i) { forN(len, [&](Value *i) {
BasicBlock *sep = newBlock("sep"), *item = newBlock("item"); BasicBlock *sep = newBlock("sep"), *item = newBlock("item");
b.CreateCondBr(b.CreateICmpSGT(i, b.getInt64(0)), sep, item); b.CreateCondBr(b.CreateICmpSGT(i, b.getInt64(0)), sep, item);
b.SetInsertPoint(sep); b.SetInsertPoint(sep);
printf_(", ", {}); bufText(buf, ", ");
b.CreateBr(item); b.CreateBr(item);
b.SetInsertPoint(item); b.SetInsertPoint(item);
emitPrint(b.CreateLoad(ty(et), b.CreateInBoundsGEP(ty(et), data, i)), et, true); emitFormat(buf, b.CreateLoad(ty(et), b.CreateInBoundsGEP(ty(et), data, i)), et, true);
}); });
printf_("]", {}); bufText(buf, "]");
} else if (t.kind == FType::Map) { // {"a": 1, "b": 2}
StructType *ET = mapEntryTy(t);
Value *firstItem = tmp(b.getInt1(true));
bufText(buf, "{");
forEachEntry(a, t, [&](Value *, Value *entry) {
BasicBlock *sep = newBlock("sep"), *item = newBlock("item");
b.CreateCondBr(b.CreateLoad(b.getInt1Ty(), firstItem), item, sep);
b.SetInsertPoint(sep);
bufText(buf, ", ");
b.CreateBr(item);
b.SetInsertPoint(item);
b.CreateStore(b.getInt1(false), firstItem);
emitFormat(buf, b.CreateLoad(ty(*t.key), b.CreateStructGEP(ET, entry, 1)), *t.key, true);
bufText(buf, ": ");
emitFormat(buf, b.CreateLoad(ty(*t.elem), b.CreateStructGEP(ET, entry, 2)), *t.elem, true);
});
bufText(buf, "}");
} else if (t.kind == FType::Struct) { } else if (t.kind == FType::Struct) {
printf_(t.info->name + "(", {}); bufText(buf, t.info->name + "(");
bool first = true; bool first = true;
for (auto &fl : t.info->fields) { for (auto &fl : t.info->fields) {
printf_((first ? "" : ", ") + fl.name + ": ", {}); if (fl.hidden) continue;
bufText(buf, (first ? "" : ", ") + fl.name + ": ");
first = false; first = false;
emitPrint(b.CreateLoad(ty(fl.type), b.CreateStructGEP(t.info->llvm, a, fl.llvmIndex)), fl.type, true); emitFormat(buf, b.CreateLoad(ty(fl.type), b.CreateStructGEP(t.info->llvm, a, fl.llvmIndex)), fl.type, true);
} }
printf_(")", {}); bufText(buf, ")");
} }
b.CreateRetVoid(); b.CreateRetVoid();
return f; return f;
@ -486,6 +543,16 @@ DIType *Codegen::diType(const FType &t) {
d = di->createStructType(diUnit, t.show(), f, 0, 192, 64, DINode::FlagZero, nullptr, di->getOrCreateArray(els)); d = di->createStructType(diUnit, t.show(), f, 0, 192, 64, DINode::FlagZero, nullptr, di->getOrCreateArray(els));
break; break;
} }
case FType::Map: { // shown by its parts; the entries are behind `entries`
DIType *i64 = diType(FType::I64), *pt = di->createPointerType(nullptr, 64);
DIFile *f = diFiles[0];
const char *names[] = {"entries", "len", "used", "cap", "index", "icap"};
std::vector<Metadata *> els;
for (unsigned k = 0; k < 6; k++)
els.push_back(di->createMemberType(diUnit, names[k], f, 0, 64, 64, 64 * k, DINode::FlagZero, k == 0 || k == 4 ? pt : i64));
d = di->createStructType(diUnit, t.show(), f, 0, 384, 64, DINode::FlagZero, nullptr, di->getOrCreateArray(els));
break;
}
case FType::Struct: { case FType::Struct: {
StructInfo *s = t.info; StructInfo *s = t.info;
DIFile *f = s->isC ? diFiles[0] : diFiles[s->pos.file]; DIFile *f = s->isC ? diFiles[0] : diFiles[s->pos.file];

View file

@ -10,7 +10,7 @@ static const std::unordered_map<std::string, Tok> keywords = {
{"in", Tok::In}, {"break", Tok::Break}, {"continue", Tok::Continue}, {"in", Tok::In}, {"break", Tok::Break}, {"continue", Tok::Continue},
{"true", Tok::True}, {"false", Tok::False}, {"null", Tok::Null}, {"true", Tok::True}, {"false", Tok::False}, {"null", Tok::Null},
{"import", Tok::Import}, {"link", Tok::Link}, {"struct", Tok::Struct}, {"import", Tok::Import}, {"link", Tok::Link}, {"struct", Tok::Struct},
{"defer", Tok::Defer}, {"defer", Tok::Defer}, {"or", Tok::OrElse}, {"try", Tok::Try},
}; };
std::vector<Token> lex(int file) { std::vector<Token> lex(int file) {
@ -169,6 +169,8 @@ const char *tokName(Tok t) {
case Tok::Link: return "'link'"; case Tok::Link: return "'link'";
case Tok::Struct: return "'struct'"; case Tok::Struct: return "'struct'";
case Tok::Defer: return "'defer'"; case Tok::Defer: return "'defer'";
case Tok::OrElse: return "'or'";
case Tok::Try: return "'try'";
case Tok::Colon: return "':'"; case Tok::Colon: return "':'";
case Tok::LParen: return "'('"; case Tok::LParen: return "'('";
case Tok::RParen: return "')'"; case Tok::RParen: return "')'";

View file

@ -9,6 +9,7 @@ enum class Tok {
// keywords // keywords
Fn, Return, If, Else, While, For, In, Break, Continue, True, False, Null, Import, Link, Struct, Defer, Fn, Return, If, Else, While, For, In, Break, Continue, True, False, Null, Import, Link, Struct, Defer,
OrElse, Try, // or, try
// punctuation // punctuation
LParen, RParen, LBrace, RBrace, LBracket, RBracket, Comma, Colon, Dot, DotDot, Arrow, LParen, RParen, LBrace, RBrace, LBracket, RBracket, Comma, Colon, Dot, DotDot, Arrow,

View file

@ -412,17 +412,34 @@ json::Value Server::completion(const std::string &path, int line, int col) {
if (v.name == name && v.line <= line && line <= v.fnEnd + 1) type = v.type; if (v.name == name && v.line <= line && line <= v.fnEnd + 1) type = v.type;
if (element && type.rfind("[]", 0) == 0) type = type.substr(2); if (element && type.rfind("[]", 0) == 0) type = type.substr(2);
if (element && type == "str") type = "char"; if (element && type == "str") type = "char";
if (element && type.rfind("map[", 0) == 0) { // m[key]. -> the value type
int depth = 0;
for (size_t k = 3; k < type.size(); k++) {
if (type[k] == '[') depth++;
else if (type[k] == ']' && --depth == 0) {
type = type.substr(k + 1);
break;
}
}
}
if (type.rfind("ptr[", 0) == 0) type = type.substr(4, type.size() - 5); // fields through a pointer if (type.rfind("ptr[", 0) == 0) type = type.substr(4, type.size() - 5); // fields through a pointer
if (type == "str") { if (type == "str") {
for (const BuiltinDoc &d : kStrMethods) items.push_back(item(d.name, d.name[0] == 'l' || d.name[0] == 'p' ? KField : KMethod, d.signature, d.doc)); for (const BuiltinDoc &d : kStrMethods) items.push_back(item(d.name, d.name[0] == 'l' || d.name[0] == 'p' ? KField : KMethod, d.signature, d.doc));
} else if (type.rfind("[]", 0) == 0) { } else if (type.rfind("[]", 0) == 0) {
for (const BuiltinDoc &d : kArrayMethods) items.push_back(item(d.name, std::string(d.name) == "len" || std::string(d.name) == "ptr" ? KField : KMethod, d.signature, d.doc)); for (const BuiltinDoc &d : kArrayMethods) items.push_back(item(d.name, std::string(d.name) == "len" || std::string(d.name) == "ptr" ? KField : KMethod, d.signature, d.doc));
} else if (type.rfind("map[", 0) == 0) {
for (const BuiltinDoc &d : kMapMethods) items.push_back(item(d.name, std::string(d.name) == "len" ? KField : KMethod, d.signature, d.doc));
} else if (!type.empty()) { } else if (!type.empty()) {
std::string mod, sname = type; std::string mod, sname = type;
if (size_t dot = type.find('.'); dot != std::string::npos) mod = type.substr(0, dot), sname = type.substr(dot + 1); if (size_t dot = type.find('.'); dot != std::string::npos) mod = type.substr(0, dot), sname = type.substr(dot + 1);
for (const StructIndex &s2 : a->index.structs) for (const StructIndex &s2 : a->index.structs)
if (s2.name == sname && (mod.empty() || s2.module == mod)) if (s2.name == sname && (mod.empty() || s2.module == mod))
for (auto &[fname, ftype] : s2.fields) items.push_back(item(fname, KField, ftype + " " + fname)); for (auto &[fname, ftype] : s2.fields) items.push_back(item(fname, KField, ftype + " " + fname));
std::set<std::string> seen;
for (const FnInfo &f : a->index.fns) // methods: indexed as Struct.name
if (f.name.size() > sname.size() + 1 && f.name.compare(0, sname.size() + 1, sname + ".") == 0 &&
seen.insert(f.name).second)
items.push_back(item(f.name.substr(sname.size() + 1), KMethod, f.signature));
if (type.rfind("ptr", 0) == 0 || items.empty()) items.push_back(item("value", KField, "the value the pointer points to")); if (type.rfind("ptr", 0) == 0 || items.empty()) items.push_back(item("value", KField, "the value the pointer points to"));
} }
return json::Object{{"isIncomplete", false}, {"items", std::move(items)}}; return json::Object{{"isIncomplete", false}, {"items", std::move(items)}};
@ -511,9 +528,12 @@ json::Value Server::signatureHelp(const std::string &path, int line, int col) {
std::string label, doc; std::string label, doc;
std::vector<std::string> params; std::vector<std::string> params;
if (a) { if (a) {
for (const FnInfo &f : a->index.fns) for (const FnInfo &f : a->index.fns) {
if (f.name == name && (f.module == mod || (mod.empty() && f.module.empty()) || !a->modules.count(mod))) bool method = f.name.size() > name.size() && f.name.compare(f.name.size() - name.size() - 1, name.size() + 1, "." + name) == 0;
if ((f.name == name && (f.module == mod || (mod.empty() && f.module.empty()) || !a->modules.count(mod))) ||
(method && !mod.empty() && !a->modules.count(mod))) // p.move( -> Point.move
label = f.signature, params = f.params; label = f.signature, params = f.params;
}
if (label.empty()) if (label.empty())
for (const StructIndex &s : a->index.structs) for (const StructIndex &s : a->index.structs)
if (s.name == name) { if (s.name == name) {
@ -544,6 +564,7 @@ json::Value Server::signatureHelp(const std::string &path, int line, int col) {
if (!mod.empty()) { if (!mod.empty()) {
look(std::begin(kArrayMethods), std::end(kArrayMethods)); look(std::begin(kArrayMethods), std::end(kArrayMethods));
look(std::begin(kStrMethods), std::end(kStrMethods)); look(std::begin(kStrMethods), std::end(kStrMethods));
look(std::begin(kMapMethods), std::end(kMapMethods));
} }
} }
if (label.empty()) return nullptr; if (label.empty()) return nullptr;

View file

@ -39,10 +39,11 @@ static void usage() {
" finch build <file.fch> [-o name] compile to a program\n" " finch build <file.fch> [-o name] compile to a program\n"
" finch ir <file.fch> show the generated LLVM IR\n" " finch ir <file.fch> show the generated LLVM IR\n"
" finch version show the version\n" " finch version show the version\n"
" finch lsp the language server, for editors (VS Code)\n" " finch lsp the language server, for editors (VS Code, Kate, ...)\n"
"\n" "\n"
" -l <lib> link a C library, same as link \"lib\" in the file\n" " -l <lib> link a C library, same as link \"lib\" in the file\n"
" --target <name> build for another system: windows, linux, or an LLVM triple\n" " --target <name> build for another system: windows, linux, arm64 (Linux), macos,\n"
" or an LLVM triple\n"
" -g add debug info (gdb / lldb / Visual Studio)\n" " -g add debug info (gdb / lldb / Visual Studio)\n"
" -O0 skip optimizations (to read the raw IR)\n"); " -O0 skip optimizations (to read the raw IR)\n");
std::exit(1); std::exit(1);

View file

@ -79,13 +79,20 @@ private:
// A token can continue an expression only if it is on the same line (or we're inside brackets). // A token can continue an expression only if it is on the same line (or we're inside brackets).
bool sameLine() const { return parenDepth > 0 || !cur().newlineBefore; } bool sameLine() const { return parenDepth > 0 || !cur().newlineBefore; }
// int, u8, ptr, ptr[int], []str, Point, math.Vec ... // int, u8, ptr, ptr[int], []str, map[str]int, Point, math.Vec ...
Type type() { Type type() {
if (accept(Tok::LBracket)) { if (accept(Tok::LBracket)) {
expect(Tok::RBracket, "']' (an array type looks like []int)"); expect(Tok::RBracket, "']' (an array type looks like []int)");
return Type::arrayOf(type()); return Type::arrayOf(type());
} }
if (!at(Tok::Ident)) unexpected("(expected a type like int, str, []int, ptr[int] or a struct name)"); if (!at(Tok::Ident)) unexpected("(expected a type like int, str, []int, ptr[int] or a struct name)");
if (cur().text == "map" && peekTok().kind == Tok::LBracket) {
next();
next();
Type k = type();
expect(Tok::RBracket, "']' (a map type looks like map[str]int)");
return Type::mapOf(k, type());
}
std::string name = next().text; std::string name = next().text;
Type t; Type t;
if (typeFromName(name, t)) { if (typeFromName(name, t)) {
@ -111,7 +118,7 @@ private:
if (!at(Tok::Ident)) return false; if (!at(Tok::Ident)) return false;
Tok n = peekTok().kind; Tok n = peekTok().kind;
if (n == Tok::Ident) return !peekTok().newlineBefore; if (n == Tok::Ident) return !peekTok().newlineBefore;
if (cur().text == "ptr" && n == Tok::LBracket) return true; if ((cur().text == "ptr" || cur().text == "map") && n == Tok::LBracket) return true;
return n == Tok::Dot && peekTok(2).kind == Tok::Ident && peekTok(3).kind == Tok::Ident && return n == Tok::Dot && peekTok(2).kind == Tok::Ident && peekTok(3).kind == Tok::Ident &&
!peekTok(3).newlineBefore; !peekTok(3).newlineBefore;
} }
@ -179,6 +186,12 @@ private:
fn.namePos = pos(); fn.namePos = pos();
fn.name = expect(Tok::Ident, "a function name").text; fn.name = expect(Tok::Ident, "a function name").text;
if (accept(Tok::Dot)) { // fn Point.move(...): a method
fn.recv = fn.name;
fn.recvPos = fn.namePos;
fn.namePos = pos();
fn.name = expect(Tok::Ident, "a method name (fn Point.name(...))").text;
}
expect(Tok::LParen); expect(Tok::LParen);
parenDepth++; parenDepth++;
if (!at(Tok::RParen)) { if (!at(Tok::RParen)) {
@ -193,7 +206,12 @@ private:
} }
parenDepth--; parenDepth--;
expect(Tok::RParen); expect(Tok::RParen);
fn.ret = accept(Tok::Arrow) ? type() : Type(Type::Void); fn.ret = Type(Type::Void);
if (accept(Tok::Arrow)) {
if (!at(Tok::Not)) fn.ret = type();
fn.fallible = accept(Tok::Not); // -> int! or -> !
if (!fn.fallible && fn.ret.kind == Type::Void) unexpected("(after '->' comes a type, or ! for a function that can fail)");
}
fn.body = block(); fn.body = block();
return fn; return fn;
} }
@ -236,8 +254,15 @@ private:
next(); next();
Pos varPos = pos(); Pos varPos = pos();
std::string var = expect(Tok::Ident, "a loop variable name").text; std::string var = expect(Tok::Ident, "a loop variable name").text;
std::string var2;
Pos var2Pos;
if (accept(Tok::Comma)) { // for i, x in list / for key, value in map
var2Pos = pos();
var2 = expect(Tok::Ident, "a second loop variable name").text;
}
expect(Tok::In); expect(Tok::In);
ExprPtr first = expr(); ExprPtr first = expr();
if (!var2.empty() && at(Tok::DotDot)) fail(var2Pos.line, var2Pos.col, "a range (for i in 0..n) has one loop variable");
if (accept(Tok::DotDot)) { // for i in 0..10 if (accept(Tok::DotDot)) { // for i in 0..10
auto s = std::make_unique<ForStmt>(p); auto s = std::make_unique<ForStmt>(p);
s->var = var; s->var = var;
@ -250,6 +275,8 @@ private:
auto s = std::make_unique<ForEachStmt>(p); // for x in list auto s = std::make_unique<ForEachStmt>(p); // for x in list
s->var = var; s->var = var;
s->varPos = varPos; s->varPos = varPos;
s->var2 = var2;
s->var2Pos = var2Pos;
s->list = std::move(first); s->list = std::move(first);
s->body = block(); s->body = block();
return s; return s;
@ -323,7 +350,7 @@ private:
return s; return s;
} }
if (e->kind != ExprKind::Call && e->kind != ExprKind::Method) if (e->kind != ExprKind::Call && e->kind != ExprKind::Method && e->kind != ExprKind::Try && e->kind != ExprKind::OrElse)
fail(p.line, p.col, "this value is computed but never used"); fail(p.line, p.col, "this value is computed but never used");
return std::make_unique<ExprStmt>(p, std::move(e)); return std::make_unique<ExprStmt>(p, std::move(e));
} }
@ -342,7 +369,17 @@ private:
// ---------- expressions (lowest to highest precedence) ---------- // ---------- expressions (lowest to highest precedence) ----------
ExprPtr expr() { return orExpr(); } // f(x) or fallback / f(x) or { ... }: the lowest precedence, right to left
ExprPtr expr() {
ExprPtr l = orExpr();
if (!sameLine() || !at(Tok::OrElse)) return l;
Pos p = pos();
next();
auto e = std::make_unique<OrElseExpr>(p, std::move(l));
if (at(Tok::LBrace)) e->block = block();
else e->fallback = expr();
return e;
}
ExprPtr orExpr() { ExprPtr orExpr() {
ExprPtr l = andExpr(); ExprPtr l = andExpr();
@ -429,6 +466,7 @@ private:
if (accept(Tok::Minus)) return std::make_unique<UnaryExpr>(p, '-', unary()); if (accept(Tok::Minus)) return std::make_unique<UnaryExpr>(p, '-', unary());
if (accept(Tok::Not)) return std::make_unique<UnaryExpr>(p, '!', unary()); if (accept(Tok::Not)) return std::make_unique<UnaryExpr>(p, '!', unary());
if (accept(Tok::Tilde)) return std::make_unique<UnaryExpr>(p, '~', unary()); if (accept(Tok::Tilde)) return std::make_unique<UnaryExpr>(p, '~', unary());
if (accept(Tok::Try)) return std::make_unique<TryExpr>(p, postfix());
return postfix(); return postfix();
} }
@ -506,14 +544,37 @@ private:
expect(Tok::RParen); expect(Tok::RParen);
return e; return e;
} }
case Tok::LBracket: { // [1, 2, 3] case Tok::LBracket: { // [1, 2, 3] or a map: ["a": 1, "b": 2], [:]
next(); next();
auto a = std::make_unique<ArrayLitExpr>(p);
parenDepth++; parenDepth++;
while (!at(Tok::RBracket)) { if (accept(Tok::Colon)) {
a->elems.push_back(expr()); parenDepth--;
if (!accept(Tok::Comma)) break; // a trailing comma is fine expect(Tok::RBracket, "']' (an empty map is [:])");
return std::make_unique<MapLitExpr>(p);
} }
if (!at(Tok::RBracket)) {
ExprPtr first = expr();
if (accept(Tok::Colon)) {
auto m = std::make_unique<MapLitExpr>(p);
m->keys.push_back(std::move(first));
m->values.push_back(expr());
while (accept(Tok::Comma) && !at(Tok::RBracket)) { // a trailing comma is fine
m->keys.push_back(expr());
expect(Tok::Colon, "':' (every map entry is key: value)");
m->values.push_back(expr());
}
parenDepth--;
expect(Tok::RBracket);
return m;
}
auto a = std::make_unique<ArrayLitExpr>(p);
a->elems.push_back(std::move(first));
while (accept(Tok::Comma) && !at(Tok::RBracket)) a->elems.push_back(expr());
parenDepth--;
expect(Tok::RBracket);
return a;
}
auto a = std::make_unique<ArrayLitExpr>(p);
parenDepth--; parenDepth--;
expect(Tok::RBracket); expect(Tok::RBracket);
return a; return a;

View file

@ -21,11 +21,12 @@ fi
pass=0; failed=0; skipped=0 pass=0; failed=0; skipped=0
for f in tests/run/*.fch; do for f in tests/run/*.fch; do
grep -q "^fn main" "$f" || continue grep -q "^fn main" "$f" || continue
# the bootstrap compiler doesn't do C imports, sized numbers or defer # the bootstrap compiler doesn't do C imports, sized numbers, defer, maps, methods or errors as values
# (checked in the test and in the modules it imports) # (checked in the test and in the modules it imports)
files="$f" files="$f"
for m in $(sed -n 's/^import \([a-z_]*\)$/\1/p' "$f"); do files="$files tests/run/$m.fch"; done for m in $(sed -n 's/^import \([a-z_]*\)$/\1/p' "$f"); do files="$files tests/run/$m.fch"; done
if grep -qE '^import "|^link |defer |0x' $files || grep -qwE 'u8|u16|u32|u64|i8|i16|i32|f32|f64' $files; then if grep -qE '^import "|^link |defer |0x| or |try |map\[|!\s*\{|^fn [A-Z][A-Za-z0-9_]*\.|for [a-z_]+, ' $files ||
grep -qwE 'u8|u16|u32|u64|i8|i16|i32|f32|f64' $files; then
skipped=$((skipped + 1)) skipped=$((skipped + 1))
continue continue
fi fi

View file

@ -0,0 +1,9 @@
// expect: 'check' can't fail, so it can't return an error (make it fallible: -> !)
fn check(int n) {
if n < 0 {
return error("negative")
}
}
fn main() {
check(1)
}

View file

@ -0,0 +1,4 @@
// expect: error(...) makes an error to return from a function that can fail
fn main() {
e := error("oops")
}

View file

@ -0,0 +1,4 @@
// expect: an empty [:] needs a type, like: map[str]int m = [:]
fn main() {
m := [:]
}

View file

@ -0,0 +1,5 @@
// expect: a map key must be a whole number, a char, a bool or a str, not float
fn main() {
map[float]int m
print(m)
}

View file

@ -0,0 +1,5 @@
// expect: the key "zoe" is not in the map
fn main() {
ages := ["anna": 31]
print(ages["zoe"])
}

View file

@ -0,0 +1,9 @@
// expect: the struct Point already has a field named 'x'
struct Point {
int x
}
fn Point.x() -> int {
return 1
}
fn main() {
}

View file

@ -0,0 +1,12 @@
// expect: the loop variable 'c' can't be changed
struct Counter {
int n
}
fn Counter.bump() {
self.n += 1
}
fn main() {
for c in [Counter(1), Counter(2)] {
c.bump()
}
}

View file

@ -0,0 +1,8 @@
// expect: the struct Point has no methods; add one with fn Point.norm(...) { ... }
struct Point {
int x
}
fn main() {
p := Point(1)
print(p.norm())
}

View file

@ -0,0 +1,5 @@
// expect: this block must leave with return, break, continue or exit
fn main() {
n := int("x") or { print("not a number") }
print(n)
}

View file

@ -0,0 +1,4 @@
// expect: this call can't fail, so it needs no 'or' or 'try'
fn main() {
print(int(3.5) or 0)
}

View file

@ -0,0 +1,10 @@
// expect: try passes the error on, so 'twice' must be able to fail too (-> int!)
fn parse(str s) -> int! {
return int(s) or { return error("bad") }
}
fn twice(str s) -> int {
return try parse(s) * 2
}
fn main() {
print(twice("2"))
}

View file

@ -0,0 +1,7 @@
// expect: 'parse' can fail, so say what happens then
fn parse(str s) -> int! {
return int(s) or { return error("bad") }
}
fn main() {
print(parse("1"))
}

View file

@ -131,6 +131,61 @@ check("error shown while typing", diags and "must be int, but this is str" in di
c.notify("textDocument/didChange", {"textDocument": {"uri": uri, "version": 7}, "contentChanges": [{"text": src}]}) c.notify("textDocument/didChange", {"textDocument": {"uri": uri, "version": 7}, "contentChanges": [{"text": src}]})
check("error cleared after the fix", c.diagnostics(uri) == []) check("error cleared after the fix", c.diagnostics(uri) == [])
# methods, maps, errors as values
src2 = """struct Counter {
int count
}
fn Counter.add(int n) {
self.count += n
}
fn parse(str s) -> int! {
return int(s) or { return error("bad") }
}
fn main() {
c := Counter(0)
c.add(2)
map[str]int m
n := parse("4") or 0
print(c, m, n)
}
"""
lines2 = src2.split("\n")
path2 = os.path.join(d, "more.fch")
open(path2, "w").write(src2)
uri2 = pathlib.Path(path2).resolve().as_uri()
td2 = {"uri": uri2}
c.notify("textDocument/didOpen", {"textDocument": {"uri": uri2, "languageId": "finch", "version": 1, "text": src2}})
check("methods, maps and errors compile", c.diagnostics(uri2) == [])
h = c.request("textDocument/hover", {"textDocument": td2, "position": {"line": 14, "character": lines2[14].index("add")}})
check("hover: method signature", h and "fn Counter.add(int n)" in h["contents"]["value"], h)
h = c.request("textDocument/hover", {"textDocument": td2, "position": {"line": 16, "character": lines2[16].index("parse")}})
check("hover: fallible signature", h and "-> int!" in h["contents"]["value"], h)
defn = c.request("textDocument/definition", {"textDocument": td2, "position": {"line": 14, "character": lines2[14].index("add")}})
check("definition: method", defn and defn["range"]["start"]["line"] == 4, defn)
for text, line, col, want, what in [
(" c.\n", 18, 6, ["add", "count"], "completion: methods and fields after c."),
(" m.\n", 18, 6, ["has", "get", "keys", "remove"], "completion: map methods"),
]:
e2 = src2.replace(" print(c, m, n)\n", " print(c, m, n)\n" + text)
c.notify("textDocument/didChange", {"textDocument": {"uri": uri2, "version": 2}, "contentChanges": [{"text": e2}]})
comp = c.request("textDocument/completion", {"textDocument": td2, "position": {"line": line, "character": col}})
labels = [i["label"] for i in comp["items"]]
check(what, all(x in labels for x in want), labels)
e2 = src2.replace(" print(c, m, n)\n", " print(c, m, n)\n c.add(\n")
c.notify("textDocument/didChange", {"textDocument": {"uri": uri2, "version": 3}, "contentChanges": [{"text": e2}]})
sig = c.request("textDocument/signatureHelp", {"textDocument": td2, "position": {"line": 18, "character": 10}})
check("signature help: method", sig and sig["signatures"][0]["label"].startswith("fn Counter.add"), sig)
e2 = src2.replace('n := parse("4") or 0', 'n := parse("4")')
c.notify("textDocument/didChange", {"textDocument": {"uri": uri2, "version": 4}, "contentChanges": [{"text": e2}]})
diags = c.diagnostics(uri2)
check("error: unhandled failure", diags and "can fail" in diags[0]["message"], diags)
c.request("shutdown", None) c.request("shutdown", None)
c.notify("exit", None) c.notify("exit", None)
c.p.wait(timeout=5) c.p.wait(timeout=5)

View file

@ -27,7 +27,8 @@ for f in run/*.fch; do
if [ "${MEMCHECK:-0}" = 1 ]; then if [ "${MEMCHECK:-0}" = 1 ]; then
exe="$tmp/$(basename "${f%.fch}")" exe="$tmp/$(basename "${f%.fch}")"
"$FINCH" build "$f" -o "$exe" >/dev/null 2>&1 "$FINCH" build "$f" -o "$exe" >/dev/null 2>&1
if ! valgrind -q --leak-check=full --errors-for-leak-kinds=all --error-exitcode=99 "$exe" < "$input" >/dev/null 2>"$tmp/vg"; then valgrind -q --leak-check=full --errors-for-leak-kinds=all --error-exitcode=99 "$exe" < "$input" >/dev/null 2>"$tmp/vg"
if [ $? -eq 99 ]; then # (other exit codes are the program's own)
failed=$((failed + 1)) failed=$((failed + 1))
echo "MEMCHECK FAIL $f" echo "MEMCHECK FAIL $f"
head -20 "$tmp/vg" head -20 "$tmp/vg"

86
tests/run/errors.fch Normal file
View file

@ -0,0 +1,86 @@
struct Config {
str name
int port = 8080
}
fn parse_port(str text) -> int! {
n := int(text) or { return error("the port '" + text + "' is not a number") }
if n < 1 || n > 65535 {
return error("the port " + str(n) + " is out of range")
}
return n
}
fn parse_config(str line) -> Config! {
parts := line.split("=")
if parts.len != 2 {
return error("expected name=port, got '" + line + "'")
}
port := try parse_port(parts[1].trim()) // a failure here fails parse_config too
return Config(parts[0].trim(), port)
}
fn save(str path, str text) -> ! {
if path.len == 0 {
return error("no file name")
}
try write_file(path, text)
}
struct Account {
int balance
}
fn Account.withdraw(int amount) -> ! {
if amount > self.balance {
return error("not enough money: " + str(self.balance) + " < " + str(amount))
}
self.balance -= amount
}
fn first_number([]str words) -> int! {
for w in words {
n := int(w) or { continue }
return n
}
return error("no numbers")
}
fn main() -> ! {
print(parse_port("80") or 0)
print(parse_port("abc") or -1)
print(parse_port("99999") or -1)
for line in ["web = 80", "db=5432", "bad", "x = y", "big = 70000"] {
c := parse_config(line) or {
print("skipped:", err)
continue
}
print(c)
}
print(int("12") or 0, int(" 7 ") or 0, int("seven") or 0, float("2.5") or 0, float("") or -1.0)
text := read_file("does/not/exist.txt") or "default text"
print(text)
read_file("does/not/exist.txt") or { print("read failed:", err.starts_with("can't read the file")) }
save("", "x") or { print("save failed:", err) }
try save("errors_tmp.txt", "saved")
print(try read_file("errors_tmp.txt"))
try delete_file("errors_tmp.txt")
delete_file("errors_tmp.txt") or { print("second delete failed") }
a := Account(100)
a.withdraw(30) or { print("no") }
a.withdraw(500) or { print(err) }
print(a.balance)
print(first_number(["a", "b", "42", "7"]) or 0, first_number(["x"]) or -1)
// nested: the fallback can be another call that may fail
print(parse_port("x") or parse_port("443") or 0)
try parse_port("not a port") // main fails: prints the error, exit code 1
print("never printed")
}

19
tests/run/errors.out Normal file
View file

@ -0,0 +1,19 @@
80
-1
-1
Config(name: "web", port: 80)
Config(name: "db", port: 5432)
skipped: expected name=port, got 'bad'
skipped: the port 'y' is not a number
skipped: the port 70000 is out of range
12 7 0 2.5 -1
default text
read failed: true
save failed: no file name
saved
second delete failed
not enough money: 70 < 500
70
42 -1
443
error: the port 'not a port' is not a number

View file

@ -16,3 +16,8 @@ fn add(Vec a, Vec b) -> Vec {
fn describe(Vec v) -> str { fn describe(Vec v) -> str {
return "(" + str(v.x) + ", " + str(v.y) + ") of length " + str(length(v)) return "(" + str(v.x) + ", " + str(v.y) + ") of length " + str(length(v))
} }
// a method in a module: callable as v.scaled(2) wherever Vec is used
fn Vec.scaled(float k) -> Vec {
return Vec(self.x * k, self.y * k)
}

101
tests/run/maps.fch Normal file
View file

@ -0,0 +1,101 @@
struct Stats {
int count
[]str words
}
fn count_words(str text) -> map[str]int {
map[str]int counts
for w in text.split(" ") {
if w.len > 0 {
counts[w.lower()] += 1 // a missing key starts at 0
}
}
return counts
}
fn main() {
ages := ["anna": 31, "bob": 25]
ages["cyril"] = 40
ages["bob"] = 26
print(ages, ages.len)
print(ages["anna"], ages.has("bob"), ages.has("zoe"), ages.get("zoe", -1))
for name, age in ages {
print(name, "is", age)
}
for name in ages {
print(name)
}
print(ages.remove("bob"), ages.remove("bob"), ages)
ages["dave"] = 19 // goes to the end; insertion order is kept
print(ages.keys(), ages.values())
counts := count_words("the cat and The dog and THE bird")
print(counts)
print(str(counts))
// values that own memory
map[str][]int groups
for i in 0..10 {
key := "odd"
if i % 2 == 0 {
key = "even"
}
groups[key].push(i) // m[k].push(x) adds the key with an empty array first
}
print(groups)
copy := groups
copy["odd"].clear()
print(groups["odd"].len, copy["odd"].len)
map[int]Stats by_len
for w in ["a", "bb", "cc", "ddd", "e"] {
by_len[w.len].count += 1
by_len[w.len].words.push(w)
}
for n, s in by_len {
print(n, s.count, s.words)
}
// many keys: growing, removing and adding again
map[int]int squares
for i in 0..1000 {
squares[i] = i * i
}
for i in 0..1000 {
if i % 3 != 0 {
squares.remove(i)
}
}
for i in 2000..2010 {
squares[i] = 1
}
total := 0
for k, v in squares {
total += v
}
print(squares.len, total, squares[999], squares.has(998))
map[char]bool seen
for c in "hello" {
seen[c] = true
}
print(seen.keys())
empty := ages
empty.clear()
print(empty, empty.len, ages.len)
map[str]str m = [:]
m["x"] = "1"
print(m)
for i, w in ["zero", "one", "two"] {
print(i, w)
}
for i, c in "hey" {
print(i, c)
}
}

27
tests/run/maps.out Normal file
View file

@ -0,0 +1,27 @@
{"anna": 31, "bob": 26, "cyril": 40} 3
31 true false -1
anna is 31
bob is 26
cyril is 40
anna
bob
cyril
true false {"anna": 31, "cyril": 40}
["anna", "cyril", "dave"] [31, 40, 19]
{"the": 3, "cat": 1, "and": 2, "dog": 1, "bird": 1}
{"the": 3, "cat": 1, "and": 2, "dog": 1, "bird": 1}
{"even": [0, 2, 4, 6, 8], "odd": [1, 3, 5, 7, 9]}
5 0
1 2 ["a", "e"]
2 2 ["bb", "cc"]
3 1 ["ddd"]
344 111277621 998001 false
['h', 'e', 'l', 'o']
{} 0 3
{"x": "1"}
0 zero
1 one
2 two
0 h
1 e
2 y

101
tests/run/methods.fch Normal file
View file

@ -0,0 +1,101 @@
import geometry
import "abi.h"
link "abi.c"
struct Counter {
str name
int count
[]int history
}
fn Counter.add(int n) {
self.count += n
self.history.push(n)
}
fn Counter.add_twice(int n) {
self.add(n) // a method calling another method on self
self.add(n)
}
fn Counter.total() -> int {
sum := 0
for x in self.history {
sum += x
}
return sum
}
fn Counter.describe() -> str {
return self.name + ": " + str(self.count) + " " + str(self.history)
}
fn Counter.copy_named(str name) -> Counter {
c := self // a copy of the struct, not the original
c.name = name
return c
}
fn Counter.merge(Counter other) {
for x in other.history {
self.add(x)
}
}
struct Node {
int value
ptr[Node] next
}
fn Node.sum() -> int {
total := self.value
if self.next != null {
total += self.next.sum() // through a pointer
}
return total
}
// a method on a struct from C
fn Vec2.length2() -> f32 {
return self.x * self.x + self.y * self.y
}
fn main() {
c := Counter(name: "clicks")
c.add(3)
c.add_twice(2)
print(c.describe(), c.total())
d := c.copy_named("copy")
d.add(10)
print(c.describe())
print(d.describe())
c.merge(c) // the argument lives inside self: the method gets its own copy
print(c.describe())
[]Counter all = [Counter(name: "a"), Counter(name: "b")]
all[1].add(7)
for i in 0..all.len {
all[i].add(i)
}
print(all[0].describe(), "|", all[1].describe())
print(Counter(name: "temp", count: 5).describe())
last := new(Node(3, null))
first := new(Node(1, new(Node(2, last))))
print(first.sum())
p := first.next
p.value = 20
print(first.sum(), p.sum())
free(last)
free(first.next)
free(first)
v := geometry.Vec(3, 4)
print(geometry.length(v.scaled(2)))
w := Vec2(3, 4)
print(w.length2())
}

10
tests/run/methods.out Normal file
View file

@ -0,0 +1,10 @@
clicks: 7 [3, 2, 2] 7
clicks: 7 [3, 2, 2]
copy: 17 [3, 2, 2, 10]
clicks: 14 [3, 2, 2, 3, 2, 2]
a: 0 [0] | b: 8 [7, 1]
temp: 5 []
6
24 23
10
25