From b872be317676c9fa2fb2cfe835c2493c3ff2dd7c Mon Sep 17 00:00:00 2001 From: Nexoniarz Date: Fri, 9 Oct 2026 01:11:12 +0200 Subject: [PATCH] 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 , 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 --- .github/workflows/ci.yml | 13 + CMakeLists.txt | 3 +- README.md | 42 ++- README.pl.md | 42 ++- docs/en/cheatsheet.md | 81 +++- docs/en/for-beginners.md | 154 +++++++- docs/en/for-engineers.md | 241 +++++++++--- docs/en/for-technicians.md | 247 +++++++++++-- docs/pl/dla-inzynierow.md | 246 ++++++++++--- docs/pl/dla-poczatkujacych.md | 157 +++++++- docs/pl/dla-technikow.md | 249 +++++++++++-- docs/pl/sciaga.md | 82 ++++- editors/kate/finch.xml | 10 +- editors/vscode/CHANGELOG.md | 6 + editors/vscode/package-lock.json | 4 +- editors/vscode/package.json | 2 +- editors/vscode/snippets/finch.json | 172 ++++++++- editors/vscode/syntaxes/finch.tmLanguage.json | 22 +- editors/vscode/test/grammar.js | 11 + examples/wordcount.fch | 62 ++++ runtime/finch_rt.c | 317 +++++++++++++++- src/ast.h | 44 ++- src/builtins_doc.h | 27 +- src/codegen.cpp | 346 ++++++++++++++++-- src/codegen_builtins.cpp | 149 +++++++- src/codegen_impl.h | 38 +- src/codegen_map.cpp | 242 ++++++++++++ src/codegen_types.cpp | 121 ++++-- src/lexer.cpp | 4 +- src/lexer.h | 1 + src/lsp.cpp | 25 +- src/main.cpp | 5 +- src/parser.cpp | 81 +++- tests/boot.sh | 5 +- tests/fail/error_not_fallible.fch | 9 + tests/fail/error_outside_return.fch | 4 + tests/fail/map_empty_literal.fch | 4 + tests/fail/map_float_key.fch | 5 + tests/fail/map_missing_key.fch | 5 + tests/fail/method_field_clash.fch | 9 + tests/fail/method_loop_readonly.fch | 12 + tests/fail/method_missing.fch | 8 + tests/fail/or_block_value.fch | 5 + tests/fail/or_cannot_fail.fch | 4 + tests/fail/try_outside_fallible.fch | 10 + tests/fail/unhandled_error.fch | 7 + tests/lsp_test.py | 55 +++ tests/run.sh | 3 +- tests/run/errors.fch | 86 +++++ tests/run/errors.out | 19 + tests/run/geometry.fch | 5 + tests/run/maps.fch | 101 +++++ tests/run/maps.out | 27 ++ tests/run/methods.fch | 101 +++++ tests/run/methods.out | 10 + 55 files changed, 3402 insertions(+), 338 deletions(-) create mode 100644 examples/wordcount.fch create mode 100644 src/codegen_map.cpp create mode 100644 tests/fail/error_not_fallible.fch create mode 100644 tests/fail/error_outside_return.fch create mode 100644 tests/fail/map_empty_literal.fch create mode 100644 tests/fail/map_float_key.fch create mode 100644 tests/fail/map_missing_key.fch create mode 100644 tests/fail/method_field_clash.fch create mode 100644 tests/fail/method_loop_readonly.fch create mode 100644 tests/fail/method_missing.fch create mode 100644 tests/fail/or_block_value.fch create mode 100644 tests/fail/or_cannot_fail.fch create mode 100644 tests/fail/try_outside_fallible.fch create mode 100644 tests/fail/unhandled_error.fch create mode 100644 tests/run/errors.fch create mode 100644 tests/run/errors.out create mode 100644 tests/run/maps.fch create mode 100644 tests/run/maps.out create mode 100644 tests/run/methods.fch create mode 100644 tests/run/methods.out diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 75c1b39..a3e6484 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -69,6 +69,19 @@ jobs: run: CC=/usr/bin/clang tests/boot.sh - name: Language server 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 run: | mkdir -p dist/finch && cp build/finch README.md LICENSE dist/finch/ && cp -r examples dist/finch/ diff --git a/CMakeLists.txt b/CMakeLists.txt index 8ac9328..4dcfbcc 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -1,5 +1,5 @@ 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_REQUIRED ON) @@ -15,6 +15,7 @@ add_executable(finch src/codegen.cpp src/codegen_types.cpp src/codegen_builtins.cpp + src/codegen_map.cpp src/abi.cpp src/cimport.cpp src/target.cpp diff --git a/README.md b/README.md index b81833f..a6230d1 100644 --- a/README.md +++ b/README.md @@ -38,6 +38,8 @@ fn main() { - **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. 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:** ``` 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 -**Windows:** download `finch-windows-x64.zip` from 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. +Ready builds are in the [releases](https://github.com/Nexoniarz/Finch/releases): -**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 git clone https://github.com/Nexoniarz/Finch.git @@ -66,7 +75,7 @@ ninja -C build ./build/finch run examples/hello.fch ./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 ``` @@ -86,17 +95,20 @@ Pick the guide that fits you: | | | |---|---| | 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 }` | | 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]` | | 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 | | Modules | `import shapes` → `shapes.area(b)` | | C interop | `import "stdio.h"`, `link "glfw"`, `link "mine.c"`, `addr(fn)` for callbacks | | 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 @@ -106,6 +118,7 @@ Pick the guide that fits you: | [`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/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/types.fch`](examples/types.fch) | sized numbers and pointers | | [`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] 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 -- [ ] Maps, methods on structs, `match` -- [ ] Error values instead of stopping the program -- [ ] More platforms (macOS, ARM64) +- [x] macOS and ARM64 (Linux and Apple Silicon), with their C calling conventions; Kate support +- [x] Methods on structs, maps, errors as values (`!`, `or`, `try`) +- [ ] Generics, `match`, enums +- [ ] A standard library in Finch (JSON, paths, time, processes) ## 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 boot/ the Finch compiler written in Finch (~3k lines) examples/ example programs -editors/ the VS Code extension -tests/ run/ (golden output), fail/ (expected errors), run.sh, boot.sh, windows.sh, lsp_test.py +editors/ VS Code extension, Kate highlighting and LSP setup +tests/ run/ (golden output), fail/ (expected errors), run.sh, boot.sh, cross.sh, lsp_test.py docs/ en/ and pl/ guides for three audiences ``` diff --git a/README.pl.md b/README.pl.md index be57269..9974d1f 100644 --- a/README.pl.md +++ b/README.pl.md @@ -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 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ć). +- **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:** ``` 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 -**Windows:** pobierz `finch-windows-x64.zip` z [wydań](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. +Gotowe wersje są w [wydaniach](https://github.com/Nexoniarz/Finch/releases): -**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 git clone https://github.com/Nexoniarz/Finch.git @@ -66,7 +75,7 @@ ninja -C build ./build/finch run examples/hello.fch ./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 ``` @@ -86,17 +95,20 @@ Wybierz przewodnik dla siebie: | | | |---|---| | 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 }` | | 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]` | | 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 | | Moduły | `import ksztalty` → `ksztalty.pole(p)` | | 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` | -| 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 @@ -106,6 +118,7 @@ Wybierz przewodnik dla siebie: | [`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/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/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` | @@ -132,19 +145,20 @@ Wybierz przewodnik dla 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] Serwer języka (`finch lsp`) i rozszerzenie VS Code -- [ ] Mapy, metody w strukturach, `match` -- [ ] Błędy jako wartości zamiast zatrzymywania programu -- [ ] Więcej platform (macOS, ARM64) +- [x] macOS i ARM64 (Linux i Apple Silicon) z ich konwencjami wywołań C; obsługa Kate +- [x] Metody w strukturach, mapy, błędy jako wartości (`!`, `or`, `try`) +- [ ] Typy generyczne, `match`, enumy +- [ ] Biblioteka standardowa w Finchu (JSON, ścieżki, czas, procesy) ## 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 boot/ kompilator Fincha napisany w Finchu (ok. 3 tys. linii) examples/ przykładowe programy -editors/ rozszerzenie VS Code -tests/ run/ (wzorcowe wyjście), fail/ (oczekiwane błędy), run.sh, boot.sh, windows.sh, lsp_test.py +editors/ rozszerzenie VS Code, podświetlanie i konfiguracja LSP dla Kate +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 ``` diff --git a/docs/en/cheatsheet.md b/docs/en/cheatsheet.md index 8cb36c5..a9f7408 100644 --- a/docs/en/cheatsheet.md +++ b/docs/en/cheatsheet.md @@ -16,7 +16,7 @@ Everything in Finch on one page. For explanations, see the [guides](../../README | `finch lsp` | the language server (editors use it) | | `finch version` | version | | `-l lib` | link a C library (like `link "lib"`) | -| `--target windows` / `linux` / triple | build for another system | +| `--target windows` / `linux` / `arm64` / `macos` / triple | build for another system | | `-g` | debug info (gdb, lldb, Visual Studio) | | `-O0` | no optimizations | @@ -71,6 +71,7 @@ One statement per line, no semicolons, `{` on the same line, no brackets around | `u8 u16 u32 u64` | unsigned, sized | | `f32 f64` | floats | | `[]T` | array: `[1, 2, 3]`, `[]str names` | +| `map[K]V` | map: `["a": 1]`, `map[str]int counts`, `[:]` | | `ptr[T]`, `ptr` | pointer, untyped pointer | | `Name` / `module.Name` | a struct | @@ -100,6 +101,8 @@ if a > b { … } else if a == b { … } else { … } while x > 0 { … } for i in 0..10 { … } // 0 … 9 for item in list { … } // every element (or char of a str) +for i, item in list { … } // with its index +for key, value in m { … } // every map entry, in insertion order break continue return value defer cleanup() // runs when the block ends ``` @@ -121,6 +124,47 @@ Parameters behave like copies. Order in the file doesn't matter. A comma after t --- +## Methods + +```c +fn Point.move(int dx, int dy) { // self is the Point it's called on (not a copy) + self.x += dx + self.y += dy +} +fn Point.dist() -> float { … } + +p.move(1, 2) list[0].move(1, 1) ptr_to_point.move(0, 1) +``` + +Also on structs from C: `fn Vector2.length() -> f32 { … }`. + +--- + +## Errors as values + +```c +fn parse(str s) -> int! { // ! = can fail + if s.len == 0 { + return error("empty") + } + return int(s) or { return error("not a number: " + s) } +} +fn save(str p, str t) -> ! { // can fail, no result + try write_file(p, t) +} +``` + +| Code | On failure | +|---|---| +| `x := parse(s) or 0` | use 0 | +| `x := parse(s) or { print(err); return }` | run the block (`err`: the message) | +| `x := try parse(s)` | fail the current function too | +| `fn main() -> !` | a failure prints `error: …`, exit code 1 | + +Fallible built-ins: `int(s)`, `float(s)`, `read_file`, `write_file`, `delete_file`. + +--- + ## Structs | Code | Meaning | @@ -155,6 +199,23 @@ Parameters behave like copies. Order in the file doesn't matter. A comma after t --- +## Maps `map[K]V` + +| Code | Gives | +|---|---| +| `m := ["a": 1, "b": 2]` | a map (keys: numbers, char, bool, str) | +| `m[k]` | value (missing key: the program stops) | +| `m[k] = v` | add / replace | +| `m[k] += 1`, `m[k].push(x)` | a missing key starts at the default (0, `[]`, …) | +| `m.len` | number of keys | +| `m.has(k)` | bool | +| `m.get(k, default)` | value or default | +| `m.remove(k)` | bool: was it there | +| `m.clear()` | empty it | +| `m.keys()`, `m.values()` | arrays, insertion order | + +--- + ## Text `str` | Code | Gives | @@ -189,6 +250,8 @@ Parameters behave like copies. Order in the file doesn't matter. A comma after t | `char(66)` | `'B'` | | `bool(0)` | `false` | | `str(42)`, `str(1.5)`, `str(true)`, `str('c')` | text | +| `str([1, 2])`, `str(m)`, `str(point)` | the same text `print` shows | +| `int("x") or 0`, `float(s) or 0.0` | a number, or the fallback | | `str(bytes)` | `[]u8` → text | | `str(p)` | C `char*` → text | | `ptr(p)`, `ptr(16)` | untyped pointer, from a pointer or a number | @@ -210,6 +273,7 @@ Automatic only when nothing is lost: smaller → bigger number, any int → floa | `delete_file(path)` | bool | | `shell(command)` | run a command, its exit code | | `exit(code)` | stop the program now | +| `error(message)` | `return error("…")` in a `-> T!` function | | `addr(x)` | pointer to x; `addr(fn)` = C callback | | `new(value)` | value on the heap → `ptr[T]` | | `free(p)` | give it back | @@ -281,11 +345,17 @@ shapes.Box other // in declarations | `pop() on an empty array` | | | `used .value on a null pointer` | `.value` / `.field` / `[i]` through `null` | | `can't turn "x" into int` | `int("x")` | -| `can't read the file "…"` | `read_file` | +| `can't read the file "…"` | `read_file` (without `or`) | +| `the key "x" is not in the map` | `m["x"]` | --- -## VS Code (extension "Finch") +## Editors + +Kate: `editors/kate/install.sh`, then enable the **LSP Client** plugin; run with **Build & Run** → `finch run %f`. +Any LSP editor: `finch lsp`. + +### VS Code (extension "Finch") | Key / action | Does | |---|---| @@ -295,11 +365,12 @@ shapes.Box other // in declarations | hover | type / signature / docs | | `Ctrl+Space` | completion (after `.`: fields, methods, module members) | | `Ctrl+Shift+O` | functions and structs in the file | -| `main`, `fn`, `fnr`, `struct`, `for`, `foreach`, `if`, `ife`, `while`, `input` + Tab | snippets | +| `main`, `fn`, `fnr`, `fnf`, `method`, `struct`, `for`, `foreach`, `forkv`, `map`, `orb`, `if`, `ife`, `while`, `input` + Tab | snippets | | setting `finch.path` | where finch / finch.exe is | --- ## Keywords -`fn return if else while for in break continue true false null import link struct defer` +`fn return if else while for in break continue true false null import link struct defer or try` +(and `self` inside methods) diff --git a/docs/en/for-beginners.md b/docs/en/for-beginners.md index a0a7e4c..179f48e 100644 --- a/docs/en/for-beginners.md +++ b/docs/en/for-beginners.md @@ -77,7 +77,7 @@ To check it works, type: ./build/finch version ``` -You should see something like `finch 2.3.0`. +You should see something like `finch 2.4.0`. --- @@ -505,8 +505,68 @@ fn main() { } ``` +If you also want to know *where* you are in the list, give `for` two names: the first one counts +0, 1, 2…, the second one is the value: + +```c +fn main() { + for i, fruit in ["apple", "banana", "cherry"] { + print(i + 1, fruit) + } +} +``` + +``` +1 apple +2 banana +3 cherry +``` + Lists can do more: `contains`, `find`, `insert`, `remove`, `sort`, `reverse`, `join`. -The [guide for technicians](for-technicians.md#arrays) lists them all. +The [guide for technicians](for-technicians.md#10-arrays-and-maps) lists them all. + +### Looking things up by name: maps + +A list finds things by their place: `fruits[0]`. Sometimes you want to find things by a **name** +instead, like looking up a word in a dictionary, or a phone number in a phone book. +That's a **map**: each **key** (the name) has a **value**: + +```c +fn main() { + phone := ["Ola": "555-1234", "Jan": "555-9876"] + print(phone["Ola"]) // 555-1234 + + phone["Ewa"] = "555-0000" // add a new name + print(phone.len) // 3 + + if phone.has("Tom") { + print("Tom is here") + } else { + print("no Tom") + } + + for name, number in phone { // every name with its number + print(name, number) + } +} +``` + +- `["key": value, ...]` makes a map. Keys are often text, but numbers work too. +- `phone["Ola"]` gets the value for a key. Asking for a key that isn't there stops the program, + so check first with `phone.has("Tom")`, or use `phone.get("Tom", "unknown")`. +- `for name, number in phone` visits every key with its value, in the order you added them. + +Maps are great for **counting**. A new key starts at 0, so `+= 1` just works: + +```c +fn main() { + map[str]int votes // an empty map: text keys, whole-number values + for v in ["cat", "dog", "cat", "cat", "fish"] { + votes[v] += 1 + } + print(votes) // {"cat": 3, "dog": 1, "fish": 1} +} +``` --- @@ -618,6 +678,36 @@ fn main() { And you can keep many of them in a list: `[]Book shelf`, then `shelf.push(b)`. +### Commands for your struct: methods + +A struct can have its own commands. Write the struct's name, a dot, and the command's name. +Inside, `self` is the struct the command was used on: + +```c +struct Player { + str name + int lives = 3 +} + +fn Player.hit() { + self.lives -= 1 + print(self.name, "has", self.lives, "lives left") +} + +fn Player.is_alive() -> bool { + return self.lives > 0 +} + +fn main() { + p := Player(name: "Ola") + p.hit() // Ola has 2 lives left + p.hit() // Ola has 1 lives left + print(p.is_alive()) // true +} +``` + +`p.hit()` changes `p` itself, so after two hits `p.lives` really is 1. + --- ## 15. Saving to a file @@ -680,6 +770,57 @@ Here we simply misspelled `age`. | `index 5 is out of range (the length is 3)` | You asked a list for a place it doesn't have | Remember counting starts at 0; check `.len` | | `can't turn "abc" into int` | `int(...)` got text that isn't a number | Check what the person typed | | `can't use '+' on str and int` | Text plus a number | Use `str(...)` on the number | +| `'f' can fail, so say what happens then` | You used a command that can fail without saying what to do | Add `or ...` (see below) | + +### Things that can fail: `or` and `try` + +Some things can go wrong *while the program runs*: the person types `abc` where you wanted a number, +or a file isn't there. Instead of stopping the program, you can say what to do then, with **`or`**: + +```c +fn main() { + age := int(input("Your age? ")) or 0 // not a number? use 0 + print("Next year you'll be", age + 1) + + note := read_file("note.txt") or "(no note yet)" + print(note) +} +``` + +`or` can also be followed by a block. Inside it, `err` is the message that says what went wrong: + +```c +fn main() { + while true { + n := int(input("Pick a number: ")) or { + print("That's not a number, try again") + continue + } + print("You picked", n) + break + } +} +``` + +Your own functions can fail too. Put `!` after the result type, and use `return error("...")`: + +```c +fn safe_divide(int a, int b) -> int! { + if b == 0 { + return error("can't divide by zero") + } + return a / b +} + +fn main() { + print(safe_divide(10, 2) or -1) // 5 + print(safe_divide(10, 0) or -1) // -1 +} +``` + +Finch won't let you forget: calling `safe_divide(10, 0)` without `or` is an error that tells you +to handle it. (You can also pass the problem on with `try`; the +[guide for technicians](for-technicians.md#errors-as-values) explains it.) --- @@ -882,10 +1023,13 @@ struct Pet { // your own kind of value int age = 1 } -// lists: nums := [1, 2, 3] nums.push(4) nums[0] nums.len for n in nums { } +// lists: nums := [1, 2, 3] nums.push(4) nums[0] nums.len for n in nums { } for i, n in nums { } +// maps: ages := ["Ola": 9] ages["Jan"] = 10 ages["Ola"] ages.has("Tom") for name, age in ages { } // text: "a" + "b" str(42) int("42") s.len s.upper() s.contains("x") // ask: name := input("Name? ") // files: write_file("f.txt", text) read_file("f.txt") +// methods: fn Pet.grow() { self.age += 1 } p.grow() +// failing: n := int(text) or 0 x := read_file("f.txt") or { print(err) ... } ``` --- @@ -905,7 +1049,9 @@ struct Pet { // your own kind of value | **condition** | A yes/no question, like `x > 5` | | **list / array** | Many values kept in order, like `[1, 2, 3]` | | **index** | The position of a value in a list, starting at 0 | -| **struct** | Your own kind of value, made of named parts | +| **struct** | Your own kind of value made of named parts | +| **method** | A command that belongs to a struct, like `p.hit()` | +| **map** | Values you find by a key (a name), like a dictionary | | **input** | What the person types into the program | | **error** | A message saying something is wrong, and where | | **bug** | A mistake in a program | diff --git a/docs/en/for-engineers.md b/docs/en/for-engineers.md index f3d3758..5d3d628 100644 --- a/docs/en/for-engineers.md +++ b/docs/en/for-engineers.md @@ -38,9 +38,9 @@ For the language from a user's point of view, see the [technicians' guide](for-t 20. [Semantics vs. C](#20-semantics-vs-c) 21. [Function reference](#21-function-reference) 22. [Extending the compiler](#22-extending-the-compiler) -23. [Targets and Windows](#23-targets-and-windows) +23. [Targets: Windows, macOS, ARM64](#23-targets-windows-macos-arm64) 24. [The language server](#24-the-language-server) -25. [The VS Code extension](#25-the-vs-code-extension) +25. [Editor integrations](#25-editor-integrations) 26. [Known limitations](#26-known-limitations) --- @@ -90,20 +90,24 @@ Design decisions that shape the rest: | File | Lines | Responsibility | |---|---|---| -| `src/error.h` | 40 | `g_files` (all sources), `failAt(file, line, col, msg)`, `fail(line, col, msg)` | +| `src/error.h` | 50 | `g_files` (all sources), `failAt(file, line, col, msg)`, `fail(line, col, msg)`, `FinchError` | | `src/lexer.h/.cpp` | 250 | `Tok`, `Token`, `lex(fileIndex)`, `tokName()` | -| `src/ast.h` | 310 | `Type` (incl. Array, Fixed, Struct, Named), all `Expr`/`Stmt` nodes, `FnDecl`, `StructDecl`, `Import`, `Link`, `Program` | -| `src/parser.h/.cpp` | 540 | Recursive-descent `Parser`, `typeFromName()` | -| `src/cimport.h/.cpp` | 420 | libclang import: functions, C structs (fields, offsets), constants, globals | +| `src/ast.h` | 360 | `Type` (incl. Array, Map, Fixed, Struct, Named), all `Expr`/`Stmt` nodes, `FnDecl`, `StructDecl`, `Import`, `Link`, `Program` | +| `src/parser.h/.cpp` | 610 | Recursive-descent `Parser`, `typeFromName()` | +| `src/loader.h/.cpp` | 90 | Reading files and modules (`Loader`), `FINCH_PATH` | +| `src/target.h/.cpp` | 210 | `g_target`: triple, C compiler, processes, paths; backend initialisation | +| `src/cimport.h/.cpp` | 430 | libclang import: functions, C structs (fields, offsets), constants, globals | | `src/codegen.h` | 15 | `generate()` | -| `src/codegen_impl.h` | 265 | The `Codegen` class and its helper structs, shared by the four files below | -| `src/codegen.cpp` | 1090 | Program, declarations, scopes and cleanups, statements, expressions, places (`ref`), operators, mutation analysis | -| `src/codegen_types.cpp` | 500 | LLVM types, type resolution, struct layout, coercions, ownership (copy/drop helpers), printing, DWARF types | -| `src/codegen_builtins.cpp` | 710 | Calls, constructors, built-ins, conversions, array and str methods, runtime declarations, panics | -| `src/abi.cpp` | 310 | System V x86-64 classification, C calls with structs by value, C-callable thunks | -| `src/main.cpp` | 390 | Driver: CLI, loader, target machine, O2, object emission, runtime cache, linking, link-error advice, `run` | -| `runtime/finch_rt.c` | 440 | The runtime library (§12) | -| `boot/*.fch` | 3080 | The self-hosted compiler (§17) | +| `src/codegen_impl.h` | 310 | The `Codegen` class and its helper structs, shared by the files below | +| `src/codegen.cpp` | 1460 | Program, declarations (functions, methods), scopes and cleanups, statements, expressions, places (`ref`), operators, errors as values, mutation analysis | +| `src/codegen_types.cpp` | 580 | LLVM types, type resolution, struct layout, coercions, ownership (copy/drop helpers), text formatting, debug types | +| `src/codegen_builtins.cpp` | 890 | Calls, methods, constructors, built-ins, conversions, array and str methods, runtime declarations, panics | +| `src/codegen_map.cpp` | 240 | Maps: indexing, literals, methods, iteration | +| `src/abi.cpp` | 370 | C calling conventions (System V x86-64, Microsoft x64, AAPCS64), C calls with structs by value, C-callable thunks | +| `src/lsp.cpp`, `src/index.h`, `src/builtins_doc.h` | 700 | The language server (§24) | +| `src/main.cpp` | 350 | Driver: CLI, target machine, O2, object emission, runtime cache, linking, link-error advice, `run` | +| `runtime/finch_rt.c` | 740 | The runtime library (§12) | +| `boot/*.fch` | 3100 | The self-hosted compiler (§17) | --- @@ -120,7 +124,8 @@ struct Token { Tok kind; std::string text; int line, col; bool newlineBefore; }; - **Positions:** 1-based; `col` counts characters (UTF-8 continuation bytes don't advance it), so carets line up under `"Błąd"`. A stray non-ASCII character is reported whole, with a hint that names are ASCII-only. - **Identifiers / keywords:** `[A-Za-z_][A-Za-z0-9_]*`. Keywords: - `fn return if else while for in break continue true false null import link struct defer`. + `fn return if else while for in break continue true false null import link struct defer or try`. + (`self` and `map` are ordinary identifiers with a meaning in context: inside a method, before `[`.) **Type names are not keywords**: `int`, `u8`, `ptr`… are `Ident`s recognised by the parser through `typeFromName()`, so `u8(x)` is an ordinary call and the lexer doesn't depend on the type list. - **Numbers:** decimal or `0x` hex with `_` separators. No octal (`010` is 10). A `.` belongs to a number @@ -142,27 +147,31 @@ program = { import | link | struct | function } ; import = "import" ( STRING | IDENT ) ; (* "x.h" = C header, name = Finch module *) link = "link" STRING ; (* "glfw" | "file.c" | "file.o" | "lib.a" *) struct = "struct" IDENT "{" { type IDENT [ "=" expr ] NEWLINE } "}" ; -function = "fn" IDENT "(" [ param { "," param } ] ")" [ "->" type ] block ; +function = "fn" IDENT [ "." IDENT ] "(" [ param { "," param } ] ")" [ "->" ( type [ "!" ] | "!" ) ] block ; + (* fn Point.move: a method; "!": can fail *) param = type IDENT ; -type = "[" "]" type | "ptr" [ "[" type "]" ] | TYPENAME | IDENT [ "." IDENT ] ; +type = "[" "]" type | "map" "[" type "]" type | "ptr" [ "[" type "]" ] + | TYPENAME | IDENT [ "." IDENT ] ; block = "{" { statement TERMINATOR } "}" ; statement = if | while | for | return | "break" | "continue" | "defer" statement | block | type IDENT [ "=" expr ] (* declaration, see atDeclaration *) | IDENT ":=" expr | target assignop expr (* target: var, field, element, p.value *) - | call | method ; -for = "for" IDENT "in" expr ( ".." expr block | block ) ; (* range | for-each *) + | call | method | try | orelse ; +for = "for" IDENT [ "," IDENT ] "in" expr ( ".." expr block | block ) ; (* range | for-each *) assignop = "=" | "+=" | "-=" | "*=" | "/=" | "%=" ; -expr = or ; or = and { "||" and } ; and = cmp { "&&" cmp } ; +expr = or [ "or" ( block | expr ) ] ; (* f(x) or 0, f(x) or { ... }: lowest, right to left *) +or = and { "||" and } ; and = cmp { "&&" cmp } ; cmp = add { ( "==" | "!=" | "<" | "<=" | ">" | ">=" ) add } ; add = mul { ( "+" | "-" | "|" | "^" ) mul } ; mul = unary { ( "*" | "/" | "%" | "<<" | ">>" | "&" ) unary } ; -unary = ( "-" | "!" | "~" ) unary | postfix ; +unary = ( "-" | "!" | "~" ) unary | "try" postfix | postfix ; postfix = primary { "." IDENT [ args ] | "[" expr "]" } ; primary = INT | FLOAT | STRING | CHAR | "true" | "false" | "null" - | IDENT [ args ] | "(" expr ")" | "[" [ expr { "," expr } [","] ] "]" ; + | IDENT [ args ] | "(" expr ")" | "[" [ expr { "," expr } [","] ] "]" + | "[" ":" "]" | "[" expr ":" expr { "," expr ":" expr } [","] "]" ; (* maps *) args = "(" [ arg { "," arg } [","] ] ")" ; arg = [ IDENT ":" ] expr ; ``` @@ -201,11 +210,16 @@ All nodes are in `src/ast.h`: small class hierarchies with an explicit `kind`, d | `Member` | `MemberExpr` | `obj`, `field` (`.x`, `.len`, `.ptr`, `.value`) | | `Index` | `IndexExpr` | `obj`, `index` | | `ArrayLit` | `ArrayLitExpr` | `elems` | -| `Method` | `MethodExpr` | `obj`, `name`, `args`, `argNames`; also `module.fch(...)` | +| `Method` | `MethodExpr` | `obj`, `name`, `args`, `argNames`; also `module.fn(...)` and `p.method(...)` | +| `MapLit` | `MapLitExpr` | `keys`, `values` (`[:]` has none) | +| `Try` | `TryExpr` | `call` | +| `OrElse` | `OrElseExpr` | `call`, and either `fallback` (an expression) or `block` | | StmtKind | Node | |---|---| -| `Block` `VarDecl` `Assign` `Expr` `If` `While` `For` `ForEach` `Return` `Break` `Continue` `Defer` | as named; `ForEachStmt` has `var`, `list`, `body`; `DeferStmt` holds one statement | +| `Block` `VarDecl` `Assign` `Expr` `If` `While` `For` `ForEach` `Return` `Break` `Continue` `Defer` | as named; `ForEachStmt` has `var`, `var2` (`for i, x` / `for k, v`), `list`, `body`; `DeferStmt` holds one statement | + +`FnDecl` has `recv` (the struct of a method, `""` otherwise) and `fallible` (`-> T!`). `math.sqrt(x)` parses as a `MethodExpr` on `VarExpr("math")`; whether `math` is a module or a variable is decided during code generation. Calls name their callee instead of holding an expression: Finch has @@ -220,9 +234,10 @@ There is no "load" node: whether `x` means its address or its value depends on c ```cpp struct Type { enum Kind { Void, Bool, Char, I8, I16, I32, I64, U8, U16, U32, U64, F32, F64, - Str, Ptr, Null, Array, Fixed, Struct, Named }; + Str, Ptr, Null, Array, Map, Fixed, Struct, Named }; Kind kind; - std::shared_ptr elem; // Ptr (null = untyped `ptr`), Array, Fixed + std::shared_ptr elem; // Ptr (null = untyped `ptr`), Array, Fixed; Map: the value + std::shared_ptr key; // Map long long count; // Fixed std::string name, module; // Named (as written) / Struct (resolved) StructInfo *info; // Struct @@ -299,9 +314,12 @@ type names (conversion) → function of the current module → struct of the cur | `char` `i8` `u8` / `i16` `u16` / `i32` `u32` / `int` `u64` | `i8` / `i16` / `i32` / `i64` | | `f32` / `float` | `float` / `double` | | `str`, `[]T` | `{ ptr, i64, i64 }` (pointer, length, capacity) | +| `map[K]V` | `{ ptr, i64, i64, i64, ptr, i64 }` (entries, len, used, cap, index, icap; §10.12) | | `ptr`, `ptr[T]`, `null` | `ptr` (opaque) | | `[N]T` | `[N x T]` | | Finch struct | named struct type, natural layout | +| a method's `self` | `ptr` (the first parameter) | +| the result of `-> T!` | `{ i1 ok, T value, str error }` (`i8` in place of `T` for `-> !`) | | C struct | named **packed** struct with explicit padding bytes (§13.2) | Signedness lives only in the Finch type and selects `sdiv/udiv`, `ashr/lshr`, `icmp s*/u*`, @@ -375,6 +393,8 @@ value when it goes away. There is no reference counting and no garbage collector | a temporary nobody keeps (`print(a + b)`, `f(g())`, an expression statement) | **`release(v)`**: dropped right after use | | `a.pop()`, `a.remove(i)` | the element is moved out (fresh) | | `for x in list` | `x` borrows each element; if the body may change `list`, each element is copied instead | +| a method call `v.m()` | `self` **is** `v` (its address is passed): no copy; changes are visible to the caller | +| maps | own their keys and values like arrays own elements; `m[k] = v` moves/copies `v` in and drops the old value | A string literal is a fresh value with `cap = 0`: moving it costs nothing, dropping it is skipped (`release()` ignores constants), and writing into it (`s[i] = c`) first makes a heap copy @@ -389,6 +409,11 @@ parameters are borrowed (`Fn::borrowParam`); changed ones are copied once at ent syntactic and conservative: anything that *might* change the parameter makes a copy, which is always correct. The same analysis decides whether `for x in list` must copy elements. +Methods take part: `declareFns()` first finds which methods change `self` (`mutates("self", body)`), +repeating until nothing changes, because calling a changing method on `self` changes `self` too. Their +names go into `mutatingMethods`, which `mutatesExpr()` treats like `push`: `p.move(1)` changes `p`. +Only then is `borrowParam` computed for every function and method. + ### 9.3 Copy and drop `dropAt(addr, T)` / `copyAt(dst, src, T)` work on addresses: @@ -507,7 +532,9 @@ one. Compound `x += v`: evaluate `v`, load the target, `arith()`, store, drop th ### 10.4 Functions and `main` Functions are `internal` with their natural LLVM signature (aggregates by value). Owning parameters are -copied at entry only if `borrowParam` is false. `fn main` is the internal `finch.main`; a separate +copied at entry only if `borrowParam` is false. Methods (§10.10) get `self`'s address as an extra first +parameter; fallible functions (§10.11) return `{ i1, T, str }`. `fn main() -> !` / `-> int!` makes the +wrapper print `error: ` to stderr and exit with 1 on failure. `fn main` is the internal `finch.main`; a separate external `i32 main(i32, ptr)` (`defineMainWrapper`) builds `[]str args` with `finch_args` if requested, calls it, drops the arguments and returns the exit code. A function that falls off its end gets the scope cleanups and `ret void`, or the error `can reach its end without returning T`. @@ -537,9 +564,12 @@ scopes swapped out, or zero). C calls are in §13. ### 10.8 Printing -`print` emits `printf` per argument with a format chosen by type (`%lld`, `%llu`, `%g`, `%c`, `%.*s` for -`str` with its length, `%p`), separated by spaces, then `\n`. Arrays, fixed arrays and structs call -generated helpers `finch.print.` that print `[1, 2]` / `Point(x: 1, y: 2)`, quoting strings and chars inside. +`print` emits `printf` per scalar argument with a format chosen by type (`%lld`, `%llu`, `%g`, `%c`, +`%.*s` for `str` with its length), separated by spaces, then `\n`. Everything else (pointers, arrays, +fixed arrays, maps, structs) is first **formatted** into a heap `str` buffer by `emitFormat()` and +printed with `finch_buf_print`. Composite types get a generated helper `finch.format.(buf, value*)` +that appends `[1, 2]` / `{"a": 1}` / `Point(x: 1, y: 2)`, quoting strings and chars inside. `str(x)` of +an array, map or struct runs the same helper, so `str(x)` is always what `print(x)` shows. ### 10.9 Array and str methods @@ -549,6 +579,65 @@ Arrays: `push` (`finch_arr_reserve` + store + len++), `pop`/`remove` (move out, (swap loop), `sort` (libc `qsort` with a generated comparator `finch.cmp.`), `join` (runtime). Methods that change the array require an address (a variable/field/element). Strings: `sub find contains starts_with ends_with split trim upper lower replace repeat bytes`, all in the runtime. +`for i, x in list` adds a read-only `int` index variable next to the element. + +### 10.10 Methods + +`fn Point.move(int dx)` is declared like a function named `finch.[module.]Point.move` with a leading +`ptr` parameter and stored in `Codegen::methods[StructInfo*]` (so methods follow the struct into every +module that uses it, and C structs can have methods too). In `define()`, `self` is a `Var` whose `slot` +**is that pointer argument**: `self.x` is a GEP from the caller's own struct, and `self = …` writes the +caller's value. No copy happens, ever. + +At a call `obj.m(args)` (`structMethod()`), `ref(obj)` gives an address when `obj` is a place (a variable, +field, element, `m[key]`), the loaded pointer for a `ptr[T]` (null-checked), or a stack temporary for a +value (`Point(1, 2).length()`), which is dropped after the call. Two safety rules come from the mutation +analysis: a method that changes `self` can't be called on a read-only loop variable, and if such a method +is given an argument that lives inside the receiver (`c.merge(c)`, `p.add(p.items)`), the argument is +copied first, so `self` can't free memory the argument still points to. + +### 10.11 Errors as values + +A fallible function (`-> T!`) returns `{ i1 ok, T value, str error }`; on failure `value` is zero, so the +caller never has to drop it. `return v` wraps `{1, v, zero}` (`retValue()`); `return error(msg)` evaluates +the message, runs the cleanups and returns `{0, zero, msg}` (`retError()`). + +Calling a fallible function is only allowed as the operand of `or` / `try`. `fallible(call)` sets +`fallibleTarget` to the call expression; `callFinch()` (and the fallible built-ins) compare their own +`CallExpr`/`MethodExpr` against it, and if it matches, leave `fallibleFailed` (`i1`) and `fallibleMsg` +(`str`) for the caller; if a fallible call doesn't match, it's the compile error *'f' can fail, so say what +happens then*. Nested handlers save and restore the three fields. + +| Form | Lowering | +|---|---| +| `f(x) or fallback` | `br failed, or.failed, or.ok`; the failed block drops the message and evaluates the fallback (only there); a φ joins the two values | +| `f(x) or { … }` | the failed block declares `err` (owning the message) and runs the block. If the value is used, the block must end in a terminator; as a statement it may fall through, and the success path releases the unused value | +| `try f(x)` | the failed block runs `emitCleanups(0)` and returns `{0, zero, msg}` from the current function, which must be fallible | + +Built-ins with a failure mode check `&call == fallibleTarget` and switch to `finch_str_try_int`, +`finch_str_try_float`, `finch_try_read_file`, `finch_try_write_file`, `finch_try_delete_file`, which +return 1 / 0 and fill an error `str` instead of panicking. The cost on success is one predictable branch; +nothing is allocated unless a failure happens. + +### 10.12 Maps + +The runtime (§12) keeps an insertion-ordered hash table, in the style of CPython's `dict`: an `entries` +array of `{ i64 hash, K key, V value }` slots in insertion order (hash 0 = removed), plus an `index` of +`icap` slots (power of two, ≥ 2 × cap) with linear probing (−1 empty, −2 removed, else an entry number). +The runtime only hashes (FNV-1a + a 64-bit mix; text keys by content, others by bytes), finds and places +entries; the compiler stores, copies and drops keys and values, exactly like array elements. + +| Code | Lowering | +|---|---| +| `m[k]` read | `finch_map_find`; −1 → `finch_map_missing` (noreturn, with the key as text); else a place at `entry.value` | +| `m[k]` write (`=`, `+=`, a changing method, `addr`) | `mapSlot()`: `finch_map_slot` returns the entry, adding it if missing; a new entry gets a copy of the key and the value type's default | +| `["a": 1, …]` | a zeroed map + `mapSlot` per entry (the first entry decides the types; number values are all `float` if one has a dot) | +| `has`, `get`, `remove`, `clear`, `keys`, `values` | `codegen_map.cpp`; `get`'s default is evaluated only when the key is missing | +| `for k, v in m` | a loop over `0..used` that skips removed slots, re-reading `entries` and `used` every round | +| copy / drop / format | helpers like arrays': `finch_map_clone_raw` then deep copies of owning keys and values; drop each live entry, then `finch_map_free` | + +Removing marks the slot (and the index entry) removed; slots are packed when the table would grow while +at least a quarter of them are removed. Removing the last key resets the table. --- @@ -565,7 +654,9 @@ well-predicted branch. LLVM removes checks it can prove (constant indexes in ran | array/str/fixed-array index out of range (`icmp uge`, so negative indexes too) | `boundsCheck()` | | `.value`, `.field`, `[i]` through null | `member()`, `index()` | | `pop()` on empty, `slice()`/`sub()` out of range | methods, runtime | -| bad text in `int(s)`/`float(s)`, unreadable file in `read_file` | runtime | +| bad text in `int(s)`/`float(s)`, unreadable file in `read_file` (without `or` / `try`) | runtime | +| `m[key]` read with a missing key | `mapIndex()` | +| a method called through a null `ptr[T]` | `structMethod()` | A 20-million-element sieve with a bounds check on every access runs as fast as the same C (≈0.09 s): the checks are hoisted or folded. @@ -591,8 +682,11 @@ Functions take and return these **by pointer**, so their C ABI is trivial (no st string building (`concat`, `from_int/uint/float/char/bool`, `sub`, `trim`, `upper`, `lower`, `replace`, `split`, `join`, `repeat`, `from_bytes`), comparison (`eq`, `cmp`, `find`, `starts`, `ends`), ownership (`copy`, `own`, `drop`, `from_c`), arrays (`reserve`, `make`, `resize`, `clone_raw`, `free`, -`insert_gap`, `remove_gap`), `args`, `input`, files, `shell`, and the panics. Allocation failure prints -`out of memory` and exits. +`insert_gap`, `remove_gap`), maps (`map_find`, `map_slot`, `map_remove_at`, `map_clear`, `map_free`, +`map_clone_raw`, `map_missing`; §10.12), text buffers for `print`/`str` (`buf_add`, `buf_int`, …, +`buf_print`), the fallible variants (`str_try_int`, `str_try_float`, `try_read_file`, `try_write_file`, +`try_delete_file`, `report_error`), `args`, `input`, files, `shell`, and the panics. Allocation failure +prints `out of memory` and exits. --- @@ -749,7 +843,7 @@ with the expected one. It takes about 2.3 s for the self-hosted compiler to comp - libclang: `find_path(clang-c/Index.h)` + `find_library(clang)`. - `runtime/finch_rt.c` is read at configure time into `build/rt_source.inc` as a raw string literal; `CMAKE_CONFIGURE_DEPENDS` re-runs the configure step when it changes. -- `FINCH_VERSION` from `project(VERSION 2.3.0)`. C++17, `-Wall -Wextra` (MSVC: `/W3`), warning-free. +- `FINCH_VERSION` from `project(VERSION 2.4.0)`. C++17, `-Wall -Wextra` (MSVC: `/W3`), warning-free. - `shell.nix` lists `llvmPackages.clang` before `llvmPackages.libclang`: the latter also ships an unwrapped `clang` that can't find the system headers. @@ -759,9 +853,12 @@ with the expected one. It takes about 2.3 s for the self-hosted compiler to comp - `tests/run.sh`: every `tests/run/*.fch` with `fn main` must print exactly its `.out` (stdin from `.in` if present); files without `main` are modules or helpers. Every `tests/fail/*.fch` must fail with the text - from its `// expect:` line. Currently **59 passed, 0 failed**. + from its `// expect:` line. Currently **74 passed, 0 failed**. - `MEMCHECK=1 tests/run.sh`: the same, plus valgrind on every program (no leaks, no invalid access). - `tests/boot.sh`: the self-hosting fixpoint and the subset run (§17). +- `tests/cross.sh windows|arm64`: every test built with `--target` and run under Wine / QEMU. +- `python3 tests/lsp_test.py`: the language server, driven like an editor. +- CI (`.github/workflows/ci.yml`) runs all of it on Linux x86-64, Linux ARM64, macOS ARM64 and Windows. --- @@ -850,11 +947,14 @@ with the expected one. It takes about 2.3 s for the self-hosted compiler to comp | Function | Does | |---|---| | `call` / `method` | resolve a call / a method or module-qualified call | -| `callFinch` / `construct` / `callC` | Finch function / struct constructor / C function | +| `callFinch` / `construct` / `callC` | Finch function or method / struct constructor / C function | +| `structMethod` | `obj.m(...)`: the receiver's address, safety checks, the call | | `print`, `addrOf`, `convert`, `arrayMethod` | built-ins | +| `fallible`, `tryExpr`, `orElse`, `retValue`, `retError` (`codegen.cpp`) | errors as values (§10.11) | +| `mapIndex`, `mapLit`, `mapMethod`, `mapSlot`, `forEachEntry` (`codegen_map.cpp`) | maps (§10.12) | | `rt` / `libc` | declare a runtime / libc function | | `fileName`, `panicIf`, `boundsCheck`, `checkDivisor` | runtime checks | -| `classify`, `makePlan`, `declareC`, `emitCCall`, `cThunk` | System V classification, register plan, declaration, call, callback thunk | +| `classify`, `makePlan`, `declareC`, `emitCCall`, `cThunk` | ABI classification (System V, Microsoft x64, AAPCS64), register plan, declaration, call, callback thunk | --- @@ -877,12 +977,15 @@ emission in `arithOp`/`compare`, an explanation in `badOperands`. --- -## 23. Targets and Windows +## 23. Targets: Windows, macOS, ARM64 -`src/target.h` holds `g_target`: the LLVM triple, `windows` / `msvc` / `cross` flags, the C compiler (`cc`) -and the executable suffix. `setTarget()` picks the C compiler: `FINCH_CC`, else `CC` (not when -cross-compiling), else `x86_64-w64-mingw32-gcc` for `--target windows` from Linux, `clang` on Windows, -`cc` on Unix. Everything platform-dependent goes through it: +`src/target.h` holds `g_target`: the LLVM triple, `windows` / `msvc` / `darwin` / `cross` flags, the C +compiler (`cc`) and the executable suffix. `setTarget()` maps `windows`, `linux`, `arm64`, `macos` (or a full +triple) to a triple and picks the C compiler: `FINCH_CC`, else `CC` (not when cross-compiling), else +`x86_64-w64-mingw32-gcc` / `aarch64-linux-gnu-gcc` for Windows / ARM64 Linux from another system, +`clang --target=` 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=` (so `long` is 32 bits on Windows and the right headers are used) and, on Windows, `-D_USE_MATH_DEFINES`. Include folders come from ` -E -v`. @@ -893,7 +996,12 @@ cross-compiling), else `x86_64-w64-mingw32-gcc` for `--target windows` from Linu - **Linking:** no `-lm` with MSVC; `.exe` names; the runtime cache key includes the triple and the C compiler. MSVC linker messages (`LNK2019 unresolved external symbol`, `LNK1104`/`LNK1181 cannot open file`) are translated like GNU ld's. -- **Running:** a Windows program built on Linux runs with `wine`. +- **Running:** `runPrefix()`: a Windows program built on Linux runs with `wine`, an ARM64 Linux one + elsewhere with `qemu-aarch64`. +- **macOS:** ` -E -v` lists `(framework directory)` entries; they become `-iframework` for libclang, + so `import "OpenGL/gl.h"` works. `link "Cocoa.framework"` becomes `-framework Cocoa`. Apple's linker + messages (`Undefined symbols for architecture arm64: "_f"`, `ld: library 'x' not found`, + `framework 'X' not found`) are translated like the others, without Mach-O's leading `_`. - **Debug info:** CodeView for MSVC targets, DWARF elsewhere. - **Runtime:** no `getline` or `sys/wait.h` on Windows; `stdin`/`stdout`/`stderr` are macros there, so `finch_std_stream(i)` gives them to Finch when a header has no `extern` variable for them. @@ -908,10 +1016,30 @@ receives a plain pointer). Results of 1/2/4/8 bytes come back in `rax` as an int through a hidden `sret` pointer. Each argument takes one slot, so there is no register counting. `cThunk()` mirrors this for callbacks. -`tests/windows.sh` builds every test with `--target windows`, runs it with Wine and compares the output: +`tests/cross.sh windows` builds every test with `--target windows`, runs it with Wine and compares the output: all of them pass, including `c_structs` (which then exercises the Microsoft rules against a C library built with MinGW). CI additionally builds `finch.exe` with MSVC on Windows Server and runs `tests/run.sh` there. +### AAPCS64 (ARM64 Linux and Apple) + +`classify()` mirrors clang's AArch64 lowering: + +- A **homogeneous floating-point aggregate** (1–4 members, all `float` or all `double`, nested structs + and arrays flattened) goes in vector registers: passed as `[n x float]` / `[n x double]`, returned as + `{ float, … }`. On Linux the argument also gets `alignstack(8)`, as clang does; Apple doesn't. +- Any other struct of up to 16 bytes goes in general registers: `i64` (≤ 8 bytes) or `[2 x i64]` + (`i128` if its alignment is 16); it is returned as an integer of its exact size or `[2 x i64]`. +- Bigger structs are **Indirect**: a copy on the caller's stack, passed by address (no `byval`); returned + through `sret`. +- There is no register counting: the backend places each part. +- C's plain `char` is **unsigned** on ARM64 Linux and signed on Apple and x86, which matters for the + sign/zero extension of `char` arguments and results (`charIsSigned()`). +- Apple passes the variadic part of `printf(...)` on the stack; LLVM does that from the `...` in the + function type. + +`tests/cross.sh arm64` runs the whole suite (including the struct ABI test against a C library built by +GCC) under QEMU, and CI runs it natively on ARM64 Linux and Apple Silicon. + ### Building finch.exe CI uses Visual Studio 2022, Ninja and the official `clang+llvm-21.x-x86_64-pc-windows-msvc` package @@ -957,7 +1085,19 @@ signature help (and mirrored by the cheat sheets). --- -## 25. The VS Code extension +## 25. Editor integrations + +### Kate + +`editors/kate/finch.xml` is a KSyntaxHighlighting definition (the engine of Kate, KWrite and KDevelop): +keyword lists that match `builtins_doc.h`, a context for `fn Type.method`, strings with escapes, and +folding regions for `{ }` and block comments. `lspclient.json` is the entry for Kate's LSP +client (`highlightingModeRegex: ^Finch$`); `install.sh` copies the definition and merges the entry into +`~/.config/kate/lspclient/settings.json`. Kate sends more requests than VS Code (`documentHighlight`, +`foldingRange`, …); the server answers every request it doesn't implement with `MethodNotFound`, so no +client waits. + +### VS Code `editors/vscode` is a plain JavaScript extension: `extension.js` starts `finch lsp` through `vscode-languageclient` (path from the `finch.path` setting) and adds **Run** / **Build** as @@ -969,10 +1109,15 @@ templates. CI packages it with `vsce` into `finch-lang.vsix`. ## 26. Known limitations -- No methods on structs, generics, maps, `match`, closures, error values (`int("x")`, `read_file` panic). +- No generics, interfaces, `match`, closures or Finch-side enums. - Strings are bytes: `.len`, `s[i]`, `upper()` are not Unicode-aware. +- Changing a map's keys inside a `for` over the same map is not detected (it is memory-safe, but entries + may be skipped or seen twice). +- A `try` / `or` failure in the middle of an expression doesn't release fresh temporaries computed + earlier in that expression (a small leak on the failure path only). - Dangling pointers (`addr` of a local that went away, use after `free`) are not detected. - Shift amounts and float→int conversions are not range-checked (LLVM poison, as in C). - C: unions and bit-field structs by value, function-like macros, `long double`. -- Two systems: Linux and Windows, both x86-64 (System V and Microsoft x64 ABIs). No macOS or ARM yet. -- The self-hosted compiler has no C imports, sized integers or `defer`, and doesn't free memory. +- Targets: x86-64 and ARM64 (Linux, macOS, Windows on x86-64). No Windows on ARM64, 32-bit, or WebAssembly. +- The self-hosted compiler has no C imports, sized integers, `defer`, maps, methods or errors as values, + and doesn't free memory. diff --git a/docs/en/for-technicians.md b/docs/en/for-technicians.md index b29128f..78b7666 100644 --- a/docs/en/for-technicians.md +++ b/docs/en/for-technicians.md @@ -20,16 +20,16 @@ compiler works inside. For that, see the [guide for engineers](for-engineers.md) 7. [Operators](#7-operators) 8. [Control flow](#8-control-flow) 9. [Functions](#9-functions) -10. [Arrays](#10-arrays) +10. [Arrays and maps](#10-arrays-and-maps) 11. [Text (str)](#11-text-str) -12. [Structs](#12-structs) +12. [Structs and methods](#12-structs-and-methods) 13. [Memory: who frees what](#13-memory-who-frees-what) 14. [Pointers](#14-pointers) 15. [Built-in functions](#15-built-in-functions) 16. [Modules](#16-modules) 17. [Using C libraries](#17-using-c-libraries) 18. [Errors](#18-errors) -19. [Editor: VS Code](#19-editor-vs-code) +19. [Editors: VS Code, Kate and others](#19-editors-vs-code-kate-and-others) 20. [Debugging](#20-debugging) 21. [Troubleshooting](#21-troubleshooting) 22. [Project layout and tests](#22-project-layout-and-tests) @@ -60,7 +60,9 @@ messages that tell you how to fix the problem. ## 2. Installing -Finch runs on **Linux and Windows, x86-64**, and is built with **LLVM 21**. +Finch runs on **Linux** (x86-64 and ARM64), **macOS** (Apple Silicon and Intel) and **Windows** (x86-64), +and is built with **LLVM 21**. Each [release](https://github.com/Nexoniarz/Finch/releases) has a ready +`finch` for Windows, Linux x86-64, Linux ARM64 and macOS ARM64. ### Windows @@ -76,8 +78,30 @@ Finch runs on **Linux and Windows, x86-64**, and is built with **LLVM 21**. Programs are normal `.exe` files. To build from source on Windows instead, follow `.github/workflows/ci.yml` (Visual Studio 2022 + the `clang+llvm-21.x-x86_64-pc-windows-msvc` package from LLVM's releases). +### macOS + +```sh +brew install llvm@21 # Finch's LLVM; programs are linked with Apple's clang +``` + +Then either unpack **`finch-macos-arm64.tar.gz`** from the releases (it uses Homebrew's `llvm@21`), +or build it yourself (Xcode command line tools: `xcode-select --install`): + +```sh +git clone https://github.com/Nexoniarz/Finch.git && cd Finch +cmake -S . -B build -G Ninja -DCMAKE_BUILD_TYPE=Release -DLLVM_DIR="$(brew --prefix llvm@21)/lib/cmake/llvm" +ninja -C build +./build/finch version +``` + +Apple frameworks link with `link "Cocoa.framework"` (`-framework Cocoa`), and their headers import as usual: +`import "OpenGL/gl.h"`. + ### Linux +The ready `finch-linux-x64.tar.gz` / `finch-linux-arm64.tar.gz` need LLVM 21's libraries +(Debian/Ubuntu: `libllvm21 libclang1-21` from [apt.llvm.org](https://apt.llvm.org)) and a C compiler (`cc`). +Building it yourself works the same on x86-64 and ARM64 (Raspberry Pi 4/5, Graviton, …). Other LLVM versions will likely fail to compile, because LLVM's C++ API changes between releases. ### Option A: Nix (recommended, nothing to install by hand) @@ -88,7 +112,7 @@ cd Finch nix-shell # downloads LLVM, libclang, clang, cmake, ninja, pkg-config cmake -S . -B build -G Ninja ninja -C build -./build/finch version # finch 2.3.0 (LLVM 21.x) +./build/finch version # finch 2.4.0 (LLVM 21.x) ``` ### Option B: your distribution's packages @@ -109,16 +133,27 @@ cmake -S . -B build -G Ninja -DLLVM_DIR=/usr/lib/llvm-21/lib/cmake/llvm ninja -C build ``` -### Building Windows programs on Linux +### Building for another system -With the MinGW-w64 cross compiler installed (`x86_64-w64-mingw32-gcc`; on Nix -`pkgsCross.mingwW64.buildPackages.gcc`): +`--target` builds a program for another system. Finch needs that system's C compiler to link: + +| `--target` | Builds | C compiler used | `finch run` uses | +|---|---|---|---| +| `windows` | a Windows `.exe` | `x86_64-w64-mingw32-gcc` (MinGW-w64) | Wine | +| `arm64` | an ARM64 Linux program | `aarch64-linux-gnu-gcc` | `qemu-aarch64` | +| `linux` | an x86-64 Linux program | `clang --target=…` | | +| `macos` | a macOS program (on a Mac: for the other processor) | `clang --target=…` | | +| any LLVM triple | e.g. `aarch64-unknown-linux-gnu` | `clang --target=` | | ```sh finch build game.fch --target windows # game.exe -finch run game.fch --target windows # runs it with Wine, if installed +finch run game.fch --target arm64 # builds for ARM64 and runs it with QEMU +FINCH_CC=aarch64-unknown-linux-gnu-gcc finch build tool.fch --target arm64 # another compiler name ``` +On Nix the compilers are `pkgsCross.mingwW64.buildPackages.gcc` and +`pkgsCross.aarch64-multiplatform.buildPackages.gcc`. + ### Putting `finch` on your PATH (optional) ```sh @@ -137,12 +172,14 @@ Finch's small runtime library, which is kept in `~/.cache/finch`. Set `CC` to us finch run [args...] compile and run right away finch build [-o name] compile into a program (default name: the file's name) finch ir print the LLVM IR (for the curious) +finch lsp the language server, for editors finch version show the version options: - -l link a C library, same as link "lib" in the file - -g add debug info (for gdb / lldb) - -O0 skip optimizations + -l link a C library, same as link "lib" in the file + --target build for another system: windows, linux, arm64, macos, or an LLVM triple + -g add debug info (for gdb / lldb / Visual Studio) + -O0 skip optimizations ``` Examples: @@ -227,6 +264,7 @@ x += 1 // also -= *= /= %= | `char` | one byte-sized character | `'A'`, `'\n'` | | `str` | text | `"hello\tworld"` | | `[]T` | a list (array) of `T` | `[1, 2, 3]` | +| `map[K]V` | values of type `V` found by a key of type `K` | `["a": 1, "b": 2]` | | your structs | named groups of values | `Point(1, 2)` | String and char escapes: `\n` new line, `\t` tab, `\r`, `\0`, `\\`, `\"`, `\'`. @@ -333,6 +371,14 @@ for i in 0..10 { // i = 0, 1, …, 9 (the end is not included) for name in names { // every element of an array (or every char of a str) print(name) } + +for i, name in names { // with its index: 0, 1, 2, ... + print(i, name) +} + +for key, value in ages { // every entry of a map, in the order the keys were added + print(key, value) // (for key in ages: just the keys) +} ``` - A condition must be a `bool`. `if x {` with a number is an error; write `if x != 0 {`. @@ -375,11 +421,13 @@ fn no_result(str message) { // no "->": returns nothing To change the caller's variable, pass a pointer: see [Pointers](#14-pointers). - A function with `-> type` must return a value on every path. Finch checks this. - A function cannot be defined inside another function. +- A function that can fail is written `-> int!` (or `-> !` with no result); see + [Errors as values](#errors-as-values). - Long calls can be split over several lines inside the brackets; a comma after the last argument is fine. --- -## 10. Arrays +## 10. Arrays and maps ```c nums := [5, 3, 8] // an array of int @@ -408,6 +456,44 @@ grid := [[1, 2], [3, 4]] // arrays of arrays **Assigning copies:** after `b := a`, changing `b` does not change `a`. +### Maps + +A `map[K]V` finds values by a key. Keys can be whole numbers, `char`, `bool` or `str`; values can be +anything (arrays, structs, other maps). Iterating goes in the order the keys were first added. + +```c +ages := ["anna": 31, "bob": 25] // a map literal: map[str]int +map[str][]str groups // an empty map +map[int]str names = [:] // [:] is an empty map where the type is known +``` + +| Operation | Meaning | +|---|---| +| `m[k]` | the value for `k`; a missing key stops the program with `the key "k" is not in the map` | +| `m[k] = v` | add or replace | +| `m[k] += 1`, `m[k].push(x)`, `m[k].field = …` | changing a missing key adds it first, with the value type's default (0, `""`, `[]`, …) | +| `m.len` | the number of keys | +| `m.has(k)` | is `k` there? | +| `m.get(k, default)` | the value, or `default` (only computed when needed) | +| `m.remove(k)` | removes `k`; `true` if it was there | +| `m.clear()` | removes everything | +| `m.keys()`, `m.values()` | new arrays, in insertion order | +| `print(m)`, `str(m)` | `{"anna": 31, "bob": 25}` | + +```c +map[str]int counts +for word in text.split(" ") { + counts[word] += 1 // counting: a new word starts at 0 +} +for word, n in counts { + print(word, n) +} +``` + +Lookups are hash-based (average constant time). Like arrays, maps own their keys and values, +copy on assignment, and are freed at the end of their block. Don't add or remove keys of a map +inside a `for` over that same map; collect them in an array and change the map after the loop. + --- ## 11. Text (str) @@ -437,7 +523,7 @@ to C as `char*` works. --- -## 12. Structs +## 12. Structs and methods ```c struct Player { @@ -457,7 +543,34 @@ print(p) // Player(name: "Ola", lives: 2, scores: [10], pos: Point(x: 5, y - Assigning or passing a struct copies it, together with its arrays and text. - A struct can't contain itself directly; use `ptr[Node]` or `[]Node` for that field (trees, lists). -- Structs have no methods. Write functions that take the struct: `fn heal(Player p) -> Player`. + +### Methods + +A method is a function that belongs to a struct: `fn Struct.name(...)`. Inside, `self` is the struct +it was called on, and it is the caller's value itself, not a copy: changes to `self` stay. + +```c +fn Player.heal(int amount) { + self.lives += amount +} + +fn Player.is_alive() -> bool { + return self.lives > 0 +} + +p := Player(name: "Ola") +p.heal(2) // p.lives is now 5 +if p.is_alive() { ... } + +team[0].heal(1) // on an array element, a field, m[key], through a ptr[Player] ... +``` + +- A method goes in the same file (module) as its struct, and is available wherever the struct is. +- Methods can also be added to **structs from C headers**: `fn Vector2.length() -> f32 { ... }`. +- A method can't have the name of a field. +- Calling a method that changes `self` on a loop variable (`for p in team { p.heal(1) }`) is an error, + because the loop variable is read-only; use `for i in 0..team.len { team[i].heal(1) }`. +- Like functions, methods can fail (`-> !`); see [Errors as values](#errors-as-values). --- @@ -520,14 +633,16 @@ Using `.value`, `.field` or `[i]` on a `null` pointer **stops the program** with |---|---| | `print(a, b, ...)` | Prints all values separated by spaces, then a new line. Works with every type, including arrays and structs. | | `input()`, `input("question")` | Reads one line typed by the user (without the newline). Empty text at end of input. | -| `read_file(path)` | The whole file as `str`. Stops the program with a clear message if it can't be read. | -| `write_file(path, text)` | Writes (replaces) the file; gives back `true` if it worked. | +| `read_file(path)` | The whole file as `str`. Stops the program with a clear message if it can't be read, unless handled: `read_file(p) or ...`. | +| `write_file(path, text)` | Writes (replaces) the file; gives back `true` if it worked. With `or` / `try`, the failure carries the reason. | | `file_exists(path)` | `true` / `false` | +| `delete_file(path)` | Deletes the file; `true` if it worked (or the reason, with `or` / `try`). | +| `error(message)` | The failure a fallible function returns: `return error("...")`. | | `shell(command)` | Runs a shell command, gives back its exit code. | | `exit(code)` | Ends the program right away. | | `addr(x)` | A pointer to `x`; `addr(function)` gives C a function pointer (callback). | | `new(value)`, `free(p)` | Manual heap memory (see [Memory](#13-memory-who-frees-what)). | -| `int(x)`, `str(x)`, `u8(x)`, … | Conversions (see [Conversions](#conversions)). | +| `int(x)`, `str(x)`, `u8(x)`, … | Conversions (see [Conversions](#conversions)). `str(x)` works on every type and gives the same text `print` shows. `int(text) or 0` handles text that isn't a number. | --- @@ -669,6 +784,54 @@ Both work directly; `examples/opengl.fch` and `examples/vulkan.fch` are complete ## 18. Errors +### Errors as values + +Some failures are normal: the user types `abc`, a file is missing. Finch handles them without +exceptions and without hidden control flow. A function that can fail says so with `!` after its result +type, and fails with `return error("message")`: + +```c +fn parse_port(str text) -> int! { + n := int(text) or { return error("'" + text + "' is not a number") } + if n < 1 || n > 65535 { + return error("port " + str(n) + " is out of range") + } + return n +} + +fn save(str path, str text) -> ! { // can fail, gives back nothing + try write_file(path, text) +} +``` + +The caller **must** handle the failure, in one of three ways (forgetting is a compile error): + +| Code | On failure | +|---|---| +| `port := parse_port(s) or 8080` | use this value instead (it's only computed on failure; it can be another call with `or`) | +| `port := parse_port(s) or { print(err); return }` | run the block; `err` is the message (a `str`). The block must leave (`return`, `break`, `continue`, `exit`) unless the result isn't used. | +| `port := try parse_port(s)` | fail the current function with the same error (it must be fallible too) | + +```c +fn load(str path) -> Config! { + text := try read_file(path) + port := try parse_port(text.trim()) + return Config(port: port) +} + +fn main() -> ! { // main can fail too: the error is printed, and the exit code is 1 + config := try load("app.conf") + ... +} +``` + +Built-ins that can fail work the same way: `int(text)`, `float(text)`, `read_file`, `write_file`, +`delete_file`. Without `or` / `try` they behave as before (`int("x")` stops the program, +`write_file` gives back a `bool`). + +Under the hood a fallible function returns its value plus a flag and the message: no allocation on +success, no unwinding, and the cost of a failure is building its message. + ### Compile errors ``` @@ -691,11 +854,30 @@ Instead of undefined behavior (as in C), Finch stops with a message and exit cod | `runtime error: pop() on an empty array` | | | `runtime error: used .value on a null pointer` | also `.field` and `[i]` through `null` | | `runtime error: can't turn "x" into int` | `int(...)` / `float(...)` on text that isn't a number | -| `runtime error: can't read the file "…"` | `read_file` on a missing or unreadable file | +| `runtime error: can't read the file "…"` | `read_file` on a missing or unreadable file (handle it with `or`) | +| `runtime error: the key "x" is not in the map …` | `m["x"]` when the map has no such key (use `.has` or `.get`) | +| `runtime error: called .f() on a null pointer` | a method called through a `null` `ptr[T]` | --- -## 19. Editor: VS Code +## 19. Editors: VS Code, Kate and others + +Every editor feature comes from the same language server, `finch lsp`, which uses the real compiler: +errors as you type, types and docs on hover, go to definition, completion (fields, methods, map and array +methods after `.`, module members, names from C headers), the file's outline, and parameter hints. + +### Kate (and KWrite, KDevelop) + +```sh +editors/kate/install.sh +``` + +It installs the highlighting (`~/.local/share/org.kde.syntax-highlighting/syntax/finch.xml`) and adds `finch` +to Kate's LSP client settings, keeping your other servers. Restart Kate and enable the **LSP Client** plugin. +To run programs, add `finch run %f` as a target in the **Build & Run** plugin; Finch's +`file:line:column: error:` messages are then clickable. Details: `editors/kate/README.md`. + +### VS Code The **Finch** extension (`editors/vscode`, also attached to each release as a `.vsix` file) gives you: highlighting, errors as you type, completion (also fields and methods after `.`), types on hover, @@ -703,8 +885,12 @@ go to definition (F12), an outline of the file, parameter hints, snippets, and a (`Ctrl+F5`). Errors from **Finch: Build This File** appear in the Problems panel. Install: Extensions → `…` → *Install from VSIX…* → pick `finch-lang-*.vsix`. The extension runs `finch` -from your PATH; if it is somewhere else, set **Finch: Path** in the settings. Other editors that speak the -Language Server Protocol (Neovim, Helix, Zed, Emacs, Sublime) can use `finch lsp` directly. +from your PATH; if it is somewhere else, set **Finch: Path** in the settings. + +### Others + +Neovim, Helix, Zed, Emacs, Sublime and any other editor that speaks the Language Server Protocol: +run `finch lsp` for files ending in `.fch` (stdin/stdout, no options). ## 20. Debugging @@ -737,6 +923,9 @@ Visual Studio debugger and WinDbg read. | A library is not found when the program starts | It was linked from a folder the system doesn't search. Run inside the same `nix-shell`, or install it system-wide. | | Windows: `finch` can't link (`LNK…` errors about `libcmt`, `kernel32`) | Install Visual Studio Build Tools with *Desktop development with C++*. | | Windows: `finch.exe` doesn't start (missing `libclang.dll`) | Keep `libclang.dll` next to `finch.exe`. | +| macOS: `finch` doesn't start (`Library not loaded: …libLLVM…`) | `brew install llvm@21`. | +| macOS: `can't find the C header 'stdio.h'` | Install the command line tools: `xcode-select --install`. | +| `--target arm64`: `no C compiler found to link with` | Install `aarch64-linux-gnu-gcc` (Debian: `gcc-aarch64-linux-gnu`), or set `FINCH_CC`. | | `clang` inside `nix-shell` can't find `stdio.h` | You have an old `shell.nix`: `llvmPackages.clang` must be listed before `llvmPackages.libclang`. | | A program never ends | A loop condition never becomes false. Press Ctrl+C. | @@ -755,17 +944,18 @@ Finch/ │ ├── fail/ programs that must fail, with the expected error in line 1 │ ├── run.sh the test runner (MEMCHECK=1 also checks memory with valgrind) │ └── boot.sh builds the self-hosted compiler with itself and compares -├── editors/vscode/ the VS Code extension +├── editors/ the VS Code extension (vscode/), Kate highlighting and LSP setup (kate/) ├── docs/ this documentation (en, pl), including the cheat sheet ├── shell.nix the Nix development environment └── CMakeLists.txt ``` ```sh -tests/run.sh # → 59 passed, 0 failed +tests/run.sh # → 74 passed, 0 failed MEMCHECK=1 tests/run.sh # the same under valgrind: no leaks, no bad memory access tests/boot.sh # the self-hosting check (needs clang) -tests/windows.sh # every test built for Windows and run with Wine +tests/cross.sh windows # every test built for Windows and run with Wine +tests/cross.sh arm64 # every test built for ARM64 Linux and run with QEMU python3 tests/lsp_test.py # the language server ``` @@ -773,12 +963,11 @@ python3 tests/lsp_test.py # the language server ## 23. Current limits -Finch 2.0 is a complete small language, but not a finished one. Not there yet: +Not there yet: -- Methods on structs, generics, maps/dictionaries, `match`. (Use arrays of structs and functions.) -- Error values: errors in `int("x")` or `read_file` stop the program. Check first (`file_exists`). +- Generics (your own `List[T]`), interfaces, `match`, enums written in Finch. - Unicode-aware text: `.len`, `s[i]` and `upper()` work on bytes / ASCII. - Threads. -- Platforms other than Linux and Windows on x86-64 (macOS, ARM). +- Windows on ARM64, 32-bit systems, WebAssembly. See the [roadmap in the README](../../README.md#roadmap). diff --git a/docs/pl/dla-inzynierow.md b/docs/pl/dla-inzynierow.md index 4af2c9a..0c17234 100644 --- a/docs/pl/dla-inzynierow.md +++ b/docs/pl/dla-inzynierow.md @@ -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) 21. [Spis funkcji](#21-spis-funkcji) 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) -25. [Rozszerzenie VS Code](#25-rozszerzenie-vs-code) +25. [Integracje z edytorami](#25-integracje-z-edytorami) 26. [Znane ograniczenia](#26-znane-ograniczenia) --- @@ -90,20 +90,24 @@ Decyzje, które kształtują resztę: | 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/ast.h` | 310 | `Type` (z Array, 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/cimport.h/.cpp` | 420 | Import przez libclang: funkcje, struktury C (pola, przesunięcia), stałe, zmienne globalne | +| `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` | 610 | Parser zstępujący `Parser`, `typeFromName()` | +| `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_impl.h` | 265 | Klasa `Codegen` i jej struktury pomocnicze, wspólne dla czterech 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_types.cpp` | 500 | Typy LLVM, rozwiązywanie typów, układ struktur, konwersje, własność (helpery kopiowania/zwalniania), wypisywanie, typy DWARF | -| `src/codegen_builtins.cpp` | 710 | Wywołania, 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/main.cpp` | 390 | Sterownik: CLI, ładowanie, maszyna docelowa, O2, emisja, cache runtime'u, linkowanie, porady przy błędach linkera, `run` | -| `runtime/finch_rt.c` | 440 | Biblioteka uruchomieniowa (§12) | -| `boot/*.fch` | 3080 | Kompilator samohostujący (§17) | +| `src/codegen_impl.h` | 310 | Klasa `Codegen` i jej struktury pomocnicze, wspólne dla plików poniżej | +| `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` | 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` | 890 | Wywołania, metody, konstruktory, funkcje wbudowane, konwersje, metody tablic i tekstów, deklaracje runtime'u, panic | +| `src/codegen_map.cpp` | 240 | Mapy: indeksowanie, literały, metody, przechodzenie | +| `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 | +| `src/lsp.cpp`, `src/index.h`, `src/builtins_doc.h` | 700 | Serwer języka (§24) | +| `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 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: - `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 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). @@ -142,27 +147,31 @@ program = { import | link | struct | function } ; import = "import" ( STRING | IDENT ) ; (* "x.h" = nagłówek C, nazwa = moduł Fincha *) link = "link" STRING ; (* "glfw" | "plik.c" | "plik.o" | "lib.a" *) 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 ; -type = "[" "]" type | "ptr" [ "[" type "]" ] | TYPENAME | IDENT [ "." IDENT ] ; +type = "[" "]" type | "map" "[" type "]" type | "ptr" [ "[" type "]" ] + | TYPENAME | IDENT [ "." IDENT ] ; block = "{" { statement TERMINATOR } "}" ; statement = if | while | for | return | "break" | "continue" | "defer" statement | block | type IDENT [ "=" expr ] (* deklaracja, zobacz atDeclaration *) | IDENT ":=" expr | target assignop expr (* cel: zmienna, pole, element, p.value *) - | call | method ; -for = "for" IDENT "in" expr ( ".." expr block | block ) ; (* zakres | po elementach *) + | call | method | try | orelse ; +for = "for" IDENT [ "," IDENT ] "in" expr ( ".." expr block | block ) ; (* zakres | po elementach *) 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 } ; add = mul { ( "+" | "-" | "|" | "^" ) mul } ; mul = unary { ( "*" | "/" | "%" | "<<" | ">>" | "&" ) unary } ; -unary = ( "-" | "!" | "~" ) unary | postfix ; +unary = ( "-" | "!" | "~" ) unary | "try" postfix | postfix ; postfix = primary { "." IDENT [ args ] | "[" expr "]" } ; primary = INT | FLOAT | STRING | CHAR | "true" | "false" | "null" - | IDENT [ args ] | "(" expr ")" | "[" [ expr { "," expr } [","] ] "]" ; + | IDENT [ args ] | "(" expr ")" | "[" [ expr { "," expr } [","] ] "]" + | "[" ":" "]" | "[" expr ":" expr { "," expr ":" expr } [","] "]" ; (* mapy *) 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`) | | `Index` | `IndexExpr` | `obj`, `index` | | `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ł | |---|---| -| `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ą, 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 struct Type { enum Kind { Void, Bool, Char, I8, I16, I32, I64, U8, U16, U32, U64, F32, F64, - Str, Ptr, Null, Array, Fixed, Struct, Named }; + Str, Ptr, Null, Array, Map, Fixed, Struct, Named }; Kind kind; - std::shared_ptr elem; // Ptr (null = `ptr` bez typu), Array, Fixed + std::shared_ptr elem; // Ptr (null = `ptr` bez typu), Array, Fixed; Map: wartość + std::shared_ptr key; // Map long long count; // Fixed std::string name, module; // Named (jak napisano) / Struct (rozwiązany) 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` | | `f32` / `float` | `float` / `double` | | `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) | | `[N]T` | `[N x T]` | | 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) | 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 | | `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 | +| 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 (`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. 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 `dropAt(addr, T)` / `copyAt(dst, src, T)` działają na adresach: @@ -513,7 +538,10 @@ wartości (dla `str +=`). ### 10.4 Funkcje i `main` 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: ` 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_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`. @@ -543,10 +571,13 @@ zasięgami wywołującego, albo zero). Wywołania C opisuje §13. ### 10.8 Wypisywanie -`print` emituje `printf` dla każdego argumentu z formatem wybranym wg typu (`%lld`, `%llu`, `%g`, `%c`, -`%.*s` dla `str` z jego długością, `%p`), oddzielając spacjami, a na końcu `\n`. Tablice, tablice o stałym -rozmiarze i struktury wołają generowane helpery `finch.print.`, które wypisują `[1, 2]` / -`Punkt(x: 1, y: 2)`, biorąc teksty i znaki w środku w cudzysłów. +`print` emituje `printf` dla każdego argumentu skalarnego z formatem wybranym wg typu (`%lld`, `%llu`, +`%g`, `%c`, `%.*s` dla `str` z jego długością), oddzielając spacjami, a na końcu `\n`. Wszystko inne +(wskaźniki, tablice, tablice o stałym rozmiarze, mapy, struktury) jest najpierw **zamieniane na tekst** +w buforze `str` na stercie przez `emitFormat()` i wypisywane przez `finch_buf_print`. Typy złożone dostają +generowany helper `finch.format.(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 @@ -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.`), `join` (runtime). 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. +`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()` | | `.value`, `.pole`, `[i]` przez null | `member()`, `index()` | | `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 (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`, `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`, -`insert_gap`, `remove_gap`), `args`, `input`, pliki, `shell` i panic. Brak pamięci wypisuje -`out of memory` i kończy proces. +`insert_gap`, `remove_gap`), mapy (`map_find`, `map_slot`, `map_remove_at`, `map_clear`, `map_free`, +`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)`. - `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. -- `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ż „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 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). - `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 | |---|---| | `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 | +| `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 | | `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`) -i rozszerzenie plików wykonywalnych. `setTarget()` wybiera kompilator C: `FINCH_CC`, potem `CC` (nie przy -kompilacji krzyżowej), potem `x86_64-w64-mingw32-gcc` dla `--target windows` z Linuksa, `clang` na -Windowsie, `cc` na Uniksie. Przez tę warstwę idzie wszystko, co zależy od platformy: +`src/target.h` trzyma `g_target`: triple LLVM, flagi `windows` / `msvc` / `darwin` / `cross`, kompilator C +(`cc`) i rozszerzenie plików wykonywalnych. `setTarget()` zamienia `windows`, `linux`, `arm64`, `macos` +(albo pełny triple) na triple i wybiera kompilator C: `FINCH_CC`, potem `CC` (nie przy kompilacji +krzyżowej), potem `x86_64-w64-mingw32-gcc` / `aarch64-linux-gnu-gcc` dla Windowsa / Linuksa ARM64 z innego +systemu, `clang --target=` 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=` (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 ` -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. Komunikaty linkera MSVC (`LNK2019 unresolved external symbol`, `LNK1104`/`LNK1181 cannot open file`) są 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:** ` -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. - **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`. @@ -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 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). 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 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 `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 -- 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. +- 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. - 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`. -- Dwa systemy: Linux i Windows, oba x86-64 (ABI System V i Microsoft x64). Jeszcze bez macOS i ARM. -- Kompilator samohostujący nie ma importu C, liczb z rozmiarem ani `defer` i nie zwalnia pamięci. +- 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, `defer`, map, metod ani błędów jako + wartości i nie zwalnia pamięci. diff --git a/docs/pl/dla-poczatkujacych.md b/docs/pl/dla-poczatkujacych.md index aac09b0..6718f54 100644 --- a/docs/pl/dla-poczatkujacych.md +++ b/docs/pl/dla-poczatkujacych.md @@ -77,7 +77,7 @@ Jeśli ostatnia linijka skończy się bez słowa `error`, Finch jest gotowy. ./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ń), -`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)`. +### 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 @@ -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` | | `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(...)` | +| `'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 } -// 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") // pytanie: imie := input("Imię? ") // 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]` | | **indeks** | Pozycja wartości na liście, liczona od 0 | | **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 | | **błąd** (error) | Komunikat, że coś jest nie tak, i gdzie | | **bug** | Pomyłka w programie | @@ -934,6 +1080,11 @@ struct Zwierzak { // własny rodzaj wartości | `input` | wejście (pytanie) | | `len` (od *length*) | długość | | `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ź | --- diff --git a/docs/pl/dla-technikow.md b/docs/pl/dla-technikow.md index 20302b8..97d7a95 100644 --- a/docs/pl/dla-technikow.md +++ b/docs/pl/dla-technikow.md @@ -20,16 +20,16 @@ kompilator działa w środku. Do tego służy [przewodnik dla inżynierów](dla- 7. [Operatory](#7-operatory) 8. [Sterowanie przebiegiem](#8-sterowanie-przebiegiem) 9. [Funkcje](#9-funkcje) -10. [Tablice](#10-tablice) +10. [Tablice i mapy](#10-tablice-i-mapy) 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) 14. [Wskaźniki](#14-wskaźniki) 15. [Funkcje wbudowane](#15-funkcje-wbudowane) 16. [Moduły](#16-moduły) 17. [Biblioteki z C](#17-biblioteki-z-c) 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) 21. [Rozwiązywanie problemów](#21-rozwiązywanie-problemów) 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 -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 @@ -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` (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 +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. ### 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 cmake -S . -B build -G Ninja ninja -C build -./build/finch version # finch 2.3.0 (LLVM 21.x) +./build/finch version # finch 2.4.0 (LLVM 21.x) ``` ### 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 ``` -### 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 -`pkgsCross.mingwW64.buildPackages.gcc`): +`--target` buduje program na inny system. Do linkowania Finch potrzebuje kompilatora C dla tego systemu: + +| `--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=` | | ```sh 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) ```sh @@ -137,12 +172,14 @@ Inny kompilator wskażesz zmienną `CC`. finch run [argumenty...] skompiluj i od razu uruchom finch build [-o nazwa] skompiluj do programu (domyślna nazwa: nazwa pliku) finch ir pokaż LLVM IR (dla ciekawskich) +finch lsp serwer języka, dla edytorów finch version pokaż wersję opcje: - -l dolinkuj bibliotekę C, to samo co link "lib" w pliku - -g dodaj informacje dla debuggera (gdb / lldb) - -O0 bez optymalizacji + -l dolinkuj bibliotekę C, to samo co link "lib" w pliku + --target zbuduj na inny system: windows, linux, arm64, macos albo triple LLVM + -g dodaj informacje dla debuggera (gdb / lldb / Visual Studio) + -O0 bez optymalizacji ``` Przykłady: @@ -226,6 +263,7 @@ x += 1 // także -= *= /= %= | `char` | jeden znak wielkości bajtu | `'A'`, `'\n'` | | `str` | tekst | `"cześć\tświecie"` | | `[]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)` | 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) 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 {`. @@ -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). - Funkcja z `-> typ` musi zwrócić wartość na każdej ścieżce. Finch to sprawdza. - 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. --- -## 10. Tablice +## 10. Tablice i mapy ```c 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`. +### 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) @@ -437,7 +524,7 @@ przekazanie go do C jako `char*` działa. --- -## 12. Struktury +## 12. Struktury i metody ```c 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. - 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. | | `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. | -| `write_file(sciezka, tekst)` | Zapisuje (zastępuje) plik; oddaje `true`, jeśli się udało. | +| `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. Z `or` / `try` porażka niesie przyczynę. | | `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. | | `exit(kod)` | Natychmiast kończy program. | | `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)). | -| `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 +### 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 ``` @@ -692,11 +856,31 @@ Zamiast niezdefiniowanego zachowania (jak w C) Finch zatrzymuje się z komunikat | `runtime error: pop() on an empty array` | | | `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 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: 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. 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 -Language Server Protocol (Neovim, Helix, Zed, Emacs, Sublime) mogą używać `finch lsp` bezpośrednio. +`finch` z PATH; jeśli jest gdzie indziej, ustaw **Finch: Path** w ustawieniach. + +### 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 @@ -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. | | 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`. | +| 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`. | | 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 │ ├── run.sh uruchamia testy (MEMCHECK=1 sprawdza też pamięć valgrindem) │ └── 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ą ├── shell.nix środowisko deweloperskie Nix └── CMakeLists.txt ``` ```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 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 ``` @@ -774,12 +966,11 @@ python3 tests/lsp_test.py # serwer języka ## 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.) -- Błędów jako wartości: błąd w `int("x")` albo `read_file` zatrzymuje program. Sprawdzaj wcześniej (`file_exists`). +- Typów generycznych (własnego `Lista[T]`), interfejsów, `match`, enumów pisanych w Finchu. - Tekstu świadomego Unicode: `.len`, `s[i]` i `upper()` działają na bajtach / ASCII. - 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). diff --git a/docs/pl/sciaga.md b/docs/pl/sciaga.md index a90241b..8f31f26 100644 --- a/docs/pl/sciaga.md +++ b/docs/pl/sciaga.md @@ -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 version` | wersja | | `-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) | | `-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 | | `f32 f64` | zmiennoprzecinkowe | | `[]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 | | `Nazwa` / `modul.Nazwa` | struktura | @@ -100,6 +101,8 @@ if a > b { … } else if a == b { … } else { … } while x > 0 { … } for i in 0..10 { … } // 0 … 9 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 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 | 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` | Kod | Daje | @@ -189,6 +250,8 @@ Parametry zachowują się jak kopie. Kolejność w pliku nie ma znaczenia. Przec | `char(66)` | `'B'` | | `bool(0)` | `false` | | `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(p)` | `char*` z C → tekst | | `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 | | `shell(polecenie)` | uruchom polecenie, jego kod wyjścia | | `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 | | `new(wartosc)` | wartość na stercie → `ptr[T]` | | `free(p)` | oddaj ją | @@ -281,11 +345,17 @@ ksztalty.Pudlo inne // w deklaracjach | `pop() on an empty array` | `pop()` na pustej tablicy | | `used .value on a null pointer` | `.value` / `.pole` / `[i]` przez `null` | | `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 | |---|---| @@ -295,7 +365,7 @@ ksztalty.Pudlo inne // w deklaracjach | najechanie myszą | typ / sygnatura / opis | | `Ctrl+Spacja` | podpowiedzi (po `.`: pola, metody, zawartość modułu) | | `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 | --- @@ -314,3 +384,7 @@ ksztalty.Pudlo inne // w deklaracjach | `import` / `link` | zaimportuj / dolinkuj | | `struct` | struktura | | `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 | diff --git a/editors/kate/finch.xml b/editors/kate/finch.xml index b13b876..9de46e1 100644 --- a/editors/kate/finch.xml +++ b/editors/kate/finch.xml @@ -5,7 +5,7 @@ Install: copy to ~/.local/share/org.kde.syntax-highlighting/syntax/finch.xml (or run editors/kate/install.sh), then restart Kate. --> - @@ -22,6 +22,8 @@ continue return defer + or + try import @@ -31,6 +33,7 @@ true false null + self int @@ -39,6 +42,7 @@ char str ptr + map i8 i16 i32 @@ -62,6 +66,7 @@ addr new free + error @@ -99,8 +104,11 @@ + + + diff --git a/editors/vscode/CHANGELOG.md b/editors/vscode/CHANGELOG.md index 31ca53b..168a8cd 100644 --- a/editors/vscode/CHANGELOG.md +++ b/editors/vscode/CHANGELOG.md @@ -1,5 +1,11 @@ # 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 - First release: highlighting, snippets, language server, Run and Build. diff --git a/editors/vscode/package-lock.json b/editors/vscode/package-lock.json index 1ff68e8..f98d8ad 100644 --- a/editors/vscode/package-lock.json +++ b/editors/vscode/package-lock.json @@ -1,12 +1,12 @@ { "name": "finch-lang", - "version": "2.3.0", + "version": "2.4.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "finch-lang", - "version": "2.3.0", + "version": "2.4.0", "license": "Apache-2.0", "dependencies": { "vscode-languageclient": "^9.0.1" diff --git a/editors/vscode/package.json b/editors/vscode/package.json index 47074ef..2d01781 100644 --- a/editors/vscode/package.json +++ b/editors/vscode/package.json @@ -2,7 +2,7 @@ "name": "finch-lang", "displayName": "Finch", "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", "license": "Apache-2.0", "icon": "images/icon.png", diff --git a/editors/vscode/snippets/finch.json b/editors/vscode/snippets/finch.json index 0a4a2b4..363076e 100644 --- a/editors/vscode/snippets/finch.json +++ b/editors/vscode/snippets/finch.json @@ -1,16 +1,160 @@ { - "main": { "prefix": "main", "body": ["fn main() {", "\t$0", "}"], "description": "The program's start" }, - "main with arguments": { "prefix": "mainargs", "body": ["fn main([]str args) {", "\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" } + "main": { + "prefix": "main", + "body": [ + "fn main() {", + "\t$0", + "}" + ], + "description": "The program's start" + }, + "main with arguments": { + "prefix": "mainargs", + "body": [ + "fn main([]str args) {", + "\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)" + } } diff --git a/editors/vscode/syntaxes/finch.tmLanguage.json b/editors/vscode/syntaxes/finch.tmLanguage.json index 2110def..3246326 100644 --- a/editors/vscode/syntaxes/finch.tmLanguage.json +++ b/editors/vscode/syntaxes/finch.tmLanguage.json @@ -85,12 +85,18 @@ } }, "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": { "1": { "name": "storage.type.function.finch" }, "2": { + "name": "entity.name.type.finch" + }, + "3": { + "name": "punctuation.accessor.finch" + }, + "4": { "name": "entity.name.function.finch" } } @@ -111,7 +117,7 @@ }, { "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", @@ -126,11 +132,15 @@ "match": "\\b(true|false|null)\\b" }, { - "name": "storage.type.primitive.finch", - "match": "\\b(int|float|bool|char|str|ptr|i8|i16|i32|i64|u8|u16|u32|u64|f32|f64)\\b" + "name": "variable.language.self.finch", + "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": { "1": { "name": "support.function.builtin.finch" @@ -238,4 +248,4 @@ ] } } -} \ No newline at end of file +} diff --git a/editors/vscode/test/grammar.js b/editors/vscode/test/grammar.js index 0aa4a4d..3d5ba77 100644 --- a/editors/vscode/test/grammar.js +++ b/editors/vscode/test/grammar.js @@ -20,6 +20,12 @@ const code = [ ' p := Point(x: 1)', ' 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] [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'], [8, 'Point', 'entity.name.type'], [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) => { let state = vsctm.INITIAL, toks = []; diff --git a/examples/wordcount.fch b/examples/wordcount.fch new file mode 100644 index 0000000..4d64c6b --- /dev/null +++ b/examples/wordcount.fch @@ -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 ") + } + 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]) + } +} diff --git a/runtime/finch_rt.c b/runtime/finch_rt.c index c87b2d1..89e6519 100644 --- a/runtime/finch_rt.c +++ b/runtime/finch_rt.c @@ -8,9 +8,11 @@ // cap == 0 : a string literal, valid forever, never written to // cap == -1: borrowed from C, must be copied before it is kept // []T = { T *ptr; int64 len; int64 cap } owns ptr when cap > 0 +// map[K]V: see "maps" below #include #include +#include #include #include #include @@ -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"); } -static void bad_number(const FStr *s, const char *kind, const char *file, int64_t line) { - char msg[200]; - 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); +static void number_error(char *msg, size_t size, const FStr *s, const char *kind) { + snprintf(msg, size, "can't turn \"%.*s\" into %s", (int)(s->len > 100 ? 100 : s->len), s->ptr ? s->ptr : "", kind); } -int64_t finch_str_to_int(const FStr *s, const char *file, int64_t line) { - const char *p = s->ptr; +static int parse_int(const FStr *s, int64_t *out) { + const char *p = s->ptr ? s->ptr : ""; while (isspace((unsigned char)*p)) p++; char *end; errno = 0; long long v = strtoll(p, &end, 10); while (isspace((unsigned char)*end)) end++; - if (end == p || *end || errno == ERANGE) bad_number(s, "int", file, line); - return v; + *out = v; + return !(end == p || *end || errno == ERANGE); } -double finch_str_to_float(const FStr *s, const char *file, int64_t line) { - const char *p = s->ptr; +static int parse_float(const FStr *s, double *out) { + const char *p = s->ptr ? s->ptr : ""; while (isspace((unsigned char)*p)) p++; char *end; double v = strtod(p, &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; } +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) { if (start < 0 || end > s->len || start > end) { 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); } +// ---------- 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 ---------- 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; } +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) { - FILE *f = fopen(path->ptr, "rb"); + FILE *f = fopen(path->ptr ? path->ptr : "", "rb"); if (!f) { char msg[300]; 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_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 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) { - FILE *f = fopen(path->ptr, "wb"); + FILE *f = fopen(path->ptr ? path->ptr : "", "wb"); 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; } + +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); +} diff --git a/src/ast.h b/src/ast.h index b996a50..87c3828 100644 --- a/src/ast.h +++ b/src/ast.h @@ -11,12 +11,14 @@ struct Type { Void, Bool, Char, I8, I16, I32, I64, U8, U16, U32, U64, F32, F64, Str, Ptr, Null, 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 Struct, // a resolved struct Named, // a struct name as written, before codegen resolves it }; Kind kind = Void; - std::shared_ptr elem; // Ptr (null = untyped `ptr`), Array, Fixed + std::shared_ptr elem; // Ptr (null = untyped `ptr`), Array, Fixed; Map: the value + std::shared_ptr key; // Map long long count = 0; // Fixed std::string name; // Named / Struct std::string module; // Named: written qualifier ("" = none); Struct: defining module @@ -34,6 +36,12 @@ struct Type { a.elem = std::make_shared(t); return a; } + static Type mapOf(const Type &k, const Type &v) { + Type m(Map); + m.key = std::make_shared(k); + m.elem = std::make_shared(v); + return m; + } bool isInt() const { return kind >= I8 && kind <= U64; } bool isSigned() const { return kind >= I8 && kind <= I64; } @@ -58,6 +66,7 @@ struct Type { if (!elem || !o.elem) return !elem && !o.elem; 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 Struct: return info == o.info; case Named: return name == o.name && module == o.module; @@ -86,6 +95,7 @@ struct Type { case Ptr: return elem ? "ptr[" + elem->show() + "]" : "ptr"; case Null: return "null"; case Array: return "[]" + elem->show(); + case Map: return "map[" + key->show() + "]" + elem->show(); case Fixed: return "[" + std::to_string(count) + "]" + elem->show(); case Struct: case Named: return module.empty() ? name : module + "." + name; @@ -104,7 +114,8 @@ struct Pos { // ---------- 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 { ExprKind kind; @@ -179,7 +190,11 @@ struct ArrayLitExpr : Expr { // [1, 2, 3] std::vector elems; 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 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 { ExprPtr obj; 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)) {} }; +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 block; // or a block that sees `err` + OrElseExpr(Pos p, ExprPtr c) : Expr(ExprKind::OrElse, p), call(std::move(c)) {} + ~OrElseExpr() override; +}; + // ---------- statements ---------- 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) {} }; using BlockPtr = std::unique_ptr; +inline OrElseExpr::~OrElseExpr() = default; struct VarDeclStmt : Stmt { bool hasType; // `int x = 5` vs `x := 5` @@ -246,7 +277,9 @@ struct ForStmt : Stmt { struct ForEachStmt : Stmt { std::string var; 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; ForEachStmt(Pos p) : Stmt(StmtKind::ForEach, p) {} }; @@ -278,7 +311,10 @@ struct FnDecl { Pos pos; Pos namePos; 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` + bool fallible = false; // -> int! / -> ! : can fail with an error std::vector params; BlockPtr body; }; diff --git a/src/builtins_doc.h b/src/builtins_doc.h index 6e82aa3..05d2687 100644 --- a/src/builtins_doc.h +++ b/src/builtins_doc.h @@ -11,18 +11,19 @@ struct BuiltinDoc { inline const BuiltinDoc kBuiltins[] = { {"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."}, - {"read_file", "read_file(str path) -> str", "The whole file as text. Stops the program if it can't be read."}, - {"write_file", "write_file(str path, str text) -> bool", "Writes (replaces) the file. true if it worked."}, + {"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; with or / try, the reason it didn't."}, {"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."}, {"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."}, {"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."}, {"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)."}, - {"float", "float(x) -> float", "Converts to a decimal number; parses text."}, + {"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 (int(s) or 0.0 handles bad text)."}, {"char", "char(int n) -> char", "The character with this code."}, {"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)."}, @@ -62,8 +63,18 @@ inline const BuiltinDoc kStrMethods[] = { {"bytes", "bytes() -> []u8", "The bytes of the text."}, }; -inline const char *kKeywords[] = {"fn", "return", "if", "else", "while", "for", "in", "break", "continue", "true", - "false", "null", "import", "link", "struct", "defer"}; +inline const BuiltinDoc kMapMethods[] = { + {"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"}; diff --git a/src/codegen.cpp b/src/codegen.cpp index e07e490..aac0585 100644 --- a/src/codegen.cpp +++ b/src/codegen.cpp @@ -39,6 +39,8 @@ std::unique_ptr Codegen::run() { if (!modules[""].fns.count("main")) failAt(0, 1, 1, "there is no main function (add 'fn main() { ... }')"); for (auto &[name, m] : modules) for (auto &[fname, fn] : m.fns) define(fn); + for (auto &[s, ms] : methods) + for (auto &[mname, fn] : ms) define(fn); defineMainWrapper(); if (di) di->finalize(); @@ -102,19 +104,72 @@ void Codegen::declareStructs() { void Codegen::declareFns() { for (const Program &p : progs) 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) { ModuleScope &m = modules[p.module]; 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; + 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; f.decl = &fn; f.module = p.module; + f.recv = recv; + f.fallible = fn.fallible; f.ret = resolve(fn.ret, 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 params; - 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)); - } + if (recv) params.push_back(b.getPtrTy()); // self + for (size_t i = 0; i < f.params.size(); i++) params.push_back(ty(f.params[i])); // User functions get a prefix so they never clash with C library names. - std::string name = "finch." + (p.module.empty() ? "" : p.module + ".") + fn.name; - f.llvm = Function::Create(FunctionType::get(ty(f.ret), params, false), Function::InternalLinkage, name, mod.get()); - m.fns[fn.name] = f; + std::string name = "finch." + (p.module.empty() ? "" : p.module + ".") + (recv ? recv->name + "." : "") + fn.name; + f.llvm = Function::Create(FunctionType::get(retType(f), params, false), Function::InternalLinkage, name, mod.get()); + 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. @@ -155,6 +209,17 @@ void Codegen::defineMainWrapper() { } Value *r = b.CreateCall(fm.llvm, args); 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)); else b.CreateRet(b.CreateIntCast(r, b.getInt32Ty(), true)); } @@ -164,7 +229,8 @@ void Codegen::define(Fn &fn) { curFn = &fn; curModule = fn.module; 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); g_index->fns.push_back(fi); note(d.namePos, d.name.size(), "```finch\n" + signature(fn) + "\n```", d.namePos); @@ -186,12 +252,33 @@ void Codegen::define(Fn &fn) { varOrder = 0; pushScope(); 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 FType &t = fn.params[i]; 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 - else addVar(p.namePos, p.name, t, &arg, false, false, i + 1); + unsigned argNo = i + 1 + (fn.recv ? 1 : 0); + 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++; } blockBody(*d.body); @@ -199,7 +286,7 @@ void Codegen::define(Fn &fn) { 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()); emitCleanups(0); - b.CreateRetVoid(); + retValue(nullptr); } scopes.clear(); diFn = nullptr; @@ -315,7 +402,11 @@ void Codegen::stmt(const Stmt &s) { case StmtKind::Block: block(static_cast(s)); break; case StmtKind::VarDecl: varDecl(static_cast(s)); break; case StmtKind::Assign: assign(static_cast(s)); break; - case StmtKind::Expr: release(expr(*static_cast(s).expr)); break; + case StmtKind::Expr: { + const Expr &x = *static_cast(s).expr; + release(x.kind == ExprKind::OrElse ? orElse(static_cast(x), true) : expr(x)); + break; + } case StmtKind::If: ifStmt(static_cast(s)); break; case StmtKind::While: whileStmt(static_cast(s)); break; case StmtKind::For: forStmt(static_cast(s)); break; @@ -486,7 +577,8 @@ void Codegen::forStmt(const ForStmt &s) { 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) { pushScope(); LRef r = ref(*s.list); @@ -500,9 +592,50 @@ void Codegen::forEachStmt(const ForEachStmt &s) { lt = r.val.type; 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) 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; // 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); @@ -531,8 +664,16 @@ void Codegen::forEachStmt(const ForEachStmt &s) { Value *elem = b.CreateLoad(ty(et), elemAddr); loops.push_back({step, end, scopes.size()}); pushScope(); - Var &xv = addVar(s.varPos, s.var, et, copyElems ? copyValue(elem, et) : elem, copyElems, true); - 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])"; + std::string elemVar = s.var; + 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); popScope(); loops.pop_back(); @@ -546,14 +687,46 @@ void Codegen::forEachStmt(const ForEachStmt &s) { 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) { const FnDecl &d = *curFn->decl; 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(*s.value).callee == "error") { + auto &c = static_cast(*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 (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')"); emitCleanups(0); - b.CreateRetVoid(); + retValue(nullptr); return; } 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; emitCleanups(0); scopes[idx].vars[name].moved = false; - b.CreateRet(v); + retValue(v); return; } 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 *out = own(v); emitCleanups(0); - b.CreateRet(out); + retValue(out); } // ---------- 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) { static const std::set changing = {"push", "pop", "insert", "remove", "clear", "resize", "sort", "reverse"}; switch (e.kind) { + case ExprKind::Try: return mutatesExpr(name, *static_cast(e).call); + case ExprKind::OrElse: { + auto &x = static_cast(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(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(e).operand); case ExprKind::Binary: { auto &x = static_cast(e); @@ -618,7 +802,7 @@ bool Codegen::mutatesExpr(const std::string &name, const Expr &e) { return false; case ExprKind::Method: { auto &m = static_cast(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; for (auto &a : m.args) 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) { if (e.kind == ExprKind::ArrayLit) return arrayLit(static_cast(e), &want); + if (e.kind == ExprKind::MapLit) return mapLit(static_cast(e), &want); 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 / ... 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 )"); + 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) { switch (e.kind) { case ExprKind::Int: return {b.getInt64(static_cast(e).value), FType::I64, true}; @@ -711,6 +982,9 @@ Value_ Codegen::expr(const Expr &e) { case ExprKind::Call: return call(static_cast(e)); case ExprKind::Method: return method(static_cast(e)); case ExprKind::ArrayLit: return arrayLit(static_cast(e), nullptr); + case ExprKind::MapLit: return mapLit(static_cast(e), nullptr); + case ExprKind::Try: return tryExpr(static_cast(e)); + case ExprKind::OrElse: return orElse(static_cast(e)); } return {nullptr, FType::Void}; } @@ -785,6 +1059,19 @@ LRef Codegen::member(const MemberExpr &m, bool forWrite) { 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 (m.field == "len" || m.field == "ptr") 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; LRef obj = ref(*e.obj, forWrite); 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; 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}}; } 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; 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"); @@ -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 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; 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; } diff --git a/src/codegen_builtins.cpp b/src/codegen_builtins.cpp index 2895dfd..caa8c46 100644 --- a/src/codegen_builtins.cpp +++ b/src/codegen_builtins.cpp @@ -9,7 +9,7 @@ namespace finch { bool Codegen::isBuiltin(const std::string &n) { static const std::set 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; return names.count(n) || typeFromName(n, t); } @@ -95,6 +95,28 @@ Function *Codegen::rt(const std::string &name) { {"finch_shell", {I, {P}}}, {"finch_delete_file", {I32, {P}}}, {"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); 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, 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::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; } @@ -185,6 +207,14 @@ Value_ Codegen::call(const CallExpr &c) { 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") { checkArgs(c.args, c.argNames, 0, p, n, true); 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 *ss = tmpOf(ty(FType::Str)); 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}); dropAt(ss, FType::Str); release(path); @@ -248,6 +285,10 @@ Value_ Codegen::call(const CallExpr &c) { Value_ res; if (n == "file_exists") { 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 { Value *out = tmpOf(ty(FType::Str)); 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); 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"); + 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)}); release(path); release(text); @@ -277,13 +325,13 @@ Value_ Codegen::call(const CallExpr &c) { FType target; if (typeFromName(n, target)) { 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]; 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); - 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)) { 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 &args, const std::vector &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; - 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 , " + shown + + "(...) or { ... }, or try " + shown + "(...)"); checkArgs(args, names, fn.params.size(), p, shown); std::vector vals; std::vector fresh; + if (self) vals.push_back(self); 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 + "'"); + // 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 if (v.fresh) fresh.push_back(v); } setLoc(p); Value *r = b.CreateCall(fn.llvm, vals); 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}; return {r, fn.ret, false, owning(fn.ret)}; } @@ -433,7 +494,7 @@ Value_ Codegen::addrOf(const std::vector &args, Pos p) { } // 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 &args, Pos, const std::string &name) { +Value_ Codegen::convert(const FType &to, const std::vector &args, Pos, const std::string &name, bool canFail) { Value_ v = expr(*args[0]); const FType &from = v.type; Pos p = args[0]->pos; @@ -449,12 +510,28 @@ Value_ Codegen::convert(const FType &to, const std::vector &args, Pos, 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)}); 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 { 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}; } 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())) { Value *sp = tmp(v.v); Value_ r; @@ -494,7 +571,7 @@ Value_ Codegen::method(const MethodExpr &m) { Pos namePos{p.line, p.col + 1, p.file}; 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); - 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()) { 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)"); } static const std::set 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; + 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)) { auto look = [&](const BuiltinDoc *first, const BuiltinDoc *last) { for (; first != last; ++first) @@ -589,11 +678,47 @@ Value_ Codegen::method(const MethodExpr &m) { if (!r.isPlace) release(sv); 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"); } +// 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) static bool comparable(const FType &t) { return t.isNumber() || t.kind == FType::Char || t.kind == FType::Bool || t.kind == FType::Str; diff --git a/src/codegen_impl.h b/src/codegen_impl.h index ea1959d..b87ea9f 100644 --- a/src/codegen_impl.h +++ b/src/codegen_impl.h @@ -80,6 +80,9 @@ struct Fn { llvm::Function *cThunk = nullptr; FType ret; // resolved types std::vector 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 { @@ -139,6 +142,13 @@ public: int deferLimit = -1; // while emitting a defer: hide variables declared after it int inDefer = 0; std::map helpers; // drop/copy/print per type + std::map> methods; + std::set 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) bool debug; @@ -164,6 +174,9 @@ public: void forStmt(const ForStmt &s); void forEachStmt(const ForEachStmt &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); llvm::Value *condition(const Expr &e); void continueAt(llvm::BasicBlock *bb, llvm::BasicBlock *deadEnd); @@ -194,6 +207,10 @@ public: LRef member(const MemberExpr &m, bool forWrite); LRef index(const IndexExpr &e, bool forWrite); 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); bool mutates(const std::string &name, const Stmt &s); bool mutatesExpr(const std::string &name, const Expr &e); @@ -225,7 +242,9 @@ public: std::string typeKey(const FType &t); 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); - 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 args); llvm::Value *strConst(const std::string &s); llvm::Value *cstr(llvm::Value *strValue); // str -> char* for C @@ -236,13 +255,26 @@ public: Value_ call(const CallExpr &c); Value_ method(const MethodExpr &m); Value_ callFinch(const Fn &fn, const std::vector &args, const std::vector &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 &args, const std::vector &names, Pos p); Value_ callC(const CFunc &f, const std::vector &args, Pos p); - Value_ convert(const FType &to, const std::vector &args, Pos p, const std::string &name); + Value_ convert(const FType &to, const std::vector &args, Pos p, const std::string &name, bool canFail = false); Value_ print(const std::vector &args, Pos p); Value_ addrOf(const std::vector &args, Pos p); 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 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 &body); + llvm::Value *keyText(const Value_ &key); bool isBuiltin(const std::string &n); std::string headerHint(const std::string &name); void checkArgs(const std::vector &args, const std::vector &names, size_t want, Pos p, diff --git a/src/codegen_map.cpp b/src/codegen_map.cpp new file mode 100644 index 0000000..d404fd2 --- /dev/null +++ b/src/codegen_map.cpp @@ -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 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 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 &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(y)->op == '-') y = static_cast(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 diff --git a/src/codegen_types.cpp b/src/codegen_types.cpp index 9705834..311675c 100644 --- a/src/codegen_types.cpp +++ b/src/codegen_types.cpp @@ -25,6 +25,10 @@ llvm::Type *Codegen::ty(const FType &t) { case FType::F64: return b.getDoubleTy(); case FType::Str: 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::Null: return b.getPtrTy(); 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; return FType::ptrTo(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: { FType f = t; f.elem = std::make_shared(resolveT(*t.elem, p, deep)); @@ -136,7 +148,8 @@ void Codegen::resolveStruct(StructInfo *s) { bool Codegen::owning(const FType &t) { switch (t.kind) { case FType::Str: - case FType::Array: return true; + case FType::Array: + case FType::Map: return true; case FType::Struct: resolveStruct(t.info); return t.info->owning; @@ -279,6 +292,7 @@ void Codegen::forN(Value *n, const std::function &body) { std::string Codegen::typeKey(const FType &t) { switch (t.kind) { 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::Ptr: return "ptr"; 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); }); } 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) { for (auto &fl : t.info->fields) 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); }); } + } 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) { b.CreateStore(b.CreateLoad(ty(t), src), dst); 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::Str) 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... - Value *isNull = b.CreateIsNull(v); - BasicBlock *nul = newBlock("ptr.null"), *addr = newBlock("ptr.addr"), *done = newBlock("ptr.done"); - b.CreateCondBr(isNull, nul, addr); - 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 { // pointers, arrays, maps, structs: built as text first (the same text str(x) gives) + Value *buf = tmp(zero(FType::Str)); + emitFormat(buf, v, t, quoted); + b.CreateCall(rt("finch_buf_print"), {buf}); } - else if (t.kind == FType::Null) printf_("null", {}); - else b.CreateCall(printFn(t), {tmp(v)}); } -Function *Codegen::printFn(const FType &t) { - std::string key = "print." + typeKey(t); +void Codegen::bufText(Value *buf, const std::string &text) { + 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; - Function *f = Function::Create(FunctionType::get(b.getVoidTy(), {b.getPtrTy()}, false), Function::InternalLinkage, - "finch." + key, mod.get()); + Function *f = Function::Create(FunctionType::get(b.getVoidTy(), {b.getPtrTy(), b.getPtrTy()}, false), + Function::InternalLinkage, "finch." + key, mod.get()); helpers[key] = 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) { FType et = *t.elem; Value *data, *len; @@ -422,26 +461,44 @@ Function *Codegen::printFn(const FType &t) { data = b.CreateLoad(b.getPtrTy(), b.CreateStructGEP(ty(t), a, 0)); len = b.CreateLoad(b.getInt64Ty(), b.CreateStructGEP(ty(t), a, 1)); } - printf_("[", {}); + bufText(buf, "["); forN(len, [&](Value *i) { BasicBlock *sep = newBlock("sep"), *item = newBlock("item"); b.CreateCondBr(b.CreateICmpSGT(i, b.getInt64(0)), sep, item); b.SetInsertPoint(sep); - printf_(", ", {}); + bufText(buf, ", "); b.CreateBr(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) { - printf_(t.info->name + "(", {}); + bufText(buf, t.info->name + "("); bool first = true; for (auto &fl : t.info->fields) { - printf_((first ? "" : ", ") + fl.name + ": ", {}); + if (fl.hidden) continue; + bufText(buf, (first ? "" : ", ") + fl.name + ": "); 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(); 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)); 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 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: { StructInfo *s = t.info; DIFile *f = s->isC ? diFiles[0] : diFiles[s->pos.file]; diff --git a/src/lexer.cpp b/src/lexer.cpp index 30d2874..ffe367f 100644 --- a/src/lexer.cpp +++ b/src/lexer.cpp @@ -10,7 +10,7 @@ static const std::unordered_map keywords = { {"in", Tok::In}, {"break", Tok::Break}, {"continue", Tok::Continue}, {"true", Tok::True}, {"false", Tok::False}, {"null", Tok::Null}, {"import", Tok::Import}, {"link", Tok::Link}, {"struct", Tok::Struct}, - {"defer", Tok::Defer}, + {"defer", Tok::Defer}, {"or", Tok::OrElse}, {"try", Tok::Try}, }; std::vector lex(int file) { @@ -169,6 +169,8 @@ const char *tokName(Tok t) { case Tok::Link: return "'link'"; case Tok::Struct: return "'struct'"; case Tok::Defer: return "'defer'"; + case Tok::OrElse: return "'or'"; + case Tok::Try: return "'try'"; case Tok::Colon: return "':'"; case Tok::LParen: return "'('"; case Tok::RParen: return "')'"; diff --git a/src/lexer.h b/src/lexer.h index 4404e82..23761b8 100644 --- a/src/lexer.h +++ b/src/lexer.h @@ -9,6 +9,7 @@ enum class Tok { // keywords Fn, Return, If, Else, While, For, In, Break, Continue, True, False, Null, Import, Link, Struct, Defer, + OrElse, Try, // or, try // punctuation LParen, RParen, LBrace, RBrace, LBracket, RBracket, Comma, Colon, Dot, DotDot, Arrow, diff --git a/src/lsp.cpp b/src/lsp.cpp index ff3a306..938b0b0 100644 --- a/src/lsp.cpp +++ b/src/lsp.cpp @@ -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 (element && type.rfind("[]", 0) == 0) type = type.substr(2); 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 == "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)); } 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)); + } 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()) { 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); for (const StructIndex &s2 : a->index.structs) if (s2.name == sname && (mod.empty() || s2.module == mod)) for (auto &[fname, ftype] : s2.fields) items.push_back(item(fname, KField, ftype + " " + fname)); + std::set 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")); } 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::vector params; if (a) { - for (const FnInfo &f : a->index.fns) - if (f.name == name && (f.module == mod || (mod.empty() && f.module.empty()) || !a->modules.count(mod))) + for (const FnInfo &f : a->index.fns) { + 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; + } if (label.empty()) for (const StructIndex &s : a->index.structs) if (s.name == name) { @@ -544,6 +564,7 @@ json::Value Server::signatureHelp(const std::string &path, int line, int col) { if (!mod.empty()) { look(std::begin(kArrayMethods), std::end(kArrayMethods)); look(std::begin(kStrMethods), std::end(kStrMethods)); + look(std::begin(kMapMethods), std::end(kMapMethods)); } } if (label.empty()) return nullptr; diff --git a/src/main.cpp b/src/main.cpp index d886d0d..73ec72c 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -39,10 +39,11 @@ static void usage() { " finch build [-o name] compile to a program\n" " finch ir show the generated LLVM IR\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" " -l link a C library, same as link \"lib\" in the file\n" - " --target build for another system: windows, linux, or an LLVM triple\n" + " --target build for another system: windows, linux, arm64 (Linux), macos,\n" + " or an LLVM triple\n" " -g add debug info (gdb / lldb / Visual Studio)\n" " -O0 skip optimizations (to read the raw IR)\n"); std::exit(1); diff --git a/src/parser.cpp b/src/parser.cpp index 58e7c45..4a76616 100644 --- a/src/parser.cpp +++ b/src/parser.cpp @@ -79,13 +79,20 @@ private: // 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; } - // int, u8, ptr, ptr[int], []str, Point, math.Vec ... + // int, u8, ptr, ptr[int], []str, map[str]int, Point, math.Vec ... Type type() { if (accept(Tok::LBracket)) { expect(Tok::RBracket, "']' (an array type looks like []int)"); return Type::arrayOf(type()); } 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; Type t; if (typeFromName(name, t)) { @@ -111,7 +118,7 @@ private: if (!at(Tok::Ident)) return false; Tok n = peekTok().kind; 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 && !peekTok(3).newlineBefore; } @@ -179,6 +186,12 @@ private: fn.namePos = pos(); 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); parenDepth++; if (!at(Tok::RParen)) { @@ -193,7 +206,12 @@ private: } parenDepth--; 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(); return fn; } @@ -236,8 +254,15 @@ private: next(); Pos varPos = pos(); 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); 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 auto s = std::make_unique(p); s->var = var; @@ -250,6 +275,8 @@ private: auto s = std::make_unique(p); // for x in list s->var = var; s->varPos = varPos; + s->var2 = var2; + s->var2Pos = var2Pos; s->list = std::move(first); s->body = block(); return s; @@ -323,7 +350,7 @@ private: 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"); return std::make_unique(p, std::move(e)); } @@ -342,7 +369,17 @@ private: // ---------- 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(p, std::move(l)); + if (at(Tok::LBrace)) e->block = block(); + else e->fallback = expr(); + return e; + } ExprPtr orExpr() { ExprPtr l = andExpr(); @@ -429,6 +466,7 @@ private: if (accept(Tok::Minus)) return std::make_unique(p, '-', unary()); if (accept(Tok::Not)) return std::make_unique(p, '!', unary()); if (accept(Tok::Tilde)) return std::make_unique(p, '~', unary()); + if (accept(Tok::Try)) return std::make_unique(p, postfix()); return postfix(); } @@ -506,14 +544,37 @@ private: expect(Tok::RParen); return e; } - case Tok::LBracket: { // [1, 2, 3] + case Tok::LBracket: { // [1, 2, 3] or a map: ["a": 1, "b": 2], [:] next(); - auto a = std::make_unique(p); parenDepth++; - while (!at(Tok::RBracket)) { - a->elems.push_back(expr()); - if (!accept(Tok::Comma)) break; // a trailing comma is fine + if (accept(Tok::Colon)) { + parenDepth--; + expect(Tok::RBracket, "']' (an empty map is [:])"); + return std::make_unique(p); } + if (!at(Tok::RBracket)) { + ExprPtr first = expr(); + if (accept(Tok::Colon)) { + auto m = std::make_unique(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(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(p); parenDepth--; expect(Tok::RBracket); return a; diff --git a/tests/boot.sh b/tests/boot.sh index 4e1b969..1c63512 100755 --- a/tests/boot.sh +++ b/tests/boot.sh @@ -21,11 +21,12 @@ fi pass=0; failed=0; skipped=0 for f in tests/run/*.fch; do 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) files="$f" 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)) continue fi diff --git a/tests/fail/error_not_fallible.fch b/tests/fail/error_not_fallible.fch new file mode 100644 index 0000000..dbcdf43 --- /dev/null +++ b/tests/fail/error_not_fallible.fch @@ -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) +} diff --git a/tests/fail/error_outside_return.fch b/tests/fail/error_outside_return.fch new file mode 100644 index 0000000..20395a4 --- /dev/null +++ b/tests/fail/error_outside_return.fch @@ -0,0 +1,4 @@ +// expect: error(...) makes an error to return from a function that can fail +fn main() { + e := error("oops") +} diff --git a/tests/fail/map_empty_literal.fch b/tests/fail/map_empty_literal.fch new file mode 100644 index 0000000..9f19c87 --- /dev/null +++ b/tests/fail/map_empty_literal.fch @@ -0,0 +1,4 @@ +// expect: an empty [:] needs a type, like: map[str]int m = [:] +fn main() { + m := [:] +} diff --git a/tests/fail/map_float_key.fch b/tests/fail/map_float_key.fch new file mode 100644 index 0000000..e11b2f8 --- /dev/null +++ b/tests/fail/map_float_key.fch @@ -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) +} diff --git a/tests/fail/map_missing_key.fch b/tests/fail/map_missing_key.fch new file mode 100644 index 0000000..20c227f --- /dev/null +++ b/tests/fail/map_missing_key.fch @@ -0,0 +1,5 @@ +// expect: the key "zoe" is not in the map +fn main() { + ages := ["anna": 31] + print(ages["zoe"]) +} diff --git a/tests/fail/method_field_clash.fch b/tests/fail/method_field_clash.fch new file mode 100644 index 0000000..90a9237 --- /dev/null +++ b/tests/fail/method_field_clash.fch @@ -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() { +} diff --git a/tests/fail/method_loop_readonly.fch b/tests/fail/method_loop_readonly.fch new file mode 100644 index 0000000..0f1d04f --- /dev/null +++ b/tests/fail/method_loop_readonly.fch @@ -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() + } +} diff --git a/tests/fail/method_missing.fch b/tests/fail/method_missing.fch new file mode 100644 index 0000000..3253156 --- /dev/null +++ b/tests/fail/method_missing.fch @@ -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()) +} diff --git a/tests/fail/or_block_value.fch b/tests/fail/or_block_value.fch new file mode 100644 index 0000000..1849c1f --- /dev/null +++ b/tests/fail/or_block_value.fch @@ -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) +} diff --git a/tests/fail/or_cannot_fail.fch b/tests/fail/or_cannot_fail.fch new file mode 100644 index 0000000..4672bfa --- /dev/null +++ b/tests/fail/or_cannot_fail.fch @@ -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) +} diff --git a/tests/fail/try_outside_fallible.fch b/tests/fail/try_outside_fallible.fch new file mode 100644 index 0000000..38f0271 --- /dev/null +++ b/tests/fail/try_outside_fallible.fch @@ -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")) +} diff --git a/tests/fail/unhandled_error.fch b/tests/fail/unhandled_error.fch new file mode 100644 index 0000000..a23f244 --- /dev/null +++ b/tests/fail/unhandled_error.fch @@ -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")) +} diff --git a/tests/lsp_test.py b/tests/lsp_test.py index aec0e9f..25ead42 100755 --- a/tests/lsp_test.py +++ b/tests/lsp_test.py @@ -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}]}) 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.notify("exit", None) c.p.wait(timeout=5) diff --git a/tests/run.sh b/tests/run.sh index 4cce245..fd4d0af 100755 --- a/tests/run.sh +++ b/tests/run.sh @@ -27,7 +27,8 @@ for f in run/*.fch; do if [ "${MEMCHECK:-0}" = 1 ]; then exe="$tmp/$(basename "${f%.fch}")" "$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)) echo "MEMCHECK FAIL $f" head -20 "$tmp/vg" diff --git a/tests/run/errors.fch b/tests/run/errors.fch new file mode 100644 index 0000000..d6d0239 --- /dev/null +++ b/tests/run/errors.fch @@ -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") +} diff --git a/tests/run/errors.out b/tests/run/errors.out new file mode 100644 index 0000000..3576ea5 --- /dev/null +++ b/tests/run/errors.out @@ -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 diff --git a/tests/run/geometry.fch b/tests/run/geometry.fch index 6a89699..093609a 100644 --- a/tests/run/geometry.fch +++ b/tests/run/geometry.fch @@ -16,3 +16,8 @@ fn add(Vec a, Vec b) -> Vec { fn describe(Vec v) -> str { 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) +} diff --git a/tests/run/maps.fch b/tests/run/maps.fch new file mode 100644 index 0000000..7107b4e --- /dev/null +++ b/tests/run/maps.fch @@ -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) + } +} diff --git a/tests/run/maps.out b/tests/run/maps.out new file mode 100644 index 0000000..68713e9 --- /dev/null +++ b/tests/run/maps.out @@ -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 diff --git a/tests/run/methods.fch b/tests/run/methods.fch new file mode 100644 index 0000000..7568294 --- /dev/null +++ b/tests/run/methods.fch @@ -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()) +} diff --git a/tests/run/methods.out b/tests/run/methods.out new file mode 100644 index 0000000..725c386 --- /dev/null +++ b/tests/run/methods.out @@ -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