diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 1c85d92..f173eb8 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -1,8 +1,8 @@ name: CI # The compiler is written in Finch. It is built from bootstrap/.ll, its own LLVM IR (see bootstrap/README.md), -# then from itself. Seeds: Linux x86-64 and ARM64 (tested here), Windows x86-64 (tested under Wine, and here), -# macOS ARM64 and x86-64 (made with Apple's headers; only checked as IR until the macos job below passes). +# then from itself, and tested, on every system it runs on: Linux x86-64 and ARM64, macOS ARM64 and x86-64, Windows x86-64. +# A tag v* also makes the release: every package below is attached to it. on: push: @@ -75,7 +75,14 @@ jobs: fi macos: - runs-on: macos-15 + strategy: + fail-fast: false + matrix: + include: + - { os: macos-15, arch: arm64 } + - { os: macos-15-intel, arch: x64 } + name: macos-${{ matrix.arch }} + runs-on: ${{ matrix.os }} steps: - uses: actions/checkout@v4 - name: Install LLVM 21 (libclang) @@ -92,11 +99,11 @@ jobs: - name: Package run: | mkdir -p dist/finch && cp build/finch README.md LICENSE dist/finch/ && cp -r runtime std examples dist/finch/ - tar -czf finch-macos-arm64.tar.gz -C dist finch + tar -czf finch-macos-${{ matrix.arch }}.tar.gz -C dist finch - uses: actions/upload-artifact@v4 with: - name: finch-macos-arm64 - path: finch-macos-arm64.tar.gz + name: finch-macos-${{ matrix.arch }} + path: finch-macos-${{ matrix.arch }}.tar.gz windows: runs-on: windows-2022 @@ -154,3 +161,27 @@ jobs: with: name: finch-vscode path: editors/vscode/finch-lang.vsix + + release: + if: startsWith(github.ref, 'refs/tags/v') + needs: [linux, cross, macos, windows, vscode] + runs-on: ubuntu-24.04 + permissions: + contents: write + steps: + - uses: actions/download-artifact@v4 + with: + path: dist + merge-multiple: true + - name: Attach the packages to the release (made if it isn't there yet) + uses: softprops/action-gh-release@v2 + with: + name: Finch ${{ github.ref_name }} + generate_release_notes: true + files: | + dist/finch-linux-x64.tar.gz + dist/finch-linux-arm64.tar.gz + dist/finch-macos-arm64.tar.gz + dist/finch-macos-x64.tar.gz + dist/finch-windows-x64.zip + dist/finch-lang.vsix diff --git a/README.md b/README.md index 7f62239..938d072 100644 --- a/README.md +++ b/README.md @@ -40,6 +40,8 @@ fn main() { 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. +- **SIMD built in.** `f32x8`, `i8x32`, `f64x4`: numbers side by side, added and multiplied all at once. LLVM turns them + into SSE, AVX2, AVX-512 or NEON instructions, so one piece of code is fast on a desktop, a laptop and ARM. - **Errors that talk like a person:** ``` game.fch:3:13: error: 'y' must be i32, but this is int (use i32(...) to convert) @@ -48,13 +50,17 @@ fn main() { ``` - **Any C library, directly.** `import "raylib.h"` + `link "raylib"`, and you can call it, structs by value and callbacks included. Headers are read by libclang, so there is nothing to declare by hand. -- **Written in itself.** The compiler is Finch code, and nothing else: `src/*.fch`, about 8.6k lines. It is built from a - seed (its own LLVM IR, `bootstrap/`) and then from itself, and the two come out identical. Its first version, in C++, - is kept on the `before-bootstrap` branch. +- **Written in itself.** The compiler is Finch code, and nothing else: `src/*.fch`, about 12k lines. It is built from a + seed (its own LLVM IR, `bootstrap/`) and then from itself, and the two come out identical. ## Quick start -**Building it** (Linux x86-64 or ARM64; needs `clang` and libclang, LLVM 21): +**Ready builds** for Linux (x64, ARM64), macOS (Apple Silicon, Intel) and Windows (x64) are in the +[releases](https://github.com/Nexoniarz/Finch/releases). Unpack one and keep `runtime/` and `std/` next to `finch`. What +each system needs besides is in the [technicians' guide](docs/en/for-technicians.md#2-installing): clang and libclang +from LLVM 21 on Linux, `brew install llvm@21` on a Mac, llvm-mingw on Windows. + +**Building it yourself** (on Linux, macOS, or Windows with Git Bash; needs `clang` and libclang, LLVM 21): ```sh git clone https://github.com/Nexoniarz/Finch.git @@ -64,13 +70,12 @@ bootstrap/build.sh # the compiler is build/finch: built from th ./build/finch run examples/hello.fch ./build/finch build examples/tour.fch -o tour && ./tour -tests/run.sh # every program, every refusal, -g, a shared library, the language server +tests/run.sh # every program, every expected error, -g, a shared library, the language server ``` -Ready builds of the compiler are in the [releases](https://github.com/Nexoniarz/Finch/releases) (Linux x64 and ARM64: -unpack, keep `runtime/` and `std/` next to `finch`). **Programs for Windows, ARM64 and macOS** are built from Linux with -`--target windows|arm64|macos` (see the [guide](docs/en/for-technicians.md#3-the-finch-command)); the compiler itself does not run on -macOS or Windows yet (it needs a seed made with that system's headers: see [bootstrap/README.md](bootstrap/README.md)). +The same `bootstrap/build.sh` works on a Mac (with Homebrew's `llvm@21`) and on Windows (with llvm-mingw): see +[bootstrap/README.md](bootstrap/README.md). Programs for another system are built with `--target windows|linux|arm64|macos` +(see the [guide](docs/en/for-technicians.md#building-for-another-system)). Editors: **Kate** (`editors/kate/install.sh`), **VS Code** (the `finch-lang` `.vsix` from the release), or any editor with LSP support (`finch lsp`). @@ -91,13 +96,14 @@ Pick the guide that fits you: | | | |---|---| | Variables | `x := 5` (inferred) or `int x = 5`; `const W = 640` | -| Types | `int` `float` `bool` `char` `str`, sized `i8`…`i64` `u8`…`u64` `f32` `f64`, arrays `[]T`, maps `map[K]V`, 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]`, vectors `f32x8` `i8x32`… | | 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)` | | 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)` | +| Vectors (SIMD) | `v := f32x8(list, i)`, `v * 2.0 + w`, `v.sum()`, `v.dot(w)`, `min(v, 1.0)`, `v.store(list, i)`, `v[3]` | | Text | `"hp \(hp)"`, `"a" + "b"`, `str(42)`, `int("42")`, `s.split(",")`, `s.upper()`, `s[0]` | | Control | `if` / `else if` / `else` (also as a value), `while`, `for i in 0..10`, `break`, `continue`, `defer` | | Memory | automatic for arrays, text and structs; `new(...)` / `free(...)` for your own heap structures | @@ -108,7 +114,7 @@ Pick the guide that fits you: | Tests | `test "name" { assert(x == 3) }` next to the code, run with `finch test` | | C interop | `import "stdio.h"`, `link "glfw"`, `link "mine.c"` / `"glue.cpp"`, unions, `#define`s (also `(Color){…}`), `addr(fn)` for callbacks | | I/O | `print(...)`, `input("? ")`, `read_file`, `write_file`, `shell` | -| Tools | `finch run/build/test/fmt/ir/lsp`, `finch.toml` for a project, a build cache, `--target windows/arm64/macos/android`, `-g` for gdb/lldb, `-O0`, the chain of calls on runtime errors | +| Tools | `finch run/build/test/fmt/ir/lsp`, `finch.toml` for a project, a build cache, `--target windows/arm64/macos/android`, `--cpu native`, `-g` for gdb/lldb, `-O0`, the chain of calls on runtime errors | ## Examples @@ -122,6 +128,7 @@ Pick the guide that fits you: | [`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` | +| [`examples/simd.fch`](examples/simd.fch) | vectors: a dot product eight numbers at a time, and the time it saves | | [`examples/window.fch`](examples/window.fch) | a GLFW + OpenGL window | | [`examples/opengl.fch`](examples/opengl.fch) | modern OpenGL 3.3: shaders, buffers, a spinning triangle | | [`examples/vulkan.fch`](examples/vulkan.fch) | Vulkan: list the GPUs, their memory and queues, create a device | @@ -151,14 +158,16 @@ Pick the guide that fits you: - [x] A standard library in Finch: `json`, `path`, `time`, `proc`, `fs` - [x] `const`, `if` as a value, `"\(x)"` in text, `"""` text, C unions and `#define` expressions - [x] `test` blocks and `finch test`, `finch fmt`, `finch.toml`, a build cache, the chain of calls on runtime errors -- [x] Threads (`import thread`) and atomics; C++ files; `--target android` (compiles; NDK linking and devices untried) +- [x] Threads (`import thread`) and atomics; C++ files; `--target android` +- [x] The compiler on every system it builds for: Linux, macOS (Apple Silicon and Intel), Windows; releases made by CI +- [x] Vector types (`f32x8`, `i8x32`, …) for SIMD, and `--cpu native` ## Layout ``` src/ the compiler, in Finch: lexer, parser, AST, code generation (to LLVM IR as text), C import (libclang) and the C calling conventions, the language server, the driver -std/ the standard library, in Finch: json, path, time, proc, fs +std/ the standard library, in Finch: json, path, time, proc, fs, thread runtime/ the small C runtime linked into every program bootstrap/ build.sh and the seeds: the compiler's own LLVM IR, one per system examples/ example programs diff --git a/README.pl.md b/README.pl.md index 52b438d..098f317 100644 --- a/README.pl.md +++ b/README.pl.md @@ -5,8 +5,8 @@

-**Mały, zwinny, skuteczny.** Język programowania na LLVM ze składnią podobną do C, -na tyle prosty, że nauczysz się go w godzinę, i tak szybki jak C. +**Mały, szybki, celny.** Język programowania oparty na LLVM, ze składnią w stylu C. Nauczysz się go w godzinę, +a programy działają tak szybko jak w C. > 🇬🇧 [English version](README.md) @@ -19,7 +19,7 @@ fn przeciwprostokatna(float a, float b) -> float { fn main() { imie := "Finch" - print("Cześć od", imie, "!") + print("Cześć, tu", imie, "!") for i in 1..4 { print(i, "→", przeciwprostokatna(float(i), 1.0)) @@ -29,51 +29,58 @@ fn main() { ## Dlaczego Finch -- **Prosty.** Bez średników, bez plików nagłówkowych, bez preprocesora. Każdą rzecz robi się na jeden sposób. -- **Szybki.** Kompiluje się przez potok `-O2` z LLVM do kodu natywnego, bez garbage collectora. - Rekurencyjne `fib(40)` trwa ok. 0,19 s (`clang -O2` na tym samym kodzie C: ok. 0,23 s), a kod na tablicach - z kontrolą zakresu dorównuje C. -- **Pamięć bez bólu.** Listy, teksty i struktury są wartościami: przypisanie kopiuje, a koniec bloku je zwalnia. - Nie ma `free`, o którym można zapomnieć, nie ma użycia po zwolnieniu, a każdy test jest czysty pod valgrindem. -- **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:** +- **Prosty.** Nie ma średników, plików nagłówkowych ani preprocesora. Na każdą rzecz jest jeden sposób. +- **Szybki.** Kod przechodzi przez optymalizacje LLVM (`-O2`) i staje się zwykłym programem, bez garbage collectora. + Rekurencyjne `fib(40)` liczy się ok. 0,19 s (ten sam kod w C z `clang -O2`: ok. 0,23 s), a pętle po tablicach + ze sprawdzaniem zakresu są tak samo szybkie jak w C. +- **Pamięć bez kłopotów.** Listy, teksty i struktury są wartościami: przypisanie robi kopię, a koniec bloku sprząta. + Nie ma `free`, o którym da się zapomnieć, ani odwołań do zwolnionej pamięci. Valgrind nie znajduje błędów w żadnym teście. +- **Bezpieczniejszy niż C tam, gdzie nic to nie kosztuje.** Liczby nie zmieniają po cichu typu, a liczb ze znakiem + i bez znaku nie da się przypadkiem pomieszać. Indeks poza tablicą, dzielenie przez zero czy `null` kończą program + jasnym komunikatem, a nie niezdefiniowanym zachowaniem. Kolejność działań jest taka, jakiej się spodziewasz + (`x & 1 == 0` znaczy dokładnie to, co widać). +- **Praktyczny.** Mapy, metody, błędy jako wartości (`int(s) or 0`, `try read_file(p)`), moduły, dowolna biblioteka C + i serwer języka dla edytora. Działa na Linuksie, macOS i Windowsie, na procesorach x86-64 i ARM64. +- **SIMD w języku.** `f32x8`, `i8x32`, `f64x4`: kilka liczb obok siebie, dodawanych i mnożonych naraz. LLVM zamienia je + na instrukcje SSE, AVX2, AVX-512 albo NEON, więc ten sam kod jest szybki na komputerze stacjonarnym, laptopie i ARM-ie. +- **Komunikaty, które da się zrozumieć:** ``` gra.fch:3:13: error: 'y' must be i32, but this is int (use i32(...) to convert) 3 | i32 y = x | ^ ``` -- **Każda biblioteka C, bezpośrednio.** Piszesz `import "raylib.h"` i `link "raylib"`, i możesz z niej korzystać, - także ze struktur przekazywanych przez wartość i z callbacków. Nagłówki czyta libclang, więc nie trzeba niczego deklarować ręcznie. -- **Napisany w sobie.** Kompilator to kod w Finchu i nic więcej: `src/*.fch`, ok. 8,6 tys. linii. Buduje się go z ziarna - (własny kod LLVM IR, `bootstrap/`), potem z samego siebie, a oba wyniki są identyczne. Pierwsza wersja, w C++, - jest w gałęzi `before-bootstrap`. +- **Biblioteki C bez pośredników.** Wystarczy `import "raylib.h"` i `link "raylib"`, żeby z niej korzystać, łącznie + ze strukturami przekazywanymi przez wartość i funkcjami zwrotnymi. Nagłówki czyta libclang, więc niczego nie trzeba + deklarować ręcznie. +- **Napisany w samym sobie.** Kompilator to wyłącznie kod w Finchu: `src/*.fch`, ok. 12 tys. linii. Buduje się go + z pliku startowego (jego własnego kodu LLVM IR, w `bootstrap/`), a potem jeszcze raz nim samym. Oba wyniki są identyczne. -## Szybki start +## Na start -**Budowanie** (Linux x86-64 lub ARM64; potrzebny `clang` i libclang, LLVM 21): +**Gotowe kompilatory** na Linuksa (x64, ARM64), macOS (Apple Silicon, Intel) i Windowsa (x64) znajdziesz w +[wydaniach](https://github.com/Nexoniarz/Finch/releases). Rozpakuj archiwum i trzymaj katalogi `runtime/` i `std/` obok +pliku `finch`. Co jeszcze trzeba doinstalować, opisuje [przewodnik dla techników](docs/pl/dla-technikow.md#2-instalacja): +na Linuksie clang i libclang z LLVM 21, na Macu `brew install llvm@21`, na Windowsie llvm-mingw. + +**Samodzielne budowanie** (na Linuksie, macOS albo Windowsie w Git Bash; potrzebne są `clang` i libclang z LLVM 21): ```sh git clone https://github.com/Nexoniarz/Finch.git cd Finch -nix-shell # albo zainstaluj samodzielnie clang + libclang (LLVM 21) + pkg-config -bootstrap/build.sh # kompilator to build/finch: z ziarna, potem z samego siebie +nix-shell # albo zainstaluj clang, libclang (LLVM 21) i pkg-config po swojemu +bootstrap/build.sh # gotowy kompilator to build/finch ./build/finch run examples/hello.fch ./build/finch build examples/tour.fch -o tour && ./tour -tests/run.sh # każdy program, każda odmowa, -g, biblioteka współdzielona, serwer języka +tests/run.sh # wszystkie programy testowe, oczekiwane błędy, -g, biblioteka współdzielona, serwer języka ``` -Gotowe kompilatory są w [wydaniach](https://github.com/Nexoniarz/Finch/releases) (Linux x64 i ARM64: rozpakuj, zostaw -`runtime/` i `std/` obok `finch`). **Programy dla Windowsa, ARM64 i macOS** buduje się z Linuksa przez -`--target windows|arm64|macos`; sam kompilator nie działa jeszcze na macOS ani Windowsie (potrzebuje ziarna -zrobionego z nagłówkami tego systemu: patrz [bootstrap/README.md](bootstrap/README.md)). +Ten sam `bootstrap/build.sh` działa na Macu (z `llvm@21` z Homebrew) i na Windowsie (z llvm-mingw); szczegóły są w +[bootstrap/README.md](bootstrap/README.md). Program na inny system zbudujesz opcją `--target windows|linux|arm64|macos` +(zob. [przewodnik](docs/pl/dla-technikow.md#budowanie-na-inny-system)). -Edytory: **Kate** (`editors/kate/install.sh`), **VS Code** (plik `.vsix` `finch-lang` z wydania) albo dowolny -edytor z obsługą LSP (`finch lsp`). +Edytory: **Kate** (`editors/kate/install.sh`), **VS Code** (plik `.vsix` `finch-lang` z wydania) albo każdy edytor, +który obsługuje LSP (`finch lsp`). ## Dokumentacja @@ -81,90 +88,94 @@ Wybierz przewodnik dla siebie: | Przewodnik | Dla kogo | Polski | English | |---|---|---|---| -| **Początkujący** | Nigdy nie programowałeś: uczniowie, seniorzy, ciekawscy | [dla-poczatkujacych.md](docs/pl/dla-poczatkujacych.md) | [for-beginners.md](docs/en/for-beginners.md) | -| **Technicy** | Znasz komputery, programowanie jest nowe: instalacja, cały język, biblioteki C, problemy | [dla-technikow.md](docs/pl/dla-technikow.md) | [for-technicians.md](docs/en/for-technicians.md) | -| **Inżynierowie** | Wszystko: lekser, gramatyka, AST, reguły typów, tłumaczenie na IR, import przez libclang, ABI, wnętrze | [dla-inzynierow.md](docs/pl/dla-inzynierow.md) | [for-engineers.md](docs/en/for-engineers.md) | -| **Ściąga** | Każda funkcja, metoda, operator i polecenie na jednej stronie | [sciaga.md](docs/pl/sciaga.md) | [cheatsheet.md](docs/en/cheatsheet.md) | +| **Dla początkujących** | Dla tych, którzy nigdy nie programowali: uczniów, seniorów, ciekawych świata | [dla-poczatkujacych.md](docs/pl/dla-poczatkujacych.md) | [for-beginners.md](docs/en/for-beginners.md) | +| **Dla techników** | Znasz komputery, ale programowanie jest dla ciebie nowe: instalacja, cały język, biblioteki C, rozwiązywanie problemów | [dla-technikow.md](docs/pl/dla-technikow.md) | [for-technicians.md](docs/en/for-technicians.md) | +| **Dla inżynierów** | Wszystko: lekser, gramatyka, AST, reguły typów, generowanie IR, import przez libclang, ABI, budowa kompilatora | [dla-inzynierow.md](docs/pl/dla-inzynierow.md) | [for-engineers.md](docs/en/for-engineers.md) | +| **Ściąga** | Wszystkie funkcje, metody, operatory i polecenia na jednej stronie | [sciaga.md](docs/pl/sciaga.md) | [cheatsheet.md](docs/en/cheatsheet.md) | ## Język w pigułce | | | |---|---| -| Zmienne | `x := 5` (typ zgadnięty) albo `int x = 5`; `const W = 640` | -| 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)` | +| Zmienne | `x := 5` (typ wynika z wartości) albo `int x = 5`; `const W = 640` | +| Typy | `int` `float` `bool` `char` `str`, o podanym rozmiarze: `i8`…`i64` `u8`…`u64` `f32` `f64`, tablice `[]T`, mapy `map[K]V`, wskaźniki `ptr[T]`, wektory `f32x8` `i8x32`… | +| Funkcje | `fn dodaj(int a, int b) -> int { return a + b }` | +| Struktury | `struct Punkt { … }`, jedno pole w linii (`int x`, `int y = 0`); tworzy się je przez `Punkt(1, 2)` albo `Punkt(x: 1)` | | 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 | `"hp \(hp)"`, `"a" + "b"`, `str(42)`, `int("42")`, `s.split(",")`, `s.upper()`, `s[0]` | -| Sterowanie | `if` / `else if` / `else` (też jako wartość), `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 | -| Enumy, match | `enum Kolor { Czerwony, Zielony }`, `match k { Czerwony -> ... else -> ... }` (także jako wartość) | -| Generyki | `fn max[T](T a, T b) -> T`, `struct Stos[T]`, `Stos[int]()` | +| Mapy | `wiek := ["ania": 31]`, `wiek["bob"] = 25`, `wiek.has("cyryl")`, `ile[slowo] += 1`, `for k, v in wiek { }` | +| Błędy | `fn parsuj(str s) -> int! { … return error("zła liczba") }` → `parsuj(s) or 0`, `parsuj(s) or { … }`, `try parsuj(s)` | +| Wektory (SIMD) | `v := f32x8(lista, i)`, `v * 2.0 + w`, `v.sum()`, `v.dot(w)`, `min(v, 1.0)`, `v.store(lista, i)`, `v[3]` | +| Tekst | `"hp: \(hp)"`, `"a" + "b"`, `str(42)`, `int("42")`, `s.split(",")`, `s.upper()`, `s[0]` | +| Sterowanie | `if` / `else if` / `else` (również jako wartość), `while`, `for i in 0..10`, `break`, `continue`, `defer` | +| Pamięć | tablice, teksty i struktury zwalniają się same; `new(...)` / `free(...)` dla własnych struktur na stercie | +| Enumy i `match` | `enum Kolor { Czerwony, Zielony }`, `match k { Czerwony -> ... else -> ... }` (również jako wartość) | +| Typy generyczne | `fn max[T](T a, T b) -> T`, `struct Stos[T]`, `Stos[int]()` | | Moduły | `import ksztalty` → `ksztalty.pole(p)`, `import ui/button as btn`; biblioteka standardowa: `json`, `path`, `time`, `proc`, `fs`, `thread` | | Wątki | `thread.spawn(addr(praca), addr(dane))`, `t.join()`, `thread.mutex()`, `atomic_add(addr(n), 1)` | -| Testy | `test "nazwa" { assert(x == 3) }` obok kodu, uruchamiane przez `finch test` | -| Współpraca z C | `import "stdio.h"`, `link "glfw"`, `link "moje.c"` / `"glue.cpp"`, unie, `#define` (też `(Color){…}`), `addr(fn)` dla callbacków | -| Wejście/wyjście | `print(...)`, `input("? ")`, `read_file`, `write_file`, `shell` | -| Narzędzia | `finch run/build/test/fmt/ir/lsp`, `finch.toml` dla projektu, cache budowania, `--target windows/arm64/macos/android`, `-g` dla gdb/lldb, `-O0`, łańcuch wywołań przy błędach w czasie działania | +| Testy | `test "nazwa" { assert(x == 3) }` tuż obok kodu, uruchamiane przez `finch test` | +| Współpraca z C | `import "stdio.h"`, `link "glfw"`, `link "moje.c"` / `"glue.cpp"`, unie, `#define` (także `(Color){…}`), `addr(fn)` jako funkcja zwrotna | +| Wejście i wyjście | `print(...)`, `input("? ")`, `read_file`, `write_file`, `shell` | +| Narzędzia | `finch run/build/test/fmt/ir/lsp`, `finch.toml` dla projektu, pamięć podręczna kompilacji, `--target windows/arm64/macos/android`, `--cpu native`, `-g` dla gdb i lldb, `-O0`, ścieżka wywołań przy błędzie w trakcie działania | ## Przykłady -| Plik | Pokazuje | +| Plik | Co pokazuje | |---|---| -| [`examples/hello.fch`](examples/hello.fch) | najmniejszy program | +| [`examples/hello.fch`](examples/hello.fch) | najkrótszy program | | [`examples/tour.fch`](examples/tour.fch) | zmienne, funkcje, warunki, pętle | -| [`examples/guess.fch`](examples/guess.fch) | gra w zgadywanie: wejście, pętle, `rand` z C | +| [`examples/guess.fch`](examples/guess.fch) | gra w zgadywanie liczby: wczytywanie, 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/structs.fch`](examples/structs.fch) | struktury, tablice struktur, lista jednokierunkowa z `new`/`free` | +| [`examples/types.fch`](examples/types.fch) | liczby o podanym rozmiarze i wskaźniki | | [`examples/c_import.fch`](examples/c_import.fch) | `printf`, `math.h`, `malloc`/`free`, `stderr` | -| [`examples/window.fch`](examples/window.fch) | okno GLFW + OpenGL | +| [`examples/simd.fch`](examples/simd.fch) | wektory: iloczyn skalarny liczony po osiem liczb naraz i zysk na czasie | +| [`examples/window.fch`](examples/window.fch) | okno GLFW z OpenGL | | [`examples/opengl.fch`](examples/opengl.fch) | nowoczesny OpenGL 3.3: shadery, bufory, obracający się trójkąt | -| [`examples/vulkan.fch`](examples/vulkan.fch) | Vulkan: lista kart graficznych, ich pamięć i kolejki, tworzenie urządzenia | -| [`examples/raylib.fch`](examples/raylib.fch) | raylib ze strukturami C przez wartość | -| [`examples/llvm.fch`](examples/llvm.fch) | Finch budujący LLVM IR przez API LLVM-C | -| [`src/`](src/) | kompilator napisany w Finchu (ziarno do jego zbudowania jest w [`bootstrap/`](bootstrap/)) | +| [`examples/vulkan.fch`](examples/vulkan.fch) | Vulkan: lista kart graficznych z ich pamięcią i kolejkami, utworzenie urządzenia | +| [`examples/raylib.fch`](examples/raylib.fch) | raylib i struktury C przekazywane przez wartość | +| [`examples/llvm.fch`](examples/llvm.fch) | Finch, który buduje LLVM IR przez API LLVM-C | +| [`src/`](src/) | kompilator napisany w Finchu (plik startowy do jego zbudowania leży w [`bootstrap/`](bootstrap/)) | ## Plan rozwoju - [x] Lekser, parser, generowanie kodu LLVM, optymalizacja O2 -- [x] Typy, w tym liczby z rozmiarem, wskaźniki, konwersje; funkcje; `if`/`while`/`for` -- [x] Czytelne błędy kompilacji; kontrole w czasie działania (dzielenie, null, zakres) -- [x] Operatory bitowe z rozsądnymi priorytetami -- [x] Nagłówki C przez libclang; `link` z pkg-config; porady przy błędach linkowania -- [x] `struct`, także struktury C przez wartość (ABI System V) i callbacki -- [x] Tablice z `.len`, kontrolą zakresu i metodami; `for x in lista` -- [x] Pamięć: zwalnianie na końcu bloku, `defer`, `new`/`free` +- [x] Typy, w tym liczby o podanym rozmiarze, wskaźniki i konwersje; funkcje; `if`/`while`/`for` +- [x] Czytelne błędy kompilacji; sprawdzanie w trakcie działania (dzielenie, `null`, zakres tablicy) +- [x] Operatory bitowe z rozsądną kolejnością działań +- [x] Nagłówki C przez libclang; `link` z pkg-config; podpowiedzi przy błędach linkera +- [x] `struct`, w tym struktury C przekazywane przez wartość (ABI System V) i funkcje zwrotne +- [x] Tablice z `.len`, sprawdzaniem zakresu i metodami; `for x in lista` +- [x] Pamięć: wartości zwalniane na końcu bloku, `defer`, `new`/`free` - [x] Teksty: `+`, `.len`, indeksowanie, metody, `str(x)`, `int(s)`; `input()`; pliki - [x] Moduły Fincha (`import nazwa`) - [x] Informacje dla debuggera (`-g`) -- [x] Bootstrap: kompilator Fincha napisany w Finchu, który buduje sam siebie +- [x] Bootstrap: kompilator Fincha napisany w Finchu, który kompiluje 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 +- [x] Serwer języka (`finch lsp`) i rozszerzenie do VS Code - [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`) +- [x] Metody struktur, mapy, błędy jako wartości (`!`, `or`, `try`) - [x] Typy generyczne (`fn max[T]`, `struct Stos[T]`), `match`, enumy - [x] Biblioteka standardowa w Finchu: `json`, `path`, `time`, `proc`, `fs` -- [x] `const`, `if` jako wartość, `"\(x)"` w tekście, tekst `"""`, unie C i wyrażenia `#define` -- [x] Bloki `test` i `finch test`, `finch fmt`, `finch.toml`, cache budowania, łańcuch wywołań przy błędach -- [x] Wątki (`import thread`) i operacje atomowe; pliki C++; `--target android` (kompiluje się; linkowanie z NDK i urządzenia niesprawdzone) +- [x] `const`, `if` jako wartość, `"\(x)"` w tekście, tekst `"""`, unie C i wyrażenia w `#define` +- [x] Bloki `test` i `finch test`, `finch fmt`, `finch.toml`, pamięć podręczna kompilacji, ścieżka wywołań przy błędach +- [x] Wątki (`import thread`) i operacje atomowe; pliki C++; `--target android` +- [x] Kompilator na każdym systemie, na który buduje: Linux, macOS (Apple Silicon i Intel), Windows; wydania robi CI +- [x] Typy wektorowe (`f32x8`, `i8x32`, …) dla SIMD i `--cpu native` -## Struktura +## Co gdzie leży ``` -src/ kompilator, w Finchu: lekser, parser, AST, generowanie kodu (do LLVM IR jako tekst), import C (libclang) - i konwencje wywołań C, serwer języka, sterownik -std/ biblioteka standardowa, w Finchu: json, path, time, proc, fs -runtime/ mały runtime w C dołączany do każdego programu -bootstrap/ build.sh i ziarna: własny LLVM IR kompilatora, po jednym na system +src/ kompilator w Finchu: lekser, parser, AST, generowanie kodu (LLVM IR jako tekst), import C (libclang) + i konwencje wywołań C, serwer języka, polecenie finch +std/ biblioteka standardowa w Finchu: json, path, time, proc, fs, thread +runtime/ mały runtime w C, dołączany do każdego programu +bootstrap/ build.sh i pliki startowe: własny LLVM IR kompilatora, osobno dla każdego systemu examples/ przykładowe programy -editors/ rozszerzenie VS Code, podświetlanie i konfiguracja LSP dla Kate -tests/ run/ new/ (wzorcowe wyjście), fail/ new/fail/ (oczekiwane błędy), run.sh, cross.sh, shared.sh, lsp_test.py -docs/ przewodniki en/ i pl/ dla trzech grup odbiorców +editors/ rozszerzenie do VS Code, podświetlanie składni i konfiguracja LSP dla Kate +tests/ run/ new/ (wzorcowe wyniki), fail/ new/fail/ (oczekiwane błędy), run.sh, cross.sh, shared.sh, lsp_test.py +docs/ przewodniki po angielsku (en/) i po polsku (pl/) dla trzech grup czytelników ``` ## Licencja diff --git a/bootstrap/README.md b/bootstrap/README.md index f4825d8..3ea5dbe 100644 --- a/bootstrap/README.md +++ b/bootstrap/README.md @@ -3,12 +3,16 @@ Finch's compiler is written in Finch. To build it you need something that already runs Finch: the **seed**, which is the compiler's own LLVM IR, one file per system: -| File | System | Checked | +| File | System | Built and tested in CI on | |---|---|---| -| `linux-x86_64.ll` | Linux x86-64 | built and tested here and in CI | -| `linux-aarch64.ll` | Linux ARM64 | made with the ARM64 cross compiler; CI builds and tests it on an ARM64 runner | -| `windows-x86_64.ll` | Windows x86-64 (MinGW: llvm-mingw's clang + libclang.dll) | built and run under Wine: it compiles itself, and every test program runs | -| `macos-arm64.ll`, `macos-x86_64.ll` | macOS | made on Linux with the macOS 11.3 SDK; checked as IR only (nobody has run them yet) | +| `linux-x86_64.ll` | Linux x86-64 | ubuntu-24.04 | +| `linux-aarch64.ll` | Linux ARM64 | ubuntu-24.04-arm | +| `macos-arm64.ll` | macOS, Apple Silicon | macos-15 | +| `macos-x86_64.ll` | macOS, Intel | macos-15-intel | +| `windows-x86_64.ll` | Windows x86-64 (MinGW: llvm-mingw's clang + libclang.dll) | windows-2022 | + +Each CI job builds the compiler from its seed, then from itself, runs every test, and packs the result; a tag `v*` attaches +the packages to the release. `bootstrap/build.sh` compiles the seed with `clang` (and the runtime, and libclang) into `build/finch0`; `finch0` builds `src/` into `finch1`; `finch1` builds `src/` again into `build/finch`; the two must make the same IR, or the script stops. @@ -41,5 +45,6 @@ official LLVM for Windows); `bootstrap/winbuild.sh` turns it into `finch.exe` fo `bootstrap/build.sh` does the same from Git Bash: put llvm-mingw's `bin` in PATH and set `FINCH_LIBCLANG` (the path of `libclang.dll`), `FINCH_LLVM_INCLUDE` (its `include` folder) and `FINCH_LDFLAGS=-L`. -macOS: the seeds were made with the macOS 11.3 SDK; `brew install llvm@21` gives clang and libclang; the CI job sets -`FINCH_CLANG`, `FINCH_LIBCLANG` and `FINCH_LLVM_INCLUDE`. If the macOS build fails, that job is where it shows first. +macOS: the seeds were made with the macOS 11.3 SDK. `brew install llvm@21` +gives clang and libclang; set `FINCH_CLANG`, `FINCH_LIBCLANG` and `FINCH_LLVM_INCLUDE` as the CI job does (the +[technicians' guide](../docs/en/for-technicians.md#option-b-build-it-yourself) has the lines). diff --git a/docs/en/cheatsheet.md b/docs/en/cheatsheet.md index d27262c..615856d 100644 --- a/docs/en/cheatsheet.md +++ b/docs/en/cheatsheet.md @@ -21,8 +21,9 @@ Everything in Finch on one page. For explanations, see the [guides](../../README | `finch version` | version | | `-l lib` | link a C library (like `link "lib"`) | | `--target windows` / `linux` / `arm64` / `macos` / `android` / triple | build for another system (`android`: the NDK's clang from `ANDROID_NDK_HOME`, usually with `--shared`) | -| `-g` | debug info (gdb, lldb, Visual Studio) | +| `-g` | debug info (gdb, lldb) | | `-O0` | no optimizations | +| `--cpu native` / `x86-64-v3` / `apple-m1` … | use every instruction of that processor (AVX2, AVX-512, NEON); the program then needs one like it | | `--trace` / `--no-trace` | runtime errors show the chain of calls; on for `run`, `test`, `-g`, `-O0`, off for a plain `build` (it costs speed) | Environment: `FINCH_PATH` (folders with modules), `FINCH_CC` (the C compiler that links), `FINCH_CXX` (for `.cpp` files). @@ -39,6 +40,7 @@ links = ["SDL3", "native/glue.cpp"] # libraries and your own .c / .cpp / .o / .a libdirs = ["vendor/SDL3/lib"] cflags = ["-DFAST"] # for your .c / .cpp files ldflags = ["-Wl,-rpath,."] +cpu = "native" # the same as --cpu native [target.windows] # only when building for that system (also linux, macos, android, linux-arm64) links = ["ws2_32"] @@ -96,6 +98,7 @@ One statement per line, no semicolons, `{` on the same line, no brackets around | `[]T` | array: `[1, 2, 3]`, `[]str names` | | `map[K]V` | map: `["a": 1]`, `map[str]int counts`, `[:]` | | `ptr[T]`, `ptr` | pointer, untyped pointer | +| `f32x8`, `i8x32`, `f64x4`, `u16x16` … | vector: N numbers worked on at once (SIMD), see below | | `Name` / `module.Name` | a struct | Escapes: `\n \t \r \0 \\ \" \'`. @@ -240,6 +243,27 @@ Fallible built-ins: `int(s)`, `float(s)`, `read_file`, `write_file`, `delete_fil --- +## Vectors `f32x8` (SIMD) + +Types: `f32x4/8/16`, `f64x2/4/8`, `i8x16/32/64`, `i16x8/16/32`, `i32x4/8/16`, `i64x2/4/8`, and `u…` the same; at most 512 bits. + +| Code | Gives | +|---|---| +| `f32x8(x)` | `x` in every lane | +| `f32x8(a, b, …, h)` | one number per lane | +| `f32x8(list, i)` | `list[i]` … `list[i+7]` (a `[]f32` or `ptr[f32]`) | +| `f32x8(v)` | an 8-lane vector converted lane by lane (`f32x8(ints)`, `i32x8(floats)`) | +| `v + w`, `v * 2.0`, `-v` | lane by lane; a plain number counts for every lane (`/` only on floats) | +| `& \| ^ << >> ~` | lane by lane, whole-number lanes | +| `v == w`, `v != w` | bool: every lane equal / not | +| `v[i]`, `v[i] = x`, `v.len` | a lane; the number of lanes | +| `min(v, w)`, `max`, `abs(v)`, `clamp(v, lo, hi)` | lane by lane | +| `v.sum()`, `v.min()`, `v.max()`, `v.dot(w)` | one number from all the lanes | +| `v.sqrt()`, `v.floor()`, `v.ceil()`, `v.round()`, `v.fma(b, c)` | every lane (float lanes; `fma` = `v * b + c`) | +| `v.store(list, i)` | lanes into `list[i]` … | + +--- + ## Text `str` | Code | Gives | diff --git a/docs/en/for-beginners.md b/docs/en/for-beginners.md index fd3bedc..935e0b6 100644 --- a/docs/en/for-beginners.md +++ b/docs/en/for-beginners.md @@ -53,8 +53,9 @@ The computer reads your program from top to bottom, line by line. ## 2. Getting Finch ready -Finch runs on **Linux** computers. If a teacher, parent or friend has already set it up -for you, skip to the next section. +Finch runs on **Linux**, **macOS** and **Windows** computers. If a teacher, parent or friend has already set it up +for you, skip to the next section. (On a Mac or on Windows, ask someone to follow the +[guide for technicians](for-technicians.md#2-installing): it has a ready-made Finch for both.) Otherwise, open the **Terminal** program (a window where you type commands) and type these lines one by one, pressing **Enter** after each: @@ -76,7 +77,7 @@ To check it works, type: ./build/finch version ``` -You should see something like `Finch 2.5.0`. +You should see something like `Finch 2.6.0`. --- diff --git a/docs/en/for-engineers.md b/docs/en/for-engineers.md index 56ce0a9..68bc9e8 100644 --- a/docs/en/for-engineers.md +++ b/docs/en/for-engineers.md @@ -1,13 +1,11 @@ # Finch for Engineers -**The internals of Finch 2.5, whose compiler is written in Finch.** How source becomes tokens, an AST and LLVM IR *as text*, +**The internals of Finch 2.6, whose compiler is written in Finch.** How source becomes tokens, an AST and LLVM IR *as text*, which `clang` compiles and links with a small C runtime; how C headers are read with libclang from Finch itself and how the C calling conventions are reproduced by hand; the type and ownership rules; how the compiler builds itself from a seed; and the language server. Assumed: you can read Finch (see the [technicians' guide](for-technicians.md)) and have at least seen LLVM IR. -The compiler's first version, in C++ on LLVM's C++ API, is kept on the `before-bootstrap` branch; this guide describes -the Finch one. > 🇵🇱 Polska wersja: [dla-inzynierow.md](../pl/dla-inzynierow.md) @@ -34,7 +32,7 @@ the Finch one. ## 1. Architecture The compiler has no LLVM library inside it. It writes LLVM IR as **text** and lets `clang` do optimisation, code -generation and linking. That is what makes it small (about 8k lines) and what lets it be written in Finch itself. +generation and linking. That is what makes it small (about 12k lines) and what lets it be written in Finch itself. ``` main.fch ─► loader ── lex ── parse ──► Program (+ every imported module, recursively) @@ -60,7 +58,7 @@ Design decisions that shape the rest: Finch type (a string kind: `int`, `f32`, `str`, `array`, `map`, `struct`, `enum`, `named`…) and flags. - **Let clang do the work.** Locals are `alloca`s promoted by `mem2reg`. Optimisation is clang's `-O2` (or `-O0` with `-g`). - **Values, not references.** Arrays, strings and structs have value semantics; ownership is decided at compile time (§8). -- **A tiny C runtime** (`runtime/finch_rt.c`, ~760 lines) does what is tedious in IR: strings, array growth, maps, input, +- **A small C runtime** (`runtime/finch_rt.c`, ~1150 lines) does what is tedious in IR: strings, array growth, maps, input, files, panics. It is compiled once and cached in `~/.cache/finch`. --- @@ -70,17 +68,17 @@ Design decisions that shape the rest: | File | Lines | Responsibility | |---|---|---| | `src/lexer.fch` | 470 | Tokens, positions, `"""` text, `"\(x)"` turned into `("…" + str(x) + "…")` tokens | -| `src/ast.fch` | 120 | One `Node` struct with a `kind` string, children, type names, positions (`line`, `endLine`) | +| `src/ast.fch` | 170 | One `Node` struct with a `kind` string, children, type names, positions (`line`, `endLine`) | | `src/parser.fch` | 910 | Recursive descent: declarations, statements, expressions, generics (`[T]`), `enum`, `match`, `export`, `link`, `const`, `test "…" { }` | -| `src/gen.fch` | 6450 | Type check + IR: structs, functions, methods, scopes and drops, statements, expressions, built-ins, maps, errors as values, generics (monomorphisation), enums and `match`, `-g` debug metadata, the `index` mode | +| `src/gen.fch` | 7030 | Type check + IR: structs, functions, methods, scopes and drops, statements, expressions, built-ins, maps, errors as values, generics (monomorphisation), enums and `match`, vectors (SIMD), `-g` debug metadata, the `index` mode | | `src/cimport.fch` | 790 | libclang: functions, structs and unions (fields, offsets, padding), enums, constants, globals, macros (also struct-valued) | -| `src/platform.fch` | 90 | `Target`: triple, data layout, C compiler, signedness of `char` per system | -| `src/linker.fch` | 430 | The cache (runtime, program object, C/C++ files), libraries (`link`, pkg-config), link-error advice | -| `src/main.fch` | 515 | Driver: CLI, loader with module lookup and overlays, `build/run/test/fmt/ir/index/lsp` | +| `src/platform.fch` | 120 | `Target`: triple, data layout, C compiler, signedness of `char` per system | +| `src/linker.fch` | 480 | The cache (runtime, program object, C/C++ files), libraries (`link`, pkg-config), link-error advice | +| `src/main.fch` | 530 | Driver: CLI, loader with module lookup and overlays, `build/run/test/fmt/ir/index/lsp` | | `src/manifest.fch` | 240 | `finch.toml`: a small TOML reader, per-target lists | | `src/fmt.fch` | 225 | `finch fmt` and LSP formatting | -| `src/lsp.fch` | 910 | The language server (§11) | -| `src/docs.fch` | 110 | One description of the built-ins, shared by hover and completion | +| `src/lsp.fch` | 950 | The language server (§11) | +| `src/docs.fch` | 140 | One description of the built-ins, shared by hover and completion | | `std/*.fch` | 1040 | `json`, `path`, `time` (+`_windows`), `proc` (+`_windows`), `fs`, `thread` | | `runtime/finch_rt.c` | 1150 | The C runtime: text, arrays, maps, files, float printing, fs, threads, the test runner, the call chain | | `bootstrap/` | | `build.sh`, `install.sh`, the seeds (§10) | @@ -116,18 +114,19 @@ dedented by the closing line's indentation in `longString()`. ## 4. Code generation -`gen.fch` is one big `Gen` struct plus functions over it. The passes are: +`gen.fch` is one big `Gen` struct plus functions over it. `generate()` runs: -1. `declareStructs`: every Finch and C struct gets an LLVM type (C structs with their real padding); recursive-by-value - structs are refused. -2. `declareFns`: signatures, with a *borrow analysis* for parameters (a parameter that is never stored is passed without - a copy). -3. `define`: each body, type checked and emitted. Scopes track what must be dropped when they end (`defer` runs first). -4. `defineMainWrapper`: the C `main(argc, argv)`, and `finch_on_fatal` for embedders. -5. With `-g`, DWARF metadata is attached as the IR is written (use `-O0`). +1. `declare()`: every Finch and C struct gets an LLVM type (C structs with their real padding; recursive-by-value + structs are refused), then every function gets its signature, with a *borrow analysis* for parameters (§8.2). +2. `define()` for each function: the body, type checked and emitted. Scopes track what must be dropped when they end. + Instantiating a generic adds functions while this goes on, so the loop runs until no new ones appear. +3. The C entry point `main(argc, argv)` (or, for `--shared`, `finch_module_on_fatal` and the `export fn` wrappers). +4. `assemble()`: the declarations, struct types, string constants, helpers and bodies, as one `.ll` text. + +With `-g`, DWARF metadata is attached as the IR is written (use `-O0`). Generics are **monomorphised**: a call `max(1, 2)` infers `T`, then the template's AST is generated again with `T` bound, -under a mangled name (`max$int`). `match` over an enum is exhaustive (a missing variant is an error, `else` covers the +under its shown name (`max[int]`, a symbol `max.int`). `match` over an enum is exhaustive (a missing variant is an error, `else` covers the rest); on a value it is a chain of comparisons. Enum values are `i32`; `Color(n)` is range checked; printing uses a generated name table. @@ -164,10 +163,13 @@ offset 0, and a field is read through memory. They can't be passed by value to C ## 6. Driver, linking and targets -`finch build|run|ir|index file.fch [-o out] [-O0] [-g] [-l lib] [-I dir] [--target t] [--shared] [--overlay path=file]`. +`finch build|run|ir|index file.fch [-o out] [-O0] [-g] [-l lib] [-I dir] [--target t] [--cpu c] [--shared] [--overlay path=file]`. `ir` prints the IR; `index` prints the records the language server uses (§11); `--shared` makes a shared library of the `export fn`s. +`--cpu ` (or `cpu` in `finch.toml`) adds `-march=` on x86-64 or `-mcpu=` on ARM64 to the clang that +compiles the IR (not the runtime); it is part of the cache key. + `--target windows|linux|arm64|macos|android|` changes the triple and data layout, the C `char` signedness, the ABI rules above, the OS variant of modules (`name_windows.fch`), and the C compiler: `FINCH_CC` or `clang --target=…`. Linking asks `pkg-config` for flags for `link "lib"`; when the linker fails the output is analysed and the likely missing @@ -210,6 +212,7 @@ owned at that point is not freed. A line that starts at bracket depth 0 and doesn't continue a line ending in an operator or `,` is re-indented to 4 × brace depth; other lines (continuations, `"""` bodies) are shifted by the same amount as the line that started the statement, so their shape (and the `"""` dedent) is kept. + --- ## 7. Type system @@ -222,9 +225,10 @@ statement, so their shape (and the `"""` dedent) is kept. | `char` `i8` `u8` / `i16` `u16` / `i32` `u32` / `int` `u64` | `i8` / `i16` / `i32` / `i64` | | `f32` / `float` | `float` / `double` | | `str`, `[]T` | `{ ptr, i64, i64 }` (pointer, length, capacity) | -| `map[K]V` | `{ ptr, i64, i64, i64, ptr, i64 }` (entries, len, used, cap, index, icap; see src/gen.fch) | +| `map[K]V` | `{ ptr, i64, i64, i64, ptr, i64, i64 }` (entries, len, used, cap, index, icap, adds; see src/gen.fch) | | `ptr`, `ptr[T]`, `null` | `ptr` (opaque) | | `[N]T` | `[N x T]` | +| `f32x8`, `i8x32`, … | `<8 x float>`, `<32 x i8>` (§7, Vectors) | | 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 `-> !`) | @@ -235,11 +239,16 @@ Signedness lives only in the Finch type and selects `sdiv/udiv`, `ashr/lshr`, `i ### Values -```cpp -struct Value_ { Value *v; FType type; bool literal; bool fresh; }; +```c +struct Val { + str v // the IR operand: %t12, 42, @.str.3, zeroinitializer + ast.Type ty + bool fresh + bool lit +} ``` -- **`literal`**: a number written in the code or an imported C constant. It may become any integer type +- **`lit`**: a number written in the code or an imported C constant. It may become any integer type it fits (`fits()`), or `f32`. It survives unary minus and literal-only arithmetic that folds to a constant; explicit conversions clear it (`u8(250)` is a typed constant, so `u8(250) + 10` wraps to 4). - **`fresh`**: an owning value (str/array/owning struct) that nobody holds yet: the result of a call, @@ -281,13 +290,43 @@ Pointers/null: `icmp eq/ne ptr` only. `str`: `finch_str_eq` (length + `memcmp`) `finch_str_cmp` for ordering. Floats: `oeq olt ole ogt oge` and `une` for `!=`. Integers/char by signedness (char unsigned). Bool: equality only. Arrays/structs: error. ---- +### Vectors + +`ast.vecType(name)` recognises `x` (lanes `i8`…`u64`, `f32`, `f64`; N = 2…64; at most 512 bits) and gives +`Type(kind: "vec", elem: [lane], count: N)`, which is an LLVM vector ``. The parser takes such a name wherever a +type goes; a call `f32x8(…)` goes to `vecMake()`. + +| Finch | IR | +|---|---| +| `f32x8(x)` | `insertelement` into lane 0 + `shufflevector … zeroinitializer` (a splat) | +| `f32x8(a, …, h)` | a chain of `insertelement` | +| `f32x8(list, i)` | bounds check of `i … i+7`, then `load <8 x float>, ptr …, align 4` (the lane's alignment) | +| `f32x8(i32x8 v)` | `sitofp`, `fptosi`, `sext`, `trunc`… on the whole vector | +| `v + w`, `v * 2.0` | `fadd`/`add`… on vectors; a scalar operand is coerced to the lane type, then splatted | +| `v[i]` | a place: `getelementptr , ptr %v, i64 i` after a bounds check; or `extractelement` on a temporary | +| `v == w` | `fcmp oeq`/`icmp eq` → `` → `llvm.vector.reduce.and` | +| `min`, `max`, `abs` | `llvm.minnum`/`maxnum`, `llvm.smin`/`umin`…, `llvm.fabs`/`llvm.abs` | +| `v.sum()`, `.min()`, `.max()` | `llvm.vector.reduce.fadd` (with `reassoc`), `.add`, `.fmin`, `.smin`… | +| `v.sqrt()`, `.floor()`, `.ceil()`, `.round()` | the `llvm.*` intrinsic of that name | +| `v.fma(b, c)` | `llvm.fmuladd` (fused where the target has FMA; `llvm.fma` would be a libm call per lane without it) | +| `v.store(list, i)` | bounds check, `store <8 x float> …, align 4` | + +Integer `/` and `%` are refused: neither x86 nor ARM has a lane-wise integer divide, and LLVM would scalarise it. +Ordering comparisons are refused because their result would be a mask, which has no Finch type. + +**Alignment.** LLVM gives `<8 x float>` an ABI alignment of 32 (its size), but `malloc` and Finch's `alloca … align 16` +promise only 16. `llty()` records every vector wider than 16 bytes in `g.wide`, and `resolveStructBody` records a struct +that holds one; `load()` and `store()` add `, align 16` for those types. Without it LLVM may emit `vmovaps` on a 16-byte +aligned address and crash. + +**Which instructions** is clang's choice: `--cpu ` adds `-march=` (x86-64) or `-mcpu=` (ARM64) to the +IR's compile, so the same IR becomes SSE2, AVX2 + FMA, AVX-512 or NEON code. Lanes wider than the registers are split. --- ## 8. Ownership and memory -### 9.1 The model +### 8.1 The model Every value of an **owning type** (`str`, `[]T`, or a Finch struct with an owning field) has exactly one owner: a variable, an array element, a struct field, or a heap cell from `new()`. The owner drops the @@ -311,21 +350,21 @@ A string literal is a fresh value with `cap = 0`: moving it costs nothing, dropp (`release()` ignores constants), and writing into it (`s[i] = c`) first makes a heap copy (`finch_str_own`). Strings from C are borrowed with `cap = -1` and always copied before being kept. -### 9.2 Parameter borrowing: `mutates()` +### 8.2 Parameter borrowing: `mutates()` When declaring a function, every owning parameter is checked with `mutates(name, body)`: an assignment whose root variable (`rootVar()`: through `.field` and `[i]`) is the parameter, a changing method on it (`push pop insert remove clear resize sort reverse`), or `addr(…)` of it. Unchanged -parameters are borrowed (`Fn::borrowParam`); changed ones are copied once at entry. The analysis is +parameters are borrowed (`FnSig.borrow`); changed ones are copied once at entry. The analysis is syntactic and conservative: anything that *might* change the parameter makes a copy, which is always correct. The same analysis decides whether `for x in list` must copy elements. -Methods take part: `declareFns()` first finds which methods change `self` (`mutates("self", body)`), +Methods take part: `declare()` 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. +names go into `g.mm`, which `mutates()` treats like `push`: `p.move(1)` changes `p`. +Only then is `borrow` computed for every function and method. -### 9.3 Copy and drop +### 8.3 Copy and drop `dropAt(addr, T)` / `copyAt(dst, src, T)` work on addresses: @@ -335,10 +374,10 @@ Only then is `borrowParam` computed for every function and method. element and then `finch_arr_free`; copies call `finch_arr_clone_raw` and then deep-copy owning elements over the raw bits. Structs drop/copy only their owning fields. -Helpers are emitted in their own functions (`HelperScope` saves the builder position and switches off -debug locations), may call each other recursively (`struct Node { []Node kids }`), and are inlined by O2. +Helpers are emitted in their own functions (`beginHelper()` / `endHelper()` set the function being written aside +and switch off debug locations), may call each other recursively (`struct Node { []Node kids }`), and are inlined by O2. -### 9.4 What it looks like +### 8.4 What it looks like ```c fn greet(str name) -> str { @@ -381,22 +420,22 @@ The `-O0` IR of `main` (abridged): and `greet` returns `msg` without copying (`ret … %5` after loading the slot: the variable was moved out). -### 9.5 defer +### 8.5 defer -`defer stmt` pushes a `Cleanup{isDefer, body, order = current varOrder}` into the current scope; it -emits nothing there. Whenever the scope's cleanups are emitted (falling off the end of the block, +`defer stmt` records the statement in the current scope (`Scope.defers`, with `orders`: how many variables existed +then) and a `Cleanup{isDefer}`; it emits nothing there. Whenever the scope's cleanups are emitted (falling off the end of the block, `return`, `break`/`continue` leaving it) the body is generated **at that exit**, with `deferLimit = order` so that `lookup()` hides variables declared after the defer: those are already dropped by then (cleanups run in reverse). `return`/`break`/`continue` inside a deferred statement are errors (`inDefer`). Defer bodies are therefore duplicated per exit path, which is what makes them free on the normal path. -### 9.6 Manual memory +### 8.6 Manual memory `new(v)` allocates `sizeof(T)` with `finch_alloc`, stores `own(v)` and returns `ptr[T]`. `free(p)` drops the pointee if `T` is owning (skipping null) and calls libc `free`. Pointers never own anything: `addr(x)` can dangle after `x`'s block ends; this is not checked. -### 9.7 Verifying it +### 8.7 Verifying it `MEMCHECK=1 tests/run.sh` builds every test program and runs it under valgrind with `--leak-check=full --errors-for-leak-kinds=all`: zero leaks and zero invalid accesses across all of them, @@ -405,8 +444,6 @@ iteration, nested arrays of structs, and C struct passing. --- ---- - ## 9. Generics, enums and match See §4 for how they are generated. For the rules a user sees, read sections 23 and 24 of the @@ -428,8 +465,9 @@ and stops unless `finch1` and `finch` produce byte-identical IR (a fixpoint: the whatever built it). `bootstrap/build.sh seed` rewrites the seed from the current compiler; do it whenever the compiler's own source starts to use a feature the committed seed lacks. -A seed is per system because the C calling convention for the libclang calls is baked into it (§5). -Linux x86-64 and Linux ARM64 have seeds. macOS and Windows would need seeds made with Apple's and Microsoft's headers. +A seed is per system because the C calling convention for the libclang calls is baked into it (§5). There are seeds +for Linux x86-64 and ARM64, macOS ARM64 and x86-64 (made with Apple's SDK headers), and Windows x86-64 (made with +llvm-mingw's headers). CI builds the compiler from each one on that system and runs the tests there. --- @@ -445,16 +483,17 @@ mode the compiler prints, besides IR-free output, tab-separated records: | `@V name type file line col fnFile fnLine fnEnd` | a variable and the function it lives in | | `@F module name recv file line col endLine signature params` | a function or method | | `@S` / `@SF`, `@E`, `@I` | structs (and fields), enums, imports | +| `@W` | a warning | The first error is printed as `path:line:col: error: message` and becomes a diagnostic. When code does not compile, the server keeps answering completion and signature help from the last version that did. `tests/lsp_test.py` is the acceptance -test (22 checks). +test. --- ## 12. Semantics vs. C -| Situation | C | Finch 2.0 | +| Situation | C | Finch | |---|---|---| | Signed integer overflow | UB | defined wraparound (no `nsw`) | | Integer division by zero, `INT_MIN / -1` | UB | compile error if constant, otherwise runtime error | @@ -467,13 +506,9 @@ test (22 checks). | Uninitialized variables | UB | impossible (zero or defaults) | | Missing return | UB if used | compile error | | Order of evaluation | mostly unspecified | left to right; in an assignment the right side first | -| Shift ≥ width, out-of-range float→int | UB | still poison (not checked yet) | +| Shift ≥ width, out-of-range float→int | UB | poison, as in LLVM (not checked) | | Dangling pointer from `addr()` / `new` + `free` | UB | still UB | ---- - ---- - ### The chain of calls on runtime errors With `Options.trace` (default for `run`, `test`, `-g`, `-O0`; `--trace`/`--no-trace`; never with `--shared`) every Finch @@ -518,13 +553,10 @@ evaluated twice and temporaries are never pointed to. ## 13. Known limitations -- The compiler runs on Linux x86-64 and ARM64 only (no seed for macOS or Windows yet). Programs can be built for all three. -- macOS output is only checked as IR; Windows and ARM64 are run under Wine and qemu in CI. - One error per run, no recovery. - `-g` debug info is best with `-O0`. -- The driver uses `sh` utilities (`rm`, `mkdir`), so it needs a POSIX shell. - Generics are monomorphised (code size grows with instantiations); there are no trait bounds, a wrong use is an error inside the instantiated body. - A failed test's owned memory is not freed (the test runner `longjmp`s out of it). -- `--target android` is checked to the object file only; linking needs the NDK and has not been run on a device. +- Vectors have no lane masks (`v < w`), shuffles or integer division. diff --git a/docs/en/for-technicians.md b/docs/en/for-technicians.md index 9e41283..34aa27c 100644 --- a/docs/en/for-technicians.md +++ b/docs/en/for-technicians.md @@ -41,6 +41,7 @@ compiler works inside. For that, see the [guide for engineers](for-engineers.md) 28. [A project: finch.toml](#28-a-project-finchtoml) 29. [Threads](#29-threads) 30. [C++, Rust and Android](#30-c-rust-and-android) +31. [Vectors (SIMD)](#31-vectors-simd) --- @@ -67,19 +68,24 @@ messages that tell you how to fix the problem. ## 2. Installing -The Finch compiler is written in Finch (see the [engineers' guide](for-engineers.md)). It runs on **Linux** -(x86-64 and ARM64) and builds programs for **Linux, Windows and macOS**. It needs **clang 21** and **libclang** -(LLVM 21) to read C headers and to compile its output. +The Finch compiler is written in Finch (see the [engineers' guide](for-engineers.md)). It runs on **Linux** (x86-64 and +ARM64), **macOS** (Apple Silicon and Intel) and **Windows** (x86-64), and builds programs for all of them. On every +system it needs **clang** (it compiles each program with it) and **libclang** from LLVM 21 (it reads C headers with it). -The compiler does not run on macOS or Windows yet; it needs a seed made with that system's headers -([bootstrap/README.md](../../bootstrap/README.md) says why). Until then, build your Windows and macOS programs from Linux -with `--target` (below), or use the older C++ compiler from the `before-bootstrap` branch. +### Option A: a ready build -### Option A: a ready build (Linux) +Download the archive for your system from the [releases](https://github.com/Nexoniarz/Finch/releases) and unpack it. +Keep `runtime/` and `std/` next to the `finch` file (or install it, below). -Download `finch-linux-x64.tar.gz` or `finch-linux-arm64.tar.gz` from the -[releases](https://github.com/Nexoniarz/Finch/releases) and unpack it. Keep `runtime/` and `std/` next to the `finch` file -(or install it, below). You also need `clang` 21 and its libclang library: +| System | Archive | Also install | +|---|---|---| +| Linux x86-64 | `finch-linux-x64.tar.gz` | clang 21 and libclang (table below) | +| Linux ARM64 (Raspberry Pi 4/5, Graviton…) | `finch-linux-arm64.tar.gz` | the same | +| macOS, Apple Silicon | `finch-macos-arm64.tar.gz` | `xcode-select --install` (clang and the system headers), `brew install llvm@21` (libclang) | +| macOS, Intel | `finch-macos-x64.tar.gz` | the same | +| Windows x64 | `finch-windows-x64.zip` (`finch.exe` with `libclang.dll`) | [llvm-mingw](https://github.com/mstorsjo/llvm-mingw/releases) (the `ucrt-x86_64` zip): unpack it and put its `bin` folder on your PATH | + +On Linux: | Distribution | Packages | |---|---| @@ -88,12 +94,15 @@ Download `finch-linux-x64.tar.gz` or `finch-linux-arm64.tar.gz` from the | Arch | `clang pkgconf` | | Nix | `nix-shell` in the repository gives all of it | +On Windows, llvm-mingw gives Finch `clang`, the C headers and the linker in one folder; Windows programs built with it +need nothing else to run. Keep `libclang.dll` next to `finch.exe`. + ### Option B: build it yourself ```sh git clone https://github.com/Nexoniarz/Finch.git cd Finch -nix-shell # clang, libclang, pkg-config (or install them with your distribution) +nix-shell # Linux: clang, libclang, pkg-config (or install them with your distribution) bootstrap/build.sh # build/finch ./build/finch version ``` @@ -102,6 +111,26 @@ bootstrap/build.sh # build/finch `src/` with it, builds `src/` again with the result and checks the two are identical. If libclang isn't where the linker looks, tell it: `FINCH_LIBCLANG="-L/usr/lib/llvm-21/lib -lclang-21" bootstrap/build.sh`. +On a Mac, with Homebrew's LLVM: + +```sh +brew install llvm@21 +export FINCH_CLANG="$(brew --prefix llvm@21)/bin/clang" +export FINCH_LIBCLANG="-L$(brew --prefix llvm@21)/lib -lclang" +export FINCH_LLVM_INCLUDE="$(brew --prefix llvm@21)/include" +bootstrap/build.sh +``` + +On Windows, in Git Bash, with llvm-mingw on the PATH and the official LLVM for Windows (for `libclang.dll` and its headers) +unpacked to `C:/llvm`: + +```sh +export FINCH_LIBCLANG="C:/llvm/bin/libclang.dll" +export FINCH_LLVM_INCLUDE="C:/llvm/include" +export FINCH_LDFLAGS="-LC:/llvm/bin" +bootstrap/build.sh # build/finch.exe, with libclang.dll copied next to it +``` + ### Building for another system `--target` builds a program for another system. Finch needs that system's C compiler to link: @@ -151,7 +180,9 @@ options: -l link a C library, same as link "lib" in the file -I look for C headers there too --target build for another system: windows, linux, arm64, macos, android, or an LLVM triple - -g add debug info (for gdb / lldb / Visual Studio) + --cpu use every instruction of a processor: native (this one), x86-64-v3 (AVX2), + x86-64-v4 (AVX-512), apple-m1, ... (section 31) + -g add debug info (for gdb and lldb) -O0 skip optimizations --trace runtime errors show the chain of calls (on anyway for run, test, -g and -O0) --no-trace they don't (a plain finch build doesn't: keeping the chain makes calls slower) @@ -946,8 +977,7 @@ gdb ./game ``` `-g` adds debug info (lines, functions, variables, struct fields). `-O0` keeps every variable -visible; without it the optimizer may remove some. On Windows, `-g` writes CodeView, which the -Visual Studio debugger and WinDbg read. +visible; without it the optimizer may remove some. On Windows the debug info is DWARF as well: use the `lldb` that comes with llvm-mingw, or gdb. --- @@ -959,10 +989,11 @@ Visual Studio debugger and WinDbg read. | `… come from "x.h", but its library isn't linked` | Add `link "name"` at the top of the file. | | `the library 'x' wasn't found` | Not installed, or a wrong name. On Nix, add it to `shell.nix` and run inside `nix-shell`. | | `the C function 'f' can't be used from Finch yet` | It uses a union, bit fields or another unsupported type by value. Look for a variant that takes pointers. | +| `--cpu native`: the program stops with `Illegal instruction` on another computer | It was built for the processor that built it. Build without `--cpu`, or with an older one (`x86-64-v2`). | | `can't find the module 'x'` | Put `x.fch` next to the importing file, or set `FINCH_PATH`. | | `couldn't build the Finch runtime` | No working C compiler. Install gcc or clang, or set `CC`. | | 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: `no C compiler found to link with ('clang')` | Put llvm-mingw's `bin` folder on your PATH (section 2). | | 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`. | @@ -980,7 +1011,7 @@ Finch/ ├── std/ the standard library, in Finch: json, path, time, proc, fs, thread ├── runtime/ finch_rt.c: the small runtime (text, arrays, maps, input, files) ├── bootstrap/ build.sh, install.sh and the seeds (the compiler's own IR, one per system) -├── examples/ hello, tour, types, structs, todo, guess, c_import, window, opengl, vulkan, raylib, llvm +├── examples/ hello, tour, types, structs, todo, guess, wordcount, c_import, simd, window, opengl, vulkan, raylib, llvm ├── tests/ │ ├── run/ new/ programs + the exact output they must print (.out), input (.in) │ ├── fail/ new/fail/ programs that must fail, with the expected error in line 1 @@ -994,35 +1025,12 @@ Finch/ ``` ```sh -tests/run.sh # programs, refusals, -g builds, a shared library, the language server +tests/run.sh # programs, expected errors, -g builds, a shared library, the language server MEMCHECK=1 tests/run.sh # the same under valgrind: no leaks, no bad memory access 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 ``` -### Variants that hold values - -A variant can carry values. Make one with `Shape.Circle(2.0)`, take them out with `match`, which names them: - -```c -enum Shape { - Circle(float) - Rect(float, float) - Point -} - -fn Shape.area() -> float { - return match self { - Circle(r) -> 3.14 * r * r - Rect(w, h) -> w * h - Point -> 0.0 - } -} -``` - -`_` ignores a value. `match` must handle every variant (or end with `else`). Printing shows `Rect(2, 3.5)`. Such an enum is -copied and freed like a struct; it can't be compared with `==` (match on it). Variants of these enums can't have numbers. - --- ## 23. Enums and match @@ -1069,6 +1077,29 @@ match word { handle **every variant** or end with `else` (the compiler says which are missing); any other match needs `else`. In an expression, every arm gives the same type. +### Variants that hold values + +A variant can carry values. Make one with `Shape.Circle(2.0)`, take them out with `match`, which names them: + +```c +enum Shape { + Circle(float) + Rect(float, float) + Point +} + +fn Shape.area() -> float { + return match self { + Circle(r) -> 3.14 * r * r + Rect(w, h) -> w * h + Point -> 0.0 + } +} +``` + +`_` ignores a value. `match` must handle every variant (or end with `else`). Printing shows `Rect(2, 3.5)`. Such an enum is +copied and freed like a struct; it can't be compared with `==` (match on it). Variants of these enums can't have numbers. + ## 24. Generics Write a function or a struct once for any type, with the type as a name in square brackets: @@ -1134,7 +1165,7 @@ Not there yet: - Interfaces. - Unicode-aware text: `.len`, `s[i]` and `upper()` work on bytes / ASCII. - Checks that stop two threads from changing the same array at once (use a mutex). -- Windows on ARM64, 32-bit systems, WebAssembly. `--target android` makes code for Android, but linking with the NDK and running on a device are untried. +- Windows on ARM64, 32-bit systems, WebAssembly. ### Warnings about big copies @@ -1193,6 +1224,7 @@ links = ["SDL3", "native/glue.cpp"] # libraries, and your own .c / .cpp / .o / libdirs = ["vendor/SDL3/lib"] # folders with libraries cflags = ["-DFAST"] # for your .c / .cpp files ldflags = ["-Wl,-rpath,$ORIGIN"] # for the linker +cpu = "native" # the same as --cpu native (section 31) [target.windows] # added only when building for Windows links = ["ws2_32"] @@ -1292,6 +1324,82 @@ Write the header (`float path_len(float x, float y);`, or let `cbindgen` write i **Android**: `finch build game.fch --target android --shared` makes `libgame.so` for 64-bit ARM Android (API 24 and up) with the NDK's clang, found through `ANDROID_NDK_HOME` (or `FINCH_CC`). Load it from an app, for example through -SDL3's Android project (the library's `SDL_main` is an `export fn`). This is new and only partly checked: Finch's -output compiles for Android (thread-local data goes through the emulated TLS older Android needs), but linking with the -NDK and running on a device have not been tried yet. +SDL3's Android project (the library's `SDL_main` is an `export fn`). Thread-local data goes through the emulated TLS +that older Android versions need. + +--- + +## 31. Vectors (SIMD) + +A processor can add or multiply several numbers with one instruction: 4 floats at once with SSE, 8 with AVX2, 16 with +AVX-512, 4 with ARM's NEON. That is SIMD (single instruction, multiple data), and it is how image filters, audio, physics +and neural networks get their speed. In Finch it is a type: **`f32x8` is 8 `f32` side by side**. + +```c +a := f32x4(1.0, 2.0, 3.0, 4.0) +b := f32x4(0.5) // 0.5 in every lane +print(a + b) // f32x4(1.5, 2.5, 3.5, 4.5) +print(a * 2.0) // a plain number counts for every lane: f32x4(2, 4, 6, 8) +print(a.sum(), a[2], a.len) // 10 3 4 +``` + +**The types** are a number type, `x`, and how many lanes: `f32x4` `f32x8` `f32x16`, `f64x2` `f64x4` `f64x8`, +`i8x16` `i8x32` `i8x64`, `i16x8` `i16x16`, `i32x4` `i32x8` `i32x16`, `i64x2` `i64x4`, and the same with `u` (unsigned). +2 to 64 lanes, at most 512 bits in all. A vector is a plain value like an `int`: it goes into variables, struct fields, +arrays, maps and function parameters, and `print` and `str` show it. + +**Making one:** + +| | | +|---|---| +| `f32x8(x)` | `x` in every lane | +| `f32x8(a, b, c, d, e, f, g, h)` | one number per lane | +| `f32x8(list, i)` | `list[i]` … `list[i+7]` from a `[]f32` (or through a `ptr[f32]`); stops the program if they aren't all in the list | +| `f32x8(v)` | another 8-lane vector converted lane by lane: `f32x8(ints)`, `i32x8(floats)` (cuts the fractions), `i16x8(i32s)` | + +**Working with it:** + +| | | +|---|---| +| `+ - * /` | lane by lane (`/` on floats only: x86 and ARM can't divide whole numbers lane by lane) | +| `& \| ^ << >> ~` | lane by lane, on whole-number lanes | +| `-v` | every lane negated | +| `v == w`, `v != w` | `true` if every lane is equal / if any differs | +| `v[i]`, `v[i] = x`, `v.len` | one lane; the number of lanes | +| `min(v, w)`, `max(v, w)`, `abs(v)`, `clamp(v, lo, hi)` | lane by lane; `lo`, `hi` and `w` can be plain numbers | +| `v.sum()`, `v.min()`, `v.max()` | all the lanes made into one number | +| `v.dot(w)` | `(v * w).sum()` | +| `v.sqrt()`, `v.floor()`, `v.ceil()`, `v.round()` | every lane (float lanes) | +| `v.fma(b, c)` | `v * b + c`, as one instruction where the processor has one | +| `v.store(list, i)` | writes the lanes to `list[i]` … (or through a `ptr`) | + +`<` and the other comparisons would give an answer per lane, so they aren't there; use `min`, `max` or `v[i] < w[i]`. + +**A real loop.** The usual shape: as many whole vectors as fit, then the rest one by one. + +```c +fn dot([]f32 a, []f32 b) -> f32 { + acc := f32x8(0.0) + i := 0 + while i + 8 <= a.len { + acc = f32x8(a, i).fma(f32x8(b, i), acc) + i += 8 + } + total := acc.sum() + while i < a.len { + total += a[i] * b[i] + i += 1 + } + return total +} +``` + +`examples/simd.fch` times this against the plain loop. + +**Which instructions.** By default a program runs on every processor of its system (on x86-64 that means SSE2, so an +`f32x8` is done as two halves). `--cpu native` (or `cpu = "native"` in `finch.toml`) lets LLVM use everything the processor +that builds it has: AVX2 and FMA, AVX-512, or NEON's newer parts on ARM. Such a program needs a processor like that one to +run. To pick a level instead: `--cpu x86-64-v3` (AVX2, most PCs since 2015), `--cpu x86-64-v4` (AVX-512), `--cpu apple-m1`. + +Lanes wider than the processor's registers are split by LLVM, and the same code builds for x86-64 and ARM64 alike. +`v.sum()` on float lanes adds them in whatever order is fastest, so the last digits can differ from adding them one by one. diff --git a/docs/pl/dla-inzynierow.md b/docs/pl/dla-inzynierow.md index c948e86..425c6d0 100644 --- a/docs/pl/dla-inzynierow.md +++ b/docs/pl/dla-inzynierow.md @@ -1,12 +1,11 @@ # Finch dla inżynierów -**Wnętrze Fincha 2.5, którego kompilator jest napisany w Finchu.** Jak kod źródłowy staje się tokenami, drzewem AST i -LLVM IR *jako tekstem*, który `clang` kompiluje i linkuje z małym runtime'em w C; jak nagłówki C czyta libclang wywoływany -z samego Fincha i jak ręcznie odtwarzane są konwencje wywołań C; reguły typów i własności; jak kompilator buduje sam siebie -z ziarna; oraz serwer języka. +**Jak od środka działa Finch 2.6, którego kompilator jest napisany w Finchu.** Jak kod źródłowy zamienia się w tokeny, +drzewo AST i LLVM IR *w postaci tekstu*, który `clang` kompiluje i linkuje z małym runtime'em w C; jak nagłówki C są +czytane przez libclang z poziomu samego Fincha i jak ręcznie odtworzono konwencje wywołań C; reguły typów i własności +wartości; jak kompilator buduje sam siebie z pliku startowego; i jak działa serwer języka. -Zakładam, że czytasz Fincha (zobacz [przewodnik dla techników](dla-technikow.md)) i widziałeś już LLVM IR. -Pierwsza wersja kompilatora, w C++ na API C++ LLVM, jest w gałęzi `before-bootstrap`; ten przewodnik opisuje wersję w Finchu. +Zakładam, że umiesz czytać kod w Finchu (zob. [przewodnik dla techników](dla-technikow.md)) i że LLVM IR nie jest ci obcy. > 🇬🇧 English version: [for-engineers.md](../en/for-engineers.md) @@ -15,194 +14,210 @@ Pierwsza wersja kompilatora, w C++ na API C++ LLVM, jest w gałęzi `before-boot ## Spis treści 1. [Architektura](#1-architektura) -2. [Drzewo źródeł](#2-drzewo-źródeł) -3. [Front end: lekser, parser, moduły](#3-front-end-lekser-parser-moduły) +2. [Pliki źródłowe](#2-pliki-źródłowe) +3. [Część frontowa: lekser, parser, moduły](#3-część-frontowa-lekser-parser-moduły) 4. [Generowanie kodu](#4-generowanie-kodu) -5. [Import C i ABI C](#5-import-c-i-abi-c) -6. [Sterownik, linkowanie i cele](#6-sterownik-linkowanie-i-cele) +5. [Import z C i ABI języka C](#5-import-z-c-i-abi-języka-c) +6. [Sterownik, linkowanie i systemy docelowe](#6-sterownik-linkowanie-i-systemy-docelowe) 7. [System typów](#7-system-typów) 8. [Własność i pamięć](#8-własność-i-pamięć) -9. [Generyki, enumy i match](#9-generyki-enumy-i-match) +9. [Typy generyczne, enumy i match](#9-typy-generyczne-enumy-i-match) 10. [Bootstrap](#10-bootstrap) 11. [Serwer języka](#11-serwer-języka) -12. [Semantyka a C](#12-semantyka-a-c) +12. [Semantyka w porównaniu z C](#12-semantyka-w-porównaniu-z-c) 13. [Znane ograniczenia](#13-znane-ograniczenia) --- ## 1. Architektura -W kompilatorze nie ma biblioteki LLVM. Pisze LLVM IR jako **tekst**, a optymalizację, generowanie kodu i linkowanie zostawia -`clangowi`. Dzięki temu jest mały (ok. 8 tys. linii) i może być napisany w samym Finchu. +W kompilatorze nie ma biblioteki LLVM. Zapisuje on LLVM IR jako **tekst**, a optymalizację, generowanie kodu maszynowego +i linkowanie zostawia `clangowi`. Dzięki temu jest mały (ok. 12 tys. linii) i dało się go napisać w samym Finchu. ``` main.fch ─► loader ── lex ── parse ──► Program (+ każdy importowany moduł, rekurencyjnie) │ - `import "x.h"` ─► cimport.fch (libclang wołany z Fincha) ─► funkcje, struktury, stałe C + `import "x.h"` ─► cimport.fch (libclang wołany z Fincha) ─► funkcje, struktury i stałe C │ ▼ - gen.fch ─► generate(): struktury, sygnatury, ciała funkcji - kontrola typów i IR w jednym przejściu; klasyfikacja ABI wywołań C + gen.fch ─► generate(): deklaracje struktur i funkcji, potem ciała funkcji; + sprawdzanie typów i emisja IR w jednym przejściu; klasyfikacja ABI dla wywołań C │ (tekst IR) ▼ linker.fch ─► clang -c prog.ll ─► clang prog.o rt-.o [twoje .c/.o/.a] -lm [pkg-config / -l] │ ▼ - plik wykonywalny (finch run: uruchamiany z pliku tymczasowego, potem usuwany) + plik wykonywalny (finch run: uruchamiany z pliku tymczasowego, potem usuwany) ``` -Decyzje projektowe: +Decyzje, od których zależy cała reszta: -- **Szybko się przerywa.** Pierwszy błąd wypisuje `plik:linia:kol: error: …` z linią źródła i strzałką, i kompilator kończy. - Bez odzyskiwania po błędach, więc kolejne fazy nie widzą półpoprawnych danych. -- **Jedno przejście.** `gen.fch` sprawdza typy i emituje IR jednym przejściem po AST. Wyrażenie daje operand IR, typ Fincha - (napis: `int`, `f32`, `str`, `array`, `map`, `struct`, `enum`, `named`…) i flagi. -- **Niech clang pracuje.** Zmienne lokalne to `alloca`, zamieniane przez `mem2reg`. Optymalizacja to `-O2` clanga (albo `-O0` z `-g`). -- **Wartości, nie referencje.** Tablice, teksty i struktury mają semantykę wartości; własność ustala się w czasie kompilacji (§8). -- **Malutki runtime w C** (`runtime/finch_rt.c`, ok. 760 linii) robi to, co w IR jest uciążliwe: teksty, rośnięcie tablic, mapy, - wejście, pliki, paniki. Kompilowany raz i trzymany w `~/.cache/finch`. +- **Pierwszy błąd kończy pracę.** Kompilator wypisuje `plik:linia:kolumna: error: …` z linią źródła i strzałką, + po czym się zatrzymuje. Nie próbuje kontynuować po błędzie, więc dalsze etapy nigdy nie widzą częściowo poprawnego wejścia. +- **Jedno przejście.** `gen.fch` sprawdza typy i emituje IR w jednym przejściu po AST. Każde wyrażenie daje operand IR, + swój typ Fincha (rodzaj zapisany tekstem: `int`, `f32`, `str`, `array`, `map`, `struct`, `enum`, `vec`, `named`…) i flagi. +- **Resztę robi clang.** Zmienne lokalne to `alloca`, które `mem2reg` zamienia na rejestry. Optymalizuje `-O2` clanga + (albo `-O0` przy `-g`). +- **Wartości, nie referencje.** Tablice, teksty i struktury zachowują się jak wartości; o własności decyduje kompilator (§8). +- **Mały runtime w C** (`runtime/finch_rt.c`, ok. 1150 linii) robi to, co w IR byłoby żmudne: teksty, powiększanie tablic, + mapy, wczytywanie, pliki, błędy w trakcie działania. Kompiluje się go raz i trzyma w `~/.cache/finch`. --- -## 2. Drzewo źródeł +## 2. Pliki źródłowe -| Plik | Linie | Zadanie | +| Plik | Linie | Za co odpowiada | |---|---|---| -| `src/lexer.fch` | 470 | Tokeny i pozycje, tekst `"""`, `"\(x)"` zamienione na tokeny `("…" + str(x) + "…")` | -| `src/ast.fch` | 120 | Jedna struktura `Node` z napisem `kind`, dziećmi, nazwami typów, pozycjami (`line`, `endLine`) | -| `src/parser.fch` | 910 | Zejście rekurencyjne: deklaracje, instrukcje, wyrażenia, generyki (`[T]`), `enum`, `match`, `export`, `link`, `const`, `test "…" { }` | -| `src/gen.fch` | 6450 | Kontrola typów + IR: struktury, funkcje, metody, zakresy i zwolnienia, instrukcje, wyrażenia, wbudowane, mapy, błędy jako wartości, generyki (monomorfizacja), enumy i `match`, metadane `-g`, tryb `index` | -| `src/cimport.fch` | 790 | libclang: funkcje, struktury i unie (pola, offsety, wypełnienie), enumy, stałe, globalne, makra (także będące strukturą) | -| `src/platform.fch` | 90 | `Target`: triple, układ danych, kompilator C, znak `char` zależnie od systemu | -| `src/linker.fch` | 430 | Pamięć podręczna (runtime, obiekt programu, pliki C/C++), biblioteki (`link`, pkg-config), podpowiedzi przy błędach linkera | -| `src/main.fch` | 515 | Sterownik: CLI, loader z wyszukiwaniem modułów i nakładkami, `build/run/test/fmt/ir/index/lsp` | -| `src/manifest.fch` | 240 | `finch.toml`: mały czytnik TOML, listy zależne od celu | -| `src/fmt.fch` | 225 | `finch fmt` i formatowanie w LSP | -| `src/lsp.fch` | 910 | Serwer języka (§11) | -| `src/docs.fch` | 110 | Jeden opis wbudowanych funkcji dla podpowiedzi i uzupełniania | +| `src/lexer.fch` | 470 | Tokeny, pozycje, tekst `"""`, zamiana `"\(x)"` na tokeny `("…" + str(x) + "…")` | +| `src/ast.fch` | 170 | Jedna struktura `Node` z rodzajem (`kind`) jako tekstem, dziećmi, nazwami typów i pozycją (`line`, `endLine`); rozpoznawanie typów wektorowych | +| `src/parser.fch` | 910 | Zejście rekurencyjne: deklaracje, instrukcje, wyrażenia, typy generyczne (`[T]`), `enum`, `match`, `export`, `link`, `const`, `test "…" { }` | +| `src/gen.fch` | 7030 | Sprawdzanie typów i IR: struktury, funkcje, metody, zasięgi i zwalnianie, instrukcje, wyrażenia, funkcje wbudowane, mapy, błędy jako wartości, typy generyczne (monomorfizacja), enumy i `match`, wektory (SIMD), metadane debuggera (`-g`), tryb `index` | +| `src/cimport.fch` | 790 | libclang: funkcje, struktury i unie (pola, przesunięcia, wyrównanie), enumy, stałe, zmienne globalne, makra (także o wartości struktury) | +| `src/platform.fch` | 120 | `Target`: triple, układ danych, kompilator C, znak typu `char` na danym systemie | +| `src/linker.fch` | 480 | Pamięć podręczna (runtime, obiekt programu, pliki C/C++), biblioteki (`link`, pkg-config), podpowiedzi przy błędach linkera | +| `src/main.fch` | 530 | Sterownik: wiersz poleceń, wczytywanie modułów i nakładek, `build/run/test/fmt/ir/index/lsp` | +| `src/manifest.fch` | 240 | `finch.toml`: mały czytnik TOML, listy dla poszczególnych systemów | +| `src/fmt.fch` | 225 | `finch fmt` i formatowanie w serwerze języka | +| `src/lsp.fch` | 950 | Serwer języka (§11) | +| `src/docs.fch` | 140 | Jeden opis funkcji wbudowanych, wspólny dla podpowiedzi i dymków | | `std/*.fch` | 1040 | `json`, `path`, `time` (+`_windows`), `proc` (+`_windows`), `fs`, `thread` | -| `runtime/finch_rt.c` | 1150 | Runtime w C: tekst, tablice, mapy, pliki, wypisywanie liczb, fs, wątki, uruchamianie testów, łańcuch wywołań | -| `bootstrap/` | | `build.sh`, `install.sh`, ziarna (§10) | +| `runtime/finch_rt.c` | 1150 | Runtime w C: teksty, tablice, mapy, pliki, wypisywanie liczb, fs, wątki, uruchamianie testów, ścieżka wywołań | +| `bootstrap/` | | `build.sh`, `install.sh`, pliki startowe (§10) | --- -## 3. Front end: lekser, parser, moduły +## 3. Część frontowa: lekser, parser, moduły -**Lekser.** Koniec linii kończy instrukcję; wcięcia nic nie znaczą. Tokeny niosą plik, linię i kolumnę, z których korzysta każdy błąd. +**Lekser.** Koniec linii kończy instrukcję; wcięcia nie mają znaczenia. Każdy token zna plik, linię i kolumnę, z których +korzystają wszystkie komunikaty o błędach. -**Parser.** Zejście rekurencyjne, jedna funkcja na konstrukcję. Typy są parsowane do napisu (`[]str`, `map[str]int`, -`ptr[char]`, `Stack[int]`); `fn Typ.metoda` deklaruje metodę; `fn nazwa[T]` i `struct Nazwa[T]` to szablony; `enum` i `match` -mają własne węzły; `export fn` oznacza funkcję widoczną dla C; blok zapamiętuje `endLine` (dla serwera języka). Stara -kolejność `fn nazwa(...) typ` to błąd składni z podpowiedzią. +**Parser.** Zejście rekurencyjne, jedna funkcja na każdą konstrukcję. Warto wiedzieć, że: typy są parsowane do nazwy +(`[]str`, `map[str]int`, `ptr[char]`, `Stack[int]`, `f32x8`); `fn Typ.metoda` deklaruje metodę; `fn nazwa[T]` +i `struct Nazwa[T]` deklarują szablony; `enum` i `match` mają własne węzły; `export fn` oznacza funkcję widoczną z C; +blok zapamiętuje swoją `endLine` (korzysta z niej serwer języka). Stara kolejność `fn nazwa(...) typ` daje błąd z podpowiedzią. -**Moduły.** `import nazwa` szuka kolejno `nazwa_.fch` obok importującego pliku (`windows`, `linux`, `macos`), `nazwa.fch`, -katalogów z `FINCH_PATH`, potem biblioteki standardowej `std/`. Każdy moduł jest parsowany raz; jego nazwy są używane jako -`modul.nazwa`. `import "x.h"` trafia do importera C. +**Moduły.** `import nazwa` szuka po kolei: `nazwa_.fch` obok importującego pliku (`windows`, `linux`, `macos`), +`nazwa.fch` w tym samym miejscu, katalogów z `FINCH_PATH`, a na końcu biblioteki standardowej `std/`. Każdy moduł jest +parsowany raz; jego nazwy są dostępne jako `modul.nazwa`. `import "x.h"` trafia zamiast tego do importu z C. -Moduł rozpoznaje się po **pliku**: kluczem jest jego ścieżka od folderu głównego pliku (albo od folderu z `FINCH_PATH` -lub `std/`) bez `.fch` i bez końcówki `_`, np. `ui/przycisk`. `Program.imports` trzyma nazwę używaną w danym -pliku (`przycisk`, albo `p` przy `import ui/przycisk as p`), a `Program.modules` klucz; `modFor()` zamienia jedno na drugie. -Symbole używają klucza z `/` zamienionym na `$` (`@finch.ui$przycisk.rysuj`). +Moduł rozpoznaje się po **pliku**: kluczem jest jego ścieżka względem katalogu głównego pliku (albo katalogu z +`FINCH_PATH`, albo `std/`), bez `.fch` i bez końcówki `_`, np. `ui/button`. `Program.imports` przechowuje nazwę, +której używa dany plik (`button` albo `btn` przy `import ui/button as btn`), a `Program.modules` klucz; `modFor()` +przechodzi od jednego do drugiego. Symbole używają klucza, w którym `/` zamieniono na `$` (`@finch.ui$button.make`). -**Stałe.** `const NAZWA = wyrażenie` to węzeł najwyższego poziomu; `constValue()` liczy wyrażenie przy każdym użyciu, w -module, w którym je zdefiniowano, bez zmiennych lokalnych, więc wychodzi literał (operatory na literałach całkowitych -składa `arith`). `checkConst()` dopuszcza tylko literały, nazwy, operatory, pola i konwersje liczb; `constBusy` łapie pętle. +**Stałe.** `const NAZWA = wyrazenie` to węzeł najwyższego poziomu; `constValue()` oblicza wyrażenie przy każdym użyciu, +w module, w którym je zdefiniowano, bez zmiennych lokalnych, więc wynik zawsze składa się do literału (operatory na +literałach całkowitych są składane w `arith`). `checkConst()` dopuszcza tylko literały, nazwy, operatory, pola +i konwersje liczbowe; `constBusy` wykrywa cykle. -**Dodatki leksera.** `1e9` / `2.5e-3` to tokeny float (tekst dostaje `.` dla LLVM); teksty `"""` tracą wcięcie -linii zamykającej w `longString()`. +**Drobiazgi leksera.** `1e9` i `2.5e-3` to tokeny zmiennoprzecinkowe (tekst jest uzupełniany o `.`, którego wymaga LLVM); +z tekstu `"""` funkcja `longString()` usuwa wcięcie linii zamykającej. --- ## 4. Generowanie kodu -`gen.fch` to jedna duża struktura `Gen` i funkcje na niej. Przejścia: +`gen.fch` to jedna duża struktura `Gen` i funkcje, które na niej pracują. `generate()` wykonuje kolejno: -1. `declareStructs`: każda struktura Fincha i C dostaje typ LLVM (struktury C z prawdziwym wypełnieniem); rekurencja przez wartość jest odrzucana. -2. `declareFns`: sygnatury, z *analizą pożyczania* parametrów (parametr, który nigdzie nie jest zapisywany, jest przekazywany bez kopii). -3. `define`: każde ciało, sprawdzone i wyemitowane. Zakresy pamiętają, co zwolnić na końcu (`defer` działa pierwszy). -4. `defineMainWrapper`: C-owe `main(argc, argv)` i `finch_on_fatal` dla osadzających. -5. Z `-g` metadane DWARF dokładane są w trakcie pisania IR (używaj `-O0`). +1. `declare()`: każda struktura Fincha i C dostaje typ LLVM (struktury C z prawdziwym wyrównaniem; struktury, które + przez wartość zawierają same siebie, są odrzucane), a potem każda funkcja dostaje sygnaturę z *analizą pożyczania* + parametrów (§8.2). +2. `define()` dla każdej funkcji: ciało, ze sprawdzeniem typów i emisją IR. Zasięgi pamiętają, co trzeba zwolnić przy + wyjściu. Tworzenie instancji typów generycznych dokłada w tym czasie nowe funkcje, więc pętla trwa, dopóki pojawiają się nowe. +3. Punkt wejścia C, `main(argc, argv)` (a przy `--shared`: `finch_module_on_fatal` i opakowania funkcji `export fn`). +4. `assemble()`: deklaracje, typy struktur, stałe tekstowe, funkcje pomocnicze i ciała funkcji jako jeden tekst `.ll`. -Generyki są **monomorfizowane**: wywołanie `max(1, 2)` wnioskuje `T`, potem AST szablonu generowane jest ponownie z -przypisanym `T`, pod zmangowaną nazwą (`max$int`). `match` na enumie jest wyczerpujący (brak wariantu to błąd, `else` łapie -resztę); na wartości to ciąg porównań. Wartości enuma to `i32`; `Color(n)` jest sprawdzane; wypisywanie używa wygenerowanej tabeli nazw. +Przy `-g` metadane DWARF są dopisywane na bieżąco, w trakcie zapisywania IR (najlepiej razem z `-O0`). -Błędy jako wartości: `-> T!` zwraca parę (wartość, tekst błędu); `try` wraca wcześniej przy błędzie, `or` go obsługuje, `error("…")` go tworzy. +Typy generyczne są **monomorfizowane**: wywołanie `max(1, 2)` wyznacza `T`, a potem AST szablonu jest generowane jeszcze +raz z podstawionym `T`, pod nazwą `max[int]` (symbol `max.int`). `match` na enumie musi być wyczerpujący (brak wariantu +to błąd, `else` obejmuje resztę); na zwykłej wartości to ciąg porównań. Wartości enumów to `i32`; `Kolor(n)` sprawdza +zakres; do wypisywania służy wygenerowana tabela nazw. + +Błędy jako wartości: `-> T!` zwraca parę (wartość, tekst błędu); `try` przy błędzie wychodzi z funkcji, `or` go obsługuje, +`error("…")` go tworzy. --- -## 5. Import C i ABI C +## 5. Import z C i ABI języka C -`cimport.fch` woła **libclang** wprost z Fincha (sam jest napisany z `import "clang-c/Index.h"`), parsuje nagłówek z flagami -celu i przechodzi po kursorach. Typy: parametr `const char *` → `str`, inny `char *` → `ptr[char]`, wynik `char *` → `str`; -liczby całkowite według rozmiaru i znaku (`char` jest bez znaku na ARM64 Linux); struktury i enumy według układu. +`cimport.fch` woła **libclang** bezpośrednio z Fincha (sam jest napisany z `import "clang-c/Index.h"`), parsuje nagłówek +z flagami systemu docelowego i przechodzi po kursorach. Typy tłumaczy tak: parametr `const char *` → `str`, inne +`char *` → `ptr[char]`, wynik `char *` → `str`; liczby całkowite według rozmiaru i znaku (`char` jest bez znaku na +Linuksie ARM64); struktury i enumy według ich układu w pamięci. -LLVM IR nie realizuje za ciebie konwencji wywołań C, więc `gen.fch` ją odtwarza: +LLVM IR nie realizuje sam konwencji wywołań C, więc `gen.fch` odtwarza ją ręcznie: -- `classify`: System V x86-64: struktura do 16 bajtów idzie w rejestrach według klas ośmiobajtów (INTEGER/SSE), większa w pamięci; -- `classifyArm64`: AAPCS64, z jednorodnymi agregatami zmiennoprzecinkowymi (HFA); -- reguły Microsoft x64 (struktury 1, 2, 4, 8 bajtów w rejestrze, reszta przez wskaźnik); -- `makePlan` zamienia klasyfikację w plan wywołania; `cThunk` robi z `addr(fn)` coś, co C może wywołać (callbacki). - -**Makra.** `#define` będący jedną liczbą albo tekstem czyta się z tokenów. Taki, którego tokeny mogą być wyrażeniem -stałym (bez klamer, średników i słów instrukcji), liczy clang: drugie parsowanie nagłówka dodaje -`static const __auto_type __finch_m_NAZWA = (NAZWA);` dla każdego, a `clang_Cursor_Evaluate` daje liczbę całkowitą, -double albo tekst. Te, które nie są stałymi, dają tylko błędy w tym drugim parsowaniu i są pomijane. - -**Unie** to `CStruct` z `isUnion`: typ LLVM to `{ [n x iA] }` (rozmiar i wyrównanie od clanga), każde pole ma -przesunięcie 0, a pole czyta się przez pamięć. Nie da się ich przekazać do C przez wartość (`unsupported` mówi dlaczego). +- `classify`: System V x86-64; struktura do 16 bajtów idzie w rejestrach według klasy każdych 8 bajtów (INTEGER/SSE), większa przez pamięć; +- `classifyArm64`: AAPCS64, w tym jednorodne agregaty liczb zmiennoprzecinkowych (HFA) i wariant Apple; +- reguły Microsoft x64 (struktury 1, 2, 4 i 8-bajtowe przez wartość w rejestrze, pozostałe przez wskaźnik); +- `makePlan` zamienia klasyfikację w plan wywołania; `cThunk` sprawia, że `addr(fn)` da się wywołać z C (funkcje zwrotne). Ten sam kod pozwala kompilatorowi wołać libclang, którego API przekazuje `CXCursor`, `CXType` i `CXString` przez wartość. +**Makra.** `#define` z jedną liczbą albo jednym literałem tekstowym jest odczytywany z tokenów. Taki, którego tokeny +mogą tworzyć wyrażenie stałe (bez klamer, średników i słów kluczowych instrukcji), oblicza clang: drugie parsowanie +nagłówka dopisuje dla każdego `static const __auto_type __finch_m_NAZWA = (NAZWA);`, a `clang_Cursor_Evaluate` zwraca +liczbę całkowitą, liczbę typu double albo tekst. Makra, które nie są stałymi, dają w tym drugim parsowaniu tylko błędy +i są pomijane. + +**Unie** to `CStruct` z `isUnion`: typ LLVM to `{ [n x iA] }` (rozmiar i wyrównanie podaje clang), każde pole ma +przesunięcie 0, a pole czyta się przez pamięć. Nie da się ich przekazać do C przez wartość (pole `unsupported` mówi dlaczego). + --- -## 6. Sterownik, linkowanie i cele +## 6. Sterownik, linkowanie i systemy docelowe -`finch build|run|ir|index plik.fch [-o wyjście] [-O0] [-g] [-l biblioteka] [-I katalog] [--target cel] [--shared] [--overlay ścieżka=plik]`. -`ir` wypisuje IR; `index` wypisuje rekordy dla serwera języka (§11); `--shared` robi bibliotekę współdzieloną z `export fn`. +`finch build|run|ir|index plik.fch [-o wynik] [-O0] [-g] [-l bib] [-I katalog] [--target t] [--cpu c] [--shared] [--overlay sciezka=plik]`. +`ir` wypisuje IR; `index` wypisuje rekordy dla serwera języka (§11); `--shared` tworzy bibliotekę współdzieloną z funkcji +`export fn`. -`--target windows|linux|arm64|macos|android|` zmienia triple i układ danych, znak `char`, reguły ABI powyżej, wariant OS -modułów (`nazwa_windows.fch`) i kompilator C: `FINCH_CC` albo `clang --target=…`. Przy linkowaniu `pkg-config` podaje flagi -dla `link "biblioteka"`; gdy linker zawiedzie, wynik jest analizowany i podpowiadane jest brakujące `link "…"`. Obiekt -runtime'u jest kluczowany skrótem jego źródła i celu. +`--cpu ` (albo `cpu` w `finch.toml`) dodaje `-march=` na x86-64 albo `-mcpu=` na ARM64 do wywołania +clanga, które kompiluje IR (nie runtime); wartość wchodzi do klucza pamięci podręcznej. -Zmienne środowiskowe: `FINCH_PATH` (katalogi modułów), `FINCH_RT` (źródło runtime'u), `FINCH_CC` (kompilator/linker C; IR -zawsze kompiluje `clang`, nigdy `CC`), `FINCH_LIBCLANG` (dla `bootstrap/build.sh`). +`--target windows|linux|arm64|macos|android|` zmienia triple i układ danych, znak typu `char` w C, reguły ABI +opisane wyżej, wariant modułów dla systemu (`nazwa_windows.fch`) i kompilator C: `FINCH_CC` albo `clang --target=…`. +Przy `link "bib"` linkowanie pyta `pkg-config` o flagi; gdy linker zawiedzie, jego wyjście jest analizowane +i kompilator podpowiada brakujące `link "…"`. Obiekt runtime'u ma klucz z hasza swojego źródła i systemu docelowego. -Kompilator znajduje `runtime/` i `std/` obok siebie (`../`, `./`) albo w `../share/finch/` (`bootstrap/install.sh`). +Zmienne środowiskowe: `FINCH_PATH` (katalogi modułów), `FINCH_RT` (źródło runtime'u), `FINCH_CC` (kompilator C i linker; +IR zawsze kompiluje `clang`, nigdy `CC`), `FINCH_LIBCLANG` (dla `bootstrap/build.sh`). -**Pamięć podręczna budowania** (`host.cacheDir()`: `$XDG_CACHE_HOME/finch`, `~/.cache/finch`, `%LOCALAPPDATA%\finch`): +Kompilator szuka `runtime/` i `std/` obok siebie (`../`, `./`) albo w `../share/finch/` (`bootstrap/install.sh`). -- `ir/.o` z plikiem `.key`, w którym jest skrót clanga, flag i całego IR. Jedno miejsce - na program, więc cache nie rośnie z każdą zmianą; to samo IR daje ten sam obiekt bez uruchamiania clanga. -- `c/.o` dla `link "x.c"` / `.cpp`, budowane z `-MMD`; `.key` to skrót każdego - pliku z listy `.d` (źródła i jego nagłówków). Zmiana któregokolwiek przebudowuje obiekt. -- Obiekty są zapisywane do pliku `.part` i przemianowywane, więc dwa budowania naraz nie widzą pół pliku. - Skróty to dwa niezależne 32-bitowe (FNV-1a i djb2), razem 64 bity. +**Pamięć podręczna kompilacji** (`host.cacheDir()`: `$XDG_CACHE_HOME/finch`, `~/.cache/finch`, `%LOCALAPPDATA%\finch`): -**`finch.toml`** (`src/manifest.fch`) jest szukany od folderu pliku głównego (albo bieżącego) w górę. Trafia do -`values["sekcja.klucz"]` i `lists["sekcja.klucz"]`; `list(klucz, cele)` skleja listę z najwyższego poziomu i te z -`[target.]` oraz `[target.-]`. Ścieżki liczą się od folderu manifestu. Wpisy trafiają tam, gdzie opcje -z linii poleceń: `paths` → loader, `include` → `-I` (libclang i pliki C), `links` → `link`, `libdirs` i `ldflags` → -polecenie linkowania, `cflags` → pliki C/C++. +- `ir/.o` z plikiem `.key`, w którym jest hash clanga, flag i całego IR. Jedno + miejsce na program, więc pamięć nie rośnie przy każdej zmianie kodu; to samo IR daje ten sam obiekt bez uruchamiania clanga. +- `c/.o` dla `link "x.c"` / `.cpp`, budowany z `-MMD`; `.key` to hash + wszystkich plików wymienionych w `.d` (źródła i jego nagłówków). Zmiana któregokolwiek z nich wymusza ponowną kompilację. +- Obiekty są zapisywane do pliku `.part` i dopiero potem przemianowywane, więc dwie równoległe kompilacje nie + zobaczą połowy pliku. Hasze to dwa niezależne 32-bitowe (FNV-1a i djb2), razem 64 bity. -**Pliki C++**: `.cpp`/`.cc`/`.cxx` w `links` kompiluje `cxxFor(cc)` (`FINCH_CXX` albo `clang` → `clang++`, -`…gcc` → `…g++`, `cc` → `c++`), a potem ten sam sterownik linkuje, co dokłada standardową bibliotekę C++. +**`finch.toml`** (`src/manifest.fch`) jest szukany od katalogu głównego pliku (albo katalogu roboczego) w górę. +Trafia do `values["sekcja.klucz"]` i `lists["sekcja.klucz"]`; `list(klucz, systemy)` łączy listę z najwyższego poziomu +z listami z `[target.]` i `[target.-]`. Ścieżki są liczone względem katalogu manifestu. +Wpisy trafiają tam, gdzie opcje z wiersza poleceń: `paths` → wczytywanie modułów, `include` → `-I` (libclang i pliki C), +`links` → `link`, `libdirs` i `ldflags` → polecenie linkowania, `cflags` → pliki C/C++. + +**Pliki C++**: plik `.cpp`/`.cc`/`.cxx` z `links` kompiluje `cxxFor(cc)` (`FINCH_CXX` albo `clang` → `clang++`, +`…gcc` → `…g++`, `cc` → `c++`), a potem ten sam sterownik linkuje, co dołącza bibliotekę standardową C++. **Android**: `--target android` to `aarch64-linux-android24`; kompilator C to clang z NDK, -`toolchains/llvm/prebuilt//bin/aarch64-linux-android24-clang` (z `ANDROID_NDK_HOME`/`ANDROID_NDK_ROOT`), linkowanie -dodaje `-llog` i nie dodaje `-lpthread` (bionic ma go w libc). Wariant OS modułów to `linux`. +`toolchains/llvm/prebuilt//bin/aarch64-linux-android24-clang` (z `ANDROID_NDK_HOME`/`ANDROID_NDK_ROOT`); +linkowanie dodaje `-llog`, a nie dodaje `-lpthread` (bionic ma wątki w libc). Wariantem modułów jest `linux`. -**`finch test`** kompiluje jak `run`, z `Options.test`: wszystkie funkcje są dalej definiowane, ale `@main` wywołuje -`finch_test_run(nazwa, fn)` dla każdego bloku `test` z modułów użytkownika (`__test_`, funkcja `void` zrobiona przez -parser), a na końcu `finch_test_summary()`. Runtime uruchamia każdy test pod `setjmp`; `fatal()` (każdy błąd w czasie -działania i nieudany `assert`) wraca przez `longjmp` z `fatal_hook`, więc jeden nieudany test nie kończy całości. -Pamięć, którą test miał w tym momencie, nie jest zwalniana. +**`finch test`** kompiluje tak jak `run`, z `Options.test`: wszystkie funkcje są definiowane jak zwykle, ale `@main` woła +`finch_test_run(nazwa, fn)` dla każdego bloku `test` z modułów użytkownika (`__test_`, funkcja `void` utworzona przez +parser), a potem `finch_test_summary()`. Runtime uruchamia każdy test pod `setjmp`; `fatal()` (każdy błąd w trakcie +działania i nieudany `assert`) wraca przez `longjmp` z pomocą `fatal_hook`, więc jeden nieudany test nie przerywa reszty. +Pamięć, którą test miał w chwili błędu, nie jest zwalniana. + +**`finch fmt`** działa na liniach: śledzi `{ }`, `( )`/`[ ]`, teksty (razem z `\( )`), `'…'`, `"""` i `/* */`. +Linia, która zaczyna się na głębokości nawiasów 0 i nie kontynuuje linii zakończonej operatorem albo `,`, dostaje +wcięcie 4 × głębokość klamer; pozostałe (kontynuacje, wnętrza `"""`) przesuwają się o tyle samo co linia, od której +zaczęła się instrukcja, więc zachowują kształt (i usuwanie wcięcia z `"""`). -**`finch fmt`** działa na liniach: śledzi `{ }`, `( )`/`[ ]`, teksty (z `\( )`), `'…'`, `"""` i `/* */`. Linia, która -zaczyna się na głębokości nawiasów 0 i nie ciągnie linii zakończonej operatorem albo `,`, dostaje wcięcie 4 × głębokość -klamer; pozostałe (ciągi dalsze, wnętrza `"""`) przesuwa się o tyle samo co linię, która zaczęła instrukcję, więc ich -kształt (i ucinanie wcięcia `"""`) zostaje. --- ## 7. System typów @@ -215,55 +230,61 @@ kształt (i ucinanie wcięcia `"""`) zostaje. | `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; zob. src/gen.fch) | +| `map[K]V` | `{ ptr, i64, i64, i64, ptr, i64, i64 }` (wpisy, len, used, cap, indeks, icap, adds; zob. src/gen.fch) | | `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 (§5) | +| `f32x8`, `i8x32`, … | `<8 x float>`, `<32 x i8>` (§7, Wektory) | +| struktura Fincha | nazwany typ struktury, naturalny układ | +| `self` w metodzie | `ptr` (pierwszy parametr) | +| wynik `-> T!` | `{ i1 ok, T wartosc, str blad }` (`i8` zamiast `T` dla `-> !`) | +| struktura C | nazwana struktura **packed** z jawnymi bajtami wyrównania (§5) | -Znakowość istnieje tylko w typie Fincha i wybiera `sdiv/udiv`, `ashr/lshr`, `icmp s*/u*`, -`sext/zext`, `sitofp/uitofp`, `fptosi/fptoui`. +Znak liczby istnieje tylko w typie Fincha i wybiera `sdiv/udiv`, `ashr/lshr`, `icmp s*/u*`, `sext/zext`, +`sitofp/uitofp`, `fptosi/fptoui`. ### Wartości -```cpp -struct Value_ { Value *v; FType type; bool literal; bool fresh; }; +```c +struct Val { + str v // operand IR: %t12, 42, @.str.3, zeroinitializer + ast.Type ty + bool fresh + bool lit +} ``` -- **`literal`**: liczba wpisana w kod albo zaimportowana stała C. Może stać się dowolnym typem całkowitym, - w którym się mieści (`fits()`), albo `f32`. Przechodzi przez minus jednoargumentowy i arytmetykę samych - literałów zwijaną do stałej; jawne konwersje ją kasują (`u8(250)` to stała z typem, więc `u8(250) + 10` zawija się do 4). -- **`fresh`**: wartość posiadająca pamięć (str/tablica/struktura z takimi polami), której nikt jeszcze nie - trzyma: wynik wywołania, literał, sklejenie, konstruktor. Trzeba ją zapisać (przenieść) albo zwolnić (§8). +- **`lit`**: liczba zapisana w kodzie albo stała zaimportowana z C. Może stać się dowolnym typem całkowitym, + w którym się mieści (`fits()`), albo `f32`. Przetrwa jednoargumentowy minus i działania na samych literałach, + które składają się do stałej; jawne konwersje ją usuwają (`u8(250)` to stała z typem, więc `u8(250) + 10` daje 4). +- **`fresh`**: wartość posiadająca pamięć (str, tablica, struktura z takimi polami), której jeszcze nikt nie trzyma: + wynik wywołania, literał, połączenie tekstów, konstruktor. Trzeba ją zapisać (przenieść) albo zwolnić (§8). ### Konwersje niejawne: `coerce(v, want)` -Identyczne typy; `null` → dowolny wskaźnik; wskaźnik z typem ↔ `ptr` bez typu; całkowita → całkowita, -jeśli wartość to mieszczący się literał albo typ się rozszerza (`widens()`: ta sama znakowość i więcej bitów -albo bez znaku → ściśle większa ze znakiem); całkowita → zmiennoprzecinkowa; `f32` → `f64`; literał `f64` → `f32`. -Nic więcej: błąd podpowiada jawną konwersję. Typy posiadające pamięć konwertują się tylko na siebie. -Literały tablic biorą typ elementu z miejsca, do którego trafiają (`exprWant()`), więc `[]f32 xs = [1, 2.5]` działa. +Identyczne typy; `null` → dowolny wskaźnik; wskaźnik z typem ↔ `ptr` bez typu; liczba całkowita → całkowita, jeśli +to pasujący literał albo typ się „poszerza” (`widens()`: ten sam znak i więcej bitów, albo bez znaku → większy ze znakiem); +liczba całkowita → zmiennoprzecinkowa; `f32` → `f64`; literał `f64` → `f32`. Nic poza tym: komunikat błędu podpowiada +jawną konwersję. Typy posiadające pamięć przechodzą tylko w siebie. Literał tablicy bierze typ elementów od miejsca, +do którego trafia (`exprWant()`), więc `[]f32 xs = [1, 2.5]` działa. ### Argumenty operatorów: `unify(l, r)` -Strona-literał dopasowuje się do drugiej (także literał float obok `f32`: `x * 2.0` zostaje `f32`); w przeciwnym razie -rośnie strona, która się rozszerza; `i32` + `u32` to błąd; całkowita + zmiennoprzecinkowa → zmiennoprzecinkowa; -f32 + f64 → f64. Nie ma promocji całkowitych z C: `u8 + u8` zostaje `u8`. Dwa literały całkowite składają się w literał, -więc `FLAG_A | FLAG_B` dalej mieści się w `u64` albo `u32`. +Strona, która jest literałem, dopasowuje się do drugiej (także literał zmiennoprzecinkowy obok `f32`: `x * 2.0` +zostaje `f32`); w przeciwnym razie rośnie ta strona, którą da się poszerzyć; `i32` + `u32` to błąd; całkowita + +zmiennoprzecinkowa → zmiennoprzecinkowa; f32 + f64 → f64. Nie ma promocji liczb całkowitych jak w C: `u8 + u8` +zostaje `u8`. Dwa literały całkowite składają się w literał, więc `FLAG_A | FLAG_B` nadal mieści się w `u64` albo `u32`. ### Konwersje jawne: `convert()` | Z → Na | IR | |---|---| -| int/char ↔ int/char | `trunc`/`sext`/`zext` wg znakowości źródła | +| int/char ↔ int/char | `trunc`/`sext`/`zext` według znaku źródła | | bool → int | `zext` | -| float ↔ int | `fptosi`/`fptoui`, `sitofp`/`uitofp` | -| float ↔ float | `fpext`/`fptrunc` | +| zmiennoprzecinkowa ↔ całkowita | `fptosi`/`fptoui`, `sitofp`/`uitofp` | +| zmiennoprzecinkowa ↔ zmiennoprzecinkowa | `fpext`/`fptrunc` | | int → bool | `icmp ne 0` | | liczba/bool/char → str | `finch_str_from_int/uint/float/char/bool` (fresh) | -| str → int/float | `finch_str_to_int/float` (panic przy złym tekście) | +| str → int/float | `finch_str_to_int/float` (przy złym tekście błąd w trakcie działania) | | `[]u8`/`[]char` → str | `finch_str_from_bytes` | | ptr → str | `finch_str_from_c` (pożyczony, `cap = -1`) | | ptr ↔ ptr | nic | @@ -271,102 +292,133 @@ więc `FLAG_A | FLAG_B` dalej mieści się w `u64` albo `u32`. ### Porównania -Wskaźniki/null: tylko `icmp eq/ne ptr`. `str`: `finch_str_eq` (długość + `memcmp`) dla `==`/`!=`, -`finch_str_cmp` dla porządku. Liczby zmiennoprzecinkowe: `oeq olt ole ogt oge` i `une` dla `!=`. -Całkowite i znaki wg znakowości (char bez znaku). Bool: tylko równość. Tablice/struktury: błąd. +Wskaźniki i `null`: tylko `icmp eq/ne ptr`. `str`: `finch_str_eq` (długość + `memcmp`) dla `==`/`!=`, +`finch_str_cmp` dla kolejności. Liczby zmiennoprzecinkowe: `oeq olt ole ogt oge` oraz `une` dla `!=`. Liczby całkowite +i `char` według znaku (`char` bez znaku). Bool: tylko równość. Tablice i struktury: błąd. ---- +### Wektory + +`ast.vecType(nazwa)` rozpoznaje `x` (elementy `i8`…`u64`, `f32`, `f64`; N = 2…64; najwyżej 512 bitów) +i zwraca `Type(kind: "vec", elem: [element], count: N)`, czyli wektor LLVM ``. Parser przyjmuje taką nazwę +wszędzie tam, gdzie może stać typ; wywołanie `f32x8(…)` trafia do `vecMake()`. + +| Finch | IR | +|---|---| +| `f32x8(x)` | `insertelement` do elementu 0 + `shufflevector … zeroinitializer` (rozpowielenie) | +| `f32x8(a, …, h)` | ciąg `insertelement` | +| `f32x8(lista, i)` | sprawdzenie zakresu `i … i+7`, potem `load <8 x float>, ptr …, align 4` (wyrównanie elementu) | +| `f32x8(i32x8 v)` | `sitofp`, `fptosi`, `sext`, `trunc`… na całym wektorze | +| `v + w`, `v * 2.0` | `fadd`/`add`… na wektorach; argument skalarny jest dopasowywany do typu elementu i rozpowielany | +| `v[i]` | miejsce w pamięci: `getelementptr , ptr %v, i64 i` po sprawdzeniu zakresu; albo `extractelement` na wartości tymczasowej | +| `v == w` | `fcmp oeq`/`icmp eq` → `` → `llvm.vector.reduce.and` | +| `min`, `max`, `abs` | `llvm.minnum`/`maxnum`, `llvm.smin`/`umin`…, `llvm.fabs`/`llvm.abs` | +| `v.sum()`, `.min()`, `.max()` | `llvm.vector.reduce.fadd` (z `reassoc`), `.add`, `.fmin`, `.smin`… | +| `v.sqrt()`, `.floor()`, `.ceil()`, `.round()` | funkcja wewnętrzna `llvm.*` o tej nazwie | +| `v.fma(b, c)` | `llvm.fmuladd` (jedna instrukcja tam, gdzie procesor ma FMA; `llvm.fma` bez FMA wołałoby funkcję z libm dla każdego elementu) | +| `v.store(lista, i)` | sprawdzenie zakresu, `store <8 x float> …, align 4` | + +Dzielenie całkowite `/` i `%` jest odrzucane: ani x86, ani ARM nie mają instrukcji dzielącej liczby całkowite element +po elemencie, więc LLVM i tak rozbiłby je na pojedyncze dzielenia. Porównania kolejności są odrzucane, bo ich wynikiem +byłaby maska, dla której Finch nie ma typu. + +**Wyrównanie.** LLVM daje `<8 x float>` wyrównanie ABI równe 32 (jego rozmiarowi), a `malloc` i finchowe `alloca … align 16` +gwarantują tylko 16. `llty()` zapisuje każdy wektor szerszy niż 16 bajtów w `g.wide`, a `resolveStructBody` zapisuje tam +strukturę, która taki wektor zawiera; `load()` i `store()` dodają dla tych typów `, align 16`. Bez tego LLVM mógłby +użyć `vmovaps` na adresie wyrównanym do 16 i program by się wysypał. + +**Jakie instrukcje** wybiera clang: `--cpu ` dodaje `-march=` (x86-64) albo `-mcpu=` (ARM64) +do kompilacji IR, więc to samo IR staje się kodem SSE2, AVX2 + FMA, AVX-512 albo NEON. Wektory szersze niż rejestry +procesora są dzielone na części. --- ## 8. Własność i pamięć -### 9.1 Model +### 8.1 Model -Każda wartość **typu posiadającego pamięć** (`str`, `[]T` albo struktura Fincha z takim polem) ma -dokładnie jednego właściciela: zmienną, element tablicy, pole struktury albo komórkę na stercie z `new()`. -Właściciel zwalnia wartość, gdy sam znika. Nie ma liczenia referencji ani garbage collectora; -każda decyzja zapada statycznie. +Każda wartość **typu posiadającego pamięć** (`str`, `[]T` albo struktura Fincha z takim polem) ma dokładnie jednego +właściciela: zmienną, element tablicy, pole struktury albo komórkę na stercie z `new()`. Właściciel zwalnia wartość, +gdy sam znika. Nie ma liczenia referencji ani garbage collectora; wszystkie decyzje zapadają przy kompilacji. | Sytuacja | Co się dzieje | |---|---| -| `x := expr` / `T x = expr` / `x = expr` / pole, element, `push` | **`own(v)`**: wartość fresh jest przenoszona; pożyczona jest kopiowana w głąb | -| `x = …` (typ posiadający) | najpierw liczona jest nowa wartość, stara zwalniana po zapisie | -| koniec bloku, `return`, `break`, `continue` | posiadane zmienne każdego opuszczanego zasięgu są zwalniane (od najnowszej), uruchamiają się defery | -| `return lokalna` | zmienna jest **przenoszona na zewnątrz** (oznaczana jako `moved` na czas sprzątania tego returna) | -| przekazanie argumentu | **pożyczka**: funkcja dostaje płytką kopię i nigdy jej nie zwalnia; argumenty fresh zwalnia wywołujący po wywołaniu | -| parametr, który funkcja zmienia | kopiowany na wejściu i posiadany przez funkcję (decyduje `mutates()`, niżej) | -| 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ść | +| `x := wyr` / `T x = wyr` / `x = wyr` / pole, element, `push` | **`own(v)`**: wartość świeża zostaje przeniesiona, pożyczona jest głęboko kopiowana | +| `x = …` (typ posiadający) | najpierw liczona jest nowa wartość, a stara jest zwalniana po zapisie | +| koniec bloku, `return`, `break`, `continue` | zmienne we wszystkich opuszczanych zasięgach są zwalniane (od najnowszej), wykonują się `defer` | +| `return zmienna_lokalna` | zmienna jest **przenoszona** (oznaczona jako `moved` na czas sprzątania przy tym `return`) | +| przekazanie argumentu | **pożyczka**: funkcja dostaje płytką kopię i nigdy jej nie zwalnia; świeże argumenty zwalnia wywołujący po wywołaniu | +| parametr, który funkcja zmienia | kopiowany na wejściu i należący do funkcji (decyduje `mutates()`, niżej) | +| wartość tymczasowa, której nikt nie zatrzymuje (`print(a + b)`, `f(g())`, wyrażenie jako instrukcja) | **`release(v)`**: zwalniana zaraz po użyciu | +| `a.pop()`, `a.remove(i)` | element jest przenoszony na zewnątrz (świeży) | +| `for x in lista` | `x` pożycza kolejne elementy; jeśli ciało pętli może zmienić `lista`, elementy są kopiowane | +| wywołanie metody `v.m()` | `self` **to jest** `v` (przekazywany jest 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 albo kopiuje `v` i zwalnia starą wartość | -Literał tekstowy to wartość fresh z `cap = 0`: przeniesienie nic nie kosztuje, zwalnianie jest pomijane +Literał tekstowy to świeża wartość z `cap = 0`: przeniesienie go 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`). -Teksty z C są pożyczane z `cap = -1` i zawsze kopiowane, zanim zostaną zatrzymane. +Teksty z C są pożyczone z `cap = -1` i zawsze kopiowane, zanim zostaną zatrzymane. -### 9.2 Pożyczanie parametrów: `mutates()` +### 8.2 Pożyczanie parametrów: `mutates()` -Przy deklarowaniu funkcji każdy parametr posiadający pamięć jest sprawdzany przez `mutates(nazwa, ciało)`: -przypisanie, którego zmienna-korzeń (`rootVar()`: przez `.pole` i `[i]`) jest tym parametrem, metoda -zmieniająca (`push pop insert remove clear resize sort reverse`) albo `addr(…)` na nim. Niezmieniane -parametry są pożyczane (`Fn::borrowParam`), zmieniane kopiowane raz na wejściu. Analiza jest składniowa -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. +Przy deklarowaniu funkcji każdy parametr typu posiadającego jest sprawdzany przez `mutates(nazwa, cialo)`: przypisanie, +którego zmienną bazową (`rootVar()`: przez `.pole` i `[i]`) jest ten parametr, zmieniająca metoda na nim +(`push pop insert remove clear resize sort reverse`) albo `addr(…)` na nim. Niezmieniane parametry są pożyczane +(`FnSig.borrow`); zmieniane są kopiowane raz, na wejściu. Analiza jest składniowa i ostrożna: wszystko, co *mogłoby* +zmienić parametr, powoduje kopię, a to 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. +Metody też w tym uczestniczą: `declare()` najpierw ustala, które metody zmieniają `self` (`mutates("self", cialo)`), +powtarzając to, dopóki coś się zmienia, bo wywołanie zmieniającej metody na `self` też zmienia `self`. Ich nazwy trafiają +do `g.mm`, które `mutates()` traktuje jak `push`: `p.przesun(1)` zmienia `p`. Dopiero potem dla każdej funkcji i metody +liczone jest `borrow`. -### 9.3 Kopiowanie i zwalnianie +### 8.3 Kopiowanie i zwalnianie -`dropAt(addr, T)` / `copyAt(dst, src, T)` działają na adresach: +`dropAt(adres, T)` / `copyAt(cel, zrodlo, T)` działają na adresach: - `str`: `finch_str_drop` (zwalnia, gdy `cap > 0`) / `finch_str_copy` (literały współdzieli, resztę duplikuje). -- `[]T` i struktury posiadające: generowane helpery `finch.drop.` i `finch.copy.`, tworzone - raz na typ (`dropFn`, `copyFn`, klucz z `typeKey()`, np. `arr_S_main_Gracz`). Tablice zwalniają każdy posiadający - element, a potem `finch_arr_free`; kopie wołają `finch_arr_clone_raw`, a potem kopiują w głąb posiadające - elementy na surowe bity. Struktury zwalniają/kopiują tylko pola posiadające. +- `[]T` i struktury posiadające pamięć: wygenerowane funkcje pomocnicze `finch.drop.` i `finch.copy.`, + tworzone raz na typ (`dropFn`, `copyFn`, klucz z `typeKey()`, np. `arr_S_main_Player`). Tablica zwalnia każdy + element posiadający pamięć, a potem woła `finch_arr_free`; kopia woła `finch_arr_clone_raw`, a potem głęboko kopiuje + takie elementy w miejsce surowych bitów. Struktury zwalniają i kopiują tylko pola posiadające pamięć. -Helpery są emitowane we własnych funkcjach (`HelperScope` zapamiętuje pozycję buildera i wyłącza lokacje -debugowe), mogą wołać się rekurencyjnie (`struct Wezel { []Wezel dzieci }`) i są wstawiane (inline) przez O2. +Funkcje pomocnicze powstają jako osobne funkcje (`beginHelper()` / `endHelper()` odkładają na bok funkcję, która jest +w trakcie pisania, i wyłączają lokalizacje debuggera), mogą wołać się nawzajem rekurencyjnie (`struct Node { []Node kids }`), +a O2 je wkleja w miejsce wywołań. -### 9.4 Jak to wygląda +### 8.4 Jak to wygląda ```c -fn greet(str name) -> str { - msg := "Hi, " + name - return msg +fn przywitaj(str imie) -> str { + wiad := "Cześć, " + imie + return wiad } fn main() { a := ["x"] b := a - b.push(greet("Ola")) + b.push(przywitaj("Ola")) print(b[1]) } ``` -IR funkcji `main` z `-O0` (skrócony): +IR funkcji `main` przy `-O0` (w skrócie): ```llvm %3 = call ptr @finch_alloc(i64 24) ; ["x"]: jeden element na stercie store { ptr, i64, i64 } { ptr @str.1, i64 1, i64 0 }, ptr %4 ; literał, cap 0 ... - store { ptr, i64, i64 } %7, ptr %a ; przeniesione do a (fresh) + store { ptr, i64, i64 } %7, ptr %a ; przeniesione do a (świeże) %a1 = load { ptr, i64, i64 }, ptr %a store { ptr, i64, i64 } %a1, ptr %1 call void @finch.copy.arr_str(ptr %2, ptr %1) ; b := a kopiuje %8 = load { ptr, i64, i64 }, ptr %2 store { ptr, i64, i64 } %8, ptr %b - %9 = call { ptr, i64, i64 } @finch.greet({ ptr, i64, i64 } { ptr @str.2, i64 3, i64 0 }) + %9 = call { ptr, i64, i64 } @finch.przywitaj({ ptr, i64, i64 } { ptr @str.2, i64 3, i64 0 }) ... - call void @finch_arr_reserve(ptr %b, i64 %12, i64 24) ; push: powiększ, jeśli trzeba - store { ptr, i64, i64 } %9, ptr %15 ; wynik greet przeniesiony, bez kopii + call void @finch_arr_reserve(ptr %b, i64 %12, i64 24) ; push: powiększenie, jeśli trzeba + store { ptr, i64, i64 } %9, ptr %15 ; wynik przywitaj przeniesiony, bez kopii ... - %20 = icmp uge i64 1, %19 ; b[1]: kontrola zakresu + %20 = icmp uge i64 1, %19 ; b[1]: sprawdzenie zakresu br i1 %20, label %out.of.range, label %in.range ... call void @finch.drop.arr_str(ptr %b) ; koniec main: od najnowszej @@ -374,103 +426,101 @@ IR funkcji `main` z `-O0` (skrócony): ret void ``` -a `greet` zwraca `msg` bez kopiowania (`ret … %5` po wczytaniu slotu: zmienna została przeniesiona). +a `przywitaj` zwraca `wiad` bez kopiowania (`ret … %5` po wczytaniu zmiennej: została przeniesiona). -### 9.5 defer +### 8.5 defer -`defer instrukcja` dokłada do bieżącego zasięgu `Cleanup{isDefer, body, order = bieżące varOrder}` -i niczego w tym miejscu nie emituje. Za każdym razem, gdy emitowane jest sprzątanie zasięgu (dojście do -końca bloku, `return`, wyjście przez `break`/`continue`), ciało jest generowane **w tym wyjściu**, z -`deferLimit = order`, żeby `lookup()` ukrył zmienne zadeklarowane po defer: te są wtedy już zwolnione -(sprzątanie idzie w odwrotnej kolejności). `return`/`break`/`continue` w instrukcji defer to błędy (`inDefer`). -Ciała defer są więc powielane na każdą ścieżkę wyjścia, przez co na zwykłej ścieżce nic nie kosztują. +`defer instrukcja` zapisuje instrukcję w bieżącym zasięgu (`Scope.defers`, razem z `orders`: ile zmiennych wtedy +istniało) oraz `Cleanup{isDefer}`; w tym miejscu nic nie emituje. Ilekroć emitowane jest sprzątanie zasięgu (koniec +bloku, `return`, wyjście przez `break`/`continue`), ciało jest generowane **przy tym wyjściu**, z `deferLimit` +ustawionym tak, by `lookup()` ukrywało zmienne zadeklarowane po `defer`: w tym momencie są już zwolnione (sprzątanie +idzie od końca). `return`/`break`/`continue` wewnątrz odroczonej instrukcji to błąd (`inDefer`). Ciała `defer` są więc +powielane na każdą drogę wyjścia i dlatego na zwykłej ścieżce nic nie kosztują. -### 9.6 Pamięć ręczna +### 8.6 Pamięć zarządzana ręcznie -`new(v)` alokuje `sizeof(T)` przez `finch_alloc`, zapisuje `own(v)` i zwraca `ptr[T]`. `free(p)` zwalnia -wskazywaną wartość, jeśli `T` posiada pamięć (pomijając null), i woła `free` z libc. Wskaźniki nigdy -niczego nie posiadają: `addr(x)` może wisieć po końcu bloku `x`; nie jest to sprawdzane. +`new(v)` przydziela `sizeof(T)` przez `finch_alloc`, zapisuje `own(v)` i zwraca `ptr[T]`. `free(p)` zwalnia to, na co +wskazuje wskaźnik, jeśli `T` posiada pamięć (pomija `null`), i woła `free` z libc. Wskaźniki nigdy niczego nie posiadają: +`addr(x)` może wskazywać w próżnię po końcu bloku `x`; tego się nie sprawdza. -### 9.7 Weryfikacja +### 8.7 Sprawdzanie `MEMCHECK=1 tests/run.sh` buduje każdy program testowy i uruchamia go pod valgrindem z -`--leak-check=full --errors-for-leak-kinds=all`: zero wycieków i zero nieprawidłowych dostępów we wszystkich, -w tym przy returnach z wnętrza pętli, `continue`/`break` z posiadanymi zmiennymi, deferach, modyfikacji -podczas iteracji, zagnieżdżonych tablicach struktur i przekazywaniu struktur C. +`--leak-check=full --errors-for-leak-kinds=all`: zero wycieków i zero błędnych dostępów we wszystkich, w tym przy +`return` z wnętrza pętli, `continue`/`break` ze zmiennymi posiadającymi pamięć, `defer`, zmianach w trakcie iteracji, +zagnieżdżonych tablicach struktur i przekazywaniu struktur C. --- ---- +## 9. Typy generyczne, enumy i match -## 9. Generyki, enumy i match - -Jak są generowane, opisuje §4. Reguły widoczne dla użytkownika: sekcje 23 i 24 [przewodnika dla techników](dla-technikow.md). -Testy: `tests/new/generics.fch`, `tests/new/enums.fch` i odmowy w `tests/new/fail/`. +Jak są generowane, opisuje §4. Reguły widoczne dla użytkownika są w rozdziałach 23 i 24 +[przewodnika dla techników](dla-technikow.md). Testy to `tests/new/generics.fch`, `tests/new/enums.fch` i oczekiwane +błędy w `tests/new/fail/`. --- ## 10. Bootstrap -Kompilator budowany jest z **ziarna**: własnego IR zapisanego jako `bootstrap/.ll` (zobacz -[bootstrap/README.md](../../bootstrap/README.md)). `bootstrap/build.sh`: +Kompilator powstaje z **pliku startowego** (ang. *seed*): jego własnego IR, zapisanego w repozytorium jako +`bootstrap/.ll` (zob. [bootstrap/README.md](../../bootstrap/README.md)). `bootstrap/build.sh` robi: ``` -ziarno .ll ──clang──► finch0 ──buduje src/──► finch1 ──buduje src/──► build/finch +plik .ll ──clang──► finch0 ──buduje src/──► finch1 ──buduje src/──► build/finch ``` -i przerywa, jeśli `finch1` i `finch` nie dają identycznego IR (punkt stały: kompilator kompiluje siebie tak samo, niezależnie -od tego, kto go zbudował). `bootstrap/build.sh seed` przepisuje ziarno z bieżącego kompilatora; rób to, gdy źródło -kompilatora zacznie używać czegoś, czego zatwierdzone ziarno nie ma. +i przerywa, jeśli `finch1` i `finch` nie dają identycznego bajt w bajt IR (punkt stały: kompilator kompiluje sam siebie +tak samo, niezależnie od tego, co go zbudowało). `bootstrap/build.sh seed` zapisuje plik startowy na nowo z bieżącego +kompilatora; trzeba to zrobić, gdy kod kompilatora zaczyna używać czegoś, czego zapisany plik startowy nie zna. -Ziarno jest na system, bo zapieczono w nim konwencję wywołań C dla wywołań libclang (§5). Linux x86-64 i Linux ARM64 mają -ziarna. Dla macOS i Windowsa trzeba by ziaren zrobionych z nagłówkami Apple i Microsoftu. +Plik startowy jest osobny dla każdego systemu, bo zawiera konwencję wywołań C dla funkcji libclang (§5). Są pliki +dla Linuksa x86-64 i ARM64, macOS ARM64 i x86-64 (zrobione z nagłówkami SDK Apple) oraz Windowsa x86-64 (z nagłówkami +llvm-mingw). CI na każdym z tych systemów buduje z nich kompilator i uruchamia tam testy. --- ## 11. Serwer języka -`finch lsp` mówi JSON-RPC przez stdin/stdout (ramki `Content-Length`, `std/json`). Sam nie sprawdza typów: przy każdej zmianie -uruchamia `finch index plik.fch --overlay ścieżka=kopia …`, gdzie nakładki to niezapisany tekst edytora. W trybie `index` -kompilator wypisuje rekordy rozdzielone tabulatorami: +`finch lsp` rozmawia przez JSON-RPC na stdin/stdout (ramki `Content-Length`, `std/json`). Sam nie sprawdza typów: +przy każdej zmianie uruchamia `finch index plik.fch --overlay sciezka=kopia …`, gdzie nakładki to niezapisany tekst +z edytora. W trybie `index` kompilator zamiast IR wypisuje rekordy rozdzielone tabulatorami: | Rekord | Znaczenie | |---|---| -| `@R plik linia kol dł plikDef liniaDef kolDef podpowiedź` | odwołanie: co pokazuje hover i gdzie jest definicja | +| `@R plik linia kol dlug plikDef liniaDef kolDef dymek` | odwołanie: co pokazać w dymku i gdzie jest definicja | | `@V nazwa typ plik linia kol plikFn liniaFn koniecFn` | zmienna i funkcja, w której żyje | -| `@F moduł nazwa odb plik linia kol koniec sygnatura parametry` | funkcja lub metoda | +| `@F modul nazwa odbiorca plik linia kol koniec sygnatura parametry` | funkcja albo metoda | | `@S` / `@SF`, `@E`, `@I` | struktury (i pola), enumy, importy | +| `@W` | ostrzeżenie | -Pierwszy błąd wypisywany jest jako `ścieżka:linia:kol: error: komunikat` i staje się diagnostyką. Gdy kod się nie kompiluje, -serwer podpowiada dalej z ostatniej wersji, która się skompilowała. Test akceptacyjny: `tests/lsp_test.py` (22 sprawdzenia). +Pierwszy błąd jest wypisywany jako `sciezka:linia:kol: error: komunikat` i staje się diagnostyką. Gdy kod się nie +kompiluje, serwer nadal podpowiada uzupełnienia i parametry na podstawie ostatniej wersji, która się kompilowała. +Testem akceptacyjnym jest `tests/lsp_test.py`. --- -## 12. Semantyka a C +## 12. Semantyka w porównaniu z C -| Sytuacja | C | Finch 2.0 | +| Sytuacja | C | Finch | |---|---|---| -| Przepełnienie liczby ze znakiem | UB | zdefiniowane zawinięcie (bez `nsw`) | -| Dzielenie całkowite przez zero, `INT_MIN / -1` | UB | błąd kompilacji, jeśli stałe, inaczej błąd w czasie działania | -| Indeks tablicy poza zakresem | UB | błąd w czasie działania (także dla pól `[N]T` struktur C) | -| Dereferencja null | UB | błąd w czasie działania dla `.value`, `.pole`, `[i]` | +| Przepełnienie liczby całkowitej ze znakiem | UB | zdefiniowane zawinięcie (bez `nsw`) | +| Dzielenie całkowite przez zero, `INT_MIN / -1` | UB | błąd kompilacji, jeśli to stała; w przeciwnym razie błąd w trakcie działania | +| Indeks tablicy poza zakresem | UB | błąd w trakcie działania (także dla pól `[N]T` struktur C) | +| Dereferencja `null` | UB | błąd w trakcie działania dla `.value`, `.pole`, `[i]` | | Użycie po zwolnieniu / podwójne zwolnienie tablic, tekstów, struktur | częste błędy | niemożliwe: tych nie zwalnia się ręcznie | -| Wyciek tablic, tekstów, struktur | częsty błąd | niemożliwy bez `new` | +| Wyciek pamięci tablic, tekstów, struktur | częsty błąd | niemożliwy bez `new` | | `x & 1 == 0` | `x & (1 == 0)` | `(x & 1) == 0` | -| Niejawne zwężanie, mieszanie ze znakiem / bez znaku | po cichu | błędy kompilacji | -| Niezainicjalizowane zmienne | UB | niemożliwe (zero albo wartości domyślne) | -| Brak return | UB, jeśli użyty | błąd kompilacji | -| Kolejność obliczeń | w większości nieokreślona | od lewej do prawej; w przypisaniu najpierw prawa strona | -| Przesunięcie ≥ szerokość, float→int poza zakresem | UB | nadal poison (jeszcze niesprawdzane) | +| Niejawne zwężanie, mieszanie liczb ze znakiem i bez | po cichu | błędy kompilacji | +| Niezainicjowane zmienne | UB | niemożliwe (zero albo wartości domyślne) | +| Brak `return` | UB, jeśli wynik jest użyty | błąd kompilacji | +| Kolejność obliczania | w dużej mierze nieokreślona | od lewej do prawej; w przypisaniu najpierw prawa strona | +| Przesunięcie o ≥ szerokość, liczba zmiennoprzecinkowa poza zakresem przy zamianie na całkowitą | UB | poison, jak w LLVM (niesprawdzane) | | Wiszący wskaźnik z `addr()` / `new` + `free` | UB | nadal UB | ---- - ---- - -### Łańcuch wywołań przy błędach w czasie działania +### Ścieżka wywołań przy błędach w trakcie działania Z `Options.trace` (domyślnie dla `run`, `test`, `-g`, `-O0`; `--trace`/`--no-trace`; nigdy z `--shared`) każda funkcja -Fincha ma ramkę na swoim stosie: +Fincha ma na stosie ramkę: ```llvm %frame = alloca { ptr, ptr, ptr, i64 } ; ramka wywołującego, nazwa, plik, linia @@ -479,45 +529,44 @@ store ptr @"nazwa", ...pole 1 ; ptr @"plik", pole 2 ; i64 , pole 3 store ptr %frame, ptr @finch_trace ``` -`@finch_trace` to globalna `thread_local` w module programu (model initial-exec, chyba że biblioteka albo Windows); -każda instrukcja zapisuje swoją linię do pola 3. Po każdym wywołaniu funkcji Fincha wywołujący wpisuje z powrotem swoją -ramkę do `@finch_trace`, co sprząta też ramki funkcji, która wróciła wcześniej, więc wiele ścieżek `return` nie -potrzebuje epilogu. Opakowania wołane z C (thunki `addr(fn)`, `export fn`) zapamiętują i przywracają `@finch_trace`. -`main` woła `finch_trace_init(top, set)` z dwiema małymi funkcjami do czytania i zapisu, i tak runtime (`print_trace()` -w `fatal()` oraz uruchamianie testów) dostaje się do zmiennej wątku, której nie zna z nazwy. Koszt: ok. 3 razy na -`fib(40)`, który jest samymi wywołaniami; dlatego zwykły `finch build` go pomija. +`@finch_trace` to globalna zmienna `thread_local` w module programu (model initial-exec, chyba że to biblioteka +albo Windows); każda instrukcja zapisuje swoją linię do pola 3. Po każdym wywołaniu funkcji Fincha wywołujący zapisuje +z powrotem swoją ramkę do `@finch_trace`, co przy okazji usuwa ramki pozostawione przez funkcję, która wyszła wcześniej, +więc na licznych ścieżkach powrotu nie trzeba nic sprzątać. Opakowania wywoływane z C (`addr(fn)`, `export fn`) +zapamiętują i odtwarzają `@finch_trace` wokół wywołania. `main` woła `finch_trace_init(top, set)` z dwiema małymi +funkcjami, które ją czytają i zapisują; tak runtime (`print_trace()` w `fatal()` i uruchamianie testów) dostaje się do +zmiennej `thread_local`, której nie potrafi nazwać. Koszt: ok. 3 razy wolniej na `fib(40)`, które składa się wyłącznie +z wywołań; dlatego zwykłe `finch build` tego nie dodaje. ### Wątki i operacje atomowe -`std/thread.fch` to cienka warstwa nad runtime'em (`finch_thread_spawn/join`, `finch_mutex_*`: pthreads albo -`CreateThread` i `CRITICAL_SECTION` na Windowsie). Funkcja wątku to dowolna `fn f(ptr)` podana przez `addr(f)`, co robi -thunk wołany z C. `atomic_add/sub/swap` to `atomicrmw add/sub/xchg … seq_cst`, `atomic_load/store` to -`load/store atomic … seq_cst, align `, a `atomic_cas` to `cmpxchg … seq_cst seq_cst` zwracające `i1`. Biorą -`ptr[T]` do dowolnej liczby całkowitej. Tablice, teksty i mapy Fincha nie są synchronizowane; dzielenie ich między -wątkami wymaga mutexu. +`std/thread.fch` to cienka warstwa nad runtime'em (`finch_thread_spawn/join`, `finch_mutex_*`: pthreads albo, na +Windowsie, `CreateThread` i `CRITICAL_SECTION`). Funkcja wątku to dowolna `fn f(ptr)`, przekazywana przez `addr(f)`, +co tworzy opakowanie wywoływalne z C. `atomic_add/sub/swap` stają się `atomicrmw add/sub/xchg … seq_cst`, +`atomic_load/store` to `load/store atomic … seq_cst, align `, a `atomic_cas` to `cmpxchg … seq_cst seq_cst` +zwracające `i1`. Przyjmują `ptr[T]` do dowolnego typu całkowitego. Tablice, teksty i mapy Fincha nie są synchronizowane; +dzielenie ich między wątkami wymaga muteksu. ### Ostrzeżenia -`warn()` wypisuje `plik:linia:kolumna: warning: …` na stderr (więc `finch ir` daje samo IR) albo, w trybie `index`, -rekord `@W`, który serwer języka publikuje z ważnością 2. Jedno na miejsce w kodzie. Są dwa: parametr-struktura oraz -element `for x in lista`, gdy ich zwykłe dane (`byteSize()`: liczby, zagnieżdżone struktury, `[N]T` z C; `str`, tablice -i mapy liczą się jako 0, bo kopiowany jest tylko ich uchwyt) mają 256 bajtów lub więcej. +`warn()` wypisuje `plik:linia:kol: warning: …` na stderr (żeby `finch ir` pozostało czystym IR) albo, w trybie `index`, +rekord `@W`, który serwer języka publikuje z ważnością 2. Jedno ostrzeżenie na miejsce w kodzie. Są dwa rodzaje: +parametr będący strukturą oraz element w `for x in lista`, jeśli ich zwykłe dane (`byteSize()`: liczby, zagnieżdżone +struktury, `[N]T` z C; `str`, tablice i mapy liczą się jako 0, bo kopiuje się tylko ich uchwyt) mają co najmniej 256 bajtów. -### Struktura jako argument przez adres +### Struktury przekazywane przez adres -`argWant()` (tylko argumenty wywołań, nie przypisania): gdy parametr to `ptr[S]`, a argument to zwykła zmienna typu `S`, -argumentem jest adres zmiennej, jakby napisano `addr(x)`. Tylko zmienne, więc nic nie jest liczone dwa razy, a na -wartości tymczasowe nic nie wskazuje. +`argWant()` (tylko argumenty wywołań, nie przypisania): gdy parametr ma typ `ptr[S]`, a argument jest zwykłą zmienną +typu `S`, argumentem staje się adres tej zmiennej, tak jakby napisano `addr(x)`. Dotyczy to tylko zmiennych, więc nic +nie jest obliczane dwa razy, a wskaźnik nigdy nie wskazuje na wartość tymczasową. + +--- ## 13. Znane ograniczenia -- Kompilator działa tylko na Linuksie x86-64 i ARM64 (brak ziarna dla macOS i Windowsa). Programy można budować na wszystkie trzy. -- Wynik dla macOS jest sprawdzany tylko jako IR; Windows i ARM64 uruchamiane są w CI pod Wine i qemu. -- Jeden błąd na uruchomienie, bez odzyskiwania. -- Informacje `-g` najlepiej z `-O0`. -- Sterownik używa narzędzi `sh` (`rm`, `mkdir`), więc wymaga powłoki POSIX. -- Generyki są monomorfizowane (kod rośnie z liczbą instancji); nie ma ograniczeń na typy, błędne użycie to błąd wewnątrz - wygenerowanego ciała. -- Pamięć nieudanego testu nie jest zwalniana (uruchamianie testów wychodzi z niego przez `longjmp`). -- `--target android` jest sprawdzany do pliku obiektowego; linkowanie wymaga NDK i nie było uruchamiane na urządzeniu. - +- Jeden błąd na uruchomienie, bez kontynuowania po nim. +- Informacje dla debuggera (`-g`) są najlepsze z `-O0`. +- Typy generyczne są monomorfizowane (kod rośnie z każdą instancją); nie ma ograniczeń na parametry typu, więc złe + użycie daje błąd wewnątrz ciała instancji. +- Pamięć, którą miał nieudany test, nie jest zwalniana (uruchamianie testów wychodzi z niego przez `longjmp`). +- Wektory nie mają masek (`v < w`), przetasowań ani dzielenia całkowitego. diff --git a/docs/pl/dla-poczatkujacych.md b/docs/pl/dla-poczatkujacych.md index 5b48cf2..4f79846 100644 --- a/docs/pl/dla-poczatkujacych.md +++ b/docs/pl/dla-poczatkujacych.md @@ -1,8 +1,8 @@ # Finch dla początkujących -**Ten przewodnik jest dla ciebie, jeśli jeszcze nigdy nie pisałeś programu.** -Nie potrzebujesz żadnego doświadczenia ani matematyki ponad to, czego uczy się w podstawówce. -Czytaj powoli, po jednym rozdziale, i przepisuj każdy przykład samodzielnie. Tak zostaje w głowie najlepiej. +**Programowanie to dla ciebie coś zupełnie nowego? Ten przewodnik jest dla ciebie.** +Nie musisz nic umieć: wystarczy matematyka ze szkoły podstawowej. +Czytaj spokojnie, rozdział po rozdziale, i każdy przykład przepisuj sam: tak najlepiej zostaje w głowie. > 🇬🇧 English version: [for-beginners.md](../en/for-beginners.md) @@ -13,7 +13,7 @@ Czytaj powoli, po jednym rozdziale, i przepisuj każdy przykład samodzielnie. T 1. [Co to jest program?](#1-co-to-jest-program) 2. [Przygotowanie Fincha](#2-przygotowanie-fincha) 3. [Twój pierwszy program](#3-twój-pierwszy-program) -4. [Wypisywanie na ekran](#4-wypisywanie-na-ekran) +4. [Wypisywanie na ekranie](#4-wypisywanie-na-ekranie) 5. [Pudełka z nazwami: zmienne](#5-pudełka-z-nazwami-zmienne) 6. [Rodzaje wartości](#6-rodzaje-wartości) 7. [Liczenie](#7-liczenie) @@ -28,36 +28,35 @@ Czytaj powoli, po jednym rozdziale, i przepisuj każdy przykład samodzielnie. T 16. [Kiedy coś pójdzie nie tak](#16-kiedy-coś-pójdzie-nie-tak) 17. [Małe projekty do zrobienia](#17-małe-projekty-do-zrobienia) 18. [Ściąga](#18-ściąga) -19. [Słowniczek](#19-słowniczek) +19. [Słowa, które będziesz słyszeć](#19-słowa-które-będziesz-słyszeć) --- ## 1. Co to jest program? -Komputer jest bardzo szybki, ale nie myśli. Robi dokładnie to, co mu się każe, -krok po kroku. +Komputer jest bardzo szybki, ale nie myśli. Robi dokładnie to, co mu każesz, krok po kroku. -**Program** to spisana lista takich kroków. Trochę jak przepis kuchenny: +**Program** to spisana lista takich kroków. Trochę jak przepis: > 1. Weź dwa jajka. > 2. Wbij je do miski. > 3. Wymieszaj. -**Język programowania** to sposób zapisania takiego przepisu, który komputer rozumie. -**Finch** (po polsku: zięba) jest jednym z takich języków. Zrobiono go tak, żeby był -mały i prosty, więc nauczysz się go szybko. +**Język programowania** to sposób zapisania takiego przepisu tak, żeby komputer go zrozumiał. +**Finch** to jeden z takich języków. Powstał po to, żeby był mały i prosty, więc szybko się go nauczysz. -Komputer czyta program od góry do dołu, linijka po linijce. +Komputer czyta program od góry do dołu, linia po linii. --- ## 2. Przygotowanie Fincha -Finch działa na komputerach z **Linuksem**. Jeśli nauczyciel, rodzic albo znajomy już go -dla ciebie zainstalował, przejdź od razu do następnego rozdziału. +Finch działa na komputerach z **Linuksem**, **macOS** i **Windowsem**. Jeśli nauczyciel, rodzic albo znajomy +już go dla ciebie przygotował, przejdź do następnego rozdziału. (Na Macu i na Windowsie poproś kogoś o skorzystanie +z [przewodnika dla techników](dla-technikow.md#2-instalacja): są tam gotowe wersje Fincha na oba systemy.) -W przeciwnym razie otwórz program **Terminal** (okienko, w którym wpisuje się polecenia) -i wpisz te linijki po kolei, naciskając **Enter** po każdej: +W przeciwnym razie otwórz program **Terminal** (okno, w którym wpisuje się polecenia) i wpisz po kolei te linie, +naciskając po każdej **Enter**: ```sh git clone https://github.com/Nexoniarz/Finch.git @@ -66,24 +65,24 @@ nix-shell bootstrap/build.sh ``` -Jeśli ostatnia linijka skończy się bez słowa `error`, Finch jest gotowy. -(Jeśli komputer nie zna polecenia `nix-shell`, poproś kogoś o skorzystanie z -[przewodnika dla techników](dla-technikow.md#2-instalacja), gdzie opisano inny sposób.) +Jeśli ostatnie polecenie skończy się bez słowa `error`, Finch jest gotowy. +(Jeśli na twoim komputerze nie ma `nix-shell`, poproś kogoś o skorzystanie z +[przewodnika dla techników](dla-technikow.md#2-instalacja). Jest tam opisany inny sposób.) -Żeby sprawdzić, czy działa, wpisz: +Żeby sprawdzić, czy wszystko działa, wpisz: ```sh ./build/finch version ``` -Powinieneś zobaczyć coś w stylu `finch 2.5.0`. +Powinien pojawić się napis w rodzaju `Finch 2.6.0`. --- ## 3. Twój pierwszy program -1. Otwórz dowolny edytor tekstu (na przykład *Edytor tekstu*, *Kate*, *gedit* albo *VS Code*). -2. Wpisz dokładnie to: +1. Otwórz dowolny edytor tekstu (np. *Edytor tekstu*, *Kate*, *gedit* albo *VS Code*). +2. Przepisz dokładnie to: ```c fn main() { @@ -91,11 +90,11 @@ fn main() { } ``` -3. Zapisz plik w folderze `Finch` pod nazwą **`czesc.fch`**. Końcówka `.fch` mówi, że to program w Finchu. +3. Zapisz plik w katalogu `Finch` pod nazwą **`hello.fch`**. Końcówka `.fch` mówi, że to program w Finchu. 4. W Terminalu wpisz: ```sh -./build/finch run czesc.fch +./build/finch run hello.fch ``` Zobaczysz: @@ -104,7 +103,7 @@ Zobaczysz: Cześć! ``` -🎉 **Właśnie napisałeś i uruchomiłeś program.** +🎉 **Właśnie udało ci się napisać i uruchomić program.** ### Co właściwie napisaliśmy? @@ -114,26 +113,26 @@ fn main() { ← „tu zaczyna się główna część mojego programu } ← „tu główna część się kończy” ``` -- `fn main()` to miejsce, od którego **zaczyna się** każdy program w Finchu. Każdy program ma dokładnie jedno `main`. +- `fn main()` to miejsce, w którym **zaczyna się** każdy program w Finchu. Każdy program ma dokładnie jedno `main`. - Nawiasy klamrowe `{` i `}` obejmują wszystko, co należy do `main`, jak dwie okładki książki. - `print(...)` pokazuje coś na ekranie. -- Tekst zawsze piszemy w cudzysłowie: `"Cześć!"`. +- Tekst zawsze stoi w cudzysłowie: `"Cześć!"`. -Spacje przed `print` nie są obowiązkowe, ale dzięki nim od razu widać, co jest w środku -klamer. Zawsze je dodawaj, to dobry nawyk. +Spacje przed `print` nie są obowiązkowe, ale dzięki nim od razu widać, co jest w środku nawiasów. +Zawsze je dodawaj. To dobry nawyk. --- -## 4. Wypisywanie na ekran +## 4. Wypisywanie na ekranie -`print` umie pokazać tekst i liczby. Między kolejne rzeczy wstaw przecinek, a Finch sam doda spację: +`print` potrafi pokazać tekst i liczby. Oddziel je przecinkami, a Finch sam wstawi między nimi spacje: ```c fn main() { print("Mam", 10, "lat") print("2 + 2 =", 2 + 2) print() - print("Linijka wyżej jest pusta") + print("Linia wyżej jest pusta") } ``` @@ -143,10 +142,10 @@ Pokaże: Mam 10 lat 2 + 2 = 4 -Linijka wyżej jest pusta +Linia wyżej jest pusta ``` -Każdy `print` zaczyna nową linijkę. `print()` bez niczego w środku robi pustą linijkę. +Każde `print` zaczyna nową linię. `print()` bez niczego w środku robi pustą linię. ### Notatki dla siebie: komentarze @@ -163,8 +162,8 @@ fn main() { ## 5. Pudełka z nazwami: zmienne -Wyobraź sobie pudełko z naklejoną etykietą. Możesz coś do niego włożyć, a potem sprawdzić, -co jest w środku, czytając etykietę. W programowaniu takie pudełko nazywa się **zmienną**. +Wyobraź sobie pudełko z naklejoną etykietą. Możesz coś do niego włożyć, a później sprawdzić, co jest w środku, +czytając etykietę. W programowaniu takie pudełko nazywa się **zmienną**. ```c fn main() { @@ -173,9 +172,9 @@ fn main() { } ``` -`wiek := 10` znaczy: *„zrób nowe pudełko, nazwij je `wiek` i włóż do niego 10”*. +`wiek := 10` znaczy: *„zrób nowe pudełko, nazwij je `wiek` i włóż do niego 10”.* -Później możesz zmienić zawartość pudełka, używając samego `=`: +Później możesz zmienić zawartość pudełka, pisząc samo `=`: ```c fn main() { @@ -196,42 +195,39 @@ Punkty: 5 Punkty: 6 ``` -`punkty = punkty + 1` czytamy tak: *„weź to, co jest w `punkty`, dodaj 1 i włóż wynik z powrotem do `punkty`”*. -To samo można zapisać krócej: `punkty += 1`. +`punkty = punkty + 1` czyta się tak: *„weź to, co jest w `punkty`, dodaj 1 i włóż wynik z powrotem do `punkty`”.* +To samo można napisać krócej: `punkty += 1`. **Zapamiętaj:** -- `:=` robi **nowe** pudełko. Używasz tego tylko raz dla danej nazwy. +- `:=` robi **nowe** pudełko. Dla każdej nazwy używasz tego tylko raz. - `=` wkłada nową wartość do pudełka, **które już istnieje**. ### Nazywanie pudełek -Nazwy mogą zawierać angielskie litery `a`–`z` i `A`–`Z`, cyfry i znak `_`, ale nie mogą zaczynać się od cyfry: -`wiek`, `suma_punktow`, `gracz2` są w porządku. `2gracz` już nie. - -⚠️ **Bez polskich liter w nazwach!** `ą`, `ę`, `ł`, `ż` i inne mogą być tylko w tekście w cudzysłowie. -Pisz `zolw`, a nie `żółw`. Natomiast `print("Żółw")` działa bez problemu. - +W nazwach można używać liter od `a` do `z` i od `A` do `Z`, cyfr i znaku `_`, ale nazwa nie może zaczynać się od cyfry: +`wiek`, `suma_punktow`, `gracz2` są w porządku, a `2gracz` nie. +Polskich liter (`ą`, `ę`, `ż`…) nie wolno używać w nazwach, tylko w tekstach w cudzysłowie: `print("Łódź")` jest w porządku. Wybieraj nazwy, które mówią, co jest w środku: `cena` jest lepsza niż `c`. --- ## 6. Rodzaje wartości -Finch pilnuje, *jakiego rodzaju* rzecz jest w każdym pudełku. Ten rodzaj nazywa się **typem**. +Finch pilnuje, jaki *rodzaj* rzeczy leży w każdym pudełku. Taki rodzaj to **typ**. Najczęściej będziesz używać tych: -| Typ | Co przechowuje | Przykłady | -|---------|---------------------------|--------------------------| -| `int` | liczby całkowite | `0`, `42`, `-7` | -| `float` | liczby z przecinkiem | `3.14`, `0.5`, `-2.0` | -| `str` | tekst | `"Cześć"`, `"Ola"` | -| `bool` | tak albo nie | `true` (prawda), `false` (fałsz) | -| `char` | jeden znak | `'A'`, `'z'`, `'?'` | +| Typ | Co przechowuje | Przykłady | +|---------|-----------------------------|--------------------------| +| `int` | liczby całkowite | `0`, `42`, `-7` | +| `float` | liczby z przecinkiem | `3.14`, `0.5`, `-2.0` | +| `str` | tekst | `"Cześć"`, `"Ola"` | +| `bool` | tak albo nie | `true`, `false` | +| `char` | jedną literę albo znak | `'A'`, `'z'`, `'?'` | -⚠️ W liczbach zamiast przecinka piszemy **kropkę**: `3.14`, a nie `3,14`. Tak jest w prawie wszystkich językach programowania. +Uwaga: w programach część ułamkową oddziela się **kropką**, nie przecinkiem: `3.14`, a nie `3,14`. -Kiedy używasz `:=`, Finch sam rozpoznaje typ: +Przy `:=` Finch sam rozpoznaje typ: ```c imie := "Ola" // str @@ -240,7 +236,7 @@ wzrost := 1.42 // float lubi_koty := true // bool ``` -Typ możesz też napisać sam, przed nazwą: +Typ można też napisać samemu, przed nazwą: ```c int wiek = 10 @@ -249,24 +245,23 @@ str imie = "Ola" Oba sposoby robią to samo. Wybierz ten, który ci bardziej pasuje. -**Pudełko nie zmienia typu.** Jeśli w `wiek` są liczby całkowite, nie włożysz tam potem tekstu. -Finch zatrzyma się i powie o tym, co pomaga wcześnie wyłapywać pomyłki. +**Pudełko trzyma się swojego typu.** Jeśli w `wiek` są liczby całkowite, nie włożysz tam potem tekstu. +Finch się zatrzyma i ci o tym powie, a to pomaga wcześnie wyłapywać pomyłki. --- ## 7. Liczenie -| Znak | Co robi | Przykład | Wynik | -|------|-----------------------------|----------|-------| -| `+` | dodawanie | `7 + 2` | `9` | -| `-` | odejmowanie | `7 - 2` | `5` | -| `*` | mnożenie | `7 * 2` | `14` | -| `/` | dzielenie | `7 / 2` | `3` | -| `%` | reszta z dzielenia | `7 % 2` | `1` | +| Znak | Znaczenie | Przykład | Wynik | +|------|--------------------------------|----------|--------| +| `+` | dodawanie | `7 + 2` | `9` | +| `-` | odejmowanie | `7 - 2` | `5` | +| `*` | mnożenie | `7 * 2` | `14` | +| `/` | dzielenie | `7 / 2` | `3` | +| `%` | reszta z dzielenia | `7 % 2` | `1` | -**Uwaga na dzielenie liczb całkowitych:** `7 / 2` daje `3`, a nie `3.5`. Obie liczby są -całkowite, więc wynik też jest całkowity (część po kropce jest odrzucana). -Jeśli chcesz dostać `3.5`, użyj liczby z kropką: `7.0 / 2`. +**Uważaj przy dzieleniu liczb całkowitych:** `7 / 2` daje `3`, a nie `3.5`, bo obie liczby są całkowite, więc wynik +też jest całkowity (część po kropce znika). Jeśli chcesz `3.5`, użyj liczby z kropką: `7.0 / 2`. Mnożenie i dzielenie wykonują się przed dodawaniem i odejmowaniem, tak jak w szkole. Nawiasy zmieniają kolejność: @@ -276,28 +271,28 @@ print(2 + 3 * 4) // 14 print((2 + 3) * 4) // 20 ``` -`%` przydaje się do sprawdzania, czy liczba jest parzysta. Liczba parzysta dzielona przez 2 -nie daje reszty, więc `n % 2` wynosi `0`. +`%` przydaje się do sprawdzania, czy liczba jest parzysta: liczba parzysta dzielona przez 2 nie daje reszty, +więc `n % 2` wynosi `0`. -**Nie wolno dzielić przez zero.** Jeśli program spróbuje, zatrzyma się i powie, w której linijce to się stało. +**Nie wolno dzielić przez zero.** Jeśli program spróbuje, zatrzyma się i powie, w której linii to się stało. --- ## 8. Podejmowanie decyzji: if -Program może wybierać, co zrobić. `if` (czyt. „if”, po polsku „jeżeli”) znaczy *„zrób to tylko wtedy, gdy coś jest prawdą”*. +Program może wybierać, co zrobić. `if` znaczy *„zrób to tylko wtedy, gdy coś jest prawdą”*. ```c fn main() { temperatura := 25 if temperatura > 20 { - print("Jest ciepło, załóż koszulkę") + print("Jest ciepło, włóż koszulkę") } } ``` -Dodaj `else` („w przeciwnym razie”): +Dopisz `else`, żeby powiedzieć *„a jeśli nie, zrób to”*: ```c if temperatura > 20 { @@ -307,12 +302,11 @@ if temperatura > 20 { } ``` -A `else if` dodaje kolejne możliwości. Finch sprawdza je od góry i wykonuje tylko -**pierwszą** prawdziwą: +A `else if` daje więcej możliwości. Finch sprawdza je od góry do dołu i wykonuje tylko **pierwszą**, która jest prawdą: ```c if temperatura > 25 { - print("Gorąco!") + print("Upał!") } else if temperatura > 15 { print("Przyjemnie") } else { @@ -328,18 +322,18 @@ if temperatura > 25 { | `!=` | nie jest równe | | `<` | jest mniejsze niż | | `>` | jest większe niż | -| `<=` | jest mniejsze lub równe | -| `>=` | jest większe lub równe | +| `<=` | jest mniejsze albo równe | +| `>=` | jest większe albo równe | ⚠️ **Jedno `=` wkłada wartość do pudełka. Dwa `==` pytają „czy to jest równe?”.** -Tekst też można porównywać: `if imie == "Ola" { ... }`. +Porównywać można też teksty: `if imie == "Ola" { ... }`. ### Łączenie pytań - `&&` znaczy **i**: oba warunki muszą być prawdziwe. `if wiek > 6 && wiek < 12 { ... }` -- `||` znaczy **lub**: wystarczy, że jeden jest prawdziwy. `if dzien == "sob" || dzien == "nd" { ... }` -- `!` znaczy **nie**: odwraca prawdę i fałsz. `if !pada { ... }` +- `||` znaczy **lub**: przynajmniej jeden musi być prawdziwy. `if dzien == "sob" || dzien == "niedz" { ... }` +- `!` znaczy **nie**: zamienia prawdę w fałsz i odwrotnie. `if !pada { ... }` --- @@ -365,7 +359,7 @@ Liczba 4 Liczba 5 ``` -`for i in 1..6` znaczy: *„licz za pomocą `i` od 1 do 6, ale **bez** 6”*. +`for i in 1..6` znaczy: *„licz w `i` od 1 do 6, ale **bez** 6”*. Czyli `0..10` daje dziesięć liczb: 0, 1, 2 … 9. ### Powtarzanie, dopóki coś jest prawdą: `while` @@ -388,8 +382,8 @@ fn main() { Start! ``` -⚠️ Upewnij się, że pętla może się skończyć. Gdyby `licznik` nigdy nie malał, pętla kręciłaby się -w nieskończoność. Jeśli tak się stanie, naciśnij w Terminalu **Ctrl + C**, żeby ją zatrzymać. +⚠️ Upewnij się, że pętla może się skończyć. Gdyby `licznik` nigdy nie malał, pętla kręciłaby się bez końca. +Jeśli tak się stanie, naciśnij w Terminalu **Ctrl + C**, żeby ją zatrzymać. ### Wcześniejsze wyjście i pomijanie @@ -399,9 +393,9 @@ w nieskończoność. Jeśli tak się stanie, naciśnij w Terminalu **Ctrl + C**, ```c for i in 1..100 { if i == 5 { - break // stop, gdy dojdziemy do 5 + break // zatrzymaj się, gdy dojdziemy do 5 } - print(i) // pokaże 1 2 3 4 + print(i) // wypisze 1 2 3 4 } ``` @@ -409,7 +403,7 @@ for i in 1..100 { ## 10. Własne polecenia: funkcje -Kiedy robisz to samo w wielu miejscach, nadaj temu nazwę. Taki nazwany kawałek kodu to **funkcja**. +Kiedy robisz to samo w wielu miejscach, nadaj temu nazwę. Taki nazwany blok to **funkcja**. Jedną już znasz: `main`. ```c @@ -428,12 +422,12 @@ Cześć, Ola Cześć, Tomek ``` -`str imie` w nawiasie znaczy: *„ta funkcja potrzebuje jednego tekstu, a w środku nazywamy go `imie`”*. -To, co wpiszesz w nawias przy wywołaniu, zostanie przekazane do środka. +`str imie` w nawiasach znaczy: *„ta funkcja potrzebuje jednego tekstu i w środku będziemy go nazywać `imie`”.* +To, co wpiszesz w nawiasach przy jej użyciu, trafia do środka. -### Funkcje, które oddają odpowiedź +### Funkcje, które dają odpowiedź -Napisz `->` i typ odpowiedzi, a odpowiedź oddaj słowem `return`: +Napisz `->` i typ odpowiedzi, a odpowiedź oddaj przez `return`: ```c fn dodaj(int a, int b) -> int { @@ -446,14 +440,14 @@ fn main() { } ``` -Nie ma znaczenia, gdzie w pliku umieścisz swoje funkcje, nad czy pod `main`. +Nie ma znaczenia, gdzie w pliku umieścisz swoje funkcje, nad `main` czy pod nim. --- ## 11. Listy: wiele wartości w jednym pudełku -Czasem jedno pudełko nie wystarczy. Na liście zakupów jest wiele rzeczy. -**Lista** (programiści mówią też **tablica**) trzyma wiele wartości po kolei: +Czasem jedno pudełko nie wystarcza. Na liście zakupów jest wiele rzeczy. +**Lista** (programiści mówią też **tablica**) przechowuje wiele wartości po kolei: ```c fn main() { @@ -470,34 +464,34 @@ Mam 3 owoce Pierwszy to jabłko ``` -- Nawiasy kwadratowe `[ ]` tworzą listę. Wartości oddzielamy przecinkami. +- Nawiasy kwadratowe `[ ]` tworzą listę. Wartości oddziela się przecinkami. - `.len` mówi, ile rzeczy jest na liście. -- `owoce[0]` to **pierwsza** rzecz. Liczymy od **0**, więc `owoce[1]` to druga, a `owoce[2]` trzecia. +- `owoce[0]` to **pierwsza** rzecz. Liczy się od **0**, więc `owoce[1]` to druga, a `owoce[2]` trzecia. Jeśli zapytasz o miejsce, którego nie ma, np. `owoce[10]`, program się zatrzyma i powie: -`index 10 is out of range (the length is 3)`, czyli „indeks 10 jest poza zakresem (długość to 3)”. +`index 10 is out of range (the length is 3)` (indeks 10 jest poza listą, która ma długość 3). ### Dodawanie i zabieranie ```c fn main() { []int wyniki // pusta lista liczb całkowitych - wyniki.push(10) // dołóż 10 na koniec + wyniki.push(10) // dodaj 10 na końcu wyniki.push(25) wyniki.push(7) print(wyniki) // [10, 25, 7] - ostatni := wyniki.pop() // zabierz ostatni + ostatni := wyniki.pop() // zdejmij ostatni print(ostatni, wyniki) // 7 [10, 25] - wyniki.sort() // posortuj + wyniki.sort() print(wyniki) } ``` ### Przechodzenie po liście -`for ... in` odwiedza każdą wartość po kolei: +`for ... in` odwiedza po kolei każdą wartość: ```c fn main() { @@ -509,8 +503,8 @@ 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ść: +Jeśli chcesz też wiedzieć, *w którym miejscu* listy jesteś, podaj w `for` dwie nazwy: pierwsza liczy 0, 1, 2…, +a druga to wartość: ```c fn main() { @@ -526,21 +520,20 @@ fn main() { 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#10-tablice-i-mapy). +Listy potrafią więcej: `contains`, `find`, `insert`, `remove`, `sort`, `reverse`, `join`. +Wszystkie są opisane w [przewodniku 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ść**: +Lista znajduje rzeczy po miejscu: `owoce[0]`. Czasem jednak chcesz szukać po **nazwie**, tak jak szuka się słowa +w słowniku albo numeru w książce telefonicznej. Do tego służy **mapa**: 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 + telefon["Ewa"] = "555-0000" // dodaj nowe imię print(telefon.len) // 3 if telefon.has("Tomek") { @@ -549,26 +542,26 @@ fn main() { print("nie ma Tomka") } - for imie, numer in telefon { // każde imię z jego numerem + for imie, numer in telefon { // każde imię razem z numerem print(imie, numer) } } ``` -- `["klucz": wartość, ...]` tworzy mapę. Klucze to zwykle tekst, ale mogą być też liczby. +- `["klucz": wartość, ...]` tworzy mapę. Kluczami są często teksty, 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. + więc najpierw sprawdź `telefon.has("Tomek")` albo użyj `telefon.get("Tomek", "nieznany")`. +- `for imie, numer in telefon` odwiedza każdy klucz z jego wartością, w kolejności, w jakiej je dodano. 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 + map[str]int glosy // pusta mapa: klucze to teksty, wartości to liczby całkowite for g in ["kot", "pies", "kot", "kot", "rybka"] { glosy[g] += 1 } - print(glosy) // {"kot": 3, "pies": 1, "rybka": 1} + print(glosy) // {"kot": 3, "pies": 1, "rybka": 1} } ``` @@ -576,7 +569,7 @@ fn main() { ## 12. Zabawa tekstem -Teksty sklejasz znakiem `+`: +Teksty można sklejać przez `+`: ```c fn main() { @@ -600,16 +593,16 @@ fn main() { slowo := "Finch" print(slowo.len) // 5 liter print(slowo[0]) // F (pierwsza litera) - print(slowo.upper()) // FINCH (wielkimi literami) - print(slowo.lower()) // finch (małymi literami) - print(slowo.contains("in")) // true (czy zawiera "in") + print(slowo.upper()) // FINCH + print(slowo.lower()) // finch + print(slowo.contains("in")) // true for litera in slowo { - print(litera) // F, i, n, c, h, każda w osobnej linijce + print(litera) // F, i, n, c, h, każda w osobnej linii } } ``` -⚠️ `.len` liczy bajty, a polskie litery (ą, ę, ż…) zajmują po 2 bajty. `"żółw".len` to 7, a nie 4. +(`.len` liczy bajty, a polskie litery zajmują po dwa, więc `"żółw".len` to 7, a nie 4.) --- @@ -634,15 +627,15 @@ fn main() { } ``` -Jeśli ktoś wpisze coś, co nie jest liczbą, program zatrzyma się z komunikatem -w stylu `can't turn "abc" into int` („nie da się zamienić "abc" na int”). +Jeśli ktoś wpisze coś, co nie jest liczbą, program się zatrzyma z komunikatem +w rodzaju `can't turn "abc" into int` (nie da się zamienić "abc" na liczbę). --- ## 14. Własne rodzaje wartości: struktury -Wyobraź sobie kartę w bibliotece: każda karta ma tytuł, autora i rok. -**Struktura** (`struct`) pozwala zrobić własny rodzaj wartości z nazwanymi częściami: +Pomyśl o karcie w bibliotece: każda ma tytuł, autora i rok wydania. +**Struktura** pozwala stworzyć własny rodzaj wartości z nazwanymi częściami: ```c struct Ksiazka { @@ -664,11 +657,11 @@ Hobbit napisał Tolkien Ksiazka(tytul: "Hobbit", autor: "Tolkien", rok: 1938) ``` -- `struct Ksiazka { ... }` opisuje, co ma każda książka. Pisz to **poza** `main`. +- `struct Ksiazka { ... }` opisuje, co ma każda książka. Umieść to **poza** `main`. - `Ksiazka(tytul: ..., autor: ..., rok: ...)` tworzy jedną książkę. -- `k.tytul` odczytuje jedną część, a `k.rok = 1938` ją zmienia. +- `k.tytul` odczytuje jedną część. `k.rok = 1938` ją zmienia. -Częściom możesz nadać wartość początkową. Wtedy nie trzeba ich podawać: +Częściom można nadać wartość początkową. Wtedy nie trzeba ich wypełniać: ```c struct Gracz { @@ -682,12 +675,12 @@ fn main() { } ``` -Możesz też trzymać wiele struktur na liście: `[]Ksiazka polka`, a potem `polka.push(k)`. +Wiele takich wartości można trzymać na liście: `[]Ksiazka polka`, a potem `polka.push(k)`. -### Polecenia twojej struktury: metody +### Polecenia dla 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: +Struktura może mieć własne polecenia. Napisz nazwę struktury, kropkę i nazwę polecenia. +W środku `self` to struktura, na której polecenie użyto: ```c struct Gracz { @@ -697,7 +690,7 @@ struct Gracz { fn Gracz.trafiony() { self.zycia -= 1 - print(self.imie, "ma jeszcze", self.zycia, "życia") + print(self.imie, "- pozostałe życia:", self.zycia) } fn Gracz.zyje() -> bool { @@ -706,8 +699,8 @@ fn Gracz.zyje() -> bool { fn main() { g := Gracz(imie: "Ola") - g.trafiony() // Ola ma jeszcze 2 życia - g.trafiony() // Ola ma jeszcze 1 życia + g.trafiony() // Ola - pozostałe życia: 2 + g.trafiony() // Ola - pozostałe życia: 1 print(g.zyje()) // true } ``` @@ -731,14 +724,14 @@ fn main() { - `read_file(nazwa)` oddaje tekst z pliku. - `file_exists(nazwa)` mówi, czy plik istnieje (`true` albo `false`). -Plik pojawi się w folderze, w którym uruchomiłeś program. +Plik pojawi się w katalogu, w którym uruchomiono program. --- ## 16. Kiedy coś pójdzie nie tak -Każdy się myli przy programowaniu, nawet eksperci, codziennie. -Finch stara się tłumaczyć, co poszło nie tak, prostymi słowami (na razie po angielsku). Przykład: +Każdy, kto programuje, robi błędy, nawet eksperci, codziennie. +Finch bardzo się stara wyjaśnić prostymi słowami, co poszło nie tak. Przykład: ```c fn main() { @@ -748,68 +741,67 @@ fn main() { ``` ``` -czesc.fch:3:11: error: there is no variable named 'wiekk' +hello.fch:3:11: error: there is no variable named 'wiekk' 3 | print(wiekk) | ^ ``` Jak to czytać: -- `czesc.fch` to plik, -- `3` to numer **linijki**, -- `11` to który znak w linijce (**kolumna**), -- potem wyjaśnienie, a pod nim ta linijka ze strzałką `^` wskazującą problem. +- `hello.fch` to plik, +- `3` to numer **linii**, +- `11` mówi, jak daleko w linii (to **kolumna**), +- dalej jest wyjaśnienie, a pod nim ta linia ze znakiem `^`, który wskazuje problem. -Tutaj po prostu źle napisaliśmy `wiek`. Komunikat znaczy: „nie ma zmiennej o nazwie 'wiekk'”. +Komunikaty są po angielsku; tutaj znaczy to „nie ma zmiennej o nazwie 'wiekk'”. Po prostu literówka w `wiek`. -### Częste błędy +### Częste pomyłki | Finch mówi | Co to znaczy | Jak naprawić | |---|---|---| -| `there is no variable named 'x'` | Nie ma zmiennej `x`: literówka albo zapomniałeś ją zrobić | Sprawdź pisownię. Utwórz ją przez `x := ...` | -| `a variable named 'x' already exists here` | Użyłeś `:=` dwa razy dla tej samej nazwy | Za drugim razem użyj `=` | +| `there is no variable named 'x'` | Literówka w nazwie albo takie pudełko nigdy nie powstało | Sprawdź pisownię. Utwórz je przez `x := ...` | +| `a variable named 'x' already exists here` | Użyto `:=` dwa razy dla tej samej nazwy | Za drugim razem napisz `=` | | `'x' must be int, but this is str` | Zły rodzaj wartości dla tego pudełka | Włóż wartość właściwego rodzaju | -| `put each statement on its own line` | Dwa polecenia w jednej linijce | Wciśnij Enter między nimi | +| `put each statement on its own line` | Dwa polecenia w jednej linii | Rozdziel je Enterem | | `this '{' is never closed` | Brakuje `}` | Dopisz `}` | -| `a condition must be bool` | `if` potrzebuje pytania tak/nie | Porównaj coś: `if x > 0` | -| `unexpected character 'ż'` | Polska litera w nazwie | Zmień na zwykłą literę: `zolw` | -| `runtime error: division by zero` | Program podzielił przez 0 w trakcie działania | Sprawdź liczbę przed dzieleniem | -| `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) | +| `a condition must be bool` | `if` potrzebuje pytania typu tak/nie | Porównaj coś: `if x > 0` | +| `runtime error: division by zero` | W trakcie działania programu nastąpiło dzielenie przez 0 | Sprawdź liczbę przed dzieleniem | +| `index 5 is out of range (the length is 3)` | Pytanie listy o miejsce, którego nie ma | Pamiętaj, że liczy się od 0; sprawdź `.len` | +| `can't turn "abc" into int` | `int(...)` dostało tekst, który nie jest liczbą | Sprawdź, co wpisał użytkownik | +| `can't use '+' on str and int` | Tekst plus liczba | Użyj `str(...)` na liczbie | +| `'f' can fail, so say what happens then` | Użyto polecenia, które może się nie udać, bez powiedzenia, co wtedy zrobić | Dopisz `or ...` (zob. 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”): +Niektóre rzeczy mogą się nie udać *w trakcie działania programu*: ktoś wpisze `abc` tam, gdzie miała być liczba, +albo nie ma pliku. Zamiast zatrzymywać program, możesz przez **`or`** powiedzieć, co wtedy zrobić: ```c fn main() { - wiek := int(input("Ile masz lat? ")) or 0 // to nie liczba? weź 0 + 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)" + notatka := read_file("notatka.txt") or "(jeszcze nie ma notatki)" print(notatka) } ``` -Po `or` może też być blok. W środku `err` to komunikat, co poszło nie tak: +Po `or` może też stać blok. W jego środku `err` to komunikat, który mówi, co poszło nie tak: ```c fn main() { while true { - n := int(input("Podaj liczbę: ")) or { + n := int(input("Wybierz liczbę: ")) or { print("To nie jest liczba, spróbuj jeszcze raz") continue } - print("Wybrałeś", n) + print("Wybrana liczba:", n) break } } ``` -Twoje własne funkcje też mogą się nie udać. Dopisz `!` po typie wyniku i użyj `return error("...")`: +Twoje własne funkcje też mogą się nie udać. Postaw `!` po typie wyniku i użyj `return error("...")`: ```c fn bezpieczne_dzielenie(int a, int b) -> int! { @@ -825,15 +817,15 @@ fn main() { } ``` -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 +Finch nie pozwoli o tym zapomnieć: wywołanie `bezpieczne_dzielenie(10, 0)` bez `or` to błąd, który każe ci to obsłużyć. +(Problem można też przekazać dalej przez `try`; wyjaśnia to [przewodnik dla techników](dla-technikow.md#błędy-jako-wartości).) --- ## 17. Małe projekty do zrobienia -Spróbuj najpierw napisać każdy sam. Do rozwiązania zajrzyj dopiero, gdy utkniesz. +Spróbuj najpierw napisać każdy sam. Do rozwiązania zajrzyj dopiero wtedy, gdy utkniesz. ### Projekt 1: Tabliczka mnożenia @@ -852,7 +844,7 @@ fn main() { ### Projekt 2: Parzysta czy nieparzysta -Dla każdej liczby od 1 do 10 napisz, czy jest parzysta, czy nieparzysta. +Dla każdej liczby od 1 do 10 pokaż, czy jest parzysta, czy nieparzysta.
Rozwiązanie @@ -871,7 +863,7 @@ fn main() { ### Projekt 3: Stopnie Celsjusza na Fahrenheita -Pokaż tabelkę od 0 °C do 100 °C co 10 stopni. Wzór: `F = C * 9 / 5 + 32`. +Pokaż tabelę od 0 °C do 100 °C co 10 stopni. Wzór: `F = C * 9 / 5 + 32`.
Rozwiązanie @@ -893,8 +885,8 @@ fn main() { ### Projekt 4: Fizz Buzz -Policz od 1 do 20. Dla liczb podzielnych przez 3 napisz `Fizz`, przez 5 napisz `Buzz`, -przez obie naraz napisz `FizzBuzz`, a w pozostałych przypadkach samą liczbę. +Policz od 1 do 20. Dla liczb podzielnych przez 3 powiedz `Fizz`, przez 5 `Buzz`, +przez obie `FizzBuzz`, a w pozostałych przypadkach samą liczbę.
Rozwiązanie @@ -917,7 +909,7 @@ fn main() { ### Projekt 5: Największa z trzech -Napisz funkcję `najwieksza(int a, int b, int c) -> int`, która oddaje największą z trzech liczb. +Napisz funkcję `najwieksza(int a, int b, int c) -> int`, która zwraca największą liczbę.
Rozwiązanie @@ -940,7 +932,8 @@ fn main() { ### Projekt 6: Zgadnij liczbę -Komputer ma tajną liczbę. Gracz zgaduje, aż trafi, a komputer za każdym razem mówi „więcej” albo „mniej”. +Komputer wybiera tajną liczbę. Gracz zgaduje, aż trafi, a komputer za każdym razem +mówi „więcej” albo „mniej”.
Rozwiązanie @@ -956,7 +949,7 @@ fn main() { } else if strzal > tajna { print("Mniej!") } else { - print("Tak! Potrzebowałeś", proby, "prób") + print("Tak! Liczba prób:", proby) return } } @@ -968,7 +961,7 @@ fn main() { ### Projekt 7: Średnia ocen -Zapytaj o 3 oceny, zapisz je na liście, a potem pokaż listę i średnią. +Zapytaj o 3 oceny, zapisz je na liście i pokaż listę oraz średnią.
Rozwiązanie @@ -998,9 +991,9 @@ fn main() { fn main() { // tu zaczyna się program print("tekst", 42) // pokaż coś - x := 5 // nowe pudełko (typ zgadnięty) + x := 5 // nowe pudełko (typ rozpoznany sam) int y = 10 // nowe pudełko (typ podany) - x = 7 // zmień zawartość + x = 7 // zmiana zawartości pudełka x += 1 // dodaj 1 do x if x > 5 { // decyzja @@ -1020,72 +1013,49 @@ fn main() { // tu zaczyna się program } } -fn dodaj(int a, int b) -> int { // własne polecenie +fn dodaj(int a, int b) -> int { // twoje własne polecenie return a + b } -struct Zwierzak { // własny rodzaj wartości +struct Zwierzak { // twój własny rodzaj wartości str imie int wiek = 1 } -// 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") +// 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, w 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() +// 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) ... } ``` --- -## 19. Słowniczek +## 19. Słowa, które będziesz słyszeć | Słowo | Znaczenie | |---|---| | **program** | Lista kroków dla komputera | | **kod** | Tekst programu | | **uruchomić** | Kazać komputerowi wykonać kroki | -| **kompilować** | Przetłumaczyć kod na coś, co komputer umie uruchomić. `finch run` robi to za ciebie | -| **zmienna** | Pudełko z nazwą, w którym jest wartość | +| **skompilować** | Przetłumaczyć kod na coś, co komputer potrafi uruchomić. `finch run` robi to za ciebie | +| **zmienna** | Pudełko z nazwą, w którym leży wartość | | **typ** | Rodzaj wartości: liczba, tekst, tak/nie… | | **funkcja** | Nazwana grupa kroków, której można używać wiele razy | | **pętla** | Kroki, które się powtarzają | -| **warunek** | Pytanie tak/nie, np. `x > 5` | +| **warunek** | Pytanie typu tak/nie, np. `x > 5` | | **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 | +| **indeks** | Miejsce wartości na liście, liczone od 0 | +| **struktura** | Twój własny rodzaj wartości 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 | +| **mapa** | Wartości wyszukiwane po kluczu (nazwie), jak w słowniku | +| **wejście** (ang. *input*) | To, co ktoś wpisuje do programu | +| **błąd** (ang. *error*) | Komunikat, że coś jest nie tak, i gdzie | | **bug** | Pomyłka w programie | -### Słówka po angielsku, które spotkasz w Finchu - -| Słowo | Po polsku | -|---|---| -| `print` | wypisz | -| `if` / `else` | jeżeli / w przeciwnym razie | -| `for` … `in` | dla … w | -| `while` | dopóki | -| `break` / `continue` | przerwij / kontynuuj | -| `return` | zwróć (oddaj) | -| `true` / `false` | prawda / fałsz | -| `fn` (od *function*) | funkcja | -| `struct` | struktura | -| `push` / `pop` | dołóż / zabierz | -| `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ź | - --- -**Co dalej?** Kiedy to wszystko wyda ci się łatwe, przejdź do +**Co dalej?** Kiedy to wszystko będzie już łatwe, przejdź do [przewodnika dla techników](dla-technikow.md). Opisuje każdą część Fincha. diff --git a/docs/pl/dla-technikow.md b/docs/pl/dla-technikow.md index 4ebceba..a925f03 100644 --- a/docs/pl/dla-technikow.md +++ b/docs/pl/dla-technikow.md @@ -1,9 +1,9 @@ # Finch dla techników -**Dla osób, które dobrze znają komputery, ale programować nie umieją albo umieją niewiele.** -Ten przewodnik opisuje instalację Fincha, obsługę polecenia `finch`, cały język, pamięć, -moduły, biblioteki C, debugowanie i rozwiązywanie typowych problemów. Nie opisuje, jak -kompilator działa w środku. Do tego służy [przewodnik dla inżynierów](dla-inzynierow.md). +**Dla osób, które dobrze znają komputer, ale niewiele albo wcale nie programowały.** +Ten przewodnik opisuje instalację Fincha, polecenie `finch`, cały język, pamięć, moduły, biblioteki C, debugowanie +i rozwiązywanie typowych problemów. Nie opisuje, jak kompilator działa od środka; o tym jest +[przewodnik dla inżynierów](dla-inzynierow.md). > 🇬🇧 English version: [for-technicians.md](../en/for-technicians.md) @@ -18,7 +18,7 @@ kompilator działa w środku. Do tego służy [przewodnik dla inżynierów](dla- 5. [Zmienne](#5-zmienne) 6. [Typy](#6-typy) 7. [Operatory](#7-operatory) -8. [Sterowanie przebiegiem](#8-sterowanie-przebiegiem) +8. [Sterowanie](#8-sterowanie) 9. [Funkcje](#9-funkcje) 10. [Tablice i mapy](#10-tablice-i-mapy) 11. [Tekst (str)](#11-tekst-str) @@ -27,12 +27,12 @@ kompilator działa w środku. Do tego służy [przewodnik dla inżynierów](dla- 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) +17. [Biblioteki C](#17-biblioteki-c) 18. [Błędy](#18-błędy) 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) +22. [Układ repozytorium i testy](#22-układ-repozytorium-i-testy) 23. [Enumy i match](#23-enumy-i-match) 24. [Typy generyczne](#24-typy-generyczne) 25. [Biblioteka standardowa](#25-biblioteka-standardowa) @@ -41,82 +41,111 @@ kompilator działa w środku. Do tego służy [przewodnik dla inżynierów](dla- 28. [Projekt: finch.toml](#28-projekt-finchtoml) 29. [Wątki](#29-wątki) 30. [C++, Rust i Android](#30-c-rust-i-android) +31. [Wektory (SIMD)](#31-wektory-simd) --- ## 1. Czym jest Finch -Finch to język **kompilowany**. Narzędzie `finch` tłumaczy plik `.fch` na prawdziwy program -(natywny plik wykonywalny, taki jak te z C). Korzysta przy tym z **LLVM**, tego samego zaplecza -kompilatorów, na którym działają clang, Rust i Swift. +Finch to **kompilowany** język programowania. Polecenie `finch` tłumaczy plik `.fch` na prawdziwy program (natywny plik +wykonywalny, taki jak programy napisane w C) za pomocą **LLVM**, tego samego zaplecza, z którego korzystają clang, +Rust i Swift. -Co to daje: +Co z tego wynika: -- **Szybkość zbliżoną do C.** Nie ma interpretera, maszyny wirtualnej ani garbage collectora. - Kontrola zakresu tablic i inne zabezpieczenia po optymalizacji prawie nic nie kosztują. +- **Szybkość bliska C.** Nie ma interpretera, maszyny wirtualnej ani garbage collectora. Sprawdzanie zakresów tablic + i inne zabezpieczenia po optymalizacji prawie nic nie kosztują. - **Małe, samodzielne programy**, które można skopiować i uruchomić. -- **Pamięć obsługiwana za ciebie.** Listy i teksty są zwalniane automatycznie na końcu bloku, - do którego należą, bez garbage collectora. -- **Bezpośredni dostęp do bibliotek C.** Piszesz `import "stdio.h"` i wołasz, co chcesz, - nawet funkcje przyjmujące struktury. +- **Pamięć zarządza się sama.** Listy i teksty są zwalniane automatycznie na końcu bloku, do którego należą, + bez udziału garbage collectora. +- **Bezpośredni dostęp do bibliotek C.** `import "stdio.h"` i możesz wołać wszystko, co tam jest, nawet funkcje + przyjmujące struktury. -Zasady projektu: jeden sposób na każdą rzecz, składnia podobna do C bez jej pułapek -i komunikaty błędów, które mówią, jak problem naprawić. +Zasady projektu: na każdą rzecz jeden sposób, składnia jak w C, ale bez jego pułapek, i komunikaty błędów, które mówią, +jak naprawić problem. --- ## 2. Instalacja -Kompilator Fincha jest napisany w Finchu (zobacz [przewodnik dla inżynierów](dla-inzynierow.md)). Działa na **Linuksie** -(x86-64 i ARM64) i buduje programy dla **Linuksa, Windowsa i macOS**. Potrzebuje **clanga 21** i **libclang** (LLVM 21), -żeby czytać nagłówki C i kompilować to, co wygeneruje. +Kompilator Fincha jest napisany w Finchu (zob. [przewodnik dla inżynierów](dla-inzynierow.md)). Działa na **Linuksie** +(x86-64 i ARM64), **macOS** (Apple Silicon i Intel) i **Windowsie** (x86-64) i na każdy z tych systemów buduje programy. +Wszędzie potrzebuje **clanga** (kompiluje nim każdy program) i **libclang** z LLVM 21 (czyta nim nagłówki C). -Kompilator nie działa jeszcze na macOS ani Windowsie: potrzebuje ziarna zrobionego z nagłówkami tego systemu -([bootstrap/README.md](../../bootstrap/README.md) tłumaczy dlaczego). Do tego czasu programy na Windows i macOS buduj z Linuksa -przez `--target` (niżej) albo użyj starszego kompilatora w C++ z gałęzi `before-bootstrap`. +### Sposób A: gotowa wersja -### Opcja A: gotowa wersja (Linux) +Pobierz archiwum dla swojego systemu z [wydań](https://github.com/Nexoniarz/Finch/releases) i rozpakuj je. +Katalogi `runtime/` i `std/` muszą leżeć obok pliku `finch` (albo zainstaluj go, zob. niżej). -Pobierz `finch-linux-x64.tar.gz` albo `finch-linux-arm64.tar.gz` z [wydań](https://github.com/Nexoniarz/Finch/releases) -i rozpakuj. Zostaw `runtime/` i `std/` obok pliku `finch` (albo zainstaluj, niżej). Potrzebny jest też `clang` 21 -z biblioteką libclang: +| System | Archiwum | Doinstaluj | +|---|---|---| +| Linux x86-64 | `finch-linux-x64.tar.gz` | clang 21 i libclang (tabela niżej) | +| Linux ARM64 (Raspberry Pi 4/5, Graviton…) | `finch-linux-arm64.tar.gz` | to samo | +| macOS, Apple Silicon | `finch-macos-arm64.tar.gz` | `xcode-select --install` (clang i nagłówki systemu), `brew install llvm@21` (libclang) | +| macOS, Intel | `finch-macos-x64.tar.gz` | to samo | +| Windows x64 | `finch-windows-x64.zip` (`finch.exe` z `libclang.dll`) | [llvm-mingw](https://github.com/mstorsjo/llvm-mingw/releases) (archiwum `ucrt-x86_64`): rozpakuj i dodaj jego katalog `bin` do PATH | + +Na Linuksie: | Dystrybucja | Pakiety | |---|---| | Debian / Ubuntu | `clang-21 libclang-21-dev` z [apt.llvm.org](https://apt.llvm.org), `pkg-config` | | Fedora | `clang clang-devel pkgconf` | | Arch | `clang pkgconf` | -| Nix | `nix-shell` w repozytorium daje wszystko | +| Nix | `nix-shell` w repozytorium daje wszystko naraz | -### Opcja B: zbuduj sam +Na Windowsie llvm-mingw daje Finchowi w jednym katalogu `clang`, nagłówki C i linker; zbudowane nim programy nie +potrzebują do działania niczego więcej. Plik `libclang.dll` trzymaj obok `finch.exe`. + +### Sposób B: zbuduj sam ```sh git clone https://github.com/Nexoniarz/Finch.git cd Finch -nix-shell # clang, libclang, pkg-config (albo zainstaluj z dystrybucji) +nix-shell # Linux: clang, libclang, pkg-config (albo zainstaluj je z dystrybucji) bootstrap/build.sh # build/finch ./build/finch version ``` -`bootstrap/build.sh` kompiluje ziarno (`bootstrap/.ll`, własny LLVM IR kompilatora) do pierwszego kompilatora, -buduje nim `src/`, buduje `src/` jeszcze raz wynikiem i sprawdza, że oba są identyczne. Jeśli libclang nie leży tam, gdzie -szuka linker: `FINCH_LIBCLANG="-L/usr/lib/llvm-21/lib -lclang-21" bootstrap/build.sh`. +`bootstrap/build.sh` kompiluje plik startowy (`bootstrap/.ll`, własny LLVM IR kompilatora) w pierwszy kompilator, +buduje nim `src/`, buduje `src/` jeszcze raz otrzymanym kompilatorem i sprawdza, czy oba wyniki są identyczne. Jeśli +linker nie znajduje libclang, wskaż ją: `FINCH_LIBCLANG="-L/usr/lib/llvm-21/lib -lclang-21" bootstrap/build.sh`. + +Na Macu, z LLVM z Homebrew: + +```sh +brew install llvm@21 +export FINCH_CLANG="$(brew --prefix llvm@21)/bin/clang" +export FINCH_LIBCLANG="-L$(brew --prefix llvm@21)/lib -lclang" +export FINCH_LLVM_INCLUDE="$(brew --prefix llvm@21)/include" +bootstrap/build.sh +``` + +Na Windowsie, w Git Bash, z llvm-mingw w PATH i oficjalnym LLVM dla Windowsa (dla `libclang.dll` i jej nagłówków) +rozpakowanym do `C:/llvm`: + +```sh +export FINCH_LIBCLANG="C:/llvm/bin/libclang.dll" +export FINCH_LLVM_INCLUDE="C:/llvm/include" +export FINCH_LDFLAGS="-LC:/llvm/bin" +bootstrap/build.sh # build/finch.exe, z libclang.dll skopiowaną obok +``` ### Budowanie na inny system -`--target` buduje program na inny system. Do linkowania Finch potrzebuje kompilatora C dla tego systemu: +`--target` buduje program na inny system. Do linkowania Finch potrzebuje kompilatora C tamtego systemu: -| `--target` | Buduje | Kompilator C | `finch run` używa | +| `--target` | Buduje | Używany kompilator C | `finch run` uruchamia przez | |---|---|---|---| -| `windows` | `.exe` dla Windowsa | `x86_64-w64-mingw32-gcc` (MinGW-w64) | Wine | +| `windows` | plik `.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=…` | | +| `macos` | program dla macOS (na Macu: dla drugiego rodzaju 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 arm64 # buduje na ARM64 i uruchamia w QEMU +finch run gra.fch --target arm64 # buduje dla ARM64 i uruchamia przez QEMU FINCH_CC=aarch64-unknown-linux-gnu-gcc finch build narzedzie.fch --target arm64 # inna nazwa kompilatora ``` @@ -131,41 +160,44 @@ bootstrap/install.sh ~/.local # albo do własnego katalogu finch version ``` -Podczas działania Finch potrzebuje `clanga` (kompiluje IR każdego programu i go linkuje). Mała biblioteka uruchomieniowa -budowana jest raz i trzymana w `~/.cache/finch`. `FINCH_CC` wskazuje inny kompilator do linkowania; `FINCH_PATH` dodaje -katalogi, w których szukać modułów. +Przy pracy Finch potrzebuje `clanga` (kompiluje nim IR każdego programu i linkuje go). Mała biblioteka runtime +jest budowana raz i trzymana w `~/.cache/finch`. `FINCH_CC` wskazuje inny kompilator do linkowania; `FINCH_PATH` +dodaje katalogi, w których Finch szuka modułów. --- ## 3. Polecenie finch ``` -finch run [argumenty...] skompiluj i od razu uruchom -finch build [-o nazwa] skompiluj do programu (domyślna nazwa: nazwa pliku) -finch test uruchom jego bloki test "..." { } (rozdział 27) -finch fmt [--check] [pliki/foldery] uporządkuj wcięcia plików .fch (domyślnie wszystkich stąd w dół) -finch ir pokaż LLVM IR (dla ciekawskich) -finch lsp serwer języka, dla edytorów -finch version pokaż wersję +finch run [argumenty...] kompiluje i od razu uruchamia +finch build [-o nazwa] kompiluje do programu (domyślna nazwa: nazwa pliku) +finch test uruchamia jego bloki test "..." { } (rozdział 27) +finch fmt [--check] [pliki/katalogi] porządkuje wcięcia plików .fch (domyślnie wszystkich w bieżącym katalogu) +finch ir wypisuje LLVM IR (dla ciekawskich) +finch lsp serwer języka, dla edytorów +finch version pokazuje wersję opcje: - -l dolinkuj bibliotekę C, to samo co link "lib" w pliku - -I szukaj nagłówków C także tam + -l dolinkuj bibliotekę C, to samo co link "bib" w pliku + -I szukaj nagłówków C także tam --target zbuduj na inny system: windows, linux, arm64, macos, android albo triple LLVM - -g dodaj informacje dla debuggera (gdb / lldb / Visual Studio) + --cpu użyj wszystkich instrukcji procesora: native (tego), x86-64-v3 (AVX2), + x86-64-v4 (AVX-512), apple-m1, ... (rozdział 31) + -g dodaj informacje dla debuggera (gdb i lldb) -O0 bez optymalizacji - --trace błędy w czasie działania pokazują łańcuch wywołań (i tak włączone dla run, test, -g i -O0) - --no-trace nie pokazują (zwykły finch build go nie ma: pilnowanie łańcucha spowalnia wywołania) + --trace błędy w trakcie działania pokazują ścieżkę wywołań (i tak włączone dla run, test, -g i -O0) + --no-trace nie pokazują (zwykłe finch build ich nie pokazuje: pamiętanie ścieżki spowalnia wywołania) ``` -W folderze z plikiem `finch.toml` (rozdział 28) `finch run`, `finch build` i `finch test` nie potrzebują nazwy pliku. +W katalogu z plikiem `finch.toml` (rozdział 28) `finch run`, `finch build` i `finch test` nie potrzebują nazwy pliku. -Skompilowany program i pliki C, które buduje (`link "x.c"`), zostają w cache (`~/.cache/finch`, na Windowsie -`%LOCALAPPDATA%`). Drugie budowanie tego samego kodu albo kodu, którego pliki C się nie zmieniły, pomija tę pracę. +Skompilowany program i zbudowane przez niego pliki C (`link "x.c"`) trafiają do pamięci podręcznej (`~/.cache/finch`, +na Windowsie `%LOCALAPPDATA%`). Druga kompilacja tego samego kodu, albo kodu, którego pliki C się nie zmieniły, pomija tę pracę. -`finch fmt` zmienia tylko układ: cztery spacje na poziom `{ }`, bez spacji na końcach linii, najwyżej jedna pusta linia -z rzędu. Linie, które ciągną wyrażenie (w `( )` albo po `+`), i tekst `"""` zachowują swój kształt. `finch fmt --check` -niczego nie zmienia i kończy się błędem, gdy jakiś plik by się zmienił (dla CI). Edytory z serwerem języka formatują na żądanie. +`finch fmt` zmienia tylko układ: cztery spacje na każdy poziom `{ }`, żadnych spacji na końcach linii, najwyżej jedna +pusta linia z rzędu. Linie kontynuujące wyrażenie (w nawiasach `( )` albo po `+`) i tekst `"""` zachowują swój kształt. +`finch fmt --check` niczego nie zmienia i kończy się błędem, jeśli jakiś plik by się zmienił (przydatne w CI). +Edytory z serwerem języka formatują na żądanie. Przykłady: @@ -173,10 +205,10 @@ Przykłady: finch run hello.fch # wypisuje wynik finch run narzedzie.fch dane.txt -v # argumenty po nazwie pliku trafiają do programu finch build gra.fch -o mojagra # tworzy ./mojagra -finch build gra.fch -g -O0 # wersja dla debuggera +finch build gra.fch -g -O0 # wersja do debuggera ``` -`finch run` przekazuje dalej kod wyjścia programu. Jeśli program się wysypie, powie jak +`finch run` przekazuje kod wyjścia twojego programu. Jeśli program się wysypie, mówi jak (np. `the program crashed: Segmentation fault`). --- @@ -186,13 +218,13 @@ finch build gra.fch -g -O0 # wersja dla debuggera ```c // komentarz zaczyna się od dwóch ukośników /* albo zajmuje - kilka linijek */ + kilka linii */ -import "math.h" // nagłówki C (opcjonalne), na górze -import ksztalty // moduły Fincha (opcjonalne): ksztalty.fch obok tego pliku -link "m" // biblioteki albo pliki C do dolinkowania (opcjonalne) +import "math.h" // nagłówki C (opcjonalnie), na początku pliku +import ksztalty // moduły Fincha (opcjonalnie): ksztalty.fch obok tego pliku +link "m" // biblioteki albo pliki C do dolinkowania (opcjonalnie) -struct Punkt { // własne typy +struct Punkt { // twoje własne typy int x int y } @@ -204,32 +236,32 @@ fn main() { // tu zaczyna się program Zasady, które warto znać: -- **Jedna instrukcja na linijkę.** Średników nie ma. Długie wyrażenie możesz złamać **po** operatorze - (`a +⏎ b`) albo w dowolnym miejscu wewnątrz nawiasów `( )` lub `[ ]`. -- **Klamry są zawsze obowiązkowe** po `if`, `else`, `for`, `while`, a otwierająca `{` stoi w tej samej linijce. -- **Bez nawiasów wokół warunków:** `if x > 5 {`, a nie `if (x > 5) {`. -- Funkcje i struktury mogą być w pliku w dowolnej kolejności. +- **Jedna instrukcja w linii.** Nie ma średników. Długie wyrażenie można złamać **po** operatorze (`a +⏎ b`) + albo w dowolnym miejscu w nawiasach `( )` lub `[ ]`. +- **Klamry są zawsze obowiązkowe** po `if`, `else`, `for`, `while`, a otwierająca `{` stoi w tej samej linii. +- **Warunki bez nawiasów:** `if x > 5 {`, a nie `if (x > 5) {`. +- Funkcje i struktury mogą stać w pliku w dowolnej kolejności. - `main` to `fn main()`, `fn main() -> int` (zwraca kod wyjścia) albo `fn main([]str args)` - (dostaje argumenty z linii poleceń; `args[0]` to ścieżka samego programu). + (dostaje argumenty z wiersza poleceń; `args[0]` to ścieżka samego programu). --- ## 5. Zmienne ```c -x := 5 // nowa zmienna, typ zgadnięty (int) -int y = 10 // nowa zmienna, typ podany jawnie -int z // bez wartości: startuje od zera ("" dla str, [] dla tablic, null dla wskaźników) +x := 5 // nowa zmienna, typ wynika z wartości (int) +int y = 10 // nowa zmienna z podanym typem +int z // bez wartości: zero ("" dla str, [] dla tablic, null dla wskaźników) Punkt p // struktura bez wartości: zera albo wartości domyślne struktury x = 7 // przypisanie x += 1 // także -= *= /= %= ``` -- Nazwę można zadeklarować **raz** w funkcji. Finch nie pozwala użyć jej ponownie w bloku - wewnętrznym (nie ma „przesłaniania”), bo to częste źródło pomyłek. -- Zmienna istnieje od deklaracji do końca bloku `{ }`, w którym powstała. -- Nazwy: litery ASCII, cyfry i `_`, bez cyfry na początku. Polskie litery tylko w tekstach w cudzysłowie. +- Nazwę można zadeklarować w funkcji **tylko raz**. Finch nie pozwala użyć tej samej nazwy w bloku wewnętrznym + (nie ma „przesłaniania”), bo to częste źródło pomyłek. +- Zmienna istnieje od deklaracji do końca bloku `{ }`, w którym stoi. +- Nazwy: litery ASCII, cyfry i `_`, nie mogą zaczynać się od cyfry. Polskie litery wolno pisać tylko w tekstach. Nazwy typów (`int`, `u8`, …), funkcji wbudowanych i słowa kluczowe są zarezerwowane. **Słowa kluczowe:** `fn return if else while for in break continue true false null import link struct enum match defer or try const export` @@ -243,8 +275,8 @@ const f32 GRAWITACJA = 9.8 // z typem const TYTUL = "Moja gra" ``` -Stałą piszesz na najwyższym poziomie pliku, obok funkcji, i nie da się jej zmienić. Składa się z liczb, tekstu, -`true`/`false`, znaków, innych stałych i operatorów; stała z modułu to `ksztalty.BOKI`. +Stałą zapisuje się na najwyższym poziomie pliku, obok funkcji, i nie da się jej zmienić. Składa się z liczb, tekstów, +`true`/`false`, znaków, innych stałych i operatorów; stała z modułu to np. `ksztalty.BOKI`. --- @@ -254,18 +286,18 @@ Stałą piszesz na najwyższym poziomie pliku, obok funkcji, i nie da się jej z | Typ | Przechowuje | Przykład | |---------|-------------|----------| -| `int` | liczba całkowita, 64-bitowa (to samo co `i64`) | `42`, `-7`, `0xFF`, `1_000_000` | -| `float` | liczba z przecinkiem, 64-bitowa (to samo co `f64`) | `3.14` | +| `int` | liczbę całkowitą, 64 bity (to samo co `i64`) | `42`, `-7`, `0xFF`, `1_000_000` | +| `float` | liczbę zmiennoprzecinkową, 64 bity (to samo co `f64`) | `3.14` | | `bool` | `true` albo `false` | | | `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]` | +| `[]T` | listę (tablicę) wartości typu `T` | `[1, 2, 3]` | +| `map[K]V` | wartości typu `V` wyszukiwane 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`, `\\`, `\"`, `\'`. +Znaki specjalne w tekstach i znakach: `\n` nowa linia, `\t` tabulator, `\r`, `\0`, `\\`, `\"`, `\'`. -### Typy z rozmiarem (do plików, sprzętu, bibliotek C) +### Typy o podanym rozmiarze (do plików, sprzętu, bibliotek C) | Ze znakiem | Zakres | Bez znaku | Zakres | |---|---|---|---| @@ -274,7 +306,9 @@ Znaki specjalne w tekstach: `\n` nowa linia, `\t` tabulator, `\r`, `\0`, `\\`, ` | `i32` | ok. ±2,1 mld | `u32` | 0 … ok. 4,29 mld | | `i64` | ok. ±9,2 × 10¹⁸ | `u64` | 0 … ok. 1,8 × 10¹⁹ | -`f32` to 32-bitowa liczba z przecinkiem (ok. 7 cyfr znaczących), `f64` to 64-bitowa (ok. 15–16). +`f32` to 32-bitowa liczba zmiennoprzecinkowa (ok. 7 cyfr znaczących), `f64` to 64-bitowa (ok. 15–16). + +Są też **wektory**, czyli kilka liczb obok siebie liczonych naraz: `f32x8`, `i8x32` i podobne (rozdział 31). ### Zamiana typów @@ -282,27 +316,27 @@ Finch zamienia typy sam **tylko wtedy, gdy nic nie może zginąć**: | Z | Na | Automatycznie? | |---|---|---| -| mniejsza liczba całkowita | większa o tej samej znakowości (`i8`→`i32`, `u8`→`u64`) | ✅ | -| bez znaku | ściśle większa ze znakiem (`u8`→`i16`, `u32`→`i64`) | ✅ | +| mniejsza liczba całkowita | większa o tym samym znaku (`i8`→`i32`, `u8`→`u64`) | ✅ | +| bez znaku | większa ze znakiem (`u8`→`i16`, `u32`→`i64`) | ✅ | | dowolna liczba całkowita | `float` / `f32` | ✅ | | `f32` | `f64` | ✅ | | `null` | dowolny wskaźnik | ✅ | -| dowolny wskaźnik | `ptr`, a `ptr` na dowolny wskaźnik | ✅ | -| wszystko inne | | ❌ napisz to jawnie: `typ(wartość)` | +| dowolny wskaźnik | `ptr` i z `ptr` na dowolny wskaźnik | ✅ | +| wszystko inne | | ❌ trzeba napisać: `typ(wartosc)` | -**Liczby wpisane w kod się dopasowują.** Liczba napisana wprost w kodzie pasuje do każdego typu, -który ją pomieści: `u8 b = 200` działa, a `u8 b = 300` to błąd. To samo dotyczy stałych z nagłówków C. +**Liczby wpisane w kod dopasowują się.** Liczba zapisana w kodzie pasuje do każdego typu, który ją pomieści: +`u8 b = 200` działa, `u8 b = 300` to błąd. Tak samo jest ze stałymi z nagłówków C. -**Jawna zamiana** wygląda jak wywołanie funkcji o nazwie typu: +**Jawne zamiany** zapisuje się nazwą typu, jak wywołanie funkcji: ```c -int(3.99) // 3 (obcina część ułamkową) +int(3.99) // 3 (ucina część ułamkową) float(7) / 2 // 3.5 -u8(300) // 44 (zostawia najniższe 8 bitów) +u8(300) // 44 (zostaje 8 najniższych bitów) int('A') // 65 char(66) // 'B' bool(0) // false (każda liczba różna od zera to true) -str(42) // "42" (też float, bool, char; str([]u8) zamienia bajty na tekst) +str(42) // "42" (także liczby zmiennoprzecinkowe, bool i znaki; str([]u8) zamienia bajty na tekst) int("42") // 42 (zatrzymuje program, jeśli tekst nie jest liczbą) float("2.5") // 2.5 ptr(p) // wskaźnik z typem jako wskaźnik bez typu @@ -311,8 +345,8 @@ int(p) // wskaźnik jako liczba (jego adres) str(p) // wskaźnik na znaki z C jako tekst ``` -**Liczby o stałym rozmiarze się „zawijają”**, jak w prawdziwym sprzęcie: `u8` równe 255 plus 1 daje 0. -Działania liczą się w typie użytych wartości, więc `u8(250) + 10` daje `4`, a nie `260`. +**Liczby o stałym rozmiarze się zawijają**, jak w prawdziwym sprzęcie: `u8` równe 255 plus 1 daje 0. +Obliczenia odbywają się w typie użytych wartości, więc `u8(250) + 10` to `4`, a nie `260`. --- @@ -322,13 +356,13 @@ Działania liczą się w typie użytych wartości, więc `u8(250) + 10` daje `4` | Priorytet | Operatory | Znaczenie | |---|---|---| -| 5 | `*` `/` `%` `<<` `>>` `&` | mnożenie, dzielenie, reszta, przesunięcia bitów, bitowe AND | -| 4 | `+` `-` `\|` `^` | dodawanie (i sklejanie tekstu), odejmowanie, bitowe OR, bitowe XOR | +| 5 | `*` `/` `%` `<<` `>>` `&` | mnożenie, dzielenie, reszta, przesunięcie w lewo/prawo, bitowe AND | +| 4 | `+` `-` `\|` `^` | dodawanie (i łączenie tekstów), odejmowanie, bitowe OR, bitowe XOR | | 3 | `==` `!=` `<` `<=` `>` `>=` | porównania | -| 2 | `&&` | logiczne I | -| 1 | `\|\|` | logiczne LUB | +| 2 | `&&` | logiczne AND | +| 1 | `\|\|` | logiczne OR | -Jednoargumentowe (przed wartością): `-x` zmiana znaku, `!x` logiczne NIE, `~x` odwrócenie wszystkich bitów. +Jednoargumentowe (przed wartością): `-x` zmiana znaku, `!x` logiczne NOT, `~x` odwrócenie wszystkich bitów. > **Celowo inaczej niż w C:** operatory bitowe wiążą mocniej niż porównania, więc > `flagi & 4 != 0` znaczy `(flagi & 4) != 0`. @@ -336,20 +370,20 @@ Jednoargumentowe (przed wartością): `-x` zmiana znaku, `!x` logiczne NIE, `~x` Uwagi: - `/` na liczbach całkowitych zaokrągla w stronę zera: `7 / 2` to `3`. `%` ma znak lewej strony. -- Dzielenie przez zero **zatrzymuje program** komunikatem `runtime error: division by zero` z numerem linijki. -- `&&` i `||` liczą prawą stronę tylko wtedy, gdy to potrzebne. -- Tekst: `+` skleja, `==` `!=` porównują treść, `<` `>` porównują alfabetycznie (bajt po bajcie). -- Wskaźniki porównuje się przez `==` i `!=`, także z `null`. `str` nigdy nie jest `null`. -- Tablic i struktur nie porównasz przez `==`; porównuj ich części. +- Dzielenie przez zero **zatrzymuje program** komunikatem `runtime error: division by zero` z numerem linii. +- `&&` i `||` obliczają prawą stronę tylko wtedy, gdy trzeba. +- Teksty: `+` łączy, `==` `!=` porównują zawartość, `<` `>` porównują alfabetycznie (bajt po bajcie). +- Wskaźniki można porównywać przez `==` i `!=`, także z `null`. `str` nigdy nie jest `null`. +- Tablic i struktur nie da się porównać przez `==`; porównaj ich części. --- -## 8. Sterowanie przebiegiem +## 8. Sterowanie -`if` może też dawać wartość; każda gałąź to jedna wartość, a `else` jest wymagane: +`if` może też dawać wartość; każda gałąź zawiera jedną wartość, a `else` jest obowiązkowe: ```c -opis := if wynik > 100 { "dużo" } else if wynik > 10 { "średnio" } else { "mało" } +ocena := if wynik > 100 { "wysoki" } else if wynik > 10 { "średni" } else { "niski" } f32 predkosc = if biegnie { 2.5 } else { 1.0 } ``` @@ -366,12 +400,12 @@ while licznik > 0 { licznik -= 1 } -for i in 0..10 { // i = 0, 1, …, 9 (koniec nie jest wliczany) +for i in 0..10 { // i = 0, 1, …, 9 (koniec nie wchodzi) if i == 3 { continue } if i == 8 { break } } -for imie in imiona { // każdy element tablicy (albo każdy znak str) +for imie in imiona { // każdy element tablicy (albo każdy znak tekstu) print(imie) } @@ -379,31 +413,30 @@ 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) +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 {`. -- W `for i in a..b` oba końce są liczone **raz**. `i` jest typu `int` i nie można go zmieniać w pętli. -- W `for x in lista` zmiennej `x` też nie zmienisz. Żeby zmieniać elementy, przechodź po indeksach: - `for i in 0..lista.len { lista[i] = ... }`. Dodawanie do listy w trakcie pętli jest dozwolone. +- W `for i in a..b` oba końce są obliczane **raz**. `i` jest typu `int` i nie można go zmieniać w pętli. +- W `for x in lista` też nie można zmieniać `x`. Żeby zmieniać elementy, idź po indeksach: + `for i in 0..lista.len { lista[i] = ... }`. Dodawanie do listy w trakcie pętli jest w porządku. - `while true { ... }` kręci się do `break` albo `return`. -- Kod po `return`, `break` lub `continue` w tym samym bloku to błąd, bo nigdy się nie wykona. +- Kod po `return`, `break` czy `continue` w tym samym bloku to błąd, bo nigdy się nie wykona. ### defer -`defer` uruchamia instrukcję, gdy bieżący blok się kończy, niezależnie od tego, jak się kończy: -normalnie, przez `return`, `break` lub `continue`. Kilka `defer` wykonuje się w odwrotnej kolejności. -Przydaje się do sprzątania: +`defer` wykonuje instrukcję, gdy kończy się bieżący blok, niezależnie od tego, jak się kończy: normalnie, przez +`return`, `break` albo `continue`. Kilka `defer` wykonuje się w odwrotnej kolejności. Przydaje się do sprzątania: ```c f := fopen("dane.txt", "r") defer fclose(f) -// ... używaj f; zostanie zamknięty na końcu bloku, na każdej ścieżce +// ... używaj f; zostanie zamknięty na końcu bloku, którąkolwiek drogą program z niego wyjdzie ``` -Instrukcja w `defer` nie może zawierać `return`, `break` ani `continue`. +Odroczona instrukcja nie może zawierać `return`, `break` ani `continue`. --- @@ -415,19 +448,19 @@ fn nazwa(typ1 param1, typ2 param2) -> typ_wyniku { return wartosc } -fn bez_wyniku(str wiadomosc) { // bez "->": nic nie zwraca - print(wiadomosc) +fn bez_wyniku(str komunikat) { // bez "->": niczego nie zwraca + print(komunikat) } ``` -- Argumenty zachowują się jak kopie: zmiana parametru nigdy nie zmienia zmiennej u wywołującego. +- Argumenty zachowują się jak kopie: zmiana parametru nigdy nie zmienia zmiennej wywołującego. (Finch kopiuje naprawdę tylko wtedy, gdy trzeba: funkcja, która nie zmienia tablicy, dostaje ją za darmo.) - Ż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 + Żeby zmienić zmienną wywołującego, przekaż wskaźnik: zob. [Wskaźniki](#14-wskaźniki). +- Funkcja z `-> typ` musi zwracać wartość na każdej ścieżce. Finch to sprawdza. +- Nie można zdefiniować funkcji wewnątrz innej funkcji. +- Funkcję, która może się nie udać, zapisuje się `-> int!` (albo `-> !` bez wyniku); zob. [Błędy jako wartości](#błędy-jako-wartości). -- Długie wywołania możesz rozbić na kilka linijek wewnątrz nawiasów; przecinek po ostatnim argumencie jest dozwolony. +- Długie wywołania można rozbić na kilka linii w nawiasach; przecinek po ostatnim argumencie jest dozwolony. --- @@ -440,20 +473,20 @@ siatka := [[1, 2], [3, 4]] // tablice tablic []f32 xs = [1, 2.5] // typ decyduje, czym staną się liczby ``` -| Operacja | Znaczenie | +| Działanie | Znaczenie | |---|---| | `a.len` | liczba elementów | -| `a[i]` | element `i` (od 0); poza zakresem program zatrzymuje się z czytelnym komunikatem | +| `a[i]` | element `i` (licząc od 0); indeks poza zakresem zatrzymuje program jasnym komunikatem | | `a[i] = x` | zmiana elementu | -| `a.push(x)` | dodaj na końcu | -| `a.pop()` | zabierz ostatni i go oddaj | -| `a.insert(i, x)` | wstaw `x` na pozycję `i`, przesuwając resztę | -| `a.remove(i)` | usuń pozycję `i` i ją oddaj | -| `a.clear()` | usuń wszystko | -| `a.resize(n)` | zmień długość na `n` (nowe elementy to zera) | -| `a.contains(x)`, `a.find(x)` | czy `x` jest w środku? jego indeks albo -1 (liczby, znaki, bool, str) | +| `a.push(x)` | dodanie na końcu | +| `a.pop()` | zdjęcie ostatniego elementu i zwrócenie go | +| `a.insert(i, x)` | wstawienie `x` na pozycję `i` z przesunięciem reszty | +| `a.remove(i)` | usunięcie pozycji `i` i zwrócenie jej | +| `a.clear()` | usunięcie wszystkiego | +| `a.resize(n)` | zmiana długości na `n` (nowe elementy to zera) | +| `a.contains(x)`, `a.find(x)` | czy `x` tam jest? jego indeks albo -1 (liczby, znaki, bool, str) | | `a.sort()`, `a.reverse()` | w miejscu (liczby, znaki, str) | -| `a.slice(s, e)` | nowa tablica z elementami `s` … `e-1` | +| `a.slice(p, k)` | nowa tablica z elementami `p` … `k-1` | | `a.join(sep)` | dla `[]str`: jeden tekst z `sep` między częściami | | `a.ptr` | adres pierwszego elementu (dla C) | | `print(a)` | wypisuje `[1, 2, 3]` | @@ -462,24 +495,23 @@ siatka := [[1, 2], [3, 4]] // tablice tablic ### 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. +`map[K]V` znajduje wartości po kluczu. Kluczami mogą być liczby całkowite, `char`, `bool` albo `str`; wartościami +cokolwiek (tablice, struktury, inne mapy). Przechodzenie po mapie idzie w kolejności, w jakiej klucze dodano po raz pierwszy. ```c -wiek := ["anna": 31, "bob": 25] // literał mapy: map[str]int +wiek := ["anna": 31, "bob": 25] // mapa zapisana wprost: map[str]int map[str][]str grupy // pusta mapa map[int]str nazwy = [:] // [:] to pusta mapa tam, gdzie typ jest znany ``` -| Operacja | Znaczenie | +| Działanie | 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[k] = v` | dodanie albo podmiana | +| `m[k] += 1`, `m[k].push(x)`, `m[k].pole = …` | zmiana nieistnieją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.has(k)` | czy `k` tam jest? | +| `m.get(k, domyslna)` | wartość albo `domyslna` (obliczana tylko wtedy, gdy trzeba) | | `m.remove(k)` | usuwa `k`; `true`, jeśli był | | `m.clear()` | usuwa wszystko | | `m.keys()`, `m.values()` | nowe tablice, w kolejności dodawania | @@ -495,9 +527,9 @@ for slowo, n in ile { } ``` -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. +Wyszukiwanie korzysta z haszowania (średnio stały czas). Tak jak tablice, mapy są właścicielami swoich 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 w tablicy i zmień mapę po pętli. --- @@ -505,37 +537,37 @@ mapy wewnątrz `for` po tej samej mapie; zbierz je do tablicy i zmień mapę po ```c s := "Cześć" -s += ", świecie" // sklejanie -print(s.len, s[0]) // .len liczy bajty; ą, ś, ż… zajmują po 2 -s[0] = 'D' // zmiana znaku +s += ", świecie" // łączenie +print(s.len, s[0]) // 17 C (.len liczy bajty; ą, ś, ż, … zajmują po 2) +s[0] = 'K' // zmiana znaku ``` -| Metoda | Oddaje | +| Metoda | Zwraca | |---|---| | `s.sub(a, b)` | fragment od `a` do `b-1` | | `s.find(t)` | gdzie zaczyna się `t`, albo -1 | | `s.contains(t)`, `s.starts_with(t)`, `s.ends_with(t)` | `bool` | | `s.split(sep)` | `[]str`; `split("")` daje pojedyncze znaki | -| `s.trim()` | bez spacji i nowych linii na obu końcach | -| `s.upper()`, `s.lower()` | zmienione litery A–Z / a–z (tylko ASCII) | +| `s.trim()` | bez spacji i znaków nowej linii na obu końcach | +| `s.upper()`, `s.lower()` | zamienione A–Z / a–z (litery ASCII) | | `s.replace(a, b)` | każde `a` zamienione na `b` | -| `s.repeat(n)` | `s` n razy | +| `s.repeat(n)` | `s` powtórzone n razy | | `s.bytes()` | `[]u8` z bajtami | -| `s.ptr` | `ptr[char]` dla C (ważny, dopóki żyje `s`) | +| `s.ptr` | `ptr[char]` dla C (ważny, dopóki `s` istnieje) | -Wartości wstawia się do tekstu przez `\(...)`: dowolny kod, a `, 2` po liczbie daje dwie cyfry po przecinku. -`\` przed `(`, które ma zostać zwykłym znakiem: `"\\(to nie kod)"`. +Wartości wstawia się w tekst przez `\(...)`: dowolny kod, a po liczbie `, 2` dla dwóch cyfr po przecinku. +Jeśli `(` ma zostać zwykłym znakiem, poprzedź `\` drugim ukośnikiem: `"\\(to nie kod)"`. ```c print("hp \(hp) z \(max_hp), prędkość \(v, 2), imię \(p.imie.upper())") ``` -`str` pamięta swoją długość, więc `.len` działa natychmiast. Zawsze kończy się zerem, więc -przekazanie go do C jako `char*` działa. W drugą stronę: funkcja C, która zwraca `char *`, daje ci `str` -(kopia powstaje, gdy go zachowujesz), a `str(p)` czyta `ptr[char]` zdobyty inaczej. +`str` pamięta swoją długość, więc `.len` jest natychmiastowe, a na końcu zawsze ma bajt zerowy, więc można go przekazać +do C jako `char*`. W drugą stronę: funkcja C, która zwraca `char *`, daje ci `str` (kopia powstaje, gdy go zatrzymujesz), +a `str(p)` czyta `ptr[char]` zdobyty w inny sposób. -Tekst w kilku liniach piszesz między `"""`. Znak nowej linii zaraz po otwarciu znika, a z każdej linii znika -wcięcie zamykających cudzysłowów, więc tekst może być wcięty razem z kodem: +Tekst na kilka linii zapisuje się między `"""`. Znak nowej linii po otwierającym cudzysłowie znika, a z każdej linii +znika też wcięcie zamykającego cudzysłowu, więc tekst może mieć wcięcie razem z kodem: ```c shader := """ @@ -544,8 +576,8 @@ shader := """ """ ``` -Liczby jako tekst: `str(x)` to najkrótszy tekst, który czyta się z powrotem jako ta sama liczba (`0.1`, -`0.30000000000000004`, `1e+20`); `str(x, 2)` ma zawsze dwie cyfry po kropce (`3.14`). +Liczby jako tekst: `str(x)` to najkrótszy zapis, który da z powrotem tę samą liczbę (`0.1`, `0.30000000000000004`, +`1e+20`); `str(x, 2)` ma zawsze dwie cyfry po przecinku (`3.14`). --- @@ -556,27 +588,27 @@ struct Gracz { str imie int zycia = 3 // wartość domyślna []int wyniki - Punkt pozycja // struktury w strukturach + Punkt poz // struktury w strukturach } -g := Gracz(imie: "Ola") // po nazwie: brakujące pola dostają domyślne (albo zero) +p := Gracz(imie: "Ola") // przez nazwy: brakujące pola dostają wartość domyślną (albo zero) q := Punkt(1, 2) // po kolei: wtedy trzeba podać każde pole -g.zycia -= 1 -g.wyniki.push(10) -g.pozycja.x = 5 -print(g) // Gracz(imie: "Ola", zycia: 2, wyniki: [10], pozycja: Punkt(x: 5, y: 0)) +p.zycia -= 1 +p.wyniki.push(10) +p.poz.x = 5 +print(p) // Gracz(imie: "Ola", zycia: 2, wyniki: [10], poz: Punkt(x: 5, y: 0)) ``` -- 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). +- Przypisanie albo przekazanie struktury kopiuje ją, razem z jej tablicami i tekstami. +- Struktura nie może bezpośrednio zawierać samej siebie; do tego służy pole `ptr[Wezel]` albo `[]Wezel` (drzewa, listy). ### 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ą. +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, a nie kopia: zmiany w `self` zostają. ```c -fn Gracz.wylecz(int ile) { +fn Gracz.ulecz(int ile) { self.zycia += ile } @@ -584,41 +616,41 @@ fn Gracz.zyje() -> bool { return self.zycia > 0 } -g := Gracz(imie: "Ola") -g.wylecz(2) // g.zycia wynosi teraz 5 -if g.zyje() { ... } +p := Gracz(imie: "Ola") +p.ulecz(2) // p.zycia wynosi teraz 5 +if p.zyje() { ... } -druzyna[0].wylecz(1) // na elemencie tablicy, polu, m[klucz], przez ptr[Gracz] ... +druzyna[0].ulecz(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 stoi w tym samym pliku (module) co jej struktura i jest dostępna wszędzie tam, gdzie ta struktura. +- Metody można dodawać też 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). +- Wywołanie metody zmieniającej `self` na zmiennej pętli (`for p in druzyna { p.ulecz(1) }`) to błąd, bo zmienna pętli + jest tylko do odczytu; użyj `for i in 0..druzyna.len { druzyna[i].ulecz(1) }`. +- Metody, tak jak funkcje, mogą się nie udać (`-> !`); zob. [Błędy jako wartości](#błędy-jako-wartości). --- ## 13. Pamięć: kto co zwalnia -Tablic, tekstów i struktur nie zwalniasz nigdy. Finch robi to za ciebie, w przewidywalnym momencie: -**gdy kończy się blok, do którego wartość należy** (`}`), albo wcześniej przy `return`, `break`, `continue`. +Tablic, tekstów i struktur nigdy nie zwalniasz. Finch robi to za ciebie, w przewidywalnym momencie: +**gdy kończy się blok, do którego wartość należy** (`}`), albo wcześniej, przy `return`, `break`, `continue`. -- Zmienna **jest właścicielem** swojej wartości. Przypisanie kopiuje, więc dwie zmienne nigdy nie dzielą jednej listy. -- Wartość z wywołania funkcji albo wyrażenia jest **przenoszona** tam, gdzie ją zapisujesz, bez kopiowania. -- `return lista` przenosi zmienną poza funkcję, bez kopiowania. -- Parametry funkcji są **pożyczane**: funkcja dostaje wartość wywołującego bez kopii. Jeśli ją zmienia, - Finch najpierw robi jej prywatną kopię. Zauważysz to najwyżej po szybkości. +- Zmienna jest **właścicielem** swojej wartości. Przypisanie ją kopiuje, więc dwie zmienne nigdy nie dzielą jednej listy. +- Wartość z wywołania funkcji albo z wyrażenia jest **przenoszona** tam, gdzie trafia: bez kopii. +- `return lista` przenosi zmienną na zewnątrz funkcji: bez kopii. +- Parametry funkcji są **pożyczane**: funkcja dostaje wartość wywołującego bez kopii. Jeśli funkcja zmienia parametr, + Finch najpierw robi jej prywatną kopię. Zauważysz to najwyżej w szybkości. -Do pamięci zarządzanej samodzielnie (listy wiązane, drzewa, dzielenie między strukturami): +Dla pamięci, którą zarządzasz sam (listy wiązane, drzewa, współdzielenie między strukturami): ```c -wezel := new(Wezel(wartosc: 1)) // wstaw wartość na stertę, dostajesz ptr[Wezel] -defer free(wezel) // free(...) ją oddaje (razem z jej tablicami i tekstami) +wezel := new(Wezel(wartosc: 1)) // wartość na stercie, dostajesz ptr[Wezel] +defer free(wezel) // free(...) oddaje pamięć (i zwalnia tablice i teksty w środku) ``` -Pamięć z C (`malloc`, obiekty bibliotek) zwalniasz funkcjami z C, jak w C. +Pamięć z C (`malloc`, obiekty bibliotek) zwalnia się funkcjami C, tak jak w C. --- @@ -632,8 +664,8 @@ Wskaźnik przechowuje **adres** wartości. Finch zamiast `*` i `&` z C używa s | `&x` | `addr(x)` | adres x (także `addr(a[i])`, `addr(s.pole)`) | | `*p` | `p.value` | wartość, na którą wskazuje p | | `p->pole` | `p.pole` | pole struktury, na którą wskazuje p | -| `p[i]` | `p[i]` | indeksowanie jak w C (bez kontroli zakresu) | -| `NULL` | `null` | wskazuje donikąd | +| `p[i]` | `p[i]` | indeksowanie jak w C (bez sprawdzania zakresu) | +| `NULL` | `null` | nigdzie nie wskazuje | | `void *` | `ptr` | wskazuje na coś nieznanego typu | ```c @@ -642,14 +674,14 @@ fn podwoj(ptr[int] p) { } x := 21 -podwoj(addr(x)) // x to 42 +podwoj(addr(x)) // x wynosi 42 ``` Jeśli struktura ma własne pole o nazwie `value`, to `p.value` na wskaźniku do niej oznacza to pole. -Użycie `.value`, `.pole` albo `[i]` na wskaźniku `null` **zatrzymuje program** z czytelnym komunikatem. +Użycie `.value`, `.pole` albo `[i]` na wskaźniku `null` **zatrzymuje program** jasnym komunikatem. -⚠️ Wskaźnika na zmienną nie wolno używać po zakończeniu jej bloku. Finch tego nie sprawdza. +⚠️ Wskaźnika na zmienną nie wolno używać po zakończeniu bloku tej zmiennej. Finch tego nie sprawdza. --- @@ -657,34 +689,34 @@ Użycie `.value`, `.pole` albo `[i]` na wskaźniku `null` **zatrzymuje program** | Funkcja | Co robi | |---|---| -| `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, 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ę. | +| `print(a, b, ...)` | Wypisuje wszystkie wartości oddzielone spacjami, a na końcu przechodzi do nowej linii. Działa z każdym typem, także z tablicami i strukturami. | +| `input()`, `input("pytanie")` | Wczytuje jedną linię wpisaną przez użytkownika (bez znaku nowej linii). Na końcu wejścia zwraca pusty tekst. | +| `read_file(sciezka)` | Cały plik jako `str`. Jeśli pliku nie da się przeczytać, zatrzymuje program jasnym komunikatem, chyba że to obsłużysz: `read_file(p) or ...`. | +| `write_file(sciezka, tekst)` | Zapisuje (zastępuje) plik; zwraca `true`, jeśli się udało. Z `or` / `try` niepowodzenie 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. | +| `error(komunikat)` | Niepowodzenie, które zwraca funkcja mogąca się nie udać: `return error("...")`. | +| `shell(polecenie)` | Uruchamia polecenie powłoki i zwraca 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)). `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ą. | +| `addr(x)` | Wskaźnik na `x`; `addr(funkcja)` daje C wskaźnik na funkcję (funkcję zwrotną). | +| `new(wartosc)`, `free(p)` | Ręczna pamięć na stercie (zob. [Pamięć](#13-pamięć-kto-co-zwalnia)). | +| `int(x)`, `str(x)`, `u8(x)`, … | Zamiany typów (zob. [Zamiana typów](#zamiana-typów)). `str(x)` działa na każdym typie i daje ten sam tekst, który pokazuje `print`. `int(tekst) or 0` obsługuje tekst, który nie jest liczbą. | --- ## 16. Moduły -Program możesz podzielić na pliki. `import ksztalty` wczytuje `ksztalty.fch` z tego samego folderu -(albo z folderu wymienionego w zmiennej środowiskowej `FINCH_PATH`, rozdzielonych `:`). +Program można podzielić na pliki. `import ksztalty` wczytuje `ksztalty.fch` z tego samego katalogu (albo z katalogu +wymienionego w zmiennej środowiskowej `FINCH_PATH`; kilka katalogów oddziela się `:`). ```c // ksztalty.fch -struct Pudlo { +struct Pudelko { float w float h } -fn pole(Pudlo p) -> float { +fn pole(Pudelko p) -> float { return p.w * p.h } ``` @@ -694,45 +726,45 @@ fn pole(Pudlo p) -> float { import ksztalty fn main() { - p := ksztalty.Pudlo(2, 3) + p := ksztalty.Pudelko(2, 3) print(ksztalty.pole(p)) - ksztalty.Pudlo inne // typ z modułu w deklaracji + ksztalty.Pudelko inne // typ z modułu w deklaracji } ``` -- Wszystkiego z modułu używasz z jego nazwą z przodu: `ksztalty.pole`, `ksztalty.Pudlo`. +- Wszystkiego z modułu używa się z nazwą modułu na początku: `ksztalty.pole`, `ksztalty.Pudelko`. - Moduł nie ma `main`. Moduły mogą importować inne moduły. -- Moduły w folderach: `import ui/przycisk` wczytuje `ui/przycisk.fch`, a używasz go jako `przycisk.rysuj()`. -- Inna nazwa: `import ui/przycisk as p` → `p.rysuj()`. Dwa importy nie mogą mieć tej samej nazwy. +- Moduły w katalogach: `import ui/button` wczytuje `ui/button.fch`, a używa się go jako `button.draw()`. +- Pod inną nazwą: `import ui/button as btn` → `btn.draw()`. Dwa importy nie mogą mieć tej samej nazwy. --- -## 17. Biblioteki z C +## 17. Biblioteki C ### Krok 1: zaimportuj nagłówek ```c import "stdio.h" import "GLFW/glfw3.h" // ścieżka w systemowych katalogach nagłówków -import "mojalib.h" // nagłówek obok twojego pliku .fch +import "mojabib.h" // nagłówek obok twojego pliku .fch ``` Finch czyta nagłówek przez **libclang** (parser C z clanga) i udostępnia: - **funkcje**, także te ze zmienną liczbą argumentów, jak `printf`, -- **struktury**, przez wartość i przez wskaźnik (`Vector2`, `SDL_Rect`, `CXCursor`, …), razem z polami, -- **wartości `enum`** i **proste stałe `#define`** (`M_PI`, `GL_COLOR_BUFFER_BIT`), -- **zmienne globalne**, np. `stdout` i `stderr`. +- **struktury**, przez wartość i przez wskaźnik (`Vector2`, `SDL_Rect`, `CXCursor`, …), razem z ich polami, +- **wartości `enum`** i **proste stałe z `#define`** (`M_PI`, `GL_COLOR_BUFFER_BIT`), +- **zmienne globalne**, jak `stdout` i `stderr`. ### Krok 2: dolinkuj bibliotekę ```c -link "glfw" // używa libglfw.so (najpierw pyta pkg-config, jeśli jest) -link "pomocnicze.c" // skompiluj i dołącz własny plik C (obok pliku .fch) +link "glfw" // używa libglfw.so (najpierw pyta pkg-config, jeśli jest zainstalowany) +link "pomocnicze.c" // kompiluje i linkuje twój plik C (obok pliku .fch) link "gotowe.a" // albo plik obiektowy / bibliotekę statyczną ``` -Jeśli zapomnisz linijki `link`, Finch powie dokładnie, czego brakuje: +Jeśli zapomnisz o linii `link`, Finch powie dokładnie, czego brakuje: ``` error: glfwInit, glfwCreateWindow, glfwMakeContextCurrent and 5 more come from "GLFW/glfw3.h", but its library isn't linked @@ -749,15 +781,15 @@ error: glfwInit, glfwCreateWindow, glfwMakeContextCurrent and 5 more come from " | `unsigned …` | `u16`, `u32`, `u64` | | `float`, `double` | `f32`, `f64` | | `_Bool` / `bool` | `bool` | -| `enum` | odpowiadająca liczba całkowita ze znakiem | +| `enum` | odpowiednia liczba całkowita ze znakiem | | `char *` jako **parametr albo wynik** | `str` | -| `char *` w strukturze albo za wskaźnikiem | `ptr[char]` (czytasz przez `str(p)`) | +| `char *` w strukturze albo za wskaźnikiem | `ptr[char]` (czyta się go przez `str(p)`) | | `struct X` przez wartość | `X`, struktura z tymi samymi polami | | `struct X *`, `int *`, … | `ptr[X]`, `ptr[i32]`, … | -| `int arr[16]` w strukturze | `[16]i32`: indeksujesz i używasz `.len` | +| `int tab[16]` w strukturze | `[16]i32`: można go indeksować i użyć `.len` | | `void *`, wskaźniki na funkcje, niekompletne struktury | `ptr` | -### Przykład: struktury i raylib +### Przykład: struktury i funkcje zwrotne ```c import "raylib.h" @@ -775,15 +807,15 @@ fn main() { } ``` -Żeby dać C funkcję do wywołania zwrotnego (np. porównywarkę dla `qsort`), użyj `addr(mojaFunkcja)`. +Żeby dać C funkcję do wywołania (jak funkcję porównującą dla `qsort`), użyj `addr(mojaFunkcja)`. Jej parametry muszą być typami C (liczby, wskaźniki, struktury C). ### Grafika: OpenGL i Vulkan Oba działają bezpośrednio; `examples/opengl.fch` i `examples/vulkan.fch` to kompletne programy. -- **OpenGL 3+** na Linuksie: nowoczesne funkcje (`glCreateShader`, `glGenVertexArrays`, …) są deklarowane - dopiero po zdefiniowaniu `GL_GLEXT_PROTOTYPES`. Finch nie definiuje makr C, więc dołączenia umieść w małym +- **OpenGL 3+** na Linuksie: nowoczesne funkcje (`glCreateShader`, `glGenVertexArrays`, …) są zadeklarowane tylko + wtedy, gdy zdefiniowane jest `GL_GLEXT_PROTOTYPES`. Finch nie definiuje makr C, więc umieść te `#include` w malutkim nagłówku obok programu i zaimportuj go: ```c @@ -794,21 +826,21 @@ Oba działają bezpośrednio; `examples/opengl.fch` i `examples/vulkan.fch` to k #include ``` - Potem `import "opengl.h"`, `link "glfw"`, `link "GL"`. Parametry wyjściowe przekazujesz przez `addr(...)` - (`glGenBuffers(1, addr(vbo))`), dane bufora przez `.ptr` (`vertices.ptr`), a przesunięcia w bajtach przez - `ptr(...)` (`glVertexAttribPointer(1, 3, GL_FLOAT, GL_FALSE, stride, ptr(8))`). -- **Vulkan**: `import "vulkan/vulkan.h"` i `link "vulkan"`. Struktury „create info” budujesz z nazwanymi polami - (brakujące są zerami), uchwyty to `ptr`, a listy pobierasz typowym wzorcem „dwóch wywołań”: raz po liczbę, - raz z `lista.ptr` po `lista.resize(liczba)`. Makra-funkcje, jak `VK_MAKE_API_VERSION`, nie są dostępne; - policz liczbę sam (`1 << 22` to Vulkan 1.0). + Potem `import "opengl.h"`, `link "glfw"`, `link "GL"`. Parametry wyjściowe przekazuje się przez `addr(...)` + (`glGenBuffers(1, addr(vbo))`), dane bufora przez `.ptr` (`wierzcholki.ptr`), a przesunięcia w bajtach przez + `ptr(...)` (`glVertexAttribPointer(1, 3, GL_FLOAT, GL_FALSE, krok, ptr(8))`). +- **Vulkan**: `import "vulkan/vulkan.h"` i `link "vulkan"`. Struktury „create info” buduje się przez nazwy pól + (brakujące są zerami), uchwyty to `ptr`, a listy pobiera się zwykłym sposobem z dwoma wywołaniami: raz po liczbę, + drugi raz z `lista.ptr` po `lista.resize(liczba)`. Makra z parametrami, takie jak `VK_MAKE_API_VERSION`, nie są + dostępne; zamiast nich policz liczbę (`1 << 22` to Vulkan 1.0). -### Co jeszcze nie działa +### Czego jeszcze nie ma -- **Unie** C i struktury z **polami bitowymi** przekazywane do funkcji C albo zwracane z niej **przez wartość** - (wskaźniki do nich działają, unie jako zmienne też: `SDL_Event e`, `e.type`, `e.key.key`, `SDL_PollEvent(addr(e))`). -- **Makra-funkcje** (`#define MAX(a, b) ...`). Makra będące liczbą, tekstem, wyrażeniem z nich - (`(1u << 3)`, `((Uint64)1)`, `(A | B)`) albo strukturą z liczb (`RAYWHITE` z raylib, `(Color){ 245, 245, 245, 255 }`) działają. -- `long double`, liczby 128-bitowe. +- **Unii** C i struktur z **polami bitowymi** przekazywanych do funkcji C albo z niej zwracanych **przez wartość** + (wskaźniki na nie działają, unie jako zmienne też: `SDL_Event e`, `e.type`, `e.key.key`, `SDL_PollEvent(addr(e))`). +- **Makr z parametrami** (`#define MAX(a, b) ...`). Działają makra będące liczbą, tekstem, wyrażeniem z nich + (`(1u << 3)`, `((Uint64)1)`, `(A | B)`) albo strukturą liczb (`RAYWHITE` z raylib, `(Color){ 245, 245, 245, 255 }`). +- `long double` i liczb 128-bitowych. --- @@ -816,12 +848,12 @@ Oba działają bezpośrednio; `examples/opengl.fch` i `examples/vulkan.fch` to k ### 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")`: +Niektóre niepowodzenia są normalne: użytkownik wpisze `abc`, pliku brakuje. Finch obsługuje je bez wyjątków i bez +ukrytych skoków. Funkcja, która może się nie udać, mówi o tym znakiem `!` po typie wyniku, a niepowodzenie zgłasza +przez `return error("komunikat")`: ```c -fn port_z_tekstu(str tekst) -> int! { +fn parsuj_port(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") @@ -829,23 +861,23 @@ fn port_z_tekstu(str tekst) -> int! { return n } -fn zapisz(str sciezka, str tekst) -> ! { // może się nie udać, nic nie oddaje +fn zapisz(str sciezka, str tekst) -> ! { // może się nie udać, niczego nie zwraca 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): +Wywołujący **musi** obsłużyć niepowodzenie, na jeden z trzech sposobów (zapomnienie to błąd kompilacji): -| Kod | W razie porażki | +| Kod | Gdy się nie uda | |---|---| -| `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ć) | +| `port := parsuj_port(s) or 8080` | użyj tej wartości (jest obliczana tylko przy niepowodzeniu; może to być kolejne wywołanie z `or`) | +| `port := parsuj_port(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 parsuj_port(s)` | przerwij bieżącą funkcję z tym samym błędem (ona też musi móc się nie udać) | ```c fn wczytaj(str sciezka) -> Konfig! { tekst := try read_file(sciezka) - port := try port_z_tekstu(tekst.trim()) + port := try parsuj_port(tekst.trim()) return Konfig(port: port) } @@ -855,12 +887,11 @@ fn main() -> ! { // main też może się nie udać: błąd zostan } ``` -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`). +Tak samo działają wbudowane funkcje, które mogą się nie udać: `int(tekst)`, `float(tekst)`, `read_file`, `write_file`, +`delete_file`. Bez `or` / `try` zachowują się jak dawniej (`int("x")` zatrzymuje program, `write_file` zwraca `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. +Pod spodem taka funkcja zwraca wartość razem z flagą i komunikatem: przy powodzeniu nic nie jest przydzielane, +nie ma odwijania stosu, a niepowodzenie kosztuje tyle, co zbudowanie komunikatu. ### Błędy kompilacji @@ -870,12 +901,11 @@ gra.fch:12:9: error: 'wynik' must be int, but this is str | ^ ``` -Format: `plik:linijka:kolumna: error: wyjaśnienie`, a pod spodem linijka ze znakiem `^`. -Komunikaty są po angielsku. +Format: `plik:linia:kolumna: error: wyjaśnienie`, a pod spodem linia z `^` pod miejscem błędu. ### Błędy w trakcie działania -Zamiast niezdefiniowanego zachowania (jak w C) Finch zatrzymuje się z komunikatem i kodem wyjścia 1: +Zamiast niezdefiniowanego zachowania (jak w C) Finch zatrzymuje program z komunikatem i kodem wyjścia 1: | Komunikat | Przyczyna | |---|---| @@ -887,29 +917,29 @@ Zamiast niezdefiniowanego zachowania (jak w C) Finch zatrzymuje się z komunikat | `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 (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]` | +| `runtime error: called .f() on a null pointer` | metoda wywołana przez `ptr[T]` równy `null` | | `runtime error: assert failed: …` | `assert(warunek, "…")` z fałszywym warunkiem | -Pod komunikatem jest łańcuch wywołań, które do niego doprowadziły, od najgłębszego: +Pod komunikatem jest ścieżka wywołań, które do niego doprowadziły, od najgłębszego: ``` gra.fch:6: runtime error: index 3 is out of range (the length is 2) - in Enemy.next (gra.fch:6) - in step (gra.fch:10) + in Wrog.nastepny (gra.fch:6) + in krok (gra.fch:10) in main (gra.fch:15) ``` -`finch run`, `finch test` oraz budowanie z `-g` i `-O0` go mają; zwykły `finch build` go pomija, bo pilnowanie go kosztuje -czas w każdym wywołaniu (ok. 3 razy w programie złożonym z samych drobnych wywołań, dużo mniej w prawdziwym). `--trace` go dodaje. +`finch run`, `finch test` oraz kompilacje z `-g` i `-O0` ją pokazują; zwykłe `finch build` jej nie dodaje, bo +pamiętanie jej kosztuje czas przy każdym wywołaniu (ok. 3 razy wolniej w programie złożonym wyłącznie z drobnych +wywołań, w prawdziwym programie dużo mniej). `--trace` ją włącza. --- ## 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. +Wszystkie funkcje edytorów pochodzą z jednego serwera języka, `finch lsp`, który korzysta z prawdziwego kompilatora: +błędy w trakcie pisania, typy i opisy po najechaniu kursorem, przejście do definicji, podpowiedzi (pola, metody, +metody map i tablic po `.`, zawartość modułów, nazwy z nagłówków C), spis zawartości pliku i podpowiedzi parametrów. ### Kate (oraz KWrite, KDevelop) @@ -917,20 +947,20 @@ parametrów. 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ę +Skrypt instaluje podświetlanie składni (`~/.local/share/org.kde.syntax-highlighting/syntax/finch.xml`) i dopisuje +`finch` do ustawień klienta LSP w Kate, nie ruszając twoich innych serwerów. 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`. +`plik:linia:kolumna: error:` będą 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 -myszą, idź do definicji (F12), konspekt pliku, podpowiedzi parametrów, szablony i przycisk ▶ **Run** -(`Ctrl+F5`). Błędy z **Finch: Build This File** trafiają do panelu Problems. +podświetlanie, błędy w trakcie pisania, podpowiedzi (także pól i metod po `.`), typy po najechaniu kursorem, +przejście do definicji (F12), spis zawartości pliku, podpowiedzi parametrów, gotowe fragmenty kodu i przycisk +▶ **Run** (`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. +Instalacja: Extensions → `…` → *Install from VSIX…* → wybierz `finch-lang-*.vsix`. Rozszerzenie uruchamia `finch` +z PATH; jeśli leży gdzie indziej, ustaw **Finch: Path** w ustawieniach. ### Inne @@ -944,14 +974,14 @@ finch build gra.fch -g -O0 -o gra gdb ./gra (gdb) break gra.fch:12 (gdb) run -(gdb) bt # gdzie jesteśmy, przez które funkcje +(gdb) bt # gdzie jesteśmy i przez które funkcje (gdb) info locals # wszystkie zmienne (gdb) print gracz.zycia ``` -`-g` dodaje informacje dla debuggera (linijki, funkcje, zmienne, pola struktur). `-O0` zostawia wszystkie -zmienne widoczne; bez niego optymalizator może część z nich usunąć. Na Windowsie `-g` zapisuje format -CodeView, który czyta debugger Visual Studio i WinDbg. +`-g` dodaje informacje dla debuggera (linie, funkcje, zmienne, pola struktur, wektory). `-O0` zostawia widoczną +każdą zmienną; bez tego optymalizator może niektóre usunąć. Na Windowsie to też jest DWARF: użyj `lldb` z llvm-mingw +albo gdb. --- @@ -960,73 +990,51 @@ CodeView, który czyta debugger Visual Studio i WinDbg. | Problem | Rozwiązanie | |---|---| | `can't find the C header 'x.h'` | Brakuje plików deweloperskich biblioteki. Zainstaluj je (Debian: `libx-dev`; Nix: dopisz do `shell.nix`). | -| `… come from "x.h", but its library isn't linked` | Dopisz `link "nazwa"` na górze pliku. | -| `the library 'x' wasn't found` | Nie jest zainstalowana albo nazwa jest zła. Na Niksie dopisz ją do `shell.nix` i pracuj w `nix-shell`. | -| `the C function 'f' can't be used from Finch yet` | Używa unii, pól bitowych albo innego nieobsługiwanego typu przez wartość. Poszukaj wariantu ze wskaźnikami. | +| `… come from "x.h", but its library isn't linked` | Dodaj `link "nazwa"` na początku pliku. | +| `the library 'x' wasn't found` | Biblioteka nie jest zainstalowana albo nazwa jest błędna. Na Niksie dopisz ją do `shell.nix` i pracuj w `nix-shell`. | +| `the C function 'f' can't be used from Finch yet` | Używa przez wartość unii, pól bitowych albo innego nieobsługiwanego typu. Poszukaj wariantu, który przyjmuje wskaźniki. | +| `--cpu native`: na innym komputerze program kończy się `Illegal instruction` | Został zbudowany pod procesor, na którym go budowano. Zbuduj bez `--cpu` albo ze starszym (`x86-64-v2`). | | `can't find the module 'x'` | Połóż `x.fch` obok importującego pliku albo ustaw `FINCH_PATH`. | -| `couldn't build the Finch runtime` | Brak działającego kompilatora C. Zainstaluj gcc lub clang albo ustaw `CC`. | -| 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++*. | +| `couldn't build the Finch runtime` | Brak działającego kompilatora C. Zainstaluj gcc albo clang, albo ustaw `CC`. | +| Program po uruchomieniu nie znajduje biblioteki | Została dolinkowana z katalogu, którego system nie przeszukuje. Uruchamiaj w tym samym `nix-shell` albo zainstaluj ją w systemie. | +| Windows: `no C compiler found to link with ('clang')` | Dodaj katalog `bin` z llvm-mingw do PATH (rozdział 2). | | Windows: `finch.exe` się nie uruchamia (brak `libclang.dll`) | Trzymaj `libclang.dll` obok `finch.exe`. | | macOS: `finch` się nie uruchamia (`Library not loaded: …libLLVM…`) | `brew install llvm@21`. | | macOS: `can't find the C header 'stdio.h'` | Zainstaluj narzędzia wiersza poleceń: `xcode-select --install`. | | `--target arm64`: `no C compiler found to link with` | Zainstaluj `aarch64-linux-gnu-gcc` (Debian: `gcc-aarch64-linux-gnu`) albo ustaw `FINCH_CC`. | -| `clang` w `nix-shell` nie widzi `stdio.h` | Masz stary `shell.nix`: `llvmPackages.clang` musi być przed `llvmPackages.libclang`. | +| `clang` w `nix-shell` nie znajduje `stdio.h` | Masz stary `shell.nix`: `llvmPackages.clang` musi stać przed `llvmPackages.libclang`. | | Program nigdy się nie kończy | Warunek pętli nigdy nie staje się fałszywy. Naciśnij Ctrl+C. | --- -## 22. Struktura projektu i testy +## 22. Układ repozytorium i testy ``` Finch/ -├── src/ kompilator, w Finchu (lekser, parser, gen: LLVM IR jako tekst, cimport: libclang, lsp, main ...) -├── std/ biblioteka standardowa, w Finchu: json, path, time, proc, fs, thread -├── runtime/ finch_rt.c: mała biblioteka uruchomieniowa (teksty, tablice, mapy, wejście, pliki) -├── bootstrap/ build.sh, install.sh i ziarna (własny IR kompilatora, po jednym na system) -├── examples/ hello, tour, types, structs, todo, guess, c_import, window, opengl, vulkan, raylib, llvm +├── src/ kompilator w Finchu (lexer, parser, gen: LLVM IR jako tekst, cimport: libclang, lsp, main ...) +├── std/ biblioteka standardowa w Finchu: json, path, time, proc, fs, thread +├── runtime/ finch_rt.c: mały runtime (teksty, tablice, mapy, wczytywanie, pliki) +├── bootstrap/ build.sh, install.sh i pliki startowe (własny IR kompilatora, osobno dla każdego systemu) +├── examples/ hello, tour, types, structs, todo, guess, wordcount, c_import, simd, window, opengl, vulkan, raylib, llvm ├── tests/ -│ ├── run/ new/ programy + dokładny wynik (.out) i wejście (.in) -│ ├── fail/ new/fail/ programy, które muszą się nie udać, z oczekiwanym błędem w 1. linijce -│ ├── tool/ finch test, finch fmt, łańcuch wywołań, projekt z finch.toml +│ ├── run/ new/ programy + dokładny wynik, który muszą wypisać (.out), wejście (.in) +│ ├── fail/ new/fail/ programy, które muszą się nie skompilować albo zatrzymać, z oczekiwanym błędem w 1. linii +│ ├── tool/ finch test, finch fmt, ścieżka wywołań, projekt z finch.toml │ ├── run.sh uruchamia testy (MEMCHECK=1 sprawdza też pamięć valgrindem) -│ ├── cross.sh każdy test zbudowany na Windows / ARM64 i uruchomiony w Wine / QEMU -│ └── lsp_test.py serwer języka, obsługiwany jak przez edytor +│ ├── cross.sh każdy test zbudowany na Windowsa / ARM64 i uruchomiony przez Wine / QEMU +│ └── lsp_test.py serwer języka, z którym test rozmawia tak jak edytor ├── 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 +├── docs/ ta dokumentacja (en, pl), razem ze ściągą +└── shell.nix środowisko programistyczne dla Niksa ``` ```sh -tests/run.sh # programy, odmowy, buildy z -g, biblioteka współdzielona, serwer języka -MEMCHECK=1 tests/run.sh # to samo pod valgrindem: bez wycieków i złych dostępów do pamięci -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 +tests/run.sh # programy, oczekiwane błędy, kompilacje z -g, biblioteka współdzielona, serwer języka +MEMCHECK=1 tests/run.sh # to samo pod valgrindem: bez wycieków i błędnych dostępów do pamięci +tests/cross.sh windows # każdy test zbudowany na Windowsa i uruchomiony przez Wine +tests/cross.sh arm64 # każdy test zbudowany na Linuksa ARM64 i uruchomiony przez QEMU ``` -### Warianty z wartościami - -Wariant może nieść wartości. Tworzysz go przez `Shape.Circle(2.0)`, a wyciągasz `match`-em, który je nazywa: - -```c -enum Shape { - Circle(float) - Rect(float, float) - Point -} - -fn Shape.area() -> float { - return match self { - Circle(r) -> 3.14 * r * r - Rect(w, h) -> w * h - Point -> 0.0 - } -} -``` - -`_` pomija wartość. `match` musi obsłużyć każdy wariant (albo kończyć się `else`). Wypisanie pokazuje `Rect(2, 3.5)`. Taki enum -jest kopiowany i zwalniany jak struktura; nie da się go porównać przez `==` (użyj `match`). Warianty takich enumów nie mają numerów. - --- ## 23. Enumy i match @@ -1035,22 +1043,22 @@ jest kopiowany i zwalniany jak struktura; nie da się go porównać przez `==` ( ``` enum Kolor { Czerwony, Zielony, Niebieski } -enum Status { Ok = 200, Brak = 404 } // wariantowi można nadać numer +enum Status { Ok = 200, NotFound = 404 } // wariant może dostać liczbę, jeśli jej potrzebuje k := Kolor.Zielony print(k) // Zielony -print(int(Status.Brak)) // 404 -print(Kolor(2)) // Niebieski: liczba zamieniona na enum (sprawdzana: Kolor(7) to błąd) +print(int(Status.NotFound)) // 404 +print(Kolor(2)) // Niebieski: liczba zamieniona na enum (sprawdzane: Kolor(7) to błąd) ``` -Warianty pisze się `Kolor.Czerwony`. Można je porównywać przez `==` i `!=` (nie przez `<`), używać jako klucze map, -pola struktur i elementy tablic, a enum może mieć metody jak struktura: `fn Kolor.nastepny() -> Kolor { ... }`. +Warianty zapisuje się `Kolor.Czerwony`. Można je porównywać przez `==` i `!=` (nie przez `<`), używać jako kluczy map, +pól struktur i elementów tablic, a enum może mieć metody tak jak struktura: `fn Kolor.nastepny() -> Kolor { ... }`. -**match** wybiera, co zrobić, na podstawie wartości — zamiast drabinki `if`: +**match** wybiera, co zrobić, na podstawie wartości, zamiast drabinki `if`: ``` match k { - Czerwony -> print("ciepły") // w matchu na enumie wystarczy sama nazwa + Czerwony -> print("ciepły") // w match na enumie wystarczy sama nazwa Zielony, Niebieski -> print("zimny") // kilka wartości w jednej gałęzi } @@ -1065,17 +1073,41 @@ match slowo { print("pa") return } - else -> print("co?") + else -> print("słucham?") } ``` -`match` działa na enumach, liczbach całkowitych, znakach, tekście i bool. Gałęzie sprawdzane są po kolei. Match na -enumie musi obsłużyć **każdy wariant** albo kończyć się `else` (kompilator wskaże brakujące); każdy inny match -potrzebuje `else`. W wyrażeniu wszystkie gałęzie dają ten sam typ. +`match` działa na enumach, liczbach całkowitych, znakach, tekstach i wartościach bool. Gałęzie są sprawdzane po kolei. +`match` na enumie musi obsłużyć **każdy wariant** albo kończyć się `else` (kompilator mówi, których brakuje); każdy inny +`match` potrzebuje `else`. W wyrażeniu każda gałąź daje wartość tego samego typu. + +### Warianty z wartościami + +Wariant może przenosić wartości. Tworzy się go przez `Ksztalt.Kolo(2.0)`, a wartości wyjmuje przez `match`, który je nazywa: + +```c +enum Ksztalt { + Kolo(float) + Prostokat(float, float) + Punkt +} + +fn Ksztalt.pole() -> float { + return match self { + Kolo(r) -> 3.14 * r * r + Prostokat(w, h) -> w * h + Punkt -> 0.0 + } +} +``` + +`_` pomija wartość. `match` musi obsłużyć każdy wariant (albo kończyć się `else`). Wypisanie pokazuje `Prostokat(2, 3.5)`. +Taki enum kopiuje się i zwalnia jak struktura; nie da się go porównać przez `==` (użyj `match`). Warianty takich enumów +nie mogą mieć liczb. ## 24. Typy generyczne -Napisz funkcję lub strukturę raz dla dowolnego typu, z typem jako nazwą w nawiasach kwadratowych: +Funkcję albo strukturę można napisać raz dla dowolnego typu, podając typ jako nazwę w nawiasach kwadratowych: ``` fn max[T](T a, T b) -> T { @@ -1089,73 +1121,72 @@ struct Stos[T] { []T elementy } -fn Stos[T].dodaj(T x) { +fn Stos[T].push(T x) { self.elementy.push(x) } -print(max(3, 9), max("jabłko", "gruszka")) // T wynika z argumentów -print(max[float](1, 2.5)) // albo zapisane wprost, gdzie nie da się go ustalić -s := Stos[int]() // generyczną strukturę zawsze pisze się z typem -s.dodaj(7) +print(max(3, 9), max("jabłko", "gruszka")) // T wynika z argumentów +print(max[float](1, 2.5)) // albo jest podany, gdy nie da się go ustalić +s := Stos[int]() // struktura generyczna zawsze ma podany typ +s.push(7) ``` -Parametrów może być kilka (`struct Para[A, B]`), typy można zagnieżdżać (`Stos[Para[int, str]]`). Każde użycie tworzy -własną kopię kodu, więc generyk jest tak szybki jak kod napisany ręcznie. Nie ma ograniczeń: jeśli `T` nie potrafi -tego, co robi funkcja (`a > b` na strukturze), błąd mówi o tym i podaje, którą wersję budowano, np. -`(while building wiekszy[Punkt])`. +Działa kilka parametrów (`struct Para[A, B]`) i typy zagnieżdżone (`Stos[Para[int, str]]`). Każde użycie tworzy własną +kopię kodu, więc typ generyczny jest tak szybki jak kod napisany ręcznie. Nie ma ograniczeń na typ: jeśli `T` nie potrafi +tego, czego wymaga funkcja (`a > b` na strukturze), błąd o tym mówi i podaje, którą wersję budował, np. +`(while building bigger[Punkt])`. ## 25. Biblioteka standardowa -Moduły dołączone do Fincha, napisane w Finchu. `import json` szuka najpierw `json.fch` obok Twojego pliku, potem w -bibliotece — więc Twój własny plik o tej nazwie ma pierwszeństwo. +Moduły dostarczane z Finchem, napisane w Finchu. `import json` najpierw szuka `json.fch` obok twojego pliku, a dopiero +potem w bibliotece, więc twój plik o tej samej nazwie ma pierwszeństwo. | Moduł | Co robi | |---|---| -| `path` | `join`, `base`, `dir`, `ext`, `stem`, `parts`, `clean`, `is_abs`: ścieżki jako tekst, rozdzielone `/` | -| `time` | `now()`, `millis()` (zegar do mierzenia), `sleep(ms)`, `date(t)`, `local(t)`, `format(t, "%Y-%m-%d")`, `days_in_month` | -| `proc` | `run(cmd)` (kod wyjścia), `output(cmd)` (co wypisało; błąd, jeśli się nie udało), `env(nazwa, domyślna)`, `pid()`, `cwd()` | -| `json` | `parse(tekst)`, `stringify(v)`, `pretty(v)` oraz typ wartości `Json` z `get`, `at`, `set`, `push`, `has`, `str_or`, `int_or` ... | -| `fs` | `read_bytes(p)` / `write_bytes(p, dane)` (`[]u8`, dowolne bajty), `exists`, `is_dir`, `mkdir` (z folderami wyżej), `list` (posortowane nazwy), `remove_dir`, `data_dir(app)` (folder na zapisy użytkownika) | +| `path` | `join`, `base`, `dir`, `ext`, `stem`, `parts`, `clean`, `is_abs`: ścieżki jako tekst, rozdzielane `/` | +| `time` | `now()`, `millis()` (zegar do mierzenia czasu), `sleep(ms)`, `date(t)`, `local(t)`, `format(t, "%Y-%m-%d")`, `days_in_month` | +| `proc` | `run(polecenie)` (kod wyjścia), `output(polecenie)` (to, co wypisało; błąd, jeśli się nie udało), `env(nazwa, domyslna)`, `pid()`, `cwd()` | +| `json` | `parse(tekst)`, `stringify(v)`, `pretty(v)` i typ wartości `Json` z `get`, `at`, `set`, `push`, `has`, `str_or`, `int_or` ... | +| `fs` | `read_bytes(p)` / `write_bytes(p, dane)` (`[]u8`, dowolne bajty), `exists`, `is_dir`, `mkdir` (razem z katalogami nadrzędnymi), `list` (posortowane nazwy), `remove_dir`, `data_dir(aplikacja)` (katalog na zapisy użytkownika) | | `thread` | `spawn(addr(praca), addr(dane))`, `t.join()`, `mutex()` z `lock`, `unlock`, `close`, `cpus()` (rozdział 29) | -`min(a, b, ...)`, `max(a, b, ...)`, `abs(x)` i `clamp(x, od, do)` działają na dowolnych liczbach bez importu (twoja -własna funkcja o tej samej nazwie wygrywa). +`min(a, b, ...)`, `max(a, b, ...)`, `abs(x)` i `clamp(x, lo, hi)` działają na dowolnych liczbach (i wektorach) +bez importu (twoja funkcja o tej samej nazwie ma pierwszeństwo). ``` import json import path dok := try json.parse(read_file("config.json")) -nazwa := dok.get("name").str_or("bez nazwy") // brakujący klucz to null, więc łańcuch nigdy się nie psuje +nazwa := dok.get("name").str_or("bez nazwy") // brakujący klucz to null, więc łańcuch wywołań się nie wysypie dok.set("name", json.new_text(nazwa + "!")) write_file(path.join("out", "config.json"), json.pretty(dok)) ``` ## 26. Obecne ograniczenia -Jeszcze nie ma: +Czego jeszcze nie ma: - Interfejsów. -- Tekstu świadomego Unicode: `.len`, `s[i]` i `upper()` działają na bajtach / ASCII. -- Pilnowania, żeby dwa wątki nie zmieniały tej samej tablicy naraz (do tego mutex). -- Windowsa na ARM64, systemów 32-bitowych, WebAssembly. `--target android` robi kod dla Androida, ale linkowanie - z NDK i uruchomienie na urządzeniu nie były jeszcze próbowane. +- Tekstów świadomych Unicode: `.len`, `s[i]` i `upper()` działają na bajtach / ASCII. +- Zabezpieczenia przed zmienianiem tej samej tablicy przez dwa wątki naraz (użyj muteksu). +- Windowsa na ARM64, systemów 32-bitowych, WebAssembly. ### Ostrzeżenia o dużych kopiach -Struktura z 256 lub więcej bajtami zwykłych danych (liczby, struktury z liczb, `[N]T` z C) jest kopiowana bajt po bajcie, -gdy jest parametrem (`fn f(Cialo c)`) i w `for x in ciala`. Finch o tym mówi, a program i tak buduje: +Struktura z co najmniej 256 bajtami zwykłych danych (liczb, struktur z liczbami, `[N]T` z C) jest kopiowana bajt po +bajcie, gdy jest parametrem (`fn f(Cialo c)`) i w `for x in ciala`. Finch o tym ostrzega, ale i tak buduje program: ``` -gra.fch:27:11: warning: 'c' is a Cialo (264 bytes), copied on every call to waga; take ptr[Cialo] to share it instead (callers can still pass the variable as it is) -gra.fch:45:5: warning: each Cialo (264 bytes) is copied into 'x'; for i in 0..ciala.len with ciala[i] works on it in place +gra.fch:27:11: warning: 'b' is a Body (264 bytes), copied on every call to weight; take ptr[Body] to share it instead (callers can still pass the variable as it is) +gra.fch:45:5: warning: each Body (264 bytes) is copied into 'x'; for i in 0..bodies.len with bodies[i] works on it in place ``` -Teksty, tablice i mapy w strukturze się nie liczą: przekazanie struktury kopiuje tylko ich uchwyty, nie zawartość. -`b := a` nie dostaje ostrzeżenia: w kodzie widać, że to kopia. Metoda nigdy nie kopiuje `self`. Edytory pokazują +Teksty, tablice i mapy w strukturze się nie liczą: przekazanie struktury kopiuje tylko ich uchwyty, a nie zawartość. +`b := a` nie daje ostrzeżenia, bo w kodzie widać, że to kopia. Metoda nigdy nie kopiuje `self`. Edytory pokazują ostrzeżenia na żółto. -Plan rozwoju jest w [README](../../README.pl.md#plan-rozwoju). +Zob. [plan rozwoju w README](../../README.pl.md#plan-rozwoju). ## 27. Testy: bloki test i finch test @@ -1172,33 +1203,35 @@ test "obrażenia nigdy nie schodzą poniżej zera" { } ``` -`finch test gra.fch` uruchamia każdy blok `test` z pliku i z modułów, które importuje (bez biblioteki standardowej), -każdy osobno: nieudany `assert` albo dowolny błąd w czasie działania kończy tylko ten test, a następny rusza dalej. +`finch test gra.fch` uruchamia każdy blok `test` z tego pliku i z importowanych modułów (ale nie z biblioteki +standardowej), każdy osobno: nieudany `assert` albo dowolny błąd w trakcie działania kończy tylko ten jeden test, +a następny rusza dalej. ``` ok obrażenia nigdy nie schodzą poniżej zera -FAIL wczytanie poziomu - poziom.fch:31: runtime error: can't read the file "levels/1.txt" +FAIL wczytywanie poziomu + poziom.fch:31: runtime error: can't read the file "poziomy/1.txt" 1 passed, 1 failed ``` -Kod wyjścia to 1, gdy jakiś test nie przeszedł, więc CI może z tego korzystać. `finch run` i `finch build` pomijają bloki test. -`assert(warunek)` działa też poza testami: zatrzymuje program z `assert failed`, gdy warunek jest fałszywy. +Gdy któryś test się nie uda, kod wyjścia to 1, więc CI może z tego korzystać. `finch run` i `finch build` pomijają +bloki test. `assert(warunek)` działa też poza testami: zatrzymuje program komunikatem `assert failed`, gdy warunek jest fałszywy. ## 28. Projekt: finch.toml -`finch.toml` obok kodu mówi raz to, co inaczej trzeba by podawać w każdym poleceniu: +Plik `finch.toml` obok kodu zapisuje raz to, co inaczej trzeba by podawać przy każdym poleceniu: ```toml -name = "gra" # program, który robi finch build (z entry) -entry = "src/main.fch" # gdy finch run / build / test nie dostaną pliku -paths = ["src", "lib"] # foldery z modułami Fincha -include = ["vendor/SDL3/include"] # foldery z nagłówkami C (też dla własnych .c / .cpp) -links = ["SDL3", "native/glue.cpp"] # biblioteki i własne pliki .c / .cpp / .o / .a -libdirs = ["vendor/SDL3/lib"] # foldery z bibliotekami -cflags = ["-DFAST"] # dla własnych plików .c / .cpp +name = "gra" # program, który zbuduje finch build (z pliku entry) +entry = "src/main.fch" # używany, gdy finch run / build / test nie dostaną pliku +paths = ["src", "lib"] # katalogi z modułami Fincha +include = ["vendor/SDL3/include"] # katalogi z nagłówkami C (także dla twoich plików .c / .cpp) +links = ["SDL3", "native/glue.cpp"] # biblioteki i twoje pliki .c / .cpp / .o / .a +libdirs = ["vendor/SDL3/lib"] # katalogi z bibliotekami +cflags = ["-DFAST"] # dla twoich plików .c / .cpp ldflags = ["-Wl,-rpath,$ORIGIN"] # dla linkera +cpu = "native" # to samo co --cpu native (rozdział 31) [target.windows] # dodawane tylko przy budowaniu na Windowsa links = ["ws2_32"] @@ -1208,8 +1241,8 @@ libdirs = ["vendor/SDL3/lib/x64"] libdirs = ["vendor/SDL3/lib/arm64"] ``` -Foldery liczą się od `finch.toml`. Finch szuka go w folderze pliku i w folderach wyżej, więc -`finch run src/narzedzia/edytor.fch` wewnątrz projektu też z niego korzysta. Czytana jest tylko taka część TOML: +Ścieżki są liczone względem `finch.toml`. Finch szuka go w katalogu pliku i w katalogach nad nim, więc +`finch run src/narzedzia/edytor.fch` w środku projektu też z niego skorzysta. Z formatu TOML czytane jest tylko tyle: `[sekcje]`, `klucz = "tekst"`, `klucz = ["a", "b"]` (także w kilku liniach), liczby, `true`/`false` i komentarze `#`. ## 29. Wątki @@ -1222,10 +1255,10 @@ struct Kawalek { float szczyt } -fn analizuj(ptr arg) { // funkcja wątku bierze jeden ptr +fn analizuj(ptr arg) { // funkcja wątku przyjmuje jeden ptr ptr[Kawalek] k = arg - for s in k.probki { - k.szczyt = max(k.szczyt, abs(s)) + for p in k.probki { + k.szczyt = max(k.szczyt, abs(p)) } } @@ -1239,65 +1272,143 @@ fn main() { }) } for i in 0..dzialajace.len { - dzialajace[i].join() // poczekaj na wszystkie + dzialajace[i].join() // czekamy na wszystkie } } ``` -To, co dostaje wątek, jest współdzielone, nie kopiowane, więc: +To, co dostaje wątek, jest współdzielone, a nie kopiowane, więc: -- musi żyć do `join()` (tu `kawalki` żyje do końca `main`); -- nie zmieniaj tablicy, w którą wskazują wątki (bez `push`), dopóki się nie skończą; -- to, co zmienia więcej niż jeden wątek, idzie pod blokadę albo jest liczbą zmienianą przez `atomic_...`. +- musi istnieć aż do `join()` (tutaj `kawalki` żyje do końca `main`); +- nie zmieniaj tablicy, do której wskazują wątki (żadnego `push`), dopóki nie zostaną połączone; +- to, co zmienia więcej niż jeden wątek, trzeba chronić blokadą albo musi to być liczba zmieniana przez `atomic_...`. ```c m := thread.mutex() defer m.close() m.lock() -dziennik.push("gotowe") // jeden wątek naraz między lock a unlock +dziennik.push("gotowe") // między lock a unlock pracuje tylko jeden wątek naraz m.unlock() atomic_add(addr(gotowe), 1) // gotowe to int wspólny dla wątków n := atomic_load(addr(gotowe)) ``` -`atomic_add`, `atomic_sub`, `atomic_swap` zwracają starą wartość; `atomic_cas(p, stara, nowa)` wstawia `nowa` tylko -wtedy, gdy jest tam `stara`, i mówi, czy to zrobił. `thread.cpus()` mówi, ile wątków komputer wykonuje naraz. +`atomic_add`, `atomic_sub` i `atomic_swap` zwracają poprzednią wartość; `atomic_cas(p, stara, nowa)` wpisuje `nowa` +tylko wtedy, gdy jest tam `stara`, i mówi, czy to zrobiło. `thread.cpus()` mówi, ile wątków komputer wykonuje naraz. ## 30. C++, Rust i Android -Finch rozmawia z innymi językami przez C, jak każdy język. +Finch rozmawia z innymi językami przez C, tak jak każdy język. -**C++**: to, co woła Finch, wystaw przez nagłówek C, z `extern "C"` po stronie C++: +**C++**: to, co ma wołać Finch, wystaw przez nagłówek C, z `extern "C"` po stronie C++: ```cpp // glue.h #ifdef __cplusplus extern "C" { #endif -int physics_step(float dt); +int krok_fizyki(float dt); #ifdef __cplusplus } #endif ``` -i `import "glue.h"` z `link "glue.cpp"` (albo `links = ["glue.cpp"]` w `finch.toml`). Plik `.cpp` buduje kompilator C++ -(`clang++` albo `g++`, albo `FINCH_CXX`), który wtedy też linkuje, więc biblioteka C++ dochodzi sama. -Bibliotekę C++ z API w C (Box2D 3, JoltC dla Jolt, wrapper C dla PhysX) importuje się jak każdą bibliotekę C. +i `import "glue.h"` z `link "glue.cpp"` (albo `links = ["glue.cpp"]` w `finch.toml`). Plik `.cpp` kompiluje kompilator +C++ (`clang++` albo `g++`, albo `FINCH_CXX`), który potem też linkuje, więc biblioteka C++ dołącza się sama. +Bibliotekę C++ z API w C (Box2D 3, JoltC dla Jolta, nakładka C na PhysX) importuje się jak każdą bibliotekę C. -**Rust**: zrób `staticlib` i wystaw funkcje C: +**Rust**: zbuduj `staticlib` i wystaw funkcje C: ```rust // Cargo.toml: [lib] crate-type = ["staticlib"] #[no_mangle] -pub extern "C" fn path_len(x: f32, y: f32) -> f32 { (x * x + y * y).sqrt() } +pub extern "C" fn dlugosc(x: f32, y: f32) -> f32 { (x * x + y * y).sqrt() } ``` -Napisz nagłówek (`float path_len(float x, float y);` albo niech napisze go `cbindgen`), potem `import "pathfind.h"` i -`link "target/release/libpathfind.a"`. Na Linuksie statyczna biblioteka Rusta potrzebuje też `links = ["dl", "pthread"]`. +Napisz nagłówek (`float dlugosc(float x, float y);` albo wygeneruj go przez `cbindgen`), a potem `import "trasa.h"` +i `link "target/release/libtrasa.a"`. Na Linuksie biblioteka statyczna z Rusta potrzebuje też `links = ["dl", "pthread"]`. -**Android**: `finch build gra.fch --target android --shared` robi `libgra.so` dla 64-bitowego Androida na ARM (API 24 -i wyżej) clangiem z NDK, znalezionym przez `ANDROID_NDK_HOME` (albo `FINCH_CC`). Wczytaj go z aplikacji, na przykład -przez projekt Androida z SDL3 (`SDL_main` biblioteki to `export fn`). To nowość, sprawdzona tylko częściowo: kod Fincha -kompiluje się dla Androida (dane wątków idą przez emulowane TLS, którego wymaga starszy Android), ale linkowanie z NDK -i uruchomienie na urządzeniu nie były jeszcze próbowane. +**Android**: `finch build gra.fch --target android --shared` tworzy `libgra.so` dla 64-bitowego Androida na ARM +(API 24 i nowsze) clangiem z NDK, którego Finch szuka przez `ANDROID_NDK_HOME` (albo `FINCH_CC`). Wczytaj ją z aplikacji, +np. przez projekt Androida z SDL3 (`SDL_main` biblioteki to `export fn`). Dane lokalne dla wątków idą przez emulowane +TLS, którego wymagają starsze wersje Androida. + +--- + +## 31. Wektory (SIMD) + +Procesor potrafi dodać albo pomnożyć kilka liczb jedną instrukcją: 4 liczby `float` naraz przez SSE, 8 przez AVX2, +16 przez AVX-512, 4 przez NEON na ARM. To SIMD (jedna instrukcja, wiele danych) i to dzięki temu filtry obrazu, +dźwięk, fizyka i sieci neuronowe są szybkie. W Finchu to po prostu typ: **`f32x8` to 8 liczb `f32` obok siebie**. + +```c +a := f32x4(1.0, 2.0, 3.0, 4.0) +b := f32x4(0.5) // 0.5 w każdym elemencie +print(a + b) // f32x4(1.5, 2.5, 3.5, 4.5) +print(a * 2.0) // zwykła liczba działa na każdy element: f32x4(2, 4, 6, 8) +print(a.sum(), a[2], a.len) // 10 3 4 +``` + +**Typy** zapisuje się jako typ liczby, `x` i liczbę elementów: `f32x4` `f32x8` `f32x16`, `f64x2` `f64x4` `f64x8`, +`i8x16` `i8x32` `i8x64`, `i16x8` `i16x16`, `i32x4` `i32x8` `i32x16`, `i64x2` `i64x4` i to samo z `u` (bez znaku). +Od 2 do 64 elementów, razem najwyżej 512 bitów. Wektor to zwykła wartość, tak jak `int`: może być zmienną, polem +struktury, elementem tablicy, wartością w mapie i parametrem funkcji, a `print` i `str` go pokazują. + +**Tworzenie:** + +| | | +|---|---| +| `f32x8(x)` | `x` w każdym elemencie | +| `f32x8(a, b, c, d, e, f, g, h)` | po jednej liczbie na element | +| `f32x8(lista, i)` | `lista[i]` … `lista[i+7]` z `[]f32` (albo przez `ptr[f32]`); zatrzymuje program, jeśli nie wszystkie są w liście | +| `f32x8(v)` | inny 8-elementowy wektor przeliczony element po elemencie: `f32x8(calkowite)`, `i32x8(ulamki)` (ucina części ułamkowe), `i16x8(i32ki)` | + +**Działania:** + +| | | +|---|---| +| `+ - * /` | element po elemencie (`/` tylko na liczbach zmiennoprzecinkowych: x86 i ARM nie dzielą liczb całkowitych element po elemencie) | +| `& \| ^ << >> ~` | element po elemencie, na liczbach całkowitych | +| `-v` | każdy element ze zmienionym znakiem | +| `v == w`, `v != w` | `true`, jeśli wszystkie elementy są równe / jeśli któryś się różni | +| `v[i]`, `v[i] = x`, `v.len` | jeden element; liczba elementów | +| `min(v, w)`, `max(v, w)`, `abs(v)`, `clamp(v, lo, hi)` | element po elemencie; `lo`, `hi` i `w` mogą być zwykłymi liczbami | +| `v.sum()`, `v.min()`, `v.max()` | wszystkie elementy złożone w jedną liczbę | +| `v.dot(w)` | `(v * w).sum()` | +| `v.sqrt()`, `v.floor()`, `v.ceil()`, `v.round()` | każdy element (liczby zmiennoprzecinkowe) | +| `v.fma(b, c)` | `v * b + c`, jedną instrukcją tam, gdzie procesor ją ma | +| `v.store(lista, i)` | zapisuje elementy do `lista[i]` … (albo przez `ptr`) | + +`<` i inne porównania dałyby osobną odpowiedź dla każdego elementu, więc ich nie ma; użyj `min`, `max` albo `v[i] < w[i]`. + +**Prawdziwa pętla.** Zwykły schemat: tyle całych wektorów, ile się zmieści, a resztę po jednej liczbie. + +```c +fn iloczyn([]f32 a, []f32 b) -> f32 { + suma := f32x8(0.0) + i := 0 + while i + 8 <= a.len { + suma = f32x8(a, i).fma(f32x8(b, i), suma) + i += 8 + } + wynik := suma.sum() + while i < a.len { + wynik += a[i] * b[i] + i += 1 + } + return wynik +} +``` + +`examples/simd.fch` mierzy czas tej pętli i porównuje go ze zwykłą. + +**Jakie instrukcje.** Domyślnie program działa na każdym procesorze swojego systemu (na x86-64 oznacza to SSE2, więc +`f32x8` liczy się w dwóch połówkach). `--cpu native` (albo `cpu = "native"` w `finch.toml`) pozwala LLVM użyć wszystkiego, +co ma procesor, na którym budujesz: AVX2 i FMA, AVX-512 albo nowszych części NEON na ARM. Taki program potrzebuje potem +podobnego procesora. Można też wybrać poziom: `--cpu x86-64-v3` (AVX2, większość komputerów od 2015 roku), +`--cpu x86-64-v4` (AVX-512), `--cpu apple-m1`. + +Wektory szersze niż rejestry procesora LLVM dzieli na części, a ten sam kod buduje się tak samo na x86-64 i na ARM64. +`v.sum()` na liczbach zmiennoprzecinkowych dodaje elementy w kolejności, która jest najszybsza, więc ostatnie cyfry mogą +się różnić od dodawania ich po kolei. diff --git a/docs/pl/sciaga.md b/docs/pl/sciaga.md index cb4e7e7..d0d6c49 100644 --- a/docs/pl/sciaga.md +++ b/docs/pl/sciaga.md @@ -1,6 +1,6 @@ -# Ściąga Fincha +# Ściąga z Fincha -Cały Finch na jednej stronie. Wyjaśnienia są w [przewodnikach](../../README.pl.md#dokumentacja). +Cały Finch na jednej stronie. Wyjaśnienia znajdziesz w [przewodnikach](../../README.pl.md#dokumentacja). > 🇬🇧 English version: [cheatsheet.md](../en/cheatsheet.md) @@ -8,39 +8,41 @@ Cały Finch na jednej stronie. Wyjaśnienia są w [przewodnikach](../../README.p ## Polecenie `finch` -| Polecenie | Robi | +| Polecenie | Co robi | |---|---| -| `finch run plik.fch [argumenty…]` | skompiluj i uruchom (argumenty trafiają do programu) | -| `finch build plik.fch [-o nazwa]` | zrób program (`nazwa.exe` na Windowsie) | -| `finch build plik.fch --shared` | zrób bibliotekę (`libplik.so`, `plik.dll`) dla programu w C; jej `export fn` to jej funkcje C | -| `finch test plik.fch` | uruchom jego bloki `test "…" { }`, każdy osobno (także z importowanych modułów) | -| `finch fmt [--check] [pliki/foldery]` | wcięcia wg `{ }` po 4 spacje, bez spacji na końcach linii, najwyżej jedna pusta linia (`--check`: tylko wypisz, co by zmienił) | -| `finch run` / `build` / `test` bez pliku | w folderze z `finch.toml`: jego `entry` | -| `finch ir plik.fch` | pokaż LLVM IR | +| `finch run plik.fch [argumenty…]` | kompiluje i uruchamia (argumenty trafiają do programu) | +| `finch build plik.fch [-o nazwa]` | tworzy program (na Windowsie `nazwa.exe`) | +| `finch build plik.fch --shared [-I katalog]` | tworzy bibliotekę (`libplik.so`, `plik.dll`), którą wczyta program w C; jej funkcjami C są funkcje `export fn` (liczby, bool, char, wskaźniki) | +| `finch test plik.fch` | uruchamia każdy blok `test "…" { }` osobno (także w importowanych modułach) | +| `finch fmt [--check] [pliki/katalogi]` | wcięcia po 4 spacje na każdy poziom `{ }`, bez spacji na końcach linii, najwyżej jedna pusta linia z rzędu (`--check`: tylko wypisuje, co by zmienił) | +| `finch run` / `build` / `test` bez pliku | w katalogu z `finch.toml`: plik z pola `entry` | +| `finch ir plik.fch` | wypisuje LLVM IR | | `finch lsp` | serwer języka (korzystają z niego edytory) | | `finch version` | wersja | -| `-l lib` | dolinkuj bibliotekę C (jak `link "lib"`) | -| `--target windows` / `linux` / `arm64` / `macos` / `android` / triple | zbuduj dla innego systemu (`android`: clang z NDK wskazanego przez `ANDROID_NDK_HOME`, zwykle z `--shared`) | -| `-g` | informacje dla debuggera (gdb, lldb, Visual Studio) | +| `-l bib` | dolinkowuje bibliotekę C (to samo co `link "bib"`) | +| `--target windows` / `linux` / `arm64` / `macos` / `android` / triple | buduje na inny system (`android`: clang z NDK wskazanego przez `ANDROID_NDK_HOME`, zwykle z `--shared`) | +| `-g` | informacje dla debuggera (gdb, lldb) | | `-O0` | bez optymalizacji | -| `--trace` / `--no-trace` | błędy w czasie działania pokazują łańcuch wywołań; włączone dla `run`, `test`, `-g`, `-O0`, wyłączone dla zwykłego `build` (kosztuje szybkość) | +| `--cpu native` / `x86-64-v3` / `apple-m1` … | używa wszystkich instrukcji danego procesora (AVX2, AVX-512, NEON); program będzie wtedy wymagał podobnego procesora | +| `--trace` / `--no-trace` | błąd w trakcie działania pokazuje ścieżkę wywołań; domyślnie włączone dla `run`, `test`, `-g` i `-O0`, wyłączone dla zwykłego `build` (spowalnia program) | -Zmienne środowiskowe: `FINCH_PATH` (foldery z modułami), `FINCH_CC` (kompilator C, który linkuje), `FINCH_CXX` (dla plików `.cpp`). -Skompilowany kod programu i jego pliki `link "x.c"` zostają w cache (`~/.cache/finch`), dopóki one albo ich nagłówki się nie zmienią. +Zmienne środowiskowe: `FINCH_PATH` (katalogi z modułami), `FINCH_CC` (kompilator C, który linkuje), `FINCH_CXX` (dla plików `.cpp`). +Skompilowany program i jego pliki z `link "x.c"` zostają w pamięci podręcznej (`~/.cache/finch`), dopóki nie zmienią się one same albo ich nagłówki. -`finch.toml` (obok kodu; `finch run` w tym folderze nie potrzebuje pliku ani flag): +`finch.toml` (obok kodu; wtedy `finch run` nie potrzebuje ani nazwy pliku, ani opcji): ```toml -name = "gra" # co robi finch build +name = "gra" # co zbuduje finch build entry = "src/main.fch" -paths = ["src"] # foldery z modułami +paths = ["src"] # katalogi z modułami include = ["vendor/SDL3/include"] # nagłówki C -links = ["SDL3", "native/glue.cpp"] # biblioteki i własne .c / .cpp / .o / .a +links = ["SDL3", "native/glue.cpp"] # biblioteki i twoje pliki .c / .cpp / .o / .a libdirs = ["vendor/SDL3/lib"] -cflags = ["-DFAST"] # dla własnych plików .c / .cpp +cflags = ["-DFAST"] # dla twoich plików .c / .cpp ldflags = ["-Wl,-rpath,."] +cpu = "native" # to samo co --cpu native -[target.windows] # tylko przy budowaniu dla tego systemu (też linux, macos, android, linux-arm64) +[target.windows] # tylko przy budowaniu na ten system (są też linux, macos, android, linux-arm64) links = ["ws2_32"] ``` @@ -63,8 +65,8 @@ fn main() { } ``` -`main` to `fn main()`, `fn main() -> int` (kod wyjścia) albo `fn main([]str args)`. -Jedna instrukcja na linijkę, bez średników, `{` w tej samej linijce, bez nawiasów wokół warunków. +`main` może wyglądać tak: `fn main()`, `fn main() -> int` (zwraca kod wyjścia) albo `fn main([]str args)`. +Jedna instrukcja w linii, bez średników, `{` w tej samej linii, warunki bez nawiasów. --- @@ -72,12 +74,12 @@ Jedna instrukcja na linijkę, bez średników, `{` w tej samej linijce, bez nawi | Kod | Znaczenie | |---|---| -| `x := 5` | nowa zmienna, typ zgadnięty | -| `int x = 5` | nowa zmienna, typ podany | -| `int x` | zero / `""` / `[]` / `null` / wartości domyślne struktury (też struktur w niej) | +| `x := 5` | nowa zmienna, typ wynika z wartości | +| `int x = 5` | nowa zmienna z podanym typem | +| `int x` | zero / `""` / `[]` / `null` / wartości domyślne struktury (także struktur w jej środku) | | `x = 7` | przypisanie | | `x += 1` `-=` `*=` `/=` `%=` | zmiana | -| `const W = 640`, `const f32 G = 9.8` | stała (na najwyższym poziomie pliku; `ksztalty.W` z modułu) | +| `const W = 640`, `const f32 G = 9.8` | stała (na najwyższym poziomie pliku; z modułu: `ksztalty.W`) | --- @@ -87,15 +89,16 @@ Jedna instrukcja na linijkę, bez średników, `{` w tej samej linijce, bez nawi |---|---| | `int` (= `i64`) | `42`, `-7`, `0xFF`, `1_000_000` | | `float` (= `f64`) | `3.14`, `1e9`, `2.5e-3` | -| `bool` | `true` (prawda), `false` (fałsz) | +| `bool` | `true`, `false` | | `char` | `'A'`, `'\n'` | -| `str` | `"tekst\t\"w cudzysłowie\""`, `"hp \(hp), prędkość \(v, 2)"` (kod w tekście; `, 2` = dwie cyfry po przecinku), `"""` kilka linii `"""` (wcięcie zamykających cudzysłowów znika) | -| `i8 i16 i32 i64` | ze znakiem, z rozmiarem | -| `u8 u16 u32 u64` | bez znaku, z rozmiarem | -| `f32 f64` | zmiennoprzecinkowe | +| `str` | `"tekst\t\"w cudzysłowie\""`, `"hp \(hp), prędkość \(v, 2)"` (kod w tekście; `, 2` = dwie cyfry po przecinku), `"""` kilka linii `"""` (wcięcie zamykającego cudzysłowu znika) | +| `i8 i16 i32 i64` | liczby całkowite ze znakiem, o podanym rozmiarze | +| `u8 u16 u32 u64` | liczby całkowite bez znaku | +| `f32 f64` | liczby 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 | +| `f32x8`, `i8x32`, `f64x4`, `u16x16` … | wektor: N liczb liczonych naraz (SIMD), zob. niżej | | `Nazwa` / `modul.Nazwa` | struktura | Znaki specjalne: `\n \t \r \0 \\ \" \'`. @@ -107,11 +110,11 @@ Znaki specjalne: `\n \t \r \0 \\ \" \'`. | Priorytet | Operatory | |---|---| | 5 | `*` `/` `%` `<<` `>>` `&` | -| 4 | `+` `-` `\|` `^` (`+` skleja też tekst) | +| 4 | `+` `-` `\|` `^` (`+` łączy też teksty) | | 3 | `==` `!=` `<` `<=` `>` `>=` | -| 2 | `&&` (i) | -| 1 | `\|\|` (lub) | -| jednoargumentowe | `-x` `!x` (nie) `~x` | +| 2 | `&&` | +| 1 | `\|\|` | +| jednoargumentowe | `-x` `!x` `~x` | `x & 1 == 0` znaczy `(x & 1) == 0`. @@ -121,14 +124,14 @@ Znaki specjalne: `\n \t \r \0 \\ \" \'`. ```c if a > b { … } else if a == b { … } else { … } -x := if a > b { a } else { b } // jako wartość: jedna wartość na gałąź, else wymagane +x := if a > b { a } else { b } // jako wartość: po jednej wartości w każdej gałęzi, else obowiązkowe while x > 0 { … } for i in 0..10 { … } // 0 … 9 -for el in lista { … } // każdy element (albo znak str) +for el in lista { … } // każdy element (albo znak tekstu) for i, el in lista { … } // razem z indeksem -for klucz, wart in m { … } // każdy wpis mapy, w kolejności dodawania +for klucz, wartosc in m { … } // każdy wpis mapy, w kolejności dodania break continue return wartosc -defer sprzatanie() // uruchomi się na końcu bloku +defer sprzatnij() // wykona się, gdy blok się skończy ``` --- @@ -144,48 +147,48 @@ fn przywitaj(str imie) { // bez wyniku } ``` -Parametry zachowują się jak kopie. Kolejność w pliku nie ma znaczenia. Przecinek po ostatnim argumencie jest dozwolony. +Parametry zachowują się jak kopie. Kolejność funkcji w pliku nie ma znaczenia. Po ostatnim argumencie może stać przecinek. --- ## Metody ```c -fn Punkt.przesun(int dx, int dy) { // self to Punkt, na którym wywołano (nie kopia) +fn Punkt.przesun(int dx, int dy) { // self to punkt, na którym ją 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) +p.przesun(1, 2) lista[0].przesun(1, 1) wskaznik_na_punkt.przesun(0, 1) ``` -Także na strukturach z C: `fn Vector2.length() -> f32 { … }`. +Działają też na strukturach z C: `fn Vector2.length() -> f32 { … }`. --- ## Błędy jako wartości ```c -fn parsuj(str s) -> int! { // ! = może się nie udać +fn parsuj(str s) -> int! { // ! = może się nie udać if s.len == 0 { - return error("pusty") + return error("pusty tekst") } return int(s) or { return error("to nie liczba: " + s) } } -fn zapisz(str p, str t) -> ! { // może się nie udać, bez wyniku +fn zapisz(str p, str t) -> ! { // może się nie udać, bez wyniku try write_file(p, t) } ``` -| Kod | Przy porażce | +| Kod | Gdy się nie uda | |---|---| -| `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 | +| `x := parsuj(s) or 0` | weź 0 | +| `x := parsuj(s) or { print(err); return }` | wykonaj blok (`err` to komunikat) | +| `x := try parsuj(s)` | przerwij też bieżącą funkcję z tym samym błędem | +| `fn main() -> !` | błąd wypisuje `error: …`, kod wyjścia 1 | -Funkcje wbudowane, które mogą się nie udać: `int(s)`, `float(s)`, `read_file`, `write_file`, `delete_file`. +Wbudowane funkcje, które mogą się nie udać: `int(s)`, `float(s)`, `read_file`, `write_file`, `delete_file`. --- @@ -193,9 +196,9 @@ Funkcje wbudowane, które mogą się nie udać: `int(s)`, `float(s)`, `read_file | Kod | Znaczenie | |---|---| -| `struct P { … }` | definicja (jedno pole na linijkę, `= domyślna` opcjonalnie) | +| `struct P { … }` | definicja (jedno pole w linii, `= wartość` opcjonalnie) | | `P(1, 2)` | utworzenie, wszystkie pola po kolei | -| `P(x: 1)` | utworzenie po nazwie, reszta domyślna/zero | +| `P(x: 1)` | utworzenie przez nazwy pól, reszta domyślna albo zero | | `p.x`, `p.x = 5` | odczyt, zmiana pola | | `b := a` | pełna kopia | @@ -203,62 +206,83 @@ Funkcje wbudowane, które mogą się nie udać: `int(s)`, `float(s)`, `read_file ## Tablice `[]T` -| Kod | Daje | +| Kod | Wynik | |---|---| | `a.len` | liczba elementów | -| `a[i]`, `a[i] = x` | element (z kontrolą zakresu) | -| `a.push(x)` | dodaj na końcu | -| `a.pop()` | zabierz ostatni i go oddaj | -| `a.insert(i, x)` | wstaw na pozycję i | -| `a.remove(i)` | usuń pozycję i i ją oddaj | -| `a.clear()` | wyczyść | -| `a.resize(n)` | długość n (nowe elementy to zera) | +| `a[i]`, `a[i] = x` | element (zakres jest sprawdzany) | +| `a.push(x)` | dodaje na końcu | +| `a.pop()` | zdejmuje ostatni i go zwraca | +| `a.insert(i, x)` | wstawia na pozycję i | +| `a.remove(i)` | usuwa pozycję i i ją zwraca | +| `a.clear()` | opróżnia | +| `a.resize(n)` | długość n (nowe elementy są zerami) | | `a.find(x)` | indeks albo -1 | | `a.contains(x)` | bool | -| `a.sort()` | posortuj (liczby, znaki, str) | -| `a.reverse()` | odwróć | -| `a.slice(s, e)` | nowa tablica z s … e-1 | -| `a.join(sep)` | `[]str` → jeden str | +| `a.sort()` | sortuje (liczby, znaki, teksty) | +| `a.reverse()` | odwraca kolejność | +| `a.slice(p, k)` | nowa tablica z elementów p … k-1 | +| `a.join(sep)` | `[]str` → jeden tekst | | `a.ptr` | adres pierwszego elementu (dla C) | --- ## Mapy `map[K]V` -| Kod | Daje | +| Kod | Wynik | |---|---| | `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]` | wartość (brak klucza zatrzymuje program) | +| `m[k] = v` | dodaje albo podmienia | | `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 | +| `m.has(k)` | bool | +| `m.get(k, domyslna)` | wartość albo `domyslna` | +| `m.remove(k)` | bool: czy klucz był | +| `m.clear()` | opróżnia | +| `m.keys()`, `m.values()` | tablice, w kolejności dodania | + +--- + +## Wektory `f32x8` (SIMD) + +Typy: `f32x4/8/16`, `f64x2/4/8`, `i8x16/32/64`, `i16x8/16/32`, `i32x4/8/16`, `i64x2/4/8` i tak samo `u…`; najwyżej 512 bitów. + +| Kod | Wynik | +|---|---| +| `f32x8(x)` | `x` w każdym elemencie | +| `f32x8(a, b, …, h)` | po jednej liczbie na element | +| `f32x8(lista, i)` | `lista[i]` … `lista[i+7]` (z `[]f32` albo `ptr[f32]`) | +| `f32x8(v)` | 8-elementowy wektor przeliczony element po elemencie (`f32x8(calkowite)`, `i32x8(ulamki)`) | +| `v + w`, `v * 2.0`, `-v` | element po elemencie; zwykła liczba działa na każdy element (`/` tylko dla liczb zmiennoprzecinkowych) | +| `& \| ^ << >> ~` | element po elemencie, dla liczb całkowitych | +| `v == w`, `v != w` | bool: czy wszystkie elementy są równe / czy któryś się różni | +| `v[i]`, `v[i] = x`, `v.len` | jeden element; liczba elementów | +| `min(v, w)`, `max`, `abs(v)`, `clamp(v, lo, hi)` | element po elemencie | +| `v.sum()`, `v.min()`, `v.max()`, `v.dot(w)` | jedna liczba ze wszystkich elementów | +| `v.sqrt()`, `v.floor()`, `v.ceil()`, `v.round()`, `v.fma(b, c)` | każdy element (liczby zmiennoprzecinkowe; `fma` = `v * b + c`) | +| `v.store(lista, i)` | zapisuje elementy do `lista[i]` … | --- ## Tekst `str` -| Kod | Daje | +| Kod | Wynik | |---|---| -| `s.len` | liczba bajtów (ą, ż… to 2) | +| `s.len` | liczba bajtów | | `s[i]`, `s[i] = 'x'` | znak | -| `a + b` | sklejony tekst | -| `"x = \(x)"` | wartość dowolnego kodu w tekście (`\(x, 2)`: liczba z 2 cyframi po przecinku) | +| `a + b` | połączony tekst | +| `"x = \(x)"` | wartość dowolnego kodu wstawiona w tekst (`\(x, 2)`: liczba z dwiema cyframi po przecinku) | | `s.sub(a, b)` | fragment a … b-1 | | `s.find(t)` | pozycja albo -1 | -| `s.contains(t)` | czy zawiera | -| `s.starts_with(t)`, `s.ends_with(t)` | czy zaczyna / kończy się | +| `s.contains(t)` | bool | +| `s.starts_with(t)`, `s.ends_with(t)` | bool | | `s.split(sep)` | `[]str` | | `s.trim()` | bez spacji na końcach | | `s.upper()`, `s.lower()` | A–Z / a–z | -| `s.replace(a, b)` | każde a → b | -| `s.repeat(n)` | n razy | +| `s.replace(a, b)` | każde a zamienione na b | +| `s.repeat(n)` | tekst n razy | | `s.bytes()` | `[]u8` | -| `s.ptr` | `ptr[char]` (dla C; `str` trafia też do parametru `const char *` bez zmian) | +| `s.ptr` | `ptr[char]` (dla C; parametr `const char *` przyjmie też samo `str`) | | `==` `!=` `<` `>` | porównanie | --- @@ -271,41 +295,41 @@ Funkcje wbudowane, które mogą się nie udać: `int(s)`, `float(s)`, `read_file | `int("42")` | `42` (zatrzymuje program, jeśli to nie liczba) | | `int('A')` | `65` | | `float(7)`, `float("2.5")` | `7`, `2.5` | -| `u8(300)` | `44` (zostawia najniższe bity) | +| `u8(300)` | `44` (zostają najniższe bity) | | `char(66)` | `'B'` | | `bool(0)` | `false` | -| `str(42)`, `str(1.5)`, `str(true)`, `str('c')` | tekst (float: najkrótszy tekst, który jest tą samą liczbą) | -| `str(3.14159, 2)` | `"3.14"` (tyle cyfr po kropce) | +| `str(42)`, `str(1.5)`, `str(true)`, `str('c')` | tekst (liczba zmiennoprzecinkowa: najkrótszy zapis tej samej liczby) | +| `str(3.14159, 2)` | `"3.14"` (tyle cyfr po przecinku) | | `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 | +| `ptr(p)`, `ptr(16)` | wskaźnik bez typu, ze wskaźnika albo z liczby | | `int(p)` | wskaźnik → adres | -Automatycznie tylko wtedy, gdy nic nie ginie: mniejsza → większa liczba, dowolna całkowita → float, `f32` → `f64`. +Automatycznie tylko wtedy, gdy nic nie ginie: mniejsza liczba → większa, liczba całkowita → zmiennoprzecinkowa, `f32` → `f64`. --- ## Funkcje wbudowane -| Funkcja | Robi | +| Funkcja | Co robi | |---|---| -| `print(a, b, …)` | wypisz, spacje między, nowa linia na końcu | -| `input()`, `input("Pytanie? ")` | wczytaj linijkę | +| `print(a, b, …)` | wypisuje wartości oddzielone spacjami i kończy linię | +| `input()`, `input("Pytanie? ")` | wczytuje linię | | `read_file(sciezka)` | plik → str | | `write_file(sciezka, tekst)` | str → plik, `true`, jeśli się udało | -| `file_exists(sciezka)` | czy plik istnieje | -| `delete_file(sciezka)` | usuń plik | -| `shell(polecenie)` | uruchom polecenie, jego kod wyjścia | -| `exit(kod)` | zakończ program od razu (kończy też blok `or { }`) | -| `min(a, b, …)`, `max(a, b, …)`, `abs(x)`, `clamp(x, od, do)` | dla dowolnych liczb (twoja funkcja o tej nazwie wygrywa) | +| `file_exists(sciezka)` | bool | +| `delete_file(sciezka)` | bool | +| `shell(polecenie)` | uruchamia polecenie, zwraca jego kod wyjścia | +| `exit(kod)` | od razu kończy program (kończy też blok `or { }`) | +| `min(a, b, …)`, `max(a, b, …)`, `abs(x)`, `clamp(x, lo, hi)` | dla dowolnych liczb (twoja funkcja o tej samej nazwie ma pierwszeństwo) | | `error(komunikat)` | `return error("…")` w funkcji `-> T!` | -| `addr(x)` | wskaźnik na x; `addr(fn)` = callback dla C | -| `assert(warunek)`, `assert(warunek, "dlaczego")` | zatrzymaj się z błędem, gdy warunek jest fałszywy (w bloku `test`: ten test nie przechodzi) | -| `atomic_add(p, n)`, `atomic_sub`, `atomic_load(p)`, `atomic_store(p, n)`, `atomic_swap(p, n)`, `atomic_cas(p, stara, nowa)` | liczba całkowita zmieniana też przez inne wątki, przez jej `ptr` (`addr(licznik)`) | +| `addr(x)` | wskaźnik na x; `addr(fn)` = funkcja zwrotna dla C | +| `assert(warunek)`, `assert(warunek, "dlaczego")` | zatrzymuje program, gdy warunek jest fałszywy (w bloku `test`: ten test nie przechodzi) | +| `atomic_add(p, n)`, `atomic_sub`, `atomic_load(p)`, `atomic_store(p, n)`, `atomic_swap(p, n)`, `atomic_cas(p, stara, nowa)` | liczba całkowita, którą zmieniają też inne wątki, przez jej `ptr` (`addr(licznik)`) | | `new(wartosc)` | wartość na stercie → `ptr[T]` | -| `free(p)` | oddaj ją | +| `free(p)` | oddaje tę pamięć | --- @@ -314,7 +338,7 @@ Automatycznie tylko wtedy, gdy nic nie ginie: mniejsza → większa liczba, dowo | Finch | C | |---|---| | `ptr[int] p` | `int *p` | -| `addr(x)` | `&x` (zmienna-struktura podana tam, gdzie idzie `ptr[TaStruktura]`: `pchnij(cialo)` = `pchnij(addr(cialo))`) | +| `addr(x)` | `&x` (zmienna-struktura podana tam, gdzie oczekiwany jest `ptr[TaStruktura]`: `push(cialo)` = `push(addr(cialo))`) | | `p.value` | `*p` | | `p.pole` | `p->pole` | | `p[i]` | `p[i]` | @@ -325,23 +349,23 @@ Automatycznie tylko wtedy, gdy nic nie ginie: mniejsza → większa liczba, dowo ## Pamięć -- Tablice, teksty i struktury zwalniają się same na końcu swojego bloku. Bez `free`. -- `b := a` kopiuje; wynik wywołania jest przenoszony; `return x` przenosi. -- Przekazywanie struktury: `fn f(Cialo c)` pracuje na własnej kopii (ta u wywołującego się nie zmienia), `fn f(ptr[Cialo] c)` zmienia tę u wywołującego (`f(cialo)` albo `f(addr(cialo))`), metoda dostaje `self` przez adres, nigdy kopię. Struktura z 256+ bajtami liczb przekazana przez wartość albo w `for x in lista` dostaje ostrzeżenie. -- `new(...)` + `free(...)` (często z `defer`) do list wiązanych, drzew, współdzielenia. -- Pamięć z C: funkcjami z C (`malloc` / `free`, `fclose`, …). +- Tablice, teksty i struktury zwalniają się same na końcu swojego bloku. `free` nie jest potrzebne. +- `b := a` kopiuje; wynik funkcji jest przenoszony; `return x` przenosi. +- Przekazywanie struktury: `fn f(Cialo c)` pracuje na własnej kopii (struktura wywołującego się nie zmienia), `fn f(ptr[Cialo] c)` zmienia strukturę wywołującego (`f(cialo)` albo `f(addr(cialo))`), metoda dostaje `self` przez adres, nigdy jako kopię. Struktura z co najmniej 256 bajtami liczb, przekazywana przez wartość albo w `for x in lista`, dostaje ostrzeżenie. +- `new(...)` z `free(...)` (często razem z `defer`) przydaje się do list wiązanych, drzew i współdzielenia. +- Pamięć z C zwalniasz funkcjami C (`malloc` / `free`, `fclose`, …). --- ## Moduły ```c -import ksztalty // ksztalty.fch obok tego pliku (albo w FINCH_PATH) -ksztalty.pole(b) // jego funkcje -ksztalty.Pudlo(2, 3) // jego struktury -ksztalty.Pudlo inne // w deklaracjach -import ui/przycisk // ui/przycisk.fch: przycisk.rysuj() -import ui/przycisk as p // inna nazwa: p.rysuj() +import ksztalty // ksztalty.fch obok tego pliku (albo w FINCH_PATH) +ksztalty.pole(b) // jego funkcje +ksztalty.Pudelko(2, 3) // jego struktury +ksztalty.Pudelko inne // w deklaracjach +import ui/button // ui/button.fch: button.draw() +import ui/button as btn // pod inną nazwą: btn.draw() ``` --- @@ -350,14 +374,14 @@ import ui/przycisk as p // inna nazwa: p.rysuj() | Kod | Znaczenie | |---|---| -| `import "x.h"` | funkcje, struktury, enumy, liczbowe `#define`, zmienne globalne | -| `link "nazwa"` | libnazwa.so / nazwa.lib (najpierw pkg-config) | -| `link "plik.c"`, `link "glue.cpp"` | skompiluj własny plik C albo C++ (C++: funkcje w `extern "C"`) | -| `addr(mojaFn)` | funkcja Fincha jako callback | +| `import "x.h"` | funkcje, struktury, enumy, liczby z `#define`, zmienne globalne | +| `link "nazwa"` | libnazwa.so / nazwa.lib (najpierw pyta pkg-config) | +| `link "plik.c"`, `link "glue.cpp"` | kompiluje twój plik C albo C++ (w C++ funkcje w `extern "C"`) | +| `addr(mojaFn)` | przekazuje funkcję Fincha jako funkcję zwrotną | -| Typ C | Finch | +| Typ w C | W Finchu | |---|---| -| `int` / `long long` / `unsigned` | `i32` / `i64` / `u32` (wg rozmiaru) | +| `int` / `long long` / `unsigned` | `i32` / `i64` / `u32` (według rozmiaru) | | `float` / `double` | `f32` / `f64` | | `char *` (parametr, wynik) | `str` | | `char *` (pole, za wskaźnikiem) | `ptr[char]` | @@ -365,46 +389,46 @@ import ui/przycisk as p // inna nazwa: p.rysuj() | `T *` | `ptr[T]` | | `void *`, uchwyty | `ptr` | | `T x[N]` (w strukturze) | `[N]T` | -| `union U` | `U`: `U u`, `u.pole`, `U(pole: v)`, przez wskaźnik do C (nie przez wartość) | -| `#define N 4`, `"tekst"`, `(1u << 3)`, `((Uint64)2)` | stałe (bez makr-funkcji) | -| `#define RAYWHITE (Color){ 245, 245, 245, 255 }` | `Color` | +| `union U` | `U`: `U u`, `u.pole`, `U(pole: v)`, do C przez wskaźnik (nie przez wartość) | +| `#define N 4`, `"tekst"`, `(1u << 3)`, `((Uint64)2)` | stałe (bez makr z parametrami) | +| `#define RAYWHITE (Color){ 245, 245, 245, 255 }` | wartość typu `Color` | -**`char *` to `str`**: funkcja C, która bierze albo zwraca `char *`, bierze albo daje `str` Fincha (bez `.ptr`, bez zamiany); tylko pole `char *` i `char **` zostają `ptr[char]`. -C++ i Rust: przez nagłówek C. C++ z `extern "C"` (i `link "x.cpp"`), Rust z `#[no_mangle] pub extern "C" fn` zbudowany jako `staticlib` (`link "target/release/libgra.a"`) i nagłówek (napisze go cbindgen). +**`char *` to `str`**: funkcja C, która przyjmuje albo zwraca `char *`, przyjmuje albo zwraca `str` Fincha (bez `.ptr` i bez zamiany); tylko pole `char *` i `char **` zostają jako `ptr[char]`. +C++ i Rust: przez nagłówek C. C++ z `extern "C"` (i `link "x.cpp"`), Rust z `#[no_mangle] pub extern "C" fn`, zbudowany jako `staticlib` (`link "target/release/libgra.a"`), z nagłówkiem (może go napisać cbindgen). -Wynik `char *` z funkcji C to od razu `str`; struktury przez wartość działają w konwencjach System V, Windows x64 i ARM64 Linux (sprawdzone w emulatorach; macOS przez CI). +Wynik `char *` funkcji C od razu jest `str`; struktury przez wartość działają w konwencjach wywołań System V, Windows x64 i ARM64 (Linux i macOS). --- -## Błędy w czasie działania (program zatrzymuje się z plikiem i linijką) +## Błędy w trakcie działania (program kończy się z plikiem i linią) | Komunikat | Przyczyna | |---|---| | `division by zero` | `/` albo `%` przez 0 | | `index 5 is out of range (the length is 3)` | `a[5]` | -| `pop() on an empty array` | `pop()` na pustej tablicy | +| `pop() on an empty array` | | | `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` (bez `or`) | | `the key "x" is not in the map` | `m["x"]` | | `assert failed: dlaczego` | `assert(warunek, "dlaczego")` | -Pod komunikatem wywołania, które do niego doprowadziły (`finch run`, `finch test`, `-g`, `-O0`, `--trace`): +Pod komunikatem pojawiają się wywołania, które do niego doprowadziły (`finch run`, `finch test`, `-g`, `-O0`, `--trace`): ``` gra.fch:6: runtime error: index 3 is out of range (the length is 2) - in Enemy.next (gra.fch:6) - in step (gra.fch:10) + in Wrog.nastepny (gra.fch:6) + in krok (gra.fch:10) in main (gra.fch:15) ``` ## Testy ```c -test "wysokość skoku" { // obok kodu; finch test plik.fch je uruchamia, finch run/build je pomijają - g := Gracz() - g.skocz() - assert(g.y > 0.0, "y to \(g.y)") +test "wysokość skoku" { // obok kodu; uruchamia je finch test plik.fch, finch run/build je pomijają + p := Gracz() + p.skocz() + assert(p.y > 0.0, "y wynosi \(p.y)") } ``` @@ -413,74 +437,55 @@ test "wysokość skoku" { // obok kodu; finch test plik.fch je uruchamia ## 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`. +Dowolny edytor z LSP: `finch lsp`. ### VS Code (rozszerzenie „Finch”) -| Klawisz / akcja | Robi | +| Klawisz / akcja | Co robi | |---|---| -| ▶ / `Ctrl+F5` | uruchom plik | -| `Finch: Build This File` | zbuduj; błędy trafiają do panelu Problems | -| `F12` | idź do definicji | -| najechanie myszą | typ / sygnatura / opis | +| ▶ / `Ctrl+F5` | uruchamia plik | +| `Finch: Build This File` | buduje; błędy trafiają do panelu Problems | +| `F12` | przejście do definicji | +| najechanie kursorem | typ / sygnatura / opis | | `Ctrl+Spacja` | podpowiedzi (po `.`: pola, metody, zawartość modułu) | | `Ctrl+Shift+O` | funkcje i struktury w pliku | -| `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 | +| `main`, `fn`, `fnr`, `fnf`, `method`, `struct`, `for`, `foreach`, `forkv`, `map`, `orb`, `if`, `ife`, `while`, `input` + Tab | gotowe fragmenty kodu | +| ustawienie `finch.path` | gdzie leży finch / finch.exe | --- ## Enumy i `match` ``` -enum Kolor { Czerwony, Zielony } enum Status { Ok = 200, Brak = 404 } -k := Kolor.Zielony int(k) Kolor(1) k == Kolor.Czerwony // tylko == i !=; Kolor(7) to błąd +enum Kolor { Czerwony, Zielony, Niebieski } enum Status { Ok = 200, NotFound = 404 } +k := Kolor.Zielony int(k) Kolor(2) k == Kolor.Czerwony // tylko == i !=; Kolor(7) to błąd fn Kolor.nastepny() -> Kolor { ... } // metody działają -match k { Czerwony -> ... Zielony -> { ... } else -> ... } // same nazwy wariantów; każdy wariant albo else -x := match n { 1, 2 -> "mało" else -> "dużo" } // wyrażenie: gałęzie tego samego typu +match k { Czerwony -> ... Zielony, Niebieski -> { ... } else -> ... } // same nazwy wariantów; każdy wariant albo else +x := match n { 1, 2 -> "kilka" else -> "dużo" } // wyrażenie: każda gałąź tego samego typu ``` ## Typy generyczne ``` -fn max[T](T a, T b) -> T { ... } max(3, 9) max[float](1, 2.5) // wynika z argumentów albo zapisane -struct Stos[T] { []T elementy } fn Stos[T].dodaj(T x) { ... } // typ zawsze podaje się przy tworzeniu: +fn max[T](T a, T b) -> T { ... } max(3, 9) max[float](1, 2.5) // typ wywnioskowany albo podany +struct Stos[T] { []T el } fn Stos[T].push(T x) { ... } // typ zawsze idzie z nazwą: s := Stos[int]() Stos[int] t Para[str, int]("wiek", 31) // nigdy samo Stos() ``` -## Biblioteka standardowa (`import nazwa`; własny plik o tej nazwie ma pierwszeństwo) +## Biblioteka standardowa (`import nazwa`; twój plik o tej samej nazwie ma pierwszeństwo) | | | |---|---| | `path` | `join(a, b)` `base(p)` `dir(p)` `ext(p)` `stem(p)` `parts(p)` `clean(p)` `is_abs(p)` | -| `time` | `now()` `millis()` `sleep(ms)` `date(t)` `local(t)` `format(t, fmt)` `days_in_month(r, m)` | -| `proc` | `run(cmd)` `output(cmd)` (błąd z jego wyjściem) `env(nazwa, domyślna)` `pid()` `cwd()` | +| `time` | `now()` `millis()` `sleep(ms)` `date(t)` `local(t)` `format(t, wzor)` `days_in_month(r, m)` | +| `proc` | `run(polecenie)` `output(polecenie)` (przy błędzie zwraca jego wyjście) `env(nazwa, domyslna)` `pid()` `cwd()` | | `thread` | `spawn(addr(praca), addr(dane))` → `t.join()`; `mutex()` → `m.lock()` `m.unlock()` `m.close()`; `cpus()` (funkcja wątku: `fn praca(ptr arg)`) | -| `fs` | `read_bytes(p)` `write_bytes(p, dane)` `exists(p)` `is_dir(p)` `mkdir(p)` `list(folder)` `remove_dir(p)` `data_dir(app)` | +| `fs` | `read_bytes(p)` `write_bytes(p, dane)` `exists(p)` `is_dir(p)` `mkdir(p)` `list(katalog)` `remove_dir(p)` `data_dir(aplikacja)` | | `json` | `parse(tekst)` `stringify(v)` `pretty(v)` `new_text/number/bool/null/list/object` `v.get(k)` `v.at(i)` `v.set(k, x)` `v.push(x)` `v.has(k)` `v.str_or(d)` `v.int_or(d)` `v.float_or(d)` `v.bool_or(d)` | ## Słowa kluczowe -| Słowo | Po polsku | -|---|---| -| `fn` | funkcja | -| `return` | zwróć | -| `if` / `else` | jeżeli / w przeciwnym razie | -| `while` | dopóki | -| `for` … `in` | dla … w | -| `break` / `continue` | przerwij / kontynuuj | -| `true` / `false` / `null` | prawda / fałsz / nic | -| `import` / `link` | zaimportuj / dolinkuj | -| `struct` | struktura | -| `enum` | wyliczenie | -| `match` | dopasuj | -| `defer` | odłóż (na koniec bloku) | -| `or` | albo (co zrobić przy porażce) | -| `try` | spróbuj (porażkę przekaż dalej) | -| `const` | stała | -| `export` | wystaw (funkcja dla programu w C) | -| `self` | sam (w metodzie: struktura, na której ją wywołano) | -| `test` | test (tylko jako `test "nazwa" { }`) | -| `as` | jako (tylko w `import x as y`) | -| `map` | mapa | +`fn return if else while for in break continue true false null import link struct enum match defer or try const export` +(`test` i `as` tylko tam, gdzie coś znaczą: `test "nazwa" { }`, `import x as y`) +(oraz `self` wewnątrz metod) diff --git a/editors/kate/finch.xml b/editors/kate/finch.xml index 883a6e0..f9f2642 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. --> - @@ -103,6 +103,8 @@ + + diff --git a/editors/vscode/CHANGELOG.md b/editors/vscode/CHANGELOG.md index 195374b..a0a750c 100644 --- a/editors/vscode/CHANGELOG.md +++ b/editors/vscode/CHANGELOG.md @@ -1,5 +1,9 @@ # Changelog +## 2.6.0 + +- Vector types (`f32x8`, `i8x32`, `f64x4`, …) highlighted as types; completion of their methods (`sum`, `dot`, `fma`, `store`, …). + ## 2.5.0 - Highlighting for code inside text (`"hp \(hp)"`), `test "…" { }` blocks, `const`, `"""` text, `1e9`. diff --git a/editors/vscode/package-lock.json b/editors/vscode/package-lock.json index f98d8ad..5a0404d 100644 --- a/editors/vscode/package-lock.json +++ b/editors/vscode/package-lock.json @@ -1,12 +1,12 @@ { "name": "finch-lang", - "version": "2.4.0", + "version": "2.6.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "finch-lang", - "version": "2.4.0", + "version": "2.6.0", "license": "Apache-2.0", "dependencies": { "vscode-languageclient": "^9.0.1" diff --git a/editors/vscode/package.json b/editors/vscode/package.json index 40c7715..9c17bd1 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.5.0", + "version": "2.6.0", "publisher": "nexoniarz", "license": "Apache-2.0", "icon": "images/icon.png", diff --git a/editors/vscode/syntaxes/finch.tmLanguage.json b/editors/vscode/syntaxes/finch.tmLanguage.json index 9154a4e..e3c2dea 100644 --- a/editors/vscode/syntaxes/finch.tmLanguage.json +++ b/editors/vscode/syntaxes/finch.tmLanguage.json @@ -148,7 +148,7 @@ }, { "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(int|float|bool|char|str|ptr|map|i8|i16|i32|i64|u8|u16|u32|u64|f32|f64|[iuf](?:8|16|32|64)x(?:2|4|8|16|32|64))\\b" }, { "match": "\\b(print|input|read_file|write_file|file_exists|delete_file|shell|exit|addr|new|free|error|min|max|abs|clamp|assert|atomic_add|atomic_sub|atomic_load|atomic_store|atomic_swap|atomic_cas)\\s*(?=\\()", diff --git a/editors/vscode/test/grammar.js b/editors/vscode/test/grammar.js index bf2aae7..0903ec5 100644 --- a/editors/vscode/test/grammar.js +++ b/editors/vscode/test/grammar.js @@ -29,6 +29,7 @@ const code = [ 'test "lives \\(hp + f(1)) left" {', ' assert(clamp(x, 0, 9) == 3)', '}', + 'fn dot(f32x8 a, i8x32 b) {', ]; const expect = [ // [line, token text, scope that must be present] [0, 'import', 'keyword.control.import'], [0, '"stdio.h"', 'string.quoted.double'], @@ -49,6 +50,7 @@ const expect = [ // [line, token text, scope that must be present] [17, 'test', 'keyword.other.test'], [17, '\\(', 'punctuation.section.embedded.begin'], [17, 'hp', 'meta.embedded.line'], [17, 'f', 'meta.embedded.line'], [17, 'left', 'string.quoted.double'], [18, 'assert', 'support.function.builtin'], [18, 'clamp', 'support.function.builtin'], + [20, 'f32x8', 'storage.type.primitive'], [20, 'i8x32', 'storage.type.primitive'], ]; registry.loadGrammar('source.finch').then((g) => { let state = vsctm.INITIAL, toks = []; diff --git a/examples/simd.fch b/examples/simd.fch new file mode 100644 index 0000000..691227f --- /dev/null +++ b/examples/simd.fch @@ -0,0 +1,69 @@ +// Vectors (SIMD): eight f32 at once instead of one. +// +// finch run examples/simd.fch runs on any x86-64 or ARM64 processor +// finch run examples/simd.fch --cpu native uses all of this one's instructions (AVX2, AVX-512, NEON) +import time + +// one number at a time +fn dot_plain([]f32 a, []f32 b) -> f32 { + f32 total = 0.0 + for i in 0..a.len { + total += a[i] * b[i] + } + return total +} + +// eight at a time: f32x8(list, i) takes list[i] ... list[i+7], fma multiplies and adds in one step +fn dot_simd([]f32 a, []f32 b) -> f32 { + acc := f32x8(0.0) + i := 0 + while i + 8 <= a.len { + acc = f32x8(a, i).fma(f32x8(b, i), acc) + i += 8 + } + total := acc.sum() + while i < a.len { // the few that are left + total += a[i] * b[i] + i += 1 + } + return total +} + +// brighten every color: the same work on every lane, with min keeping it below 1 +fn brighten([]f32 pixels, f32 amount) { + i := 0 + while i + 8 <= pixels.len { + v := min(f32x8(pixels, i) * (1.0 + amount), 1.0) + v.store(pixels, i) + i += 8 + } +} + +fn main() { + []f32 a + []f32 b + for i in 0..4_000_000 { + a.push(f32(i % 100) / 100.0) + b.push(0.5) + } + + start := time.millis() + x := 0.0 + for round in 0..20 { + x = float(dot_plain(a, b)) + } + plain := time.millis() - start + + start = time.millis() + y := 0.0 + for round in 0..20 { + y = float(dot_simd(a, b)) + } + simd := time.millis() - start + + print("one at a time: ", str(x, 1), "in", plain, "ms") + print("eight at a time:", str(y, 1), "in", simd, "ms") + + brighten(a, 0.5) + print("brightened:", a[10], a[90], a.len) +} diff --git a/runtime/finch_rt.c b/runtime/finch_rt.c index ab806a3..e9a63fb 100644 --- a/runtime/finch_rt.c +++ b/runtime/finch_rt.c @@ -2,7 +2,7 @@ // It is embedded in the compiler, compiled once into a cache folder, and linked // statically into every Finch program. Everything here is plain C with no state. // -// Layout shared with the compiler (src/codegen.cpp): +// Layout shared with the compiler (src/gen.fch): // str = { char *ptr; int64 len; int64 cap } ptr is always NUL-terminated // cap > 0 : heap memory owned by the value (cap bytes allocated) // cap == 0 : a string literal, valid forever, never written to diff --git a/src/ast.fch b/src/ast.fch index 0870037..4c60451 100644 --- a/src/ast.fch +++ b/src/ast.fch @@ -36,6 +36,41 @@ fn ptrTo(Type t) -> Type { return out } +// f32x8, i8x32, u16x16, f64x4: N numbers of one type side by side, worked on all at once (SIMD). +// Its kind is "vec", elem[0] the type of one lane, count the number of lanes. kind "" if `name` is no such type. +fn vecType(str name) -> Type { + x := name.find("x") + if x < 2 || x + 1 >= name.len { + return Type() + } + lane := name.sub(0, x) + if !["i8", "i16", "i32", "i64", "u8", "u16", "u32", "u64", "f32", "f64"].contains(lane) { + return Type() + } + n := 0 + for c in name.sub(x + 1, name.len) { + if c < '0' || c > '9' { + return Type() + } + n = n * 10 + (int(c) - int('0')) + } + if n != 2 && n != 4 && n != 8 && n != 16 && n != 32 && n != 64 { + return Type() + } + bits := int(lane.sub(1, lane.len)) or 0 + if n * bits > 512 { + return Type() // the widest registers (AVX-512) hold 512 bits + } + if lane == "i64" { + lane = "int" + } else if lane == "f64" { + lane = "float" + } + out := Type(kind: "vec", count: n) + out.elem.push(simple(lane)) + return out +} + fn same(Type a, Type b) -> bool { if a.kind != b.kind { return false @@ -43,10 +78,10 @@ fn same(Type a, Type b) -> bool { if a.kind == "struct" || a.kind == "named" || a.kind == "enum" { return a.name == b.name && a.module == b.module } - if a.kind == "fixed" && a.count != b.count { + if (a.kind == "fixed" || a.kind == "vec") && a.count != b.count { return false } - if a.kind == "array" || a.kind == "ptr" || a.kind == "map" || a.kind == "fixed" { + if a.kind == "array" || a.kind == "ptr" || a.kind == "map" || a.kind == "fixed" || a.kind == "vec" { if a.elem.len != b.elem.len { return false } @@ -67,6 +102,15 @@ fn show(Type t) -> str { if t.kind == "fixed" { return "[" + str(t.count) + "]" + show(t.elem[0]) } + if t.kind == "vec" { + lane := t.elem[0].kind + if lane == "int" { + lane = "i64" + } else if lane == "float" { + lane = "f64" + } + return lane + "x" + str(t.count) + } if t.kind == "map" { return "map[" + show(t.elem[0]) + "]" + show(t.elem[1]) } diff --git a/src/cimport.fch b/src/cimport.fch index 8d1701f..5d8fa14 100644 --- a/src/cimport.fch +++ b/src/cimport.fch @@ -1,10 +1,9 @@ // C headers, read through libclang, written in Finch. // // What `import "stdio.h"` teaches the compiler: the C functions, the constants (enum values and -// plain #define numbers), the extern variables (stdout) and the structs. The same job as -// src/cimport.cpp, done by calling libclang's C API through Finch's own C import -- which is -// what makes this the part that closes the bootstrap: to compile this file the self-hosted -// compiler has to read libclang's headers with this very code. +// plain #define numbers), the extern variables (stdout) and the structs. It calls libclang's C API +// through Finch's own C import -- so to compile this file, the compiler reads libclang's headers +// with this very code. // // C structs by value appear in types as the named type `module "C"`. import ast diff --git a/src/docs.fch b/src/docs.fch index 977f0de..271660a 100644 --- a/src/docs.fch +++ b/src/docs.fch @@ -40,7 +40,8 @@ fn builtins() -> []Doc { doc("float", "float(x) -> float", "Converts to a decimal number; parses text (int(s) or 0.0 handles bad text)."), doc("char", "char(int n) -> char", "The character with this code."), doc("bool", "bool(int n) -> bool", "true for any number but 0."), - doc("ptr", "ptr(x) -> ptr", "An untyped pointer: from a typed pointer, or from a number (an address or offset for C).") + doc("ptr", "ptr(x) -> ptr", "An untyped pointer: from a typed pointer, or from a number (an address or offset for C)."), + doc("f32x8", "f32x8(x) / f32x8(a, b, ...) / f32x8(list, i) / f32x8(v)", "A vector: 8 f32 side by side, worked on all at once (SIMD). Every lane x; one value per lane; 8 numbers from list[i]; or another 8-lane vector converted. The same goes for i8x16, i32x8, f64x4 and the rest.") ] } @@ -94,13 +95,32 @@ fn mapMethods() -> []Doc { ] } +fn vecMethods() -> []Doc { + return [ + doc("len", ".len -> int", "The number of lanes."), + doc("sum", "sum() -> T", "All the lanes added up (floats: in whatever order is fastest)."), + doc("min", "min() -> T", "The smallest lane."), + doc("max", "max() -> T", "The biggest lane."), + doc("dot", "dot(V w) -> T", "Lane times lane, added up: (v * w).sum()."), + doc("sqrt", "sqrt() -> V", "The square root of every lane (f32/f64 lanes)."), + doc("floor", "floor() -> V", "Every lane rounded down (f32/f64 lanes)."), + doc("ceil", "ceil() -> V", "Every lane rounded up (f32/f64 lanes)."), + doc("round", "round() -> V", "Every lane rounded to the nearest whole number, halves away from 0 (f32/f64 lanes)."), + doc("abs", "abs() -> V", "Every lane without its minus sign."), + doc("fma", "fma(V b, V c) -> V", "v * b + c, as one instruction where the processor has one (f32/f64 lanes)."), + doc("store", "store([]T list, int i)", "Writes the lanes into list[i], list[i+1], ... (or through a ptr[T]).") + ] +} + fn keywords() -> []str { return ["fn", "return", "if", "else", "while", "for", "in", "break", "continue", "true", "false", "null", "import", "link", "struct", "enum", "match", "export", "defer", "or", "try", "self", "const"] } fn typeNames() -> []str { - return ["int", "float", "bool", "char", "str", "ptr", "map", "i8", "i16", "i32", "i64", "u8", "u16", "u32", "u64", "f32", "f64"] + return ["int", "float", "bool", "char", "str", "ptr", "map", "i8", "i16", "i32", "i64", "u8", "u16", "u32", "u64", "f32", "f64", + "f32x4", "f32x8", "f32x16", "f64x2", "f64x4", "f64x8", "i8x16", "i8x32", "i8x64", "u8x16", "u8x32", "u8x64", + "i16x8", "i16x16", "i16x32", "i32x4", "i32x8", "i32x16", "u32x4", "u32x8", "u32x16", "i64x2", "i64x4", "i64x8"] } // the entry called `name`, or an empty Doc diff --git a/src/embedded.fch b/src/embedded.fch index cfef97d..68230e4 100644 --- a/src/embedded.fch +++ b/src/embedded.fch @@ -20,7 +20,7 @@ fn names() -> []str { fn texts() -> []str { []str list - list.push("// The Finch runtime: string and array helpers, input, files and runtime errors.\n// It is embedded in the compiler, compiled once into a cache folder, and linked\n// statically into every Finch program. Everything here is plain C with no state.\n//\n// Layout shared with the compiler (src/codegen.cpp):\n// str = { char *ptr; int64 len; int64 cap } ptr is always NUL-terminated\n// cap > 0 : heap memory owned by the value (cap bytes allocated)\n// cap == 0 : a string literal, valid forever, never written to\n// cap == -1: borrowed from C, must be copied before it is kept\n// []T = { T *ptr; int64 len; int64 cap } owns ptr when cap > 0\n// map[K]V: see \"maps\" below\n\n#include \n#include \n#include \n#include \n#include \n#include \n#include \n#include \n#include \n#ifdef _WIN32\n#define WIN32_LEAN_AND_MEAN\n#define NOMINMAX\n#include \n#include \n#include \n#else\n#include \n#include \n#include \n#include \n#include \n#endif\n\ntypedef struct {\n char *ptr;\n int64_t len;\n int64_t cap;\n} FStr;\n\ntypedef struct {\n void *ptr;\n int64_t len;\n int64_t cap;\n} FArr;\n\nstatic char empty[1] = \"\";\n\n// ---------- runtime errors ----------\n\n// A program that loads Finch libraries (finch build --shared) can take runtime errors over\n// instead of the process exiting: finch_module_on_fatal(handler) in each library. The handler\n// gets the message and must not return (longjmp out, or exit itself).\nstatic void (*fatal_hook)(const char *msg);\nvoid finch_on_fatal(void (*hook)(const char *msg)) { fatal_hook = hook; }\n\n// The chain of Finch calls, kept by the program itself (see @finch_trace in the compiler's output):\n// each frame is { the caller's frame, the function's name, its file, the line it is at }.\ntypedef struct FFrame {\n struct FFrame *prev;\n const char *name;\n const char *file;\n int64_t line;\n} FFrame;\nstatic void *(*trace_top)(void);\nstatic void (*trace_set)(void *);\nvoid finch_trace_init(void *(*top)(void), void (*set)(void *)) {\n trace_top = top;\n trace_set = set;\n}\n\nstatic void print_trace(void) {\n if (!trace_top) return;\n FFrame *f = trace_top();\n for (int n = 0; f; n++, f = f->prev) {\n if (n == 30) {\n fprintf(stderr, \" ...\\n\");\n break;\n }\n fprintf(stderr, \" in %s (%s:%lld)\\n\", f->name, f->file, (long long)f->line);\n }\n}\n\nstatic __attribute__((noreturn)) void fatal(const char *msg) {\n if (fatal_hook) fatal_hook(msg);\n fflush(stdout);\n fprintf(stderr, \"%s\\n\", msg);\n print_trace();\n exit(1);\n}\n\n__attribute__((noreturn, cold)) void finch_panic(const char *file, int64_t line, const char *msg) {\n char full[1024];\n snprintf(full, sizeof full, \"%s:%lld: runtime error: %s\", file, (long long)line, msg);\n fflush(stdout);\n fatal(full);\n}\n\n__attribute__((noreturn, cold)) void finch_panic_index(const char *file, int64_t line, int64_t i, int64_t len) {\n char msg[128];\n snprintf(msg, sizeof msg, \"index %lld is out of range (the length is %lld)\", (long long)i, (long long)len);\n finch_panic(file, line, msg);\n}\n\nstatic void *must(void *p) {\n if (!p) {\n fflush(stdout);\n fatal(\"runtime error: out of memory\");\n }\n return p;\n}\n\nvoid *finch_alloc(int64_t n) { return must(malloc(n ? (size_t)n : 1)); }\nvoid *finch_zalloc(int64_t n) { return must(calloc(1, n ? (size_t)n : 1)); }\n\n// ---------- assert and finch test ----------\n\n__attribute__((noreturn, cold)) void finch_assert_failed(const char *file, int64_t line, const FStr *why) {\n char msg[600];\n if (why->len) snprintf(msg, sizeof msg, \"assert failed: %.*s\", (int)(why->len > 500 ? 500 : why->len), why->ptr);\n else snprintf(msg, sizeof msg, \"assert failed\");\n finch_panic(file, line, msg);\n}\n\n// Each test runs on its own: a runtime error (a failed assert too) ends that test, and the next one runs.\nstatic jmp_buf test_jump;\nstatic char test_message[1100];\nstatic int64_t tests_passed, tests_failed;\n\nstatic void test_fatal(const char *msg) {\n snprintf(test_message, sizeof test_message, \"%s\", msg);\n longjmp(test_jump, 1);\n}\n\nvoid finch_test_run(const char *name, void (*fn)(void)) {\n void (*saved)(const char *) = fatal_hook;\n void *chain = trace_top ? trace_top() : NULL;\n test_message[0] = 0;\n fatal_hook = test_fatal;\n int failed = setjmp(test_jump);\n if (!failed) fn();\n fatal_hook = saved;\n if (trace_set) trace_set(chain); // a test that stopped early left its calls on the chain\n fflush(stdout);\n if (failed) {\n tests_failed++;\n printf(\"FAIL %s\\n %s\\n\", name, test_message);\n } else {\n tests_passed++;\n printf(\"ok %s\\n\", name);\n }\n fflush(stdout);\n}\n\nint finch_test_summary(void) {\n printf(\"\\n%lld passed, %lld failed\\n\", (long long)tests_passed, (long long)tests_failed);\n return tests_failed ? 1 : 0;\n}\n\n// ---------- strings ----------\n\nstatic void str_heap(FStr *out, const char *p, int64_t len) {\n char *m = finch_alloc(len + 1);\n if (len) memcpy(m, p, (size_t)len);\n m[len] = 0;\n out->ptr = m;\n out->len = len;\n out->cap = len + 1;\n}\n\nstatic void str_static(FStr *out, const char *lit) {\n out->ptr = (char *)lit;\n out->len = (int64_t)strlen(lit);\n out->cap = 0;\n}\n\n// A copy that can be kept: literals are shared, everything else is duplicated.\nvoid finch_str_copy(FStr *out, const FStr *s) {\n if (s->cap == 0) *out = *s;\n else str_heap(out, s->ptr, s->len);\n}\n\n// Make `s` writable (before s[i] = c).\nvoid finch_str_own(FStr *s) {\n if (s->cap > 0) return;\n FStr t;\n str_heap(&t, s->ptr, s->len);\n *s = t;\n}\n\nvoid finch_str_drop(FStr *s) {\n if (s->cap > 0) free(s->ptr);\n}\n\nvoid finch_str_from_c(FStr *out, const char *p) {\n if (!p) {\n str_static(out, empty);\n return;\n }\n out->ptr = (char *)p;\n out->len = (int64_t)strlen(p);\n out->cap = -1;\n}\n\nvoid finch_str_concat(FStr *out, const FStr *a, const FStr *b) {\n int64_t len = a->len + b->len;\n char *m = finch_alloc(len + 1);\n memcpy(m, a->ptr, (size_t)a->len);\n memcpy(m + a->len, b->ptr, (size_t)b->len);\n m[len] = 0;\n out->ptr = m;\n out->len = len;\n out->cap = len + 1;\n}\n\nint finch_str_eq(const FStr *a, const FStr *b) {\n return a->len == b->len && memcmp(a->ptr, b->ptr, (size_t)a->len) == 0;\n}\n\nint64_t finch_str_cmp(const FStr *a, const FStr *b) {\n int64_t n = a->len < b->len ? a->len : b->len;\n int c = memcmp(a->ptr, b->ptr, (size_t)n);\n if (c) return c < 0 ? -1 : 1;\n return a->len < b->len ? -1 : a->len > b->len;\n}\n\nvoid finch_str_from_int(FStr *out, int64_t v) {\n char buf[32];\n int n = snprintf(buf, sizeof buf, \"%lld\", (long long)v);\n str_heap(out, buf, n);\n}\n\nvoid finch_str_from_uint(FStr *out, uint64_t v) {\n char buf[32];\n int n = snprintf(buf, sizeof buf, \"%llu\", (unsigned long long)v);\n str_heap(out, buf, n);\n}\n\n// The shortest text that reads back as the same number (as an f32 when `single`):\n// 0.1 -> \"0.1\", 1.0/3 -> \"0.3333333333333333\", 1e8 -> \"100000000\", 1e20 -> \"1e+20\".\nstatic int fmt_float(char *buf, size_t size, double v, int single) {\n if (isnan(v)) return snprintf(buf, size, \"nan\");\n if (isinf(v)) return snprintf(buf, size, v < 0 ? \"-inf\" : \"inf\");\n int p = 1;\n for (; p < 17; p++) {\n snprintf(buf, size, \"%.*e\", p - 1, v);\n double back = strtod(buf, NULL);\n if (single ? (float)back == (float)v : back == v) break;\n }\n int exp = atoi(strchr(buf, 'e') + 1);\n if (exp < -5 || exp >= 17) return snprintf(buf, size, \"%.*g\", p, v);\n int decimals = p - 1 - exp;\n return snprintf(buf, size, \"%.*f\", decimals > 0 ? decimals : 0, v);\n}\n\nvoid finch_str_from_float(FStr *out, double v) {\n char buf[64];\n int n = fmt_float(buf, sizeof buf, v, 0);\n str_heap(out, buf, n);\n}\n\nvoid finch_str_from_f32(FStr *out, double v) {\n char buf[64];\n int n = fmt_float(buf, sizeof buf, v, 1);\n str_heap(out, buf, n);\n}\n\n// str(x, 2): a fixed number of digits after the point\nvoid finch_str_fixed(FStr *out, double v, int64_t digits) {\n char buf[400];\n if (digits < 0) digits = 0;\n if (digits > 60) digits = 60;\n int n = snprintf(buf, sizeof buf, \"%.*f\", (int)digits, v);\n str_heap(out, buf, n);\n}\n\nvoid finch_print_float(double v, int32_t single) {\n char buf[64];\n fmt_float(buf, sizeof buf, v, single);\n fputs(buf, stdout);\n}\n\nvoid finch_str_from_char(FStr *out, char c) { str_heap(out, &c, 1); }\n\nvoid finch_str_from_bool(FStr *out, int b) { str_static(out, b ? \"true\" : \"false\"); }\n\nstatic void number_error(char *msg, size_t size, const FStr *s, const char *kind) {\n snprintf(msg, size, \"can't turn \\\"%.*s\\\" into %s\", (int)(s->len > 100 ? 100 : s->len), s->ptr ? s->ptr : \"\", kind);\n}\n\nstatic int parse_int(const FStr *s, int64_t *out) {\n const char *p = s->ptr ? s->ptr : \"\";\n while (isspace((unsigned char)*p)) p++;\n char *end;\n errno = 0;\n long long v = strtoll(p, &end, 10);\n while (isspace((unsigned char)*end)) end++;\n *out = v;\n return !(end == p || *end || errno == ERANGE);\n}\n\nstatic int parse_float(const FStr *s, double *out) {\n const char *p = s->ptr ? s->ptr : \"\";\n while (isspace((unsigned char)*p)) p++;\n char *end;\n double v = strtod(p, &end);\n while (isspace((unsigned char)*end)) end++;\n *out = v;\n return !(end == p || *end);\n}\n\nint64_t finch_str_to_int(const FStr *s, const char *file, int64_t line) {\n int64_t v;\n if (!parse_int(s, &v)) {\n char msg[200];\n number_error(msg, sizeof msg, s, \"int\");\n finch_panic(file, line, msg);\n }\n return v;\n}\n\ndouble finch_str_to_float(const FStr *s, const char *file, int64_t line) {\n double v;\n if (!parse_float(s, &v)) {\n char msg[200];\n number_error(msg, sizeof msg, s, \"float\");\n finch_panic(file, line, msg);\n }\n return v;\n}\n\n// ---------- errors as values: int(s) or 0, try read_file(p) ... ----------\n// Each gives back 1 on success; on failure 0, with the message in *err.\n\nstatic void set_error(FStr *err, const char *msg) { str_heap(err, msg, (int64_t)strlen(msg)); }\n\nint finch_str_try_int(const FStr *s, int64_t *out, FStr *err) {\n if (parse_int(s, out)) return 1;\n char msg[200];\n number_error(msg, sizeof msg, s, \"int\");\n set_error(err, msg);\n *out = 0;\n return 0;\n}\n\nint finch_str_try_float(const FStr *s, double *out, FStr *err) {\n if (parse_float(s, out)) return 1;\n char msg[200];\n number_error(msg, sizeof msg, s, \"float\");\n set_error(err, msg);\n *out = 0;\n return 0;\n}\n\nstatic void file_error(FStr *err, const char *what, const FStr *path) {\n char msg[300];\n snprintf(msg, sizeof msg, \"can't %s the file \\\"%.*s\\\": %s\", what, (int)(path->len > 200 ? 200 : path->len),\n path->ptr ? path->ptr : \"\", strerror(errno));\n set_error(err, msg);\n}\n\n// when `fn main() -> !` fails\nvoid finch_report_error(const FStr *msg) {\n fflush(stdout);\n fprintf(stderr, \"error: %.*s\\n\", (int)msg->len, msg->ptr ? msg->ptr : \"\");\n finch_str_drop((FStr *)msg);\n}\n\nvoid finch_str_sub(FStr *out, const FStr *s, int64_t start, int64_t end, const char *file, int64_t line) {\n if (start < 0 || end > s->len || start > end) {\n char msg[160];\n snprintf(msg, sizeof msg, \"sub(%lld, %lld) is out of range (the length is %lld)\", (long long)start,\n (long long)end, (long long)s->len);\n finch_panic(file, line, msg);\n }\n str_heap(out, s->ptr + start, end - start);\n}\n\nint64_t finch_str_find(const FStr *s, const FStr *t) {\n if (t->len == 0) return 0;\n for (int64_t i = 0; i + t->len <= s->len; i++)\n if (s->ptr[i] == t->ptr[0] && memcmp(s->ptr + i, t->ptr, (size_t)t->len) == 0) return i;\n return -1;\n}\n\nint finch_str_starts(const FStr *s, const FStr *t) {\n return t->len <= s->len && memcmp(s->ptr, t->ptr, (size_t)t->len) == 0;\n}\n\nint finch_str_ends(const FStr *s, const FStr *t) {\n return t->len <= s->len && memcmp(s->ptr + s->len - t->len, t->ptr, (size_t)t->len) == 0;\n}\n\nvoid finch_str_trim(FStr *out, const FStr *s) {\n int64_t a = 0, b = s->len;\n while (a < b && isspace((unsigned char)s->ptr[a])) a++;\n while (b > a && isspace((unsigned char)s->ptr[b - 1])) b--;\n str_heap(out, s->ptr + a, b - a);\n}\n\nvoid finch_str_upper(FStr *out, const FStr *s) {\n str_heap(out, s->ptr, s->len);\n for (int64_t i = 0; i < out->len; i++) out->ptr[i] = (char)toupper((unsigned char)out->ptr[i]);\n}\n\nvoid finch_str_lower(FStr *out, const FStr *s) {\n str_heap(out, s->ptr, s->len);\n for (int64_t i = 0; i < out->len; i++) out->ptr[i] = (char)tolower((unsigned char)out->ptr[i]);\n}\n\nvoid finch_str_replace(FStr *out, const FStr *s, const FStr *from, const FStr *to) {\n if (from->len == 0) {\n str_heap(out, s->ptr, s->len);\n return;\n }\n int64_t count = 0;\n for (int64_t i = 0; i + from->len <= s->len;) {\n if (memcmp(s->ptr + i, from->ptr, (size_t)from->len) == 0) {\n count++;\n i += from->len;\n } else {\n i++;\n }\n }\n int64_t len = s->len + count * (to->len - from->len);\n char *m = finch_alloc(len + 1), *w = m;\n for (int64_t i = 0; i < s->len;) {\n if (i + from->len <= s->len && memcmp(s->ptr + i, from->ptr, (size_t)from->len) == 0) {\n memcpy(w, to->ptr, (size_t)to->len);\n w += to->len;\n i += from->len;\n } else {\n *w++ = s->ptr[i++];\n }\n }\n *w = 0;\n out->ptr = m;\n out->len = len;\n out->cap = len + 1;\n}\n\nstatic void arr_push_str(FArr *a, const char *p, int64_t len);\n\n// \"a,b,,c\".split(\",\") -> [\"a\", \"b\", \"\", \"c\"]; split(\"\") gives single characters.\nvoid finch_str_split(FArr *out, const FStr *s, const FStr *sep) {\n out->ptr = NULL;\n out->len = out->cap = 0;\n if (sep->len == 0) {\n for (int64_t i = 0; i < s->len; i++) arr_push_str(out, s->ptr + i, 1);\n return;\n }\n int64_t start = 0;\n for (int64_t i = 0; i + sep->len <= s->len;) {\n if (memcmp(s->ptr + i, sep->ptr, (size_t)sep->len) == 0) {\n arr_push_str(out, s->ptr + start, i - start);\n i += sep->len;\n start = i;\n } else {\n i++;\n }\n }\n arr_push_str(out, s->ptr + start, s->len - start);\n}\n\nvoid finch_str_join(FStr *out, const FArr *parts, const FStr *sep) {\n const FStr *p = parts->ptr;\n int64_t len = 0;\n for (int64_t i = 0; i < parts->len; i++) len += p[i].len + (i ? sep->len : 0);\n char *m = finch_alloc(len + 1), *w = m;\n for (int64_t i = 0; i < parts->len; i++) {\n if (i) {\n memcpy(w, sep->ptr, (size_t)sep->len);\n w += sep->len;\n }\n memcpy(w, p[i].ptr, (size_t)p[i].len);\n w += p[i].len;\n }\n *w = 0;\n out->ptr = m;\n out->len = len;\n out->cap = len + 1;\n}\n\nvoid finch_str_repeat(FStr *out, const FStr *s, int64_t n) {\n if (n < 0) n = 0;\n int64_t len = s->len * n;\n char *m = finch_alloc(len + 1);\n for (int64_t i = 0; i < n; i++) memcpy(m + i * s->len, s->ptr, (size_t)s->len);\n m[len] = 0;\n out->ptr = m;\n out->len = len;\n out->cap = len + 1;\n}\n\nvoid finch_str_from_bytes(FStr *out, const FArr *bytes) { str_heap(out, bytes->ptr, bytes->len); }\n\n// ---------- building text: print(x) and str(x) of arrays, maps and structs ----------\n// A buffer starts as an empty str and grows on the heap.\n\nvoid finch_buf_add(FStr *b, const char *p, int64_t n) {\n if (b->len + n + 1 > b->cap) {\n int64_t cap = b->cap > 0 ? b->cap * 2 : 32;\n while (cap < b->len + n + 1) cap *= 2;\n b->ptr = must(realloc(b->cap > 0 ? b->ptr : NULL, (size_t)cap));\n b->cap = cap;\n }\n if (n) memcpy(b->ptr + b->len, p, (size_t)n);\n b->len += n;\n b->ptr[b->len] = 0;\n}\n\nstatic void buf_fmt(FStr *b, const char *fmt, ...) __attribute__((format(printf, 2, 3)));\nstatic void buf_fmt(FStr *b, const char *fmt, ...) {\n char t[64];\n va_list ap;\n va_start(ap, fmt);\n int n = vsnprintf(t, sizeof t, fmt, ap);\n va_end(ap);\n finch_buf_add(b, t, n);\n}\n\nvoid finch_buf_int(FStr *b, int64_t v) { buf_fmt(b, \"%lld\", (long long)v); }\nvoid finch_buf_uint(FStr *b, uint64_t v) { buf_fmt(b, \"%llu\", (unsigned long long)v); }\nvoid finch_buf_float(FStr *b, double v) {\n char t[64];\n finch_buf_add(b, t, fmt_float(t, sizeof t, v, 0));\n}\nvoid finch_buf_f32(FStr *b, double v) {\n char t[64];\n finch_buf_add(b, t, fmt_float(t, sizeof t, v, 1));\n}\nvoid finch_buf_bool(FStr *b, int v) { finch_buf_add(b, v ? \"true\" : \"false\", v ? 4 : 5); }\nvoid finch_buf_ptr(FStr *b, const void *p) {\n if (p) buf_fmt(b, \"0x%llx\", (unsigned long long)(uintptr_t)p);\n else finch_buf_add(b, \"null\", 4);\n}\n\nvoid finch_buf_char(FStr *b, char c, int quoted) {\n if (quoted) finch_buf_add(b, \"'\", 1);\n finch_buf_add(b, &c, 1);\n if (quoted) finch_buf_add(b, \"'\", 1);\n}\n\nvoid finch_buf_str(FStr *b, const FStr *s, int quoted) {\n if (quoted) finch_buf_add(b, \"\\\"\", 1);\n finch_buf_add(b, s->ptr, s->len);\n if (quoted) finch_buf_add(b, \"\\\"\", 1);\n}\n\n// print the buffer and free it\nvoid finch_buf_print(FStr *b) {\n if (b->len) fwrite(b->ptr, 1, (size_t)b->len, stdout);\n if (b->cap > 0) free(b->ptr);\n}\n\n// ---------- arrays ----------\n\nvoid finch_arr_reserve(FArr *a, int64_t need, int64_t es) {\n if (need <= a->cap) return;\n int64_t cap = a->cap * 2;\n if (cap < 4) cap = 4;\n if (cap < need) cap = need;\n a->ptr = must(realloc(a->cap > 0 ? a->ptr : NULL, (size_t)(cap * es)));\n a->cap = cap;\n}\n\nvoid finch_arr_make(FArr *out, int64_t len, int64_t es) {\n if (len < 0) len = 0;\n out->ptr = len ? finch_zalloc(len * es) : NULL;\n out->len = len;\n out->cap = len;\n}\n\n// grow or shrink to `len`; new elements are zero\nvoid finch_arr_resize(FArr *a, int64_t len, int64_t es) {\n if (len < 0) len = 0;\n finch_arr_reserve(a, len, es);\n if (len > a->len) memset((char *)a->ptr + a->len * es, 0, (size_t)((len - a->len) * es));\n a->len = len;\n}\n\n// a byte-for-byte copy of the elements (the compiler deep-copies owning elements afterwards)\nvoid finch_arr_clone_raw(FArr *out, const FArr *a, int64_t es) {\n out->len = a->len;\n out->cap = a->len;\n out->ptr = a->len ? finch_alloc(a->len * es) : NULL;\n if (a->len) memcpy(out->ptr, a->ptr, (size_t)(a->len * es));\n}\n\nvoid finch_arr_free(FArr *a) {\n if (a->cap > 0) free(a->ptr);\n}\n\n// open a hole at i (0 <= i <= len), len grows by one\nvoid finch_arr_insert_gap(FArr *a, int64_t i, int64_t es, const char *file, int64_t line) {\n if (i < 0 || i > a->len) finch_panic_index(file, line, i, a->len);\n finch_arr_reserve(a, a->len + 1, es);\n char *p = a->ptr;\n memmove(p + (i + 1) * es, p + i * es, (size_t)((a->len - i) * es));\n a->len++;\n}\n\n// close the hole at i (the element was already moved out), len shrinks by one\nvoid finch_arr_remove_gap(FArr *a, int64_t i, int64_t es) {\n char *p = a->ptr;\n memmove(p + i * es, p + (i + 1) * es, (size_t)((a->len - i - 1) * es));\n a->len--;\n}\n\nstatic void arr_push_str(FArr *a, const char *p, int64_t len) {\n finch_arr_reserve(a, a->len + 1, (int64_t)sizeof(FStr));\n str_heap((FStr *)a->ptr + a->len, p, len);\n a->len++;\n}\n\nvoid finch_bytes_from_str(FArr *out, const FStr *s) {\n out->len = out->cap = s->len;\n out->ptr = s->len ? finch_alloc(s->len) : NULL;\n if (s->len) memcpy(out->ptr, s->ptr, (size_t)s->len);\n}\n\n// ---------- program arguments, input and files ----------\n\nvoid finch_args(FArr *out, int argc, char **argv) {\n out->len = out->cap = argc;\n out->ptr = finch_alloc((int64_t)sizeof(FStr) * (argc ? argc : 1));\n for (int i = 0; i < argc; i++) str_heap((FStr *)out->ptr + i, argv[i], (int64_t)strlen(argv[i]));\n}\n\n// after main: the arguments go\nvoid finch_args_free(FArr *a) {\n for (int64_t i = 0; i < a->len; i++) finch_str_drop((FStr *)a->ptr + i);\n finch_arr_free(a);\n}\n\n// input(\"Name? \") prints the prompt and reads one line, without the newline.\nvoid finch_input(FStr *out, const FStr *prompt) {\n if (prompt->len) fwrite(prompt->ptr, 1, (size_t)prompt->len, stdout);\n fflush(stdout);\n // read one line of any length (getline isn't available everywhere)\n size_t size = 128, n = 0;\n char *line = finch_alloc((int64_t)size);\n int c, got = 0;\n while ((c = fgetc(stdin)) != EOF) {\n got = 1;\n if (c == '\\n') break;\n if (n + 1 >= size) line = must(realloc(line, size *= 2));\n line[n++] = (char)c;\n }\n if (!got) {\n free(line);\n str_static(out, empty);\n return;\n }\n while (n > 0 && line[n - 1] == '\\r') n--;\n line[n] = 0;\n out->ptr = line;\n out->len = (int64_t)n;\n out->cap = (int64_t)size;\n}\n\nint finch_file_exists(const FStr *path) {\n FILE *f = fopen(path->ptr, \"rb\");\n if (f) fclose(f);\n return f != NULL;\n}\n\nint finch_try_read_file(FStr *out, const FStr *path, FStr *err) {\n FILE *f = fopen(path->ptr ? path->ptr : \"\", \"rb\");\n if (!f) {\n file_error(err, \"read\", path);\n return 0;\n }\n fseek(f, 0, SEEK_END);\n long size = ftell(f);\n fseek(f, 0, SEEK_SET);\n char *m = finch_alloc(size + 1);\n size_t got = fread(m, 1, (size_t)size, f);\n fclose(f);\n m[got] = 0;\n out->ptr = m;\n out->len = (int64_t)got;\n out->cap = size + 1;\n return 1;\n}\n\nvoid finch_read_file(FStr *out, const FStr *path, const char *file, int64_t line) {\n FILE *f = fopen(path->ptr ? path->ptr : \"\", \"rb\");\n if (!f) {\n char msg[300];\n snprintf(msg, sizeof msg, \"can't read the file \\\"%.*s\\\": %s\", (int)(path->len > 200 ? 200 : path->len),\n path->ptr, strerror(errno));\n finch_panic(file, line, msg);\n }\n fseek(f, 0, SEEK_END);\n long size = ftell(f);\n fseek(f, 0, SEEK_SET);\n char *m = finch_alloc(size + 1);\n size_t got = fread(m, 1, (size_t)size, f);\n fclose(f);\n m[got] = 0;\n out->ptr = m;\n out->len = (int64_t)got;\n out->cap = size + 1;\n}\n\n// shell(\"ls -l\"): run a command, give back its exit code\nint64_t finch_shell(const FStr *cmd) {\n fflush(stdout);\n int status = system(cmd->ptr ? cmd->ptr : \"\");\n#ifdef _WIN32\n return status; // Windows gives the exit code directly\n#else\n if (status == -1) return -1;\n if (WIFEXITED(status)) return WEXITSTATUS(status);\n return 128 + (WIFSIGNALED(status) ? WTERMSIG(status) : 0);\n#endif\n}\n\nint finch_delete_file(const FStr *path) { return remove(path->ptr ? path->ptr : \"\") == 0; }\n\nint finch_try_delete_file(const FStr *path, FStr *err) {\n if (remove(path->ptr ? path->ptr : \"\") == 0) return 1;\n file_error(err, \"delete\", path);\n return 0;\n}\n\n// stdin/stdout/stderr are macros on some systems (Windows), so Finch asks for them here\nvoid *finch_std_stream(int i) { return i == 0 ? (void *)stdin : i == 1 ? (void *)stdout : (void *)stderr; }\n\nint finch_write_file(const FStr *path, const FStr *text) {\n FILE *f = fopen(path->ptr ? path->ptr : \"\", \"wb\");\n if (!f) return 0;\n size_t put = text->len ? fwrite(text->ptr, 1, (size_t)text->len, f) : 0;\n return fclose(f) == 0 && put == (size_t)text->len;\n}\n\nint finch_try_write_file(const FStr *path, const FStr *text, FStr *err) {\n errno = 0;\n if (finch_write_file(path, text)) return 1;\n if (!errno) errno = EIO;\n file_error(err, \"write\", path);\n return 0;\n}\n\n// ---------- folders (std/fs.fch, through std/finch_fs.h) ----------\n\nint finch_fs_is_dir(const char *path) {\n#ifdef _WIN32\n struct _finddata_t d;\n intptr_t h = _findfirst(path, &d);\n if (h == -1) return 0;\n _findclose(h);\n return (d.attrib & _A_SUBDIR) != 0;\n#else\n struct stat st;\n return stat(path, &st) == 0 && S_ISDIR(st.st_mode);\n#endif\n}\n\nint finch_fs_exists(const char *path) {\n#ifdef _WIN32\n return _access(path, 0) == 0;\n#else\n struct stat st;\n return stat(path, &st) == 0;\n#endif\n}\n\nstatic int make_one_dir(const char *p) {\n#ifdef _WIN32\n return _mkdir(p) == 0 || errno == EEXIST;\n#else\n return mkdir(p, 0777) == 0 || errno == EEXIST;\n#endif\n}\n\n// mkdir -p: the parents too; 1 if the folder is there afterwards\nint finch_fs_mkdir(const char *path) {\n size_t n = strlen(path);\n char *p = finch_alloc((int64_t)n + 1);\n memcpy(p, path, n + 1);\n for (size_t i = 1; i < n; i++) {\n if (p[i] == '/' || p[i] == '\\\\') {\n if (p[i - 1] == ':') continue; /* C: is a drive */\n char c = p[i];\n p[i] = 0;\n make_one_dir(p);\n p[i] = c;\n }\n }\n int ok = make_one_dir(p) && finch_fs_is_dir(p);\n free(p);\n return ok;\n}\n\nint finch_fs_remove_dir(const char *path) {\n#ifdef _WIN32\n return _rmdir(path) == 0;\n#else\n return rmdir(path) == 0;\n#endif\n}\n\n// the names in a folder, one per call (\"\" at the end), without . and ..\ntypedef struct {\n#ifdef _WIN32\n intptr_t h;\n struct _finddata_t d;\n int first;\n#else\n DIR *d;\n#endif\n} FsList;\n\nvoid *finch_fs_list_open(const char *path) {\n FsList *l = finch_zalloc((int64_t)sizeof(FsList));\n#ifdef _WIN32\n size_t n = strlen(path);\n char *pat = finch_alloc((int64_t)n + 3);\n memcpy(pat, path, n);\n memcpy(pat + n, \"\\\\*\", 3);\n l->h = _findfirst(pat, &l->d);\n free(pat);\n if (l->h == -1) {\n free(l);\n return NULL;\n }\n l->first = 1;\n#else\n l->d = opendir(path);\n if (!l->d) {\n free(l);\n return NULL;\n }\n#endif\n return l;\n}\n\nconst char *finch_fs_list_next(void *handle) {\n FsList *l = handle;\n for (;;) {\n#ifdef _WIN32\n if (!l->first && _findnext(l->h, &l->d) != 0) return \"\";\n l->first = 0;\n const char *name = l->d.name;\n#else\n struct dirent *e = readdir(l->d);\n if (!e) return \"\";\n const char *name = e->d_name;\n#endif\n if (strcmp(name, \".\") != 0 && strcmp(name, \"..\") != 0) return name;\n }\n}\n\nvoid finch_fs_list_close(void *handle) {\n FsList *l = handle;\n#ifdef _WIN32\n _findclose(l->h);\n#else\n closedir(l->d);\n#endif\n free(l);\n}\n\n// where a program keeps its own files for this user (saves, settings); \"\" if unknown\n// Linux: $XDG_DATA_HOME or ~/.local/share macOS: ~/Library/Application Support Windows: %APPDATA%\nconst char *finch_fs_data_home(void) {\n static char buf[4096];\n const char *v;\n#if defined(_WIN32)\n v = getenv(\"APPDATA\");\n snprintf(buf, sizeof buf, \"%s\", v ? v : \"\");\n#elif defined(__APPLE__)\n v = getenv(\"HOME\");\n if (v) snprintf(buf, sizeof buf, \"%s/Library/Application Support\", v);\n else buf[0] = 0;\n#else\n v = getenv(\"XDG_DATA_HOME\");\n if (v && *v) snprintf(buf, sizeof buf, \"%s\", v);\n else if ((v = getenv(\"HOME\"))) snprintf(buf, sizeof buf, \"%s/.local/share\", v);\n else buf[0] = 0;\n#endif\n return buf;\n}\n\n// ---------- threads (std/thread.fch, through std/finch_thread.h) ----------\n\ntypedef struct {\n void (*fn)(void *);\n void *arg;\n} ThreadStart;\n\n#ifdef _WIN32\nstatic DWORD WINAPI thread_main(LPVOID p) {\n ThreadStart st = *(ThreadStart *)p;\n free(p);\n st.fn(st.arg);\n return 0;\n}\n#else\nstatic void *thread_main(void *p) {\n ThreadStart st = *(ThreadStart *)p;\n free(p);\n st.fn(st.arg);\n return NULL;\n}\n#endif\n\n// run fn(arg) on a new thread; NULL if it couldn't start\nvoid *finch_thread_spawn(void (*fn)(void *), void *arg) {\n ThreadStart *st = finch_alloc((int64_t)sizeof(ThreadStart));\n st->fn = fn;\n st->arg = arg;\n#ifdef _WIN32\n HANDLE h = CreateThread(NULL, 0, thread_main, st, 0, NULL);\n if (!h) {\n free(st);\n return NULL;\n }\n return h;\n#else\n pthread_t *t = finch_alloc((int64_t)sizeof(pthread_t));\n if (pthread_create(t, NULL, thread_main, st) != 0) {\n free(st);\n free(t);\n return NULL;\n }\n return t;\n#endif\n}\n\n// wait for the thread to end; 1 if that worked\nint finch_thread_join(void *t) {\n if (!t) return 0;\n#ifdef _WIN32\n int ok = WaitForSingleObject((HANDLE)t, INFINITE) == WAIT_OBJECT_0;\n CloseHandle((HANDLE)t);\n return ok;\n#else\n int ok = pthread_join(*(pthread_t *)t, NULL) == 0;\n free(t);\n return ok;\n#endif\n}\n\nvoid *finch_mutex_new(void) {\n#ifdef _WIN32\n CRITICAL_SECTION *m = finch_alloc((int64_t)sizeof(CRITICAL_SECTION));\n InitializeCriticalSection(m);\n#else\n pthread_mutex_t *m = finch_alloc((int64_t)sizeof(pthread_mutex_t));\n pthread_mutex_init(m, NULL);\n#endif\n return m;\n}\n\nvoid finch_mutex_lock(void *m) {\n#ifdef _WIN32\n EnterCriticalSection((CRITICAL_SECTION *)m);\n#else\n pthread_mutex_lock((pthread_mutex_t *)m);\n#endif\n}\n\nvoid finch_mutex_unlock(void *m) {\n#ifdef _WIN32\n LeaveCriticalSection((CRITICAL_SECTION *)m);\n#else\n pthread_mutex_unlock((pthread_mutex_t *)m);\n#endif\n}\n\nvoid finch_mutex_free(void *m) {\n if (!m) return;\n#ifdef _WIN32\n DeleteCriticalSection((CRITICAL_SECTION *)m);\n#else\n pthread_mutex_destroy((pthread_mutex_t *)m);\n#endif\n free(m);\n}\n\nint finch_cpu_count(void) {\n#ifdef _WIN32\n SYSTEM_INFO si;\n GetSystemInfo(&si);\n return (int)si.dwNumberOfProcessors;\n#else\n long n = sysconf(_SC_NPROCESSORS_ONLN);\n return n > 0 ? (int)n : 1;\n#endif\n}\n\n// ---------- maps ----------\n// map[K]V = { entries; len; used; cap; index; icap; adds }\n// entries: `used` slots of es bytes, each { uint64 hash; K key; V value }, in insertion order.\n// A removed entry keeps its slot (hash 0) until the next resize packs the slots.\n// index: icap slots (a power of two, at least 2 * cap): -1 empty, -2 removed, else an entry number.\n// adds: how many keys were ever added; a `for` over the map stops with an error if it changes.\n// The compiler stores, copies and drops keys and values; the runtime only finds and places entries.\n// Keys are compared byte for byte (numbers, chars, bools) or as text (kstr).\n\ntypedef struct {\n char *entries;\n int64_t len, used, cap;\n int64_t *index;\n int64_t icap;\n int64_t adds;\n} FMap;\n\nstatic uint64_t map_hash(const void *key, int64_t ks, int kstr) {\n const unsigned char *p;\n size_t n;\n if (kstr) {\n const FStr *s = key;\n p = (const unsigned char *)s->ptr;\n n = (size_t)s->len;\n } else {\n p = key;\n n = (size_t)ks;\n }\n uint64_t h = 1469598103934665603ULL; // FNV-1a, then mixed so the low bits spread well\n for (size_t i = 0; i < n; i++) {\n h ^= p[i];\n h *= 1099511628211ULL;\n }\n h ^= h >> 31;\n h *= 0xbf58476d1ce4e5b9ULL;\n h ^= h >> 29;\n return h | 1; // 0 marks a removed entry\n}\n\nstatic int map_key_eq(const char *a, const void *b, int64_t ks, int kstr) {\n if (!kstr) return memcmp(a, b, (size_t)ks) == 0;\n const FStr *x = (const FStr *)a, *y = b;\n return x->len == y->len && (x->len == 0 || memcmp(x->ptr, y->ptr, (size_t)x->len) == 0);\n}\n\nstatic void map_reindex(FMap *m, int64_t es) {\n if (m->len != m->used) { // pack the slots of removed entries away\n int64_t j = 0;\n for (int64_t i = 0; i < m->used; i++) {\n char *e = m->entries + i * es;\n if (*(uint64_t *)e == 0) continue;\n if (i != j) memmove(m->entries + j * es, e, (size_t)es);\n j++;\n }\n m->used = j;\n }\n int64_t icap = 16;\n while (icap < m->cap * 2) icap *= 2;\n free(m->index);\n m->index = finch_alloc(icap * (int64_t)sizeof(int64_t));\n memset(m->index, 0xff, (size_t)icap * sizeof(int64_t));\n m->icap = icap;\n for (int64_t i = 0; i < m->used; i++) {\n uint64_t h = *(uint64_t *)(m->entries + i * es);\n int64_t s = (int64_t)(h & (uint64_t)(icap - 1));\n while (m->index[s] != -1) s = (s + 1) & (icap - 1);\n m->index[s] = i;\n }\n}\n\n// the entry number of `key`, or -1\nint64_t finch_map_find(const FMap *m, const void *key, int64_t es, int64_t ks, int32_t kstr) {\n if (m->len == 0) return -1;\n uint64_t h = map_hash(key, ks, kstr);\n for (int64_t s = (int64_t)(h & (uint64_t)(m->icap - 1));; s = (s + 1) & (m->icap - 1)) {\n int64_t i = m->index[s];\n if (i == -1) return -1;\n if (i < 0) continue;\n char *e = m->entries + i * es;\n if (*(uint64_t *)e == h && map_key_eq(e + 8, key, ks, kstr)) return i;\n }\n}\n\n// The entry for `key`. A new one is zeroed except its hash (*added = 1); the compiler then stores the key.\nint64_t finch_map_slot(FMap *m, const void *key, int64_t es, int64_t ks, int32_t kstr, int32_t *added) {\n int64_t i = finch_map_find(m, key, es, ks, kstr);\n *added = i < 0;\n if (i >= 0) return i;\n if (m->used == m->cap) {\n if (m->len * 4 < m->cap * 3) { // many removed slots: pack them instead of growing\n map_reindex(m, es);\n } else {\n m->cap = m->cap ? m->cap * 2 : 8;\n m->entries = must(realloc(m->entries, (size_t)(m->cap * es)));\n map_reindex(m, es);\n }\n }\n uint64_t h = map_hash(key, ks, kstr);\n i = m->used++;\n char *e = m->entries + i * es;\n memset(e, 0, (size_t)es);\n *(uint64_t *)e = h;\n int64_t s = (int64_t)(h & (uint64_t)(m->icap - 1));\n while (m->index[s] >= 0) s = (s + 1) & (m->icap - 1);\n m->index[s] = i;\n m->len++;\n m->adds++;\n return i;\n}\n\n// Forget entry i (its key and value were already dropped by the compiler).\nvoid finch_map_remove_at(FMap *m, int64_t i, int64_t es) {\n char *e = m->entries + i * es;\n uint64_t h = *(uint64_t *)e;\n int64_t s = (int64_t)(h & (uint64_t)(m->icap - 1));\n while (m->index[s] != i) s = (s + 1) & (m->icap - 1);\n m->index[s] = -2;\n *(uint64_t *)e = 0;\n if (--m->len == 0) {\n m->used = 0;\n memset(m->index, 0xff, (size_t)m->icap * sizeof(int64_t));\n }\n}\n\nvoid finch_map_clear(FMap *m) {\n m->len = m->used = 0;\n if (m->index) memset(m->index, 0xff, (size_t)m->icap * sizeof(int64_t));\n}\n\nvoid finch_map_free(FMap *m) {\n free(m->entries);\n free(m->index);\n}\n\n// A copy of the slots and the index; the compiler then copies the keys and values that own memory.\nvoid finch_map_clone_raw(FMap *out, const FMap *m, int64_t es) {\n *out = *m;\n if (!m->cap) return;\n out->entries = finch_alloc(m->cap * es);\n memcpy(out->entries, m->entries, (size_t)(m->used * es));\n out->index = finch_alloc(m->icap * (int64_t)sizeof(int64_t));\n memcpy(out->index, m->index, (size_t)m->icap * sizeof(int64_t));\n}\n\n__attribute__((noreturn, cold)) void finch_map_changed(const char *file, int64_t line) {\n finch_panic(file, line, \"a key was added to the map while looping over it (collect the new keys, and add them after the loop)\");\n}\n\n__attribute__((noreturn, cold)) void finch_map_missing(const char *file, int64_t line, const FStr *key) {\n char msg[200];\n snprintf(msg, sizeof msg, \"the key %.*s is not in the map (check with .has(key), or use .get(key, default))\",\n (int)(key->len > 100 ? 100 : key->len), key->ptr ? key->ptr : \"\");\n finch_panic(file, line, msg);\n}\n") + list.push("// The Finch runtime: string and array helpers, input, files and runtime errors.\n// It is embedded in the compiler, compiled once into a cache folder, and linked\n// statically into every Finch program. Everything here is plain C with no state.\n//\n// Layout shared with the compiler (src/gen.fch):\n// str = { char *ptr; int64 len; int64 cap } ptr is always NUL-terminated\n// cap > 0 : heap memory owned by the value (cap bytes allocated)\n// cap == 0 : a string literal, valid forever, never written to\n// cap == -1: borrowed from C, must be copied before it is kept\n// []T = { T *ptr; int64 len; int64 cap } owns ptr when cap > 0\n// map[K]V: see \"maps\" below\n\n#include \n#include \n#include \n#include \n#include \n#include \n#include \n#include \n#include \n#ifdef _WIN32\n#define WIN32_LEAN_AND_MEAN\n#define NOMINMAX\n#include \n#include \n#include \n#else\n#include \n#include \n#include \n#include \n#include \n#endif\n\ntypedef struct {\n char *ptr;\n int64_t len;\n int64_t cap;\n} FStr;\n\ntypedef struct {\n void *ptr;\n int64_t len;\n int64_t cap;\n} FArr;\n\nstatic char empty[1] = \"\";\n\n// ---------- runtime errors ----------\n\n// A program that loads Finch libraries (finch build --shared) can take runtime errors over\n// instead of the process exiting: finch_module_on_fatal(handler) in each library. The handler\n// gets the message and must not return (longjmp out, or exit itself).\nstatic void (*fatal_hook)(const char *msg);\nvoid finch_on_fatal(void (*hook)(const char *msg)) { fatal_hook = hook; }\n\n// The chain of Finch calls, kept by the program itself (see @finch_trace in the compiler's output):\n// each frame is { the caller's frame, the function's name, its file, the line it is at }.\ntypedef struct FFrame {\n struct FFrame *prev;\n const char *name;\n const char *file;\n int64_t line;\n} FFrame;\nstatic void *(*trace_top)(void);\nstatic void (*trace_set)(void *);\nvoid finch_trace_init(void *(*top)(void), void (*set)(void *)) {\n trace_top = top;\n trace_set = set;\n}\n\nstatic void print_trace(void) {\n if (!trace_top) return;\n FFrame *f = trace_top();\n for (int n = 0; f; n++, f = f->prev) {\n if (n == 30) {\n fprintf(stderr, \" ...\\n\");\n break;\n }\n fprintf(stderr, \" in %s (%s:%lld)\\n\", f->name, f->file, (long long)f->line);\n }\n}\n\nstatic __attribute__((noreturn)) void fatal(const char *msg) {\n if (fatal_hook) fatal_hook(msg);\n fflush(stdout);\n fprintf(stderr, \"%s\\n\", msg);\n print_trace();\n exit(1);\n}\n\n__attribute__((noreturn, cold)) void finch_panic(const char *file, int64_t line, const char *msg) {\n char full[1024];\n snprintf(full, sizeof full, \"%s:%lld: runtime error: %s\", file, (long long)line, msg);\n fflush(stdout);\n fatal(full);\n}\n\n__attribute__((noreturn, cold)) void finch_panic_index(const char *file, int64_t line, int64_t i, int64_t len) {\n char msg[128];\n snprintf(msg, sizeof msg, \"index %lld is out of range (the length is %lld)\", (long long)i, (long long)len);\n finch_panic(file, line, msg);\n}\n\nstatic void *must(void *p) {\n if (!p) {\n fflush(stdout);\n fatal(\"runtime error: out of memory\");\n }\n return p;\n}\n\nvoid *finch_alloc(int64_t n) { return must(malloc(n ? (size_t)n : 1)); }\nvoid *finch_zalloc(int64_t n) { return must(calloc(1, n ? (size_t)n : 1)); }\n\n// ---------- assert and finch test ----------\n\n__attribute__((noreturn, cold)) void finch_assert_failed(const char *file, int64_t line, const FStr *why) {\n char msg[600];\n if (why->len) snprintf(msg, sizeof msg, \"assert failed: %.*s\", (int)(why->len > 500 ? 500 : why->len), why->ptr);\n else snprintf(msg, sizeof msg, \"assert failed\");\n finch_panic(file, line, msg);\n}\n\n// Each test runs on its own: a runtime error (a failed assert too) ends that test, and the next one runs.\nstatic jmp_buf test_jump;\nstatic char test_message[1100];\nstatic int64_t tests_passed, tests_failed;\n\nstatic void test_fatal(const char *msg) {\n snprintf(test_message, sizeof test_message, \"%s\", msg);\n longjmp(test_jump, 1);\n}\n\nvoid finch_test_run(const char *name, void (*fn)(void)) {\n void (*saved)(const char *) = fatal_hook;\n void *chain = trace_top ? trace_top() : NULL;\n test_message[0] = 0;\n fatal_hook = test_fatal;\n int failed = setjmp(test_jump);\n if (!failed) fn();\n fatal_hook = saved;\n if (trace_set) trace_set(chain); // a test that stopped early left its calls on the chain\n fflush(stdout);\n if (failed) {\n tests_failed++;\n printf(\"FAIL %s\\n %s\\n\", name, test_message);\n } else {\n tests_passed++;\n printf(\"ok %s\\n\", name);\n }\n fflush(stdout);\n}\n\nint finch_test_summary(void) {\n printf(\"\\n%lld passed, %lld failed\\n\", (long long)tests_passed, (long long)tests_failed);\n return tests_failed ? 1 : 0;\n}\n\n// ---------- strings ----------\n\nstatic void str_heap(FStr *out, const char *p, int64_t len) {\n char *m = finch_alloc(len + 1);\n if (len) memcpy(m, p, (size_t)len);\n m[len] = 0;\n out->ptr = m;\n out->len = len;\n out->cap = len + 1;\n}\n\nstatic void str_static(FStr *out, const char *lit) {\n out->ptr = (char *)lit;\n out->len = (int64_t)strlen(lit);\n out->cap = 0;\n}\n\n// A copy that can be kept: literals are shared, everything else is duplicated.\nvoid finch_str_copy(FStr *out, const FStr *s) {\n if (s->cap == 0) *out = *s;\n else str_heap(out, s->ptr, s->len);\n}\n\n// Make `s` writable (before s[i] = c).\nvoid finch_str_own(FStr *s) {\n if (s->cap > 0) return;\n FStr t;\n str_heap(&t, s->ptr, s->len);\n *s = t;\n}\n\nvoid finch_str_drop(FStr *s) {\n if (s->cap > 0) free(s->ptr);\n}\n\nvoid finch_str_from_c(FStr *out, const char *p) {\n if (!p) {\n str_static(out, empty);\n return;\n }\n out->ptr = (char *)p;\n out->len = (int64_t)strlen(p);\n out->cap = -1;\n}\n\nvoid finch_str_concat(FStr *out, const FStr *a, const FStr *b) {\n int64_t len = a->len + b->len;\n char *m = finch_alloc(len + 1);\n memcpy(m, a->ptr, (size_t)a->len);\n memcpy(m + a->len, b->ptr, (size_t)b->len);\n m[len] = 0;\n out->ptr = m;\n out->len = len;\n out->cap = len + 1;\n}\n\nint finch_str_eq(const FStr *a, const FStr *b) {\n return a->len == b->len && memcmp(a->ptr, b->ptr, (size_t)a->len) == 0;\n}\n\nint64_t finch_str_cmp(const FStr *a, const FStr *b) {\n int64_t n = a->len < b->len ? a->len : b->len;\n int c = memcmp(a->ptr, b->ptr, (size_t)n);\n if (c) return c < 0 ? -1 : 1;\n return a->len < b->len ? -1 : a->len > b->len;\n}\n\nvoid finch_str_from_int(FStr *out, int64_t v) {\n char buf[32];\n int n = snprintf(buf, sizeof buf, \"%lld\", (long long)v);\n str_heap(out, buf, n);\n}\n\nvoid finch_str_from_uint(FStr *out, uint64_t v) {\n char buf[32];\n int n = snprintf(buf, sizeof buf, \"%llu\", (unsigned long long)v);\n str_heap(out, buf, n);\n}\n\n// The shortest text that reads back as the same number (as an f32 when `single`):\n// 0.1 -> \"0.1\", 1.0/3 -> \"0.3333333333333333\", 1e8 -> \"100000000\", 1e20 -> \"1e+20\".\nstatic int fmt_float(char *buf, size_t size, double v, int single) {\n if (isnan(v)) return snprintf(buf, size, \"nan\");\n if (isinf(v)) return snprintf(buf, size, v < 0 ? \"-inf\" : \"inf\");\n int p = 1;\n for (; p < 17; p++) {\n snprintf(buf, size, \"%.*e\", p - 1, v);\n double back = strtod(buf, NULL);\n if (single ? (float)back == (float)v : back == v) break;\n }\n int exp = atoi(strchr(buf, 'e') + 1);\n if (exp < -5 || exp >= 17) return snprintf(buf, size, \"%.*g\", p, v);\n int decimals = p - 1 - exp;\n return snprintf(buf, size, \"%.*f\", decimals > 0 ? decimals : 0, v);\n}\n\nvoid finch_str_from_float(FStr *out, double v) {\n char buf[64];\n int n = fmt_float(buf, sizeof buf, v, 0);\n str_heap(out, buf, n);\n}\n\nvoid finch_str_from_f32(FStr *out, double v) {\n char buf[64];\n int n = fmt_float(buf, sizeof buf, v, 1);\n str_heap(out, buf, n);\n}\n\n// str(x, 2): a fixed number of digits after the point\nvoid finch_str_fixed(FStr *out, double v, int64_t digits) {\n char buf[400];\n if (digits < 0) digits = 0;\n if (digits > 60) digits = 60;\n int n = snprintf(buf, sizeof buf, \"%.*f\", (int)digits, v);\n str_heap(out, buf, n);\n}\n\nvoid finch_print_float(double v, int32_t single) {\n char buf[64];\n fmt_float(buf, sizeof buf, v, single);\n fputs(buf, stdout);\n}\n\nvoid finch_str_from_char(FStr *out, char c) { str_heap(out, &c, 1); }\n\nvoid finch_str_from_bool(FStr *out, int b) { str_static(out, b ? \"true\" : \"false\"); }\n\nstatic void number_error(char *msg, size_t size, const FStr *s, const char *kind) {\n snprintf(msg, size, \"can't turn \\\"%.*s\\\" into %s\", (int)(s->len > 100 ? 100 : s->len), s->ptr ? s->ptr : \"\", kind);\n}\n\nstatic int parse_int(const FStr *s, int64_t *out) {\n const char *p = s->ptr ? s->ptr : \"\";\n while (isspace((unsigned char)*p)) p++;\n char *end;\n errno = 0;\n long long v = strtoll(p, &end, 10);\n while (isspace((unsigned char)*end)) end++;\n *out = v;\n return !(end == p || *end || errno == ERANGE);\n}\n\nstatic int parse_float(const FStr *s, double *out) {\n const char *p = s->ptr ? s->ptr : \"\";\n while (isspace((unsigned char)*p)) p++;\n char *end;\n double v = strtod(p, &end);\n while (isspace((unsigned char)*end)) end++;\n *out = v;\n return !(end == p || *end);\n}\n\nint64_t finch_str_to_int(const FStr *s, const char *file, int64_t line) {\n int64_t v;\n if (!parse_int(s, &v)) {\n char msg[200];\n number_error(msg, sizeof msg, s, \"int\");\n finch_panic(file, line, msg);\n }\n return v;\n}\n\ndouble finch_str_to_float(const FStr *s, const char *file, int64_t line) {\n double v;\n if (!parse_float(s, &v)) {\n char msg[200];\n number_error(msg, sizeof msg, s, \"float\");\n finch_panic(file, line, msg);\n }\n return v;\n}\n\n// ---------- errors as values: int(s) or 0, try read_file(p) ... ----------\n// Each gives back 1 on success; on failure 0, with the message in *err.\n\nstatic void set_error(FStr *err, const char *msg) { str_heap(err, msg, (int64_t)strlen(msg)); }\n\nint finch_str_try_int(const FStr *s, int64_t *out, FStr *err) {\n if (parse_int(s, out)) return 1;\n char msg[200];\n number_error(msg, sizeof msg, s, \"int\");\n set_error(err, msg);\n *out = 0;\n return 0;\n}\n\nint finch_str_try_float(const FStr *s, double *out, FStr *err) {\n if (parse_float(s, out)) return 1;\n char msg[200];\n number_error(msg, sizeof msg, s, \"float\");\n set_error(err, msg);\n *out = 0;\n return 0;\n}\n\nstatic void file_error(FStr *err, const char *what, const FStr *path) {\n char msg[300];\n snprintf(msg, sizeof msg, \"can't %s the file \\\"%.*s\\\": %s\", what, (int)(path->len > 200 ? 200 : path->len),\n path->ptr ? path->ptr : \"\", strerror(errno));\n set_error(err, msg);\n}\n\n// when `fn main() -> !` fails\nvoid finch_report_error(const FStr *msg) {\n fflush(stdout);\n fprintf(stderr, \"error: %.*s\\n\", (int)msg->len, msg->ptr ? msg->ptr : \"\");\n finch_str_drop((FStr *)msg);\n}\n\nvoid finch_str_sub(FStr *out, const FStr *s, int64_t start, int64_t end, const char *file, int64_t line) {\n if (start < 0 || end > s->len || start > end) {\n char msg[160];\n snprintf(msg, sizeof msg, \"sub(%lld, %lld) is out of range (the length is %lld)\", (long long)start,\n (long long)end, (long long)s->len);\n finch_panic(file, line, msg);\n }\n str_heap(out, s->ptr + start, end - start);\n}\n\nint64_t finch_str_find(const FStr *s, const FStr *t) {\n if (t->len == 0) return 0;\n for (int64_t i = 0; i + t->len <= s->len; i++)\n if (s->ptr[i] == t->ptr[0] && memcmp(s->ptr + i, t->ptr, (size_t)t->len) == 0) return i;\n return -1;\n}\n\nint finch_str_starts(const FStr *s, const FStr *t) {\n return t->len <= s->len && memcmp(s->ptr, t->ptr, (size_t)t->len) == 0;\n}\n\nint finch_str_ends(const FStr *s, const FStr *t) {\n return t->len <= s->len && memcmp(s->ptr + s->len - t->len, t->ptr, (size_t)t->len) == 0;\n}\n\nvoid finch_str_trim(FStr *out, const FStr *s) {\n int64_t a = 0, b = s->len;\n while (a < b && isspace((unsigned char)s->ptr[a])) a++;\n while (b > a && isspace((unsigned char)s->ptr[b - 1])) b--;\n str_heap(out, s->ptr + a, b - a);\n}\n\nvoid finch_str_upper(FStr *out, const FStr *s) {\n str_heap(out, s->ptr, s->len);\n for (int64_t i = 0; i < out->len; i++) out->ptr[i] = (char)toupper((unsigned char)out->ptr[i]);\n}\n\nvoid finch_str_lower(FStr *out, const FStr *s) {\n str_heap(out, s->ptr, s->len);\n for (int64_t i = 0; i < out->len; i++) out->ptr[i] = (char)tolower((unsigned char)out->ptr[i]);\n}\n\nvoid finch_str_replace(FStr *out, const FStr *s, const FStr *from, const FStr *to) {\n if (from->len == 0) {\n str_heap(out, s->ptr, s->len);\n return;\n }\n int64_t count = 0;\n for (int64_t i = 0; i + from->len <= s->len;) {\n if (memcmp(s->ptr + i, from->ptr, (size_t)from->len) == 0) {\n count++;\n i += from->len;\n } else {\n i++;\n }\n }\n int64_t len = s->len + count * (to->len - from->len);\n char *m = finch_alloc(len + 1), *w = m;\n for (int64_t i = 0; i < s->len;) {\n if (i + from->len <= s->len && memcmp(s->ptr + i, from->ptr, (size_t)from->len) == 0) {\n memcpy(w, to->ptr, (size_t)to->len);\n w += to->len;\n i += from->len;\n } else {\n *w++ = s->ptr[i++];\n }\n }\n *w = 0;\n out->ptr = m;\n out->len = len;\n out->cap = len + 1;\n}\n\nstatic void arr_push_str(FArr *a, const char *p, int64_t len);\n\n// \"a,b,,c\".split(\",\") -> [\"a\", \"b\", \"\", \"c\"]; split(\"\") gives single characters.\nvoid finch_str_split(FArr *out, const FStr *s, const FStr *sep) {\n out->ptr = NULL;\n out->len = out->cap = 0;\n if (sep->len == 0) {\n for (int64_t i = 0; i < s->len; i++) arr_push_str(out, s->ptr + i, 1);\n return;\n }\n int64_t start = 0;\n for (int64_t i = 0; i + sep->len <= s->len;) {\n if (memcmp(s->ptr + i, sep->ptr, (size_t)sep->len) == 0) {\n arr_push_str(out, s->ptr + start, i - start);\n i += sep->len;\n start = i;\n } else {\n i++;\n }\n }\n arr_push_str(out, s->ptr + start, s->len - start);\n}\n\nvoid finch_str_join(FStr *out, const FArr *parts, const FStr *sep) {\n const FStr *p = parts->ptr;\n int64_t len = 0;\n for (int64_t i = 0; i < parts->len; i++) len += p[i].len + (i ? sep->len : 0);\n char *m = finch_alloc(len + 1), *w = m;\n for (int64_t i = 0; i < parts->len; i++) {\n if (i) {\n memcpy(w, sep->ptr, (size_t)sep->len);\n w += sep->len;\n }\n memcpy(w, p[i].ptr, (size_t)p[i].len);\n w += p[i].len;\n }\n *w = 0;\n out->ptr = m;\n out->len = len;\n out->cap = len + 1;\n}\n\nvoid finch_str_repeat(FStr *out, const FStr *s, int64_t n) {\n if (n < 0) n = 0;\n int64_t len = s->len * n;\n char *m = finch_alloc(len + 1);\n for (int64_t i = 0; i < n; i++) memcpy(m + i * s->len, s->ptr, (size_t)s->len);\n m[len] = 0;\n out->ptr = m;\n out->len = len;\n out->cap = len + 1;\n}\n\nvoid finch_str_from_bytes(FStr *out, const FArr *bytes) { str_heap(out, bytes->ptr, bytes->len); }\n\n// ---------- building text: print(x) and str(x) of arrays, maps and structs ----------\n// A buffer starts as an empty str and grows on the heap.\n\nvoid finch_buf_add(FStr *b, const char *p, int64_t n) {\n if (b->len + n + 1 > b->cap) {\n int64_t cap = b->cap > 0 ? b->cap * 2 : 32;\n while (cap < b->len + n + 1) cap *= 2;\n b->ptr = must(realloc(b->cap > 0 ? b->ptr : NULL, (size_t)cap));\n b->cap = cap;\n }\n if (n) memcpy(b->ptr + b->len, p, (size_t)n);\n b->len += n;\n b->ptr[b->len] = 0;\n}\n\nstatic void buf_fmt(FStr *b, const char *fmt, ...) __attribute__((format(printf, 2, 3)));\nstatic void buf_fmt(FStr *b, const char *fmt, ...) {\n char t[64];\n va_list ap;\n va_start(ap, fmt);\n int n = vsnprintf(t, sizeof t, fmt, ap);\n va_end(ap);\n finch_buf_add(b, t, n);\n}\n\nvoid finch_buf_int(FStr *b, int64_t v) { buf_fmt(b, \"%lld\", (long long)v); }\nvoid finch_buf_uint(FStr *b, uint64_t v) { buf_fmt(b, \"%llu\", (unsigned long long)v); }\nvoid finch_buf_float(FStr *b, double v) {\n char t[64];\n finch_buf_add(b, t, fmt_float(t, sizeof t, v, 0));\n}\nvoid finch_buf_f32(FStr *b, double v) {\n char t[64];\n finch_buf_add(b, t, fmt_float(t, sizeof t, v, 1));\n}\nvoid finch_buf_bool(FStr *b, int v) { finch_buf_add(b, v ? \"true\" : \"false\", v ? 4 : 5); }\nvoid finch_buf_ptr(FStr *b, const void *p) {\n if (p) buf_fmt(b, \"0x%llx\", (unsigned long long)(uintptr_t)p);\n else finch_buf_add(b, \"null\", 4);\n}\n\nvoid finch_buf_char(FStr *b, char c, int quoted) {\n if (quoted) finch_buf_add(b, \"'\", 1);\n finch_buf_add(b, &c, 1);\n if (quoted) finch_buf_add(b, \"'\", 1);\n}\n\nvoid finch_buf_str(FStr *b, const FStr *s, int quoted) {\n if (quoted) finch_buf_add(b, \"\\\"\", 1);\n finch_buf_add(b, s->ptr, s->len);\n if (quoted) finch_buf_add(b, \"\\\"\", 1);\n}\n\n// print the buffer and free it\nvoid finch_buf_print(FStr *b) {\n if (b->len) fwrite(b->ptr, 1, (size_t)b->len, stdout);\n if (b->cap > 0) free(b->ptr);\n}\n\n// ---------- arrays ----------\n\nvoid finch_arr_reserve(FArr *a, int64_t need, int64_t es) {\n if (need <= a->cap) return;\n int64_t cap = a->cap * 2;\n if (cap < 4) cap = 4;\n if (cap < need) cap = need;\n a->ptr = must(realloc(a->cap > 0 ? a->ptr : NULL, (size_t)(cap * es)));\n a->cap = cap;\n}\n\nvoid finch_arr_make(FArr *out, int64_t len, int64_t es) {\n if (len < 0) len = 0;\n out->ptr = len ? finch_zalloc(len * es) : NULL;\n out->len = len;\n out->cap = len;\n}\n\n// grow or shrink to `len`; new elements are zero\nvoid finch_arr_resize(FArr *a, int64_t len, int64_t es) {\n if (len < 0) len = 0;\n finch_arr_reserve(a, len, es);\n if (len > a->len) memset((char *)a->ptr + a->len * es, 0, (size_t)((len - a->len) * es));\n a->len = len;\n}\n\n// a byte-for-byte copy of the elements (the compiler deep-copies owning elements afterwards)\nvoid finch_arr_clone_raw(FArr *out, const FArr *a, int64_t es) {\n out->len = a->len;\n out->cap = a->len;\n out->ptr = a->len ? finch_alloc(a->len * es) : NULL;\n if (a->len) memcpy(out->ptr, a->ptr, (size_t)(a->len * es));\n}\n\nvoid finch_arr_free(FArr *a) {\n if (a->cap > 0) free(a->ptr);\n}\n\n// open a hole at i (0 <= i <= len), len grows by one\nvoid finch_arr_insert_gap(FArr *a, int64_t i, int64_t es, const char *file, int64_t line) {\n if (i < 0 || i > a->len) finch_panic_index(file, line, i, a->len);\n finch_arr_reserve(a, a->len + 1, es);\n char *p = a->ptr;\n memmove(p + (i + 1) * es, p + i * es, (size_t)((a->len - i) * es));\n a->len++;\n}\n\n// close the hole at i (the element was already moved out), len shrinks by one\nvoid finch_arr_remove_gap(FArr *a, int64_t i, int64_t es) {\n char *p = a->ptr;\n memmove(p + i * es, p + (i + 1) * es, (size_t)((a->len - i - 1) * es));\n a->len--;\n}\n\nstatic void arr_push_str(FArr *a, const char *p, int64_t len) {\n finch_arr_reserve(a, a->len + 1, (int64_t)sizeof(FStr));\n str_heap((FStr *)a->ptr + a->len, p, len);\n a->len++;\n}\n\nvoid finch_bytes_from_str(FArr *out, const FStr *s) {\n out->len = out->cap = s->len;\n out->ptr = s->len ? finch_alloc(s->len) : NULL;\n if (s->len) memcpy(out->ptr, s->ptr, (size_t)s->len);\n}\n\n// ---------- program arguments, input and files ----------\n\nvoid finch_args(FArr *out, int argc, char **argv) {\n out->len = out->cap = argc;\n out->ptr = finch_alloc((int64_t)sizeof(FStr) * (argc ? argc : 1));\n for (int i = 0; i < argc; i++) str_heap((FStr *)out->ptr + i, argv[i], (int64_t)strlen(argv[i]));\n}\n\n// after main: the arguments go\nvoid finch_args_free(FArr *a) {\n for (int64_t i = 0; i < a->len; i++) finch_str_drop((FStr *)a->ptr + i);\n finch_arr_free(a);\n}\n\n// input(\"Name? \") prints the prompt and reads one line, without the newline.\nvoid finch_input(FStr *out, const FStr *prompt) {\n if (prompt->len) fwrite(prompt->ptr, 1, (size_t)prompt->len, stdout);\n fflush(stdout);\n // read one line of any length (getline isn't available everywhere)\n size_t size = 128, n = 0;\n char *line = finch_alloc((int64_t)size);\n int c, got = 0;\n while ((c = fgetc(stdin)) != EOF) {\n got = 1;\n if (c == '\\n') break;\n if (n + 1 >= size) line = must(realloc(line, size *= 2));\n line[n++] = (char)c;\n }\n if (!got) {\n free(line);\n str_static(out, empty);\n return;\n }\n while (n > 0 && line[n - 1] == '\\r') n--;\n line[n] = 0;\n out->ptr = line;\n out->len = (int64_t)n;\n out->cap = (int64_t)size;\n}\n\nint finch_file_exists(const FStr *path) {\n FILE *f = fopen(path->ptr, \"rb\");\n if (f) fclose(f);\n return f != NULL;\n}\n\nint finch_try_read_file(FStr *out, const FStr *path, FStr *err) {\n FILE *f = fopen(path->ptr ? path->ptr : \"\", \"rb\");\n if (!f) {\n file_error(err, \"read\", path);\n return 0;\n }\n fseek(f, 0, SEEK_END);\n long size = ftell(f);\n fseek(f, 0, SEEK_SET);\n char *m = finch_alloc(size + 1);\n size_t got = fread(m, 1, (size_t)size, f);\n fclose(f);\n m[got] = 0;\n out->ptr = m;\n out->len = (int64_t)got;\n out->cap = size + 1;\n return 1;\n}\n\nvoid finch_read_file(FStr *out, const FStr *path, const char *file, int64_t line) {\n FILE *f = fopen(path->ptr ? path->ptr : \"\", \"rb\");\n if (!f) {\n char msg[300];\n snprintf(msg, sizeof msg, \"can't read the file \\\"%.*s\\\": %s\", (int)(path->len > 200 ? 200 : path->len),\n path->ptr, strerror(errno));\n finch_panic(file, line, msg);\n }\n fseek(f, 0, SEEK_END);\n long size = ftell(f);\n fseek(f, 0, SEEK_SET);\n char *m = finch_alloc(size + 1);\n size_t got = fread(m, 1, (size_t)size, f);\n fclose(f);\n m[got] = 0;\n out->ptr = m;\n out->len = (int64_t)got;\n out->cap = size + 1;\n}\n\n// shell(\"ls -l\"): run a command, give back its exit code\nint64_t finch_shell(const FStr *cmd) {\n fflush(stdout);\n int status = system(cmd->ptr ? cmd->ptr : \"\");\n#ifdef _WIN32\n return status; // Windows gives the exit code directly\n#else\n if (status == -1) return -1;\n if (WIFEXITED(status)) return WEXITSTATUS(status);\n return 128 + (WIFSIGNALED(status) ? WTERMSIG(status) : 0);\n#endif\n}\n\nint finch_delete_file(const FStr *path) { return remove(path->ptr ? path->ptr : \"\") == 0; }\n\nint finch_try_delete_file(const FStr *path, FStr *err) {\n if (remove(path->ptr ? path->ptr : \"\") == 0) return 1;\n file_error(err, \"delete\", path);\n return 0;\n}\n\n// stdin/stdout/stderr are macros on some systems (Windows), so Finch asks for them here\nvoid *finch_std_stream(int i) { return i == 0 ? (void *)stdin : i == 1 ? (void *)stdout : (void *)stderr; }\n\nint finch_write_file(const FStr *path, const FStr *text) {\n FILE *f = fopen(path->ptr ? path->ptr : \"\", \"wb\");\n if (!f) return 0;\n size_t put = text->len ? fwrite(text->ptr, 1, (size_t)text->len, f) : 0;\n return fclose(f) == 0 && put == (size_t)text->len;\n}\n\nint finch_try_write_file(const FStr *path, const FStr *text, FStr *err) {\n errno = 0;\n if (finch_write_file(path, text)) return 1;\n if (!errno) errno = EIO;\n file_error(err, \"write\", path);\n return 0;\n}\n\n// ---------- folders (std/fs.fch, through std/finch_fs.h) ----------\n\nint finch_fs_is_dir(const char *path) {\n#ifdef _WIN32\n struct _finddata_t d;\n intptr_t h = _findfirst(path, &d);\n if (h == -1) return 0;\n _findclose(h);\n return (d.attrib & _A_SUBDIR) != 0;\n#else\n struct stat st;\n return stat(path, &st) == 0 && S_ISDIR(st.st_mode);\n#endif\n}\n\nint finch_fs_exists(const char *path) {\n#ifdef _WIN32\n return _access(path, 0) == 0;\n#else\n struct stat st;\n return stat(path, &st) == 0;\n#endif\n}\n\nstatic int make_one_dir(const char *p) {\n#ifdef _WIN32\n return _mkdir(p) == 0 || errno == EEXIST;\n#else\n return mkdir(p, 0777) == 0 || errno == EEXIST;\n#endif\n}\n\n// mkdir -p: the parents too; 1 if the folder is there afterwards\nint finch_fs_mkdir(const char *path) {\n size_t n = strlen(path);\n char *p = finch_alloc((int64_t)n + 1);\n memcpy(p, path, n + 1);\n for (size_t i = 1; i < n; i++) {\n if (p[i] == '/' || p[i] == '\\\\') {\n if (p[i - 1] == ':') continue; /* C: is a drive */\n char c = p[i];\n p[i] = 0;\n make_one_dir(p);\n p[i] = c;\n }\n }\n int ok = make_one_dir(p) && finch_fs_is_dir(p);\n free(p);\n return ok;\n}\n\nint finch_fs_remove_dir(const char *path) {\n#ifdef _WIN32\n return _rmdir(path) == 0;\n#else\n return rmdir(path) == 0;\n#endif\n}\n\n// the names in a folder, one per call (\"\" at the end), without . and ..\ntypedef struct {\n#ifdef _WIN32\n intptr_t h;\n struct _finddata_t d;\n int first;\n#else\n DIR *d;\n#endif\n} FsList;\n\nvoid *finch_fs_list_open(const char *path) {\n FsList *l = finch_zalloc((int64_t)sizeof(FsList));\n#ifdef _WIN32\n size_t n = strlen(path);\n char *pat = finch_alloc((int64_t)n + 3);\n memcpy(pat, path, n);\n memcpy(pat + n, \"\\\\*\", 3);\n l->h = _findfirst(pat, &l->d);\n free(pat);\n if (l->h == -1) {\n free(l);\n return NULL;\n }\n l->first = 1;\n#else\n l->d = opendir(path);\n if (!l->d) {\n free(l);\n return NULL;\n }\n#endif\n return l;\n}\n\nconst char *finch_fs_list_next(void *handle) {\n FsList *l = handle;\n for (;;) {\n#ifdef _WIN32\n if (!l->first && _findnext(l->h, &l->d) != 0) return \"\";\n l->first = 0;\n const char *name = l->d.name;\n#else\n struct dirent *e = readdir(l->d);\n if (!e) return \"\";\n const char *name = e->d_name;\n#endif\n if (strcmp(name, \".\") != 0 && strcmp(name, \"..\") != 0) return name;\n }\n}\n\nvoid finch_fs_list_close(void *handle) {\n FsList *l = handle;\n#ifdef _WIN32\n _findclose(l->h);\n#else\n closedir(l->d);\n#endif\n free(l);\n}\n\n// where a program keeps its own files for this user (saves, settings); \"\" if unknown\n// Linux: $XDG_DATA_HOME or ~/.local/share macOS: ~/Library/Application Support Windows: %APPDATA%\nconst char *finch_fs_data_home(void) {\n static char buf[4096];\n const char *v;\n#if defined(_WIN32)\n v = getenv(\"APPDATA\");\n snprintf(buf, sizeof buf, \"%s\", v ? v : \"\");\n#elif defined(__APPLE__)\n v = getenv(\"HOME\");\n if (v) snprintf(buf, sizeof buf, \"%s/Library/Application Support\", v);\n else buf[0] = 0;\n#else\n v = getenv(\"XDG_DATA_HOME\");\n if (v && *v) snprintf(buf, sizeof buf, \"%s\", v);\n else if ((v = getenv(\"HOME\"))) snprintf(buf, sizeof buf, \"%s/.local/share\", v);\n else buf[0] = 0;\n#endif\n return buf;\n}\n\n// ---------- threads (std/thread.fch, through std/finch_thread.h) ----------\n\ntypedef struct {\n void (*fn)(void *);\n void *arg;\n} ThreadStart;\n\n#ifdef _WIN32\nstatic DWORD WINAPI thread_main(LPVOID p) {\n ThreadStart st = *(ThreadStart *)p;\n free(p);\n st.fn(st.arg);\n return 0;\n}\n#else\nstatic void *thread_main(void *p) {\n ThreadStart st = *(ThreadStart *)p;\n free(p);\n st.fn(st.arg);\n return NULL;\n}\n#endif\n\n// run fn(arg) on a new thread; NULL if it couldn't start\nvoid *finch_thread_spawn(void (*fn)(void *), void *arg) {\n ThreadStart *st = finch_alloc((int64_t)sizeof(ThreadStart));\n st->fn = fn;\n st->arg = arg;\n#ifdef _WIN32\n HANDLE h = CreateThread(NULL, 0, thread_main, st, 0, NULL);\n if (!h) {\n free(st);\n return NULL;\n }\n return h;\n#else\n pthread_t *t = finch_alloc((int64_t)sizeof(pthread_t));\n if (pthread_create(t, NULL, thread_main, st) != 0) {\n free(st);\n free(t);\n return NULL;\n }\n return t;\n#endif\n}\n\n// wait for the thread to end; 1 if that worked\nint finch_thread_join(void *t) {\n if (!t) return 0;\n#ifdef _WIN32\n int ok = WaitForSingleObject((HANDLE)t, INFINITE) == WAIT_OBJECT_0;\n CloseHandle((HANDLE)t);\n return ok;\n#else\n int ok = pthread_join(*(pthread_t *)t, NULL) == 0;\n free(t);\n return ok;\n#endif\n}\n\nvoid *finch_mutex_new(void) {\n#ifdef _WIN32\n CRITICAL_SECTION *m = finch_alloc((int64_t)sizeof(CRITICAL_SECTION));\n InitializeCriticalSection(m);\n#else\n pthread_mutex_t *m = finch_alloc((int64_t)sizeof(pthread_mutex_t));\n pthread_mutex_init(m, NULL);\n#endif\n return m;\n}\n\nvoid finch_mutex_lock(void *m) {\n#ifdef _WIN32\n EnterCriticalSection((CRITICAL_SECTION *)m);\n#else\n pthread_mutex_lock((pthread_mutex_t *)m);\n#endif\n}\n\nvoid finch_mutex_unlock(void *m) {\n#ifdef _WIN32\n LeaveCriticalSection((CRITICAL_SECTION *)m);\n#else\n pthread_mutex_unlock((pthread_mutex_t *)m);\n#endif\n}\n\nvoid finch_mutex_free(void *m) {\n if (!m) return;\n#ifdef _WIN32\n DeleteCriticalSection((CRITICAL_SECTION *)m);\n#else\n pthread_mutex_destroy((pthread_mutex_t *)m);\n#endif\n free(m);\n}\n\nint finch_cpu_count(void) {\n#ifdef _WIN32\n SYSTEM_INFO si;\n GetSystemInfo(&si);\n return (int)si.dwNumberOfProcessors;\n#else\n long n = sysconf(_SC_NPROCESSORS_ONLN);\n return n > 0 ? (int)n : 1;\n#endif\n}\n\n// ---------- maps ----------\n// map[K]V = { entries; len; used; cap; index; icap; adds }\n// entries: `used` slots of es bytes, each { uint64 hash; K key; V value }, in insertion order.\n// A removed entry keeps its slot (hash 0) until the next resize packs the slots.\n// index: icap slots (a power of two, at least 2 * cap): -1 empty, -2 removed, else an entry number.\n// adds: how many keys were ever added; a `for` over the map stops with an error if it changes.\n// The compiler stores, copies and drops keys and values; the runtime only finds and places entries.\n// Keys are compared byte for byte (numbers, chars, bools) or as text (kstr).\n\ntypedef struct {\n char *entries;\n int64_t len, used, cap;\n int64_t *index;\n int64_t icap;\n int64_t adds;\n} FMap;\n\nstatic uint64_t map_hash(const void *key, int64_t ks, int kstr) {\n const unsigned char *p;\n size_t n;\n if (kstr) {\n const FStr *s = key;\n p = (const unsigned char *)s->ptr;\n n = (size_t)s->len;\n } else {\n p = key;\n n = (size_t)ks;\n }\n uint64_t h = 1469598103934665603ULL; // FNV-1a, then mixed so the low bits spread well\n for (size_t i = 0; i < n; i++) {\n h ^= p[i];\n h *= 1099511628211ULL;\n }\n h ^= h >> 31;\n h *= 0xbf58476d1ce4e5b9ULL;\n h ^= h >> 29;\n return h | 1; // 0 marks a removed entry\n}\n\nstatic int map_key_eq(const char *a, const void *b, int64_t ks, int kstr) {\n if (!kstr) return memcmp(a, b, (size_t)ks) == 0;\n const FStr *x = (const FStr *)a, *y = b;\n return x->len == y->len && (x->len == 0 || memcmp(x->ptr, y->ptr, (size_t)x->len) == 0);\n}\n\nstatic void map_reindex(FMap *m, int64_t es) {\n if (m->len != m->used) { // pack the slots of removed entries away\n int64_t j = 0;\n for (int64_t i = 0; i < m->used; i++) {\n char *e = m->entries + i * es;\n if (*(uint64_t *)e == 0) continue;\n if (i != j) memmove(m->entries + j * es, e, (size_t)es);\n j++;\n }\n m->used = j;\n }\n int64_t icap = 16;\n while (icap < m->cap * 2) icap *= 2;\n free(m->index);\n m->index = finch_alloc(icap * (int64_t)sizeof(int64_t));\n memset(m->index, 0xff, (size_t)icap * sizeof(int64_t));\n m->icap = icap;\n for (int64_t i = 0; i < m->used; i++) {\n uint64_t h = *(uint64_t *)(m->entries + i * es);\n int64_t s = (int64_t)(h & (uint64_t)(icap - 1));\n while (m->index[s] != -1) s = (s + 1) & (icap - 1);\n m->index[s] = i;\n }\n}\n\n// the entry number of `key`, or -1\nint64_t finch_map_find(const FMap *m, const void *key, int64_t es, int64_t ks, int32_t kstr) {\n if (m->len == 0) return -1;\n uint64_t h = map_hash(key, ks, kstr);\n for (int64_t s = (int64_t)(h & (uint64_t)(m->icap - 1));; s = (s + 1) & (m->icap - 1)) {\n int64_t i = m->index[s];\n if (i == -1) return -1;\n if (i < 0) continue;\n char *e = m->entries + i * es;\n if (*(uint64_t *)e == h && map_key_eq(e + 8, key, ks, kstr)) return i;\n }\n}\n\n// The entry for `key`. A new one is zeroed except its hash (*added = 1); the compiler then stores the key.\nint64_t finch_map_slot(FMap *m, const void *key, int64_t es, int64_t ks, int32_t kstr, int32_t *added) {\n int64_t i = finch_map_find(m, key, es, ks, kstr);\n *added = i < 0;\n if (i >= 0) return i;\n if (m->used == m->cap) {\n if (m->len * 4 < m->cap * 3) { // many removed slots: pack them instead of growing\n map_reindex(m, es);\n } else {\n m->cap = m->cap ? m->cap * 2 : 8;\n m->entries = must(realloc(m->entries, (size_t)(m->cap * es)));\n map_reindex(m, es);\n }\n }\n uint64_t h = map_hash(key, ks, kstr);\n i = m->used++;\n char *e = m->entries + i * es;\n memset(e, 0, (size_t)es);\n *(uint64_t *)e = h;\n int64_t s = (int64_t)(h & (uint64_t)(m->icap - 1));\n while (m->index[s] >= 0) s = (s + 1) & (m->icap - 1);\n m->index[s] = i;\n m->len++;\n m->adds++;\n return i;\n}\n\n// Forget entry i (its key and value were already dropped by the compiler).\nvoid finch_map_remove_at(FMap *m, int64_t i, int64_t es) {\n char *e = m->entries + i * es;\n uint64_t h = *(uint64_t *)e;\n int64_t s = (int64_t)(h & (uint64_t)(m->icap - 1));\n while (m->index[s] != i) s = (s + 1) & (m->icap - 1);\n m->index[s] = -2;\n *(uint64_t *)e = 0;\n if (--m->len == 0) {\n m->used = 0;\n memset(m->index, 0xff, (size_t)m->icap * sizeof(int64_t));\n }\n}\n\nvoid finch_map_clear(FMap *m) {\n m->len = m->used = 0;\n if (m->index) memset(m->index, 0xff, (size_t)m->icap * sizeof(int64_t));\n}\n\nvoid finch_map_free(FMap *m) {\n free(m->entries);\n free(m->index);\n}\n\n// A copy of the slots and the index; the compiler then copies the keys and values that own memory.\nvoid finch_map_clone_raw(FMap *out, const FMap *m, int64_t es) {\n *out = *m;\n if (!m->cap) return;\n out->entries = finch_alloc(m->cap * es);\n memcpy(out->entries, m->entries, (size_t)(m->used * es));\n out->index = finch_alloc(m->icap * (int64_t)sizeof(int64_t));\n memcpy(out->index, m->index, (size_t)m->icap * sizeof(int64_t));\n}\n\n__attribute__((noreturn, cold)) void finch_map_changed(const char *file, int64_t line) {\n finch_panic(file, line, \"a key was added to the map while looping over it (collect the new keys, and add them after the loop)\");\n}\n\n__attribute__((noreturn, cold)) void finch_map_missing(const char *file, int64_t line, const FStr *key) {\n char msg[200];\n snprintf(msg, sizeof msg, \"the key %.*s is not in the map (check with .has(key), or use .get(key, default))\",\n (int)(key->len > 100 ? 100 : key->len), key->ptr ? key->ptr : \"\");\n finch_panic(file, line, msg);\n}\n") list.push("// Files and folders: bytes, folders, listing, and a place for a program's own files.\n//\n// import fs\n// data := try fs.read_bytes(\"atlas.png\") the whole file as []u8 (any bytes, NUL included)\n// try fs.write_bytes(\"out.bin\", data)\n// try fs.mkdir(\"saves/slot1\") with the folders above it\n// names := try fs.list(\"saves\") the names in a folder, sorted\n// fs.exists(p) fs.is_dir(p) try fs.remove_dir(p)\n// dir := try fs.data_dir(\"MyGame\") a folder for this user's saves, made if needed\nimport \"finch_fs.h\"\n\nfn read_bytes(str path) -> []u8! {\n text := try read_file(path)\n return text.bytes()\n}\n\nfn write_bytes(str path, []u8 data) -> ! {\n try write_file(path, str(data))\n}\n\nfn exists(str path) -> bool {\n return finch_fs_exists(path) != 0\n}\n\nfn is_dir(str path) -> bool {\n return finch_fs_is_dir(path) != 0\n}\n\nfn mkdir(str path) -> ! {\n if finch_fs_mkdir(path) == 0 {\n return error(\"can't make the folder \\\"\" + path + \"\\\"\")\n }\n}\n\n// only an empty folder\nfn remove_dir(str path) -> ! {\n if finch_fs_remove_dir(path) == 0 {\n return error(\"can't remove the folder \\\"\" + path + \"\\\" (is it empty?)\")\n }\n}\n\nfn list(str path) -> []str! {\n l := finch_fs_list_open(path)\n if l == null {\n return error(\"can't list the folder \\\"\" + path + \"\\\"\")\n }\n []str names\n while true {\n name := finch_fs_list_next(l)\n if name.len == 0 {\n break\n }\n names.push(name)\n }\n finch_fs_list_close(l)\n names.sort()\n return names\n}\n\n// The folder where a program keeps this user's files (saves, settings), made if it isn't there:\n// ~/.local/share/ on Linux, ~/Library/Application Support/ on macOS, %APPDATA%\\ on Windows.\nfn data_dir(str app) -> str! {\n home := finch_fs_data_home()\n if home.len == 0 {\n return error(\"this system doesn't say where programs keep their files\")\n }\n dir := home + \"/\" + app\n try mkdir(dir)\n return dir\n}\n") list.push("// JSON: read it, build it, write it.\n//\n// import json\n//\n// doc := try json.parse(\"{\\\"name\\\": \\\"Finch\\\", \\\"tags\\\": [\\\"fast\\\", \\\"small\\\"], \\\"stars\\\": 5}\")\n// print(doc.get(\"name\").str_or(\"\")) Finch\n// print(doc.get(\"tags\").at(1).str_or(\"\")) small\n// print(doc.get(\"stars\").int_or(0)) 5\n// print(doc.get(\"missing\").is_null()) true (a missing key is null, so chains never fail)\n//\n// out := json.new_object()\n// out.set(\"ok\", json.new_bool(true))\n// list := json.new_list()\n// list.push(json.new_number(1.5))\n// out.set(\"values\", list)\n// print(json.stringify(out)) {\"ok\":true,\"values\":[1.5]}\n// print(json.pretty(out)) the same, over several lines\n\nenum Kind { Null, Bool, Number, Text, List, Object }\n\n// One JSON value. `kind` says which of the other fields it uses: flag for Bool, num for Number,\n// text for Text, items for List, and items with keys (the same length, in the same order) for Object.\nstruct Json {\n Kind kind\n bool flag\n float num\n str text\n []Json items\n []str keys\n}\n\nfn new_null() -> Json {\n return Json(kind: Kind.Null)\n}\n\nfn new_bool(bool b) -> Json {\n return Json(kind: Kind.Bool, flag: b)\n}\n\nfn new_number(float n) -> Json {\n return Json(kind: Kind.Number, num: n)\n}\n\nfn new_text(str s) -> Json {\n return Json(kind: Kind.Text, text: s)\n}\n\nfn new_list() -> Json {\n return Json(kind: Kind.List)\n}\n\nfn new_object() -> Json {\n return Json(kind: Kind.Object)\n}\n\n// ---------- looking inside ----------\n\nfn Json.is_null() -> bool {\n return self.kind == Kind.Null\n}\n\n// the number of elements of a list, of keys of an object, of bytes of a text; 0 for the rest\nfn Json.len() -> int {\n return match self.kind {\n List, Object -> self.items.len\n Text -> self.text.len\n else -> 0\n }\n}\n\nfn Json.has(str key) -> bool {\n return self.kind == Kind.Object && self.keys.contains(key)\n}\n\n// the value of a key of an object; null if there is none (or this is not an object)\nfn Json.get(str key) -> Json {\n if self.kind != Kind.Object {\n return new_null()\n }\n i := self.keys.find(key)\n if i < 0 {\n return new_null()\n }\n return self.items[i]\n}\n\n// the element of a list; null if there is none\nfn Json.at(int i) -> Json {\n if self.kind != Kind.List || i < 0 || i >= self.items.len {\n return new_null()\n }\n return self.items[i]\n}\n\n// the value if it is of that kind, else what was given\nfn Json.str_or(str fallback) -> str {\n if self.kind == Kind.Text {\n return self.text\n }\n return fallback\n}\n\nfn Json.float_or(float fallback) -> float {\n if self.kind == Kind.Number {\n return self.num\n }\n return fallback\n}\n\nfn Json.int_or(int fallback) -> int {\n if self.kind == Kind.Number {\n return int(self.num)\n }\n return fallback\n}\n\nfn Json.bool_or(bool fallback) -> bool {\n if self.kind == Kind.Bool {\n return self.flag\n }\n return fallback\n}\n\n// the keys of an object, in the order they were written\nfn Json.names() -> []str {\n return self.keys\n}\n\n// ---------- building ----------\n\n// add to the end of a list\nfn Json.push(Json v) {\n self.items.push(v)\n}\n\n// give a key a value in an object; a key that is already there is replaced\nfn Json.set(str key, Json v) {\n i := self.keys.find(key)\n if i >= 0 {\n self.items[i] = v\n return\n }\n self.keys.push(key)\n self.items.push(v)\n}\n\n// ---------- writing ----------\n\nfn quote(str s) -> str {\n []str parts = [\"\\\"\"]\n for c in s {\n n := int(c)\n if c == '\"' {\n parts.push(\"\\\\\\\"\")\n } else if c == '\\\\' {\n parts.push(\"\\\\\\\\\")\n } else if c == '\\n' {\n parts.push(\"\\\\n\")\n } else if c == '\\r' {\n parts.push(\"\\\\r\")\n } else if c == '\\t' {\n parts.push(\"\\\\t\")\n } else if n < 32 {\n parts.push(\"\\\\u00\" + hexdigit(n / 16) + hexdigit(n % 16))\n } else {\n parts.push(str(c))\n }\n }\n parts.push(\"\\\"\")\n return parts.join(\"\")\n}\n\nfn hexdigit(int n) -> str {\n digits := \"0123456789abcdef\"\n return str(digits[n])\n}\n\nfn number_text(float n) -> str {\n if n == float(int(n)) && n > -1000000000000000.0 && n < 1000000000000000.0 {\n return str(int(n))\n }\n return str(n)\n}\n\nstruct Writer {\n []str out\n}\n\nfn write(Json j, str indent, str pad, ptr[Writer] w) {\n nl := \"\"\n inner := \"\"\n sep := \",\"\n colon := \":\"\n if indent.len > 0 {\n nl = \"\\n\"\n inner = pad + indent\n colon = \": \"\n }\n match j.kind {\n Null -> w.out.push(\"null\")\n Bool -> {\n if j.flag {\n w.out.push(\"true\")\n } else {\n w.out.push(\"false\")\n }\n }\n Number -> w.out.push(number_text(j.num))\n Text -> w.out.push(quote(j.text))\n List -> {\n if j.items.len == 0 {\n w.out.push(\"[]\")\n return\n }\n w.out.push(\"[\" + nl)\n for i, item in j.items {\n if i > 0 {\n w.out.push(sep + nl)\n }\n w.out.push(inner)\n write(item, indent, inner, w)\n }\n w.out.push(nl + pad + \"]\")\n }\n Object -> {\n if j.items.len == 0 {\n w.out.push(\"{}\")\n return\n }\n w.out.push(\"{\" + nl)\n for i, item in j.items {\n if i > 0 {\n w.out.push(sep + nl)\n }\n w.out.push(inner + quote(j.keys[i]) + colon)\n write(item, indent, inner, w)\n }\n w.out.push(nl + pad + \"}\")\n }\n }\n}\n\n// the value as text on one line\nfn stringify(Json j) -> str {\n w := Writer()\n write(j, \"\", \"\", addr(w))\n return w.out.join(\"\")\n}\n\n// the value as text over several lines, indented by two spaces\nfn pretty(Json j) -> str {\n w := Writer()\n write(j, \" \", \"\", addr(w))\n return w.out.join(\"\")\n}\n\n// ---------- reading ----------\n\nstruct Reader {\n str src\n int i\n}\n\n// the line and column of a position, for a message\nfn where(ptr[Reader] r) -> str {\n line := 1\n col := 1\n for k in 0..r.i {\n if k < r.src.len && r.src[k] == '\\n' {\n line += 1\n col = 1\n } else {\n col += 1\n }\n }\n return \"line \" + str(line) + \", column \" + str(col)\n}\n\nfn peek(ptr[Reader] r) -> char {\n if r.i < r.src.len {\n return r.src[r.i]\n }\n return char(0)\n}\n\nfn skip_space(ptr[Reader] r) {\n while r.i < r.src.len && (r.src[r.i] == ' ' || r.src[r.i] == '\\n' || r.src[r.i] == '\\t' || r.src[r.i] == '\\r') {\n r.i += 1\n }\n}\n\nfn unexpected(ptr[Reader] r, str wanted) -> str {\n if r.i >= r.src.len {\n return \"json: the text ends where \" + wanted + \" should be (\" + where(r) + \")\"\n }\n return \"json: found '\" + str(r.src[r.i]) + \"' where \" + wanted + \" should be (\" + where(r) + \")\"\n}\n\n// does `word` come next?\nfn comes(ptr[Reader] r, str word) -> bool {\n return r.src.len - r.i >= word.len && r.src.sub(r.i, r.i + word.len) == word\n}\n\nfn expect_word(ptr[Reader] r, str word) -> ! {\n if !comes(r, word) {\n return error(unexpected(r, \"'\" + word + \"'\"))\n }\n r.i += word.len\n}\n\nfn hex_value(char c) -> int {\n if c >= '0' && c <= '9' {\n return int(c) - int('0')\n }\n if c >= 'a' && c <= 'f' {\n return int(c) - int('a') + 10\n }\n if c >= 'A' && c <= 'F' {\n return int(c) - int('A') + 10\n }\n return -1\n}\n\n// a code point as UTF-8\nfn utf8(int cp) -> str {\n []u8 b\n if cp < 128 {\n b.push(u8(cp))\n } else if cp < 2048 {\n b.push(u8(192 + cp / 64))\n b.push(u8(128 + cp % 64))\n } else if cp < 65536 {\n b.push(u8(224 + cp / 4096))\n b.push(u8(128 + (cp / 64) % 64))\n b.push(u8(128 + cp % 64))\n } else {\n b.push(u8(240 + cp / 262144))\n b.push(u8(128 + (cp / 4096) % 64))\n b.push(u8(128 + (cp / 64) % 64))\n b.push(u8(128 + cp % 64))\n }\n return str(b)\n}\n\nfn read_hex4(ptr[Reader] r) -> int! {\n n := 0\n for k in 0..4 {\n if r.i >= r.src.len || hex_value(r.src[r.i]) < 0 {\n return error(unexpected(r, \"a hexadecimal digit\"))\n }\n n = n * 16 + hex_value(r.src[r.i])\n r.i += 1\n }\n return n\n}\n\nfn read_string(ptr[Reader] r) -> str! {\n r.i += 1 // the opening quote\n []str parts\n start := r.i\n while true {\n if r.i >= r.src.len {\n return error(\"json: a text is never closed (\" + where(r) + \")\")\n }\n c := r.src[r.i]\n if c == '\"' {\n parts.push(r.src.sub(start, r.i))\n r.i += 1\n return parts.join(\"\")\n }\n if int(c) < 32 {\n return error(\"json: a line break or control character inside a text (\" + where(r) + \")\")\n }\n if c != '\\\\' {\n r.i += 1\n continue\n }\n parts.push(r.src.sub(start, r.i))\n r.i += 1\n e := peek(r)\n r.i += 1\n if e == 'n' {\n parts.push(\"\\n\")\n } else if e == 't' {\n parts.push(\"\\t\")\n } else if e == 'r' {\n parts.push(\"\\r\")\n } else if e == 'b' {\n parts.push(utf8(8))\n } else if e == 'f' {\n parts.push(utf8(12))\n } else if e == '\"' || e == '\\\\' || e == '/' {\n parts.push(str(e))\n } else if e == 'u' {\n cp := try read_hex4(r)\n // a pair of 😀 stands for one character above U+FFFF\n if cp >= 55296 && cp < 56320 && comes(r, \"\\\\u\") {\n r.i += 2\n low := try read_hex4(r)\n cp = 65536 + (cp - 55296) * 1024 + (low - 56320)\n }\n parts.push(utf8(cp))\n } else {\n return error(\"json: unknown escape \\\\\" + str(e) + \" in a text (\" + where(r) + \")\")\n }\n start = r.i\n }\n}\n\nfn read_number(ptr[Reader] r) -> Json! {\n start := r.i\n if peek(r) == '-' {\n r.i += 1\n }\n while r.i < r.src.len && ((r.src[r.i] >= '0' && r.src[r.i] <= '9') || r.src[r.i] == '.' || r.src[r.i] == 'e' || r.src[r.i] == 'E' || r.src[r.i] == '+' || r.src[r.i] == '-') {\n r.i += 1\n }\n n := float(r.src.sub(start, r.i)) or {\n r.i = start\n return error(unexpected(r, \"a number\"))\n }\n return new_number(n)\n}\n\nfn read_value(ptr[Reader] r) -> Json! {\n skip_space(r)\n c := peek(r)\n if r.i >= r.src.len {\n return error(unexpected(r, \"a value\"))\n }\n if c == '{' {\n obj := new_object()\n r.i += 1\n skip_space(r)\n if peek(r) == '}' {\n r.i += 1\n return obj\n }\n while true {\n skip_space(r)\n if peek(r) != '\"' {\n return error(unexpected(r, \"a key in quotes\"))\n }\n key := try read_string(r)\n skip_space(r)\n if peek(r) != ':' {\n return error(unexpected(r, \"':'\"))\n }\n r.i += 1\n v := try read_value(r)\n obj.set(key, v)\n skip_space(r)\n if peek(r) == ',' {\n r.i += 1\n continue\n }\n if peek(r) == '}' {\n r.i += 1\n return obj\n }\n return error(unexpected(r, \"',' or '}'\"))\n }\n }\n if c == '[' {\n list := new_list()\n r.i += 1\n skip_space(r)\n if peek(r) == ']' {\n r.i += 1\n return list\n }\n while true {\n v := try read_value(r)\n list.push(v)\n skip_space(r)\n if peek(r) == ',' {\n r.i += 1\n continue\n }\n if peek(r) == ']' {\n r.i += 1\n return list\n }\n return error(unexpected(r, \"',' or ']'\"))\n }\n }\n if c == '\"' {\n s := try read_string(r)\n return new_text(s)\n }\n if c == 't' {\n try expect_word(r, \"true\")\n return new_bool(true)\n }\n if c == 'f' {\n try expect_word(r, \"false\")\n return new_bool(false)\n }\n if c == 'n' {\n try expect_word(r, \"null\")\n return new_null()\n }\n if c == '-' || (c >= '0' && c <= '9') {\n return try read_number(r)\n }\n return error(unexpected(r, \"a value\"))\n}\n\n// the JSON in `text`; an error with the line and column where it went wrong\nfn parse(str text) -> Json! {\n r := Reader(src: text)\n v := try read_value(addr(r))\n skip_space(addr(r))\n if r.i < r.src.len {\n return error(unexpected(addr(r), \"the end of the text\"))\n }\n return v\n}\n") list.push("// Paths, as text. Elements are separated by \"/\".\n//\n// import path\n// path.join(\"a\", \"b.txt\") a/b.txt\n// path.base(\"/tmp/x/a.tar.gz\") a.tar.gz path.stem(...) a.tar path.ext(...) .gz\n// path.dir(\"/tmp/x/a.txt\") /tmp/x\n// path.clean(\"a//b/../c/./d\") a/c/d\n\n// a and b with one separator between; an absolute b replaces a\nfn join(str a, str b) -> str {\n if a.len == 0 {\n return b\n }\n if b.len == 0 {\n return a\n }\n if b[0] == '/' {\n return b\n }\n if a[a.len - 1] == '/' {\n return a + b\n }\n return a + \"/\" + b\n}\n\nfn is_abs(str p) -> bool {\n return p.len > 0 && p[0] == '/'\n}\n\n// the last element: \"a/b.txt\" -> \"b.txt\", \"a/b/\" -> \"b\", \"\" -> \".\", \"/\" -> \"/\"\nfn base(str p) -> str {\n if p.len == 0 {\n return \".\"\n }\n end := p.len\n while end > 1 && p[end - 1] == '/' {\n end -= 1\n }\n if end == 1 && p[0] == '/' {\n return \"/\"\n }\n start := end\n while start > 0 && p[start - 1] != '/' {\n start -= 1\n }\n return p.sub(start, end)\n}\n\n// everything before the last element: \"a/b.txt\" -> \"a\", \"b.txt\" -> \".\", \"/b\" -> \"/\"\nfn dir(str p) -> str {\n end := p.len\n while end > 1 && p[end - 1] == '/' {\n end -= 1\n }\n i := end\n while i > 0 && p[i - 1] != '/' {\n i -= 1\n }\n if i == 0 {\n return \".\"\n }\n while i > 1 && p[i - 1] == '/' {\n i -= 1\n }\n return p.sub(0, i)\n}\n\n// the part of the last element from its last dot: \"a.tar.gz\" -> \".gz\"; \"\" when there is none\nfn ext(str p) -> str {\n b := base(p)\n i := b.len\n while i > 0 && b[i - 1] != '.' {\n i -= 1\n }\n if i <= 1 { // no dot, or a name that only starts with one (.bashrc)\n return \"\"\n }\n return b.sub(i - 1, b.len)\n}\n\n// the last element without its extension\nfn stem(str p) -> str {\n b := base(p)\n e := ext(p)\n return b.sub(0, b.len - e.len)\n}\n\n// the elements of a path: \"/a/b/c\" -> [\"a\", \"b\", \"c\"]\nfn parts(str p) -> []str {\n []str out\n for piece in p.split(\"/\") {\n if piece.len > 0 {\n out.push(piece)\n }\n }\n return out\n}\n\n// the shortest path with the same meaning: no \"//\", no \".\", and \"..\" resolved where it can be\nfn clean(str p) -> str {\n if p.len == 0 {\n return \".\"\n }\n abs := is_abs(p)\n []str out\n for piece in parts(p) {\n if piece == \".\" {\n continue\n }\n if piece == \"..\" {\n if out.len > 0 && out[out.len - 1] != \"..\" {\n out.pop()\n } else if !abs {\n out.push(\"..\")\n }\n continue\n }\n out.push(piece)\n }\n joined := out.join(\"/\")\n if abs {\n return \"/\" + joined\n }\n if joined.len == 0 {\n return \".\"\n }\n return joined\n}\n") diff --git a/src/gen.fch b/src/gen.fch index 4768440..d3b3eb3 100644 --- a/src/gen.fch +++ b/src/gen.fch @@ -1,8 +1,7 @@ // Type checking and LLVM IR generation (as text), written in Finch. // -// The output is a .ll file that clang turns into a program, linked with the same -// C runtime the main compiler uses. This compiler copies values like the main one -// (so programs behave the same) but never frees them: it is the bootstrap compiler. +// The output is a .ll file that clang turns into a program, linked with Finch's C runtime +// (runtime/finch_rt.c). Values are copied when assigned and freed when their block ends. import ast import docs import cimport @@ -206,6 +205,7 @@ struct Gen { str cwd []str md // the metadata, numbered by position: !0 = md[0] map[str]int mdcache // metadata made already: types by key, locations by line:column + map[str]int wide // LLVM types that want more than the 16 bytes malloc and the stack promise: loaded with align 16 []int mdfiles // the DIFile of each source file int dbgScope // the DISubprogram of the function being built, or -1 (helpers have none) int dbgLine @@ -367,7 +367,7 @@ fn byteSize(ptr[Gen] g, ast.Type t) -> int { } return n } - if k == "fixed" && t.elem.len > 0 { + if (k == "fixed" || k == "vec") && t.elem.len > 0 { return t.count * byteSize(g, t.elem[0]) } if k == "str" || k == "array" || k == "map" { @@ -436,12 +436,22 @@ fn alloca(ptr[Gen] g, str ty) -> str { fn load(ptr[Gen] g, str ty, str at) -> str { t := tmpName(g) - emit(g, t + " = load " + ty + ", ptr " + at) + emit(g, t + " = load " + ty + ", ptr " + at + alignFor(g, ty)) return t } fn store(ptr[Gen] g, str ty, str v, str at) { - emit(g, "store " + ty + " " + v + ", ptr " + at) + emit(g, "store " + ty + " " + v + ", ptr " + at + alignFor(g, ty)) +} + +// A vector of 32 or 64 bytes (f32x8, i8x64) wants to sit at an address that many bytes apart, and so does a struct +// that holds one; memory from malloc and the stack is 16 apart. Telling LLVM so keeps it from using the +// instructions that crash on a smaller alignment. +fn alignFor(ptr[Gen] g, str ty) -> str { + if g.wide.len > 0 && g.wide.has(ty) { + return ", align 16" + } + return "" } fn spill(ptr[Gen] g, Val v) -> str { @@ -466,7 +476,7 @@ fn T(str kind) -> ast.Type { return ast.simple(kind) } -// `int` is i64 and `float` is f64, as in the C++ compiler: the everyday names +// `int` is i64 and `float` is f64: the everyday names // for the two default sizes, and the only way they are ever written back. fn isSigned(ast.Type t) -> bool { k := t.kind @@ -703,6 +713,13 @@ fn llty(ptr[Gen] g, ast.Type t) -> str { if k == "fixed" { return "[" + str(t.count) + " x " + llty(g, t.elem[0]) + "]" } + if k == "vec" { + out := "<" + str(t.count) + " x " + llty(g, t.elem[0]) + ">" + if t.count * bitsOf(t.elem[0]) > 128 && !g.wide.has(out) { + g.wide[out] = 1 + } + return out + } if k == "enum" { return "i32" } @@ -774,6 +791,9 @@ fn typeKey(ast.Type t) -> str { if t.kind == "enum" { return "E_" + modName(t.module) + "_" + t.name } + if t.kind == "vec" { + return ast.show(t) + } if t.kind == "struct" { return "S_" + modName(t.module) + "_" + mangle(t.name) } @@ -1221,7 +1241,19 @@ fn formatFn(ptr[Gen] g, ast.Type t) -> str { } g.done.push(name) s := beginHelper(g) - if t.kind == "array" { + if t.kind == "vec" { + // f32x4(1, 2.5, 3, 4) + v := load(g, llty(g, t), "%a") + bufText(g, "%buf", ast.show(t) + "(") + for k in 0..t.count { + if k > 0 { + bufText(g, "%buf", ", ") + } + lane := op(g, "extractelement " + llty(g, t) + " " + v + ", i64 " + str(k)) + emitFormat(g, "%buf", lane, t.elem[0], false) + } + bufText(g, "%buf", ")") + } else if t.kind == "array" { et := t.elem[0] d := load(g, "ptr", "%a") len := load(g, "i64", op(g, "getelementptr inbounds { ptr, i64, i64 }, ptr %a, i32 0, i32 1")) @@ -1428,6 +1460,12 @@ fn coerce(ptr[Gen] g, Val v, ast.Type want, ast.Node at, str what) -> Val { if want.kind == "str" && (isNumber(v.ty) || v.ty.kind == "bool" || v.ty.kind == "char") { msg = msg + " (turn it into text with str(...))" } + if want.kind == "vec" && isNumber(v.ty) { + msg = msg + " (" + ast.show(want) + "(x) puts x in every lane)" + } + if want.kind == "vec" && v.ty.kind == "vec" { + msg = msg + " (convert it: " + ast.show(want) + "(...))" + } fail(g, at, msg) return v } @@ -2054,7 +2092,13 @@ fn resolveStructBody(ptr[Gen] g, int i) { g.structs[i].owning = owns g.module = saved g.structs[i].state = 2 - g.head.push("%S." + modName(g.structs[i].module) + "." + mangle(g.structs[i].name) + " = type { " + fields.join(", ") + " }") + name := "%S." + modName(g.structs[i].module) + "." + mangle(g.structs[i].name) + for f in fields { + if g.wide.has(f) { + g.wide[name] = 1 // it holds a wide vector (or a struct that does) + } + } + g.head.push(name + " = type { " + fields.join(", ") + " }") } // ---------- debug info (-g) ---------- @@ -2108,6 +2152,10 @@ fn sizeAlign(ptr[Gen] g, ast.Type t) -> SizeAlign { e := sizeAlign(g, t.elem[0]) return SizeAlign(size: e.size * t.count, align: e.align) } + if k == "vec" { + n := t.count * sizeAlign(g, t.elem[0]).size + return SizeAlign(size: n, align: n) // LLVM lays a vector out at a multiple of its size + } if k == "struct" { sd := g.structs[findStruct(g, t.module, t.name)] if sd.isC { @@ -2196,6 +2244,10 @@ fn diType(ptr[Gen] g, ast.Type t) -> int { base = diType(g, t.elem[0]) } g.md[id] = "!DIDerivedType(tag: DW_TAG_pointer_type, baseType: " + mdRef(base) + ", size: 64)" + } else if k == "vec" { + e := sizeAlign(g, t.elem[0]) + sub := md(g, "!DISubrange(count: " + str(t.count) + ")") + g.md[id] = "!DICompositeType(tag: DW_TAG_array_type, baseType: " + mdRef(diType(g, t.elem[0])) + ", size: " + str(e.size * t.count * 8) + ", flags: DIFlagVector, elements: !{!" + str(sub) + "})" } else if k == "fixed" { e := sizeAlign(g, t.elem[0]) sub := md(g, "!DISubrange(count: " + str(t.count) + ")") @@ -3628,6 +3680,9 @@ fn expr(ptr[Gen] g, ast.Node e) -> Val { fn unary(ptr[Gen] g, ast.Node e) -> Val { v := expr(g, e.kids[0]) + if v.ty.kind == "vec" && e.name != "!" { + return vecUnary(g, e, v) + } if e.name == "!" { c := coerce(g, v, T("bool"), e.kids[0], "the value after '!'") return Val(v: op(g, "xor i1 " + c.v + ", true"), ty: T("bool")) @@ -3651,6 +3706,13 @@ fn unary(ptr[Gen] g, ast.Node e) -> Val { return Val(v: op(g, "sub " + llty(g, v.ty) + " 0, " + v.v), ty: v.ty) } if isFloat(v.ty) { + if v.lit { + // -1.5 is still a number written in the code: f32 x = -1.5 works like f32 x = 1.5 + if v.v.starts_with("-") { + return Val(v: v.v.sub(1, v.v.len), ty: v.ty, lit: true) + } + return Val(v: "-" + v.v, ty: v.ty, lit: true) + } return Val(v: op(g, "fneg " + llty(g, v.ty) + " " + v.v), ty: v.ty) } fail(g, e, "can't put '-' in front of " + ast.show(v.ty)) @@ -3746,6 +3808,9 @@ fn arith(ptr[Gen] g, str o, Val l, Val r, ast.Node at) -> Val { release(g, r) return Val(v: load(g, "{ ptr, i64, i64 }", out), ty: T("str"), fresh: true) } + if l.ty.kind == "vec" || r.ty.kind == "vec" { + return vecArith(g, o, l, r, at) + } if !unify(g, addr(l), addr(r)) { badOperands(g, o, l, r, at) } @@ -3847,6 +3912,9 @@ fn compare(ptr[Gen] g, str o, Val l, Val r, ast.Node at) -> Val { release(g, r) return Val(v: res, ty: T("bool")) } + if l.ty.kind == "vec" || r.ty.kind == "vec" { + return vecCompare(g, o, l, r, at) + } if !unify(g, addr(l), addr(r)) { badOperands(g, o, l, r, at) } @@ -4281,6 +4349,15 @@ fn member(ptr[Gen] g, ast.Node m, bool write) -> Ref { obj = Ref(isPlace: true, pl: Place(addr: pv, ty: target)) t = target } + if t.kind == "vec" { + if m.name != "len" { + fail(g, m, "a " + ast.show(t) + " has .len and [i] (and methods like .sum()), not '." + m.name + "'") + } + if write { + fail(g, m, ".len can't be changed") + } + return Ref(isPlace: false, val: Val(v: str(t.count), ty: T("int"), lit: true)) + } if t.kind == "fixed" { if write { fail(g, m, "." + m.name + " can't be changed directly") @@ -4376,6 +4453,15 @@ fn indexOf(ptr[Gen] g, ast.Node e, Ref obj, bool write) -> Ref { return mapIndex(g, e, obj, t, write) } i := coerce(g, expr(g, e.kids[1]), T("int"), e.kids[1], "an index").v + if t.kind == "vec" { + // a lane: v[2], v[i] = x + boundsCheck(g, i, str(t.count), e) + if obj.isPlace { + ep := op(g, "getelementptr inbounds " + llty(g, t.elem[0]) + ", ptr " + obj.pl.addr + ", i64 " + i) + return Ref(isPlace: true, pl: Place(addr: ep, ty: t.elem[0])) + } + return Ref(isPlace: false, val: Val(v: op(g, "extractelement " + llty(g, t) + " " + obj.val.v + ", i64 " + i), ty: t.elem[0])) + } if t.kind == "fixed" { base := "" if obj.isPlace { @@ -4555,11 +4641,12 @@ fn convert(ptr[Gen] g, ast.Node c, ast.Type to) -> Val { return v } -// ---------- the C calling convention for structs passed by value (System V x86-64) ---------- +// ---------- the C calling convention for structs passed by value ---------- // -// A struct of up to 16 bytes travels in registers, one INTEGER or SSE class per eightbyte; +// System V x86-64: a struct of up to 16 bytes travels in registers, one INTEGER or SSE class per eightbyte; // bigger ones are copied to the stack (byval) and returned through a hidden pointer (sret). -// Other targets (Windows, ARM64) have their own rules and are not done here yet. +// ARM64 (AAPCS64, and Apple's variant) and Windows x64 have their own rules: classifyArm64, and the +// windows branch of classify. struct Abi { str kind // "direct", "expand" (in registers: `parts`) or "memory" @@ -5307,7 +5394,7 @@ fn call(ptr[Gen] g, ast.Node c) -> Val { store(g, "{ ptr, i64, i64 }", "zeroinitializer", out) emit(g, "call void @finch_str_from_bytes(ptr " + out + ", ptr " + spill(g, v) + ")") release(g, v) - } else if k == "bool" || k == "char" || k == "array" || k == "map" || k == "struct" || k == "enum" { + } else if k == "bool" || k == "char" || k == "array" || k == "map" || k == "struct" || k == "enum" || k == "vec" { store(g, "{ ptr, i64, i64 }", "zeroinitializer", out) emitFormat(g, out, v.v, v.ty, false) release(g, v) @@ -5320,6 +5407,10 @@ fn call(ptr[Gen] g, ast.Node c) -> Val { if to.len > 0 { return convert(g, c, T(to)) } + vt := ast.vecType(n) + if vt.kind.len > 0 { + return vecMake(g, c, vt) + } if n == "read_file" || n == "file_exists" { checkArgs(g, c, 0, 1, n) pv := strArg(g, c.kids[0], "the file name") @@ -5570,7 +5661,7 @@ fn mathCall(ptr[Gen] g, ast.Node c) -> Val { []Val vals for a in c.kids { v := expr(g, a) - if !isNumber(v.ty) { + if !isNumber(v.ty) && v.ty.kind != "vec" { fail(g, a, n + "(...) is for numbers, but this is " + ast.show(v.ty)) } vals.push(v) @@ -5578,6 +5669,9 @@ fn mathCall(ptr[Gen] g, ast.Node c) -> Val { if n == "abs" { checkArgs(g, c, 0, 1, n) x := vals[0] + if x.ty.kind == "vec" { + return vecAbs(g, x) + } if isUnsigned(x.ty) { return x } @@ -5659,6 +5753,9 @@ fn atomicCall(ptr[Gen] g, ast.Node c) -> Val { // the smaller (o "<") or bigger (o ">") of two numbers, in the type both fit fn pick(ptr[Gen] g, str o, Val a, Val b, ast.Node at) -> Val { + if a.ty.kind == "vec" || b.ty.kind == "vec" { + return vecPick(g, o, a, b, at) + } l := a r := b if !unify(g, addr(l), addr(r)) { @@ -5902,6 +5999,15 @@ fn method(ptr[Gen] g, ast.Node m) -> Val { if r.isPlace { t = r.pl.ty } + if t.kind == "vec" { + v := r.val + if r.isPlace { + v = Val(v: load(g, llty(g, t), r.pl.addr), ty: t) + } + noteMethod(g, m, docs.vecMethods()) + heldEnd(g, mark) + return vecMethod(g, m, v) + } if t.kind == "array" { a := "" if r.isPlace { @@ -6530,3 +6636,398 @@ fn generate([]ast.Program progs, []str files, cimport.CImports ci, Options opt) g.out.push("define i32 @main(i32 %argc, ptr %argv) {\nentry:\n" + traceInit() + pre + " " + callText + "\n" + after + ret + "}\n") return assemble(g) } + +// ---------- vectors: f32x8, i8x32, f64x4, ... (SIMD) ---------- +// +// A vector is N numbers of one type side by side: <8 x float> in LLVM. + - * / work on every lane at once, and +// LLVM picks the instructions: SSE, AVX2 or AVX-512 on x86-64, NEON on ARM64 (with --cpu native, the ones of the +// machine that builds it). A plain number next to a vector counts for every lane: v * 2.0. + +fn laneBytes(ast.Type vt) -> str { + return str(bitsOf(vt.elem[0]) / 8) +} + +// v8f32, v32i8: how LLVM's vector intrinsics are named for this type +fn vecSuffix(ast.Type vt) -> str { + if isFloat(vt.elem[0]) { + return "v" + str(vt.count) + "f" + str(bitsOf(vt.elem[0])) + } + return "v" + str(vt.count) + "i" + str(bitsOf(vt.elem[0])) +} + +fn declareOnce(ptr[Gen] g, str decl) { + if !has(g.done, decl) { + g.done.push(decl) + g.head.push(decl) + } +} + +// call @llvm..(args...), declared the first time +fn vecIntrinsic(ptr[Gen] g, str ret, str name, ast.Type vt, []str argTypes, []str args, str flags) -> str { + fname := "@llvm." + name + "." + vecSuffix(vt) + declareOnce(g, "declare " + ret + " " + fname + "(" + argTypes.join(", ") + ")") + []str both + for i in 0..args.len { + both.push(argTypes[i] + " " + args[i]) + } + return op(g, "call " + flags + ret + " " + fname + "(" + both.join(", ") + ")") +} + +// the same number in every lane +fn splat(ptr[Gen] g, str v, ast.Type vt) -> str { + lt := llty(g, vt) + one := op(g, "insertelement " + lt + " poison, " + llty(g, vt.elem[0]) + " " + v + ", i64 0") + return op(g, "shufflevector " + lt + " " + one + ", " + lt + " poison, <" + str(vt.count) + " x i32> zeroinitializer") +} + +// a value next to a vector: the vector itself, or a number put in every lane +fn asVec(ptr[Gen] g, Val v, ast.Type vt, str o, ast.Node at) -> Val { + if v.ty.kind == "vec" { + if !ast.same(v.ty, vt) { + fail(g, at, "can't use '" + o + "' on " + ast.show(vt) + " and " + ast.show(v.ty) + " (convert one: " + ast.show(vt) + "(...))") + } + return v + } + s := coerce(g, v, vt.elem[0], at, "a number used with " + ast.show(vt)) + return Val(v: splat(g, s.v, vt), ty: vt) +} + +fn vecOf(Val l, Val r) -> ast.Type { + if l.ty.kind == "vec" { + return l.ty + } + return r.ty +} + +// i, i+1, ..., i+n-1 are all in a list of `len` +fn vecBounds(ptr[Gen] g, str i, int n, str len, ast.Node at) { + end := op(g, "add i64 " + i + ", " + str(n)) + before := op(g, "icmp slt i64 " + i + ", 0") + after := op(g, "icmp sgt i64 " + end + ", " + len) + panicIf(g, op(g, "or i1 " + before + ", " + after), "the " + str(n) + " numbers of the vector don't all fit in the list there", at) +} + +// f32x8(x), f32x8(a, b, ..., h), f32x8(list, i), f32x8(i32x8 v) +fn vecMake(ptr[Gen] g, ast.Node c, ast.Type vt) -> Val { + if g.index { + noteRef(g, c, c.col, c.name.len, docs.hover(docs.find(docs.builtins(), "f32x8")), "", 0, 0) + } + for nm in c.names { + if nm.len > 0 { + fail(g, c, "named arguments (" + nm + ": ...) are only for creating structs") + } + } + shown := ast.show(vt) + lane := vt.elem[0] + lt := llty(g, vt) + n := vt.count + usage := shown + "(x) puts x in every lane, " + shown + "(a, b, ...) takes " + str(n) + " numbers, " + shown + "(list, i) loads them from a list" + if c.kids.len == 0 { + fail(g, c, usage) + } + first := ref(g, c.kids[0], false) + ft := first.val.ty + if first.isPlace { + ft = first.pl.ty + } + if ft.kind == "array" || ft.kind == "ptr" { + if c.kids.len != 2 { + fail(g, c, shown + "(list, i): which index to start at? (" + shown + "(list, 0) for the first " + str(n) + ")") + } + if ft.elem.len == 0 || !ast.same(ft.elem[0], lane) { + fail(g, c.kids[0], shown + "(list, i) loads " + ast.show(lane) + " numbers, but this is " + ast.show(ft)) + } + mark := g.pending.len + if !first.isPlace { + hold(g, first.val) + } + i := coerce(g, expr(g, c.kids[1]), T("int"), c.kids[1], "an index").v + heldEnd(g, mark) + data := "" + if ft.kind == "ptr" { + data = first.val.v + if first.isPlace { + data = load(g, "ptr", first.pl.addr) + } + } else { + av := first.val.v + if first.isPlace { + av = load(g, "{ ptr, i64, i64 }", first.pl.addr) + } + vecBounds(g, i, n, op(g, "extractvalue { ptr, i64, i64 } " + av + ", 1"), c) + data = op(g, "extractvalue { ptr, i64, i64 } " + av + ", 0") + } + ep := op(g, "getelementptr inbounds " + llty(g, lane) + ", ptr " + data + ", i64 " + i) + out := op(g, "load " + lt + ", ptr " + ep + ", align " + laneBytes(vt)) + if !first.isPlace { + release(g, first.val) + } + return Val(v: out, ty: vt) + } + v := first.val + if first.isPlace { + v = Val(v: load(g, llty(g, ft), first.pl.addr), ty: ft) + } + if c.kids.len == 1 { + if ft.kind == "vec" { + return vecConvert(g, v, vt, c.kids[0]) + } + if !isNumber(ft) { + fail(g, c.kids[0], usage + "; this is " + ast.show(ft)) + } + s := coerce(g, v, lane, c.kids[0], "the number for every lane") + return Val(v: splat(g, s.v, vt), ty: vt) + } + if c.kids.len != n { + fail(g, c, usage + "; this has " + str(c.kids.len) + " values") + } + cur := "poison" + for k in 0..n { + x := v + if k > 0 { + x = expr(g, c.kids[k]) + } + s := coerce(g, x, lane, c.kids[k], "lane " + str(k)) + cur = op(g, "insertelement " + lt + " " + cur + ", " + llty(g, lane) + " " + s.v + ", i64 " + str(k)) + } + return Val(v: cur, ty: vt) +} + +// every lane converted, as the same conversion of one number would: f32x8(ints), i32x8(floats) cuts fractions +fn vecConvert(ptr[Gen] g, Val v, ast.Type to, ast.Node at) -> Val { + from := v.ty + if ast.same(from, to) { + return v + } + if from.count != to.count { + fail(g, at, "can't turn " + ast.show(from) + " into " + ast.show(to) + ": they have a different number of lanes") + } + fl := from.elem[0] + tl := to.elem[0] + inst := "" + if isInt(fl) && isInt(tl) { + if bitsOf(fl) == bitsOf(tl) { + return Val(v: v.v, ty: to) // i32x8 <-> u32x8: the same bits + } + if bitsOf(tl) < bitsOf(fl) { + inst = "trunc" + } else if isSigned(fl) { + inst = "sext" + } else { + inst = "zext" + } + } else if isInt(fl) { + inst = "uitofp" + if isSigned(fl) { + inst = "sitofp" + } + } else if isInt(tl) { + inst = "fptoui" + if isSigned(tl) { + inst = "fptosi" + } + } else if bitsOf(tl) > bitsOf(fl) { + inst = "fpext" + } else { + inst = "fptrunc" + } + return Val(v: op(g, inst + " " + llty(g, from) + " " + v.v + " to " + llty(g, to)), ty: to) +} + +fn vecUnary(ptr[Gen] g, ast.Node e, Val v) -> Val { + lane := v.ty.elem[0] + lt := llty(g, v.ty) + if e.name == "~" { + if !isInt(lane) { + fail(g, e, "'~' flips the bits of whole numbers, not of " + ast.show(v.ty)) + } + return Val(v: op(g, "xor " + lt + " " + v.v + ", " + splat(g, "-1", v.ty)), ty: v.ty) + } + if isUnsigned(lane) { + fail(g, e, ast.show(v.ty) + " can't be negative (its lanes are unsigned)") + } + if isFloat(lane) { + return Val(v: op(g, "fneg " + lt + " " + v.v), ty: v.ty) + } + return Val(v: op(g, "sub " + lt + " zeroinitializer, " + v.v), ty: v.ty) +} + +fn vecArith(ptr[Gen] g, str o, Val l, Val r, ast.Node at) -> Val { + vt := vecOf(l, r) + a := asVec(g, l, vt, o, at) + b := asVec(g, r, vt, o, at) + lane := vt.elem[0] + names := ["+", "-", "*", "/", "&", "|", "^", "<<", ">>"] + insts := ["add", "sub", "mul", "", "and", "or", "xor", "shl", "ashr"] + if isUnsigned(lane) { + insts[8] = "lshr" + } + if isFloat(lane) { + names = ["+", "-", "*", "/"] + insts = ["fadd", "fsub", "fmul", "fdiv"] + } + k := names.find(o) + if k < 0 || insts[k].len == 0 { + msg := "can't use '" + o + "' on " + ast.show(vt) + if o == "/" || o == "%" { + msg = msg + " (x86 and ARM have no instruction that divides whole numbers lane by lane; convert to floats: f32x" + str(vt.count) + "(v))" + } else if isFloat(lane) { + msg = msg + " (bit operators work on whole numbers only)" + } + fail(g, at, msg) + } + return Val(v: op(g, insts[k] + " " + llty(g, vt) + " " + a.v + ", " + b.v), ty: vt) +} + +// v == w: every lane is equal. (Lane by lane: min, max, or v[i] < w[i].) +fn vecCompare(ptr[Gen] g, str o, Val l, Val r, ast.Node at) -> Val { + vt := vecOf(l, r) + if o != "==" && o != "!=" { + fail(g, at, "'" + o + "' on " + ast.show(vt) + " would have an answer for every lane; use min(v, w), max(v, w), or compare lanes: v[i] " + o + " w[i]") + } + a := asVec(g, l, vt, o, at) + b := asVec(g, r, vt, o, at) + cmp := "" + if isFloat(vt.elem[0]) { + cmp = op(g, "fcmp oeq " + llty(g, vt) + " " + a.v + ", " + b.v) + } else { + cmp = op(g, "icmp eq " + llty(g, vt) + " " + a.v + ", " + b.v) + } + bits := ast.Type(kind: "vec", count: vt.count) + bits.elem.push(T("bool")) + all := vecIntrinsic(g, "i1", "vector.reduce.and", bits, ["<" + str(vt.count) + " x i1>"], [cmp], "") + if o == "!=" { + return Val(v: op(g, "xor i1 " + all + ", true"), ty: T("bool")) + } + return Val(v: all, ty: T("bool")) +} + +// min(v, w) and max(v, w), lane by lane (clamp is the two) +fn vecPick(ptr[Gen] g, str o, Val l, Val r, ast.Node at) -> Val { + vt := vecOf(l, r) + a := asVec(g, l, vt, o, at) + b := asVec(g, r, vt, o, at) + lane := vt.elem[0] + name := "" + if isFloat(lane) { + name = "minnum" + if o == ">" { + name = "maxnum" + } + } else { + sign := "u" + if isSigned(lane) { + sign = "s" + } + name = sign + "min" + if o == ">" { + name = sign + "max" + } + } + lt := llty(g, vt) + return Val(v: vecIntrinsic(g, lt, name, vt, [lt, lt], [a.v, b.v], ""), ty: vt) +} + +fn vecAbs(ptr[Gen] g, Val v) -> Val { + lane := v.ty.elem[0] + lt := llty(g, v.ty) + if isUnsigned(lane) { + return v + } + if isFloat(lane) { + return Val(v: vecIntrinsic(g, lt, "fabs", v.ty, [lt], [v.v], ""), ty: v.ty) + } + return Val(v: vecIntrinsic(g, lt, "abs", v.ty, [lt, "i1"], [v.v, "false"], ""), ty: v.ty) +} + +// all the lanes made into one number: "add", "min" or "max" +fn vecReduce(ptr[Gen] g, Val v, str how) -> Val { + lane := v.ty.elem[0] + lt := llty(g, v.ty) + et := llty(g, lane) + if isFloat(lane) { + if how == "add" { + // reassoc: the lanes may be added in any order, which is what makes it fast + return Val(v: vecIntrinsic(g, et, "vector.reduce.fadd", v.ty, [et, lt], ["-0.0", v.v], "reassoc "), ty: lane) + } + return Val(v: vecIntrinsic(g, et, "vector.reduce.f" + how, v.ty, [lt], [v.v], ""), ty: lane) + } + if how == "add" { + return Val(v: vecIntrinsic(g, et, "vector.reduce.add", v.ty, [lt], [v.v], ""), ty: lane) + } + sign := "u" + if isSigned(lane) { + sign = "s" + } + return Val(v: vecIntrinsic(g, et, "vector.reduce." + sign + how, v.ty, [lt], [v.v], ""), ty: lane) +} + +fn vecMethod(ptr[Gen] g, ast.Node m, Val v) -> Val { + vt := v.ty + lane := vt.elem[0] + lt := llty(g, vt) + n := m.name + if n == "sum" || n == "min" || n == "max" { + need(g, m, 0) + how := n + if n == "sum" { + how = "add" + } + return vecReduce(g, v, how) + } + if n == "dot" { + need(g, m, 1) + w := asVec(g, expr(g, m.kids[1]), vt, "dot", m.kids[1]) + return vecReduce(g, vecArith(g, "*", v, w, m), "add") + } + if n == "abs" { + need(g, m, 0) + return vecAbs(g, v) + } + if n == "sqrt" || n == "floor" || n == "ceil" || n == "round" || n == "fma" { + if !isFloat(lane) { + fail(g, m, n + "() is for f32 and f64 lanes, but this is " + ast.show(vt)) + } + if n == "fma" { + need(g, m, 2) + b := asVec(g, expr(g, m.kids[1]), vt, "fma", m.kids[1]) + c := asVec(g, expr(g, m.kids[2]), vt, "fma", m.kids[2]) + // fmuladd: one fused instruction where the processor has one (FMA, NEON), a multiply and an add where + // it doesn't (llvm.fma would call a slow library function there, to round only once) + return Val(v: vecIntrinsic(g, lt, "fmuladd", vt, [lt, lt, lt], [v.v, b.v, c.v], ""), ty: vt) + } + need(g, m, 0) + return Val(v: vecIntrinsic(g, lt, n, vt, [lt], [v.v], ""), ty: vt) + } + if n == "store" { + need(g, m, 2) + dst := ref(g, m.kids[1], true) + dt := dst.val.ty + if dst.isPlace { + dt = dst.pl.ty + } + if (dt.kind != "array" && dt.kind != "ptr") || dt.elem.len == 0 || !ast.same(dt.elem[0], lane) { + fail(g, m.kids[1], "store(list, i) writes " + ast.show(lane) + " numbers into a []" + ast.show(lane) + " or a ptr[" + ast.show(lane) + "], but this is " + ast.show(dt)) + } + if dt.kind == "array" && !dst.isPlace { + fail(g, m.kids[1], "store(list, i) needs a list in a variable (this one would be thrown away)") + } + i := coerce(g, expr(g, m.kids[2]), T("int"), m.kids[2], "an index").v + data := "" + if dt.kind == "ptr" { + data = dst.val.v + if dst.isPlace { + data = load(g, "ptr", dst.pl.addr) + } + } else { + av := load(g, "{ ptr, i64, i64 }", dst.pl.addr) + vecBounds(g, i, vt.count, op(g, "extractvalue { ptr, i64, i64 } " + av + ", 1"), m) + data = op(g, "extractvalue { ptr, i64, i64 } " + av + ", 0") + } + ep := op(g, "getelementptr inbounds " + llty(g, lane) + ", ptr " + data + ", i64 " + i) + emit(g, "store " + lt + " " + v.v + ", ptr " + ep + ", align " + laneBytes(vt)) + return Val(ty: T("void")) + } + fail(g, m, "a " + ast.show(vt) + " has no method '" + n + "' (it has: sum, min, max, dot, abs, sqrt, floor, ceil, round, fma, store)") + return v +} diff --git a/src/lsp.fch b/src/lsp.fch index 0bc0fcc..fc8299d 100644 --- a/src/lsp.fch +++ b/src/lsp.fch @@ -7,6 +7,7 @@ import json import host import docs +import ast import fmt import "stdio.h" import "stdlib.h" @@ -621,6 +622,8 @@ fn completionAt(Analysis a, str text, str path, int line, int col) -> json.Json methodItems(addr(items), docs.strMethods()) } else if type.starts_with("map[") { methodItems(addr(items), docs.mapMethods()) + } else if ast.vecType(type).kind.len > 0 { + methodItems(addr(items), docs.vecMethods()) // f32x8, i8x32, ... } else { base := type for st in a.structs { diff --git a/src/main.fch b/src/main.fch index f4c4f00..8bf35bd 100644 --- a/src/main.fch +++ b/src/main.fch @@ -1,7 +1,4 @@ -// finch-boot: the Finch compiler written in Finch. -// -// finch-boot build [-o name] compile a program -// finch-boot ir print the LLVM IR +// finch: the Finch compiler, written in Finch (`finch` with no arguments lists the commands). // // It reads the program and the modules it imports, checks it, writes LLVM IR as text // and lets clang turn that into a program, linked with Finch's C runtime. @@ -174,6 +171,9 @@ fn usage() { print(" --trace runtime errors show the chain of calls (finch run, finch test, -g and -O0 do it anyway)") print(" --no-trace they don't (finch build without -g or -O0 doesn't, it costs speed)") print(" --target build for another system: windows, linux, arm64 (Linux), macos, or an LLVM triple") + print(" --cpu use all of this processor's instructions (AVX2, AVX-512, NEON...): native is the one") + print(" building it; or x86-64-v3 (AVX2), x86-64-v4 (AVX-512), apple-m1, ... The program then") + print(" needs such a processor. Default: any processor of the target system") exit(1) } @@ -243,7 +243,7 @@ fn format([]str args) -> int { fn main([]str args) -> int { if args.len == 2 && (args[1] == "version" || args[1] == "--version") { - print("Finch 2.5.0 (the compiler is written in Finch)") + print("Finch 2.6.0 (the compiler is written in Finch)") return 0 } if args.len >= 2 && args[1] == "fmt" { @@ -293,6 +293,7 @@ fn main([]str args) -> int { trace := cmd != "build" // finch build: only with --trace, -g or -O0 (the chain costs speed) traceGiven := false targetName := "" + cpu := "" opt := "-O2" []str extraLibs []str extraDirs @@ -335,6 +336,9 @@ fn main([]str args) -> int { } else if a == "--target" && ai + 1 < args.len { targetName = args[ai + 1] ai += 1 + } else if a == "--cpu" && ai + 1 < args.len { + cpu = args[ai + 1] + ai += 1 } else if cmd == "run" { progArgs.push(a) } else { @@ -418,6 +422,9 @@ fn main([]str args) -> int { for f in man.list("cflags", targets) { cflags = cflags + " " + linker.quote(f) } + if cpu.len == 0 { + cpu = man.value("cpu", "") + } } for d in getenv("FINCH_PATH").split(host.pathSeparator()) { if d.len > 0 { @@ -494,6 +501,14 @@ fn main([]str args) -> int { if debug { extra = extra + " -g" // or clang drops the debug info that is in the IR } + if cpu.len > 0 { + // x86-64 names a processor with -march, ARM with -mcpu + if tg.arm64 { + extra = extra + " -mcpu=" + linker.quote(cpu) + } else { + extra = extra + " -march=" + linker.quote(cpu) + } + } []str cflagList for d in dirs { cflagList.push(" -I" + linker.quote(d)) // also the program's own folders (its headers, for inline C functions) diff --git a/src/parser.fch b/src/parser.fch index ce3aff3..db97175 100644 --- a/src/parser.fch +++ b/src/parser.fch @@ -86,7 +86,7 @@ fn node(ptr[Parser] p, str kind) -> ast.Node { fn isTypeName(str n) -> bool { names := ["int", "float", "bool", "char", "str", "ptr", "i8", "i16", "i32", "i64", "u8", "u16", "u32", "u64", "f32", "f64"] - return names.contains(n) + return names.contains(n) || ast.vecType(n).kind.len > 0 } // int, str, ptr[T], []T, Name, module.Name @@ -117,6 +117,10 @@ fn parseType(ptr[Parser] p) -> ast.Type { if name == "f64" { return ast.simple("float") } + vt := ast.vecType(name) + if vt.kind.len > 0 { + return vt + } return ast.simple(name) } t := ast.Type(kind: "named", name: name) diff --git a/src/platform.fch b/src/platform.fch index 44a678f..2cbb0c2 100644 --- a/src/platform.fch +++ b/src/platform.fch @@ -1,6 +1,5 @@ // The system a program is built for, and what depends on it: the C compiler that links, the file // name of the program, how it is run here, and (through the flags) the C calling convention. -// The same as src/target.cpp. import host import "stdlib.h" diff --git a/tests/fail/vec_intdiv.fch b/tests/fail/vec_intdiv.fch new file mode 100644 index 0000000..146aa49 --- /dev/null +++ b/tests/fail/vec_intdiv.fch @@ -0,0 +1,5 @@ +// expect: no instruction that divides whole numbers lane by lane +fn main() { + a := i32x4(8) + print(a / 2) +} diff --git a/tests/fail/vec_lane_index.fch b/tests/fail/vec_lane_index.fch new file mode 100644 index 0000000..8d71340 --- /dev/null +++ b/tests/fail/vec_lane_index.fch @@ -0,0 +1,6 @@ +// expect: index 4 +fn main() { + v := i32x4(1) + i := 4 + print(v[i]) +} diff --git a/tests/fail/vec_lanes.fch b/tests/fail/vec_lanes.fch new file mode 100644 index 0000000..3e4d0f2 --- /dev/null +++ b/tests/fail/vec_lanes.fch @@ -0,0 +1,4 @@ +// expect: f32x4(a, b, ...) takes 4 numbers +fn main() { + print(f32x4(1.0, 2.0, 3.0)) +} diff --git a/tests/fail/vec_less.fch b/tests/fail/vec_less.fch new file mode 100644 index 0000000..e9dadb0 --- /dev/null +++ b/tests/fail/vec_less.fch @@ -0,0 +1,5 @@ +// expect: use min(v, w), max(v, w), or compare lanes: v[i] < w[i] +fn main() { + a := f32x4(1.0) + print(a < a) +} diff --git a/tests/fail/vec_load_end.fch b/tests/fail/vec_load_end.fch new file mode 100644 index 0000000..aa5cb71 --- /dev/null +++ b/tests/fail/vec_load_end.fch @@ -0,0 +1,5 @@ +// expect: the 8 numbers of the vector don't all fit in the list there +fn main() { + []f32 data = [1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0, 9.0] + print(f32x8(data, 2)) +} diff --git a/tests/fail/vec_mix.fch b/tests/fail/vec_mix.fch new file mode 100644 index 0000000..4b05a3c --- /dev/null +++ b/tests/fail/vec_mix.fch @@ -0,0 +1,6 @@ +// expect: can't use '+' on f32x8 and i32x8 (convert one: f32x8(...)) +fn main() { + a := f32x8(1.0) + b := i32x8(1) + print(a + b) +} diff --git a/tests/fail/vec_type.fch b/tests/fail/vec_type.fch new file mode 100644 index 0000000..0f44299 --- /dev/null +++ b/tests/fail/vec_type.fch @@ -0,0 +1,5 @@ +// expect: f32x8(x) puts x in every lane +fn main() { + f32x8 v = 1.0 + print(v) +} diff --git a/tests/run/vectors.fch b/tests/run/vectors.fch new file mode 100644 index 0000000..2d7d95f --- /dev/null +++ b/tests/run/vectors.fch @@ -0,0 +1,69 @@ +// Vectors: N numbers side by side, worked on all at once (SIMD) + +struct Particle { + f32x8 pos + int id = 7 +} + +fn scale(f32x8 v, f32 k) -> f32x8 { + return v * k +} + +// a dot product of two lists, 8 at a time, with the rest one by one +fn dot([]f32 a, []f32 b) -> f32 { + acc := f32x8(0.0) + i := 0 + while i + 8 <= a.len { + acc = f32x8(a, i).fma(f32x8(b, i), acc) + i += 8 + } + total := acc.sum() + while i < a.len { + total += a[i] * b[i] + i += 1 + } + return total +} + +fn main() { + a := f32x4(1.0, 2.0, 3.0, 4.0) + b := f32x4(0.5) + print(a + b, a * 2.0, 10.0 - a, -a) + print(a.sum(), a.min(), a.max(), a.dot(a), a.len) + print((a * a).sqrt(), f32x4(1.5, -1.5, 2.5, -0.2).round(), f32x4(1.7).floor(), f32x4(1.2).ceil()) + print(abs(f32x4(-1.0, 2.0, -3.0, 4.0)), min(a, 2.5), max(a, f32x4(2.0)), clamp(a, 1.5, 3.5)) + + // lanes + a[0] = 9.0 + print(a[0], a[3], a) + print(a == f32x4(9.0, 2.0, 3.0, 4.0), a != a) + + // whole numbers, bits, conversions + n := i32x8(1, 2, 3, 4, 5, 6, 7, 8) + print(n * 3 - 1, n << 2, n & 1, ~n, n.sum(), n.max()) + print(f32x8(n) / 2.0, i32x8(f32x8(2.9)), i16x8(n), u8x16(200).sum()) + print(abs(i32x4(-3, 3, -7, 0)), min(u8x16(5), u8x16(9)).max()) + + // from lists and back + []f32 data + for i in 0..20 { + data.push(f32(i)) + } + v := f32x8(data, 4) + print(v) + (v * 10.0).store(data, 12) + print(data) + print(dot(data, data)) + + // in structs, arrays, functions, and str() + p := Particle(pos: f32x8(1.0)) + p.pos = scale(p.pos, 3.0) + p.pos[7] = 0.5 + print(p) + []f64x4 list + list.push(f64x4(1.0, 2.0, 3.0, 4.0)) + list.push(list[0] * list[0]) + print(list, str(list[1]).len) + i64x2 big = i64x2(1 << 40, -5) + print(big + big) +} diff --git a/tests/run/vectors.out b/tests/run/vectors.out new file mode 100644 index 0000000..8e130e6 --- /dev/null +++ b/tests/run/vectors.out @@ -0,0 +1,15 @@ +f32x4(1.5, 2.5, 3.5, 4.5) f32x4(2, 4, 6, 8) f32x4(9, 8, 7, 6) f32x4(-1, -2, -3, -4) +10 1 4 30 4 +f32x4(1, 2, 3, 4) f32x4(2, -2, 3, -0) f32x4(1, 1, 1, 1) f32x4(2, 2, 2, 2) +f32x4(1, 2, 3, 4) f32x4(1, 2, 2.5, 2.5) f32x4(2, 2, 3, 4) f32x4(1.5, 2, 3, 3.5) +9 4 f32x4(9, 2, 3, 4) +true false +i32x8(2, 5, 8, 11, 14, 17, 20, 23) i32x8(4, 8, 12, 16, 20, 24, 28, 32) i32x8(1, 0, 1, 0, 1, 0, 1, 0) i32x8(-2, -3, -4, -5, -6, -7, -8, -9) 36 8 +f32x8(0.5, 1, 1.5, 2, 2.5, 3, 3.5, 4) i32x8(2, 2, 2, 2, 2, 2, 2, 2) i16x8(1, 2, 3, 4, 5, 6, 7, 8) 128 +i32x4(3, 3, 7, 0) 5 +f32x8(4, 5, 6, 7, 8, 9, 10, 11) +[0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 40, 50, 60, 70, 80, 90, 100, 110] +49706 +Particle(pos: f32x8(3, 3, 3, 3, 3, 3, 3, 0.5), id: 7) +[f64x4(1, 2, 3, 4), f64x4(1, 4, 9, 16)] 18 +i64x2(2199023255552, -10)